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
+10 -5
View File
@@ -283,12 +283,14 @@ import {
} from '@/api/aihr/processing';
import { uploadKnowledgeDocAsync, listUploadItems, retryUploadItem, type UploadItem } from '@/api/aihr/sop';
const acceptTypes = '.txt,.md,.markdown,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.webp,.bmp,.mp4,.mov,.avi,.mkv,.webm,.m4v';
const acceptTypes = '.txt,.md,.markdown,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.webp,.bmp,.mp4,.mov,.avi,.mkv,.webm,.m4v,.zip';
const autoCategory = '__auto__';
const supportedPattern = /\.(txt|md|markdown|pdf|doc|docx|xls|xlsx|ppt|pptx|jpg|jpeg|png|gif|webp|bmp|mp4|mov|avi|mkv|webm|m4v)$/i;
const supportedPattern = /\.(txt|md|markdown|pdf|doc|docx|xls|xlsx|ppt|pptx|jpg|jpeg|png|gif|webp|bmp|mp4|mov|avi|mkv|webm|m4v|zip)$/i;
const videoPattern = /\.(mp4|mov|avi|mkv|webm|m4v)$/i;
const archivePattern = /\.zip$/i;
const maxFileSize = 100 * 1024 * 1024;
const maxVideoSize = 500 * 1024 * 1024;
const maxArchiveSize = 500 * 1024 * 1024;
const browserBatchWarnCount = 20;
const browserBatchWarnSize = 200 * 1024 * 1024;
@@ -484,14 +486,17 @@ async function handlePickedFiles(event: Event) {
async function uploadFiles(files: File[]) {
const readyFiles = files.filter(
(file) => supportedPattern.test(file.name) && file.size > 0 && file.size <= (videoPattern.test(file.name) ? maxVideoSize : maxFileSize)
(file) =>
supportedPattern.test(file.name) &&
file.size > 0 &&
file.size <= (videoPattern.test(file.name) ? maxVideoSize : archivePattern.test(file.name) ? maxArchiveSize : maxFileSize)
);
if (readyFiles.length === 0) {
ElMessage.warning('仅支持非空的 txt/md/pdf/Word/Excel/PPT/图片(100MB内)或视频(500MB内)文件');
ElMessage.warning('仅支持非空的 txt/md/pdf/Word/Excel/PPT/图片(100MB内)、视频或 ZIP(500MB内)文件');
return;
}
if (readyFiles.length < files.length) {
ElMessage.warning(`已跳过 ${files.length - readyFiles.length} 个不支持、空文件或超限文件(文档/图片 100MB、视频 500MB)`);
ElMessage.warning(`已跳过 ${files.length - readyFiles.length} 个不支持、空文件或超限文件(文档/图片 100MB、视频/ZIP 500MB)`);
}
const totalSize = readyFiles.reduce((sum, file) => sum + file.size, 0);
+131 -2
View File
@@ -89,6 +89,50 @@
</article>
</section>
<section class="panel pending-review-panel">
<div class="panel-title with-actions">
<div class="panel-title-main">
<el-icon><DocumentChecked /></el-icon>
<div>
<h2>答案待复核</h2>
<p>员工反馈为「没解决」的答案会进入这里,复核后从待办队列移除。</p>
</div>
</div>
<div class="review-toolbar">
<el-select v-model="pendingReviewStatus" size="small" @change="loadPendingReviews">
<el-option label="待复核" value="待复核" />
<el-option label="已复核" value="已复核" />
</el-select>
<el-button size="small" :loading="pendingReviewLoading" @click="loadPendingReviews">刷新</el-button>
</div>
</div>
<el-table v-loading="pendingReviewLoading" :data="pendingReviews" size="small" class="record-table">
<el-table-column prop="queryText" label="员工问题" min-width="260" />
<el-table-column prop="verdict" label="反馈" width="90">
<template #default="{ row }">
<span class="review-verdict negative">{{ row.verdict === 'down' ? '没解决' : row.verdict }}</span>
</template>
</el-table-column>
<el-table-column prop="position" label="岗位" width="120" />
<el-table-column prop="createTime" label="提交时间" width="170" />
<el-table-column label="操作" width="110" fixed="right">
<template #default="{ row }">
<el-button
v-if="row.reviewStatus === '待复核'"
link
type="danger"
:loading="reviewingFeedbackId === row.id"
@click="markFeedbackReviewed(row)"
>
标记已复核
</el-button>
<span v-else class="review-done">已复核</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!pendingReviewLoading && !pendingReviews.length" description="暂无待复核答案" />
</section>
<section class="panel record-panel">
<div class="panel-title">
<el-icon><Tickets /></el-icon>
@@ -110,9 +154,18 @@
import { DocumentChecked, Reading, Tickets, Upload } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import type { UploadFile } from 'element-plus';
import { computed, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { reviewKnowledgeSop, searchKnowledgeSop, uploadKnowledgeDoc, type SopRecord, type SopSearchResponse } from '@/api/aihr/sop';
import {
listAnswerFeedback,
reviewAnswerFeedback,
reviewKnowledgeSop,
searchKnowledgeSop,
uploadKnowledgeDoc,
type AnswerFeedbackItem,
type SopRecord,
type SopSearchResponse
} from '@/api/aihr/sop';
const router = useRouter();
@@ -212,6 +265,10 @@ const uploading = ref(false);
const uploadedRecords = ref<SopRecord[]>([]);
const reviewSaving = ref(false);
const reviewStatus = ref('');
const pendingReviewStatus = ref('待复核');
const pendingReviewLoading = ref(false);
const reviewingFeedbackId = ref<number | null>(null);
const pendingReviews = ref<AnswerFeedbackItem[]>([]);
const activeSeed = computed(() => sopSeeds[selectedCategory.value]);
const answerText = computed(() => apiResult.value?.answer ?? activeSeed.value.answer);
@@ -318,6 +375,32 @@ async function markReview(usable: boolean) {
}
}
async function loadPendingReviews() {
pendingReviewLoading.value = true;
try {
const { data } = await listAnswerFeedback(pendingReviewStatus.value);
pendingReviews.value = data ?? [];
} catch {
pendingReviews.value = [];
ElMessage.error('答案待复核列表加载失败');
} finally {
pendingReviewLoading.value = false;
}
}
async function markFeedbackReviewed(row: AnswerFeedbackItem) {
reviewingFeedbackId.value = row.id;
try {
await reviewAnswerFeedback(row.id);
pendingReviews.value = pendingReviews.value.filter((item) => item.id !== row.id);
ElMessage.success('答案已标记为已复核');
} catch {
ElMessage.error('答案复核失败');
} finally {
reviewingFeedbackId.value = null;
}
}
async function handleDocSelected(uploadFile: UploadFile) {
const raw = uploadFile.raw;
if (!raw) return;
@@ -359,6 +442,10 @@ async function generateTraining() {
trainingGenerated.value = true;
ElMessage.success('已基于引用片段生成训练题');
}
onMounted(() => {
loadPendingReviews();
});
</script>
<style lang="scss" scoped>
@@ -425,6 +512,48 @@ async function generateTraining() {
}
}
.panel-title.with-actions {
justify-content: space-between;
}
.panel-title-main,
.review-toolbar {
display: flex;
align-items: center;
}
.panel-title-main {
gap: 8px;
p {
margin: 4px 0 0;
color: #657184;
font-size: 12px;
font-weight: 400;
}
}
.review-toolbar {
gap: 8px;
}
.pending-review-panel {
margin-top: 12px;
}
.review-verdict,
.review-done {
font-size: 12px;
}
.review-verdict.negative {
color: #c80f1f;
}
.review-done {
color: #15844a;
}
.category-list {
display: grid;
gap: 8px;