diff --git a/web/src/components/creator/WorksGallery.tsx b/web/src/components/creator/WorksGallery.tsx
index 205f70a..6685803 100644
--- a/web/src/components/creator/WorksGallery.tsx
+++ b/web/src/components/creator/WorksGallery.tsx
@@ -111,7 +111,7 @@ interface CollectionProgress {
}
interface WorksGalleryProps {
- /** 不传则不过滤来源(如竞品分析页跨全部竞品);监控/自有账号详情传具体账号 ID。 */
+ /** 不传则不限定账号;仍按 sourceType 限定来源类型。监控/自有账号详情传具体账号 ID。 */
sourceId?: string;
sourceType?: 'owned' | 'competitor';
emptyHint?: string;
diff --git a/web/src/pages/creator/competitors/index.tsx b/web/src/pages/creator/competitors/index.tsx
index ffb19ef..2f6a58f 100644
--- a/web/src/pages/creator/competitors/index.tsx
+++ b/web/src/pages/creator/competitors/index.tsx
@@ -69,7 +69,7 @@ export default function Page() {
/>
-
+
);
}
diff --git a/web/tests/competitor-analysis-source.test.cjs b/web/tests/competitor-analysis-source.test.cjs
new file mode 100644
index 0000000..fbcd78a
--- /dev/null
+++ b/web/tests/competitor-analysis-source.test.cjs
@@ -0,0 +1,87 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const Module = require('node:module');
+const ts = require('typescript');
+
+const pagePath = path.join(__dirname, '../src/pages/creator/competitors/index.tsx');
+
+function createPage() {
+ let filters = {};
+ const gallery = 'WorksGallery';
+ const element = (type, props) => ({ type, props });
+ const mocks = {
+ react: {
+ useMemo: (factory) => factory(),
+ useState: () => [filters, (update) => { filters = update(filters); }],
+ },
+ 'react/jsx-runtime': { jsx: element, jsxs: element },
+ antd: {
+ Card: 'Card', DatePicker: 'DatePicker', Flex: 'Flex', InputNumber: 'InputNumber',
+ Select: 'Select', Space: 'Space', Typography: { Text: 'Text' },
+ },
+ '@/components/creator/WorksGallery': { __esModule: true, default: gallery },
+ };
+ const compiled = ts.transpileModule(fs.readFileSync(pagePath, 'utf8'), {
+ fileName: pagePath,
+ compilerOptions: {
+ module: ts.ModuleKind.CommonJS, jsx: ts.JsxEmit.ReactJSX, esModuleInterop: true,
+ inlineSourceMap: true, inlineSources: true,
+ },
+ }).outputText;
+ const loaded = new Module(pagePath, module);
+ loaded.filename = pagePath;
+ loaded.paths = Module._nodeModulePaths(path.dirname(pagePath));
+ loaded.require = (name) => Object.hasOwn(mocks, name) ? mocks[name] : module.require(name);
+ loaded._compile(compiled, pagePath);
+
+ function descendants(node) {
+ if (!node || typeof node !== 'object') return [];
+ return [node, ...[node.props?.children].flat(Infinity).flatMap(descendants)];
+ }
+ return () => {
+ const nodes = descendants(loaded.exports.default());
+ return {
+ gallery: nodes.find((node) => node.type === gallery).props,
+ controls: nodes.filter((node) => node.props.onChange),
+ };
+ };
+}
+
+function assertCompetitorOnly(props) {
+ assert.equal(props.sourceType, 'competitor', '竞品分析必须排除自有账号作品');
+ assert.equal(props.sourceId, undefined, '竞品分析应包含所有监测账号,而非限定某个账号');
+}
+
+test('竞品分析首次加载只查询监测账号作品', () => {
+ const render = createPage();
+ assertCompetitorOnly(render().gallery);
+});
+
+test('调整及清空各项筛选后仍只查询监测账号作品', () => {
+ const render = createPage();
+ const date = { format: (pattern) => {
+ assert.equal(pattern, 'YYYY-MM-DD');
+ return '2026-05-18';
+ } };
+ const cases = [
+ ['最低点赞', 'min_likes', 12, 12],
+ ['最低评论', 'min_comments', 3, 3],
+ ['最低转发', 'min_shares', 5, 5],
+ ['全部状态', 'published_at_status', 'verified', 'verified'],
+ ['发布开始日期', 'published_after', date, '2026-05-18T00:00:00Z'],
+ ['发布结束日期', 'published_before', date, '2026-05-18T23:59:59Z'],
+ ];
+ for (const [placeholder, key, value, expected] of cases) {
+ render().controls.find((control) => control.props.placeholder === placeholder).props.onChange(value);
+ assertCompetitorOnly(render().gallery);
+ assert.equal(render().gallery.extraFilters[key], expected);
+ }
+ for (const [placeholder, key] of cases) {
+ const clearValue = key === 'published_at_status' ? undefined : null;
+ render().controls.find((control) => control.props.placeholder === placeholder).props.onChange(clearValue);
+ assertCompetitorOnly(render().gallery);
+ assert.equal(render().gallery.extraFilters[key], undefined);
+ }
+});