<script setup lang="ts">
|
/* eslint-disable vue/custom-event-name-casing -- Existing public emit names are part of the plugin contract. */
|
import type { TemplateBinding, TemplateCheckboxGroup, TemplateCheckboxGroupDraft, TemplateFieldOption, TemplateStep } from '../domain/types';
|
import type { OfficeContentControl } from '../onlyoffice/types';
|
import type { RuntimeStatus } from '../stores/annotation';
|
|
import { computed, h, onBeforeUnmount, ref, watch } from 'vue';
|
|
import { AimOutlined, ArrowLeftOutlined, DeleteOutlined, EditOutlined, MoreOutlined, ReloadOutlined } from '@ant-design/icons-vue';
|
import { Badge, Modal } from 'ant-design-vue';
|
|
import { checkboxGroupCodeValid as groupCodeValid, checkboxGroupOptions, summarizeCheckboxGroups } from '../domain/checkbox-group';
|
import { parseTag } from '../domain/tags';
|
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
const props = withDefaults(
|
defineProps<{
|
bindings?: TemplateBinding[];
|
checkboxGroups?: TemplateCheckboxGroup[];
|
controls: OfficeContentControl[];
|
disabled?: boolean;
|
fields?: TemplateFieldOption[];
|
readOnly?: boolean;
|
repairBindingId?: null | string;
|
status: RuntimeStatus;
|
steps?: TemplateStep[];
|
}>(),
|
{ bindings: () => [], checkboxGroups: () => [], disabled: false, fields: () => [], readOnly: false, repairBindingId: null, steps: () => [] },
|
);
|
const emit = defineEmits<{
|
'change-checkbox-group': [bindingId: string, checkboxGroup: null | TemplateCheckboxGroupDraft, expectedVersion: number];
|
'change-checkbox-group-rule': [groupCode: string, minSelected: number, maxSelected: null | number, expectedVersion: number];
|
'insert-reference': [bindingId: string];
|
'promote-reference': [bindingId: string];
|
refresh: [];
|
'remove-binding': [bindingId: string, internalId: string];
|
'remove-orphan-control': [internalId: string];
|
repair: [bindingId: string];
|
select: [internalId: string];
|
'upgrade-binding': [bindingId: string, targetFieldVersionId: string];
|
}>();
|
|
const keyword = ref('');
|
const debouncedKeyword = ref('');
|
const filter = ref<'all' | 'anomaly' | 'group'>('all');
|
const openActionBindingId = ref<string>();
|
const upgradeBindingId = ref('');
|
const selectedUpgradeVersionId = ref<string>();
|
const checkboxGroupBindingId = ref('');
|
const selectedCheckboxGroupCode = ref('');
|
const selectedCheckboxGroupMin = ref(0);
|
const selectedCheckboxGroupMax = ref(1);
|
const selectedCheckboxGroupUnlimited = ref(false);
|
const checkboxGroupRuleCode = ref('');
|
const checkboxGroupRuleMin = ref(0);
|
const checkboxGroupRuleMax = ref(1);
|
const checkboxGroupRuleUnlimited = ref(false);
|
const bindingByTag = computed(() => new Map(props.bindings.filter((item) => item.status === 'ACTIVE').map((item) => [item.controlTag, item])));
|
const rows = computed(() => props.controls.map((control) => ({ binding: bindingByTag.value.get(control.Tag || ''), control, parsed: parseTag(control.Tag) })));
|
const missingBindings = computed(() =>
|
props.bindings.filter(
|
(binding) =>
|
binding.status === 'ACTIVE' &&
|
(!props.repairBindingId || binding.id === props.repairBindingId) &&
|
!props.controls.some((control) => control.Tag === binding.controlTag),
|
),
|
);
|
const activeUpgradeBinding = computed(() => props.bindings.find((binding) => binding.id === upgradeBindingId.value));
|
const activeCheckboxGroupBinding = computed(() => props.bindings.find((binding) => binding.id === checkboxGroupBindingId.value));
|
const fieldByVersionId = computed(() => new Map(props.fields.map((field) => [field.fieldVersionId, field])));
|
const stepNameById = computed(() => new Map(props.steps.map((step) => [step.id, step.name])));
|
const checkboxGroupSummaries = computed(() => summarizeCheckboxGroups(props.bindings, props.checkboxGroups));
|
const filteredCheckboxGroups = computed(() => {
|
const value = debouncedKeyword.value.trim().toLocaleLowerCase();
|
return checkboxGroupSummaries.value.filter(
|
(group) =>
|
!value ||
|
group.code.toLocaleLowerCase().includes(value) ||
|
group.members.some((binding) => [binding.controlAlias, binding.fieldCode].some((item) => (item || '').toLocaleLowerCase().includes(value))),
|
);
|
});
|
const displayedCheckboxGroups = computed(() =>
|
filter.value === 'anomaly' ? filteredCheckboxGroups.value.filter((group) => group.anomalies.length) : filteredCheckboxGroups.value,
|
);
|
const checkboxGroupCandidateOptions = computed(() => {
|
const binding = activeCheckboxGroupBinding.value;
|
if (!binding) return [];
|
return checkboxGroupOptions(props.bindings, binding.repeatGroupCode ? { repeatGroupCode: binding.repeatGroupCode } : { stepId: binding.stepId });
|
});
|
const checkboxGroupCodeValid = computed(() => groupCodeValid(selectedCheckboxGroupCode.value));
|
const selectedCheckboxGroup = computed(() => props.checkboxGroups.find((group) => group.groupCode === selectedCheckboxGroupCode.value.trim()));
|
const activeCheckboxGroupRule = computed(() => props.checkboxGroups.find((group) => group.groupCode === checkboxGroupRuleCode.value));
|
const selectedCheckboxGroupLimitsValid = computed(() => limitsValid(selectedCheckboxGroupMin.value, selectedCheckboxGroupUnlimited.value ? null : selectedCheckboxGroupMax.value));
|
const checkboxGroupRuleLimitsValid = computed(() => limitsValid(checkboxGroupRuleMin.value, checkboxGroupRuleUnlimited.value ? null : checkboxGroupRuleMax.value));
|
const anomalyCount = computed(
|
() =>
|
rows.value.filter((row) => rowIsAnomaly(row)).length +
|
missingBindings.value.length +
|
checkboxGroupSummaries.value.filter((group) => group.anomalies.length).length,
|
);
|
const anomalyBadgeStyle = { backgroundColor: '#ff4d4f', boxShadow: 'none', color: '#fff' };
|
function anomalyFilterLabel(count: number) {
|
return h('span', { class: 'document-filter-label' }, [
|
h('span', '异常'),
|
count > 0 ? h(Badge, { class: 'document-filter-badge', count, numberStyle: anomalyBadgeStyle }) : null,
|
]);
|
}
|
const filterOptions = computed(() => [
|
{ label: '全部', value: 'all' },
|
{ label: '选择组', value: 'group' },
|
{ label: anomalyFilterLabel(anomalyCount.value), value: 'anomaly' },
|
]);
|
const filteredRows = computed(() => {
|
const value = debouncedKeyword.value.trim().toLocaleLowerCase();
|
return rows.value
|
.filter((row) => {
|
const matchesFilter = filter.value === 'all' || (filter.value === 'anomaly' && rowIsAnomaly(row));
|
return (
|
matchesFilter &&
|
(!value ||
|
[row.control.Alias, row.control.Tag, metadata(row), row.binding?.checkboxGroupCode].some((item) => (item || '').toLocaleLowerCase().includes(value)))
|
);
|
})
|
.sort((left, right) => Number(rowIsAnomaly(right)) - Number(rowIsAnomaly(left)));
|
});
|
const filteredMissingBindings = computed(() => {
|
if (filter.value !== 'all' && filter.value !== 'anomaly') return [];
|
const value = debouncedKeyword.value.trim().toLocaleLowerCase();
|
return missingBindings.value.filter(
|
(binding) =>
|
!value ||
|
[binding.controlAlias, binding.fieldCode, binding.controlTag, binding.checkboxGroupCode].some((item) => (item || '').toLocaleLowerCase().includes(value)),
|
);
|
});
|
const emptyDescription = computed(() => {
|
if (filter.value === 'group') {
|
return displayedCheckboxGroups.value.length ? '' : '没有匹配的选择组';
|
}
|
if (filter.value === 'anomaly') {
|
return displayedCheckboxGroups.value.length || filteredRows.value.length || filteredMissingBindings.value.length ? '' : '没有异常字段';
|
}
|
if (props.status === 'ready' && props.controls.length === 0 && missingBindings.value.length === 0) {
|
return '当前文档没有内容控件';
|
}
|
return props.controls.length > 0 && filteredRows.value.length === 0 && filteredMissingBindings.value.length === 0 ? '没有匹配的文档字段' : '';
|
});
|
const locationDisabled = computed(() => props.disabled || props.status !== 'ready');
|
const writeDisabled = computed(() => props.readOnly || locationDisabled.value);
|
let searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
watch(
|
keyword,
|
(value) => {
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
searchDebounceTimer = setTimeout(() => {
|
debouncedKeyword.value = value;
|
searchDebounceTimer = undefined;
|
}, SEARCH_DEBOUNCE_MS);
|
},
|
{ flush: 'sync' },
|
);
|
onBeforeUnmount(() => {
|
if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
|
});
|
|
function rowIsAnomaly(row: (typeof rows.value)[number]): boolean {
|
return Boolean(row.parsed && !row.binding);
|
}
|
function canEditCheckboxGroup(binding: TemplateBinding): boolean {
|
return (
|
binding.status === 'ACTIVE' &&
|
(binding.controlRole === 'PRIMARY' || binding.controlRole === 'REPEAT') &&
|
binding.controlType === 'CHECKBOX' &&
|
!binding.readOnly
|
);
|
}
|
function limitsValid(minSelected: number, maxSelected: null | number): boolean {
|
return (
|
Number.isInteger(minSelected) &&
|
minSelected >= 0 &&
|
(maxSelected === null || (Number.isInteger(maxSelected) && maxSelected >= 1 && minSelected <= maxSelected))
|
);
|
}
|
function ruleLabel(groupCode: string): string {
|
const rule = props.checkboxGroups.find((item) => item.groupCode === groupCode);
|
if (!rule) return '规则缺失';
|
return `至少 ${rule.minSelected} 项,${rule.maxSelected === null ? '最多不限' : `最多 ${rule.maxSelected} 项`}`;
|
}
|
function displayScope(scope: string): string {
|
const prefix = '步骤:';
|
if (!scope.startsWith(prefix)) return scope;
|
const stepId = scope.slice(prefix.length);
|
return `${prefix}${stepNameById.value.get(stepId) || stepId}`;
|
}
|
function checkboxGroupStyle(code: string): Record<string, string> {
|
const colors = ['#1677ff', '#389e0d', '#d46b08', '#08979c', '#c41d7f'];
|
const index = [...code].reduce((hash, character) => (hash * 31 + character.charCodeAt(0)) >>> 0, 0) % colors.length;
|
return { borderColor: colors[index]!, color: colors[index]! };
|
}
|
function controlForBinding(binding: TemplateBinding): OfficeContentControl | undefined {
|
return props.controls.find((control) => control.Tag === binding.controlTag);
|
}
|
function focusCheckboxGroup(code: string): void {
|
filter.value = 'group';
|
keyword.value = code;
|
debouncedKeyword.value = code;
|
}
|
function anomalyReason(row: (typeof rows.value)[number]): string {
|
return rowIsAnomaly(row) ? '文档控件没有有效或待确认绑定' : '';
|
}
|
function metadata(row: (typeof rows.value)[number]): string {
|
if (!row.parsed) return '非 ELN 内容控件';
|
return row.parsed.fieldCode;
|
}
|
function roleLabel(row: (typeof rows.value)[number]): string {
|
if (row.parsed?.type === 'primary') return '主字段';
|
if (row.parsed?.type === 'reference') return '引用';
|
if (row.parsed?.type === 'repeat') return '重复行';
|
if (rowIsAnomaly(row)) return '异常';
|
return '其他';
|
}
|
function roleKind(row: (typeof rows.value)[number]): string {
|
return row.parsed?.type || 'other';
|
}
|
function controlTypeLabel(row: (typeof rows.value)[number]): string {
|
const fieldType = row.binding ? fieldByVersionId.value.get(row.binding.fieldVersionId)?.type : undefined;
|
const type = fieldType?.toUpperCase() || row.binding?.controlType?.toUpperCase() || row.control.ControlType?.toUpperCase();
|
const labels: Record<string, string> = {
|
CHECKBOX: '复选框',
|
DATE: '日期',
|
DATETIME: '日期时间',
|
DECIMAL: '数值',
|
DROPDOWN: '下拉选择',
|
MULTILINE: '多行文本',
|
SELECT: '下拉选择',
|
TEXT: '单行文本',
|
TEXTAREA: '多行文本',
|
};
|
return type ? labels[type] || type : '未知类型';
|
}
|
function upgradeCandidates(binding: TemplateBinding) {
|
return props.fields
|
.filter(
|
(field) =>
|
field.fieldId === binding.fieldId &&
|
field.fieldVersionId !== binding.fieldVersionId &&
|
(field.versionStatus === 'PUBLISHED' || field.versionStatus === 'PRE_RELEASE'),
|
)
|
.map((field) => ({
|
label: `${field.label}${field.versionStatus === 'PRE_RELEASE' ? '(预发布)' : ''}`,
|
value: field.fieldVersionId,
|
}));
|
}
|
function selectedUpgradeIsPreRelease(): boolean {
|
return props.fields.some((field) => field.fieldVersionId === selectedUpgradeVersionId.value && field.versionStatus === 'PRE_RELEASE');
|
}
|
function repairDisabled(binding: TemplateBinding): boolean {
|
return props.repairBindingId ? writeDisabled.value || props.repairBindingId !== binding.id : writeDisabled.value;
|
}
|
function select(control: OfficeContentControl): void {
|
if (!locationDisabled.value && control.InternalId) emit('select', control.InternalId);
|
}
|
function openUpgrade(binding: TemplateBinding): void {
|
if (writeDisabled.value) return;
|
upgradeBindingId.value = binding.id;
|
selectedUpgradeVersionId.value = undefined;
|
}
|
function closeUpgrade(): void {
|
upgradeBindingId.value = '';
|
selectedUpgradeVersionId.value = undefined;
|
}
|
function openCheckboxGroup(binding: TemplateBinding): void {
|
if (writeDisabled.value || !canEditCheckboxGroup(binding)) return;
|
checkboxGroupBindingId.value = binding.id;
|
const existing = props.checkboxGroups.find((group) => group.groupCode === binding.checkboxGroupCode);
|
selectedCheckboxGroupMin.value = existing?.minSelected ?? 0;
|
selectedCheckboxGroupMax.value = existing?.maxSelected ?? 1;
|
selectedCheckboxGroupUnlimited.value = existing?.maxSelected === null;
|
selectedCheckboxGroupCode.value = binding.checkboxGroupCode || '';
|
}
|
function closeCheckboxGroup(): void {
|
checkboxGroupBindingId.value = '';
|
selectedCheckboxGroupCode.value = '';
|
}
|
watch(selectedCheckboxGroupCode, (value, previousValue) => {
|
const existing = props.checkboxGroups.find((group) => group.groupCode === value.trim());
|
if (existing) {
|
selectedCheckboxGroupMin.value = existing.minSelected;
|
selectedCheckboxGroupMax.value = existing.maxSelected ?? 1;
|
selectedCheckboxGroupUnlimited.value = existing.maxSelected === null;
|
return;
|
}
|
if (props.checkboxGroups.some((group) => group.groupCode === previousValue.trim())) {
|
selectedCheckboxGroupMin.value = 0;
|
selectedCheckboxGroupMax.value = 1;
|
selectedCheckboxGroupUnlimited.value = false;
|
}
|
});
|
function saveCheckboxGroup(): void {
|
const binding = activeCheckboxGroupBinding.value;
|
if (!binding || writeDisabled.value || !checkboxGroupCodeValid.value || !selectedCheckboxGroupLimitsValid.value) return;
|
emit(
|
'change-checkbox-group',
|
binding.id,
|
{
|
groupCode: selectedCheckboxGroupCode.value.trim(),
|
maxSelected: selectedCheckboxGroupUnlimited.value ? null : selectedCheckboxGroupMax.value,
|
minSelected: selectedCheckboxGroupMin.value,
|
},
|
binding.version,
|
);
|
}
|
function removeCheckboxGroup(binding: TemplateBinding): void {
|
if (writeDisabled.value || !binding.checkboxGroupCode || !canEditCheckboxGroup(binding)) return;
|
emit('change-checkbox-group', binding.id, null, binding.version);
|
}
|
function openCheckboxGroupRule(groupCode: string): void {
|
if (writeDisabled.value) return;
|
const rule = props.checkboxGroups.find((item) => item.groupCode === groupCode);
|
if (!rule) return;
|
checkboxGroupRuleMin.value = rule.minSelected;
|
checkboxGroupRuleMax.value = rule.maxSelected ?? 1;
|
checkboxGroupRuleUnlimited.value = rule.maxSelected === null;
|
checkboxGroupRuleCode.value = groupCode;
|
}
|
function closeCheckboxGroupRule(): void {
|
checkboxGroupRuleCode.value = '';
|
}
|
function saveCheckboxGroupRule(): void {
|
const rule = activeCheckboxGroupRule.value;
|
if (!rule || writeDisabled.value || !checkboxGroupRuleLimitsValid.value) return;
|
emit(
|
'change-checkbox-group-rule',
|
rule.groupCode,
|
checkboxGroupRuleMin.value,
|
checkboxGroupRuleUnlimited.value ? null : checkboxGroupRuleMax.value,
|
rule.version,
|
);
|
}
|
function confirmUpgrade(): void {
|
if (!activeUpgradeBinding.value || !selectedUpgradeVersionId.value || writeDisabled.value) return;
|
emit('upgrade-binding', activeUpgradeBinding.value.id, selectedUpgradeVersionId.value);
|
}
|
function confirmRemoval(binding: TemplateBinding, control: OfficeContentControl): void {
|
if (writeDisabled.value || !control.InternalId) return;
|
Modal.confirm({
|
cancelText: '取消',
|
getContainer: () => (document.querySelector('.plugin-shell') as HTMLElement) || document.body,
|
okText: '删除',
|
okType: 'danger',
|
onOk: () => emit('remove-binding', binding.id, control.InternalId!),
|
title: '删除该字段控件?',
|
});
|
}
|
function confirmOrphanRemoval(control: OfficeContentControl): void {
|
if (writeDisabled.value || !control.InternalId) return;
|
Modal.confirm({
|
cancelText: '取消',
|
content: '删除后将保留控件内的文字,您可以按当前字段重新标注。',
|
getContainer: () => (document.querySelector('.plugin-shell') as HTMLElement) || document.body,
|
okText: '删除控件',
|
okType: 'danger',
|
onOk: () => emit('remove-orphan-control', control.InternalId!),
|
title: '删除异常控件?',
|
});
|
}
|
function setActionMenuOpen(bindingId: string, open: boolean): void {
|
if (open) {
|
openActionBindingId.value = bindingId;
|
} else if (openActionBindingId.value === bindingId) {
|
openActionBindingId.value = undefined;
|
}
|
}
|
function getPopupContainer(trigger: HTMLElement): HTMLElement {
|
return (trigger.closest('.plugin-shell') as HTMLElement) || document.body;
|
}
|
</script>
|
|
<template>
|
<section class="document-fields" aria-labelledby="document-fields-title">
|
<div v-if="!activeUpgradeBinding && !activeCheckboxGroupBinding && !activeCheckboxGroupRule" class="document-main" data-test="document-main">
|
<h2 id="document-fields-title" class="visually-hidden">文档字段</h2>
|
<div class="document-toolbar" data-test="document-toolbar">
|
<div class="document-search-row">
|
<a-input v-model:value="keyword" allow-clear class="document-search-input" data-test="field-search" placeholder="搜索名称或编码" />
|
<a-tooltip title="刷新文档字段" placement="topRight" :get-popup-container="getPopupContainer">
|
<a-button
|
data-test="refresh-controls"
|
:disabled="disabled || status === 'initializing' || status === 'working'"
|
shape="circle"
|
type="text"
|
aria-label="刷新文档字段"
|
@click="emit('refresh')">
|
<ReloadOutlined />
|
</a-button>
|
</a-tooltip>
|
</div>
|
<a-segmented v-model:value="filter" block class="document-filter" :options="filterOptions" />
|
</div>
|
|
<a-empty v-if="emptyDescription" class="empty-controls" data-test="document-empty" :description="emptyDescription" />
|
|
<div v-if="displayedCheckboxGroups.length && (filter === 'group' || filter === 'anomaly')" class="checkbox-group-list">
|
<section v-for="group in displayedCheckboxGroups" :key="group.code" class="checkbox-group" data-test="checkbox-group">
|
<header class="checkbox-group-header">
|
<div class="checkbox-group-heading">
|
<span class="checkbox-group-marker" :style="checkboxGroupStyle(group.code)" aria-hidden="true"></span>
|
<span class="checkbox-group-title" :title="group.code">{{ group.code }}</span>
|
<span class="checkbox-group-count">{{ group.members.length }} 个成员</span>
|
</div>
|
<a-tooltip title="修改选择组规则" :get-popup-container="getPopupContainer">
|
<a-button
|
v-if="checkboxGroups.some((item) => item.groupCode === group.code)"
|
data-test="edit-checkbox-group-rule"
|
:disabled="writeDisabled"
|
shape="circle"
|
type="text"
|
aria-label="修改选择组规则"
|
@click="openCheckboxGroupRule(group.code)"><EditOutlined /></a-button>
|
</a-tooltip>
|
</header>
|
<div class="checkbox-group-meta">
|
<span class="checkbox-group-rule">{{ ruleLabel(group.code) }}</span>
|
<span v-for="scope in group.scopes" :key="scope" class="checkbox-group-scope" :title="displayScope(scope)">{{ displayScope(scope) }}</span>
|
</div>
|
<p v-if="group.anomalies.length" class="anomaly-reason" data-test="checkbox-group-anomaly">{{ group.anomalies.join(';') }}</p>
|
<div class="checkbox-group-members">
|
<a-button
|
v-for="member in group.members"
|
:key="member.id"
|
block
|
class="checkbox-group-member"
|
data-test="checkbox-group-member-locate"
|
:disabled="locationDisabled || !controlForBinding(member)?.InternalId"
|
type="text"
|
:aria-label="`在文档中定位 ${member.controlAlias || member.fieldCode}`"
|
@click="select(controlForBinding(member)!)">
|
<span class="checkbox-group-member-name">{{ member.controlAlias || member.fieldCode }}</span>
|
<span class="checkbox-group-locate-icon" aria-hidden="true"><AimOutlined /></span>
|
</a-button>
|
</div>
|
</section>
|
</div>
|
|
<a-list v-if="filteredMissingBindings.length" class="missing-list">
|
<a-list-item v-for="binding in filteredMissingBindings" :key="binding.id" data-test="missing-binding">
|
<a-list-item-meta>
|
<template #title>
|
<span>{{ binding.controlRole === 'PRIMARY' ? '主字段' : '引用' }}控件缺失:{{ binding.controlAlias || binding.fieldCode }}</span>
|
</template>
|
<template #description>
|
<span>{{ binding.fieldCode }}</span>
|
<span data-test="anomaly-reason" class="anomaly-reason">绑定存在但 DOCX 控件已缺失</span>
|
</template>
|
</a-list-item-meta>
|
<a-button data-test="repair-binding" data-write-action="true" :disabled="repairDisabled(binding)" @click="emit('repair', binding.id)">
|
{{ repairBindingId ? '在文档选择位置后恢复' : '恢复' }}
|
</a-button>
|
</a-list-item>
|
</a-list>
|
|
<a-list v-if="filteredRows.length" class="control-list">
|
<a-list-item v-for="({ binding, control, parsed }, index) in filteredRows" :key="control.InternalId || `${control.Tag}-${index}`" class="control-row">
|
<a-button class="control-item" data-test="control-item" :disabled="locationDisabled || !control.InternalId" type="text" @click="select(control)">
|
<span class="control-main-line">
|
<a-tooltip :title="control.Alias || `未命名字段 ${index + 1}`" :get-popup-container="getPopupContainer">
|
<span class="control-alias">{{ control.Alias || `未命名字段 ${index + 1}` }}</span>
|
</a-tooltip>
|
<span class="control-role" data-test="control-role" :data-role="roleKind({ binding, control, parsed })">
|
<span class="control-role-dot" aria-hidden="true">·</span>
|
<span>{{ roleLabel({ binding, control, parsed }) }}</span>
|
</span>
|
<span class="control-type" data-test="control-type">{{ controlTypeLabel({ binding, control, parsed }) }}</span>
|
</span>
|
<span class="control-metadata">{{ metadata({ binding, control, parsed }) }}</span>
|
<span
|
v-if="binding?.checkboxGroupCode"
|
class="checkbox-group-label"
|
data-test="checkbox-group-label"
|
:style="{ color: checkboxGroupStyle(binding.checkboxGroupCode).color }"
|
role="button"
|
tabindex="0"
|
@click.stop="focusCheckboxGroup(binding.checkboxGroupCode)"
|
@keydown.enter.stop="focusCheckboxGroup(binding.checkboxGroupCode)">
|
选择组 · {{ binding.checkboxGroupCode }}
|
</span>
|
<span v-if="anomalyReason({ binding, control, parsed })" data-test="anomaly-reason" class="anomaly-reason">
|
{{ anomalyReason({ binding, control, parsed }) }}
|
</span>
|
</a-button>
|
|
<a-tooltip
|
v-if="binding && !repairBindingId"
|
title="更多操作"
|
placement="topRight"
|
:get-popup-container="getPopupContainer"
|
:open="openActionBindingId === binding.id ? false : undefined">
|
<a-dropdown :get-popup-container="getPopupContainer" :trigger="['click']" @open-change="setActionMenuOpen(binding.id, $event)">
|
<a-button data-test="row-actions" data-write-action="true" :disabled="writeDisabled" shape="circle" type="text" aria-label="字段操作">
|
<MoreOutlined />
|
</a-button>
|
<template #overlay>
|
<a-menu>
|
<a-menu-item v-if="binding.controlRole === 'PRIMARY'" data-test="insert-reference-primary" @click="emit('insert-reference', binding.id)">
|
插入引用
|
</a-menu-item>
|
<a-menu-item v-if="binding.controlRole === 'REFERENCE'" data-test="promote-reference" @click="emit('promote-reference', binding.id)">
|
设为主字段
|
</a-menu-item>
|
<a-menu-item
|
v-if="binding.controlRole === 'PRIMARY' && binding.status === 'ACTIVE' && upgradeCandidates(binding).length"
|
data-test="open-upgrade-binding"
|
@click="openUpgrade(binding)">
|
升级版本
|
</a-menu-item>
|
<a-menu-item v-if="canEditCheckboxGroup(binding)" data-test="change-checkbox-group" @click="openCheckboxGroup(binding)">
|
{{ binding.checkboxGroupCode ? '修改选择组' : '设置选择组' }}
|
</a-menu-item>
|
<a-menu-item
|
v-if="canEditCheckboxGroup(binding) && binding.checkboxGroupCode"
|
data-test="remove-checkbox-group"
|
@click="removeCheckboxGroup(binding)">
|
移出选择组
|
</a-menu-item>
|
<a-menu-divider />
|
<a-menu-item danger data-test="remove-binding" @click="confirmRemoval(binding, control)">删除控件</a-menu-item>
|
</a-menu>
|
</template>
|
</a-dropdown>
|
</a-tooltip>
|
<a-tooltip
|
v-else-if="rowIsAnomaly({ binding, control, parsed }) && !repairBindingId"
|
title="删除异常控件"
|
placement="topRight"
|
:get-popup-container="getPopupContainer">
|
<a-button
|
danger
|
data-test="remove-orphan-control"
|
data-write-action="true"
|
:disabled="writeDisabled || !control.InternalId"
|
shape="circle"
|
type="text"
|
aria-label="删除异常控件"
|
@click="confirmOrphanRemoval(control)">
|
<DeleteOutlined />
|
</a-button>
|
</a-tooltip>
|
</a-list-item>
|
</a-list>
|
</div>
|
|
<div v-else-if="activeCheckboxGroupBinding" class="secondary-page" data-test="checkbox-group-page">
|
<header class="secondary-page-header">
|
<a-button class="secondary-page-back" type="text" aria-label="返回文档字段" @click="closeCheckboxGroup"><ArrowLeftOutlined /></a-button>
|
<h2>{{ activeCheckboxGroupBinding.checkboxGroupCode ? '修改选择组' : '设置选择组' }}</h2>
|
</header>
|
<p class="upgrade-field-name">{{ activeCheckboxGroupBinding.controlAlias || activeCheckboxGroupBinding.fieldCode }}</p>
|
<a-form layout="vertical">
|
<a-form-item label="选择组编码">
|
<a-auto-complete
|
v-model:value="selectedCheckboxGroupCode"
|
data-test="checkbox-group-edit-code"
|
:disabled="writeDisabled"
|
:maxlength="80"
|
:options="checkboxGroupCandidateOptions"
|
placeholder="输入新编码或选择已有选择组" />
|
<p v-if="selectedCheckboxGroupCode && !checkboxGroupCodeValid" class="field-error" data-test="checkbox-group-edit-error">
|
选择组编码只能使用字母、数字、下划线或连字符,且不超过 80 个字符
|
</p>
|
</a-form-item>
|
<div v-if="selectedCheckboxGroupCode.trim()" class="checkbox-group-limits">
|
<a-form-item label="最少选择数">
|
<a-input-number
|
v-model:value="selectedCheckboxGroupMin"
|
data-test="checkbox-group-edit-min"
|
:disabled="writeDisabled || Boolean(selectedCheckboxGroup)"
|
:min="0"
|
:precision="0" />
|
</a-form-item>
|
<a-form-item>
|
<template #label>
|
<span class="checkbox-limit-label">
|
<span>最多选择数</span>
|
<a-switch
|
v-model:checked="selectedCheckboxGroupUnlimited"
|
data-test="checkbox-group-edit-unlimited"
|
:disabled="writeDisabled || Boolean(selectedCheckboxGroup)"
|
checked-children="不限"
|
un-checked-children="限额" />
|
</span>
|
</template>
|
<a-input-number
|
v-if="!selectedCheckboxGroupUnlimited"
|
v-model:value="selectedCheckboxGroupMax"
|
data-test="checkbox-group-edit-max"
|
:disabled="writeDisabled || Boolean(selectedCheckboxGroup)"
|
:min="1"
|
:precision="0" />
|
</a-form-item>
|
</div>
|
</a-form>
|
<div class="sticky-action">
|
<a-button
|
block
|
data-test="save-checkbox-group"
|
data-write-action="true"
|
:disabled="writeDisabled || !checkboxGroupCodeValid || !selectedCheckboxGroupLimitsValid"
|
type="primary"
|
@click="saveCheckboxGroup">
|
保存选择组
|
</a-button>
|
</div>
|
</div>
|
|
<div v-else-if="activeCheckboxGroupRule" class="secondary-page" data-test="checkbox-group-rule-page">
|
<header class="secondary-page-header">
|
<a-button class="secondary-page-back" type="text" aria-label="返回选择组" @click="closeCheckboxGroupRule"><ArrowLeftOutlined /></a-button>
|
<h2>修改选择组规则</h2>
|
</header>
|
<p class="upgrade-field-name">{{ activeCheckboxGroupRule.groupCode }}</p>
|
<a-form layout="vertical">
|
<div class="checkbox-group-limits">
|
<a-form-item label="最少选择数">
|
<a-input-number v-model:value="checkboxGroupRuleMin" data-test="checkbox-group-rule-min" :disabled="writeDisabled" :min="0" :precision="0" />
|
</a-form-item>
|
<a-form-item>
|
<template #label>
|
<span class="checkbox-limit-label">
|
<span>最多选择数</span>
|
<a-switch
|
v-model:checked="checkboxGroupRuleUnlimited"
|
data-test="checkbox-group-rule-unlimited"
|
:disabled="writeDisabled"
|
checked-children="不限"
|
un-checked-children="限额" />
|
</span>
|
</template>
|
<a-input-number
|
v-if="!checkboxGroupRuleUnlimited"
|
v-model:value="checkboxGroupRuleMax"
|
data-test="checkbox-group-rule-max"
|
:disabled="writeDisabled"
|
:min="1"
|
:precision="0" />
|
</a-form-item>
|
</div>
|
</a-form>
|
<div class="sticky-action">
|
<a-button
|
block
|
data-test="save-checkbox-group-rule"
|
data-write-action="true"
|
:disabled="writeDisabled || !checkboxGroupRuleLimitsValid"
|
type="primary"
|
@click="saveCheckboxGroupRule">保存规则</a-button>
|
</div>
|
</div>
|
|
<div v-else-if="activeUpgradeBinding" class="secondary-page" data-test="binding-upgrade-page">
|
<header class="secondary-page-header">
|
<a-button class="secondary-page-back" type="text" aria-label="返回文档字段" @click="closeUpgrade"><ArrowLeftOutlined /></a-button>
|
<h2>升级字段版本</h2>
|
</header>
|
<p class="upgrade-field-name">{{ activeUpgradeBinding.controlAlias || activeUpgradeBinding.fieldCode }}</p>
|
<a-form layout="vertical">
|
<a-form-item label="目标版本">
|
<a-select
|
v-model:value="selectedUpgradeVersionId"
|
data-test="upgrade-version"
|
:disabled="writeDisabled"
|
:get-popup-container="getPopupContainer"
|
:options="upgradeCandidates(activeUpgradeBinding)"
|
placeholder="选择目标版本" />
|
</a-form-item>
|
</a-form>
|
<a-alert v-if="selectedUpgradeIsPreRelease()" message="预发布版本仅供模板验证" show-icon type="warning" />
|
<div class="sticky-action">
|
<a-button
|
block
|
data-test="upgrade-binding"
|
data-write-action="true"
|
:disabled="writeDisabled || !selectedUpgradeVersionId"
|
type="primary"
|
@click="confirmUpgrade">
|
确认升级
|
</a-button>
|
</div>
|
</div>
|
</section>
|
</template>
|
|
<style scoped>
|
.document-toolbar {
|
position: sticky;
|
z-index: 3;
|
top: 0;
|
width: 100%;
|
min-width: 0;
|
background: #fff;
|
}
|
.document-fields,
|
.document-fields > div {
|
width: 100%;
|
max-width: 100%;
|
min-width: 0;
|
}
|
.document-fields {
|
height: 100%;
|
}
|
.document-main {
|
display: flex;
|
flex-direction: column;
|
min-height: 100%;
|
}
|
.document-search-row {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
width: 100%;
|
min-width: 0;
|
}
|
.document-search-input {
|
flex: 1 1 auto;
|
min-width: 0;
|
}
|
.document-filter {
|
margin-top: 8px;
|
}
|
.document-filter :deep(.ant-segmented-item-label) {
|
overflow: visible;
|
}
|
.document-filter :deep(.document-filter-label) {
|
position: relative;
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
min-width: 0;
|
}
|
.document-filter :deep(.document-filter-badge) {
|
position: absolute;
|
z-index: 1;
|
top: -7px;
|
right: -10px;
|
pointer-events: none;
|
}
|
.document-filter :deep(.ant-badge-count) {
|
position: relative;
|
min-width: 18px;
|
height: 18px;
|
padding: 0 5px;
|
font-size: 11px;
|
line-height: 18px;
|
transform: none;
|
}
|
.control-list,
|
.missing-list {
|
width: 100%;
|
min-width: 0;
|
margin-top: 8px;
|
}
|
.control-list :deep(.ant-list-item-meta),
|
.control-list :deep(.ant-list-item-meta-content),
|
.missing-list :deep(.ant-list-item-meta),
|
.missing-list :deep(.ant-list-item-meta-content) {
|
min-width: 0;
|
}
|
.control-row {
|
gap: 4px;
|
padding: 6px 0 !important;
|
}
|
.control-item {
|
display: grid;
|
flex: 1 1 auto;
|
min-width: 0;
|
height: auto;
|
padding: 4px 6px;
|
text-align: left;
|
}
|
.control-main-line {
|
display: flex;
|
gap: 6px;
|
align-items: center;
|
min-width: 0;
|
}
|
.control-alias {
|
min-width: 0;
|
overflow: hidden;
|
color: rgb(0 0 0 / 88%);
|
font-weight: 600;
|
line-height: 20px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.control-role {
|
--role-color: #8c8c8c;
|
|
display: inline-flex;
|
flex: 0 0 auto;
|
gap: 3px;
|
align-items: center;
|
color: var(--role-color);
|
font-size: 13px;
|
font-weight: 400;
|
line-height: 20px;
|
}
|
.control-role[data-role='primary'] {
|
--role-color: #4096ff;
|
}
|
.control-role[data-role='reference'] {
|
--role-color: #52c41a;
|
}
|
.control-role[data-role='repeat'] {
|
--role-color: #9254de;
|
}
|
.control-role-dot {
|
font-size: 18px;
|
font-weight: 600;
|
line-height: 1;
|
}
|
.control-type {
|
flex: 0 0 auto;
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
font-weight: 400;
|
line-height: 20px;
|
}
|
.control-metadata {
|
min-width: 0;
|
overflow: hidden;
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
line-height: 18px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.checkbox-group-label {
|
display: block;
|
width: fit-content;
|
max-width: 100%;
|
overflow: hidden;
|
font-size: 12px;
|
line-height: 18px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.checkbox-group-list {
|
display: grid;
|
gap: 8px;
|
margin-top: 8px;
|
}
|
.checkbox-group {
|
padding: 12px;
|
background: #fff;
|
border: 1px solid #d9d9d9;
|
border-radius: 6px;
|
}
|
.checkbox-group-header {
|
display: flex;
|
gap: 6px;
|
align-items: center;
|
justify-content: space-between;
|
min-width: 0;
|
}
|
.checkbox-group-heading {
|
display: flex;
|
flex: 1;
|
gap: 8px;
|
align-items: center;
|
min-width: 0;
|
}
|
.checkbox-group-marker {
|
flex: 0 0 8px;
|
width: 8px;
|
height: 8px;
|
background: currentColor;
|
border-radius: 50%;
|
}
|
.checkbox-group-title {
|
min-width: 0;
|
overflow: hidden;
|
color: rgb(0 0 0 / 88%);
|
font-size: 14px;
|
font-weight: 600;
|
line-height: 22px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.checkbox-group-count {
|
flex: none;
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
line-height: 20px;
|
}
|
.checkbox-group-meta {
|
display: grid;
|
gap: 2px;
|
margin-top: 6px;
|
}
|
.checkbox-group-rule,
|
.checkbox-group-scope {
|
min-width: 0;
|
overflow: hidden;
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
line-height: 20px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.checkbox-group-members {
|
display: grid;
|
gap: 2px;
|
margin-top: 10px;
|
padding-top: 6px;
|
border-top: 1px solid #f0f0f0;
|
}
|
|
.checkbox-group-limits {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
gap: 12px;
|
}
|
|
.checkbox-group-limits :deep(.ant-input-number) {
|
width: 100%;
|
}
|
.checkbox-group-limits :deep(.ant-form-item) {
|
margin-bottom: 0;
|
}
|
.checkbox-limit-label {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
justify-content: space-between;
|
width: 100%;
|
}
|
.checkbox-group-member {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) 16px;
|
gap: 8px;
|
align-items: center;
|
min-width: 0;
|
height: auto;
|
min-height: 32px;
|
padding: 4px 8px;
|
text-align: left;
|
}
|
.checkbox-group-member-name {
|
display: block;
|
min-width: 0;
|
line-height: 22px;
|
overflow-wrap: anywhere;
|
white-space: normal;
|
}
|
.checkbox-group-locate-icon {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
color: rgb(0 0 0 / 25%);
|
font-size: 14px;
|
}
|
.checkbox-group .anomaly-reason {
|
margin: 8px 0 0;
|
padding: 5px 8px;
|
overflow: visible;
|
background: #fff2f0;
|
border-radius: 4px;
|
line-height: 18px;
|
text-overflow: initial;
|
white-space: normal;
|
}
|
.field-error {
|
margin: 4px 0 0;
|
color: #ff4d4f;
|
font-size: 12px;
|
line-height: 18px;
|
}
|
.anomaly-reason {
|
display: block;
|
overflow: hidden;
|
margin-top: 2px;
|
color: #ff4d4f;
|
font-size: 12px;
|
line-height: 18px;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.control-item .anomaly-reason {
|
text-align: left;
|
}
|
.missing-list :deep(.ant-list-item) {
|
gap: 8px;
|
padding: 8px;
|
background: #fff2f0;
|
}
|
.missing-list :deep(.ant-list-item-meta-title) {
|
margin: 0;
|
color: #cf1322;
|
}
|
.empty-controls {
|
display: flex;
|
flex: 1 1 auto;
|
flex-direction: column;
|
justify-content: center;
|
min-height: 180px;
|
margin: 0;
|
color: rgb(0 0 0 / 45%);
|
text-align: center;
|
}
|
.secondary-page-header {
|
display: flex;
|
gap: 4px;
|
align-items: center;
|
min-height: 32px;
|
margin-bottom: 8px;
|
}
|
.secondary-page-header h2 {
|
margin: 0;
|
font-size: 16px;
|
line-height: 22px;
|
}
|
.secondary-page-back {
|
flex: 0 0 32px;
|
width: 32px;
|
min-width: 32px;
|
height: 32px;
|
padding: 0;
|
}
|
.upgrade-field-name {
|
margin: 0 0 10px;
|
font-weight: 600;
|
}
|
.sticky-action {
|
position: sticky;
|
bottom: 0;
|
padding-top: 12px;
|
background: #fff;
|
}
|
.visually-hidden {
|
position: absolute;
|
width: 1px;
|
height: 1px;
|
padding: 0;
|
overflow: hidden;
|
clip: rect(0, 0, 0, 0);
|
white-space: nowrap;
|
border: 0;
|
}
|
</style>
|