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
+126 -5
View File
@@ -6,10 +6,37 @@
<p>集中处理主管待复盘、逐句标注、复盘建议、学分奖励和下一次训练派发。</p>
</div>
<div class="head-actions">
<el-button type="danger" :loading="exporting" @click="exportPilot">导出试点 CSV</el-button>
<el-button :icon="Refresh" :loading="loading" @click="loadReviews">刷新</el-button>
</div>
</section>
<section class="filter-panel panel">
<el-date-picker v-model="startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
<el-date-picker v-model="endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
<el-select v-model="selectedPosition" placeholder="岗位">
<el-option label="生活顾问" value="生活顾问" />
</el-select>
<el-select v-model="selectedProjectCode" clearable filterable placeholder="组织范围">
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
</el-select>
</section>
<section class="metric-grid">
<div class="metric-card">
<span>校准样本</span><strong>{{ metrics.calibrationCount }} / 20</strong>
</div>
<div class="metric-card">
<span>分档一致率</span><strong>{{ metrics.consistency }}</strong>
</div>
<div class="metric-card">
<span>SOP 可用率</span><strong>{{ metrics.sopUsable }}</strong>
</div>
<div class="metric-card">
<span>严格门禁</span><strong :class="metrics.ready ? 'is-ready' : 'is-pending'">{{ metrics.ready ? '通过' : '待补样本' }}</strong>
</div>
</section>
<section class="review-layout">
<article class="panel list-panel">
<div class="panel-title">
@@ -17,9 +44,6 @@
<el-tag type="danger">{{ visibleReviews.length }} 条</el-tag>
</div>
<div class="list-filters">
<el-select v-model="selectedProjectCode" clearable filterable placeholder="项目范围">
<el-option v-for="item in orgSnapshot?.projects || []" :key="item.projectCode" :label="item.projectName" :value="item.projectCode" />
</el-select>
<el-select v-model="selectedExtPartyId" clearable filterable placeholder="组织人员">
<el-option
v-for="person in orgPeople"
@@ -122,7 +146,7 @@
import { Refresh } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { calibratePracticeSession, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import { calibratePracticeSession, exportPracticePilotCsv, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
import {
getPracticeAudioBlobUrl,
getOrgSnapshot,
@@ -142,6 +166,11 @@ const scenarios = ref<PracticeScenarioResponse[]>([]);
const orgSnapshot = ref<OrgSnapshotResponse | null>(null);
const selectedProjectCode = ref('');
const selectedExtPartyId = ref('');
const selectedPosition = ref('生活顾问');
const exporting = ref(false);
const startDate = ref('2026-07-01');
const endDate = ref('2026-07-31');
const metrics = reactive({ calibrationCount: 0, consistency: '--', sopUsable: '--', ready: false });
const audioSources = ref<Record<string, string>>({});
const audioLoading = ref<Record<string, boolean>>({});
const audioErrors = ref<Record<string, string>>({});
@@ -228,6 +257,41 @@ const loadReviews = async () => {
}
};
const loadMetrics = async () => {
try {
const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value });
const csv = await blob.text();
const value = (key: string) => csv.match(new RegExp(`(?:^|\\n)"?${key}"?,"?([^"\\n]*)`))?.[1] || '';
metrics.calibrationCount = Number(value('calibration_sample_count')) || 0;
metrics.consistency = value('ai_human_band_consistency_rate') || '--';
metrics.sopUsable = value('sop_answer_usable_rate') || '--';
metrics.ready = value('pilot_metric_ready') === 'true';
} catch {
metrics.calibrationCount = reviews.value.length;
metrics.consistency = '--';
metrics.sopUsable = '--';
metrics.ready = false;
}
};
const exportPilot = async () => {
exporting.value = true;
try {
const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `aihr-pilot-${startDate.value}-${endDate.value}.csv`;
link.click();
URL.revokeObjectURL(url);
ElMessage.success('试点 CSV 已导出');
} catch {
ElMessage.error('试点 CSV 导出失败');
} finally {
exporting.value = false;
}
};
const selectReview = async (row: ReviewRecord) => {
releaseAudioSources();
const result = await getPracticeReview(row.id);
@@ -290,10 +354,15 @@ watch(selectedExtPartyId, () => {
loadReviews();
});
watch([startDate, endDate], () => {
loadMetrics();
});
onMounted(() => {
loadScenarios();
loadOrgSnapshot();
loadReviews();
loadMetrics();
});
onBeforeUnmount(releaseAudioSources);
@@ -332,6 +401,51 @@ onBeforeUnmount(releaseAudioSources);
}
}
.head-actions {
display: flex;
align-items: center;
gap: 10px;
}
.filter-panel {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 14px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 14px;
margin-top: 14px;
}
.metric-card {
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #fff;
padding: 16px;
span {
display: block;
color: #667085;
font-size: 13px;
}
strong {
display: block;
margin-top: 8px;
color: #16864b;
font-size: 24px;
}
.is-pending {
color: #d97706;
font-size: 18px;
}
}
.review-layout {
display: grid;
grid-template-columns: minmax(420px, 0.9fr) minmax(0, 1.4fr);
@@ -445,8 +559,15 @@ onBeforeUnmount(releaseAudioSources);
.review-layout,
.list-filters,
.score-grid,
.form-grid {
.form-grid,
.filter-panel,
.metric-grid {
grid-template-columns: 1fr;
}
.head-actions {
align-items: stretch;
flex-direction: column;
}
}
</style>