<script lang="ts" setup>
|
import type { OpenBatchFormConfig } from './types';
|
|
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue';
|
|
import { useMessage } from '@jnpf/hooks';
|
import { BasicPopup } from '@jnpf/ui/popup';
|
import { buildUUID, createAsyncComponent, getScriptFunc } from '@jnpf/utils';
|
|
import { cloneDeep } from 'lodash-es';
|
|
import { batchCreateModel, batchUpdateModel, getBatchModelInfo, getConfigData } from '#/api/onlineDev/visualDev';
|
import { buildAfterSubmitParameter, getBatchSubmitRecordIds } from '#/components/FormGenerator/src/helper/afterSubmit';
|
import { buildAuditDisplayFields } from '#/components/FormGenerator/src/helper/auditDisplay';
|
import { buildDisplayOnlySubmitData } from '#/components/FormGenerator/src/helper/displayOnly';
|
import { onlineUtils } from '#/utils/jnpf';
|
|
import { BATCH_ROWS_FIELD, createBatchFormConf } from './batchFormAdapter';
|
|
const Parser = createAsyncComponent(() => import('#/components/FormGenerator/src/components/Parser.vue'));
|
const emitter = onlineUtils.getEmitter();
|
const { createMessage } = useMessage();
|
const parserRef = ref<any>();
|
const pendingRowScripts = new Set<Promise<void>>();
|
const state = reactive({
|
config: null as null | OpenBatchFormConfig,
|
confirmLoading: false,
|
formConf: {} as any,
|
key: 0,
|
loading: false,
|
open: false,
|
originalFormConf: {} as any,
|
preparedData: null as any,
|
rowScriptError: null as any,
|
title: '批量操作',
|
});
|
|
const okText = computed(() => state.originalFormConf.confirmButtonText || '确定');
|
|
function getBatchField(field: string) {
|
return `${BATCH_ROWS_FIELD}.${field}`;
|
}
|
|
function createScriptContext(row: any, rowIndex: number) {
|
return {
|
formData: row,
|
onlineUtils,
|
params: state.config?.params || {},
|
setDisabled: (field: string, value: any) => parserRef.value?.setDisabled?.(getBatchField(field), value, rowIndex),
|
setFormData: (field: string, value: any) => {
|
row[field] = value;
|
parserRef.value?.setFormData?.(getBatchField(field), value, rowIndex);
|
},
|
setRequired: (field: string, value: any) => parserRef.value?.setRequired?.(getBatchField(field), value, rowIndex),
|
setShowOrHide: (field: string, value: any) => parserRef.value?.setShowOrHide?.(getBatchField(field), value, rowIndex),
|
};
|
}
|
|
async function runRowScript(name: 'afterSubmit' | 'beforeSubmit' | 'onLoad', row: any, rowIndex: number, id?: number | string) {
|
const script = state.originalFormConf?.funcs?.[name];
|
if (!script) return;
|
const func: any = getScriptFunc(script);
|
if (func) {
|
const parameter = createScriptContext(row, rowIndex);
|
await Promise.resolve(func(name === 'afterSubmit' ? buildAfterSubmitParameter(parameter, id) : parameter));
|
}
|
}
|
|
function handleRowAdd(row: any, rowIndex: number) {
|
const task = runRowScript('onLoad', row, rowIndex);
|
pendingRowScripts.add(task);
|
void task
|
.catch((error) => {
|
state.rowScriptError = error;
|
console.error('[GlobalBatchFormPopup] onLoad error:', error);
|
createMessage.error(error?.message || `第${rowIndex + 1}条记录加载脚本执行失败`);
|
})
|
.finally(() => pendingRowScripts.delete(task));
|
}
|
|
function makeInitialRow(data: Record<string, any> = {}) {
|
return { ...cloneDeep(data), __batchRowId: buildUUID() };
|
}
|
|
async function loadRows(config: OpenBatchFormConfig) {
|
if (config.mode === 'create') return [makeInitialRow(config.params)];
|
const ids = [...new Set((config.ids || []).filter(Boolean))];
|
if (!ids.length) throw new Error('请选择需要批量编辑的数据');
|
const requestRows = ids.map((id) => ({ clientRowId: buildUUID(), id }));
|
const res = await getBatchModelInfo(config.modelId, {
|
menuId: config.menuId,
|
onlineUtilsOpen: true,
|
rows: requestRows,
|
});
|
return (res?.data?.rows || []).map((item) => {
|
const data = typeof item.data === 'string' ? JSON.parse(item.data) : item.data || {};
|
return { ...data, id: item.id, __batchRowId: item.clientRowId || buildUUID() };
|
});
|
}
|
|
async function handleOpen(config: OpenBatchFormConfig) {
|
if (!config?.modelId) return createMessage.error('缺少在线表单ID');
|
state.config = config;
|
state.loading = true;
|
state.rowScriptError = null;
|
state.open = true;
|
state.title = config.title || (config.mode === 'edit' ? '批量编辑' : '批量新增');
|
try {
|
const [configRes, loadedRows] = await Promise.all([getConfigData(config.modelId, { onlineUtilsOpen: true }), loadRows(config)]);
|
if (configRes?.data?.webType === 4) throw new Error('数据接口类型不支持批量表单');
|
const formConf = configRes?.data?.formData ? JSON.parse(configRes.data.formData) : {};
|
if (formConf?.funcs?.customSubmit) throw new Error('启用自定义提交的表单暂不支持批量操作');
|
if (formConf?.batchConfig?.enabled !== true) throw new Error('当前表单未启用批量操作');
|
if (config.mode === 'create' && formConf.batchConfig.allowCreate === false) throw new Error('当前表单未启用批量新增');
|
if (config.mode === 'edit' && formConf.batchConfig.allowEdit === false) throw new Error('当前表单未启用批量编辑');
|
const maxRows = Math.min(Math.max(Number(formConf.batchConfig.maxRows) || 20, 1), 100);
|
const defaultRows = Math.min(Math.max(Number(formConf.batchConfig.defaultRows) || 1, 1), maxRows);
|
const rows = config.mode === 'create' ? Array.from({ length: defaultRows }, () => makeInitialRow(config.params)) : loadedRows;
|
state.originalFormConf = cloneDeep(formConf);
|
state.formConf = createBatchFormConf(formConf, rows, config.mode, handleRowAdd);
|
state.key++;
|
await nextTick();
|
} catch (error: any) {
|
createMessage.error(error?.message || '批量表单加载失败');
|
close(false);
|
} finally {
|
state.loading = false;
|
}
|
}
|
|
function handlePrepared(data: any) {
|
state.preparedData = data;
|
}
|
|
async function validateAndGetRows() {
|
state.preparedData = null;
|
const valid = await parserRef.value?.handleSubmit?.(true);
|
if (!valid || !state.preparedData) return null;
|
const rows = parserRef.value?.getFormData?.()?.[BATCH_ROWS_FIELD];
|
return Array.isArray(rows) ? rows : null;
|
}
|
|
function requestSignature(rows: any[]) {
|
const confirmConfig = state.originalFormConf.confirmBtnConfig;
|
if (!confirmConfig?.biz_sign_enabled) return Promise.resolve<any>(null);
|
if (!confirmConfig.biz_sign_field) {
|
createMessage.error('表单已启用电子签名,但未配置电子签名字段');
|
return Promise.reject(new Error('未配置电子签名字段'));
|
}
|
return new Promise<any>((resolve) => {
|
onlineUtils.sign({
|
metaData: {
|
biz_button: okText.value,
|
biz_data: rows.map((row) => buildDisplayOnlySubmitData(state.originalFormConf.fields, row)),
|
biz_form_id: state.config?.modelId || '',
|
is_biz_form: true,
|
is_review_button: false,
|
},
|
onCancel: () => resolve(null),
|
onSubmit: (data) => resolve(data),
|
});
|
});
|
}
|
|
async function handleSubmit() {
|
if (state.confirmLoading || state.loading || !state.config) return;
|
state.confirmLoading = true;
|
try {
|
await Promise.all(pendingRowScripts);
|
if (state.rowScriptError) throw state.rowScriptError;
|
let rows = await validateAndGetRows();
|
if (!rows?.length) return;
|
for (const [index, row] of rows.entries()) await runRowScript('beforeSubmit', row, index);
|
rows = await validateAndGetRows();
|
if (!rows?.length) return;
|
|
const signData = await requestSignature(rows);
|
const confirmConfig = state.originalFormConf.confirmBtnConfig;
|
if (confirmConfig?.biz_sign_enabled && !signData?.biz_sign) return;
|
if (confirmConfig?.biz_sign_field && signData?.biz_sign) {
|
for (const row of rows) row[confirmConfig.biz_sign_field] = signData.biz_sign;
|
}
|
|
const payload = {
|
bizSign: signData?.biz_sign || '',
|
menuId: state.config.menuId,
|
onlineUtilsOpen: true,
|
rows: rows.map((row) => {
|
const data = buildDisplayOnlySubmitData(state.originalFormConf.fields, row);
|
delete data.__batchRowId;
|
delete data.jnpfId;
|
if (state.config?.mode === 'create') delete data.id;
|
return {
|
auditDisplayFields: buildAuditDisplayFields(state.originalFormConf.fields, data),
|
clientRowId: row.__batchRowId || row.jnpfId,
|
data: JSON.stringify(data),
|
id: row.id,
|
};
|
}),
|
};
|
const method = state.config.mode === 'edit' ? batchUpdateModel : batchCreateModel;
|
const res = await method(state.config.modelId, payload);
|
createMessage.success(res.msg || '批量保存成功');
|
try {
|
const ids = getBatchSubmitRecordIds(rows, res.data?.rows, state.config.mode === 'edit');
|
for (const [index, row] of rows.entries()) await runRowScript('afterSubmit', row, index, ids[index]);
|
} catch (error) {
|
console.error('[GlobalBatchFormPopup] afterSubmit error:', error);
|
createMessage.warning('数据已保存,但提交后置脚本执行失败');
|
}
|
try {
|
state.config.onSuccess?.(res.data);
|
} catch (error) {
|
console.error('[GlobalBatchFormPopup] onSuccess error:', error);
|
}
|
close(true);
|
} catch (error: any) {
|
console.error('[GlobalBatchFormPopup] submit error:', error);
|
if (error?.message) createMessage.error(error.message);
|
} finally {
|
state.confirmLoading = false;
|
}
|
}
|
|
function close(success = false) {
|
const config = state.config;
|
state.open = false;
|
state.config = null;
|
state.formConf = {};
|
state.originalFormConf = {};
|
state.preparedData = null;
|
state.rowScriptError = null;
|
pendingRowScripts.clear();
|
if (!success) config?.onCancel?.();
|
return Promise.resolve(true);
|
}
|
|
onMounted(() => emitter.on('OPEN_BATCH_FORM', handleOpen));
|
onUnmounted(() => emitter.off('OPEN_BATCH_FORM', handleOpen));
|
</script>
|
|
<template>
|
<BasicPopup
|
:open="state.open"
|
:loading="state.loading"
|
:confirm-loading="state.confirmLoading"
|
:ok-text="okText"
|
:close-func="() => close(false)"
|
destroy-on-close
|
show-ok-btn
|
class="full-popup global-batch-form-popup"
|
@ok="handleSubmit">
|
<template #title>
|
<div class="text-[16px] font-medium">{{ state.title }}</div>
|
</template>
|
<div class="global-batch-form-popup__body">
|
<Parser
|
v-if="!state.loading && state.formConf.fields"
|
ref="parserRef"
|
:key="state.key"
|
:form-conf="state.formConf"
|
:model-id="state.config?.modelId"
|
:params="state.config?.params"
|
:require-review="false"
|
@submit="handlePrepared" />
|
</div>
|
</BasicPopup>
|
</template>
|
|
<style scoped>
|
.global-batch-form-popup__body {
|
height: 100%;
|
min-height: 0;
|
overflow: auto;
|
padding: 10px;
|
}
|
</style>
|