629 lines
26 KiB
Vue
629 lines
26 KiB
Vue
<script setup lang="ts">
|
||
import { ref, computed, onMounted, h } from 'vue';
|
||
import { Icon } from '@iconify/vue'
|
||
import { useThemeStore } from '@/store/modules/theme';
|
||
import { useRouterPush } from '@/hooks/common/router';
|
||
import { tagTextColor, tagBgColor, tagBorderColor, getTimeRangeStamp, formatTimestamp } from '@/utils/common';
|
||
import { systemConfigApi } from '@/service/api/system';
|
||
import { orgListApi } from '@/service/api/user';
|
||
import { changeLedgerListApi, exportChangeLedgerApi } from '@/service/api/changeLedger';
|
||
|
||
const { routerPushByKey } = useRouterPush();
|
||
const themeStore = useThemeStore();
|
||
const props = defineProps(['self', 'keyValue'])
|
||
|
||
const loading = ref<boolean>(false);
|
||
// 当前选中时间范围
|
||
const period = ref<string>("year");
|
||
// 组织树
|
||
const orgTree = ref<any[]>([]);
|
||
// 筛选条件
|
||
const formInfo = ref<any>({
|
||
kw: '',
|
||
type: null,
|
||
level: null,
|
||
duration: null,
|
||
status: '',//英文逗号隔开
|
||
sort: null,
|
||
org_id: 1,
|
||
apply_time_start: getTimeRangeStamp(period.value)?.from ?? null,
|
||
apply_time_end: getTimeRangeStamp(period.value)?.to ?? null,
|
||
self: props.self,
|
||
});
|
||
const pagination = ref<{page: number, pageSize: number, itemCount: number}>({
|
||
page: 1,
|
||
pageSize: 20,
|
||
itemCount: 0,
|
||
})
|
||
// 变更类型列表
|
||
const typeList = ref<any[]>([{ label: "全部", value: null }]);
|
||
// 变更等级列表
|
||
const levelList = ref<any[]>([{ label: "全部", value: null }]);
|
||
// 状态选项
|
||
const STATUS_OPTIONS = [
|
||
{ label: "全部", value: "", type:'all' },
|
||
{ label: "审批中", value: "APPROVING", type:'warning' },
|
||
{ label: "已通过", value: "APPROVED", type:'success' },
|
||
{ label: "待验收", value: "IMPLEMENTED", type:'success' },
|
||
{ label: "待关闭", value: "ACCEPTED", type:'error' },
|
||
{ label: "已关闭", value: "CLOSED", type:'success' },
|
||
{ label: "已撤回", value: "WITHDRAWN", type:'error' },
|
||
{ label: "已驳回", value: "REJECTED", type:'error' },
|
||
];
|
||
// 排序选项
|
||
const SORT_OPTS = [
|
||
{ label: "申请时间 · 最新", value: "apply_time,desc" },
|
||
{ label: "申请时间 · 最早", value: "apply_time,asc" },
|
||
{ label: "计划投用 · 最新", value: "plan_use_date,desc" },
|
||
{ label: "计划投用 · 最早", value: "plan_use_date,asc" },
|
||
{ label: "状态更新 · 最新", value: "status_changed_at,desc" },
|
||
{ label: "状态更新 · 最早", value: "status_changed_at,asc" },
|
||
];
|
||
// 时限选项
|
||
const DUR_OPTIONS = [
|
||
{ label: "全部", value: null },
|
||
{ label: "永久", value: 1 },
|
||
{ label: "临时", value: 2 }
|
||
];
|
||
// 时间选项
|
||
const TIME_OPTIONS = [
|
||
{ label: "近一年", value: 'year' },
|
||
{ label: "近半年", value: 'half_year' },
|
||
{ label: "近三个月", value: 'quarter' },
|
||
{ label: "近一个月", value: 'month' },
|
||
{ label: "自定义", value: 'custom' },
|
||
];
|
||
// 是否显示全部
|
||
const isAll = ref<boolean>(true);
|
||
// 当前选中行
|
||
const currentRow = ref<any>(null);
|
||
// 进度抽屉
|
||
const progressDrawer = ref<boolean>(false);
|
||
// 撤回/未实施情况说明弹框
|
||
const revokeModal = ref<boolean>(false);
|
||
const modalType = ref<string>('');
|
||
const reason = ref<string>('');
|
||
const revokeSaving = ref<boolean>(false);
|
||
// 列表
|
||
const list = ref<any[]>([]);
|
||
|
||
// 导出台账
|
||
const exportChanges = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await exportChangeLedgerApi({
|
||
kw: formInfo.value.kw,
|
||
type: formInfo.value.type,
|
||
level: formInfo.value.level,
|
||
status: formInfo.value.status,
|
||
org_id: formInfo.value.org_id,
|
||
apply_time_start: formatTimestamp(formInfo.value.apply_time_start),
|
||
apply_time_end: formatTimestamp(formInfo.value.apply_time_end),
|
||
});
|
||
if(!error){
|
||
if (!data) {
|
||
return;
|
||
}
|
||
// 确保 res 为 Blob 类型(若 API 返回 ArrayBuffer 或原始数据,需适配)
|
||
let blob;
|
||
if (data instanceof Blob) {
|
||
blob = data;
|
||
} else {
|
||
// 假设返回的是二进制数据(如 ArrayBuffer),指定 MIME 类型
|
||
blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||
}
|
||
const url = window.URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = '变更台账.xlsx'; // 推荐使用 download 属性
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
// 清理:移除链接并释放 URL 对象
|
||
document.body.removeChild(link);
|
||
window.URL.revokeObjectURL(url);
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
// 自定义渲染展开图标
|
||
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 matchObj= (list:any[],val:any) => {
|
||
const item = list.find((item:any) => item.value == val);
|
||
return item || null;
|
||
}
|
||
|
||
// 搜索
|
||
const searchHandle = () => {
|
||
pagination.value.page = 1;
|
||
getList();
|
||
}
|
||
// 重置
|
||
const reset = () => {
|
||
period.value = 'year';
|
||
formInfo.value.kw = '';
|
||
formInfo.value.type = null;
|
||
formInfo.value.level = null;
|
||
formInfo.value.duration = null;
|
||
formInfo.value.status = '';
|
||
formInfo.value.org_id = 1;
|
||
formInfo.value.sort = null;
|
||
formInfo.value.apply_time_start = getTimeRangeStamp(period.value)?.from ?? null;
|
||
formInfo.value.apply_time_end = getTimeRangeStamp(period.value)?.to ?? null;
|
||
isAll.value = true;
|
||
searchHandle();
|
||
};
|
||
// 组织选择改变
|
||
const orgChange = (val:any) => {
|
||
formInfo.value.org_id = val;
|
||
searchHandle();
|
||
}
|
||
// 显示全部改变
|
||
const showAll = () => {
|
||
isAll.value = !isAll.value;
|
||
if(isAll.value){
|
||
formInfo.value.status = '';
|
||
}else{
|
||
formInfo.value.status = 'APPROVING,APPROVED,IMPLEMENTED,ACCEPTED,WITHDRAWN,REJECTED';
|
||
}
|
||
searchHandle();
|
||
}
|
||
// 类型选择
|
||
const typeChoose = (val:any) => {
|
||
formInfo.value.type = val;
|
||
searchHandle();
|
||
}
|
||
// 时限选择
|
||
const durChoose = (val:any) => {
|
||
formInfo.value.duration = val;
|
||
searchHandle();
|
||
}
|
||
// 时间范围选择
|
||
const timeChoose = (val:string) => {
|
||
period.value = val;
|
||
if(val === 'custom'){
|
||
formInfo.value.apply_time_start = null;
|
||
formInfo.value.apply_time_end = null;
|
||
return
|
||
}else{
|
||
formInfo.value.apply_time_start = getTimeRangeStamp(val)?.from ?? null;
|
||
formInfo.value.apply_time_end = getTimeRangeStamp(val)?.to ?? null;
|
||
}
|
||
searchHandle();
|
||
}
|
||
// 自定义时间选择
|
||
const timeChange = (type:string,val:any) => {
|
||
if(type === 'start'){
|
||
formInfo.value.apply_time_start = val;
|
||
}else{
|
||
formInfo.value.apply_time_end = val;
|
||
}
|
||
if(formInfo.value.apply_time_start && formInfo.value.apply_time_end){
|
||
if(formInfo.value.apply_time_start > formInfo.value.apply_time_end){
|
||
return window.$message?.warning('开始时间不能大于结束时间');
|
||
}
|
||
searchHandle();
|
||
}
|
||
}
|
||
// 状态选择
|
||
const statusChoose = (val:string) => {
|
||
formInfo.value.status = val;
|
||
searchHandle();
|
||
}
|
||
// 排序选择
|
||
const sortChoose = (val:string) => {
|
||
formInfo.value.sort = val;
|
||
searchHandle();
|
||
}
|
||
|
||
// 跳转详情
|
||
const jumpTo = (r:any) => {
|
||
routerPushByKey("details_index",{ query: { id: r?.id?.toString(),key:props.keyValue } });
|
||
}
|
||
// 打开进度
|
||
const openProgress = (r: any) => {
|
||
currentRow.value = r;
|
||
progressDrawer.value = true;
|
||
};
|
||
// 撤回/未实施情况说明
|
||
const openModal = (r: any,type:string) => {
|
||
currentRow.value = r;
|
||
modalType.value = type;
|
||
revokeModal.value = true;
|
||
}
|
||
// 撤回/未实施情况说明提交
|
||
const revokeSave = async () => {
|
||
if (!reason.value) {
|
||
return window.$message?.warning(modalType.value === "withdraw" ? "请输入撤回理由" : "请输入未实施情况说明");
|
||
}
|
||
revokeSaving.value = true;
|
||
setTimeout(() => {
|
||
revokeSaving.value = false;
|
||
}, 1000);
|
||
}
|
||
|
||
// 下载文件
|
||
const downloadFiles = (r:any,type:string) => {
|
||
// 下载申请表
|
||
if(type==='apply'){
|
||
|
||
}
|
||
// 下载已验收待关闭
|
||
if(type==='accepted'){
|
||
|
||
}
|
||
// 下载归档包
|
||
if(type==='closed'){
|
||
|
||
}
|
||
window.$message?.info('还差接口');
|
||
}
|
||
|
||
// 分页改变
|
||
const pageChange = (page: number) => {
|
||
pagination.value.page = page;
|
||
getList();
|
||
}
|
||
// 列表查询
|
||
const getList = async () => {
|
||
loading.value = true;
|
||
const {error, data} = await changeLedgerListApi({
|
||
kw: formInfo.value.kw,
|
||
type: formInfo.value.type,
|
||
level: formInfo.value.level,
|
||
duration: formInfo.value.duration,
|
||
status: formInfo.value.status,
|
||
org_id: formInfo.value.org_id,
|
||
apply_time_start: formatTimestamp(formInfo.value.apply_time_start),
|
||
apply_time_end: formatTimestamp(formInfo.value.apply_time_end),
|
||
self: formInfo.value.self,
|
||
page_num: pagination.value.page,
|
||
page_size: pagination.value.pageSize,
|
||
sort: formInfo.value.sort,
|
||
});
|
||
if (!error) {
|
||
list.value = data?.list ?? [];
|
||
pagination.value.itemCount = data?.total ?? 0;
|
||
}
|
||
loading.value = false;
|
||
}
|
||
// 获取配置
|
||
const getConfig = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await systemConfigApi();
|
||
if(!error){
|
||
// 解析 变更类型设置
|
||
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
|
||
if (changeTypeGroup) {
|
||
typeList.value = typeList.value.concat(changeTypeGroup.items);
|
||
}
|
||
// 解析 变更等级设置
|
||
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
|
||
if (changeLevelGroup) {
|
||
levelList.value = levelList.value.concat(changeLevelGroup.items);
|
||
}
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
// 获取组织列表
|
||
const getOrgList = async () => {
|
||
loading.value = true;
|
||
const {data,error} = await orgListApi();
|
||
if(!error){
|
||
orgTree.value = data;
|
||
if(orgTree.value.length > 0){
|
||
getList();
|
||
}
|
||
}
|
||
loading.value = false;
|
||
}
|
||
|
||
onMounted(async () => {
|
||
await getConfig();
|
||
getOrgList();
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<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-2 border px-3 py-2"
|
||
:style="{ borderRadius: `${themeStore.themeRadius}px` }"
|
||
>
|
||
<span class="flex items-center gap-1.5 text-sm font-semibold text-slate-700">
|
||
<Icon icon="akar-icons:file" class="size-16px" :style="{ color: themeStore.themeColor }" /> 全部变更
|
||
<span class="rounded bg-slate-100 px-1.5 text-xs text-slate-500">{{ pagination.itemCount }}</span>
|
||
</span>
|
||
<NTreeSelect
|
||
class="!w-200px"
|
||
:value="formInfo.org_id"
|
||
:options="orgTree"
|
||
size="small"
|
||
default-expand-all
|
||
label-field="label"
|
||
key-field="id"
|
||
:render-switcher-icon="renderSwitcherIcon"
|
||
placeholder="请选择组织层级"
|
||
@update:value="orgChange"
|
||
/>
|
||
<NRadio
|
||
size="small"
|
||
:checked="isAll"
|
||
:value="true"
|
||
name="显示全部"
|
||
:on-update:checked="showAll"
|
||
>
|
||
显示全部
|
||
</NRadio>
|
||
<NRadio
|
||
size="small"
|
||
:checked="!isAll"
|
||
:value="false"
|
||
name="显示未关闭"
|
||
:on-update:checked="showAll"
|
||
>
|
||
显示未关闭
|
||
</NRadio>
|
||
<div class="relative ml-auto">
|
||
<NInput v-model:value="formInfo.kw" size="small" type="text" clearable placeholder="搜索变更名称 / 编号" @clear="formInfo.kw = ''; searchHandle()">
|
||
<template #suffix>
|
||
<Icon icon="akar-icons:search" class="size-14px text-slate-400 cursor-pointer" @click="searchHandle" />
|
||
</template>
|
||
</NInput>
|
||
</div>
|
||
<NButton size="small" class="text-sm" @click="reset">
|
||
<Icon icon="system-uicons:reset" class="size-12px" />重置
|
||
</NButton>
|
||
<NButton size="small" class="text-sm" @click="exportChanges">
|
||
<Icon icon="material-symbols:download" class="size-14px" />导出台账
|
||
</NButton>
|
||
</div>
|
||
<!-- 筛选与排序行 -->
|
||
<div
|
||
class="flex flex-col gap-y-2 border px-3 py-2 my-3"
|
||
:style="{ borderRadius: `${themeStore.themeRadius}px` }"
|
||
>
|
||
<div class="flex items-center gap-x-4">
|
||
<p class="flex items-center gap-1.5 min-h-28px">
|
||
<span class="text-xs text-slate-400">类型</span>
|
||
<NButton size="tiny" round v-for="t in typeList" :key="t.label" :ghost="formInfo.type === t.value ? false : true" :type="formInfo.type === t.value ? 'primary' : 'default'" @click="typeChoose(t.value)">{{ t.label }}</NButton>
|
||
</p>
|
||
<p class="flex items-center gap-1.5 min-h-28px">
|
||
<span class="text-xs text-slate-400">时限</span>
|
||
<NButton size="tiny" round v-for="d in DUR_OPTIONS" :key="d.label" :ghost="formInfo.duration === d.value ? false : true" :type="formInfo.duration === d.value ? 'primary' : 'default'" @click="durChoose(d.value)">{{ d.label }}</NButton>
|
||
</p>
|
||
<p class="flex flex-wrap items-center gap-1.5 min-h-28px">
|
||
<span class="text-xs text-slate-400">时间</span>
|
||
<NButton size="tiny" round v-for="p in TIME_OPTIONS" :key="p.value" :ghost="period === p.value ? false : true" :type="period === p.value ? 'primary' : 'default'" @click="timeChoose(p.value)">{{ p.label }}</NButton>
|
||
<span v-if="period === 'custom'" class="flex items-center gap-1">
|
||
<NDatePicker :value="formInfo.apply_time_start" type="date" size="small" class="w-130px" :on-update:value="(v) => timeChange('start',v)" />
|
||
<span class="text-xs text-slate-400">至</span>
|
||
<NDatePicker :value="formInfo.apply_time_end" type="date" size="small" class="w-130px" :on-update:value="(v) => timeChange('end',v)" />
|
||
</span>
|
||
</p>
|
||
</div>
|
||
<div class="flex items-center gap-x-4">
|
||
<p class="flex flex-wrap items-center gap-1.5 min-h-28px">
|
||
<span class="text-xs text-slate-400">状态</span>
|
||
<NButton size="tiny" round v-for="s in STATUS_OPTIONS" :key="s.value" :ghost="formInfo.status === s.value ? false : true" :type="formInfo.status === s.value ? 'primary' : 'default'" @click="statusChoose(s.value)">{{ s.label }}</NButton>
|
||
</p>
|
||
<p class="ml-auto flex items-center gap-1.5 min-h-28px">
|
||
<span class="text-xs text-slate-400">排序</span>
|
||
<NSelect size="small" class="!w-36" v-model:value="formInfo.sort" :options="SORT_OPTS" :on-update:value="sortChoose" />
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-3 scroll">
|
||
<!-- 列表 -->
|
||
<div
|
||
v-for="(r, i) in list" :key="r.id"
|
||
class="rounded-lg border bg-white px-4 py-3"
|
||
:style="{
|
||
'--theme-color': themeStore.themeColor
|
||
}"
|
||
>
|
||
<div class="flex flex-wrap items-start gap-3">
|
||
<div class="min-w-0 flex-1">
|
||
<div class="flex flex-wrap items-center gap-2">
|
||
<span class="text-xs text-slate-300">{{ Number(i)+1 }}</span>
|
||
<p class="cursor-pointer text-left text-sm font-semibold text-slate-800 hover:text-[var(--theme-color)] hover:underline" @click="jumpTo(r)">
|
||
{{ r.title }}
|
||
</p>
|
||
<NTag size="small" v-if="r.urgent" type="error">紧急</NTag>
|
||
<span class="font-mono text-[11px] text-slate-400">{{ r.change_no }}</span>
|
||
</div>
|
||
<div class="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||
<!-- 类型 -->
|
||
<NTag
|
||
size="small"
|
||
:color="{
|
||
color: tagBgColor(matchObj(typeList,r.change_type)?.color),
|
||
textColor: tagTextColor(matchObj(typeList,r.change_type)?.color),
|
||
borderColor: tagBorderColor(matchObj(typeList,r.change_type)?.color),
|
||
}"
|
||
>
|
||
{{ matchObj(typeList,r.change_type)?.label }}
|
||
</NTag>
|
||
<!-- 等级 -->
|
||
<NTag
|
||
size="small"
|
||
:color="{
|
||
color: tagBgColor(matchObj(levelList,r.change_level)?.color),
|
||
textColor: tagTextColor(matchObj(levelList,r.change_level)?.color),
|
||
borderColor: tagBorderColor(matchObj(levelList,r.change_level)?.color),
|
||
}"
|
||
>
|
||
{{ matchObj(levelList,r.change_level)?.label }}
|
||
</NTag>
|
||
<NTag v-if="r.duration_type===2" size="small" type="warning">临时</NTag>
|
||
<NTag v-if="r.duration_type===1" size="small" type="info">永久</NTag>
|
||
<NTag v-if="r.extend_count" size="small" type="warning">延期</NTag>
|
||
</div>
|
||
<div class="mt-1.5 flex flex-wrap gap-x-4 gap-y-0.5 text-[11px] text-slate-500">
|
||
<span v-if="r.apply_time">申请 {{ r.apply_time?.slice(0,16) }}</span>
|
||
<span>{{ r.org_name }} · {{ r.applicant_name }}</span>
|
||
<span v-if="r.plan_use_date">计划投用 {{ r.plan_use_date?.slice(0,16) }}</span>
|
||
<span v-if="r.use_time">实际投用 {{ r.use_time?.slice(0,16) }}</span>
|
||
<span v-if="r.restore_deadline">计划恢复 {{ r.restore_deadline?.slice(0,16) }}</span>
|
||
<span v-if="r.close_time">关闭 {{ r.close_time?.slice(0,16) }}</span>
|
||
<span v-if="r.status_changed_at">状态更新 {{ r.status_changed_at?.slice(0,16) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="flex shrink-0 flex-col items-end gap-1.5">
|
||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||
<!-- 状态标签 -->
|
||
<NTag v-if="r.status==='APPROVING'" size="small" type="warning">审批中</NTag>
|
||
<NTag v-if="r.status==='APPROVED'" size="small" type="success">已通过</NTag>
|
||
<NTag v-if="r.status==='IMPLEMENTED'" size="small" type="success">待验收</NTag>
|
||
<NTag v-if="r.status==='ACCEPTED'" size="small" type="error">待关闭</NTag>
|
||
<NTag v-if="r.status==='CLOSED'" size="small" type="success">已关闭</NTag>
|
||
<NTag v-if="r.status==='WITHDRAWN'" size="small" type="error">已撤回</NTag>
|
||
<NTag v-if="r.status==='REJECTED'" size="small" type="error">已驳回</NTag>
|
||
<!-- 超期 -->
|
||
<span v-if="r.overdue_status===2" class="text-[11px] font-medium text-red-500">超期 {{ r.overdue_days }} 天</span>
|
||
</div>
|
||
<!-- 操作按钮 -->
|
||
<div class="flex flex-wrap items-center justify-end gap-1 pt-0.5">
|
||
<!-- 审批中 -->
|
||
<!-- <NButton v-if="r.status==='APPROVING'" size="small" ghost type="error" class="text-xs" @click="openModal(r,'withdraw')">撤回</NButton> -->
|
||
<!-- 已批准待实施 -->
|
||
<!-- <NButton v-if="r.status==='APPROVED'" size="small" ghost type="primary" class="text-xs" @click="openModal(r,'unimplemented')">未实施情况说明</NButton> -->
|
||
<!-- <NButton v-if="r.status==='APPROVED'" size="small" ghost type="success" class="text-xs" @click="downloadFiles(r,'apply')">下载申请表</NButton> -->
|
||
<!-- 已验收待关闭 -->
|
||
<!-- <NButton v-if="r.status==='ACCEPTED'" size="small" ghost type="primary" class="text-xs" @click="openModal(r,'unuse')">未投用情况说明</NButton> -->
|
||
<!-- <NButton v-if="r.status==='ACCEPTED'" size="small" ghost type="success" class="text-xs" @click="downloadFiles(r,'accepted')">下载</NButton> -->
|
||
<!-- 已关闭 -->
|
||
<!-- <NButton v-if="r.status==='CLOSED'" size="small" ghost type="success" class="text-xs" @click="downloadFiles(r,'closed')">下载归档包</NButton> -->
|
||
<NButton size="small" type="primary" class="text-xs" @click="jumpTo(r)">详情</NButton>
|
||
<NButton size="small" type="default" class="text-xs" @click="openProgress(r)">进度</NButton>
|
||
</div>
|
||
<!-- 临时变更 -->
|
||
<div v-if="r.duration_type === 2" class="text-right text-[10px] leading-4 text-slate-400">临时变更恢复 / 延期 / 转永久请在详情页中操作</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="list.length === 0" class="h-100% flex items-center justify-center">
|
||
<NEmpty description="暂无数据"></NEmpty>
|
||
</div>
|
||
</div>
|
||
<div class="flex justify-end mt-3">
|
||
<NPagination
|
||
v-model:page="pagination.page"
|
||
:page-size="pagination.pageSize"
|
||
:item-count="pagination.itemCount"
|
||
:on-update:page="pageChange"
|
||
>
|
||
<template #prefix="{ itemCount }">
|
||
共 {{ itemCount }} 条
|
||
</template>
|
||
</NPagination>
|
||
</div>
|
||
<!-- 撤回/未实施情况说明/未投用情况说明弹框 -->
|
||
<NModal
|
||
v-model:show="revokeModal"
|
||
preset="card"
|
||
:auto-focus="false"
|
||
:style="{ width: '450px', height: 'auto' }"
|
||
:segmented="{ content: true, footer: true }"
|
||
>
|
||
<template #header>
|
||
<h3 class="text-sm font-bold text-slate-800">
|
||
<span v-if="modalType === 'withdraw'">撤回变更申请</span>
|
||
<span v-else-if="modalType === 'unuse'">未投用情况说明</span>
|
||
<span v-else>未实施情况说明</span>
|
||
{{currentRow?.change_no}}
|
||
</h3>
|
||
</template>
|
||
<template #default>
|
||
<div class="rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||
{{ modalType === 'withdraw'
|
||
? '撤回后申请退回草稿状态,已产生的审批记录保留可查;修改内容后可重新提交。仅本人发起的审批中变更可撤回。'
|
||
: '变更审批通过 / 投用后未实施或未投用的,由申请人填写情况说明,提交后变更转入关闭流程,由变更主管部门专责确认关闭并记录关闭时间。' }}
|
||
</div>
|
||
<NInput
|
||
class="mt-3"
|
||
v-model:value="reason"
|
||
type="textarea"
|
||
:rows="3"
|
||
:placeholder="modalType === 'withdraw' ? '撤回理由(必填)…' : '情况说明(必填):原因、现场当前状态、后续处置安排…'"
|
||
/>
|
||
</template>
|
||
<template #footer>
|
||
<div class="flex justify-end gap-[10px]">
|
||
<NButton size="small" @click="revokeModal = false">取消</NButton>
|
||
<NButton size="small" type="primary" :loading="revokeSaving" @click="revokeSave">提交</NButton>
|
||
</div>
|
||
</template>
|
||
</NModal>
|
||
<!-- 进度弹框 -->
|
||
<NDrawer v-model:show="progressDrawer" :width="502" placement="right" :auto-focus="false">
|
||
<NDrawerContent title="变更进度">
|
||
<div class="rounded-lg border bg-slate-50/60 px-3 py-2.5">
|
||
<div class="text-sm font-semibold text-slate-800">{{ currentRow?.title }}</div>
|
||
<div class="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-400">
|
||
<span class="font-mono">{{ currentRow?.change_no }}</span>
|
||
<NTag v-if="currentRow.status==='APPROVING'" size="small" type="warning">审批中</NTag>
|
||
<NTag v-if="currentRow.status==='APPROVED'" size="small" type="success">已通过</NTag>
|
||
<NTag v-if="currentRow.status==='IMPLEMENTED'" size="small" type="success">待验收</NTag>
|
||
<NTag v-if="currentRow.status==='ACCEPTED'" size="small" type="error">待关闭</NTag>
|
||
<NTag v-if="currentRow.status==='CLOSED'" size="small" type="success">已关闭</NTag>
|
||
<NTag v-if="currentRow.status==='WITHDRAWN'" size="small" type="error">已撤回</NTag>
|
||
<NTag v-if="currentRow.status==='REJECTED'" size="small" type="error">已驳回</NTag>
|
||
<span v-if="currentRow?.overdue_status===2" class="font-medium text-red-500">超期 {{ currentRow?.overdue_days }} 天</span>
|
||
</div>
|
||
<div v-if="currentRow?.dept || currentRow?.applicant" class="mt-1 text-[11px] text-slate-400">{{ [currentRow?.dept, currentRow?.applicant].filter(Boolean).join(' · ') }}</div>
|
||
<div v-if="currentRow?.meta" class="mt-1 text-[11px] text-slate-400">{{ currentRow?.meta }}</div>
|
||
</div>
|
||
<div class="mt-4">
|
||
<div v-for="(s, i) in currentRow?.approval_records" :key="s.id" class="flex gap-3">
|
||
<div class="flex flex-col items-center">
|
||
<span class="flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2 border-[#7CC242] bg-[#7CC242] text-white">
|
||
<Icon icon="ix:success" />
|
||
</span>
|
||
<span
|
||
v-if="Number(i) < Number(currentRow?.approval_records?.length) - 1"
|
||
class="w-0.5 flex-1"
|
||
:class="Number(i) < Number(currentRow?.approval_records?.length) ? 'bg-[#7CC242]' : 'bg-slate-200'"
|
||
></span>
|
||
</div>
|
||
<div class="pb-5">
|
||
<p class="text-sm">{{ s.action }}</p>
|
||
<span class="mt-0.5 text-[11px] text-slate-400">{{s.finished_at?.slice(0, 16)}}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="currentRow?.duration_type === 2" class="rounded-lg border border-amber-200 bg-amber-50/50 px-3 py-2 text-[11px] leading-5 text-amber-700">
|
||
临时变更:计划恢复 {{ currentRow?.restore_deadline ?? '—' }} · 恢复 / 延期 / 转永久请在详情页中操作
|
||
</div>
|
||
</NDrawerContent>
|
||
</NDrawer>
|
||
</NCard>
|
||
</NSpin>
|
||
</NSpace>
|
||
</template>
|
||
|
||
<style scoped>
|
||
|
||
</style>
|
||
<style scoped lang="scss">
|
||
.scroll {
|
||
height: calc(100vh - 338px);
|
||
overflow-y: auto;
|
||
}
|
||
</style>
|