feat(mobile): add employee learning page
This commit is contained in:
+1
-1
@@ -98,7 +98,7 @@
|
||||
### 零散待办
|
||||
|
||||
- **总结卡「保存图片」按钮**(设计基准 summary-modal 图内有):需 html2canvas 类依赖做 DOM 转图,2026-07-05 P0-2 实现时按"不加依赖"红线砍掉;引依赖或后端出图方案定型后补。
|
||||
- **员工端「学」板块**(我的地图橙色块):当前进「建设中」整页,承接 B8 五段旅程的岗前课程/每日小训模块。
|
||||
- **员工端「学」板块**(我的地图橙色块):已在 `mobile-uni/src/pages/user/learning/index.vue` 落成第一版,从今日任务、岗位 SOP 检索和训练前预习卡读取现有真实接口;后续再接课程/考试/视频学习数据。
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 一期 MVP 演示与验收清单
|
||||
|
||||
本文件是 MVP 现场演示入口:先跑预检,再按演示流逐条点击。当前证明一期 MVP 路径可讲、可点、可截图;用户侧三端已在 `mobile-uni/` 按真实页面栈重建,portless 入口为 `https://wygj-mobile-uni.localhost/h5/`,已接手机号登录、问师傅、员工训练、案例素材、候选人面试/资料和主管复盘/派专项;员工端训练完成后写入 `aihr_practice_session`,用于训练历史、主管待复盘列表和能力画像;候选人端面试复用 AI 面试 API,补充资料上传写 `sys_oss` 和 `aihr_candidate_material`,HR 可在管理端审核通过/驳回;SOP 检索已可查本地 MySQL seed 片段,并支持 txt/md/PDF/Word/Excel/PPT 上传解析为本地 fragment,配置 vector 模型后会写入 embedding 并尽力同步 Qdrant,管理端 AI 面试和案例沉淀按真实模型/ASR 优先验收。
|
||||
本文件是 MVP 现场演示入口:先跑预检,再按演示流逐条点击。当前证明一期 MVP 路径可讲、可点、可截图;用户侧三端已在 `mobile-uni/` 按真实页面栈重建,portless 入口为 `https://wygj-mobile-uni.localhost/h5/`,已接手机号登录、问师傅、员工训练、员工学习、案例素材、候选人面试/资料和主管复盘/派专项;员工端训练完成后写入 `aihr_practice_session`,用于训练历史、主管待复盘列表和能力画像;候选人端面试复用 AI 面试 API,补充资料上传写 `sys_oss` 和 `aihr_candidate_material`,HR 可在管理端审核通过/驳回;SOP 检索已可查本地 MySQL seed 片段,并支持 txt/md/PDF/Word/Excel/PPT 上传解析为本地 fragment,配置 vector 模型后会写入 embedding 并尽力同步 Qdrant,管理端 AI 面试和案例沉淀按真实模型/ASR 优先验收。
|
||||
|
||||
## 演示前预检
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| 顺序 | 页面 | 点击路径 | 必须看到 |
|
||||
|---|---|---|---|
|
||||
| 1 | 首页 `/index` | 打开首页 | 只展示“首页 / AI面试 / 三角色对练 / 案例沉淀 / SOP知识库 / 资料处理 / 系统设置-模型配置” |
|
||||
| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端今日页进入问师傅、练习、案例素材和个人页;练习页完成开始练习、提交回应、结束评分、每日三题提交;问师傅页可检索、反馈、生成总结卡;案例素材页可上传语音并回读入库记录;候选人端点“开始面试/面试练习”和“补充资料”;主管端查看团队画像、待复盘、标记已复盘并从画像派专项 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;底部 tab 整页切换(员工端为 今日/练/问/我);未实现入口进「建设中」整页(返回按钮),无死按钮;主管端待复盘列表增加,复盘详情可看评分、话术、导师改写,并可标记已复盘;从团队画像派专项后最近专项立即回读;候选人端可拉题/答题/评分,资料上传后显示审核状态;HR 审核后候选人端显示 `已通过/已驳回` |
|
||||
| 2 | uni-app 三端 `https://wygj-mobile-uni.localhost/h5/#/pages/user/today/index`、`/pages/candidate/index/index`、`/pages/supervisor/index/index` | 打开后先手机号登录;已配置 `AIHR_SMS_LOGIN_TEMPLATE_ID` 时可真实收短信,新手机号会自动注册;员工端今日页进入问师傅、练习、学习页、案例素材和个人页;学习页读取今日任务、岗位 SOP 和训练前预习卡;练习页完成开始练习、提交回应、结束评分、每日三题提交;问师傅页可检索、反馈、生成总结卡;案例素材页可上传语音并回读入库记录;候选人端点“开始面试/面试练习”和“补充资料”;主管端查看团队画像、待复盘、标记已复盘并从画像派专项 | 员工端显示评分、“训练完成,主管端待复盘已更新”、训练历史和能力画像;学习页不再落到「建设中」,内容来自现有任务、知识库和预习卡接口;底部 tab 整页切换(员工端为 今日/练/问/我);未实现入口进「建设中」整页(返回按钮),无死按钮;主管端待复盘列表增加,复盘详情可看评分、话术、导师改写,并可标记已复盘;从团队画像派专项后最近专项立即回读;候选人端可拉题/答题/评分,资料上传后显示审核状态;HR 审核后候选人端显示 `已通过/已驳回` |
|
||||
| 3 | AI面试 `/recruit/interview` | 生成题目 → 填满参考回答或输入真实回答 → 完成评分 | 配置 chat 模型时真实生成题目和评分;未配置时用本地 Rubric 估分,不返回固定候选人分数;应看到新增面试记录 |
|
||||
| 4 | 三角色对练 `/train/practice` | 开始对练 → 填入参考回复或输入真实回复 → 继续一轮 → 结束并评分 | 已完成闭环、导师改写、新增对练记录;数据库已启用 chat 模型时,客户回复与评分为真实 LLM 生成(回复不再是固定台词,分数随话术变化),未配置时使用本地剧本兜底 |
|
||||
| 5 | 案例沉淀 `/knowledge/cases` | 上传真实语音 → ASR 转写 → AI 整理 → 送审 → 入库 → 查看样片 | 已完成闭环,案例列表新增真实转写生成的记录;视频仍用样片兜底 |
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
"navigationBarTitleText": "案例素材"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/learning/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "学"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/auth/login/index",
|
||||
"style": {
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
<template>
|
||||
<view class="page learning-page">
|
||||
<view class="learning-head">
|
||||
<text class="page-title">学</text>
|
||||
<text class="page-subtitle">按你的岗位读取今日任务、SOP 依据和训练前预习卡。</text>
|
||||
</view>
|
||||
|
||||
<view v-if="message" class="status-message">{{ message }}</view>
|
||||
|
||||
<view v-if="!loggedIn" class="panel login-panel">
|
||||
<view>
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后才能按手机号读取今日任务和岗位学习内容。</text>
|
||||
</view>
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="learning-summary">
|
||||
<view class="summary-item">
|
||||
<text class="summary-value">{{ pendingCount }}</text>
|
||||
<text class="summary-label">待学习</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-value">{{ completedCount }}</text>
|
||||
<text class="summary-label">已完成</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-value">{{ referenceCount }}</text>
|
||||
<text class="summary-label">知识引用</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">今日学习</text>
|
||||
<button class="ghost-button compact-button" :disabled="assignmentLoading" @click="loadAssignments">
|
||||
{{ assignmentLoading ? '刷新中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="assignments.length" class="task-list">
|
||||
<view v-for="item in assignments" :key="item.id" class="task-row">
|
||||
<view class="task-copy">
|
||||
<text class="task-badge" :class="{ daily: item.source === 'daily' }">
|
||||
{{ item.source === 'daily' ? '每日三题' : '专项训练' }}
|
||||
</text>
|
||||
<text class="task-title">{{ item.question || item.scenarioName }}</text>
|
||||
<text v-if="assignmentDesc(item)" class="task-desc">{{ assignmentDesc(item) }}</text>
|
||||
</view>
|
||||
<button
|
||||
class="secondary-button compact-button"
|
||||
:disabled="item.status === '已完成' && item.source !== 'daily'"
|
||||
@click="openAssignment(item)"
|
||||
>
|
||||
{{ item.status === '已完成' ? '查看' : '去完成' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">暂无今日学习任务</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">岗位 SOP</text>
|
||||
<button class="ghost-button compact-button" :disabled="knowledgeLoading" @click="loadKnowledge(activeTopic)">
|
||||
{{ knowledgeLoading ? '检索中' : '重查' }}
|
||||
</button>
|
||||
</view>
|
||||
<view class="chip-row topic-row">
|
||||
<button
|
||||
v-for="topic in topics"
|
||||
:key="topic"
|
||||
class="chip"
|
||||
:class="{ active: topic === activeTopic }"
|
||||
:disabled="knowledgeLoading"
|
||||
@click="loadKnowledge(topic)"
|
||||
>
|
||||
{{ topic }}
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="knowledgeResult?.answer" class="answer-text">{{ knowledgeResult.answer }}</text>
|
||||
<view v-if="knowledgeResult?.keyPoints?.length" class="keypoints">
|
||||
<text v-for="(point, index) in knowledgeResult.keyPoints" :key="point" class="keypoint">
|
||||
{{ index + 1 }}. {{ cleanOrderedText(point) }}
|
||||
</text>
|
||||
</view>
|
||||
<view v-if="knowledgeResult?.snippets?.length" class="snippet-list">
|
||||
<view v-for="(snippet, index) in knowledgeResult.snippets?.slice(0, 3)" :key="`${snippet.docTitle}-${index}`" class="snippet-row">
|
||||
<text class="snippet-title">{{ snippet.docTitle || '知识片段' }}</text>
|
||||
<text class="snippet-content">{{ snippetExcerpt(snippet.content) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!knowledgeLoading" class="empty">知识库暂未命中可引用内容</view>
|
||||
<button class="secondary-button full-action" @click="askTopic(activeTopic)">继续问师傅</button>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="panel-head">
|
||||
<text class="panel-title">训练前预习卡</text>
|
||||
<button class="ghost-button compact-button" :disabled="prepLoading" @click="loadPrepCard">
|
||||
{{ prepLoading ? '加载中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="prepCard" class="prep-grid">
|
||||
<view class="prep-block">
|
||||
<text class="prep-label">要点</text>
|
||||
<text v-for="item in prepCard.keyPoints.slice(0, 3)" :key="`point-${item}`" class="prep-line">{{ item }}</text>
|
||||
</view>
|
||||
<view class="prep-block red">
|
||||
<text class="prep-label">红线</text>
|
||||
<text v-for="item in prepCard.redLines.slice(0, 3)" :key="`red-${item}`" class="prep-line">{{ item }}</text>
|
||||
</view>
|
||||
<view class="prep-block script">
|
||||
<text class="prep-label">可说话术</text>
|
||||
<text v-for="item in prepCard.scripts.slice(0, 3)" :key="`script-${item}`" class="prep-line">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!prepLoading" class="empty">暂无可用预习卡</view>
|
||||
<button class="primary-button full-action" @click="goPractice">练一遍</button>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel action-panel">
|
||||
<text class="panel-title">学习后动作</text>
|
||||
<view class="action-list">
|
||||
<button class="secondary-button action-button" @click="goPractice">进入训练</button>
|
||||
<button class="secondary-button action-button" @click="askTopic(activeTopic)">问师傅</button>
|
||||
<button class="secondary-button action-button" @click="goCases">提交素材</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type {
|
||||
KnowledgeSearchResponse,
|
||||
PracticeAssignment,
|
||||
PracticePrepCard
|
||||
} from '@/types/api';
|
||||
import { getPracticeAssignments, getPracticePrepCard } from '@/services/practice';
|
||||
import { searchKnowledge } from '@/services/knowledge';
|
||||
import { getSelectedPositionProfile, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { ensureEmployeePosition } from '@/services/position';
|
||||
|
||||
const defaultScenarioId = 'complaint-water';
|
||||
const pendingAssignmentKey = 'aihr_pending_assignment_id';
|
||||
const pendingSopQueryKey = 'aihr_sop_pending_query';
|
||||
|
||||
const loggedIn = ref(false);
|
||||
const message = ref('');
|
||||
const assignments = ref<PracticeAssignment[]>([]);
|
||||
const prepCard = ref<PracticePrepCard | null>(null);
|
||||
const knowledgeResult = ref<KnowledgeSearchResponse | null>(null);
|
||||
const activeTopic = ref('岗前 SOP');
|
||||
const assignmentLoading = ref(false);
|
||||
const prepLoading = ref(false);
|
||||
const knowledgeLoading = ref(false);
|
||||
const positionProfile = ref(getSelectedPositionProfile());
|
||||
|
||||
const topics = computed(() => {
|
||||
const profile = positionProfile.value;
|
||||
return [
|
||||
`${profile.value} 岗前 SOP`,
|
||||
...profile.quickQuestions
|
||||
];
|
||||
});
|
||||
|
||||
const pendingCount = computed(() => assignments.value.filter((item) => item.status !== '已完成').length);
|
||||
const completedCount = computed(() => assignments.value.filter((item) => item.status === '已完成').length);
|
||||
const referenceCount = computed(() => knowledgeResult.value?.snippets?.length || 0);
|
||||
const firstPracticeAssignment = computed(() =>
|
||||
assignments.value.find((item) => item.status !== '已完成' && item.source !== 'daily')
|
||||
|| assignments.value.find((item) => item.source !== 'daily')
|
||||
|| null
|
||||
);
|
||||
const prepScenarioId = computed(() => firstPracticeAssignment.value?.scenarioId || defaultScenarioId);
|
||||
|
||||
const refreshAuthState = () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
positionProfile.value = getSelectedPositionProfile();
|
||||
if (!topics.value.includes(activeTopic.value)) {
|
||||
activeTopic.value = topics.value[0] || '岗前 SOP';
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequestError = (error: unknown, fallback: string) => {
|
||||
refreshAuthState();
|
||||
if (!loggedIn.value) {
|
||||
message.value = '登录已过期,请重新登录';
|
||||
return;
|
||||
}
|
||||
message.value = error instanceof Error ? error.message : fallback;
|
||||
};
|
||||
|
||||
const assignmentDesc = (item: PracticeAssignment) => {
|
||||
if (item.source === 'daily') {
|
||||
if (item.status === '已完成' && item.feedback) return item.feedback;
|
||||
return '提交后显示反馈与参考答案';
|
||||
}
|
||||
return item.reason || item.feedback;
|
||||
};
|
||||
|
||||
const snippetExcerpt = (text = '') => {
|
||||
const value = text.trim();
|
||||
return value.length > 180 ? `${value.slice(0, 180)}...` : value;
|
||||
};
|
||||
|
||||
const cleanOrderedText = (text = '') =>
|
||||
text
|
||||
.trim()
|
||||
.replace(/^(?:[一二三四五六七八九十]+[、..]|[0-9]+[.)、..])\s*/, '')
|
||||
.trim();
|
||||
|
||||
const loadAssignments = async () => {
|
||||
if (!loggedIn.value) return;
|
||||
assignmentLoading.value = true;
|
||||
try {
|
||||
assignments.value = await getPracticeAssignments(true);
|
||||
} catch (error) {
|
||||
handleRequestError(error, '学习任务加载失败');
|
||||
} finally {
|
||||
assignmentLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPrepCard = async () => {
|
||||
if (!loggedIn.value) return;
|
||||
prepLoading.value = true;
|
||||
try {
|
||||
prepCard.value = await getPracticePrepCard(prepScenarioId.value);
|
||||
} catch (error) {
|
||||
prepCard.value = null;
|
||||
handleRequestError(error, '预习卡加载失败');
|
||||
} finally {
|
||||
prepLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadKnowledge = async (topic: string) => {
|
||||
if (!loggedIn.value || knowledgeLoading.value) return;
|
||||
activeTopic.value = topic;
|
||||
knowledgeLoading.value = true;
|
||||
try {
|
||||
knowledgeResult.value = await searchKnowledge(topic);
|
||||
} catch (error) {
|
||||
knowledgeResult.value = null;
|
||||
handleRequestError(error, '岗位 SOP 检索失败');
|
||||
} finally {
|
||||
knowledgeLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
refreshAuthState();
|
||||
message.value = '';
|
||||
if (!loggedIn.value) {
|
||||
assignments.value = [];
|
||||
prepCard.value = null;
|
||||
knowledgeResult.value = null;
|
||||
return;
|
||||
}
|
||||
if (!(await ensureEmployeePosition('/pages/user/learning/index'))) {
|
||||
return;
|
||||
}
|
||||
refreshAuthState();
|
||||
activeTopic.value = topics.value[0] || activeTopic.value;
|
||||
await Promise.all([
|
||||
loadAssignments(),
|
||||
loadKnowledge(activeTopic.value)
|
||||
]);
|
||||
await loadPrepCard();
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect('/pages/user/learning/index');
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const openAssignment = (item: PracticeAssignment) => {
|
||||
uni.setStorageSync(pendingAssignmentKey, String(item.id));
|
||||
uni.switchTab({ url: '/pages/user/practice/index' });
|
||||
};
|
||||
|
||||
const goPractice = () => {
|
||||
if (firstPracticeAssignment.value) {
|
||||
uni.setStorageSync(pendingAssignmentKey, String(firstPracticeAssignment.value.id));
|
||||
}
|
||||
uni.switchTab({ url: '/pages/user/practice/index' });
|
||||
};
|
||||
|
||||
const askTopic = (topic: string) => {
|
||||
uni.setStorageSync(pendingSopQueryKey, topic);
|
||||
uni.switchTab({ url: '/pages/user/sop/index' });
|
||||
};
|
||||
|
||||
const goCases = () => {
|
||||
uni.navigateTo({ url: '/pages/user/cases/index' });
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
void loadPage();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.learning-page {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
|
||||
.learning-head {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
min-width: 104px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.learning-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
min-height: 82px;
|
||||
padding: 14px 10px;
|
||||
border: 1px solid #dbeafe;
|
||||
border-radius: 14px;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
display: block;
|
||||
color: #1d4ed8;
|
||||
font-size: 25px;
|
||||
font-weight: 850;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: #475569;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.compact-button {
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-list,
|
||||
.snippet-list,
|
||||
.action-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.task-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 13px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.task-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
display: inline-flex;
|
||||
min-height: 24px;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.task-badge.daily {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.task-title,
|
||||
.snippet-title,
|
||||
.prep-label {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.task-desc,
|
||||
.snippet-content,
|
||||
.prep-line {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.topic-row {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: #e60012;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
color: #1e293b;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.keypoints {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.keypoint {
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.snippet-row {
|
||||
padding: 13px;
|
||||
border-left: 4px solid #2563eb;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.full-action {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.prep-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.prep-block {
|
||||
padding: 13px;
|
||||
border-radius: 12px;
|
||||
background: #ecfeff;
|
||||
}
|
||||
|
||||
.prep-block.red {
|
||||
background: #fff1f2;
|
||||
}
|
||||
|
||||
.prep-block.script {
|
||||
background: #fefce8;
|
||||
}
|
||||
|
||||
.action-panel {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -243,6 +243,10 @@ const openTool = (title: string) => {
|
||||
uni.switchTab({ url: '/pages/user/practice/index' });
|
||||
return;
|
||||
}
|
||||
if (title.includes('学') || title.includes('案例学习')) {
|
||||
uni.navigateTo({ url: '/pages/user/learning/index' });
|
||||
return;
|
||||
}
|
||||
if (title.includes('案例') || title.includes('素材') || title.includes('提交')) {
|
||||
uni.navigateTo({ url: '/pages/user/cases/index' });
|
||||
return;
|
||||
|
||||
@@ -129,6 +129,7 @@ contains frontend/src/router/index.ts "/system/model"
|
||||
|
||||
contains mobile-uni/package.json "aihr-mobile-uni"
|
||||
contains mobile-uni/src/pages.json "pages/user/today/index"
|
||||
contains mobile-uni/src/pages.json "pages/user/learning/index"
|
||||
contains mobile-uni/src/pages.json "pages/supervisor/index/index"
|
||||
contains mobile-uni/src/pages.json "pages/candidate/index/index"
|
||||
contains mobile-uni/src/pages/auth/login/index.vue "/resource/sms/code"
|
||||
@@ -144,6 +145,10 @@ contains mobile-uni/src/services/knowledge.ts "/api/knowledge/summary-card"
|
||||
contains mobile-uni/src/services/case.ts "/api/knowledge/case/upload"
|
||||
contains mobile-uni/src/services/candidate.ts "/api/recruit/interview/start"
|
||||
contains mobile-uni/src/services/candidate.ts "/api/aihr/mobile/candidate/materials"
|
||||
contains mobile-uni/src/pages/user/learning/index.vue "岗位 SOP"
|
||||
contains mobile-uni/src/pages/user/learning/index.vue "getPracticeAssignments"
|
||||
contains mobile-uni/src/pages/user/learning/index.vue "searchKnowledge"
|
||||
contains mobile-uni/src/pages/user/learning/index.vue "getPracticePrepCard"
|
||||
contains frontend/src/views/recruit/interview.vue "建议复试"
|
||||
contains frontend/src/views/train/practice.vue "导师改写"
|
||||
contains frontend/src/views/knowledge/cases.vue "真实语音"
|
||||
|
||||
Reference in New Issue
Block a user