fix(aihr): harden broadcast and mobile assistant flows

This commit is contained in:
2026-07-23 03:35:36 +08:00
parent 62c8b41672
commit 4b31848e3d
30 changed files with 1127 additions and 208 deletions
+49 -36
View File
@@ -25,9 +25,11 @@ test('SOP 查询框不再覆盖 textarea 的 box-sizing', async () => {
test('员工首页核心工具保留真实业务入口', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /<text class="voice-title">问老师傅<\/text>/);
assert.match(source, /<text class="question-board-title">问题榜<\/text>/);
assert.match(source, /@click="goSop"/);
assert.match(source, /class="voice-card employee-card"/);
assert.match(source, /\{\{ voiceTitle \}\}/);
assert.match(source, /class="tool-cell tool-questions"/);
assert.match(source, /<text class="tool-name">问题榜<\/text>/);
assert.match(source, /const ask = \(query: string\) =>/);
assert.match(source, /const goPractice = \(\) =>/);
assert.match(source, /const openQuestions = \(\) =>/);
});
@@ -47,9 +49,10 @@ test('训练结果回到顶部,SOP 答案生成后定位到回答区', async (
pageSource('../src/services/navigation.ts')
]);
assert.match(practiceSource, /result\.value = await finishPractice\([\s\S]*?resetPageScroll\(\)/);
assert.match(sopSource, /import \{ resetPageScroll, scrollPageToSelector \} from '@\/services\/navigation';/);
assert.match(sopSource, /result\.value = await searchKnowledge\(value\);\s*scrollPageToSelector\('\.answer-panel'/);
assert.match(practiceSource, /const nextResult = await finishPractice\([\s\S]*?result\.value = nextResult;\s*resetPageScroll\(\)/);
assert.match(sopSource, /const scrollToBottom = \(\) =>/);
assert.match(sopSource, /replaceMsg\(pending\.id/);
assert.match(sopSource, /threadScrollTop\.value = 100000/);
assert.match(navigationSource, /export const scrollPageToSelector = \(selector: string/);
});
@@ -65,9 +68,9 @@ test('训练历史按摘要、逐句复盘、SOP 依据和行动建议分层', a
test('案例页为无权限和无案例提供明确空状态', async () => {
const source = await pageSource('../src/pages/user/cases/index.vue');
assert.match(source, /class="case-empty-state"/);
assert.match(source, /暂时还没有可学习案例/);
assert.match(source, /提交权限由组织角色决定/);
assert.match(source, /<StateBlock v-else type="empty" :card="false" title="暂无入库案例"/);
assert.match(source, /当前岗位可查看本项目已入库案例/);
assert.match(source, /提交案例素材需要主管或项目负责人权限/);
});
test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
@@ -89,7 +92,7 @@ test('员工端 tabBar 继续保留四个固定入口', async () => {
test('登录页按高保真原型保留品牌结构和紧凑验证码行', async () => {
const source = await pageSource('../src/pages/auth/login/index.vue');
assert.match(source, /高效协同 · 贴心服务 · 让工作更简单/);
assert.match(source, /手机号登录,立刻上手/);
assert.match(source, /<uni-icons[^>]+type="phone"/);
assert.match(source, /<uni-icons[^>]+type="locked"/);
assert.doesNotMatch(source, /class="logo-badge"/);
@@ -133,9 +136,9 @@ test('工作助手与查全网使用一致的来源切换头部', async () => {
assert.doesNotMatch(page, /(?:^|\n)\s*\.chat-source-switch(?:\s+\.source-option(?:\.active)?)?\s*\{/);
}
assert.match(headerSource, /<text class="head-title">问 · 数字师傅<\/text>/);
assert.match(headerSource, /\.source-option\s*\{[^}]*height:\s*44px/);
assert.match(headerSource, /\.new-chat-button\s*\{[^}]*min-height:\s*44px/);
assert.match(headerSource, /<text class="brand-name">数字师傅<\/text>/);
assert.match(headerSource, /\.source-option\s*\{[^}]*height:\s*28px/);
assert.match(headerSource, /\.new-chat-btn\s*\{[^}]*height:\s*28px/);
assert.match(headerSource, /@click="emit\('newConversation'\)"/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-switch\s*\{/);
assert.doesNotMatch(sopSource, /(?:^|\n)\s*\.source-option(?:\.active)?\s*\{/);
@@ -179,12 +182,12 @@ test('主管可切换到学员显示模式且组织角色保持不变', async ()
test('员工首页保留师傅区、语音条、今日安排和问题榜结构', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /mentor-hero/);
assert.match(source, /voice-panel/);
assert.match(source, /hero-immersive/);
assert.match(source, /voice-card/);
assert.match(source, /今日安排/);
assert.match(source, /问题榜/);
assert.match(source, /brand-bar/);
assert.match(source, /ask-card/);
assert.match(source, /mentor-chips/);
assert.match(source, /小贴士/);
});
@@ -192,24 +195,24 @@ test('员工首页高保真入口继续使用真实图标和业务跳转', async
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /type="notification"/);
assert.match(source, /type="chatboxes"/);
assert.match(source, /type="compose"/);
assert.match(source, /type="location"/);
assert.match(source, /const goSop = \(\) =>/);
assert.match(source, /type="chatboxes-filled"/);
assert.match(source, /type="paperplane-filled"/);
assert.match(source, /type="staff-filled"/);
assert.match(source, /const ask = \(query: string\) =>/);
assert.match(source, /const openPrimarySchedule = \(\) =>/);
assert.match(source, /getMobileHome\('user'\)/);
assert.match(source, /getPracticeAssignments\(true\)/);
assert.match(source, /src="\/static\/mentor-hifi\.png"/);
assert.match(source, /src="\/static\/mentor-female\.png"/);
});
test('员工首页今日安排保留派发任务来源', async () => {
const source = await pageSource('../src/pages/user/today/index.vue');
assert.match(source, /primaryAssignmentSourceLabel/);
assert.match(source, /primaryAssignmentTagText/);
assert.match(source, /source === 'retry'[\s\S]*错题再练/);
assert.match(source, /source === 'camp'[\s\S]*训练营/);
assert.match(source, /class="schedule-source"/);
assert.match(source, /今天先完成每日三题/);
assert.match(source, /class="schedule-tag"/);
assert.match(source, /提交后显示反馈与参考答案/);
});
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
@@ -231,8 +234,14 @@ test('SOP 没解决先收集结构化原因再提交', async () => {
assert.match(source, /feedbackReasonOptions/);
assert.match(source, /negative-feedback-panel/);
assert.match(source, /submitNegativeFeedback/);
assert.match(source, /reasonCodes:\s*selectedFeedbackReasons\.value/);
assert.match(source, /comment:\s*feedbackComment\.value\.trim\(\)/);
for (const code of ['NO_ANSWER', 'NOT_SPECIFIC', 'WRONG_REFERENCE', 'NOT_APPLICABLE']) {
assert.match(source, new RegExp(`code: '${code}'`));
}
assert.doesNotMatch(source, /MISSING_POLICY|ANSWER_INCORRECT/);
assert.match(source, /:disabled="msg\.feedbackSubmitting \|\| !msg\.feedbackReasonCodes\.length"/);
assert.match(source, /if \(!msg\.feedbackReasonCodes\.length\) \{[\s\S]*请至少选择一项未解决原因/);
assert.match(source, /reasonCodes: verdict === 'down' \? msg\.feedbackReasonCodes/);
assert.match(source, /comment: verdict === 'down' \? msg\.feedbackComment\.trim\(\)/);
assert.match(service, /reasonCodes\?:\s*string\[\]/);
assert.match(service, /comment\?:\s*string/);
});
@@ -250,8 +259,9 @@ test('训练页保留任务卡、对话区、回复区和结果区并互斥切
assert.match(source, /practiceView === 'prep'/);
assert.match(source, /practiceView === 'active'/);
assert.match(source, /practiceView\.value === 'result'/);
assert.match(source, /result\.score\?\.length/);
assert.match(source, /class="result-dimensions"/);
assert.match(source, /result\.totalScore/);
assert.match(source, /result\.aiComment/);
assert.match(source, /result\.mentorRewrite/);
});
test('底部固定导航页面复用安全操作区', async () => {
@@ -283,7 +293,8 @@ test('员工问师傅页显示当前岗位而不是固定生活顾问', async ()
assert.match(source, /getSelectedPosition/);
assert.match(source, /const positionLabel = ref\(getSelectedPosition\(\)\)/);
assert.match(source, /:context="`\$\{positionLabel\} · 住宅项目`"/);
assert.match(source, /const headerContext = computed\(\(\) =>/);
assert.match(source, /\$\{positionLabel\.value\} · \$\{currentProject\.value\?\.projectName \|\| '住宅项目'\}/);
assert.doesNotMatch(source, /sop-context">管家(生活顾问) · 住宅项目/);
});
@@ -331,14 +342,15 @@ test('学习页显示岗位 SOP 适配状态而不冒充上岗资格', async ()
]);
assert.match(source, /positionSopSummary/);
assert.match(source, /正式上岗资格/);
assert.match(source, /上岗资格/);
assert.match(knowledgeService, /\/api\/knowledge\/position-sop/);
});
test('员工画像明确参考等级与人工贡献分语义', async () => {
const source = await pageSource('../src/pages/user/profile/index.vue');
assert.match(source, /训练画像等级(参考)/);
assert.match(source, /AI训练综合分仅作参考/);
assert.match(source, /参考等级/);
assert.match(source, /人工贡献分/);
});
@@ -368,7 +380,7 @@ test('员工岗位只能来自人力系统且个人页不再允许手动切换',
pageSource('../src/services/auth.ts')
]);
assert.match(profileSource, /岗位来自人力系统组织同步/);
assert.match(profileSource, /岗位由人力系统组织同步/);
assert.doesNotMatch(profileSource, /changePosition/);
assert.match(positionSource, /员工端不支持手动切换岗位/);
assert.doesNotMatch(positionSource, /saveSelectedPosition|selectPosition/);
@@ -388,12 +400,13 @@ test('主管专项从正式团队名单按姓名多选并一次批量派发', as
assert.match(source, /snapshot\.members/);
assert.match(source, /targets: selectedMembers\.map/);
assert.match(source, /createPracticeAssignmentsBatch/);
assert.match(source, /当天/);
assert.match(source, /明天/);
assert.match(source, /3天内/);
assert.match(source, /7天内/);
assert.match(source, /3天后/);
assert.match(source, /7天后/);
assert.match(source, /dueDate: dueDate\.value/);
assert.match(source, /batchResultItems/);
assert.match(source, /const resultItems: PracticeAssignmentBatchItemResult\[\] = result\.results \|\| \[\]/);
assert.match(source, /assignResultStorageKey/);
assert.match(source, /uni\.redirectTo\(\{ url: '\/pages\/supervisor\/assign\/result' \}\)/);
assert.match(source, /const maxAssignmentTargets = 100/);
assert.match(source, /teamMembers\.value\.slice\(0, maxAssignmentTargets\)/);
assert.match(source, /persistPendingBatchDraft/);