2683 lines
91 KiB
Vue
2683 lines
91 KiB
Vue
<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>
|