系统配置接口完成

This commit is contained in:
2026-08-28 15:04:44 +08:00
parent 6651cb6749
commit a473951901
4 changed files with 332 additions and 325 deletions
+24
View File
@@ -30,3 +30,27 @@ export function deleteSystemConfigApi(id:number) {
method: 'delete', method: 'delete',
}); });
} }
// 审批模板列表
export function templateListApi() {
return request({
url: '/api/approval-templates',
method: 'get',
});
}
// 审批用户列表
export function peopleListApi() {
return request({
url: '/api/approval-flow/users',
method: 'get',
});
}
// 批量修改ai配置项和变更编号规则
export function updateConfigApi(data:{items:any[]}) {
return request({
url: '/api/system-config/batch',
method: 'put',
data,
});
}
+2 -7
View File
@@ -29,11 +29,6 @@ const rules = ref({
}) })
const roleModal = ref<boolean>(false); const roleModal = ref<boolean>(false);
const permissionModal = ref<boolean>(false); const permissionModal = ref<boolean>(false);
const pagination = ref<{ page: number, pageSize: number, pageCount: number }>({
page: 1,
pageSize: 20,
pageCount: 0,
})
const columns = ref([ const columns = ref([
{ {
title: '序号', title: '序号',
@@ -41,7 +36,7 @@ const columns = ref([
width: 5, width: 5,
align: 'center', align: 'center',
render: (_row: any, index: number) => { render: (_row: any, index: number) => {
return h('span', {}, { default: () => (pagination.value.page - 1) * pagination.value.pageSize + index + 1 }) return h('span', {}, { default: () => index + 1 })
} }
}, },
{ {
@@ -230,7 +225,7 @@ onMounted(() => {
:single-line="false" :single-line="false"
:columns="columns" :columns="columns"
:data="list" :data="list"
:pagination="pagination" :pagination="false"
max-height="calc(100vh - 290px)" max-height="calc(100vh - 290px)"
/> />
</div> </div>
+108 -97
View File
@@ -4,26 +4,21 @@ import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common'; import { getColorWithOpacity, tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { systemConfigApi, addSystemConfigApi, editSystemConfigApi, deleteSystemConfigApi } from '@/service/api/system'; import { systemConfigApi, addSystemConfigApi, editSystemConfigApi, deleteSystemConfigApi, updateConfigApi } from '@/service/api/system';
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16)); const gap = computed(() => (appStore.isMobile ? 0 : 16));
// AI 辅助功能配置 // *********************AI 辅助功能配置*********************//
const aiEnabled = ref<boolean>(true); const aiConfig = ref<any[]>([])
const aiConfig = ref<{address:string, mode:number|null, key:string,timeout:null|number}>({ const switchLoading = ref<boolean>(false);
address:'',
mode:null,
key:'',
timeout:null,
})
const aiTested = ref<string>("none");
const modeOptions = ref<{label:string, value:number}[]>([ const modeOptions = ref<{label:string, value:number}[]>([
{label:'kimi-k2(企业私有化部署)',value:1} {label:'kimi-k2(企业私有化部署)',value:1}
]) ])
const testing = ref<boolean>(false); const testing = ref<boolean>(false);
const showLog = ref<boolean>(false); const showLog = ref<boolean>(false);
const AI_LOGS = [ const AI_LOGS = [
{ scene: "AI-01 变更识别", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:12", cost: "1.8s", adopted: "修改后采纳" }, { scene: "AI-01 变更识别", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:12", cost: "1.8s", adopted: "修改后采纳" },
{ scene: "AI-03 风险预分析", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:15", cost: "2.4s", adopted: "采纳" }, { scene: "AI-03 风险预分析", user: "张工艺", change: "MOC-2026-R01-0041", time: "08-24 09:15", cost: "2.4s", adopted: "采纳" },
@@ -32,6 +27,34 @@ const AI_LOGS = [
{ scene: "AI-09 关闭辅助判定", user: "系统管理员", change: "MOC-2026-R01-0035", time: "08-23 15:02", cost: "1.5s", adopted: "采纳" }, { scene: "AI-09 关闭辅助判定", user: "系统管理员", change: "MOC-2026-R01-0035", time: "08-23 15:02", cost: "1.5s", adopted: "采纳" },
]; ];
// AI 辅助功能切换
const toggle = async (v: boolean, item:any) => {
switchLoading.value = true;
const {error} = await updateConfigApi({items:[{
id:item.id,
label:item.label,
remark:item.remark,
value:v ? '1' : '0'
}]});
if(!error){
item.value = v ? '1' : '0';
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
}
switchLoading.value = false;
}
// 测试连接
const testConn = () => {
testing.value = true;
setTimeout(() => {
testing.value = false;
window.$message?.success(`连接测试通过:${aiConfig.value.mode} 响应正常(耗时 0.8s`);
}, 900);
}
// 保存ai配置
const saveAiCfg = () => {
window.$message?.success("AI 接口配置已保存(配置变更留痕,立即生效)");
}
// *********************变更类型*********************// // *********************变更类型*********************//
const typeRef = ref<any>(null); const typeRef = ref<any>(null);
@@ -212,27 +235,24 @@ const levelSave = async (e:MouseEvent) => {
// *********************变更编号规则*********************// // *********************变更编号规则*********************//
const ruleLoading = ref<boolean>(false);
const previewNumber = computed(() => { const previewNumber = computed(() => {
let prefix = ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.value || '';
let year = ''; let year = '';
let code = ruleConfig.value.find((item:any) => item.item_key === 'unit_code')?.value || '';
let digit = ''; let digit = '';
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
if (ruleInfo.value.year === 'yyyy') { if (ruleConfig.value.find((item:any) => item.item_key === 'year_format')?.value === 'yyyy') {
year = currentYear.toString(); year = currentYear.toString();
} else if (ruleInfo.value.year === 'yy') { } else if (ruleConfig.value.find((item:any) => item.item_key === 'year_format')?.value === 'yy') {
year = currentYear.toString().substring(2); year = currentYear.toString().substring(2);
} }
if (ruleInfo.value.digit) { if (ruleConfig.value.find((item:any) => item.item_key === 'seq_length')?.value) {
digit = '0'.repeat(ruleInfo.value.digit - 1) + '1'; digit = '0'.repeat(ruleConfig.value.find((item:any) => item.item_key === 'seq_length')?.value - 1) + '1';
} }
return ruleInfo.value.prefix+'-' + year + '-' + ruleInfo.value.code + '-' + digit; return prefix+'-' + year + '-' + code + '-' + digit;
}); });
const ruleInfo = ref<{prefix:string, year:null|string, code:string|null, digit:number|null, rule:string|null}>({ const ruleConfig = ref<any[]>([])
prefix:'',
year:null,
code: null,
digit: null,
rule: null,
})
const yearOptions = ref<{label:string, value:string}[]>([ const yearOptions = ref<{label:string, value:string}[]>([
{label:'四位年份(yyyy',value:'yyyy'}, {label:'四位年份(yyyy',value:'yyyy'},
{label:'两位年份(yy',value:'yy'}, {label:'两位年份(yy',value:'yy'},
@@ -247,7 +267,21 @@ const ruleOptions = ref<{label:string, value:string}[]>([
{label:'连续不重置',value:'CONTINUE'}, {label:'连续不重置',value:'CONTINUE'},
]) ])
// ---------- 风险矩阵设置(按分析方法) ---------- // 保存编号规则
const saveRule = async () => {
if(ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.value===''){
return window.$message?.warning(`请输入${ruleConfig.value.find((item:any) => item.item_key === 'prefix')?.label}`);
}
ruleLoading.value = true;
const {error} = await updateConfigApi({items:ruleConfig.value});
if(!error){
window.$message?.success("保存成功");
}
ruleLoading.value = false;
}
// *************** 风险矩阵设置(按分析方法) ***************//
const dimensionModal = ref<boolean>(false); const dimensionModal = ref<boolean>(false);
const dimensionLoading = ref<boolean>(false); const dimensionLoading = ref<boolean>(false);
const dimForm = ref({ s: [], l: [] }); const dimForm = ref({ s: [], l: [] });
@@ -299,22 +333,6 @@ const cellLevel = (s:number, l:number) => {
const sc = cellScore(s, l); const sc = cellScore(s, l);
return curMatrix.value.levels.find((lv:{min:number,max:number}) => sc >= lv.min && sc <= lv.max); return curMatrix.value.levels.find((lv:{min:number,max:number}) => sc >= lv.min && sc <= lv.max);
} }
// AI 辅助功能切换
const toggle = (v: boolean) => {
window.$message?.success(v ? "AI 辅助功能已启用" : "AI 辅助功能已关闭");
aiEnabled.value = v;
}
const testConn = () => {
testing.value = true;
setTimeout(() => {
testing.value = false;
aiTested.value = "ok";
window.$message?.success(`连接测试通过:${aiConfig.value.mode} 响应正常(耗时 0.8s`);
}, 900);
}
const saveAiCfg = () => {
window.$message?.success("AI 接口配置已保存(配置变更留痕,立即生效)");
}
const dimensionSave = () => { const dimensionSave = () => {
dimensionLoading.value = true; dimensionLoading.value = true;
setTimeout(() => { setTimeout(() => {
@@ -340,17 +358,11 @@ const getConfig = async () => {
// 解析 AI 辅助功能配置 // 解析 AI 辅助功能配置
const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist'); const aiAssistGroup = data.find((group:{group_key:string}) => group.group_key === 'ai_assist');
if (aiAssistGroup) { if (aiAssistGroup) {
const items = aiAssistGroup.items; aiConfig.value = aiAssistGroup.items;
items.forEach((item:any) => { const target = aiConfig.value.find((item:any) => item.item_key === 'timeout_seconds');
switch (item.item_key) { if (target) {
case 'enabled': aiEnabled.value = item.value === '1'; break; target.value = Number(target.value);
case 'api_url': aiConfig.value.address = item.value; break;
case 'model': aiConfig.value.mode = item.value; break;
case 'api_key': aiConfig.value.key = item.value; break;
case 'timeout_seconds': aiConfig.value.timeout = Number(item.value); break;
default: break;
} }
});
} }
// 解析 变更类型设置 // 解析 变更类型设置
const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type'); const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
@@ -365,17 +377,11 @@ const getConfig = async () => {
// 解析 变更编号规则 // 解析 变更编号规则
const changeCodeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_no_rule'); const changeCodeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_no_rule');
if (changeCodeGroup) { if (changeCodeGroup) {
const items = changeCodeGroup.items; ruleConfig.value = changeCodeGroup.items;
items.forEach((item:any) => { const target = ruleConfig.value.find((item:any) => item.item_key === 'seq_length');
switch (item.item_key) { if (target) {
case 'prefix': ruleInfo.value.prefix = item.value; break; target.value = Number(target.value);
case 'year_format': ruleInfo.value.year = item.value; break;
case 'unit_code': ruleInfo.value.code = item.value; break;
case 'seq_length': ruleInfo.value.digit = Number(item.value); break;
case 'seq_reset': ruleInfo.value.rule = item.value; break;
default: break;
} }
});
} }
} }
} }
@@ -405,59 +411,62 @@ onMounted(() => {
<Icon icon="lucide:sparkles" class="size-14px text-violet-500 mr-1" />AI 辅助功能 <Icon icon="lucide:sparkles" class="size-14px text-violet-500 mr-1" />AI 辅助功能
</p> </p>
<div class="py-3 px-4"> <div class="py-3 px-4">
<div class="rounded-lg border p-4"> <template v-for="item in aiConfig" :key="item.item_key">
<div v-if="item.item_key==='enabled'" class="rounded-lg border p-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-sm font-medium">启用 AI 辅助</div> <div class="text-sm font-medium">{{item.label}}</div>
<NSwitch size="small" :value="aiEnabled" @update:value="toggle" /> <NSwitch size="small" :value="item.value==='1'" :loading="switchLoading" @update:value="(v)=>toggle(v,item)" />
</div> </div>
<div class="mt-1 text-xs text-slate-500"> <div class="mt-1 text-xs text-slate-500">
开启后变更申请界面显示变更识别按钮AI 自动完成变更前后比较生成变更申请表均支持手动修改关闭后全部内容手动填写 {{item.remark}}
</div> </div>
</div> </div>
<div class="mt-3 flex items-center gap-2 rounded-lg px-3 py-2 text-xs" <div v-if="item.item_key==='enabled'" class="mt-3 flex items-center gap-2 rounded-lg px-3 py-2 text-xs"
:class="aiEnabled ? 'bg-[var(--success-bg)] text-[var(--success-color)]' : 'bg-[var(--warning-bg)] text-[var(--warning-color)]'"> :class="item.value==='1' ? 'bg-[var(--success-bg)] text-[var(--success-color)]' : 'bg-[var(--warning-bg)] text-[var(--warning-color)]'">
<Icon v-if="aiEnabled" icon="material-symbols:check-circle-outline" class="size-16px" /> <Icon v-if="item.value==='1'" icon="material-symbols:check-circle-outline" class="size-16px" />
<Icon v-else icon="proicons:alert-triangle" class="size-16px" /> <Icon v-else icon="proicons:alert-triangle" class="size-16px" />
当前状态{{ aiEnabled ? "已启用 —— 变更申请界面提供 AI 变更识别与申请表生成" : "已关闭 —— 变更申请界面为纯手动模式" }} 当前状态{{ item.value==='1' ? "已启用 —— 变更申请界面提供 AI 变更识别与申请表生成" : "已关闭 —— 变更申请界面为纯手动模式" }}
</div> </div>
</template>
<div class="space-y-2 border rounded-md p-3 mt-3"> <div class="space-y-2 border rounded-md p-3 mt-3">
<div class="flex items-center"> <div class="flex items-center">
<Icon icon="bx:plug" class="size-14px text-slate-400 mr-1" /> <Icon icon="bx:plug" class="size-14px text-slate-400 mr-1" />
AI 接口配置 AI 接口配置
<NTag v-if="aiTested === 'ok'" size="small" type="success" class="ml-auto">连接正常</NTag>
</div> </div>
<div class="flex items-center"> <template v-for="item in aiConfig" :key="item.item_key">
<p class="w-100px text-left">接口地址</p> <div v-if="item.item_key==='api_url'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NInput v-model:value="aiConfig.address" placeholder="请输入接口地址" /> <NInput v-model:value="item.value" placeholder="请输入接口地址" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='model'" class="flex items-center">
<p class="w-100px text-left">模型</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NSelect v-model:value="aiConfig.mode" :options="modeOptions" placeholder="请选择模型" /> <NSelect v-model:value="item.value" :options="modeOptions" placeholder="请选择模型" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='api_key'" class="flex items-center">
<p class="w-100px text-left">API Key</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NInput v-model:value="aiConfig.key" placeholder="请输入API Key" /> <NInput v-model:value="item.value" placeholder="请输入API Key" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='timeout_seconds'" class="flex items-center">
<p class="w-100px text-left">超时</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NInputNumber v-model:value="aiConfig.timeout" :min="1" :show-button="false" :precision="0" placeholder="请输入超时时间" /> <NInputNumber v-model:value="item.value" :min="1" :show-button="false" :precision="0" placeholder="请输入超时时间" />
</div> </div>
</div> </div>
<div class="flex items-center gap-3"> </template>
<!-- <div class="flex items-center gap-3">
<NButton size="small" type="primary" class="text-xs" :loading="testing" @click="testConn">测试连接</NButton> <NButton size="small" type="primary" class="text-xs" :loading="testing" @click="testConn">测试连接</NButton>
<NButton size="small" class="text-xs" @click="saveAiCfg">保存配置</NButton> <NButton size="small" class="text-xs" @click="saveAiCfg">保存配置</NButton>
<NButton size="small" class="text-xs" @click="showLog = true"> <NButton size="small" class="text-xs" @click="showLog = true">
<Icon icon="akar-icons:file" class="size-14px mr-1" /> <Icon icon="akar-icons:file" class="size-14px mr-1" />
调用记录查询 调用记录查询
</NButton> </NButton>
</div> </div> -->
</div> </div>
</div> </div>
</NCard> </NCard>
@@ -610,40 +619,42 @@ onMounted(() => {
</div> </div>
<div class="py-3 px-4"> <div class="py-3 px-4">
<div class="space-y-2"> <div class="space-y-2">
<div class="flex items-center"> <template v-for="item in ruleConfig" :key="item.item_key">
<p class="w-100px text-left">编号前缀</p> <div v-if="item.item_key==='prefix'" class="flex items-center">
<p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NInput v-model:value="ruleInfo.prefix" placeholder="请输入编号前缀" /> <NInput v-model:value="item.value" :placeholder="`请输入${item.label}`" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='year_format'" class="flex items-center">
<p class="w-100px text-left">年份格式</p> <p class="w-100px text-left">年份格式</p>
<div class="flex-1"> <div class="flex-1">
<NSelect v-model:value="ruleInfo.year" :options="yearOptions" placeholder="请选择年份格式" /> <NSelect v-model:value="item.value" :options="yearOptions" :placeholder="`请选择${item.label}`" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='unit_code'" class="flex items-center">
<p class="w-100px text-left">装置代码段</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NSelect v-model:value="ruleInfo.code" :options="codeOptions" placeholder="请选择装置代码段" /> <NSelect v-model:value="item.value" :options="codeOptions" :placeholder="`请选择${item.label}`" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='seq_length'" class="flex items-center">
<p class="w-100px text-left">流水号位数</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NInputNumber v-model:value="ruleInfo.digit" :min="1" :show-button="false" :precision="0" placeholder="请输入流水号位数" /> <NInputNumber v-model:value="item.value" :min="1" :show-button="false" :precision="0" :placeholder="`请输入${item.label}`" />
</div> </div>
</div> </div>
<div class="flex items-center"> <div v-if="item.item_key==='seq_reset'" class="flex items-center">
<p class="w-100px text-left">流水重置规则</p> <p class="w-100px text-left">{{item.label}}</p>
<div class="flex-1"> <div class="flex-1">
<NSelect v-model:value="ruleInfo.rule" :options="ruleOptions" placeholder="请选择流水重置规则" /> <NSelect v-model:value="item.value" :options="ruleOptions" :placeholder="`请选择${item.label}`" />
</div> </div>
</div> </div>
</template>
<div class="flex items-center p-3 bg-[#f8fafc] rounded-md"> <div class="flex items-center p-3 bg-[#f8fafc] rounded-md">
<span>编号示例预览</span> <span>编号示例预览</span>
<span class="text-[var(--theme-color)]">{{previewNumber}}</span> <span class="text-[var(--theme-color)]">{{previewNumber}}</span>
<NButton size="small" type="primary" class="text-xs ml-auto">保存规则</NButton> <NButton size="small" type="primary" class="text-xs ml-auto" :loading="ruleLoading" @click="saveRule">保存规则</NButton>
</div> </div>
</div> </div>
<p class="mt-3 text-xs text-slate-400">保存草稿即预占编号提交后正式生效;规则修改不影响历史单据编号</p> <p class="mt-3 text-xs text-slate-400">保存草稿即预占编号提交后正式生效;规则修改不影响历史单据编号</p>
+170 -193
View File
@@ -3,10 +3,10 @@ import { ref, computed, h, onMounted } from 'vue';
import { useAppStore } from '@/store/modules/app'; import { useAppStore } from '@/store/modules/app';
import { Icon } from '@iconify/vue' import { Icon } from '@iconify/vue'
import { useThemeStore } from '@/store/modules/theme'; import { useThemeStore } from '@/store/modules/theme';
import { useRouter } from 'vue-router';
import { NButton, NPopconfirm, NTag } from 'naive-ui'; import { NButton, NPopconfirm, NTag } from 'naive-ui';
import { tagTextColor, tagBgColor, tagBorderColor } from '@/utils/common';
import { templateListApi, systemConfigApi, peopleListApi } from '@/service/api/system';
const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
const themeStore = useThemeStore(); const themeStore = useThemeStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16)); const gap = computed(() => (appStore.isMobile ? 0 : 16));
@@ -16,71 +16,47 @@ const btnLoading = ref<boolean>(false);
const currentType = ref<string>('add'); const currentType = ref<string>('add');
const readonly = computed(() => currentType.value === "view"); const readonly = computed(() => currentType.value === "view");
const formRef = ref<any>(null); const formRef = ref<any>(null);
const currentTemplate = ref<{ id: number, category: string | null, level: string | null, timeLimit: string | null, steps: any[] }>({ const currentTemplate = ref<{ id: number, category: string | null, level: string | null, duration: number | null, steps: any[] }>({
id: 0, id: 0,
category: null, category: null,
level: null, level: null,
timeLimit: null, duration: null,
steps: [], steps: [],
}); });
const rules = ref({ const rules = ref({
category: { required: true, message: '变更类别不能为空', trigger: ['blur'] }, category: { required: true, message: '变更类别不能为空', trigger: ['blur'] },
level: { required: true, message: '变更等级不能为空', trigger: ['blur'] }, level: { required: true, message: '变更等级不能为空', trigger: ['blur'] },
timeLimit: { required: true, message: '变更时效不能为空', trigger: ['blur'] }, duration: { required: true, message: '变更时效不能为空', trigger: ['blur'] },
step: { required: true, message: '审批步骤不能为空', trigger: ['blur'] } step: { required: true, message: '审批步骤不能为空', trigger: ['blur'] }
}) })
const templateModal = ref<boolean>(false); const templateModal = ref<boolean>(false);
const categoryOptions = ref([ const categoryOptions = ref<{ label: string, value: string, color: string }[]>([])
{ const levelOptions = ref<{ label: string, value: string, color: string }[]>([])
label: '工艺', const durationOptions = ref<{ label: string, value: number }[]>([
value: '工艺' { label: '永久', value: 1 },
}, { label: '临时', value: 2 },
{ ])
label: '设备', // 专业
value: '设备' const SPECIALTIES = [
}, { label: '生产', value: '生产' },
{ { label: '技术', value: '技术' },
label: '管理', { label: '仪表', value: '仪表' },
value: '管理' { label: '电气', value: '电气' },
{ label: '设备', value: '设备' },
{ label: '安全', value: '安全' },
{ label: '质量', value: '质量' }
];
// 审批用户列表
const peopleList = ref<{label: string, value: number,children: any[]}[]>([]);
// 根据value匹配label和color
const getLabel = (type: string, value: number) => {
if(type=='level'){
return levelOptions.value.find((item: { label: string, value: string, color: string }) => item.value === value.toString()) || null;
} }
]) return categoryOptions.value.find((item: { label: string, value: string, color: string }) => item.value === value.toString()) || null;
}
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([ const columns = ref([
{ {
title: '序号', title: '序号',
@@ -88,7 +64,7 @@ const columns = ref([
width: 6, width: 6,
align: 'center', align: 'center',
render: (row: any, index: number) => { render: (row: any, index: number) => {
return h('span', {}, { default: () => (pagination.value.page - 1) * pagination.value.pageSize + index + 1 }) return h('span', {}, { default: () => index + 1 })
} }
}, },
{ {
@@ -96,11 +72,15 @@ const columns = ref([
key: 'category', key: 'category',
width: 8, width: 8,
align: 'center', align: 'center',
render: (row: any, index: number) => { render: (row: any) => {
return h(NTag, { return h(NTag, {
size: 'small', size: 'small',
type: 'default' color: {
}, { default: () => `${row.category}` }) 'color': tagBgColor(getLabel('category', row.category)?.color || ''),
'textColor': tagTextColor(getLabel('category', row.category)?.color || ''),
'borderColor': tagBorderColor(getLabel('category', row.category)?.color || ''),
},
}, { default: () => `${getLabel('category', row.category)?.label || ''}` })
} }
}, },
{ {
@@ -108,23 +88,27 @@ const columns = ref([
key: 'level', key: 'level',
width: 8, width: 8,
align: 'center', align: 'center',
render: (row: any, index: number) => { render: (row: any) => {
return h(NTag, { return h(NTag, {
size: 'small', size: 'small',
type: `${row.level==='重要'?'error':'default'}` color: {
}, { default: () => `${row.level}` }) 'color': tagBgColor(getLabel('level', row.level)?.color || ''),
'textColor': tagTextColor(getLabel('level', row.level)?.color || ''),
'borderColor': tagBorderColor(getLabel('level', row.level)?.color || ''),
},
}, { default: () => `${getLabel('level', row.level)?.label || ''}` })
} }
}, },
{ {
title: '变更时效', title: '变更时效',
key: 'timeLimit', key: 'duration',
width: 8, width: 8,
align: 'center', align: 'center',
render: (row: any, index: number) => { render: (row: any) => {
return h(NTag, { return h(NTag, {
size: 'small', size: 'small',
type: `${row.timeLimit==='临时'?'warning':'info'}` type: `${row.duration===2?'warning':'info'}`
}, { default: () => `${row.timeLimit}` }) }, { default: () => `${row.duration===2?'临时':'永久'}` })
} }
}, },
{ {
@@ -194,46 +178,8 @@ const columns = ref([
} }
}, },
]) ])
const list = ref<{ id: number, category: string, level: string, timeLimit: string, steps: any[] }[]>([]) const list = ref<{ id: number, category: string, level: string, duration: number | null, 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 = () => { const addTemplate = () => {
@@ -242,7 +188,7 @@ const addTemplate = () => {
id: 0, id: 0,
category: null, category: null,
level: null, level: null,
timeLimit: null, duration: null,
steps: [], steps: [],
}; };
templateModal.value = true; templateModal.value = true;
@@ -252,26 +198,26 @@ const handleView = (row: any) => {
currentType.value = 'view'; currentType.value = 'view';
currentTemplate.value = { currentTemplate.value = {
id: row.id, id: row.id,
category: row.category, category: row.category.toString(),
level: row.level, level: row.level.toString(),
timeLimit: row.timeLimit, duration: row.duration,
steps: row.steps, steps: row.steps,
}; };
templateModal.value = true; templateModal.value = true;
} }
// 编辑角色 // 编辑模板
const handleEdit = (row: any) => { const handleEdit = (row: any) => {
currentType.value = 'edit'; currentType.value = 'edit';
currentTemplate.value = { currentTemplate.value = {
id: row.id, id: row.id,
category: row.category, category: row.category.toString(),
level: row.level, level: row.level.toString(),
timeLimit: row.timeLimit, duration: row.duration,
steps: row.steps, steps: row.steps,
}; };
templateModal.value = true; templateModal.value = true;
} }
// 删除角色 // 删除模板
const handleDelete = (row: any) => { const handleDelete = (row: any) => {
loading.value = true; loading.value = true;
setTimeout(() => { setTimeout(() => {
@@ -280,8 +226,8 @@ const handleDelete = (row: any) => {
window.$message?.success('删除成功') window.$message?.success('删除成功')
}, 1000) }, 1000)
} }
// 保存角色 // 保存模板
const saveRole = async (e: MouseEvent) => { const saveTemplate = async (e: MouseEvent) => {
e.preventDefault() e.preventDefault()
formRef.value?.validate((errors: any) => { formRef.value?.validate((errors: any) => {
if (!errors) { if (!errors) {
@@ -293,7 +239,7 @@ const saveRole = async (e: MouseEvent) => {
id:list.value.length+1, id:list.value.length+1,
category:currentTemplate.value?.category as string, category:currentTemplate.value?.category as string,
level:currentTemplate.value?.level as string, level:currentTemplate.value?.level as string,
timeLimit:currentTemplate.value?.timeLimit as string, duration:currentTemplate.value?.duration as number,
steps:currentTemplate.value?.steps as any[] steps:currentTemplate.value?.steps as any[]
}) })
} }
@@ -303,7 +249,7 @@ const saveRole = async (e: MouseEvent) => {
if (target) { if (target) {
target.category = currentTemplate.value?.category as string; target.category = currentTemplate.value?.category as string;
target.level = currentTemplate.value?.level as string; target.level = currentTemplate.value?.level as string;
target.timeLimit = currentTemplate.value?.timeLimit as string; target.duration = currentTemplate.value?.duration as number;
target.steps = currentTemplate.value?.steps as any[]; target.steps = currentTemplate.value?.steps as any[];
} }
} }
@@ -326,84 +272,114 @@ const setStepCountersign = (target: any, v: boolean) => {
const pickerStepId = ref<number | null>(null); const pickerStepId = ref<number | null>(null);
const pick = ref<string | null>(null); const pick = ref<string | null>(null);
const togglePicker = (s: any) => { const togglePicker = (s: any) => {
pickerStepId.value = pickerStepId.value === s.id ? null : s.id; pickerStepId.value = pickerStepId.value === s.unique_id ? null : s.unique_id;
pick.value = null; pick.value = null;
} }
// 添加审批步骤人员
const addMember = (s: any, v: string | 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 }); console.log(s,v)
if (v && !s.members.some((m:any) => m.user_name === v)){
s.members.push({ user_name: v, specialty: "", countersign: false });
pick.value = null; pick.value = null;
pickerStepId.value = null; pickerStepId.value = null;
}
} }
// 删除审批步骤人员
const delMember = (s: any, p: string) => { const delMember = (s: any, p: string) => {
s.members = s.members.filter((m:any) => m.person !== p); s.members = s.members.filter((m:any) => m.person !== p);
} }
// 删除审批步骤
const removeStep = (idx: number) => { const removeStep = (idx: number) => {
currentTemplate.value.steps.splice(idx, 1); 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 addStep = (idx: number) => {
const addStepAfter = (idx: number) => { let step = {
currentTemplate.value.steps.splice(idx + 1, 0, mkStep("")); unique_id: Date.now(),
step_order: currentTemplate.value.steps.length + 1,
name: "",
countersign: false,
members: [],
}
currentTemplate.value.steps.splice(idx + 1, 0, step);
} }
const ORG_TREE = [
{ // 获取列表
id: "factory", name: "演示工厂", short: "YS", leaf: false, sort: 0, const getList = async () => {
children: [ loading.value = true;
{ const {data,error} = await templateListApi();
id: "scb", name: "生产部", short: "SCB", leaf: false, sort: 0, if(!error){
children: [ list.value = data || [];
{ id: "cj1", name: "测试车间一", short: "CJ1", leaf: true, sort: 0 }, }
{ id: "cj2", name: "测试车间二", short: "CJ2", leaf: true, sort: 1 }, loading.value = false;
{ 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) => ({ const getConfig = async () => {
label: o.name, loading.value = true;
value: o.id, const {data,error} = await systemConfigApi();
children: ORG_MEMBERS if(!error){
.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 })), const changeTypeGroup = data.find((group:{group_key:string}) => group.group_key === 'change_type');
})).filter((o) => o.children.length > 0); if (changeTypeGroup) {
categoryOptions.value = changeTypeGroup.items;
}
// 变更等级
const changeLevelGroup = data.find((group:{group_key:string}) => group.group_key === 'change_level');
if (changeLevelGroup) {
levelOptions.value = changeLevelGroup.items;
}
getList();
}
loading.value = false;
}
// 人员数据转换
const groupUsersByOrg = (users:any) => {
const orgMap = new Map(); // key: org.id, value: { id, label, children }
users.forEach((user:any) => {
const { id: userId, realname, user_departments } = user;
if (!user_departments || user_departments.length === 0) return;
user_departments.forEach((dept:any) => {
const org = dept.org;
if (!org || !org.id) return;
if (!orgMap.has(org.id)) {
orgMap.set(org.id, {
value: org.id,
label: org.label,
children: []
});
}
const targetOrg = orgMap.get(org.id);
// 避免同一个用户重复加入同一个组织(理论上不会发生,但做去重保护)
if (!targetOrg.children.some((child:any) => child.id === userId)) {
targetOrg.children.push({
value: userId,
label: realname
});
}
});
});
return Array.from(orgMap.values());
}
// 获取审批用户列表
const getPeopleList = async () => {
const {data,error} = await peopleListApi();
if(!error){
peopleList.value = groupUsersByOrg(data);
console.log(groupUsersByOrg(data))
}
}
onMounted(() => { onMounted(() => {
getList() getPeopleList();
getConfig()
}) })
</script> </script>
@@ -434,7 +410,7 @@ onMounted(() => {
:single-line="false" :single-line="false"
:columns="columns" :columns="columns"
:data="list" :data="list"
:pagination="pagination" :pagination="false"
max-height="calc(100vh - 290px)" max-height="calc(100vh - 290px)"
:scroll-x="820" :scroll-x="820"
/> />
@@ -469,15 +445,15 @@ onMounted(() => {
</NFormItemGi> </NFormItemGi>
<NFormItemGi :span="8" label="变更时效" path="timeLimit"> <NFormItemGi :span="8" label="变更时效" path="timeLimit">
<NSelect <NSelect
v-model:value="currentTemplate.timeLimit" v-model:value="currentTemplate.duration"
:options="timeLimitOptions" :options="durationOptions"
:disabled="readonly" :disabled="readonly"
placeholder="请选择" placeholder="请选择"
/> />
</NFormItemGi> </NFormItemGi>
</NGrid> </NGrid>
<!-- 审批步骤 --> <!-- 审批步骤 -->
<div class="flex flex-col gap-3 mb-3"> <div v-if="currentTemplate.steps.length" 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 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"> <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> <span class="w-7 shrink-0 text-center text-xs text-slate-400">{{ idx + 1 }}</span>
@@ -496,42 +472,43 @@ onMounted(() => {
<div class="mt-2 flex gap-2 pl-10"> <div class="mt-2 flex gap-2 pl-10">
<span class="shrink-0 text-xs leading-7 text-slate-400">预设人员:</span> <span class="shrink-0 text-xs leading-7 text-slate-400">预设人员:</span>
<div class="min-w-0 flex-1 space-y-1.5"> <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"> <div v-for="m in s.members" :key="m.id" 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> <span class="w-20 shrink-0 truncate text-xs font-medium text-slate-700">{{ m.user_name }}</span>
<template v-if="s.countersign"> <template v-if="s.countersign">
<span class="text-xs text-slate-400"><span class="mr-0.5 text-red-500">*</span>专业:</span> <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" <NSelect :value="m.specialty || null" clearable size="small" placeholder="请选择专业" class="w-28"
:options="SPECIALTIES" :options="SPECIALTIES"
@update:value="(v) => m.specialty = v ?? ''" @update:value="(v) => m.specialty = v ?? null"
/> />
<NCheckbox :checked="m.countersign" size="small" <NCheckbox :checked="m.is_sign" size="small"
@update:checked="(v) => m.countersign = v" @update:checked="(v) => m.is_sign = v"
> >
<span class="text-xs text-slate-500">必须参与会签</span> <span class="text-xs text-slate-500">必须参与会签</span>
</NCheckbox> </NCheckbox>
</template> </template>
<NButton v-if="!readonly" text type="error" @click="delMember(s, m.person)" title="删除该人员"> <NButton v-if="!readonly" text type="error" @click="delMember(s, m.user_name)" title="删除该人员">
<Icon icon="iconamoon:trash" class="size-16px" /> <Icon icon="iconamoon:trash" class="size-16px" />
</NButton> </NButton>
</div> </div>
<p v-if="s.countersign && s.members.length" class="text-[11px] leading-5 text-slate-400"> <p v-if="s.countersign && s.members.length" class="text-[11px] leading-5 text-slate-400">
必须参与会签勾选的人员必须参与专业会签未勾选的人员为可选会签按需参加 必须参与会签勾选的人员必须参与专业会签未勾选的人员为可选会签按需参加
</p> </p>
<NCascader v-if="!readonly && pickerStepId === s.id" :value="pick" :options="peopleOpts(s)" <NCascader v-if="!readonly && pickerStepId === s.unique_id" :value="pick" :options="peopleList"
placeholder="请选择人员" size="small" check-strategy="child" :show-path="false" expand-trigger="click" 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))" /> class="w-56" :on-update:value="(v) => addMember(s, v ? null : String(v))" />
<span v-if="readonly && s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span> <span v-if="readonly && s.members.length === 0" class="text-xs leading-7 text-slate-300">未预设</span>
</div> </div>
</div> </div>
</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> <NButton v-if="!readonly" size="small" ghost dashed class="text-xs" @click="addStep(currentTemplate.steps.length - 1)"><Icon icon="ic:round-plus" class="size-14px" />添加审批步骤</NButton>
</NForm> </NForm>
</template> </template>
<template #footer> <template #footer>
<div v-if="!readonly" class="flex justify-end gap-[10px]"> <div v-if="!readonly" class="flex justify-end gap-[10px]">
<NButton @click="templateModal = false">取消</NButton> <NButton @click="templateModal = false">取消</NButton>
<NButton type="primary" :loading="btnLoading" @click="saveRole">确定</NButton> <NButton type="primary" :loading="btnLoading" @click="saveTemplate">确定</NButton>
</div> </div>
</template> </template>
</NModal> </NModal>