提交 5a8e52f3 authored 作者: liuluyu's avatar liuluyu

计划管理流转记录更新

上级 ae0a6c59
<template> <template>
<a-drawer <a-drawer
:title="drawerTitle" :title="drawerTitle"
:visible="visible" :visible="visibleRef"
:width="drawerWidth" :width="drawerWidth"
:closable="true" :closable="true"
:mask-closable="maskClosable" :mask-closable="maskClosable"
...@@ -11,11 +11,7 @@ ...@@ -11,11 +11,7 @@
<!-- 选项卡模式 --> <!-- 选项卡模式 -->
<a-tabs v-model:activeKey="activeTabKey" type="card" class="form-tabs"> <a-tabs v-model:activeKey="activeTabKey" type="card" class="form-tabs">
<!-- 只读选项卡:索引小于 currentNodeIndex 的节点 --> <!-- 只读选项卡:索引小于 currentNodeIndex 的节点 -->
<a-tab-pane <a-tab-pane v-for="node in readonlyNodes" :key="node.id" :tab="node.name">
v-for="node in readonlyNodes"
:key="node.id"
:tab="node.name"
>
<template #tab> <template #tab>
<span> <span>
{{ node.name }} {{ node.name }}
...@@ -34,11 +30,7 @@ ...@@ -34,11 +30,7 @@
</a-tab-pane> </a-tab-pane>
<!-- 可编辑选项卡:索引等于 currentNodeIndex 的节点 --> <!-- 可编辑选项卡:索引等于 currentNodeIndex 的节点 -->
<a-tab-pane <a-tab-pane v-if="editableNode" :key="editableNode.id" :tab="editableNode.name">
v-if="editableNode"
:key="editableNode.id"
:tab="editableNode.name"
>
<template #tab> <template #tab>
<span> <span>
{{ editableNode.name }} {{ editableNode.name }}
...@@ -64,579 +56,666 @@ ...@@ -64,579 +56,666 @@
<a-empty description="未找到有效表单节点" /> <a-empty description="未找到有效表单节点" />
</div> </div>
<template #footer> <!-- <template #footer>
<div class="drawer-footer"> <div class="drawer-footer">
<a-button @click="handleClose">取消</a-button> <a-button @click="handleClose">取消123</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit"> <a-button type="primary" :loading="submitLoading" @click="handleSubmit"> 提交 </a-button>
提交
</a-button>
</div> </div>
</template> </template> -->
</a-drawer> </a-drawer>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, onMounted, defineAsyncComponent, h, watch, ComponentPublicInstance } from 'vue' import { ref, computed, onMounted, defineAsyncComponent, h, watch, ComponentPublicInstance, getCurrentInstance } from 'vue';
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue';
import { useModalInner } from '/@/components/Modal';
interface WorkflowNode {
id: string interface WorkflowNode {
name: string id: string;
formUrl?: string name: string;
formListUrl?: string formUrl?: string;
procDefId?: string formListUrl?: string;
[key: string]: any procDefId?: string;
} [key: string]: any;
}
// 表单组件实例类型
interface FormComponentInstance extends ComponentPublicInstance { // 表单组件实例类型
validate?: () => Promise<any> interface FormComponentInstance extends ComponentPublicInstance {
getFormData?: () => any validate?: () => Promise<any>;
submitForm?: () => Promise<any> // 添加 submitForm 方法类型 getFormData?: () => any;
formData?: any submitForm?: () => Promise<any>; // 添加 submitForm 方法类型
[key: string]: any formData?: any;
} [key: string]: any;
}
const props = defineProps({
visible: { const props = defineProps({
type: Boolean, visible: {
default: false type: Boolean,
}, default: false,
title: { },
type: String, title: {
default: '表单处理' type: String,
}, default: '表单处理',
width: { },
type: [Number, String], width: {
default: 720 type: [Number, String],
}, default: 900,
maskClosable: { },
type: Boolean, maskClosable: {
default: false type: Boolean,
}, default: false,
// 当前节点索引(从0开始),这个索引对应的节点是可编辑的 },
currentNodeIndex: { // 当前节点索引(从0开始),这个索引对应的节点是可编辑的
type: Number, currentNodeIndex: {
required: true, type: Number,
default: 2 required: true,
}, default: 2,
workflowNodes: { },
type: Array as () => WorkflowNode[], workflowNodes: {
required: true, type: Array as () => WorkflowNode[],
default: () => [] required: true,
}, default: () => [],
externalFormData: { },
type: Object as () => Record<string, any>, externalFormData: {
default: () => ({}) type: Object as () => Record<string, any>,
}, default: () => ({}),
procDefId: { },
type: String, procDefId: {
default: '' type: String,
} default: '',
}) },
});
const emit = defineEmits(['update:visible', 'submit', 'close', 'form-data-update'])
// 组件缓存
const componentCache = new Map()
const modules = import.meta.glob('@/views/**/*.vue')
// 状态
const loading = ref(false)
const submitLoading = ref(false)
// 使用数组来存储表单组件实例
const editableFormRefs = ref<FormComponentInstance[]>([])
const currentFormData = ref<any>({})
const formDataMap = ref<Record<string, any>>({})
const activeTabKey = ref<string>('')
// 计算属性
const drawerTitle = computed(() => props.title)
const drawerWidth = computed(() => props.width)
// 只读节点:索引小于 currentNodeIndex 的节点
const readonlyNodes = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) {
return []
}
const idx = props.currentNodeIndex
console.log('只读节点 - 当前索引:', idx)
console.log('只读节点 - 所有节点:', props.workflowNodes.map((n, i) => `${i}:${n.name}`))
if (idx <= 0) return []
const nodes = props.workflowNodes.slice(0, idx)
console.log('只读节点:', nodes.map(n => n.name))
return nodes
})
// 可编辑节点:索引等于 currentNodeIndex 的节点
const editableNode = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) {
return null
}
const idx = props.currentNodeIndex
if (idx < 0 || idx >= props.workflowNodes.length) {
console.warn('可编辑节点索引无效:', idx)
return null
}
const node = props.workflowNodes[idx]
console.log('可编辑节点:', node?.name, '索引:', idx)
return node
})
// 设置表单组件 ref 的方法
function setEditableFormRef(el: any) {
if (el) {
// 清除旧的引用,只保留当前激活的可编辑表单
editableFormRefs.value = [el]
console.log('表单组件已挂载,组件方法:', Object.keys(el))
console.log('是否有 submitForm 方法:', typeof el.submitForm === 'function')
}
}
// 获取当前可编辑的表单组件实例
function getCurrentEditableForm(): FormComponentInstance | null {
return editableFormRefs.value[0] || null
}
// 获取表单数据
function getFormData(nodeId: string): any {
const data = formDataMap.value[nodeId] || {}
console.log('获取表单数据 - 节点:', nodeId, '数据:', data)
return data
}
// 获取或加载组件
function getComponent(url: string) {
if (!url) {
console.warn('URL为空,返回空组件')
return createEmptyComponent()
}
if (componentCache.has(url)) { const emit = defineEmits(['update:visible', 'submit', 'close', 'form-data-update', 'register']);
return componentCache.get(url)
// expose minimal modal methods so useModal can register this drawer
const instance = getCurrentInstance();
const modalMethods = {
setModalProps: (props: Record<string, any>) => {
if (Reflect.has(props, 'open')) {
visibleRef.value = !!props.open;
emit('update:visible', !!props.open);
}
if (Reflect.has(props, 'visible')) {
visibleRef.value = !!props.visible;
emit('update:visible', !!props.visible);
}
if (Reflect.has(props, 'loading')) {
submitLoading.value = !!props.loading;
}
},
// allow parent to read current form data programmatically
getFormData: async () => {
try {
const data = await getFormDataFromComponent();
return data;
} catch (e) {
console.error('getFormData error:', e);
return null;
}
},
// will be set by useModal.register
emitVisible: undefined,
redoModalHeight: () => {},
};
// internal editable inputs provided via openModal data
const innerWorkflowNodes = ref<any[]>(props.workflowNodes || []);
const innerCurrentNodeIndex = ref<number>(props.currentNodeIndex || 0);
const innerExternalFormData = ref<Record<string, any>>(props.externalFormData || {});
const innerTitle = ref<string>(props.title || '表单处理');
const innerProcDefId = ref<string>(props.procDefId || '');
// useModalInner receives data written by useModal.openModal
const [registerInner, { setModalProps: innerSetModalProps }] = useModalInner(async (data: any) => {
if (!data) return;
if (data.workflowNodes) innerWorkflowNodes.value = data.workflowNodes;
if (data.currentNodeIndex !== undefined) innerCurrentNodeIndex.value = Number(data.currentNodeIndex);
if (data.externalFormData) innerExternalFormData.value = data.externalFormData;
if (data.procDefId) innerProcDefId.value = data.procDefId;
if (data.title) innerTitle.value = data.title;
// ensure drawer opens
visibleRef.value = true;
// reset form data for new nodes/data
resetFormData();
preloadComponents();
});
// 组件缓存
const componentCache = new Map();
const modules = import.meta.glob('@/views/**/*.vue');
// 状态
const loading = ref(false);
const submitLoading = ref(false);
// internal visible state so parent doesn't need v-model binding
const visibleRef = ref(false);
// 使用数组来存储表单组件实例
const editableFormRefs = ref<FormComponentInstance[]>([]);
const currentFormData = ref<any>({});
const formDataMap = ref<Record<string, any>>({});
const activeTabKey = ref<string>('');
// 计算属性
const drawerTitle = computed(() => innerTitle.value || props.title);
const drawerWidth = computed(() => props.width);
// 只读节点:索引小于 currentNodeIndex 的节点
const readonlyNodes = computed(() => {
if (!innerWorkflowNodes.value || innerWorkflowNodes.value.length === 0) {
return [];
}
const idx = innerCurrentNodeIndex.value;
console.log('只读节点 - 当前索引:', idx);
console.log(
'只读节点 - 所有节点:',
innerWorkflowNodes.value.map((n, i) => `${i}:${n.name}`)
);
if (idx <= 0) return [];
const nodes = innerWorkflowNodes.value.slice(0, idx);
console.log(
'只读节点:',
nodes.map((n: any) => n.name)
);
return nodes;
});
// 可编辑节点:索引等于 currentNodeIndex 的节点
const editableNode = computed(() => {
if (!innerWorkflowNodes.value || innerWorkflowNodes.value.length === 0) {
return null;
}
const idx = innerCurrentNodeIndex.value;
if (idx < 0 || idx >= innerWorkflowNodes.value.length) {
console.warn('可编辑节点索引无效:', idx);
return null;
}
const node = innerWorkflowNodes.value[idx];
console.log('可编辑节点:', node?.name, '索引:', idx);
return node;
});
// 设置表单组件 ref 的方法
function setEditableFormRef(el: any) {
if (el) {
// 清除旧的引用,只保留当前激活的可编辑表单
editableFormRefs.value = [el];
console.log('表单组件已挂载,组件方法:', Object.keys(el));
console.log('是否有 submitForm 方法:', typeof el.submitForm === 'function');
}
} }
let componentPath = '' // 获取当前可编辑的表单组件实例
if (url.includes('/views')) { function getCurrentEditableForm(): FormComponentInstance | null {
componentPath = `/src${url}` return editableFormRefs.value[0] || null;
} else {
componentPath = `/src/views${url}`
} }
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) { // 获取表单数据
componentPath += '.vue' function getFormData(nodeId: string): any {
const data = formDataMap.value[nodeId] || {};
console.log('获取表单数据 - 节点:', nodeId, '数据:', data);
return data;
} }
console.log('加载组件路径:', componentPath) // 获取或加载组件
function getComponent(url: string) {
if (!url) {
console.warn('URL为空,返回空组件');
return createEmptyComponent();
}
const loader = modules[componentPath] if (componentCache.has(url)) {
return componentCache.get(url);
}
if (!loader) { let componentPath = '';
console.error('未找到组件:', componentPath) if (url.includes('/views')) {
const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`) componentPath = `/src${url}`;
componentCache.set(url, ErrorComponent) } else {
return ErrorComponent componentPath = `/src/views${url}`;
} }
const AsyncComponent = defineAsyncComponent({ if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
loader: () => loader() as Promise<{ default: any }>, componentPath += '.vue';
loadingComponent: { }
render: () => h('div', { style: 'text-align: center; padding: 20px;' }, '加载中...')
},
errorComponent: {
render: () => h('div', { style: 'color: red; padding: 20px;' }, '组件加载失败')
},
delay: 200,
timeout: 3000
})
componentCache.set(url, AsyncComponent) console.log('加载组件路径:', componentPath);
return AsyncComponent
}
function createEmptyComponent() { const loader = modules[componentPath];
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
}
}
function createErrorComponent(msg: string) { if (!loader) {
return { console.error('未找到组件:', componentPath);
render: () => h('div', { style: 'color: red; padding: 20px;' }, msg) const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`);
} componentCache.set(url, ErrorComponent);
} return ErrorComponent;
}
// 处理表单数据更新
function handleFormDataUpdate(data: any) {
currentFormData.value = { ...currentFormData.value, ...data }
emit('form-data-update', currentFormData.value)
}
/**
* 从表单组件获取数据
* 优先调用组件的 getFormData 方法,如果没有则返回 formData 属性或 currentFormData
*/
async function getFormDataFromComponent(): Promise<any> {
const formComponent = getCurrentEditableForm()
if (!formComponent) {
console.warn('未找到表单组件实例')
return currentFormData.value
}
console.log('当前表单组件实例:', formComponent) const AsyncComponent = defineAsyncComponent({
console.log('组件方法列表:', Object.keys(formComponent)) loader: () => loader() as Promise<{ default: any }>,
loadingComponent: {
render: () => h('div', { style: 'text-align: center; padding: 20px;' }, '加载中...'),
},
errorComponent: {
render: () => h('div', { style: 'color: red; padding: 20px;' }, '组件加载失败'),
},
delay: 200,
timeout: 3000,
});
// 方式1:调用组件的 getFormData 方法 componentCache.set(url, AsyncComponent);
if (typeof formComponent.getFormData === 'function') { return AsyncComponent;
try {
const data = await formComponent.getFormData()
console.log('通过 getFormData 方法获取的数据:', data)
return data
} catch (error) {
console.error('调用 getFormData 失败:', error)
}
} }
// 方式2:获取组件的 formData 属性 function createEmptyComponent() {
if (formComponent.formData !== undefined) { return {
console.log('通过 formData 属性获取的数据:', formComponent.formData) render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单'),
return formComponent.formData };
} }
// 方式3:如果组件有内部表单数据,尝试获取 function createErrorComponent(msg: string) {
if (formComponent.getValues && typeof formComponent.getValues === 'function') { return {
try { render: () => h('div', { style: 'color: red; padding: 20px;' }, msg),
const data = await formComponent.getValues() };
console.log('通过 getValues 方法获取的数据:', data)
return data
} catch (error) {
console.error('调用 getValues 失败:', error)
}
} }
// 方式4:返回本地维护的 currentFormData // 处理表单数据更新
console.log('使用本地维护的 currentFormData:', currentFormData.value) function handleFormDataUpdate(data: any) {
return currentFormData.value currentFormData.value = { ...currentFormData.value, ...data };
} emit('form-data-update', currentFormData.value);
/**
* 验证表单数据
*/
async function validateForm(): Promise<boolean> {
const formComponent = getCurrentEditableForm()
if (!formComponent) {
return true
} }
// 方式1:调用组件的 validate 方法 /**
if (typeof formComponent.validate === 'function') { * 从表单组件获取数据
try { * 优先调用组件的 getFormData 方法,如果没有则返回 formData 属性或 currentFormData
await formComponent.validate() */
return true async function getFormDataFromComponent(): Promise<any> {
} catch (error) { const formComponent = getCurrentEditableForm();
console.error('表单验证失败:', error)
return false if (!formComponent) {
console.warn('未找到表单组件实例');
return currentFormData.value;
} }
}
// 方式2:如果组件有 vee-validate 或其他验证库的实例 console.log('当前表单组件实例:', formComponent);
if (formComponent.v$ && typeof formComponent.v$.$validate === 'function') { console.log('组件方法列表:', Object.keys(formComponent));
try {
const isValid = await formComponent.v$.$validate() // 方式1:调用组件的 getFormData 方法
return isValid if (typeof formComponent.getFormData === 'function') {
} catch (error) { try {
console.error('验证失败:', error) const data = await formComponent.getFormData();
return false console.log('通过 getFormData 方法获取的数据:', data);
return data;
} catch (error) {
console.error('调用 getFormData 失败:', error);
}
} }
}
// 如果没有验证方法,默认通过 // 方式2:获取组件的 formData 属性
return true if (formComponent.formData !== undefined) {
} console.log('通过 formData 属性获取的数据:', formComponent.formData);
return formComponent.formData;
}
// 提交处理 // 方式3:如果组件有内部表单数据,尝试获取
async function handleSubmit() { if (formComponent.getValues && typeof formComponent.getValues === 'function') {
if (!editableNode.value) { try {
message.warning('没有可编辑的表单') const data = await formComponent.getValues();
return console.log('通过 getValues 方法获取的数据:', data);
return data;
} catch (error) {
console.error('调用 getValues 失败:', error);
}
}
// 方式4:返回本地维护的 currentFormData
console.log('使用本地维护的 currentFormData:', currentFormData.value);
return currentFormData.value;
} }
submitLoading.value = true /**
* 验证表单数据
try { */
const formComponent = getCurrentEditableForm() async function validateForm(): Promise<boolean> {
const formComponent = getCurrentEditableForm();
// 🔥 优先调用子组件的 submitForm 方法
if (formComponent && typeof formComponent.submitForm === 'function') { if (!formComponent) {
console.log('调用子组件的 submitForm 方法') return true;
}
// 调用子组件的 submitForm 方法,并等待返回结果
const result = await formComponent.submitForm() // 方式1:调用组件的 validate 方法
if (typeof formComponent.validate === 'function') {
// 如果子组件的 submitForm 返回了数据,则使用返回的数据 try {
if (result !== undefined) { await formComponent.validate();
console.log('submitForm 返回的数据:', result) return true;
} catch (error) {
// 触发提交事件 console.error('表单验证失败:', error);
emit('submit', { return false;
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: result,
procDefId: props.procDefId,
formComponent: formComponent
})
message.success('提交成功')
handleClose()
return
} }
} }
// 如果没有 submitForm 方法或 submitForm 没有返回数据,则使用原来的逻辑 // 方式2:如果组件有 vee-validate 或其他验证库的实例
console.log('使用默认提交逻辑') if (formComponent.v$ && typeof formComponent.v$.$validate === 'function') {
try {
// 1. 先进行表单验证 const isValid = await formComponent.v$.$validate();
const isValid = await validateForm() return isValid;
if (!isValid) { } catch (error) {
message.error('请完善表单信息') console.error('验证失败:', error);
return return false;
}
} }
// 2. 获取表单数据 // 如果没有验证方法,默认通过
const submitData = await getFormDataFromComponent() return true;
console.log('最终提交数据:', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId
})
// 3. 触发提交事件
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId,
formComponent: formComponent
})
message.success('提交成功')
// 提交成功后关闭抽屉
handleClose()
} catch (error: any) {
console.error('提交失败:', error)
message.error(error?.message || '提交失败,请重试')
} finally {
submitLoading.value = false
} }
}
// 提交处理
// 关闭抽屉 async function handleSubmit() {
function handleClose() { if (!editableNode.value) {
emit('update:visible', false) message.warning('没有可编辑的表单');
emit('close') return;
// 关闭后清空表单引用 }
editableFormRefs.value = []
} submitLoading.value = true;
// 重置数据 try {
function resetFormData() { const formComponent = getCurrentEditableForm();
currentFormData.value = {}
// 🔥 优先调用子组件的 submitForm 方法
const newFormDataMap: Record<string, any> = {} if (formComponent && typeof formComponent.submitForm === 'function') {
readonlyNodes.value.forEach(node => { console.log('调用子组件的 submitForm 方法');
newFormDataMap[node.id] = props.externalFormData[node.id] || {}
}) // 调用子组件的 submitForm 方法,并等待返回结果
formDataMap.value = newFormDataMap const result = await formComponent.submitForm();
if (editableNode.value && props.externalFormData[editableNode.value.id]) { // 如果子组件的 submitForm 返回了数据,则使用返回的数据
currentFormData.value = { ...props.externalFormData[editableNode.value.id] } if (result !== undefined) {
console.log('submitForm 返回的数据:', result);
// 触发提交事件
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: result,
procDefId: props.procDefId,
formComponent: formComponent,
});
message.success('提交成功');
handleClose();
return;
}
}
// 如果没有 submitForm 方法或 submitForm 没有返回数据,则使用原来的逻辑
console.log('使用默认提交逻辑');
// 1. 先进行表单验证
const isValid = await validateForm();
if (!isValid) {
message.error('请完善表单信息');
return;
}
// 2. 获取表单数据
const submitData = await getFormDataFromComponent();
console.log('最终提交数据:', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId,
});
// 3. 触发提交事件
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId,
formComponent: formComponent,
});
message.success('提交成功');
// 提交成功后关闭抽屉
handleClose();
} catch (error: any) {
console.error('提交失败:', error);
message.error(error?.message || '提交失败,请重试');
} finally {
submitLoading.value = false;
}
} }
// 设置默认激活的选项卡为可编辑的选项卡 // 关闭抽屉
if (editableNode.value) { function handleClose() {
activeTabKey.value = editableNode.value.id visibleRef.value = false;
console.log('设置激活选项卡为可编辑节点:', editableNode.value.name) emit('update:visible', false);
} else if (readonlyNodes.value.length > 0) { emit('close');
activeTabKey.value = readonlyNodes.value[0].id // 关闭后清空表单引用
console.log('设置激活选项卡为第一个只读节点:', readonlyNodes.value[0].name) editableFormRefs.value = [];
} }
}
// 重置数据
// 预加载组件 function resetFormData() {
function preloadComponents() { currentFormData.value = {};
props.workflowNodes.forEach(node => {
const url = node.formUrl || node.formListUrl const newFormDataMap: Record<string, any> = {};
if (url) { readonlyNodes.value.forEach((node) => {
getComponent(url) newFormDataMap[node.id] = innerExternalFormData.value[node.id] || {};
});
formDataMap.value = newFormDataMap;
if (editableNode.value && innerExternalFormData.value[editableNode.value.id]) {
currentFormData.value = { ...innerExternalFormData.value[editableNode.value.id] };
}
// 设置默认激活的选项卡为可编辑的选项卡
if (editableNode.value) {
activeTabKey.value = editableNode.value.id;
console.log('设置激活选项卡为可编辑节点:', editableNode.value.name);
} else if (readonlyNodes.value.length > 0) {
activeTabKey.value = readonlyNodes.value[0].id;
console.log('设置激活选项卡为第一个只读节点:', readonlyNodes.value[0].name);
} }
})
}
// 监听抽屉打开
watch(() => props.visible, (newVal) => {
if (newVal) {
console.log('抽屉打开,currentNodeIndex:', props.currentNodeIndex)
console.log('所有节点:', props.workflowNodes)
resetFormData()
preloadComponents()
// 清空之前的表单引用
editableFormRefs.value = []
} }
}, { immediate: true })
// 预加载组件
// 监听外部数据变化 function preloadComponents() {
watch(() => props.externalFormData, (newData) => { innerWorkflowNodes.value.forEach((node) => {
if (newData && Object.keys(newData).length > 0) { const url = node.formUrl || node.formListUrl;
console.log('外部数据变化:', newData) if (url) {
const newFormDataMap = { ...formDataMap.value } getComponent(url);
readonlyNodes.value.forEach(node => {
if (newData[node.id]) {
newFormDataMap[node.id] = newData[node.id]
} }
}) });
formDataMap.value = newFormDataMap
if (editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id]
}
} }
}, { deep: true })
// 监听外部 visible prop 同步到内部 visibleRef
onMounted(() => { watch(
console.log('组件挂载,workflowNodes:', props.workflowNodes) () => props.visible,
console.log('currentNodeIndex:', props.currentNodeIndex) (newVal) => {
resetFormData() visibleRef.value = !!newVal;
preloadComponents() },
}) { immediate: true }
);
defineExpose({
resetFormData, // 监听内部抽屉打开
getFormData: () => currentFormData.value, watch(
getCurrentFormData: getFormDataFromComponent, () => visibleRef.value,
validate: validateForm, (newVal) => {
submit: handleSubmit if (newVal) {
}) console.log('抽屉打开,currentNodeIndex:', innerCurrentNodeIndex.value);
console.log('所有节点:', innerWorkflowNodes.value);
resetFormData();
preloadComponents();
// 清空之前的表单引用
editableFormRefs.value = [];
}
},
{ immediate: true }
);
// 监听外部数据变化
watch(
() => innerExternalFormData.value,
(newData) => {
if (newData && Object.keys(newData).length > 0) {
console.log('外部数据变化:', newData);
const newFormDataMap = { ...formDataMap.value };
readonlyNodes.value.forEach((node) => {
if (newData[node.id]) {
newFormDataMap[node.id] = newData[node.id];
}
});
formDataMap.value = newFormDataMap;
if (editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id];
}
}
},
{ deep: true }
);
onMounted(() => {
console.log('组件挂载,workflowNodes:', props.workflowNodes);
console.log('currentNodeIndex:', props.currentNodeIndex);
resetFormData();
preloadComponents();
// register modal methods for useModal() via useModalInner.register
if (instance) {
try {
registerInner(modalMethods, instance.uid);
} catch (e) {
// fallback to emit if registerInner not available
emit('register', modalMethods, instance.uid);
}
}
});
defineExpose({
resetFormData,
getFormData: () => currentFormData.value,
getCurrentFormData: getFormDataFromComponent,
validate: validateForm,
submit: handleSubmit,
});
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.workflow-form-drawer { .workflow-form-drawer {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow-y: auto; overflow-y: auto;
.form-tabs { .form-tabs {
:deep(.ant-tabs-nav) { :deep(.ant-tabs-nav) {
margin-bottom: 0; margin-bottom: 0;
background-color: #fafbfc; background-color: #fafbfc;
padding: 0 16px; padding: 0 16px;
border-bottom: 1px solid #e8eef2; border-bottom: 1px solid #e8eef2;
} }
:deep(.ant-tabs-tab) { :deep(.ant-tabs-tab) {
padding: 12px 20px; padding: 12px 20px;
font-weight: 500; font-weight: 500;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
color: #1890ff; color: #1890ff;
}
} }
}
:deep(.ant-tabs-tab-active) { :deep(.ant-tabs-tab-active) {
.ant-tabs-tab-btn { .ant-tabs-tab-btn {
color: #1890ff; color: #1890ff;
}
} }
}
:deep(.ant-tabs-ink-bar) { :deep(.ant-tabs-ink-bar) {
background: #1890ff; background: #1890ff;
}
} }
}
.tab-content { .tab-content {
padding: 24px; padding: 24px;
min-height: 400px; min-height: 400px;
background-color: #fff; background-color: #fff;
&.readonly-content { &.readonly-content {
background-color: #f5f5f5; background-color: #f5f5f5;
:deep(input), :deep(input),
:deep(textarea), :deep(textarea),
:deep(.ant-input), :deep(.ant-input),
:deep(.ant-select-selector), :deep(.ant-select-selector),
:deep(.ant-picker) { :deep(.ant-picker) {
background-color: #f5f5f5 !important; background-color: #f5f5f5 !important;
cursor: not-allowed !important; cursor: not-allowed !important;
color: #666 !important; color: #666 !important;
}
:deep(.ant-input-affix-wrapper) {
background-color: #f5f5f5 !important;
}
} }
:deep(.ant-input-affix-wrapper) { &.editable-content {
background-color: #f5f5f5 !important; background-color: #fff;
} }
} }
&.editable-content { .tab-tag {
background-color: #fff; margin-left: 8px;
font-size: 12px;
transform: scale(0.9);
display: inline-block;
} }
}
.tab-tag { .empty-state {
margin-left: 8px; padding: 60px 0;
font-size: 12px; text-align: center;
transform: scale(0.9); }
display: inline-block;
}
.empty-state { &::-webkit-scrollbar {
padding: 60px 0; width: 6px;
text-align: center; }
}
&::-webkit-scrollbar { &::-webkit-scrollbar-track {
width: 6px; background: #f1f1f1;
} border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&::-webkit-scrollbar-track { &:hover {
background: #f1f1f1; background: #a8a8a8;
border-radius: 3px; }
}
} }
&::-webkit-scrollbar-thumb { .drawer-footer {
background: #c1c1c1; text-align: right;
border-radius: 3px;
&:hover { :deep(.ant-btn) {
background: #a8a8a8; margin-left: 8px;
&:first-child {
margin-left: 0;
}
} }
} }
}
:deep(.ant-drawer-body) {
.drawer-footer { padding: 16px;
text-align: right; background-color: #f5f7fa;
}
:deep(.ant-btn) {
margin-left: 8px; :deep(.ant-drawer-footer) {
padding: 12px 16px;
&:first-child { border-top: 1px solid #e8eef2;
margin-left: 0;
}
} }
} </style>
:deep(.ant-drawer-body) {
padding: 16px;
background-color: #f5f7fa;
}
:deep(.ant-drawer-footer) {
padding: 12px 16px;
border-top: 1px solid #e8eef2;
}
</style>
\ No newline at end of file
...@@ -2,21 +2,21 @@ ...@@ -2,21 +2,21 @@
<div class="app-container"> <div class="app-container">
<a-card class="box-card"> <a-card class="box-card">
<template #title> <template #title>
<span><FileTextOutlined /> 待办任务</span> <!-- <span><FileTextOutlined /> 待办任务</span> -->
<div style="float: right"> <div style="float: right">
<a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag> <a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag>
<a-tag>任务节点:{{ taskName }}</a-tag> <a-tag>任务节点:{{ taskName }}</a-tag>
</div> </div>
</template> </template>
<a-tabs v-model:activeKey="activeName" @tabClick="handleClick"> <a-tabs v-model:activeKey="activeName" @tab-click="handleClick">
<!--表单信息--> <!--表单信息-->
<a-tab-pane key="1" tab="主表单信息"> <a-tab-pane key="1" tab="主表单信息">
<div v-show="formUrl" class="iframe-container"> <div v-show="formUrl" class="iframe-container">
<iFrame :src="formUrl" class="responsive-iframe"></iFrame> <iFrame :src="formUrl" class="responsive-iframe"></iFrame>
</div> </div>
<div v-show="!formUrl" style="margin:10px;width:100%"> <div v-show="!formUrl" style="margin: 10px; width: 100%">
<FlowInnerForm ref="refInnerForm"></FlowInnerForm> <FlowInnerForm ref="refInnerForm"></FlowInnerForm>
</div> </div>
</a-tab-pane> </a-tab-pane>
...@@ -24,37 +24,19 @@ ...@@ -24,37 +24,19 @@
<a-tab-pane key="2" tab="流转记录"> <a-tab-pane key="2" tab="流转记录">
<div class="block"> <div class="block">
<a-steps direction="vertical" size="small"> <a-steps direction="vertical" size="small">
<a-step <a-step v-for="(item, index) in flowRecordList" :key="index" :status="getStepStatus(item.finishTime)" :title="item.taskName">
v-for="(item, index) in flowRecordList"
:key="index"
:status="getStepStatus(item.finishTime)"
:title="item.taskName"
>
<template #description> <template #description>
<a-card :body-style="{ padding: '0px', marginTop: '0px' }"> <a-card :body-style="{ padding: '0px', marginTop: '0px' }">
<a-descriptions <a-descriptions class="margin-top" :column="1" size="small" bordered>
class="margin-top" <a-descriptions-item v-if="item.assigneeName" label="办理人">
:column="1"
size="small"
bordered
>
<a-descriptions-item
v-if="item.assigneeName"
label="办理人"
>
<template #label> <template #label>
<UserOutlined /> <UserOutlined />
办理人 办理人
</template> </template>
{{ item.assigneeName }} {{ item.assigneeName }}
<a-tag color="gray" size="small">{{ <a-tag color="gray" size="small">{{ item.deptName }}</a-tag>
item.deptName
}}</a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.candidate" label="候选办理">
v-if="item.candidate"
label="候选办理"
>
<template #label> <template #label>
<UserOutlined /> <UserOutlined />
候选办理 候选办理
...@@ -68,30 +50,21 @@ ...@@ -68,30 +50,21 @@
</template> </template>
{{ item.createTime }} {{ item.createTime }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.finishTime" label="处理时间">
v-if="item.finishTime"
label="处理时间"
>
<template #label> <template #label>
<CalendarOutlined /> <CalendarOutlined />
处理时间 处理时间
</template> </template>
{{ item.finishTime }} {{ item.finishTime }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.duration" label="耗时">
v-if="item.duration"
label="耗时"
>
<template #label> <template #label>
<ClockCircleOutlined /> <ClockCircleOutlined />
耗时 耗时
</template> </template>
{{ item.duration }} {{ item.duration }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.comment" label="处理意见">
v-if="item.comment"
label="处理意见"
>
<template #label> <template #label>
<FormOutlined /> <FormOutlined />
处理意见 处理意见
...@@ -115,182 +88,166 @@ ...@@ -115,182 +88,166 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive,nextTick } from 'vue' import { ref, reactive, nextTick } from 'vue';
import { import { FileTextOutlined, UserOutlined, CalendarOutlined, ClockCircleOutlined, FormOutlined } from '@ant-design/icons-vue';
FileTextOutlined, import { flowRecord } from '/@/components/Process/api/finished';
UserOutlined, import { flowXmlAndNode } from '/@/components/Process/api/definition';
CalendarOutlined, import { findFlowFormVal } from '/@/components/Process/api/todo';
ClockCircleOutlined,
FormOutlined, import { flowFormData } from '/@/components/Process/api/process';
} from '@ant-design/icons-vue' import BpmnViewer from '/@/components/Process/viewer/index.vue';
import { flowRecord } from "/@/components/Process/api/finished" import FlowInnerForm from '/@/views/flowable/task/components/FlowInnerForm.vue';
import { flowXmlAndNode } from "/@/components/Process/api/definition"
import { findFlowFormVal} from "/@/components/Process/api/todo" interface FlowRecordItem {
taskName: string;
import { flowFormData } from "/@/components/Process/api/process" finishTime: string | null;
import BpmnViewer from '/@/components/Process/viewer/index.vue' assigneeName: string;
import FlowInnerForm from '/@/views/flowable/task/components/FlowInnerForm.vue' deptName: string;
candidate: string;
createTime: string;
interface FlowRecordItem { duration: string;
taskName: string comment: {
finishTime: string | null comment: string;
assigneeName: string };
deptName: string
candidate: string
createTime: string
duration: string
comment: {
comment: string
} }
}
interface TaskForm {
returnTaskShow: boolean
delegateTaskShow: boolean
defaultTaskShow: boolean
comment: string
procInsId: string
instanceId: string
deployId: string
taskId: string
procDefId: string
targetKey: string
variables: Record<string, any>
executionId?: string
}
const flowData = ref({})
const activeName = ref('1')
const formUrl = ref();
const refInnerForm = ref();
// 遮罩层
const loading = ref(true)
const flowRecordList = ref<FlowRecordItem[]>([])
const taskForm = reactive<TaskForm>({
returnTaskShow: false,
delegateTaskShow: false,
defaultTaskShow: true,
comment: '',
procInsId: '',
instanceId: '',
deployId: '',
taskId: '',
procDefId: '',
targetKey: '',
variables: {},
})
interface TaskForm {
returnTaskShow: boolean;
delegateTaskShow: boolean;
defaultTaskShow: boolean;
comment: string;
procInsId: string;
instanceId: string;
deployId: string;
taskId: string;
procDefId: string;
targetKey: string;
variables: Record<string, any>;
executionId?: string;
}
const flowData = ref({});
const taskName = ref<string | null>(null) // 任务节点 const activeName = ref('1');
const startUser = ref<string | null>(null) // 发起人信息 const formUrl = ref();
const refInnerForm = ref();
const handleClick = (key: string) => {
if (key === '3') { // 遮罩层
const loading = ref(true);
const flowRecordList = ref<FlowRecordItem[]>([]);
const taskForm = reactive<TaskForm>({
returnTaskShow: false,
delegateTaskShow: false,
defaultTaskShow: true,
comment: '',
procInsId: '',
instanceId: '',
deployId: '',
taskId: '',
procDefId: '',
targetKey: '',
variables: {},
});
const taskName = ref<string | null>(null); // 任务节点
const startUser = ref<string | null>(null); // 发起人信息
const handleClick = (key: string) => {
if (key === '3') {
flowXmlAndNode({ flowXmlAndNode({
procInsId: taskForm.procInsId, procInsId: taskForm.procInsId,
deployId: taskForm.deployId, deployId: taskForm.deployId,
}).then((res) => { }).then((res) => {
flowData.value = res flowData.value = res;
console.log("xml",JSON.stringify(res)) console.log('xml', JSON.stringify(res));
});
}
};
// 修改 setColor 方法为 getStepStatus
const getStepStatus = (finishTime: string | null) => {
if (finishTime) {
return 'finish'; // 已完成
} else {
return 'wait'; // 等待中
}
};
const setFlowRecordList = async (procInsId: string, deployId: string) => {
const params = { procInsId, deployId };
await flowRecord(params)
.then((res) => {
console.log('flowList', res);
flowRecordList.value = res.flowList;
}) })
} .catch(() => {
} //goBack()
});
// 修改 setColor 方法为 getStepStatus };
const getStepStatus = (finishTime: string | null) => {
if (finishTime) { const iniData = async (data) => {
return 'finish' // 已完成 taskName.value = data.taskName as string;
} else { startUser.value = data.startUserName as string;
return 'wait' // 等待中 taskForm.deployId = data.deployId as string;
} taskForm.taskId = data.taskId as string;
} taskForm.procInsId = data.procInsId as string;
taskForm.executionId = data.executionId as string;
const setFlowRecordList = async (procInsId: string, deployId: string) => { taskForm.instanceId = data.procInsId as string;
const params = { procInsId, deployId }
await flowRecord(params) await flowFormData({ deployId: taskForm.deployId }).then((resData) => {
.then((res) => {
console.log("flowList",res)
flowRecordList.value = res.flowList
})
.catch(() => {
//goBack()
})
}
const iniData = async (data) => {
taskName.value = data.taskName as string
startUser.value = data.startUserName as string
taskForm.deployId = data.deployId as string
taskForm.taskId = data.taskId as string
taskForm.procInsId = data.procInsId as string
taskForm.executionId = data.executionId as string
taskForm.instanceId = data.procInsId as string
await flowFormData({deployId: taskForm.deployId }).then(resData => {
nextTick(() => { nextTick(() => {
if(resData.formUrl) { if (resData.formUrl) {
formUrl.value = resData.formUrl formUrl.value = resData.formUrl;
} else { } else {
formUrl.value = "" formUrl.value = '';
refInnerForm.value.iniData(resData) refInnerForm.value.iniData(resData);
} }
}) });
}) });
if(taskForm.taskId) { if (taskForm.taskId) {
await findFlowFormVal({taskId: taskForm.taskId }).then((resValues) => { await findFlowFormVal({ taskId: taskForm.taskId }).then((resValues) => {
nextTick(() => { nextTick(() => {
refInnerForm.value.setFormData(resValues) refInnerForm.value.setFormData(resValues);
}) });
}) });
} }
setFlowRecordList(taskForm.procInsId, taskForm.deployId) setFlowRecordList(taskForm.procInsId, taskForm.deployId);
};
}
defineExpose({
iniData,
})
defineExpose({
iniData,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.clearfix:before,
.clearfix:after {
display: table;
content: '';
}
.clearfix:after {
clear: both;
}
.clearfix:before, .app-container {
.clearfix:after { height: calc(100vh - 150px);
display: table; margin: 0px;
content: ""; padding: 5px;
} }
.clearfix:after {
clear: both
}
.app-container {
height: calc(100vh - 150px);
margin: 0px;
padding: 5px;
}
.my-label { .my-label {
background: #E1F3D8; background: #e1f3d8;
} }
.iframe-container { .iframe-container {
height: calc(100vh - 200px); height: calc(100vh - 200px);
width: 100%; width: 100%;
} }
.responsive-iframe { .responsive-iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
</style> </style>
\ No newline at end of file
...@@ -2,21 +2,21 @@ ...@@ -2,21 +2,21 @@
<div class="app-container"> <div class="app-container">
<a-card class="box-card"> <a-card class="box-card">
<template #title> <template #title>
<span><FileTextOutlined /> 待办任务</span> <!-- <span><FileTextOutlined /> 待办任务</span> -->
<div style="float: right"> <div style="float: right">
<a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag> <a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag>
<a-tag>任务节点:{{ taskName }}</a-tag> <a-tag>任务节点:{{ taskName }}</a-tag>
</div> </div>
</template> </template>
<a-tabs v-model:activeKey="activeName" @tabClick="handleClick"> <a-tabs v-model:activeKey="activeName" @tab-click="handleClick">
<!--表单信息--> <!--表单信息-->
<a-tab-pane key="1" tab="主表单信息"> <a-tab-pane key="1" tab="主表单信息">
<div v-show="formUrl" class="iframe-container"> <div v-show="formUrl" class="iframe-container">
<iFrame :src="formUrl" class="responsive-iframe"></iFrame> <iFrame :src="formUrl" class="responsive-iframe"></iFrame>
</div> </div>
<div v-show="!formUrl" style="margin:10px;width:100%"> <div v-show="!formUrl" style="margin: 10px; width: 100%">
<FlowInnerForm ref="refInnerForm"></FlowInnerForm> <FlowInnerForm ref="refInnerForm"></FlowInnerForm>
</div> </div>
</a-tab-pane> </a-tab-pane>
...@@ -24,43 +24,25 @@ ...@@ -24,43 +24,25 @@
<a-tab-pane key="2" tab="流转记录"> <a-tab-pane key="2" tab="流转记录">
<div class="block"> <div class="block">
<a-steps direction="vertical" size="small"> <a-steps direction="vertical" size="small">
<a-step <a-step v-for="(item, index) in flowRecordList" :key="index" :status="getStepStatus(item.finishTime)">
v-for="(item, index) in flowRecordList"
:key="index"
:status="getStepStatus(item.finishTime)"
>
<template #title> <template #title>
<div style="margin: 5px"> <div style="margin: 5px">
<span style="margin-right: 50px;">节点名称:{{ item.taskName }} </span> <span style="margin-right: 50px">节点名称:{{ item.taskName }} </span>
<a-button type="link" @click="showNodeFormData(item)">查看表单</a-button> <a-button type="link" @click="showNodeFormData(item)">查看表单</a-button>
</div> </div>
</template> </template>
<template #description> <template #description>
<a-card :body-style="{ padding: '0px', marginTop: '0px' }"> <a-card :body-style="{ padding: '0px', marginTop: '0px' }">
<a-descriptions <a-descriptions class="margin-top" :column="1" size="small" bordered>
class="margin-top" <a-descriptions-item v-if="item.assigneeName" label="办理人">
:column="1"
size="small"
bordered
>
<a-descriptions-item
v-if="item.assigneeName"
label="办理人"
>
<template #label> <template #label>
<UserOutlined /> <UserOutlined />
办理人 办理人
</template> </template>
{{ item.assigneeName }} {{ item.assigneeName }}
<a-tag color="gray" size="small">{{ <a-tag color="gray" size="small">{{ item.deptName }}</a-tag>
item.deptName
}}</a-tag>
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.candidate" label="候选办理">
v-if="item.candidate"
label="候选办理"
>
<template #label> <template #label>
<UserOutlined /> <UserOutlined />
候选办理 候选办理
...@@ -74,30 +56,21 @@ ...@@ -74,30 +56,21 @@
</template> </template>
{{ item.createTime }} {{ item.createTime }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.finishTime" label="处理时间">
v-if="item.finishTime"
label="处理时间"
>
<template #label> <template #label>
<CalendarOutlined /> <CalendarOutlined />
处理时间 处理时间
</template> </template>
{{ item.finishTime }} {{ item.finishTime }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.duration" label="耗时">
v-if="item.duration"
label="耗时"
>
<template #label> <template #label>
<ClockCircleOutlined /> <ClockCircleOutlined />
耗时 耗时
</template> </template>
{{ item.duration }} {{ item.duration }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item <a-descriptions-item v-if="item.comment?.comment" label="处理意见">
v-if="item.comment?.comment"
label="处理意见"
>
<template #label> <template #label>
<FormOutlined /> <FormOutlined />
处理意见 处理意见
...@@ -117,200 +90,207 @@ ...@@ -117,200 +90,207 @@
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-card> </a-card>
<ShowFormModal @register="registerShowFormModal" /> <ShowFormModal @register="registerShowFormModal" @submit="handleShowFormSubmit" />
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive,nextTick } from 'vue' import { ref, reactive, nextTick } from 'vue';
import { import { FileTextOutlined, UserOutlined, CalendarOutlined, ClockCircleOutlined, FormOutlined } from '@ant-design/icons-vue';
FileTextOutlined, import { flowRecord } from '/@/components/Process/api/finished';
UserOutlined, import { flowXmlAndNode } from '/@/components/Process/api/definition';
CalendarOutlined, import { findFlowFormVal } from '/@/components/Process/api/todo';
ClockCircleOutlined, import { useModal } from '/@/components/Modal';
FormOutlined, import { flowFormData } from '/@/components/Process/api/process';
} from '@ant-design/icons-vue' import BpmnViewer from '/@/components/Process/viewer/index.vue';
import { flowRecord } from "/@/components/Process/api/finished" import FlowInnerForm from '/@/views/flowable/task/components/FlowInnerForm.vue';
import { flowXmlAndNode } from "/@/components/Process/api/definition" import ShowFormModal from '/@/views/flowable/task/components/ShowFormModal.vue';
import { findFlowFormVal} from "/@/components/Process/api/todo"
import { useModal } from '/@/components/Modal';
import { flowFormData } from "/@/components/Process/api/process"
import BpmnViewer from '/@/components/Process/viewer/index.vue'
import FlowInnerForm from '/@/views/flowable/task/components/FlowInnerForm.vue'
import ShowFormModal from '/@/views/flowable/task/components/ShowFormModal.vue'
interface FlowRecordItem {
interface FlowRecordItem { taskName: string;
taskName: string finishTime: string | null;
finishTime: string | null assigneeName: string;
assigneeName: string deptName: string;
deptName: string candidate: string;
candidate: string createTime: string;
createTime: string duration: string;
duration: string comment: {
comment: { comment: string;
comment: string };
} }
}
interface TaskForm {
returnTaskShow: boolean
delegateTaskShow: boolean
defaultTaskShow: boolean
comment: string
procInsId: string
instanceId: string
deployId: string
taskId: string
procDefId: string
targetKey: string
variables: Record<string, any>
executionId?: string
}
const flowData = ref({})
const activeName = ref('1')
const formUrl = ref();
const refInnerForm = ref();
const [registerShowFormModal, { openModal, setModalProps }] = useModal(); interface TaskForm {
returnTaskShow: boolean;
// 遮罩层 delegateTaskShow: boolean;
const loading = ref(true) defaultTaskShow: boolean;
const flowRecordList = ref<FlowRecordItem[]>([]) comment: string;
const taskForm = reactive<TaskForm>({ procInsId: string;
returnTaskShow: false, instanceId: string;
delegateTaskShow: false, deployId: string;
defaultTaskShow: true, taskId: string;
comment: '', procDefId: string;
procInsId: '', targetKey: string;
instanceId: '', variables: Record<string, any>;
deployId: '', executionId?: string;
taskId: '', }
procDefId: '',
targetKey: '',
variables: {},
})
const flowData = ref({});
const activeName = ref('1');
const formUrl = ref();
const refInnerForm = ref();
const [registerShowFormModal, showFormModalApi] = useModal();
const taskName = ref<string | null>(null) // 任务节点 // 遮罩层
const startUser = ref<string | null>(null) // 发起人信息 const loading = ref(true);
const flowRecordList = ref<FlowRecordItem[]>([]);
const taskForm = reactive<TaskForm>({
returnTaskShow: false,
delegateTaskShow: false,
defaultTaskShow: true,
comment: '',
procInsId: '',
instanceId: '',
deployId: '',
taskId: '',
procDefId: '',
targetKey: '',
variables: {},
});
const taskName = ref<string | null>(null); // 任务节点
const startUser = ref<string | null>(null); // 发起人信息
const handleClick = (key: string) => { const handleClick = (key: string) => {
if (key === '3') { if (key === '3') {
flowXmlAndNode({ flowXmlAndNode({
procInsId: taskForm.procInsId, procInsId: taskForm.procInsId,
deployId: taskForm.deployId, deployId: taskForm.deployId,
}).then((res) => { }).then((res) => {
flowData.value = res flowData.value = res;
}) });
} }
} };
// 修改 setColor 方法为 getStepStatus // 修改 setColor 方法为 getStepStatus
const getStepStatus = (finishTime: string | null) => { const getStepStatus = (finishTime: string | null) => {
if (finishTime) { if (finishTime) {
return 'finish' // 已完成 return 'finish'; // 已完成
} else { } else {
return 'wait' // 等待中 return 'wait'; // 等待中
} }
} };
const setFlowRecordList = async (procInsId: string, deployId: string) => { const setFlowRecordList = async (procInsId: string, deployId: string) => {
const params = { procInsId, deployId } const params = { procInsId, deployId };
await flowRecord(params) await flowRecord(params)
.then((res) => { .then((res) => {
console.log("flowList",res) console.log('flowList', res);
flowRecordList.value = res.flowList flowRecordList.value = res.flowList;
}) })
.catch(() => { .catch(() => {
//goBack() //goBack()
}) });
} };
const taskDataObj = ref() const taskDataObj = ref();
const iniData = async (data) => { const iniData = async (data) => {
taskDataObj.value = data taskDataObj.value = data;
taskName.value = data.taskName as string taskName.value = data.taskName as string;
startUser.value = data.startUserName as string startUser.value = data.startUserName as string;
taskForm.deployId = data.deployId as string taskForm.deployId = data.deployId as string;
taskForm.taskId = data.taskId as string taskForm.taskId = data.taskId as string;
taskForm.procInsId = data.procInsId as string taskForm.procInsId = data.procInsId as string;
taskForm.executionId = data.executionId as string taskForm.executionId = data.executionId as string;
taskForm.instanceId = data.procInsId as string taskForm.instanceId = data.procInsId as string;
//console.log('taskForm.taskId:', taskForm.taskId); //console.log('taskForm.taskId:', taskForm.taskId);
await flowFormData({deployId: taskForm.deployId, taskId:taskForm.taskId}).then(resData => { await flowFormData({ deployId: taskForm.deployId, taskId: taskForm.taskId }).then((resData) => {
nextTick(() => { nextTick(() => {
if(resData.formUrl) { if (resData.formUrl) {
formUrl.value = resData.formUrl formUrl.value = resData.formUrl;
} else { } else {
formUrl.value = "" formUrl.value = '';
refInnerForm.value.iniData(resData) refInnerForm.value.iniData(resData);
} }
}) });
}) });
if(taskForm.taskId) { if (taskForm.taskId) {
await findFlowFormVal({taskId: taskForm.taskId }).then((resValues) => { await findFlowFormVal({ taskId: taskForm.taskId }).then((resValues) => {
nextTick(() => { nextTick(() => {
refInnerForm.value.setFormData(resValues) refInnerForm.value.setFormData(resValues);
}) });
}) });
} }
setFlowRecordList(taskForm.procInsId, taskForm.deployId) setFlowRecordList(taskForm.procInsId, taskForm.deployId);
};
} const showNodeFormData = (data) => {
// Build workflowNodes list for the drawer. Adjust component paths as needed.
const nodes = [
{ id: 'stplanman', name: '主表单', formUrl: '/views/project/plan/components/StPlanManForm.vue' },
{ id: 'stplanexcute', name: '执行表单', formUrl: '/views/project/plan/components/StPlanExcuteForm.vue' },
];
const showNodeFormData = (data) => { // pick which node to show — customize the rule as necessary
openModal(true, { const currentIndex = (data?.taskName || '').includes('执行') ? 1 : 0;
// externalFormData: map node id -> form data object
const externalFormData = {
[nodes[currentIndex].id]: data?.data || {},
};
showFormModalApi.openModal(true, {
workflowNodes: nodes,
currentNodeIndex: currentIndex,
externalFormData,
title: data?.taskName || '表单',
showFooter: true, showFooter: true,
data
}); });
} };
defineExpose({ // Handle submit event emitted from ShowFormModal
iniData, const handleShowFormSubmit = (payload) => {
}) console.log('ShowFormModal submit payload:', payload);
// payload contains: { nodeId, nodeName, formData, procDefId, formComponent }
// Add your handling logic here (e.g., call API, close modal, refresh list)
};
defineExpose({
iniData,
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.clearfix:before,
.clearfix:after {
display: table;
content: '';
}
.clearfix:after {
clear: both;
}
.clearfix:before, .app-container {
.clearfix:after { height: calc(100vh - 150px);
display: table; margin: 0px;
content: ""; padding: 5px;
} }
.clearfix:after {
clear: both
}
.app-container {
height: calc(100vh - 150px);
margin: 0px;
padding: 5px;
}
.my-label { .my-label {
background: #E1F3D8; background: #e1f3d8;
} }
.iframe-container { .iframe-container {
height: calc(100vh - 200px); height: calc(100vh - 200px);
width: 100%; width: 100%;
} }
.responsive-iframe { .responsive-iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
</style> </style>
\ No newline at end of file
...@@ -57,20 +57,20 @@ ...@@ -57,20 +57,20 @@
<div class="content-wrapper"> <div class="content-wrapper">
<!-- 左侧主表单 --> <!-- 左侧主表单 -->
<div class="main-form-section"> <div class="main-form-section">
<a-card title="审批表单" :bordered="false" class="form-card"> <!-- <a-card title="审批表单" :bordered="false" class="form-card">
<template #extra> <template #extra>
<a-button type="link" @click="refreshForm"> <ReloadOutlined /> 刷新 </a-button> <a-button type="link" @click="refreshForm"> <ReloadOutlined /> 刷新 </a-button>
</template> </template> -->
<div class="form-content"> <div class="form-card">
<div v-show="formTp == 1" class="form-wrapper"> <div v-show="formTp == 1" class="form-wrapper">
<FlowInnerForm ref="refCruInnerForm" :key="formKey" style="width: 100%" /> <FlowInnerForm ref="refCruInnerForm" :key="formKey" style="width: 100%; height: 100%" />
</div>
<div v-show="formTp == 2" class="iframe-container" style="height: 470px">
<iFrame :key="formKey" :src="formUrl" class="responsive-iframe" style="width: 100%; height: 100%" />
</div>
</div> </div>
</a-card> <div v-show="formTp == 2" class="form-wrapper">
<iFrame :key="formKey" :src="formUrl" class="responsive-iframe" style="width: 100%; height: 100%" />
</div>
</div>
<!-- </a-card> -->
</div> </div>
<!-- 右侧审批栏 --> <!-- 右侧审批栏 -->
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
// API // API
import { flowRecord } from '/@/components/Process/api/finished'; import { flowRecord } from '/@/components/Process/api/finished';
import { flowXmlAndNode } from '/@/components/Process/api/definition'; import { flowXmlAndNode } from '/@/components/Process/api/definition';
import { complete, flowTaskForm, getNextFlowNode, getMyTaskFlow,assign,assignRead } from '/@/components/Process/api/todo'; import { complete, flowTaskForm, getNextFlowNode, getMyTaskFlow, assign, assignRead } from '/@/components/Process/api/todo';
import { flowTaskInfo } from '/@/components/Process/api/process'; import { flowTaskInfo } from '/@/components/Process/api/process';
// 组件 // 组件
...@@ -723,8 +723,8 @@ ...@@ -723,8 +723,8 @@
} }
}; };
//转办处理 //转办处理
const handleAssignTask = async () => { const handleAssignTask = async () => {
if (assigning.value) return; if (assigning.value) return;
try { try {
assigning.value = true; assigning.value = true;
...@@ -738,33 +738,32 @@ ...@@ -738,33 +738,32 @@
const formData = await validate(); const formData = await validate();
Object.assign(submitData, formData); Object.assign(submitData, formData);
submitData.comment = submitData.comment || ''; submitData.comment = submitData.comment || '';
//test1 1958436761110269953 //test1 1958436761110269953
// submitData.values['targetUserId'] = '1959869916657950721'; // submitData.values['targetUserId'] = '1959869916657950721';
if (userType.value === 'user') { if (userType.value === 'user') {
submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user';
if (formData.checkSendUser) {
submitData.values['targetUserId'] = formData.checkSendUser; submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user'; submitData.values['approvalType'] = 'user';
if (formData.checkSendUser) {
submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user';
}
} else if (formData.checkSendRole) {
submitData.values['targetUserId'] = formData.checkSendRole;
submitData.values['approvalType'] = 'role';
} }
const dataId = formUrl.value.split('=').pop(); } else if (formData.checkSendRole) {
submitData.values['dataId'] = dataId; submitData.values['targetUserId'] = formData.checkSendRole;
submitData.values['approvalType'] = 'role';
const lastEqualIndex = formUrl.value.lastIndexOf('='); }
// 往前找到 & 或 ? 的位置 const dataId = formUrl.value.split('=').pop();
let startIndex = formUrl.value.lastIndexOf('&', lastEqualIndex); submitData.values['dataId'] = dataId;
if (startIndex === -1) {
startIndex = formUrl.value.lastIndexOf('?', lastEqualIndex); const lastEqualIndex = formUrl.value.lastIndexOf('=');
} // 往前找到 & 或 ? 的位置
const dataName = formUrl.value.substring(startIndex + 1, lastEqualIndex); let startIndex = formUrl.value.lastIndexOf('&', lastEqualIndex);
submitData.values['dataName'] = dataName; if (startIndex === -1) {
startIndex = formUrl.value.lastIndexOf('?', lastEqualIndex);
}
const dataName = formUrl.value.substring(startIndex + 1, lastEqualIndex);
submitData.values['dataName'] = dataName;
// 执行发送 // 执行发送
const result = await assign(submitData); const result = await assign(submitData);
...@@ -784,8 +783,8 @@ ...@@ -784,8 +783,8 @@
} }
}; };
//转阅处理 //转阅处理
const handleAssignReadTask = async () => { const handleAssignReadTask = async () => {
if (assiReadgning.value) return; if (assiReadgning.value) return;
try { try {
assiReadgning.value = true; assiReadgning.value = true;
...@@ -799,23 +798,21 @@ ...@@ -799,23 +798,21 @@
const formData = await validate(); const formData = await validate();
Object.assign(submitData, formData); Object.assign(submitData, formData);
submitData.comment = submitData.comment || ''; submitData.comment = submitData.comment || '';
//test1 1958436761110269953 //test1 1958436761110269953
// submitData.values['targetUserId'] = '1959869916657950721'; // submitData.values['targetUserId'] = '1959869916657950721';
if (userType.value === 'user') { if (userType.value === 'user') {
submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user';
if (formData.checkSendUser) {
submitData.values['targetUserId'] = formData.checkSendUser; submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user'; submitData.values['approvalType'] = 'user';
if (formData.checkSendUser) {
submitData.values['targetUserId'] = formData.checkSendUser;
submitData.values['approvalType'] = 'user';
}
} else if (formData.checkSendRole) {
submitData.values['targetUserId'] = formData.checkSendRole;
submitData.values['approvalType'] = 'role';
} }
} else if (formData.checkSendRole) {
submitData.values['targetUserId'] = formData.checkSendRole;
submitData.values['approvalType'] = 'role';
}
// 执行发送 // 执行发送
const result = await assignRead(submitData); const result = await assignRead(submitData);
...@@ -835,8 +832,6 @@ ...@@ -835,8 +832,6 @@
} }
}; };
defineExpose({ defineExpose({
iniData, iniData,
}); });
...@@ -868,7 +863,7 @@ ...@@ -868,7 +863,7 @@
height: 100vh; height: 100vh;
margin: 0; margin: 0;
padding: 0; padding: 0;
background: linear-gradient(135deg, #b0b3c2 0%, #764ba2 100%); // background: linear-gradient(135deg, #b0b3c2 0%, #764ba2 100%);
overflow: hidden; overflow: hidden;
} }
...@@ -945,6 +940,7 @@ ...@@ -945,6 +940,7 @@
} }
.next-node-label { .next-node-label {
width: 120px;
color: #666; color: #666;
font-size: 14px; font-size: 14px;
} }
......
<template> <template>
<div class="app-container"> <div class="app-container">
<vxe-grid <vxe-grid ref="xGrid" v-bind="gridOptions" @checkbox-change="onSelectChange" @checkbox-all="onSelectChange">
ref="xGrid"
v-bind="gridOptions"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
>
<!-- 搜索表单插槽 --> <!-- 搜索表单插槽 -->
<template #flowNameItem="{ data }"> <template #flowNameItem="{ data }">
<vxe-input v-model="data.procDefName" placeholder="请输入流程名称" clearable @keyup.enter="searchEvent"></vxe-input> <vxe-input v-model="data.procDefName" placeholder="请输入流程名称" clearable @keyup.enter="searchEvent"></vxe-input>
</template> </template>
<template #taskNameItem="{ data }"> <template #taskNameItem="{ data }">
<vxe-input v-model="data.taskName" placeholder="请输入任务名称" clearable @keyup.enter="searchEvent"></vxe-input> <vxe-input v-model="data.taskName" placeholder="请输入任务名称" clearable @keyup.enter="searchEvent"></vxe-input>
</template> </template>
<template #startTimeItem="{ data }"> <template #startTimeItem="{ data }">
<vxe-input type="date" v-model="data.startTime" placeholder="请选择开始时间" clearable></vxe-input> <vxe-input type="date" v-model="data.startTime" placeholder="请选择开始时间" clearable></vxe-input>
</template> </template>
<template #actionItem> <template #actionItem>
<vxe-button status="primary" icon="vxe-icon-search" @click="searchEvent">搜索</vxe-button> <vxe-button status="primary" icon="vxe-icon-search" @click="searchEvent">搜索</vxe-button>
<vxe-button icon="vxe-icon-refresh" @click="resetEvent">重置</vxe-button> <vxe-button icon="vxe-icon-refresh" @click="resetEvent">重置</vxe-button>
...@@ -33,14 +28,9 @@ ...@@ -33,14 +28,9 @@
<span class="dept-tag">{{ row.startDeptName }}</span> <span class="dept-tag">{{ row.startDeptName }}</span>
</div> </div>
</template> </template>
<template #action_default="{ row }"> <template #action_default="{ row }">
<vxe-button <vxe-button type="text" icon="vxe-icon-edit" status="primary" @click="handleProcess(row)">处理</vxe-button>
type="text"
icon="vxe-icon-edit"
status="primary"
@click="handleProcess(row)"
>处理</vxe-button>
</template> </template>
</vxe-grid> </vxe-grid>
<div v-if="isShowDrawer"> <div v-if="isShowDrawer">
...@@ -53,288 +43,283 @@ ...@@ -53,288 +43,283 @@
title="待办任务" title="待办任务"
placement="right" placement="right"
width="90%" width="90%"
style="margin: 0px;padding: 0px;" style="margin: 0px; padding: 0px"
> >
<template #extra> <template #extra>
<div style="float: right"> <div style="float: right">
<a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag> <a-tag style="margin-left: 10px">发起人:{{ startUser }}</a-tag>
<a-tag>任务节点:{{ taskName }}</a-tag> <a-tag>任务节点:{{ taskName }}</a-tag>
</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> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, ref, onMounted,nextTick } from 'vue' import { reactive, ref, onMounted, nextTick } from 'vue';
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue';
import type { VxeGridProps, VxeGridInstance } from 'vxe-table' import type { VxeGridProps, VxeGridInstance } from 'vxe-table';
import { todoList, delDeployment } from '/@/components/Process/api/todo' import { todoList, delDeployment } from '/@/components/Process/api/todo';
import XEUtils from 'xe-utils' import XEUtils from 'xe-utils';
import TodoIndex from './components/TodoIndex.vue' import TodoIndex from './components/TodoIndex.vue';
const loading = ref(false) const loading = ref(false);
const isShowDrawer = ref(false) const isShowDrawer = ref(false);
const refTodoIndex = ref() const refTodoIndex = ref();
const startUser = ref() const startUser = ref();
const taskName = ref() const taskName = ref();
interface QueryParams { interface QueryParams {
pageNum: number pageNum: number;
pageSize: number pageSize: number;
procDefName?: string procDefName?: string;
taskName?: string taskName?: string;
startTime?: string startTime?: string;
} }
const queryParams = reactive<QueryParams>({ const queryParams = reactive<QueryParams>({
pageNum: 1, pageNum: 1,
pageSize: 10,
procDefName: undefined,
taskName: undefined,
startTime: undefined,
})
interface TodoItem {
taskId: string
procDefName: string
taskName: string
procDefVersion: number
startUserName: string
startDeptName: string
createTime: string
procInsId: string
executionId: string
deployId: string
}
const defaultData = reactive({
procDefName: '',
taskName: '',
startTime: ''
})
const selectedRowKeys = ref<string[]>([])
const selectRecords = ref<TodoItem[]>([])
const multiple = ref(true)
const xGrid = ref<VxeGridInstance>()
const gridOptions = reactive<VxeGridProps<any>>({
loading: loading.value,
showOverflow: true,
border: true,
rowConfig: {
keyField: 'taskId',
isHover: true
},
columnConfig: {
resizable: true
},
pagerConfig: {
enabled: true,
pageSize: 10, pageSize: 10,
pageSizes: [10, 20, 50, 100], procDefName: undefined,
layouts: ['PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'Sizes', 'FullJump', 'Total'] taskName: undefined,
}, startTime: undefined,
checkboxConfig: { });
highlight: true,
range: true interface TodoItem {
}, taskId: string;
layouts: ['Top', 'Form', 'Toolbar','Table', 'Bottom', 'Pager'], procDefName: string;
formConfig: { taskName: string;
data: XEUtils.clone(defaultData, true), procDefVersion: number;
items: [ startUserName: string;
{ field: 'procDefName', title: '流程名称', span: 6, itemRender: {}, slots: { default: 'flowNameItem' } }, startDeptName: string;
{ field: 'taskName', title: '任务名称', span: 6, itemRender: {}, slots: { default: 'taskNameItem' } }, createTime: string;
{ field: 'startTime', title: '开始时间', span: 6, itemRender: {}, slots: { default: 'startTimeItem' } }, procInsId: string;
{ span: 6, align: 'center', itemRender: {}, slots: { default: 'actionItem' } } executionId: string;
] deployId: string;
}, }
proxyConfig: {
response: { const defaultData = reactive({
result: 'result', procDefName: '',
total: 'page.total' taskName: '',
startTime: '',
});
const selectedRowKeys = ref<string[]>([]);
const selectRecords = ref<TodoItem[]>([]);
const multiple = ref(true);
const xGrid = ref<VxeGridInstance>();
const gridOptions = reactive<VxeGridProps<any>>({
loading: loading.value,
showOverflow: true,
border: true,
rowConfig: {
keyField: 'taskId',
isHover: true,
}, },
ajax: { columnConfig: {
query: ({ page }) => { resizable: true,
return findPageList(page.currentPage, page.pageSize) },
} pagerConfig: {
enabled: true,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
layouts: ['PrevJump', 'PrevPage', 'Number', 'NextPage', 'NextJump', 'Sizes', 'FullJump', 'Total'],
},
checkboxConfig: {
highlight: true,
range: true,
},
layouts: ['Top', 'Form', 'Toolbar', 'Table', 'Bottom', 'Pager'],
formConfig: {
data: XEUtils.clone(defaultData, true),
items: [
{ field: 'procDefName', title: '流程名称', span: 6, itemRender: {}, slots: { default: 'flowNameItem' } },
{ field: 'taskName', title: '任务名称', span: 6, itemRender: {}, slots: { default: 'taskNameItem' } },
{ field: 'startTime', title: '开始时间', span: 6, itemRender: {}, slots: { default: 'startTimeItem' } },
{ span: 6, align: 'center', itemRender: {}, slots: { default: 'actionItem' } },
],
},
proxyConfig: {
response: {
result: 'result',
total: 'page.total',
},
ajax: {
query: ({ page }) => {
return findPageList(page.currentPage, page.pageSize);
},
},
},
columns: [
{ type: 'checkbox', width: 60, fixed: 'left' },
{ type: 'seq', width: 70, fixed: 'left' },
{ field: 'taskId', title: '任务编号', minWidth: 120, showOverflow: true },
{ field: 'procDefName', title: '流程名称', minWidth: 160, showOverflow: true },
{ field: 'taskName', title: '当前节点', minWidth: 140, showOverflow: true },
{ field: 'taskType', title: '节点类型', minWidth: 140, showOverflow: true },
{ field: 'procDefVersion', title: '流程版本', width: 100, align: 'center', slots: { default: 'procDefVersion_default' } },
{ field: 'startUser', title: '流程发起人', minWidth: 180, align: 'center', slots: { default: 'startUser_default' } },
{ field: 'createTime', title: '接收时间', width: 180, align: 'center' },
{
title: '操作',
width: 120,
fixed: 'right',
align: 'center',
slots: { default: 'action_default' },
},
],
});
const findPageList = async (currentPage: number, pageSize: number) => {
queryParams.pageNum = currentPage;
queryParams.pageSize = pageSize;
if (gridOptions.formConfig?.data) {
queryParams.procDefName = gridOptions.formConfig.data.procDefName;
queryParams.taskName = gridOptions.formConfig.data.taskName;
queryParams.startTime = gridOptions.formConfig.data.startTime;
} }
},
columns: [ try {
{ type: 'checkbox', width: 60, fixed: 'left' }, loading.value = true;
{ type: 'seq', width: 70, fixed: 'left' }, const retData = await todoList(queryParams);
{ field: 'taskId', title: '任务编号', minWidth: 120, showOverflow: true }, return {
{ field: 'procDefName', title: '流程名称', minWidth: 160, showOverflow: true }, page: {
{ field: 'taskName', title: '当前节点', minWidth: 140, showOverflow: true }, total: retData.total,
{ field: 'taskType', title: '节点类型', minWidth: 140, showOverflow: true }, },
{ field: 'procDefVersion', title: '流程版本', width: 100, align: 'center', slots: { default: 'procDefVersion_default' } }, result: retData.records,
{ field: 'startUser', title: '流程发起人', minWidth: 180, align: 'center', slots: { default: 'startUser_default' } }, };
{ field: 'createTime', title: '接收时间', width: 180, align: 'center' }, } catch (error) {
{ console.error('查询数据失败:', error);
title: '操作', message.error('查询数据失败');
width: 120, return {
fixed: 'right', page: { total: 0 },
align: 'center', result: [],
slots: { default: 'action_default' } };
} finally {
loading.value = false;
} }
] };
})
const searchEvent = async () => {
const findPageList = async (currentPage: number, pageSize: number) => { queryParams.pageNum = 1;
queryParams.pageNum = currentPage if (xGrid.value) {
queryParams.pageSize = pageSize await xGrid.value.commitProxy('query');
if (gridOptions.formConfig?.data) { }
queryParams.procDefName = gridOptions.formConfig.data.procDefName };
queryParams.taskName = gridOptions.formConfig.data.taskName
queryParams.startTime = gridOptions.formConfig.data.startTime const resetEvent = () => {
} if (gridOptions.formConfig) {
gridOptions.formConfig.data = XEUtils.clone(defaultData, true);
try {
loading.value = true
const retData = await todoList(queryParams)
return {
page: {
total: retData.total
},
result: retData.records
} }
} catch (error) { searchEvent();
console.error('查询数据失败:', error) };
message.error('查询数据失败')
return { const onSelectChange = () => {
page: { total: 0 }, if (xGrid.value) {
result: [] const checkedRecords = xGrid.value.getCheckboxRecords();
selectRecords.value = checkedRecords;
selectedRowKeys.value = checkedRecords.map((item) => item.taskId);
multiple.value = !selectedRowKeys.value.length;
} }
} finally { };
loading.value = false
const handleProcess = async (row) => {
isShowDrawer.value = true;
startUser.value = row.startUserName;
taskName.value = row.taskName;
await nextTick(() => {
if (refTodoIndex.value) {
refTodoIndex.value.iniData(row);
} else {
isShowDrawer.value = false;
}
});
};
const handleSuccess = async () => {
isShowDrawer.value = false;
await searchEvent();
};
</script>
<style scoped>
.app-container {
padding: 16px;
width: 100%;
height: 100%;
} }
}
const searchEvent = async () => { .user-info {
queryParams.pageNum = 1 display: flex;
if (xGrid.value) { align-items: center;
await xGrid.value.commitProxy('query') justify-content: center;
gap: 8px;
flex-wrap: nowrap;
} }
}
const resetEvent = () => { .user-name {
if (gridOptions.formConfig) { font-weight: 500;
gridOptions.formConfig.data = XEUtils.clone(defaultData, true) white-space: nowrap;
} }
searchEvent()
}
.dept-tag {
background: #f0f0f0;
color: #666;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
const onSelectChange = () => { .version-tag {
if (xGrid.value) { background: #1890ff;
const checkedRecords = xGrid.value.getCheckboxRecords() color: white;
selectRecords.value = checkedRecords padding: 2px 8px;
selectedRowKeys.value = checkedRecords.map(item => item.taskId) border-radius: 4px;
multiple.value = !selectedRowKeys.value.length font-size: 12px;
} }
}
const handleProcess = async (row) => {
isShowDrawer.value = true
startUser.value = row.startUserName
taskName.value = row.taskName
await nextTick(()=>{
if(refTodoIndex.value) {
refTodoIndex.value.iniData(row)
} else {
isShowDrawer.value = false
}
}) /* Vxe Grid 样式调整 */
:deep(.vxe-form--wrapper) {
background: #fafafa;
padding: 16px;
border-radius: 4px;
border: 1px solid #e8e8e8;
margin-bottom: 16px;
}
} :deep(.vxe-form--item-title) {
font-weight: 500;
color: #333;
}
const handleSuccess = async () => { :deep(.vxe-grid--wrapper) {
isShowDrawer.value = false font-family: inherit;
await searchEvent() }
}
</script> :deep(.vxe-grid--header) {
background-color: #fafafa;
}
<style scoped> :deep(.vxe-grid--body) {
.app-container { background-color: #fff;
padding: 16px; }
width: 100%;
height: 100%; :deep(.vxe-cell) {
} padding: 8px 4px;
}
.user-info {
display: flex; :deep(.vxe-toolbar) {
align-items: center; background: transparent;
justify-content: center; padding: 8px 0;
gap: 8px; margin-bottom: 8px;
flex-wrap: nowrap; }
}
:deep(.vxe-table--render-wrapper) {
.user-name { border-radius: 4px;
font-weight: 500; }
white-space: nowrap; </style>
}
.dept-tag {
background: #f0f0f0;
color: #666;
padding: 2px 6px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
}
.version-tag {
background: #1890ff;
color: white;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
/* Vxe Grid 样式调整 */
:deep(.vxe-form--wrapper) {
background: #fafafa;
padding: 16px;
border-radius: 4px;
border: 1px solid #e8e8e8;
margin-bottom: 16px;
}
:deep(.vxe-form--item-title) {
font-weight: 500;
color: #333;
}
:deep(.vxe-grid--wrapper) {
font-family: inherit;
}
:deep(.vxe-grid--header) {
background-color: #fafafa;
}
:deep(.vxe-grid--body) {
background-color: #fff;
}
:deep(.vxe-cell) {
padding: 8px 4px;
}
:deep(.vxe-toolbar) {
background: transparent;
padding: 8px 0;
margin-bottom: 8px;
}
:deep(.vxe-table--render-wrapper) {
border-radius: 4px;
}
</style>
\ No newline at end of file
<template> <template>
<div class="plan-management-page"> <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="main-content">
<!-- 搜索区域 --> <!-- 搜索区域 -->
...@@ -43,12 +19,12 @@ ...@@ -43,12 +19,12 @@
<JSearchSelect placeholder="请选择类型" v-model:value="queryParam['projectType']" dict="projecttype" /> <JSearchSelect placeholder="请选择类型" v-model:value="queryParam['projectType']" dict="projecttype" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :xl="5" :lg="8" :md="12" :sm="24"> <a-col :xl="7" :lg="8" :md="12" :sm="24">
<a-form-item label="执行部门"> <a-form-item label="执行部门">
<JSelectDept placeholder="请选择执行部门" v-model:value="queryParam['execDepCode']" /> <JSelectDept placeholder="请选择执行部门" v-model:value="queryParam['execDepCode']" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :xl="4" :lg="8" :md="12" :sm="24"> <a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="计划状态"> <a-form-item label="计划状态">
<a-select <a-select
v-model:value="queryParam['status']" v-model:value="queryParam['status']"
...@@ -66,6 +42,8 @@ ...@@ -66,6 +42,8 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row>
<a-row :gutter="16">
<a-col :xl="5" :lg="8" :md="12" :sm="24"> <a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="计划日期"> <a-form-item label="计划日期">
<a-range-picker <a-range-picker
...@@ -76,8 +54,6 @@ ...@@ -76,8 +54,6 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row>
<a-row :gutter="16">
<a-col :xl="5" :lg="8" :md="12" :sm="24"> <a-col :xl="5" :lg="8" :md="12" :sm="24">
<a-form-item label="优先级"> <a-form-item label="优先级">
<a-select <a-select
...@@ -106,7 +82,7 @@ ...@@ -106,7 +82,7 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :xl="14" :lg="16" :md="24" :sm="24"> <a-col :xl="7" :lg="16" :md="24" :sm="24">
<a-form-item class="search-btn-group"> <a-form-item class="search-btn-group">
<a-space :size="8"> <a-space :size="8">
<a-button type="primary" @click="searchQuery">查询</a-button> <a-button type="primary" @click="searchQuery">查询</a-button>
...@@ -166,12 +142,12 @@ ...@@ -166,12 +142,12 @@
<!-- 待办抽屉 --> <!-- 待办抽屉 -->
<div v-if="isShowDrawer"> <div v-if="isShowDrawer">
<a-drawer destroyOnClose v-model:open="isShowDrawer" class="flat-drawer" title="待办任务" placement="right" width="90%"> <a-drawer destroyOnClose v-model:open="isShowDrawer" class="flat-drawer" title="待办任务" placement="right" width="90%">
<template #extra> <!-- <template #extra>
<div class="drawer-tags"> <div class="drawer-tags">
<span class="tag">发起人: {{ startUser }}</span> <span class="tag">发起人: {{ startUser }}</span>
<span class="tag">任务节点: {{ taskName }}</span> <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>
...@@ -545,8 +521,8 @@ ...@@ -545,8 +521,8 @@
{ {
label: '待办', label: '待办',
ifShow: () => { ifShow: () => {
console.log("-------------record['uid'] ",record['uid']); console.log("-------------record['uid'] ", record['uid']);
console.log("-------------userStore.getUserInfo.id ",userStore.getUserInfo.id); console.log('-------------userStore.getUserInfo.id ', userStore.getUserInfo.id);
if (record['bpmStatus'] == '2' && record['uid'] == userStore.getUserInfo.id) return true; if (record['bpmStatus'] == '2' && record['uid'] == userStore.getUserInfo.id) return true;
else return false; else return false;
}, },
......
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, watch, nextTick } from 'vue'; import { ref, reactive, onMounted, watch, nextTick, toRaw } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { saveOrUpdate } from '../StPlanMan.api'; import { saveOrUpdate } from '../StPlanMan.api';
...@@ -107,6 +107,25 @@ ...@@ -107,6 +107,25 @@
attachments: [] as any[], attachments: [] as any[],
}); });
// accept external formData prop from parent modal
const props = defineProps({
formData: {
type: Object,
default: () => ({}),
},
});
// sync incoming prop into internal reactive formData
watch(
() => props.formData,
(v) => {
if (v && Object.keys(v).length > 0) {
Object.assign(formData, v);
}
},
{ immediate: true, deep: true }
);
// 监听formData变化,用于调试 // 监听formData变化,用于调试
watch( watch(
...@@ -272,7 +291,8 @@ ...@@ -272,7 +291,8 @@
const response = await saveOrUpdate(submitData, true); const response = await saveOrUpdate(submitData, true);
if (response) { if (response) {
message.success('保存成功'); // message.success('保存成功');
console.log('保存成功:');
// 更新缓存 // 更新缓存
planFormStore.updateFormDataCache(formData); planFormStore.updateFormDataCache(formData);
console.log(response); console.log(response);
...@@ -355,6 +375,7 @@ ...@@ -355,6 +375,7 @@
resetForm, resetForm,
initFormData, initFormData,
isInitialized, isInitialized,
getFormData: () => toRaw(formData),
}); });
</script> </script>
...@@ -489,10 +510,8 @@ ...@@ -489,10 +510,8 @@
/* ==================== 表单底部按钮 ==================== */ /* ==================== 表单底部按钮 ==================== */
.form-footer { .form-footer {
padding: 14px 32px; padding: 14px 32px;
background: var(--color-bg-section);
border-top: 1px solid var(--color-border);
display: flex; display: flex;
justify-content: flex-end; justify-content: center;
} }
/* ==================== 上传区域 ==================== */ /* ==================== 上传区域 ==================== */
......
...@@ -13,12 +13,7 @@ ...@@ -13,12 +13,7 @@
<template #planBasis="{ model, field }"> <template #planBasis="{ model, field }">
<div class="basis-container" v-if="model[field]"> <div class="basis-container" v-if="model[field]">
<div v-if="isValidJson(model[field])" class="basis-tags"> <div v-if="isValidJson(model[field])" class="basis-tags">
<a-tag <a-tag v-for="item in safeJsonParse(model[field])" @click="viewBasisDetail(item)" :key="item.id" class="basis-tag">
v-for="item in safeJsonParse(model[field])"
@click="viewBasisDetail(item)"
:key="item.id"
class="basis-tag"
>
{{ item.name }} {{ item.name }}
</a-tag> </a-tag>
</div> </div>
...@@ -46,7 +41,7 @@ ...@@ -46,7 +41,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { BasicForm, useForm } from '/@/components/Form/index'; import { BasicForm, useForm } from '/@/components/Form/index';
import { computed, ref, onMounted, watchEffect, toRaw } from 'vue'; import { computed, ref, onMounted, watchEffect, toRaw, watch } from 'vue';
import { defHttp } from '/@/utils/http/axios'; import { defHttp } from '/@/utils/http/axios';
import { getBpmFormSchema } from '../StPlanMan.data'; import { getBpmFormSchema } from '../StPlanMan.data';
import { saveOrUpdate } from '../StPlanMan.api'; import { saveOrUpdate } from '../StPlanMan.api';
...@@ -79,6 +74,22 @@ ...@@ -79,6 +74,22 @@
baseColProps: { span: 24 }, baseColProps: { span: 24 },
}); });
// watch incoming prop and populate the form when provided
watch(
() => props.formData,
(v) => {
if (v && Object.keys(v).length > 0) {
formData.value = { ...v };
try {
setFieldsValue(v);
} catch (e) {
console.warn('setFieldsValue failed:', e);
}
}
},
{ immediate: true, deep: true }
);
const formDisabled = computed(() => { const formDisabled = computed(() => {
return props.formData?.disabled !== false; return props.formData?.disabled !== false;
}); });
...@@ -186,194 +197,202 @@ ...@@ -186,194 +197,202 @@
onMounted(() => { onMounted(() => {
initFormData(); initFormData();
}); });
// expose getFormData so parent modal can read current values
defineExpose({
submitForm,
// resetForm,
initFormData,
getFormData: () => toRaw(formData.value),
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
/* ==================== 柔和中性风格 - CSS 变量 ==================== */ /* ==================== 柔和中性风格 - CSS 变量 ==================== */
.plan-form-container { .plan-form-container {
--color-primary: #3b5bdb; --color-primary: #3b5bdb;
--color-primary-hover: #364fc7; --color-primary-hover: #364fc7;
--color-primary-light: #e8ecfd; --color-primary-light: #e8ecfd;
--color-success: #2f9e44; --color-success: #2f9e44;
--color-warning: #e67700; --color-warning: #e67700;
--color-error: #c92a2a; --color-error: #c92a2a;
--color-text-primary: #1c1c1e; --color-text-primary: #1c1c1e;
--color-text-secondary: #555770; --color-text-secondary: #555770;
--color-text-muted: #a0a3b1; --color-text-muted: #a0a3b1;
--color-border: #e4e4e9; --color-border: #e4e4e9;
--color-border-strong: #c8c8d0; --color-border-strong: #c8c8d0;
--color-bg-page: #f5f5f7; --color-bg-page: #f5f5f7;
--color-bg-white: #ffffff; --color-bg-white: #ffffff;
--color-bg-section: #f0f0f4; --color-bg-section: #f0f0f4;
--radius: 6px; --radius: 6px;
background: var(--color-bg-white); background: var(--color-bg-white);
min-height: 100vh; min-height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Helvetica Neue', sans-serif;
} }
/* ==================== 表单头部 ==================== */ /* ==================== 表单头部 ==================== */
.form-header { .form-header {
background: var(--color-bg-white); background: var(--color-bg-white);
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
padding: 20px 32px; padding: 20px 32px;
border-left: 3px solid var(--color-primary); border-left: 3px solid var(--color-primary);
padding-left: 28px; 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;
} }
}
.form-title {
.basis-alert { font-size: 18px;
border-radius: var(--radius); font-weight: 600;
} color: var(--color-text-primary);
margin: 0 0 3px 0;
.basis-empty { letter-spacing: -0.2px;
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, .form-subtitle {
&.ant-input-focused { font-size: 13px;
border-color: var(--color-primary) !important; color: var(--color-text-muted);
box-shadow: 0 0 0 2px var(--color-primary-light) !important; 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 {
:deep(.ant-select-focused .ant-select-selector) { display: inline-flex;
border-color: var(--color-primary) !important; align-items: center;
box-shadow: 0 0 0 2px var(--color-primary-light) !important; padding: 4px 10px;
} background: var(--color-primary-light);
border: 1px solid var(--color-primary);
/* ==================== 按钮样式 ==================== */ border-radius: var(--radius);
:deep(.ant-btn) { color: var(--color-primary);
border-radius: var(--radius); font-size: 13px;
font-weight: 500; cursor: pointer;
font-size: 13px; transition: background 0.15s;
height: 34px;
padding: 0 18px;
box-shadow: none;
&.ant-btn-primary {
background: var(--color-primary);
border-color: var(--color-primary);
&:hover { &:hover {
background: var(--color-primary-hover); background: #d0d9f8;
border-color: var(--color-primary-hover);
} }
} }
&.ant-btn-default { .basis-alert {
border-color: var(--color-border-strong); 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); color: var(--color-text-secondary);
background: var(--color-bg-white); }
: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 { &:hover {
border-color: var(--color-primary); border-color: var(--color-border-strong) !important;
color: var(--color-primary); }
background: var(--color-primary-light);
&: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-spin-container) { border-color: var(--color-primary) !important;
min-height: 400px; box-shadow: 0 0 0 2px var(--color-primary-light) !important;
} }
/* ==================== 响应式设计 ==================== */ /* ==================== 按钮样式 ==================== */
@media (max-width: 768px) { :deep(.ant-btn) {
.form-header { border-radius: var(--radius);
padding: 16px 20px; 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);
}
}
} }
.form-body { /* ==================== 加载状态 ==================== */
padding: 20px; :deep(.ant-spin-container) {
min-height: 400px;
} }
.form-footer { /* ==================== 响应式设计 ==================== */
padding: 12px 20px; @media (max-width: 768px) {
.form-header {
padding: 16px 20px;
}
.form-body {
padding: 20px;
}
.form-footer {
padding: 12px 20px;
}
} }
}
</style> </style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论