diff --git a/AGENTS.md b/AGENTS.md index 414358d1..3fad531f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,14 @@ ## 快速读取顺序 -1. 用“模块快速路由”选择最小业务模块,进入后读取该目录最近的 `AGENTS.md`;按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不先扫描全部 `src/`。 -2. 视觉还原、布局/交互承载方式变更或真实页面验收时读取 `../../docs/Admin-Client前端原型还原验收规则.md`;需要原型依据才读取 `../../docs/悦/货代SAAS平台后台/AGENTS.md` 并定位目标片段。纯文案、逻辑或单元测试任务不自动读取。 - Admin 样式优化还须读取 `../../docs/Admin高密度界面规范.md` 的“样式维护边界”和“新模块接入规则”;以已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式。国内仓、客户管理是样板;新增菜单按本轮范围显式接入,货柜管理入口见 `../../docs/Admin货柜管理样式核对记录.md`。 -3. 标准列表或公共组件任务读取 `src/shared/components/AGENTS.md`;国内/海外 HTTP、Token 或刷新任务读取 `src/shared/api/AGENTS.md`;Finance 任务读取 `src/modules/finance/AGENTS.md`;海外仓全流程任务先读 `../../docs/海外仓业务/README.md`。 -4. 平台/客户层级与权限公共语义读取 `../../docs/平台客户层级与权限契约.md`;统一 API、Token、会话撤销或可靠事件任务按需读取 `../../docs/统一认证与服务通信契约.md` 对应章节。 +1. 由“模块快速路由”选最小模块并读最近 `AGENTS.md`,按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不扫描全部 `src/`。 +2. 视觉、布局/交互承载变更或真实页面验收先读 `../../docs/Admin-Client前端原型还原验收规则.md`;需原型依据再读 `../../docs/悦/货代SAAS平台后台/AGENTS.md` 并定位片段。纯文案、逻辑或单元测试不触发。 + 样式优化另读 `../../docs/Admin高密度界面规范.md` 的“样式维护边界”“新模块接入规则”,保留已确认密度、彩色页签、业务语义及承载。国内仓、客户管理为样板;新增菜单按本轮范围显式接入,货柜入口见 `../../docs/Admin货柜管理样式核对记录.md`。 + 蓝白样式接入见 [样板指南](../../docs/界面参考/蓝白卡片式工作台风格参考.md)。 +3. 标准列表/公共组件读 `src/shared/components/AGENTS.md`;HTTP、Token/刷新读 `src/shared/api/AGENTS.md`;Finance 读 `src/modules/finance/AGENTS.md`;海外仓全流程先读 `../../docs/海外仓业务/README.md`。 +4. 平台/客户层级及权限读 `../../docs/平台客户层级与权限契约.md`;统一 API、Token、会话撤销或可靠事件按需读 `../../docs/统一认证与服务通信契约.md` 对应章节。 -普通单端任务的最小读取集是本文件、目标模块最近的 `AGENTS.md` 和目标代码命中段;只有跨服务状态、权限或接口边界无法判断时才增加后端契约。读取预算遵循根指引的行数与字符数标准,先读适用规则,再定向读取业务表,不全文加载无关专题。 +单端仅读本指引、最近模块指引及命中代码;跨服务状态、权限或接口边界不明才加后端契约。预算遵循根指引,先读适用规则、定向查业务表,跳过无关专题。 ## 项目定位与技术基线 diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index c9874ae8..eb6a43a8 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -56,7 +56,7 @@ describe('adminNavigation', () => { { title: '系统管理', children: ['用户管理', '角色权限', '菜单权限'] }, ]) expect(visible).toHaveLength(13) - expect(visible.flatMap(node => node.children ?? [])).toHaveLength(57) + expect(visible.flatMap(node => node.children ?? [])).toHaveLength(58) expect(adminNavigation.find((item) => item.key === 'notifications')).toBeUndefined() }) diff --git a/src/modules/self-operated/collectionPresentation.test.ts b/src/modules/self-operated/collectionPresentation.test.ts index 1e778560..a860ce1a 100644 --- a/src/modules/self-operated/collectionPresentation.test.ts +++ b/src/modules/self-operated/collectionPresentation.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Task } from './types' -import { taskStateText, taskProgressText, taskCompletionText } from './collectionPresentation' +import { collectionControlState, taskStateText, taskProgressText, taskCompletionText } from './collectionPresentation' const base: Task = { taskNo: 'T-A', warehouseCode: 'WH-A', taskType: 'BACKFILL', dataFamily: 'OUTBOUND', taskStatus: 'RUNNING', applyState: 'NOT_STARTED', attemptCount: 1, latestAttemptNo: 1, resultPublicationState: 'NOT_PUBLISHED', nextRetryAt: null, allowedActions: [], actionBlockReason: null, progress: { phase: 'DETAIL', completed: 792, total: 2014, totalKnown: true, unit: 'ORDER' }, queryFrom: null, queryUntil: null } describe('plain collection task presentation', () => { it('requires the matching yielded attempt before describing RETRY_WAIT as normal continuation', () => { @@ -22,6 +22,25 @@ describe('plain collection task presentation', () => { expect(taskStateText({ ...base, taskStatus: 'DEAD' })).toBe('已停止,需处理') expect(taskProgressText({ ...base, taskStatus: 'WAITING_SOURCE', attemptCount: 0 })).toBe('尚未开始,等待数据准备') }) + it('gives explicit off and pause evidence priority over automatic continuation copy', () => { + const waiting = { ...base, taskStatus: 'YIELDED' } + expect(taskStateText(waiting, { collectionMode: 'OFF' })).toBe('采集已关闭') + expect(taskStateText({ ...waiting, blockingReason: 'COLLECTION_OFF' })).toBe('采集已关闭') + expect(taskStateText(waiting, { accountPaused: true })).toBe('已暂停') + expect(taskStateText({ ...waiting, blockingReason: 'ACCOUNT_PAUSED' })).toBe('已暂停') + expect(taskStateText({ ...waiting, blockingReason: 'EXECUTION_DISABLED' })).toBe('执行未启用') + expect(collectionControlState(null, { collectionMode: 'OFF' })?.text).toBe('采集已关闭') + }) + it('does not infer that an executing attempt stopped from a control switch or pause reason', () => { + for (const taskStatus of ['RUNNING', 'APPLYING']) { + const task = { ...base, taskStatus } + expect(taskStateText(task, { collectionMode: 'OFF' })).toBe('已关闭 · 执行中') + expect(taskStateText(task, { accountPaused: true })).toBe('已暂停 · 执行中') + expect(collectionControlState(task, { blockingReason: 'EXECUTION_DISABLED' })?.running).toBe(true) + } + expect(taskStateText({ ...base, taskStatus: 'SUCCEEDED' }, { collectionMode: 'OFF' })).toBe('执行完成') + expect(taskStateText({ ...base, taskStatus: 'DEAD', blockingReason: 'COLLECTION_OFF' })).toBe('已停止,需处理') + }) it('shows a stage percentage without presenting it as overall completion', () => { expect(taskProgressText(base)).toBe('处理出库订单,已处理 792 / 2014单(39.3%,当前阶段)') expect(taskCompletionText(base)).toBe('完成时间暂无法预计') diff --git a/src/modules/self-operated/collectionPresentation.ts b/src/modules/self-operated/collectionPresentation.ts index 0e8381b0..4c36abb0 100644 --- a/src/modules/self-operated/collectionPresentation.ts +++ b/src/modules/self-operated/collectionPresentation.ts @@ -1,6 +1,24 @@ import type { Task } from './types' import { label, phaseText, quantityText, unitText } from './state' +export interface CollectionControlEvidence { + collectionMode?: string | null + accountPaused?: boolean + blockingReason?: string | null +} + +export function collectionControlState(task: Task | null | undefined, evidence: CollectionControlEvidence = {}) { + if (task && ['SUCCEEDED', 'FAILED', 'DEAD', 'ABORTED'].includes(task.taskStatus)) return null + const reasons = [task?.blockingReason, evidence.blockingReason] + const text = evidence.collectionMode === 'OFF' || reasons.includes('COLLECTION_OFF') ? '采集已关闭' + : evidence.accountPaused || reasons.includes('ACCOUNT_PAUSED') ? '已暂停' + : reasons.includes('EXECUTION_DISABLED') ? '执行未启用' : null + if (!text) return null + // A control switch alone cannot establish that an already running attempt has stopped. + const running = Boolean(task && ['RUNNING', 'APPLYING'].includes(task.taskStatus)) + return { text, running, summary: running ? `${text === '采集已关闭' ? '已关闭' : text} · 执行中` : text } +} + export function isNormalContinuation(task: Task | null | undefined) { const attempt = task?.latestAttempt return task?.taskStatus === 'RETRY_WAIT' && attempt?.status === 'YIELDED' @@ -9,8 +27,10 @@ export function isNormalContinuation(task: Task | null | undefined) { && !attempt.errorCode && !attempt.failureSummary } -export function taskStateText(task: Task | null | undefined) { +export function taskStateText(task: Task | null | undefined, evidence: CollectionControlEvidence = {}) { if (!task) return '尚无任务' + const control = collectionControlState(task, evidence) + if (control) return control.summary if (isNormalContinuation(task)) return '本批已完成,等待续采' const states: Record = { WAITING_SOURCE: '等待数据准备', WAITING_UPSTREAM: '等待数据准备', READY: '等待执行', QUEUED: '等待执行', WAITING: '等待执行', RUNNING: '正在执行', RETRY_WAIT: '等待重试', YIELDED: '本批已完成,等待续采', DEAD: '已停止,需处理', SUCCEEDED: '执行完成', FAILED: '执行失败' } return states[task.taskStatus] ?? label(task.taskStatus) diff --git a/src/modules/self-operated/components/AnalyticsView.vue b/src/modules/self-operated/components/AnalyticsView.vue index 11ee052f..a79cfdc5 100644 --- a/src/modules/self-operated/components/AnalyticsView.vue +++ b/src/modules/self-operated/components/AnalyticsView.vue @@ -1,5 +1,6 @@