feat(aihr): complete uni-app role workflows
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
<template>
|
||||
<view class="page candidate-page">
|
||||
<view class="page-head">
|
||||
<view>
|
||||
<text class="page-title">候选人</text>
|
||||
<text class="page-subtitle">{{ candidateSubtitle }}</text>
|
||||
</view>
|
||||
<button v-if="loggedIn" class="ghost-button refresh-button" :disabled="loading" @click="loadPage">刷新</button>
|
||||
</view>
|
||||
|
||||
<view v-if="message" class="status-message">{{ message }}</view>
|
||||
|
||||
<view v-if="!loggedIn" class="panel login-panel">
|
||||
<view>
|
||||
<text class="panel-title">需要登录</text>
|
||||
<text class="muted block">登录后查看面试任务并提交补充资料。</text>
|
||||
</view>
|
||||
<button class="primary-button login-button" @click="goLogin">去登录</button>
|
||||
</view>
|
||||
|
||||
<view v-if="candidatePrimary" class="panel task-panel">
|
||||
<text class="task-badge">{{ candidatePrimary.badge }}</text>
|
||||
<text class="task-title">{{ candidatePrimary.title }}</text>
|
||||
<text class="task-desc">{{ candidatePrimary.desc }}</text>
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: `${candidatePrimary.progress}%` }" />
|
||||
</view>
|
||||
<text class="muted">{{ candidatePrimary.progressText }}</text>
|
||||
<button class="primary-button interview-start" :disabled="primaryBusy" @click="runPrimaryAction">
|
||||
{{ interviewStatus === 'active' && !needsMaterialUpload ? '重新生成题目' : primaryBusy ? '处理中' : candidatePrimary.cta }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="panel progress-panel">
|
||||
<text class="panel-title">进度通知</text>
|
||||
<text class="progress-notice">{{ progressNotice }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="interviewQuestions.length" class="panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">面试问答</text>
|
||||
</view>
|
||||
<view class="question-list">
|
||||
<view v-for="item in interviewQuestions" :key="item.questionId" class="question-card">
|
||||
<text class="question-meta">{{ item.seq }} · {{ item.scenario }}</text>
|
||||
<text class="question-title">{{ item.questionText }}</text>
|
||||
<text class="question-point">{{ item.point }}</text>
|
||||
<textarea
|
||||
v-model="interviewAnswers[item.questionId]"
|
||||
class="textarea answer-textarea"
|
||||
maxlength="400"
|
||||
:disabled="interviewStatus !== 'active'"
|
||||
placeholder="输入你的回答"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<button class="primary-button finish-button" :disabled="interviewStatus !== 'active'" @click="finishInterview">
|
||||
提交评分
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="interviewResult" class="panel result-panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">面试结果</text>
|
||||
<text class="result-score">{{ interviewResult.totalScore }}分</text>
|
||||
</view>
|
||||
<text class="result-title">{{ interviewResult.result }}</text>
|
||||
<text class="result-desc">{{ interviewResult.aiSummary }}</text>
|
||||
<view v-if="interviewResult.dimensions?.length" class="dimension-list">
|
||||
<view v-for="item in interviewResult.dimensions" :key="item.label" class="dimension-row">
|
||||
<view>
|
||||
<text class="dimension-title">{{ item.label }}</text>
|
||||
<text class="dimension-note">{{ item.note }}</text>
|
||||
</view>
|
||||
<text class="dimension-value">{{ item.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">最近面试</text>
|
||||
<text class="muted">{{ interviewRecords.length }} 条</text>
|
||||
</view>
|
||||
<view v-if="interviewRecords.length" class="interview-list">
|
||||
<view v-for="item in interviewRecords" :key="`${item.time}-${item.candidate}-${item.score}`" class="interview-row">
|
||||
<view class="interview-copy">
|
||||
<text class="interview-title">{{ item.candidate }} · {{ item.position }}</text>
|
||||
<text class="interview-desc">{{ item.time }} · {{ item.summary }}</text>
|
||||
</view>
|
||||
<view class="interview-score">
|
||||
<text>{{ item.score }}</text>
|
||||
<text>{{ item.result }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">暂无面试记录</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">岗前预习</text>
|
||||
<button class="ghost-button study-button" :disabled="studyLoading" @click="loadStudy">
|
||||
{{ loggedIn ? studyLoading ? '检索中' : '生成预习' : '登录查看' }}
|
||||
</button>
|
||||
</view>
|
||||
<text v-if="studyAnswer" class="study-answer">{{ studyAnswer }}</text>
|
||||
<view v-if="studyRefs.length" class="study-refs">
|
||||
<text v-for="item in studyRefs" :key="item" class="study-ref">{{ item }}</text>
|
||||
</view>
|
||||
<view v-if="!studyAnswer" class="empty">{{ loggedIn ? '点击生成预习,从正式知识库生成要点' : '登录后从正式知识库生成预习要点' }}</view>
|
||||
</view>
|
||||
|
||||
<view v-if="loggedIn" class="panel">
|
||||
<view class="section-head">
|
||||
<text class="panel-title">补充资料</text>
|
||||
<button class="primary-button upload-button" :disabled="uploading" @click="uploadMaterial">
|
||||
{{ uploading ? '上传中' : '上传' }}
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="materials.length" class="material-list">
|
||||
<view v-for="item in materials" :key="item.id" class="material-row">
|
||||
<view class="material-copy">
|
||||
<text class="material-title">{{ item.fileName }}</text>
|
||||
<text class="material-desc">OSS {{ item.ossId }} · {{ item.materialType }} · {{ formatSize(item.fileSize) }}</text>
|
||||
</view>
|
||||
<view class="status-pill" :class="statusTone(item.status)">
|
||||
{{ item.status }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty">暂无已提交资料</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<text class="panel-title">流程</text>
|
||||
<view class="step-list">
|
||||
<view v-for="step in steps" :key="step.title" class="step-row">
|
||||
<text class="step-dot">{{ step.index }}</text>
|
||||
<view>
|
||||
<text class="step-title">{{ step.title }}</text>
|
||||
<text class="step-desc">{{ step.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { onShow } from '@dcloudio/uni-app';
|
||||
import type { CandidateMaterial, InterviewFinishResponse, InterviewQuestion, InterviewRecord, PrimaryTask, RoleHome } from '@/types/api';
|
||||
import { getMobileHome } from '@/services/home';
|
||||
import { isLoggedIn, rememberLoginRedirect } from '@/services/auth';
|
||||
import { searchKnowledge } from '@/services/knowledge';
|
||||
import {
|
||||
chooseCandidateFile,
|
||||
finishCandidateInterview,
|
||||
getCandidateInterviewRecords,
|
||||
getCandidateName,
|
||||
getCandidateMaterials,
|
||||
saveInterviewAnswer,
|
||||
startCandidateInterview,
|
||||
uploadCandidateMaterial
|
||||
} from '@/services/candidate';
|
||||
|
||||
const home = ref<RoleHome | null>(null);
|
||||
const materials = ref<CandidateMaterial[]>([]);
|
||||
const interviewRecords = ref<InterviewRecord[]>([]);
|
||||
const interviewQuestions = ref<InterviewQuestion[]>([]);
|
||||
const interviewAnswers = ref<Record<string, string>>({});
|
||||
const interviewResult = ref<InterviewFinishResponse | null>(null);
|
||||
const interviewSessionId = ref('');
|
||||
const interviewStatus = ref<'idle' | 'starting' | 'active' | 'submitting' | 'finished'>('idle');
|
||||
const loggedIn = ref(false);
|
||||
const loading = ref(false);
|
||||
const uploading = ref(false);
|
||||
const studyLoading = ref(false);
|
||||
const studyAnswer = ref('');
|
||||
const studyRefs = ref<string[]>([]);
|
||||
const message = ref('');
|
||||
const interviewBusy = computed(() => interviewStatus.value === 'starting' || interviewStatus.value === 'submitting');
|
||||
const latestMaterialStatus = computed(() => materials.value[0]?.status || '');
|
||||
const candidateSubtitle = computed(() => loggedIn.value ? `尾号${getCandidateName().slice(-4)},面试、补资料、等通知` : '面试、补资料、等通知');
|
||||
const hasInterview = computed(() => interviewRecords.value.length > 0 || interviewStatus.value === 'finished');
|
||||
const needsMaterialUpload = computed(() =>
|
||||
loggedIn.value && hasInterview.value && (!materials.value.length || latestMaterialStatus.value === '已驳回')
|
||||
);
|
||||
const primaryBusy = computed(() => loading.value || interviewBusy.value || (needsMaterialUpload.value && uploading.value));
|
||||
const defaultCandidateTask: PrimaryTask = {
|
||||
badge: '待面试',
|
||||
title: '结构化面试',
|
||||
desc: '按岗位题目完成回答,提交后生成面试记录。',
|
||||
progress: 0,
|
||||
progressText: '0/2',
|
||||
cta: '开始面试'
|
||||
};
|
||||
const candidatePrimary = computed<PrimaryTask | null>(() => {
|
||||
if (!loggedIn.value) return null;
|
||||
const base = home.value?.primary || defaultCandidateTask;
|
||||
const materialStatus = latestMaterialStatus.value;
|
||||
if (!hasInterview.value) return { ...base, badge: '待面试', progress: 0, progressText: '0/2', cta: '开始面试' };
|
||||
if (materialStatus === '已通过') return { ...base, badge: '资料已通过', title: '等待后续通知', desc: '面试和补充资料已完成,留意 HR 通知。', progress: 100, progressText: '2/2', cta: '刷新进度' };
|
||||
if (materialStatus === '已驳回') return { ...base, badge: '资料被驳回', title: '重新提交资料', desc: '请按驳回结果补充或替换资料。', progress: 50, progressText: '1/2', cta: '重新上传' };
|
||||
if (materials.value.length) return { ...base, badge: '审核中', title: '资料审核中', desc: '资料已提交,等待 HR 审核。', progress: 75, progressText: '1/2', cta: '刷新进度' };
|
||||
return { ...base, badge: '待补资料', title: '上传补充资料', desc: '面试已完成,请补充简历、证书或图片资料。', progress: 50, progressText: '1/2', cta: '上传资料' };
|
||||
});
|
||||
const progressNotice = computed(() => {
|
||||
if (!loggedIn.value) return '登录后查看面试和资料进度';
|
||||
if (!hasInterview.value) return '待完成结构化面试';
|
||||
if (!materials.value.length) return '面试已完成,请上传补充资料';
|
||||
if (latestMaterialStatus.value === '已通过') return '资料已通过,等待后续通知';
|
||||
if (latestMaterialStatus.value === '已驳回') return '资料被驳回,请重新上传';
|
||||
return '资料已提交,等待 HR 审核';
|
||||
});
|
||||
const steps = [
|
||||
{ index: 1, title: '完成面试', desc: '按岗位题目回答,生成面试记录' },
|
||||
{ index: 2, title: '补充资料', desc: '上传简历、证书或图片资料' },
|
||||
{ index: 3, title: '等待审核', desc: 'HR 审核后同步显示状态' }
|
||||
];
|
||||
|
||||
const clearCandidatePrivateData = () => {
|
||||
materials.value = [];
|
||||
interviewRecords.value = [];
|
||||
interviewQuestions.value = [];
|
||||
interviewAnswers.value = {};
|
||||
interviewResult.value = null;
|
||||
interviewSessionId.value = '';
|
||||
interviewStatus.value = 'idle';
|
||||
studyAnswer.value = '';
|
||||
studyRefs.value = [];
|
||||
};
|
||||
|
||||
const clearInterviewDraft = () => {
|
||||
interviewQuestions.value = [];
|
||||
interviewAnswers.value = {};
|
||||
interviewResult.value = null;
|
||||
interviewSessionId.value = '';
|
||||
};
|
||||
|
||||
const refreshAuthState = () => {
|
||||
loggedIn.value = isLoggedIn();
|
||||
};
|
||||
|
||||
const handleRequestError = (error: unknown, fallback: string, wasLoggedIn = loggedIn.value) => {
|
||||
refreshAuthState();
|
||||
if (wasLoggedIn && !loggedIn.value) {
|
||||
clearCandidatePrivateData();
|
||||
message.value = '登录已过期,请重新登录';
|
||||
return true;
|
||||
}
|
||||
message.value = error instanceof Error ? error.message : fallback;
|
||||
return false;
|
||||
};
|
||||
|
||||
const loadPage = async () => {
|
||||
refreshAuthState();
|
||||
const wasLoggedIn = loggedIn.value;
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
home.value = await getMobileHome('candidate');
|
||||
} catch (error) {
|
||||
home.value = null;
|
||||
}
|
||||
try {
|
||||
if (!loggedIn.value) {
|
||||
clearCandidatePrivateData();
|
||||
return;
|
||||
}
|
||||
const [nextMaterials, nextRecords] = await Promise.all([
|
||||
getCandidateMaterials(),
|
||||
getCandidateInterviewRecords()
|
||||
]);
|
||||
materials.value = nextMaterials;
|
||||
interviewRecords.value = nextRecords;
|
||||
} catch (error) {
|
||||
handleRequestError(error, '候选人数据加载失败', wasLoggedIn);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const uploadMaterial = async () => {
|
||||
if (uploading.value) return;
|
||||
if (!loggedIn.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
uploading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const file = await chooseCandidateFile();
|
||||
const saved = await uploadCandidateMaterial(file);
|
||||
message.value = `资料已上传:#${saved.id} / OSS ${saved.ossId}`;
|
||||
materials.value = await getCandidateMaterials();
|
||||
} catch (error) {
|
||||
handleRequestError(error, '资料上传失败', true);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadStudy = async () => {
|
||||
if (studyLoading.value) return;
|
||||
if (!loggedIn.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
studyLoading.value = true;
|
||||
message.value = '';
|
||||
try {
|
||||
const result = await searchKnowledge('生活顾问岗前预习 投诉处理 报事报修 客户沟通');
|
||||
studyAnswer.value = result.answer;
|
||||
studyRefs.value = (result.docs || []).map((item) => item.title).filter(Boolean).slice(0, 3);
|
||||
} catch (error) {
|
||||
handleRequestError(error, '岗前预习生成失败', true);
|
||||
} finally {
|
||||
studyLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startInterview = async () => {
|
||||
if (interviewBusy.value) return;
|
||||
if (!loggedIn.value) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
interviewStatus.value = 'starting';
|
||||
clearInterviewDraft();
|
||||
message.value = '';
|
||||
try {
|
||||
const data = await startCandidateInterview();
|
||||
interviewSessionId.value = data.sessionId;
|
||||
interviewQuestions.value = data.questions || [];
|
||||
interviewAnswers.value = Object.fromEntries(interviewQuestions.value.map((item) => [item.questionId, '']));
|
||||
interviewStatus.value = 'active';
|
||||
message.value = `已生成 ${interviewQuestions.value.length} 道题`;
|
||||
} catch (error) {
|
||||
interviewStatus.value = 'idle';
|
||||
handleRequestError(error, '面试启动失败', true);
|
||||
}
|
||||
};
|
||||
|
||||
const runPrimaryAction = () => {
|
||||
if (needsMaterialUpload.value) {
|
||||
void uploadMaterial();
|
||||
return;
|
||||
}
|
||||
if (hasInterview.value && materials.value.length) {
|
||||
void loadPage();
|
||||
return;
|
||||
}
|
||||
void startInterview();
|
||||
};
|
||||
|
||||
const finishInterview = async () => {
|
||||
if (interviewBusy.value) return;
|
||||
if (interviewStatus.value !== 'active' || !interviewSessionId.value) {
|
||||
message.value = '请先开始面试';
|
||||
return;
|
||||
}
|
||||
const answers = Object.fromEntries(
|
||||
Object.entries(interviewAnswers.value).map(([key, value]) => [key, value.trim()])
|
||||
);
|
||||
if (interviewQuestions.value.some((item) => !answers[item.questionId])) {
|
||||
message.value = `请填写全部 ${interviewQuestions.value.length} 题回答`;
|
||||
return;
|
||||
}
|
||||
interviewStatus.value = 'submitting';
|
||||
message.value = '';
|
||||
try {
|
||||
await Promise.all(Object.entries(answers).filter(([, value]) => value).map(([questionId, answer]) =>
|
||||
saveInterviewAnswer(interviewSessionId.value, questionId, answer)
|
||||
));
|
||||
interviewResult.value = await finishCandidateInterview(interviewSessionId.value, answers);
|
||||
interviewRecords.value = interviewResult.value.records?.length
|
||||
? interviewResult.value.records
|
||||
: await getCandidateInterviewRecords();
|
||||
interviewStatus.value = 'finished';
|
||||
message.value = '面试完成,已生成面试记录';
|
||||
} catch (error) {
|
||||
if (!handleRequestError(error, '面试提交失败', true)) {
|
||||
interviewStatus.value = 'active';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = (size: number) => {
|
||||
if (!size) return '0KB';
|
||||
if (size < 1024 * 1024) return `${Math.ceil(size / 1024)}KB`;
|
||||
return `${(size / 1024 / 1024).toFixed(1)}MB`;
|
||||
};
|
||||
|
||||
const statusTone = (status: string) => {
|
||||
if (status === '已通过') return 'passed';
|
||||
if (status === '已驳回') return 'rejected';
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
rememberLoginRedirect('/pages/candidate/index/index');
|
||||
uni.navigateTo({ url: '/pages/auth/login/index' });
|
||||
};
|
||||
|
||||
onShow(loadPage);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.candidate-page {
|
||||
min-height: 100vh;
|
||||
background: #f4f7fb;
|
||||
}
|
||||
|
||||
.page-head,
|
||||
.section-head,
|
||||
.login-panel,
|
||||
.interview-row,
|
||||
.material-row,
|
||||
.step-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.refresh-button,
|
||||
.login-button,
|
||||
.upload-button,
|
||||
.study-button {
|
||||
min-width: 84px;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.task-panel {
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.task-badge {
|
||||
display: inline-flex;
|
||||
padding: 5px 9px;
|
||||
border-radius: 999px;
|
||||
background: #fed7aa;
|
||||
color: #9a3412;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
margin: 16px 0 10px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background: #ffedd5;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: #ea580c;
|
||||
}
|
||||
|
||||
.progress-panel {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.progress-notice {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: #1e3a8a;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.interview-start,
|
||||
.finish-button {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.question-list,
|
||||
.dimension-list {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
border-radius: 14px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.question-meta {
|
||||
color: #ea580c;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.question-title {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #0f172a;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.question-point {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.answer-textarea {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.result-score {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.result-desc {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.dimension-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.dimension-title {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dimension-note {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dimension-value {
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.interview-list {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.interview-row {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.interview-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.interview-title {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.interview-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.interview-score {
|
||||
display: flex;
|
||||
min-width: 64px;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
color: #0f172a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.interview-score text:first-child {
|
||||
color: #ea580c;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.study-answer {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
color: #0f172a;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.study-refs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.study-ref {
|
||||
padding: 5px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.material-list,
|
||||
.step-list {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.material-row {
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.material-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.material-title,
|
||||
.step-title {
|
||||
display: block;
|
||||
color: #0f172a;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.material-desc,
|
||||
.step-desc {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
flex: 0 0 auto;
|
||||
padding: 6px 9px;
|
||||
border-radius: 999px;
|
||||
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;
|
||||
}
|
||||
|
||||
.step-row {
|
||||
justify-content: flex-start;
|
||||
margin-top: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.step-dot {
|
||||
display: flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex: 0 0 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: #e0f2fe;
|
||||
color: #0369a1;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user