feat(mobile): 重做员工端首页、问答和训练页视觉

统一员工端 tabBar、共享卡片和三大核心页面的视觉基础,同时保持现有移动端业务流程不变。补充 H5 输入兼容、页面结构和训练回滚行为的回归测试。
This commit is contained in:
2026-07-11 01:13:14 +08:00
parent ebbc69eed6
commit ee396d1e7a
19 changed files with 2171 additions and 439 deletions
+1
View File
@@ -2,6 +2,7 @@
"name": "银城员工端",
"appid": "__UNI__AIHR",
"description": "物业AI人力资源系统用户侧",
"vueVersion": "3",
"versionName": "0.1.0",
"versionCode": "100",
"uniStatistics": {
+26 -8
View File
@@ -1,4 +1,10 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": [
{
"path": "pages/user/today/index",
@@ -120,30 +126,42 @@
"navigationStyle": "custom",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "银城员工端",
"navigationBarBackgroundColor": "#f8fafc",
"backgroundColor": "#f5f7fb"
"navigationBarBackgroundColor": "#f3f5f7",
"backgroundColor": "#f3f5f7"
},
"tabBar": {
"color": "#64748b",
"selectedColor": "#0f172a",
"color": "#7b8494",
"selectedColor": "#2f6bff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"height": "66px",
"fontSize": "12px",
"iconWidth": "25px",
"spacing": "3px",
"list": [
{
"pagePath": "pages/user/today/index",
"text": "今日"
"text": "今日",
"iconPath": "static/tabbar/today-normal.png",
"selectedIconPath": "static/tabbar/today-active.png"
},
{
"pagePath": "pages/user/practice/index",
"text": "练"
"text": "练",
"iconPath": "static/tabbar/practice-normal.png",
"selectedIconPath": "static/tabbar/practice-active.png"
},
{
"pagePath": "pages/user/sop/index",
"text": "问"
"text": "问",
"iconPath": "static/tabbar/ask-normal.png",
"selectedIconPath": "static/tabbar/ask-active.png"
},
{
"pagePath": "pages/user/profile/index",
"text": "我"
"text": "我",
"iconPath": "static/tabbar/profile-normal.png",
"selectedIconPath": "static/tabbar/profile-active.png"
}
]
}
+299 -109
View File
@@ -1,13 +1,15 @@
<template>
<view class="page practice-page">
<view class="page practice-page employee-page-shell">
<view class="practice-head">
<text class="page-title">场景练</text>
<text class="page-subtitle">按当前场景完成三角色对练,结束后自动评分并写入训练记录。</text>
<text class="employee-kicker practice-eyebrow">数字师傅 · 场景训练</text>
<text class="page-title">场景模拟</text>
<text class="page-subtitle">与业主、老师傅完成一轮真实工作对话,结束后自动写入训练记录。</text>
<text class="practice-tip">先看预习卡,再开始练习;红线命中时按老师傅建议重说本轮。</text>
</view>
<view v-if="message" class="status-message">{{ message }}</view>
<view v-if="!loggedIn" class="panel login-panel">
<view v-if="!loggedIn" class="panel login-panel employee-card">
<view>
<text class="panel-title">需要登录</text>
<text class="muted block">登录后查看今日任务、开始练习并保存训练记录。</text>
@@ -15,41 +17,12 @@
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<view v-if="loggedIn" class="panel assignment-panel">
<view class="assignment-head">
<text class="panel-title">今日任务</text>
<button class="ghost-button refresh-button" :disabled="assignmentLoading" @click="loadAssignments">刷新</button>
<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>
</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 class="assignment-copy">
<text class="assignment-badge">{{ 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">
<textarea
v-model="drillAnswers[item.id]"
class="textarea daily-textarea"
maxlength="220"
:disabled="item.status === '已完成'"
placeholder="写一句你的处理话术"
/>
<button class="primary-button small-button" :disabled="Boolean(answeringId) || item.status === '已完成'" @click="submitDaily(item)">
{{ 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>
<view v-if="loggedIn" class="panel scenario-card">
<text class="scenario-badge">今日小训</text>
<text class="scenario-title">{{ activeScenarioName }}</text>
<text class="scenario-desc">{{ scenarioDescription }}</text>
<view v-if="prepCard" class="prep-card">
<view class="prep-head">
<text>训练前预习卡</text>
@@ -78,9 +51,43 @@
</button>
</view>
<view v-if="turns.length" class="panel dialogue-card">
<view v-if="loggedIn" class="panel assignment-panel">
<view class="assignment-head">
<text class="panel-title">今日任务</text>
<button class="ghost-button refresh-button" :disabled="assignmentLoading" @click="loadAssignments">刷新</button>
</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 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">
<textarea
v-model="drillAnswers[item.id]"
class="textarea daily-textarea"
maxlength="220"
:disabled="item.status === '已完成'"
placeholder="写一句你的处理话术"
/>
<button class="primary-button small-button" :disabled="Boolean(answeringId) || item.status === '已完成'" @click="submitDaily(item)">
{{ 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>
<view v-if="turns.length" class="panel dialogue-card employee-card">
<view class="dialogue-head">
<text class="panel-title">对话</text>
<view class="dialogue-heading-block">
<text class="panel-title">对话</text>
<text class="dialogue-caption">保持承接情绪、说明动作、给出反馈节点。</text>
</view>
<text class="round-label">第 {{ Math.min(roundIndex + 1, 3) }} 轮</text>
</view>
<view v-if="hasCoachState" class="coach-state">
@@ -117,16 +124,16 @@
</view>
</view>
<view v-if="status === 'active' || status === 'submitting'" class="panel reply-card">
<view v-if="status === 'active' || status === 'submitting'" class="panel reply-card employee-card">
<text class="panel-title">你的回应</text>
<textarea
v-model="draft"
class="textarea"
class="textarea practice-textarea"
maxlength="500"
placeholder="先承接业主情绪,再说明处理动作和反馈时间。"
/>
<view class="reply-actions">
<button class="secondary-button" :disabled="busy || transcribing" @click="toggleRecording">
<button class="secondary-button" :class="{ 'recording-active': recording }" :disabled="busy || transcribing" @click="toggleRecording">
{{ recording ? '停止录音' : '录音转写' }}
</button>
<button class="ghost-button" :disabled="busy || recording || transcribing" @click="pickAudio">
@@ -143,7 +150,7 @@
<text v-if="draftAudioUrl" class="voice-state">已附带语音回放</text>
</view>
<view v-if="helpAnswer || helpRefs.length" class="panel help-card">
<view v-if="helpAnswer || helpRefs.length" class="panel help-card employee-card">
<text class="panel-title">SOP 提示</text>
<text v-if="helpAnswer" class="help-answer">{{ helpAnswer }}</text>
<view v-if="helpRefs.length" class="help-refs">
@@ -151,7 +158,7 @@
</view>
</view>
<view v-if="result" class="panel result-card">
<view v-if="result" class="panel result-card employee-card">
<text class="result-label">本次评分</text>
<text class="result-score">{{ result.totalScore }}</text>
<text class="result-comment">{{ result.aiComment }}</text>
@@ -625,6 +632,7 @@ const submit = async () => {
message.value = '';
const audioUrl = draftAudioUrl.value || undefined;
const audioOssId = draftAudioOssId.value;
const previousTurns = turns.value.slice();
draft.value = '';
clearDraftAudio();
const retryingRedFlag = redFlagActive.value && lastCoachRoundIndex.value === roundIndex.value;
@@ -659,6 +667,7 @@ const submit = async () => {
status.value = 'active';
} catch (error) {
status.value = 'active';
turns.value = previousTurns;
draft.value = text;
draftAudioUrl.value = audioUrl || '';
draftAudioOssId.value = audioOssId;
@@ -704,21 +713,114 @@ onShow(() => {
<style scoped>
.practice-page {
padding-bottom: 92px;
background: #f4f7fb;
padding: 0 18px 96px;
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.practice-head {
padding-top: 8px;
position: relative;
margin: 0 -18px;
padding: 30px 28px 24px;
overflow: hidden;
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
}
.practice-head::after {
position: absolute;
right: 28px;
bottom: 24px;
width: 56px;
height: 3px;
background: #e70014;
content: '';
}
.practice-eyebrow {
display: inline-flex;
margin-bottom: 12px;
}
.practice-head .page-title {
color: #1f2b3d;
font-size: 31px;
line-height: 1.12;
}
.practice-head .page-subtitle {
max-width: 274px;
margin-top: 10px;
color: #607086;
font-size: 14px;
line-height: 1.55;
}
.practice-tip {
display: block;
max-width: 298px;
margin-top: 14px;
color: #315b8f;
font-size: 12px;
line-height: 1.5;
}
.login-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-top: 22px;
border-left: 4px solid #e70014;
box-shadow: var(--employee-card-shadow);
}
.login-panel .muted {
display: block;
max-width: 218px;
margin-top: 6px;
font-size: 13px;
line-height: 1.5;
}
.login-panel .login-button {
min-width: 84px;
min-height: 46px;
padding: 0 12px;
}
.scenario-card,
.dialogue-card,
.reply-card,
.help-card,
.result-card {
display: flex;
flex-direction: column;
gap: 12px;
gap: 14px;
}
.scenario-card {
margin-top: 22px;
padding: 20px;
border-color: #d7e5dc;
border-left: 4px solid #168243;
background: #ffffff;
box-shadow: var(--employee-card-shadow);
}
.scenario-headline {
display: flex;
flex-direction: column;
gap: 10px;
}
.scenario-card .primary-button {
width: 100%;
margin-top: 2px;
background: #168243;
box-shadow: 0 7px 14px rgba(22, 130, 67, 0.2);
}
.scenario-card .primary-button:active {
background: #106b35;
}
.assignment-head,
@@ -734,39 +836,71 @@ onShow(() => {
min-width: 82px;
}
.assignment-panel {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.assignment-head {
margin-top: 26px;
}
.assignment-head .panel-title {
color: #25282d;
font-size: 20px;
}
.assignment-head .refresh-button {
min-height: 36px;
padding: 0 12px;
border: 1px solid #dedee1;
border-radius: 18px;
background: #ffffff;
color: #66696f;
font-size: 13px;
}
.assignment-list {
margin-top: 12px;
margin-top: 14px;
}
.assignment-row {
margin-top: 10px;
padding: 12px;
border: 1px solid transparent;
border-radius: 14px;
background: #f8fafc;
margin-top: 12px;
padding: 16px;
border: 1px solid #deebff;
border-radius: 20px;
background: #ffffff;
box-shadow: 0 10px 26px rgba(47, 107, 255, 0.08);
}
.assignment-row.highlighted {
border-color: #2563eb;
background: #eff6ff;
border-color: #e70014;
background: #fff7f7;
}
.assignment-badge {
display: inline-flex;
width: fit-content;
padding: 4px 8px;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
border-radius: 5px;
background: #fff0f1;
color: #bd0010;
font-size: 12px;
font-weight: 800;
}
.assignment-badge.daily {
background: #fff5df;
color: #a86100;
}
.assignment-title {
display: block;
margin-top: 8px;
color: #0f172a;
font-size: 15px;
color: #25282d;
font-size: 16px;
font-weight: 900;
line-height: 1.45;
}
@@ -775,7 +909,7 @@ onShow(() => {
.assignment-reference {
display: block;
margin-top: 6px;
color: #64748b;
color: #6d7076;
font-size: 13px;
line-height: 1.45;
}
@@ -792,21 +926,21 @@ onShow(() => {
.scenario-badge {
width: fit-content;
padding: 5px 10px;
border-radius: 999px;
background: #dbeafe;
color: #1d4ed8;
border-radius: 5px;
background: #edf7f0;
color: #168243;
font-size: 13px;
font-weight: 900;
}
.scenario-title {
color: #0f172a;
font-size: 24px;
color: #25282d;
font-size: 25px;
font-weight: 900;
}
.scenario-desc {
color: #64748b;
color: #696c72;
font-size: 15px;
line-height: 1.55;
}
@@ -814,10 +948,10 @@ onShow(() => {
.prep-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px;
border-radius: 14px;
background: #f8fafc;
gap: 12px;
padding: 14px 0 0;
border-top: 1px solid #dfe8e2;
background: transparent;
}
.prep-head {
@@ -829,13 +963,13 @@ onShow(() => {
.prep-head text:first-child,
.prep-label {
color: #0f172a;
color: #34373c;
font-size: 13px;
font-weight: 900;
}
.prep-head text:last-child {
color: #94a3b8;
color: #898c92;
font-size: 12px;
}
@@ -846,7 +980,7 @@ onShow(() => {
}
.prep-line {
color: #475569;
color: #5e6167;
font-size: 13px;
line-height: 1.45;
}
@@ -858,26 +992,53 @@ onShow(() => {
gap: 10px;
}
.dialogue-head {
justify-content: space-between;
}
.dialogue-heading-block {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 6px;
}
.dialogue-caption {
color: #6d7a8d;
font-size: 12px;
line-height: 1.45;
}
.dialogue-card {
border-color: #e1e2e5;
box-shadow: var(--employee-card-shadow);
}
.reply-actions {
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.reply-actions button {
min-height: 48px;
min-width: 92px;
flex: 1;
min-width: 0;
padding: 0 8px;
white-space: nowrap;
word-break: keep-all;
}
.reply-actions .primary-button {
grid-column: span 2;
}
.voice-state {
color: #2563eb;
color: #168243;
font-size: 13px;
font-weight: 800;
}
.round-label {
color: #64748b;
color: #6d7076;
font-size: 13px;
}
@@ -893,7 +1054,7 @@ onShow(() => {
align-items: center;
grid-template-columns: 72px 1fr;
gap: 10px;
color: #475569;
color: #5f6268;
font-size: 13px;
font-weight: 800;
}
@@ -901,28 +1062,30 @@ onShow(() => {
.state-bar {
height: 8px;
overflow: hidden;
border-radius: 999px;
background: #e2e8f0;
border-radius: 4px;
background: #e3e4e6;
}
.state-fill {
height: 100%;
border-radius: 999px;
transition: width 0.3s ease-in-out;
}
.emotion-fill {
background: #f97316;
background: #d98200;
}
.trust-fill {
background: #16a34a;
background: #168243;
}
.red-flag {
padding: 9px 10px;
border-radius: 12px;
background: #fef2f2;
color: #b91c1c;
border-left: 3px solid #e70014;
border-radius: 4px;
background: #fff2f3;
color: #bd0010;
font-size: 13px;
font-weight: 800;
line-height: 1.45;
@@ -939,27 +1102,42 @@ onShow(() => {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px;
border-radius: 14px;
background: #f8fafc;
padding: 14px;
border: 1px solid #e5edf7;
border-left: 3px solid #9ca0a6;
border-radius: 18px;
background: #f8fbff;
}
.turn-row.customer {
border-left-color: #315b8f;
background: #f0f4f8;
}
.turn-row.trainee {
background: #eff6ff;
border-left-color: #e70014;
background: #fff6f6;
}
.turn-row.coach {
background: #fff7ed;
border-left-color: #168243;
background: #edf7f0;
}
.reply-actions button.recording-active {
border-color: #e70014;
background: #fff5f5;
color: #e70014;
}
.turn-role {
color: #334155;
color: #42454b;
font-size: 13px;
font-weight: 900;
}
.turn-text {
color: #172033;
color: #282b30;
font-size: 15px;
line-height: 1.55;
}
@@ -973,14 +1151,25 @@ onShow(() => {
.style-chip {
width: auto;
border-color: #c9d9ce;
background: #ffffff;
color: #168243;
}
.reply-actions button {
flex: 1;
.reply-card {
border-top: 0;
box-shadow: var(--employee-card-shadow);
}
.practice-textarea {
min-height: 124px;
border-color: #d6deea;
border-radius: 18px;
background: #f9fbff;
}
.help-answer {
color: #334155;
color: #45484e;
font-size: 15px;
line-height: 1.6;
}
@@ -993,21 +1182,21 @@ onShow(() => {
.help-ref {
padding: 5px 8px;
border-radius: 999px;
background: #e0f2fe;
color: #0369a1;
border-radius: 5px;
background: #edf7f0;
color: #168243;
font-size: 12px;
font-weight: 800;
}
.result-label {
color: #64748b;
color: #6d7076;
font-size: 14px;
font-weight: 800;
}
.result-score {
color: #1d4ed8;
color: #2f6bff;
font-size: 44px;
font-weight: 900;
line-height: 1;
@@ -1015,7 +1204,7 @@ onShow(() => {
.result-comment,
.mentor-rewrite text:last-child {
color: #334155;
color: #45484e;
font-size: 15px;
line-height: 1.55;
}
@@ -1025,12 +1214,13 @@ onShow(() => {
flex-direction: column;
gap: 6px;
padding: 12px;
border-radius: 12px;
background: #f8fafc;
border-left: 3px solid #d98200;
border-radius: 6px;
background: #fff9ed;
}
.mentor-rewrite text:first-child {
color: #0f172a;
color: #855000;
font-weight: 900;
}
</style>
+534 -60
View File
@@ -1,27 +1,32 @@
<template>
<view class="page sop-page">
<text class="page-title">问师傅</text>
<text class="page-subtitle">直接问工作中的 SOP、话术和流程。答案来自知识库检索,引用在后面。</text>
<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="toolbar-tip">先把问题问清楚,再按需生成总结卡,不伪造分类能力。</text>
</view>
<view class="panel query-panel">
<view class="panel query-panel employee-card">
<textarea
v-model="query"
class="textarea"
class="textarea sop-query-textarea"
maxlength="200"
:placeholder="quickQuestionPlaceholder"
/>
<text class="query-caption">常见问题可以直接点下面快捷问法,结果会保留引用依据。</text>
<view class="chip-row">
<button v-for="item in quickQuestions" :key="item" class="chip" :disabled="loading" @click="useQuickQuestion(item)">
{{ item }}
</button>
</view>
<button class="primary-button" :disabled="loading" @click="search">
{{ loading ? '检索中' : '查询' }}
<button class="primary-button query-submit" :disabled="loading" @click="search">
{{ loading ? '检索中' : '问老师傅' }}
</button>
<text v-if="message" class="status-message">{{ message }}</text>
</view>
<view v-if="!loggedIn" class="panel login-panel">
<view v-if="!loggedIn" class="panel login-panel employee-card">
<view>
<text class="panel-title">需要登录</text>
<text class="muted block">登录后才能查询 SOP、生成总结卡并反馈答案。</text>
@@ -29,7 +34,7 @@
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<view v-if="result" class="panel answer-panel">
<view v-if="result" class="panel answer-panel employee-card">
<text class="panel-title">直给答案</text>
<text class="answer-text">{{ result.answer || '未生成直接答案,请查看下方引用。' }}</text>
@@ -54,16 +59,16 @@
</button>
<button
class="secondary-button"
:class="{ active: feedbackVerdict === 'down' }"
:class="{ active: feedbackVerdict === 'down', negative: feedbackVerdict === 'down' }"
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
@click="feedback('down')"
>
不准
需要复核
</button>
</view>
</view>
<view v-if="result?.snippets?.length" class="panel">
<view v-if="result?.snippets?.length" class="panel sources-panel">
<text class="panel-title">引用依据</text>
<view v-for="(snippet, index) in result.snippets" :key="`${snippet.docTitle}-${index}`" class="snippet">
<text class="snippet-title">{{ snippet.docTitle }}</text>
@@ -72,35 +77,81 @@
</view>
</view>
<view v-if="result" class="panel summary-panel">
<view v-if="result" class="panel summary-panel employee-card">
<text class="panel-title">总结卡</text>
<text class="muted">需要时再生成,避免每次查询都调用模型。</text>
<text class="muted summary-copy">需要时再生成,适合把一段 SOP 整理成步骤卡、话术卡和提醒卡。</text>
<button class="ghost-button" :disabled="summaryLoading" @click="buildSummaryCard">
{{ summaryLoading ? '生成中' : '生成总结卡' }}
</button>
</view>
<view v-if="summaryCard" class="panel">
<text class="panel-title">{{ summaryCard.title }}</text>
<view class="summary-block">
<text class="summary-heading">步骤</text>
<text v-for="(step, index) in summaryCard.steps" :key="`${step.label}-${index}`" class="summary-line">
{{ index + 1 }}. {{ step.label }}: {{ step.desc }}
</text>
</view>
<view v-if="summaryCard.scripts.length" class="summary-block">
<text class="summary-heading">可说话术</text>
<text v-for="item in summaryCard.scripts" :key="item" class="summary-line">{{ item }}</text>
</view>
<view v-if="summaryCard.objections.length" class="summary-block">
<text class="summary-heading">异议回应</text>
<text v-for="item in summaryCard.objections" :key="item.concern" class="summary-line">
{{ item.concern }}: {{ item.response }}
</text>
</view>
<view v-if="summaryCard.reminders.length" class="summary-block">
<text class="summary-heading">提醒</text>
<text v-for="item in summaryCard.reminders" :key="item" class="summary-line">{{ item }}</text>
<view v-if="summaryCard" class="modal-mask" @click="closeSummary">
<view class="modal-card" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ summaryCard.title }}</text>
<button class="modal-close" @click="closeSummary">×</button>
</view>
<scroll-view scroll-y class="modal-body">
<view v-if="summaryCard.steps?.length" class="info-section">
<view class="section-title-row">
<text class="badge-num">1</text>
<text class="info-section-title">核心步骤</text>
</view>
<view class="timeline">
<view v-for="(step, index) in summaryCard.steps" :key="index" class="timeline-item">
<view class="timeline-node">{{ index + 1 }}</view>
<view class="timeline-content">
<text class="timeline-label">{{ step.label }}</text>
<text class="timeline-desc">{{ step.desc }}</text>
</view>
</view>
</view>
</view>
<view v-if="summaryCard.scripts?.length" class="info-section speech-section">
<view class="section-title-row">
<text class="badge-num">2</text>
<text class="info-section-title">推荐话术</text>
</view>
<view class="speech-bubbles">
<view v-for="(script, index) in summaryCard.scripts" :key="index" class="speech-bubble">
<text class="quote-mark">“</text>
<text class="speech-text">{{ script }}</text>
</view>
</view>
</view>
<view v-if="summaryCard.objections?.length" class="info-section objection-section">
<view class="section-title-row">
<text class="badge-num">3</text>
<text class="info-section-title">常见异议与回应</text>
</view>
<view class="objection-list">
<view v-for="(obj, index) in summaryCard.objections" :key="index" class="objection-card">
<view class="objection-q">
<text class="q-badge">问</text>
<text class="q-text">{{ obj.concern }}</text>
</view>
<view class="objection-a">
<text class="a-badge">答</text>
<text class="a-text">{{ obj.response }}</text>
</view>
</view>
</view>
</view>
<view v-if="summaryCard.reminders?.length" class="info-section reminder-section">
<view class="section-title-row">
<text class="badge-num">4</text>
<text class="info-section-title">师傅提醒</text>
</view>
<view class="reminder-list">
<view v-for="(rem, index) in summaryCard.reminders" :key="index" class="reminder-item">
<text class="reminder-text">{{ rem }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="modal-footer">
<button class="primary-button footer-btn" @click="closeSummary">我知道了</button>
</view>
</view>
</view>
</view>
@@ -259,6 +310,10 @@ const buildSummaryCard = async () => {
}
};
const closeSummary = () => {
summaryCard.value = null;
};
const goLogin = () => {
rememberLoginRedirect('/pages/user/sop/index');
uni.navigateTo({ url: '/pages/auth/login/index' });
@@ -297,7 +352,53 @@ onShow(() => {
<style scoped>
.sop-page {
padding-bottom: 48px;
padding: 0 18px 96px;
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.sop-head {
position: relative;
margin: 0 -18px;
padding: 30px 28px 24px;
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
}
.sop-head::after {
position: absolute;
right: 28px;
bottom: 24px;
width: 56px;
height: 3px;
background: #e70014;
content: '';
}
.sop-eyebrow {
display: inline-flex;
margin-bottom: 12px;
}
.sop-head .page-title {
color: #1f2b3d;
font-size: 31px;
line-height: 1.12;
}
.sop-head .page-subtitle {
max-width: 292px;
margin-top: 10px;
color: #607086;
font-size: 14px;
line-height: 1.55;
}
.toolbar-tip {
display: block;
max-width: 286px;
margin-top: 14px;
color: #315b8f;
font-size: 12px;
line-height: 1.5;
}
.query-panel,
@@ -308,30 +409,103 @@ onShow(() => {
gap: 14px;
}
.query-panel {
margin-top: 22px;
padding: 20px;
border-top: 0;
box-shadow: var(--employee-card-shadow);
}
.sop-query-textarea {
min-height: 112px;
border-color: #d6deea;
background: #f9fbff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.query-caption {
color: #6f7b8f;
font-size: 12px;
line-height: 1.5;
}
.query-panel .textarea {
min-height: 104px;
height: 104px;
border-color: #d9dade;
border-radius: 7px;
background: #fbfbfb;
color: #25282d;
}
.query-panel .chip-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 7px;
}
.query-panel .chip {
min-height: 38px;
padding: 0 10px;
border-color: #dbe5f4;
border-radius: 999px;
background: #f7faff;
color: #47607f;
font-size: 13px;
white-space: nowrap;
}
.query-submit {
width: 100%;
margin-top: 2px;
min-height: 50px;
border-radius: 18px;
background: linear-gradient(135deg, #2f6bff, #5a8cff);
box-shadow: 0 14px 28px rgba(47, 107, 255, 0.22);
}
.login-panel {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
gap: 14px;
margin-top: 22px;
border-left: 4px solid #e70014;
box-shadow: var(--employee-card-shadow);
}
.block {
display: block;
margin-top: 8px;
font-size: 14px;
font-size: 13px;
line-height: 1.5;
}
.login-button {
min-width: 84px;
width: 96px;
min-width: 96px;
flex: 0 0 96px;
padding: 0 8px;
white-space: nowrap;
}
.answer-text {
color: #0f172a;
color: #243246;
font-size: 18px;
font-weight: 700;
line-height: 1.65;
}
.answer-panel {
border-color: #d9e7de;
border-left: 4px solid #168243;
box-shadow: var(--employee-card-shadow);
}
.answer-panel .panel-title {
color: #168243;
}
.keypoints {
display: flex;
flex-direction: column;
@@ -340,9 +514,10 @@ onShow(() => {
.keypoint {
padding: 10px 12px;
border-radius: 10px;
background: #f8fafc;
color: #334155;
border-left: 3px solid #168243;
border-radius: 5px;
background: #f2f8f4;
color: #45484e;
font-size: 15px;
line-height: 1.5;
}
@@ -352,9 +527,10 @@ onShow(() => {
flex-direction: column;
gap: 6px;
padding: 10px 12px;
border-radius: 10px;
background: #fff7ed;
color: #9a3412;
border-left: 3px solid #d98200;
border-radius: 5px;
background: #fff8e9;
color: #855000;
font-size: 13px;
line-height: 1.45;
}
@@ -366,9 +542,29 @@ onShow(() => {
}
.feedback-actions .active {
border-color: #16a34a;
background: #dcfce7;
color: #166534;
border-color: #168243;
background: #edf7f0;
color: #168243;
}
.feedback-actions .negative {
border-color: #e70014;
background: #fff1f2;
color: #bd0010;
}
.sources-panel {
padding: 0;
border: 0;
background: transparent;
box-shadow: none;
}
.sources-panel .panel-title {
display: block;
margin-top: 26px;
color: #25282d;
font-size: 20px;
}
.snippet {
@@ -376,24 +572,26 @@ onShow(() => {
margin-top: 12px;
flex-direction: column;
gap: 6px;
padding: 12px;
border-radius: 12px;
background: #f8fafc;
padding: 14px;
border: 1px solid #e3e4e7;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 5px 14px rgba(37, 40, 45, 0.04);
}
.snippet-title {
color: #0f172a;
color: #34373c;
font-weight: 800;
}
.snippet-content {
color: #475569;
color: #60636a;
font-size: 14px;
line-height: 1.55;
}
.snippet-id {
color: #94a3b8;
color: #96999f;
font-size: 12px;
}
@@ -404,14 +602,290 @@ onShow(() => {
gap: 8px;
}
.summary-heading {
color: #1d4ed8;
.summary-panel {
border-color: #eadfbd;
background: #fffdf7;
box-shadow: var(--employee-card-shadow);
}
.summary-panel .muted {
font-size: 13px;
line-height: 1.5;
}
.summary-copy {
max-width: 278px;
}
.summary-panel .ghost-button {
min-height: 48px;
border: 1px solid #d7e1f5;
border-radius: 18px;
background: #f6f9ff;
color: #315b8f;
}
/* Modal mask overlay */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(15, 23, 42, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
padding: 20px;
}
.modal-card {
width: 100%;
max-width: 400px;
max-height: 80vh;
background: #ffffff;
border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
display: flex;
flex-direction: column;
overflow: hidden;
border-top: 5px solid #168243;
}
.modal-header {
padding: 16px 20px;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-size: 18px;
font-weight: 900;
color: #0f172a;
}
.modal-close {
background: transparent;
border: none;
font-size: 24px;
color: #94a3b8;
cursor: pointer;
padding: 0;
line-height: 1;
width: auto;
min-height: auto;
}
.modal-body {
flex: 1;
padding: 20px;
overflow-y: auto;
max-height: 55vh;
}
.modal-footer {
padding: 16px 20px;
border-top: 1px solid #f1f5f9;
}
.footer-btn {
width: 100%;
background: #168243;
color: #ffffff;
}
/* Infographic Sections */
.info-section {
margin-bottom: 24px;
}
.section-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
}
.badge-num {
width: 20px;
height: 20px;
background: #168243;
color: #ffffff;
border-radius: 50%;
font-size: 11px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
}
.info-section-title {
font-size: 15px;
font-weight: 900;
color: #1e293b;
}
/* Steps Timeline */
.timeline {
position: relative;
padding-left: 14px;
}
.timeline::before {
content: '';
position: absolute;
left: 5px;
top: 6px;
bottom: 6px;
width: 2px;
background: #e2e8f0;
}
.timeline-item {
position: relative;
padding-bottom: 16px;
display: flex;
gap: 12px;
}
.timeline-item:last-child {
padding-bottom: 0;
}
.timeline-node {
position: absolute;
left: -14px;
width: 12px;
height: 12px;
background: #168243;
color: #ffffff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 800;
}
.summary-line {
.timeline-content {
flex: 1;
}
.timeline-label {
display: block;
font-size: 14px;
font-weight: 800;
color: #334155;
font-size: 15px;
line-height: 1.55;
margin-bottom: 4px;
}
.timeline-desc {
display: block;
font-size: 12px;
color: #64748b;
line-height: 1.45;
}
/* Speech bubbles/scripts */
.speech-bubble {
position: relative;
background: #f2f8f4;
border-left: 3px solid #168243;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 10px;
}
.quote-mark {
position: absolute;
left: 6px;
top: -2px;
font-size: 24px;
color: rgba(22, 130, 67, 0.15);
font-family: Georgia, serif;
line-height: 1;
}
.speech-text {
font-size: 13.5px;
color: #1e293b;
line-height: 1.5;
position: relative;
z-index: 1;
}
/* Objections Q&A */
.objection-card {
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 12px;
margin-bottom: 10px;
}
.objection-q,
.objection-a {
display: flex;
gap: 8px;
}
.objection-q {
margin-bottom: 8px;
}
.q-badge,
.a-badge {
flex: 0 0 18px;
height: 18px;
font-size: 10px;
font-weight: 800;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.q-badge {
background: #fee2e2;
color: #b91c1c;
}
.a-badge {
background: #dcfce7;
color: #166534;
}
.q-text {
font-size: 13px;
font-weight: 800;
color: #334155;
line-height: 1.4;
}
.a-text {
font-size: 13px;
color: #475569;
line-height: 1.45;
}
/* Reminders alert box */
.reminder-item {
display: flex;
gap: 8px;
background: #fffbeb;
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
align-items: flex-start;
border-left: 3px solid #d97706;
}
.reminder-text {
font-size: 13px;
color: #78350f;
line-height: 1.45;
}
</style>
+367 -199
View File
@@ -1,9 +1,12 @@
<template>
<view class="page today-page">
<view class="page today-page employee-page-shell">
<view v-if="message" class="status-message">{{ message }}</view>
<view class="mentor-hero">
<image class="hero-scene" src="/static/mentor.png" mode="aspectFill" />
<view class="hero-scene-scrim" />
<view class="hero-copy">
<text class="employee-kicker hero-kicker">数字师傅 · 今日带练</text>
<text class="hero-role">{{ positionProfile.value }}</text>
<text class="hero-title">{{ positionProfile.heroTitle }}</text>
<text class="hero-sub">有问题,随时问老师傅</text>
@@ -16,8 +19,13 @@
<view class="voice-panel">
<button class="voice-primary" @click="goSop">
<text class="voice-mark">问</text>
<text>问老师傅</text>
<view class="voice-emblem">
<text class="voice-mark">问</text>
</view>
<view class="voice-copy">
<text>问老师傅</text>
<text>语音或文字都可以</text>
</view>
</button>
<button class="voice-secondary" @click="goSop">
<text class="keyboard-mark">键</text>
@@ -33,7 +41,7 @@
</view>
</view>
<view class="section-card login-section" v-if="!loggedIn">
<view class="section-card employee-card login-section" v-if="!loggedIn">
<view>
<text class="login-title">登录后查看今日安排</text>
<text class="login-desc">训练任务、复习提醒和成长数据会按你的手机号展示。</text>
@@ -41,11 +49,14 @@
<button class="primary-button login-button" @click="goLogin">去登录</button>
</view>
<view class="section-card" v-else-if="hasSchedule">
<view class="section-card employee-card" v-else-if="hasSchedule">
<view class="section-head">
<view class="section-title">
<text class="section-icon">日</text>
<text>今日安排</text>
<view class="section-heading-block">
<view class="section-title">
<text class="section-icon">日</text>
<text>今日安排</text>
</view>
<text class="section-subtitle">先完成训练,再补一轮复习,今天的学习节奏就完整了。</text>
</view>
<button class="section-link" @click="openTool('训练')">查看全部</button>
</view>
@@ -69,11 +80,14 @@
</view>
</view>
<view v-if="loggedIn" class="section-card">
<view v-if="loggedIn" class="section-card employee-card">
<view class="section-head">
<view class="section-title">
<text class="section-icon">图</text>
<text>{{ loggedIn ? '我的地图' : '常用入口' }}</text>
<view class="section-heading-block">
<view class="section-title">
<text class="section-icon">图</text>
<text>{{ loggedIn ? '我的地图' : '常用入口' }}</text>
</view>
<text class="section-subtitle">把今天最常用的问、练、学、报都放在手边。</text>
</view>
</view>
<view v-if="mapTiles.length" class="map-grid">
@@ -84,7 +98,7 @@
:class="tile.tone"
@click="openTool(tile.title)"
>
<image class="map-icon" :src="tile.icon" mode="aspectFit" />
<text class="map-symbol">{{ tile.shortTitle }}</text>
<view class="map-copy">
<text class="map-title">{{ tile.title }}</text>
<text class="map-desc">{{ tile.desc }}</text>
@@ -123,13 +137,6 @@ const primaryTipQuestion = computed(() => quickQuestions.value[0] || '岗位SOP'
const pendingSopQueryKey = 'aihr_sop_pending_query';
const pendingAssignmentKey = 'aihr_pending_assignment_id';
const iconFor = (title: string) => {
if (title.includes('SOP') || title.includes('查')) return '/static/map-icons/ask.png';
if (title.includes('练')) return '/static/map-icons/practice.png';
if (title.includes('学') || title.includes('案例')) return '/static/map-icons/learn.png';
return '/static/map-icons/report.png';
};
const toneFor = (title: string) => {
if (title.includes('SOP') || title.includes('查')) return 'ask';
if (title.includes('练')) return 'practice';
@@ -137,14 +144,28 @@ const toneFor = (title: string) => {
return 'report';
};
const shortTitleFor = (tone: string) => {
if (tone === 'ask') return '问';
if (tone === 'practice') return '练';
if (tone === 'learn') return '学';
return '报';
};
const toolOrder = ['ask', 'practice', 'learn', 'report'];
const mapTiles = computed(() => {
if (!home.value?.tools?.length) return [];
return home.value.tools.map((item) => ({
title: item.title,
desc: item.desc.replace(/\n/g, ' / '),
tone: toneFor(item.title),
icon: iconFor(item.title)
}));
return home.value.tools
.map((item) => {
const tone = toneFor(item.title);
return {
title: item.title,
shortTitle: shortTitleFor(tone),
desc: item.desc.replace(/\n/g, ' / '),
tone
};
})
.sort((left, right) => toolOrder.indexOf(left.tone) - toolOrder.indexOf(right.tone));
});
const primaryAssignment = computed(() =>
@@ -261,169 +282,261 @@ onShow(() => {
</script>
<style scoped>
/* Homepage composition follows the approved employee-home visual reference. */
.today-page {
min-height: 100vh;
padding: 0 14px 86px;
background: #f4f7fb;
padding: 0 18px 96px;
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.mentor-hero {
position: relative;
height: 224px;
margin: 0 -14px;
height: 244px;
margin: 0 -18px;
overflow: hidden;
background:
linear-gradient(180deg, rgba(237, 247, 255, 0.72), rgba(244, 247, 251, 0.98)),
linear-gradient(135deg, #d9ecff 0%, #f7fbff 45%, #dfeff2 100%);
border: 0;
background: #f0f1ee;
}
.mentor-hero::before {
display: none;
}
.hero-scene,
.hero-scene-scrim {
position: absolute;
right: -36px;
bottom: 12px;
width: 190px;
height: 96px;
border-radius: 30px;
background: rgba(255, 255, 255, 0.46);
content: '';
inset: -18px;
width: calc(100% + 36px);
height: calc(100% + 36px);
}
.hero-scene {
object-fit: cover;
opacity: 0.18;
filter: blur(13px) saturate(0.34) brightness(1.28);
transform: scale(1.12);
}
.hero-scene-scrim {
background: rgba(247, 247, 248, 0.76);
}
.hero-copy {
position: absolute;
left: 22px;
left: 28px;
top: 34px;
z-index: 2;
display: flex;
flex-direction: column;
}
.hero-kicker {
margin-bottom: 12px;
}
.hero-role,
.hero-title {
color: #11235a;
font-size: 22px;
font-weight: 900;
line-height: 1.08;
color: #25282d;
font-size: 29px;
line-height: 1.04;
letter-spacing: 0;
}
.hero-sub {
margin-top: 8px;
color: #1e3a8a;
font-size: 13px;
font-weight: 700;
margin-top: 12px;
color: #6a6d72;
font-size: 15px;
line-height: 1.35;
}
.mentor-image {
position: absolute;
right: 88px;
bottom: 14px;
right: 105px;
bottom: 0;
z-index: 1;
width: 104px;
height: 138px;
border-radius: 24px;
box-shadow: 0 14px 36px rgba(15, 23, 42, 0.14);
width: 132px;
height: 176px;
border-radius: 0;
filter: drop-shadow(0 8px 8px rgba(37, 40, 45, 0.16));
-webkit-clip-path: ellipse(44% 56% at 50% 48%);
clip-path: ellipse(44% 56% at 50% 48%);
-webkit-mask-image: radial-gradient(ellipse 68% 61% at 50% 42%, #000 58%, transparent 100%);
mask-image: radial-gradient(ellipse 68% 61% at 50% 42%, #000 58%, transparent 100%);
}
.mentor-bubble {
position: absolute;
right: 16px;
top: 80px;
right: 18px;
top: 96px;
z-index: 3;
width: 128px;
padding: 12px 14px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.12);
width: 142px;
padding: 14px 16px;
border: 1px solid rgba(255, 255, 255, 0.9);
border-radius: 22px;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 10px 24px rgba(37, 40, 45, 0.14);
}
.mentor-bubble text {
color: #111827;
color: #1c2940;
font-size: 17px;
font-weight: 700;
line-height: 1.45;
font-weight: 800;
line-height: 1.4;
}
.voice-panel {
position: relative;
z-index: 4;
display: grid;
margin: -34px 8px 0;
padding: 16px;
grid-template-columns: 1fr 1fr;
gap: 14px;
border-radius: 22px;
min-height: 106px;
margin: -42px 10px 0;
padding: 15px;
grid-template-columns: minmax(0, 1fr) 112px;
gap: 0;
border: 1px solid rgba(47, 74, 117, 0.08);
border-radius: 25px;
background: #ffffff;
box-shadow: 0 14px 38px rgba(15, 23, 42, 0.12);
box-shadow: 0 14px 36px rgba(47, 107, 255, 0.12);
}
.voice-primary,
.voice-secondary {
display: flex;
min-height: 64px;
min-height: 76px;
align-items: center;
justify-content: center;
gap: 10px;
border-radius: 18px;
border-radius: 0;
background: transparent;
color: #1d4ed8;
font-size: 20px;
font-weight: 800;
color: #e70014;
}
.voice-secondary {
color: #4b5563;
background: #f1f5f9;
.voice-primary {
justify-content: flex-start;
gap: 14px;
padding: 0 16px 0 4px;
border-right: 1px solid #e7e7e9;
}
.voice-primary text:last-child,
.voice-secondary text:last-child {
white-space: nowrap;
}
.voice-mark,
.keyboard-mark {
.voice-emblem {
display: flex;
width: 46px;
height: 46px;
width: 60px;
height: 60px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(135deg, #2563eb, #1d9bf0);
background: #e70014;
box-shadow: 0 0 0 9px #fff0f1, 0 7px 14px rgba(231, 0, 20, 0.22);
}
.voice-mark {
width: auto;
height: auto;
border-radius: 0;
background: transparent;
color: #ffffff;
font-size: 18px;
font-size: 26px;
}
.voice-copy {
display: flex;
min-width: 0;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.voice-copy text:first-child {
font-size: 21px;
font-weight: 900;
white-space: nowrap;
}
.voice-copy text:last-child {
color: #777a80;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.voice-secondary {
gap: 7px;
padding: 0 8px;
border-radius: 24px;
background: #f1f1f2;
color: #45474d;
font-size: 17px;
}
.keyboard-mark {
background: #e5e7eb;
color: #4b5563;
display: flex;
width: 30px;
height: 30px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 9px;
background: #dedee1;
color: #45474d;
font-size: 14px;
}
.mentor-chips {
margin-top: 14px;
margin: 22px 0 0;
}
.mentor-chips .chip-row {
justify-content: space-between;
gap: 8px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.mentor-chips .chip {
min-height: 42px;
flex: 1;
border: 1px solid rgba(203, 213, 225, 0.7);
position: relative;
display: flex;
min-height: 48px;
align-items: center;
justify-content: center;
gap: 5px;
padding: 0 6px;
border: 0;
border-radius: 24px;
background: #ffffff;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
color: #1f2937;
font-size: 15px;
box-shadow: 0 7px 18px rgba(37, 40, 45, 0.08);
color: #34373c;
font-size: 14px;
font-weight: 800;
white-space: nowrap;
}
.mentor-chips .chip::before {
width: 8px;
height: 8px;
flex: none;
border-radius: 50%;
background: #e70014;
content: '';
}
.mentor-chips .chip::after {
content: none;
}
.mentor-chips .chip:nth-child(2)::before {
background: #d98200;
}
.mentor-chips .chip:nth-child(3)::before {
background: #168243;
}
.section-card {
margin-top: 18px;
margin-top: 24px;
padding: 18px;
border-radius: 18px;
border: 1px solid rgba(47, 74, 117, 0.08);
border-radius: var(--employee-card-radius);
background: #ffffff;
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.07);
box-shadow: var(--employee-card-shadow);
}
.login-section {
@@ -431,6 +544,7 @@ onShow(() => {
align-items: center;
justify-content: space-between;
gap: 12px;
border-left: 4px solid #e70014;
}
.login-title,
@@ -439,74 +553,84 @@ onShow(() => {
}
.login-title {
color: #111827;
font-size: 18px;
font-weight: 900;
font-size: 19px;
}
.login-desc {
margin-top: 6px;
color: #64748b;
font-size: 13px;
line-height: 1.4;
max-width: 190px;
font-size: 14px;
line-height: 1.48;
}
.today-page .primary-button,
.login-button {
flex: 0 0 84px;
min-width: 84px;
white-space: nowrap;
width: 96px;
min-width: 96px;
min-height: 50px;
flex: 0 0 96px;
border-radius: 14px;
background: #e70014;
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.2);
}
.section-head,
.section-title {
display: flex;
align-items: center;
gap: 10px;
color: #25282d;
font-size: 22px;
}
.section-head {
justify-content: space-between;
.section-heading-block {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 8px;
}
.section-title {
gap: 9px;
color: #111827;
font-size: 21px;
font-weight: 900;
.section-subtitle {
color: #707682;
font-size: 13px;
line-height: 1.5;
}
.section-icon {
width: 28px;
height: 28px;
border-radius: 9px;
background: #fff0f1;
color: #e70014;
font-size: 14px;
}
.section-head {
display: flex;
width: 24px;
height: 24px;
align-items: center;
justify-content: center;
border-radius: 8px;
background: #dbeafe;
color: #2563eb;
font-size: 13px;
font-weight: 900;
justify-content: space-between;
}
.section-link {
color: #64748b;
font-size: 14px;
flex: none;
font-size: 15px;
}
.schedule-list {
display: flex;
margin-top: 16px;
margin-top: 18px;
flex-direction: column;
gap: 10px;
gap: 12px;
}
.schedule-row {
display: flex;
min-height: 82px;
min-height: 94px;
align-items: center;
gap: 12px;
padding: 12px;
border-radius: 15px;
background: #f8fafc;
padding: 14px;
border: 0;
border-radius: 18px;
background: #f4f4f5;
}
.step-dot {
@@ -523,11 +647,11 @@ onShow(() => {
}
.step-dot.blue {
background: #2563eb;
background: #e70014;
}
.step-dot.orange {
background: #f97316;
background: #d98200;
}
.schedule-copy {
@@ -539,26 +663,22 @@ onShow(() => {
}
.schedule-title {
color: #111827;
font-size: 16px;
font-weight: 900;
line-height: 1.25;
font-size: 17px;
}
.schedule-desc {
color: #64748b;
font-size: 13px;
line-height: 1.35;
font-size: 14px;
}
.schedule-action {
display: flex;
width: 68px;
min-height: 42px;
width: 76px;
min-height: 48px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 12px;
border-radius: 14px;
box-shadow: 0 7px 14px rgba(231, 0, 20, 0.2);
color: #ffffff;
font-size: 16px;
font-weight: 900;
@@ -566,117 +686,165 @@ onShow(() => {
}
.blue-action {
background: #2563eb;
background: #e70014;
}
.orange-action {
background: #f97316;
background: #d98200;
box-shadow: 0 7px 14px rgba(217, 130, 0, 0.18);
}
.map-grid {
display: grid;
margin-top: 16px;
margin-top: 18px;
grid-template-columns: 1fr 1fr;
gap: 10px;
gap: 12px;
}
.map-tile {
position: relative;
display: flex;
min-height: 108px;
display: grid;
min-height: 128px;
grid-template-columns: 42px minmax(0, 1fr) 14px;
align-items: center;
gap: 12px;
gap: 9px;
padding: 16px 12px;
border-radius: 17px;
border: 0;
border-radius: 19px;
color: #ffffff;
overflow: hidden;
text-align: left;
}
.map-tile.ask {
background: linear-gradient(135deg, #1d4ed8, #2682ff);
background: linear-gradient(135deg, #c90012, #f02135);
}
.map-tile.practice {
background: linear-gradient(135deg, #16a34a, #22c55e);
background: linear-gradient(135deg, #0d7137, #239954);
}
.map-tile.learn {
background: linear-gradient(135deg, #f59e0b, #fb923c);
background: linear-gradient(135deg, #ba6d00, #e89410);
}
.map-tile.report {
background: linear-gradient(135deg, #ef4444, #f97316);
background: linear-gradient(135deg, #5a5d63, #85888d);
}
.map-icon {
width: 42px;
height: 42px;
flex: none;
filter: brightness(0) invert(1);
.map-symbol {
display: flex;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.76);
border-radius: 14px;
color: #ffffff;
font-size: 22px;
font-weight: 900;
}
.map-title {
color: #ffffff;
font-size: 26px;
line-height: 1;
}
.map-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
}
.map-title {
color: #ffffff;
font-size: 24px;
font-weight: 900;
line-height: 1.1;
}
.map-desc {
margin-top: 6px;
color: rgba(255, 255, 255, 0.9);
margin-top: 7px;
color: rgba(255, 255, 255, 0.92);
font-size: 13px;
line-height: 1.35;
line-height: 1.28;
}
.map-arrow {
color: #ffffff;
font-size: 28px;
font-weight: 500;
font-size: 25px;
}
.tip-card {
display: flex;
margin-top: 18px;
min-height: 58px;
margin-top: 22px;
min-height: 64px;
align-items: center;
gap: 10px;
padding: 0 14px;
border-radius: 16px;
background: #ffffff;
color: #475569;
box-shadow: 0 8px 22px rgba(15, 23, 42, 0.06);
padding: 0 16px;
border: 1px solid rgba(255, 179, 64, 0.18);
border-radius: 20px;
background: #fff7eb;
color: #64666b;
box-shadow: 0 10px 24px rgba(255, 155, 61, 0.08);
}
.tip-mark {
display: flex;
width: 28px;
height: 28px;
width: 30px;
height: 30px;
flex: none;
align-items: center;
justify-content: center;
border-radius: 50%;
background: #fef3c7;
color: #d97706;
font-size: 13px;
font-weight: 900;
border-radius: 10px;
background: #fff0c4;
color: #bf7000;
}
.tip-card text:nth-child(2) {
flex: 1;
font-size: 14px;
line-height: 1.45;
font-size: 15px;
}
.tip-arrow {
color: #94a3b8;
color: #85878c;
font-size: 24px;
}
@media (max-width: 350px) {
.hero-role,
.hero-title {
font-size: 26px;
}
.mentor-image {
right: 63px;
width: 122px;
height: 164px;
}
.mentor-bubble {
right: 12px;
width: 128px;
padding: 12px;
}
.voice-panel {
grid-template-columns: minmax(0, 1fr) 98px;
margin-right: 4px;
margin-left: 4px;
padding: 12px;
}
.voice-primary {
gap: 10px;
padding-right: 10px;
}
.voice-emblem {
width: 52px;
height: 52px;
}
.voice-copy text:first-child {
font-size: 18px;
}
.mentor-chips .chip {
font-size: 13px;
}
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

@@ -0,0 +1,16 @@
Lucide icons
Copyright (c) 2020 Lucide Contributors
ISC License
Permission to use, copy, modify, and distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+138 -63
View File
@@ -1,17 +1,41 @@
page {
min-height: 100%;
background: #f5f7fb;
color: #172033;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f3f5f7;
color: #182230;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Segoe UI", sans-serif;
}
:root {
--mobile-shell-max-width: 430px;
--surface: #ffffff;
--canvas: #f3f5f7;
--ink: #182230;
--muted: #647083;
--line: #dfe4ea;
--brand: #e70014;
--brand-pressed: #bd0010;
--knowledge: #315b8f;
--success: #168243;
--warning: #a86100;
--radius: 8px;
--employee-bg: #f4f6fb;
--employee-bg-soft: #f8fbff;
--employee-surface: #ffffff;
--employee-primary: #2f6bff;
--employee-primary-soft: #eaf1ff;
--employee-success: #34b368;
--employee-warning: #ff9b3d;
--employee-danger: #ff6b5f;
--employee-text: #182230;
--employee-muted: #6b7280;
--employee-line: rgba(47, 74, 117, 0.08);
--employee-card-radius: 24px;
--employee-chip-radius: 18px;
--employee-card-shadow: 0 16px 40px rgba(47, 107, 255, 0.1);
}
view,
text,
input,
button {
box-sizing: border-box;
}
@@ -19,9 +43,9 @@ button {
button {
margin: 0;
border: 0;
border-radius: 12px;
border-radius: var(--radius);
font-size: 16px;
line-height: 1.2;
line-height: 1.25;
}
button::after {
@@ -30,29 +54,63 @@ button::after {
.page {
min-height: 100vh;
padding: 20px 16px 32px;
padding: 22px 16px 96px;
}
.employee-page-shell {
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
}
.employee-card {
border: 1px solid var(--employee-line);
border-radius: var(--employee-card-radius);
background: var(--employee-surface);
box-shadow: var(--employee-card-shadow);
}
.employee-kicker {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--employee-primary);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
}
.employee-section-title {
color: var(--employee-text);
font-size: 20px;
font-weight: 900;
line-height: 1.2;
}
.employee-section-subtitle {
color: var(--employee-muted);
font-size: 14px;
line-height: 1.55;
}
.page-title {
display: block;
font-size: 28px;
font-weight: 800;
line-height: 1.15;
color: #0f172a;
font-size: 26px;
font-weight: 900;
line-height: 1.2;
color: var(--ink);
}
.page-subtitle {
display: block;
margin-top: 8px;
font-size: 15px;
line-height: 1.6;
color: #64748b;
margin-top: 6px;
font-size: 14px;
line-height: 1.55;
color: var(--muted);
}
.subpage-header {
display: flex;
gap: 12px;
align-items: flex-start;
align-items: center;
}
.subpage-heading {
@@ -62,66 +120,68 @@ button::after {
.subpage-back-button {
display: flex;
flex: 0 0 44px;
width: 44px;
min-width: 44px;
height: 44px;
min-height: 44px;
flex: 0 0 40px;
width: 40px;
min-width: 40px;
height: 40px;
min-height: 40px;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
color: #1e293b;
font-size: 24px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
color: var(--ink);
font-size: 22px;
font-weight: 700;
line-height: 1;
}
.panel {
margin-top: 16px;
padding: 18px;
border: 1px solid #e2e8f0;
border-radius: 14px;
background: #ffffff;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: 0 1px 2px rgba(24, 34, 48, 0.04);
}
.panel-title {
font-size: 18px;
font-weight: 800;
color: #1e293b;
font-size: 17px;
font-weight: 900;
color: var(--ink);
}
.muted {
color: #64748b;
color: var(--muted);
}
.primary-button,
.secondary-button,
.ghost-button {
display: flex;
min-height: 48px;
min-height: 46px;
align-items: center;
justify-content: center;
padding: 0 16px;
font-weight: 700;
font-weight: 800;
}
.primary-button {
background: #e60012;
background: var(--brand);
color: #ffffff;
box-shadow: 0 4px 10px rgba(231, 0, 20, 0.16);
}
.secondary-button {
border: 1px solid #cbd5e1;
background: #ffffff;
color: #1e293b;
border: 1px solid #c9d1db;
background: var(--surface);
color: var(--ink);
}
.ghost-button {
background: #eff6ff;
color: #1d4ed8;
background: #eef3f9;
color: #38506c;
}
.primary-button[disabled],
@@ -132,12 +192,12 @@ button::after {
.input {
width: 100%;
min-height: 48px;
min-height: 46px;
padding: 0 14px;
border: 1px solid #cbd5e1;
border-radius: 12px;
background: #ffffff;
color: #0f172a;
border: 1px solid #c9d1db;
border-radius: var(--radius);
background: var(--surface);
color: var(--ink);
font-size: 16px;
}
@@ -145,10 +205,10 @@ button::after {
width: 100%;
min-height: 112px;
padding: 12px 14px;
border: 1px solid #cbd5e1;
border-radius: 12px;
background: #ffffff;
color: #0f172a;
border: 1px solid #c9d1db;
border-radius: var(--radius);
background: var(--surface);
color: var(--ink);
font-size: 16px;
line-height: 1.5;
}
@@ -165,26 +225,30 @@ button::after {
}
.chip {
min-height: 34px;
min-height: 36px;
padding: 0 12px;
border-radius: 999px;
background: #f1f5f9;
color: #334155;
border: 1px solid #dbe1e8;
border-radius: var(--radius);
background: #f8fafc;
color: #3d4b5f;
font-size: 14px;
font-weight: 700;
}
.status-message {
margin-top: 12px;
padding: 10px 12px;
border-radius: 10px;
background: #fff7ed;
color: #9a3412;
border: 1px solid #f0d7a7;
border-left: 3px solid #d38a13;
border-radius: var(--radius);
background: #fffaf0;
color: #80520f;
font-size: 14px;
line-height: 1.5;
}
.supervisor-access-panel {
border-left: 4px solid #e60012;
border-left: 4px solid var(--brand);
}
.supervisor-access-copy {
@@ -209,11 +273,22 @@ button::after {
}
.empty {
padding: 24px 12px;
color: #64748b;
padding: 28px 12px;
color: var(--muted);
text-align: center;
}
.primary-button:active {
background: var(--brand-pressed);
}
uni-tabbar.uni-tabbar-bottom,
uni-tabbar.uni-tabbar-bottom .uni-tabbar {
border-top: 1px solid var(--line) !important;
background: rgba(255, 255, 255, 0.98) !important;
box-shadow: 0 -10px 30px rgba(47, 107, 255, 0.08);
}
@media (min-width: 520px) {
:root {
--mobile-shell-side-offset: calc((100vw - var(--mobile-shell-max-width)) / 2);
@@ -234,8 +309,8 @@ button::after {
uni-app {
min-height: 100vh;
background: #f5f7fb;
box-shadow: 0 0 0 1px rgba(15, 23, 42, 0.06);
background: var(--canvas);
box-shadow: 0 0 0 1px rgba(24, 34, 48, 0.07), 0 16px 42px rgba(24, 34, 48, 0.1);
}
uni-tabbar.uni-tabbar-bottom {