提交 4c79503d authored 作者: liuluyu's avatar liuluyu

计划管理样式更新

上级 6a232359
/**
* 计划执行表单状态管理
* 用于在不同页面之间共享表单保存方法
*/
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { reactive, ref } from 'vue';
export const usePlanFormStore = defineStore('planForm', () => { export const usePlanFormStore = defineStore('planFormStore', () => {
// 存储表单的 ref 回调 // 表单提交回调函数
const submitCallback = ref<() => Promise<any> | null>(null); const submitCallback = ref<(() => Promise<void>) | null>(null);
const formData = ref<any>(null);
/** // 表单数据缓存
* 注册表单提交回调(在 StPlanExcuteForm 中调用) const formDataCache = reactive({
* @param callback - 表单的 submitForm 方法 id: '',
*/ executeStatus: '',
const registerSubmitCallback = (callback: () => Promise<any>) => { actualStartTime: null,
actualEndTime: null,
executeEcord: '',
attachments: [] as any[],
});
// 表单加载状态
const formLoadingState = reactive({
isLoading: false,
isError: false,
errorMessage: '',
lastLoadedId: '',
});
// 注册表单提交回调
const registerSubmitCallback = (callback: () => Promise<void>) => {
submitCallback.value = callback; submitCallback.value = callback;
console.log('[PlanFormStore] 已注册表单保存回调');
}; };
/** // 执行表单提交
* 注册表单数据(用于跨页面访问) const executeSubmit = async () => {
* @param data - 表单数据 if (submitCallback.value) {
*/ try {
const setFormData = (data: any) => { formLoadingState.isError = false;
formData.value = data; formLoadingState.errorMessage = '';
await submitCallback.value();
} catch (error: any) {
formLoadingState.isError = true;
formLoadingState.errorMessage = error?.message || '表单提交失败';
throw error;
}
}
}; };
/** // 更新表单数据缓存
* 获取表单数据 const updateFormDataCache = (data: any) => {
*/ Object.assign(formDataCache, data);
const getFormData = () => {
return formData.value;
}; };
/** // 清除表单缓存
* 执行表单保存(在 TodoIndex 中调用) const clearFormCache = () => {
*/ Object.assign(formDataCache, {
const submitPlanForm = async () => { id: '',
if (!submitCallback.value) { executeStatus: '',
console.warn('[PlanFormStore] 未注册表单保存回调'); actualStartTime: null,
return null; actualEndTime: null,
} executeEcord: '',
attachments: [],
});
formLoadingState.lastLoadedId = '';
};
try { // 设置加载状态
console.log('[PlanFormStore] 开始执行表单保存...'); const setLoadingState = (isLoading: boolean) => {
const result = await submitCallback.value(); formLoadingState.isLoading = isLoading;
console.log('[PlanFormStore] 表单保存成功:', result);
return result;
} catch (error) {
console.error('[PlanFormStore] 表单保存失败:', error);
throw error;
}
}; };
/** // 设置最后加载的ID
* 清空回调(可选) const setLastLoadedId = (id: string) => {
*/ formLoadingState.lastLoadedId = id;
const clearCallback = () => {
submitCallback.value = null;
console.log('[PlanFormStore] 已清空表单保存回调');
}; };
return { return {
submitCallback, submitCallback,
formData, formDataCache,
formLoadingState,
registerSubmitCallback, registerSubmitCallback,
setFormData, executeSubmit,
getFormData, updateFormDataCache,
submitPlanForm, clearFormCache,
clearCallback, setLoadingState,
setLastLoadedId,
}; };
}); });
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { BasicColumn, FormSchema } from '/@/components/Table'; import { BasicColumn, FormSchema } from '/@/components/Table';
import { render } from '/@/utils/common/renderUtils'; import { render } from '/@/utils/common/renderUtils';
//列表数据
// ==================== 执行规则选项 ====================
export const exeRuleOptions = [
{ value: 1, label: '一次性' },
{ value: 2, label: '周期执行' },
{ value: 3, label: '事件触发' },
];
// ==================== 执行周期选项 ====================
export const exePeriodOptions = [
{ value: 'daily', label: '每日' },
{ value: 'weekly', label: '每周' },
{ value: 'monthly', label: '每月' },
{ value: 'quarterly', label: '每季度' },
{ value: 'halfyear', label: '每半年' },
{ value: 'yearly', label: '每年' },
];
// ==================== 优先级选项 ====================
export const priorityOptions = [
{ value: '1', label: '高' },
{ value: '2', label: '中' },
{ value: '3', label: '低' },
];
// ==================== 计划状态选项 ====================
export const planStatusOptions = [
{ value: '0', label: '草稿' },
{ value: '1', label: '审批中' },
{ value: '2', label: '已通过' },
{ value: '3', label: '已拒绝' },
{ value: '4', label: '执行中' },
{ value: '5', label: '已完成' },
{ value: '6', label: '已作废' },
];
// 列表数据
export const columns: BasicColumn[] = [ export const columns: BasicColumn[] = [
{ {
title: '计划名称', title: '计划名称',
align: 'left', align: 'left',
dataIndex: 'projectName', dataIndex: 'projectName',
width: 200, width: 180,
ellipsis: true, ellipsis: true,
}, },
{ {
title: '类型', title: '计划类型',
align: 'center', align: 'center',
dataIndex: 'projectTypeName', dataIndex: 'projectTypeName',
width: 120, width: 120,
ellipsis: true, ellipsis: true,
}, },
// { {
// title: '执行部门', title: '执行部门',
// align: 'center', align: 'center',
// dataIndex: 'execDepName', dataIndex: 'execDepName',
// width: 150, width: 140,
// ellipsis: true, ellipsis: true,
// }, },
{ {
title: '负责人', title: '负责人',
align: 'center', align: 'center',
...@@ -32,19 +68,39 @@ export const columns: BasicColumn[] = [ ...@@ -32,19 +68,39 @@ export const columns: BasicColumn[] = [
ellipsis: true, ellipsis: true,
}, },
{ {
title: '计划开始日期', title: '优先级',
align: 'center', align: 'center',
dataIndex: 'planStartDate', dataIndex: 'priority',
width: 130, width: 80,
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
return !text ? '-' : text.length > 10 ? text.substr(0, 10) : text; const priorityMap = {
'1': '高',
'2': '中',
'3': '低',
};
return priorityMap[text] || '-';
}, },
}, },
{ {
title: '计划结束日期', title: '执行规则',
align: 'center', align: 'center',
dataIndex: 'planEndDate', dataIndex: 'exeRule',
width: 100,
ellipsis: true,
customRender: ({ text }) => {
const ruleMap = {
1: '事件触发',
2: '周期执行',
3: '一次性执行',
};
return ruleMap[text] || '-';
},
},
{
title: '计划开始日期',
align: 'center',
dataIndex: 'planStartDate',
width: 130, width: 130,
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
...@@ -52,22 +108,17 @@ export const columns: BasicColumn[] = [ ...@@ -52,22 +108,17 @@ export const columns: BasicColumn[] = [
}, },
}, },
{ {
title: '执行规则', title: '计划结束日期',
align: 'center', align: 'center',
dataIndex: 'exeRule', dataIndex: 'planEndDate',
width: 100, width: 130,
ellipsis: true, ellipsis: true,
customRender: ({ text }) => { customRender: ({ text }) => {
const ruleMap = { return !text ? '-' : text.length > 10 ? text.substr(0, 10) : text;
1: '每发生',
2: '周期性',
3: '一次性',
};
return ruleMap[text] || '-';
}, },
}, },
{ {
title: '状态', title: '计划状态',
align: 'center', align: 'center',
dataIndex: 'statusName', dataIndex: 'statusName',
width: 100, width: 100,
...@@ -91,7 +142,8 @@ export const columns: BasicColumn[] = [ ...@@ -91,7 +142,8 @@ export const columns: BasicColumn[] = [
ellipsis: false, ellipsis: false,
}, },
]; ];
//查询数据
// 查询表单数据
export const searchFormSchema: FormSchema[] = [ export const searchFormSchema: FormSchema[] = [
{ {
label: '计划名称', label: '计划名称',
...@@ -100,7 +152,7 @@ export const searchFormSchema: FormSchema[] = [ ...@@ -100,7 +152,7 @@ export const searchFormSchema: FormSchema[] = [
colProps: { span: 6 }, colProps: { span: 6 },
}, },
{ {
label: '类型', label: '计划类型',
field: 'projectType', field: 'projectType',
component: 'Select', component: 'Select',
colProps: { span: 6 }, colProps: { span: 6 },
...@@ -112,12 +164,58 @@ export const searchFormSchema: FormSchema[] = [ ...@@ -112,12 +164,58 @@ export const searchFormSchema: FormSchema[] = [
{ {
label: '执行部门', label: '执行部门',
field: 'execDepCode', field: 'execDepCode',
component: 'Input', component: 'Select',
colProps: { span: 6 }, colProps: { span: 6 },
componentProps: {
allowClear: true,
placeholder: '请选择执行部门',
},
},
{
label: '计划状态',
field: 'status',
component: 'Select',
colProps: { span: 6 },
componentProps: {
allowClear: true,
placeholder: '请选择状态',
options: planStatusOptions,
},
},
{
label: '计划日期',
field: 'planDateRange',
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
placeholder: ['开始日期', '结束日期'],
},
},
{
label: '优先级',
field: 'priority',
component: 'Select',
colProps: { span: 5 },
componentProps: {
allowClear: true,
placeholder: '请选择优先级',
options: priorityOptions,
},
},
{
label: '执行规则',
field: 'exeRule',
component: 'Select',
colProps: { span: 5 },
componentProps: {
allowClear: true,
placeholder: '请选择执行规则',
options: exeRuleOptions,
},
}, },
]; ];
//表单数据 // 表单数据
export const formSchema: FormSchema[] = [ export const formSchema: FormSchema[] = [
{ {
label: '计划名称', label: '计划名称',
...@@ -126,11 +224,11 @@ export const formSchema: FormSchema[] = [ ...@@ -126,11 +224,11 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 }, colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: true, message: '请输入项目名称!' }]; return [{ required: true, message: '请输入计划名称!' }];
}, },
}, },
{ {
label: '类型', label: '计划类型',
field: 'projectType', field: 'projectType',
component: 'JCategorySelect', component: 'JCategorySelect',
componentProps: { componentProps: {
...@@ -140,10 +238,29 @@ export const formSchema: FormSchema[] = [ ...@@ -140,10 +238,29 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 }, colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: true, message: '请选择项目类型!' }]; return [{ required: true, message: '请选择计划类型!' }];
},
},
{
label: '执行部门',
field: 'execDepCode',
component: 'JSelectDept',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请选择执行部门!' }];
},
},
{
label: '负责人',
field: 'headId',
component: 'JSearchSelectDuty',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请选择负责人!' }];
}, },
}, },
{ {
label: '计划开始日期', label: '计划开始日期',
field: 'planStartDate', field: 'planStartDate',
...@@ -151,7 +268,7 @@ export const formSchema: FormSchema[] = [ ...@@ -151,7 +268,7 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 }, colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请选择计划开始日期!' }]; return [{ required: true, message: '请选择计划开始日期!' }];
}, },
}, },
{ {
...@@ -161,26 +278,81 @@ export const formSchema: FormSchema[] = [ ...@@ -161,26 +278,81 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 }, colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请选择计划结束日期!' }]; return [{ required: true, message: '请选择计划结束日期!' }];
}, },
}, },
{ {
label: '依据', label: '优先级',
field: 'planBasis', field: 'priority',
component: 'Input', // 使用基础组件类型 component: 'Select',
slot: 'planBasis', colProps: { lg: 12 },
itemProps: { itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
labelCol: { xs: { span: 24 }, sm: { span: 3 } }, componentProps: {
wrapperCol: { xs: { span: 24 }, sm: { span: 21 } }, options: priorityOptions,
placeholder: '请选择优先级',
}, },
}, },
{ {
label: '要求', label: '执行规则',
field: 'planRequest', field: 'exeRule',
component: 'InputTextArea', component: 'RadioButtonGroup',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
componentProps: {
options: exeRuleOptions,
},
dynamicRules: ({ model, schema }) => {
return [{ required: true, message: '请选择执行规则!' }];
},
},
{
label: '触发事件名称',
field: 'triggerEventName',
component: 'Input',
colProps: { lg: 24 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } },
dynamicShow: ({ model }) => {
return model.exeRule === 1;
},
dynamicRules: ({ model, schema }) => {
return model.exeRule === 1 ? [{ required: true, message: '请填写触发事件名称!' }] : [];
},
},
{
label: '执行周期',
field: 'exePeriod',
component: 'Select',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
componentProps: {
options: exePeriodOptions,
placeholder: '请选择执行周期',
},
dynamicShow: ({ model }) => {
return model.exeRule === 2;
},
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请输入要求!' }]; return model.exeRule === 2 ? [{ required: true, message: '请选择执行周期!' }] : [];
},
},
{
label: '首次执行日期',
field: 'firstExecDate',
component: 'DatePicker',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicShow: ({ model }) => {
return model.exeRule === 2;
},
},
{
label: '计划依据',
field: 'planBasis',
component: 'Input',
slot: 'planBasis',
itemProps: {
labelCol: { xs: { span: 24 }, sm: { span: 3 } },
wrapperCol: { xs: { span: 24 }, sm: { span: 21 } },
}, },
}, },
{ {
...@@ -193,30 +365,13 @@ export const formSchema: FormSchema[] = [ ...@@ -193,30 +365,13 @@ export const formSchema: FormSchema[] = [
return [{ required: true, message: '请输入交付物!' }]; return [{ required: true, message: '请输入交付物!' }];
}, },
}, },
// { {
// label: '审核类型', label: '备注说明',
// field: 'executeType',
// component: 'Select',
// componentProps: {
// options: [
// { label: '流程A', value: 1 },
// { label: '流程B', value: 2 },
// { label: '流程C', value: 3 },
// ],
// },
// colProps: { lg: 12 },
// itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
// dynamicRules: ({ model, schema }) => {
// return [{ required: false, message: '请选择执行规则!' }];
// },
// },
{
label: '描述',
field: 'projectDesc', field: 'projectDesc',
component: 'InputTextArea', component: 'InputTextArea',
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } },
dynamicRules: ({ model, schema }) => { dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请输入项目描述!' }]; return [{ required: false, message: '请输入备注说明!' }];
}, },
}, },
{ {
...@@ -233,7 +388,7 @@ export const formSchema: FormSchema[] = [ ...@@ -233,7 +388,7 @@ export const formSchema: FormSchema[] = [
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } }, itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } },
show: false, show: false,
}, },
// TODO 主键隐藏字段,目前写死为ID // 主键隐藏字段
{ {
label: '', label: '',
field: 'id', field: 'id',
......
<template> <template>
<div> <div class="plan-management-page">
<div class="jeecg-basic-table-form-container" @keyup.enter="searchQuery"> <!-- 页面头部区域 -->
<a-form :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol"> <!-- <div class="page-header">
<a-row :gutter="24"> <div class="header-content">
<a-col :lg="6"> <div class="header-left">
<a-form-item :label="searchFormSchema[0].label"> <h1 class="page-title">计划编制管理</h1>
<JInput placeholder="请输入" v-model:value="queryParam[searchFormSchema[0].field]" /> <p class="page-desc">统一管理和追踪所有业务计划的编制与审批流程</p>
</a-form-item> </div>
</a-col> <div class="header-stats">
<a-col :lg="6"> <div class="stat-item">
<a-form-item :label="searchFormSchema[1].label"> <span class="stat-value">--</span>
<JSearchSelect placeholder="请输入" v-model:value="queryParam[searchFormSchema[1].field]" dict="projecttype" /> <span class="stat-label">计划总数</span>
</a-form-item> </div>
</a-col> <div class="stat-item warning">
<!-- <a-col :lg="6"> <span class="stat-value">--</span>
<a-form-item :label="searchFormSchema[2].label"> <span class="stat-label">待处理</span>
<JSelectDept placeholder="请输入" v-model:value="jSelectDeptVal" :multiple="false" @change="updateJSelectDept" /> </div>
</a-form-item> <div class="stat-item success">
</a-col> --> <span class="stat-value">--</span>
<a-col :lg="6"> <span class="stat-label">已完成</span>
<a-form-item> </div>
<a-space :size="5"> </div>
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button> </div>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button> </div> -->
</a-space>
</a-form-item> <!-- 主内容区 -->
</a-col> <div class="main-content">
</a-row> <!-- 搜索区域 -->
</a-form> <div class="search-section" @keyup.enter="searchQuery">
<div class="section-header">
<!--引用表格--> <span class="section-title">筛选条件</span>
<BasicTable @register="registerTable" :rowSelection="rowSelection"> </div>
<!--插槽:table标题--> <a-form :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" class="search-form">
<template #tableTitle> <a-row :gutter="16">
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新建</a-button> <a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-button v-show="showUpBtn" type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button> <a-form-item label="计划名称">
<j-upload-button v-show="showUpBtn" type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button> <JInput placeholder="请输入计划名称" v-model:value="queryParam['projectName']" />
<a-dropdown v-if="selectedRowKeys.length > 0 && showUpBtn"> </a-form-item>
<template #overlay> </a-col>
<a-menu> <a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-menu-item key="1" @click="batchHandleDelete"> <a-form-item label="计划类型">
<Icon icon="ant-design:delete-outlined" /> <JSearchSelect placeholder="请选择类型" v-model:value="queryParam['projectType']" dict="projecttype" />
删除 </a-form-item>
</a-menu-item> </a-col>
</a-menu> <a-col :xl="5" :lg="8" :md="12" :sm="24">
</template> <a-form-item label="执行部门">
<a-button <JSelectDept placeholder="请选择执行部门" v-model:value="queryParam['execDepCode']" />
>批量操作 </a-form-item>
<Icon icon="mdi:chevron-down" /> </a-col>
</a-button> <a-col :xl="4" :lg="8" :md="12" :sm="24">
</a-dropdown> <a-form-item label="计划状态">
</template> <a-select
<!--操作栏--> v-model:value="queryParam['status']"
<template #action="{ record }"> placeholder="请选择状态"
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" /> allow-clear
</template> :options="[
<!--字段回显插槽--> { label: '草稿', value: '0' },
<template #htmlSlot="{ text }"> { label: '审批中', value: '1' },
<div v-html="text"></div> { label: '已通过', value: '2' },
</template> { label: '已拒绝', value: '3' },
<template #fileSlot="{ text }"> { label: '执行中', value: '4' },
<span v-if="!text" style="font-size: 12px; font-style: italic">无文件</span> { label: '已完成', value: '5' },
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)" { label: '已作废', value: '6' },
>下载</a-button ]"
> />
</template> </a-form-item>
</BasicTable> </a-col>
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="计划日期">
<a-range-picker
v-model:value="queryParam['planDateRange']"
value-format="YYYY-MM-DD"
:placeholder="['开始日期', '结束日期']"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="优先级">
<a-select
v-model:value="queryParam['priority']"
placeholder="请选择优先级"
allow-clear
:options="[
{ label: '高', value: '1' },
{ label: '中', value: '2' },
{ label: '低', value: '3' },
]"
/>
</a-form-item>
</a-col>
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="执行规则">
<a-select
v-model:value="queryParam['exeRule']"
placeholder="请选择执行规则"
allow-clear
:options="[
{ label: '一次性', value: '1' },
{ label: '周期执行', value: '2' },
{ label: '事件触发', value: '3' },
]"
/>
</a-form-item>
</a-col>
<a-col :xl="14" :lg="16" :md="24" :sm="24">
<a-form-item class="search-btn-group">
<a-space :size="8">
<a-button type="primary" @click="searchQuery">查询</a-button>
<a-button @click="searchReset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<!-- 表格区域 -->
<div class="table-section">
<BasicTable @register="registerTable" :rowSelection="rowSelection" class="flat-table">
<!-- 插槽:table标题 -->
<template #tableTitle>
<div class="table-toolbar">
<a-button type="primary" @click="handleAdd">新建计划</a-button>
<a-button v-show="showUpBtn" @click="onExportXls">导出</a-button>
<j-upload-button v-show="showUpBtn" @click="onImportXls">导入</j-upload-button>
<a-dropdown v-if="selectedRowKeys.length > 0 && showUpBtn">
<template #overlay>
<a-menu>
<a-menu-item key="1" @click="batchHandleDelete" class="danger-item">
<Icon icon="ant-design:delete-outlined" />
批量删除
</a-menu-item>
</a-menu>
</template>
<a-button>
已选 {{ selectedRowKeys.length }} 项
<Icon icon="mdi:chevron-down" />
</a-button>
</a-dropdown>
</div>
</template>
<!-- 操作栏 -->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" :dropDownActions="getDropDownAction(record)" />
</template>
<!-- 字段回显插槽 -->
<template #htmlSlot="{ text }">
<div v-html="text"></div>
</template>
<template #fileSlot="{ text }">
<span v-if="!text" class="no-file">无文件</span>
<a-button v-else :ghost="true" type="primary" size="small" @click="downloadFile(text)">下载</a-button>
</template>
</BasicTable>
</div>
<!-- 表单区域 --> <!-- 表单区域 -->
<StPlanManModal @register="registerModal" @success="handleSuccess" :showSelectorBtn="true" /> <StPlanManModal @register="registerModal" @success="handleSuccess" :showSelectorBtn="true" />
<StPlanManFlowModal ref="refStPlanManFlow" /> <StPlanManFlowModal ref="refStPlanManFlow" />
</div> </div>
<!-- 待办抽屉 --> <!-- 待办抽屉 -->
<div v-if="isShowDrawer"> <div v-if="isShowDrawer">
<a-drawer <a-drawer destroyOnClose v-model:open="isShowDrawer" class="flat-drawer" title="待办任务" placement="right" width="90%">
destroyOnClose
v-model:open="isShowDrawer"
class="custom-class"
root-class-name="root-class-name"
:root-style="{ color: 'blue' }"
title="待办任务"
placement="right"
width="90%"
style="margin: 0px; padding: 0px"
>
<template #extra> <template #extra>
<div style="float: right"> <div class="drawer-tags">
<a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag> <span class="tag">发起人: {{ startUser }}</span>
<a-tag>任务节点:{{ taskName }}</a-tag> <span class="tag">任务节点: {{ taskName }}</span>
</div> </div>
</template> </template>
<TodoIndex v-if="isShowDrawer" ref="refTodoIndex" @callback="handleSuccess" /> <TodoIndex v-if="isShowDrawer" ref="refTodoIndex" @callback="handleSuccess" />
</a-drawer> </a-drawer>
</div> </div>
<!-- 流程详情抽屉 --> <!-- 流程详情抽屉 -->
<div v-if="isShowDetailDrawer"> <div v-if="isShowDetailDrawer">
<a-drawer <a-drawer destroyOnClose v-model:open="isShowDetailDrawer" class="flat-drawer" title="流程详情" placement="right" width="90%">
destroyOnClose
v-model:open="isShowDetailDrawer"
class="custom-class"
root-class-name="root-class-name"
:root-style="{ color: 'blue' }"
title="流程详情"
placement="right"
width="90%"
style="margin: 0px; padding: 0px"
>
<template #extra> <template #extra>
<div style="float: right"> <a-button type="text" @click="handleDetailDrawerClose">关闭</a-button>
<a-button type="text" @click="handleDetailDrawerClose">关闭</a-button>
</div>
</template> </template>
<Detail ref="refDetail" /> <Detail ref="refDetail" />
</a-drawer> </a-drawer>
...@@ -136,6 +208,7 @@ ...@@ -136,6 +208,7 @@
import { todoList, getMyTaskFlow } from '/@/components/Process/api/todo'; import { todoList, getMyTaskFlow } from '/@/components/Process/api/todo';
// 引入详情组件 // 引入详情组件
import Detail from '../../flowable/task/myProcess/components/Detail.vue'; import Detail from '../../flowable/task/myProcess/components/Detail.vue';
const refTodoIndex = ref(); const refTodoIndex = ref();
const isShowDrawer = ref(false); const isShowDrawer = ref(false);
const startUser = ref<string>(''); const startUser = ref<string>('');
...@@ -147,10 +220,12 @@ ...@@ -147,10 +220,12 @@
// 流程详情抽屉相关 // 流程详情抽屉相关
const refDetail = ref(); const refDetail = ref();
const isShowDetailDrawer = ref(false); const isShowDetailDrawer = ref(false);
//注册model
// 注册model
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
const jSelectDeptVal = ref([]); const jSelectDeptVal = ref([]);
//注册table数据
// 注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({ const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: { tableProps: {
title: '计划编制', title: '计划编制',
...@@ -159,7 +234,6 @@ ...@@ -159,7 +234,6 @@
canResize: false, canResize: false,
useSearchForm: false, useSearchForm: false,
formConfig: { formConfig: {
//labelWidth: 120,
schemas: searchFormSchema, schemas: searchFormSchema,
autoSubmitOnEnter: true, autoSubmitOnEnter: true,
showAdvancedButton: true, showAdvancedButton: true,
...@@ -167,7 +241,7 @@ ...@@ -167,7 +241,7 @@
fieldMapToTime: [], fieldMapToTime: [],
}, },
actionColumn: { actionColumn: {
width: 240, width: 280,
fixed: 'right', fixed: 'right',
}, },
showTableSetting: false, showTableSetting: false,
...@@ -186,15 +260,17 @@ ...@@ -186,15 +260,17 @@
}); });
const [registerTable, { setProps, reload }, { rowSelection, selectedRowKeys }] = tableContext; const [registerTable, { setProps, reload }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({ const labelCol = reactive({
xs: { span: 24 }, xs: { span: 24 },
sm: { span: 8 }, sm: { span: 6 },
}); });
const wrapperCol = reactive({ const wrapperCol = reactive({
xs: { span: 24 }, xs: { span: 24 },
sm: { span: 16 }, sm: { span: 18 },
}); });
const queryParam = reactive({}); const queryParam = reactive({});
function initParam() { function initParam() {
const schemas = unref(searchFormSchema); const schemas = unref(searchFormSchema);
schemas.forEach((item) => { schemas.forEach((item) => {
...@@ -205,6 +281,7 @@ ...@@ -205,6 +281,7 @@
jSelectDeptVal.value = []; jSelectDeptVal.value = [];
} }
initParam(); initParam();
function updateJSelectDept(val) { function updateJSelectDept(val) {
if (val.length > 0) { if (val.length > 0) {
queryParam[searchFormSchema[2].field] = val[0]; queryParam[searchFormSchema[2].field] = val[0];
...@@ -213,14 +290,17 @@ ...@@ -213,14 +290,17 @@
queryParam[searchFormSchema[2].field] = ''; queryParam[searchFormSchema[2].field] = '';
} }
} }
function searchQuery() { function searchQuery() {
setProps({ searchInfo: toRaw(queryParam) }); setProps({ searchInfo: toRaw(queryParam) });
reload(); reload();
} }
function searchReset() { function searchReset() {
initParam(); initParam();
reload(); reload();
} }
/** /**
* 新增事件 * 新增事件
*/ */
...@@ -230,6 +310,7 @@ ...@@ -230,6 +310,7 @@
showFooter: true, showFooter: true,
}); });
} }
/** /**
* 编辑事件 * 编辑事件
*/ */
...@@ -240,6 +321,7 @@ ...@@ -240,6 +321,7 @@
showFooter: true, showFooter: true,
}); });
} }
/** /**
* 详情 * 详情
*/ */
...@@ -250,6 +332,7 @@ ...@@ -250,6 +332,7 @@
showFooter: false, showFooter: false,
}); });
} }
/** /**
* 流转记录 * 流转记录
*/ */
...@@ -263,9 +346,6 @@ ...@@ -263,9 +346,6 @@
try { try {
const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id }); const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id });
console.log('获取流程任务信息:', myTaskFlow);
// 提取任务流信息,支持多层返回结构(data/result/直接对象)
const taskData = pickStartResult(myTaskFlow); const taskData = pickStartResult(myTaskFlow);
if (!taskData || !taskData.taskId) { if (!taskData || !taskData.taskId) {
...@@ -273,19 +353,14 @@ ...@@ -273,19 +353,14 @@
return; return;
} }
// 若未指定是否审批,则默认展示审批意见(保持与发起流程一致)
if (taskData.nodeisApprove == null) { if (taskData.nodeisApprove == null) {
taskData.nodeisApprove = true; taskData.nodeisApprove = true;
} }
// 保存缓存
taskCache.set(String(dataId), taskData); taskCache.set(String(dataId), taskData);
// 打开流程详情抽屉并初始化数据
isShowDetailDrawer.value = true; isShowDetailDrawer.value = true;
await nextTick(); await nextTick();
if (refDetail.value) { if (refDetail.value) {
// 使用获取到的taskData,确保包含所有必要参数
refDetail.value.iniData({ refDetail.value.iniData({
...record, ...record,
...taskData, ...taskData,
...@@ -301,7 +376,6 @@ ...@@ -301,7 +376,6 @@
} }
async function findTodoTaskByProcInsId(procInsId: string) { async function findTodoTaskByProcInsId(procInsId: string) {
// 启动流程后,待办任务可能存在短暂延迟,做一个轻量重试
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const ret = await todoList({ pageNum: 1, pageSize: 10, procInsId }); const ret = await todoList({ pageNum: 1, pageSize: 10, procInsId });
const records = ret?.records || []; const records = ret?.records || [];
...@@ -313,12 +387,10 @@ ...@@ -313,12 +387,10 @@
} }
function pickStartResult(res: any) { function pickStartResult(res: any) {
// defHttp 返回形态在不同后端/拦截器下可能是 data/result/直接对象,这里做一次兜底
const base = res?.data ?? res?.result ?? res; const base = res?.data ?? res?.result ?? res;
return base || {}; return base || {};
} }
// 发起任务:点击后若流程未启动则先启动,再在抽屉中完成流程操作
async function handleTodo(record: Recordable) { async function handleTodo(record: Recordable) {
const dataId = record.id || record.dataId || record.businessId; const dataId = record.id || record.dataId || record.businessId;
if (!dataId) { if (!dataId) {
...@@ -326,13 +398,11 @@ ...@@ -326,13 +398,11 @@
return; return;
} }
// 优先使用本地缓存(用于刚启动后列表未回写 taskId/procInsId 的场景)
const cached = taskCache.get(String(dataId)); const cached = taskCache.get(String(dataId));
if (cached && !record.taskId) { if (cached && !record.taskId) {
record = Object.assign({}, record, cached); record = Object.assign({}, record, cached);
} }
// 流程未启动时,先启动流程
const needStartFlow = const needStartFlow =
!record.taskId && !record.procInsId && (record['bpmStatus'] == null || record['bpmStatus'] == '' || record['bpmStatus'] == '1'); !record.taskId && !record.procInsId && (record['bpmStatus'] == null || record['bpmStatus'] == '' || record['bpmStatus'] == '1');
...@@ -340,27 +410,14 @@ ...@@ -340,27 +410,14 @@
try { try {
const formData = { dataId, dataName: 'id' }; const formData = { dataId, dataName: 'id' };
const startResRaw = await definitionStartByDeployId(record.deployId, formData); const startResRaw = await definitionStartByDeployId(record.deployId, formData);
//console.log("definitionStartByDeployId 返回值",startResRaw);
/**definitionStartByDeployId 返回值
{procInsId: 'e962d600-1e88-11f1-8c5b-9a8d469af623',
executionId: 'e962fd16-1e88-11f1-8c5b-9a8d469af623',
instanceId: 'e962d600-1e88-11f1-8c5b-9a8d469af623',
deployId: '7fc9bc36-0591-11f1-9cb1-9a8d469af623',
taskId: 'e962fd1a-1e88-11f1-8c5b-9a8d469af623'}
*/
const startRes = pickStartResult(startResRaw); const startRes = pickStartResult(startResRaw);
let payload: any = Object.assign({}, record, startRes); let payload: any = Object.assign({}, record, startRes);
//console.log("definitionStartByDeployId 返回值-payload ",payload);
// 若启动接口未返回 taskId,则尝试通过 procInsId 从待办列表反查
if (!payload.taskId && payload.procInsId) { if (!payload.taskId && payload.procInsId) {
//console.log("definitionStartByDeployId 返回值-payload -1");
const todoRow = await findTodoTaskByProcInsId(payload.procInsId); const todoRow = await findTodoTaskByProcInsId(payload.procInsId);
if (todoRow?.taskId) { if (todoRow?.taskId) {
payload = Object.assign({}, record, startRes, todoRow); payload = Object.assign({}, record, startRes, todoRow);
} }
} else {
//console.log("definitionStartByDeployId 返回值-payload -2");
} }
if (!payload.taskId) { if (!payload.taskId) {
...@@ -379,11 +436,9 @@ ...@@ -379,11 +436,9 @@
console.error('启动流程或获取任务失败:', e); console.error('启动流程或获取任务失败:', e);
message.error('操作失败,请重试'); message.error('操作失败,请重试');
} }
//console.log("definitionStartByDeployId 返回值-payload- return");
return; return;
} }
console.log('流程已启动:优先使用 record.taskId,否则尝试用 procInsId 从待办列表反查');
// 流程已启动:优先使用 record.taskId,否则尝试用 procInsId 从待办列表反查
try { try {
let payload: any = { ...record }; let payload: any = { ...record };
...@@ -412,7 +467,6 @@ ...@@ -412,7 +467,6 @@
} }
} }
// 待办任务:
async function handleTodoDb(record: Recordable) { async function handleTodoDb(record: Recordable) {
const dataId = record.id; const dataId = record.id;
const deployId = record.deployId; const deployId = record.deployId;
...@@ -423,9 +477,6 @@ ...@@ -423,9 +477,6 @@
try { try {
const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id }); const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id });
console.log('获取流程任务信息:', myTaskFlow);
// 提取任务流信息,支持多层返回结构(data/result/直接对象)
const taskData = pickStartResult(myTaskFlow); const taskData = pickStartResult(myTaskFlow);
if (!taskData || !taskData.taskId) { if (!taskData || !taskData.taskId) {
...@@ -433,15 +484,11 @@ ...@@ -433,15 +484,11 @@
return; return;
} }
// 若未指定是否审批,则默认展示审批意见(保持与发起流程一致)
if (taskData.nodeisApprove == null) { if (taskData.nodeisApprove == null) {
taskData.nodeisApprove = true; taskData.nodeisApprove = true;
} }
// 保存缓存
taskCache.set(String(dataId), taskData); taskCache.set(String(dataId), taskData);
// 打开抽屉并初始化待办任务数据
startUser.value = taskData.startUserName || taskData.startUser || ''; startUser.value = taskData.startUserName || taskData.startUser || '';
taskName.value = taskData.taskName || taskData.currentTaskName || ''; taskName.value = taskData.taskName || taskData.currentTaskName || '';
...@@ -460,12 +507,14 @@ ...@@ -460,12 +507,14 @@
async function handleDelete(record) { async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess); await deleteOne({ id: record.id }, handleSuccess);
} }
/** /**
* 批量删除事件 * 批量删除事件
*/ */
async function batchHandleDelete() { async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess); await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
} }
/** /**
* 成功回调 * 成功回调
*/ */
...@@ -479,6 +528,7 @@ ...@@ -479,6 +528,7 @@
function handleDetailDrawerClose() { function handleDetailDrawerClose() {
isShowDetailDrawer.value = false; isShowDetailDrawer.value = false;
} }
/** /**
* 操作栏 * 操作栏
*/ */
...@@ -523,7 +573,6 @@ ...@@ -523,7 +573,6 @@
if (record['bpmStatus'] == null || record['bpmStatus'] == '') return true; if (record['bpmStatus'] == null || record['bpmStatus'] == '') return true;
else return false; else return false;
}, },
// disabled: record['planFlag'] != '0' && record['planFlag'] != '2' && record['planFlag'] != '9',
popConfirm: { popConfirm: {
title: '是否确认删除该计划项', title: '是否确认删除该计划项',
confirm: handleDelete.bind(null, record), confirm: handleDelete.bind(null, record),
...@@ -531,6 +580,7 @@ ...@@ -531,6 +580,7 @@
}, },
]; ];
} }
/** /**
* 下拉操作栏 * 下拉操作栏
*/ */
...@@ -539,4 +589,352 @@ ...@@ -539,4 +589,352 @@
} }
</script> </script>
<style scoped></style> <style scoped lang="less">
/* ==================== 柔和中性风格 - CSS 变量 ==================== */
.plan-management-page {
--color-primary: #3b5bdb;
--color-primary-hover: #364fc7;
--color-primary-light: #e8ecfd;
--color-accent: #3b5bdb;
--color-success: #2f9e44;
--color-success-light: #ebfbee;
--color-warning: #e67700;
--color-warning-light: #fff9db;
--color-error: #c92a2a;
--color-text-primary: #1c1c1e;
--color-text-secondary: #555770;
--color-text-muted: #a0a3b1;
--color-border: #e4e4e9;
--color-border-strong: #c8c8d0;
--color-bg-page: #f5f5f7;
--color-bg-white: #ffffff;
--color-bg-subtle: #fafafa;
--color-bg-section: #f0f0f4;
--radius: 6px;
min-height: 100vh;
background: var(--color-bg-page);
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
}
/* ==================== 页面头部 ==================== */
.page-header {
background: var(--color-bg-white);
border-bottom: 1px solid var(--color-border);
padding: 20px 32px;
}
.header-content {
max-width: 100%;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.header-left {
flex: 1;
border-left: 3px solid var(--color-primary);
padding-left: 14px;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 3px 0;
letter-spacing: -0.2px;
}
.page-desc {
font-size: 13px;
color: var(--color-text-muted);
margin: 0;
}
.header-stats {
display: flex;
gap: 8px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 20px;
background: var(--color-bg-section);
border-radius: var(--radius);
min-width: 80px;
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
line-height: 1.2;
}
.stat-label {
font-size: 11px;
color: var(--color-text-muted);
margin-top: 3px;
white-space: nowrap;
}
&.warning .stat-value {
color: var(--color-warning);
}
&.success .stat-value {
color: var(--color-success);
}
}
/* ==================== 主内容区 ==================== */
.main-content {
max-width: 100%;
margin: 0 auto;
padding: 20px 32px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ==================== 搜索区域 ==================== */
.search-section {
background: var(--color-bg-white);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
.section-header {
padding: 11px 16px;
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: 8px;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: var(--color-primary);
border-radius: 2px;
flex-shrink: 0;
}
}
.section-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.search-form {
padding: 16px 16px 4px;
}
.search-btn-group {
margin-bottom: 0;
}
/* ==================== 表格区域 ==================== */
.table-section {
background: var(--color-bg-white);
border: 1px solid var(--color-border);
border-radius: var(--radius);
}
.table-toolbar {
display: flex;
gap: 8px;
padding: 12px 0;
flex-wrap: wrap;
}
.flat-table {
:deep(.ant-table) {
border-radius: 0;
font-size: 13px;
}
:deep(.ant-table-thead > tr > th) {
background: var(--color-bg-section);
border-bottom: 1px solid var(--color-border-strong);
font-weight: 600;
font-size: 12px;
color: var(--color-text-secondary);
padding: 10px 16px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
:deep(.ant-table-tbody > tr > td) {
padding: 11px 16px;
border-bottom: 1px solid var(--color-border);
color: var(--color-text-primary);
}
:deep(.ant-table-tbody > tr:hover > td) {
background: #f8f8fc;
}
:deep(.ant-pagination) {
margin: 14px 16px;
}
:deep(.ant-table-row-selected > td) {
background: var(--color-primary-light) !important;
}
}
.no-file {
color: var(--color-text-muted);
font-size: 12px;
}
.danger-item {
color: var(--color-error);
}
/* ==================== 抽屉样式 ==================== */
.flat-drawer {
:deep(.ant-drawer-header) {
background: var(--color-bg-white);
border-bottom: 1px solid var(--color-border);
padding: 14px 24px;
}
:deep(.ant-drawer-title) {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
:deep(.ant-drawer-body) {
padding: 0;
background: var(--color-bg-page);
}
}
.drawer-tags {
display: flex;
gap: 8px;
}
.tag {
font-size: 12px;
color: var(--color-text-secondary);
padding: 3px 10px;
background: var(--color-bg-section);
border-radius: 3px;
border: 1px solid var(--color-border);
white-space: nowrap;
}
/* ==================== 表单控件 ==================== */
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-picker) {
border-radius: var(--radius) !important;
border-color: var(--color-border) !important;
box-shadow: none !important;
font-size: 13px;
&:hover {
border-color: var(--color-border-strong) !important;
}
&:focus,
&.ant-input-focused {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
}
:deep(.ant-select-focused .ant-select-selector) {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
/* ==================== 按钮样式 ==================== */
:deep(.ant-btn) {
border-radius: var(--radius);
font-weight: 500;
font-size: 13px;
box-shadow: none;
&.ant-btn-primary {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&.ant-btn-default {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background: var(--color-bg-white);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
}
/* ==================== 表单项 ==================== */
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-form-item-label > label) {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
}
/* ==================== 复选框 ==================== */
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
background: var(--color-primary);
border-color: var(--color-primary);
}
/* ==================== 响应式 ==================== */
@media (max-width: 768px) {
.page-header {
padding: 16px;
}
.header-content {
flex-direction: column;
align-items: flex-start;
}
.header-stats {
width: 100%;
justify-content: space-between;
}
.stat-item {
flex: 1;
padding: 8px 12px;
min-width: unset;
}
.main-content {
padding: 12px 16px;
}
.table-toolbar {
flex-wrap: wrap;
}
}
</style>
<template> <template>
<div style="background-color: #fff; padding: 100px"> <div class="execute-form-container">
<a-form ref="formRef" :model="formData" :label-col="{ span: 4 }" :wrapper-col="{ span: 8 }"> <!-- 表单头部 -->
<!-- 执行状态 --> <div class="form-header">
<a-form-item label="执行状态" prop="executeStatus"> <h2 class="form-title">执行信息录入</h2>
<a-select v-model:value="formData.executeStatus" placeholder="请选择执行状态"> <p class="form-subtitle">更新计划执行状态和相关记录</p>
<a-select-option value="0">未开始</a-select-option> </div>
<a-select-option value="1">进行中</a-select-option>
<a-select-option value="2">已完成</a-select-option> <!-- 表单主体 -->
<a-select-option value="3">已暂停</a-select-option> <div class="form-body">
</a-select> <a-form ref="formRef" :model="formData" layout="vertical" class="styled-form">
</a-form-item> <!-- 执行状态 -->
<a-form-item label="执行状态" name="executeStatus">
<!-- 实际开始时间 --> <a-select v-model:value="formData.executeStatus" placeholder="请选择执行状态">
<a-form-item label="实际开始时间" prop="actualStartTime"> <a-select-option value="0">
<a-date-picker v-model="formData.actualStartTime" type="datetime" placeholder="选择时间"></a-date-picker> <span class="status-option">
</a-form-item> <span class="status-indicator pending"></span>
未开始
<!-- 实际结束时间 --> </span>
<a-form-item label="实际结束时间" prop="actualEndTime"> </a-select-option>
<a-date-picker v-model="formData.actualEndTime" type="datetime" placeholder="选择时间"></a-date-picker> <a-select-option value="1">
</a-form-item> <span class="status-option">
<span class="status-indicator processing"></span>
<!-- 执行记录 --> 进行中
<a-form-item label="执行记录" prop="executeRecord"> </span>
<a-textarea v-model="formData.executeRecord" :rows="4" placeholder="请输入执行记录"></a-textarea> </a-select-option>
</a-form-item> <a-select-option value="2">
<span class="status-option">
<!-- 附件 --> <span class="status-indicator success"></span>
<a-form-item label="附件" prop="attachments"> 已完成
<!-- <JUpload v-model:value="formModel.fileUploadPath" desText="支持扩展名: .rar .zip .doc .docx .pdf .jpg..." :disabled="isDetail" /> --> </span>
</a-form-item> </a-select-option>
<a-select-option value="3">
<!-- 操作按钮 --> <span class="status-option">
<a-form-item :wrapper-col="{ offset: 6 }"> <span class="status-indicator paused"></span>
已暂停
</span>
</a-select-option>
</a-select>
</a-form-item>
<!-- 时间选择区域 -->
<div class="form-row">
<a-form-item label="实际开始时间" name="actualStartTime" class="form-item-half">
<a-date-picker v-model:value="formData.actualStartTime" placeholder="选择时间" format="YYYY-MM-DD"></a-date-picker>
</a-form-item>
<a-form-item label="实际结束时间" name="actualEndTime" class="form-item-half" :rules="endDateRules">
<a-date-picker v-model:value="formData.actualEndTime" placeholder="选择时间" format="YYYY-MM-DD"></a-date-picker>
</a-form-item>
</div>
<!-- 执行记录 -->
<a-form-item label="执行记录" name="executeEcord">
<a-textarea v-model:value="formData.executeEcord" :rows="3" placeholder="请输入执行记录和详细说明..." show-count :maxlength="500" />
</a-form-item>
<!-- 附件上传 -->
<a-form-item label="相关附件" name="attachments">
<JUpload v-model:value="formData.attachments" desText="支持扩展名: .rar .zip .doc .docx .pdf .jpg..." />
</a-form-item>
</a-form>
</div>
<!-- 表单底部按钮 -->
<div class="form-footer">
<a-space :size="12">
<a-button @click="resetForm">重置</a-button>
<a-button type="primary" @click="submitForm">保存</a-button> <a-button type="primary" @click="submitForm">保存</a-button>
<a-button style="margin-left: 40px" @click="resetForm">重置</a-button> </a-space>
</a-form-item> </div>
</a-form>
</div> </div>
</template> </template>
<script setup> <script setup lang="ts">
import { ref } from 'vue'; import { ref, reactive, onMounted, watch, nextTick } from 'vue';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { saveOrUpdate } from '../StPlanMan.api';
import { defHttp } from '/@/utils/http/axios';
import { useRoute } from 'vue-router';
import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue'; import JUpload from '/@/components/Form/src/jeecg/components/JUpload/JUpload.vue';
import { usePlanFormStore } from '/@/store/modules/planFormStore';
const route = useRoute();
const formRef = ref(); const formRef = ref();
const formData = ref({ const loading = ref(false);
const submitting = ref(false);
const planId = ref('');
const planFormStore = usePlanFormStore();
const isInitialized = ref(false);
// const formData = ref({
// id: '',
// executeStatus: '',
// actualStartTime: null,
// actualEndTime: null,
// executeEcord: '',
// attachments: [],
// });
const formData = reactive({
id: '',
executeStatus: '', executeStatus: '',
actualStartTime: '', actualStartTime: null as any,
actualEndTime: '', actualEndTime: null as any,
executeRecord: '', executeEcord: '',
attachments: [], attachments: [] as any[],
}); });
const submitForm = () => { // 监听formData变化,用于调试
formRef.value.validate((valid) => {
if (valid) { watch(
console.log('Form submitted:', formData.value); () => formData,
// 提交逻辑 (newVal) => {
console.log('[v0] formData 响应式更新触发:', JSON.stringify(newVal));
},
{ deep: true, immediate: true }
);
// 安全解析JSON
const safeJsonParse = (str) => {
if (!str) return [];
if (Array.isArray(str)) return str;
try {
return JSON.parse(str);
} catch (e) {
return [];
}
};
// 初始化表单数据
const initFormData = async (id?: string) => {
try {
// 检查是否已正在加载,避免重复请求
if (loading.value && planId.value === (id || route.query.id)) {
console.log('[v0] 表单已在加载中,跳过重复初始化');
return;
} }
});
loading.value = true;
planFormStore.setLoadingState(true);
// 从参数或URL获取id
const targetId = id || (route.query.id as string);
if (!targetId) {
message.warning('未获取到计划ID');
console.warn('[v0] 未获取到计划ID, id参数:', id, '路由ID:', route.query.id);
return;
}
console.log('[v0] 开始初始化表单,ID:', targetId);
planId.value = targetId;
planFormStore.setLastLoadedId(targetId);
// 根据id查询现有数据
const timestamp = new Date().getTime();
const queryUrl = '/plan.main/stPlanMan/queryById';
const data = await defHttp.get({
url: queryUrl,
params: { id: targetId, _t: timestamp },
});
// 使用查询到的数据初始化表单,只保留执行相关字段
if (data) {
const newFormData = {
id: data.id || '',
executeStatus: data.executeStatus || '',
actualStartTime: data.actualStartTime ? dayjs(data.actualStartTime) : null,
actualEndTime: data.actualEndTime ? dayjs(data.actualEndTime) : null,
executeEcord: data.executeEcord || '',
// 安全解析附件字段(可能是字符串或数组)
attachments: safeJsonParse(data.attachments),
};
// 直接替换整个 ref 值,确保触发响应式更新
Object.assign(formData, newFormData);
// 强制等待 DOM 更新
await nextTick();
planFormStore.updateFormDataCache(newFormData);
isInitialized.value = true;
console.log('[v0] 表单初始化成功,数据:', formData);
// Object.assign(formData, newFormData);
// planFormStore.updateFormDataCache(newFormData);
// isInitialized.value = true;
// console.log('[v0] 表单初始化成功,数据:', newFormData);
}
} catch (error: any) {
console.error('初始化表单数据失败:', error);
message.error('初始化表单数据失败');
planFormStore.formLoadingState.isError = true;
planFormStore.formLoadingState.errorMessage = error?.message || '加载失败';
} finally {
loading.value = false;
planFormStore.setLoadingState(false);
// 确保表单在下一个更新周期中可用
await nextTick();
}
};
// 结束日期验证规则
const endDateRules = [
{
validator: (rule, value) => {
if (!value) {
return Promise.resolve();
}
if (!formData.actualStartTime) {
return Promise.resolve();
}
// 比较日期:结束日期不能早于开始日期
const endTime = dayjs(value);
const startTime = dayjs(formData.actualStartTime);
if (endTime.isBefore(startTime, 'day')) {
return Promise.reject(new Error('结束日期不能早于开始日期'));
}
return Promise.resolve();
},
trigger: 'change',
},
];
const submitForm = async () => {
try {
// 验证表单
try {
await formRef.value.validate();
} catch (validateError) {
return;
}
if (!formData.id) {
message.error('缺少计划ID,无法保存');
return;
}
submitting.value = true;
planFormStore.setLoadingState(true);
// 格式化日期为字符串(仅保留日期,不含时分秒)
const formatDate = (date) => {
if (!date) return '';
// 如果是Dayjs对象
if (date && typeof date.format === 'function') {
return date.format('YYYY-MM-DD');
}
// 如果是Date对象
if (date instanceof Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 如果已是字符串
return String(date);
};
// 准备提交数据,将数组类型转换为字符串,日期格式化
const submitData = {
id: formData.id,
executeStatus: formData.executeStatus || '',
actualStartTime: formatDate(formData.actualStartTime),
actualEndTime: formatDate(formData.actualEndTime),
executeEcord: formData.executeEcord || '',
// 将附件数组转换为JSON字符串(如果是数组)或保留原值(如果已是字符串)
attachments: Array.isArray(formData.attachments) ? JSON.stringify(formData.attachments) : formData.attachments || '',
};
// 调用 saveOrUpdate 接口,isUpdate=true表示执行更新操作
const response = await saveOrUpdate(submitData, true);
if (response) {
message.success('保存成功');
// 更新缓存
planFormStore.updateFormDataCache(formData);
console.log(response);
}
} catch (error: any) {
console.error('保存失败:', error);
message.error(error?.message || '保存失败,请检查表单输入');
} finally {
submitting.value = false;
planFormStore.setLoadingState(false);
}
}; };
const resetForm = () => { const resetForm = () => {
formRef.value.resetFields(); formRef.value.resetFields();
// 重置为初始化的数据
initFormData();
}; };
// 1 接收 id 2 保存调用 StPlanMan.api 的 saveOrUpdate // 组件挂载时初始化表单
onMounted(async () => {
// 等待 iframe DOM 完全渲染
await nextTick();
// 从 URL 直接解析 id 参数(iframe 场景)
const urlParams = new URLSearchParams(window.location.search);
const idFromUrl = urlParams.get('id');
const targetId = idFromUrl || (route.query.id as string);
console.log('[v0] StPlanExcuteForm mounted - idFromUrl:', idFromUrl, 'routeId:', route.query.id, 'finalId:', targetId);
// 如果从 URL 获取到 ID,直接初始化
if (idFromUrl) {
initFormData(idFromUrl);
} else if (targetId) {
// 否则使用路由参数
initFormData(targetId);
} else {
// 都没有则延迟检查,确保 URL 参数已加载
await new Promise((resolve) => setTimeout(resolve, 100));
const delayedUrlParams = new URLSearchParams(window.location.search);
const delayedIdFromUrl = delayedUrlParams.get('id');
if (delayedIdFromUrl) {
initFormData(delayedIdFromUrl);
}
}
planFormStore.registerSubmitCallback(submitForm);
console.log('[StPlanExcuteForm] 已向 store 注册表单保存回调');
});
// 监听路由参数变化
watch(
() => route.query.id,
(newId) => {
if (newId) {
// 重置初始化标志,确保新的ID会被加载
isInitialized.value = false;
initFormData(newId as string);
}
}
);
// 监听store中的加载状态变化
watch(
() => planFormStore.formLoadingState.lastLoadedId,
(newId) => {
// 当store中的lastLoadedId变化时,无论当前状态如何,都重新初始化表单
if (newId) {
// 重置初始化标志,确保表单可以重新初始化
isInitialized.value = false;
initFormData(newId);
}
}
);
// 暴露方法给外部组件调用
defineExpose({
submitForm,
resetForm,
initFormData,
isInitialized,
});
</script> </script>
<style scoped></style> <style scoped lang="less">
/* ==================== 柔和中性风格 - CSS 变量 ==================== */
.execute-form-container {
--color-primary: #3b5bdb;
--color-primary-hover: #364fc7;
--color-primary-light: #e8ecfd;
--color-success: #2f9e44;
--color-warning: #e67700;
--color-error: #c92a2a;
--color-text-primary: #1c1c1e;
--color-text-secondary: #555770;
--color-text-muted: #a0a3b1;
--color-border: #e4e4e9;
--color-border-strong: #c8c8d0;
--color-bg-page: #f5f5f7;
--color-bg-white: #ffffff;
--color-bg-section: #f0f0f4;
--radius: 6px;
background: var(--color-bg-white);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
}
/* ==================== 表单头部 ==================== */
.form-header {
background: var(--color-bg-white);
border-bottom: 1px solid var(--color-border);
padding: 20px 32px;
border-left: 3px solid var(--color-primary);
padding-left: 28px;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 3px 0;
letter-spacing: -0.2px;
}
.form-subtitle {
font-size: 13px;
color: var(--color-text-muted);
margin: 0;
}
/* ==================== 表单主体 ==================== */
.form-body {
padding: 24px 32px;
max-width: 800px;
margin: 0 auto;
}
.styled-form {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-row {
display: flex;
gap: 24px;
.form-item-half {
flex: 1;
}
}
/* 状态选项样式 */
.status-option {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 2px;
&.pending {
background: var(--color-text-muted);
}
&.processing {
background: var(--color-primary);
}
&.success {
background: var(--color-success);
}
&.paused {
background: var(--color-warning);
}
}
/* 上传区域样式 */
.upload-area {
border: 1px dashed var(--color-border-dark);
border-radius: var(--radius);
padding: 32px;
background: var(--color-bg-gray);
cursor: pointer;
transition: border-color 0.15s;
&:hover {
border-color: var(--color-primary);
}
}
.upload-content {
text-align: center;
}
.upload-text {
font-size: 14px;
color: var(--color-text-secondary);
margin: 0 0 4px 0;
}
.upload-hint {
font-size: 12px;
color: var(--color-text-muted);
margin: 0;
}
/* ==================== 表单底部按钮 ==================== */
.form-footer {
padding: 14px 32px;
background: var(--color-bg-section);
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
}
/* ==================== 上传区域 ==================== */
.upload-area {
border: 1px dashed var(--color-border-strong);
border-radius: var(--radius);
padding: 28px;
background: var(--color-bg-section);
cursor: pointer;
transition: border-color 0.15s;
&:hover {
border-color: var(--color-primary);
}
}
/* ==================== 表单控件样式覆盖 ==================== */
:deep(.ant-form-item) {
margin-bottom: 18px;
}
:deep(.ant-form-item-label > label) {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
}
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-picker),
:deep(.ant-input-textarea textarea) {
border-radius: var(--radius) !important;
border-color: var(--color-border) !important;
font-size: 13px;
box-shadow: none !important;
&:hover {
border-color: var(--color-border-strong) !important;
}
&:focus,
&.ant-input-focused {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
}
:deep(.ant-select-focused .ant-select-selector),
:deep(.ant-picker-focused) {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
/* ==================== 按钮样式 ==================== */
:deep(.ant-btn) {
border-radius: var(--radius);
font-weight: 500;
font-size: 13px;
height: 34px;
padding: 0 18px;
box-shadow: none;
&.ant-btn-primary {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&.ant-btn-default {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background: var(--color-bg-white);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.form-header {
padding: 16px 20px;
}
.form-body {
padding: 20px;
}
.form-row {
flex-direction: column;
gap: 0;
}
.form-footer {
padding: 12px 20px;
}
}
</style>
<template> <template>
<div> <div>
<vxe-drawer <vxe-drawer
...@@ -9,21 +8,40 @@ ...@@ -9,21 +8,40 @@
width="100%" width="100%"
height="100%" height="100%"
:loading="loading" :loading="loading"
class="flat-flow-drawer"
> >
<iframe <template #header>
id="iframeId" <div class="drawer-header">
ref="iframeRef" <h3 class="drawer-title">{{ pageTilte }}</h3>
:src="frmUrl" <span class="drawer-subtitle">计划审批流程</span>
frameborder="0" </div>
style="width: 100%; height: 100%;" </template>
></iframe>
<div class="drawer-body">
<div class="iframe-container">
<div v-if="loading" class="loading-overlay">
<div class="loading-content">
<a-spin size="large" />
<span class="loading-text">加载流程中...</span>
</div>
</div>
<iframe
id="iframeId"
ref="iframeRef"
:src="frmUrl"
frameborder="0"
class="flow-iframe"
@load="handleIframeLoad"
></iframe>
</div>
</div>
</vxe-drawer> </vxe-drawer>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted,nextTick } from "vue"; import { ref, onMounted, nextTick } from "vue";
import { useUserStoreWithOut } from "/@/store/modules/user"; import { useUserStoreWithOut } from "/@/store/modules/user";
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
import { getToken } from '/@/utils/auth'; import { getToken } from '/@/utils/auth';
...@@ -35,18 +53,190 @@ const loading = ref(false); ...@@ -35,18 +53,190 @@ const loading = ref(false);
const pageTilte = ref(""); const pageTilte = ref("");
const iframeRef = ref<HTMLIFrameElement>(); const iframeRef = ref<HTMLIFrameElement>();
const handleIframeLoad = () => {
loading.value = false;
};
const iniPage = async (data) => { const iniPage = async (data) => {
pageTilte.value = data.projectName; pageTilte.value = data.projectName;
showPopup.value = true; showPopup.value = true;
loading.value = true;
frmUrl.value = `${import.meta.env.VITE_APP_JFLOW_CORE_ADDR}/#/WF/MyFlow?FlowNo=087&Token=${user.getJflowToken}&tid=${data.id}`; frmUrl.value = `${import.meta.env.VITE_APP_JFLOW_CORE_ADDR}/#/WF/MyFlow?FlowNo=087&Token=${user.getJflowToken}&tid=${data.id}`;
const setSourctUrl = '/api/jflow/setCCWorkId'; const setSourctUrl = '/api/jflow/setCCWorkId';
await defHttp.get({ await defHttp.get({
url: setSourctUrl, url: setSourctUrl,
params: {"targetId":data.id,"targetKey":"targetKey","token":getToken()}, params: {"targetId":data.id,"targetKey":"targetKey","token":getToken()},
}); });
} }
// 暴露方法 // 暴露方法
defineExpose({ iniPage }); defineExpose({ iniPage });
</script> </script>
<style scoped lang="less">
/* ==================== 柔和中性风格 - CSS 变量 ==================== */
.flat-flow-drawer {
--color-primary: #3b5bdb;
--color-primary-hover: #364fc7;
--color-primary-light: #e8ecfd;
--color-success: #2f9e44;
--color-warning: #e67700;
--color-error: #c92a2a;
--color-text-primary: #1c1c1e;
--color-text-secondary: #555770;
--color-text-muted: #a0a3b1;
--color-border: #e4e4e9;
--color-border-strong: #c8c8d0;
--color-bg-page: #f5f5f7;
--color-bg-white: #ffffff;
--color-bg-section: #f0f0f4;
--radius: 6px;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
}
/* ==================== 抽屉头部样式 ==================== */
.drawer-header {
display: flex;
align-items: baseline;
gap: 10px;
border-left: 3px solid var(--color-primary);
padding-left: 10px;
}
.drawer-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0;
letter-spacing: -0.2px;
}
.drawer-subtitle {
font-size: 12px;
color: var(--color-text-muted);
}
/* ==================== 抽屉主体样式 ==================== */
.drawer-body {
height: 100%;
background: var(--color-bg-page);
padding: 12px;
}
.iframe-container {
position: relative;
width: 100%;
height: 100%;
background: var(--color-bg-white);
border: 1px solid var(--color-border);
border-radius: var(--radius);
overflow: hidden;
}
.flow-iframe {
width: 100%;
height: 100%;
border: none;
}
/* ==================== 加载状态 ==================== */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-text {
font-size: 14px;
color: var(--color-text-secondary);
}
/* ==================== VXE Drawer 样式覆盖 ==================== */
:deep(.vxe-drawer--wrapper) {
.vxe-drawer--header {
background: var(--color-bg-section);
border-bottom: 1px solid var(--color-border);
padding: 14px 24px;
}
.vxe-drawer--body {
padding: 0;
background: var(--color-bg-page);
}
.vxe-drawer--footer {
background: var(--color-bg-white);
border-top: 1px solid var(--color-border);
padding: 12px 24px;
}
.vxe-button {
border-radius: var(--radius);
font-weight: 500;
font-size: 13px;
box-shadow: none;
&.type--primary {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&.type--default {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background: var(--color-bg-white);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
}
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.drawer-header {
flex-direction: column;
gap: 4px;
}
.drawer-title {
font-size: 16px;
}
.drawer-body {
padding: 12px;
}
:deep(.vxe-drawer--wrapper) {
.vxe-drawer--header {
padding: 12px 16px;
}
.vxe-drawer--footer {
padding: 10px 16px;
}
}
}
</style>
<template> <template>
<div style="background-color: #fff; padding: 100px"> <div class="plan-form-container">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<BasicForm @register="registerForm"> <!-- 表单头部 -->
<template #planBasis="{ model, field }"> <div class="form-header">
<div v-if="model[field]"> <h2 class="form-title">计划详情</h2>
<div v-if="isValidJson(model[field])"> <p class="form-subtitle">查看和编辑计划信息</p>
<a-tag </div>
v-for="item in safeJsonParse(model[field])"
@click="viewBasisDetail(item)" <!-- 表单内容 -->
:key="item.id" <div class="form-body">
style="margin-bottom: 8px; cursor: pointer" <BasicForm @register="registerForm">
> <template #planBasis="{ model, field }">
{{ item.name }} <div class="basis-container" v-if="model[field]">
</a-tag> <div v-if="isValidJson(model[field])" class="basis-tags">
<a-tag
v-for="item in safeJsonParse(model[field])"
@click="viewBasisDetail(item)"
:key="item.id"
class="basis-tag"
>
{{ item.name }}
</a-tag>
</div>
<a-alert v-else type="warning" :message="`无效的数据格式: ${model[field]}`" class="basis-alert" />
</div> </div>
<a-alert v-else type="warning" :message="`无效的数据格式: ${model[field]}`" /> <div v-else class="basis-empty">
</div> <a-empty description="暂无依据数据" :image-style="{ height: '40px' }" />
<a-empty v-else description="暂无数据" /> </div>
</template> </template>
</BasicForm> </BasicForm>
</a-spin> </div>
<div style="width: 100%; text-align: center; margin-top: 24px" v-if="!formDisabled"> <!-- 操作按钮 -->
<a-space> <div class="form-footer" v-if="!formDisabled">
<a-button @click="submitForm" type="primary" :loading="submitting" pre-icon="ant-design:check">提 交</a-button> <a-space :size="12">
<a-button @click="handleReset">重 置</a-button> <a-button @click="handleReset">重置</a-button>
</a-space> <a-button @click="submitForm" type="primary" :loading="submitting">提交</a-button>
</div> </a-space>
</div>
</a-spin>
<AuditInnerDetailDrawer ref="auditInnerDetailDrawerRef" :visible="showDetailDrawer" :basis="selectedBasis" @close="handleDrawerClose" /> <AuditInnerDetailDrawer ref="auditInnerDetailDrawerRef" :visible="showDetailDrawer" :basis="selectedBasis" @close="handleDrawerClose" />
</div> </div>
...@@ -125,29 +137,12 @@ ...@@ -125,29 +137,12 @@
const initFormData = async () => { const initFormData = async () => {
try { try {
loading.value = true; loading.value = true;
//const { cctoken, WorkID } = getUrlParams();
//console.log('Token:', cctoken, 'WorkID:', WorkID);
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
/**
const gettokeyUrl = '/api/jflow/getCCWorkTokenAndTid';
const {tid,token} = await defHttp.get({
url: gettokeyUrl,
params: {
"targetKey":"targetKey",
"flowToken":cctoken,
"WorkID":WorkID,
"_t": timestamp},
});
*/
let tid = toRaw(route.query).id; let tid = toRaw(route.query).id;
console.log('tid:', tid); console.log('tid:', tid);
//setAuthCache(TOKEN_KEY, token);
//console.log('tid:', tid, 'token:', token);
const queryByIdUrl = '/plan.main/stPlanMan/queryById'; const queryByIdUrl = '/plan.main/stPlanMan/queryById';
const data = await defHttp.get({ const data = await defHttp.get({
url: queryByIdUrl, url: queryByIdUrl,
...@@ -169,14 +164,216 @@ ...@@ -169,14 +164,216 @@
loading.value = false; loading.value = false;
} }
}; };
const handleReset = () => {
resetFields();
};
const submitForm = async () => {
try {
submitting.value = true;
await validate();
const values = getFieldsValue();
await saveOrUpdate(values, true);
createMessage.success('提交成功');
} catch (error) {
console.error('提交失败:', error);
} finally {
submitting.value = false;
}
};
onMounted(() => { onMounted(() => {
initFormData(); initFormData();
}); });
</script> </script>
<style scoped> <style scoped lang="less">
.ant-tag { /* ==================== 柔和中性风格 - CSS 变量 ==================== */
margin-right: 8px; .plan-form-container {
cursor: pointer; --color-primary: #3b5bdb;
--color-primary-hover: #364fc7;
--color-primary-light: #e8ecfd;
--color-success: #2f9e44;
--color-warning: #e67700;
--color-error: #c92a2a;
--color-text-primary: #1c1c1e;
--color-text-secondary: #555770;
--color-text-muted: #a0a3b1;
--color-border: #e4e4e9;
--color-border-strong: #c8c8d0;
--color-bg-page: #f5f5f7;
--color-bg-white: #ffffff;
--color-bg-section: #f0f0f4;
--radius: 6px;
background: var(--color-bg-white);
min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
}
/* ==================== 表单头部 ==================== */
.form-header {
background: var(--color-bg-white);
border-bottom: 1px solid var(--color-border);
padding: 20px 32px;
border-left: 3px solid var(--color-primary);
padding-left: 28px;
}
.form-title {
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary);
margin: 0 0 3px 0;
letter-spacing: -0.2px;
}
.form-subtitle {
font-size: 13px;
color: var(--color-text-muted);
margin: 0;
}
/* ==================== 表单主体 ==================== */
.form-body {
padding: 24px 32px;
background: var(--color-bg-white);
}
/* 依据标签样式 */
.basis-container {
padding: 8px 0;
}
.basis-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.basis-tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: var(--color-primary-light);
border: 1px solid var(--color-primary);
border-radius: var(--radius);
color: var(--color-primary);
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
&:hover {
background: #d0d9f8;
}
}
.basis-alert {
border-radius: var(--radius);
}
.basis-empty {
padding: 16px;
background: var(--color-bg-section);
border-radius: var(--radius);
border: 1px dashed var(--color-border-strong);
}
/* ==================== 表单底部按钮 ==================== */
.form-footer {
padding: 14px 32px;
background: var(--color-bg-section);
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
}
/* ==================== 表单控件样式覆盖 ==================== */
:deep(.ant-form-item) {
margin-bottom: 18px;
}
:deep(.ant-form-item-label > label) {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
}
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-picker),
:deep(.ant-input-textarea textarea) {
border-radius: var(--radius) !important;
border-color: var(--color-border) !important;
font-size: 13px;
box-shadow: none !important;
&:hover {
border-color: var(--color-border-strong) !important;
}
&:focus,
&.ant-input-focused {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
}
:deep(.ant-select-focused .ant-select-selector) {
border-color: var(--color-primary) !important;
box-shadow: 0 0 0 2px var(--color-primary-light) !important;
}
/* ==================== 按钮样式 ==================== */
:deep(.ant-btn) {
border-radius: var(--radius);
font-weight: 500;
font-size: 13px;
height: 34px;
padding: 0 18px;
box-shadow: none;
&.ant-btn-primary {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
&.ant-btn-default {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background: var(--color-bg-white);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
}
/* ==================== 加载状态 ==================== */
:deep(.ant-spin-container) {
min-height: 400px;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.form-header {
padding: 16px 20px;
}
.form-body {
padding: 20px;
}
.form-footer {
padding: 12px 20px;
} }
}
</style> </style>
...@@ -3,83 +3,211 @@ ...@@ -3,83 +3,211 @@
v-bind="$attrs" v-bind="$attrs"
@register="registerModal" @register="registerModal"
destroyOnClose destroyOnClose
title="发起计划"
:width="1000" :width="1000"
@ok="handleSubmit" @ok="handleSubmit"
@cancel="handleCance" @cancel="handleCance"
:centered="true" :centered="true"
:title="'发起计划'"
> >
<a-form :model="formModel" ref="formRef" style="overflow-x: hidden" :disabled="isDetail"> <div class="modal-body">
<a-row gutter="8"> <a-form ref="formRef" :model="formModel" class="styled-form" :disabled="isDetail">
<a-col :span="12"> <!-- 第一行:计划名称 + 类型 -->
<a-form-item label="计划名称" name="projectName" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> <a-row :gutter="16">
<a-input v-model:value="formModel.projectName" allow-clear :disabled="isDetail" /> <a-col :span="12">
</a-form-item> <a-form-item
</a-col> label="计划名称"
<a-col :span="12"> name="projectName"
<a-form-item label="类型" name="projectType" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> :label-col="{ span: 6 }"
<JCategorySelect pcode="B09" :value="formModel.projectType" :disabled="isDetail" @change="changeJCategory" /> :wrapper-col="{ span: 18 }"
</a-form-item> :rules="[{ required: true, message: '请输入计划名称' }]"
</a-col> >
<a-col :span="12"> <a-input v-model:value="formModel.projectName" allow-clear placeholder="请输入计划名称" :disabled="isDetail" />
<a-form-item label="计划开始日期" name="planStartDate" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> </a-form-item>
<a-date-picker v-model:value="formModel.planStartDate" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isDetail" /> </a-col>
</a-form-item> <a-col :span="12">
</a-col> <a-form-item
<a-col :span="12"> label="计划类型"
<a-form-item label="计划结束日期" name="planEndDate" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> name="projectType"
<a-date-picker v-model:value="formModel.planEndDate" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isDetail" /> :label-col="{ span: 6 }"
</a-form-item> :wrapper-col="{ span: 18 }"
</a-col> :rules="[{ required: true, message: '请选择计划类型' }]"
<a-col :span="24"> >
<a-form-item label="依据" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> <JCategorySelect pcode="B09" :value="formModel.projectType" :disabled="isDetail" @change="changeJCategory" placeholder="请选择类型" />
<div> </a-form-item>
<a-tag </a-col>
v-for="item in formModel.basisList" </a-row>
:key="item.id" <!-- 第二行:执行部门 + 负责人 -->
closable <a-row :gutter="16">
@close="removeBasis(item.id)" <a-col :span="12">
@click="viewBasisDetail(item)" <a-form-item label="执行部门" name="execDepCode" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<JSelectDept v-model:value="formModel.execDepCode" placeholder="请选择执行部门" :disabled="isDetail" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="负责人" name="headId" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<JSearchSelectDuty v-model:value="formModel.headId" placeholder="请选择负责人" :disabled="isDetail" />
</a-form-item>
</a-col>
</a-row>
<!-- 第三行:开始日期 + 结束日期 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="计划开始日期" name="planStartDate" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-date-picker
v-model:value="formModel.planStartDate"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled="isDetail" :disabled="isDetail"
> placeholder="选择开始日期"
{{ item.name }} />
</a-tag> </a-form-item>
<a-button type="link" v-if="showSelectorBtn" @click="openBasisSelector" :disabled="isDetail">添加依据</a-button> </a-col>
</div> <a-col :span="12">
</a-form-item> <a-form-item label="计划结束日期" name="planEndDate" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
</a-col> <a-date-picker
<a-col :span="24"> v-model:value="formModel.planEndDate"
<a-form-item label="要求" name="planRequest" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> value-format="YYYY-MM-DD"
<a-textarea v-model:value="formModel.planRequest" rows="3" allow-clear :disabled="isDetail" /> style="width: 100%"
</a-form-item> :disabled="isDetail"
</a-col> placeholder="选择结束日期"
<a-col :span="12"> />
<a-form-item label="交付物" name="planDeliverable" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> </a-form-item>
<a-input v-model:value="formModel.planDeliverable" allow-clear :disabled="isDetail" /> </a-col>
</a-form-item> </a-row>
</a-col> <!-- 第四行:优先级 + 执行规则 -->
<a-col :span="24"> <a-row :gutter="16">
<a-form-item label="描述" name="projectDesc" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> <a-col :span="12">
<a-textarea v-model:value="formModel.projectDesc" rows="3" allow-clear :disabled="isDetail" /> <a-form-item label="优先级" name="priority" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
</a-form-item> <a-select
</a-col> v-model:value="formModel.priority"
<a-col :span="24"> placeholder="请选择优先级"
<a-form-item label="执行规则" name="exeRule" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> :disabled="isDetail"
<vxe-radio-group v-model="formModel.exeRule" :options="exeRules" /> :options="[
</a-form-item> { label: '高', value: '1' },
</a-col> { label: '中', value: '2' },
<a-col :span="24"> { label: '低', value: '3' },
<a-form-item label="相关附件" name="fileUploadPath" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }"> ]"
<JUpload v-model:value="formModel.fileUploadPath" desText="支持扩展名: .rar .zip .doc .docx .pdf .jpg..." :disabled="isDetail" /> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="0"> <a-col :span="12">
<a-form-item name="id"> <a-form-item label="执行规则" name="exeRule" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-input v-model:value="formModel.id" type="hidden" /> <a-radio-group v-model:value="formModel.exeRule" :disabled="isDetail">
</a-form-item> <a-radio-button :value="1">一次性</a-radio-button>
</a-col> <a-radio-button :value="2">周期执行</a-radio-button>
</a-row> <a-radio-button :value="3">事件触发</a-radio-button>
</a-form> </a-radio-group>
</a-form-item>
</a-col>
</a-row>
<!-- 事件触发:填写触发事件名称 -->
<a-row :gutter="16" v-if="formModel.exeRule === 3">
<a-col :span="12">
<a-form-item
label="触发事件名称"
name="triggerEventName"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
:rules="[{ required: true, message: '请填写触发该计划执行的事件名称' }]"
>
<a-input v-model:value="formModel.triggerEventName" allow-clear :disabled="isDetail" placeholder="如:发现安全隐患、设备故障上报等" />
</a-form-item>
</a-col>
</a-row>
<!-- 周期执行:选择执行周期 -->
<a-row :gutter="16" v-if="formModel.exeRule === 2">
<a-col :span="12">
<a-form-item
label="执行周期"
name="exePeriod"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
:rules="[{ required: true, message: '请选择执行周期' }]"
>
<a-select
v-model:value="formModel.exePeriod"
placeholder="请选择执行周期"
:disabled="isDetail"
:options="[
{ label: '每日', value: 'daily' },
{ label: '每周', value: 'weekly' },
{ label: '每月', value: 'monthly' },
{ label: '每季度', value: 'quarterly' },
{ label: '每半年', value: 'halfyear' },
{ label: '每年', value: 'yearly' },
]"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="首次执行日期" name="firstExecDate" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-date-picker
v-model:value="formModel.firstExecDate"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled="isDetail"
placeholder="选择首次执行日期"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 计划依据 -->
<a-form-item label="计划依据" name="planBasis" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<div class="basis-tags-container">
<a-tag
v-for="item in formModel.basisList"
:key="item.id"
closable
@close="removeBasis(item.id)"
@click="viewBasisDetail(item)"
:disabled="isDetail"
class="basis-tag"
>
{{ item.name }}
</a-tag>
<a-button type="dashed" v-if="showSelectorBtn && !isDetail" @click="openBasisSelector" class="btn-add-basis"> + 添加依据 </a-button>
</div>
</a-form-item>
<!-- 计划要求 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item
label="交付物"
name="planDeliverable"
:rules="[{ required: true, message: '请输入交付物' }]"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-input
v-model:value="formModel.planDeliverable"
allow-clear
:disabled="isDetail"
placeholder="请输入需提交的交付物,如报告、台账等"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="备注说明" name="projectDesc" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<a-textarea v-model:value="formModel.projectDesc" :rows="3" allow-clear :disabled="isDetail" placeholder="请输入补充说明..." />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="相关附件" name="fileUploadPath" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }">
<JUpload v-model:value="formModel.fileUploadPath" desText="支持扩展名: .rar .zip .doc .docx .pdf .jpg..." :disabled="isDetail" />
</a-form-item>
</a-col>
</a-row>
<!-- 相关附件 -->
<!-- 隐藏字段 -->
<a-form-item name="id" style="display: none">
<a-input v-model:value="formModel.id" type="hidden" />
</a-form-item>
</a-form>
</div>
<!-- 选择依据组件 --> <!-- 选择依据组件 -->
<BasicCtrlSelector ref="selectorRef" @click="handleSelectorClick" /> <BasicCtrlSelector ref="selectorRef" @click="handleSelectorClick" />
<!-- 依据详情抽屉 --> <!-- 依据详情抽屉 -->
...@@ -97,12 +225,13 @@ ...@@ -97,12 +225,13 @@
import { list } from '/@/views/newlib/components/api/AuditInnerCtrl.api'; import { list } from '/@/views/newlib/components/api/AuditInnerCtrl.api';
import { listAll } from '/@/views/newlib/components/api/AuditInnerCtrlItem.api'; import { listAll } from '/@/views/newlib/components/api/AuditInnerCtrlItem.api';
import { saveOrUpdate } from '../StPlanMan.api'; import { saveOrUpdate } from '../StPlanMan.api';
import AuditInnerDetailDrawer from '/@/views/newlib/components/modal/AuditInnerDetailDrawer.vue'; import AuditInnerDetailDrawer from '/@/views/newlib/components/modal/AuditInnerDetailDrawer.vue';
import BasicCtrlSelector from '/@/components/BasicCtrlSelector.vue';
defineProps<{ defineProps<{
showSelectorBtn?: boolean; showSelectorBtn?: boolean;
}>(); }>();
const AuditInnerDetailDrawerRef = ref(); const AuditInnerDetailDrawerRef = ref();
const showDetailDrawer = ref(false); const showDetailDrawer = ref(false);
const selectedBasis = ref<any>(null); const selectedBasis = ref<any>(null);
...@@ -116,6 +245,7 @@ ...@@ -116,6 +245,7 @@
AuditInnerDetailDrawerRef.value?.open(data); AuditInnerDetailDrawerRef.value?.open(data);
showDetailDrawer.value = true; showDetailDrawer.value = true;
}; };
// Emits声明 // Emits声明
const emit = defineEmits(['register', 'success', 'selector-click']); const emit = defineEmits(['register', 'success', 'selector-click']);
const isUpdate = ref(true); const isUpdate = ref(true);
...@@ -123,16 +253,20 @@ ...@@ -123,16 +253,20 @@
const visible = ref(false); const visible = ref(false);
const formRef = ref(); const formRef = ref();
const exeRules = ref([ // const exeRules = ref([
{ value: 1, label: '每发生' }, // { value: 1, label: '事件触发' },
{ value: 2, label: '周期性' }, // { value: 2, label: '周期执行' },
{ value: 3, label: '一次性' }, // { value: 3, label: '一次性执行' },
]); // ]);
const formModel = reactive({ const formModel = reactive({
projectName: '', projectName: '',
projectType: '', projectType: '',
execDepCode: '',
execDepName: '',
headId: '',
headName: '',
priority: '2',
planRequest: '', planRequest: '',
planDeliverable: '', planDeliverable: '',
planStartDate: '', planStartDate: '',
...@@ -142,13 +276,24 @@ ...@@ -142,13 +276,24 @@
projectDesc: '', projectDesc: '',
fileUploadPath: '', fileUploadPath: '',
planBasis: '', planBasis: '',
exeRule:1, exeRule: 1,
triggerEventName: '',
exePeriod: undefined,
firstExecDate: '',
completionRate: 0,
statusName: '',
id: '', id: '',
}); });
const resetForm = () => { const resetForm = () => {
Object.assign(formModel, { Object.assign(formModel, {
projectName: '', projectName: '',
projectType: '', projectType: '',
execDepCode: '',
execDepName: '',
headId: '',
headName: '',
priority: '2',
planRequest: '', planRequest: '',
planDeliverable: '', planDeliverable: '',
planStartDate: '', planStartDate: '',
...@@ -158,10 +303,16 @@ ...@@ -158,10 +303,16 @@
projectDesc: '', projectDesc: '',
fileUploadPath: '', fileUploadPath: '',
planBasis: '', planBasis: '',
exeRule:1, exeRule: 1,
triggerEventName: '',
exePeriod: undefined,
firstExecDate: '',
completionRate: 0,
statusName: '',
id: '', id: '',
}); });
}; };
//表单赋值 //表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => { const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter }); setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
...@@ -184,10 +335,6 @@ ...@@ -184,10 +335,6 @@
const handleSubmit = async () => { const handleSubmit = async () => {
try { try {
const valids = await formRef.value.validate(); const valids = await formRef.value.validate();
//const idList = formModel.basisList?.map(item => item.id);
//if(idList) {
// valids["planBasis"] = JSON.stringify(formModel.basisList)
//}
if (formModel.basisList) { if (formModel.basisList) {
valids['planBasis'] = JSON.stringify(formModel.basisList); valids['planBasis'] = JSON.stringify(formModel.basisList);
...@@ -205,7 +352,7 @@ ...@@ -205,7 +352,7 @@
setModalProps({ confirmLoading: false }); setModalProps({ confirmLoading: false });
} }
}; };
// const basisSelectMode = ref<'flat' | 'nested'>('flat');
const selectorRef = ref(); const selectorRef = ref();
const basisSelectorConfig = { const basisSelectorConfig = {
title: '选择依据', title: '选择依据',
...@@ -222,7 +369,6 @@ ...@@ -222,7 +369,6 @@
'a', 'a',
{ {
onClick: async () => { onClick: async () => {
// 加载二级数据并更新组件中的表格
const detailData = await listAll({ checkid: record.id }); const detailData = await listAll({ checkid: record.id });
selectorRef.value.setDetail({ selectorRef.value.setDetail({
title: '详细内容', title: '详细内容',
...@@ -278,22 +424,180 @@ ...@@ -278,22 +424,180 @@
const changeJCategory = (val, obj) => { const changeJCategory = (val, obj) => {
formModel['projectType'] = val; formModel['projectType'] = val;
}; };
const formatTp = (row) => { const formatTp = (row) => {
return row.tp == 1 ? '工具要求' : row.tp == 2 ? '记录要求' : '制度要求'; return row.tp == 1 ? '工具要求' : row.tp == 2 ? '记录要求' : '制度要求';
}; };
const formatComeFrom = (row) => { const formatComeFrom = (row) => {
return row.comeFrom == 1 ? '自建' : row.tp == 2 ? '合规库' : '内控制度'; return row.comeFrom == 1 ? '自建' : row.tp == 2 ? '合规库' : '内控制度';
}; };
const formatExeRules = (row) => { const formatExeRules = (row) => {
return row.comeFrom == 1 ? '每发生' : row.tp == 2 ? '周期性' : '一次性'; return row.comeFrom == 1 ? '一次性' : row.tp == 2 ? '周期执行' : '事件触发';
}; };
</script> </script>
<style scoped> <style scoped lang="less">
.ant-tag { /* ==================== 弹窗主体 ==================== */
.modal-body {
max-height: 70vh;
// overflow: hidden;
}
.styled-form {
display: flex;
flex-direction: column;
gap: 4px;
overflow-x: hidden;
}
/* ==================== 表单项样式 ==================== */
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-form-item-label > label) {
font-size: 13px;
font-weight: 500;
}
/* ==================== 依据标签样式 ==================== */
.basis-tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
min-height: 32px;
}
.basis-tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: var(--color-primary-light);
border: 1px solid var(--color-primary);
border-radius: var(--radius);
color: var(--color-primary);
font-size: 13px;
cursor: pointer; cursor: pointer;
margin-right: 4px; margin: 0;
margin-bottom: 4px; transition: background 0.15s;
&:hover {
background: #d0d9f8;
}
:deep(.ant-tag-close-icon) {
color: var(--color-primary);
margin-left: 6px;
&:hover {
color: var(--color-error);
}
}
}
.btn-add-basis {
display: inline-flex;
align-items: center;
height: 30px;
border-radius: var(--radius);
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
font-size: 13px;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
/* ==================== 单选按钮组样式 ==================== */
.styled-radio-group {
:deep(.ant-radio-button-wrapper) {
border-radius: 0;
border-color: var(--color-border);
&:first-child {
border-radius: var(--radius) 0 0 var(--radius);
}
&:last-child {
border-radius: 0 var(--radius) var(--radius) 0;
}
&:hover {
color: var(--color-primary);
}
&.ant-radio-button-wrapper-checked {
background: var(--color-primary);
border-color: var(--color-primary);
color: #ffffff;
}
}
}
/* ==================== 弹窗全局样式覆盖 ==================== */
:deep(.ant-modal-header) {
padding: 14px 24px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-section);
}
:deep(.ant-modal-title) {
font-size: 15px;
font-weight: 600;
color: var(--color-text-primary);
}
:deep(.ant-modal-body) {
padding: 20px 24px;
background: var(--color-bg-white);
}
:deep(.ant-modal-footer) {
padding: 12px 24px;
border-top: 1px solid var(--color-border);
background: var(--color-bg-section);
}
:deep(.ant-modal-footer .ant-btn) {
border-radius: var(--radius);
height: 34px;
padding: 0 18px;
font-weight: 500;
font-size: 13px;
box-shadow: none;
}
:deep(.ant-modal-footer .ant-btn-primary) {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover {
background: var(--color-primary-hover);
border-color: var(--color-primary-hover);
}
}
:deep(.ant-modal-footer .ant-btn-default) {
border-color: var(--color-border-strong);
color: var(--color-text-secondary);
background: var(--color-bg-white);
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
background: var(--color-primary-light);
}
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 768px) {
.modal-body {
max-height: 60vh;
}
} }
</style> </style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论