test(admin): 收紧 Muse 管理端验证门禁
补齐 P3 E2E 未处理接口断言和契约字段校验,修正管理端请求前缀,并记录验证产物清理规则。
This commit is contained in:
parent
a33560a03b
commit
c35f64ffcc
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,7 +7,6 @@ ehthumbs.db
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# ====================
|
||||
# Editor & IDE
|
||||
@ -77,7 +76,6 @@ ENV/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
lerna-debug.log*
|
||||
.DS_Store
|
||||
|
||||
# ====================
|
||||
# Testing & Coverage
|
||||
@ -86,6 +84,8 @@ coverage/
|
||||
.nyc_output/
|
||||
.coverage
|
||||
.coverage.*
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
# ====================
|
||||
# Sub-repos (独立 git 仓库,不纳入本仓)
|
||||
@ -160,3 +160,4 @@ vite.config.ts.*
|
||||
.agents
|
||||
.cursor
|
||||
skills-lock.json
|
||||
precommit-scan-*.md
|
||||
|
||||
@ -84,6 +84,20 @@
|
||||
- `echarts` 插件去除重复类型导出并显式导出 `ECOption`。
|
||||
- `core-user` store 在 `setUserInfo(null)` 时同步清空 `userRoles`,避免登出后旧角色残留。
|
||||
|
||||
二次完善 E2E 覆盖时发现部分 Muse 页面在真实 Vite 运行时不会自动解析本地未注册的 Ant Design Vue 组件。已在 P3 页面内显式导入实际使用的 Ant 组件,并同步补齐页面测试中的 Ant mock export:
|
||||
|
||||
- `muse-admin/apps/web-antd/src/views/muse/governance/function-orch/index.vue`
|
||||
- `muse-admin/apps/web-antd/src/views/muse/knowledge/index.vue`
|
||||
- `muse-admin/apps/web-antd/src/views/muse/market/index.vue`
|
||||
- `muse-admin/apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts`
|
||||
- `muse-admin/apps/web-antd/src/views/muse/market/__tests__/index.test.ts`
|
||||
|
||||
审查后继续收紧 E2E 时,发现 `museAdminApi` 原先把 `/admin-api/muse` 直接传给 Vben `requestClient`。但当前 `web-antd` 的 `VITE_GLOB_API_URL` 已经是 `/admin-api`,真实浏览器请求会变成 `/admin-api/admin-api/muse/**`。已修正为:
|
||||
|
||||
- `museAdminApi` 内部只拼接 `/muse/**` 资源路径。
|
||||
- 由 Vben `requestClient` baseURL 统一组合成运行时 `/admin-api/muse/**`。
|
||||
- `apps/web-antd/src/api/muse/__tests__/client.spec.ts` 同步断言资源路径,避免再次双拼 `/admin-api`。
|
||||
|
||||
后续需要主代理确认长期 Git 边界:要么让 `muse-admin` 成为真正独立仓库,要么调整根 `.gitignore` 和纳管策略。
|
||||
|
||||
### 2. 不伪造后端契约
|
||||
@ -131,13 +145,55 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
|
||||
- OpenAPI TypeScript 生成通过。
|
||||
- P3 定向 Vitest 初次实现:10 个测试文件、19 个测试全部通过。
|
||||
- P3 完善后定向 Vitest:13 个测试文件、26 个测试全部通过。
|
||||
- P3 Playwright E2E:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
|
||||
- P3 Playwright E2E 初版:1 个 Chromium 用例通过,覆盖账号、任务、审计三个治理页的真实路由访问。
|
||||
- P3 Playwright E2E 二次完善:2 个 Chromium 用例通过,覆盖 MetaSchema、功能编排、AI 配置、全局知识库、市场治理、Account/New-API、任务治理、日志审计全部主要页面的真实路由访问和边界文案。
|
||||
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||
- `web-antd` typecheck 通过。
|
||||
- 全量 `test:unit` 通过:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||
- `web-antd` build 通过。
|
||||
- `git diff --check` 通过。
|
||||
|
||||
二次完善提交前复核:
|
||||
|
||||
```bash
|
||||
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
|
||||
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
|
||||
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||
pnpm --dir muse-admin test:unit
|
||||
pnpm --dir muse-admin --filter @vben/web-antd run build
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- `web-antd` typecheck 退出码 0。
|
||||
- P3 Playwright E2E:2 个 Chromium 用例通过;Vite dev server 仍输出 `ResizeObserver loop completed with undelivered notifications` 开发噪声,但 Playwright 退出码为 0。
|
||||
- P3 定向 Vitest:13 个测试文件、26 个测试通过。
|
||||
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||
- 全量 `test:unit`:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||
- `web-antd` build 退出码 0,并已清理生成的 `dist/` 和 `dist.zip`,不纳入提交。
|
||||
|
||||
审查修复后再次复核:
|
||||
|
||||
```bash
|
||||
pnpm --dir muse-admin exec playwright test -c playwright.config.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||
pnpm --dir muse-admin --filter @vben/web-antd run typecheck
|
||||
pnpm --dir muse-admin exec vitest run apps/web-antd/src/api/muse/newapi/__tests__/index.spec.ts apps/web-antd/src/api/muse/audit/__tests__/index.spec.ts apps/web-antd/src/api/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/api/muse/governance/__tests__/governance-api.test.ts apps/web-antd/src/api/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/jobs/__tests__/index.spec.ts apps/web-antd/src/api/muse/account/__tests__/index.spec.ts apps/web-antd/src/views/muse/knowledge/__tests__/index.test.ts apps/web-antd/src/views/muse/ai/__tests__/index.test.ts apps/web-antd/src/views/muse/market/__tests__/index.test.ts apps/web-antd/src/api/muse/__tests__/client.spec.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/src/views/muse/__tests__/governance-pages.test.ts --dom
|
||||
pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/views/muse apps/web-antd/src/router/routes/modules/muse.ts apps/web-antd/src/router/routes/__tests__/muse.test.ts apps/web-antd/e2e/muse-admin-governance.spec.ts
|
||||
pnpm --dir muse-admin test:unit
|
||||
pnpm --dir muse-admin --filter @vben/web-antd run build
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- P3 Playwright E2E:2 个 Chromium 用例通过;E2E 已改为未显式 mock 的 `/admin-api/muse/**` 请求返回 500 并在用例末尾断言无遗漏,同时断言每个主要页面的真实 mock 数据。
|
||||
- 审查后删除了 E2E 中会绕开统一 mock 的重复 specific routes,所有 Muse 管理端请求都由同一个 `/admin-api/muse/**` route 处理;Account、Purchase、Jobs、Source Event mock 字段已改为契约字段,并补充关键列断言,避免字段漂移仍然绿。
|
||||
- `web-antd` typecheck 退出码 0。
|
||||
- P3 定向 Vitest:13 个测试文件、26 个测试通过。
|
||||
- P3 定向 oxlint:35 个文件,0 warning、0 error。
|
||||
- 全量 `test:unit`:48 个测试文件、336 个测试通过;仍有既有 `use-sortable.test.ts` nested `vi.mock` 警告。
|
||||
- `web-antd` build 退出码 0,并已清理生成的 `test-results/`、`dist/` 和 `dist.zip`,不纳入提交。
|
||||
|
||||
## 提交与 Hook 说明
|
||||
|
||||
本次 P3 相关提交:
|
||||
@ -153,9 +209,20 @@ pnpm --dir muse-admin exec oxlint apps/web-antd/src/api/muse apps/web-antd/src/v
|
||||
- Muse 路由注册测试。注意测试文件不能放在 `src/router/routes/modules/**`,该目录会被 Vite 动态路由 glob 当作业务路由加载。
|
||||
- Account/New-API、任务治理、审计页面的最小集成测试。
|
||||
- Playwright E2E,使用本地 mock 覆盖 Vben 登录态、权限菜单、租户、字典、通知和 P3 API。
|
||||
- 二次扩展 Playwright E2E,将 MetaSchema、功能编排、AI 配置、全局知识库、市场治理也纳入真实路由访问,并检查管理员边界文案、影响预览、原因和审计提示。
|
||||
- 修复 E2E 暴露的 Ant Design Vue 组件运行时解析问题,Muse 页面改为显式导入实际使用的 Ant 组件。
|
||||
- 审查修复后,E2E 对未处理的 Muse 管理端 API 不再返回空分页兜底,避免错误 URL 或漏 mock 仍然绿;同时修正 New-API 真实运行时双 `/admin-api` 前缀问题。
|
||||
- 再次审查后,删除 Account/Jobs/Audit 的重复 route 注册,避免 specific route 抢先匹配导致 unified mock 失效;同步修正 E2E mock 字段契约漂移。
|
||||
|
||||
提交前发现暂存区已有其他会话 staged 的 `P1-execution-prompt.md` 和 `P3-execution-prompt.md`,已仅执行 `git restore --staged`,没有改动文件内容。
|
||||
|
||||
整理提交阶段补充:
|
||||
|
||||
- `muse-admin/apps/web-antd/dist/`、`dist.zip`、`test-results/` 属于验证产物,提交前必须清理。
|
||||
- 暂存区如果混入 `docs/memorys/2026-05-25-muse-cloud后端P1收口.md`,只执行 `git restore --staged` 移出 P3 提交,不删除并行 P1 会话文件。
|
||||
- 根目录 `.DS_Store` 曾被仓库跟踪;本轮改为从 Git 索引移除,并保留本地文件由 `.gitignore` 忽略,避免继续进入提交。
|
||||
- 根 `.gitignore` 保留 `muse-admin/` 独立子仓库边界,同时补充 `test-results/`、`playwright-report/` 和 `precommit-scan-*.md`,避免验证报告和安全扫描报告误入主提交。
|
||||
|
||||
提交时按当前项目约定使用:
|
||||
|
||||
```bash
|
||||
@ -168,4 +235,4 @@ touch /tmp/.opsera-pre-commit-scan-passed
|
||||
|
||||
1. 主代理确认 `muse-admin/` 的长期 Git 纳管方式,避免后续继续依赖 `git add -f`。
|
||||
2. 后端补齐 Account 角色/权限写接口、任务独立重验接口后,管理端可接入真实命令入口。
|
||||
3. P3 已补齐最小 E2E;后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看。
|
||||
3. P3 已补齐覆盖全部主要页面的最小 E2E;后续可继续扩展 Playwright 覆盖 MetaSchema 发布、市场审核/下架、任务重试和审计详情查看等具体命令流。
|
||||
|
||||
@ -9,6 +9,23 @@ const commonResult = (data: unknown) => ({
|
||||
|
||||
const ACCESS_STORE_KEY = 'muse-vben-antd-5.7.0-dev-core-access';
|
||||
|
||||
const pageResult = (list: unknown[]) => ({
|
||||
list,
|
||||
total: list.length,
|
||||
});
|
||||
|
||||
const unhandledMuseAdminApiPathsByPage = new WeakMap<Page, Set<string>>();
|
||||
|
||||
const expectNoUnhandledMuseAdminApis = (page: Page) => {
|
||||
const unhandledPaths = [
|
||||
...(unhandledMuseAdminApiPathsByPage.get(page) ?? new Set<string>()),
|
||||
];
|
||||
expect(
|
||||
unhandledPaths,
|
||||
`Unhandled Muse admin API mocks: ${unhandledPaths.join(', ')}`,
|
||||
).toEqual([]);
|
||||
};
|
||||
|
||||
const routeCommonResult = async (
|
||||
page: Page,
|
||||
patterns: string[],
|
||||
@ -24,6 +41,450 @@ const routeCommonResult = async (
|
||||
}
|
||||
};
|
||||
|
||||
const routeMuseAdminApis = async (page: Page) => {
|
||||
const unhandledMuseAdminApiPaths = new Set<string>();
|
||||
unhandledMuseAdminApiPathsByPage.set(page, unhandledMuseAdminApiPaths);
|
||||
|
||||
await page.route('**/admin-api/muse/**', (route) => {
|
||||
const { pathname } = new URL(route.request().url());
|
||||
const path = pathname.replace('/admin-api/muse', '');
|
||||
|
||||
if (path === '/governance/meta-schemas') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activeVersion: 1,
|
||||
affectedObjectCount: 3,
|
||||
displayName: '故事草稿结构',
|
||||
draftVersion: 2,
|
||||
fieldType: 'object',
|
||||
migrationRisk: 'low',
|
||||
schemaKey: 'story.draft',
|
||||
scope: 'work',
|
||||
status: 'active',
|
||||
updatedAt: '2026-05-25T00:00:00Z',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/governance/function-chains') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activeVersion: 1,
|
||||
chainKey: 'story-pipeline',
|
||||
description: '系统故事生成链路',
|
||||
displayName: '故事链路',
|
||||
openSlotCount: 2,
|
||||
protectionNodeCount: 3,
|
||||
status: 'active',
|
||||
updatedAt: '2026-05-25T00:00:00Z',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/governance/protection-nodes') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
auditSummary: '系统保护节点',
|
||||
displayName: '事实校验',
|
||||
irreplaceable: true,
|
||||
irreplaceableReason: '保持事实边界',
|
||||
nodeKey: 'fact-guard',
|
||||
nodeType: 'guard',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/ai/prompts') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activeVersion: 1,
|
||||
latestVersion: 2,
|
||||
name: '候选生成 Prompt',
|
||||
promptKey: 'candidate.generate',
|
||||
updatedAt: '2026-05-25T00:00:00Z',
|
||||
variableCount: 4,
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/ai/agents') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activeVersion: 1,
|
||||
agentId: 'system-story-agent',
|
||||
description: '系统写作智能体',
|
||||
name: '系统故事智能体',
|
||||
promptKey: 'candidate.generate',
|
||||
scope: 'system',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
activeVersion: 3,
|
||||
agentId: 'user-private-agent',
|
||||
name: '用户私有智能体摘要',
|
||||
promptKey: 'user.prompt',
|
||||
scope: 'user',
|
||||
status: 'active',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/ai/tool-grants') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
agentId: 'system-story-agent',
|
||||
agentName: '系统故事智能体',
|
||||
approvedBy: 'Security facade',
|
||||
budget: { amount: 1000, period: 'daily' },
|
||||
grantId: 'grant-e2e',
|
||||
outboundPolicy: {
|
||||
allowedDestinations: ['search.internal'],
|
||||
dataPolicy: 'redacted',
|
||||
networkMode: 'allowlist',
|
||||
},
|
||||
securityApprovalId: 'sec-approval-e2e',
|
||||
status: 'approved',
|
||||
toolType: 'web_search',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/ai/quality-policies') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activeVersion: 1,
|
||||
description: '候选交付质量门控',
|
||||
dimensionCount: 3,
|
||||
latestVersion: 2,
|
||||
name: '候选交付策略',
|
||||
policyKey: 'candidate.delivery',
|
||||
updatedAt: '2026-05-25T00:00:00Z',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/knowledge/global-kbs') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
activePolicyVersion: 1,
|
||||
bindingCount: 2,
|
||||
currentVersion: 4,
|
||||
documentCount: 8,
|
||||
kbId: 'global-kb-e2e',
|
||||
name: '系统世界观库',
|
||||
status: 'active',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path.includes('/knowledge/global-kbs/') && path.endsWith('/documents')) {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
documentId: 'doc-e2e',
|
||||
name: '系统设定资料',
|
||||
processingStatus: 'searchable',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
path.includes('/knowledge/global-kbs/') &&
|
||||
path.endsWith('/processing-tasks')
|
||||
) {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(pageResult([{ status: 'completed', taskId: 'kb-task-e2e' }])),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/knowledge/source-bindings') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
bindingId: 'kb-binding-e2e',
|
||||
kbId: 'global-kb-e2e',
|
||||
sourceStatus: 'active',
|
||||
sourceSummary: '系统资产来源',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/market/publish-requests') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
assetName: '待审资产',
|
||||
publisherName: '发布者',
|
||||
requestId: 'review-e2e',
|
||||
riskTags: ['版权'],
|
||||
status: 'pending',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/market/assets') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
assetId: 'asset-e2e',
|
||||
name: '市场资产',
|
||||
listingStatus: 'listed',
|
||||
reviewStatus: 'approved',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/market/appeals') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
appealId: 'appeal-e2e',
|
||||
assetId: 'asset-e2e',
|
||||
appealType: 'delist',
|
||||
appellantName: '申诉人',
|
||||
assetName: '市场资产',
|
||||
processorId: 'admin-e2e',
|
||||
reason: '申诉材料',
|
||||
status: 'pending',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/account/users') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
status: 'active',
|
||||
entitlementSource: 'plan',
|
||||
nickname: 'E2E 用户',
|
||||
newApiBindingStatus: 'bound',
|
||||
quotaStatus: 'normal',
|
||||
riskFlags: ['manual-review'],
|
||||
userId: 'user-e2e',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/account/usage-records') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
attributionStatus: 'attributed',
|
||||
nickname: 'E2E 用户',
|
||||
period: '2026-05',
|
||||
recordId: 'usage-e2e',
|
||||
totalTokens: 42,
|
||||
userId: 'user-e2e',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/account/purchase-records') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
amount: 0,
|
||||
assetName: '市场资产',
|
||||
assetType: 'agent',
|
||||
createdAt: '2026-05-25T00:00:00Z',
|
||||
purchaseType: 'admin_grant',
|
||||
recordId: 'purchase-e2e',
|
||||
status: 'completed',
|
||||
userId: 'user-e2e',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/account/new-api-bindings') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
bindingStatus: 'bound',
|
||||
externalUserId: 'newapi-user-e2e',
|
||||
syncStatus: 'synced',
|
||||
userId: 'user-e2e',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path.includes('/account/users/') && path.endsWith('/balance-snapshots')) {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([{ balance: 100, snapshotId: 'balance-e2e', userId: 'user-e2e' }]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/jobs') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
createdAt: '2026-05-25T00:00:00Z',
|
||||
createdBy: 'system',
|
||||
jobId: 1001,
|
||||
owner: 'market',
|
||||
retryCount: 1,
|
||||
status: 'failed',
|
||||
type: 'source-projection',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/source-events') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
eventId: 2001,
|
||||
actionPolicy: 'needs_recheck',
|
||||
affectedScope: 'asset-e2e',
|
||||
createdAt: '2026-05-25T00:00:00Z',
|
||||
sourceId: 1,
|
||||
sourceStatus: 'revoked',
|
||||
sourceType: 'market_asset',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/audit/api-logs') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
actor: 'admin',
|
||||
createdAt: '2026-05-25T00:00:00Z',
|
||||
durationMs: 12,
|
||||
logId: 1,
|
||||
method: 'GET',
|
||||
module: 'audit',
|
||||
pathSummary: '/admin-api/muse/audit/api-logs',
|
||||
requestId: 'req-e2e',
|
||||
status: 'success',
|
||||
statusCode: 200,
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (path === '/audit/business-events') {
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult(
|
||||
pageResult([
|
||||
{
|
||||
createdAt: '2026-05-25T00:00:00Z',
|
||||
eventId: 1,
|
||||
eventType: 'market.delist',
|
||||
operator: 'admin',
|
||||
resourceId: 'asset-e2e',
|
||||
resourceType: 'market_asset',
|
||||
summary: '市场资产下架',
|
||||
},
|
||||
]),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
unhandledMuseAdminApiPaths.add(path);
|
||||
return route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: {
|
||||
code: 500,
|
||||
data: null,
|
||||
msg: `Unhandled Muse admin E2E mock: ${path}`,
|
||||
},
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript((accessStoreKey) => {
|
||||
localStorage.setItem(
|
||||
@ -50,34 +511,94 @@ test.beforeEach(async ({ page }) => {
|
||||
menus: [
|
||||
{
|
||||
children: [
|
||||
{
|
||||
component: 'muse/governance/meta-schema/index',
|
||||
componentName: 'MuseMetaSchemaList',
|
||||
id: 101,
|
||||
name: '元结构定义',
|
||||
parentId: 100,
|
||||
path: 'governance/meta-schemas',
|
||||
sort: 10,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/governance/function-orch/index',
|
||||
componentName: 'MuseFunctionOrchestration',
|
||||
id: 102,
|
||||
name: '功能编排',
|
||||
parentId: 100,
|
||||
path: 'governance/function-orchestration',
|
||||
sort: 20,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/ai/index',
|
||||
componentName: 'MuseAiConfig',
|
||||
id: 103,
|
||||
name: 'AI 配置',
|
||||
parentId: 100,
|
||||
path: 'ai',
|
||||
sort: 30,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/knowledge/index',
|
||||
componentName: 'MuseGlobalKnowledge',
|
||||
id: 104,
|
||||
name: '全局知识库',
|
||||
parentId: 100,
|
||||
path: 'knowledge',
|
||||
sort: 40,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/market/index',
|
||||
componentName: 'MuseMarketGovernance',
|
||||
id: 105,
|
||||
name: '市场治理',
|
||||
parentId: 100,
|
||||
path: 'market',
|
||||
sort: 50,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/account/index',
|
||||
componentName: 'MuseAccountGovernance',
|
||||
id: 101,
|
||||
id: 106,
|
||||
name: '用户与权限',
|
||||
parentId: 100,
|
||||
path: 'account',
|
||||
sort: 10,
|
||||
sort: 60,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/newapi/index',
|
||||
componentName: 'MuseNewApiUsage',
|
||||
id: 107,
|
||||
name: 'New-API 与用量',
|
||||
parentId: 100,
|
||||
path: 'new-api',
|
||||
sort: 70,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/jobs/index',
|
||||
componentName: 'MuseTaskOps',
|
||||
id: 102,
|
||||
id: 108,
|
||||
name: '任务治理',
|
||||
parentId: 100,
|
||||
path: 'jobs',
|
||||
sort: 20,
|
||||
sort: 80,
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
component: 'muse/audit/index',
|
||||
componentName: 'MuseAuditLog',
|
||||
id: 103,
|
||||
id: 109,
|
||||
name: '日志与审计',
|
||||
parentId: 100,
|
||||
path: 'audit',
|
||||
sort: 30,
|
||||
sort: 90,
|
||||
visible: true,
|
||||
},
|
||||
],
|
||||
@ -133,110 +654,7 @@ test.beforeEach(async ({ page }) => {
|
||||
0,
|
||||
);
|
||||
|
||||
await page.route('**/admin-api/muse/account/users**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [
|
||||
{
|
||||
accountStatus: 'active',
|
||||
entitlementSource: 'plan',
|
||||
nickname: 'E2E 用户',
|
||||
quotaStatus: 'normal',
|
||||
userId: 'user-e2e',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.route('**/admin-api/muse/account/usage-records**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [{ recordId: 'usage-e2e', totalTokens: 42, userId: 'user-e2e' }],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.route('**/admin-api/muse/account/purchase-records**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [{ orderId: 'order-e2e', status: 'paid', userId: 'user-e2e' }],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route('**/admin-api/muse/jobs**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [
|
||||
{
|
||||
jobId: 'job-e2e',
|
||||
jobType: 'source-projection',
|
||||
owner: 'market',
|
||||
status: 'failed',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.route('**/admin-api/muse/source-events**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [
|
||||
{
|
||||
eventId: 'event-e2e',
|
||||
objectSummary: 'asset-e2e',
|
||||
sourceStatus: 'revoked',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await page.route('**/admin-api/muse/audit/api-logs**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [
|
||||
{
|
||||
actor: 'admin',
|
||||
durationMs: 12,
|
||||
logId: 1,
|
||||
method: 'GET',
|
||||
pathSummary: '/admin-api/muse/audit/api-logs',
|
||||
requestId: 'req-e2e',
|
||||
status: 'success',
|
||||
statusCode: 200,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await page.route('**/admin-api/muse/audit/business-events**', (route) =>
|
||||
route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: commonResult({
|
||||
list: [
|
||||
{
|
||||
eventId: 1,
|
||||
eventType: 'market.delist',
|
||||
objectSummary: 'asset-e2e',
|
||||
operator: 'admin',
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
await routeMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test('管理员可进入账号、任务和审计治理页面并看到边界文案', async ({
|
||||
@ -246,13 +664,73 @@ test('管理员可进入账号、任务和审计治理页面并看到边界文
|
||||
await expect(page.getByText('用户、角色与权限治理')).toBeVisible();
|
||||
await expect(page.getByText('不提供用户侧完整个人中心或私有正文入口')).toBeVisible();
|
||||
await expect(page.getByText('E2E 用户')).toBeVisible();
|
||||
await expect(page.getByText('manual-review')).toBeVisible();
|
||||
await page.getByRole('tab', { name: '用量与购买摘要' }).click();
|
||||
await expect(page.getByText('usage-e2e')).toBeVisible();
|
||||
await expect(page.getByText('purchase-e2e')).toBeVisible();
|
||||
await expect(page.getByText('2026-05-25T00:00:00Z')).toBeVisible();
|
||||
|
||||
await page.goto('/muse/jobs');
|
||||
await expect(page.getByText('管理员不能代用户重新生成写作候选')).toBeVisible();
|
||||
await expect(page.getByText('job-e2e')).toBeVisible();
|
||||
await expect(page.getByText('1001')).toBeVisible();
|
||||
await expect(page.getByText('source-projection')).toBeVisible();
|
||||
await page.getByRole('tab', { name: '来源事件' }).click();
|
||||
await expect(page.getByText('2001')).toBeVisible();
|
||||
await expect(page.getByText('market_asset')).toBeVisible();
|
||||
await expect(page.getByText('needs_recheck')).toBeVisible();
|
||||
|
||||
await page.goto('/muse/audit');
|
||||
await expect(page.getByRole('tab', { name: '接口调用日志' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: '业务审计日志' })).toBeVisible();
|
||||
await expect(page.getByText('req-e2e')).toBeVisible();
|
||||
await page.getByRole('tab', { name: '业务审计日志' }).click();
|
||||
await expect(page.getByText('市场资产下架')).toBeVisible();
|
||||
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
test('管理员可进入核心治理配置页面并看到系统边界', async ({ page }) => {
|
||||
await page.goto('/muse/governance/meta-schemas');
|
||||
await expect(page.getByText('元结构定义').first()).toBeVisible();
|
||||
await expect(page.getByText('story.draft')).toBeVisible();
|
||||
|
||||
await page.goto('/muse/governance/function-orchestration');
|
||||
await expect(
|
||||
page.getByText('开放槽位只表示可替换的非保护节点').first(),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('故事链路')).toBeVisible();
|
||||
await page.getByRole('button', { name: '保护节点' }).first().click();
|
||||
await expect(page.getByText('事实校验')).toBeVisible();
|
||||
|
||||
await page.goto('/muse/ai');
|
||||
await expect(page.getByText('AI 配置域治理边界')).toBeVisible();
|
||||
await expect(page.getByText('候选生成 Prompt')).toBeVisible();
|
||||
await page.getByRole('tab', { name: '系统智能体' }).click();
|
||||
await expect(page.getByText('用户私有 Agent 边界')).toBeVisible();
|
||||
await expect(page.getByText('系统故事智能体')).toBeVisible();
|
||||
|
||||
await page.goto('/muse/knowledge');
|
||||
await expect(page.getByRole('heading', { name: '全局知识管理' })).toBeVisible();
|
||||
await expect(page.getByText('不读取用户私有正文')).toBeVisible();
|
||||
await expect(page.getByText('系统世界观库').first()).toBeVisible();
|
||||
|
||||
await page.goto('/muse/market');
|
||||
await expect(page.getByRole('heading', { name: '市场治理' })).toBeVisible();
|
||||
await expect(page.getByText('待审资产').first()).toBeVisible();
|
||||
await page.getByRole('tab', { name: '资产治理' }).click();
|
||||
await expect(page.getByText('不改用户私有副本').first()).toBeVisible();
|
||||
await expect(page.getByText('市场资产').first()).toBeVisible();
|
||||
await page.getByRole('tab', { name: '申诉处理' }).click();
|
||||
await expect(page.getByText('申诉人').first()).toBeVisible();
|
||||
|
||||
await page.goto('/muse/new-api');
|
||||
await expect(
|
||||
page.getByText('New-API 页面只展示治理和对账摘要'),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText('不展示凭据')).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'user-e2e' })).toBeVisible();
|
||||
await page.getByRole('tab', { name: '调用归属' }).click();
|
||||
await expect(page.getByText('usage-e2e')).toBeVisible();
|
||||
|
||||
expectNoUnhandledMuseAdminApis(page);
|
||||
});
|
||||
|
||||
@ -21,11 +21,11 @@ describe('muse admin api client', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('为所有管理端 GET 请求追加 Muse 前缀和版本头', async () => {
|
||||
it('为所有管理端 GET 请求追加 Muse 资源前缀和版本头', async () => {
|
||||
await museAdminApi.get('/audit/api-logs', { pageNo: 1, pageSize: 20 });
|
||||
|
||||
expect(requestClientMock.get).toHaveBeenCalledWith(
|
||||
'/admin-api/muse/audit/api-logs',
|
||||
'/muse/audit/api-logs',
|
||||
{
|
||||
headers: { 'X-API-Version': '1' },
|
||||
params: { pageNo: 1, pageSize: 20 },
|
||||
@ -41,12 +41,12 @@ describe('muse admin api client', () => {
|
||||
await museAdminApi.delete('/knowledge/global-kbs/kb-1/documents/doc-1');
|
||||
|
||||
expect(requestClientMock.post).toHaveBeenCalledWith(
|
||||
'/admin-api/muse/jobs/1/retry',
|
||||
'/muse/jobs/1/retry',
|
||||
body,
|
||||
{ headers: { 'X-API-Version': '1' } },
|
||||
);
|
||||
expect(requestClientMock.request).toHaveBeenCalledWith(
|
||||
'/admin-api/muse/knowledge/global-kbs/kb-1',
|
||||
'/muse/knowledge/global-kbs/kb-1',
|
||||
{
|
||||
data: body,
|
||||
headers: { 'X-API-Version': '1' },
|
||||
@ -54,7 +54,7 @@ describe('muse admin api client', () => {
|
||||
},
|
||||
);
|
||||
expect(requestClientMock.delete).toHaveBeenCalledWith(
|
||||
'/admin-api/muse/knowledge/global-kbs/kb-1/documents/doc-1',
|
||||
'/muse/knowledge/global-kbs/kb-1/documents/doc-1',
|
||||
{
|
||||
headers: { 'X-API-Version': '1' },
|
||||
params: undefined,
|
||||
|
||||
@ -4,9 +4,11 @@ import { requestClient } from '#/api/request';
|
||||
* Muse 管理端 API 客户端
|
||||
*
|
||||
* 统一收口 `/admin-api/muse/**` 请求,避免页面层拼错前缀或遗漏版本头。
|
||||
* Vben requestClient 的 baseURL 已经是 `/admin-api`,这里仅拼接资源段 `/muse/**`,
|
||||
* 运行时真实请求才会落到 `/admin-api/muse/**`,避免出现 `/admin-api/admin-api/muse/**`。
|
||||
* Vben 的 requestClient 已经按 `code/data/msg` 解包,这里返回值即为后端 data。
|
||||
*/
|
||||
const MUSE_ADMIN_BASE_URL = '/admin-api/muse';
|
||||
const MUSE_ADMIN_RESOURCE_PREFIX = '/muse';
|
||||
|
||||
/** Muse API v1 强制版本头,所有管理端请求都必须显式携带。 */
|
||||
const MUSE_API_HEADERS = {
|
||||
@ -21,7 +23,7 @@ type MuseRequestBody = unknown;
|
||||
|
||||
/** 拼接 Muse 管理端路径,确保调用方不能越过 `/admin-api/muse` 边界。 */
|
||||
function toMuseUrl(url: string) {
|
||||
return `${MUSE_ADMIN_BASE_URL}${url}`;
|
||||
return `${MUSE_ADMIN_RESOURCE_PREFIX}${url}`;
|
||||
}
|
||||
|
||||
export const museAdminApi = {
|
||||
|
||||
@ -17,7 +17,27 @@ import { computed, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Drawer as ADrawer,
|
||||
Empty as AEmpty,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
InputNumber as AInputNumber,
|
||||
List as AList,
|
||||
ListItem as AListItem,
|
||||
Space as ASpace,
|
||||
Table as ATable,
|
||||
Tag as ATag,
|
||||
Textarea as ATextarea,
|
||||
message,
|
||||
Modal,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import {
|
||||
|
||||
@ -1,8 +1,33 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||
|
||||
import KnowledgeIndex from '../index.vue';
|
||||
|
||||
function makeStub(name: string) {
|
||||
return defineComponent({
|
||||
name,
|
||||
props: ['dataSource', 'description', 'message', 'title', 'value'],
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h('section', { 'data-stub': name }, [
|
||||
props.title,
|
||||
props.message,
|
||||
props.description,
|
||||
props.value,
|
||||
Array.isArray(props.dataSource)
|
||||
? JSON.stringify(props.dataSource)
|
||||
: '',
|
||||
slots.default?.(),
|
||||
slots.extra?.(),
|
||||
slots.bodyCell?.({
|
||||
column: { dataIndex: 'status', key: 'status' },
|
||||
record: {},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: {
|
||||
name: 'Page',
|
||||
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('ant-design-vue', () => ({
|
||||
Alert: makeStub('Alert'),
|
||||
Button: makeStub('Button'),
|
||||
Card: makeStub('Card'),
|
||||
Col: makeStub('Col'),
|
||||
Descriptions: makeStub('Descriptions'),
|
||||
DescriptionsItem: makeStub('DescriptionsItem'),
|
||||
Form: makeStub('Form'),
|
||||
FormItem: makeStub('FormItem'),
|
||||
Input: makeStub('Input'),
|
||||
Modal: makeStub('Modal'),
|
||||
Row: makeStub('Row'),
|
||||
Select: makeStub('Select'),
|
||||
SelectOption: makeStub('SelectOption'),
|
||||
Space: makeStub('Space'),
|
||||
Statistic: makeStub('Statistic'),
|
||||
Table: makeStub('Table'),
|
||||
TabPane: makeStub('TabPane'),
|
||||
Tabs: makeStub('Tabs'),
|
||||
Tag: makeStub('Tag'),
|
||||
Textarea: makeStub('Textarea'),
|
||||
TypographyParagraph: makeStub('TypographyParagraph'),
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@ -17,7 +17,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
Modal as AModal,
|
||||
Row as ARow,
|
||||
Select as ASelect,
|
||||
SelectOption as ASelectOption,
|
||||
Space as ASpace,
|
||||
Statistic as AStatistic,
|
||||
Table as ATable,
|
||||
TabPane as ATabPane,
|
||||
Tabs as ATabs,
|
||||
Tag as ATag,
|
||||
Textarea as ATextarea,
|
||||
TypographyParagraph as ATypographyParagraph,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
disableGlobalKnowledgeBaseApi,
|
||||
|
||||
@ -1,8 +1,33 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createApp, nextTick } from 'vue';
|
||||
import { createApp, defineComponent, h, nextTick } from 'vue';
|
||||
|
||||
import MarketIndex from '../index.vue';
|
||||
|
||||
function makeStub(name: string) {
|
||||
return defineComponent({
|
||||
name,
|
||||
props: ['dataSource', 'description', 'message', 'title', 'value'],
|
||||
setup(props, { slots }) {
|
||||
return () =>
|
||||
h('section', { 'data-stub': name }, [
|
||||
props.title,
|
||||
props.message,
|
||||
props.description,
|
||||
props.value,
|
||||
Array.isArray(props.dataSource)
|
||||
? JSON.stringify(props.dataSource)
|
||||
: '',
|
||||
slots.default?.(),
|
||||
slots.extra?.(),
|
||||
slots.bodyCell?.({
|
||||
column: { dataIndex: 'status', key: 'status' },
|
||||
record: {},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('@vben/common-ui', () => ({
|
||||
Page: {
|
||||
name: 'Page',
|
||||
@ -11,9 +36,31 @@ vi.mock('@vben/common-ui', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('ant-design-vue', () => ({
|
||||
Alert: makeStub('Alert'),
|
||||
Button: makeStub('Button'),
|
||||
Card: makeStub('Card'),
|
||||
Col: makeStub('Col'),
|
||||
Descriptions: makeStub('Descriptions'),
|
||||
DescriptionsItem: makeStub('DescriptionsItem'),
|
||||
Form: makeStub('Form'),
|
||||
FormItem: makeStub('FormItem'),
|
||||
Input: makeStub('Input'),
|
||||
Modal: makeStub('Modal'),
|
||||
Row: makeStub('Row'),
|
||||
Select: makeStub('Select'),
|
||||
SelectOption: makeStub('SelectOption'),
|
||||
Space: makeStub('Space'),
|
||||
Statistic: makeStub('Statistic'),
|
||||
Table: makeStub('Table'),
|
||||
TabPane: makeStub('TabPane'),
|
||||
Tabs: makeStub('Tabs'),
|
||||
Tag: makeStub('Tag'),
|
||||
Textarea: makeStub('Textarea'),
|
||||
TypographyParagraph: makeStub('TypographyParagraph'),
|
||||
message: {
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@ -21,7 +21,30 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
Alert as AAlert,
|
||||
Button as AButton,
|
||||
Card as ACard,
|
||||
Col as ACol,
|
||||
Descriptions as ADescriptions,
|
||||
DescriptionsItem as ADescriptionsItem,
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Input as AInput,
|
||||
Modal as AModal,
|
||||
Row as ARow,
|
||||
Select as ASelect,
|
||||
SelectOption as ASelectOption,
|
||||
Space as ASpace,
|
||||
Statistic as AStatistic,
|
||||
Table as ATable,
|
||||
TabPane as ATabPane,
|
||||
Tabs as ATabs,
|
||||
Tag as ATag,
|
||||
Textarea as ATextarea,
|
||||
TypographyParagraph as ATypographyParagraph,
|
||||
message,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
approveMarketPublishRequestApi,
|
||||
@ -470,13 +493,26 @@ onMounted(loadMarketDashboard);
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="openReviewAction('approve', record)">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="
|
||||
openReviewAction(
|
||||
'approve',
|
||||
record as AdminPublishRequestSummaryVO,
|
||||
)
|
||||
"
|
||||
>
|
||||
通过
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
@click="openReviewAction('reject', record)"
|
||||
@click="
|
||||
openReviewAction(
|
||||
'reject',
|
||||
record as AdminPublishRequestSummaryVO,
|
||||
)
|
||||
"
|
||||
>
|
||||
驳回
|
||||
</a-button>
|
||||
@ -518,16 +554,37 @@ onMounted(loadMarketDashboard);
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="openAssetAction('preview', record)">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="
|
||||
openAssetAction(
|
||||
'preview',
|
||||
record as AdminMarketAssetSummaryVO,
|
||||
)
|
||||
"
|
||||
>
|
||||
影响预览
|
||||
</a-button>
|
||||
<a-button size="small" @click="openAssetAction('delist', record)">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="
|
||||
openAssetAction(
|
||||
'delist',
|
||||
record as AdminMarketAssetSummaryVO,
|
||||
)
|
||||
"
|
||||
>
|
||||
下架
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
@click="openAssetAction('recall', record)"
|
||||
@click="
|
||||
openAssetAction(
|
||||
'recall',
|
||||
record as AdminMarketAssetSummaryVO,
|
||||
)
|
||||
"
|
||||
>
|
||||
召回
|
||||
</a-button>
|
||||
@ -558,7 +615,10 @@ onMounted(loadMarketDashboard);
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'actions'">
|
||||
<a-button size="small" @click="openAppealAction(record)">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="openAppealAction(record as AdminAppealSummaryVO)"
|
||||
>
|
||||
处理
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user