feat(aihr): align figma pages for release
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<view class="supervisor-tabbar">
|
||||
<view
|
||||
v-for="item in items"
|
||||
:key="item.key"
|
||||
class="supervisor-tab"
|
||||
:class="{ active: active === item.key }"
|
||||
@click="open(item.path)"
|
||||
>
|
||||
<uni-icons
|
||||
:type="active === item.key ? item.activeIcon : item.icon"
|
||||
size="22"
|
||||
:color="active === item.key ? 'var(--supervisor-nav-active)' : 'var(--supervisor-nav-muted)'"
|
||||
/>
|
||||
<text>{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ active: 'today' | 'team' | 'review' | 'profile' }>();
|
||||
|
||||
const items = [
|
||||
{ key: 'today', label: '今日', icon: 'home', activeIcon: 'home-filled', path: '/pages/supervisor/index/index' },
|
||||
{ key: 'team', label: '团队', icon: 'staff', activeIcon: 'staff-filled', path: '/pages/supervisor/team/index' },
|
||||
{ key: 'review', label: '复盘', icon: 'list', activeIcon: 'list', path: '/pages/supervisor/reviews/index' },
|
||||
{ key: 'profile', label: '我', icon: 'person', activeIcon: 'person-filled', path: '/pages/supervisor/profile/index' }
|
||||
] as const;
|
||||
|
||||
const open = (path: string) => {
|
||||
const pages = getCurrentPages();
|
||||
const current = pages[pages.length - 1]?.route;
|
||||
if (current === path.replace(/^\//, '')) return;
|
||||
uni.redirectTo({ url: path });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.supervisor-tabbar {
|
||||
--supervisor-nav-active: #e60012;
|
||||
--supervisor-nav-muted: #858991;
|
||||
--supervisor-nav-radius: 18px;
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 12px;
|
||||
left: 20px;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
padding: 10px 8px 9px;
|
||||
border: 1px solid rgba(24, 34, 48, 0.06);
|
||||
border-radius: var(--supervisor-nav-radius);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
box-shadow: 0 10px 28px rgba(24, 34, 48, 0.1);
|
||||
}
|
||||
|
||||
.supervisor-tab {
|
||||
display: flex;
|
||||
min-height: 44px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--supervisor-nav-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.supervisor-tab.active {
|
||||
color: var(--supervisor-nav-active);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.supervisor-tabbar {
|
||||
right: calc((100vw - var(--mobile-shell-max-width)) / 2 + 20px);
|
||||
left: calc((100vw - var(--mobile-shell-max-width)) / 2 + 20px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -31,6 +31,12 @@
|
||||
"navigationBarTitleText": "我"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/history/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "训练详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/cases/index",
|
||||
"style": {
|
||||
@@ -91,6 +97,12 @@
|
||||
"navigationBarTitleText": "复盘详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/supervisor/profile/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的主管工作台"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/candidate/index/index",
|
||||
"style": {
|
||||
@@ -112,7 +124,7 @@
|
||||
{
|
||||
"path": "pages/candidate/progress/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "进度通知"
|
||||
"navigationBarTitleText": "面试记录"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -131,7 +143,7 @@
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#7b8494",
|
||||
"selectedColor": "#e70014",
|
||||
"selectedColor": "#e60012",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"height": "66px",
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
type="number"
|
||||
maxlength="11"
|
||||
:clearable="false"
|
||||
primary-color="#e70014"
|
||||
primary-color="#e60012"
|
||||
@input="onPhoneInput"
|
||||
placeholder="请输入手机号"
|
||||
placeholder-style="color: #b5b8c1"
|
||||
@@ -53,7 +53,7 @@
|
||||
type="number"
|
||||
maxlength="6"
|
||||
:clearable="false"
|
||||
primary-color="#e70014"
|
||||
primary-color="#e60012"
|
||||
@input="onSmsCodeInput"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-style="color: #b5b8c1"
|
||||
@@ -207,13 +207,13 @@ onShow(() => {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
background:
|
||||
radial-gradient(circle at 18% 10%, rgba(255, 255, 255, 0.98), rgba(247, 247, 247, 0.86) 38%, rgba(239, 239, 239, 0.72) 100%),
|
||||
linear-gradient(180deg, #fafafa 0%, #f2f2f2 100%);
|
||||
radial-gradient(circle at 18% 10%, rgba(255, 255, 255, 0.98), rgba(247, 248, 251, 0.9) 38%, rgba(239, 242, 247, 0.78) 100%),
|
||||
linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
|
||||
}
|
||||
|
||||
.login-shell {
|
||||
min-height: 100vh;
|
||||
padding: 114px 30px 52px;
|
||||
padding: 72px 24px 36px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -243,14 +243,14 @@ onShow(() => {
|
||||
.login-head {
|
||||
position: relative;
|
||||
max-width: 720px;
|
||||
margin: 0 auto 43px;
|
||||
margin: 0 auto 32px;
|
||||
}
|
||||
|
||||
.brand-lockup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 36px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
@@ -283,7 +283,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.login-eyebrow {
|
||||
color: #1d9668;
|
||||
color: #e60012;
|
||||
}
|
||||
|
||||
.login-head .page-title {
|
||||
@@ -312,8 +312,8 @@ onShow(() => {
|
||||
margin: 0 auto;
|
||||
padding: 36px 20px 36px;
|
||||
border: 1px solid rgba(225, 227, 232, 0.95);
|
||||
border-top: 4px solid #f20b1a;
|
||||
border-radius: 12px;
|
||||
border-top: 4px solid #e60012;
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
box-shadow: 0 12px 28px rgba(29, 31, 35, 0.12), 0 1px 0 rgba(255, 255, 255, 0.8) inset;
|
||||
}
|
||||
@@ -321,7 +321,7 @@ onShow(() => {
|
||||
.login-card::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 12px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.35) 0%, rgba(255, 255, 255, 0) 26%);
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
@@ -369,8 +369,8 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.field-shell:focus-within {
|
||||
border-color: #e70014;
|
||||
box-shadow: 0 0 0 3px rgba(231, 0, 20, 0.08);
|
||||
border-color: #e60012;
|
||||
box-shadow: 0 0 0 3px rgba(230, 0, 18, 0.08);
|
||||
}
|
||||
|
||||
.field-shell :deep(.uni-easyinput) {
|
||||
@@ -387,6 +387,9 @@ onShow(() => {
|
||||
|
||||
.field-shell :deep(.uni-easyinput__content-input) {
|
||||
height: 28px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: transparent !important;
|
||||
color: #25282d;
|
||||
font-size: 15px;
|
||||
line-height: 28px;
|
||||
@@ -433,10 +436,10 @@ onShow(() => {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid #f20b1a;
|
||||
border: 1px solid #e60012;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fffdfd 100%);
|
||||
color: #f20b1a;
|
||||
color: #e60012;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
@@ -453,14 +456,13 @@ onShow(() => {
|
||||
justify-content: center;
|
||||
align-self: stretch;
|
||||
padding: 0 20px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.34) 0%, rgba(255, 255, 255, 0) 12%),
|
||||
linear-gradient(180deg, #ff1124 0%, #f40619 100%);
|
||||
border-radius: 14px;
|
||||
background: #e60012;
|
||||
color: #ffffff;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
box-shadow: 0 8px 18px rgba(231, 0, 20, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
box-shadow: 0 8px 18px rgba(230, 0, 18, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
.login-card :deep(.uni-noticebar) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<template>
|
||||
<view class="page candidate-subpage progress-page">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回候选首页" title="返回候选首页" @click="goBack">←</button>
|
||||
<view class="subpage-heading">
|
||||
<text class="page-title">进度通知</text>
|
||||
<text class="page-subtitle">面试与资料审核状态</text>
|
||||
<view class="page candidate-record-page">
|
||||
<view class="record-header">
|
||||
<button class="back-button" aria-label="返回候选首页" @click="goBack">←</button>
|
||||
<view>
|
||||
<text class="page-title">面试记录</text>
|
||||
<text class="page-subtitle">{{ latestRecord ? `${latestRecord.time} · ${latestRecord.position}` : '面试与资料审核状态' }}</text>
|
||||
</view>
|
||||
<button v-if="loggedIn" class="ghost-button refresh-button" :disabled="loading" @click="loadPage">
|
||||
{{ loading ? '刷新中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="message" class="status-message">{{ message }}</view>
|
||||
@@ -16,66 +13,50 @@
|
||||
<view v-if="!loggedIn" class="panel login-panel">
|
||||
<view>
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后查看本人面试和资料审核进度。</text>
|
||||
<text class="muted block">登录后查看本人面试记录。</text>
|
||||
</view>
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="panel status-panel">
|
||||
<view class="status-head">
|
||||
<text class="status-badge">{{ flow.badge }}</text>
|
||||
<text class="status-progress">{{ flow.progressText }}</text>
|
||||
<template v-else-if="latestRecord">
|
||||
<view class="score-card">
|
||||
<view class="score-card-head">
|
||||
<view>
|
||||
<text class="score-value">{{ latestRecord.score }} 分</text>
|
||||
<text class="score-meta">AI 初评 · {{ latestRecord.result }}</text>
|
||||
</view>
|
||||
<text class="review-badge">{{ reviewStatusLabel }}</text>
|
||||
</view>
|
||||
<text class="status-title">{{ flow.notice }}</text>
|
||||
<text class="status-desc">{{ flow.desc }}</text>
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: `${flow.progress}%` }" />
|
||||
</view>
|
||||
<button class="primary-button next-button" :disabled="loading" @click="runCurrentAction">
|
||||
{{ flow.stage === 'review' || flow.stage === 'complete' ? '刷新进度' : flow.cta }}
|
||||
</button>
|
||||
<text class="score-summary">{{ latestRecord.summary }}</text>
|
||||
<text class="score-evidence">{{ questionRecords.length }} 道题 · 已保存真实回答</text>
|
||||
</view>
|
||||
|
||||
<view class="panel steps-panel">
|
||||
<text class="panel-title">办理流程</text>
|
||||
<view class="step-list">
|
||||
<view v-for="step in steps" :key="step.index" class="step-row" :class="step.status">
|
||||
<view class="step-rail">
|
||||
<text class="step-dot">{{ step.status === 'complete' ? '✓' : step.index }}</text>
|
||||
<view v-if="step.index < steps.length" class="step-line" />
|
||||
</view>
|
||||
<view class="step-copy">
|
||||
<text class="step-title">{{ step.title }}</text>
|
||||
<text class="step-desc">{{ step.desc }}</text>
|
||||
</view>
|
||||
<text class="step-status">{{ stepStatusLabel(step.status) }}</text>
|
||||
<view class="question-card">
|
||||
<text class="section-title">问答记录</text>
|
||||
<view v-for="item in questionRecords" :key="item.seq" class="question-record">
|
||||
<view class="question-title-row">
|
||||
<text class="question-index">{{ item.seq }}</text>
|
||||
<text class="question-title">{{ item.question }}</text>
|
||||
</view>
|
||||
<text class="answer-text">回答:{{ item.answer }}</text>
|
||||
<text class="answer-rating" :class="ratingTone(item.rating)">{{ item.rating }}</text>
|
||||
</view>
|
||||
<view v-if="!questionRecords.length" class="empty question-empty">暂无问答明细</view>
|
||||
</view>
|
||||
|
||||
<view class="panel evidence-panel">
|
||||
<text class="panel-title">办理依据</text>
|
||||
<view v-if="latestRecord" class="evidence-row">
|
||||
<text class="evidence-label">面试记录</text>
|
||||
<view class="evidence-copy">
|
||||
<text class="evidence-title">{{ latestRecord.position }} · {{ latestRecord.score }}分</text>
|
||||
<text class="evidence-desc">{{ latestRecord.time }} · {{ latestRecord.result }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="latestMaterial" class="evidence-row">
|
||||
<text class="evidence-label">最新资料</text>
|
||||
<view class="evidence-copy">
|
||||
<view class="material-title-row">
|
||||
<text class="evidence-title">{{ latestMaterial.fileName }}</text>
|
||||
<text class="status-pill" :class="materialStatusTone(latestMaterial.status)">{{ latestMaterial.status }}</text>
|
||||
</view>
|
||||
<text class="evidence-desc">{{ latestMaterial.time }} · {{ formatCandidateFileSize(latestMaterial.fileSize) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="!latestRecord && !latestMaterial && !loading" class="empty compact-empty">暂无办理记录</view>
|
||||
<view class="review-status-card">
|
||||
<text class="review-status-title">人工复核状态</text>
|
||||
<text class="review-status-desc">{{ reviewStatusDescription }}</text>
|
||||
</view>
|
||||
|
||||
<button class="primary-button home-button" @click="goBack">返回候选人中心</button>
|
||||
</template>
|
||||
|
||||
<view v-else class="panel empty-record-panel">
|
||||
<text class="panel-title">暂无面试记录</text>
|
||||
<text class="muted block">完成一次结构化面试后,这里会展示评分和问答明细。</text>
|
||||
<button class="primary-button home-button" @click="openInterview">开始面试</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -85,84 +66,51 @@ import { onShow } from '@dcloudio/uni-app';
|
||||
import type { CandidateMaterial, InterviewRecord } from '@/types/api';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { getCandidateInterviewRecords, getCandidateMaterials } from '@/services/candidate';
|
||||
import {
|
||||
buildCandidateFlow,
|
||||
buildCandidateSteps,
|
||||
candidateRoutes,
|
||||
formatCandidateFileSize,
|
||||
materialStatusTone
|
||||
} from '@/services/candidate-view';
|
||||
import { candidateRoutes } from '@/services/candidate-view';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
|
||||
const currentPath = '/pages/candidate/progress/index';
|
||||
const records = ref<InterviewRecord[]>([]);
|
||||
const materials = ref<CandidateMaterial[]>([]);
|
||||
const loggedIn = ref(false);
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
|
||||
const flow = computed(() => buildCandidateFlow(records.value, materials.value));
|
||||
const steps = computed(() => buildCandidateSteps(flow.value));
|
||||
const latestRecord = computed(() => records.value[0] || null);
|
||||
const latestMaterial = computed(() => materials.value[0] || null);
|
||||
|
||||
const refreshAuthState = () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
};
|
||||
|
||||
const clearData = () => {
|
||||
records.value = [];
|
||||
materials.value = [];
|
||||
};
|
||||
const questionRecords = computed(() => latestRecord.value?.questions || []);
|
||||
const reviewStatusLabel = computed(() => latestMaterial.value?.status === '已通过' ? '人工复核完成' : '人工复核中');
|
||||
const reviewStatusDescription = computed(() => latestMaterial.value?.status === '已通过'
|
||||
? 'HR 已完成 AI 评分与补充资料复核,后续通知会同步更新。'
|
||||
: 'HR 正在复核 AI 评分与补充资料,预计 1-2 个工作日更新。');
|
||||
|
||||
const loadPage = async () => {
|
||||
refreshAuthState();
|
||||
message.value = '';
|
||||
loggedIn.value = isLoggedIn();
|
||||
if (!loggedIn.value) {
|
||||
clearData();
|
||||
records.value = [];
|
||||
materials.value = [];
|
||||
return;
|
||||
}
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const [nextMaterials, nextRecords] = await Promise.all([
|
||||
[materials.value, records.value] = await Promise.all([
|
||||
getCandidateMaterials(),
|
||||
getCandidateInterviewRecords()
|
||||
]);
|
||||
materials.value = nextMaterials;
|
||||
records.value = nextRecords;
|
||||
message.value = '';
|
||||
} catch (error) {
|
||||
refreshAuthState();
|
||||
if (!loggedIn.value) {
|
||||
clearData();
|
||||
message.value = '登录已过期,请重新登录';
|
||||
} else {
|
||||
message.value = error instanceof Error ? error.message : '办理进度加载失败';
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loggedIn.value = isLoggedIn();
|
||||
message.value = loggedIn.value ? (error instanceof Error ? error.message : '面试记录加载失败') : '登录已过期,请重新登录';
|
||||
}
|
||||
};
|
||||
|
||||
const runCurrentAction = () => {
|
||||
if (flow.value.stage === 'review' || flow.value.stage === 'complete') {
|
||||
void loadPage();
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: flow.value.route });
|
||||
};
|
||||
|
||||
const stepStatusLabel = (status: 'complete' | 'current' | 'pending') => {
|
||||
if (status === 'complete') return '已完成';
|
||||
if (status === 'current') return '当前';
|
||||
return '待办理';
|
||||
};
|
||||
const ratingTone = (rating: string) => rating === '优秀' ? 'excellent' : rating === '良好' ? 'good' : 'needs-work';
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(currentPath);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
const openInterview = () => uni.navigateTo({ url: candidateRoutes.interview });
|
||||
|
||||
const goBack = () => {
|
||||
if (getCurrentPages().length <= 1) {
|
||||
uni.redirectTo({ url: candidateRoutes.index });
|
||||
@@ -178,228 +126,189 @@ onShow(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.candidate-subpage {
|
||||
background: #f4f7fb;
|
||||
.candidate-record-page {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.candidate-subpage .panel,
|
||||
.candidate-subpage button {
|
||||
border-radius: 8px;
|
||||
.record-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 10px 4px 16px;
|
||||
}
|
||||
|
||||
.login-panel,
|
||||
.status-head,
|
||||
.evidence-row,
|
||||
.material-title-row {
|
||||
.back-button {
|
||||
width: 28px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #111;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #858991;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.score-card,
|
||||
.question-card,
|
||||
.review-status-card {
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.score-card {
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.score-card-head,
|
||||
.question-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.refresh-button {
|
||||
min-width: 76px;
|
||||
padding: 0 10px;
|
||||
.score-value {
|
||||
display: block;
|
||||
color: #e60012;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.score-meta,
|
||||
.score-evidence {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #858991;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.score-evidence {
|
||||
margin-top: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.score-summary {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #858991;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.review-badge {
|
||||
padding: 6px 10px;
|
||||
border-radius: 16px;
|
||||
background: #fff4df;
|
||||
color: #d98200;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
margin-top: 14px;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: #111;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.question-record {
|
||||
padding: 18px 0 0;
|
||||
}
|
||||
|
||||
.question-index {
|
||||
display: inline-flex;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: 0 0 22px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #fff0f1;
|
||||
color: #e60012;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
flex: 1;
|
||||
color: #111;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.answer-text {
|
||||
display: block;
|
||||
margin: 8px 0 0 34px;
|
||||
color: #858991;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.answer-rating {
|
||||
display: block;
|
||||
margin: 6px 0 0 34px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.answer-rating.excellent { color: #16a34a; }
|
||||
.answer-rating.good { color: #e60012; }
|
||||
.answer-rating.needs-work { color: #d98200; }
|
||||
|
||||
.review-status-card {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.review-status-title {
|
||||
display: block;
|
||||
color: #111;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.review-status-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #858991;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.home-button {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.login-panel,
|
||||
.empty-record-panel {
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
min-width: 96px;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
border-left: 4px solid #e60012;
|
||||
}
|
||||
|
||||
.status-badge,
|
||||
.status-progress {
|
||||
color: #b91c1c;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
display: block;
|
||||
margin-top: 14px;
|
||||
color: #172033;
|
||||
font-size: 21px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
.question-empty {
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #fee2e2;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #e60012;
|
||||
}
|
||||
|
||||
.next-button {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.step-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
display: grid;
|
||||
min-height: 76px;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.step-rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
display: flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step-row.complete .step-dot {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.step-row.current .step-dot {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.step-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.step-row.complete .step-line {
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.step-title {
|
||||
display: block;
|
||||
color: #172033;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.step-row.current .step-status {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.step-row.complete .step-status {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.evidence-row {
|
||||
justify-content: flex-start;
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.evidence-label {
|
||||
flex: 0 0 64px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.evidence-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.evidence-title {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: #172033;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.evidence-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
flex: 0 0 auto;
|
||||
padding: 5px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-pill.pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status-pill.passed {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.status-pill.rejected {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.compact-empty {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -92,17 +92,38 @@
|
||||
</view>
|
||||
<view v-else-if="!historyLoading" class="empty compact-empty">暂无专项指派记录</view>
|
||||
</view>
|
||||
|
||||
<view class="panel progress-panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">派发进度与预警</text>
|
||||
<text class="muted">{{ progressSummary }}</text>
|
||||
</view>
|
||||
<view class="progress-metrics">
|
||||
<view><text>{{ progressCounts.pending }}</text><label>待训练</label></view>
|
||||
<view><text>{{ progressCounts.completed }}</text><label>已完成</label></view>
|
||||
<view><text>{{ progressCounts.retry }}</text><label>低分再练</label></view>
|
||||
</view>
|
||||
<view v-if="alerts.length" class="assignment-alert-list">
|
||||
<view v-for="alert in alerts" :key="alert.type" class="assignment-alert" :class="alert.severity">
|
||||
<text>{{ alert.title }}</text>
|
||||
<text>{{ alert.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="detail-empty">当前暂无团队预警</view>
|
||||
</view>
|
||||
</template>
|
||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="team" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, ref } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import type { PracticeAssignment } from '@/types/api';
|
||||
import type { PracticeAlert, PracticeAssignment } from '@/types/api';
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { createPracticeAssignment, getPracticeTeamSnapshot } from '@/services/practice';
|
||||
import { createPracticeAssignment, getPracticeAlerts, getPracticeTeamSnapshot } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import {
|
||||
createAsyncGenerationGuard,
|
||||
inferSupervisorScenarioId,
|
||||
@@ -118,6 +139,7 @@ const targetName = ref('');
|
||||
const assignmentScenarioId = ref<SupervisorScenarioId>('fee-parking');
|
||||
const assignmentReason = ref('');
|
||||
const assignments = ref<PracticeAssignment[]>([]);
|
||||
const alerts = ref<PracticeAlert[]>([]);
|
||||
const loggedIn = ref(false);
|
||||
const accessDenied = ref(false);
|
||||
const assigning = ref(false);
|
||||
@@ -132,6 +154,15 @@ const selectedTargetLabel = computed(() => {
|
||||
return targetName.value.trim() ? `${targetName.value.trim()} · ${target}` : target;
|
||||
});
|
||||
const recentAssignments = computed(() => manualPracticeAssignments(assignments.value).slice(0, 10));
|
||||
const progressCounts = computed(() => {
|
||||
const items = recentAssignments.value;
|
||||
return {
|
||||
pending: items.filter((item) => item.status !== '已完成').length,
|
||||
completed: items.filter((item) => item.status === '已完成').length,
|
||||
retry: items.filter((item) => item.source === 'retry').length
|
||||
};
|
||||
});
|
||||
const progressSummary = computed(() => `${progressCounts.value.pending} 项未完成`);
|
||||
|
||||
const queryText = (value: unknown) => {
|
||||
if (typeof value !== 'string') return '';
|
||||
@@ -159,6 +190,7 @@ const resetDraft = () => {
|
||||
const clearPrivateState = () => {
|
||||
resetDraft();
|
||||
assignments.value = [];
|
||||
alerts.value = [];
|
||||
historyLoading.value = false;
|
||||
historyRequests.invalidate();
|
||||
};
|
||||
@@ -191,10 +223,11 @@ const loadAssignments = async (force = false) => {
|
||||
const generation = historyRequests.begin();
|
||||
historyLoading.value = true;
|
||||
try {
|
||||
const snapshot = await getPracticeTeamSnapshot();
|
||||
const [snapshot, nextAlerts] = await Promise.all([getPracticeTeamSnapshot(), getPracticeAlerts()]);
|
||||
if (historyRequests.isCurrent(generation)) {
|
||||
accessDenied.value = false;
|
||||
assignments.value = snapshot.assignments;
|
||||
alerts.value = nextAlerts;
|
||||
}
|
||||
} catch (error) {
|
||||
if (historyRequests.isCurrent(generation)) {
|
||||
@@ -306,8 +339,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.assign-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: 112px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.login-panel,
|
||||
@@ -368,8 +401,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.scenario-chip.active {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
@@ -429,4 +462,80 @@ onBeforeUnmount(() => {
|
||||
.compact-empty {
|
||||
padding: 18px 8px 4px;
|
||||
}
|
||||
|
||||
.progress-panel {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.progress-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
padding: 12px 8px;
|
||||
border-radius: 14px;
|
||||
background: var(--employee-primary-soft);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-metrics view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.progress-metrics text {
|
||||
color: var(--employee-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.progress-metrics label {
|
||||
color: var(--employee-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.assignment-alert-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.assignment-alert {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-left: 3px solid #f59e0b;
|
||||
border-radius: 10px;
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.assignment-alert.high {
|
||||
border-left-color: #ef4444;
|
||||
background: #fff1f2;
|
||||
}
|
||||
|
||||
.assignment-alert text:first-child {
|
||||
color: #a15c00;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.assignment-alert.high text:first-child {
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.assignment-alert text:last-child {
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.detail-empty {
|
||||
margin-top: 12px;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -123,6 +123,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="today" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -138,6 +139,7 @@ import type {
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeAlerts, getPracticeReviews, getPracticeTeamSnapshot } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import {
|
||||
actionablePracticeAlerts,
|
||||
buildSupervisorMetrics,
|
||||
@@ -259,8 +261,8 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.supervisor-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: 112px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.page-head,
|
||||
@@ -295,27 +297,27 @@ onShow(() => {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid #dbe7ff;
|
||||
border-radius: 8px;
|
||||
background: #e2e8f0;
|
||||
background: #dbe7ff;
|
||||
}
|
||||
|
||||
.metric-item {
|
||||
min-height: 104px;
|
||||
padding: 14px;
|
||||
background: #f8fafc;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.metric-item.primary {
|
||||
background: #fff5f5;
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.metric-item.primary .metric-value {
|
||||
color: #e70014;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.metric-item.primary .metric-label {
|
||||
color: #bd0010;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.metric-value,
|
||||
@@ -361,15 +363,16 @@ onShow(() => {
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: transparent;
|
||||
border: 1px solid #e5edf7;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.task-row:last-child,
|
||||
.alert-row:last-child,
|
||||
.preview-row:last-child {
|
||||
border-bottom: 0;
|
||||
border-bottom: 1px solid #e5edf7;
|
||||
}
|
||||
|
||||
.task-copy,
|
||||
@@ -432,7 +435,7 @@ onShow(() => {
|
||||
.preview-score {
|
||||
min-width: 42px;
|
||||
flex: 0 0 auto;
|
||||
color: #dc2626;
|
||||
color: var(--employee-primary);
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<view class="page supervisor-profile-page">
|
||||
<view class="page-head">
|
||||
<view>
|
||||
<text class="page-title">我的主管工作台</text>
|
||||
<text class="page-subtitle">账号与主管身份</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel account-card">
|
||||
<text class="panel-title">当前账号</text>
|
||||
<text class="account-phone">{{ phone || '未登录' }}</text>
|
||||
<text class="muted account-note">主管端团队数据仅对当前项目范围可见。</text>
|
||||
</view>
|
||||
|
||||
<view class="panel identity-card">
|
||||
<text class="panel-title">主管身份</text>
|
||||
<view class="identity-row">
|
||||
<text class="identity-label">角色</text>
|
||||
<text class="identity-value">项目主管</text>
|
||||
</view>
|
||||
<view class="identity-row">
|
||||
<text class="identity-label">可操作范围</text>
|
||||
<text class="identity-value">团队画像、复盘、专项派发</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="secondary-button logout-button" @click="logout">退出登录</button>
|
||||
<SupervisorTabBar active="profile" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import { clearAuth, getAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
|
||||
const phone = ref('');
|
||||
|
||||
const refreshAuth = () => {
|
||||
phone.value = getAuth().phone;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
clearAuth();
|
||||
rememberLoginRedirect('/pages/supervisor/profile/index');
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
if (!isLoggedIn()) {
|
||||
rememberLoginRedirect('/pages/supervisor/profile/index');
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
return;
|
||||
}
|
||||
refreshAuth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.supervisor-profile-page {
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.account-card,
|
||||
.identity-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.account-phone {
|
||||
color: var(--employee-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.account-note {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.identity-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--employee-line);
|
||||
}
|
||||
|
||||
.identity-label {
|
||||
color: var(--employee-muted);
|
||||
}
|
||||
|
||||
.identity-value {
|
||||
color: var(--employee-text);
|
||||
font-weight: 700;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -154,6 +154,7 @@
|
||||
<view class="empty">{{ reviewId ? '复盘详情暂不可用' : '缺少有效的复盘ID' }}</view>
|
||||
<button class="secondary-button back-list-button" @click="goToReviews">返回待复盘</button>
|
||||
</view>
|
||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="review" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -165,6 +166,7 @@ import { authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/ap
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeReviewDetail, markPracticeReviewed } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import {
|
||||
createAsyncGenerationGuard,
|
||||
isReviewEditable,
|
||||
@@ -411,8 +413,8 @@ onBeforeUnmount(() => {
|
||||
|
||||
<style scoped>
|
||||
.review-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: 112px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.subpage-header {
|
||||
@@ -514,8 +516,8 @@ onBeforeUnmount(() => {
|
||||
flex: 0 0 auto;
|
||||
padding: 7px 10px;
|
||||
border-radius: 8px;
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -618,8 +620,8 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.scenario-chip.active {
|
||||
background: #fee2e2;
|
||||
color: #b91c1c;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.next-training-section {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
</view>
|
||||
<view v-else-if="!loading" class="empty">暂无待复盘记录</view>
|
||||
</view>
|
||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="review" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -65,6 +66,7 @@ import type { PracticeAlert, PracticeRecord } from '@/types/api';
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeAlerts, getPracticeReviews } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import { buildSupervisorMetrics, isSupervisorAccessDenied } from '@/services/supervisor-view';
|
||||
|
||||
const currentPath = '/pages/supervisor/reviews/index';
|
||||
@@ -176,8 +178,8 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.reviews-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: 112px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.subpage-header {
|
||||
@@ -221,14 +223,14 @@ onShow(() => {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 13px 0;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: transparent;
|
||||
border: 1px solid #e5edf7;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.review-row:last-child {
|
||||
border-bottom: 0;
|
||||
border-bottom: 1px solid #e5edf7;
|
||||
}
|
||||
|
||||
.review-copy {
|
||||
@@ -277,7 +279,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.review-score text:first-child {
|
||||
color: #dc2626;
|
||||
color: var(--employee-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,13 @@
|
||||
<text class="muted">按风险排序</text>
|
||||
</view>
|
||||
<view v-if="teamProfiles.length" class="team-list">
|
||||
<view v-for="member in teamProfiles" :key="member.key" class="team-row" :class="member.risk">
|
||||
<view
|
||||
v-for="member in teamProfiles"
|
||||
:key="member.key"
|
||||
class="team-row"
|
||||
:class="member.risk"
|
||||
@click="selectMember(member)"
|
||||
>
|
||||
<view class="team-main">
|
||||
<view class="team-title-row">
|
||||
<text class="team-name">{{ member.displayName }}</text>
|
||||
@@ -65,12 +71,49 @@
|
||||
<text v-if="member.latestScene" class="team-tag">{{ member.latestScene }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<button class="secondary-button team-action" @click="assignMember(member)">派专项</button>
|
||||
<button class="secondary-button team-action" @click.stop="assignMember(member)">派专项</button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!loading" class="empty">暂无团队画像数据</view>
|
||||
</view>
|
||||
|
||||
<view v-if="selectedMember" class="panel member-detail-panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">{{ selectedMember.displayName }} · 能力详情</text>
|
||||
<button class="text-button" @click="selectedMember = null">收起</button>
|
||||
</view>
|
||||
<view class="member-summary">
|
||||
<view>
|
||||
<text class="member-score">{{ selectedMember.latestScoreText }}</text>
|
||||
<text class="member-summary-label">最近训练评分</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="member-score">{{ memberRecords.length }}</text>
|
||||
<text class="member-summary-label">训练次数</text>
|
||||
</view>
|
||||
<view>
|
||||
<text class="member-score">{{ selectedMember.pendingReview }}</text>
|
||||
<text class="member-summary-label">待复盘</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="subsection-title">训练趋势</text>
|
||||
<view v-if="memberRecords.length" class="trend-list">
|
||||
<view v-for="item in memberRecords" :key="item.id || item.time" class="trend-row">
|
||||
<text class="trend-scene">{{ item.scene }}</text>
|
||||
<view class="trend-track"><view class="trend-fill" :style="{ width: `${Math.max(0, Math.min(100, item.score))}%` }" /></view>
|
||||
<text class="trend-score">{{ item.score }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="detail-empty">暂无训练评分,先派发一次专项训练</view>
|
||||
<text class="subsection-title">成长证据</text>
|
||||
<view class="evidence-list">
|
||||
<text>已完成 {{ memberRecords.filter((item) => item.status === '已完成').length }} 次训练</text>
|
||||
<text>当前 {{ selectedMember.pendingAssignment }} 项专项待训练</text>
|
||||
<text>{{ selectedMember.reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<SupervisorTabBar v-if="loggedIn && !accessDenied" active="team" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -81,6 +124,7 @@ import type { PracticeAssignment, PracticeRecord, PracticeTeamMember } from '@/t
|
||||
import { clearAuth, isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
import { getPracticeTeamSnapshot } from '@/services/practice';
|
||||
import SupervisorTabBar from '@/components/SupervisorTabBar.vue';
|
||||
import {
|
||||
buildTeamProfiles,
|
||||
inferSupervisorScenarioId,
|
||||
@@ -96,6 +140,7 @@ const loggedIn = ref(false);
|
||||
const accessDenied = ref(false);
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
const selectedMember = ref<TeamProfile | null>(null);
|
||||
|
||||
const teamProfiles = computed(() => buildTeamProfiles(records.value, assignments.value, members.value));
|
||||
const riskCounts = computed(() => teamProfiles.value.reduce(
|
||||
@@ -105,11 +150,18 @@ const riskCounts = computed(() => teamProfiles.value.reduce(
|
||||
},
|
||||
{ high: 0, medium: 0, low: 0 }
|
||||
));
|
||||
const memberRecords = computed(() => {
|
||||
if (!selectedMember.value) return [];
|
||||
return records.value
|
||||
.filter((item) => item.extPartyId === selectedMember.value?.extPartyId)
|
||||
.slice(0, 6);
|
||||
});
|
||||
|
||||
const clearData = () => {
|
||||
records.value = [];
|
||||
assignments.value = [];
|
||||
members.value = [];
|
||||
selectedMember.value = null;
|
||||
};
|
||||
|
||||
const refreshAuthState = () => {
|
||||
@@ -167,6 +219,10 @@ const assignMember = (member: TeamProfile) => {
|
||||
uni.navigateTo({ url: `/pages/supervisor/assign/index?${query}` });
|
||||
};
|
||||
|
||||
const selectMember = (member: TeamProfile) => {
|
||||
selectedMember.value = selectedMember.value?.key === member.key ? null : member;
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(currentPath);
|
||||
uni.redirectTo({ url: '/pages/auth/login/index' });
|
||||
@@ -201,8 +257,8 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.team-page {
|
||||
padding-bottom: 42px;
|
||||
background: #f4f7fb;
|
||||
padding-bottom: 112px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.subpage-header {
|
||||
@@ -239,7 +295,7 @@ onShow(() => {
|
||||
display: grid;
|
||||
margin-top: 16px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
border: 1px solid #dbe7ff;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@@ -247,7 +303,7 @@ onShow(() => {
|
||||
|
||||
.risk-item {
|
||||
padding: 13px 8px;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
border-right: 1px solid #dbe7ff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -281,7 +337,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.risk-item.low .risk-value {
|
||||
color: #15803d;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.team-list {
|
||||
@@ -294,8 +350,10 @@ onShow(() => {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 14px 0 14px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
border-left: 4px solid #22c55e;
|
||||
border: 1px solid #e5edf7;
|
||||
border-left: 4px solid var(--employee-primary);
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.team-row:last-child {
|
||||
@@ -350,8 +408,8 @@ onShow(() => {
|
||||
.team-tag {
|
||||
padding: 4px 7px;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -363,4 +421,110 @@ onShow(() => {
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.member-detail-panel {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: var(--employee-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.member-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
padding: 12px 8px;
|
||||
border-radius: 14px;
|
||||
background: var(--employee-primary-soft);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.member-score,
|
||||
.member-summary-label,
|
||||
.trend-scene,
|
||||
.trend-score,
|
||||
.subsection-title,
|
||||
.evidence-list text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-score {
|
||||
color: var(--employee-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.member-summary-label {
|
||||
margin-top: 4px;
|
||||
color: var(--employee-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.subsection-title {
|
||||
margin-top: 16px;
|
||||
color: var(--employee-text);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.trend-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.trend-row {
|
||||
display: grid;
|
||||
grid-template-columns: 92px 1fr 30px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.trend-scene,
|
||||
.trend-score {
|
||||
overflow: hidden;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.trend-score {
|
||||
color: var(--employee-primary);
|
||||
font-weight: 900;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.trend-track {
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #e7eefc;
|
||||
}
|
||||
|
||||
.trend-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #6f9cff, #2f6bff);
|
||||
}
|
||||
|
||||
.evidence-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.evidence-list text,
|
||||
.detail-empty {
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<view class="page history-detail-page">
|
||||
<view class="subpage-header">
|
||||
<button class="subpage-back-button" aria-label="返回我的页面" title="返回我的页面" @click="goBack">←</button>
|
||||
<view class="subpage-heading">
|
||||
<text class="page-title">训练详情</text>
|
||||
<text class="page-subtitle">{{ detail ? `${detail.scene} · ${detail.time}` : '训练记录' }}</text>
|
||||
</view>
|
||||
<button class="ghost-button refresh-button" :disabled="loading" @click="loadDetail">
|
||||
{{ loading ? '加载中' : '刷新' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="message" class="status-message">{{ message }}</view>
|
||||
|
||||
<view v-if="!loggedIn" class="panel login-panel">
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后查看训练详情。</text>
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<template v-else-if="detail">
|
||||
<view class="panel summary-panel">
|
||||
<view class="summary-head">
|
||||
<view>
|
||||
<text class="detail-title">{{ detail.scene }}</text>
|
||||
<text class="detail-meta">{{ detail.status }} · {{ detail.score }}分</text>
|
||||
</view>
|
||||
<text class="score-pill">{{ detail.score }}</text>
|
||||
</view>
|
||||
<text class="detail-desc">{{ detail.summary || detail.aiComment || '暂无训练总结' }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.scoreItems?.length" class="panel">
|
||||
<text class="panel-title">评分明细</text>
|
||||
<view v-for="item in detail.scoreItems" :key="item.label" class="score-row">
|
||||
<view>
|
||||
<text class="score-label">{{ item.label }}</text>
|
||||
<text class="score-note">{{ item.note }}</text>
|
||||
</view>
|
||||
<text class="score-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.dialogue?.length" class="panel">
|
||||
<text class="panel-title">对话记录</text>
|
||||
<view v-for="(turn, index) in detail.dialogue" :key="`${turn.role}-${index}`" class="dialogue-row">
|
||||
<text class="dialogue-label">{{ turn.label }}</text>
|
||||
<text class="dialogue-text">{{ turn.text }}</text>
|
||||
<view v-if="hasAudio(turn)" class="audio-block">
|
||||
<audio v-if="audioSrc(turn, index)" class="audio-player" controls :src="audioSrc(turn, index)" />
|
||||
<button v-else class="secondary-button" :disabled="audioLoading[index]" @click="loadAudio(turn, index)">
|
||||
{{ audioLoading[index] ? '加载中' : '加载录音' }}
|
||||
</button>
|
||||
<text v-if="audioErrors[index]" class="audio-error">{{ audioErrors[index] }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="detail.annotations?.length || detail.mentorRewrite" class="panel">
|
||||
<text class="panel-title">复盘建议</text>
|
||||
<view v-for="item in detail.annotations" :key="`${item.turnIndex}-${item.issue}`" class="annotation-row">
|
||||
<text class="annotation-issue">{{ item.issue }}</text>
|
||||
<text class="annotation-rewrite">建议:{{ item.rewrite }}</text>
|
||||
<text class="annotation-sop">依据:{{ item.sopPoint }}</text>
|
||||
</view>
|
||||
<view v-if="detail.mentorRewrite" class="mentor-block">
|
||||
<text class="subsection-title">推荐话术</text>
|
||||
<text class="mentor-text">{{ detail.mentorRewrite }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-else-if="loggedIn && !loading" class="panel empty-panel">
|
||||
<text class="empty">{{ message || '训练记录不存在或不属于当前账号' }}</text>
|
||||
<button class="secondary-button" @click="goBack">返回我的页面</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onLoad, onUnload } from '@dcloudio/uni-app';
|
||||
import { ref } from 'vue';
|
||||
import type { ReviewDetail, ReviewDialogue } from '@/types/api';
|
||||
import { authenticatedFileUrl, releaseAuthenticatedFileUrl } from '@/services/api';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { getPracticeHistoryDetail } from '@/services/practice';
|
||||
import { resetPageScroll } from '@/services/navigation';
|
||||
|
||||
const detail = ref<ReviewDetail | null>(null);
|
||||
const reviewId = ref(0);
|
||||
const loggedIn = ref(isLoggedIn());
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
const audioSources = ref<Record<number, string>>({});
|
||||
const audioLoading = ref<Record<number, boolean>>({});
|
||||
const audioErrors = ref<Record<number, string>>({});
|
||||
|
||||
const audioSrc = (turn: ReviewDialogue, index: number) => audioSources.value[index] || turn.audioUrl || '';
|
||||
const hasAudio = (turn: ReviewDialogue) => Boolean(turn.audioOssId || turn.audioUrl);
|
||||
|
||||
const releaseAudio = () => {
|
||||
Object.values(audioSources.value).forEach(releaseAuthenticatedFileUrl);
|
||||
audioSources.value = {};
|
||||
};
|
||||
|
||||
const loadAudio = async (turn: ReviewDialogue, index: number) => {
|
||||
if (!turn.audioOssId || audioLoading.value[index]) return;
|
||||
audioLoading.value = { ...audioLoading.value, [index]: true };
|
||||
audioErrors.value = { ...audioErrors.value, [index]: '' };
|
||||
try {
|
||||
audioSources.value = {
|
||||
...audioSources.value,
|
||||
[index]: await authenticatedFileUrl(`/api/aihr/mobile/oss/${turn.audioOssId}`)
|
||||
};
|
||||
} catch {
|
||||
audioErrors.value = { ...audioErrors.value, [index]: '录音加载失败,请稍后重试' };
|
||||
} finally {
|
||||
audioLoading.value = { ...audioLoading.value, [index]: false };
|
||||
}
|
||||
};
|
||||
|
||||
const loadDetail = async () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
if (!loggedIn.value) return;
|
||||
if (!reviewId.value) {
|
||||
message.value = '缺少有效的训练记录ID';
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
releaseAudio();
|
||||
try {
|
||||
detail.value = await getPracticeHistoryDetail(reviewId.value);
|
||||
} catch (error) {
|
||||
detail.value = null;
|
||||
message.value = error instanceof Error ? error.message : '训练详情加载失败';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const goBack = () => uni.navigateBack({ delta: 1 });
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect(`/pages/user/history/index?id=${reviewId.value}`);
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
onLoad((query) => {
|
||||
reviewId.value = Number(query?.id || 0);
|
||||
resetPageScroll();
|
||||
void loadDetail();
|
||||
});
|
||||
|
||||
onUnload(releaseAudio);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.history-detail-page {
|
||||
padding: 0 18px 96px;
|
||||
}
|
||||
|
||||
.subpage-header,
|
||||
.summary-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.subpage-header {
|
||||
padding: 20px 0 16px;
|
||||
}
|
||||
|
||||
.subpage-back-button {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
border: 1px solid #e1e2e5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #25282d;
|
||||
font-size: 20px;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
.subpage-heading {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-title,
|
||||
.detail-title,
|
||||
.panel-title,
|
||||
.score-label,
|
||||
.dialogue-label,
|
||||
.annotation-issue,
|
||||
.subsection-title {
|
||||
display: block;
|
||||
color: #25282d;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.page-subtitle,
|
||||
.detail-meta,
|
||||
.detail-desc,
|
||||
.score-note,
|
||||
.dialogue-text,
|
||||
.annotation-rewrite,
|
||||
.annotation-sop,
|
||||
.mentor-text,
|
||||
.muted,
|
||||
.audio-error {
|
||||
display: block;
|
||||
color: #73767c;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.page-subtitle,
|
||||
.detail-meta {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e1e2e5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
border-top: 3px solid #e60012;
|
||||
}
|
||||
|
||||
.score-pill {
|
||||
color: #e60012;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.score-row,
|
||||
.dialogue-row,
|
||||
.annotation-row {
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #ececee;
|
||||
}
|
||||
|
||||
.score-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.score-note,
|
||||
.dialogue-text,
|
||||
.annotation-rewrite,
|
||||
.annotation-sop {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
color: #e60012;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.dialogue-row:first-of-type,
|
||||
.annotation-row:first-of-type {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.dialogue-text,
|
||||
.mentor-text {
|
||||
color: #34373c;
|
||||
}
|
||||
|
||||
.audio-block {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.audio-error {
|
||||
margin-top: 6px;
|
||||
color: #c43b3b;
|
||||
}
|
||||
|
||||
.mentor-block {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #ececee;
|
||||
}
|
||||
|
||||
.mentor-text {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.refresh-button,
|
||||
.secondary-button,
|
||||
.primary-button {
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.refresh-button,
|
||||
.secondary-button {
|
||||
border: 1px solid #dedee1;
|
||||
background: #fff;
|
||||
color: #66696f;
|
||||
}
|
||||
|
||||
.primary-button {
|
||||
border: 0;
|
||||
background: #e60012;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-panel,
|
||||
.empty-panel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-button,
|
||||
.empty-panel button {
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -774,11 +774,12 @@ onShow(() => {
|
||||
|
||||
.practice-head::after {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 24px;
|
||||
width: 56px;
|
||||
height: 3px;
|
||||
background: #e70014;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--employee-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
@@ -1058,7 +1059,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.dialogue-card {
|
||||
border-color: #f0dfe0;
|
||||
border-color: #dbe7ff;
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
@@ -1120,19 +1121,19 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.emotion-fill {
|
||||
background: #e45757;
|
||||
background: #6f9cff;
|
||||
}
|
||||
|
||||
.trust-fill {
|
||||
background: #d51b2b;
|
||||
background: #2f6bff;
|
||||
}
|
||||
|
||||
.red-flag {
|
||||
padding: 9px 10px;
|
||||
border-left: 3px solid #e70014;
|
||||
border-left: 3px solid #f59e0b;
|
||||
border-radius: 4px;
|
||||
background: #fff2f3;
|
||||
color: #bd0010;
|
||||
background: #fff7e6;
|
||||
color: #a15c00;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
@@ -1162,19 +1163,19 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.turn-row.trainee {
|
||||
border-left-color: #e70014;
|
||||
background: #fff6f6;
|
||||
border-left-color: #2f6bff;
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.turn-row.coach {
|
||||
border-left-color: #d98282;
|
||||
background: #fff5f5;
|
||||
border-left-color: #8bb1ff;
|
||||
background: #f4f7ff;
|
||||
}
|
||||
|
||||
.reply-actions button.recording-active {
|
||||
border-color: #e70014;
|
||||
background: #fff5f5;
|
||||
color: #e70014;
|
||||
border-color: #2f6bff;
|
||||
background: #eef4ff;
|
||||
color: #2f6bff;
|
||||
}
|
||||
|
||||
.turn-role {
|
||||
@@ -1198,9 +1199,9 @@ onShow(() => {
|
||||
|
||||
.style-chip {
|
||||
width: auto;
|
||||
border-color: #f0c8ca;
|
||||
border-color: #c9d9ff;
|
||||
background: #ffffff;
|
||||
color: #d51b2b;
|
||||
color: #2f6bff;
|
||||
}
|
||||
|
||||
.reply-card {
|
||||
@@ -1210,7 +1211,7 @@ onShow(() => {
|
||||
|
||||
.practice-textarea {
|
||||
min-height: 124px;
|
||||
border-color: #f0dfe0;
|
||||
border-color: #dbe7ff;
|
||||
border-radius: 18px;
|
||||
background: #f9fbff;
|
||||
}
|
||||
@@ -1230,8 +1231,8 @@ onShow(() => {
|
||||
.help-ref {
|
||||
padding: 5px 8px;
|
||||
border-radius: 5px;
|
||||
background: #fff1f2;
|
||||
color: #d51b2b;
|
||||
background: #eef4ff;
|
||||
color: #2f6bff;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1243,7 +1244,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.result-score {
|
||||
color: #e52323;
|
||||
color: #2f6bff;
|
||||
font-size: 44px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@@ -1261,55 +1262,56 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
border-left: 3px solid #e52323;
|
||||
border-left: 3px solid #2f6bff;
|
||||
border-radius: 6px;
|
||||
background: #fff6f6;
|
||||
background: #eef4ff;
|
||||
}
|
||||
|
||||
.mentor-rewrite text:first-child {
|
||||
color: #c71927;
|
||||
color: #2f6bff;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* 练页入口态:对应高保真原型,真实对话状态仍沿用下方业务面板 */
|
||||
/* 练页入口态:与员工端蓝白轻卡片体系保持一致 */
|
||||
.practice-page {
|
||||
padding: 0 18px 104px;
|
||||
background: #fafafa;
|
||||
padding: 0 16px 108px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.practice-head {
|
||||
margin: 0 -18px;
|
||||
padding: 36px 18px 26px;
|
||||
background: #ffffff;
|
||||
text-align: center;
|
||||
margin: 0 -16px;
|
||||
padding: 30px 20px 24px;
|
||||
border-bottom: 1px solid var(--employee-line);
|
||||
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.practice-head::after,
|
||||
.practice-eyebrow,
|
||||
.practice-tip {
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.practice-head .page-title {
|
||||
color: #111111;
|
||||
font-size: 38px;
|
||||
color: var(--employee-text);
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.practice-head .page-subtitle {
|
||||
max-width: none;
|
||||
margin-top: 8px;
|
||||
color: #858991;
|
||||
font-size: 16px;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.scenario-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-top: 18px;
|
||||
margin-top: 16px;
|
||||
padding: 22px 18px 18px;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--employee-card-radius);
|
||||
border-left: 0;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.06);
|
||||
@@ -1323,13 +1325,13 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
color: #111111;
|
||||
color: var(--employee-text);
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.scenario-desc {
|
||||
color: #858991;
|
||||
color: var(--employee-muted);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
@@ -1340,7 +1342,7 @@ onShow(() => {
|
||||
width: 42%;
|
||||
height: 108px;
|
||||
border-radius: 50%;
|
||||
background: #fff1f2;
|
||||
background: var(--employee-primary-soft);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
@@ -1352,7 +1354,7 @@ onShow(() => {
|
||||
height: 76px;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.08);
|
||||
box-shadow: 0 8px 16px rgba(47, 107, 255, 0.08);
|
||||
transform: rotate(7deg);
|
||||
}
|
||||
|
||||
@@ -1363,7 +1365,7 @@ onShow(() => {
|
||||
width: 48px;
|
||||
height: 6px;
|
||||
border-radius: 6px;
|
||||
background: #f5dfe1;
|
||||
background: #dce8ff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@@ -1386,11 +1388,11 @@ onShow(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
background: #ef5b61;
|
||||
background: #7ba4ff;
|
||||
color: #ffffff;
|
||||
font-size: 19px;
|
||||
letter-spacing: 3px;
|
||||
box-shadow: 0 8px 14px rgba(231, 0, 20, 0.16);
|
||||
box-shadow: 0 8px 14px rgba(47, 107, 255, 0.16);
|
||||
}
|
||||
|
||||
.visual-bubble::after {
|
||||
@@ -1399,7 +1401,7 @@ onShow(() => {
|
||||
bottom: -7px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #ef5b61;
|
||||
background: #7ba4ff;
|
||||
content: '';
|
||||
transform: rotate(35deg);
|
||||
}
|
||||
@@ -1421,27 +1423,47 @@ onShow(() => {
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid #f4d9da;
|
||||
border: 1px solid #dce8ff;
|
||||
border-radius: 12px;
|
||||
background: #fffafa;
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.prep-mini .prep-label {
|
||||
color: #e70014;
|
||||
color: var(--employee-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.prep-mini text:last-child {
|
||||
color: #8c8f96;
|
||||
color: var(--employee-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.scenario-card .primary-button {
|
||||
margin-top: 18px;
|
||||
background: #e70014;
|
||||
box-shadow: 0 8px 16px rgba(231, 0, 20, 0.18);
|
||||
background: linear-gradient(135deg, #2f6bff, #5a8cff);
|
||||
box-shadow: 0 8px 16px rgba(47, 107, 255, 0.18);
|
||||
}
|
||||
|
||||
.login-panel {
|
||||
border-left-color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.assignment-row {
|
||||
border-color: #dbe7ff;
|
||||
}
|
||||
|
||||
.assignment-row.highlighted {
|
||||
border-color: #9bbaff;
|
||||
background: #f4f7ff;
|
||||
}
|
||||
|
||||
.assignment-badge,
|
||||
.assignment-badge.daily,
|
||||
.scenario-badge {
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.recent-panel,
|
||||
@@ -1449,14 +1471,14 @@ onShow(() => {
|
||||
margin-top: 14px;
|
||||
padding: 18px;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||
}
|
||||
|
||||
.recent-link,
|
||||
.daily-action-label {
|
||||
color: #858991;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -1481,8 +1503,8 @@ onShow(() => {
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
border-radius: 50%;
|
||||
background: #fff1f2;
|
||||
color: #e52323;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1502,7 +1524,7 @@ onShow(() => {
|
||||
|
||||
.recent-row .assignment-desc {
|
||||
margin-top: 4px;
|
||||
color: #858991;
|
||||
color: var(--employee-muted);
|
||||
}
|
||||
|
||||
.recent-arrow {
|
||||
@@ -1517,7 +1539,7 @@ onShow(() => {
|
||||
.daily-subtitle {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #858991;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -1529,17 +1551,17 @@ onShow(() => {
|
||||
gap: 5px;
|
||||
margin-top: 14px;
|
||||
padding: 0 14px;
|
||||
border: 1px dashed #f0c8ca;
|
||||
border: 1px dashed #cfe0ff;
|
||||
border-radius: 14px;
|
||||
background: #fffafa;
|
||||
color: #858991;
|
||||
background: #f8fbff;
|
||||
color: var(--employee-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.daily-empty text:first-child {
|
||||
color: #d51b2b;
|
||||
color: var(--employee-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1556,13 +1578,18 @@ onShow(() => {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.daily-row .small-button {
|
||||
background: linear-gradient(135deg, #2f6bff, #5a8cff);
|
||||
box-shadow: 0 6px 12px rgba(47, 107, 255, 0.16);
|
||||
}
|
||||
|
||||
.capability-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
margin-top: 14px;
|
||||
padding: 16px 8px;
|
||||
border-radius: 18px;
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||
}
|
||||
@@ -1580,13 +1607,13 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.capability-strip text:first-child {
|
||||
color: #1a1a1a;
|
||||
color: var(--employee-text);
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.capability-strip text:last-child {
|
||||
color: #8a8d93;
|
||||
color: var(--employee-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="page profile-page">
|
||||
<view class="page profile-page employee-page-shell">
|
||||
<view class="profile-head">
|
||||
<text class="profile-eyebrow">银城物业 · 员工成长</text>
|
||||
<text class="page-title">我的成长</text>
|
||||
@@ -120,7 +120,12 @@
|
||||
<button class="ghost-button refresh-button" :disabled="loading" @click="loadData">刷新</button>
|
||||
</view>
|
||||
<view v-if="history.length" class="history-list">
|
||||
<view v-for="item in history" :key="item.id || item.sessionId || item.time" class="history-row">
|
||||
<view
|
||||
v-for="item in history"
|
||||
:key="item.id || item.sessionId || item.time"
|
||||
class="history-row history-row-clickable"
|
||||
@click="openHistory(item.id)"
|
||||
>
|
||||
<view>
|
||||
<text class="history-title">{{ item.scene }}</text>
|
||||
<text class="history-desc">{{ item.time }} · {{ item.summary }}</text>
|
||||
@@ -231,6 +236,14 @@ const changePosition = (position: UserPosition) => {
|
||||
uni.showToast({ title: '岗位已更新', icon: 'none' });
|
||||
};
|
||||
|
||||
const openHistory = (id?: number) => {
|
||||
if (!id) {
|
||||
message.value = '这条训练记录暂不支持查看详情';
|
||||
return;
|
||||
}
|
||||
uni.navigateTo({ url: `/pages/user/history/index?id=${id}` });
|
||||
};
|
||||
|
||||
onShow(() => {
|
||||
resetPageScroll();
|
||||
void loadData();
|
||||
@@ -239,45 +252,52 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
padding: 0 18px 96px;
|
||||
background: #f7f7f8;
|
||||
padding: 0 16px 108px;
|
||||
}
|
||||
|
||||
.profile-page .panel {
|
||||
border-radius: var(--employee-card-radius);
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
.profile-head {
|
||||
position: relative;
|
||||
margin: 0 -18px;
|
||||
padding: 34px 28px 26px;
|
||||
background: #f0f1ee;
|
||||
margin: 0 -16px;
|
||||
padding: 30px 20px 26px;
|
||||
border-bottom: 1px solid rgba(47, 74, 117, 0.08);
|
||||
background: linear-gradient(180deg, #f8fbff 0%, #edf4ff 100%);
|
||||
}
|
||||
|
||||
.profile-head::after {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 24px;
|
||||
width: 56px;
|
||||
height: 3px;
|
||||
background: #e70014;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--employee-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.profile-eyebrow {
|
||||
display: block;
|
||||
margin-bottom: 12px;
|
||||
color: #168243;
|
||||
margin-bottom: 10px;
|
||||
color: var(--employee-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.profile-head .page-title {
|
||||
color: #25282d;
|
||||
font-size: 32px;
|
||||
line-height: 1.16;
|
||||
color: var(--employee-text);
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.profile-head .page-subtitle {
|
||||
margin-top: 10px;
|
||||
color: #696c72;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -290,15 +310,14 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.account-card {
|
||||
margin-top: 22px;
|
||||
border-left: 4px solid #e70014;
|
||||
box-shadow: 0 8px 22px rgba(37, 40, 45, 0.08);
|
||||
margin-top: 16px;
|
||||
border-left: 4px solid var(--employee-primary);
|
||||
}
|
||||
|
||||
.account-phone {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #676a70;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -317,7 +336,7 @@ onShow(() => {
|
||||
.position-title {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -334,24 +353,24 @@ onShow(() => {
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #dedfe2;
|
||||
border-radius: 6px;
|
||||
background: #fbfbfb;
|
||||
color: #5e6167;
|
||||
border: 1px solid #dce5f2;
|
||||
border-radius: var(--employee-chip-radius);
|
||||
background: #f8fbff;
|
||||
color: #526176;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.position-chip.active {
|
||||
border-color: #168243;
|
||||
background: #edf7f0;
|
||||
color: #168243;
|
||||
border-color: var(--employee-primary);
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.profile-summary {
|
||||
border-color: #d9e7de;
|
||||
border-top: 3px solid #168243;
|
||||
border-color: var(--employee-line);
|
||||
border-top: 3px solid var(--employee-primary);
|
||||
}
|
||||
|
||||
.score-row {
|
||||
@@ -367,12 +386,12 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: #edf7f0;
|
||||
border-radius: 18px;
|
||||
background: var(--employee-primary-soft);
|
||||
}
|
||||
|
||||
.score-value {
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
font-size: 42px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@@ -380,7 +399,7 @@ onShow(() => {
|
||||
|
||||
.score-label {
|
||||
margin-top: 8px;
|
||||
color: #5f6268;
|
||||
color: var(--employee-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -396,26 +415,27 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: #f4f4f5;
|
||||
border: 1px solid var(--employee-line);
|
||||
border-radius: 14px;
|
||||
background: #fbfdff;
|
||||
}
|
||||
|
||||
.metric text:first-child {
|
||||
color: #303338;
|
||||
color: var(--employee-text);
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.metric text:last-child {
|
||||
margin-top: 4px;
|
||||
color: #73767c;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.evidence-panel {
|
||||
border-color: #eadfbd;
|
||||
border-left: 4px solid #d98200;
|
||||
background: #fffdf7;
|
||||
border-color: #f4e4c4;
|
||||
border-left: 4px solid var(--employee-warning);
|
||||
background: #fffaf2;
|
||||
}
|
||||
|
||||
.evidence-grid {
|
||||
@@ -431,19 +451,20 @@ onShow(() => {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f4e7ce;
|
||||
border-radius: 14px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.evidence-metric text:first-child {
|
||||
color: #a86100;
|
||||
color: #b56b14;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.evidence-metric text:last-child {
|
||||
margin-top: 4px;
|
||||
color: #73767c;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -494,14 +515,14 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.growth-title {
|
||||
color: #34373c;
|
||||
color: var(--employee-text);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.growth-desc {
|
||||
margin-top: 5px;
|
||||
color: #73767c;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
@@ -513,7 +534,7 @@ onShow(() => {
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -523,13 +544,13 @@ onShow(() => {
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background: #dce8e0;
|
||||
background: #dce8fb;
|
||||
}
|
||||
|
||||
.growth-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #168243;
|
||||
background: var(--employee-primary);
|
||||
}
|
||||
|
||||
.dimension-row,
|
||||
@@ -545,7 +566,7 @@ onShow(() => {
|
||||
.dimension-title,
|
||||
.history-title {
|
||||
display: block;
|
||||
color: #34373c;
|
||||
color: var(--employee-text);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@@ -553,13 +574,13 @@ onShow(() => {
|
||||
.history-desc {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #73767c;
|
||||
color: var(--employee-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.dimension-score {
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -567,14 +588,25 @@ onShow(() => {
|
||||
.refresh-button {
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #dedee1;
|
||||
border: 1px solid #dce5f2;
|
||||
border-radius: var(--employee-chip-radius);
|
||||
background: #ffffff;
|
||||
color: #66696f;
|
||||
color: #526176;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.history-panel {
|
||||
border-color: #e1e2e5;
|
||||
border-color: var(--employee-line);
|
||||
}
|
||||
|
||||
.history-row-clickable {
|
||||
min-height: 68px;
|
||||
padding: 14px 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.history-row-clickable:active {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.history-score {
|
||||
@@ -585,14 +617,14 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.history-score text:first-child {
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.history-score text:last-child {
|
||||
margin-top: 4px;
|
||||
color: #73767c;
|
||||
color: var(--employee-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,8 +61,14 @@
|
||||
</view>
|
||||
|
||||
<view v-if="result" class="panel answer-panel employee-card">
|
||||
<text class="panel-title">直给答案</text>
|
||||
<text class="answer-text">{{ result.answer || '未生成直接答案,请查看下方引用。' }}</text>
|
||||
<text class="panel-title">师傅回答</text>
|
||||
<view class="question-card">
|
||||
<text class="question-label">我的问题</text>
|
||||
<text class="question-text">{{ query }}</text>
|
||||
</view>
|
||||
<view class="answer-copy-card">
|
||||
<text class="answer-text">{{ result.answer || '未生成直接答案,请查看下方引用。' }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="result.keyPoints?.length" class="keypoints">
|
||||
<text v-for="(point, index) in result.keyPoints" :key="point" class="keypoint">
|
||||
@@ -81,7 +87,7 @@
|
||||
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
|
||||
@click="feedback('up')"
|
||||
>
|
||||
有用
|
||||
有帮助
|
||||
</button>
|
||||
<button
|
||||
class="secondary-button"
|
||||
@@ -89,9 +95,12 @@
|
||||
:disabled="feedbackSubmitting || Boolean(feedbackVerdict)"
|
||||
@click="feedback('down')"
|
||||
>
|
||||
需要复核
|
||||
没解决
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="feedbackVerdict" class="feedback-status">
|
||||
{{ feedbackVerdict === 'up' ? '已记录为有帮助' : '已提交未解决反馈,后续会继续优化这条答案' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="result?.snippets?.length" class="panel sources-panel">
|
||||
@@ -115,7 +124,7 @@
|
||||
<view class="modal-card" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">{{ summaryCard.title }}</text>
|
||||
<button class="modal-close" @click="closeSummary">×</button>
|
||||
<button class="modal-close" aria-label="关闭总结卡" title="关闭总结卡" @click="closeSummary">×</button>
|
||||
</view>
|
||||
<scroll-view scroll-y class="modal-body">
|
||||
<view v-if="summaryCard.steps?.length" class="info-section">
|
||||
@@ -378,24 +387,26 @@ onShow(() => {
|
||||
|
||||
<style scoped>
|
||||
.sop-page {
|
||||
padding: 0 18px 96px;
|
||||
padding: 0 16px 108px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.sop-head {
|
||||
position: relative;
|
||||
margin: 0 -18px;
|
||||
padding: 30px 28px 24px;
|
||||
margin: 0 -16px;
|
||||
padding: 30px 20px 24px;
|
||||
border-bottom: 1px solid var(--employee-line);
|
||||
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
|
||||
}
|
||||
|
||||
.sop-head::after {
|
||||
position: absolute;
|
||||
right: 28px;
|
||||
bottom: 24px;
|
||||
width: 56px;
|
||||
height: 3px;
|
||||
background: #e70014;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
width: 42px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--employee-primary);
|
||||
content: '';
|
||||
}
|
||||
|
||||
@@ -436,7 +447,7 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.query-panel {
|
||||
margin-top: 22px;
|
||||
margin-top: 16px;
|
||||
padding: 20px;
|
||||
border-top: 0;
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
@@ -523,13 +534,43 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.answer-panel {
|
||||
border-color: #d9e7de;
|
||||
border-left: 4px solid #168243;
|
||||
border-color: var(--employee-line);
|
||||
border-left: 4px solid var(--employee-primary);
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
.answer-panel .panel-title {
|
||||
color: #168243;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.question-card,
|
||||
.answer-copy-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
background: #fff1f1;
|
||||
}
|
||||
|
||||
.question-label {
|
||||
color: #e34b4b;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.question-text {
|
||||
color: #25282d;
|
||||
font-size: 16px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.answer-copy-card {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.keypoints {
|
||||
@@ -568,15 +609,25 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.feedback-actions .active {
|
||||
border-color: #168243;
|
||||
background: #edf7f0;
|
||||
color: #168243;
|
||||
border-color: var(--employee-primary);
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.feedback-actions .negative {
|
||||
border-color: #e70014;
|
||||
background: #fff1f2;
|
||||
color: #bd0010;
|
||||
border-color: var(--employee-danger);
|
||||
background: #fff1f0;
|
||||
color: #c34d45;
|
||||
}
|
||||
|
||||
.feedback-status {
|
||||
display: block;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.sources-panel {
|
||||
@@ -600,7 +651,8 @@ onShow(() => {
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border: 1px solid #e3e4e7;
|
||||
border-radius: 8px;
|
||||
border-color: var(--employee-line);
|
||||
border-radius: 18px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 5px 14px rgba(37, 40, 45, 0.04);
|
||||
}
|
||||
@@ -629,8 +681,8 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
border-color: #eadfbd;
|
||||
background: #fffdf7;
|
||||
border-color: #f4e4c4;
|
||||
background: #fffaf2;
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
@@ -719,7 +771,8 @@ onShow(() => {
|
||||
|
||||
.footer-btn {
|
||||
width: 100%;
|
||||
background: #168243;
|
||||
background: var(--employee-primary);
|
||||
box-shadow: 0 8px 18px rgba(47, 107, 255, 0.18);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -915,55 +968,55 @@ onShow(() => {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* 视觉稿对齐:与练页统一的红白卡片体系 */
|
||||
/* 视觉稿对齐:与员工端练页统一的蓝白轻卡片体系 */
|
||||
.sop-page {
|
||||
padding: 0 18px 96px;
|
||||
background: #fafafa;
|
||||
padding: 0 16px 108px;
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
.sop-head {
|
||||
margin: 0 -18px;
|
||||
padding: 34px 18px 24px;
|
||||
background: #ffffff;
|
||||
text-align: center;
|
||||
margin: 0 -16px;
|
||||
padding: 30px 20px 24px;
|
||||
border-bottom: 1px solid var(--employee-line);
|
||||
background: linear-gradient(180deg, #eef4ff 0%, #f5f8fd 100%);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sop-head::after,
|
||||
.sop-eyebrow,
|
||||
.toolbar-tip {
|
||||
display: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sop-head .page-title {
|
||||
color: #111111;
|
||||
font-size: 38px;
|
||||
color: var(--employee-text);
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.sop-head .page-subtitle {
|
||||
max-width: none;
|
||||
margin-top: 10px;
|
||||
color: #858991;
|
||||
font-size: 16px;
|
||||
color: var(--employee-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sop-context {
|
||||
display: inline-flex;
|
||||
margin-top: 14px;
|
||||
padding: 7px 14px;
|
||||
border: 1px solid #f8d7d8;
|
||||
border: 1px solid #cfe0ff;
|
||||
border-radius: 999px;
|
||||
background: #fff7f7;
|
||||
color: #d51b2b;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.query-panel {
|
||||
margin-top: 18px;
|
||||
margin-top: 16px;
|
||||
padding: 22px 18px 18px;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.06);
|
||||
}
|
||||
@@ -979,18 +1032,18 @@ onShow(() => {
|
||||
|
||||
.query-panel .chip {
|
||||
min-height: 40px;
|
||||
border-color: #e52323;
|
||||
border-color: #cfe0ff;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #e52323;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.query-submit {
|
||||
min-height: 54px;
|
||||
margin-top: 8px;
|
||||
border-radius: 18px;
|
||||
background: #e52323;
|
||||
box-shadow: 0 8px 16px rgba(229, 35, 35, 0.18);
|
||||
background: linear-gradient(135deg, #2f6bff, #5a8cff);
|
||||
box-shadow: 0 8px 18px rgba(47, 107, 255, 0.2);
|
||||
}
|
||||
|
||||
.knowledge-tip-card {
|
||||
@@ -1001,8 +1054,10 @@ onShow(() => {
|
||||
padding: 16px;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--employee-line);
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.05);
|
||||
box-shadow: var(--employee-card-shadow);
|
||||
}
|
||||
|
||||
.knowledge-tip-icon {
|
||||
@@ -1012,8 +1067,8 @@ onShow(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 14px;
|
||||
background: #fff0f1;
|
||||
color: #e52323;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -1050,7 +1105,7 @@ onShow(() => {
|
||||
padding: 28px 18px;
|
||||
flex-direction: column;
|
||||
border: 0;
|
||||
border-radius: 20px;
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(24, 28, 36, 0.04);
|
||||
}
|
||||
@@ -1075,67 +1130,68 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.answer-panel {
|
||||
border-color: #f0dfe0;
|
||||
border-left-color: #e52323;
|
||||
border-color: var(--employee-line);
|
||||
border-left-color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.answer-panel .panel-title,
|
||||
.result-score {
|
||||
color: #e52323;
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.keypoint,
|
||||
.speech-bubble {
|
||||
border-left-color: #e52323;
|
||||
background: #fff5f5;
|
||||
border-left-color: var(--employee-primary);
|
||||
background: var(--employee-primary-soft);
|
||||
}
|
||||
|
||||
.cautions,
|
||||
.reminder-item {
|
||||
border-left-color: #e52323;
|
||||
background: #fff5f5;
|
||||
color: #b71925;
|
||||
border-left-color: var(--employee-warning);
|
||||
background: #fff8ec;
|
||||
color: #8b5a1d;
|
||||
}
|
||||
|
||||
.feedback-actions .active {
|
||||
border-color: #e52323;
|
||||
background: #fff0f1;
|
||||
color: #e52323;
|
||||
border-color: var(--employee-primary);
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.feedback-actions .negative {
|
||||
border-color: #e52323;
|
||||
background: #fff0f1;
|
||||
color: #b71925;
|
||||
border-color: var(--employee-danger);
|
||||
background: #fff1f0;
|
||||
color: #c34d45;
|
||||
}
|
||||
|
||||
.summary-panel {
|
||||
border-color: #f0dfe0;
|
||||
border-color: #f4e4c4;
|
||||
border-radius: var(--employee-card-radius);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.summary-panel .ghost-button {
|
||||
border-color: #e52323;
|
||||
background: #ffffff;
|
||||
color: #e52323;
|
||||
border-color: #cfe0ff;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
border-top-color: #e52323;
|
||||
border-top-color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.badge-num,
|
||||
.timeline-node {
|
||||
background: #e52323;
|
||||
background: var(--employee-primary);
|
||||
}
|
||||
|
||||
.a-badge {
|
||||
background: #fff0f1;
|
||||
color: #e52323;
|
||||
background: var(--employee-primary-soft);
|
||||
color: var(--employee-primary);
|
||||
}
|
||||
|
||||
.reminder-text {
|
||||
color: #b71925;
|
||||
color: #8b5a1d;
|
||||
}
|
||||
|
||||
/* 原型对齐:问师傅主卡的标题与对话插画 */
|
||||
@@ -1155,14 +1211,14 @@ onShow(() => {
|
||||
}
|
||||
|
||||
.query-title {
|
||||
color: #111111;
|
||||
color: var(--employee-text);
|
||||
font-size: 30px;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.query-subtitle {
|
||||
color: #858991;
|
||||
color: var(--employee-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
@@ -1174,7 +1230,7 @@ onShow(() => {
|
||||
width: 42%;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
background: #fff3f3;
|
||||
background: var(--employee-primary-soft);
|
||||
}
|
||||
|
||||
.query-bubble {
|
||||
@@ -1192,7 +1248,7 @@ onShow(() => {
|
||||
width: 92px;
|
||||
height: 56px;
|
||||
border-radius: 28px 28px 28px 10px;
|
||||
background: #f58d91;
|
||||
background: #7ba4ff;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
}
|
||||
@@ -1203,7 +1259,7 @@ onShow(() => {
|
||||
left: 18px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #f58d91;
|
||||
background: #7ba4ff;
|
||||
content: '';
|
||||
transform: rotate(28deg);
|
||||
}
|
||||
@@ -1215,8 +1271,8 @@ onShow(() => {
|
||||
height: 42px;
|
||||
border-radius: 22px 22px 8px 22px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 5px 14px rgba(229, 35, 35, 0.08);
|
||||
color: #f3b7b9;
|
||||
box-shadow: 0 5px 14px rgba(47, 107, 255, 0.1);
|
||||
color: #9bb8ff;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,9 @@
|
||||
</view>
|
||||
<text class="schedule-desc">{{ primaryScheduleDesc }}</text>
|
||||
</view>
|
||||
<button class="schedule-action" @click="openPrimarySchedule">开始</button>
|
||||
<button class="schedule-action" @click="openPrimarySchedule">
|
||||
{{ primaryAssignment?.source === 'daily' ? '去作答' : '开始练习' }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="schedule-row" v-if="home?.review">
|
||||
|
||||
@@ -5,6 +5,7 @@ export const clientIdKey = 'aihr_mobile_client_id';
|
||||
export const phoneKey = 'aihr_mobile_phone';
|
||||
export const loginRedirectKey = 'aihr_mobile_login_redirect';
|
||||
export const positionKey = 'aihr_mobile_position';
|
||||
export const roleKey = 'aihr_mobile_role';
|
||||
export const phonePattern = /^1[3-9]\d{9}$/;
|
||||
|
||||
export const userPositionOptions = ['生活顾问', '保安', '保洁', '保修', '客服'] as const;
|
||||
@@ -106,6 +107,7 @@ export const saveAuth = (result: LoginResult, phone: string) => {
|
||||
const previousPhone = uni.getStorageSync(phoneKey) || '';
|
||||
if (previousPhone && previousPhone !== phone) {
|
||||
uni.removeStorageSync(positionKey);
|
||||
uni.removeStorageSync(roleKey);
|
||||
}
|
||||
uni.setStorageSync(tokenKey, result.access_token);
|
||||
uni.setStorageSync(clientIdKey, result.client_id);
|
||||
@@ -117,6 +119,7 @@ export const clearAuth = () => {
|
||||
uni.removeStorageSync(clientIdKey);
|
||||
uni.removeStorageSync(phoneKey);
|
||||
uni.removeStorageSync(positionKey);
|
||||
uni.removeStorageSync(roleKey);
|
||||
uni.removeStorageSync(loginRedirectKey);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { OrgSnapshotResponse } from '@/types/api';
|
||||
import type { MobileIdentity, OrgSnapshotResponse, RoleKey } from '@/types/api';
|
||||
import { apiRequest } from './api';
|
||||
import {
|
||||
getAuth,
|
||||
hasSelectedPosition,
|
||||
normalizePosition,
|
||||
roleKey,
|
||||
saveSelectedPosition,
|
||||
type UserPosition
|
||||
} from './auth';
|
||||
@@ -17,6 +18,31 @@ export const userTabPages = new Set([
|
||||
|
||||
export const isUserRoute = (url: string) => url.startsWith('/pages/user/');
|
||||
|
||||
export const isSupervisorRoute = (url: string) => url.startsWith('/pages/supervisor/');
|
||||
|
||||
export const routeForMobileRole = (role: RoleKey, requested: string) => {
|
||||
if (role === 'supervisor' && (isUserRoute(requested) || isSupervisorRoute(requested))) {
|
||||
return '/pages/supervisor/index/index';
|
||||
}
|
||||
return requested;
|
||||
};
|
||||
|
||||
export const hydrateMobileRole = async (): Promise<RoleKey> => {
|
||||
try {
|
||||
const identity = await apiRequest<MobileIdentity>({
|
||||
url: '/api/aihr/mobile/me',
|
||||
method: 'GET',
|
||||
timeout: 15000,
|
||||
clearAuthOnUnauthorized: false
|
||||
});
|
||||
const role = identity.role === 'supervisor' ? 'supervisor' : 'user';
|
||||
uni.setStorageSync(roleKey, role);
|
||||
return role;
|
||||
} catch {
|
||||
return (uni.getStorageSync(roleKey) === 'supervisor' ? 'supervisor' : 'user') as RoleKey;
|
||||
}
|
||||
};
|
||||
|
||||
export const positionSetupUrl = (redirect: string) =>
|
||||
`/pages/auth/position/index?redirect=${encodeURIComponent(redirect || '/pages/user/today/index')}`;
|
||||
|
||||
@@ -46,6 +72,9 @@ export const hydratePositionFromOrg = async (phone = getAuth().phone): Promise<U
|
||||
};
|
||||
|
||||
export const nextUserRouteAfterLogin = async (redirect: string) => {
|
||||
const role = await hydrateMobileRole();
|
||||
const roleRoute = routeForMobileRole(role, redirect);
|
||||
if (roleRoute !== redirect) return roleRoute;
|
||||
if (!isUserRoute(redirect) || hasSelectedPosition()) return redirect;
|
||||
const officialPosition = await hydratePositionFromOrg();
|
||||
return normalizePosition(officialPosition) ? redirect : positionSetupUrl(redirect);
|
||||
|
||||
@@ -81,6 +81,13 @@ export const getPracticeHistory = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getPracticeHistoryDetail = (id: number) =>
|
||||
apiRequest<ReviewDetail>({
|
||||
url: `/api/aihr/mobile/practice/history/${id}`,
|
||||
method: 'GET',
|
||||
timeout: 15000
|
||||
});
|
||||
|
||||
export const getCompetencyProfile = () => {
|
||||
const extPartyId = encodeURIComponent(currentExtPartyId());
|
||||
return apiRequest<CompetencyProfile>({
|
||||
|
||||
@@ -12,8 +12,8 @@ page {
|
||||
--ink: #182230;
|
||||
--muted: #647083;
|
||||
--line: #dfe4ea;
|
||||
--brand: #e70014;
|
||||
--brand-pressed: #bd0010;
|
||||
--brand: #e60012;
|
||||
--brand-pressed: #b8000f;
|
||||
--knowledge: #315b8f;
|
||||
--success: #168243;
|
||||
--warning: #a86100;
|
||||
@@ -52,12 +52,24 @@ button::after {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 3px solid rgba(230, 0, 18, 0.35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
padding: 22px 16px 96px;
|
||||
}
|
||||
|
||||
.employee-page-shell {
|
||||
--employee-bg: #f3f5f7;
|
||||
--employee-bg-soft: #fafafa;
|
||||
--employee-primary: #e60012;
|
||||
--employee-primary-soft: #fff0f1;
|
||||
--employee-card-shadow: 0 16px 40px rgba(230, 0, 18, 0.08);
|
||||
background: linear-gradient(180deg, var(--employee-bg-soft) 0%, var(--employee-bg) 100%);
|
||||
}
|
||||
|
||||
@@ -187,7 +199,11 @@ button::after {
|
||||
.primary-button[disabled],
|
||||
.secondary-button[disabled],
|
||||
.ghost-button[disabled] {
|
||||
opacity: 0.55;
|
||||
opacity: 1;
|
||||
border-color: #e1e4e8;
|
||||
background: #f1f2f4;
|
||||
box-shadow: none;
|
||||
color: #9aa0a8;
|
||||
}
|
||||
|
||||
.input {
|
||||
|
||||
@@ -11,6 +11,13 @@ export interface LoginResult {
|
||||
|
||||
export type RoleKey = 'user' | 'supervisor' | 'candidate';
|
||||
|
||||
export interface MobileIdentity {
|
||||
role: RoleKey;
|
||||
roleLabel: string;
|
||||
positionName: string;
|
||||
positionLevel: string;
|
||||
}
|
||||
|
||||
export interface ToolItem {
|
||||
title: string;
|
||||
desc: string;
|
||||
@@ -374,6 +381,14 @@ export interface InterviewRecord {
|
||||
score: number;
|
||||
result: string;
|
||||
summary: string;
|
||||
questions?: InterviewQuestionRecord[];
|
||||
}
|
||||
|
||||
export interface InterviewQuestionRecord {
|
||||
seq: number;
|
||||
question: string;
|
||||
answer: string;
|
||||
rating: string;
|
||||
}
|
||||
|
||||
export interface InterviewFinishResponse {
|
||||
|
||||
@@ -46,7 +46,7 @@ test('manifest 明确声明为 uni-app Vue3 CLI 工程', async () => {
|
||||
test('员工端 tabBar 继续保留四个固定入口', async () => {
|
||||
const source = await pageSource('../src/pages.json');
|
||||
|
||||
assert.match(source, /"selectedColor"\s*:\s*"#e70014"/);
|
||||
assert.match(source, /"selectedColor"\s*:\s*"#e60012"/);
|
||||
assert.match(source, /pages\/user\/today\/index/);
|
||||
assert.match(source, /pages\/user\/practice\/index/);
|
||||
assert.match(source, /pages\/user\/sop\/index/);
|
||||
@@ -84,6 +84,14 @@ test('员工端三页接入共享页面和卡片基座', async () => {
|
||||
assert.match(practiceSource, /scenario-card employee-card/);
|
||||
});
|
||||
|
||||
test('登录后按组织身份切换主管首页', async () => {
|
||||
const source = await pageSource('../src/services/position.ts');
|
||||
|
||||
assert.match(source, /\/api\/aihr\/mobile\/me/);
|
||||
assert.match(source, /role === 'supervisor'/);
|
||||
assert.match(source, /pages\/supervisor\/index\/index/);
|
||||
});
|
||||
|
||||
test('员工首页保留师傅区、语音条、今日安排、我的地图四段结构', async () => {
|
||||
const source = await pageSource('../src/pages/user/today/index.vue');
|
||||
|
||||
@@ -107,6 +115,7 @@ test('员工首页高保真入口继续使用真实图标和业务跳转', async
|
||||
assert.match(source, /const openPrimarySchedule = \(\) =>/);
|
||||
assert.match(source, /getMobileHome\('user'\)/);
|
||||
assert.match(source, /getPracticeAssignments\(true\)/);
|
||||
assert.match(source, /src="\/static\/mentor-hifi\.png"/);
|
||||
});
|
||||
|
||||
test('问师傅页保留搜索框、答案区和总结卡入口', async () => {
|
||||
|
||||
Reference in New Issue
Block a user