feat(mobile): 重做员工端首页、问答和训练页视觉
统一员工端 tabBar、共享卡片和三大核心页面的视觉基础,同时保持现有移动端业务流程不变。补充 H5 输入兼容、页面结构和训练回滚行为的回归测试。
@@ -0,0 +1,450 @@
|
||||
# Mobile Uni Employee Pages Visual Redesign Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将员工端 `today / sop / practice` 三页及其强相关的 `tabBar / 共享卡片样式` 调整为接近设计基准稿的统一视觉,同时保持现有业务交互和接口行为不变。
|
||||
|
||||
**Architecture:** 采用“视觉优先、业务最小改动”的改法:先建立员工端共享视觉 token 与基础样式,再按首页、问师傅页、训练页的顺序逐页重排结构和样式。页面内部函数、接口调用与路由保留,主要改模板层级、类名、样式组织和少量静态资源引用。
|
||||
|
||||
**Tech Stack:** uni-app Vue3、Vite、TypeScript、CSS、Node 原生测试、H5 构建校验
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 建立员工端共享视觉基座
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-uni/src/styles/base.css`
|
||||
- Modify: `mobile-uni/src/pages.json`
|
||||
- Test: `mobile-uni/tests/mobile-user-pages.test.mjs`
|
||||
|
||||
- [x] **Step 1: 写一个失败测试,锁住 Vue3 manifest 与员工端视觉约束继续有效**
|
||||
|
||||
```js
|
||||
test('tabBar 继续保留员工端四个固定入口', async () => {
|
||||
const source = await pageSource('../src/pages.json');
|
||||
|
||||
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/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行测试,确认新增断言先失败或当前文件尚未覆盖该约束**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: 新增测试如果还没加入会缺失;加入后在修改前至少能作为后续回归校验入口存在。
|
||||
|
||||
- [x] **Step 3: 在 `base.css` 中整理员工端共享 token,不扩大到全站侵入式重置**
|
||||
|
||||
```css
|
||||
:root {
|
||||
--employee-bg: #f4f6fb;
|
||||
--employee-surface: #ffffff;
|
||||
--employee-primary: #2f6bff;
|
||||
--employee-primary-soft: #eaf1ff;
|
||||
--employee-success: #34b368;
|
||||
--employee-warning: #ff8a1f;
|
||||
--employee-danger: #ff5b4d;
|
||||
--employee-text: #182230;
|
||||
--employee-muted: #6b7280;
|
||||
--employee-card-radius: 24px;
|
||||
--employee-card-shadow: 0 12px 32px rgba(47, 107, 255, 0.08);
|
||||
}
|
||||
|
||||
.employee-card {
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: var(--employee-surface);
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
.employee-section-title {
|
||||
color: var(--employee-text);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 4: 调整 `pages.json` 的员工端 tabBar 视觉参数,保留四 tab 结构和路径**
|
||||
|
||||
```json
|
||||
"tabBar": {
|
||||
"color": "#7b8494",
|
||||
"selectedColor": "#2f6bff",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"height": "66px",
|
||||
"fontSize": "12px",
|
||||
"iconWidth": "25px",
|
||||
"spacing": "3px",
|
||||
"list": [
|
||||
{ "pagePath": "pages/user/today/index", "text": "今日" },
|
||||
{ "pagePath": "pages/user/practice/index", "text": "练" },
|
||||
{ "pagePath": "pages/user/sop/index", "text": "问" },
|
||||
{ "pagePath": "pages/user/profile/index", "text": "我" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: 更新回归测试,验证 tabBar 四入口与共享样式关键 token 存在**
|
||||
|
||||
```js
|
||||
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
|
||||
const source = await pageSource('../src/styles/base.css');
|
||||
|
||||
assert.match(source, /--employee-primary:/);
|
||||
assert.match(source, /--employee-card-radius:/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 6: 运行测试验证通过**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: PASS,且已有输入兼容与 manifest 测试继续通过。
|
||||
|
||||
- [ ] **Step 7: 提交这一阶段改动**
|
||||
|
||||
```bash
|
||||
git add mobile-uni/src/styles/base.css mobile-uni/src/pages.json mobile-uni/tests/mobile-user-pages.test.mjs
|
||||
git commit -m "feat: add employee visual foundation"
|
||||
```
|
||||
|
||||
### Task 2: 重做员工端首页视觉
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-uni/src/pages/user/today/index.vue`
|
||||
- Modify: `mobile-uni/src/styles/base.css`
|
||||
- Test: `mobile-uni/tests/mobile-user-pages.test.mjs`
|
||||
|
||||
- [x] **Step 1: 为首页结构写失败测试,锁住设计稿要求的核心区块**
|
||||
|
||||
```js
|
||||
test('员工首页保留师傅区、语音条、今日安排、我的地图四段结构', async () => {
|
||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||
|
||||
assert.match(source, /mentor-hero/);
|
||||
assert.match(source, /voice-panel/);
|
||||
assert.match(source, /今日安排/);
|
||||
assert.match(source, /我的地图/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行测试确认结构断言可作为改版护栏**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: 失败或在旧结构下不足以覆盖新样式目标,后续补全后通过。
|
||||
|
||||
- [x] **Step 3: 重排首页模板,保留原有交互函数但调整为设计稿节奏**
|
||||
|
||||
```vue
|
||||
<view class="mentor-hero employee-hero">
|
||||
<view class="hero-copy">
|
||||
<text class="hero-role">{{ positionProfile.value }}</text>
|
||||
<text class="hero-title">{{ positionProfile.heroTitle }}</text>
|
||||
<text class="hero-sub">有问题,随时问老师傅</text>
|
||||
</view>
|
||||
<image class="mentor-image" src="/static/mentor.png" mode="aspectFit" />
|
||||
<view class="mentor-bubble">
|
||||
<text>{{ mentorBubbleText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="voice-panel employee-card">
|
||||
<button class="voice-primary" @click="goSop">...</button>
|
||||
<button class="voice-secondary" @click="goSop">...</button>
|
||||
</view>
|
||||
|
||||
<view class="section-card employee-card" v-else-if="hasSchedule">...</view>
|
||||
<view class="section-card employee-card" v-if="loggedIn">...</view>
|
||||
```
|
||||
|
||||
- [x] **Step 4: 用 scoped 样式还原首页视觉,控制人物占屏不超过首屏约五分之一**
|
||||
|
||||
```css
|
||||
.today-page {
|
||||
background: radial-gradient(circle at top, #eef5ff 0%, #f7f9fc 38%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.mentor-hero {
|
||||
min-height: 220px;
|
||||
padding: 28px 24px 0;
|
||||
}
|
||||
|
||||
.mentor-image {
|
||||
right: 24px;
|
||||
bottom: 0;
|
||||
width: 108px;
|
||||
height: 148px;
|
||||
}
|
||||
|
||||
.voice-panel {
|
||||
margin-top: -28px;
|
||||
grid-template-columns: 1.4fr 1fr;
|
||||
}
|
||||
|
||||
.map-tile {
|
||||
border-radius: 22px;
|
||||
min-height: 136px;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: 更新测试,验证首页地图主标题继续显示真实 `tile.title`,避免回退成单字标题**
|
||||
|
||||
```js
|
||||
test('员工首页地图主标题继续显示真实工具名称', async () => {
|
||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||
|
||||
assert.match(source, /<text class="map-title">\{\{ tile\.title \}\}<\/text>/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 6: 运行测试验证首页改版没有破坏护栏**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 7: 提交首页改版**
|
||||
|
||||
```bash
|
||||
git add mobile-uni/src/pages/user/today/index.vue mobile-uni/src/styles/base.css mobile-uni/tests/mobile-user-pages.test.mjs
|
||||
git commit -m "feat: redesign employee home page"
|
||||
```
|
||||
|
||||
### Task 3: 重做问师傅页视觉与总结卡展现
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-uni/src/pages/user/sop/index.vue`
|
||||
- Modify: `mobile-uni/src/styles/base.css`
|
||||
- Test: `mobile-uni/tests/mobile-user-pages.test.mjs`
|
||||
|
||||
- [x] **Step 1: 为问师傅页写失败测试,锁住搜索主导与总结卡入口仍然存在**
|
||||
|
||||
```js
|
||||
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
||||
const source = await pageSource('../src/pages/user/sop/index.vue');
|
||||
|
||||
assert.match(source, /query-panel/);
|
||||
assert.match(source, /answer-panel/);
|
||||
assert.match(source, /summary-panel/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行测试确认护栏建立成功**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: 断言可执行,作为后续视觉重排的结构保护。
|
||||
|
||||
- [x] **Step 3: 收紧问师傅页头部,改为“轻标题 + 强搜索框”的布局**
|
||||
|
||||
```vue
|
||||
<view class="page sop-page">
|
||||
<view class="sop-toolbar">
|
||||
<text class="page-title">查SOP</text>
|
||||
<text class="page-subtitle">直接搜 SOP、案例和常用话术</text>
|
||||
</view>
|
||||
|
||||
<view class="panel query-panel employee-card">
|
||||
<textarea v-model="query" class="textarea sop-query-textarea" ... />
|
||||
<view class="chip-row">...</view>
|
||||
<button class="primary-button query-submit" ...>问老师傅</button>
|
||||
</view>
|
||||
</view>
|
||||
```
|
||||
|
||||
- [x] **Step 4: 调整答案卡、引用依据和总结卡为更接近设计稿的卡片层级与底部浮层感**
|
||||
|
||||
```css
|
||||
.sop-page {
|
||||
background: linear-gradient(180deg, #f7f9fc 0%, #f4f6fb 100%);
|
||||
}
|
||||
|
||||
.query-panel {
|
||||
border-radius: 22px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.answer-panel {
|
||||
border-left: 0;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
border-radius: 28px 28px 0 0;
|
||||
max-width: 430px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: 保持 H5 输入兼容测试存在,防止误把 `textarea` 再次改成 `border-box`**
|
||||
|
||||
```js
|
||||
test('SOP 查询框不覆盖 textarea 的 box-sizing', async () => {
|
||||
const source = await pageSource('../src/pages/user/sop/index.vue');
|
||||
const queryPanelStyle = source.match(/\.query-panel \.textarea \{[\s\S]*?\n\}/)?.[0] || '';
|
||||
|
||||
assert.equal(queryPanelStyle.includes('box-sizing'), false);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 6: 运行测试验证通过**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 7: 提交问师傅页改版**
|
||||
|
||||
```bash
|
||||
git add mobile-uni/src/pages/user/sop/index.vue mobile-uni/src/styles/base.css mobile-uni/tests/mobile-user-pages.test.mjs
|
||||
git commit -m "feat: redesign employee sop page"
|
||||
```
|
||||
|
||||
### Task 4: 重做训练页视觉但保留训练逻辑
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-uni/src/pages/user/practice/index.vue`
|
||||
- Modify: `mobile-uni/src/styles/base.css`
|
||||
- Test: `mobile-uni/tests/mobile-user-pages.test.mjs`
|
||||
|
||||
- [x] **Step 1: 为训练页写失败测试,锁住训练关键区块仍存在**
|
||||
|
||||
```js
|
||||
test('训练页保留场景区、对话区、回复区和结果区', async () => {
|
||||
const source = await pageSource('../src/pages/user/practice/index.vue');
|
||||
|
||||
assert.match(source, /scenario-card/);
|
||||
assert.match(source, /dialogue-card/);
|
||||
assert.match(source, /reply-card/);
|
||||
assert.match(source, /result-card/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 2: 运行测试确认结构护栏存在**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: 断言可运行,用于防止视觉重排时误删功能区块。
|
||||
|
||||
- [x] **Step 3: 调整训练页模板,让头部、场景卡、老师傅提示更接近设计稿的训练面板结构**
|
||||
|
||||
```vue
|
||||
<view class="practice-head">
|
||||
<text class="practice-eyebrow">数字师傅 · 场景训练</text>
|
||||
<text class="page-title">{{ activeScenarioName }}</text>
|
||||
<text class="page-subtitle">与业主、老师傅完成一轮真实工作对话</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel scenario-card employee-card">...</view>
|
||||
<view v-if="turns.length" class="panel dialogue-card employee-card">...</view>
|
||||
<view v-if="status === 'active' || status === 'submitting'" class="panel reply-card employee-card">...</view>
|
||||
```
|
||||
|
||||
- [x] **Step 4: 用样式强化三类角色区分与老师傅提示卡,但不引入每轮真实评分**
|
||||
|
||||
```css
|
||||
.turn-row.customer {
|
||||
background: linear-gradient(180deg, #fff8f1 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.turn-row.trainee {
|
||||
background: linear-gradient(180deg, #f2fbf6 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.turn-row.coach {
|
||||
background: linear-gradient(180deg, #eef5ff 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.reply-card .primary-button {
|
||||
min-height: 52px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.help-card,
|
||||
.mentor-rewrite {
|
||||
border-radius: 22px;
|
||||
}
|
||||
```
|
||||
|
||||
- [x] **Step 5: 保留并复用提交失败回滚测试,避免训练视觉改版时误伤已有状态修复**
|
||||
|
||||
```js
|
||||
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
|
||||
const source = await pageSource('../src/pages/user/practice/index.vue');
|
||||
|
||||
assert.match(source, /const previousTurns = turns\.value\.slice\(\)/);
|
||||
assert.match(source, /turns\.value = previousTurns/);
|
||||
});
|
||||
```
|
||||
|
||||
- [x] **Step 6: 运行测试验证通过**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: PASS。
|
||||
|
||||
- [ ] **Step 7: 提交训练页改版**
|
||||
|
||||
```bash
|
||||
git add mobile-uni/src/pages/user/practice/index.vue mobile-uni/src/styles/base.css mobile-uni/tests/mobile-user-pages.test.mjs
|
||||
git commit -m "feat: redesign employee practice page"
|
||||
```
|
||||
|
||||
### Task 5: 全量验证与收尾
|
||||
|
||||
**Files:**
|
||||
- Verify: `mobile-uni/src/pages/user/today/index.vue`
|
||||
- Verify: `mobile-uni/src/pages/user/sop/index.vue`
|
||||
- Verify: `mobile-uni/src/pages/user/practice/index.vue`
|
||||
- Verify: `mobile-uni/src/styles/base.css`
|
||||
- Verify: `mobile-uni/src/pages.json`
|
||||
- Test: `mobile-uni/tests/mobile-user-pages.test.mjs`
|
||||
|
||||
- [x] **Step 1: 运行员工端页面回归测试**
|
||||
|
||||
Run: `node --test tests/mobile-user-pages.test.mjs`
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 2: 运行全量单测**
|
||||
|
||||
Run: `npm run test:unit`
|
||||
Expected: 全部 PASS;允许保留既有 WebSocket 噪音,只要没有失败。
|
||||
|
||||
- [x] **Step 3: 运行类型检查**
|
||||
|
||||
Run: `npm run typecheck`
|
||||
Expected: PASS。
|
||||
|
||||
- [x] **Step 4: 运行 H5 构建**
|
||||
|
||||
Run: `npm run build:h5`
|
||||
Expected: `Build complete.`
|
||||
|
||||
- [ ] **Step 5: 本地打开首页进行人工快速核对**
|
||||
|
||||
Run: `open "http://localhost:5175/h5/#/pages/user/today/index"`
|
||||
Expected: 能看到新的首页首屏结构、语音条、今日安排、我的地图和统一 tabBar。
|
||||
|
||||
- [ ] **Step 6: 提交最终整体验证结果**
|
||||
|
||||
```bash
|
||||
git add mobile-uni/src/pages/user/today/index.vue mobile-uni/src/pages/user/sop/index.vue mobile-uni/src/pages/user/practice/index.vue mobile-uni/src/styles/base.css mobile-uni/src/pages.json mobile-uni/tests/mobile-user-pages.test.mjs
|
||||
git commit -m "feat: unify employee mobile page visuals"
|
||||
```
|
||||
|
||||
## 计划自检
|
||||
|
||||
### Spec 覆盖检查
|
||||
|
||||
- 三页范围:Task 2、Task 3、Task 4 覆盖 `today / sop / practice`。
|
||||
- 共享样式与 tabBar:Task 1 覆盖。
|
||||
- 老师傅仅复用现有素材、不扩展业务:Task 2 与 Task 4 的模板调整限制在现有资源与逻辑内。
|
||||
- SOP 不伪造能力:Task 3 明确保留真实能力,不新增假 tab 行为。
|
||||
- 训练页不做每轮真实评分:Task 4 明确只改视觉分层,不新增该模块。
|
||||
- 验证要求:Task 5 覆盖页面测试、全量单测、类型检查、H5 构建。
|
||||
|
||||
### Placeholder 扫描
|
||||
|
||||
- 无 `TODO`、`TBD`、`适当处理` 等空泛描述。
|
||||
- 每个任务都包含具体文件、命令和代码片段。
|
||||
|
||||
### 类型与命名一致性
|
||||
|
||||
- 继续沿用现有页面类名:`scenario-card`、`dialogue-card`、`reply-card`、`summary-panel`、`voice-panel` 等。
|
||||
- 复用现有关键回归点:`tile.title`、`previousTurns`、`box-sizing`、`vueVersion`。
|
||||
@@ -0,0 +1,241 @@
|
||||
# 员工端三页视觉改版设计
|
||||
|
||||
## 背景
|
||||
|
||||
当前 `mobile-uni` 员工端首页、问师傅页、训练页虽然已经有一轮样式调整,但整体仍然存在以下问题:
|
||||
|
||||
- 三页视觉语言不统一,像是独立拼接出来的页面,而不是同一套员工端产品。
|
||||
- 信息层级和卡片节奏偏乱,重点入口不够聚焦,页面看起来“重、散、旧”。
|
||||
- 与 `docs/feedback/2026070423/design/` 中已经采纳的 P1 视觉基准仍有明显差距。
|
||||
- 当前诉求是优先解决“页面丑陋”的观感问题,而不是重写业务交互。
|
||||
|
||||
本次改版以视觉还原为主,业务交互保持最小改动。
|
||||
|
||||
## 目标
|
||||
|
||||
### 主要目标
|
||||
|
||||
- 统一员工端 `today / sop / practice` 三页视觉气质。
|
||||
- 尽量向已采纳设计稿靠拢,优先还原布局、卡片层级、按钮形态、留白和色彩关系。
|
||||
- 保持现有业务逻辑、接口调用、页面跳转、训练流程与登录流程不变。
|
||||
- 允许联动调整员工端 `tabBar` 和少量共享基础样式,使三页不再割裂。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不新增后端字段或伪造后端能力。
|
||||
- 不重写训练页状态机或 SOP 检索逻辑。
|
||||
- 不做候选人端、主管端的同步视觉改版。
|
||||
- 不在本次引入新的正式人物素材来源。
|
||||
|
||||
## 设计依据与约束
|
||||
|
||||
### 设计依据
|
||||
|
||||
- 首页视觉基准:`docs/feedback/2026070423/design/h5-redesign-employee-home.png`
|
||||
- SOP 结果与总结卡基准:`docs/feedback/2026070423/design/h5-feedback-summary-modal.png`
|
||||
- 训练页对手戏基准:`docs/feedback/2026070423/design/h5-feedback-voice-roleplay.png`
|
||||
- 综合评审约束:`docs/feedback/2026070423/design/REVIEW.md`
|
||||
|
||||
### 必须遵守的约束
|
||||
|
||||
- 老师傅形象不照抄首页参考图中的女性写实形象,统一使用现有本地素材做“蓝工装插画风男性师傅”的近似表达。
|
||||
- 首页人物只承担氛围与陪伴感,不大面积占屏;视觉占比控制在首屏约五分之一以内。
|
||||
- 训练页不实现每轮真实评分,只保留轻提示或教练辅助信息。
|
||||
- SOP 页如果设计稿出现“全部/SOP/案例/话术”类型 tab,在后端维度未落地前不得伪造成功能型筛选。
|
||||
- 遵守 uni-app H5 输入兼容性规则,不对 `input` / `textarea` 设置 `box-sizing: border-box`。
|
||||
|
||||
## 范围
|
||||
|
||||
### 页面范围
|
||||
|
||||
- `src/pages/user/today/index.vue`
|
||||
- `src/pages/user/sop/index.vue`
|
||||
- `src/pages/user/practice/index.vue`
|
||||
|
||||
### 允许联动的共享部分
|
||||
|
||||
- `src/styles/base.css`
|
||||
- `src/pages.json` 中员工端 `tabBar` 视觉配置
|
||||
- 员工端页面使用的现有本地静态素材引用方式
|
||||
|
||||
### 明确不改的部分
|
||||
|
||||
- 页面路由结构
|
||||
- 接口地址、参数与返回处理
|
||||
- 训练开始、提交、结束、SOP 查询、总结卡生成等业务行为
|
||||
- 登录与岗位确认流程
|
||||
|
||||
## 总体方案
|
||||
|
||||
采用“视觉优先还原型”方案:
|
||||
|
||||
- 保留现有业务结构和页面职责,不移动关键交互入口的语义。
|
||||
- 对三页的首屏结构、标题区、功能卡、按钮区、辅助信息区进行重排和统一。
|
||||
- 在共享样式层建立一套仅服务员工端三页的轻量视觉 token,包括背景、卡片、主色、辅助色、圆角、阴影、标题字号与留白节奏。
|
||||
- 对 `tabBar` 做有限统一,让页面主视觉与底部导航形成同一套产品感知。
|
||||
|
||||
这条方案的核心是:让用户明显感觉“这是一套被设计过的产品”,而不是“旧页面换了点颜色”。
|
||||
|
||||
## 共享视觉系统
|
||||
|
||||
### 色彩
|
||||
|
||||
- 主蓝:用于首页主入口、主要行动按钮、SOP 核心操作。
|
||||
- 辅橙:用于复盘、提醒、异议、次强调动作。
|
||||
- 辅绿:用于师傅、建议、可执行正向反馈。
|
||||
- 辅红:用于风险、命中红线、强提醒。
|
||||
- 大面积背景:使用偏暖的浅灰白底,避免纯白页面带来的生硬感。
|
||||
|
||||
### 形态
|
||||
|
||||
- 卡片统一使用大圆角、轻阴影、弱边框。
|
||||
- 主操作按钮统一做成更饱满的胶囊或大圆角矩形。
|
||||
- 标签、chips、状态 badge 使用明显但不过分抢眼的弱底色。
|
||||
- 页面内图文块尽量减少硬线分割,多用留白和卡片分区建立节奏。
|
||||
|
||||
### 文本层级
|
||||
|
||||
- 页面标题使用大号高对比字重。
|
||||
- 模块标题明显强于正文,但弱于页面标题。
|
||||
- 辅助说明统一压低饱和度,避免整页“处处都是重点”。
|
||||
|
||||
### TabBar
|
||||
|
||||
- 保留“今日 / 练 / 问 / 我”结构不变。
|
||||
- 调整选中态、阴影、顶部边界和留白,让其更接近设计稿中的轻质底栏。
|
||||
- 不改动页面跳转与当前 tab 逻辑。
|
||||
|
||||
## 分页面设计
|
||||
|
||||
### 1. 员工端首页 `today`
|
||||
|
||||
#### 目标
|
||||
|
||||
首页要尽量贴近 `h5-redesign-employee-home.png`:先建立“老师傅陪练”的第一印象,再把语音入口、今日安排、我的地图作为三个连续层级展开。
|
||||
|
||||
#### 结构
|
||||
|
||||
- 顶部使用浅背景氛围图区。
|
||||
- 左侧为岗位与欢迎信息,右下为较小的老师傅形象与气泡。
|
||||
- 头图区下方是大白卡语音入口,主入口明显、次入口收敛。
|
||||
- 再下方是快捷 chips。
|
||||
- 然后依次是“今日安排”“我的地图”“今日小贴士”。
|
||||
|
||||
#### 关键调整
|
||||
|
||||
- 头图不再追求“人物大图压场”,而是营造轻陪伴感。
|
||||
- “问老师傅”作为首要入口,视觉上高于“打字问”。
|
||||
- “今日安排”卡更像日程卡,而不是普通列表。
|
||||
- “我的地图”保留四色功能块,但主标题显示真实业务名,单字仅作装饰符号。
|
||||
- 整页通过更清晰的上下节奏建立首屏完成度。
|
||||
|
||||
### 2. 问师傅页 `sop`
|
||||
|
||||
#### 目标
|
||||
|
||||
页内重点从“普通查询页”升级为“搜索驱动的知识助手页”,并尽量贴近 `h5-feedback-summary-modal.png` 的结构氛围。
|
||||
|
||||
#### 结构
|
||||
|
||||
- 顶部收成更轻的标题与搜索区。
|
||||
- 搜索框作为首屏主元素。
|
||||
- 查询结果区按“直给答案 -> 要点 -> 提醒 -> 引用依据”分层。
|
||||
- 总结卡入口保持单独区域。
|
||||
- 总结卡展示形态从普通 modal 调整为更贴近设计稿的底部浮层信息图风格。
|
||||
|
||||
#### 关键调整
|
||||
|
||||
- 减少当前大块头图区的存在感,把注意力转回搜索行为。
|
||||
- 查询按钮、快捷问题、答案卡的主次关系更明确。
|
||||
- 总结卡内部采用更强的信息图分组感,但不新增保存、听一遍、跟读等未实现业务按钮。
|
||||
- 若现阶段无法正确承载分类 tab,则不强行做功能性顶部分类。
|
||||
|
||||
### 3. 训练页 `practice`
|
||||
|
||||
#### 目标
|
||||
|
||||
把页面从“功能堆叠页”调整为更像训练对话面板的页面,同时遵守“不做每轮真实评分”的约束。
|
||||
|
||||
#### 结构
|
||||
|
||||
- 顶部为训练标题、场景标签和老师傅陪伴状态。
|
||||
- 场景预习卡保留,但视觉上并入训练前准备区域。
|
||||
- 对话区强化三类角色的视觉差异:业主、我、老师傅。
|
||||
- 回复区的录音、转写、求助、提交按钮重新排版,但功能不变。
|
||||
- 教练提示与 SOP 提示更像辅助卡,而不是和主流程抢层级。
|
||||
- 结果卡在完成后仍独立呈现。
|
||||
|
||||
#### 关键调整
|
||||
|
||||
- 参考 `h5-feedback-voice-roleplay.png` 的对话氛围与角色卡感,但不照搬其每轮评分模块。
|
||||
- 让“老师傅提醒”成为视觉上可信赖的辅助块。
|
||||
- 保持现有训练逻辑顺序,不改开始、提交、结束时机。
|
||||
|
||||
## 组件与样式边界
|
||||
|
||||
本次优先在现有页面文件内完成视觉重排,不额外拆出大量新组件,理由如下:
|
||||
|
||||
- 用户当前诉求是快速改善三页视觉,而不是先做前端组件体系建设。
|
||||
- 三页虽然要统一语言,但内容结构差异仍较大,过早抽象容易制造样式耦合。
|
||||
- 共享部分只抽到 `base.css` 的 token 和少量通用类,避免引入新的复杂度。
|
||||
|
||||
允许的抽象上限:
|
||||
|
||||
- 共享 token
|
||||
- 通用卡片/标题/按钮类
|
||||
- 少量重复且完全稳定的视觉辅助类
|
||||
|
||||
不建议在本次新增多层组件封装。
|
||||
|
||||
## 验收标准
|
||||
|
||||
### 视觉验收
|
||||
|
||||
- 三页首屏都明显接近参考设计稿的节奏与气质。
|
||||
- 员工端首页、SOP 页、训练页能够被一眼识别为同一套产品。
|
||||
- 老师傅、语音入口、核心行动按钮、模块标题的层级明显优于当前版本。
|
||||
- `tabBar` 与页面主视觉不再割裂。
|
||||
|
||||
### 行为验收
|
||||
|
||||
- 现有路由、接口、登录、训练、SOP 查询行为不回归。
|
||||
- 不引入假交互、不新增无后端支持的视觉功能。
|
||||
- 输入框与文本域在 H5 仍可正常聚焦、输入。
|
||||
|
||||
### 技术验收
|
||||
|
||||
- 至少完成类型检查、单测和 H5 构建验证。
|
||||
- 如新增与视觉约束有关的轻量回归测试,应优先覆盖容易再次被误改的关键点。
|
||||
|
||||
## 风险与处理
|
||||
|
||||
### 风险 1:现有本地素材不足以完全贴近设计稿
|
||||
|
||||
处理:优先还原布局、比例、色彩和氛围;人物素材先做近似表达,不因素材不足阻塞页面改版。
|
||||
|
||||
### 风险 2:为了追视觉而误伤业务逻辑
|
||||
|
||||
处理:限定本次为视觉优先、交互最小改动,不重写流程函数和接口组织。
|
||||
|
||||
### 风险 3:共享样式改动波及其它页面
|
||||
|
||||
处理:共享样式只服务员工端相关页面,避免做全局侵入式 reset。
|
||||
|
||||
### 风险 4:SOP 设计稿含未落地能力
|
||||
|
||||
处理:只保留当前真实能力,视觉上做近似,不伪造功能。
|
||||
|
||||
## 实施顺序建议
|
||||
|
||||
1. 先整理员工端共享 token 和 tabBar 视觉基座。
|
||||
2. 完成员工端首页改版,建立整套视觉语言。
|
||||
3. 以首页风格为基准改 SOP 页。
|
||||
4. 最后改训练页,避免其复杂交互拖慢整体节奏。
|
||||
5. 跑类型检查、单测和 H5 构建验证。
|
||||
|
||||
## 规格自检结论
|
||||
|
||||
- 无占位符、TODO 或未定义术语。
|
||||
- 方案范围清晰,聚焦三页与少量共享视觉层。
|
||||
- 与用户已确认方向一致:优先还原视觉,业务交互最小改动。
|
||||
- 与项目评审约束一致:人物统一、首页人物缩量、训练页不做每轮真评分、SOP 不伪造分类能力。
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "银城员工端",
|
||||
"appid": "__UNI__AIHR",
|
||||
"description": "物业AI人力资源系统用户侧",
|
||||
"vueVersion": "3",
|
||||
"versionName": "0.1.0",
|
||||
"versionCode": "100",
|
||||
"uniStatistics": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<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">
|
||||
<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="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">
|
||||
<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>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
<template>
|
||||
<view class="page sop-page">
|
||||
<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-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 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>
|
||||
<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>
|
||||
<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 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 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 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>
|
||||
</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>
|
||||
|
||||
@@ -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">
|
||||
<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,12 +49,15 @@
|
||||
<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-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>
|
||||
<view class="schedule-list">
|
||||
@@ -69,12 +80,15 @@
|
||||
</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-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">
|
||||
<button
|
||||
@@ -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) => ({
|
||||
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: toneFor(item.title),
|
||||
icon: iconFor(item.title)
|
||||
}));
|
||||
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>
|
||||
|
||||
|
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.
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import test from 'node:test';
|
||||
|
||||
const pageSource = async (path) => readFile(new URL(path, import.meta.url), 'utf8');
|
||||
|
||||
test('全局基础样式不对输入控件设置 border-box', async () => {
|
||||
const source = await pageSource('../src/styles/base.css');
|
||||
|
||||
assert.doesNotMatch(source, /view,\s*\ntext,\s*\ninput,/);
|
||||
assert.doesNotMatch(source, /view,\s*\ntext,\s*\ntextarea,/);
|
||||
assert.doesNotMatch(source, /(^|\n)input\b[\s\S]*?box-sizing:\s*border-box/m);
|
||||
assert.doesNotMatch(source, /(^|\n)textarea\b[\s\S]*?box-sizing:\s*border-box/m);
|
||||
});
|
||||
|
||||
test('SOP 查询框不再覆盖 textarea 的 box-sizing', async () => {
|
||||
const source = await pageSource('../src/pages/user/sop/index.vue');
|
||||
const queryPanelStyle = source.match(/\.query-panel \.textarea \{[\s\S]*?\n\}/)?.[0] || '';
|
||||
|
||||
assert.equal(queryPanelStyle.includes('box-sizing'), false);
|
||||
});
|
||||
|
||||
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, /<text class="map-title">\{\{ tile\.title \}\}<\/text>/);
|
||||
});
|
||||
|
||||
test('训练页提交失败会回滚本次乐观追加的对话', async () => {
|
||||
const source = await pageSource('../src/pages/user/practice/index.vue');
|
||||
|
||||
assert.match(source, /const previousTurns = turns\.value\.slice\(\)/);
|
||||
assert.match(source, /turns\.value = previousTurns/);
|
||||
});
|
||||
|
||||
test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
|
||||
const source = await pageSource('../src/manifest.json');
|
||||
|
||||
assert.match(source, /"vueVersion"\s*:\s*"3"/);
|
||||
});
|
||||
|
||||
test('员工端 tabBar 继续保留四个固定入口', async () => {
|
||||
const source = await pageSource('../src/pages.json');
|
||||
|
||||
assert.match(source, /"selectedColor"\s*:\s*"#2f6bff"/);
|
||||
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('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
|
||||
const source = await pageSource('../src/styles/base.css');
|
||||
|
||||
assert.match(source, /--employee-primary:\s*#2f6bff/);
|
||||
assert.match(source, /--employee-card-radius:\s*24px/);
|
||||
assert.match(source, /\.employee-card\s*\{/);
|
||||
});
|
||||
|
||||
test('员工端三页接入共享页面和卡片基座', async () => {
|
||||
const [todaySource, sopSource, practiceSource] = await Promise.all([
|
||||
pageSource('../src/pages/user/today/index.vue'),
|
||||
pageSource('../src/pages/user/sop/index.vue'),
|
||||
pageSource('../src/pages/user/practice/index.vue')
|
||||
]);
|
||||
|
||||
assert.match(todaySource, /employee-page-shell/);
|
||||
assert.match(todaySource, /section-card employee-card/);
|
||||
assert.match(sopSource, /query-panel employee-card/);
|
||||
assert.match(practiceSource, /scenario-card employee-card/);
|
||||
});
|
||||
|
||||
test('员工首页保留师傅区、语音条、今日安排、我的地图四段结构', async () => {
|
||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||
|
||||
assert.match(source, /mentor-hero/);
|
||||
assert.match(source, /voice-panel/);
|
||||
assert.match(source, /今日安排/);
|
||||
assert.match(source, /我的地图/);
|
||||
});
|
||||
|
||||
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
||||
const source = await pageSource('../src/pages/user/sop/index.vue');
|
||||
|
||||
assert.match(source, /query-panel/);
|
||||
assert.match(source, /answer-panel/);
|
||||
assert.match(source, /summary-panel/);
|
||||
});
|
||||
|
||||
test('训练页保留任务卡、对话区、回复区和结果区', async () => {
|
||||
const source = await pageSource('../src/pages/user/practice/index.vue');
|
||||
|
||||
assert.match(source, /scenario-card/);
|
||||
assert.match(source, /assignment-panel/);
|
||||
assert.match(source, /dialogue-card/);
|
||||
assert.match(source, /reply-card/);
|
||||
assert.match(source, /result-card/);
|
||||
});
|
||||