Files
anxinyan/admin-web/src/pages/appraisal-tasks/index.vue
2026-05-22 13:31:02 +08:00

2683 lines
91 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
adminApi,
type AdminFileAsset,
type AdminAppraisalTaskDetail,
type AdminAppraisalTaskListItem,
type AdminAssignableAppraiserItem,
type AdminCategoryItem,
} from "../../api/admin";
import OrderStatusTag from "../../components/OrderStatusTag.vue";
import { getAdminInfo } from "../../utils/auth";
const loading = ref(false);
const detailLoading = ref(false);
const drawerVisible = ref(false);
const resultSubmitting = ref(false);
const supplementSubmitting = ref(false);
const assigneeDialogVisible = ref(false);
const assigneeOptionsLoading = ref(false);
const assigneeSubmitting = ref(false);
const materialTagBinding = ref(false);
const assigneeOptions = ref<AdminAssignableAppraiserItem[]>([]);
const selectedAssigneeId = ref(0);
const evidenceUploading = ref(false);
const appraisalTemplateLoading = ref(false);
const transferTagNo = ref("");
const transferScanLoading = ref(false);
const zhongjianReportNo = ref("");
const zhongjianReportFiles = ref<AdminFileAsset[]>([]);
const zhongjianReportUploading = ref(false);
const zhongjianReportSubmitting = ref(false);
const keyword = ref("");
const taskStage = ref("");
const status = ref("");
const serviceProvider = ref("");
const detailTab = ref("overview");
const activeWorkTab = ref("result");
const formRenderKey = ref(0);
const workbenchAsideRef = ref<HTMLElement | null>(null);
const evidenceInputRef = ref<HTMLInputElement | null>(null);
const zhongjianReportFileInputRef = ref<HTMLInputElement | null>(null);
const materialTagInput = ref("");
const tasks = ref<AdminAppraisalTaskListItem[]>([]);
const categories = ref<AdminCategoryItem[]>([]);
const detail = ref<AdminAppraisalTaskDetail | null>(null);
const currentAppraisalTemplate = ref<AdminAppraisalTaskDetail["appraisal_template"]>(null);
const resultAttachments = ref<
Array<{
file_id: string;
file_url: string;
thumbnail_url: string;
name?: string;
file_type?: string;
mime_type?: string;
}>
>([]);
const resultKeyPoints = ref<
Array<{
point_code: string;
point_name: string;
point_type: "text" | "textarea" | "select" | "boolean";
options: string[];
sort_order: number;
is_required: boolean;
point_value: string;
point_remark: string;
}>
>([]);
const resultForm = reactive({
result_text: "",
result_desc: "",
condition_grade: "",
condition_desc: "",
valuation_min: 0,
valuation_max: 0,
valuation_desc: "",
external_remark: "",
internal_remark: "",
});
const productForm = reactive({
category_id: 0,
product_name: "",
category_name: "",
brand_name: "",
color: "",
size_spec: "",
serial_no: "",
});
const supplementForm = reactive({
reason: "",
deadline: "",
items: [{ item_name: "", guide_text: "", is_required: true }],
});
const stageOptions = [
{ label: "全部阶段", value: "" },
{ label: "鉴定", value: "first_review" },
];
const statusOptions = [
{ label: "全部状态", value: "" },
{ label: "待处理", value: "pending" },
{ label: "处理中", value: "processing" },
{ label: "待用户补料", value: "returned" },
{ label: "已提交", value: "submitted" },
{ label: "已完成", value: "completed" },
];
const providerOptions = [
{ label: "全部服务", value: "" },
{ label: "实物鉴定", value: "anxinyan" },
{ label: "中检鉴定", value: "zhongjian" },
];
const detailTabOptions = [
{ label: "任务信息", value: "overview" },
{ label: "资料与轨迹", value: "materials" },
{ label: "处理工作台", value: "actions" },
];
const usageStatusMap: Record<string, string> = {
new: "全新未使用",
light_use: "轻微使用痕迹",
used: "长期使用",
};
const supplementStatusMap: Record<string, string> = {
pending: "待处理",
closed: "已关闭",
completed: "已完成",
};
const materialStatusMap: Record<string, string> = {
pending: "待补充",
uploaded: "已上传",
optional: "选填",
completed: "已完成",
};
const usageStatusText = computed(() => {
const value = detail.value?.extra_info.usage_status || "";
return value ? usageStatusMap[value] || value : "-";
});
const productTitle = computed(() => {
if (!detail.value) {
return "待完善物品信息";
}
return detail.value.product_info.product_name || "待完善物品信息";
});
const productMetaText = computed(() => {
if (!detail.value) {
return "物品信息待完善";
}
const parts = [
detail.value.product_info.category_name,
detail.value.product_info.brand_name,
].filter(Boolean);
return parts.length ? parts.join(" / ") : "物品信息待完善";
});
type ResultFormSeed = {
result_text: string;
result_desc: string;
condition_grade: string;
condition_desc?: string;
valuation_min: number;
valuation_max: number;
valuation_desc?: string;
attachments: Array<{
file_id: string;
file_url: string;
thumbnail_url: string;
name?: string;
file_type?: string;
mime_type?: string;
}>;
external_remark: string;
internal_remark: string;
key_points: Array<{
point_code: string;
point_name: string;
point_value: string;
point_remark: string;
}>;
};
function hasResultSeedValue(result: ResultFormSeed) {
return Boolean(
result.result_text ||
result.result_desc ||
result.condition_grade ||
result.condition_desc ||
result.valuation_min ||
result.valuation_max ||
result.valuation_desc ||
result.attachments?.length ||
result.key_points?.some((item) => item.point_value || item.point_remark) ||
result.external_remark ||
result.internal_remark,
);
}
function resolveResultFormSeed(data: AdminAppraisalTaskDetail): ResultFormSeed {
if (!hasResultSeedValue(data.result_info) && data.prefill_result_info) {
return data.prefill_result_info;
}
return data.result_info;
}
const currentResultLabel = computed(() => {
if (!detail.value) {
return "暂未填写";
}
if (detail.value.result_info.result_text) {
return detail.value.result_info.result_text;
}
const latestStageResult = [...detail.value.stage_tasks]
.sort((a, b) => {
const aRank = a.task_stage === "first_review" ? 2 : 1;
const bRank = b.task_stage === "first_review" ? 2 : 1;
return bRank - aRank;
})
.find((item) => item.result_text);
return latestStageResult?.result_text || "暂未填写";
});
const reviewPrefillHint = computed(() => {
if (!detail.value) {
return "";
}
return !hasResultSeedValue(detail.value.result_info) && detail.value.prefill_result_info
? `已默认带入${detail.value.prefill_result_info.source_stage_text}结论,可只修改存在差异的字段。`
: "";
});
const supplementStatusText = computed(() => {
if (!detail.value?.supplement_task) {
return "当前暂无待处理补资料任务";
}
const task = detail.value.supplement_task;
return `${supplementStatusMap[task.status] || task.status} · #${task.id}${task.deadline ? ` · 截止 ${task.deadline}` : ""}`;
});
const isTaskReadonly = computed(() => {
if (!detail.value) {
return false;
}
return (
["submitted", "completed"].includes(detail.value.task_info.status) ||
(Boolean(detail.value.task_info.submitted_at) && Boolean(detail.value.report_summary))
);
});
const reportDraftHint = computed(() => {
if (!detail.value?.report_summary) {
return "";
}
return `已生成报告草稿:${detail.value.report_summary.report_no} · ${detail.value.report_summary.report_status_text}`;
});
const canBindMaterialTag = computed(() => {
if (!detail.value?.report_summary) {
return false;
}
return detail.value.report_summary.report_status !== "published" && !detail.value.material_tag;
});
const isZhongjianTask = computed(() => detail.value?.task_info.service_provider === "zhongjian");
const canRequestSupplement = computed(() => detail.value?.task_info.status !== "completed");
const currentAdmin = computed(() => getAdminInfo());
const canClaimTask = computed(() => {
if (!detail.value || isTaskReadonly.value) {
return false;
}
const assigneeId = detail.value.task_info.assignee_id || 0;
return assigneeId <= 0 && Boolean(currentAdmin.value?.id);
});
async function fetchTasks() {
loading.value = true;
try {
const response = await adminApi.getAppraisalTasks({
keyword: keyword.value,
task_stage: taskStage.value,
status: status.value,
service_provider: serviceProvider.value,
});
tasks.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("鉴定任务列表加载失败");
} finally {
loading.value = false;
}
}
async function fetchCategories() {
try {
const response = await adminApi.getCategories();
categories.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("品类列表加载失败");
}
}
function resetSupplementForm() {
if (detail.value?.supplement_task) {
applySupplementForm(detail.value);
formRenderKey.value += 1;
return;
}
supplementForm.reason = "";
supplementForm.deadline = "";
supplementForm.items.splice(0, supplementForm.items.length, {
item_name: "",
guide_text: "",
is_required: true,
});
formRenderKey.value += 1;
}
function hydrateDetail(data: AdminAppraisalTaskDetail) {
detail.value = data;
detailTab.value = "overview";
activeWorkTab.value = data.task_info.service_provider === "zhongjian" ? "zhongjian" : (data.supplement_task ? "supplement" : "result");
applyProductForm(data.product_info);
const resultSeed = resolveResultFormSeed(data);
applyResultForm(resultSeed);
applyAppraisalTemplate(data.appraisal_template, resultSeed.key_points || []);
applyZhongjianReportForm(data);
applySupplementForm(data);
formRenderKey.value += 1;
}
function applyProductForm(data: AdminAppraisalTaskDetail["product_info"]) {
Object.assign(productForm, {
category_id: data.category_id || 0,
product_name: data.product_name || "",
category_name: data.category_name || "",
brand_name: data.brand_name || "",
color: data.color || "",
size_spec: data.size_spec || "",
serial_no: data.serial_no || "",
});
}
function applyResultForm(data: ResultFormSeed) {
Object.assign(resultForm, {
result_text: data.result_text || "",
result_desc: data.result_desc || "",
condition_grade: data.condition_grade || "",
condition_desc: data.condition_desc || "",
valuation_min: data.valuation_min || 0,
valuation_max: data.valuation_max || 0,
valuation_desc: data.valuation_desc || "",
external_remark: data.external_remark || "",
internal_remark: data.internal_remark || "",
});
resultAttachments.value = [...(data.attachments || [])];
}
function applyAppraisalTemplate(
template: AdminAppraisalTaskDetail["appraisal_template"],
savedKeyPoints: ResultFormSeed["key_points"] = [],
) {
currentAppraisalTemplate.value = template;
if (!template) {
resultKeyPoints.value = [];
return;
}
const savedMap = new Map((savedKeyPoints || []).map((item) => [item.point_code, item]));
resultKeyPoints.value = template.key_points.map((point) => {
const saved = savedMap.get(point.point_code);
return {
point_code: point.point_code,
point_name: point.point_name,
point_type: point.point_type,
options: [...(point.options || [])],
sort_order: point.sort_order,
is_required: point.is_required,
point_value: saved?.point_value ?? point.point_value ?? "",
point_remark: saved?.point_remark ?? point.point_remark ?? "",
};
});
}
function applySupplementForm(data: Pick<AdminAppraisalTaskDetail, "supplement_task">) {
if (data.supplement_task) {
supplementForm.reason = data.supplement_task.reason || "";
supplementForm.deadline = data.supplement_task.deadline || "";
supplementForm.items.splice(
0,
supplementForm.items.length,
...(data.supplement_task.items.length
? data.supplement_task.items.map((item) => ({
item_name: item.item_name,
guide_text: item.guide_text,
is_required: item.is_required,
}))
: [{ item_name: "", guide_text: "", is_required: true }]),
);
return;
}
supplementForm.reason = "";
supplementForm.deadline = "";
supplementForm.items.splice(0, supplementForm.items.length, {
item_name: "",
guide_text: "",
is_required: true,
});
}
function applyZhongjianReportForm(data: AdminAppraisalTaskDetail) {
zhongjianReportNo.value = data.zhongjian_report?.report_no || "";
zhongjianReportFiles.value = [...(data.zhongjian_report?.files || [])];
}
function resetResultForm() {
if (!detail.value) return;
applyProductForm(detail.value.product_info);
const resultSeed = resolveResultFormSeed(detail.value);
applyResultForm(resultSeed);
applyAppraisalTemplate(detail.value.appraisal_template, resultSeed.key_points || []);
formRenderKey.value += 1;
}
function resetZhongjianReportForm() {
if (!detail.value) return;
applyZhongjianReportForm(detail.value);
}
function openSupplementWorkbench() {
if (isTaskReadonly.value) {
return;
}
detailTab.value = "actions";
activeWorkTab.value = "supplement";
}
function returnToResultWorkbench() {
resetSupplementForm();
activeWorkTab.value = "result";
}
async function loadDetail(id: number) {
detailLoading.value = true;
try {
const response = await adminApi.getAppraisalTaskDetail(id);
hydrateDetail(response.data);
} catch (error) {
console.error(error);
ElMessage.error("鉴定任务详情加载失败");
} finally {
detailLoading.value = false;
}
}
async function openAssigneeDialog() {
if (!detail.value) return;
assigneeDialogVisible.value = true;
assigneeOptionsLoading.value = true;
selectedAssigneeId.value = detail.value.task_info.assignee_id || 0;
try {
const response = await adminApi.getAppraisalTaskAssignableAdmins(detail.value.task_info.id);
assigneeOptions.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("鉴定师列表加载失败");
} finally {
assigneeOptionsLoading.value = false;
}
}
async function submitAssigneeAssign() {
if (!detail.value || !selectedAssigneeId.value) {
ElMessage.warning("请先选择处理人");
return;
}
assigneeSubmitting.value = true;
try {
const response = await adminApi.assignAppraisalTask({
id: detail.value.task_info.id,
assignee_id: selectedAssigneeId.value,
});
ElMessage.success(response.message || "处理人已分配");
assigneeDialogVisible.value = false;
await loadDetail(detail.value.task_info.id);
await fetchTasks();
} catch (error) {
console.error(error);
ElMessage.error("处理人分配失败");
} finally {
assigneeSubmitting.value = false;
}
}
async function claimTask() {
if (!detail.value || !currentAdmin.value?.id) {
return;
}
assigneeSubmitting.value = true;
try {
const response = await adminApi.assignAppraisalTask({
id: detail.value.task_info.id,
assignee_id: currentAdmin.value.id,
});
ElMessage.success(response.message || "任务已认领");
await loadDetail(detail.value.task_info.id);
await fetchTasks();
} catch (error) {
console.error(error);
ElMessage.error("任务认领失败");
} finally {
assigneeSubmitting.value = false;
}
}
async function openDetail(row: AdminAppraisalTaskListItem) {
drawerVisible.value = true;
detail.value = null;
detailTab.value = "overview";
activeWorkTab.value = row.service_provider === "zhongjian" ? "zhongjian" : "result";
await loadDetail(row.id);
}
async function scanTransferTag() {
const internalTagNo = transferTagNo.value.trim();
if (!internalTagNo) {
ElMessage.warning("请扫描内部流转码");
return;
}
transferScanLoading.value = true;
try {
const response = await adminApi.scanAppraisalTransferTag(internalTagNo);
drawerVisible.value = true;
detail.value = null;
await loadDetail(response.data.task_id);
detailTab.value = "actions";
activeWorkTab.value = response.data.service_provider === "zhongjian" ? "zhongjian" : "result";
ElMessage.success(`${response.data.service_provider_text}任务已打开`);
await fetchTasks();
} catch (error: any) {
console.error(error);
ElMessage.error(error?.message || "内部流转码识别失败");
} finally {
transferScanLoading.value = false;
}
}
function previewFiles(files: Array<{ file_url: string }>, current: string) {
if (!files.length) return;
window.open(current, "_blank", "noopener,noreferrer");
}
function triggerEvidenceUpload() {
if (isTaskReadonly.value) {
return;
}
evidenceInputRef.value?.click();
}
async function handleEvidenceFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const files = Array.from(target.files || []);
if (!files.length) {
return;
}
evidenceUploading.value = true;
try {
for (const file of files) {
const response = await adminApi.uploadAppraisalEvidenceFile(file);
resultAttachments.value.push(response.data);
}
ElMessage.success("附件上传成功");
} catch (error) {
console.error(error);
ElMessage.error("附件上传失败");
} finally {
evidenceUploading.value = false;
target.value = "";
}
}
function triggerZhongjianReportUpload() {
if (isTaskReadonly.value) {
return;
}
zhongjianReportFileInputRef.value?.click();
}
async function handleZhongjianReportFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const files = Array.from(target.files || []);
if (!files.length) {
return;
}
zhongjianReportUploading.value = true;
try {
for (const file of files) {
const response = await adminApi.uploadAppraisalEvidenceFile(file);
zhongjianReportFiles.value.push(response.data);
}
ElMessage.success("中检报告文件已上传");
} catch (error) {
console.error(error);
ElMessage.error("中检报告文件上传失败");
} finally {
zhongjianReportUploading.value = false;
target.value = "";
}
}
async function removeZhongjianReportFile(fileUrl: string) {
try {
await adminApi.deleteAppraisalEvidenceFile(fileUrl);
zhongjianReportFiles.value = zhongjianReportFiles.value.filter((item) => item.file_url !== fileUrl);
ElMessage.success("报告文件已删除");
} catch (error) {
console.error(error);
ElMessage.error("报告文件删除失败");
}
}
async function removeEvidenceAttachment(fileUrl: string) {
try {
await adminApi.deleteAppraisalEvidenceFile(fileUrl);
resultAttachments.value = resultAttachments.value.filter((item) => item.file_url !== fileUrl);
ElMessage.success("附件已删除");
} catch (error) {
console.error(error);
ElMessage.error("附件删除失败");
}
}
function previewEvidence(url: string) {
window.open(url, "_blank", "noopener,noreferrer");
}
function evidenceTypeLabel(fileType?: string) {
return fileType === "image" ? "图片" : fileType === "video" ? "视频" : fileType === "pdf" ? "PDF" : "附件";
}
function selectedCategoryName(categoryId: number) {
return categories.value.find((item) => item.id === categoryId)?.name || "";
}
async function handleProductCategoryChange(categoryId: number) {
productForm.category_id = categoryId || 0;
productForm.category_name = selectedCategoryName(categoryId);
if (!categoryId || !detail.value) {
currentAppraisalTemplate.value = null;
resultKeyPoints.value = [];
return;
}
appraisalTemplateLoading.value = true;
try {
const response = await adminApi.getCategoryAppraisalTemplates(categoryId);
const template = response.data.template || response.data.list[0] || null;
if (!template) {
applyAppraisalTemplate(null);
ElMessage.warning("当前品类模板加载失败");
return;
}
applyAppraisalTemplate({
id: template.id || 0,
name: template.name,
code: template.code,
service_provider: template.service_provider,
service_provider_text: template.service_provider_text,
result_options: template.result_options,
condition_options: template.condition_options,
valuation_hint: template.valuation_hint,
key_points: template.key_points.map((point) => ({
point_code: point.point_code,
point_name: point.point_name,
point_type: point.point_type,
options: [...(point.options || [])],
sort_order: point.sort_order,
is_required: point.is_required,
point_value: "",
point_remark: "",
})),
});
formRenderKey.value += 1;
} catch (error) {
console.error(error);
ElMessage.error("鉴定模板加载失败");
} finally {
appraisalTemplateLoading.value = false;
}
}
function formatMaterialStatus(value: string) {
return materialStatusMap[value] || value;
}
function formatMaterialSource(value: string) {
return value === "supplement" ? "补交资料" : "初始资料";
}
function normalizedProductForm() {
return {
category_id: productForm.category_id,
product_name: productForm.product_name.trim(),
category_name: productForm.category_name.trim(),
brand_name: productForm.brand_name.trim(),
color: productForm.color.trim(),
size_spec: productForm.size_spec.trim(),
serial_no: productForm.serial_no.trim(),
};
}
function normalizedKeyPoints() {
return resultKeyPoints.value.map((item) => ({
point_code: item.point_code,
point_name: item.point_name,
point_value: item.point_value.trim(),
point_remark: item.point_remark.trim(),
}));
}
function validateRequiredKeyPoints() {
const missing = resultKeyPoints.value.find((item) => item.is_required && !item.point_value.trim());
if (missing) {
ElMessage.warning(`请填写鉴定模板项:${missing.point_name}`);
return false;
}
return true;
}
function hasProductFormValue() {
const product = normalizedProductForm();
return Boolean(product.product_name || product.category_name || product.brand_name);
}
async function submitResult(action: "save" | "submit") {
if (!detail.value) return;
if (detail.value.task_info.service_provider === "zhongjian") {
ElMessage.warning("中检订单请在中检报告录入页提交");
activeWorkTab.value = "zhongjian";
return;
}
if (action === "submit" && !resultForm.result_text.trim()) {
ElMessage.warning("提交前请先填写鉴定结论");
return;
}
if (action === "submit" && !hasProductFormValue()) {
ElMessage.warning("提交前请先完善物品信息");
return;
}
if (action === "submit" && !validateRequiredKeyPoints()) {
return;
}
let qrInput = "";
if (action === "submit") {
qrInput = await promptPublishMaterialTagInput();
if (!qrInput) {
return;
}
}
resultSubmitting.value = true;
try {
const response = await adminApi.saveAppraisalTaskResult({
id: detail.value.task_info.id,
action,
product_info: normalizedProductForm(),
...resultForm,
attachments: resultAttachments.value,
key_points: normalizedKeyPoints(),
...(qrInput ? { qr_input: qrInput } : {}),
});
ElMessage.success(response.message || (action === "submit" ? "验真吊牌已绑定,报告已发布" : "结论已保存"));
await loadDetail(detail.value.task_info.id);
await fetchTasks();
} catch (error) {
console.error(error);
ElMessage.error(action === "submit" ? "结论提交失败" : "结论保存失败");
} finally {
resultSubmitting.value = false;
}
}
async function publishCurrentTaskWithMaterialTag(qrInput: string) {
if (!detail.value) return;
await adminApi.publishAppraisalTaskWithMaterialTag({
id: detail.value.task_info.id,
qr_input: qrInput,
});
ElMessage.success("验真吊牌已绑定,报告已发布");
await loadDetail(detail.value.task_info.id);
await fetchTasks();
}
async function bindMaterialTag() {
const qrInput = materialTagInput.value.trim();
if (!qrInput) {
ElMessage.warning("请扫描或粘贴吊牌二维码链接");
return;
}
materialTagBinding.value = true;
try {
await publishCurrentTaskWithMaterialTag(qrInput);
materialTagInput.value = "";
} catch (error: any) {
console.error(error);
ElMessage.error(error?.message || "验真吊牌绑定或报告发布失败");
} finally {
materialTagBinding.value = false;
}
}
async function promptPublishMaterialTagInput() {
try {
const result = await ElMessageBox.prompt("是否已鉴定完成并确定发布报告?", "绑定验真吊牌并发布报告", {
type: "warning",
inputPlaceholder: "请扫描验真吊牌二维码",
inputPattern: /\S+/,
inputErrorMessage: "请扫描验真吊牌二维码",
confirmButtonText: "是的,去绑定验真吊牌",
cancelButtonText: "取消",
closeOnClickModal: false,
});
return String(result.value || "").trim();
} catch {
return "";
}
}
async function submitZhongjianReport() {
if (!detail.value) return;
if (!isZhongjianTask.value) {
ElMessage.warning("当前不是中检订单");
return;
}
if (!zhongjianReportNo.value.trim()) {
ElMessage.warning("请填写中检报告编号");
return;
}
if (!resultForm.result_text.trim()) {
ElMessage.warning("请填写鉴定结论");
activeWorkTab.value = "result";
return;
}
if (!hasProductFormValue()) {
ElMessage.warning("提交前请先完善物品信息");
activeWorkTab.value = "result";
return;
}
if (!validateRequiredKeyPoints()) {
activeWorkTab.value = "result";
return;
}
if (!zhongjianReportFiles.value.length) {
ElMessage.warning("请至少上传 1 个中检报告文件");
return;
}
const qrInput = await promptPublishMaterialTagInput();
if (!qrInput) {
return;
}
zhongjianReportSubmitting.value = true;
try {
const response = await adminApi.saveZhongjianAppraisalReport({
id: detail.value.task_info.id,
zhongjian_report_no: zhongjianReportNo.value.trim(),
product_info: normalizedProductForm(),
result_text: resultForm.result_text.trim(),
result_desc: resultForm.result_desc.trim(),
attachments: resultAttachments.value,
key_points: normalizedKeyPoints(),
report_files: zhongjianReportFiles.value,
qr_input: qrInput,
});
ElMessage.success(response.message || "验真吊牌已绑定,报告已发布");
await loadDetail(detail.value.task_info.id);
await fetchTasks();
} catch (error: any) {
console.error(error);
ElMessage.error(error?.message || "中检报告录入失败");
} finally {
zhongjianReportSubmitting.value = false;
}
}
function addSupplementItem() {
supplementForm.items.push({
item_name: "",
guide_text: "",
is_required: true,
});
}
function removeSupplementItem(index: number) {
if (supplementForm.items.length === 1) {
supplementForm.items[0].item_name = "";
supplementForm.items[0].guide_text = "";
supplementForm.items[0].is_required = true;
return;
}
supplementForm.items.splice(index, 1);
}
async function requestSupplement() {
if (!detail.value) return;
if (detail.value.task_info.status === "completed") {
ElMessage.warning("当前任务已完成,不能再发起补资料");
return;
}
const validItems = supplementForm.items.filter((item) => item.item_name.trim());
if (!supplementForm.reason.trim()) {
ElMessage.warning("请先填写补资料原因");
return;
}
if (!validItems.length) {
ElMessage.warning("请至少填写一项补资料要求");
return;
}
supplementSubmitting.value = true;
try {
const response = await adminApi.requestAppraisalTaskSupplement({
id: detail.value.task_info.id,
reason: supplementForm.reason,
deadline: supplementForm.deadline,
items: validItems.map((item) => ({
item_name: item.item_name.trim(),
guide_text: item.guide_text.trim(),
is_required: item.is_required,
})),
});
ElMessage.success(response.message || "已发起补资料要求");
await loadDetail(detail.value.task_info.id);
await fetchTasks();
} catch (error) {
console.error(error);
ElMessage.error("发起补资料失败");
} finally {
supplementSubmitting.value = false;
}
}
function resetWorkbenchScroll() {
nextTick(() => {
workbenchAsideRef.value?.scrollTo({ top: 0, behavior: "auto" });
});
}
watch(detailTab, (value) => {
if (value === "actions") {
resetWorkbenchScroll();
}
});
watch(activeWorkTab, () => {
if (detailTab.value === "actions") {
resetWorkbenchScroll();
}
});
onMounted(async () => {
await Promise.all([fetchTasks(), fetchCategories()]);
});
</script>
<template>
<el-card class="panel-card appraisal-scan-card" shadow="never">
<div class="appraisal-scan">
<div class="appraisal-scan__main">
<div class="appraisal-scan__title">鉴定作业台扫码入口</div>
<div class="appraisal-scan__desc">扫描内部流转挂牌后自动打开对应鉴定任务</div>
</div>
<div class="appraisal-scan__control">
<el-input
v-model="transferTagNo"
size="large"
placeholder="扫描内部流转码"
clearable
@keyup.enter="scanTransferTag"
>
<template #append>
<el-button :loading="transferScanLoading" @click="scanTransferTag">打开任务</el-button>
</template>
</el-input>
</div>
</div>
</el-card>
<el-card class="panel-card" shadow="never">
<div class="filters-row">
<el-input v-model="keyword" placeholder="搜索订单号 / 外部订单号 / 商品名称" clearable style="width: 340px" />
<el-select v-model="taskStage" placeholder="任务阶段" style="width: 150px">
<el-option v-for="item in stageOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="status" placeholder="任务状态" style="width: 150px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="serviceProvider" placeholder="服务类型" style="width: 160px">
<el-option v-for="item in providerOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-button type="primary" @click="fetchTasks">查询</el-button>
</div>
</el-card>
<el-card class="panel-card orders-table" shadow="never">
<el-table v-loading="loading" :data="tasks" stripe>
<el-table-column prop="order_no" label="订单号" min-width="170" />
<el-table-column prop="external_order_no" label="外部订单号" min-width="170" />
<el-table-column prop="appraisal_no" label="鉴定单号" min-width="170" />
<el-table-column prop="product_name" label="商品名称" min-width="210" />
<el-table-column label="阶段进度" min-width="220">
<template #default="{ row }">
<div class="task-stage-progress">
<div
v-for="stage in row.stage_tasks"
:key="`list-stage-${stage.id}`"
class="task-stage-progress__item"
:class="{ 'is-current': stage.is_current }"
>
<span class="task-stage-progress__name">{{ stage.task_stage_text }}</span>
<OrderStatusTag :status="stage.status_text" />
</div>
</div>
</template>
</el-table-column>
<el-table-column label="任务状态" min-width="120">
<template #default="{ row }">
<OrderStatusTag :status="row.status_text" />
</template>
</el-table-column>
<el-table-column prop="service_provider_text" label="服务类型" min-width="120" />
<el-table-column prop="assignee_name" label="处理人" min-width="120" />
<el-table-column prop="result_text" label="结论摘要" min-width="120" />
<el-table-column prop="sla_deadline" label="SLA 截止" min-width="170" />
<el-table-column label="操作" fixed="right" width="110">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-drawer v-model="drawerVisible" size="76%" title="鉴定任务详情" class="task-detail-drawer">
<div v-loading="detailLoading" v-if="detail" class="task-detail-shell">
<div class="detail-card task-hero">
<div class="task-hero__main">
<div class="task-hero__eyebrow">鉴定任务工作区</div>
<div class="task-hero__title">{{ productTitle }}</div>
<div class="task-hero__meta">{{ productMetaText }}</div>
</div>
<div class="task-hero__side">
<div class="task-hero__tags">
<span class="task-stage-chip">{{ detail.task_info.task_stage_text }}</span>
<OrderStatusTag :status="detail.task_info.status_text" />
<span class="task-stage-chip task-stage-chip--warm">{{ detail.task_info.service_provider_text }}</span>
</div>
<div class="task-hero__summary">
当前结论<strong>{{ currentResultLabel }}</strong>
</div>
</div>
</div>
<div class="task-detail-toolbar">
<div class="task-detail-toolbar__tabs">
<button
v-for="item in detailTabOptions"
:key="item.value"
type="button"
class="task-detail-toolbar__tab"
:class="{ 'is-active': detailTab === item.value }"
@click="detailTab = item.value"
>
{{ item.label }}
</button>
</div>
<div class="task-detail-toolbar__actions">
<template v-if="detailTab === 'actions'">
<template v-if="isTaskReadonly">
<span class="task-detail-toolbar__readonly">{{ reportDraftHint || "当前任务已完成,仅支持查看。" }}</span>
</template>
<template v-else-if="activeWorkTab === 'result'">
<el-button plain @click="openAssigneeDialog">分配处理人</el-button>
<el-button v-if="canClaimTask" plain type="primary" :loading="assigneeSubmitting" @click="claimTask">认领给我</el-button>
<el-button @click="resetResultForm">重置内容</el-button>
<el-button type="warning" plain @click="openSupplementWorkbench">请求补资料</el-button>
<el-button :loading="resultSubmitting" @click="submitResult('save')">保存结论</el-button>
<el-button type="primary" :loading="resultSubmitting" @click="submitResult('submit')">提交结论</el-button>
</template>
<template v-else-if="activeWorkTab === 'zhongjian'">
<el-button plain @click="openAssigneeDialog">分配处理人</el-button>
<el-button v-if="canClaimTask" plain type="primary" :loading="assigneeSubmitting" @click="claimTask">认领给我</el-button>
<el-button @click="resetZhongjianReportForm">重置内容</el-button>
<el-button type="primary" :loading="zhongjianReportSubmitting" @click="submitZhongjianReport">提交并发布报告</el-button>
</template>
<template v-else>
<el-button plain @click="returnToResultWorkbench">返回结论操作</el-button>
<el-button @click="resetSupplementForm">重置</el-button>
<el-button type="warning" :disabled="!canRequestSupplement" :loading="supplementSubmitting" @click="requestSupplement">
发起补资料
</el-button>
</template>
</template>
</div>
</div>
<el-tabs v-model="detailTab" class="task-detail-tabs">
<el-tab-pane label="任务信息" name="overview">
<div class="task-overview-grid">
<div class="detail-card task-panel task-panel--full">
<div class="detail-card__title">鉴定工单总览</div>
<div class="task-panel__desc">当前订单按单次鉴定处理提交结论后直接生成报告草稿</div>
<div class="task-stage-overview">
<div
v-for="stage in detail.stage_tasks"
:key="`detail-stage-${stage.id}`"
class="task-stage-card"
:class="{ 'is-current': stage.is_current }"
>
<div class="task-stage-card__header">
<span class="task-stage-chip">{{ stage.task_stage_text }}</span>
<OrderStatusTag :status="stage.status_text" />
</div>
<div class="task-stage-card__meta">处理人{{ stage.assignee_name }}</div>
<div class="task-stage-card__meta">结论{{ stage.result_text || "暂未填写" }}</div>
<div class="task-stage-card__meta">提交时间{{ stage.submitted_at || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">任务概览</div>
<div class="task-info-grid">
<div class="task-info-item">
<div class="task-info-item__label">订单号</div>
<div class="task-info-item__value">{{ detail.task_info.order_no }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">鉴定单号</div>
<div class="task-info-item__value">{{ detail.task_info.appraisal_no }}</div>
</div>
<div v-if="detail.task_info.external_order_no" class="task-info-item">
<div class="task-info-item__label">外部订单号</div>
<div class="task-info-item__value">{{ detail.task_info.external_order_no }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">任务阶段 / 状态</div>
<div class="task-info-item__value task-info-item__value--inline">
<span class="task-stage-chip">{{ detail.task_info.task_stage_text }}</span>
<OrderStatusTag :status="detail.task_info.status_text" />
</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">处理人</div>
<div class="task-info-item__value">{{ detail.task_info.assignee_name }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">服务类型</div>
<div class="task-info-item__value">{{ detail.task_info.service_provider_text }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">SLA 截止</div>
<div class="task-info-item__value">{{ detail.task_info.sla_deadline || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">开始时间</div>
<div class="task-info-item__value">{{ detail.task_info.started_at || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">提交时间</div>
<div class="task-info-item__value">{{ detail.task_info.submitted_at || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">商品与送检</div>
<div class="task-info-grid">
<div class="task-info-item">
<div class="task-info-item__label">商品名称</div>
<div class="task-info-item__value">{{ detail.product_info.product_name || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">品类 / 品牌</div>
<div class="task-info-item__value">{{ detail.product_info.category_name || "-" }} / {{ detail.product_info.brand_name || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">颜色 / 规格</div>
<div class="task-info-item__value">{{ detail.product_info.color || "-" }} / {{ detail.product_info.size_spec || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">购买渠道</div>
<div class="task-info-item__value">{{ detail.extra_info.purchase_channel || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">购买价格</div>
<div class="task-info-item__value">¥{{ detail.extra_info.purchase_price }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">使用情况</div>
<div class="task-info-item__value">{{ usageStatusText }}</div>
</div>
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">补充描述</div>
<div class="task-info-item__value">{{ detail.extra_info.condition_desc || detail.extra_info.remark || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">当前结论</div>
<div class="task-info-grid">
<div class="task-info-item">
<div class="task-info-item__label">鉴定结论</div>
<div class="task-info-item__value">{{ currentResultLabel }}</div>
</div>
<div v-if="detail.result_info.key_points.length" class="task-info-item task-info-item--full">
<div class="task-info-item__label">模板填写项</div>
<div class="task-pill-list">
<span v-for="item in detail.result_info.key_points" :key="item.point_code" class="task-pill">
{{ item.point_name }}{{ item.point_value || "-" }}
</span>
</div>
</div>
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">鉴定结论补充</div>
<div class="task-info-item__value">{{ detail.result_info.result_desc || "-" }}</div>
</div>
<div v-if="detail.report_summary" class="task-info-item task-info-item--full">
<div class="task-info-item__label">关联报告草稿</div>
<div class="task-info-item__value">{{ detail.report_summary.report_no }} / {{ detail.report_summary.report_status_text }}</div>
</div>
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">绑定吊牌</div>
<div v-if="detail.material_tag" class="task-info-item__value">
{{ detail.material_tag.verify_code }} / {{ detail.material_tag.bind_status_text }} / 扫码 {{ detail.material_tag.scan_count }} / 验真 {{ detail.material_tag.verify_count }}
</div>
<div v-else class="task-info-item__value">未绑定</div>
</div>
</div>
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">补资料状态</div>
<div v-if="detail.supplement_task" class="task-info-grid">
<div class="task-info-item">
<div class="task-info-item__label">当前状态</div>
<div class="task-info-item__value">{{ supplementStatusText }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">补资料数量</div>
<div class="task-info-item__value">{{ detail.supplement_task.items.length }} </div>
</div>
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">补资料原因</div>
<div class="task-info-item__value">{{ detail.supplement_task.reason }}</div>
</div>
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">资料要求</div>
<div class="task-pill-list">
<span v-for="item in detail.supplement_task.items" :key="item.item_name" class="task-pill">
{{ item.item_name }}{{ item.is_required ? " · 必传" : " · 选传" }}
</span>
</div>
</div>
</div>
<div v-else class="task-empty">
当前没有待处理补资料任务若资料完整可直接进入结论处理路径
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="资料与轨迹" name="materials">
<div class="task-stream-stack">
<div class="detail-card task-panel">
<div class="detail-card__title">资料任务项</div>
<div class="task-panel__desc">把资料核验放到单独分区只在需要时查看避免与处理动作混在一起</div>
<div v-if="detail.materials.length" class="task-material-grid">
<div v-for="item in detail.materials" :key="`${item.item_name}-${item.source_type}`" class="task-material-card">
<div class="task-material-card__header">
<div>
<div class="task-material-card__title">{{ item.item_name }}</div>
<div class="task-material-card__meta">{{ formatMaterialSource(item.source_type) }}</div>
</div>
<OrderStatusTag :status="formatMaterialStatus(item.status)" />
</div>
<div v-if="item.files.length" class="task-thumb-grid">
<div
v-for="file in item.files"
:key="file.file_id"
class="admin-upload-thumb"
@click="previewFiles(item.files, file.file_url)"
>
<img :src="file.thumbnail_url" alt="上传资料" />
</div>
</div>
<div v-else class="task-empty task-empty--compact">当前还没有上传文件</div>
</div>
</div>
<el-empty v-else description="暂无资料任务项" />
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">订单时间轴</div>
<div class="timeline-list task-timeline-list">
<div v-for="item in detail.timeline" :key="`${item.node_text}-${item.occurred_at}`" class="timeline-node">
<div class="timeline-node__title">{{ item.node_text }}</div>
<div class="timeline-node__time">{{ item.occurred_at }}</div>
<div class="timeline-node__desc">{{ item.node_desc }}</div>
</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="处理工作台" name="actions">
<div class="task-workbench-grid">
<div ref="workbenchAsideRef" class="detail-card task-panel task-workbench-aside">
<div class="task-context-card">
<div class="task-context-card__header">
<div>
<div class="task-context-card__title">检测物品信息</div>
<div class="task-context-card__meta">{{ detail.task_info.service_provider_text }} / {{ detail.task_info.task_stage_text }}</div>
</div>
<OrderStatusTag :status="detail.task_info.status_text" />
</div>
<div class="task-context-list">
<div class="task-context-list__item">
<div class="task-context-list__label">商品名称</div>
<div class="task-context-list__value">{{ detail.product_info.product_name || "-" }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">品类 / 品牌</div>
<div class="task-context-list__value">{{ detail.product_info.category_name || "-" }} / {{ detail.product_info.brand_name || "-" }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">颜色 / 规格</div>
<div class="task-context-list__value">{{ detail.product_info.color || "-" }} / {{ detail.product_info.size_spec || "-" }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">购买渠道</div>
<div class="task-context-list__value">{{ detail.extra_info.purchase_channel || "-" }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">购买价格</div>
<div class="task-context-list__value">¥{{ detail.extra_info.purchase_price }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">使用情况</div>
<div class="task-context-list__value">{{ usageStatusText }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">当前结论</div>
<div class="task-context-list__value">{{ currentResultLabel }}</div>
</div>
<div class="task-context-list__item">
<div class="task-context-list__label">补资料状态</div>
<div class="task-context-list__value">{{ supplementStatusText }}</div>
</div>
</div>
</div>
<div class="task-context-section">
<div class="task-context-section__title">用户上传资料</div>
<div class="task-context-section__meta">{{ detail.materials.length }} 项资料任务直接在此核对缩略图</div>
<div v-if="detail.materials.length" class="task-context-materials">
<div v-for="item in detail.materials" :key="`workbench-${item.item_name}-${item.source_type}`" class="task-context-material">
<div class="task-context-material__header">
<div>
<div class="task-context-material__title">{{ item.item_name }}</div>
<div class="task-context-material__meta">{{ formatMaterialSource(item.source_type) }}</div>
</div>
<OrderStatusTag :status="formatMaterialStatus(item.status)" />
</div>
<div v-if="item.files.length" class="task-thumb-grid task-thumb-grid--compact">
<div
v-for="file in item.files"
:key="`workbench-${file.file_id}`"
class="admin-upload-thumb"
@click="previewFiles(item.files, file.file_url)"
>
<img :src="file.thumbnail_url" alt="上传资料" />
</div>
</div>
<div v-else class="task-empty task-empty--compact">当前还没有上传文件</div>
</div>
</div>
<div v-else class="task-empty task-empty--compact">
当前暂无用户上传资料
</div>
</div>
</div>
<div class="detail-card task-panel">
<div class="detail-card__title">执行操作</div>
<div class="task-panel__desc">一次只保留一条操作链路完成后再切换到其他动作</div>
<el-alert v-if="reviewPrefillHint" :title="reviewPrefillHint" type="info" :closable="false" show-icon style="margin-top: 14px;" />
<el-alert v-if="isTaskReadonly" :title="reportDraftHint || '当前任务已完成,只允许查看结果,不允许继续编辑或提交。'" type="warning" :closable="false" show-icon style="margin-top: 14px;" />
<el-tabs v-model="activeWorkTab" stretch class="task-work-tabs">
<el-tab-pane label="填写结论" name="result">
<div :key="`result-${formRenderKey}`" class="task-form-stack">
<el-alert
v-if="isZhongjianTask"
title="中检订单请在中检报告录入页提交,提交时同样需要绑定验真吊牌。"
type="info"
:closable="false"
show-icon
/>
<div class="task-form-block">
<div class="task-form-block__title">物品信息</div>
<div class="task-panel__desc">客户推送订单可能不带物品信息请鉴定师根据实物和资料补全报告草稿会使用这里的内容</div>
<div class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">商品名称</div>
<el-input v-model="productForm.product_name" :disabled="isTaskReadonly" placeholder="例如 LV Neverfull MM 手袋" />
</div>
<div class="task-form-field">
<div class="task-form-field__label">品类</div>
<el-select
v-model="productForm.category_id"
:disabled="isTaskReadonly"
filterable
clearable
style="width: 100%"
placeholder="请选择鉴定品类"
@change="handleProductCategoryChange"
>
<el-option v-for="item in categories" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</div>
<div class="task-form-field">
<div class="task-form-field__label">品牌</div>
<el-input v-model="productForm.brand_name" :disabled="isTaskReadonly" placeholder="例如 Louis Vuitton" />
</div>
<div class="task-form-field">
<div class="task-form-field__label">颜色</div>
<el-input v-model="productForm.color" :disabled="isTaskReadonly" placeholder="例如 棕色老花" />
</div>
<div class="task-form-field">
<div class="task-form-field__label">规格 / 尺寸</div>
<el-input v-model="productForm.size_spec" :disabled="isTaskReadonly" placeholder="例如 MM" />
</div>
<div class="task-form-field">
<div class="task-form-field__label">序列号 / 编码</div>
<el-input v-model="productForm.serial_no" :disabled="isTaskReadonly" placeholder="输入可识别编号" />
</div>
</div>
</div>
<div class="task-form-block">
<div class="task-form-block__title">吊牌绑定</div>
<div class="task-panel__desc">提交结论或中检报告时扫描平台验真吊牌绑定成功后发布报告</div>
<div v-if="detail.material_tag" class="task-material-tag-bound">
<div class="task-info-grid">
<div class="task-info-item task-info-item--full">
<div class="task-info-item__label">二维码链接</div>
<div class="task-info-item__value" style="word-break: break-all;">{{ detail.material_tag.qr_url }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">验真编码</div>
<div class="task-info-item__value">{{ detail.material_tag.verify_code }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">扫码 / 验真次数</div>
<div class="task-info-item__value">{{ detail.material_tag.scan_count }} / {{ detail.material_tag.verify_count }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">绑定人</div>
<div class="task-info-item__value">{{ detail.material_tag.bound_by_name || "-" }}</div>
</div>
<div class="task-info-item">
<div class="task-info-item__label">绑定时间</div>
<div class="task-info-item__value">{{ detail.material_tag.bound_at || "-" }}</div>
</div>
</div>
</div>
<div v-else-if="detail.report_summary" class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">吊牌二维码链接</div>
<el-input
v-model="materialTagInput"
:disabled="!canBindMaterialTag"
placeholder="请用扫描枪扫描吊牌二维码,或粘贴二维码内链接"
clearable
@keyup.enter="bindMaterialTag"
>
<template #append>
<el-button :loading="materialTagBinding" :disabled="!canBindMaterialTag" @click="bindMaterialTag">绑定</el-button>
</template>
</el-input>
</div>
</div>
<el-alert
v-else
title="提交鉴定结论生成报告草稿后,才能绑定实物吊牌。"
type="info"
:closable="false"
show-icon
style="margin-top: 12px;"
/>
</div>
<div class="task-form-block">
<div class="task-form-block__title">基础结论</div>
<el-alert
v-if="currentAppraisalTemplate"
:title="`已加载 ${productForm.category_name || '当前品类'} 鉴定模板`"
type="success"
:closable="false"
show-icon
style="margin: 12px 0;"
/>
<el-alert
v-else-if="productForm.category_id"
title="正在加载或未能加载当前品类鉴定模板。"
type="warning"
:closable="false"
show-icon
style="margin: 12px 0;"
/>
<div class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">鉴定结论 <span class="task-form-field__required">必填</span></div>
<el-input
v-model="resultForm.result_text"
:disabled="isTaskReadonly"
type="textarea"
:rows="3"
placeholder="请输入鉴定结论"
/>
</div>
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">鉴定结论补充</div>
<el-input v-model="resultForm.result_desc" :disabled="isTaskReadonly" type="textarea" :rows="3" placeholder="可补充鉴定依据、异常点或说明" />
</div>
</div>
</div>
<div class="task-form-block">
<div class="task-form-block__title">鉴定模板填写</div>
<div class="task-panel__desc">
选择品类后自动加载对应模板按关键项逐项填写检查结论和备注
</div>
<div v-loading="appraisalTemplateLoading">
<div v-if="resultKeyPoints.length" class="task-key-point-list">
<div v-for="item in resultKeyPoints" :key="item.point_code" class="task-key-point-card">
<div class="task-key-point-card__header">
<div class="task-key-point-card__title">
{{ item.point_name }}
<span v-if="item.is_required" class="task-key-point-card__required">必填</span>
</div>
<div class="task-key-point-card__code">{{ item.point_code }}</div>
</div>
<div class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">检查结果</div>
<el-select
v-if="item.point_type === 'select'"
v-model="item.point_value"
:disabled="isTaskReadonly"
filterable
allow-create
default-first-option
style="width: 100%"
placeholder="请选择或输入检查结果"
>
<el-option v-for="option in item.options" :key="option" :label="option" :value="option" />
</el-select>
<el-radio-group v-else-if="item.point_type === 'boolean'" v-model="item.point_value" :disabled="isTaskReadonly">
<el-radio-button label="符合">符合</el-radio-button>
<el-radio-button label="不符合">不符合</el-radio-button>
<el-radio-button label="存疑">存疑</el-radio-button>
</el-radio-group>
<el-input
v-else-if="item.point_type === 'textarea'"
v-model="item.point_value"
:disabled="isTaskReadonly"
type="textarea"
:rows="3"
placeholder="请输入检查结果"
/>
<el-input v-else v-model="item.point_value" :disabled="isTaskReadonly" placeholder="请输入检查结果" />
</div>
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">备注</div>
<el-input v-model="item.point_remark" :disabled="isTaskReadonly" type="textarea" :rows="2" placeholder="可补充细节、依据或风险点" />
</div>
</div>
</div>
</div>
<div v-else class="task-empty task-empty--compact">
当前品类没有额外鉴定关键项请填写固定鉴定结论并按需上传附件
</div>
</div>
</div>
<div class="task-form-block">
<div class="task-form-block__title">附件上传</div>
<div class="task-panel__desc">支持上传图片视频PDF作为本次鉴定结论的附件保存到报告草稿</div>
<input
ref="evidenceInputRef"
type="file"
multiple
accept="image/*,video/*,.pdf,application/pdf"
style="display: none"
@change="handleEvidenceFileSelect"
/>
<div class="task-evidence-toolbar">
<el-button :disabled="isTaskReadonly" :loading="evidenceUploading" @click="triggerEvidenceUpload">上传附件</el-button>
<span class="task-evidence-hint">{{ resultAttachments.length }} 个附件</span>
</div>
<div v-if="resultAttachments.length" class="task-evidence-list">
<div v-for="attachment in resultAttachments" :key="attachment.file_id" class="task-evidence-card">
<div
class="task-evidence-card__preview"
:class="{ 'is-image': attachment.file_type === 'image' }"
@click="previewEvidence(attachment.file_url)"
>
<img v-if="attachment.file_type === 'image' && attachment.thumbnail_url" :src="attachment.thumbnail_url" :alt="attachment.name || '证据图片'" />
<div v-else class="task-evidence-card__filetype">{{ evidenceTypeLabel(attachment.file_type) }}</div>
</div>
<div class="task-evidence-card__body">
<div class="task-evidence-card__name">{{ attachment.name || attachment.file_url }}</div>
<div class="task-evidence-card__meta">{{ evidenceTypeLabel(attachment.file_type) }}</div>
<div class="task-evidence-card__actions">
<el-button link type="primary" @click="previewEvidence(attachment.file_url)">查看</el-button>
<el-button v-if="!isTaskReadonly" link type="danger" @click="removeEvidenceAttachment(attachment.file_url)">删除</el-button>
</div>
</div>
</div>
</div>
<div v-else class="task-empty task-empty--compact">当前还没有上传附件</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane v-if="isZhongjianTask" label="中检报告录入" name="zhongjian">
<div :key="`zhongjian-${formRenderKey}`" class="task-form-stack">
<el-alert
title="请先在“填写结论”中补全物品信息、鉴定结论和模板项,再提交中检报告编号和文件;绑定吊牌成功后才会发布报告。"
type="info"
:closable="false"
show-icon
/>
<div class="task-form-block">
<div class="task-form-block__title">中检报告信息</div>
<div class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">中检报告编号 <span class="task-form-field__required">必填</span></div>
<el-input
v-model="zhongjianReportNo"
:disabled="isTaskReadonly"
placeholder="扫描或输入中检报告编号"
clearable
@keyup.enter="submitZhongjianReport"
/>
</div>
<div v-if="detail.zhongjian_report?.report_entry_admin_name" class="task-form-field">
<div class="task-form-field__label">报告录入人</div>
<div class="task-readonly-value">{{ detail.zhongjian_report.report_entry_admin_name }}</div>
</div>
<div v-if="detail.zhongjian_report?.report_entered_at" class="task-form-field">
<div class="task-form-field__label">录入时间</div>
<div class="task-readonly-value">{{ detail.zhongjian_report.report_entered_at }}</div>
</div>
</div>
</div>
<div class="task-form-block">
<div class="task-form-block__title">报告文件</div>
<div class="task-panel__desc">至少上传 1 个中检报告文件支持图片视频或 PDF</div>
<input
ref="zhongjianReportFileInputRef"
type="file"
multiple
accept="image/*,video/*,.pdf,application/pdf"
style="display: none"
@change="handleZhongjianReportFileSelect"
/>
<div class="task-evidence-toolbar">
<el-button :disabled="isTaskReadonly" :loading="zhongjianReportUploading" @click="triggerZhongjianReportUpload">上传报告文件</el-button>
<span class="task-evidence-hint">{{ zhongjianReportFiles.length }} 个文件</span>
</div>
<div v-if="zhongjianReportFiles.length" class="task-evidence-list">
<div v-for="file in zhongjianReportFiles" :key="file.file_id" class="task-evidence-card">
<div
class="task-evidence-card__preview"
:class="{ 'is-image': file.file_type === 'image' }"
@click="previewEvidence(file.file_url)"
>
<img v-if="file.file_type === 'image' && file.thumbnail_url" :src="file.thumbnail_url" :alt="file.name || '中检报告'" />
<div v-else class="task-evidence-card__filetype">{{ evidenceTypeLabel(file.file_type) }}</div>
</div>
<div class="task-evidence-card__body">
<div class="task-evidence-card__name">{{ file.name || file.file_url }}</div>
<div class="task-evidence-card__meta">{{ evidenceTypeLabel(file.file_type) }}</div>
<div class="task-evidence-card__actions">
<el-button link type="primary" @click="previewEvidence(file.file_url)">查看</el-button>
<el-button v-if="!isTaskReadonly" link type="danger" @click="removeZhongjianReportFile(file.file_url)">删除</el-button>
</div>
</div>
</div>
</div>
<div v-else class="task-empty task-empty--compact">当前还没有上传中检报告文件</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="发起补资料" name="supplement" :disabled="isTaskReadonly">
<div :key="`supplement-${formRenderKey}`" class="task-form-stack">
<el-alert
v-if="detail.supplement_task"
title="当前已存在待处理补资料任务"
:description="`重新发起后会关闭任务 #${detail.supplement_task.id},并以本次要求替换。`"
type="warning"
:closable="false"
show-icon
/>
<div class="task-form-block">
<div class="task-form-block__title">触发说明</div>
<div class="task-form-grid">
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">补资料原因</div>
<el-input v-model="supplementForm.reason" :disabled="isTaskReadonly" type="textarea" :rows="3" placeholder="请输入需要用户补充资料的原因" />
</div>
<div class="task-form-field task-form-field--full">
<div class="task-form-field__label">截止时间</div>
<el-date-picker
v-model="supplementForm.deadline"
:disabled="isTaskReadonly"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm"
placeholder="选择补资料截止时间"
style="width: 100%"
/>
</div>
</div>
</div>
<div class="task-form-block">
<div class="task-form-block__title">补资料项</div>
<div class="task-supplement-list">
<div v-for="(item, index) in supplementForm.items" :key="`supplement-${index}`" class="task-supplement-item">
<div class="task-supplement-item__head">
<el-input v-model="item.item_name" :disabled="isTaskReadonly" placeholder="资料项名称,如 编码标签近照" />
<el-switch v-model="item.is_required" :disabled="isTaskReadonly" inline-prompt active-text="必传" inactive-text="选传" />
<el-button link type="danger" :disabled="isTaskReadonly" @click="removeSupplementItem(index)">删除</el-button>
</div>
<el-input
v-model="item.guide_text"
:disabled="isTaskReadonly"
type="textarea"
:rows="2"
placeholder="输入对用户的拍摄或补充指引"
/>
</div>
</div>
<div class="task-supplement-actions">
<el-button plain :disabled="isTaskReadonly" @click="addSupplementItem">新增资料项</el-button>
</div>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</el-drawer>
<el-dialog v-model="assigneeDialogVisible" title="分配处理人" width="560px">
<div v-loading="assigneeOptionsLoading" style="display: grid; gap: 14px;">
<div
v-for="item in assigneeOptions"
:key="item.id"
:style="{
border: selectedAssigneeId === item.id ? '1px solid #c8a45d' : '1px solid var(--admin-border)',
borderRadius: '14px',
padding: '16px 18px',
cursor: 'pointer',
background: selectedAssigneeId === item.id ? 'rgba(200, 164, 93, 0.08)' : '#fff',
}"
@click="selectedAssigneeId = item.id"
>
<div style="display:flex; justify-content:space-between; gap: 16px; align-items:center;">
<div style="font-weight:700;">{{ item.name }}</div>
<div style="color: var(--admin-text-subtle);">{{ item.mobile }}</div>
</div>
<div style="margin-top: 8px; color: var(--admin-text-subtle);">{{ item.role_names.join(' / ') || '未分配角色' }}</div>
</div>
</div>
<template #footer>
<el-button @click="assigneeDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="assigneeSubmitting" @click="submitAssigneeAssign">确认分配</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.appraisal-scan-card {
margin-bottom: 18px;
}
.appraisal-scan {
display: grid;
grid-template-columns: minmax(220px, 1fr) minmax(320px, 520px);
gap: 18px;
align-items: center;
}
.appraisal-scan__title {
color: var(--admin-text-main);
font-size: 18px;
font-weight: 800;
line-height: 1.3;
}
.appraisal-scan__desc {
margin-top: 6px;
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.6;
}
.appraisal-scan__control {
min-width: 0;
}
:deep(.task-detail-drawer .el-drawer__body) {
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding-top: 0;
}
.task-detail-shell {
display: flex;
flex-direction: column;
gap: 18px;
height: 100%;
min-height: 0;
}
.task-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px;
background:
radial-gradient(circle at top right, rgba(200, 164, 93, 0.12), transparent 30%),
linear-gradient(135deg, #fffdfa 0%, #fbf8f1 100%);
}
.task-hero__main {
min-width: 0;
}
.task-hero__eyebrow {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 12px;
border-radius: 999px;
background: rgba(200, 164, 93, 0.12);
color: #7a5a21;
font-size: 12px;
font-weight: 700;
}
.task-hero__title {
margin-top: 14px;
font-size: 28px;
font-weight: 800;
line-height: 1.2;
color: var(--admin-text-main);
word-break: break-word;
}
.task-hero__meta {
margin-top: 10px;
color: var(--admin-text-subtle);
font-size: 14px;
line-height: 1.6;
}
.task-hero__side {
min-width: 280px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 14px;
}
.task-hero__tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.task-hero__summary {
color: var(--admin-text-subtle);
font-size: 13px;
text-align: right;
}
.task-stage-chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
background: rgba(72, 104, 133, 0.1);
color: var(--admin-progress);
font-size: 12px;
font-weight: 700;
}
.task-stage-chip--warm {
background: rgba(200, 164, 93, 0.14);
color: #8b6327;
}
.task-detail-toolbar {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(233, 226, 210, 0.92);
background: #fff;
z-index: 3;
}
.task-detail-toolbar__tabs {
display: flex;
align-items: flex-end;
gap: 28px;
}
.task-detail-toolbar__tab {
position: relative;
padding: 0 0 12px;
border: none;
background: transparent;
color: var(--admin-text-main);
font-size: 16px;
font-weight: 700;
cursor: pointer;
}
.task-detail-toolbar__tab::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -7px;
height: 3px;
border-radius: 999px;
background: transparent;
transition: background-color 0.2s ease;
}
.task-detail-toolbar__tab.is-active {
color: #409eff;
}
.task-detail-toolbar__tab.is-active::after {
background: #409eff;
}
.task-detail-toolbar__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
min-height: 40px;
min-width: 220px;
}
.task-detail-toolbar__readonly {
color: var(--admin-text-subtle);
font-size: 13px;
font-weight: 600;
}
.task-detail-tabs {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
}
.task-detail-tabs :deep(.el-tabs__header) {
display: none;
margin: 0;
}
.task-detail-tabs :deep(.el-tabs__content) {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-top: 18px;
padding-right: 2px;
}
.task-detail-tabs :deep(.el-tabs__content::-webkit-scrollbar) {
width: 8px;
}
.task-detail-tabs :deep(.el-tabs__content::-webkit-scrollbar-thumb) {
border-radius: 999px;
background: rgba(200, 164, 93, 0.24);
}
.task-overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 16px;
}
.task-panel {
padding: 20px;
}
.task-panel--full {
grid-column: 1 / -1;
}
.task-panel__desc {
margin-top: 8px;
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.6;
}
.task-info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 16px;
}
.task-info-item {
padding: 14px 16px;
border: 1px solid #efe8d9;
border-radius: 16px;
background: #fcfaf5;
}
.task-info-item--full {
grid-column: 1 / -1;
}
.task-info-item__label {
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-info-item__value {
margin-top: 8px;
color: var(--admin-text-main);
font-size: 16px;
font-weight: 700;
line-height: 1.5;
word-break: break-word;
}
.task-info-item__value--inline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.task-pill-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.task-pill {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
background: rgba(72, 104, 133, 0.08);
color: var(--admin-progress);
font-size: 12px;
font-weight: 600;
}
.task-stage-progress {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-stage-progress__item {
display: flex;
align-items: center;
gap: 10px;
}
.task-stage-progress__item.is-current .task-stage-progress__name {
color: var(--admin-text-main);
}
.task-stage-progress__name {
min-width: 34px;
color: var(--admin-text-subtle);
font-size: 12px;
font-weight: 700;
}
.task-stage-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
margin-top: 16px;
}
.task-stage-card {
padding: 16px;
border: 1px solid #efe8d9;
border-radius: 18px;
background: #fcfaf5;
}
.task-stage-card.is-current {
box-shadow:
inset 0 0 0 1px rgba(72, 104, 133, 0.14),
0 10px 22px rgba(72, 104, 133, 0.08);
}
.task-stage-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.task-stage-card__meta {
margin-top: 10px;
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.6;
}
.task-empty {
margin-top: 16px;
padding: 18px;
border-radius: 16px;
background: #fcfaf5;
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.7;
}
.task-empty--compact {
margin-top: 12px;
padding: 14px;
}
.task-key-point-list {
display: grid;
gap: 14px;
margin-top: 14px;
}
.task-key-point-card {
padding: 16px;
border: 1px solid #efe8d9;
border-radius: 18px;
background: #fcfaf5;
}
.task-key-point-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
margin-bottom: 12px;
}
.task-key-point-card__title {
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
}
.task-key-point-card__required {
display: inline-flex;
align-items: center;
min-height: 22px;
margin-left: 8px;
padding: 0 8px;
border-radius: 999px;
background: rgba(182, 122, 45, 0.12);
color: var(--admin-warning);
font-size: 12px;
}
.task-key-point-card__code {
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-stream-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-material-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 16px;
margin-top: 16px;
}
.task-material-card {
padding: 16px;
border: 1px solid #efe8d9;
border-radius: 18px;
background: #fcfaf5;
}
.task-material-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.task-material-card__title {
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
}
.task-material-card__meta {
margin-top: 6px;
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-thumb-grid {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.task-thumb-grid--compact {
margin-top: 12px;
}
.task-timeline-list {
margin-top: 16px;
}
.task-workbench-grid {
display: grid;
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.task-workbench-aside {
display: flex;
flex-direction: column;
gap: 16px;
position: sticky;
top: 0;
align-self: start;
max-height: calc(100vh - 210px);
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.task-workbench-aside::-webkit-scrollbar {
width: 8px;
}
.task-workbench-aside::-webkit-scrollbar-thumb {
border-radius: 999px;
background: rgba(200, 164, 93, 0.26);
}
.task-workbench-aside::-webkit-scrollbar-track {
background: transparent;
}
.task-context-card {
padding: 16px;
border: 1px solid #efe8d9;
border-radius: 18px;
background: #fcfaf5;
}
.task-context-section {
padding: 2px 0 0;
}
.task-context-section__title {
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
line-height: 1.4;
}
.task-context-section__meta {
margin-top: 8px;
color: var(--admin-text-subtle);
font-size: 12px;
line-height: 1.6;
}
.task-context-card__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.task-context-card__title {
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
}
.task-context-card__meta {
margin-top: 6px;
color: var(--admin-text-subtle);
font-size: 12px;
line-height: 1.6;
}
.task-context-list {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin-top: 14px;
}
.task-context-list__item {
padding: 12px 14px;
border-radius: 14px;
background: #ffffff;
border: 1px solid rgba(239, 232, 217, 0.95);
}
.task-context-list__label {
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-context-list__value {
margin-top: 8px;
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
line-height: 1.5;
word-break: break-word;
}
.task-context-materials {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 14px;
}
.task-context-material {
padding: 14px;
border-radius: 16px;
background: #ffffff;
border: 1px solid rgba(239, 232, 217, 0.95);
}
.task-context-material__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.task-context-material__title {
color: var(--admin-text-main);
font-size: 14px;
font-weight: 700;
}
.task-context-material__meta {
margin-top: 6px;
color: var(--admin-text-subtle);
font-size: 12px;
line-height: 1.6;
}
.task-note-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
color: var(--admin-text-subtle);
font-size: 13px;
line-height: 1.6;
}
.task-work-tabs :deep(.el-tabs__header) {
margin: 0 0 18px;
}
.task-form-stack {
display: flex;
flex-direction: column;
gap: 16px;
}
.task-form-block {
padding: 18px;
border: 1px solid #efe8d9;
border-radius: 18px;
background: #fcfaf5;
}
.task-form-block__title {
color: var(--admin-text-main);
font-size: 15px;
font-weight: 700;
}
.task-material-tag-bound {
margin-top: 12px;
}
.task-evidence-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
.task-evidence-hint {
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-evidence-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.task-evidence-card {
display: grid;
grid-template-columns: 96px minmax(0, 1fr);
gap: 14px;
padding: 14px;
border-radius: 16px;
background: #ffffff;
border: 1px solid rgba(239, 232, 217, 0.95);
}
.task-evidence-card__preview {
width: 96px;
height: 96px;
border-radius: 14px;
border: 1px solid #efe8d9;
background: #fcfaf5;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: pointer;
}
.task-evidence-card__preview img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.task-evidence-card__filetype {
color: var(--admin-progress);
font-size: 13px;
font-weight: 700;
}
.task-evidence-card__body {
min-width: 0;
}
.task-evidence-card__name {
color: var(--admin-text-main);
font-size: 14px;
font-weight: 700;
line-height: 1.5;
word-break: break-word;
}
.task-evidence-card__meta {
margin-top: 6px;
color: var(--admin-text-subtle);
font-size: 12px;
}
.task-evidence-card__actions {
display: flex;
align-items: center;
gap: 12px;
margin-top: 10px;
}
.task-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin-top: 16px;
}
.task-form-field--full {
grid-column: 1 / -1;
}
.task-form-field__label {
margin-bottom: 8px;
color: var(--admin-text-subtle);
font-size: 13px;
}
.task-form-field__required {
margin-left: 6px;
color: #c2410c;
font-size: 12px;
font-weight: 600;
}
.task-readonly-value {
min-height: 32px;
padding: 7px 0;
color: var(--admin-text-main);
font-size: 14px;
font-weight: 700;
line-height: 1.5;
word-break: break-word;
}
.task-form-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.task-supplement-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}
.task-supplement-item {
padding: 14px;
border: 1px solid rgba(233, 226, 210, 0.96);
border-radius: 16px;
background: #ffffff;
}
.task-supplement-item__head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 12px;
align-items: center;
margin-bottom: 12px;
}
.task-supplement-actions {
margin-top: 12px;
}
@media (max-width: 1280px) {
.task-hero,
.task-workbench-grid {
grid-template-columns: 1fr;
display: grid;
}
.task-hero__side {
min-width: 0;
align-items: flex-start;
}
.task-workbench-aside {
position: static;
max-height: none;
overflow: visible;
}
.task-detail-toolbar {
align-items: stretch;
flex-direction: column;
}
.task-detail-toolbar__tabs {
gap: 18px;
flex-wrap: wrap;
}
.task-detail-toolbar__actions {
justify-content: flex-start;
min-width: 0;
}
.task-hero__tags,
.task-hero__summary {
justify-content: flex-start;
text-align: left;
}
}
@media (max-width: 960px) {
.appraisal-scan,
.task-info-grid,
.task-form-grid,
.task-supplement-item__head {
grid-template-columns: 1fr;
}
.task-evidence-card {
grid-template-columns: 1fr;
}
}
</style>