角色管理和审批模板管理完成

This commit is contained in:
2026-08-17 17:09:27 +08:00
parent 8008754996
commit df9f9b3693
12 changed files with 937 additions and 151 deletions
+547
View File
@@ -0,0 +1,547 @@
<script setup lang="ts">
import { ref, computed, h, onMounted } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme';
import { useRouter } from 'vue-router';
import { NButton, NPopconfirm, NTag } from 'naive-ui';
const router = useRouter();
const appStore = useAppStore();
const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const loading = ref<boolean>(false);
const btnLoading = ref<boolean>(false);
const currentType = ref<string>('add');
const readonly = computed(() => currentType.value === "view");
const formRef = ref<any>(null);
const currentTemplate = ref<{ id: number, category: string | null, level: string | null, timeLimit: string | null, steps: any[] }>({
id: 0,
category: null,
level: null,
timeLimit: null,
steps: [],
});
const rules = ref({
category: { required: true, message: '变更类别不能为空', trigger: ['blur'] },
level: { required: true, message: '变更等级不能为空', trigger: ['blur'] },
timeLimit: { required: true, message: '变更时效不能为空', trigger: ['blur'] },
step: { required: true, message: '审批步骤不能为空', trigger: ['blur'] }
})
const templateModal = ref<boolean>(false);
const categoryOptions = ref([
{
label: '工艺',
value: '工艺'
},
{
label: '设备',
value: '设备'
},
{
label: '管理',
value: '管理'
}
])
const levelOptions = ref([
{
label: '一般',
value: '一般'
},
{
label: '重要',
value: '重要'
},
])
const timeLimitOptions = ref([
{
label: '永久',
value: '永久'
},
{
label: '临时',
value: '临时'
},
])
const SPECIALTIES = ref<{ label: string, value: string }[]>([
{ label: '工艺', value: 'process' },
{ label: '设备', value: 'equipment' },
{ label: '仪表', value: 'instrument' },
{ label: '电气', value: 'electrical' },
{ label: '安全', value: 'safety' },
{ label: '质量', value: 'quality' }
]);
const pagination = ref<{ page: number, pageSize: number, pageCount: number }>({
page: 1,
pageSize: 20,
pageCount: 0,
})
const columns = ref([
{
title: '序号',
key: 'index',
width: 6,
align: 'center',
render: (row: any, index: number) => {
return h('span', {}, { default: () => (pagination.value.page - 1) * pagination.value.pageSize + index + 1 })
}
},
{
title: '变更类别',
key: 'category',
width: 8,
align: 'center',
render: (row: any, index: number) => {
return h(NTag, {
size: 'small',
type: 'default'
}, { default: () => `${row.category}` })
}
},
{
title: '变更等级',
key: 'level',
width: 8,
align: 'center',
render: (row: any, index: number) => {
return h(NTag, {
size: 'small',
type: `${row.level==='重要'?'error':'default'}`
}, { default: () => `${row.level}` })
}
},
{
title: '变更时效',
key: 'timeLimit',
width: 8,
align: 'center',
render: (row: any, index: number) => {
return h(NTag, {
size: 'small',
type: `${row.timeLimit==='临时'?'warning':'info'}`
}, { default: () => `${row.timeLimit}` })
}
},
{
title: '审批步骤',
key: 'step',
width: 44,
align: 'center',
render: (row: any) => {
const stepNames = row.steps?.map((step: any) => step.name) || [];
const text = stepNames.join(' → ');
return h('p', { class: 'text-left text-xs' }, text || '');
}
},
{
title: '操作',
key: 'action',
width: 12,
align: 'center',
fixed: 'right',
render: (row: any) => {
return h('div', { class: 'flex items-center justify-center gap-3' }, {
default: () => [
h(NButton, {
text: true,
size: 'tiny',
type: 'primary',
onClick: () => handleView(row)
}, {
// 渲染按钮文本
default: () => '查看'
}),
h(NButton, {
text: true,
size: 'tiny',
type: 'primary',
onClick: () => handleEdit(row)
}, {
// 渲染按钮文本
default: () => '编辑'
}),
h(NPopconfirm, {
// 组件 Props
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
handleDelete(row)
},
// 可选:自定义确认/取消按钮的样式
positiveButtonProps: { size: 'tiny', type: 'primary' },
negativeButtonProps: { size: 'tiny' }
}, {
// 插槽 (Slots)
trigger: () => h(NButton,
{
text: true,
size: 'tiny',
type: 'error',
},
{
default: () => '删除' // 按钮文字
}
),
default: () => '确定要删除吗?' // 弹出框的提示内容
}),
]
})
}
},
])
const list = ref<{ id: number, category: string, level: string, timeLimit: string, steps: any[] }[]>([])
// 获取列表
const getList = async () => {
loading.value = true;
setTimeout(() => {
loading.value = false;
list.value = [
{id:1,category:'工艺',level:'重要',timeLimit:'临时',steps: [
{
id:101,
name:'部门审核',
countersign: false,
members: [{person:'测试人员01',specialty:'',countersign:false}]
},
{
id:102,
name:'专业审批',
countersign: false,
members: [{person:'测试人员02',specialty:'',countersign:false}]
},
{id: 103,
name: "部门负责人批准",
countersign: false,
members: [ {person:'测试人员03',specialty:'',countersign:false}]
}
]},
{id:2,category:'设备',level:'一般',timeLimit:'临时',steps:[
]},
{id:3,category:'管理',level:'重要',timeLimit:'永久',steps:[
]},
{id:4,category:'工艺',level:'重要',timeLimit:'临时',steps:[
]},
]
}, 1000)
}
// 打开添加模板弹框
const addTemplate = () => {
currentType.value = 'add';
currentTemplate.value = {
id: 0,
category: null,
level: null,
timeLimit: null,
steps: [],
};
templateModal.value = true;
}
// 查看模板
const handleView = (row: any) => {
currentType.value = 'view';
currentTemplate.value = {
id: row.id,
category: row.category,
level: row.level,
timeLimit: row.timeLimit,
steps: row.steps,
};
templateModal.value = true;
}
// 编辑角色
const handleEdit = (row: any) => {
currentType.value = 'edit';
currentTemplate.value = {
id: row.id,
category: row.category,
level: row.level,
timeLimit: row.timeLimit,
steps: row.steps,
};
templateModal.value = true;
}
// 删除角色
const handleDelete = (row: any) => {
loading.value = true;
setTimeout(() => {
loading.value = false;
list.value = list.value.filter((item: any) => item.id !== row.id);
window.$message?.success('删除成功')
}, 1000)
}
// 保存角色
const saveRole = async (e: MouseEvent) => {
e.preventDefault()
formRef.value?.validate((errors: any) => {
if (!errors) {
btnLoading.value = true;
setTimeout(() => {
// 新增
if(currentType.value === 'add'){
list.value.push({
id:list.value.length+1,
category:currentTemplate.value?.category as string,
level:currentTemplate.value?.level as string,
timeLimit:currentTemplate.value?.timeLimit as string,
steps:currentTemplate.value?.steps as any[]
})
}
// 编辑
if(currentType.value === 'edit'){
const target = list.value.find((item:any) => item.id === currentTemplate.value.id);
if (target) {
target.category = currentTemplate.value?.category as string;
target.level = currentTemplate.value?.level as string;
target.timeLimit = currentTemplate.value?.timeLimit as string;
target.steps = currentTemplate.value?.steps as any[];
}
}
window.$message?.success('操作成功')
btnLoading.value = false;
templateModal.value = false;
}, 1000)
}
})
}
// 会签步骤:一个模板最多一步;只有会签步骤能选专业。
// 勾选/取消后,所有非会签步骤的人员专业与「必须会签」标记一并清空
const setStepCountersign = (target: any, v: boolean) => {
currentTemplate.value.steps.forEach((s:any) => {
s.countersign = s === target ? v : false;
if (!s.countersign) s.members.forEach((m:any) => { m.specialty = ""; m.countersign = false; });
});
}
// 当前打开人员选择器的步骤(步骤行「+」按钮切换)
const pickerStepId = ref<number | null>(null);
const pick = ref<string | null>(null);
const togglePicker = (s: any) => {
pickerStepId.value = pickerStepId.value === s.id ? null : s.id;
pick.value = null;
}
const addMember = (s: any, v: string | null) => {
if (v && !s.members.some((m:any) => m.person === v)) s.members.push({ person: v, specialty: "", countersign: false });
pick.value = null;
pickerStepId.value = null;
}
const delMember = (s: any, p: string) => {
s.members = s.members.filter((m:any) => m.person !== p);
}
const removeStep = (idx: number) => {
currentTemplate.value.steps.splice(idx, 1);
}
let sid = 100;
const mkStep = (name: string, extra: Partial<any> = {}): any => ({ id: ++sid, name, countersign: false, members: [], ...extra });
const addStepAfter = (idx: number) => {
currentTemplate.value.steps.splice(idx + 1, 0, mkStep(""));
}
const ORG_TREE = [
{
id: "factory", name: "演示工厂", short: "YS", leaf: false, sort: 0,
children: [
{
id: "scb", name: "生产部", short: "SCB", leaf: false, sort: 0,
children: [
{ id: "cj1", name: "测试车间一", short: "CJ1", leaf: true, sort: 0 },
{ id: "cj2", name: "测试车间二", short: "CJ2", leaf: true, sort: 1 },
{ id: "cj3", name: "测试车间三", short: "CJ3", leaf: true, sort: 2 },
],
},
{
id: "ahb", name: "安环部", short: "AHB", leaf: false, sort: 1,
children: [
{ id: "aqk", name: "安全科", short: "AQK", leaf: true, sort: 0 },
{ id: "hbk", name: "环保科", short: "HBK", leaf: true, sort: 1 },
],
},
{
id: "sbb", name: "设备部", short: "SBB", leaf: false, sort: 2,
children: [
{ id: "sbk", name: "设备科", short: "SBK", leaf: true, sort: 0 },
],
},
{
id: "zlb", name: "质量部", short: "ZLB", leaf: false, sort: 3,
children: [
{ id: "zjk", name: "质检科", short: "ZJK", leaf: true, sort: 0 },
],
},
],
},
];
const LEVEL2_ORGS = ORG_TREE[0]?.children ?? [];
const ORG_MEMBERS = [
{ id: 1, name: "测试人员01", dept: "测试车间一", gender: "男", phone: "13800000001", email: "test01@demo.com", account: "TEST001", role: "操作人员", scope: ["factory"], enabled: true },
{ id: 2, name: "测试人员02", dept: "测试车间二", gender: "女", phone: "13800000002", email: "test02@demo.com", account: "TEST002", role: "班长", scope: ["factory"], enabled: true },
{ id: 3, name: "测试人员03", dept: "测试车间三", gender: "男", phone: "13800000003", email: "test03@demo.com", account: "TEST003", role: "车间主任", scope: ["factory"], enabled: true },
{ id: 4, name: "测试人员04", dept: "安全科", gender: "女", phone: "13800000004", email: "test04@demo.com", account: "TEST004", role: "安全工程师", scope: ["factory"], enabled: true },
{ id: 5, name: "测试人员05", dept: "环保科", gender: "男", phone: "13800000005", email: "test05@demo.com", account: "TEST005", role: "环保专员", scope: ["factory"], enabled: true },
{ id: 6, name: "测试人员06", dept: "设备科", gender: "男", phone: "13800000006", email: "test06@demo.com", account: "TEST006", role: "设备工程师", scope: ["factory"], enabled: true },
{ id: 7, name: "测试人员07", dept: "质检科", gender: "女", phone: "13800000007", email: "test07@demo.com", account: "TEST007", role: "质量工程师", scope: ["factory"], enabled: true },
];
function orgSubtreeNames(n: any): string[] {
return [n.name, ...(n.children ?? []).flatMap(orgSubtreeNames)];
}
// 预设人员:级联选项(第二层组织 → 组织下可选成员,已选成员不再出现,空组织不展示)
const peopleOpts = (s: any) =>
LEVEL2_ORGS.map((o) => ({
label: o.name,
value: o.id,
children: ORG_MEMBERS
.filter((m) => m.enabled && orgSubtreeNames(o).includes(m.dept) && !s.members.some((x) => x.person === m.name))
.map((m) => ({ label: m.name, value: m.name })),
})).filter((o) => o.children.length > 0);
onMounted(() => {
getList()
})
</script>
<template>
<NSpace vertical :size="16">
<!-- 角色管理 -->
<NCard :bordered="false" size="small" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<div class="border mb-3 py-2 px-3" :style="{ borderRadius: themeStore.themeRadius + 'px' }">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:12 m:12">
<div class="h-100% flex items-center gap-2">
<Icon icon="carbon:flow-connection" class="size-18px" :style="{ color: themeStore.themeColor }" />
<p class="text-[16px] font-600">审批模板管理</p>
</div>
</NGi>
<NGi span="24 s:12 m:12" class="flex items-center justify-end gap-12px">
<NButton type="primary" size="small" @click="addTemplate">
<Icon icon="ic:round-plus" class="size-16px" />
添加模板
</NButton>
</NGi>
</NGrid>
</div>
<div class="scroll">
<NDataTable
size="small"
:loading="loading"
:single-line="false"
:columns="columns"
:data="list"
:pagination="pagination"
max-height="calc(100vh - 290px)"
:scroll-x="820"
/>
</div>
<!-- 新增/编辑/查看弹框 -->
<NModal
v-model:show="templateModal"
preset="card"
:title="currentType === 'add' ? '新增模板' : currentType === 'edit' ? '编辑模板':'查看模板'"
:auto-focus="false"
:style="{ width: '800px', height: 'auto' }"
:segmented="{ content: true, footer: true }"
>
<template #default>
<NForm class="w-full" ref="formRef" :model="currentTemplate" :rules="rules" label-placement="left" label-width="auto">
<NGrid :cols="24" :x-gap="24">
<NFormItemGi :span="8" label="变更类别" path="category">
<NSelect
v-model:value="currentTemplate.category"
:options="categoryOptions"
:disabled="readonly"
placeholder="请选择"
/>
</NFormItemGi>
<NFormItemGi :span="8" label="变更等级" path="level">
<NSelect
v-model:value="currentTemplate.level"
:options="levelOptions"
:disabled="readonly"
placeholder="请选择"
/>
</NFormItemGi>
<NFormItemGi :span="8" label="变更时效" path="timeLimit">
<NSelect
v-model:value="currentTemplate.timeLimit"
:options="timeLimitOptions"
:disabled="readonly"
placeholder="请选择"
/>
</NFormItemGi>
</NGrid>
<!-- 审批步骤 -->
<div class="flex flex-col gap-3 mb-3">
<div v-for="(s, idx) in currentTemplate.steps" :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">
<span class="w-7 shrink-0 text-center text-xs text-slate-400">{{ idx + 1 }}</span>
<NInput v-model:value="s.name" maxlength="12" :disabled="readonly" placeholder="步骤名称(如:部门审核)" class="!w-48 text-sm" />
<NCheckbox :checked="s.countersign" :disabled="readonly" size="small" @update:checked="(v) => setStepCountersign(s, v)">
<span class="text-xs text-slate-500">专业会签步骤</span>
</NCheckbox>
<NButton v-if="!readonly" ghost type="primary" size="tiny" @click="togglePicker(s)" title="添加预设人员" class="ml-auto">
<Icon icon="ic:round-plus" class="size-16px" />
</NButton>
<NButton v-if="!readonly" text type="error" @click="removeStep(idx)" title="删除该步骤">
<Icon icon="iconamoon:trash" class="size-16px" />
</NButton>
</div>
<!-- 预设人员人员必选仅会签步骤可选择专业必选且人员可勾选必须进行专业会签 -->
<div class="mt-2 flex gap-2 pl-10">
<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 in s.members" :key="m.person" class="flex min-h-7 flex-wrap items-center gap-2">
<span class="w-20 shrink-0 truncate text-xs font-medium text-slate-700">{{ m.person }}</span>
<template v-if="s.countersign">
<span class="text-xs text-slate-400"><span class="mr-0.5 text-red-500">*</span>专业:</span>
<NSelect :value="m.specialty || null" clearable size="small" placeholder="请选择专业" class="w-28"
:options="SPECIALTIES"
@update:value="(v) => m.specialty = v ?? ''"
/>
<NCheckbox :checked="m.countersign" size="small"
@update:checked="(v) => m.countersign = v"
>
<span class="text-xs text-slate-500">必须参与会签</span>
</NCheckbox>
</template>
<NButton v-if="!readonly" text type="error" @click="delMember(s, m.person)" 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="!readonly && pickerStepId === s.id" :value="pick" :options="peopleOpts(s)"
placeholder="请选择人员" size="small" check-strategy="child" :show-path="false" expand-trigger="click"
class="w-56" @update:value="(v) => addMember(s, v == null ? null : String(v))" />
<span v-if="readonly && s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span>
</div>
</div>
</div>
</div>
<NButton v-if="!readonly" size="small" ghost dashed class="text-xs" @click="addStepAfter(currentTemplate.steps.length - 1)"><Icon icon="ic:round-plus" class="size-14px" />添加审批步骤</NButton>
</NForm>
</template>
<template #footer>
<div v-if="!readonly" class="flex justify-end gap-[10px]">
<NButton @click="templateModal = false">取消</NButton>
<NButton type="primary" :loading="btnLoading" @click="saveRole">确定</NButton>
</div>
</template>
</NModal>
</NCard>
</NSpace>
</template>
<style scoped lang="scss">
.scroll {
height: calc(100vh - 205px);
overflow-y: auto;
}
</style>