feat(mobile): finish employee app page polish
This commit is contained in:
@@ -182,6 +182,7 @@ curl -k -s https://peilian.njzhmj.top/h5/ | sed -n '1,20p'
|
|||||||
- SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。
|
- SOP知识库:进入 `/knowledge/sop`,可上传 txt/md/PDF/Word/Excel/PPT 文档入库;点击“检索” → “生成训练题”,应看到“已完成闭环”、命中数据库 SOP 原文片段和训练题;数据库不可用时页面回退 seed。
|
||||||
- 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。
|
- 资料处理:进入 `/knowledge/processing`,应看到资料总量、解析任务表、处理链路、规则与风险;可用少量文件验证“批量导入/选择目录/服务端导入”。**批量导入走异步队列**:提交即返回,页面出现“本次批量上传”进度面板(排队/加工中/完成/失败 + 单条重试),后台 worker 并发 2 逐条解析入库;暂存目录默认 `./.data/staging`(`aihr.upload.staging` 覆盖)。服务端导入读取 `./.data/import` 下的相对目录,启动后台任务并在页面显示进度,运行中任务可点“取消”;目录导入的重试粒度是同目录重新导入,批量上传的重试粒度是单文件。
|
||||||
- uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。
|
- uni-app 员工端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`,手机号登录(dev 验证码固定 `123456`)。今日页进入问师傅、练习、案例素材和个人页;练习页应能完成开始练习、提交回应、结束评分、每日三题提交,个人页同步训练历史和成长证据包。
|
||||||
|
- 移动端登录后若组织快照接口返回 `401/403`,只降级为手动岗位确认,不应清除手机号登录态;岗位确认页仍需允许员工选择“生活顾问”后继续进入业务页。
|
||||||
- uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。
|
- uni-app 主管端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/supervisor/index/index`,应看到团队概览、团队画像、团队预警、指派专项、待复盘和复盘详情;从团队画像派专项后,最近专项应立即回读新记录。
|
||||||
- uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。
|
- uni-app 候选人端:进入 `https://wygj-mobile-uni.localhost/h5/#/pages/candidate/index/index`,手机号登录(dev 验证码固定 `123456`)→ “开始面试/面试练习”完成答题评分;“补充资料”选择 PDF/Word/图片后上传,应看到资料状态。Codex 内置浏览器不支持本地文件选择时,用真实 HTTP multipart smoke 代替浏览器文件选择。
|
||||||
- 真 LLM 激活:在 `/system/model` 给供应商填 api_host/api_key 并启用 `category=chat` 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 `category=asr/tts`(如硅基流动 SenseVoice/CosyVoice2)语音路径生效。未配置时全链路自动回退 seed。
|
- 真 LLM 激活:在 `/system/model` 给供应商填 api_host/api_key 并启用 `category=chat` 模型后,三角色对练的客户回复与评分即为真实 LLM 生成;再启用 `category=asr/tts`(如硅基流动 SenseVoice/CosyVoice2)语音路径生效。未配置时全链路自动回退 seed。
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "#7b8494",
|
"color": "#7b8494",
|
||||||
"selectedColor": "#2f6bff",
|
"selectedColor": "#e70014",
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"borderStyle": "white",
|
"borderStyle": "white",
|
||||||
"height": "66px",
|
"height": "66px",
|
||||||
|
|||||||
@@ -1,70 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page login-page">
|
<view class="page login-page">
|
||||||
<view class="login-head">
|
<view class="login-shell">
|
||||||
<view class="brand-lockup">
|
<view class="login-head">
|
||||||
<image class="login-logo" src="/static/brand-logo.png" mode="aspectFit" />
|
<view class="brand-lockup">
|
||||||
<view class="brand-copy">
|
<image class="login-logo" src="/static/brand-logo.png" mode="aspectFit" />
|
||||||
<text class="brand-name">银城物业</text>
|
<view class="brand-copy">
|
||||||
<text class="login-eyebrow">数字师傅</text>
|
<text class="brand-name">银城物业</text>
|
||||||
</view>
|
<text class="brand-divider">|</text>
|
||||||
</view>
|
<text class="login-eyebrow">数字师傅</text>
|
||||||
<text class="page-title">银城员工端</text>
|
|
||||||
<text class="page-subtitle">使用手机号登录后,回到当前任务。</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="panel login-card">
|
|
||||||
<text class="panel-title">手机号登录</text>
|
|
||||||
<uni-forms
|
|
||||||
ref="loginForm"
|
|
||||||
class="login-form"
|
|
||||||
:model="formData"
|
|
||||||
:rules="formRules"
|
|
||||||
label-position="top"
|
|
||||||
validate-trigger="submit"
|
|
||||||
err-show-type="undertext"
|
|
||||||
:border="false"
|
|
||||||
>
|
|
||||||
<uni-forms-item label="手机号" name="phone" required>
|
|
||||||
<uni-easyinput
|
|
||||||
v-model="formData.phone"
|
|
||||||
type="number"
|
|
||||||
maxlength="11"
|
|
||||||
:clearable="false"
|
|
||||||
primary-color="#e70014"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
placeholder-style="color: #9aa5b5"
|
|
||||||
:styles="inputStyles"
|
|
||||||
/>
|
|
||||||
</uni-forms-item>
|
|
||||||
<uni-forms-item label="验证码" name="smsCode" required class="code-form-item">
|
|
||||||
<view class="code-row">
|
|
||||||
<uni-easyinput
|
|
||||||
v-model="formData.smsCode"
|
|
||||||
class="code-input"
|
|
||||||
type="number"
|
|
||||||
maxlength="6"
|
|
||||||
:clearable="false"
|
|
||||||
primary-color="#e70014"
|
|
||||||
placeholder="请输入验证码"
|
|
||||||
placeholder-style="color: #9aa5b5"
|
|
||||||
:styles="inputStyles"
|
|
||||||
/>
|
|
||||||
<button class="secondary-button code-button" :disabled="sending" @click="sendCode">
|
|
||||||
{{ sending ? '发送中' : '获取验证码' }}
|
|
||||||
</button>
|
|
||||||
</view>
|
</view>
|
||||||
</uni-forms-item>
|
</view>
|
||||||
</uni-forms>
|
<text class="page-title">银城员工端</text>
|
||||||
<button class="primary-button" :disabled="loggingIn" @click="login">
|
<text class="page-subtitle">高效协同 · 贴心服务 · 让工作更简单</text>
|
||||||
{{ loggingIn ? '登录中' : '登录' }}
|
</view>
|
||||||
</button>
|
|
||||||
<uni-notice-bar
|
<view class="panel login-card">
|
||||||
v-if="message"
|
<uni-forms
|
||||||
:text="message"
|
ref="loginForm"
|
||||||
:background-color="message === '验证码已发送' ? '#edf7f0' : '#fff2f3'"
|
class="login-form"
|
||||||
:color="message === '验证码已发送' ? '#168243' : '#bd0010'"
|
:model="formData"
|
||||||
show-icon
|
:rules="formRules"
|
||||||
/>
|
label-position="top"
|
||||||
|
validate-trigger="submit"
|
||||||
|
err-show-type="undertext"
|
||||||
|
:border="false"
|
||||||
|
>
|
||||||
|
<uni-forms-item label="手机号" name="phone">
|
||||||
|
<view class="field-shell">
|
||||||
|
<uni-icons class="field-icon" type="phone" size="22" color="#a7adb8" />
|
||||||
|
<uni-easyinput
|
||||||
|
v-model="formData.phone"
|
||||||
|
aria-label="手机号"
|
||||||
|
type="number"
|
||||||
|
maxlength="11"
|
||||||
|
:clearable="false"
|
||||||
|
primary-color="#e70014"
|
||||||
|
@input="onPhoneInput"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
placeholder-style="color: #b5b8c1"
|
||||||
|
:styles="inputStyles"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</uni-forms-item>
|
||||||
|
<uni-forms-item label="验证码" name="smsCode">
|
||||||
|
<view class="code-row">
|
||||||
|
<view class="field-shell code-shell">
|
||||||
|
<uni-icons class="field-icon" type="locked" size="22" color="#a7adb8" />
|
||||||
|
<uni-easyinput
|
||||||
|
v-model="formData.smsCode"
|
||||||
|
class="code-input"
|
||||||
|
aria-label="验证码"
|
||||||
|
type="number"
|
||||||
|
maxlength="6"
|
||||||
|
:clearable="false"
|
||||||
|
primary-color="#e70014"
|
||||||
|
@input="onSmsCodeInput"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
placeholder-style="color: #b5b8c1"
|
||||||
|
:styles="inputStyles"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<button class="secondary-button code-button" :disabled="sending" @click="sendCode">
|
||||||
|
{{ sending ? '发送中' : '获取验证码' }}
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</uni-forms-item>
|
||||||
|
</uni-forms>
|
||||||
|
<button class="primary-button login-submit" :disabled="loggingIn" @click="login">
|
||||||
|
{{ loggingIn ? '登录中' : '登录' }}
|
||||||
|
</button>
|
||||||
|
<uni-notice-bar
|
||||||
|
v-if="message"
|
||||||
|
:text="message"
|
||||||
|
:background-color="message === '验证码已发送' ? '#edf7f0' : '#fff2f3'"
|
||||||
|
:color="message === '验证码已发送' ? '#168243' : '#bd0010'"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -103,9 +115,23 @@ const loggingIn = ref(false);
|
|||||||
const message = ref('');
|
const message = ref('');
|
||||||
const inputStyles = {
|
const inputStyles = {
|
||||||
color: '#25282d',
|
color: '#25282d',
|
||||||
borderColor: '#d7d9de',
|
borderColor: 'transparent',
|
||||||
backgroundColor: '#ffffff'
|
backgroundColor: 'transparent'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeDigits = (value: string | number, maxLength: number) =>
|
||||||
|
String(value || '')
|
||||||
|
.replace(/\D/g, '')
|
||||||
|
.slice(0, maxLength);
|
||||||
|
|
||||||
|
const onPhoneInput = (value: string | number) => {
|
||||||
|
formData.phone = normalizeDigits(value, 11);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSmsCodeInput = (value: string | number) => {
|
||||||
|
formData.smsCode = normalizeDigits(value, 6);
|
||||||
|
};
|
||||||
|
|
||||||
const goAfterLogin = async () => {
|
const goAfterLogin = async () => {
|
||||||
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
const redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
||||||
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
||||||
@@ -178,145 +204,294 @@ onShow(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.login-page {
|
.login-page {
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
padding: 0;
|
||||||
justify-content: flex-start;
|
background:
|
||||||
padding-top: 52px;
|
radial-gradient(circle at 18% 10%, rgba(255, 255, 255, 0.98), rgba(247, 247, 247, 0.86) 38%, rgba(239, 239, 239, 0.72) 100%),
|
||||||
background: #f7f7f8;
|
linear-gradient(180deg, #fafafa 0%, #f2f2f2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 114px 30px 52px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell::before,
|
||||||
|
.login-shell::after {
|
||||||
|
position: fixed;
|
||||||
|
inset: auto;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.45);
|
||||||
|
filter: blur(10px);
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell::before {
|
||||||
|
top: 64px;
|
||||||
|
right: -46px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-shell::after {
|
||||||
|
bottom: 120px;
|
||||||
|
left: -72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-head {
|
.login-head {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 32px;
|
max-width: 720px;
|
||||||
padding-top: 0;
|
margin: 0 auto 43px;
|
||||||
}
|
|
||||||
|
|
||||||
.login-head::before {
|
|
||||||
position: absolute;
|
|
||||||
top: 73px;
|
|
||||||
left: 0;
|
|
||||||
width: 42px;
|
|
||||||
height: 3px;
|
|
||||||
border-radius: 0;
|
|
||||||
background: #e70014;
|
|
||||||
content: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-lockup {
|
.brand-lockup {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
margin-bottom: 28px;
|
margin-bottom: 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-logo {
|
.login-logo {
|
||||||
width: 42px;
|
width: 48px;
|
||||||
height: 42px;
|
height: 48px;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
transform: translateX(6px) scale(1.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-copy {
|
.brand-copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 3px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-name,
|
.brand-name,
|
||||||
|
.brand-divider,
|
||||||
.login-eyebrow {
|
.login-eyebrow {
|
||||||
display: block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
color: #34373c;
|
color: #34373c;
|
||||||
font-size: 13px;
|
font-size: 18px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-divider {
|
||||||
|
color: #8d9097;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.login-eyebrow {
|
.login-eyebrow {
|
||||||
color: #168243;
|
color: #1d9668;
|
||||||
font-size: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-head .page-title {
|
.login-head .page-title {
|
||||||
color: #25282d;
|
display: block;
|
||||||
font-size: 32px;
|
color: #252931;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 800;
|
||||||
line-height: 1.16;
|
line-height: 1.16;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-head .page-subtitle {
|
.login-head .page-subtitle {
|
||||||
|
display: block;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
color: #74777d;
|
color: #6d7078;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card {
|
.login-card {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 18px;
|
||||||
padding: 26px 20px 22px;
|
max-width: 720px;
|
||||||
border: 1px solid #e3e4e7;
|
margin: 0 auto;
|
||||||
border-top: 3px solid #e70014;
|
padding: 36px 20px 36px;
|
||||||
|
border: 1px solid rgba(225, 227, 232, 0.95);
|
||||||
|
border-top: 4px solid #f20b1a;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 10px 24px rgba(27, 31, 38, 0.07);
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 12px 28px rgba(29, 31, 35, 0.12), 0 1px 0 rgba(255, 255, 255, 0.8) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card::before {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 26%);
|
||||||
|
content: '';
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form {
|
.login-form {
|
||||||
margin-top: 2px;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form :deep(.uni-forms-item) {
|
.login-form :deep(.uni-forms-item) {
|
||||||
margin-bottom: 18px;
|
margin-bottom: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form :deep(.uni-forms-item:last-child) {
|
.login-form :deep(.uni-forms-item:last-child) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form :deep(.uni-forms-item__label) {
|
.login-form :deep(.uni-forms-item__label) {
|
||||||
padding-bottom: 8px;
|
padding-bottom: 4px;
|
||||||
color: #34373c;
|
color: #20242b;
|
||||||
font-size: 14px;
|
font-size: 15px;
|
||||||
font-weight: 800;
|
font-weight: 700;
|
||||||
}
|
|
||||||
|
|
||||||
.login-form :deep(.uni-easyinput__content) {
|
|
||||||
min-height: 44px;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-form :deep(.uni-forms-item__error) {
|
.login-form :deep(.uni-forms-item__error) {
|
||||||
margin-top: 6px;
|
margin-top: 9px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-row {
|
.field-shell {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
align-items: center;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px solid #d8dce3;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.92);
|
||||||
|
transition: border-color 160ms ease, box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-shell:focus-within {
|
||||||
|
border-color: #e70014;
|
||||||
|
box-shadow: 0 0 0 3px rgba(231, 0, 20, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-shell :deep(.uni-easyinput) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-shell :deep(.uni-easyinput__content) {
|
||||||
|
min-height: auto;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-shell :deep(.uni-easyinput__content-input) {
|
||||||
|
height: 28px;
|
||||||
|
color: #25282d;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-shell :deep(.uni-easyinput__placeholder-class) {
|
||||||
|
color: #b5b8c1;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-icon {
|
||||||
|
margin-right: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 84px;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-shell,
|
||||||
.code-input {
|
.code-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-button {
|
.code-shell {
|
||||||
width: 118px;
|
padding-right: 10px;
|
||||||
min-height: 44px;
|
padding-left: 12px;
|
||||||
flex: none;
|
|
||||||
border-color: #e70014;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #e70014;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card > .primary-button {
|
.code-shell .field-icon {
|
||||||
min-height: 52px;
|
margin-right: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-shell :deep(.uni-easyinput__content-input),
|
||||||
|
.code-shell :deep(.uni-easyinput__placeholder-class) {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border: 1px solid #f20b1a;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #e70014;
|
background: linear-gradient(180deg, #ffffff 0%, #fffdfd 100%);
|
||||||
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.16);
|
color: #f20b1a;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
white-space: nowrap;
|
||||||
|
box-shadow: 0 6px 14px rgba(242, 11, 26, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-submit {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 60px;
|
||||||
|
margin-top: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: stretch;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: linear-gradient(90deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0) 12%),
|
||||||
|
linear-gradient(180deg, #ff1124 0%, #f40619 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
box-shadow: 0 8px 18px rgba(231, 0, 20, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-card :deep(.uni-noticebar) {
|
.login-card :deep(.uni-noticebar) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 375px) {
|
||||||
|
.login-shell {
|
||||||
|
padding-right: 32px;
|
||||||
|
padding-left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name,
|
||||||
|
.brand-divider,
|
||||||
|
.login-eyebrow {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-head .page-title {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
grid-template-columns: minmax(0, 1fr) 80px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page position-page">
|
<view class="page position-page">
|
||||||
|
<view class="brand-bar">
|
||||||
|
<image class="brand-logo" src="/static/brand-logo.png" mode="aspectFit" />
|
||||||
|
<text class="brand-name">银城员工端</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="position-head">
|
<view class="position-head">
|
||||||
<text class="page-title">确认岗位</text>
|
<text class="page-title">确认岗位</text>
|
||||||
<text class="page-subtitle">{{ phone }}</text>
|
<text class="page-subtitle">{{ phone }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="message" class="status-message">{{ message }}</view>
|
<view v-if="message" class="status-message">
|
||||||
|
<uni-icons type="info" size="22" color="#d98200" />
|
||||||
|
<text>{{ message }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="panel">
|
<view class="panel">
|
||||||
<text class="panel-title">选择当前岗位</text>
|
<text class="panel-title">选择当前岗位</text>
|
||||||
<view class="position-list">
|
<view class="position-grid">
|
||||||
<button
|
<button
|
||||||
v-for="item in positionCards"
|
v-for="item in positionCards"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -22,10 +30,14 @@
|
|||||||
<text class="position-name">{{ item.value }}</text>
|
<text class="position-name">{{ item.value }}</text>
|
||||||
<text class="position-title">{{ item.heroTitle }}</text>
|
<text class="position-title">{{ item.heroTitle }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="position-mark">{{ item.value === selectedPosition ? '已选' : '选择' }}</text>
|
<view class="position-radio" :class="{ checked: item.value === selectedPosition }" />
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<button class="confirm-button" :disabled="!selectedPosition || saving" @click="confirmPosition">
|
||||||
|
{{ saving ? '保存中…' : '确认岗位' }}
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -57,8 +69,13 @@ const finish = () => {
|
|||||||
|
|
||||||
const selectPosition = (position: UserPosition) => {
|
const selectPosition = (position: UserPosition) => {
|
||||||
if (saving.value) return;
|
if (saving.value) return;
|
||||||
|
selectedPosition.value = position;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmPosition = () => {
|
||||||
|
if (!selectedPosition.value || saving.value) return;
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
const saved = saveSelectedPosition(position);
|
const saved = saveSelectedPosition(selectedPosition.value);
|
||||||
selectedPosition.value = saved;
|
selectedPosition.value = saved;
|
||||||
finish();
|
finish();
|
||||||
};
|
};
|
||||||
@@ -95,39 +112,105 @@ onShow(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.position-page {
|
.position-page {
|
||||||
display: flex;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
flex-direction: column;
|
padding: 24px 24px 32px;
|
||||||
justify-content: center;
|
background: linear-gradient(180deg, #ffffff 0%, #fbfbfb 68%, #f6f6f6 100%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-name {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-head {
|
.position-head {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-list {
|
.page-title {
|
||||||
|
display: block;
|
||||||
|
color: #18202b;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #7d8794;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 16px;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
gap: 12px;
|
||||||
gap: 10px;
|
margin-bottom: 22px;
|
||||||
|
padding: 16px 14px;
|
||||||
|
border: 1px solid #f1c889;
|
||||||
|
border-left: 4px solid #d98200;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff8ec;
|
||||||
|
color: #8a5a12;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
padding: 22px 18px 24px;
|
||||||
|
border: 1px solid #eceff2;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 10px 26px rgba(24, 28, 36, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
display: block;
|
||||||
|
color: #18202b;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-card {
|
.position-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 72px;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 112px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 14px 16px;
|
padding: 16px 14px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e0e4e9;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background: #f8fafc;
|
background: #ffffff;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-card.active {
|
.position-card.active {
|
||||||
border-color: #2563eb;
|
border-color: #e70014;
|
||||||
background: #eff6ff;
|
background: #fffafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-copy {
|
.position-copy {
|
||||||
@@ -137,21 +220,45 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.position-name {
|
.position-name {
|
||||||
color: #0f172a;
|
color: #18202b;
|
||||||
font-size: 18px;
|
font-size: 19px;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-title {
|
.position-title {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
color: #64748b;
|
color: #7d8794;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.position-mark {
|
.position-radio {
|
||||||
color: #2563eb;
|
width: 22px;
|
||||||
font-size: 14px;
|
height: 22px;
|
||||||
font-weight: 900;
|
flex: none;
|
||||||
white-space: nowrap;
|
border: 2px solid #b8bec7;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.position-radio.checked {
|
||||||
|
border-color: #e70014;
|
||||||
|
box-shadow: inset 0 0 0 5px #ffffff;
|
||||||
|
background: #e70014;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 56px;
|
||||||
|
margin-top: 24px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #e70014;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
box-shadow: 0 10px 20px rgba(231, 0, 20, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-button[disabled] {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page practice-page employee-page-shell">
|
<view class="page practice-page employee-page-shell">
|
||||||
<view class="practice-head">
|
<view class="practice-head">
|
||||||
<text class="employee-kicker practice-eyebrow">数字师傅 · 场景训练</text>
|
<text class="page-title">练</text>
|
||||||
<text class="page-title">场景模拟</text>
|
<text class="page-subtitle">数字师傅 · 场景训练</text>
|
||||||
<text class="page-subtitle">与业主、老师傅完成一轮真实工作对话,结束后自动写入训练记录。</text>
|
|
||||||
<text class="practice-tip">先看预习卡,再开始练习;红线命中时按老师傅建议重说本轮。</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="message" class="status-message">{{ message }}</view>
|
<view v-if="message" class="status-message">{{ message }}</view>
|
||||||
@@ -19,26 +17,17 @@
|
|||||||
|
|
||||||
<view v-if="loggedIn" class="panel scenario-card employee-card">
|
<view v-if="loggedIn" class="panel scenario-card employee-card">
|
||||||
<view class="scenario-headline">
|
<view class="scenario-headline">
|
||||||
<text class="scenario-badge">今日小训</text>
|
<text class="scenario-title">场景模拟</text>
|
||||||
<text class="scenario-title">{{ activeScenarioName }}</text>
|
<text class="scenario-desc">先看预习卡,再开始练习</text>
|
||||||
<text class="scenario-desc">{{ scenarioDescription }}</text>
|
</view>
|
||||||
|
<view class="scenario-visual" aria-hidden="true">
|
||||||
|
<view class="visual-sheet" />
|
||||||
|
<view class="visual-bubble"><text>•••</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="prepCard" class="prep-card">
|
<view v-if="prepCard" class="prep-card">
|
||||||
<view class="prep-head">
|
<view class="prep-mini"><text class="prep-label">要点</text><text>关键要点先掌握</text></view>
|
||||||
<text>训练前预习卡</text>
|
<view class="prep-mini"><text class="prep-label">红线</text><text>风险红线要注意</text></view>
|
||||||
</view>
|
<view class="prep-mini"><text class="prep-label">可说话术</text><text>有效话术可参考</text></view>
|
||||||
<view class="prep-block">
|
|
||||||
<text class="prep-label">要点</text>
|
|
||||||
<text v-for="item in prepCard.keyPoints" :key="`point-${item}`" class="prep-line">{{ item }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="prep-block">
|
|
||||||
<text class="prep-label">红线</text>
|
|
||||||
<text v-for="item in prepCard.redLines" :key="`red-${item}`" class="prep-line">{{ item }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="prep-block">
|
|
||||||
<text class="prep-label">话术</text>
|
|
||||||
<text v-for="item in prepCard.scripts" :key="`script-${item}`" class="prep-line">{{ item }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<text v-else-if="prepLoading" class="muted">预习卡加载中</text>
|
<text v-else-if="prepLoading" class="muted">预习卡加载中</text>
|
||||||
<button
|
<button
|
||||||
@@ -51,20 +40,36 @@
|
|||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="loggedIn" class="panel assignment-panel">
|
<view v-if="loggedIn" class="panel assignment-panel recent-panel">
|
||||||
<view class="assignment-head">
|
<view class="assignment-head">
|
||||||
<text class="panel-title">今日任务</text>
|
<text class="panel-title">最近练过</text>
|
||||||
<button class="ghost-button refresh-button" :disabled="assignmentLoading" @click="loadAssignments">刷新</button>
|
<text class="recent-link">全部 ›</text>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="assignments.length" class="assignment-list">
|
<view v-if="recentPracticeItems.length" class="assignment-list">
|
||||||
<view v-for="item in assignments" :key="item.id" class="assignment-row" :class="{ highlighted: highlightedAssignmentId === item.id }">
|
<view v-for="item in recentPracticeItems" :key="item.id" class="assignment-row recent-row" :class="{ highlighted: item.assignment && highlightedAssignmentId === item.assignment.id }" @click="openRecentItem(item)">
|
||||||
|
<view class="recent-icon" aria-hidden="true">{{ item.icon }}</view>
|
||||||
|
<view class="assignment-copy">
|
||||||
|
<text class="assignment-title">{{ item.title }}</text>
|
||||||
|
<text class="assignment-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="recent-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty">暂无最近练习</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loggedIn" class="panel assignment-panel daily-panel">
|
||||||
|
<view class="assignment-head">
|
||||||
|
<view><text class="panel-title">每日三题</text><text class="daily-subtitle">今日安排</text></view>
|
||||||
|
<text class="daily-action-label">去作答 ›</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="dailyAssignments.length" class="assignment-list">
|
||||||
|
<view v-for="item in dailyAssignments" :key="item.id" class="assignment-row daily-row">
|
||||||
<view class="assignment-copy">
|
<view class="assignment-copy">
|
||||||
<text class="assignment-badge" :class="{ daily: item.source === 'daily' }">{{ item.source === 'daily' ? '每日三题' : '主管专项' }}</text>
|
|
||||||
<text class="assignment-title">{{ item.question || item.scenarioName }}</text>
|
<text class="assignment-title">{{ item.question || item.scenarioName }}</text>
|
||||||
<text v-if="assignmentDesc(item)" class="assignment-desc">{{ assignmentDesc(item) }}</text>
|
<text v-if="assignmentDesc(item)" class="assignment-desc">{{ assignmentDesc(item) }}</text>
|
||||||
<text v-if="item.status === '已完成' && item.referenceAnswer" class="assignment-reference">参考:{{ item.referenceAnswer }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-if="item.source === 'daily'" class="daily-answer">
|
<view class="daily-answer">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="drillAnswers[item.id]"
|
v-model="drillAnswers[item.id]"
|
||||||
class="textarea daily-textarea"
|
class="textarea daily-textarea"
|
||||||
@@ -76,10 +81,18 @@
|
|||||||
{{ item.status === '已完成' ? '已完成' : '提交' }}
|
{{ item.status === '已完成' ? '已完成' : '提交' }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<button v-else class="secondary-button small-button" :disabled="!canStartPractice" @click="startAssignment(item)">开始专项</button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="empty">暂无今日任务</view>
|
<view v-else class="daily-empty">
|
||||||
|
<text>今日暂无题目</text>
|
||||||
|
<text>完成专项训练后会自动生成每日练习</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loggedIn" class="capability-strip">
|
||||||
|
<view><text>情绪</text><text>识别与安抚</text></view>
|
||||||
|
<view><text>信任</text><text>建立与维护</text></view>
|
||||||
|
<view><text>红线纠偏</text><text>识别与规避</text></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="turns.length" class="panel dialogue-card employee-card">
|
<view v-if="turns.length" class="panel dialogue-card employee-card">
|
||||||
@@ -178,9 +191,10 @@ import type {
|
|||||||
PracticePrepCard,
|
PracticePrepCard,
|
||||||
PracticeAssignment,
|
PracticeAssignment,
|
||||||
PracticeRole,
|
PracticeRole,
|
||||||
PracticeTurn
|
PracticeTurn,
|
||||||
|
PracticeRecord
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { answerDailyDrill, finishPractice, getPracticeAssignments, getPracticePrepCard, startPractice, submitPracticeTurn } from '@/services/practice';
|
import { answerDailyDrill, finishPractice, getPracticeAssignments, getPracticeHistory, getPracticePrepCard, startPractice, submitPracticeTurn } from '@/services/practice';
|
||||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||||
import { searchKnowledge } from '@/services/knowledge';
|
import { searchKnowledge } from '@/services/knowledge';
|
||||||
import { resetPageScroll } from '@/services/navigation';
|
import { resetPageScroll } from '@/services/navigation';
|
||||||
@@ -188,6 +202,14 @@ import { ensureEmployeePosition } from '@/services/position';
|
|||||||
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||||
|
|
||||||
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
type PracticeStatus = 'idle' | 'starting' | 'active' | 'submitting' | 'finished';
|
||||||
|
type RecentPracticeItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
status: string;
|
||||||
|
icon: string;
|
||||||
|
assignment?: PracticeAssignment;
|
||||||
|
};
|
||||||
|
|
||||||
const defaultScenarioId = 'complaint-water';
|
const defaultScenarioId = 'complaint-water';
|
||||||
const defaultScenarioName = '投诉接待';
|
const defaultScenarioName = '投诉接待';
|
||||||
@@ -213,6 +235,7 @@ const emotionScore = ref(0);
|
|||||||
const trustScore = ref(0);
|
const trustScore = ref(0);
|
||||||
const redFlagActive = ref(false);
|
const redFlagActive = ref(false);
|
||||||
const assignments = ref<PracticeAssignment[]>([]);
|
const assignments = ref<PracticeAssignment[]>([]);
|
||||||
|
const practiceHistory = ref<PracticeRecord[]>([]);
|
||||||
const drillAnswers = ref<Record<number, string>>({});
|
const drillAnswers = ref<Record<number, string>>({});
|
||||||
const assignmentLoading = ref(false);
|
const assignmentLoading = ref(false);
|
||||||
const answeringId = ref<number | null>(null);
|
const answeringId = ref<number | null>(null);
|
||||||
@@ -231,17 +254,27 @@ const hasTraineeReply = computed(() => turns.value.some((item) => item.role ===
|
|||||||
const canFinishPractice = computed(() => hasTraineeReply.value && !redFlagActive.value);
|
const canFinishPractice = computed(() => hasTraineeReply.value && !redFlagActive.value);
|
||||||
const hasCoachState = computed(() => emotionScore.value > 0 || trustScore.value > 0 || redFlagActive.value);
|
const hasCoachState = computed(() => emotionScore.value > 0 || trustScore.value > 0 || redFlagActive.value);
|
||||||
const canAdjustCoach = computed(() => status.value === 'active' && lastCoachRoundIndex.value !== null && turns.value.some((item) => item.role === 'coach'));
|
const canAdjustCoach = computed(() => status.value === 'active' && lastCoachRoundIndex.value !== null && turns.value.some((item) => item.role === 'coach'));
|
||||||
const activeAssignment = computed(() => assignments.value.find((item) => item.id === activeAssignmentId.value));
|
const recentPracticeItems = computed<RecentPracticeItem[]>(() => {
|
||||||
const scenarioDescription = computed(() => {
|
const assignmentItems: RecentPracticeItem[] = assignments.value
|
||||||
const points = (prepCard.value?.keyPoints || []).map((item) => item.trim()).filter(Boolean).slice(0, 2);
|
.filter((item) => item.source !== 'daily')
|
||||||
if (points.length) return `本场重点:${points.join(';')}`;
|
.map((item) => ({
|
||||||
if (activeAssignment.value?.reason) return activeAssignment.value.reason;
|
id: `assignment-${item.id}`,
|
||||||
const name = activeScenarioName.value;
|
title: item.question || item.scenarioName || '专项训练',
|
||||||
if (/催|缴|费|停车/.test(name)) return '练习承接缴费异议、说明规则依据、给出缴费路径和跟进节点。';
|
desc: item.status === '已完成' ? '已完成' : '待复盘',
|
||||||
if (/报修|维修|设施|电梯|漏水/.test(name)) return '练习记录问题、安抚情绪、说明报修流转和反馈时限。';
|
status: item.status,
|
||||||
if (/突发|应急|安全|火灾|摔|道闸|车库/.test(name)) return '练习先稳现场、同步上报、留痕取证和风险告知。';
|
icon: item.scenarioName?.includes('催费') ? '⌕' : item.scenarioName?.includes('报修') ? '⌁' : '◉',
|
||||||
return '练习承接业主情绪、说明处理动作、明确反馈时间和责任边界。';
|
assignment: item
|
||||||
|
}));
|
||||||
|
const historyItems: RecentPracticeItem[] = practiceHistory.value.map((item) => ({
|
||||||
|
id: `history-${item.id || item.sessionId || item.time}`,
|
||||||
|
title: item.scene || '场景训练',
|
||||||
|
desc: `${item.score || 0} 分 · ${item.status || '已完成'}`,
|
||||||
|
status: item.status,
|
||||||
|
icon: item.scene?.includes('催费') ? '⌕' : item.scene?.includes('报修') ? '⌁' : '◉'
|
||||||
|
}));
|
||||||
|
return [...assignmentItems, ...historyItems].slice(0, 3);
|
||||||
});
|
});
|
||||||
|
const dailyAssignments = computed(() => assignments.value.filter((item) => item.source === 'daily'));
|
||||||
const styleOptions = [
|
const styleOptions = [
|
||||||
{ value: 'serious', label: '再严肃' },
|
{ value: 'serious', label: '再严肃' },
|
||||||
{ value: 'gentle', label: '再温柔' },
|
{ value: 'gentle', label: '再温柔' },
|
||||||
@@ -272,6 +305,7 @@ const refreshAuthState = () => {
|
|||||||
const clearPracticeData = () => {
|
const clearPracticeData = () => {
|
||||||
cancelRecording();
|
cancelRecording();
|
||||||
assignments.value = [];
|
assignments.value = [];
|
||||||
|
practiceHistory.value = [];
|
||||||
drillAnswers.value = {};
|
drillAnswers.value = {};
|
||||||
prepCard.value = null;
|
prepCard.value = null;
|
||||||
prepLoading.value = false;
|
prepLoading.value = false;
|
||||||
@@ -447,6 +481,11 @@ const loadAssignments = async () => {
|
|||||||
assignmentLoading.value = true;
|
assignmentLoading.value = true;
|
||||||
try {
|
try {
|
||||||
assignments.value = await getPracticeAssignments(true);
|
assignments.value = await getPracticeAssignments(true);
|
||||||
|
try {
|
||||||
|
practiceHistory.value = await getPracticeHistory();
|
||||||
|
} catch {
|
||||||
|
practiceHistory.value = [];
|
||||||
|
}
|
||||||
drillAnswers.value = Object.fromEntries(assignments.value.map((item) => [item.id, item.userAnswer || '']));
|
drillAnswers.value = Object.fromEntries(assignments.value.map((item) => [item.id, item.userAnswer || '']));
|
||||||
consumePendingAssignment();
|
consumePendingAssignment();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -456,6 +495,14 @@ const loadAssignments = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openRecentItem = (item: RecentPracticeItem) => {
|
||||||
|
if (item.assignment) {
|
||||||
|
startAssignment(item.assignment);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateTo({ url: '/pages/user/profile/index' });
|
||||||
|
};
|
||||||
|
|
||||||
const consumePendingAssignment = () => {
|
const consumePendingAssignment = () => {
|
||||||
const raw = uni.getStorageSync(pendingAssignmentKey);
|
const raw = uni.getStorageSync(pendingAssignmentKey);
|
||||||
if (!raw) return;
|
if (!raw) return;
|
||||||
@@ -869,10 +916,10 @@ onShow(() => {
|
|||||||
.assignment-row {
|
.assignment-row {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border: 1px solid #deebff;
|
border: 1px solid #f1dfe0;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
box-shadow: 0 10px 26px rgba(47, 107, 255, 0.08);
|
box-shadow: 0 8px 22px rgba(24, 28, 36, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-row.highlighted {
|
.assignment-row.highlighted {
|
||||||
@@ -885,15 +932,15 @@ onShow(() => {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #fff0f1;
|
background: #fff1f2;
|
||||||
color: #bd0010;
|
color: #d51b2b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-badge.daily {
|
.assignment-badge.daily {
|
||||||
background: #fff5df;
|
background: #fff1f2;
|
||||||
color: #a86100;
|
color: #d51b2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-title {
|
.assignment-title {
|
||||||
@@ -927,8 +974,8 @@ onShow(() => {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #edf7f0;
|
background: #fff1f2;
|
||||||
color: #168243;
|
color: #d51b2b;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
@@ -1011,7 +1058,7 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialogue-card {
|
.dialogue-card {
|
||||||
border-color: #e1e2e5;
|
border-color: #f0dfe0;
|
||||||
box-shadow: var(--employee-card-shadow);
|
box-shadow: var(--employee-card-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1073,11 +1120,11 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.emotion-fill {
|
.emotion-fill {
|
||||||
background: #d98200;
|
background: #e45757;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trust-fill {
|
.trust-fill {
|
||||||
background: #168243;
|
background: #d51b2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.red-flag {
|
.red-flag {
|
||||||
@@ -1110,8 +1157,8 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.turn-row.customer {
|
.turn-row.customer {
|
||||||
border-left-color: #315b8f;
|
border-left-color: #b8bdc5;
|
||||||
background: #f0f4f8;
|
background: #f7f7f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.turn-row.trainee {
|
.turn-row.trainee {
|
||||||
@@ -1120,8 +1167,8 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.turn-row.coach {
|
.turn-row.coach {
|
||||||
border-left-color: #168243;
|
border-left-color: #d98282;
|
||||||
background: #edf7f0;
|
background: #fff5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-actions button.recording-active {
|
.reply-actions button.recording-active {
|
||||||
@@ -1151,9 +1198,9 @@ onShow(() => {
|
|||||||
|
|
||||||
.style-chip {
|
.style-chip {
|
||||||
width: auto;
|
width: auto;
|
||||||
border-color: #c9d9ce;
|
border-color: #f0c8ca;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: #168243;
|
color: #d51b2b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reply-card {
|
.reply-card {
|
||||||
@@ -1163,7 +1210,7 @@ onShow(() => {
|
|||||||
|
|
||||||
.practice-textarea {
|
.practice-textarea {
|
||||||
min-height: 124px;
|
min-height: 124px;
|
||||||
border-color: #d6deea;
|
border-color: #f0dfe0;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
background: #f9fbff;
|
background: #f9fbff;
|
||||||
}
|
}
|
||||||
@@ -1183,8 +1230,8 @@ onShow(() => {
|
|||||||
.help-ref {
|
.help-ref {
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: #edf7f0;
|
background: #fff1f2;
|
||||||
color: #168243;
|
color: #d51b2b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
@@ -1196,7 +1243,7 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.result-score {
|
.result-score {
|
||||||
color: #2f6bff;
|
color: #e52323;
|
||||||
font-size: 44px;
|
font-size: 44px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -1214,13 +1261,332 @@ onShow(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border-left: 3px solid #d98200;
|
border-left: 3px solid #e52323;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: #fff9ed;
|
background: #fff6f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mentor-rewrite text:first-child {
|
.mentor-rewrite text:first-child {
|
||||||
color: #855000;
|
color: #c71927;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 练页入口态:对应高保真原型,真实对话状态仍沿用下方业务面板 */
|
||||||
|
.practice-page {
|
||||||
|
padding: 0 18px 104px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-head {
|
||||||
|
margin: 0 -18px;
|
||||||
|
padding: 36px 18px 26px;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-head::after,
|
||||||
|
.practice-eyebrow,
|
||||||
|
.practice-tip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-head .page-title {
|
||||||
|
color: #111111;
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.practice-head .page-subtitle {
|
||||||
|
max-width: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #858991;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 22px 18px 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
border-left: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-headline {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 58%;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-title {
|
||||||
|
color: #111111;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-desc {
|
||||||
|
color: #858991;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-visual {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 18px;
|
||||||
|
width: 42%;
|
||||||
|
height: 108px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff1f2;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-sheet {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 24px;
|
||||||
|
width: 92px;
|
||||||
|
height: 76px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.08);
|
||||||
|
transform: rotate(7deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-sheet::before,
|
||||||
|
.visual-sheet::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
width: 48px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5dfe1;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-sheet::before {
|
||||||
|
top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-sheet::after {
|
||||||
|
top: 36px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-bubble {
|
||||||
|
position: absolute;
|
||||||
|
right: 34px;
|
||||||
|
bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
width: 72px;
|
||||||
|
height: 42px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #ef5b61;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 19px;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
box-shadow: 0 8px 14px rgba(231, 0, 20, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-bubble::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
bottom: -7px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #ef5b61;
|
||||||
|
content: '';
|
||||||
|
transform: rotate(35deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prep-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prep-mini {
|
||||||
|
display: flex;
|
||||||
|
min-height: 68px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #f4d9da;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fffafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prep-mini .prep-label {
|
||||||
|
color: #e70014;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prep-mini text:last-child {
|
||||||
|
color: #8c8f96;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scenario-card .primary-button {
|
||||||
|
margin-top: 18px;
|
||||||
|
background: #e70014;
|
||||||
|
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-panel,
|
||||||
|
.daily-panel {
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-link,
|
||||||
|
.daily-action-label {
|
||||||
|
color: #858991;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-row {
|
||||||
|
display: flex;
|
||||||
|
min-height: 64px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #f0f1f3;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
flex: 0 0 42px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff1f2;
|
||||||
|
color: #e52323;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-row .assignment-copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-row .assignment-title {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-row .assignment-desc {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #858991;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-arrow {
|
||||||
|
color: #8c9098;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-panel .assignment-head {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-subtitle {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #858991;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-empty {
|
||||||
|
display: flex;
|
||||||
|
min-height: 72px;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border: 1px dashed #f0c8ca;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fffafa;
|
||||||
|
color: #858991;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-empty text:first-child {
|
||||||
|
color: #d51b2b;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-row {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12px;
|
||||||
|
border-color: #f0f1f3;
|
||||||
|
border-radius: 14px;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-row .daily-answer {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-strip {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 16px 8px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-strip view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
border-right: 1px solid #ececee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-strip view:last-child {
|
||||||
|
border-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-strip text:first-child {
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.capability-strip text:last-child {
|
||||||
|
color: #8a8d93;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,12 +2,23 @@
|
|||||||
<view class="page sop-page employee-page-shell">
|
<view class="page sop-page employee-page-shell">
|
||||||
<view class="sop-head">
|
<view class="sop-head">
|
||||||
<text class="employee-kicker sop-eyebrow">SOP · 话术 · 工作流程</text>
|
<text class="employee-kicker sop-eyebrow">SOP · 话术 · 工作流程</text>
|
||||||
<text class="page-title">问师傅</text>
|
<text class="page-title">问</text>
|
||||||
<text class="page-subtitle">直接问工作中的 SOP、话术和流程。答案附带可追溯的知识库依据。</text>
|
<text class="page-subtitle">数字师傅 · SOP问答</text>
|
||||||
|
<text class="sop-context">管家(生活顾问) · 住宅项目</text>
|
||||||
<text class="toolbar-tip">先把问题问清楚,再按需生成总结卡,不伪造分类能力。</text>
|
<text class="toolbar-tip">先把问题问清楚,再按需生成总结卡,不伪造分类能力。</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="panel query-panel employee-card">
|
<view class="panel query-panel employee-card">
|
||||||
|
<view class="query-hero">
|
||||||
|
<view class="query-copy">
|
||||||
|
<text class="query-title">问师傅</text>
|
||||||
|
<text class="query-subtitle">直接问工作中的 SOP、话术和流程</text>
|
||||||
|
</view>
|
||||||
|
<view class="query-visual" aria-hidden="true">
|
||||||
|
<view class="query-bubble primary"><text>•••</text></view>
|
||||||
|
<view class="query-bubble secondary"><text>•••</text></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<textarea
|
<textarea
|
||||||
v-model="query"
|
v-model="query"
|
||||||
class="textarea sop-query-textarea"
|
class="textarea sop-query-textarea"
|
||||||
@@ -26,6 +37,21 @@
|
|||||||
<text v-if="message" class="status-message">{{ message }}</text>
|
<text v-if="message" class="status-message">{{ message }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loggedIn && !result" class="knowledge-tip-card employee-card">
|
||||||
|
<view class="knowledge-tip-icon">⌕</view>
|
||||||
|
<view class="knowledge-tip-copy">
|
||||||
|
<text class="knowledge-tip-title">知识库引用可追溯</text>
|
||||||
|
<text class="knowledge-tip-desc">答案来自标准 SOP、附件来源,查得清楚、用得放心</text>
|
||||||
|
</view>
|
||||||
|
<text class="knowledge-tip-arrow">›</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loggedIn && !result && !loading" class="answer-empty employee-card">
|
||||||
|
<text class="answer-empty-icon">•••</text>
|
||||||
|
<text class="answer-empty-title">暂无相关回答</text>
|
||||||
|
<text class="answer-empty-desc">换个问题试试吧</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view v-if="!loggedIn" class="panel login-panel employee-card">
|
<view v-if="!loggedIn" class="panel login-panel employee-card">
|
||||||
<view>
|
<view>
|
||||||
<text class="panel-title">需要登录</text>
|
<text class="panel-title">需要登录</text>
|
||||||
@@ -888,4 +914,313 @@ onShow(() => {
|
|||||||
color: #78350f;
|
color: #78350f;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 视觉稿对齐:与练页统一的红白卡片体系 */
|
||||||
|
.sop-page {
|
||||||
|
padding: 0 18px 96px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sop-head {
|
||||||
|
margin: 0 -18px;
|
||||||
|
padding: 34px 18px 24px;
|
||||||
|
background: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sop-head::after,
|
||||||
|
.sop-eyebrow,
|
||||||
|
.toolbar-tip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sop-head .page-title {
|
||||||
|
color: #111111;
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sop-head .page-subtitle {
|
||||||
|
max-width: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #858991;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sop-context {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 7px 14px;
|
||||||
|
border: 1px solid #f8d7d8;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #fff7f7;
|
||||||
|
color: #d51b2b;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-panel {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding: 22px 18px 18px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-panel .textarea,
|
||||||
|
.sop-query-textarea {
|
||||||
|
min-height: 126px;
|
||||||
|
height: 126px;
|
||||||
|
border-color: #dedfe2;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-panel .chip {
|
||||||
|
min-height: 40px;
|
||||||
|
border-color: #e52323;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-submit {
|
||||||
|
min-height: 54px;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #e52323;
|
||||||
|
box-shadow: 0 8px 16px rgba(229, 35, 35, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #fff0f1;
|
||||||
|
color: #e52323;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-copy {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-title {
|
||||||
|
color: #25282d;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-desc {
|
||||||
|
color: #858991;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-tip-arrow {
|
||||||
|
color: #8c9098;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 28px 18px;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-empty-icon {
|
||||||
|
color: #f2b4b6;
|
||||||
|
font-size: 22px;
|
||||||
|
letter-spacing: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-empty-title {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #666a72;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-empty-desc {
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #9a9da3;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-panel {
|
||||||
|
border-color: #f0dfe0;
|
||||||
|
border-left-color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.answer-panel .panel-title,
|
||||||
|
.result-score {
|
||||||
|
color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keypoint,
|
||||||
|
.speech-bubble {
|
||||||
|
border-left-color: #e52323;
|
||||||
|
background: #fff5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cautions,
|
||||||
|
.reminder-item {
|
||||||
|
border-left-color: #e52323;
|
||||||
|
background: #fff5f5;
|
||||||
|
color: #b71925;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-actions .active {
|
||||||
|
border-color: #e52323;
|
||||||
|
background: #fff0f1;
|
||||||
|
color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feedback-actions .negative {
|
||||||
|
border-color: #e52323;
|
||||||
|
background: #fff0f1;
|
||||||
|
color: #b71925;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-panel {
|
||||||
|
border-color: #f0dfe0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-panel .ghost-button {
|
||||||
|
border-color: #e52323;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
border-top-color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-num,
|
||||||
|
.timeline-node {
|
||||||
|
background: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a-badge {
|
||||||
|
background: #fff0f1;
|
||||||
|
color: #e52323;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reminder-text {
|
||||||
|
color: #b71925;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 原型对齐:问师傅主卡的标题与对话插画 */
|
||||||
|
.query-hero {
|
||||||
|
position: relative;
|
||||||
|
min-height: 84px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-copy {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
max-width: 66%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-title {
|
||||||
|
color: #111111;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-subtitle {
|
||||||
|
color: #858991;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-visual {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -4px;
|
||||||
|
width: 42%;
|
||||||
|
height: 96px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-bubble {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-bubble.primary {
|
||||||
|
top: 7px;
|
||||||
|
right: 26px;
|
||||||
|
width: 92px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 28px 28px 28px 10px;
|
||||||
|
background: #f58d91;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-bubble.primary::after {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
left: 18px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #f58d91;
|
||||||
|
content: '';
|
||||||
|
transform: rotate(28deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-bubble.secondary {
|
||||||
|
right: 2px;
|
||||||
|
bottom: 5px;
|
||||||
|
width: 70px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 22px 22px 8px 22px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 5px 14px rgba(229, 35, 35, 0.08);
|
||||||
|
color: #f3b7b9;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-panel .query-caption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export interface RequestOptions {
|
|||||||
header?: Record<string, string>;
|
header?: Record<string, string>;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
auth?: boolean;
|
auth?: boolean;
|
||||||
|
clearAuthOnUnauthorized?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizeUrl = (url: string) => `${apiBase}${url.startsWith('/') ? url : `/${url}`}`;
|
const normalizeUrl = (url: string) => `${apiBase}${url.startsWith('/') ? url : `/${url}`}`;
|
||||||
@@ -27,11 +28,11 @@ export const authHeaders = (json = true, includeAuth = true) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readPayload = <T>(statusCode: number, data: unknown): T => {
|
export const readPayload = <T>(statusCode: number, data: unknown, clearAuthOnUnauthorized = true): T => {
|
||||||
const payload = data as ApiPayload<T>;
|
const payload = data as ApiPayload<T>;
|
||||||
const code = typeof payload?.code === 'number' ? payload.code : undefined;
|
const code = typeof payload?.code === 'number' ? payload.code : undefined;
|
||||||
if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) {
|
if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) {
|
||||||
if (statusCode === 401 || statusCode === 403 || code === 401 || code === 403) {
|
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403 || code === 401 || code === 403)) {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
}
|
}
|
||||||
throw new Error(payload?.msg || `HTTP ${statusCode}`);
|
throw new Error(payload?.msg || `HTTP ${statusCode}`);
|
||||||
@@ -40,11 +41,11 @@ export const readPayload = <T>(statusCode: number, data: unknown): T => {
|
|||||||
return payload?.data as T;
|
return payload?.data as T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const parsePayloadText = (statusCode: number, text: string) => {
|
export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUnauthorized = true) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(text || '{}');
|
return JSON.parse(text || '{}');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (statusCode === 401 || statusCode === 403) clearAuth();
|
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth();
|
||||||
throw error instanceof SyntaxError
|
throw error instanceof SyntaxError
|
||||||
? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败')
|
? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败')
|
||||||
: error;
|
: error;
|
||||||
@@ -67,7 +68,10 @@ const requestWithXhr = async <T>(options: RequestOptions): Promise<T> => {
|
|||||||
}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
|
}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
|
||||||
xhr.onload = () => {
|
xhr.onload = () => {
|
||||||
try {
|
try {
|
||||||
resolve({ status: xhr.status, data: parsePayloadText(xhr.status, xhr.responseText || 'null') });
|
resolve({
|
||||||
|
status: xhr.status,
|
||||||
|
data: parsePayloadText(xhr.status, xhr.responseText || 'null', options.clearAuthOnUnauthorized !== false)
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
reject(new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败'));
|
reject(new Error(xhr.status >= 400 ? `HTTP ${xhr.status}` : '响应解析失败'));
|
||||||
}
|
}
|
||||||
@@ -77,7 +81,7 @@ const requestWithXhr = async <T>(options: RequestOptions): Promise<T> => {
|
|||||||
xhr.send(hasBody ? JSON.stringify(options.data) : undefined);
|
xhr.send(hasBody ? JSON.stringify(options.data) : undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
return readPayload<T>(response.status, response.data);
|
return readPayload<T>(response.status, response.data, options.clearAuthOnUnauthorized !== false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const apiRequest = async <T>(options: RequestOptions): Promise<T> => {
|
export const apiRequest = async <T>(options: RequestOptions): Promise<T> => {
|
||||||
@@ -100,7 +104,7 @@ export const apiRequest = async <T>(options: RequestOptions): Promise<T> => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return readPayload<T>(response.statusCode, response.data);
|
return readPayload<T>(response.statusCode, response.data, options.clearAuthOnUnauthorized !== false);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authenticatedFileUrl = async (url: string): Promise<string> => {
|
export const authenticatedFileUrl = async (url: string): Promise<string> => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const positionProfiles: Record<UserPosition, UserPositionProfile> = {
|
|||||||
value: '生活顾问',
|
value: '生活顾问',
|
||||||
heroTitle: '客服管家',
|
heroTitle: '客服管家',
|
||||||
quickQuestions: ['催费话术', '投诉应对', '收费标准'],
|
quickQuestions: ['催费话术', '投诉应对', '收费标准'],
|
||||||
tip: '今日小贴士:催费沟通先共情,再说明,再给方案。'
|
tip: '接待客户三步法:听清诉求 → 共情理解 → 解决闭环'
|
||||||
},
|
},
|
||||||
保安: {
|
保安: {
|
||||||
value: '保安',
|
value: '保安',
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export const hydratePositionFromOrg = async (phone = getAuth().phone): Promise<U
|
|||||||
const snapshot = await apiRequest<OrgSnapshotResponse>({
|
const snapshot = await apiRequest<OrgSnapshotResponse>({
|
||||||
url: `/api/aihr/org/snapshot?keyword=${encodeURIComponent(keyword)}&status=active&pageSize=1`,
|
url: `/api/aihr/org/snapshot?keyword=${encodeURIComponent(keyword)}&status=active&pageSize=1`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
timeout: 15000
|
timeout: 15000,
|
||||||
|
clearAuthOnUnauthorized: false
|
||||||
});
|
});
|
||||||
const officialPosition = snapshot.rows?.[0]?.positionName || '';
|
const officialPosition = snapshot.rows?.[0]?.positionName || '';
|
||||||
return saveSelectedPosition(officialPosition);
|
return saveSelectedPosition(officialPosition);
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
@@ -23,8 +23,11 @@ test('SOP 查询框不再覆盖 textarea 的 box-sizing', async () => {
|
|||||||
test('员工首页地图主标题保留真实工具名称', async () => {
|
test('员工首页地图主标题保留真实工具名称', async () => {
|
||||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||||
|
|
||||||
assert.match(source, /<text class="map-symbol">\{\{ tile\.shortTitle \}\}<\/text>/);
|
assert.match(source, /class="map-symbol"/);
|
||||||
assert.match(source, /<text class="map-title">\{\{ tile\.title \}\}<\/text>/);
|
assert.match(source, /<text class="map-title">\{\{ tile\.title \}\}<\/text>/);
|
||||||
|
assert.match(source, /title:\s*'问老师傅'/);
|
||||||
|
assert.match(source, /title:\s*'刻意练习'/);
|
||||||
|
assert.match(source, /shortTitle:\s*'问'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
|
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
|
||||||
@@ -43,13 +46,23 @@ test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
|
|||||||
test('员工端 tabBar 继续保留四个固定入口', async () => {
|
test('员工端 tabBar 继续保留四个固定入口', async () => {
|
||||||
const source = await pageSource('../src/pages.json');
|
const source = await pageSource('../src/pages.json');
|
||||||
|
|
||||||
assert.match(source, /"selectedColor"\s*:\s*"#2f6bff"/);
|
assert.match(source, /"selectedColor"\s*:\s*"#e70014"/);
|
||||||
assert.match(source, /pages\/user\/today\/index/);
|
assert.match(source, /pages\/user\/today\/index/);
|
||||||
assert.match(source, /pages\/user\/practice\/index/);
|
assert.match(source, /pages\/user\/practice\/index/);
|
||||||
assert.match(source, /pages\/user\/sop\/index/);
|
assert.match(source, /pages\/user\/sop\/index/);
|
||||||
assert.match(source, /pages\/user\/profile\/index/);
|
assert.match(source, /pages\/user\/profile\/index/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('登录页按高保真原型保留品牌结构和紧凑验证码行', async () => {
|
||||||
|
const source = await pageSource('../src/pages/auth/login/index.vue');
|
||||||
|
|
||||||
|
assert.match(source, /高效协同 · 贴心服务 · 让工作更简单/);
|
||||||
|
assert.match(source, /<uni-icons[^>]+type="phone"/);
|
||||||
|
assert.match(source, /<uni-icons[^>]+type="locked"/);
|
||||||
|
assert.doesNotMatch(source, /class="logo-badge"/);
|
||||||
|
assert.doesNotMatch(source, /grid-template-columns:\s*1fr;[\s\S]*?\.code-button/);
|
||||||
|
});
|
||||||
|
|
||||||
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
|
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
|
||||||
const source = await pageSource('../src/styles/base.css');
|
const source = await pageSource('../src/styles/base.css');
|
||||||
|
|
||||||
@@ -78,6 +91,22 @@ test('员工首页保留师傅区、语音条、今日安排、我的地图四
|
|||||||
assert.match(source, /voice-panel/);
|
assert.match(source, /voice-panel/);
|
||||||
assert.match(source, /今日安排/);
|
assert.match(source, /今日安排/);
|
||||||
assert.match(source, /我的地图/);
|
assert.match(source, /我的地图/);
|
||||||
|
assert.match(source, /brand-bar/);
|
||||||
|
assert.match(source, /ask-card/);
|
||||||
|
assert.match(source, /小贴士/);
|
||||||
|
});
|
||||||
|
|
||||||
|
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, /const openPrimarySchedule = \(\) =>/);
|
||||||
|
assert.match(source, /getMobileHome\('user'\)/);
|
||||||
|
assert.match(source, /getPracticeAssignments\(true\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user