import type { FieldChange, FillDataResponse, FillFieldValue, RepeatRow, SourceSyncPreview, StepFillStructure, StepProgress } from '../domain/types';
|
import type { EditorControlEvent, OnlyOfficeBridge } from '../onlyoffice/bridge';
|
|
import { computed, ref } from 'vue';
|
|
import {
|
appendRepeatRow as requestRepeatRow,
|
deleteRepeatRow as requestDeleteRepeatRow,
|
fetchFillData,
|
previewSourceSync as requestSourceSyncPreview,
|
restoreRepeatRow as requestRestoreRepeatRow,
|
synchronizeSource as requestSourceSync,
|
} from '../api/record-fill';
|
import { checkboxGroupRuntimeIdentity } from '../domain/checkbox-group';
|
import { buildDraftFields } from '../domain/draft';
|
import { parsePluginContext, RECORD_FILL_BIZ_SCENE } from '../domain/plugin-context';
|
import { parseTag, primaryFieldTag } from '../domain/tags';
|
import { useFillerStore } from '../stores/record-filler';
|
|
const SDK_READY_TIMEOUT_MS = 5_000;
|
const DOCUMENT_CHANGE_DEBOUNCE_MS = 150;
|
function assignedStepId(tag: string, structure: StepFillStructure): string | undefined {
|
const parsed = parseTag(tag);
|
if (parsed?.type === 'repeat') return structure.repeatGroupSteps[parsed.groupCode];
|
const controlTag = primaryFieldTag(tag);
|
return controlTag ? structure.controlSteps[controlTag] : undefined;
|
}
|
|
export function useRecordFiller(bridge: OnlyOfficeBridge) {
|
const store = useFillerStore();
|
const recordHostContext = ref<null | { bizDataId: string; documentId: string }>(null);
|
const isMultiStep = computed(() => Boolean(store.stepStructure && !store.stepStructure.configuration.singleStep));
|
const currentStepId = computed(() => store.currentStepId);
|
const conflictCount = computed(() => Object.keys(store.conflicts).length);
|
const sourceSyncPreview = ref<null | SourceSyncPreview>(null);
|
const sourceSyncLoading = ref(false);
|
const visibleFields = computed(() => {
|
if (!isMultiStep.value) return store.fields;
|
const structure = store.stepStructure;
|
if (!structure || !store.currentStepId) return [];
|
return store.fields.filter((field) => assignedStepId(field.tag, structure) === store.currentStepId);
|
});
|
const visibleRepeatGroups = computed(() => {
|
if (!isMultiStep.value) return store.repeatGroups;
|
const structure = store.stepStructure;
|
if (!structure || !store.currentStepId) return [];
|
return store.repeatGroups.filter((group) => structure.repeatGroupSteps[group.groupCode] === store.currentStepId);
|
});
|
const stepProgress = computed<StepProgress[]>(() => {
|
const structure = store.stepStructure;
|
if (!structure) return [];
|
return [...structure.configuration.steps]
|
.sort((left, right) => left.order - right.order)
|
.map((step) => {
|
const belongsToStep = (tag: string) => {
|
return assignedStepId(tag, structure) === step.id;
|
};
|
const stepFields = store.fields.filter((field) => {
|
const tag = parseTag(field.tag);
|
if (tag?.type === 'repeat') {
|
return tag.rowId !== 'prototype' && structure.repeatGroupSteps[tag.groupCode] === step.id;
|
}
|
return belongsToStep(field.tag);
|
});
|
const missingFields = stepFields.filter((field) => {
|
if (/^eln\.ref\./u.test(field.tag)) return false;
|
const empty = field.type === 'checkbox' ? field.draftValue !== true : String(field.draftValue).trim() === '';
|
return (
|
field.requiredKnown === false ||
|
(Boolean(field.required) && empty) ||
|
field.formulaStatus === 'MISSING_INPUT' ||
|
(store.formulaResultsStale && Boolean(field.formulaStatus))
|
);
|
}).length;
|
const missingGroups = store.repeatGroups.filter(
|
(group) =>
|
structure.repeatGroupSteps[group.groupCode] === step.id && group.rows.filter((row) => row.status !== 'DELETE_PREPARED').length < group.minRows,
|
).length;
|
const problemTags = new Set([...Object.keys(store.fieldErrors), ...Object.keys(store.conflicts)].filter(belongsToStep));
|
const formulaErrorCount = stepFields.filter(
|
(field) => !store.formulaResultsStale && !problemTags.has(field.tag) && field.formulaStatus === 'ERROR',
|
).length;
|
const problemCount = problemTags.size + formulaErrorCount;
|
const missingCount = missingFields + missingGroups;
|
return {
|
missingCount,
|
problemCount,
|
status: problemCount ? 'problem' : missingCount ? 'in-progress' : 'complete',
|
step,
|
};
|
});
|
});
|
let context: null | ReturnType<typeof parsePluginContext> = null;
|
let initialized = false;
|
let fillData: FillDataResponse = { fields: {}, repeatGroups: [] };
|
const pendingRepeatOperations = new Map<string, string>();
|
const pendingDeleteOperations = new Map<string, string>();
|
const pendingDeletedValues = new Map<string, FieldChange[]>();
|
let sourceSyncOperationCode = '';
|
const changedControls = new Map<string, string>();
|
let documentChangeTimer: null | ReturnType<typeof setTimeout> = null;
|
let documentWriteDepth = 0;
|
function invalidateUnauthorizedContext(error: unknown): boolean {
|
const status = typeof error === 'object' && error !== null && 'status' in error ? (error as { status?: unknown }).status : undefined;
|
if (status !== 401 && status !== 403) return false;
|
context = null;
|
recordHostContext.value = null;
|
sourceSyncPreview.value = null;
|
sourceSyncOperationCode = '';
|
store.status = 'error';
|
store.message = error instanceof Error ? error.message : '填写凭证无效,请重新打开文档';
|
return true;
|
}
|
const readyTimeout = setTimeout(() => {
|
if (!initialized) {
|
store.status = 'error';
|
store.message = 'ONLYOFFICE SDK 初始化超时';
|
}
|
}, SDK_READY_TIMEOUT_MS);
|
function pendingChanges(confirmedFieldCodes: string[] = []): FieldChange[] {
|
return store.fields
|
.filter((field) => {
|
const parsed = parseTag(field.tag);
|
return field.draftValue !== field.documentValue
|
|| (parsed && parsed.type !== 'repeat' && confirmedFieldCodes.includes(parsed.fieldCode));
|
})
|
.map((field) => ({ tag: field.tag, type: field.type, value: field.draftValue }));
|
}
|
function fieldForControlEvent(control: EditorControlEvent) {
|
const tag = typeof control.Tag === 'string' ? control.Tag : '';
|
const internalId = typeof control.InternalId === 'string' ? control.InternalId : '';
|
if (!tag || !internalId) return undefined;
|
return store.fields.find((field) => field.tag === tag && field.internalId === internalId);
|
}
|
function selectFieldStep(tag: string) {
|
const structure = store.stepStructure;
|
if (!structure) return;
|
const stepId = assignedStepId(tag, structure);
|
if (stepId) store.selectStep(stepId);
|
}
|
async function flushDocumentChanges() {
|
documentChangeTimer = null;
|
if (documentWriteDepth) {
|
scheduleDocumentChanges();
|
return;
|
}
|
const expected = new Map(changedControls);
|
changedControls.clear();
|
try {
|
const fields = await bridge.inspectFields();
|
const selectedTargets = new Map<string, string[]>();
|
for (const [tag, internalId] of expected) {
|
const next = fields.find((field) => field.tag === tag && field.internalId === internalId);
|
const current = store.fields.find((field) => field.tag === tag && field.internalId === internalId);
|
if (!next || !current || next.type !== current.type) continue;
|
if (current.type === 'checkbox' && current.documentValue === false && next.currentValue === true) {
|
const identity = checkboxGroupRuntimeIdentity(current, store.stepStructure);
|
if (identity) selectedTargets.set(identity, [...(selectedTargets.get(identity) ?? []), tag]);
|
}
|
store.syncDocumentField(tag, next.currentValue);
|
}
|
const correctedTags = new Set<string>();
|
for (const [identity, targetTags] of selectedTargets) {
|
if (targetTags.length !== 1) continue;
|
const target = store.fields.find((field) => field.tag === targetTags[0]);
|
if (!target) continue;
|
const accepted = store.setDraft(target.tag, true);
|
if (!accepted) {
|
const notice = store.checkboxGroupNotice;
|
store.setDraft(target.tag, false);
|
store.checkboxGroupNotice = notice;
|
}
|
for (const candidate of store.fields) {
|
if (checkboxGroupRuntimeIdentity(candidate, store.stepStructure) !== identity) continue;
|
if (candidate.draftValue !== candidate.documentValue) correctedTags.add(candidate.tag);
|
}
|
}
|
const changes = [...correctedTags]
|
.flatMap((tag) => {
|
const field = store.fields.find((item) => item.tag === tag);
|
return field && field.draftValue !== field.documentValue
|
? [{ tag: field.tag, type: field.type, value: field.draftValue }]
|
: [];
|
});
|
if (changes.length) {
|
const results = await applyDocumentFields(changes);
|
store.applyResults(results, { preserveExistingErrors: true });
|
if (!results.every((result) => result.ok)) {
|
const verifiedFields = await bridge.inspectFields();
|
for (const change of changes) {
|
const verified = verifiedFields.find((field) => field.tag === change.tag);
|
const current = store.fields.find((field) => field.tag === change.tag);
|
if (verified && current && verified.type === current.type) {
|
store.syncDocumentField(change.tag, verified.currentValue);
|
}
|
}
|
}
|
}
|
store.documentSyncError = '';
|
} catch (error) {
|
store.documentSyncError = error instanceof Error ? error.message : String(error);
|
} finally {
|
if (changedControls.size) scheduleDocumentChanges();
|
}
|
}
|
function scheduleDocumentChanges() {
|
if (documentChangeTimer) clearTimeout(documentChangeTimer);
|
documentChangeTimer = setTimeout(() => void flushDocumentChanges(), DOCUMENT_CHANGE_DEBOUNCE_MS);
|
}
|
function onControlChanged(control: EditorControlEvent) {
|
const field = fieldForControlEvent(control);
|
if (!field) return;
|
changedControls.set(field.tag, field.internalId);
|
scheduleDocumentChanges();
|
}
|
function onControlFocused(control: EditorControlEvent) {
|
const field = fieldForControlEvent(control);
|
if (!field) return;
|
store.focusedTag = field.tag;
|
selectFieldStep(field.tag);
|
}
|
async function applyDocumentFields(changes: FieldChange[]) {
|
documentWriteDepth += 1;
|
try {
|
return await bridge.applyFields(changes);
|
} catch (error) {
|
const message = error instanceof Error ? error.message : String(error);
|
store.fieldErrors = Object.fromEntries(changes.map((change) => [change.tag, message]));
|
throw error;
|
} finally {
|
documentWriteDepth -= 1;
|
if (!documentWriteDepth && changedControls.size) scheduleDocumentChanges();
|
}
|
}
|
const pendingCount = computed(() => pendingChanges().length);
|
const canSynchronizeSource = computed(() => {
|
const initializedStatus = store.status;
|
return Boolean(initializedStatus !== 'initializing' && context?.canFill && context.bizScene === RECORD_FILL_BIZ_SCENE);
|
});
|
const recordHostState = computed(() => {
|
if (!recordHostContext.value) return null;
|
return {
|
bizDataId: recordHostContext.value.bizDataId,
|
documentId: recordHostContext.value.documentId,
|
pendingCount: pendingCount.value,
|
type: 'jnpf:eln-record-fill-state' as const,
|
version: 1 as const,
|
};
|
});
|
async function initialize() {
|
initialized = true;
|
clearTimeout(readyTimeout);
|
try {
|
context = parsePluginContext(bridge.getRuntimeOptions());
|
recordHostContext.value = context.bizScene === RECORD_FILL_BIZ_SCENE ? { bizDataId: context.bizDataId, documentId: context.documentId } : null;
|
if (context.bizScene === RECORD_FILL_BIZ_SCENE && context.canFill) {
|
await bridge.setFillOnlyMode(true);
|
}
|
const [data, fields] = await Promise.all([fetchFillData(context), bridge.inspectFields()]);
|
const pendingSync = data.pendingSourceSync;
|
fillData = data;
|
store.repeatGroups = data.repeatGroups;
|
store.setStepStructure(data.stepStructure);
|
for (const group of data.repeatGroups) {
|
for (const row of group.rows) {
|
if (row.deleteOperationCode) pendingDeleteOperations.set(repeatRowKey(group.groupCode, row.rowId), row.deleteOperationCode);
|
}
|
}
|
store.fields = buildDraftFields(fields, data, { ignoreDataValues: true });
|
store.refreshCheckboxGroupError();
|
if (!store.checkboxGroupError) {
|
store.fields = buildDraftFields(fields, data, { preferData: context.bizScene === RECORD_FILL_BIZ_SCENE });
|
store.refreshCheckboxGroupError();
|
}
|
if (context.bizScene === RECORD_FILL_BIZ_SCENE && context.canFill && !store.checkboxGroupError) {
|
const changes = pendingChanges(pendingSync?.changedFieldCodes);
|
if (changes.length) {
|
const results = await applyDocumentFields(changes);
|
store.applyResults(results);
|
if (!results.every((item) => item.ok)) {
|
store.status = 'error';
|
store.message = '数据库当前态恢复到文档失败';
|
return;
|
}
|
}
|
}
|
store.status = 'ready';
|
store.message = pendingSync && context.canFill
|
? '来源同步已恢复,等待文档保存确认'
|
: context.canFill ? '已连接' : '当前记录仅可查看';
|
} catch (error) {
|
if (!invalidateUnauthorizedContext(error)) {
|
store.status = 'error';
|
store.message = error instanceof Error ? error.message : String(error);
|
}
|
}
|
}
|
async function apply() {
|
if (!context?.canFill) return;
|
if (conflictCount.value || store.checkboxGroupError) return;
|
const changes = pendingChanges();
|
if (!changes.length) return;
|
store.status = 'working';
|
try {
|
const results = await applyDocumentFields(changes);
|
store.applyResults(results);
|
store.status = 'ready';
|
store.message = results.every((item) => item.ok) ? '已应用到文档' : '部分字段应用失败';
|
} catch (error) {
|
store.status = 'error';
|
store.message = error instanceof Error ? error.message : String(error);
|
}
|
}
|
async function previewSourceSync() {
|
if (!context || !canSynchronizeSource.value || store.status !== 'ready') return;
|
sourceSyncLoading.value = true;
|
store.status = 'working';
|
store.sourceSyncError = '';
|
try {
|
sourceSyncPreview.value = await requestSourceSyncPreview(context);
|
sourceSyncOperationCode = '';
|
store.message = sourceSyncPreview.value.changedCount ? '来源快照差异已加载' : '来源快照没有变化';
|
} catch (error) {
|
store.sourceSyncError = error instanceof Error ? error.message : String(error);
|
if (!invalidateUnauthorizedContext(error)) store.message = '来源快照差异加载失败';
|
} finally {
|
if (context) store.status = 'ready';
|
sourceSyncLoading.value = false;
|
}
|
}
|
function synchronizedValue(tag: string, data: FillDataResponse): FillFieldValue | undefined {
|
const source = data.fields[primaryFieldTag(tag) ?? ''];
|
const field = store.fields.find((item) => item.tag === tag);
|
if (!source || !field || source.type !== field.type) return undefined;
|
if (field.type === 'checkbox') return Boolean(source.value);
|
return typeof source.value === 'string' ? source.value : '';
|
}
|
function mergeSynchronizedFields(data: FillDataResponse, changedFieldCodes: Set<string>): FieldChange[] {
|
const changes: FieldChange[] = [];
|
for (const field of store.fields) {
|
const parsed = parseTag(field.tag);
|
if (!parsed || parsed.type === 'repeat' || !changedFieldCodes.has(parsed.fieldCode)) continue;
|
const value = synchronizedValue(field.tag, data);
|
if (value === undefined) throw new Error(`同步后填写数据缺少来源字段:${field.alias}`);
|
const metadata = data.fields[primaryFieldTag(field.tag) ?? ''];
|
field.draftValue = value;
|
field.required = metadata?.required === true;
|
field.requiredKnown = Boolean(metadata);
|
field.formulaStatus = metadata?.formulaStatus;
|
field.formulaMessage = metadata?.formulaMessage;
|
delete store.conflicts[field.tag];
|
// A metadata-only clear still needs a document save callback to confirm the operation.
|
changes.push({ tag: field.tag, type: field.type, value });
|
}
|
return changes;
|
}
|
async function synchronizeSource(clearReasons: Record<string, string> = {}) {
|
const preview = sourceSyncPreview.value;
|
if (!context || !preview || !canSynchronizeSource.value || store.status !== 'ready'
|
|| (!preview.changedCount && !Object.keys(clearReasons).length)) return;
|
sourceSyncLoading.value = true;
|
store.status = 'working';
|
store.sourceSyncError = '';
|
let synchronized = false;
|
try {
|
sourceSyncOperationCode ||= createOperationCode('source-sync');
|
const syncResult = await requestSourceSync(context, {
|
expectedRecordVersion: preview.recordVersion,
|
operationCode: sourceSyncOperationCode,
|
previewToken: preview.previewToken,
|
...(Object.keys(clearReasons).length ? { clearReasons } : {}),
|
});
|
synchronized = true;
|
const nextData = await fetchFillData(context);
|
const changedFieldCodes = new Set(syncResult.changedFieldCodes);
|
const changes = mergeSynchronizedFields(nextData, changedFieldCodes);
|
fillData = nextData;
|
if (changes.length) {
|
const results = await applyDocumentFields(changes);
|
store.applyResults(results);
|
if (!results.every((item) => item.ok)) {
|
throw new Error('部分来源字段写入文档失败,请重试应用');
|
}
|
}
|
store.formulaResultsStale = false;
|
sourceSyncPreview.value = null;
|
sourceSyncOperationCode = '';
|
store.message = '来源快照已写入文档,等待保存确认';
|
} catch (error) {
|
const message = error instanceof Error ? error.message : String(error);
|
store.sourceSyncError = synchronized ? `来源快照已同步,但${message}` : message;
|
if (!invalidateUnauthorizedContext(error)) {
|
store.message = synchronized ? '来源快照已同步,文档刷新失败' : '来源快照同步失败';
|
}
|
} finally {
|
if (context) store.status = 'ready';
|
sourceSyncLoading.value = false;
|
}
|
}
|
function createOperationCode(prefix = 'repeat'): string {
|
const uuid = globalThis.crypto?.randomUUID?.();
|
return `${prefix}-${uuid || `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`}`;
|
}
|
function repeatRowKey(groupCode: string, rowId: string): string {
|
return `${groupCode}\n${rowId}`;
|
}
|
function replaceRepeatRow(row: RepeatRow) {
|
const group = store.repeatGroups.find((item) => item.groupCode === row.groupCode);
|
if (!group) return;
|
const index = group.rows.findIndex((item) => item.rowId === row.rowId);
|
if (index >= 0) group.rows[index] = row;
|
else group.rows.push(row);
|
group.rows.sort((left, right) => left.rowOrder - right.rowOrder);
|
group.activeRowCount = group.rows.length;
|
}
|
async function applyPendingChanges(): Promise<void> {
|
if (conflictCount.value) throw new Error('请先处理字段冲突');
|
if (store.checkboxGroupError) throw new Error('请先处理选择组错误');
|
const changes = pendingChanges();
|
if (!changes.length) return;
|
const results = await applyDocumentFields(changes);
|
store.applyResults(results);
|
if (!results.every((item) => item.ok)) throw new Error('请先处理字段应用失败后再新增行');
|
}
|
async function appendRepeatRow(groupCode: string) {
|
if (!context?.canFill || context.bizScene !== RECORD_FILL_BIZ_SCENE || !groupCode || store.status !== 'ready') return;
|
store.status = 'working';
|
store.repeatError = '';
|
try {
|
await applyPendingChanges();
|
const operationCode = pendingRepeatOperations.get(groupCode) || createOperationCode();
|
pendingRepeatOperations.set(groupCode, operationCode);
|
const row = await requestRepeatRow(context, { groupCode, operationCode });
|
if (row.groupCode !== groupCode || !row.rowId) throw new Error('新增重复行响应与请求不一致');
|
await bridge.appendRepeatRow(groupCode, row.rowId);
|
const fields = await bridge.inspectFields();
|
replaceRepeatRow(row);
|
store.formulaResultsStale = true;
|
store.fields = buildDraftFields(fields, { ...fillData, repeatGroups: store.repeatGroups }, { ignoreDataValues: true });
|
store.refreshCheckboxGroupError();
|
store.fieldErrors = {};
|
store.repeatError = '';
|
pendingRepeatOperations.delete(groupCode);
|
store.status = 'ready';
|
store.message = `已新增第 ${row.rowOrder} 行`;
|
} catch (error) {
|
store.repeatError = error instanceof Error ? error.message : String(error);
|
if (!invalidateUnauthorizedContext(error)) {
|
store.status = 'ready';
|
store.message = '新增重复行失败,可重试';
|
}
|
}
|
}
|
async function deleteRepeatRow(groupCode: string, rowId: string, reason: string) {
|
if (!context?.canFill || context.bizScene !== RECORD_FILL_BIZ_SCENE || store.status !== 'ready') return;
|
const normalizedReason = reason.trim();
|
if (!normalizedReason) {
|
store.repeatError = '删除原因不能为空';
|
return;
|
}
|
store.status = 'working';
|
store.repeatError = '';
|
const key = repeatRowKey(groupCode, rowId);
|
const metadata = store.repeatGroups.find((group) => group.groupCode === groupCode)?.rows.find((row) => row.rowId === rowId);
|
const operationCode = pendingDeleteOperations.get(key) || metadata?.deleteOperationCode || createOperationCode();
|
pendingDeleteOperations.set(key, operationCode);
|
try {
|
await applyPendingChanges();
|
const prefix = `eln.repeat.${groupCode}.${rowId}.`;
|
const rowSnapshot = store.fields
|
.filter((field) => field.tag.startsWith(prefix))
|
.map((field) => ({ tag: field.tag, type: field.type, value: field.draftValue }));
|
const prepared = await requestDeleteRepeatRow(context, { groupCode, operationCode, reason: normalizedReason, rowId });
|
replaceRepeatRow(prepared);
|
store.formulaResultsStale = true;
|
try {
|
await bridge.removeRepeatRow(groupCode, rowId);
|
} catch (documentError) {
|
try {
|
const restored = await requestRestoreRepeatRow(context, { groupCode, operationCode, rowId });
|
replaceRepeatRow(restored);
|
} catch (restoreError) {
|
if (invalidateUnauthorizedContext(restoreError)) throw restoreError;
|
const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError);
|
throw new Error(`${documentError instanceof Error ? documentError.message : String(documentError)};服务端恢复失败:${restoreMessage}`);
|
}
|
throw documentError;
|
}
|
pendingDeletedValues.set(key, rowSnapshot);
|
const fields = await bridge.inspectFields();
|
store.fields = buildDraftFields(fields, { ...fillData, repeatGroups: store.repeatGroups }, { ignoreDataValues: true });
|
store.refreshCheckboxGroupError();
|
store.status = 'ready';
|
store.message = '重复行已标记删除,保存文档后生效';
|
} catch (error) {
|
store.repeatError = error instanceof Error ? error.message : String(error);
|
if (!invalidateUnauthorizedContext(error)) {
|
store.status = 'ready';
|
store.message = '删除重复行失败,可重试';
|
}
|
}
|
}
|
async function restoreRepeatRow(groupCode: string, rowId: string) {
|
if (!context?.canFill || context.bizScene !== RECORD_FILL_BIZ_SCENE || store.status !== 'ready') return;
|
const metadata = store.repeatGroups.find((group) => group.groupCode === groupCode)?.rows.find((row) => row.rowId === rowId);
|
const operationCode = metadata?.deleteOperationCode || pendingDeleteOperations.get(repeatRowKey(groupCode, rowId));
|
if (!metadata || metadata.status !== 'DELETE_PREPARED' || !operationCode) {
|
store.repeatError = '待恢复重复行状态无效';
|
return;
|
}
|
store.status = 'working';
|
store.repeatError = '';
|
const key = repeatRowKey(groupCode, rowId);
|
try {
|
const prefix = `eln.repeat.${groupCode}.${rowId}.`;
|
if (!store.fields.some((field) => field.tag.startsWith(prefix))) {
|
const group = store.repeatGroups.find((item) => item.groupCode === groupCode);
|
const nextRow = group?.rows
|
.filter((row) => row.rowOrder > metadata.rowOrder && store.fields.some((field) => field.tag.startsWith(`eln.repeat.${groupCode}.${row.rowId}.`)))
|
.sort((left, right) => left.rowOrder - right.rowOrder)[0];
|
if (nextRow) await bridge.appendRepeatRow(groupCode, rowId, nextRow.rowId);
|
else await bridge.appendRepeatRow(groupCode, rowId);
|
const fields = await bridge.inspectFields();
|
const restoredFields = buildDraftFields(fields, fillData, { preferData: true });
|
const cachedValues = pendingDeletedValues.get(key);
|
let changes: FieldChange[];
|
if (cachedValues) {
|
changes = cachedValues
|
.map((change) => {
|
const field = restoredFields.find((item) => item.tag === change.tag && item.type === change.type);
|
if (!field) throw new Error('恢复重复行字段结构与删除前不一致');
|
field.draftValue = change.value;
|
return change;
|
})
|
.filter((change) => restoredFields.find((field) => field.tag === change.tag)?.documentValue !== change.value);
|
} else {
|
changes = restoredFields
|
.filter((field) => field.tag.startsWith(prefix) && field.draftValue !== field.documentValue)
|
.map((field) => ({ tag: field.tag, type: field.type, value: field.draftValue }));
|
}
|
if (changes.length) {
|
const results = await applyDocumentFields(changes);
|
store.fieldErrors = Object.fromEntries(results.filter((result) => !result.ok).map((result) => [result.tag, result.error || '应用失败']));
|
for (const result of results) {
|
const field = restoredFields.find((item) => item.tag === result.tag);
|
if (field && result.ok) field.documentValue = field.draftValue;
|
}
|
if (!results.every((item) => item.ok)) throw new Error('恢复重复行字段值失败');
|
}
|
store.fields = restoredFields;
|
store.refreshCheckboxGroupError();
|
}
|
const restored = await requestRestoreRepeatRow(context, { groupCode, operationCode, rowId });
|
replaceRepeatRow(restored);
|
store.formulaResultsStale = true;
|
pendingDeletedValues.delete(key);
|
pendingDeleteOperations.delete(key);
|
store.status = 'ready';
|
store.message = '重复行已恢复';
|
} catch (error) {
|
store.repeatError = error instanceof Error ? error.message : String(error);
|
if (!invalidateUnauthorizedContext(error)) {
|
store.status = 'ready';
|
store.message = '恢复重复行失败,可重试';
|
}
|
}
|
}
|
async function focusField(tag: string) {
|
const field = store.fields.find((item) => item.tag === tag);
|
if (!field) return;
|
try {
|
await bridge.selectControl(field.internalId);
|
store.focusedTag = field.tag;
|
selectFieldStep(field.tag);
|
store.documentSyncError = '';
|
} catch (error) {
|
store.documentSyncError = error instanceof Error ? error.message : String(error);
|
}
|
}
|
async function focusFirstFieldInStep(stepId: string) {
|
const structure = store.stepStructure;
|
if (!structure) return;
|
const field = store.fields.find((item) => {
|
const parsed = parseTag(item.tag);
|
return parsed && parsed.type !== 'reference' && assignedStepId(item.tag, structure) === stepId;
|
});
|
if (field) await focusField(field.tag);
|
}
|
const cleanup = bridge.bindLifecycle({
|
onControlChanged,
|
onControlFocused,
|
onReady: () => void initialize(),
|
onThemeChanged: () => undefined,
|
});
|
return {
|
store,
|
apply,
|
appendRepeatRow,
|
deleteRepeatRow,
|
dispose: () => {
|
clearTimeout(readyTimeout);
|
if (documentChangeTimer) clearTimeout(documentChangeTimer);
|
cleanup();
|
},
|
canFill: () => Boolean(context?.canFill),
|
canSynchronizeSource,
|
currentStepId,
|
conflictCount,
|
focusField,
|
focusFirstFieldInStep,
|
isMultiStep,
|
pendingCount,
|
recordHostState,
|
previewSourceSync,
|
selectStep: (stepId: string) => store.selectStep(stepId),
|
stepProgress,
|
sourceSyncLoading,
|
sourceSyncPreview,
|
synchronizeSource,
|
visibleFields,
|
visibleRepeatGroups,
|
canAppendRepeat: (groupCode?: string) => {
|
if (!context?.canFill || context.bizScene !== RECORD_FILL_BIZ_SCENE) return false;
|
const group = groupCode ? store.repeatGroups.find((item) => item.groupCode === groupCode) : undefined;
|
return !group || group.activeRowCount < group.maxRows;
|
},
|
restoreRepeatRow,
|
};
|
}
|