Files
moc/src/views/my/initiateChange/modules/evaluation.vue
T

308 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 验收评价 -->
<script setup lang="ts">
import { h, ref, watch } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity } from "@/utils/common";
import { AiError, aiAcceptance } from "@/service/api/ai";
import Footer from './footer.vue';
const themeStore = useThemeStore();
const props = defineProps(['type','orgTree','isAiOpen','title','currentId','formInfo','currentForm','confirmed','members'])
const emit = defineEmits(['save','submit','confirmTab']);
watch(() => props.currentForm, (newVal:any) => {
const omit = (obj:any, fields: string[]) => Object.fromEntries(Object.entries(obj).filter(([k]) => !fields.includes(k)));
const result = newVal?.items.map((item:any) => omit(item, [
'created_at', 'updated_at', 'change_id', 'org_name'
]));
list.value = result || [];
})
// 是否是ai生成
const isAiGenerated = ref<boolean>(false);
const footerRef = ref<any>(null);
const editing = ref<any>(null);
const list = ref<{id:number,item:string,basis:string,standard:string,method:string,org_id:number}[]>([]);
const aiFail = (e: any) => {
if (e instanceof AiError) window.$message?.warning(`AI 分析失败:${e.message},可手动填写`);
else window.$message?.warning("AI 分析失败,可手动填写");
};
// 重新生成验收评价(footer 派发):依据申请表的变更目的与预期效果生成量化验收标准
const onRegenerate = async () => {
const purpose = (props.formInfo?.purposes ?? []).filter((x: string) => x && x.trim()).join('、');
const effect = props.formInfo?.effect ?? '';
if (!purpose && !effect.trim()) {
return window.$message?.warning("缺少变更目的与预期效果,请先在 [变更申请表] 填写");
}
footerRef.value?.setRegenerateLoading(true);
try {
const res = await aiAcceptance({ purpose, effect });
const rows = res?.rows ?? [];
if (!rows.length) {
return window.$message?.warning("AI 未生成验收项,可手动添加");
}
list.value = rows.map((r: any) => ({
id: 0,
item: r.item ?? '',
basis: r.basis ?? '',
standard: r.standard ?? '',
method: r.method ?? '',
// 建议责任方文本 → 组织树节点(按名称模糊匹配,匹配不到留空人工选择)
org_id: matchOrgId(r.owner_suggest),
}));
isAiGenerated.value = true;
window.$message?.success("验收量化标准已由 AI 生成,均可手动修改");
} catch (e: any) {
aiFail(e);
} finally {
footerRef.value?.setRegenerateLoading(false);
}
}
// 按名称在组织树中查找节点 id
const matchOrgId = (name: string): number => {
if (!name || !name.trim()) return 0;
const hit = findNodeByLabel(props.orgTree ?? [], name.trim());
return hit?.id ?? 0;
}
const findNodeByLabel = (tree: any[], name: string): any => {
for (const node of tree) {
if (node.label && (name.includes(node.label) || node.label.includes(name))) return node;
if (node.children && node.children.length) {
const found: any = findNodeByLabel(node.children, name);
if (found) return found;
}
}
return null;
}
// 添加验收项
const addRow = () => {
list.value.push({ id: 0, item: '', basis: '', standard: '', method: '',org_id: 0 });
}
// 删除验收项
const deleteRow = (i:number) => {
list.value.splice(i, 1);
}
// 自定义渲染展开图标
const renderSwitcherIcon = (node:any) => {
// 如果节点没有 children 或 children 为空数组,则不显示图标
if (!node.option.children || node.option.children.length === 0) {
return null; // 返回 null 表示不渲染任何图标
}
// 有子节点:返回 undefined 表示使用默认展开图标,也可自定义
return h(Icon,{
icon: node.option.expanded ? 'bi:caret-down-fill' : 'bi:caret-right-fill',
class: 'size-12px',
});
}
// 保存草稿
const saveDraft = () => {
emit('save');
}
// 提交
const submitConfirm = () => {
emit('submit', props.type);
}
// // 确认标签内容
const confirmTab = (tab:string, val:boolean) => {
emit('confirmTab', tab, val);
}
const findNodeById = (tree:any,id:number) => {
for (const node of tree) {
if (node.id === id) return node;
if (node.children && node.children.length) {
const found:any = findNodeById(node.children,id);
if (found) return found;
}
}
return null; // 未找到
}
defineExpose({list, onRegenerate})
</script>
<template>
<div class="p-4">
<div class="space-y-4 scroll">
<div class="flex flex-wrap items-center gap-3">
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
<Icon icon="ix:success" class="size-18px text-emerald-500" />
验收评价(依据预期效果设定可量化验收标准)
</span>
<span v-if="isAiGenerated" 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>
<span class="ml-auto text-xs text-slate-400">申请阶段由 AI 依据「预期效果」生成量化标准与验收方式,可手动修改;投用运行后由车间和各专业按标准验收并给出评估结论</span>
</div>
<div
class="space-y-1.5 rounded-lg border border-[var(--border-color)] bg-[var(--bg-color)] px-4 py-3 text-xs leading-6 text-slate-700"
:style="{
'--bg-color': getColorWithOpacity(themeStore.themeColor, 0.1),
'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)
}"
>
<div><b :style="{color: themeStore.themeColor}">变更目的:</b>{{ formInfo?.purposes?.join('、') || '' }}</div>
<div><b :style="{color: themeStore.themeColor}">预期效果:</b>{{ formInfo?.effect || '' }}</div>
<div
class="border-t border-[var(--border-color)] pt-1.5 text-slate-500"
:style="{'--border-color': getColorWithOpacity(themeStore.themeColor, 0.2)}"
>
<b :style="{color: themeStore.themeColor}">对应说明:</b>验收项目由预期效果逐条拆解设定,各项目对应的预期效果来源见表格「验收项目」列下方灰色小字(点击可修改);验收时须逐项对照预期效果判定达成程度。
</div>
</div>
<div class="overflow-x-auto border" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<table class="w-full min-w-[900px] border-collapse text-sm">
<thead>
<tr class="bg-slate-100 text-left text-slate-600">
<th class="w-12 px-3 py-2 font-medium">#</th>
<th class="w-56 px-3 py-2 font-medium">验收项目(含对应预期效果)</th>
<th class="px-3 py-2 font-medium">量化验收标准</th>
<th class="px-3 py-2 font-medium">验收方式(可执行)</th>
<th class="w-46 px-3 py-2 font-medium">组织层级</th>
<th class="w-12 px-2 py-2 font-medium text-center">操作</th>
</tr>
</thead>
<tbody>
<tr v-if="list.length===0">
<td colspan="6" class="py-4 px-2 text-center text-slate-400">暂无验收项目</td>
</tr>
<tr v-for="(row, i) in list" :key="i" class="align-top">
<td class="border-t px-3 py-2.5 text-slate-400">{{ i + 1 }}</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `item-${i}-${row.item}`"
class="text-xs"
v-model:value="row.item"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `item-${i}-${row.item}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs font-medium leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.item || '点击填写验收项目' }}
</button>
<NInput
v-if="editing === `basis-${i}-${row.basis}`"
class="text-[11px]"
size="small"
v-model:value="row.basis"
autofocus
@blur="editing = null"
/>
<button v-else @click="editing = `basis-${i}-${row.basis}`" title="本项目对应的预期效果来源,点击修改"
class="bg-transparent mt-0.5 block w-full rounded px-2 text-left text-[11px] text-slate-400 transition hover:bg-[#EAF0F9] hover:text-slate-500"
>
对应预期:{{ row.basis || '(点击填写对应预期效果)' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `standard-${i}-${row.standard}`"
class="text-xs"
v-model:value="row.standard"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `standard-${i}-${row.standard}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.standard || '点击填写量化验收标准' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NInput
v-if="editing === `method-${i}-${row.method}`"
class="text-xs"
v-model:value="row.method"
type="textarea"
autofocus
rows="2"
@blur="editing = null"
/>
<button v-else @click="editing = `method-${i}-${row.method}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ row.method || '点击填写验收方式' }}
</button>
</td>
<td class="border-t px-3 py-2.5">
<NTreeSelect
v-if="editing === `org-${i}-${row.org_id}`"
size="small"
v-model:value="row.org_id"
default-expand-all
label-field="label"
key-field="id"
:options="props.orgTree"
:render-switcher-icon="renderSwitcherIcon"
placeholder="请选择组织层级"
@blur="editing = null"
/>
<button v-else @click="editing = `org-${i}-${row.org_id}`" title="点击修改"
class="bg-transparent block w-full rounded-md px-2 py-1 text-left text-xs leading-relaxed text-slate-700 transition hover:bg-[#EAF0F9]"
>
{{ findNodeById(orgTree,row.org_id)?.label || '点击选择组织层级' }}
</button>
</td>
<td class="border-t px-2 py-2.5 text-center">
<NPopconfirm
positive-text="确定"
:positiveButtonProps="{ size: 'tiny' }"
:negativeButtonProps="{ size: 'tiny' }"
@positive-click="deleteRow(i)"
>
<template #trigger>
<NButton text type="error" class="mt-1.5" title="删除本验收项">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</template>
确定删除吗?
</NPopconfirm>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex items-center gap-3">
<NButton ghost class="text-xs" @click="addRow">
<Icon icon="ic:round-plus" class="size-16px" />添加验收项
</NButton>
<span class="text-xs text-slate-400">评估结论在验收阶段由责任单位填写:点击选择「完全达到 / 基本达到 / 未达成」,再次点击取消;行尾可删除验收项</span>
</div>
<div class="rounded-lg bg-emerald-50/80 px-4 py-2.5 text-xs leading-5 text-emerald-700">
流程说明:投用运行满验收周期后,系统向车间(属地)与涉及专业推送验收任务;各责任单位按上表逐项给出评估结论并上传佐证(数据报表 / 化验报告 / 现场照片),全部完成后汇总至综合验收结论,结论与佐证资料一并纳入变更关闭校验,验收不通过时退回整改或评估恢复原状。
</div>
</div>
</div>
<Footer
ref="footerRef"
:type="props.type"
:currentId="props.currentId"
:title="props.title"
:isAiOpen="props.isAiOpen"
:confirmed="props.confirmed"
:members="props.members"
@save="saveDraft"
@submit="submitConfirm"
@confirmTab="confirmTab"
@regenerate="onRegenerate"
/>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 379px);
overflow-y: auto;
}
</style>