import type { FormulaControlSnapshot, TemplateAnnotationApiContext, TemplateAnnotationClient } from '../api/template-annotation';
|
import type { PluginContext } from '../domain/plugin-context';
|
import type {
|
TemplateBinding,
|
TemplateChangeCheck,
|
TemplateControlSnapshot,
|
CrossTemplateScalarCandidate,
|
CrossTemplateSourceTemplate,
|
TemplateFieldOption,
|
TemplateCheckboxGroup,
|
TemplateCheckboxGroupDraft,
|
TemplatePrivateFieldDraft,
|
TemplateRepairContext,
|
TemplateRepeatGroup,
|
TemplateStepConfiguration,
|
} from '../domain/types';
|
import type { OnlyOfficeBridge } from '../onlyoffice/bridge';
|
import type {
|
FormulaBindingLockSnapshot,
|
FormulaBindingRequest,
|
FormulaResultLockSnapshot,
|
TemplateFormulaBinding,
|
TemplateFormulaVersion,
|
} from '../domain/formula-mapping';
|
|
import { TemplateAnnotationApiError, templateAnnotationClient } from '../api/template-annotation';
|
import { ref } from 'vue';
|
import { generateOpaqueId } from '../domain/tags';
|
import { buildFormulaBindingRequest } from '../domain/formula-mapping';
|
import { toTemplateFieldOptions } from '../domain/types';
|
import { useAnnotationStore } from '../stores/annotation';
|
import { usePluginContextStore } from '../stores/plugin-context';
|
|
const SDK_READY_TIMEOUT_MS = 5_000;
|
|
function message(error: unknown): string {
|
return error instanceof Error ? error.message : String(error);
|
}
|
function countTag(controls: Array<{ Tag?: string }>, tag: string): number {
|
return controls.filter((item) => item.Tag === tag).length;
|
}
|
function isExpiredTicket(error: unknown): boolean {
|
return error instanceof TemplateAnnotationApiError && (error.status === 401 || error.status === 403);
|
}
|
function isCrossTemplateReadOnlyBinding(binding: TemplateBinding): boolean {
|
return binding.readOnly === true && binding.valueSource === 'CROSS_TEMPLATE';
|
}
|
|
class FormulaConfirmAmbiguousError extends Error {
|
constructor() {
|
super('公式升级确认状态待确认,请刷新后继续');
|
}
|
}
|
|
export function useTemplateAnnotator(bridge: OnlyOfficeBridge, client: TemplateAnnotationClient = templateAnnotationClient) {
|
const store = useAnnotationStore();
|
const contextStore = usePluginContextStore();
|
const bindingState = ref<TemplateBinding[]>([]);
|
const fieldState = ref<TemplateFieldOption[]>([]);
|
const checkboxGroupState = ref<TemplateCheckboxGroup[]>([]);
|
const crossTemplateScalarCandidateState = ref<CrossTemplateScalarCandidate[]>([]);
|
const crossTemplateSourceTemplateState = ref<CrossTemplateSourceTemplate[]>([]);
|
const repeatGroupState = ref<TemplateRepeatGroup[]>([]);
|
const stepConfigurationState = ref<null | TemplateStepConfiguration>(null);
|
const currentStepIdState = ref<null | string>(null);
|
const formulaBindingState = ref<TemplateFormulaBinding[]>([]);
|
const formulaVersionState = ref<TemplateFormulaVersion[]>([]);
|
const pendingRepair = ref<null | TemplateRepairContext>(null);
|
let disposed = false;
|
let inFlight = false;
|
let instanceContext: null | PluginContext = null;
|
let readyTimeout: ReturnType<typeof setTimeout> | undefined;
|
let trackRevisionsDisabled = false;
|
let trackRevisionsPreparation: Promise<void> | undefined;
|
const abortController = new AbortController();
|
const pendingCheckedChanges = new Map<string, TemplateChangeCheck>();
|
let pendingRepairConfirmation: null | TemplateRepairContext = null;
|
|
function apiContext(): TemplateAnnotationApiContext {
|
if (disposed || !instanceContext?.ticket) throw new Error('插件运行上下文不可用');
|
return { apiBaseUrl: instanceContext.apiBaseUrl, ticket: instanceContext.ticket, signal: abortController.signal };
|
}
|
|
function setWorking(text: string, initializing = false): void {
|
if (disposed) return;
|
store.setStatus(initializing ? 'initializing' : 'working');
|
store.setOperation({ kind: 'working', message: text });
|
}
|
|
function clearReadyTimeout(): void {
|
if (readyTimeout !== undefined) clearTimeout(readyTimeout);
|
readyTimeout = undefined;
|
}
|
|
async function prepareDocumentMutation(): Promise<void> {
|
if (trackRevisionsDisabled) return;
|
if (!trackRevisionsPreparation) {
|
trackRevisionsPreparation = bridge
|
.disableTrackRevisions()
|
.then(() => {
|
if (!disposed) trackRevisionsDisabled = true;
|
})
|
.finally(() => {
|
trackRevisionsPreparation = undefined;
|
});
|
}
|
await trackRevisionsPreparation;
|
}
|
|
function applyStepConfiguration(configuration: TemplateStepConfiguration): void {
|
stepConfigurationState.value = configuration;
|
if (configuration.singleStep) {
|
currentStepIdState.value = configuration.steps[0]?.id ?? null;
|
return;
|
}
|
if (!configuration.steps.some((step) => step.id === currentStepIdState.value)) currentStepIdState.value = null;
|
}
|
|
function requireCurrentStepId(): string {
|
if (currentStepIdState.value) return currentStepIdState.value;
|
if (stepConfigurationState.value?.singleStep) throw new Error('模板步骤配置无效');
|
throw new Error('多步骤模板必须选择当前步骤');
|
}
|
|
function setCurrentStepId(stepId: null | string): void {
|
if (stepId === null) {
|
currentStepIdState.value = null;
|
return;
|
}
|
if (!stepConfigurationState.value?.steps.some((step) => step.id === stepId)) throw new Error('当前步骤不存在');
|
currentStepIdState.value = stepId;
|
}
|
|
function expireTicket(error: unknown): void {
|
if (isExpiredTicket(error)) {
|
instanceContext = null;
|
contextStore.reset();
|
store.setStatus('error');
|
}
|
}
|
|
async function exclusive<T>(operation: () => Promise<T>): Promise<T> {
|
if (disposed) throw new Error('插件已关闭');
|
if (inFlight) throw new Error('已有操作正在执行');
|
if (!instanceContext) throw new Error('插件运行上下文不可用');
|
inFlight = true;
|
try {
|
return await operation();
|
} catch (error) {
|
if (!disposed) {
|
expireTicket(error);
|
if (!isExpiredTicket(error) && store.status === 'working') {
|
store.setStatus('ready');
|
store.setOperation({ kind: 'error', message: message(error) });
|
}
|
}
|
throw error;
|
} finally {
|
inFlight = false;
|
}
|
}
|
|
function bindingsForSnapshots(snapshots: TemplateControlSnapshot[]): TemplateBinding[] {
|
return snapshots.map((snapshot) => {
|
const current = bindingState.value.find((item) => item.id === snapshot.bindingId);
|
if (!current) throw new Error(`控件快照绑定不存在:${snapshot.bindingId}`);
|
return {
|
...current,
|
controlAlias: snapshot.controlAlias ?? current.controlAlias,
|
controlConfig: snapshot.controlConfig ?? current.controlConfig,
|
controlRole: snapshot.controlRole ?? current.controlRole,
|
controlTag: snapshot.controlTag,
|
controlType: snapshot.controlType ?? current.controlType,
|
fieldVersionId: snapshot.fieldVersionId ?? current.fieldVersionId,
|
primaryBindingId: snapshot.primaryBindingId ?? null,
|
};
|
});
|
}
|
|
function formulaResultBinding(formula: TemplateFormulaBinding, bindings = bindingState.value): TemplateBinding {
|
const result = bindings.find((item) => item.id === formula.resultBindingId && item.status === 'ACTIVE' && item.controlRole === 'PRIMARY');
|
if (!result) throw new Error('公式结果字段不存在或不是 ACTIVE 主字段');
|
return result;
|
}
|
|
function formulaResultControls(resultBindingId: string, lock: string, bindings = bindingState.value): FormulaControlSnapshot[] {
|
const primary = bindings.find((item) => item.id === resultBindingId && item.status === 'ACTIVE' && item.controlRole === 'PRIMARY');
|
if (!primary) throw new Error('公式结果字段不存在或不是 ACTIVE 主字段');
|
return [primary, ...bindings.filter((item) => item.status === 'ACTIVE' && item.controlRole === 'REFERENCE' && item.primaryBindingId === primary.id)].map(
|
(item) => ({ bindingId: item.id, controlTag: item.controlTag, controlRole: item.controlRole, lock }),
|
);
|
}
|
|
function expectedFormulaVersion(expectedVersions: Record<string, number>, bindingId: string): number {
|
const version = expectedVersions[bindingId];
|
if (typeof version !== 'number') throw new Error('公式映射预检版本缺失');
|
return version;
|
}
|
|
function formulaOriginalLockControls(
|
resultBindingId: string,
|
snapshot: FormulaResultLockSnapshot,
|
bindings = bindingState.value,
|
): FormulaBindingLockSnapshot[] {
|
const expected = formulaResultControls(resultBindingId, '', bindings);
|
const locksByTag = new Map(snapshot.locks.map((item) => [item.tag, item.lock]));
|
if (
|
snapshot.primaryTag !== expected[0]?.controlTag ||
|
locksByTag.size !== expected.length ||
|
snapshot.locks.length !== expected.length ||
|
expected.some((item) => !locksByTag.has(item.controlTag))
|
) {
|
throw new Error('公式结果锁快照与当前绑定不一致');
|
}
|
return expected.map((item) => ({ ...item, lock: locksByTag.get(item.controlTag)! }));
|
}
|
|
function savedFormulaResultLockSnapshot(formula: TemplateFormulaBinding, bindings = bindingState.value): FormulaResultLockSnapshot {
|
const saved = formula.jieguoSuoKuaizhao;
|
const expected = formulaResultControls(formula.resultBindingId, '', bindings);
|
if (!Array.isArray(saved)) throw new Error('公式 PENDING 缺少服务端原始锁快照');
|
const snapshots = new Map(saved.map((item) => [item.bindingId, item]));
|
if (
|
snapshots.size !== expected.length ||
|
saved.length !== expected.length ||
|
expected.some((item) => {
|
const snapshot = snapshots.get(item.bindingId);
|
return !snapshot || snapshot.controlTag !== item.controlTag || snapshot.controlRole !== item.controlRole || typeof snapshot.lock !== 'string';
|
})
|
) {
|
throw new Error('公式 PENDING 服务端原始锁快照不完整');
|
}
|
return {
|
primaryTag: expected[0]!.controlTag,
|
locks: expected.map((item) => ({ tag: item.controlTag, lock: snapshots.get(item.bindingId)!.lock })),
|
};
|
}
|
|
async function reconcilePendingFormulaBindings(formulas: TemplateFormulaBinding[], bindings: TemplateBinding[]): Promise<boolean> {
|
let needsReconciliation = false;
|
for (const formula of formulas.filter((item) => item.status === 'PENDING')) {
|
try {
|
const result = formulaResultBinding(formula, bindings);
|
if (await bridge.isFormulaResultLocked(result.controlTag)) {
|
await client.activateFormulaBinding(apiContext(), formula.id, {
|
controls: formulaResultControls(formula.resultBindingId, 'sdtContentLocked', bindings),
|
expectedVersion: formula.version,
|
operationCode: formula.operationCode,
|
});
|
formula.status = 'ACTIVE';
|
} else {
|
await prepareDocumentMutation();
|
if (disposed) return needsReconciliation;
|
await bridge.restoreFormulaResultLocks(savedFormulaResultLockSnapshot(formula, bindings));
|
await client.cancelFormulaBinding(apiContext(), formula.id, {
|
expectedVersion: formula.version,
|
operationCode: formula.operationCode,
|
reason: 'PENDING_LOCK_INCOMPLETE',
|
});
|
formula.status = 'CANCELLED';
|
}
|
} catch (error) {
|
if (isExpiredTicket(error)) throw error;
|
needsReconciliation = true;
|
store.setOperation({ kind: 'error', message: `公式待确认需对账:${message(error)}` });
|
}
|
}
|
return needsReconciliation;
|
}
|
|
async function reconcilePendingCrossTemplateBindings(bindings: TemplateBinding[], controls: Array<{ InternalId?: string; Lock?: string; Tag?: string }>): Promise<boolean> {
|
let needsReconciliation = false;
|
for (const binding of bindings.filter((item) => item.status === 'PENDING' && isCrossTemplateReadOnlyBinding(item))) {
|
try {
|
const matches = controls.filter((control) => control.Tag === binding.controlTag);
|
if (matches.length === 1 && matches[0]?.Lock === 'sdtContentLocked') {
|
await client.activateBinding(apiContext(), binding.id, { operationCode: generateOpaqueId('cross-template-reconcile') });
|
binding.status = 'ACTIVE';
|
continue;
|
}
|
if (matches.length > 1 || (matches.length === 1 && !matches[0]?.InternalId)) {
|
throw new Error('跨模板 PENDING 控件无法唯一恢复');
|
}
|
if (matches[0]?.InternalId) await bridge.deleteControl(matches[0].InternalId);
|
await client.cancelBinding(apiContext(), binding.id, {
|
operationCode: generateOpaqueId('cross-template-cancel'),
|
reason: 'PENDING_LOCK_INCOMPLETE',
|
});
|
binding.status = 'CANCELLED';
|
} catch (error) {
|
if (isExpiredTicket(error)) throw error;
|
needsReconciliation = true;
|
store.setOperation({ kind: 'error', message: `跨模板字段待确认需对账:${message(error)}` });
|
}
|
}
|
return needsReconciliation;
|
}
|
|
async function confirmCopiedFormulaBindings(
|
formulas: { bindings: TemplateFormulaBinding[]; copiedPendingBindingIds?: string[]; upgradeOperationCode?: string },
|
bindings: TemplateBinding[],
|
): Promise<void> {
|
const copiedIds = new Set(formulas.copiedPendingBindingIds ?? []);
|
if (!copiedIds.size) return;
|
if (!formulas.upgradeOperationCode) throw new Error('公式复制 PENDING 缺少升级操作码');
|
const copied = formulas.bindings.filter((item) => copiedIds.has(item.id));
|
if (copied.length !== copiedIds.size || copied.some((item) => item.status !== 'PENDING')) throw new Error('公式复制 PENDING 上下文不一致');
|
for (const formula of copied) {
|
const result = formulaResultBinding(formula, bindings);
|
if (!(await bridge.isFormulaResultLocked(result.controlTag))) throw new Error('公式复制结果控件未完整锁定');
|
}
|
await client.confirmFormulaVersionCopy(apiContext(), {
|
controls: copied.flatMap((item) => formulaResultControls(item.resultBindingId, 'sdtContentLocked', bindings)),
|
expectedVersions: Object.fromEntries(copied.map((item) => [item.id, item.version])),
|
upgradeOperationCode: formulas.upgradeOperationCode,
|
});
|
formulas.bindings = formulas.bindings.map((item) => (copiedIds.has(item.id) ? { ...item, status: 'ACTIVE' } : item));
|
}
|
|
function confirmation(check: TemplateChangeCheck, controls: Array<{ Tag?: string }>) {
|
return {
|
operationCode: check.operationCode,
|
expectedVersions: check.expectedVersions,
|
controls: bridge.snapshotControls(bindingsForSnapshots(check.controls), controls),
|
observedControlTags: controls.flatMap((control) => (typeof control.Tag === 'string' && control.Tag.trim().length > 0 ? [control.Tag] : [])),
|
};
|
}
|
|
async function locateControl(controls: Array<{ InternalId?: string; Tag?: string }>, locateTag: string): Promise<void> {
|
const matches = controls.filter((item) => item.Tag === locateTag);
|
if (matches.length === 0) throw new Error(`定位目标 Tag 不存在:${locateTag}`);
|
if (matches.length > 1) throw new Error(`定位目标 Tag 出现多次:${locateTag}`);
|
if (!matches[0]?.InternalId) throw new Error(`定位目标控件缺少 InternalId:${locateTag}`);
|
await bridge.selectControl(matches[0].InternalId, abortController.signal);
|
}
|
|
async function confirmCopiedBindings(
|
context: Awaited<ReturnType<TemplateAnnotationClient['fetchAnnotationContext']>>,
|
formulas: Awaited<ReturnType<TemplateAnnotationClient['fetchFormulaMappings']>>,
|
controls: Array<{ Tag?: string }>,
|
): Promise<boolean> {
|
const copiedIds = new Set(context.copiedPendingBindingIds || []);
|
const pending = [...copiedIds].map((id) => {
|
const binding = context.bindings.find((item) => item.id === id);
|
if (!binding || binding.status !== 'PENDING') throw new Error('复制绑定上下文不一致');
|
return binding;
|
});
|
if (!pending.length) return false;
|
if (!context.upgradeOperationCode) throw new Error('复制绑定缺少升版操作码');
|
const inheritedFormulaLockBindingIds = new Set(formulas.inheritedFormulaLockBindingIds ?? []);
|
const snapshots = bridge.snapshotControls(pending, controls, { inheritedFormulaLockBindingIds });
|
if (snapshots.length !== pending.length) throw new Error('升版复制控件快照不完整');
|
await client.confirmVersionCopy(apiContext(), {
|
upgradeOperationCode: context.upgradeOperationCode,
|
expectedVersions: Object.fromEntries(pending.map((item) => [item.id, item.version])),
|
controls: snapshots,
|
});
|
if (disposed) return true;
|
context.bindings = context.bindings.map((item) => (copiedIds.has(item.id) ? { ...item, status: 'ACTIVE' } : item));
|
return true;
|
}
|
|
async function initialize(): Promise<void> {
|
clearReadyTimeout();
|
setWorking('正在读取模板标注...', true);
|
try {
|
const locateTag = instanceContext?.locateControlTag;
|
if (locateTag) {
|
const controls = await bridge.listControls();
|
if (disposed) return;
|
await locateControl(controls, locateTag);
|
if (disposed) return;
|
store.setControls(controls);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '目标控件已定位' });
|
return;
|
}
|
await prepareDocumentMutation();
|
if (disposed) return;
|
const controls = await bridge.listControls();
|
if (disposed) return;
|
const context = await client.fetchAnnotationContext(apiContext());
|
if (disposed) return;
|
let formulaContext = await client.fetchFormulaMappings(apiContext());
|
if (disposed) return;
|
const copiedBindingsConfirmed = await confirmCopiedBindings(context, formulaContext, controls);
|
if (disposed) return;
|
if (copiedBindingsConfirmed) formulaContext = await client.fetchFormulaMappings(apiContext());
|
if (disposed) return;
|
bindingState.value = [...context.bindings];
|
fieldState.value = toTemplateFieldOptions(context.fields);
|
checkboxGroupState.value = [...(context.checkboxGroups ?? [])];
|
repeatGroupState.value = [...(context.repeatGroups ?? [])];
|
applyStepConfiguration(context.stepConfiguration);
|
formulaBindingState.value = [...formulaContext.bindings];
|
formulaVersionState.value = [...formulaContext.versions];
|
const crossTemplateNeedsReconciliation = await reconcilePendingCrossTemplateBindings(bindingState.value, controls);
|
if (disposed) return;
|
await confirmCopiedFormulaBindings(formulaContext, bindingState.value);
|
if (disposed) return;
|
formulaBindingState.value = [...formulaContext.bindings];
|
const formulaNeedsReconciliation = await reconcilePendingFormulaBindings(formulaBindingState.value, bindingState.value);
|
if (disposed) return;
|
store.setControls(controls);
|
store.setStatus('ready');
|
if (!formulaNeedsReconciliation && !crossTemplateNeedsReconciliation) store.setOperation({ kind: 'success', message: '模板标注已加载' });
|
if (instanceContext?.repairBindingId) await prepareRepairMode(instanceContext.repairBindingId, controls);
|
} catch (error) {
|
if (!disposed) {
|
expireTicket(error);
|
store.setStatus('error');
|
store.setOperation({ kind: 'error', message: `读取模板标注失败:${message(error)}` });
|
}
|
throw error;
|
}
|
}
|
|
async function prepareRepairMode(bindingId: string, controls: Array<{ InternalId?: string; Tag?: string }>): Promise<void> {
|
const current = bindingState.value.find((item) => item.id === bindingId && item.status === 'ACTIVE');
|
if (!current) throw new Error('修复绑定不存在或状态已变化');
|
const matches = controls.filter((item) => item.Tag === current.controlTag);
|
if (matches.length > 1) throw new Error('修复目标 Tag 在文档中出现多次');
|
if (matches.length === 1) {
|
if (matches[0]?.InternalId) await bridge.selectControl(matches[0].InternalId, abortController.signal);
|
if (disposed) return;
|
pendingRepair.value = null;
|
store.setOperation({ kind: 'success', message: '目标控件已定位' });
|
return;
|
}
|
const check = await client.fetchRepairContext(apiContext(), bindingId);
|
if (disposed) return;
|
if (check.binding.id !== bindingId || check.binding.controlTag !== current.controlTag) throw new Error('修复上下文与目标绑定不一致');
|
pendingRepair.value = check;
|
store.setOperation({ kind: 'success', message: '请在文档中选择位置后恢复控件' });
|
}
|
|
async function refresh(): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在刷新模板标注...');
|
try {
|
const controls = await bridge.listControls();
|
if (disposed) return;
|
const locateTag = instanceContext?.locateControlTag;
|
if (locateTag) {
|
await locateControl(controls, locateTag);
|
if (disposed) return;
|
store.setControls(controls);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '目标控件已定位' });
|
return;
|
}
|
const context = await client.fetchAnnotationContext(apiContext());
|
if (disposed) return;
|
const formulaContext = await client.fetchFormulaMappings(apiContext());
|
if (disposed) return;
|
bindingState.value = [...context.bindings];
|
fieldState.value = toTemplateFieldOptions(context.fields);
|
checkboxGroupState.value = [...(context.checkboxGroups ?? [])];
|
repeatGroupState.value = [...(context.repeatGroups ?? [])];
|
applyStepConfiguration(context.stepConfiguration);
|
formulaBindingState.value = [...formulaContext.bindings];
|
formulaVersionState.value = [...formulaContext.versions];
|
store.setControls(controls);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '模板标注已刷新' });
|
} catch (error) {
|
if (!disposed) store.setOperation({ kind: 'error', message: `刷新失败:${message(error)}` });
|
throw error;
|
}
|
});
|
}
|
|
function loadCrossTemplateSourceTemplates(): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在读取来源模板...');
|
crossTemplateScalarCandidateState.value = [];
|
const templates = await client.fetchCrossTemplateSourceTemplates(apiContext());
|
if (disposed) return;
|
crossTemplateSourceTemplateState.value = Array.isArray(templates) ? templates : [];
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '来源模板已加载' });
|
});
|
}
|
|
function loadCrossTemplateScalarCandidates(sourceTemplateId: string): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在读取模板字段...');
|
crossTemplateScalarCandidateState.value = [];
|
const candidates = await client.fetchCrossTemplateScalarCandidates(apiContext(), sourceTemplateId);
|
if (disposed) return;
|
crossTemplateScalarCandidateState.value = Array.isArray(candidates) ? candidates : [];
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '模板字段已加载' });
|
});
|
}
|
|
async function insertReserved(reserve: () => Promise<TemplateBinding>, referencePrimaryTag?: string, readOnly = false): Promise<void> {
|
let reserved: TemplateBinding | undefined;
|
let insertedControl: { InternalId?: string; Lock?: string; Tag?: string } | undefined;
|
let readOnlyInsertAttempted = false;
|
let activationAttempted = false;
|
let confirmedControls: Array<{ InternalId?: string; Lock?: string; Tag?: string }> | undefined;
|
try {
|
reserved = await reserve();
|
if (disposed) return;
|
const before = await bridge.listControls();
|
if (disposed) return;
|
if (countTag(before, reserved.controlTag) !== 0) throw new Error('目标 Tag 已存在');
|
await prepareDocumentMutation();
|
if (disposed) return;
|
if (referencePrimaryTag) await bridge.insertReference(reserved, referencePrimaryTag, abortController.signal);
|
else if (readOnly) {
|
readOnlyInsertAttempted = true;
|
await bridge.insertReadOnlyBinding(reserved, abortController.signal);
|
}
|
else await bridge.insertBinding(reserved, abortController.signal);
|
// 销毁后不再补偿写 DOCX/API;PENDING 残留由重开对账与模板体检处理。
|
if (disposed) return;
|
const after = await bridge.listControls();
|
if (disposed) return;
|
if (countTag(after, reserved.controlTag) !== 1) throw new Error('插入后 Tag 计数不是 1');
|
if (readOnly) {
|
insertedControl = after.find((control) => control.Tag === reserved!.controlTag);
|
if (insertedControl?.Lock !== 'sdtContentLocked') throw new Error('跨模板字段控件未锁定');
|
}
|
confirmedControls = after;
|
activationAttempted = true;
|
await client.activateBinding(apiContext(), reserved.id, { operationCode: generateOpaqueId('activate') });
|
if (disposed) return;
|
bindingState.value = [...bindingState.value, { ...reserved, status: 'ACTIVE' }];
|
store.setControls(after);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: `已标注字段:${reserved.controlAlias || reserved.fieldCode}` });
|
} catch (error) {
|
let rollbackError: unknown;
|
let preserveForReconciliation = false;
|
if (!disposed && readOnly && activationAttempted && reserved && !isExpiredTicket(error)) {
|
try {
|
const context = await client.fetchAnnotationContext(apiContext());
|
if (disposed) return;
|
const persisted = context.bindings.find((binding) => binding.id === reserved!.id);
|
if (persisted?.status === 'ACTIVE' && isCrossTemplateReadOnlyBinding(persisted)) {
|
bindingState.value = [...bindingState.value.filter((binding) => binding.id !== persisted.id), persisted];
|
store.setControls(confirmedControls ?? []);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: `已标注字段:${persisted.controlAlias || persisted.fieldCode}` });
|
return;
|
}
|
if (!persisted || persisted.status !== 'PENDING') {
|
preserveForReconciliation = true;
|
rollbackError = new Error('跨模板字段激活结果待对账');
|
}
|
} catch (statusError) {
|
if (isExpiredTicket(statusError)) throw statusError;
|
preserveForReconciliation = true;
|
rollbackError = new Error('跨模板字段激活结果待对账');
|
}
|
}
|
if (!disposed && readOnlyInsertAttempted && !insertedControl && reserved && !isExpiredTicket(error)) {
|
try {
|
const controls = await bridge.listControls();
|
const matches = controls.filter((control) => control.Tag === reserved!.controlTag);
|
if (matches.length > 1 || (matches.length === 1 && !matches[0]?.InternalId)) {
|
throw new Error('跨模板失败控件无法唯一补偿');
|
}
|
insertedControl = matches[0];
|
} catch (enumerateError) {
|
rollbackError = enumerateError;
|
}
|
}
|
if (!disposed && insertedControl?.InternalId && !rollbackError && !isExpiredTicket(error)) {
|
try {
|
await bridge.deleteControl(insertedControl.InternalId);
|
} catch (deleteError) {
|
rollbackError = deleteError;
|
}
|
}
|
if (!disposed && reserved?.status === 'PENDING' && !rollbackError && !isExpiredTicket(error)) {
|
try {
|
await client.cancelBinding(apiContext(), reserved.id, { operationCode: generateOpaqueId('cancel'), reason: message(error) });
|
} catch {
|
/* 保留原始错误 */
|
}
|
}
|
if (!disposed) {
|
store.setStatus(contextStore.context ? 'ready' : 'error');
|
store.setOperation({
|
kind: 'error',
|
message: preserveForReconciliation
|
? message(rollbackError)
|
: rollbackError
|
? `标注失败,控件删除待对账:${message(rollbackError)}`
|
: `标注失败:${message(error)}`,
|
});
|
}
|
throw preserveForReconciliation ? rollbackError : error;
|
}
|
}
|
|
function insertSelectedField(field: TemplateFieldOption, checkboxGroup?: TemplateCheckboxGroupDraft): Promise<void> {
|
return exclusive(async () => {
|
if (bindingState.value.some((item) => item.status === 'ACTIVE' && item.controlRole === 'PRIMARY' && item.fieldId === field.fieldId))
|
throw new Error('该字段已有主控件,请插入引用');
|
const stepId = requireCurrentStepId();
|
setWorking('正在标注当前位置...');
|
await insertReserved(() =>
|
client.reservePrimaryBinding(apiContext(), {
|
checkboxGroupCode: field.type === 'CHECKBOX' ? checkboxGroup?.groupCode.trim() || undefined : undefined,
|
checkboxGroupMaxSelected: field.type === 'CHECKBOX' ? checkboxGroup?.maxSelected : undefined,
|
checkboxGroupMinSelected: field.type === 'CHECKBOX' ? checkboxGroup?.minSelected : undefined,
|
fieldId: field.fieldId,
|
fieldVersionId: field.fieldVersionId,
|
operationCode: generateOpaqueId('reserve'),
|
repeatRequest: false,
|
stepId,
|
}),
|
);
|
});
|
}
|
|
function suggestCode(text: string): Promise<string> {
|
return client.suggestCode(apiContext(), { text });
|
}
|
|
function insertRepeatField(field: TemplateFieldOption, group: TemplateRepeatGroup, checkboxGroup?: TemplateCheckboxGroupDraft): Promise<void> {
|
return exclusive(async () => {
|
const normalizedGroup = group.groupCode.trim();
|
const { maxRows, minRows } = group;
|
if (!/^[\w-]{1,80}$/u.test(normalizedGroup)) throw new Error('重复组编码无效');
|
if (!Number.isInteger(minRows) || !Number.isInteger(maxRows) || minRows < 0 || maxRows < minRows || maxRows > 1000) throw new Error('重复组行数约束无效');
|
const existingGroup = repeatGroupState.value.find((item) => item.groupCode === normalizedGroup);
|
if (existingGroup && (!existingGroup.stepId || !stepConfigurationState.value?.steps.some((step) => step.id === existingGroup.stepId))) {
|
throw new Error('重复组步骤归属无效');
|
}
|
const stepId = existingGroup?.stepId ?? requireCurrentStepId();
|
setWorking('正在标注重复行...');
|
await insertReserved(() =>
|
client.reservePrimaryBinding(apiContext(), {
|
checkboxGroupCode: field.type === 'CHECKBOX' ? checkboxGroup?.groupCode.trim() || undefined : undefined,
|
checkboxGroupMaxSelected: field.type === 'CHECKBOX' ? checkboxGroup?.maxSelected : undefined,
|
checkboxGroupMinSelected: field.type === 'CHECKBOX' ? checkboxGroup?.minSelected : undefined,
|
fieldId: field.fieldId,
|
fieldVersionId: field.fieldVersionId,
|
maxRows,
|
minRows,
|
operationCode: generateOpaqueId('repeat-reserve'),
|
repeatGroupCode: normalizedGroup,
|
repeatPrototypeRowId: 'prototype',
|
repeatRequest: true,
|
stepId,
|
}),
|
);
|
if (!disposed && !repeatGroupState.value.some((item) => item.groupCode === normalizedGroup)) {
|
repeatGroupState.value = [...repeatGroupState.value, { groupCode: normalizedGroup, minRows, maxRows, stepId }].sort((left, right) =>
|
left.groupCode.localeCompare(right.groupCode),
|
);
|
}
|
});
|
}
|
|
function changeCheckboxGroup(bindingId: string, checkboxGroup: null | TemplateCheckboxGroupDraft, expectedVersion: number): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在更新选择组...');
|
const updated = await client.changeCheckboxGroup(apiContext(), bindingId, {
|
checkboxGroupCode: checkboxGroup?.groupCode.trim() || null,
|
checkboxGroupMaxSelected: checkboxGroup?.maxSelected,
|
checkboxGroupMinSelected: checkboxGroup?.minSelected,
|
expectedVersion,
|
operationCode: generateOpaqueId('checkbox-group'),
|
});
|
if (disposed) return;
|
bindingState.value = bindingState.value.map((binding) => (binding.id === updated.id ? updated : binding));
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: updated.checkboxGroupCode ? `已加入选择组:${updated.checkboxGroupCode}` : '已移出选择组' });
|
});
|
}
|
|
function changeCheckboxGroupRule(
|
groupCode: string,
|
minSelected: number,
|
maxSelected: null | number,
|
expectedVersion: number,
|
): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在更新选择组规则...');
|
const updated = await client.changeCheckboxGroupRule(apiContext(), groupCode, {
|
expectedVersion,
|
maxSelected,
|
minSelected,
|
operationCode: generateOpaqueId('checkbox-group-rule'),
|
});
|
if (disposed) return;
|
checkboxGroupState.value = checkboxGroupState.value.map((group) => (group.groupCode === updated.groupCode ? updated : group));
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: `已更新选择组规则:${updated.groupCode}` });
|
});
|
}
|
|
function insertCrossTemplateScalar(candidate: CrossTemplateScalarCandidate): Promise<void> {
|
return exclusive(async () => {
|
const stepId = requireCurrentStepId();
|
setWorking('正在标注跨模板字段值...');
|
await insertReserved(
|
() =>
|
client.reserveCrossTemplateScalarBinding(apiContext(), {
|
operationCode: generateOpaqueId('cross-template-reserve'),
|
repeatRequest: false,
|
sourceFieldId: candidate.sourceFieldId,
|
sourceFieldVersionId: candidate.sourceFieldVersionId,
|
sourceTemplateCode: candidate.sourceTemplateCode,
|
sourceTemplateId: candidate.sourceTemplateId,
|
stepId,
|
}),
|
undefined,
|
true,
|
);
|
});
|
}
|
|
function createPrivateField(draft: TemplatePrivateFieldDraft): Promise<void> {
|
return exclusive(async () => {
|
const stepId = requireCurrentStepId();
|
setWorking('正在创建本模板字段...');
|
await insertReserved(() => client.reservePrivateField(apiContext(), { ...draft, operationCode: generateOpaqueId('private'), stepId }));
|
if (disposed) return;
|
const context = await client.fetchAnnotationContext(apiContext());
|
if (disposed) return;
|
bindingState.value = [...context.bindings];
|
fieldState.value = toTemplateFieldOptions(context.fields);
|
checkboxGroupState.value = [...(context.checkboxGroups ?? [])];
|
repeatGroupState.value = [...(context.repeatGroups ?? [])];
|
applyStepConfiguration(context.stepConfiguration);
|
});
|
}
|
|
function insertReference(primaryBindingId: string): Promise<void> {
|
return exclusive(async () => {
|
const primary = bindingState.value.find((item) => item.id === primaryBindingId && item.status === 'ACTIVE' && item.controlRole === 'PRIMARY');
|
if (!primary) throw new Error('主字段绑定不存在或状态已变化');
|
setWorking('正在插入字段引用...');
|
await insertReserved(
|
() => client.reserveReferenceBinding(apiContext(), primary.id, { operationCode: generateOpaqueId('reference') }),
|
primary.controlTag,
|
);
|
});
|
}
|
|
function reserveFormulaBinding(request: Omit<FormulaBindingRequest, 'operationCode'>): Promise<void> {
|
return exclusive(async () => {
|
const resultBinding = formulaResultBinding({ ...request, id: '', operationCode: '', status: 'PENDING', version: 0 });
|
setWorking('正在锁定公式结果字段...');
|
let reserved: TemplateFormulaBinding | undefined;
|
let lockSnapshot: FormulaResultLockSnapshot | undefined;
|
try {
|
await prepareDocumentMutation();
|
if (disposed) return;
|
lockSnapshot = await bridge.lockFormulaResult(resultBinding.controlTag);
|
if (disposed) return;
|
reserved = await client.reserveFormulaBinding(apiContext(), {
|
...buildFormulaBindingRequest({ ...request, operationCode: generateOpaqueId('formula-reserve') }),
|
jieguoSuoKuaizhao: formulaOriginalLockControls(request.resultBindingId, lockSnapshot),
|
});
|
if (disposed) return;
|
await client.activateFormulaBinding(apiContext(), reserved.id, {
|
controls: formulaResultControls(reserved.resultBindingId, 'sdtContentLocked'),
|
expectedVersion: reserved.version,
|
operationCode: reserved.operationCode,
|
});
|
if (disposed) return;
|
formulaBindingState.value = [...formulaBindingState.value, { ...reserved, status: 'ACTIVE' }];
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '公式映射已启用' });
|
} catch (error) {
|
let recoveryError: unknown;
|
if (!disposed && lockSnapshot && !isExpiredTicket(error)) {
|
try {
|
await bridge.restoreFormulaResultLocks(lockSnapshot);
|
} catch (restoreError) {
|
recoveryError = restoreError;
|
}
|
}
|
if (!disposed && reserved?.status === 'PENDING' && !recoveryError && !isExpiredTicket(error)) {
|
try {
|
await client.cancelFormulaBinding(apiContext(), reserved.id, {
|
expectedVersion: reserved.version,
|
operationCode: reserved.operationCode,
|
reason: message(error),
|
});
|
} catch {
|
/* 保留原始失败 */
|
}
|
}
|
if (!disposed) {
|
store.setStatus(contextStore.context ? 'ready' : 'error');
|
store.setOperation({
|
kind: 'error',
|
message: recoveryError ? `公式映射失败,结果锁需对账:${message(recoveryError)}` : `公式映射失败:${message(error)}`,
|
});
|
}
|
throw error;
|
}
|
});
|
}
|
|
function repairBindingId(bindingId: string): Promise<void> {
|
return exclusive(async () => {
|
const current = bindingState.value.find((item) => item.id === bindingId);
|
if (!current) throw new Error('修复绑定不存在');
|
setWorking('正在恢复控件...');
|
const before = await bridge.listControls();
|
if (disposed) return;
|
const matches = before.filter((item) => item.Tag === current.controlTag);
|
if (matches.length > 1) throw new Error('修复目标 Tag 在文档中出现多次');
|
if (matches.length === 1) {
|
if (!matches[0]?.InternalId) throw new Error('修复目标控件缺少 InternalId');
|
const retry = pendingRepairConfirmation?.binding.id === bindingId ? pendingRepairConfirmation : null;
|
if (retry) {
|
try {
|
await client.confirmRepair(apiContext(), bindingId, confirmation(retry, before));
|
pendingRepairConfirmation = null;
|
} catch (error) {
|
if (!(error instanceof TypeError)) pendingRepairConfirmation = null;
|
throw error;
|
}
|
if (disposed) return;
|
}
|
await bridge.selectControl(matches[0].InternalId, abortController.signal);
|
if (disposed) return;
|
if (!disposed) {
|
pendingRepair.value = null;
|
store.setControls(before);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: retry ? '控件已恢复' : '目标控件已定位' });
|
}
|
return;
|
}
|
let check = pendingRepair.value?.binding.id === bindingId ? pendingRepair.value : null;
|
if (!check) {
|
check = await client.fetchRepairContext(apiContext(), bindingId);
|
if (disposed) return;
|
}
|
await prepareDocumentMutation();
|
if (disposed) return;
|
await bridge.insertBinding(check.binding, abortController.signal);
|
if (disposed) return;
|
const after = await bridge.listControls();
|
if (disposed) return;
|
if (countTag(after, check.binding.controlTag) !== 1) throw new Error('修复后 Tag 计数不是 1');
|
pendingRepairConfirmation = check;
|
try {
|
await client.confirmRepair(apiContext(), bindingId, confirmation(check, after));
|
pendingRepairConfirmation = null;
|
} catch (error) {
|
if (!(error instanceof TypeError)) pendingRepairConfirmation = null;
|
throw error;
|
}
|
if (disposed) return;
|
if (!disposed) {
|
pendingRepair.value = null;
|
store.setControls(after);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '控件已恢复' });
|
}
|
});
|
}
|
|
function removeFormulaBinding(bindingId: string): Promise<void> {
|
return exclusive(async () => {
|
const current = formulaBindingState.value.find((item) => item.id === bindingId && item.status === 'ACTIVE');
|
if (!current) throw new Error('公式映射不存在或状态已变化');
|
const resultBinding = formulaResultBinding(current);
|
setWorking('正在解除公式结果锁...');
|
const check = await client.checkFormulaBindingRemoval(apiContext(), bindingId);
|
if (check.downstreamFormulaBindingIds.length > 0) {
|
throw new Error('公式结果仍被下游公式引用');
|
}
|
await prepareDocumentMutation();
|
if (disposed) return;
|
const lockedSnapshot = await bridge.lockFormulaResult(resultBinding.controlTag);
|
try {
|
await bridge.unlockFormulaResult(resultBinding.controlTag);
|
await client.confirmFormulaBindingRemoval(apiContext(), bindingId, {
|
controls: formulaResultControls(current.resultBindingId, ''),
|
expectedVersion: expectedFormulaVersion(check.expectedVersions, bindingId),
|
operationCode: check.operationCode,
|
});
|
formulaBindingState.value = formulaBindingState.value.map((item) => (item.id === bindingId ? { ...item, status: 'REMOVED' } : item));
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '公式映射已移除' });
|
} catch (error) {
|
if (!isExpiredTicket(error))
|
try {
|
await bridge.restoreFormulaResultLocks(lockedSnapshot);
|
} catch (restoreError) {
|
store.setOperation({ kind: 'error', message: `公式结果锁恢复失败,模板发布将被阻止:${message(restoreError)}` });
|
}
|
throw error;
|
}
|
});
|
}
|
|
function upgradeFormulaBinding(bindingId: string, request: Omit<FormulaBindingRequest, 'operationCode'>): Promise<void> {
|
return exclusive(async () => {
|
const current = formulaBindingState.value.find((item) => item.id === bindingId && item.status === 'ACTIVE');
|
if (!current) throw new Error('公式映射不存在或状态已变化');
|
setWorking('正在升级公式映射...');
|
const check = await client.checkFormulaBindingUpgrade(
|
apiContext(),
|
bindingId,
|
buildFormulaBindingRequest({ ...request, operationCode: generateOpaqueId('formula-upgrade') }),
|
);
|
const targetResult = formulaResultBinding({ ...request, id: bindingId, operationCode: '', status: 'ACTIVE', version: current.version });
|
const currentResult = formulaResultBinding(current);
|
await prepareDocumentMutation();
|
if (disposed) return;
|
const snapshots = [await bridge.lockFormulaResult(targetResult.controlTag)];
|
if (targetResult.controlTag !== currentResult.controlTag) {
|
try {
|
const oldLocked = await bridge.lockFormulaResult(currentResult.controlTag);
|
snapshots.push(oldLocked);
|
await bridge.unlockFormulaResult(currentResult.controlTag);
|
} catch (error) {
|
if (!isExpiredTicket(error)) await bridge.restoreFormulaResultLocks(snapshots[0]!);
|
throw error;
|
}
|
}
|
try {
|
const confirm = {
|
controls: formulaResultControls(request.resultBindingId, 'sdtContentLocked'),
|
expectedVersion: expectedFormulaVersion(check.expectedVersions, bindingId),
|
operationCode: check.operationCode,
|
target: buildFormulaBindingRequest({ ...request, operationCode: check.operationCode }),
|
};
|
try {
|
await client.confirmFormulaBindingUpgrade(apiContext(), bindingId, confirm);
|
} catch (error) {
|
if (!(error instanceof TypeError)) throw error;
|
try {
|
await client.confirmFormulaBindingUpgrade(apiContext(), bindingId, confirm);
|
} catch (retryError) {
|
if (retryError instanceof TypeError) throw new FormulaConfirmAmbiguousError();
|
throw retryError;
|
}
|
}
|
formulaBindingState.value = formulaBindingState.value.map((item) =>
|
item.id === bindingId ? { ...item, ...request, operationCode: check.operationCode, status: 'ACTIVE' } : item,
|
);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '公式映射已升级' });
|
} catch (error) {
|
if (!isExpiredTicket(error) && !(error instanceof FormulaConfirmAmbiguousError))
|
for (const snapshot of snapshots) {
|
try {
|
await bridge.restoreFormulaResultLocks(snapshot);
|
} catch {
|
/* 保留后端失败并要求后续对账 */
|
}
|
}
|
if (!disposed && error instanceof FormulaConfirmAmbiguousError) {
|
store.setStatus('ready');
|
store.setOperation({ kind: 'error', message: `公式升级待对账:${message(error)}` });
|
}
|
throw error;
|
}
|
});
|
}
|
|
async function executeCheckedChange(
|
operationKey: string,
|
bindingId: string,
|
operationLabel: string,
|
check: () => Promise<TemplateChangeCheck>,
|
confirm: (form: ReturnType<typeof confirmation>) => Promise<void>,
|
): Promise<void> {
|
try {
|
const result = pendingCheckedChanges.get(operationKey) ?? (await check());
|
if (disposed) return;
|
await prepareDocumentMutation();
|
if (disposed) return;
|
await bridge.applyControlPlan(bindingState.value, result.controls, abortController.signal);
|
if (disposed) return;
|
const controls = await bridge.listControls();
|
if (disposed) return;
|
const form = confirmation(result, controls);
|
try {
|
await confirm(form);
|
pendingCheckedChanges.delete(operationKey);
|
} catch (error) {
|
if (!disposed && error instanceof TypeError) pendingCheckedChanges.set(operationKey, result);
|
else pendingCheckedChanges.delete(operationKey);
|
throw error;
|
}
|
if (disposed) return;
|
const desiredIds = new Set(result.controls.map((item) => item.bindingId));
|
const affectedIds = new Set([bindingId, ...Object.keys(result.expectedVersions)]);
|
bindingState.value = bindingState.value.map((item) =>
|
desiredIds.has(item.id)
|
? bindingsForSnapshots(result.controls).find((value) => value.id === item.id)!
|
: affectedIds.has(item.id) && item.status === 'ACTIVE'
|
? { ...item, status: 'REMOVED' }
|
: item,
|
);
|
store.setControls(controls);
|
} catch (error) {
|
if (!disposed) store.setOperation({ kind: 'error', message: `${operationLabel}失败:${message(error)}` });
|
throw error;
|
}
|
}
|
|
function removeBinding(bindingId: string): Promise<void> {
|
return exclusive(async () =>
|
executeCheckedChange(
|
`remove:${bindingId}`,
|
bindingId,
|
'删除',
|
() => client.checkBindingRemoval(apiContext(), bindingId),
|
(form) => client.confirmBindingRemoval(apiContext(), bindingId, form),
|
),
|
);
|
}
|
|
function removeOrphanControl(internalId: string): Promise<void> {
|
return exclusive(async () => {
|
setWorking('正在删除异常控件...');
|
await prepareDocumentMutation();
|
if (disposed) return;
|
const controls = await bridge.deleteControl(internalId);
|
if (disposed) return;
|
store.setControls(controls);
|
store.setStatus('ready');
|
store.setOperation({ kind: 'success', message: '异常控件已删除' });
|
});
|
}
|
|
function promoteReference(bindingId: string): Promise<void> {
|
return exclusive(async () =>
|
executeCheckedChange(
|
`promote:${bindingId}`,
|
bindingId,
|
'设为主字段',
|
() => client.checkReferencePromotion(apiContext(), bindingId),
|
(form) => client.confirmReferencePromotion(apiContext(), bindingId, form),
|
),
|
);
|
}
|
|
function upgradeBinding(bindingId: string, targetFieldVersionId: string): Promise<void> {
|
return exclusive(async () =>
|
executeCheckedChange(
|
`upgrade:${bindingId}:${targetFieldVersionId}`,
|
bindingId,
|
'字段升级',
|
() => client.checkBindingVersionUpgrade(apiContext(), bindingId, { targetFieldVersionId }),
|
(form) => client.confirmBindingVersionUpgrade(apiContext(), bindingId, { ...form, targetFieldVersionId }),
|
),
|
);
|
}
|
|
function select(internalId: string): Promise<void> {
|
return exclusive(() => bridge.selectControl(internalId, abortController.signal));
|
}
|
|
const cleanup = bridge.bindLifecycle({
|
onReady: () => {
|
if (disposed || !contextStore.initialize(bridge.getRuntimeOptions())) {
|
if (!disposed) {
|
store.setStatus('error');
|
store.setOperation({ kind: 'error', message: contextStore.error });
|
}
|
return;
|
}
|
instanceContext = contextStore.context;
|
void initialize().catch(() => undefined);
|
},
|
onThemeChanged: () => undefined,
|
});
|
readyTimeout = setTimeout(() => {
|
readyTimeout = undefined;
|
if (!disposed && store.status === 'initializing') {
|
store.setStatus('error');
|
store.setOperation({ kind: 'error', message: 'ONLYOFFICE SDK 初始化超时' });
|
}
|
}, SDK_READY_TIMEOUT_MS);
|
|
function dispose(): void {
|
if (disposed) return;
|
disposed = true;
|
clearReadyTimeout();
|
abortController.abort();
|
cleanup();
|
bindingState.value = [];
|
fieldState.value = [];
|
checkboxGroupState.value = [];
|
crossTemplateScalarCandidateState.value = [];
|
crossTemplateSourceTemplateState.value = [];
|
repeatGroupState.value = [];
|
stepConfigurationState.value = null;
|
currentStepIdState.value = null;
|
formulaBindingState.value = [];
|
formulaVersionState.value = [];
|
pendingRepair.value = null;
|
pendingRepairConfirmation = null;
|
pendingCheckedChanges.clear();
|
instanceContext = null;
|
contextStore.reset();
|
}
|
|
return {
|
contextStore,
|
store,
|
initialize,
|
refresh,
|
loadCrossTemplateSourceTemplates,
|
loadCrossTemplateScalarCandidates,
|
suggestCode,
|
insertSelectedField,
|
insertRepeatField,
|
changeCheckboxGroup,
|
changeCheckboxGroupRule,
|
insertCrossTemplateScalar,
|
createPrivateField,
|
insertReference,
|
repairBindingId,
|
removeBinding,
|
removeOrphanControl,
|
removeFormulaBinding,
|
promoteReference,
|
reserveFormulaBinding,
|
upgradeBinding,
|
upgradeFormulaBinding,
|
select,
|
setCurrentStepId,
|
dispose,
|
get bindings() {
|
return bindingState.value;
|
},
|
get fields() {
|
return fieldState.value;
|
},
|
get checkboxGroups() {
|
return checkboxGroupState.value;
|
},
|
get crossTemplateScalarCandidates() {
|
return crossTemplateScalarCandidateState.value;
|
},
|
get crossTemplateSourceTemplates() {
|
return crossTemplateSourceTemplateState.value;
|
},
|
get repeatGroups() {
|
return repeatGroupState.value;
|
},
|
get stepConfiguration() {
|
return stepConfigurationState.value;
|
},
|
get currentStepId() {
|
return currentStepIdState.value;
|
},
|
get formulaBindings() {
|
return formulaBindingState.value;
|
},
|
get formulaVersions() {
|
return formulaVersionState.value;
|
},
|
get pendingRepairBindingId() {
|
return pendingRepair.value?.binding.id ?? null;
|
},
|
get repairMode() {
|
return Boolean(instanceContext?.repairBindingId);
|
},
|
};
|
}
|