<script setup lang="ts">
|
import { computed, onBeforeUnmount, ref } from 'vue';
|
|
import { fetchAnnotationCatalog } from './api/workflow-steps';
|
import { addRepeatPatternField, annotationAlias, annotationFields, annotationPlaceholder, annotationTag, displayFieldLabel, formAnnotationAlias, formAnnotationTag, removeRepeatPatternField, repeatGroups, repeatInsertSpec, repeatPatternUsesField, repeatPatternValidationError, stepKey, type AnnotationField, type FormField, type WorkflowStep } from './domain/annotation';
|
import { parsePluginContext, type QianpiyePluginContext } from './domain/context';
|
import { insertAnnotationControlCommand, parseInsertResult, stageFlowVersionPropertiesCommand } from './onlyoffice/commands';
|
import { bindPluginReady } from './onlyoffice/lifecycle';
|
|
const steps = ref<WorkflowStep[]>([]);
|
const formFields = ref<FormField[]>([]);
|
const selectedRepeat = ref<string>();
|
const repeatPatternDrafts = ref<Record<string, string>>({});
|
const selectedStep = ref<string>();
|
const loading = ref(true);
|
const error = ref('');
|
const versionWarning = ref('');
|
const insertingTag = ref('');
|
const groups = computed(() => repeatGroups(formFields.value));
|
const repeat = computed(() => groups.value.find((item) => item.key === selectedRepeat.value));
|
const repeatPattern = computed({
|
get: () => selectedRepeat.value ? repeatPatternDrafts.value[selectedRepeat.value] || '' : '',
|
set: (value: string) => {
|
if (selectedRepeat.value) repeatPatternDrafts.value[selectedRepeat.value] = value;
|
},
|
});
|
const repeatPatternError = computed(() => repeat.value && repeatPattern.value
|
? repeatPatternValidationError(repeat.value, repeatPattern.value)
|
: '');
|
const canInsertRepeat = computed(() => Boolean(repeat.value && repeatPattern.value
|
&& !repeatPatternError.value && !insertingTag.value));
|
let context: QianpiyePluginContext | undefined;
|
const step = computed(() => steps.value.find((item) => stepKey(item) === selectedStep.value));
|
const fields = computed(() => annotationFields(step.value));
|
|
async function loadSteps(refresh = false) {
|
loading.value = true;
|
error.value = '';
|
if (refresh) versionWarning.value = '';
|
try {
|
context = parsePluginContext(window.Asc?.plugin?.info?.options);
|
if (!context.canAnnotate) {
|
steps.value = [];
|
selectedStep.value = undefined;
|
error.value = '当前文档为只读,无法标注';
|
return;
|
}
|
const catalog = await fetchAnnotationCatalog(context, refresh);
|
if (refresh) await stageFlowVersionProperties(catalog.newFlowVersionId, catalog.revisionFlowVersionId);
|
steps.value = catalog.workflowSteps;
|
formFields.value = catalog.formFields.filter((field) => field.key.split('.').pop() !== 'biz_sign');
|
selectedStep.value = steps.value[0] ? stepKey(steps.value[0]) : undefined;
|
if (!refresh) void stageFlowVersionProperties(catalog.newFlowVersionId, catalog.revisionFlowVersionId)
|
.catch(() => { versionWarning.value = '流程版本将在保存时写入,请先点击刷新'; });
|
} catch (cause) {
|
error.value = cause instanceof Error ? cause.message : '加载流程步骤失败';
|
} finally {
|
loading.value = false;
|
}
|
}
|
|
function stageFlowVersionProperties(newFlowVersionId: string, revisionFlowVersionId: string): Promise<void> {
|
window.Asc.scope.dmsQianpiyeFlowVersionProperties = { newFlowVersionId, revisionFlowVersionId };
|
return new Promise((resolve, reject) => {
|
window.Asc.plugin.callCommand(stageFlowVersionPropertiesCommand, false, true, (raw) => {
|
const result = parseInsertResult(raw);
|
if (result.ok) resolve();
|
else reject(new Error(result.error || '保存签批页流程版本失败'));
|
});
|
});
|
}
|
function insert(tag: string, alias: string, placeholder = annotationPlaceholder(alias)) {
|
if (!context?.canAnnotate || insertingTag.value) return;
|
insertingTag.value = tag;
|
error.value = '';
|
window.Asc.scope.dmsQianpiyeInsert = {
|
alias,
|
placeholder,
|
tag,
|
};
|
window.Asc.plugin.callCommand(insertAnnotationControlCommand, false, true, (raw) => {
|
const result = parseInsertResult(raw);
|
if (!result.ok) {
|
error.value = result.error || '插入签批页标注失败';
|
}
|
insertingTag.value = '';
|
});
|
}
|
function insertWorkflowField(field: AnnotationField) {
|
if (!step.value) return;
|
insert(annotationTag(step.value, field), annotationAlias(step.value, field));
|
}
|
function insertFormField(field: FormField) {
|
insert(formAnnotationTag(field), formAnnotationAlias(field));
|
}
|
function toggleRepeatField(field: FormField) {
|
repeatPattern.value = repeatPatternUsesField(repeatPattern.value, field)
|
? removeRepeatPatternField(repeatPattern.value, field)
|
: addRepeatPatternField(repeatPattern.value, field);
|
}
|
function clearRepeatPattern() {
|
repeatPattern.value = '';
|
}
|
function insertRepeat() {
|
if (!repeat.value) return;
|
try {
|
const spec = repeatInsertSpec(repeat.value, repeatPattern.value);
|
insert(spec.tag, spec.alias, spec.placeholder);
|
} catch (cause) { error.value = cause instanceof Error ? cause.message : '子表格式无效'; }
|
}
|
const unbindPluginReady = bindPluginReady(window.Asc, () => {
|
void loadSteps();
|
});
|
|
onBeforeUnmount(unbindPluginReady);
|
</script>
|
|
<template>
|
<main class="panel">
|
<a-alert v-if="error" type="error" show-icon :message="error" class="alert" />
|
<a-alert v-if="versionWarning" type="warning" show-icon :message="versionWarning" class="alert" />
|
<a-spin :spinning="loading">
|
<section class="field-section">
|
<div class="section-title"><h3>流程字段</h3><a-button size="small" :loading="loading" @click="loadSteps(true)">刷新</a-button></div>
|
<a-select v-model:value="selectedStep" placeholder="选择流程步骤" class="full" :options="steps.map(item => ({ label: item.label, value: stepKey(item) }))" />
|
<div v-if="step" class="field-list">
|
<a-button v-for="field in fields" :key="field.key" block :loading="insertingTag === annotationTag(step, field)" :disabled="Boolean(insertingTag)" @click="insertWorkflowField(field)">{{ field.label }}</a-button>
|
</div>
|
<a-empty v-else-if="!loading" description="未找到可标注的发起或审核步骤" :image-style="{ height: '48px' }" />
|
</section>
|
<section class="field-section form-field-section">
|
<h3>表单字段</h3>
|
<div v-if="formFields.length" class="field-list form-field-list">
|
<a-button v-for="field in formFields" :key="field.key" block :loading="insertingTag === formAnnotationTag(field)" :disabled="Boolean(insertingTag)" @click="insertFormField(field)">{{ displayFieldLabel(field.label) }}</a-button>
|
</div>
|
<a-empty v-else-if="!loading" class="form-field-empty" description="未找到适用流程的表单字段" :image-style="{ height: '48px' }" />
|
</section>
|
<section v-if="groups.length" class="field-section">
|
<h3>子表逐行组合</h3>
|
<a-select v-model:value="selectedRepeat" class="full" placeholder="选择子表" :options="groups.map(item => ({ label: item.label, value: item.key }))" />
|
<template v-if="repeat">
|
<div class="repeat-heading">
|
<span class="repeat-label">选择字段</span>
|
<a-button v-if="repeatPattern" type="link" size="small" :disabled="Boolean(insertingTag)" @click="clearRepeatPattern">清空</a-button>
|
</div>
|
<div class="repeat-fields">
|
<a-button
|
v-for="field in repeat.fields"
|
:key="field.column"
|
block
|
:type="repeatPatternUsesField(repeatPattern, field) ? 'primary' : 'default'"
|
:aria-pressed="repeatPatternUsesField(repeatPattern, field)"
|
:disabled="Boolean(insertingTag)"
|
@click="toggleRepeatField(field)">
|
{{ displayFieldLabel(field.label.split('-').pop() || field.label) }}
|
</a-button>
|
</div>
|
<label class="repeat-label repeat-format-label" for="repeat-pattern">每条记录格式</label>
|
<a-input
|
id="repeat-pattern"
|
v-model:value="repeatPattern"
|
class="repeat-pattern"
|
:maxlength="2000"
|
:disabled="Boolean(insertingTag)"
|
placeholder="例如:{{部门}}({{份数}})" />
|
<div class="repeat-feedback" aria-live="polite">
|
<span v-if="repeatPatternError" class="repeat-error">{{ repeatPatternError }}</span>
|
<span v-else-if="repeatPattern" class="repeat-preview">预览:{{ repeatPattern }}</span>
|
</div>
|
<a-button block type="primary" :loading="insertingTag === repeat.tag" :disabled="!canInsertRepeat" @click="insertRepeat">插入逐行组合</a-button>
|
</template>
|
</section>
|
</a-spin>
|
</main>
|
</template>
|