Files
moc/src/views/home/modules/template6.vue
T
2026-09-28 11:50:58 +08:00

465 lines
22 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 { onMounted, onUnmounted, ref } from 'vue';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
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'
const themeStore = useThemeStore();
const { routerPushByKey } = useRouterPush();
const loading = ref<boolean>(false);
// 附件在线预览弹窗
const previewModal = ref<boolean>(false);
// 附件在线预览文件
const previewFile = ref<any>(null);
// 变更临期天数
const remindDays = ref<number>(0);
// 变更类型列表
const typeList = ref<any>([]);
// 变更等级列表
const levelList = ref<any>([]);
// 时间显示
const clock = ref<string>('');
let timer: any = null;
// 首页数据
const homeData = ref<any>(null);
const highRisk = [
{ title: "聚合反应温度控制上限调整", why: "涉及工艺安全边界,建议参与 HAZOP 分析" },
{ title: "R-101 搅拌提速至 180rpm", why: "搅拌轴强度与密封形式需校核" },
];
const LEVEL_UPGRADES = [
{ id: "MOC-2026-R01-0036", title: "聚合反应温度控制上限调整", change: "一般 → 重要", reason: "涉及聚合工艺温度安全边界", date: "2026-07-26" },
{ id: "MOC-2026-R02-0027", title: "冷冻机组载冷剂更换", change: "一般 → 重要", reason: "与检修交叉作业,风险叠加", date: "2026-07-10" },
];
// 跳转变更申请页面、变更台账、我的任务、发起变更
const jumpTo = (key: string,qid:number = 0) => {
if(key==='my_initiatechange' && qid){
routerPushByKey(key,{ query: { id: qid.toString() } });
}else if(key==='details_closedoc' && qid){
routerPushByKey(key,{ query: { id: qid.toString(),key:'home' } });
}else{
routerPushByKey(key as any);
}
}
// 打开审批详情
const openDetail = (row: any) => {
routerPushByKey("details_approval",{ query: { id: row?.change_id?.toString(),status:row?.status?.toString() || '',key:'home' } });
};
// 获取当前时间字符串
const getCurrentTimeStr = () => {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const date = now.getDate();
const week = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][now.getDay()];
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
return `${year}年${month}月${date}日 ${week} ${hours}:${minutes}`;
}
// 更新时间显示
const tick = () => {
clock.value = getCurrentTimeStr();
// 计算距离下一分钟还有多少毫秒(+50ms 容错,防止定时器提前触发)
const now = new Date();
const delay = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 50;
timer = setTimeout(tick, delay);
}
// 查看
const viewFile = (file: any) => {
previewFile.value = file;
previewModal.value = true;
};
// 下载
const downloadFile = (file: any) => {
downloadFileFun(file.file_url, file.file_name);
};
// 下载模板
const downloadTemplate = (record: any) => {
window.$message?.info('还差接口')
};
// 匹配对象
const matchObj = (list:any,val:number) => {
const obj = list.find((item:any) => item.value == val);
return obj || null;
}
// 获取数据
const getData = async () => {
loading.value = true;
const {data,error} = await homeSafeApi();
if(!error){
console.log(data)
homeData.value = data;
}
loading.value = false;
}
// 获取系统配置
const getConfig = async () => {
const {data,error} = await systemConfigApi();
if(!error){
// 解析 临时变更临期提醒
const remindDaysGroup = data.find((group:{group_key:string}) => group.group_key === 'change_remind');
if (remindDaysGroup) {
remindDaysGroup?.items.forEach((item:any) => {
if(item.item_key === 'temp_due_remind_days'){
remindDays.value = Number(item.value);
}
})
}
// 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
if (changeTypeGroup) {
typeList.value = changeTypeGroup.items;
}
// 解析 变更等级设置
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelList.value = changeLevelGroup.items;
}
}
}
onMounted(() => {
tick();
getConfig();
getData();
})
onUnmounted(() => {
if(timer){
clearTimeout(timer);
timer = null;
}
})
</script>
<template>
<NSpin :show="loading" size="small">
<div class="space-y-3.5">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:10">
<div class="flex-y-center">
<p
class="py-1.5 px-1.5 flex items-center justify-center bg-[rgb(var(--primary-color))] text-white text-xs font-bold"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
>
MOC
</p>
<p class="pl-12px text-14px font-semibold">我的工作台</p>
</div>
</NGi>
<NGi span="24 s:24 m:14" class="flex items-center justify-end gap-12px">
<p class="text-#999 text-xs">{{clock}}</p>
<NButton ghost size="small" @click="jumpTo('changeledger')">
投用与验收总览
</NButton>
<NButton ghost size="small" @click="jumpTo('changeledger')">
全厂风险看板
</NButton>
<NButton type="primary" size="small" @click="jumpTo('my_mytask')">
<Icon icon="lucide:clipboard-check" class="size-14px" />
审批待办
</NButton>
</NGi>
</NGrid>
</NCard>
<!-- 卡片数据 -->
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_mytask')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.dept_stats?.reject_count || 0"
class="text-26px font-600"
:style="{ color: themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">待我审批</p>
<p class="text-12px text-slate-400">安全审查节点 · 与我的任务同源</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_mytask')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.dept_stats?.reject_count || 0"
class="text-26px font-600"
:style="{ color: themeStore.themeColor }"
/>
</div>
<div>
<p class="text-12px font-600">PSSR监督</p>
<p class="text-12px text-slate-400">待PSSR / 待验收 · 按变更状态实时统计</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_mytask')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.dept_stats?.reject_count || 0"
class="text-26px font-600"
:style="{ color: themeStore.otherColor.error }"
/>
</div>
<div>
<p class="text-12px font-600">高风险在办</p>
<p class="text-12px" :style="{ color: themeStore.otherColor.error }">全厂 · 需重点跟踪</p>
</div>
</div>
</NGi>
<NGi>
<div
class="flex-1 bg-[#fff] px-16px py-12px cursor-pointer flex items-center gap-3"
:style="{ borderRadius: themeStore.themeRadius + 'px' }"
@click="jumpTo('my_mytask')"
>
<div class="flex justify-between">
<CountTo
:start-value="0"
:end-value="homeData?.dept_stats?.reject_count || 0"
class="text-26px font-600"
:style="{ color: themeStore.otherColor.error }"
/>
</div>
<div>
<p class="text-12px font-600">临时变更到期</p>
<p class="text-12px" :style="{ color: themeStore.otherColor.error }">含超期 1 起 · 红色预警</p>
</div>
</div>
</NGi>
</NGrid>
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive>
<!-- 左侧 -->
<NGi span="24 s:24 m:24 l:16">
<!-- 审批待办 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-2">
<Icon icon="lucide:clipboard-check" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">审批待办</p>
<p class="text-11px text-#94a3b8 mt-2px">安全审查 · 审批时自动展示相关附件</p>
</div>
<NButton v-if="homeData?.my_changes_progress?.total > 0" ghost size="small" class="text-xs" @click="jumpTo('my_mytask')">
我的任务
<Icon icon="formkit:right" class="size-12px" />
</NButton>
</div>
<div class="flex flex-col gap-3">
<template v-if="homeData?.my_changes_progress?.total > 0">
<div v-for="row in homeData?.my_changes_progress?.items?.slice(0, 3) || []" :key="row.change_id" @click="openDetail(row)"
class="cursor-pointer flex w-full items-center gap-3 border px-3.5 py-2.5 text-left transition hover:border-[var(--hover-border)] hover:bg-[var(--hover-bg)] hover:bg-[#EAF0F9]/40"
:style="{
borderRadius: themeStore.themeRadius + 'px',
'--hover-border': themeStore.themeColor,
'--hover-bg': getColorWithOpacity(themeStore.themeColor, 0.05)
}"
>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="font-mono text-[11px] text-slate-400">{{ row.change_no }}</span>
<NTag class="text-[11px]" v-if="row.urgent===1" type="error" size="small">紧急</NTag>
<!-- 变更等级 -->
<NTag
size="small"
:color="{
color: tagBgColor(matchObj(levelList,row.change_level)?.color),
textColor: tagTextColor(matchObj(levelList,row.change_level)?.color),
borderColor: tagBorderColor(matchObj(levelList,row.change_level)?.color),
}"
>{{matchObj(levelList,row.change_level)?.label}}</NTag>
<!-- 变更类型 -->
<NTag
size="small"
:color="{
color: tagBgColor(matchObj(typeList,row.change_type)?.color),
textColor: tagTextColor(matchObj(typeList,row.change_type)?.color),
borderColor: tagBorderColor(matchObj(typeList,row.change_type)?.color),
}"
>{{matchObj(typeList,row.change_type)?.label}}</NTag>
<NTag class="text-[11px]" v-if="row.duration_type===2" type="warning" size="small">临时</NTag>
<NTag class="text-[11px]" v-else type="info" size="small">永久</NTag>
<span class="text-xs" :style="{ color: themeStore.otherColor.error }">已超期 3 天</span>
</div>
<div class="mt-1 flex items-center gap-2">
<span class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white">{{ row.title }}</span>
<!-- 状态标签 -->
<NTag v-if="row.status==='APPROVING'" size="small" type="warning">审批中</NTag>
<NTag v-if="row.status==='APPROVED'" size="small" type="success">已通过</NTag>
<NTag v-if="row.status==='IMPLEMENTED'" size="small" type="success">待验收</NTag>
<NTag v-if="row.status==='ACCEPTED'" size="small" type="error">待关闭</NTag>
<NTag v-if="row.status==='CLOSED'" size="small" type="success">已关闭</NTag>
<NTag v-if="row.status==='WITHDRAWN'" size="small" type="error">已撤回</NTag>
<NTag v-if="row.status==='REJECTED'" size="small" type="error">已驳回</NTag>
<NButton size="small" type="primary" class="text-xs">
去审批
<Icon icon="akar-icons:arrow-right" class="size-13px ml-1" />
</NButton>
</div>
<div class="mt-0.5 truncate text-[11px] text-slate-400 flex items-center gap-2">
<p>申请人:{{row.step_name}}</p>
<span v-if="row.apply_time" class="text-[11px] text-slate-400">{{ row.apply_time?.slice(0, 10) }}</span>
</div>
</div>
</div>
<NButton dashed type="primary" size="small" class="text-xs" @click="jumpTo('my_mytask')">
还有{{(homeData?.my_changes_progress?.total - 3) > 0 ? (homeData?.my_changes_progress?.total - 3) : 0}}条,前往我的任务查看全部 →
</NButton>
</template>
<template v-else>
<p class="text-xs text-slate-400 text-center">暂无数据</p>
</template>
</div>
</NCard>
<NGrid :x-gap="16" :y-gap="16" responsive="screen" item-responsive class="mt-[16px]">
<!-- 最近已会签 -->
<NGi span="24 s:24 m:24 l:24">
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center mb-3 gap-2">
<Icon icon="lucide:list-checks" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">通过审批的变更(未关闭)</p>
<p class="text-11px text-#94a3b8 mt-2px">PSSR 材料同行显示 · 查看 / 下载</p>
<NButton ghost size="small" class="text-xs ml-auto" @click="jumpTo('my_mytask')">
进入管理
<Icon icon="formkit:right" class="size-12px" />
</NButton>
</div>
<div class="flex flex-col gap-3">
<div class="rounded-md border px-3 py-2 flex items-center justify-between gap-2 cursor-pointer">
<div>
<p class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800 dark:text-white">R-102 安全阀校验周期调整</p>
<p class="font-mono text-[11px] text-slate-400">
<span class="mt-0.5 block text-[11px] text-slate-400">MOC-2026-R02-0038 · 一车间 · 李四</span>
</p>
</div>
<div class="flex items-center gap-1.5 rounded-md border bg-slate-50/60 px-2 py-1">
<Icon icon="akar-icons:file" class="size-14px" :style="{ color: themeStore.themeColor }" />
<span class="text-[11px] font-medium text-slate-600">PSSR 检查确认表</span>
<!-- <template v-if="s.pssr_attachment"> -->
<span class="max-w-[150px] truncate text-[11px] text-slate-400">pssr检查确认表</span>
<NButton ghost size="tiny" class="text-11px" @click="viewFile({})">查看</NButton>
<NButton ghost size="tiny" class="text-11px" @click="downloadFile({})">下载</NButton>
<!-- </template> -->
<!-- <template v-else>
<span class="text-[11px] text-[var(--warning-color)]">待传</span>
<NButton ghost size="tiny" class="text-11px" @click="downloadTemplate(s)">下载模板</NButton>
</template> -->
</div>
</div>
</div>
<!-- <p class="text-xs text-slate-400 text-center">暂无数据</p> -->
</NCard>
</NGi>
</NGrid>
</NGi>
<!-- 右侧 -->
<NGi span="24 s:24 m:24 l:8">
<!-- 高风险变更跟踪 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="flex items-center mb-3 gap-2">
<Icon icon="lucide:shield-alert" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">高风险变更跟踪</p>
<p class="text-11px text-#94a3b8">重要等级 · 未关闭 · 实时推导</p>
</div>
<template v-if="highRisk.length">
<div class="space-y-2.5">
<div v-for="c in highRisk" :key="c.title" class="rounded-lg border border-red-200 bg-red-50/50 px-3.5 py-2.5">
<div class="flex items-center gap-2">
<span class="min-w-0 flex-1 truncate text-sm font-medium text-slate-800">{{ c.title }}</span>
<NButton size="tiny" ghost type="error" class="text-xs">查看分析</NButton>
</div>
<div class="mt-1 text-[11px] flex items-center gap-2" :style="{ color: themeStore.otherColor.error }">
<span>MOC-2026-R01-0035</span>
<span>审批中</span>
<span>一车间</span>
</div>
</div>
</div>
</template>
<template v-else>
<p class="text-xs text-slate-400 text-center">暂无数据</p>
</template>
</NCard>
<!-- 等级调整记录 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px',marginTop:'16px' }">
<div class="flex items-center mb-3 gap-2">
<Icon icon="proicons:alert-triangle" class="size-16px" :style="{ color: themeStore.themeColor }" />
<p class="text-13px font-700 text-#64749a">等级调整记录</p>
<p class="text-11px text-#94a3b8">近期等级被提升的变更</p>
</div>
<div class="space-y-3">
<div v-for="u in LEVEL_UPGRADES" :key="u.id" class="rounded-md border px-4 py-3">
<div class="flex items-center gap-2">
<NTag size="small" type="error" class="text-xs">{{ u.change }}</NTag>
<span class="ml-auto text-[11px] text-slate-400">{{ u.date }}</span>
</div>
<div class="mt-1 text-sm font-medium text-slate-800">{{ u.title }}</div>
<div class="mt-0.5 text-[11px] text-slate-400">{{ u.reason }} · {{ u.id }}</div>
</div>
</div>
</NCard>
</NGi>
</NGrid>
</div>
<!-- 附件在线预览弹窗 -->
<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?.slice(0, 16) }}</span>
<span v-if="previewFile?.version">{{ `版本:${previewFile?.version}(受控)` }}</span>
</div>
<previewComponent v-if="previewFile" :previewFile="previewFile" />
</div>
</template>
</NModal>
</NSpin>
</template>
<style scoped></style>