feat(aihr): align figma pages for release

This commit is contained in:
2026-07-14 02:04:36 +08:00
parent 922fe7ff29
commit 87360a25d3
51 changed files with 3575 additions and 629 deletions
+212 -303
View File
@@ -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>