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

计划管理样式更新

上级 6a232359
/**
* 计划执行表单状态管理
* 用于在不同页面之间共享表单保存方法
*/
import { defineStore } from 'pinia';
import { ref } from 'vue';
import { reactive, ref } from 'vue';
export const usePlanFormStore = defineStore('planForm', () => {
// 存储表单的 ref 回调
const submitCallback = ref<() => Promise<any> | null>(null);
const formData = ref<any>(null);
export const usePlanFormStore = defineStore('planFormStore', () => {
// 表单提交回调函数
const submitCallback = ref<(() => Promise<void>) | null>(null);
/**
* 注册表单提交回调(在 StPlanExcuteForm 中调用)
* @param callback - 表单的 submitForm 方法
*/
const registerSubmitCallback = (callback: () => Promise<any>) => {
// 表单数据缓存
const formDataCache = reactive({
id: '',
executeStatus: '',
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;
console.log('[PlanFormStore] 已注册表单保存回调');
};
/**
* 注册表单数据(用于跨页面访问)
* @param data - 表单数据
*/
const setFormData = (data: any) => {
formData.value = data;
// 执行表单提交
const executeSubmit = async () => {
if (submitCallback.value) {
try {
formLoadingState.isError = false;
formLoadingState.errorMessage = '';
await submitCallback.value();
} catch (error: any) {
formLoadingState.isError = true;
formLoadingState.errorMessage = error?.message || '表单提交失败';
throw error;
}
}
};
/**
* 获取表单数据
*/
const getFormData = () => {
return formData.value;
// 更新表单数据缓存
const updateFormDataCache = (data: any) => {
Object.assign(formDataCache, data);
};
/**
* 执行表单保存(在 TodoIndex 中调用)
*/
const submitPlanForm = async () => {
if (!submitCallback.value) {
console.warn('[PlanFormStore] 未注册表单保存回调');
return null;
}
// 清除表单缓存
const clearFormCache = () => {
Object.assign(formDataCache, {
id: '',
executeStatus: '',
actualStartTime: null,
actualEndTime: null,
executeEcord: '',
attachments: [],
});
formLoadingState.lastLoadedId = '';
};
try {
console.log('[PlanFormStore] 开始执行表单保存...');
const result = await submitCallback.value();
console.log('[PlanFormStore] 表单保存成功:', result);
return result;
} catch (error) {
console.error('[PlanFormStore] 表单保存失败:', error);
throw error;
}
// 设置加载状态
const setLoadingState = (isLoading: boolean) => {
formLoadingState.isLoading = isLoading;
};
/**
* 清空回调(可选)
*/
const clearCallback = () => {
submitCallback.value = null;
console.log('[PlanFormStore] 已清空表单保存回调');
// 设置最后加载的ID
const setLastLoadedId = (id: string) => {
formLoadingState.lastLoadedId = id;
};
return {
submitCallback,
formData,
formDataCache,
formLoadingState,
registerSubmitCallback,
setFormData,
getFormData,
submitPlanForm,
clearCallback,
executeSubmit,
updateFormDataCache,
clearFormCache,
setLoadingState,
setLastLoadedId,
};
});
import { message } from 'ant-design-vue';
import { BasicColumn, FormSchema } from '/@/components/Table';
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[] = [
{
title: '计划名称',
align: 'left',
dataIndex: 'projectName',
width: 200,
width: 180,
ellipsis: true,
},
{
title: '类型',
title: '计划类型',
align: 'center',
dataIndex: 'projectTypeName',
width: 120,
ellipsis: true,
},
// {
// title: '执行部门',
// align: 'center',
// dataIndex: 'execDepName',
// width: 150,
// ellipsis: true,
// },
{
title: '执行部门',
align: 'center',
dataIndex: 'execDepName',
width: 140,
ellipsis: true,
},
{
title: '负责人',
align: 'center',
......@@ -32,19 +68,39 @@ export const columns: BasicColumn[] = [
ellipsis: true,
},
{
title: '计划开始日期',
title: '优先级',
align: 'center',
dataIndex: 'planStartDate',
width: 130,
dataIndex: 'priority',
width: 80,
ellipsis: true,
customRender: ({ text }) => {
return !text ? '-' : text.length > 10 ? text.substr(0, 10) : text;
const priorityMap = {
'1': '高',
'2': '中',
'3': '低',
};
return priorityMap[text] || '-';
},
},
{
title: '计划结束日期',
title: '执行规则',
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,
ellipsis: true,
customRender: ({ text }) => {
......@@ -52,22 +108,17 @@ export const columns: BasicColumn[] = [
},
},
{
title: '执行规则',
title: '计划结束日期',
align: 'center',
dataIndex: 'exeRule',
width: 100,
dataIndex: 'planEndDate',
width: 130,
ellipsis: true,
customRender: ({ text }) => {
const ruleMap = {
1: '每发生',
2: '周期性',
3: '一次性',
};
return ruleMap[text] || '-';
return !text ? '-' : text.length > 10 ? text.substr(0, 10) : text;
},
},
{
title: '状态',
title: '计划状态',
align: 'center',
dataIndex: 'statusName',
width: 100,
......@@ -91,7 +142,8 @@ export const columns: BasicColumn[] = [
ellipsis: false,
},
];
//查询数据
// 查询表单数据
export const searchFormSchema: FormSchema[] = [
{
label: '计划名称',
......@@ -100,7 +152,7 @@ export const searchFormSchema: FormSchema[] = [
colProps: { span: 6 },
},
{
label: '类型',
label: '计划类型',
field: 'projectType',
component: 'Select',
colProps: { span: 6 },
......@@ -112,12 +164,58 @@ export const searchFormSchema: FormSchema[] = [
{
label: '执行部门',
field: 'execDepCode',
component: 'Input',
component: 'Select',
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[] = [
{
label: '计划名称',
......@@ -126,11 +224,11 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
dynamicRules: ({ model, schema }) => {
return [{ required: true, message: '请输入项目名称!' }];
return [{ required: true, message: '请输入计划名称!' }];
},
},
{
label: '类型',
label: '计划类型',
field: 'projectType',
component: 'JCategorySelect',
componentProps: {
......@@ -140,10 +238,29 @@ export const formSchema: FormSchema[] = [
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
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: '计划开始日期',
field: 'planStartDate',
......@@ -151,7 +268,7 @@ export const formSchema: FormSchema[] = [
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: '请选择计划开始日期!' }];
return [{ required: true, message: '请选择计划开始日期!' }];
},
},
{
......@@ -161,26 +278,81 @@ export const formSchema: FormSchema[] = [
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: '请选择计划结束日期!' }];
return [{ required: true, message: '请选择计划结束日期!' }];
},
},
{
label: '依据',
field: 'planBasis',
component: 'Input', // 使用基础组件类型
slot: 'planBasis',
itemProps: {
labelCol: { xs: { span: 24 }, sm: { span: 3 } },
wrapperCol: { xs: { span: 24 }, sm: { span: 21 } },
label: '优先级',
field: 'priority',
component: 'Select',
colProps: { lg: 12 },
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 6 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 18 } } },
componentProps: {
options: priorityOptions,
placeholder: '请选择优先级',
},
},
{
label: '要求',
field: 'planRequest',
component: 'InputTextArea',
label: '执行规则',
field: 'exeRule',
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 } } },
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 }) => {
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[] = [
return [{ required: true, message: '请输入交付物!' }];
},
},
// {
// 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: '描述',
{
label: '备注说明',
field: 'projectDesc',
component: 'InputTextArea',
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } },
dynamicRules: ({ model, schema }) => {
return [{ required: false, message: '请输入项目描述!' }];
return [{ required: false, message: '请输入备注说明!' }];
},
},
{
......@@ -233,7 +388,7 @@ export const formSchema: FormSchema[] = [
itemProps: { labelCol: { xs: { span: 24 }, sm: { span: 3 } }, wrapperCol: { xs: { span: 24 }, sm: { span: 21 } } },
show: false,
},
// TODO 主键隐藏字段,目前写死为ID
// 主键隐藏字段
{
label: '',
field: 'id',
......
<template>
<div>
<div class="jeecg-basic-table-form-container" @keyup.enter="searchQuery">
<a-form :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-row :gutter="24">
<a-col :lg="6">
<a-form-item :label="searchFormSchema[0].label">
<JInput placeholder="请输入" v-model:value="queryParam[searchFormSchema[0].field]" />
<div class="plan-management-page">
<!-- 页面头部区域 -->
<!-- <div class="page-header">
<div class="header-content">
<div class="header-left">
<h1 class="page-title">计划编制管理</h1>
<p class="page-desc">统一管理和追踪所有业务计划的编制与审批流程</p>
</div>
<div class="header-stats">
<div class="stat-item">
<span class="stat-value">--</span>
<span class="stat-label">计划总数</span>
</div>
<div class="stat-item warning">
<span class="stat-value">--</span>
<span class="stat-label">待处理</span>
</div>
<div class="stat-item success">
<span class="stat-value">--</span>
<span class="stat-label">已完成</span>
</div>
</div>
</div>
</div> -->
<!-- 主内容区 -->
<div class="main-content">
<!-- 搜索区域 -->
<div class="search-section" @keyup.enter="searchQuery">
<div class="section-header">
<span class="section-title">筛选条件</span>
</div>
<a-form :model="queryParam" :label-col="labelCol" :wrapper-col="wrapperCol" class="search-form">
<a-row :gutter="16">
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="计划名称">
<JInput placeholder="请输入计划名称" v-model:value="queryParam['projectName']" />
</a-form-item>
</a-col>
<a-col :lg="6">
<a-form-item :label="searchFormSchema[1].label">
<JSearchSelect placeholder="请输入" v-model:value="queryParam[searchFormSchema[1].field]" dict="projecttype" />
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="计划类型">
<JSearchSelect placeholder="请选择类型" v-model:value="queryParam['projectType']" dict="projecttype" />
</a-form-item>
</a-col>
<!-- <a-col :lg="6">
<a-form-item :label="searchFormSchema[2].label">
<JSelectDept placeholder="请输入" v-model:value="jSelectDeptVal" :multiple="false" @change="updateJSelectDept" />
<a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="执行部门">
<JSelectDept placeholder="请选择执行部门" v-model:value="queryParam['execDepCode']" />
</a-form-item>
</a-col> -->
<a-col :lg="6">
<a-form-item>
<a-space :size="5">
<a-button type="primary" preIcon="ant-design:search-outlined" @click="searchQuery">查询</a-button>
<a-button type="primary" preIcon="ant-design:reload-outlined" @click="searchReset">重置</a-button>
</a-col>
<a-col :xl="4" :lg="8" :md="12" :sm="24">
<a-form-item label="计划状态">
<a-select
v-model:value="queryParam['status']"
placeholder="请选择状态"
allow-clear
:options="[
{ label: '草稿', value: '0' },
{ label: '审批中', value: '1' },
{ label: '已通过', value: '2' },
{ label: '已拒绝', value: '3' },
{ label: '执行中', value: '4' },
{ label: '已完成', value: '5' },
{ label: '已作废', value: '6' },
]"
/>
</a-form-item>
</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>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<!-- 表格区域 -->
<div class="table-section">
<BasicTable @register="registerTable" :rowSelection="rowSelection" class="flat-table">
<!-- 插槽:table标题 -->
<template #tableTitle>
<a-button type="primary" @click="handleAdd" preIcon="ant-design:plus-outlined"> 新建</a-button>
<a-button v-show="showUpBtn" type="primary" preIcon="ant-design:export-outlined" @click="onExportXls"> 导出</a-button>
<j-upload-button v-show="showUpBtn" type="primary" preIcon="ant-design:import-outlined" @click="onImportXls">导入</j-upload-button>
<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">
<a-menu-item key="1" @click="batchHandleDelete" class="danger-item">
<Icon icon="ant-design:delete-outlined" />
删除
批量删除
</a-menu-item>
</a-menu>
</template>
<a-button
>批量操作
<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" style="font-size: 12px; font-style: italic">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small" @click="downloadFile(text)"
>下载</a-button
>
<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" />
<StPlanManFlowModal ref="refStPlanManFlow" />
</div>
<!-- 待办抽屉 -->
<div v-if="isShowDrawer">
<a-drawer
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"
>
<a-drawer destroyOnClose v-model:open="isShowDrawer" class="flat-drawer" title="待办任务" placement="right" width="90%">
<template #extra>
<div style="float: right">
<a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag>
<a-tag>任务节点:{{ taskName }}</a-tag>
<div class="drawer-tags">
<span class="tag">发起人: {{ startUser }}</span>
<span class="tag">任务节点: {{ taskName }}</span>
</div>
</template>
<TodoIndex v-if="isShowDrawer" ref="refTodoIndex" @callback="handleSuccess" />
</a-drawer>
</div>
<!-- 流程详情抽屉 -->
<div v-if="isShowDetailDrawer">
<a-drawer
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"
>
<a-drawer destroyOnClose v-model:open="isShowDetailDrawer" class="flat-drawer" title="流程详情" placement="right" width="90%">
<template #extra>
<div style="float: right">
<a-button type="text" @click="handleDetailDrawerClose">关闭</a-button>
</div>
</template>
<Detail ref="refDetail" />
</a-drawer>
......@@ -136,6 +208,7 @@
import { todoList, getMyTaskFlow } from '/@/components/Process/api/todo';
// 引入详情组件
import Detail from '../../flowable/task/myProcess/components/Detail.vue';
const refTodoIndex = ref();
const isShowDrawer = ref(false);
const startUser = ref<string>('');
......@@ -147,10 +220,12 @@
// 流程详情抽屉相关
const refDetail = ref();
const isShowDetailDrawer = ref(false);
//注册model
// 注册model
const [registerModal, { openModal }] = useModal();
const jSelectDeptVal = ref([]);
//注册table数据
// 注册table数据
const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
tableProps: {
title: '计划编制',
......@@ -159,7 +234,6 @@
canResize: false,
useSearchForm: false,
formConfig: {
//labelWidth: 120,
schemas: searchFormSchema,
autoSubmitOnEnter: true,
showAdvancedButton: true,
......@@ -167,7 +241,7 @@
fieldMapToTime: [],
},
actionColumn: {
width: 240,
width: 280,
fixed: 'right',
},
showTableSetting: false,
......@@ -186,15 +260,17 @@
});
const [registerTable, { setProps, reload }, { rowSelection, selectedRowKeys }] = tableContext;
const labelCol = reactive({
xs: { span: 24 },
sm: { span: 8 },
sm: { span: 6 },
});
const wrapperCol = reactive({
xs: { span: 24 },
sm: { span: 16 },
sm: { span: 18 },
});
const queryParam = reactive({});
function initParam() {
const schemas = unref(searchFormSchema);
schemas.forEach((item) => {
......@@ -205,6 +281,7 @@
jSelectDeptVal.value = [];
}
initParam();
function updateJSelectDept(val) {
if (val.length > 0) {
queryParam[searchFormSchema[2].field] = val[0];
......@@ -213,14 +290,17 @@
queryParam[searchFormSchema[2].field] = '';
}
}
function searchQuery() {
setProps({ searchInfo: toRaw(queryParam) });
reload();
}
function searchReset() {
initParam();
reload();
}
/**
* 新增事件
*/
......@@ -230,6 +310,7 @@
showFooter: true,
});
}
/**
* 编辑事件
*/
......@@ -240,6 +321,7 @@
showFooter: true,
});
}
/**
* 详情
*/
......@@ -250,6 +332,7 @@
showFooter: false,
});
}
/**
* 流转记录
*/
......@@ -263,9 +346,6 @@
try {
const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id });
console.log('获取流程任务信息:', myTaskFlow);
// 提取任务流信息,支持多层返回结构(data/result/直接对象)
const taskData = pickStartResult(myTaskFlow);
if (!taskData || !taskData.taskId) {
......@@ -273,19 +353,14 @@
return;
}
// 若未指定是否审批,则默认展示审批意见(保持与发起流程一致)
if (taskData.nodeisApprove == null) {
taskData.nodeisApprove = true;
}
// 保存缓存
taskCache.set(String(dataId), taskData);
// 打开流程详情抽屉并初始化数据
isShowDetailDrawer.value = true;
await nextTick();
if (refDetail.value) {
// 使用获取到的taskData,确保包含所有必要参数
refDetail.value.iniData({
...record,
...taskData,
......@@ -301,7 +376,6 @@
}
async function findTodoTaskByProcInsId(procInsId: string) {
// 启动流程后,待办任务可能存在短暂延迟,做一个轻量重试
for (let i = 0; i < 3; i++) {
const ret = await todoList({ pageNum: 1, pageSize: 10, procInsId });
const records = ret?.records || [];
......@@ -313,12 +387,10 @@
}
function pickStartResult(res: any) {
// defHttp 返回形态在不同后端/拦截器下可能是 data/result/直接对象,这里做一次兜底
const base = res?.data ?? res?.result ?? res;
return base || {};
}
// 发起任务:点击后若流程未启动则先启动,再在抽屉中完成流程操作
async function handleTodo(record: Recordable) {
const dataId = record.id || record.dataId || record.businessId;
if (!dataId) {
......@@ -326,13 +398,11 @@
return;
}
// 优先使用本地缓存(用于刚启动后列表未回写 taskId/procInsId 的场景)
const cached = taskCache.get(String(dataId));
if (cached && !record.taskId) {
record = Object.assign({}, record, cached);
}
// 流程未启动时,先启动流程
const needStartFlow =
!record.taskId && !record.procInsId && (record['bpmStatus'] == null || record['bpmStatus'] == '' || record['bpmStatus'] == '1');
......@@ -340,27 +410,14 @@
try {
const formData = { dataId, dataName: 'id' };
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);
let payload: any = Object.assign({}, record, startRes);
//console.log("definitionStartByDeployId 返回值-payload ",payload);
// 若启动接口未返回 taskId,则尝试通过 procInsId 从待办列表反查
if (!payload.taskId && payload.procInsId) {
//console.log("definitionStartByDeployId 返回值-payload -1");
const todoRow = await findTodoTaskByProcInsId(payload.procInsId);
if (todoRow?.taskId) {
payload = Object.assign({}, record, startRes, todoRow);
}
} else {
//console.log("definitionStartByDeployId 返回值-payload -2");
}
if (!payload.taskId) {
......@@ -379,11 +436,9 @@
console.error('启动流程或获取任务失败:', e);
message.error('操作失败,请重试');
}
//console.log("definitionStartByDeployId 返回值-payload- return");
return;
}
console.log('流程已启动:优先使用 record.taskId,否则尝试用 procInsId 从待办列表反查');
// 流程已启动:优先使用 record.taskId,否则尝试用 procInsId 从待办列表反查
try {
let payload: any = { ...record };
......@@ -412,7 +467,6 @@
}
}
// 待办任务:
async function handleTodoDb(record: Recordable) {
const dataId = record.id;
const deployId = record.deployId;
......@@ -423,9 +477,6 @@
try {
const myTaskFlow = await getMyTaskFlow({ deploymentId: record.deployId, dataId: record.id });
console.log('获取流程任务信息:', myTaskFlow);
// 提取任务流信息,支持多层返回结构(data/result/直接对象)
const taskData = pickStartResult(myTaskFlow);
if (!taskData || !taskData.taskId) {
......@@ -433,15 +484,11 @@
return;
}
// 若未指定是否审批,则默认展示审批意见(保持与发起流程一致)
if (taskData.nodeisApprove == null) {
taskData.nodeisApprove = true;
}
// 保存缓存
taskCache.set(String(dataId), taskData);
// 打开抽屉并初始化待办任务数据
startUser.value = taskData.startUserName || taskData.startUser || '';
taskName.value = taskData.taskName || taskData.currentTaskName || '';
......@@ -460,12 +507,14 @@
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
......@@ -479,6 +528,7 @@
function handleDetailDrawerClose() {
isShowDetailDrawer.value = false;
}
/**
* 操作栏
*/
......@@ -523,7 +573,6 @@
if (record['bpmStatus'] == null || record['bpmStatus'] == '') return true;
else return false;
},
// disabled: record['planFlag'] != '0' && record['planFlag'] != '2' && record['planFlag'] != '9',
popConfirm: {
title: '是否确认删除该计划项',
confirm: handleDelete.bind(null, record),
......@@ -531,6 +580,7 @@
},
];
}
/**
* 下拉操作栏
*/
......@@ -539,4 +589,352 @@
}
</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>
<div style="background-color: #fff; padding: 100px">
<a-form ref="formRef" :model="formData" :label-col="{ span: 4 }" :wrapper-col="{ span: 8 }">
<div class="execute-form-container">
<!-- 表单头部 -->
<div class="form-header">
<h2 class="form-title">执行信息录入</h2>
<p class="form-subtitle">更新计划执行状态和相关记录</p>
</div>
<!-- 表单主体 -->
<div class="form-body">
<a-form ref="formRef" :model="formData" layout="vertical" class="styled-form">
<!-- 执行状态 -->
<a-form-item label="执行状态" prop="executeStatus">
<a-form-item label="执行状态" name="executeStatus">
<a-select v-model:value="formData.executeStatus" placeholder="请选择执行状态">
<a-select-option value="0">未开始</a-select-option>
<a-select-option value="1">进行中</a-select-option>
<a-select-option value="2">已完成</a-select-option>
<a-select-option value="3">已暂停</a-select-option>
<a-select-option value="0">
<span class="status-option">
<span class="status-indicator pending"></span>
未开始
</span>
</a-select-option>
<a-select-option value="1">
<span class="status-option">
<span class="status-indicator processing"></span>
进行中
</span>
</a-select-option>
<a-select-option value="2">
<span class="status-option">
<span class="status-indicator success"></span>
已完成
</span>
</a-select-option>
<a-select-option value="3">
<span class="status-option">
<span class="status-indicator paused"></span>
已暂停
</span>
</a-select-option>
</a-select>
</a-form-item>
<!-- 实际开始时间 -->
<a-form-item label="实际开始时间" prop="actualStartTime">
<a-date-picker v-model="formData.actualStartTime" type="datetime" placeholder="选择时间"></a-date-picker>
<!-- 时间选择区域 -->
<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="实际结束时间" prop="actualEndTime">
<a-date-picker v-model="formData.actualEndTime" type="datetime" placeholder="选择时间"></a-date-picker>
<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="执行记录" prop="executeRecord">
<a-textarea v-model="formData.executeRecord" :rows="4" placeholder="请输入执行记录"></a-textarea>
<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="附件" prop="attachments">
<!-- <JUpload v-model:value="formModel.fileUploadPath" desText="支持扩展名: .rar .zip .doc .docx .pdf .jpg..." :disabled="isDetail" /> -->
<!-- 附件上传 -->
<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>
<!-- 操作按钮 -->
<a-form-item :wrapper-col="{ offset: 6 }">
<!-- 表单底部按钮 -->
<div class="form-footer">
<a-space :size="12">
<a-button @click="resetForm">重置</a-button>
<a-button type="primary" @click="submitForm">保存</a-button>
<a-button style="margin-left: 40px" @click="resetForm">重置</a-button>
</a-form-item>
</a-form>
</a-space>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
<script setup lang="ts">
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 { usePlanFormStore } from '/@/store/modules/planFormStore';
const route = useRoute();
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: '',
actualStartTime: '',
actualEndTime: '',
executeRecord: '',
attachments: [],
actualStartTime: null as any,
actualEndTime: null as any,
executeEcord: '',
attachments: [] as any[],
});
const submitForm = () => {
formRef.value.validate((valid) => {
if (valid) {
console.log('Form submitted:', formData.value);
// 提交逻辑
// 监听formData变化,用于调试
watch(
() => 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 = () => {
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>
<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>
<div>
<vxe-drawer
......@@ -9,21 +8,40 @@
width="100%"
height="100%"
:loading="loading"
class="flat-flow-drawer"
>
<template #header>
<div class="drawer-header">
<h3 class="drawer-title">{{ pageTilte }}</h3>
<span class="drawer-subtitle">计划审批流程</span>
</div>
</template>
<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"
style="width: 100%; height: 100%;"
class="flow-iframe"
@load="handleIframeLoad"
></iframe>
</div>
</div>
</vxe-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted,nextTick } from "vue";
import { ref, onMounted, nextTick } from "vue";
import { useUserStoreWithOut } from "/@/store/modules/user";
import { defHttp } from '/@/utils/http/axios';
import { getToken } from '/@/utils/auth';
......@@ -35,18 +53,190 @@ const loading = ref(false);
const pageTilte = ref("");
const iframeRef = ref<HTMLIFrameElement>();
const handleIframeLoad = () => {
loading.value = false;
};
const iniPage = async (data) => {
pageTilte.value = data.projectName;
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}`;
const setSourctUrl = '/api/jflow/setCCWorkId';
await defHttp.get({
url: setSourctUrl,
params: {"targetId":data.id,"targetKey":"targetKey","token":getToken()},
});
}
// 暴露方法
defineExpose({ iniPage });
</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>
<div style="background-color: #fff; padding: 100px">
<div class="plan-form-container">
<a-spin :spinning="loading">
<!-- 表单头部 -->
<div class="form-header">
<h2 class="form-title">计划详情</h2>
<p class="form-subtitle">查看和编辑计划信息</p>
</div>
<!-- 表单内容 -->
<div class="form-body">
<BasicForm @register="registerForm">
<template #planBasis="{ model, field }">
<div v-if="model[field]">
<div v-if="isValidJson(model[field])">
<div class="basis-container" v-if="model[field]">
<div v-if="isValidJson(model[field])" class="basis-tags">
<a-tag
v-for="item in safeJsonParse(model[field])"
@click="viewBasisDetail(item)"
:key="item.id"
style="margin-bottom: 8px; cursor: pointer"
class="basis-tag"
>
{{ item.name }}
</a-tag>
</div>
<a-alert v-else type="warning" :message="`无效的数据格式: ${model[field]}`" />
<a-alert v-else type="warning" :message="`无效的数据格式: ${model[field]}`" class="basis-alert" />
</div>
<div v-else class="basis-empty">
<a-empty description="暂无依据数据" :image-style="{ height: '40px' }" />
</div>
<a-empty v-else description="暂无数据" />
</template>
</BasicForm>
</a-spin>
</div>
<div style="width: 100%; text-align: center; margin-top: 24px" v-if="!formDisabled">
<a-space>
<a-button @click="submitForm" type="primary" :loading="submitting" pre-icon="ant-design:check">提 交</a-button>
<a-button @click="handleReset">重 置</a-button>
<!-- 操作按钮 -->
<div class="form-footer" v-if="!formDisabled">
<a-space :size="12">
<a-button @click="handleReset">重置</a-button>
<a-button @click="submitForm" type="primary" :loading="submitting">提交</a-button>
</a-space>
</div>
</a-spin>
<AuditInnerDetailDrawer ref="auditInnerDetailDrawerRef" :visible="showDetailDrawer" :basis="selectedBasis" @close="handleDrawerClose" />
</div>
......@@ -125,29 +137,12 @@
const initFormData = async () => {
try {
loading.value = true;
//const { cctoken, WorkID } = getUrlParams();
//console.log('Token:', cctoken, 'WorkID:', WorkID);
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;
console.log('tid:', tid);
//setAuthCache(TOKEN_KEY, token);
//console.log('tid:', tid, 'token:', token);
const queryByIdUrl = '/plan.main/stPlanMan/queryById';
const data = await defHttp.get({
url: queryByIdUrl,
......@@ -169,14 +164,216 @@
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(() => {
initFormData();
});
</script>
<style scoped>
.ant-tag {
margin-right: 8px;
<style scoped lang="less">
/* ==================== 柔和中性风格 - CSS 变量 ==================== */
.plan-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;
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>
......@@ -3,37 +3,157 @@
v-bind="$attrs"
@register="registerModal"
destroyOnClose
title="发起计划"
:width="1000"
@ok="handleSubmit"
@cancel="handleCance"
:centered="true"
:title="'发起计划'"
>
<a-form :model="formModel" ref="formRef" style="overflow-x: hidden" :disabled="isDetail">
<a-row gutter="8">
<div class="modal-body">
<a-form ref="formRef" :model="formModel" class="styled-form" :disabled="isDetail">
<!-- 第一行:计划名称 + 类型 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="计划名称" name="projectName" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-input v-model:value="formModel.projectName" allow-clear :disabled="isDetail" />
<a-form-item
label="计划名称"
name="projectName"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
:rules="[{ required: true, message: '请输入计划名称' }]"
>
<a-input v-model:value="formModel.projectName" allow-clear placeholder="请输入计划名称" :disabled="isDetail" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="类型" name="projectType" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<JCategorySelect pcode="B09" :value="formModel.projectType" :disabled="isDetail" @change="changeJCategory" />
<a-form-item
label="计划类型"
name="projectType"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
:rules="[{ required: true, message: '请选择计划类型' }]"
>
<JCategorySelect pcode="B09" :value="formModel.projectType" :disabled="isDetail" @change="changeJCategory" placeholder="请选择类型" />
</a-form-item>
</a-col>
</a-row>
<!-- 第二行:执行部门 + 负责人 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="计划开始日期" name="planStartDate" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-date-picker v-model:value="formModel.planStartDate" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isDetail" />
<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="planEndDate" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-date-picker v-model:value="formModel.planEndDate" value-format="YYYY-MM-DD" style="width: 100%" :disabled="isDetail" />
<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-col :span="24">
<a-form-item label="依据" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<div>
</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"
placeholder="选择开始日期"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划结束日期" name="planEndDate" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-date-picker
v-model:value="formModel.planEndDate"
value-format="YYYY-MM-DD"
style="width: 100%"
:disabled="isDetail"
placeholder="选择结束日期"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 第四行:优先级 + 执行规则 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="优先级" name="priority" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-select
v-model:value="formModel.priority"
placeholder="请选择优先级"
:disabled="isDetail"
:options="[
{ label: '高', value: '1' },
{ label: '中', value: '2' },
{ label: '低', value: '3' },
]"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="执行规则" name="exeRule" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<a-radio-group v-model:value="formModel.exeRule" :disabled="isDetail">
<a-radio-button :value="1">一次性</a-radio-button>
<a-radio-button :value="2">周期执行</a-radio-button>
<a-radio-button :value="3">事件触发</a-radio-button>
</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"
......@@ -41,45 +161,53 @@
@close="removeBasis(item.id)"
@click="viewBasisDetail(item)"
:disabled="isDetail"
class="basis-tag"
>
{{ item.name }}
</a-tag>
<a-button type="link" v-if="showSelectorBtn" @click="openBasisSelector" :disabled="isDetail">添加依据</a-button>
<a-button type="dashed" v-if="showSelectorBtn && !isDetail" @click="openBasisSelector" class="btn-add-basis"> + 添加依据 </a-button>
</div>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="要求" name="planRequest" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-textarea v-model:value="formModel.planRequest" rows="3" allow-clear :disabled="isDetail" />
</a-form-item>
</a-col>
<!-- 计划要求 -->
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="交付物" name="planDeliverable" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-input v-model:value="formModel.planDeliverable" allow-clear :disabled="isDetail" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="描述" name="projectDesc" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<a-textarea v-model:value="formModel.projectDesc" rows="3" allow-clear :disabled="isDetail" />
<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="exeRule" :label-col="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<vxe-radio-group v-model="formModel.exeRule" :options="exeRules" />
<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="{ style: { width: '120px' } }" :wrapperCol="{ span: 16 }">
<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-col :span="0">
<a-form-item name="id">
</a-row>
<!-- 相关附件 -->
<!-- 隐藏字段 -->
<a-form-item name="id" style="display: none">
<a-input v-model:value="formModel.id" type="hidden" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
<!-- 选择依据组件 -->
<BasicCtrlSelector ref="selectorRef" @click="handleSelectorClick" />
<!-- 依据详情抽屉 -->
......@@ -97,12 +225,13 @@
import { list } from '/@/views/newlib/components/api/AuditInnerCtrl.api';
import { listAll } from '/@/views/newlib/components/api/AuditInnerCtrlItem.api';
import { saveOrUpdate } from '../StPlanMan.api';
import AuditInnerDetailDrawer from '/@/views/newlib/components/modal/AuditInnerDetailDrawer.vue';
import BasicCtrlSelector from '/@/components/BasicCtrlSelector.vue';
defineProps<{
showSelectorBtn?: boolean;
}>();
const AuditInnerDetailDrawerRef = ref();
const showDetailDrawer = ref(false);
const selectedBasis = ref<any>(null);
......@@ -116,6 +245,7 @@
AuditInnerDetailDrawerRef.value?.open(data);
showDetailDrawer.value = true;
};
// Emits声明
const emit = defineEmits(['register', 'success', 'selector-click']);
const isUpdate = ref(true);
......@@ -123,16 +253,20 @@
const visible = ref(false);
const formRef = ref();
const exeRules = ref([
{ value: 1, label: '每发生' },
{ value: 2, label: '周期性' },
{ value: 3, label: '一次性' },
]);
// const exeRules = ref([
// { value: 1, label: '事件触发' },
// { value: 2, label: '周期执行' },
// { value: 3, label: '一次性执行' },
// ]);
const formModel = reactive({
projectName: '',
projectType: '',
execDepCode: '',
execDepName: '',
headId: '',
headName: '',
priority: '2',
planRequest: '',
planDeliverable: '',
planStartDate: '',
......@@ -142,13 +276,24 @@
projectDesc: '',
fileUploadPath: '',
planBasis: '',
exeRule:1,
exeRule: 1,
triggerEventName: '',
exePeriod: undefined,
firstExecDate: '',
completionRate: 0,
statusName: '',
id: '',
});
const resetForm = () => {
Object.assign(formModel, {
projectName: '',
projectType: '',
execDepCode: '',
execDepName: '',
headId: '',
headName: '',
priority: '2',
planRequest: '',
planDeliverable: '',
planStartDate: '',
......@@ -158,10 +303,16 @@
projectDesc: '',
fileUploadPath: '',
planBasis: '',
exeRule:1,
exeRule: 1,
triggerEventName: '',
exePeriod: undefined,
firstExecDate: '',
completionRate: 0,
statusName: '',
id: '',
});
};
//表单赋值
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
setModalProps({ confirmLoading: false, showCancelBtn: !!data?.showFooter, showOkBtn: !!data?.showFooter });
......@@ -184,10 +335,6 @@
const handleSubmit = async () => {
try {
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) {
valids['planBasis'] = JSON.stringify(formModel.basisList);
......@@ -205,7 +352,7 @@
setModalProps({ confirmLoading: false });
}
};
// const basisSelectMode = ref<'flat' | 'nested'>('flat');
const selectorRef = ref();
const basisSelectorConfig = {
title: '选择依据',
......@@ -222,7 +369,6 @@
'a',
{
onClick: async () => {
// 加载二级数据并更新组件中的表格
const detailData = await listAll({ checkid: record.id });
selectorRef.value.setDetail({
title: '详细内容',
......@@ -278,22 +424,180 @@
const changeJCategory = (val, obj) => {
formModel['projectType'] = val;
};
const formatTp = (row) => {
return row.tp == 1 ? '工具要求' : row.tp == 2 ? '记录要求' : '制度要求';
};
const formatComeFrom = (row) => {
return row.comeFrom == 1 ? '自建' : row.tp == 2 ? '合规库' : '内控制度';
};
const formatExeRules = (row) => {
return row.comeFrom == 1 ? '每发生' : row.tp == 2 ? '周期性' : '一次性';
return row.comeFrom == 1 ? '一次性' : row.tp == 2 ? '周期执行' : '事件触发';
};
</script>
<style scoped>
.ant-tag {
<style scoped lang="less">
/* ==================== 弹窗主体 ==================== */
.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;
margin-right: 4px;
margin-bottom: 4px;
margin: 0;
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>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论