feat(aihr): align figma pages for release
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user