fix(aihr): validate pilot export date window
This commit is contained in:
@@ -13,4 +13,10 @@ describe('resolvePilotDateWindow', () => {
|
|||||||
endDate: '2026-07-10'
|
endDate: '2026-07-10'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects malformed or reversed date windows', () => {
|
||||||
|
expect(resolvePilotDateWindow(['2026-7-07', '2026-07-10'])).toBeNull();
|
||||||
|
expect(resolvePilotDateWindow(['2026-02-30', '2026-03-01'])).toBeNull();
|
||||||
|
expect(resolvePilotDateWindow(['2026-07-10', '2026-07-07'])).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,12 +3,24 @@ export type PilotDateWindow = {
|
|||||||
endDate: string;
|
endDate: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isIsoDate = (value: string) => {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [year, month, day] = value.split('-').map(Number);
|
||||||
|
if (year < 1000 || month < 1 || month > 12 || day < 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const date = new Date(Date.UTC(year, month - 1, day));
|
||||||
|
return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
|
||||||
|
};
|
||||||
|
|
||||||
export function resolvePilotDateWindow(value: unknown): PilotDateWindow | null {
|
export function resolvePilotDateWindow(value: unknown): PilotDateWindow | null {
|
||||||
if (!Array.isArray(value) || value.length !== 2) {
|
if (!Array.isArray(value) || value.length !== 2) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const [startDate, endDate] = value;
|
const [startDate, endDate] = value;
|
||||||
if (typeof startDate !== 'string' || typeof endDate !== 'string' || !startDate || !endDate) {
|
if (typeof startDate !== 'string' || typeof endDate !== 'string' || !isIsoDate(startDate) || !isIsoDate(endDate) || startDate > endDate) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { startDate, endDate };
|
return { startDate, endDate };
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ import { Refresh } from '@element-plus/icons-vue';
|
|||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
||||||
import { calibratePracticeSession, exportPracticePilotCsv, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
import { calibratePracticeSession, exportPracticePilotCsv, listPracticeScenarios, type PracticeScenarioResponse } from '@/api/aihr/practice';
|
||||||
|
import { resolvePilotDateWindow } from './pilot-export';
|
||||||
import {
|
import {
|
||||||
getPracticeAudioBlobUrl,
|
getPracticeAudioBlobUrl,
|
||||||
getOrgSnapshot,
|
getOrgSnapshot,
|
||||||
@@ -174,6 +175,7 @@ const metrics = reactive({ calibrationCount: 0, consistency: '--', sopUsable: '-
|
|||||||
const audioSources = ref<Record<string, string>>({});
|
const audioSources = ref<Record<string, string>>({});
|
||||||
const audioLoading = ref<Record<string, boolean>>({});
|
const audioLoading = ref<Record<string, boolean>>({});
|
||||||
const audioErrors = ref<Record<string, string>>({});
|
const audioErrors = ref<Record<string, string>>({});
|
||||||
|
const pilotDateWindow = computed(() => resolvePilotDateWindow([startDate.value, endDate.value]));
|
||||||
|
|
||||||
const reviewForm = reactive({
|
const reviewForm = reactive({
|
||||||
reviewer: '主管',
|
reviewer: '主管',
|
||||||
@@ -258,8 +260,16 @@ const loadReviews = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadMetrics = async () => {
|
const loadMetrics = async () => {
|
||||||
|
const dateWindow = pilotDateWindow.value;
|
||||||
|
if (!dateWindow) {
|
||||||
|
metrics.calibrationCount = 0;
|
||||||
|
metrics.consistency = '--';
|
||||||
|
metrics.sopUsable = '--';
|
||||||
|
metrics.ready = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value });
|
const blob = await exportPracticePilotCsv(dateWindow);
|
||||||
const csv = await blob.text();
|
const csv = await blob.text();
|
||||||
const value = (key: string) => csv.match(new RegExp(`(?:^|\\n)"?${key}"?,"?([^"\\n]*)`))?.[1] || '';
|
const value = (key: string) => csv.match(new RegExp(`(?:^|\\n)"?${key}"?,"?([^"\\n]*)`))?.[1] || '';
|
||||||
metrics.calibrationCount = Number(value('calibration_sample_count')) || 0;
|
metrics.calibrationCount = Number(value('calibration_sample_count')) || 0;
|
||||||
@@ -275,13 +285,18 @@ const loadMetrics = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exportPilot = async () => {
|
const exportPilot = async () => {
|
||||||
|
const dateWindow = pilotDateWindow.value;
|
||||||
|
if (!dateWindow) {
|
||||||
|
ElMessage.warning('请选择有效的试点起止日期');
|
||||||
|
return;
|
||||||
|
}
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
try {
|
try {
|
||||||
const blob = await exportPracticePilotCsv({ startDate: startDate.value, endDate: endDate.value });
|
const blob = await exportPracticePilotCsv(dateWindow);
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `aihr-pilot-${startDate.value}-${endDate.value}.csv`;
|
link.download = `aihr-pilot-${dateWindow.startDate}-${dateWindow.endDate}.csv`;
|
||||||
link.click();
|
link.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
ElMessage.success('试点 CSV 已导出');
|
ElMessage.success('试点 CSV 已导出');
|
||||||
|
|||||||
Reference in New Issue
Block a user