feat(mobile): finish employee app page polish
This commit is contained in:
@@ -131,7 +131,7 @@
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7b8494",
|
||||
"selectedColor": "#2f6bff",
|
||||
"selectedColor": "#e70014",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"height": "66px",
|
||||
|
||||
@@ -1,70 +1,82 @@
|
||||
<template>
|
||||
<view class="page login-page">
|
||||
<view class="login-head">
|
||||
<view class="brand-lockup">
|
||||
<image class="login-logo" src="/static/brand-logo.png" mode="aspectFit" />
|
||||
<view class="brand-copy">
|
||||
<text class="brand-name">银城物业</text>
|
||||
<text class="login-eyebrow">数字师傅</text>
|
||||
</view>
|
||||
</view>
|
||||
<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 class="login-shell">
|
||||
<view class="login-head">
|
||||
<view class="brand-lockup">
|
||||
<image class="login-logo" src="/static/brand-logo.png" mode="aspectFit" />
|
||||
<view class="brand-copy">
|
||||
<text class="brand-name">银城物业</text>
|
||||
<text class="brand-divider">|</text>
|
||||
<text class="login-eyebrow">数字师傅</text>
|
||||
</view>
|
||||
</uni-forms-item>
|
||||
</uni-forms>
|
||||
<button class="primary-button" :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>
|
||||
<text class="page-title">银城员工端</text>
|
||||
<text class="page-subtitle">高效协同 · 贴心服务 · 让工作更简单</text>
|
||||
</view>
|
||||
|
||||
<view class="panel login-card">
|
||||
<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">
|
||||
<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>
|
||||
</template>
|
||||
@@ -103,9 +115,23 @@ const loggingIn = ref(false);
|
||||
const message = ref('');
|
||||
const inputStyles = {
|
||||
color: '#25282d',
|
||||
borderColor: '#d7d9de',
|
||||
backgroundColor: '#ffffff'
|
||||
borderColor: 'transparent',
|
||||
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 redirect = consumeLoginRedirect() || '/pages/user/today/index';
|
||||
goToMobileTarget(await nextUserRouteAfterLogin(redirect));
|
||||
@@ -178,145 +204,294 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.login-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding-top: 52px;
|
||||
background: #f7f7f8;
|
||||
padding: 0;
|
||||
background:
|
||||
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%),
|
||||
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 {
|
||||
position: relative;
|
||||
margin-bottom: 32px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.login-head::before {
|
||||
position: absolute;
|
||||
top: 73px;
|
||||
left: 0;
|
||||
width: 42px;
|
||||
height: 3px;
|
||||
border-radius: 0;
|
||||
background: #e70014;
|
||||
content: '';
|
||||
max-width: 720px;
|
||||
margin: 0 auto 43px;
|
||||
}
|
||||
|
||||
.brand-lockup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 28px;
|
||||
gap: 12px;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: none;
|
||||
transform: translateX(6px) scale(1.4);
|
||||
}
|
||||
|
||||
.brand-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brand-name,
|
||||
.brand-divider,
|
||||
.login-eyebrow {
|
||||
display: block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #34373c;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-divider {
|
||||
color: #8d9097;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.login-eyebrow {
|
||||
color: #168243;
|
||||
font-size: 12px;
|
||||
color: #1d9668;
|
||||
}
|
||||
|
||||
.login-head .page-title {
|
||||
color: #25282d;
|
||||
font-size: 32px;
|
||||
display: block;
|
||||
color: #252931;
|
||||
font-size: 34px;
|
||||
font-weight: 800;
|
||||
line-height: 1.16;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-head .page-subtitle {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #74777d;
|
||||
font-size: 14px;
|
||||
color: #6d7078;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 26px 20px 22px;
|
||||
border: 1px solid #e3e4e7;
|
||||
border-top: 3px solid #e70014;
|
||||
gap: 18px;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 36px 20px 36px;
|
||||
border: 1px solid rgba(225, 227, 232, 0.95);
|
||||
border-top: 4px solid #f20b1a;
|
||||
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 {
|
||||
margin-top: 2px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.login-form :deep(.uni-forms-item) {
|
||||
margin-bottom: 18px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.login-form :deep(.uni-forms-item:last-child) {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.login-form :deep(.uni-forms-item__label) {
|
||||
padding-bottom: 8px;
|
||||
color: #34373c;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.login-form :deep(.uni-easyinput__content) {
|
||||
min-height: 44px;
|
||||
border-radius: 6px;
|
||||
padding-bottom: 4px;
|
||||
color: #20242b;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.login-form :deep(.uni-forms-item__error) {
|
||||
margin-top: 6px;
|
||||
margin-top: 9px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.code-row {
|
||||
.field-shell {
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.code-button {
|
||||
width: 118px;
|
||||
min-height: 44px;
|
||||
flex: none;
|
||||
border-color: #e70014;
|
||||
background: #ffffff;
|
||||
color: #e70014;
|
||||
font-size: 14px;
|
||||
.code-shell {
|
||||
padding-right: 10px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.login-card > .primary-button {
|
||||
min-height: 52px;
|
||||
.code-shell .field-icon {
|
||||
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;
|
||||
background: #e70014;
|
||||
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.16);
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fffdfd 100%);
|
||||
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) {
|
||||
margin: 0;
|
||||
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>
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<template>
|
||||
<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">
|
||||
<text class="page-title">确认岗位</text>
|
||||
<text class="page-subtitle">{{ phone }}</text>
|
||||
</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">
|
||||
<text class="panel-title">选择当前岗位</text>
|
||||
<view class="position-list">
|
||||
<view class="position-grid">
|
||||
<button
|
||||
v-for="item in positionCards"
|
||||
:key="item.value"
|
||||
@@ -22,10 +30,14 @@
|
||||
<text class="position-name">{{ item.value }}</text>
|
||||
<text class="position-title">{{ item.heroTitle }}</text>
|
||||
</view>
|
||||
<text class="position-mark">{{ item.value === selectedPosition ? '已选' : '选择' }}</text>
|
||||
<view class="position-radio" :class="{ checked: item.value === selectedPosition }" />
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="confirm-button" :disabled="!selectedPosition || saving" @click="confirmPosition">
|
||||
{{ saving ? '保存中…' : '确认岗位' }}
|
||||
</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -57,8 +69,13 @@ const finish = () => {
|
||||
|
||||
const selectPosition = (position: UserPosition) => {
|
||||
if (saving.value) return;
|
||||
selectedPosition.value = position;
|
||||
};
|
||||
|
||||
const confirmPosition = () => {
|
||||
if (!selectedPosition.value || saving.value) return;
|
||||
saving.value = true;
|
||||
const saved = saveSelectedPosition(position);
|
||||
const saved = saveSelectedPosition(selectedPosition.value);
|
||||
selectedPosition.value = saved;
|
||||
finish();
|
||||
};
|
||||
@@ -95,39 +112,105 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.position-page {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 24px 24px 32px;
|
||||
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 {
|
||||
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;
|
||||
margin-top: 16px;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
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 {
|
||||
display: flex;
|
||||
min-height: 72px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 112px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 16px 14px;
|
||||
border: 1px solid #e0e4e9;
|
||||
border-radius: 16px;
|
||||
background: #f8fafc;
|
||||
background: #ffffff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.position-card.active {
|
||||
border-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
border-color: #e70014;
|
||||
background: #fffafa;
|
||||
}
|
||||
|
||||
.position-copy {
|
||||
@@ -137,21 +220,45 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.position-name {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: #18202b;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.position-title {
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
color: #7d8794;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.position-mark {
|
||||
color: #2563eb;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
.position-radio {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: none;
|
||||
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>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<view class="page practice-page employee-page-shell">
|
||||
<view class="practice-head">
|
||||
<text class="employee-kicker practice-eyebrow">数字师傅 · 场景训练</text>
|
||||
<text class="page-title">场景模拟</text>
|
||||
<text class="page-subtitle">与业主、老师傅完成一轮真实工作对话,结束后自动写入训练记录。</text>
|
||||
<text class="practice-tip">先看预习卡,再开始练习;红线命中时按老师傅建议重说本轮。</text>
|
||||
<text class="page-title">练</text>
|
||||
<text class="page-subtitle">数字师傅 · 场景训练</text>
|
||||
</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 class="scenario-headline">
|
||||
<text class="scenario-badge">今日小训</text>
|
||||
<text class="scenario-title">{{ activeScenarioName }}</text>
|
||||
<text class="scenario-desc">{{ scenarioDescription }}</text>
|
||||
<text class="scenario-title">场景模拟</text>
|
||||
<text class="scenario-desc">先看预习卡,再开始练习</text>
|
||||
</view>
|
||||
<view class="scenario-visual" aria-hidden="true">
|
||||
<view class="visual-sheet" />
|
||||
<view class="visual-bubble"><text>•••</text></view>
|
||||
</view>
|
||||
<view v-if="prepCard" class="prep-card">
|
||||
<view class="prep-head">
|
||||
<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 class="prep-mini"><text class="prep-label">要点</text><text>关键要点先掌握</text></view>
|
||||
<view class="prep-mini"><text class="prep-label">红线</text><text>风险红线要注意</text></view>
|
||||
<view class="prep-mini"><text class="prep-label">可说话术</text><text>有效话术可参考</text></view>
|
||||
</view>
|
||||
<text v-else-if="prepLoading" class="muted">预习卡加载中</text>
|
||||
<button
|
||||
@@ -51,20 +40,36 @@
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel assignment-panel">
|
||||
<view v-if="loggedIn" class="panel assignment-panel recent-panel">
|
||||
<view class="assignment-head">
|
||||
<text class="panel-title">今日任务</text>
|
||||
<button class="ghost-button refresh-button" :disabled="assignmentLoading" @click="loadAssignments">刷新</button>
|
||||
<text class="panel-title">最近练过</text>
|
||||
<text class="recent-link">全部 ›</text>
|
||||
</view>
|
||||
<view v-if="assignments.length" class="assignment-list">
|
||||
<view v-for="item in assignments" :key="item.id" class="assignment-row" :class="{ highlighted: highlightedAssignmentId === item.id }">
|
||||
<view v-if="recentPracticeItems.length" class="assignment-list">
|
||||
<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">
|
||||
<text class="assignment-badge" :class="{ daily: item.source === 'daily' }">{{ item.source === 'daily' ? '每日三题' : '主管专项' }}</text>
|
||||
<text class="assignment-title">{{ item.question || item.scenarioName }}</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 v-if="item.source === 'daily'" class="daily-answer">
|
||||
<view class="daily-answer">
|
||||
<textarea
|
||||
v-model="drillAnswers[item.id]"
|
||||
class="textarea daily-textarea"
|
||||
@@ -76,10 +81,18 @@
|
||||
{{ item.status === '已完成' ? '已完成' : '提交' }}
|
||||
</button>
|
||||
</view>
|
||||
<button v-else class="secondary-button small-button" :disabled="!canStartPractice" @click="startAssignment(item)">开始专项</button>
|
||||
</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 v-if="turns.length" class="panel dialogue-card employee-card">
|
||||
@@ -178,9 +191,10 @@ import type {
|
||||
PracticePrepCard,
|
||||
PracticeAssignment,
|
||||
PracticeRole,
|
||||
PracticeTurn
|
||||
PracticeTurn,
|
||||
PracticeRecord
|
||||
} 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 { searchKnowledge } from '@/services/knowledge';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
@@ -188,6 +202,14 @@ import { ensureEmployeePosition } from '@/services/position';
|
||||
import { chooseSpeechAudio, transcribeSpeechBlob, transcribeSpeechFile } from '@/services/speech';
|
||||
|
||||
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 defaultScenarioName = '投诉接待';
|
||||
@@ -213,6 +235,7 @@ const emotionScore = ref(0);
|
||||
const trustScore = ref(0);
|
||||
const redFlagActive = ref(false);
|
||||
const assignments = ref<PracticeAssignment[]>([]);
|
||||
const practiceHistory = ref<PracticeRecord[]>([]);
|
||||
const drillAnswers = ref<Record<number, string>>({});
|
||||
const assignmentLoading = ref(false);
|
||||
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 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 activeAssignment = computed(() => assignments.value.find((item) => item.id === activeAssignmentId.value));
|
||||
const scenarioDescription = computed(() => {
|
||||
const points = (prepCard.value?.keyPoints || []).map((item) => item.trim()).filter(Boolean).slice(0, 2);
|
||||
if (points.length) return `本场重点:${points.join(';')}`;
|
||||
if (activeAssignment.value?.reason) return activeAssignment.value.reason;
|
||||
const name = activeScenarioName.value;
|
||||
if (/催|缴|费|停车/.test(name)) return '练习承接缴费异议、说明规则依据、给出缴费路径和跟进节点。';
|
||||
if (/报修|维修|设施|电梯|漏水/.test(name)) return '练习记录问题、安抚情绪、说明报修流转和反馈时限。';
|
||||
if (/突发|应急|安全|火灾|摔|道闸|车库/.test(name)) return '练习先稳现场、同步上报、留痕取证和风险告知。';
|
||||
return '练习承接业主情绪、说明处理动作、明确反馈时间和责任边界。';
|
||||
const recentPracticeItems = computed<RecentPracticeItem[]>(() => {
|
||||
const assignmentItems: RecentPracticeItem[] = assignments.value
|
||||
.filter((item) => item.source !== 'daily')
|
||||
.map((item) => ({
|
||||
id: `assignment-${item.id}`,
|
||||
title: item.question || item.scenarioName || '专项训练',
|
||||
desc: item.status === '已完成' ? '已完成' : '待复盘',
|
||||
status: item.status,
|
||||
icon: item.scenarioName?.includes('催费') ? '⌕' : item.scenarioName?.includes('报修') ? '⌁' : '◉',
|
||||
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 = [
|
||||
{ value: 'serious', label: '再严肃' },
|
||||
{ value: 'gentle', label: '再温柔' },
|
||||
@@ -272,6 +305,7 @@ const refreshAuthState = () => {
|
||||
const clearPracticeData = () => {
|
||||
cancelRecording();
|
||||
assignments.value = [];
|
||||
practiceHistory.value = [];
|
||||
drillAnswers.value = {};
|
||||
prepCard.value = null;
|
||||
prepLoading.value = false;
|
||||
@@ -447,6 +481,11 @@ const loadAssignments = async () => {
|
||||
assignmentLoading.value = true;
|
||||
try {
|
||||
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 || '']));
|
||||
consumePendingAssignment();
|
||||
} 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 raw = uni.getStorageSync(pendingAssignmentKey);
|
||||
if (!raw) return;
|
||||
@@ -869,10 +916,10 @@ onShow(() => {
|
||||
.assignment-row {
|
||||
margin-top: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #deebff;
|
||||
border: 1px solid #f1dfe0;
|
||||
border-radius: 20px;
|
||||
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 {
|
||||
@@ -885,15 +932,15 @@ onShow(() => {
|
||||
width: fit-content;
|
||||
padding: 4px 8px;
|
||||
border-radius: 5px;
|
||||
background: #fff0f1;
|
||||
color: #bd0010;
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.assignment-badge.daily {
|
||||
background: #fff5df;
|
||||
color: #a86100;
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
@@ -927,8 +974,8 @@ onShow(() => {
|
||||
width: fit-content;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
background: #edf7f0;
|
||||
color: #168243;
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -1011,7 +1058,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.dialogue-card {
|
||||
border-color: #e1e2e5;
|
||||
border-color: #f0dfe0;
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
@@ -1073,11 +1120,11 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.emotion-fill {
|
||||
background: #d98200;
|
||||
background: #e45757;
|
||||
}
|
||||
|
||||
.trust-fill {
|
||||
background: #168243;
|
||||
background: #d51b2b;
|
||||
}
|
||||
|
||||
.red-flag {
|
||||
@@ -1110,8 +1157,8 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.turn-row.customer {
|
||||
border-left-color: #315b8f;
|
||||
background: #f0f4f8;
|
||||
border-left-color: #b8bdc5;
|
||||
background: #f7f7f8;
|
||||
}
|
||||
|
||||
.turn-row.trainee {
|
||||
@@ -1120,8 +1167,8 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.turn-row.coach {
|
||||
border-left-color: #168243;
|
||||
background: #edf7f0;
|
||||
border-left-color: #d98282;
|
||||
background: #fff5f5;
|
||||
}
|
||||
|
||||
.reply-actions button.recording-active {
|
||||
@@ -1151,9 +1198,9 @@ onShow(() => {
|
||||
|
||||
.style-chip {
|
||||
width: auto;
|
||||
border-color: #c9d9ce;
|
||||
border-color: #f0c8ca;
|
||||
background: #ffffff;
|
||||
color: #168243;
|
||||
color: #d51b2b;
|
||||
}
|
||||
|
||||
.reply-card {
|
||||
@@ -1163,7 +1210,7 @@ onShow(() => {
|
||||
|
||||
.practice-textarea {
|
||||
min-height: 124px;
|
||||
border-color: #d6deea;
|
||||
border-color: #f0dfe0;
|
||||
border-radius: 18px;
|
||||
background: #f9fbff;
|
||||
}
|
||||
@@ -1183,8 +1230,8 @@ onShow(() => {
|
||||
.help-ref {
|
||||
padding: 5px 8px;
|
||||
border-radius: 5px;
|
||||
background: #edf7f0;
|
||||
color: #168243;
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1196,7 +1243,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.result-score {
|
||||
color: #2f6bff;
|
||||
color: #e52323;
|
||||
font-size: 44px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@@ -1214,13 +1261,332 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border-left: 3px solid #d98200;
|
||||
border-left: 3px solid #e52323;
|
||||
border-radius: 6px;
|
||||
background: #fff9ed;
|
||||
background: #fff6f6;
|
||||
}
|
||||
|
||||
.mentor-rewrite text:first-child {
|
||||
color: #855000;
|
||||
color: #c71927;
|
||||
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>
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
<view class="page sop-page employee-page-shell">
|
||||
<view class="sop-head">
|
||||
<text class="employee-kicker sop-eyebrow">SOP · 话术 · 工作流程</text>
|
||||
<text class="page-title">问师傅</text>
|
||||
<text class="page-subtitle">直接问工作中的 SOP、话术和流程。答案附带可追溯的知识库依据。</text>
|
||||
<text class="page-title">问</text>
|
||||
<text class="page-subtitle">数字师傅 · SOP问答</text>
|
||||
<text class="sop-context">管家(生活顾问) · 住宅项目</text>
|
||||
<text class="toolbar-tip">先把问题问清楚,再按需生成总结卡,不伪造分类能力。</text>
|
||||
</view>
|
||||
|
||||
<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
|
||||
v-model="query"
|
||||
class="textarea sop-query-textarea"
|
||||
@@ -26,6 +37,21 @@
|
||||
<text v-if="message" class="status-message">{{ message }}</text>
|
||||
</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>
|
||||
<text class="panel-title">需要登录</text>
|
||||
@@ -888,4 +914,313 @@ onShow(() => {
|
||||
color: #78350f;
|
||||
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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export interface RequestOptions {
|
||||
header?: Record<string, string>;
|
||||
timeout?: number;
|
||||
auth?: boolean;
|
||||
clearAuthOnUnauthorized?: boolean;
|
||||
}
|
||||
|
||||
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 code = typeof payload?.code === 'number' ? payload.code : undefined;
|
||||
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();
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
export const parsePayloadText = (statusCode: number, text: string) => {
|
||||
export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUnauthorized = true) => {
|
||||
try {
|
||||
return JSON.parse(text || '{}');
|
||||
} catch (error) {
|
||||
if (statusCode === 401 || statusCode === 403) clearAuth();
|
||||
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth();
|
||||
throw error instanceof SyntaxError
|
||||
? new Error(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败')
|
||||
: error;
|
||||
@@ -67,7 +68,10 @@ const requestWithXhr = async <T>(options: RequestOptions): Promise<T> => {
|
||||
}).forEach(([key, value]) => xhr.setRequestHeader(key, value));
|
||||
xhr.onload = () => {
|
||||
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 {
|
||||
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);
|
||||
});
|
||||
|
||||
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> => {
|
||||
@@ -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> => {
|
||||
|
||||
@@ -30,7 +30,7 @@ const positionProfiles: Record<UserPosition, UserPositionProfile> = {
|
||||
value: '生活顾问',
|
||||
heroTitle: '客服管家',
|
||||
quickQuestions: ['催费话术', '投诉应对', '收费标准'],
|
||||
tip: '今日小贴士:催费沟通先共情,再说明,再给方案。'
|
||||
tip: '接待客户三步法:听清诉求 → 共情理解 → 解决闭环'
|
||||
},
|
||||
保安: {
|
||||
value: '保安',
|
||||
|
||||
@@ -35,7 +35,8 @@ export const hydratePositionFromOrg = async (phone = getAuth().phone): Promise<U
|
||||
const snapshot = await apiRequest<OrgSnapshotResponse>({
|
||||
url: `/api/aihr/org/snapshot?keyword=${encodeURIComponent(keyword)}&status=active&pageSize=1`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
timeout: 15000,
|
||||
clearAuthOnUnauthorized: false
|
||||
});
|
||||
const officialPosition = snapshot.rows?.[0]?.positionName || '';
|
||||
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 () => {
|
||||
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, /title:\s*'问老师傅'/);
|
||||
assert.match(source, /title:\s*'刻意练习'/);
|
||||
assert.match(source, /shortTitle:\s*'问'/);
|
||||
});
|
||||
|
||||
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
|
||||
@@ -43,13 +46,23 @@ test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
|
||||
test('员工端 tabBar 继续保留四个固定入口', async () => {
|
||||
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\/practice\/index/);
|
||||
assert.match(source, /pages\/user\/sop\/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 () => {
|
||||
const source = await pageSource('../src/styles/base.css');
|
||||
|
||||
@@ -78,6 +91,22 @@ test('员工首页保留师傅区、语音条、今日安排、我的地图四
|
||||
assert.match(source, /voice-panel/);
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user