预览文件统一修改

This commit is contained in:
2026-10-13 12:04:41 +08:00
parent 3d765d2243
commit 0b4f8f3404
9 changed files with 692 additions and 686 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor, download
import { AiError, aiTypeJudge } from "@/service/api/ai";
import { systemConfigApi } from '@/service/api/system';
import { getChangeDetailApi } from '@/service/api/change';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
import { rejectApi,agreeApi } from '@/service/api/myTasks';
const themeStore = useThemeStore();
+402 -398
View File
@@ -9,7 +9,7 @@ import { uploadFileApi, deleteFileApi, fileListApi } from "@/service/api/file";
import { systemConfigApi, peopleListApi, templateListApi } from '@/service/api/system';
import { getChangeDetailApi } from '@/service/api/change';
import { myTasksCloseMaterialApi } from '@/service/api/myTasks';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
@@ -170,7 +170,7 @@ const chooseFiles = () => {
const ids = new Set(checkedFiles.value);
const result = fileList.value.filter((item:any) => ids.has(item.id));
// 合并文件列表
let files = list.value[currentRowIndex.value]?.attachments.concat(result);
const files = list.value[currentRowIndex.value]?.attachments.concat(result);
// 按id去重
const uniqueList = [
...new Map(files.map((item:any) => [item.id, item])).values(),
@@ -349,7 +349,7 @@ const togglePicker = (s: any) => {
// 添加审批步骤人员
const addMember = (s: any, v: string | null) => {
if (v && !s.members.some((m:any) => m.user_name === v)){
let info = findUserInTree(memberList.value, v);
const info = findUserInTree(memberList.value, v);
s.members.push({ user_id: Number(v), user_name: info?.user_name || '', org_id: info?.org_id || '', org_name: info?.org_name || '', specialty: "", is_sign: 0 });
pick.value = null;
pickerStepId.value = null;
@@ -397,411 +397,415 @@ onMounted(async () => {
</script>
<template>
<NSpace vertical :size="16">
<NSpace vertical :size="16">
<!-- 资料关闭详情-->
<NSpin :show="loading" size="small">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton ghost type="primary" @click="back">
<Icon icon="ep:back" class="size-16px mr-1" />返回
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div
class="flex flex-wrap items-center gap-3 border border-slate-200 px-3 py-2 mb-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
<NButton ghost type="primary" @click="back">
<Icon icon="ep:back" class="size-16px mr-1" />返回
</NButton>
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
<NTag size="small" type="info" class="font-mono text-xs">编号:{{ currentInfo?.change_no }}</NTag>
</div>
<div class="space-y-3 scroll">
<!-- 投用前确认 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">投用前确认(含变更培训、投用前检查)</p>
<span class="text-slate-400 text-xs">上传线下纸质表照片 / 扫描件即视为完成确认,上传后可查阅、下载;空白模板可下载</span>
</div>
<div class="p-3 space-y-3">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: trainFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传培训签到</span>
<p v-if="trainFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-1 leading-4">线下培训签到表照片或扫描件;上传后视为培训完成确认,签到记录随单归档</p>
<div v-if="trainFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in trainFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{ file.file_name }}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'train')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="removeFileReal('3', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'train')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载培训签到表
</NButton>
<div class="min-w-0 flex-1 truncate text-base font-bold text-slate-800">{{ currentInfo?.title }}</div>
<NTag size="small" type="info" class="font-mono text-xs">编号:{{ currentInfo?.change_no }}</NTag>
<NUpload
:key="trainFiles.length?'train_'+trainFiles.length+1:'train'"
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('3', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: pssrFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传 PSSR 检查</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-1 leading-4">PSSR 纸质确认表照片或扫描件;上传后视为 PSSR 全项确认完成,具备投用条件</p>
<div v-if="pssrFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in pssrFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{ file.file_name }}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'pssr')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="removeFileReal('4', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'pssr')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载PSSR检查表
</NButton>
<NUpload
:key="pssrFiles.length?'pssr_'+pssrFiles.length+1:'pssr'"
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('4', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
</div>
<div class="space-y-3 scroll">
<!-- 投用前确认 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">投用前确认(含变更培训、投用前检查)</p>
<span class="text-slate-400 text-xs">上传线下纸质表照片 / 扫描件即视为完成确认,上传后可查阅、下载;空白模板可下载</span>
</div>
<div class="p-3 space-y-3">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: trainFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传培训签到</span>
<p v-if="trainFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-1 leading-4">线下培训签到表照片或扫描件;上传后视为培训完成确认,签到记录随单归档</p>
<div v-if="trainFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in trainFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'train')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFileReal('3', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'train')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载培训签到表
</NButton>
<NUpload
class="w-auto"
:key="trainFiles.length?'train_'+trainFiles.length+1:'train'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('3', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: pssrFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传 PSSR 检查</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-1 leading-4">PSSR 纸质确认表照片或扫描件;上传后视为 PSSR 全项确认完成,具备投用条件</p>
<div v-if="pssrFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in pssrFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'pssr')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFileReal('4', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'pssr')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载PSSR检查表
</NButton>
<NUpload
class="w-auto"
:key="pssrFiles.length?'pssr_'+pssrFiles.length+1:'pssr'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('4', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
</div>
</div>
<!-- 变更投用时间 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更投用时间</p>
</div>
<div class="p-3">
<div class="flex items-center gap-2">
<p class="text-sm text-slate-600">实际投用时间</p>
<NDatePicker v-model:value="useDate" type="date" size="small" />
<p class="text-xs text-slate-400 mt-1px">默认为当天日期,可按实际投用日期调整</p>
</div>
<div class="mt-2.5 rounded-lg border px-3 py-2 text-[11px] leading-5 border-amber-200 bg-amber-50/60 text-amber-700"> 投用提示:投用前应完成 PSSR 检查。 未完成的变更不得投用。</div>
</div>
</div>
<!-- 变更验收 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更验收</p>
<span class="text-slate-400 text-xs">投用后组织验收:上传验收资料并选择验收结论;上传后可查阅、下载;验收表可下载</span>
</div>
<div class="p-3 space-y-2.5">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: acceptFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传验收资料</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-0.5 leading-4">验收报告、运行数据记录等验收证明材料;上传后纳入归档清单</p>
<div v-if="acceptFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in acceptFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'accept')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFileReal('5', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'accept')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载验收表
</NButton>
<NUpload
class="w-auto"
:key="acceptFiles.length?'accept_'+acceptFiles.length+1:'accept'"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('5', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex flex-col gap-2">
<div class="flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收结论(互斥选择)</p>
<NButton size="small" :type="resultStatus === 3?'primary':'default'" class="text-xs" @click="resultStatus = 3">完全达到</NButton>
<NButton size="small" :type="resultStatus === 2?'primary':'default'" class="text-xs" @click="resultStatus = 2">基本达到</NButton>
<NButton size="small" :type="resultStatus === 1?'primary':'default'" class="text-xs" @click="resultStatus = 1">未达到</NButton>
</div>
<NInput v-model:value="acceptDesc" type="textarea" size="small" :autosize="{ minRows: 2 }" placeholder="请输入验收结论" />
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">变更材料和实施费(万元)</p>
<NInputNumber v-model:value="price" size="small" :min="0" :show-button="false" placeholder="如 12.6" />
<p class="text-xs text-slate-400 mt-1px">非必填 · 填报本次变更材料与实施费用合计,用于变更投入与效果统计</p>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收时间</p>
<NDatePicker v-model:value="acceptDate" type="date" size="small" />
</div>
</div>
</div>
<!-- 变更关闭资料归档确认 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更关闭资料归档确认</p>
<span class="text-slate-400 text-xs">涉及(本地上传或从变更附件选择)· 不涉及(无需处理)</span>
<NButton v-if="isAiOn" size="small" type="primary" ghost class="ml-auto text-xs">AI辅助判定</NButton>
</div>
<div class="p-3">
<NTable size="small">
<thead>
<tr>
<th>资料名称</th>
<th class="!text-center">是否涉及</th>
<th class="w-200px !text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="index">
<td>
<p>{{item.doc_name}}</p>
<div v-if="item.attachments.length" class="flex flex-wrap gap-2">
<div
v-for="(file, fileIndex) in item.attachments"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{file.file_name}}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'close')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="removeFileReal(item.doc_name, Number(fileIndex), Number(index))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</td>
<td>
<div class="flex justify-center gap-2">
<NButton size="small" :type="item.status === 1 ? 'primary' : 'default'" class="text-xs" @click="item.status = 1">涉及</NButton>
<NButton size="small" :type="item.status === 2 ? 'primary' : 'default'" class="text-xs" @click="item.status = 2">不涉及</NButton>
</div>
</td>
<td>
<div v-if="item.status === 1" class="flex justify-center gap-2">
<NButton size="small" class="text-xs" @click="openFileList(Number(index))">从变更附件选择</NButton>
<NUpload
class="w-auto"
:key="item.id"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile(item.doc_name, files, Number(index))"
>
<NButton type="primary" size="small" ghost class="text-xs" :disabled="loading">
本地上传
</NButton>
</NUpload>
</div>
<div v-if="item.status === 2" class="flex justify-end gap-2">
<span class="text-xs text-slate-400">无需处理</span>
</div>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<!-- 变更投用时间 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更投用时间</p>
</div>
<div class="flex items-center justify-end border-t pt-3 mt-3">
<div class="flex gap-2">
<NButton size="small" @click="approvalDrawer = true">审批流程设置</NButton>
<NButton ghost size="small" @click="back">取消</NButton>
<NButton ghost type="primary" size="small" @click="submitCloseDoc('save')">保存</NButton>
<NButton type="primary" size="small" @click="submitCloseDoc('close')">申请关闭</NButton>
</div>
<div class="p-3">
<div class="flex items-center gap-2">
<p class="text-sm text-slate-600">实际投用时间</p>
<NDatePicker v-model:value="useDate" type="date" size="small" />
<p class="text-xs text-slate-400 mt-1px">默认为当天日期,可按实际投用日期调整</p>
</div>
<div class="mt-2.5 rounded-lg border px-3 py-2 text-[11px] leading-5 border-amber-200 bg-amber-50/60 text-amber-700"> 投用提示:投用前应完成 PSSR 检查。 未完成的变更不得投用。</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawer" placement="right" width="28rem" :auto-focus="false">
<NDrawerContent>
<template #header>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
<div v-for="(s, idx) in flowList" :key="s.id" class="rounded-lg border border-slate-100 bg-slate-50/40 p-3">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{Number(idx)+1}}</NTag>
<p class="text-sm">{{s.name}}</p>
<NCheckbox :checked="s.countersign" size="small" @update:checked="(v) => setStepCountersign(s, v)">
<span class="text-xs text-slate-500">专业会签步骤</span>
</NCheckbox>
<NButton ghost type="primary" size="tiny" @click="togglePicker(s)" title="添加预设人员" class="ml-auto">
<Icon icon="ic:round-plus" class="size-16px" />
</NButton>
</div>
<!-- 预设人员:人员必选;仅会签步骤可选择专业(必选),且人员可勾选「必须进行专业会签」 -->
<div class="mt-2 flex gap-2 pl-8">
<span class="shrink-0 text-xs leading-7 text-slate-400">预设人员:</span>
<div class="min-w-0 flex-1 space-y-1.5">
<div v-for="(m, mIdx) in s.members" :key="m.id" class="flex min-h-7 flex-wrap items-center gap-2">
<span class="shrink-0 truncate text-xs font-medium text-slate-700">{{ m.user_name }}</span>
<template v-if="s.countersign">
<span class="text-xs text-slate-400"><span class="text-red-500">*</span>专业:</span>
<NSelect :value="m.specialty || null" clearable size="small" placeholder="请选择专业" class="w-28"
:options="SPECIALTIES"
@update:value="(v) => selectSpecialty(Number(idx), Number(mIdx), v)"
/>
<NCheckbox :checked="m.is_sign" size="small"
:checked-value="1"
:unchecked-value="0"
@update:checked="(v) => selectSign(Number(idx), Number(mIdx), v)"
>
<span class="text-xs text-slate-500">必须参与会签</span>
</NCheckbox>
</template>
<NButton text type="error" @click="delMember(s, m.user_id)" title="删除该人员">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</div>
<p v-if="s.countersign && s.members.length" class="text-[11px] leading-5 text-slate-400">
必须参与会签:勾选的人员必须参与专业会签,未勾选的人员为可选会签(按需参加)
</p>
<NCascader v-if="pickerStepId === s.id" :value="pick" :options="memberList"
placeholder="请选择人员" size="small" check-strategy="child" :show-path="false" expand-trigger="click"
class="w-56" :on-update:value="(v) => addMember(s, v)" />
</div>
<!-- 变更验收 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更验收</p>
<span class="text-slate-400 text-xs">投用后组织验收:上传验收资料并选择验收结论;上传后可查阅、下载;验收表可下载</span>
</div>
<div class="p-3 space-y-2.5">
<div class="border rounded-lg py-3 px-4 flex items-center gap-3">
<Icon icon="bi:upload" class="size-18px" :style="{ color: acceptFiles.length?themeStore.otherColor.success:themeStore.themeColor }" />
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="text-slate-800 text-sm font-600">上传验收资料</span>
<p v-if="pssrFiles.length" class="py-0.5 px-1.5 text-11px bg-emerald-100 rounded-md text-emerald-700">已上传 · 视为完成确认</p>
</div>
<p class="text-slate-400 text-xs mt-0.5 leading-4">验收报告、运行数据记录等验收证明材料;上传后纳入归档清单</p>
<div v-if="acceptFiles.length" class="flex flex-wrap gap-2 mt-1">
<div
v-for="(file, fileIndex) in acceptFiles"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{ file.file_name }}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'accept')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="removeFileReal('5', Number(fileIndex))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</div>
<NButton ghost type="primary" size="small" class="text-xs" @click="downloadFile(null,'accept')">
<Icon icon="akar-icons:download" class="size-14px mr-1" />
下载验收表
</NButton>
<NUpload
:key="acceptFiles.length?'accept_'+acceptFiles.length+1:'accept'"
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile('5', files)"
>
<NButton type="primary" size="small" class="text-xs" :disabled="loading">上传</NButton>
</NUpload>
</div>
<div class="border rounded-lg py-3 px-4 flex flex-col gap-2">
<div class="flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收结论(互斥选择)</p>
<NButton size="small" :type="resultStatus === 3?'primary':'default'" class="text-xs" @click="resultStatus = 3">完全达到</NButton>
<NButton size="small" :type="resultStatus === 2?'primary':'default'" class="text-xs" @click="resultStatus = 2">基本达到</NButton>
<NButton size="small" :type="resultStatus === 1?'primary':'default'" class="text-xs" @click="resultStatus = 1">未达到</NButton>
</div>
<NInput v-model:value="acceptDesc" type="textarea" size="small" :autosize="{ minRows: 2 }" placeholder="请输入验收结论" />
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">变更材料和实施费(万元)</p>
<NInputNumber v-model:value="price" size="small" :min="0" :show-button="false" placeholder="如 12.6" />
<p class="text-xs text-slate-400 mt-1px">非必填 · 填报本次变更材料与实施费用合计,用于变更投入与效果统计</p>
</div>
<div class="border rounded-lg py-3 px-4 flex items-center gap-2">
<p class="text-xs font-600" :style="{ color: themeStore.themeColor }">验收时间</p>
<NDatePicker v-model:value="acceptDate" type="date" size="small" />
</div>
</div>
</div>
<!-- 变更关闭资料归档确认 -->
<div class="border rounded-md">
<div class="flex items-center gap-x-3 px-4 py-2.5 border-b">
<p class="font-600 text-slate-800 text-sm">变更关闭资料归档确认</p>
<span class="text-slate-400 text-xs">涉及(本地上传或从变更附件选择)· 不涉及(无需处理)</span>
<NButton v-if="isAiOn" size="small" type="primary" ghost class="ml-auto text-xs">AI辅助判定</NButton>
</div>
<div class="p-3">
<NTable size="small">
<thead>
<tr>
<th>资料名称</th>
<th class="!text-center">是否涉及</th>
<th class="w-200px !text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in list" :key="index">
<td>
<p>{{ item.doc_name }}</p>
<div v-if="item.attachments.length" class="flex flex-wrap gap-2">
<div
v-for="(file, fileIndex) in item.attachments"
:key="fileIndex"
class="text-11px flex items-center gap-2 border py-0.5 px-1.5 rounded-md text-[#17407F] border-[#ACC6E5] bg-[#EAF0F9]"
>
<Icon icon="akar-icons:file" class="size-12px" />
{{ file.file_name }}
<NButton text type="primary" class="text-11px" @click="preview(file)">预览</NButton>
<NButton text type="primary" class="text-11px" @click="downloadFile(file,'close')">下载</NButton>
<NPopconfirm
positive-text="确定"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="removeFileReal(item.doc_name, Number(fileIndex), Number(index))"
>
<template #trigger>
<NButton text type="error" class="text-11px">删除</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
</div>
</td>
<td>
<div class="flex justify-center gap-2">
<NButton size="small" :type="item.status === 1 ? 'primary' : 'default'" class="text-xs" @click="item.status = 1">涉及</NButton>
<NButton size="small" :type="item.status === 2 ? 'primary' : 'default'" class="text-xs" @click="item.status = 2">不涉及</NButton>
</div>
</td>
<td>
<div v-if="item.status === 1" class="flex justify-center gap-2">
<NButton size="small" class="text-xs" @click="openFileList(Number(index))">从变更附件选择</NButton>
<NUpload
:key="item.id"
class="w-auto"
accept="image/*,.pdf,.docx,.xlsx,.pptx"
:default-upload="false"
:show-file-list="false"
@change="(files) => onUploadFile(item.doc_name, files, Number(index))"
>
<NButton type="primary" size="small" ghost class="text-xs" :disabled="loading">
本地上传
</NButton>
</NUpload>
</div>
<div v-if="item.status === 2" class="flex justify-end gap-2">
<span class="text-xs text-slate-400">无需处理</span>
</div>
</td>
</tr>
</tbody>
</NTable>
</div>
</div>
</div>
<div class="flex items-center justify-end border-t pt-3 mt-3">
<div class="flex gap-2">
<NButton size="small" @click="approvalDrawer = true">审批流程设置</NButton>
<NButton ghost size="small" @click="back">取消</NButton>
<NButton ghost type="primary" size="small" @click="submitCloseDoc('save')">保存</NButton>
<NButton type="primary" size="small" @click="submitCloseDoc('close')">申请关闭</NButton>
</div>
</div>
<!-- 审批流程设置 -->
<NDrawer v-model:show="approvalDrawer" placement="right" width="28rem" :auto-focus="false">
<NDrawerContent>
<template #header>
<div class="text-base font-semibold text-slate-800">审批流程设置</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<p class="text-xs leading-relaxed text-slate-400">审批链:{{ approvalFlowText }}。专业会签人由申请表「涉及专业」勾选自动增减,可在此调整人选;重要变更自动追加安全审查与分管领导终批。</p>
<div v-for="(s, idx) in flowList" :key="s.id" class="rounded-lg border border-slate-100 bg-slate-50/40 p-3">
<div class="flex flex-wrap items-center gap-2">
<NTag type="info" size="small">{{ Number(idx)+1 }}</NTag>
<p class="text-sm">{{ s.name }}</p>
<NCheckbox :checked="s.countersign" size="small" @update:checked="(v) => setStepCountersign(s, v)">
<span class="text-xs text-slate-500">专业会签步骤</span>
</NCheckbox>
<NButton ghost type="primary" size="tiny" title="添加预设人员" class="ml-auto" @click="togglePicker(s)">
<Icon icon="ic:round-plus" class="size-16px" />
</NButton>
</div>
<!-- 预设人员:人员必选;仅会签步骤可选择专业(必选),且人员可勾选「必须进行专业会签」 -->
<div class="mt-2 flex gap-2 pl-8">
<span class="shrink-0 text-xs leading-7 text-slate-400">预设人员:</span>
<div class="min-w-0 flex-1 space-y-1.5">
<div v-for="(m, mIdx) in s.members" :key="m.id" class="flex min-h-7 flex-wrap items-center gap-2">
<span class="shrink-0 truncate text-xs font-medium text-slate-700">{{ m.user_name }}</span>
<template v-if="s.countersign">
<span class="text-xs text-slate-400"><span class="text-red-500">*</span>专业:</span>
<NSelect
:value="m.specialty || null" clearable size="small" placeholder="请选择专业" class="w-28"
:options="SPECIALTIES"
@update:value="(v) => selectSpecialty(Number(idx), Number(mIdx), v)"
/>
<NCheckbox
:checked="m.is_sign" size="small"
:checked-value="1"
:unchecked-value="0"
@update:checked="(v) => selectSign(Number(idx), Number(mIdx), v)"
>
<span class="text-xs text-slate-500">必须参与会签</span>
</NCheckbox>
</template>
<NButton text type="error" title="删除该人员" @click="delMember(s, m.user_id)">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</div>
<p v-if="s.countersign && s.members.length" class="text-[11px] leading-5 text-slate-400">
必须参与会签:勾选的人员必须参与专业会签,未勾选的人员为可选会签(按需参加)
</p>
<NCascader
v-if="pickerStepId === s.id" :value="pick" :options="memberList"
placeholder="请选择人员" size="small" check-strategy="child" :show-path="false" expand-trigger="click"
class="w-56" :on-update:value="(v) => addMember(s, v)"
/>
<span v-if="s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span>
</div>
</div>
</div>
</div>
<template #footer>
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</template>
</NDrawerContent>
</NDrawer>
<!-- 附件选择弹框 -->
<NModal
v-model:show="filesModal"
preset="card"
title="从变更附件选择"
:auto-focus="false"
:style="{ width: '450px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div v-if="fileList.length" class="max-h-60vh overflow-y-auto">
<NCheckboxGroup v-model:value="checkedFiles">
<div class="space-y-2">
<div v-for="item in fileList" :key="item.id">
<NCheckbox :value="item.id" :label="item.file_name" />
</div>
</div>
</NCheckboxGroup>
<span v-if="s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span>
</div>
</div>
<div v-else>
<NEmpty description="暂无附件"></NEmpty>
</div>
</div>
<template #footer>
<NButton v-if="flowList.length" type="primary" class="w-full" @click="finishApprover">完成设置</NButton>
</template>
</NDrawerContent>
</NDrawer>
<!-- 附件选择弹框 -->
<NModal
v-model:show="filesModal"
preset="card"
title="从变更附件选择"
:auto-focus="false"
:style="{ width: '450px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<div v-if="fileList.length" class="max-h-60vh overflow-y-auto">
<NCheckboxGroup v-model:value="checkedFiles">
<div class="space-y-2">
<div v-for="item in fileList" :key="item.id">
<NCheckbox :value="item.id" :label="item.file_name" />
</div>
<template v-if="fileList.length" #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="filesModal = false">取消</NButton>
<NButton size="small" type="primary" @click="chooseFiles">确定选择</NButton>
</div>
</template>
</NModal>
<!-- 附件在线预览弹窗 -->
<NModal
v-model:show="previewModal"
preset="card"
:auto-focus="false"
:style="{ width: '80%', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
</div>
</template>
<template #default>
<div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
<span>上传人:{{ previewFile?.uploader_name }}</span>
<span>上传时间:{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
</div>
<previewComponent v-if="previewFile" :previewFile="previewFile" />
</div>
</template>
</NModal>
</NCard>
</div>
</NCheckboxGroup>
</div>
<div v-else>
<NEmpty description="暂无附件"></NEmpty>
</div>
<template v-if="fileList.length" #footer>
<div class="flex justify-end gap-[10px]">
<NButton size="small" @click="filesModal = false">取消</NButton>
<NButton size="small" type="primary" @click="chooseFiles">确定选择</NButton>
</div>
</template>
</NModal>
<!-- 附件在线预览弹窗 -->
<NModal
v-model:show="previewModal"
preset="card"
:auto-focus="false"
:style="{ width: '80%', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
</div>
</template>
<template #default>
<div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
<span>上传人:{{ previewFile?.uploader_name }}</span>
<span>上传时间:{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
</div>
<PreviewComponent v-if="previewFile" :preview-file="previewFile" />
</div>
</template>
</NModal>
</NCard>
</NSpin>
</NSpace>
</NSpace>
</template>
<style scoped lang="scss">
+1 -1
View File
@@ -9,7 +9,7 @@ import { AiError, aiTypeJudge } from "@/service/api/ai";
import { systemConfigApi, peopleListApi, templateListApi } from '@/service/api/system';
import { getChangeDetailApi, urgeApi } from '@/service/api/change';
import { myTasksDisposalSubmitApi, myTasksAddItemApi } from '@/service/api/myTasks';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
+1 -1
View File
@@ -8,7 +8,7 @@ import { tagTextColor, tagBgColor, tagBorderColor, downloadFileFun } from '@/uti
import { AiError, aiTypeJudge } from "@/service/api/ai";
import { systemConfigApi } from '@/service/api/system';
import { getChangeDetailApi, urgeApi } from '@/service/api/change';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
+1 -1
View File
@@ -3,7 +3,7 @@ import { ref, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { NButton, NTag, NDropdown, useDialog } from 'naive-ui';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
import { downloadFileFun } from '@/utils/common';
import {
docTreeListApi,
+1 -1
View File
@@ -3,7 +3,7 @@ import { ref, onMounted, h } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { formatTimestamp, getTimeRangeStamp, downloadFileFun } from "@/utils/common";
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
import { systemConfigApi } from '@/service/api/system';
import { orgListApi } from '@/service/api/user';
import { docArchiveTreeListApi, docArchiveDetailsApi } from "@/service/api/doc";
+1 -1
View File
@@ -6,7 +6,7 @@ import { useRouterPush } from '@/hooks/common/router';
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor, downloadFileFun } from '@/utils/common';
import { systemConfigApi } from "@/service/api/system";
import { homeSafeApi } from "@/service/api/statistics";
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
@@ -8,7 +8,7 @@ import { localStg } from '@/utils/storage';
import { AiError, aiTypeJudge, aiLevelScore, aiApplication } from "@/service/api/ai";
import { uploadFileApi, deleteFileApi } from "@/service/api/file";
import Footer from './footer.vue'
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
const themeStore = useThemeStore();
@@ -232,7 +232,7 @@ const filterMembersBySpecialties = (steps: any, specialties: string[]) => {
}
// 更新当前模板步骤
const updateCurrentTemplate = () => {
let template = props.templateList.find((item: any) =>
const template = props.templateList.find((item: any) =>
item.level === form.value?.change_level &&
item.category === form.value?.change_type &&
item.duration === form.value?.duration_type
@@ -266,7 +266,7 @@ const typeAi = ref<{ type: string; confidence: number; conclusion: string; hit_r
const typeAiLoading = ref<boolean>(false);
// 打开变更类型抽屉
const openTypeSheet = async () => {
let form = props.formInfo;
const form = props.formInfo;
if (!form?.description) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
@@ -296,7 +296,7 @@ const levelAiBasis = ref<string[]>([]);
const levelAiLoading = ref<boolean>(false);
// 打开等级判定表抽屉
const openLevelSheet = async () => {
let form = props.formInfo;
const form = props.formInfo;
if (!form?.description && props.isAiOpen) {
return window.$message?.warning("缺少变更描述,请先在 [变更预识别] 里面输入变更描述");
}
@@ -305,7 +305,7 @@ const openLevelSheet = async () => {
};
// 使用AI 评分
const useAiLevel = async () => {
let form = props.formInfo;
const form = props.formInfo;
if (levelAiLoading.value) return;
levelAiLoading.value = true;
try {
@@ -447,82 +447,82 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<!-- 申请部门 / 申请人 -->
<div class="grid gap-4 grid-cols-2">
<div class="p-4">
<div class="space-y-4 scroll">
<!-- 申请部门 / 申请人 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
<NTreeSelect
v-model:value="form.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择申请部门"
/>
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请部门 <span class="text-red-500">*</span></div>
<NTreeSelect
v-model:value="form.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择申请部门"
/>
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
<NInput :value="userInfo?.real_name" disabled />
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">申请人</div>
<NInput :value="userInfo?.real_name" disabled />
</div>
</div>
<!-- 变更目的 -->
<div class="flex gap-3">
</div>
<!-- 变更目的 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更目的</div>
<div class="rounded-lg bg-[#EAF0F9]/40 p-3">
<NCheckboxGroup v-model:value="form.purposes">
<NCheckboxGroup v-model:value="form.purposes">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
<NCheckbox v-for="p in PURPOSE_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
</NCheckboxGroup>
</div>
</div>
<!-- 预期效果 -->
<div class="flex gap-3">
</div>
<!-- 预期效果 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">预期效果</div>
<NInput type="textarea" v-model:value="form.effect" rows="2" placeholder="" />
</div>
<!-- 主要变更内容 -->
<div class="flex gap-3">
<NInput v-model:value="form.effect" type="textarea" rows="2" placeholder="" />
</div>
<!-- 主要变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">主要变更内容 <span class="text-red-500">*</span></div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.main_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.main_changes[i]" type="text" placeholder="" />
<NPopconfirm
v-if="form.main_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="form.main_changes.splice(i, 1)"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.main_changes = [...form.main_changes, '']">
<div v-for="(_, i) in form.main_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.main_changes[i]" type="text" placeholder="" />
<NPopconfirm
v-if="form.main_changes.length > 1"
positive-text="确定"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="form.main_changes.splice(i, 1)"
>
<template #trigger>
<NButton text type="error">
<Icon icon="ci:close-circle" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.main_changes = [...form.main_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加变更点
</NButton>
</NButton>
</div>
</div>
<!-- 连带变更内容 -->
<div class="flex gap-3">
</div>
<!-- 连带变更内容 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">连带变更内容</div>
<div class="flex-1 space-y-2">
<div v-for="(_, i) in form.related_changes" :key="i" class="flex items-center gap-2">
<div v-for="(_, i) in form.related_changes" :key="i" class="flex items-center gap-2">
<span class="w-5 shrink-0 text-sm text-slate-400">{{ i + 1 }}.</span>
<NInput v-model:value="form.related_changes[i]" placeholder="连带变更点(如:更新操作规程)" />
<NPopconfirm
v-if="form.related_changes.length > 1"
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="form.related_changes.splice(i, 1)"
>
<template #trigger>
@@ -532,108 +532,109 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
</template>
确定删除吗?
</NPopconfirm>
</div>
<NButton text type="primary" class="text-xs" @click="form.related_changes = [...form.related_changes, '']">
</div>
<NButton text type="primary" class="text-xs" @click="form.related_changes = [...form.related_changes, '']">
<Icon icon="ic:round-plus" class="size-14px" /> 添加连带变更点
</NButton>
</NButton>
</div>
</div>
<!-- 变更类型 / 变更等级 -->
<div class="grid gap-4 grid-cols-2">
</div>
<!-- 变更类型 / 变更等级 -->
<div class="grid gap-4 grid-cols-2">
<!-- 变更类型 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更类型 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_type">
<div class="flex gap-x-2">
<NRadio v-for="t in props.typeList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
</div>
<div class="flex gap-x-2">
<NRadio v-for="t in props.typeList" :key="t.value" :value="t.value">{{ t.label }}</NRadio>
</div>
</NRadioGroup>
<NButton v-if="props.isAiOpen" text type="primary" class="ml-auto text-xs hover:underline" @click="openTypeSheet">
<Icon icon="lucide:sparkles" class="size-12px" />
AI 判定说明
</NButton>
</div>
</div>
</div>
<!-- 变更等级 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更等级 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.change_level" disabled>
<div class="flex gap-x-2">
<NRadio v-for="t in props.levelList" :key="t.value" :value="t.value">{{t.label}}</NRadio>
</div>
<div class="flex gap-x-2">
<NRadio v-for="t in props.levelList" :key="t.value" :value="t.value">{{ t.label }}</NRadio>
</div>
</NRadioGroup>
<NButton text type="primary" class="ml-auto text-xs hover:underline" @click="openLevelSheet">
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
<Icon icon="lucide:list-checks" class="size-12px" /> 等级判定表 ({{ totalScore }}分) →
</NButton>
</div>
</div>
</div>
</div>
<!-- 变更时限 / 风险分析 -->
<div class="grid gap-4 grid-cols-2">
</div>
<!-- 变更时限 / 风险分析 -->
<div class="grid gap-4 grid-cols-2">
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">变更时限 <span class="text-red-500">*</span></div>
<div class="flex flex-1 items-center gap-4 rounded-lg bg-slate-50 px-3 py-2">
<NRadioGroup v-model:value="form.duration_type">
<div class="flex gap-x-2">
<div class="flex gap-x-2">
<NRadio v-for="t in [1, 2]" :key="t" :value="t">
{{ t === 1 ? '永久' : '临时' }}
{{ t === 1 ? '永久' : '临时' }}
</NRadio>
</div>
</div>
</NRadioGroup>
<NCheckbox v-model:checked="form.urgent" :checked-value="1" :unchecked-value="0">紧急</NCheckbox>
</div>
</div>
</div>
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
<NButton size="small" round v-for="t in RISK_TOOLS_FORM" :key="t.key" @click="toggleTool(t)" title="点击选用 / 取消;AI 辅助分析在各工具插件内使用"
class="text-sm"
:ghost="form.risk_tools?.includes(t.key) ? false : true"
:type="form.risk_tools?.includes(t.key) ? 'primary' : 'default'"
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">风险分析</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg bg-slate-50 px-3 py-2">
<NButton
v-for="t in RISK_TOOLS_FORM" :key="t.key" size="small" round title="点击选用 / 取消;AI 辅助分析在各工具插件内使用" class="text-sm"
:ghost="form.risk_tools?.includes(t.key) ? false : true"
:type="form.risk_tools?.includes(t.key) ? 'primary' : 'default'"
@click="toggleTool(t)"
>
{{ t.label }}
{{ t.label }}
</NButton>
<NButton type="primary" ghost dashed size="small" round @click="recommendTools" class="text-xs">
按等级重新推荐
<NButton type="primary" ghost dashed size="small" round class="text-xs" @click="recommendTools">
按等级重新推荐
</NButton>
</div>
</div>
</div>
</div>
<div v-if="form.urgent" class="text-xs text-red-500 ml-27">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
<!-- 计划投用时间 -->
<div class="flex gap-3">
</div>
<div v-if="form.urgent" class="text-xs text-red-500 ml-27">紧急变更为单独标记:走快速通道,事后限期补办风险分析与完整审批</div>
<!-- 计划投用时间 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">计划投用时间</div>
<div class="flex flex-1 flex-wrap items-center gap-2">
<NDatePicker v-model:value="form.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<!-- 临时 -->
<template v-if="form.duration_type === 2">
<NDatePicker v-model:value="form.plan_use_date" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<!-- 临时 -->
<template v-if="form.duration_type === 2">
<span class="whitespace-nowrap text-sm text-slate-500">计划恢复时间 <span class="text-red-500">*</span></span>
<NDatePicker v-model:value="form.restore_deadline" type="date" value-format="yyyy-MM-dd" class="!max-w-[220px]" />
<span class="text-xs text-amber-600">临时变更到期系统自动提醒恢复原状</span>
</template>
<!-- 永久 -->
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
</template>
<!-- 永久 -->
<span v-else class="whitespace-nowrap text-xs text-slate-300">(勾选「临时」后在此并排填写计划恢复时间)</span>
</div>
</div>
</div>
<!-- 所需材料 -->
<div class="flex gap-3">
<!-- 所需材料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">所需材料</div>
<div class="flex-1 space-y-2">
<div class="text-[11px] text-slate-400">施工所需材料(生成变更申请表时系统自动生成,支持手动修改)</div>
<NInput type="textarea" v-model:value="form.materials" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
<div class="text-[11px] text-slate-400">施工所需材料(生成变更申请表时系统自动生成,支持手动修改)</div>
<NInput v-model:value="form.materials" type="textarea" rows="4" placeholder="点击「生成变更申请单」后由系统按变更内容自动生成,也可直接填写" />
</div>
</div>
<!-- 资料上传 -->
<div class="flex gap-3">
</div>
<!-- 资料上传 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600 text-right">资料上传</div>
<div class="flex flex-1 flex-wrap items-center gap-2 rounded-lg border border-dashed p-3">
<div
class="relative"
:style="{'--theme-color':themeStore.themeColor}"
v-for="(f,index) in uploadFile" :key="f.id"
v-for="(f,index) in uploadFile"
:key="f.id"
class="relative" :style="{'--theme-color':themeStore.themeColor}"
>
<div
class="text-xs cursor-pointer flex items-center border py-1.5 px-2 rounded-md hover:border-[var(--theme-color)] hover:text-[var(--theme-color)]"
@@ -644,8 +645,8 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
</div>
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
:positive-button-props="{ size: 'tiny' }"
:negative-button-props="{ size: 'tiny' }"
@positive-click="removeFile(index)"
>
<template #trigger>
@@ -664,102 +665,102 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
<input ref="uploadFileRef" type="file" accept="image/*,.pdf,.docx,.xlsx" class="hidden" @change="onUploadFile" />
<span class="text-xs text-slate-400">(点击附件名弹窗预览,支持 PDF / Word / 图片在线查看)</span>
</div>
</div>
<!-- 需更新的资料 -->
<div class="flex gap-3">
</div>
<!-- 需更新的资料 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">需更新的资料</div>
<div class="flex flex-1 flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.update_docs">
<NCheckboxGroup v-model:value="form.update_docs">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in UPDATE_DOC_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">(多选,变更关闭前逐项确认上传最新版本)</span>
</div>
</div>
<!-- 涉及专业 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.disciplines">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in specialties" :key="p" :value="p" :label="p" />
<NCheckbox v-for="p in UPDATE_DOC_OPTIONS" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
<span class="self-center text-xs text-slate-400">(多选,变更关闭前逐项确认上传最新版本)</span>
</div>
<div v-if="signers && signers.length" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
</div>
<!-- 涉及专业 -->
<div class="flex gap-3">
<div class="w-24 shrink-0 pt-2 text-sm font-medium text-slate-600">涉及专业</div>
<div class="flex-1">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1.5 rounded-lg bg-slate-50 px-3 py-2">
<NCheckboxGroup v-model:value="form.disciplines">
<div class="flex flex-wrap gap-y-2 gap-x-4">
<NCheckbox v-for="p in specialties" :key="p" :value="p" :label="p" />
</div>
</NCheckboxGroup>
<span class="self-center text-xs text-slate-400">(多选,决定审批流中专业会签/审批的路由范围)</span>
</div>
<div v-if="signers && signers.length" class="mt-1.5 flex flex-wrap items-center gap-1.5 text-xs text-slate-500">
<Icon icon="lucide:users" class="size-12px" />已联动专业会签:
<NTag size="small" type="info" v-for="(p, d) in signers" :key="d">{{ p?.member?.specialty }} · {{ p?.member?.user_name }}{{p?.stepName ? ` (${p.stepName})` : ''}}</NTag>
</div>
<NTag v-for="(p, d) in signers" :key="d" size="small" type="info">{{ p?.member?.specialty }} · {{ p?.member?.user_name }}{{ p?.stepName ? ` (${p.stepName})` : '' }}</NTag>
</div>
</div>
</div>
</div>
</div>
</div>
<Footer
ref="footerRef"
:type="props.type"
:currentId="props.currentId"
:title="props.title"
:isAiOpen="props.isAiOpen"
:currentForm="form"
:confirmed="props.confirmed"
:members="props.members"
:steps="currentSteps"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 -->
<NDrawer :show="sheet !== ''" @update:show="(v) => !v && (sheet = '')" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'" :auto-focus="false">
<NDrawerContent>
<template #header>
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<!-- AI 变更类型判定说明 -->
<template v-if="sheet === 'type'">
<NTag v-if="typeAiLoading" type="info" size="small">
<template #icon>
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
</template>
AI 判定中…
</NTag>
<div v-else class="flex items-center gap-2 max-h-[22px]">
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
✨ AI 生成 · 需人工确认
</span>
<NTag v-if="typeAi?.type" type="info" size="small">判定:{{ typeAi?.type }}</NTag>
<NTag v-if="typeAi" size="small">置信度:{{ `${Math.round(typeAi.confidence * 100)}%` }}</NTag>
</div>
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
<p><b>判定依据:</b></p>
<ul class="list-disc space-y-1 pl-5">
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
</ul>
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
</div>
<Footer
ref="footerRef"
:type="props.type"
:current-id="props.currentId"
:title="props.title"
:is-ai-open="props.isAiOpen"
:current-form="form"
:confirmed="props.confirmed"
:members="props.members"
:steps="currentSteps"
@save="saveDraft"
@submit="submitConfirm"
@confirm-tab="confirmTab"
@regenerate="onRegenerate"
@update:steps="updateSteps"
/>
<!-- 侧边抽屉:AI 类型判定 / 等级判定表 -->
<NDrawer :show="sheet !== ''" placement="right" :width="sheet === 'level' ? '45rem' : '28rem'" :auto-focus="false" @update:show="(v) => !v && (sheet = '')">
<NDrawerContent>
<template #header>
<div v-if="sheet === 'type'" class="text-base font-semibold text-slate-800">AI 变更类型判定说明</div>
<div v-if="sheet === 'level'" class="text-base font-semibold text-slate-800">变更等级判定表</div>
</template>
<div class="h-full space-y-4 overflow-y-auto">
<!-- AI 变更类型判定说明 -->
<template v-if="sheet === 'type'">
<NTag v-if="typeAiLoading" type="info" size="small">
<template #icon>
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" />
</template>
AI 判定中…
</NTag>
<div v-else class="flex items-center gap-2 max-h-[22px]">
<span class="inline-flex items-center gap-1 rounded-full border border-violet-200 bg-violet-100 px-2 py-0.5 text-xs font-medium text-violet-700">
✨ AI 生成 · 需人工确认
</span>
<NTag v-if="typeAi?.type" type="info" size="small">判定:{{ typeAi?.type }}</NTag>
<NTag v-if="typeAi" size="small">置信度:{{ `${Math.round(typeAi.confidence * 100)}%` }}</NTag>
</div>
<div v-if="typeAi" class="space-y-3 text-sm leading-relaxed text-slate-700">
<p><b>判定结论:</b>{{ typeAi.conclusion }}</p>
<p><b>判定依据:</b></p>
<ul class="list-disc space-y-1 pl-5">
<li v-for="r in typeAi.hit_rules" :key="r">命中规则:{{ r }}</li>
<li v-for="r in typeAi.exclude_rules" :key="r">排除规则:{{ r }}</li>
</ul>
<p v-if="typeAi.knowledge_refs.length"><b>知识库引用:</b>{{ typeAi.knowledge_refs.join('、') }}。</p>
<p class="rounded-lg bg-amber-50 p-3 text-xs text-amber-700">AI 判定结果仅供参考,最终类型以人工确认为准;如调整为其他类型,系统将记录修改留痕。</p>
</div>
</template>
<!-- 变更等级判定表 -->
<template v-if="sheet === 'level'">
<div class="flex items-center gap-2">
<NButton v-if="props.isAiOpen" size="small" type="primary">
<div v-if="levelAiLoading" class="flex items-center gap-1 text-xs">
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />AI 评分中
</div>
<div v-else class="flex items-center gap-1 text-xs" @click="useAiLevel">
使用AI 评分
</div>
</NButton>
<!-- 变更等级判定表 -->
<template v-if="sheet === 'level'">
<div class="flex items-center gap-2">
<NButton v-if="props.isAiOpen" size="small" type="primary">
<div v-if="levelAiLoading" class="flex items-center gap-1 text-xs">
<Icon icon="ri:loader-4-fill" class="size-14px animate-spin" />AI 评分中
</div>
<div v-else class="flex items-center gap-1 text-xs" @click="useAiLevel">
使用AI 评分
</div>
</NButton>
<div class="flex items-center gap-2">
<NTag
v-if="currentLevel"
size="small"
@@ -769,86 +770,87 @@ defineExpose({form, questions, applyAiResult, generateLevel: useAiLevel})
borderColor: tagBorderColor(currentLevel?.color),
}"
>
{{ currentLevel?.label ? currentLevel.label+'变更' : '' }}
{{ currentLevel?.label ? currentLevel.label+'变更' : '' }}
</NTag>
<span class="text-xs text-slate-400">合计得分 <b class="text-[#17407F]">{{ totalScore }}</b> 分(&lt;20 分为一般,≥20 分为重要)</span>
</div>
</div>
</div>
<div class="grid gap-3 grid-cols-2">
<div class="grid gap-3 grid-cols-2">
<div v-for="(dim, di) in questions" :key="dim.name" class="rounded-lg border p-3">
<div class="mb-2 flex items-baseline justify-between gap-2">
<div class="mb-2 flex items-baseline justify-between gap-2">
<span class="text-sm font-semibold text-slate-700">
{{ dim.name }}
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">({{ dim.note }})</span>
{{ dim.name }}
<span v-if="dim.note" class="ml-1 text-xs font-normal text-slate-400">({{ dim.note }})</span>
</span>
<span class="text-sm text-slate-500">得分 <b class="text-[#17407F]">{{ questions[di].score }}</b></span>
</div>
<div class="space-y-1.5">
</div>
<div class="space-y-1.5">
<NRadioGroup v-model:value="questions[di].score" size="small" :disabled="levelAiLoading" name="radiogroup" :on-update:value="(v)=>handleScoreChange(v,di)">
<div class="flex flex-col justify-center gap-2">
<div class="flex flex-col justify-center gap-2">
<NRadio v-for="op in dim.options" :key="op.score" :value="op.score">
<p class="text-xs">{{ op.label }}({{ op.score }}分)</p>
<p class="text-xs">{{ op.label }}({{ op.score }}分)</p>
</NRadio>
</div>
</div>
</NRadioGroup>
</div>
</div>
</div>
</div>
<div v-if="props.isAiOpen && (levelAiText || levelAiBasis.some((x) => x))" class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-violet-700">
<Icon icon="lucide:sparkles" class="size-14px" />
AI 评分分析过程
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
</span>
</div>
<div class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
<p v-if="levelAiText">{{ levelAiText }}</p>
<p v-for="(dim, di) in questions" :key="dim.name">
<div v-if="props.isAiOpen && (levelAiText || levelAiBasis.some((x) => x))" class="rounded-lg border border-violet-200 bg-violet-50/50 p-4">
<div class="flex items-center gap-2 text-sm font-semibold text-violet-700">
<Icon icon="lucide:sparkles" class="size-14px" />
AI 评分分析过程
<span v-if="levelAiLoading" class="flex items-center gap-1 text-xs font-normal text-violet-500">
<Icon icon="ri:loader-4-fill" class="size-12px animate-spin" /> AI 评分中…
</span>
</div>
<div class="mt-2 space-y-3 text-xs leading-relaxed text-slate-600">
<p v-if="levelAiText">{{ levelAiText }}</p>
<p v-for="(dim, di) in questions" :key="dim.name">
<template v-if="levelAiBasis[di]"><b class="text-[#17407F]">{{ dim.name }}:</b>{{ levelAiBasis[di] }}</template>
</p>
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
</p>
<p class="text-slate-400">(各维度得分均可人工改判,修改留痕;改判后等级自动回填至申请表)</p>
</div>
</div>
</div>
</template>
</div>
</NDrawerContent>
</NDrawer>
<!-- 附件在线预览弹窗 -->
<NModal
:show="previewFile !== null"
preset="card"
:auto-focus="false"
:style="{ width: '80%', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@update:show="(v) => !v && (previewFile = null)"
>
<template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
</div>
</template>
<template #default>
<div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
<span>上传人:{{ previewFile?.uploader_name }}</span>
<span>上传时间:{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
</template>
</div>
<previewComponent v-if="previewFile" :previewFile="previewFile" />
</div>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton ghost size="small" @click="downloadFile(previewFile)">
<Icon icon="material-symbols:download" class="size-16px" /> 下载
</NButton>
<NButton size="small" type="primary" @click="previewFile = null">关闭</NButton>
</div>
</template>
</NModal>
</NDrawerContent>
</NDrawer>
<!-- 附件在线预览弹窗 -->
<NModal
:show="previewFile !== null"
preset="card"
:auto-focus="false"
:style="{ width: '80%', height: 'auto' }"
:segmented="{ content: true, footer: true }"
@update:show="(v) => !v && (previewFile = null)"
>
<template #header>
<div class="flex items-center gap-2 text-base font-semibold text-slate-800">
<Icon icon="akar-icons:file" class="size-18px" /> {{ previewFile?.file_name }}
</div>
</template>
<template #default>
<div class="space-y-3">
<div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-slate-400">
<span>上传人:{{ previewFile?.uploader_name }}</span>
<span>上传时间:{{ previewFile?.created_at }}</span>
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
</div>
<PreviewComponent v-if="previewFile" :preview-file="previewFile" />
</div>
</template>
<template #footer>
<div class="flex justify-end gap-[10px]">
<NButton ghost size="small" @click="downloadFile(previewFile)">
<Icon icon="material-symbols:download" class="size-16px" /> 下载
</NButton>
<NButton size="small" type="primary" @click="previewFile = null">关闭</NButton>
</div>
</template>
</NModal>
</template>
<style scoped lang="scss">
:deep(.n-radio) {
display: flex;
+1 -1
View File
@@ -5,7 +5,7 @@ import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouterPush } from '@/hooks/common/router';
import { formatTimestamp, getTimeRangeStamp, downloadFileFun } from '@/utils/common';
import previewComponent from '@/components/previewFile/index.vue'
import PreviewComponent from '@/components/previewFile/index.vue'
import { pssrListApi } from '@/service/api/pssr';
const themeStore = useThemeStore();