<script setup lang="ts">
|
/* eslint-disable vue/custom-event-name-casing -- Existing public emit names are part of the plugin contract. */
|
import type {
|
CrossTemplateScalarCandidate,
|
CrossTemplateSourceTemplate,
|
TemplateBinding,
|
TemplateCheckboxGroup,
|
TemplateCheckboxGroupDraft,
|
TemplateFieldOption,
|
TemplatePrivateFieldDraft,
|
TemplateRepeatGroup,
|
} from '../domain/types';
|
|
import { computed, ref, watch } from 'vue';
|
|
import { ArrowLeftOutlined, LinkOutlined, PlusOutlined, SearchOutlined, TranslationOutlined } from '@ant-design/icons-vue';
|
|
import { checkboxGroupCodeValid, checkboxGroupOptions as buildCheckboxGroupOptions } from '../domain/checkbox-group';
|
import CodeAssistModal from './CodeAssistModal.vue';
|
import PrivateFieldForm from './PrivateFieldForm.vue';
|
|
const props = withDefaults(
|
defineProps<{
|
disabled?: boolean;
|
bindings?: TemplateBinding[];
|
checkboxGroups?: TemplateCheckboxGroup[];
|
crossTemplateCandidates?: CrossTemplateScalarCandidate[];
|
crossTemplateSourceTemplates?: CrossTemplateSourceTemplate[];
|
currentStepId?: null | string;
|
fields: TemplateFieldOption[];
|
readSelectedText?: () => Promise<string>;
|
repeatGroups?: TemplateRepeatGroup[];
|
resetToken?: number;
|
suggestCode?: (text: string) => Promise<string>;
|
}>(),
|
{
|
bindings: () => [],
|
checkboxGroups: () => [],
|
crossTemplateCandidates: () => [],
|
crossTemplateSourceTemplates: () => [],
|
currentStepId: null,
|
disabled: false,
|
repeatGroups: () => [],
|
resetToken: 0,
|
},
|
);
|
const emit = defineEmits<{
|
'create-private': [draft: TemplatePrivateFieldDraft];
|
'load-cross-template-fields': [sourceTemplateId: string];
|
'load-cross-template-templates': [];
|
'submit-cross-template': [candidate: CrossTemplateScalarCandidate];
|
'submit-repeat':
|
| [field: TemplateFieldOption, group: TemplateRepeatGroup]
|
| [field: TemplateFieldOption, group: TemplateRepeatGroup, checkboxGroup: TemplateCheckboxGroupDraft];
|
submit: [field: TemplateFieldOption] | [field: TemplateFieldOption, checkboxGroup: TemplateCheckboxGroupDraft];
|
}>();
|
const selectedVersionId = ref<string>();
|
const privateVisible = ref(false);
|
const privateFieldInitialName = ref('');
|
const crossTemplateVisible = ref(false);
|
const crossTemplateQuery = ref('');
|
const selectedCrossTemplateSourceTemplateId = ref<string>();
|
const selectedCrossTemplateCandidateKey = ref<string>();
|
const annotationMode = ref<'primary' | 'repeat'>('primary');
|
const repeatGroupCode = ref('');
|
const checkboxGroupCode = ref('');
|
const checkboxGroupMinSelected = ref(0);
|
const checkboxGroupMaxSelected = ref(1);
|
const checkboxGroupUnlimited = ref(false);
|
const codeAssistVisible = ref(false);
|
const codeAssistTarget = ref<'checkbox' | 'repeat'>('repeat');
|
const checkboxGroupContinuationVisible = ref(false);
|
const repeatMinRows = ref(0);
|
const repeatMaxRows = ref(100);
|
const selected = computed(() => props.fields.find((item) => item.fieldVersionId === selectedVersionId.value));
|
const selectedSupportsCheckboxGroup = computed(() => selected.value?.type === 'CHECKBOX' && selected.value.source === 'NONE');
|
const showCheckboxGroup = computed(() => selectedSupportsCheckboxGroup.value || (!selected.value && checkboxGroupContinuationVisible.value));
|
const checkboxGroupValid = computed(() => !checkboxGroupCode.value.trim() || checkboxGroupCodeValid(checkboxGroupCode.value));
|
const existingCheckboxGroup = computed(() =>
|
props.checkboxGroups.find((item) => item.groupCode === checkboxGroupCode.value.trim()),
|
);
|
const checkboxGroupLimitsValid = computed(
|
() =>
|
Number.isInteger(checkboxGroupMinSelected.value) &&
|
checkboxGroupMinSelected.value >= 0 &&
|
(checkboxGroupUnlimited.value ||
|
(Number.isInteger(checkboxGroupMaxSelected.value) &&
|
checkboxGroupMaxSelected.value >= 1 &&
|
checkboxGroupMinSelected.value <= checkboxGroupMaxSelected.value)),
|
);
|
const repeatGroupValid = computed(() => /^[\w-]{1,80}$/u.test(repeatGroupCode.value.trim()));
|
const repeatGroupOptions = computed(() =>
|
props.repeatGroups.map((group) => ({
|
label: `${group.groupCode}(${group.minRows} 至 ${group.maxRows} 行)`,
|
value: group.groupCode,
|
})),
|
);
|
const checkboxGroupOptions = computed(() =>
|
annotationMode.value === 'repeat'
|
? repeatGroupCode.value.trim()
|
? buildCheckboxGroupOptions(props.bindings, { repeatGroupCode: repeatGroupCode.value.trim() })
|
: []
|
: buildCheckboxGroupOptions(props.bindings, { stepId: props.currentStepId }),
|
);
|
const privateCheckboxGroupOptions = computed(() => buildCheckboxGroupOptions(props.bindings, { stepId: props.currentStepId }));
|
const selectedCrossTemplateCandidate = computed(() =>
|
props.crossTemplateCandidates.find((item) => crossTemplateCandidateKey(item) === selectedCrossTemplateCandidateKey.value),
|
);
|
const crossTemplateSourceTemplateOptions = computed(() =>
|
props.crossTemplateSourceTemplates.map((template) => ({
|
label: template.sourceTemplateCode,
|
value: template.sourceTemplateId,
|
})),
|
);
|
const filteredCrossTemplateCandidates = computed(() => {
|
if (!selectedCrossTemplateSourceTemplateId.value) return [];
|
const query = crossTemplateQuery.value.trim().toLocaleLowerCase();
|
return props.crossTemplateCandidates.filter((candidate) => {
|
if (candidate.sourceTemplateId !== selectedCrossTemplateSourceTemplateId.value) return false;
|
if (!query) return true;
|
return [candidate.sourceFieldName, candidate.sourceFieldCode, candidate.sourceFieldVersionId].some((value) => value.toLocaleLowerCase().includes(query));
|
});
|
});
|
const existingRepeatGroup = computed(() => props.repeatGroups.find((item) => item.groupCode === repeatGroupCode.value.trim()));
|
const repeatLimitsValid = computed(
|
() =>
|
Number.isInteger(repeatMinRows.value) &&
|
Number.isInteger(repeatMaxRows.value) &&
|
repeatMinRows.value >= 0 &&
|
repeatMaxRows.value >= repeatMinRows.value &&
|
repeatMaxRows.value <= 1000,
|
);
|
const codeAssistTitle = computed(() => (codeAssistTarget.value === 'repeat' ? '生成重复组编码' : '生成选择组编码'));
|
const typeLabels: Record<string, string> = {
|
TEXT: '单行文本',
|
TEXTAREA: '多行文本',
|
SELECT: '下拉选择',
|
CHECKBOX: '复选框',
|
DATE: '日期',
|
DATETIME: '日期时间',
|
DECIMAL: '数值',
|
};
|
const crossTemplateTypeClasses: Record<string, string> = {
|
CHECKBOX: 'cross-template-type--checkbox',
|
DATE: 'cross-template-type--date',
|
DATETIME: 'cross-template-type--date',
|
DECIMAL: 'cross-template-type--decimal',
|
SELECT: 'cross-template-type--select',
|
TEXT: 'cross-template-type--text',
|
TEXTAREA: 'cross-template-type--textarea',
|
};
|
const groupedOptions = computed(() =>
|
[
|
{ label: '公共字段', options: props.fields.filter((field) => field.scope === 'GLOBAL').map((field) => toOption(field)) },
|
{ label: '本模板字段', options: props.fields.filter((field) => field.scope === 'TEMPLATE').map((field) => toOption(field)) },
|
].filter((group) => group.options.length),
|
);
|
|
watch(repeatGroupCode, (value) => {
|
const existing = props.repeatGroups.find((item) => item.groupCode === value.trim());
|
repeatMinRows.value = existing?.minRows ?? 0;
|
repeatMaxRows.value = existing?.maxRows ?? 100;
|
});
|
watch(
|
() => props.resetToken,
|
() => {
|
selectedVersionId.value = undefined;
|
},
|
);
|
watch(
|
selectedVersionId,
|
(value) => {
|
const field = props.fields.find((item) => item.fieldVersionId === value);
|
if (field?.type === 'CHECKBOX' && field.source === 'NONE') {
|
checkboxGroupContinuationVisible.value = true;
|
if (field.recommendedCheckboxGroupCode) {
|
checkboxGroupMinSelected.value = field.recommendedCheckboxMinSelected ?? 0;
|
checkboxGroupUnlimited.value = field.recommendedCheckboxMaxSelected === null;
|
checkboxGroupMaxSelected.value = field.recommendedCheckboxMaxSelected ?? 1;
|
checkboxGroupCode.value = field.recommendedCheckboxGroupCode;
|
}
|
} else if (value) {
|
checkboxGroupContinuationVisible.value = false;
|
checkboxGroupCode.value = '';
|
}
|
},
|
{ flush: 'sync' },
|
);
|
watch(checkboxGroupCode, (value, previousValue) => {
|
const existing = props.checkboxGroups.find((item) => item.groupCode === value.trim());
|
if (existing) {
|
checkboxGroupMinSelected.value = existing.minSelected;
|
checkboxGroupUnlimited.value = existing.maxSelected === null;
|
checkboxGroupMaxSelected.value = existing.maxSelected ?? 1;
|
return;
|
}
|
if (props.checkboxGroups.some((item) => item.groupCode === previousValue.trim())) {
|
checkboxGroupMinSelected.value = 0;
|
checkboxGroupMaxSelected.value = 1;
|
checkboxGroupUnlimited.value = false;
|
}
|
});
|
watch(
|
() => props.currentStepId,
|
() => {
|
checkboxGroupCode.value = '';
|
checkboxGroupMinSelected.value = 0;
|
checkboxGroupMaxSelected.value = 1;
|
checkboxGroupUnlimited.value = false;
|
},
|
);
|
watch(annotationMode, () => {
|
checkboxGroupCode.value = '';
|
});
|
watch(repeatGroupCode, () => {
|
checkboxGroupCode.value = '';
|
});
|
watch(selectedCrossTemplateSourceTemplateId, (sourceTemplateId) => {
|
crossTemplateQuery.value = '';
|
selectedCrossTemplateCandidateKey.value = undefined;
|
if (sourceTemplateId) emit('load-cross-template-fields', sourceTemplateId);
|
});
|
watch(
|
() => props.crossTemplateSourceTemplates,
|
(templates) => {
|
if (
|
selectedCrossTemplateSourceTemplateId.value &&
|
!templates.some((template) => template.sourceTemplateId === selectedCrossTemplateSourceTemplateId.value)
|
) {
|
selectedCrossTemplateSourceTemplateId.value = undefined;
|
}
|
},
|
);
|
function toOption(field: TemplateFieldOption) {
|
return { label: `${field.label}${field.versionStatus === 'PRE_RELEASE' ? '(预发布)' : ''}`, value: field.fieldVersionId };
|
}
|
function crossTemplateCandidateKey(candidate: CrossTemplateScalarCandidate): string {
|
return `${candidate.sourceTemplateId}::${candidate.sourceFieldVersionId}`;
|
}
|
function crossTemplateTypeClass(type: string): string {
|
return crossTemplateTypeClasses[type] ?? 'cross-template-type--default';
|
}
|
function configText(key: string): string {
|
const value = selected.value?.controlConfig?.[key];
|
if (Array.isArray(value))
|
return value.map((item) => (typeof item === 'object' && item && 'display' in item ? String(item.display) : String(item))).join('、');
|
return value === undefined || value === null || value === '' ? '未设置' : String(value);
|
}
|
function submit(): void {
|
if (props.disabled || !selected.value || !checkboxGroupValid.value || !checkboxGroupLimitsValid.value) return;
|
const normalizedCheckboxGroup = showCheckboxGroup.value ? checkboxGroupCode.value.trim() : '';
|
const checkboxGroup = normalizedCheckboxGroup
|
? {
|
groupCode: normalizedCheckboxGroup,
|
maxSelected: checkboxGroupUnlimited.value ? null : checkboxGroupMaxSelected.value,
|
minSelected: checkboxGroupMinSelected.value,
|
}
|
: undefined;
|
if (annotationMode.value === 'repeat') {
|
const groupCode = repeatGroupCode.value.trim();
|
if (!repeatGroupValid.value || !repeatLimitsValid.value) return;
|
const group = {
|
groupCode,
|
maxRows: repeatMaxRows.value,
|
minRows: repeatMinRows.value,
|
};
|
if (checkboxGroup) emit('submit-repeat', selected.value, group, checkboxGroup);
|
else emit('submit-repeat', selected.value, group);
|
return;
|
}
|
if (checkboxGroup) emit('submit', selected.value, checkboxGroup);
|
else emit('submit', selected.value);
|
}
|
function submitPrivate(draft: TemplatePrivateFieldDraft): void {
|
privateVisible.value = false;
|
emit('create-private', draft);
|
}
|
async function openPrivateField(): Promise<void> {
|
if (props.disabled) return;
|
try {
|
privateFieldInitialName.value = (await props.readSelectedText?.())?.replace(/\s+/gu, ' ').trim() ?? '';
|
} catch {
|
privateFieldInitialName.value = '';
|
}
|
privateVisible.value = true;
|
}
|
function openCrossTemplate(): void {
|
if (props.disabled) return;
|
crossTemplateQuery.value = '';
|
selectedCrossTemplateSourceTemplateId.value = undefined;
|
selectedCrossTemplateCandidateKey.value = undefined;
|
crossTemplateVisible.value = true;
|
emit('load-cross-template-templates');
|
}
|
function submitCrossTemplate(): void {
|
if (props.disabled || !selectedCrossTemplateCandidate.value) return;
|
emit('submit-cross-template', selectedCrossTemplateCandidate.value);
|
}
|
function openCodeAssist(target: 'checkbox' | 'repeat'): void {
|
if (props.disabled) return;
|
codeAssistTarget.value = target;
|
codeAssistVisible.value = true;
|
}
|
function applyCodeSuggestion(code: string): void {
|
if (codeAssistTarget.value === 'repeat') repeatGroupCode.value = code;
|
else checkboxGroupCode.value = code;
|
}
|
</script>
|
|
<template>
|
<section class="field-editor" data-test="field-editor">
|
<a-form v-if="!privateVisible && !crossTemplateVisible" data-test="annotation-main" layout="vertical">
|
<a-form-item label="标注方式">
|
<a-segmented
|
v-model:value="annotationMode"
|
block
|
data-test="annotation-mode"
|
:disabled="disabled"
|
:options="[
|
{ label: '普通字段', value: 'primary' },
|
{ label: '重复行', value: 'repeat' },
|
]" />
|
</a-form-item>
|
<a-form-item v-if="annotationMode === 'repeat'" label="重复组编码">
|
<a-auto-complete
|
v-model:value="repeatGroupCode"
|
data-test="repeat-group-code"
|
:disabled="disabled"
|
:maxlength="80"
|
:options="repeatGroupOptions"
|
placeholder="输入新编码或选择已有重复组"
|
show-arrow>
|
<template #suffixIcon>
|
<a-tooltip title="中文转拼音">
|
<button
|
aria-label="中文转拼音"
|
class="code-assist-trigger"
|
data-test="repeat-code-assist"
|
type="button"
|
@click.prevent.stop="openCodeAssist('repeat')"
|
@mousedown.prevent.stop>
|
<TranslationOutlined />
|
</button>
|
</a-tooltip>
|
</template>
|
</a-auto-complete>
|
<p v-if="repeatGroupCode.trim() && !repeatGroupValid" class="field-error" data-test="repeat-group-error">
|
重复组编码只能使用字母、数字、下划线或连字符,且不超过 80 个字符
|
</p>
|
</a-form-item>
|
<div v-if="annotationMode === 'repeat'" class="repeat-limits">
|
<a-form-item label="最少行数">
|
<a-input-number
|
v-model:value="repeatMinRows"
|
data-test="repeat-min-rows"
|
:disabled="disabled || Boolean(existingRepeatGroup)"
|
:max="1000"
|
:min="0"
|
:precision="0" />
|
</a-form-item>
|
<a-form-item label="最多行数">
|
<a-input-number
|
v-model:value="repeatMaxRows"
|
data-test="repeat-max-rows"
|
:disabled="disabled || Boolean(existingRepeatGroup)"
|
:max="1000"
|
:min="0"
|
:precision="0" />
|
</a-form-item>
|
</div>
|
<a-form-item label="字段">
|
<a-select
|
v-model:value="selectedVersionId"
|
allow-clear
|
show-search
|
option-filter-prop="label"
|
:options="groupedOptions"
|
:disabled="disabled"
|
placeholder="搜索并选择字段" />
|
</a-form-item>
|
<a-form-item v-if="showCheckboxGroup" label="选择组编码(选填)">
|
<a-auto-complete
|
v-model:value="checkboxGroupCode"
|
data-test="checkbox-group-code"
|
:disabled="disabled"
|
:maxlength="80"
|
:options="checkboxGroupOptions"
|
placeholder="输入新编码或选择已有选择组"
|
show-arrow>
|
<template #suffixIcon>
|
<a-tooltip title="中文转拼音">
|
<button
|
aria-label="中文转拼音"
|
class="code-assist-trigger"
|
data-test="checkbox-code-assist"
|
type="button"
|
@click.prevent.stop="openCodeAssist('checkbox')"
|
@mousedown.prevent.stop>
|
<TranslationOutlined />
|
</button>
|
</a-tooltip>
|
</template>
|
</a-auto-complete>
|
<p v-if="!checkboxGroupValid" class="field-error" data-test="checkbox-group-error">选择组编码只能使用字母、数字、下划线或连字符,且不超过 80 个字符</p>
|
</a-form-item>
|
<div v-if="showCheckboxGroup && checkboxGroupCode.trim()" class="checkbox-group-limits">
|
<a-form-item label="最少选择数">
|
<a-input-number
|
v-model:value="checkboxGroupMinSelected"
|
data-test="checkbox-group-min"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
:min="0"
|
:precision="0" />
|
</a-form-item>
|
<a-form-item label="最多选择数">
|
<a-switch
|
v-model:checked="checkboxGroupUnlimited"
|
data-test="checkbox-group-unlimited"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
checked-children="不限"
|
un-checked-children="限额" />
|
<a-input-number
|
v-if="!checkboxGroupUnlimited"
|
v-model:value="checkboxGroupMaxSelected"
|
class="checkbox-group-max"
|
data-test="checkbox-group-max"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
:min="1"
|
:precision="0" />
|
</a-form-item>
|
</div>
|
<p v-if="showCheckboxGroup && checkboxGroupCode.trim() && !checkboxGroupLimitsValid" class="field-error" data-test="checkbox-group-limits-error">
|
最少选择数必须为非负整数,最多选择数必须为正整数且不小于最少选择数
|
</p>
|
<a-alert
|
v-if="selected?.versionStatus === 'PRE_RELEASE'"
|
data-test="pre-release-risk"
|
message="该字段版本处于预发布状态,仅用于草稿验证"
|
show-icon
|
type="warning" />
|
<dl v-if="selected" class="field-summary field-properties" data-test="field-properties">
|
<div>
|
<dt>字段名称</dt>
|
<dd data-test="field-alias-readonly">{{ selected.alias }}</dd>
|
</div>
|
<div>
|
<dt>作用域</dt>
|
<dd data-test="field-scope-readonly">{{ selected.scope === 'GLOBAL' ? '公共字段' : '本模板字段' }}</dd>
|
</div>
|
<div>
|
<dt>字段类型</dt>
|
<dd data-test="field-type-readonly">{{ typeLabels[selected.type] }}</dd>
|
</div>
|
<div>
|
<dt>填写要求</dt>
|
<dd data-test="field-required-readonly">{{ selected.required ? '必填' : '选填' }}</dd>
|
</div>
|
<div>
|
<dt>占位文本</dt>
|
<dd data-test="field-placeholder-readonly">{{ configText('placeholder') }}</dd>
|
</div>
|
<div>
|
<dt>默认值</dt>
|
<dd data-test="field-default-readonly">{{ configText('defaultValue') }}</dd>
|
</div>
|
<div v-if="selected.type === 'SELECT'">
|
<dt>选项</dt>
|
<dd>{{ configText('options') }}</dd>
|
</div>
|
</dl>
|
<div class="field-actions">
|
<a-button
|
block
|
data-test="submit-field"
|
type="primary"
|
:disabled="
|
disabled ||
|
!selected ||
|
!checkboxGroupValid ||
|
!checkboxGroupLimitsValid ||
|
(annotationMode === 'repeat' && (!repeatGroupValid || !repeatLimitsValid))
|
"
|
@click="submit"
|
>标注当前位置</a-button
|
>
|
<a-button block data-test="open-private-field" :disabled="disabled" type="dashed" @click="openPrivateField"><PlusOutlined />本模板字段</a-button>
|
<a-button block data-test="open-cross-template" :disabled="disabled" type="dashed" @click="openCrossTemplate"
|
><LinkOutlined />引用其他模板字段</a-button
|
>
|
</div>
|
</a-form>
|
<div v-else-if="privateVisible" class="secondary-page">
|
<header class="secondary-page-header">
|
<a-button class="secondary-page-back" data-test="private-field-back" type="text" aria-label="返回标注" @click="privateVisible = false">
|
<ArrowLeftOutlined />
|
</a-button>
|
<h2>创建本模板字段</h2>
|
</header>
|
<PrivateFieldForm
|
:checkbox-group-options="privateCheckboxGroupOptions"
|
:checkbox-groups="checkboxGroups"
|
:disabled="disabled"
|
:initial-name="privateFieldInitialName"
|
:suggest-code="suggestCode"
|
@cancel="privateVisible = false"
|
@submit="submitPrivate" />
|
</div>
|
<div v-else class="secondary-page cross-template-page" data-test="cross-template-page">
|
<header class="secondary-page-header">
|
<a-button class="secondary-page-back" data-test="cross-template-back" type="text" aria-label="返回标注" @click="crossTemplateVisible = false">
|
<ArrowLeftOutlined />
|
</a-button>
|
<h2>引用其他模板字段</h2>
|
</header>
|
<a-select
|
v-model:value="selectedCrossTemplateSourceTemplateId"
|
allow-clear
|
class="cross-template-source-select"
|
data-test="cross-template-source-select"
|
:disabled="disabled"
|
option-filter-prop="label"
|
:options="crossTemplateSourceTemplateOptions"
|
placeholder="搜索并选择来源模板"
|
show-search />
|
<a-input
|
v-model:value="crossTemplateQuery"
|
allow-clear
|
class="cross-template-search"
|
data-test="cross-template-search"
|
:disabled="disabled || !selectedCrossTemplateSourceTemplateId"
|
placeholder="搜索字段名称、编码或版本">
|
<template #prefix><SearchOutlined /></template>
|
</a-input>
|
<p v-if="selectedCrossTemplateSourceTemplateId" class="cross-template-count" data-test="cross-template-count">
|
可用字段 {{ filteredCrossTemplateCandidates.length }} 个
|
</p>
|
<div class="cross-template-list" data-test="cross-template-list">
|
<a-radio-group v-model:value="selectedCrossTemplateCandidateKey" class="cross-template-options" data-test="cross-template-options" :disabled="disabled">
|
<a-radio
|
v-for="candidate in filteredCrossTemplateCandidates"
|
:key="crossTemplateCandidateKey(candidate)"
|
class="cross-template-option"
|
data-test="cross-template-option"
|
:value="crossTemplateCandidateKey(candidate)">
|
<span class="cross-template-option-content">
|
<span class="cross-template-option-main">
|
<a-tooltip :title="candidate.sourceFieldName">
|
<span class="cross-template-field-name" data-test="cross-template-field-name">{{ candidate.sourceFieldName }}</span>
|
</a-tooltip>
|
<span :class="['cross-template-type', crossTemplateTypeClass(candidate.sourceFieldType)]" data-test="cross-template-type">{{
|
typeLabels[candidate.sourceFieldType]
|
}}</span>
|
</span>
|
<a-tooltip :title="candidate.sourceTemplateCode">
|
<span class="cross-template-source-template" data-test="cross-template-source-template">{{ candidate.sourceTemplateCode }}</span>
|
</a-tooltip>
|
</span>
|
</a-radio>
|
</a-radio-group>
|
<a-empty v-if="!selectedCrossTemplateSourceTemplateId" class="content-empty" description="请先选择来源模板" />
|
<a-empty
|
v-else-if="!filteredCrossTemplateCandidates.length"
|
class="content-empty"
|
:description="crossTemplateCandidates.length ? '未找到匹配字段' : '该模板暂无可用字段'" />
|
</div>
|
<div class="cross-template-footer" data-test="cross-template-footer">
|
<section v-if="selectedCrossTemplateCandidate" class="cross-template-details" data-test="cross-template-details">
|
<h3>已选字段</h3>
|
<dl>
|
<div>
|
<dt>来源模板</dt>
|
<dd>
|
<span class="cross-template-detail-value" data-test="cross-template-detail-value" :title="selectedCrossTemplateCandidate.sourceTemplateCode">{{
|
selectedCrossTemplateCandidate.sourceTemplateCode
|
}}</span>
|
</dd>
|
</div>
|
<div>
|
<dt>字段编码</dt>
|
<dd data-test="cross-template-detail-code">
|
<span class="cross-template-detail-value" data-test="cross-template-detail-value" :title="selectedCrossTemplateCandidate.sourceFieldCode">{{
|
selectedCrossTemplateCandidate.sourceFieldCode
|
}}</span>
|
</dd>
|
</div>
|
<div>
|
<dt>字段版本</dt>
|
<dd data-test="cross-template-detail-version">
|
<span
|
class="cross-template-detail-value"
|
data-test="cross-template-detail-value"
|
:title="selectedCrossTemplateCandidate.sourceFieldVersionId"
|
>{{ selectedCrossTemplateCandidate.sourceFieldVersionId }}</span
|
>
|
</dd>
|
</div>
|
</dl>
|
</section>
|
<a-button block data-test="submit-cross-template" type="primary" :disabled="disabled || !selectedCrossTemplateCandidate" @click="submitCrossTemplate"
|
>标注当前位置</a-button
|
>
|
</div>
|
</div>
|
<CodeAssistModal
|
v-model:open="codeAssistVisible"
|
:suggest-code="suggestCode"
|
:title="codeAssistTitle"
|
@confirm="applyCodeSuggestion" />
|
</section>
|
</template>
|
|
<style scoped>
|
.field-editor {
|
height: 100%;
|
}
|
.field-summary {
|
display: grid;
|
gap: 6px;
|
margin: 0 0 12px;
|
}
|
.field-properties {
|
padding: 12px;
|
background: #fafafa;
|
border-radius: 6px;
|
}
|
.field-summary div {
|
display: grid;
|
grid-template-columns: 72px minmax(0, 1fr);
|
gap: 8px;
|
}
|
.field-summary dt {
|
color: inherit;
|
font-weight: 400;
|
text-align: right;
|
}
|
.field-summary dd {
|
min-width: 0;
|
margin: 0;
|
overflow-wrap: anywhere;
|
}
|
.field-editor :deep(.ant-alert) {
|
margin-bottom: 12px;
|
}
|
|
.field-editor :deep(.ant-select-arrow) {
|
pointer-events: auto;
|
}
|
|
.code-assist-trigger {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
padding: 0;
|
color: rgb(0 0 0 / 45%);
|
line-height: 1;
|
background: transparent;
|
border: 0;
|
cursor: pointer;
|
}
|
|
.code-assist-trigger:hover {
|
color: #1677ff;
|
}
|
|
.field-error {
|
margin: 4px 0 0;
|
color: #ff4d4f;
|
font-size: 12px;
|
line-height: 18px;
|
}
|
|
.repeat-limits {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
gap: 8px;
|
}
|
|
.checkbox-group-limits {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
gap: 8px;
|
}
|
|
.checkbox-group-limits :deep(.ant-input-number),
|
.checkbox-group-max {
|
width: 100%;
|
}
|
|
.repeat-limits :deep(.ant-input-number) {
|
width: 100%;
|
}
|
|
.cross-template-options {
|
display: grid;
|
gap: 8px;
|
width: 100%;
|
}
|
.cross-template-search {
|
margin-bottom: 8px;
|
}
|
.cross-template-source-select {
|
width: 100%;
|
margin-bottom: 8px;
|
}
|
.cross-template-count {
|
margin: 0 0 6px;
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
line-height: 20px;
|
}
|
.cross-template-list {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
min-height: 0;
|
overflow-y: auto;
|
overscroll-behavior: contain;
|
}
|
.cross-template-list .content-empty {
|
margin: auto 0;
|
}
|
.cross-template-page {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
min-height: 0;
|
}
|
.cross-template-options :deep(.ant-radio-wrapper) {
|
align-items: center;
|
width: 100%;
|
min-width: 0;
|
min-height: 62px;
|
margin-inline-start: 0;
|
padding: 8px 10px;
|
border-radius: 6px;
|
white-space: normal;
|
}
|
.cross-template-options :deep(.ant-radio-wrapper:hover),
|
.cross-template-options :deep(.ant-radio-wrapper-checked) {
|
background: #f5f5f5;
|
}
|
.cross-template-options :deep(.ant-radio-wrapper > span:last-child) {
|
min-width: 0;
|
flex: 1;
|
}
|
.cross-template-option-content {
|
display: grid;
|
gap: 3px;
|
min-width: 0;
|
}
|
.cross-template-option-main {
|
display: flex;
|
gap: 8px;
|
align-items: center;
|
min-width: 0;
|
}
|
.cross-template-field-name,
|
.cross-template-source-template {
|
display: block;
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.cross-template-field-name {
|
flex: 1;
|
font-weight: 500;
|
}
|
.cross-template-source-template {
|
color: rgb(0 0 0 / 45%);
|
font-size: 12px;
|
}
|
.cross-template-type {
|
flex: none;
|
font-size: 12px;
|
font-weight: 500;
|
}
|
.cross-template-type--text {
|
color: #1677ff;
|
}
|
.cross-template-type--textarea {
|
color: #08979c;
|
}
|
.cross-template-type--select {
|
color: #722ed1;
|
}
|
.cross-template-type--checkbox {
|
color: #d46b08;
|
}
|
.cross-template-type--date {
|
color: #389e0d;
|
}
|
.cross-template-type--decimal {
|
color: #c41d7f;
|
}
|
.cross-template-type--default {
|
color: rgb(0 0 0 / 65%);
|
}
|
.cross-template-footer {
|
position: sticky;
|
z-index: 2;
|
bottom: 0;
|
display: grid;
|
gap: 12px;
|
margin: 10px -12px 0;
|
padding: 12px 16px 16px;
|
background: #fff;
|
border-top: 1px solid #f0f0f0;
|
}
|
.cross-template-details {
|
min-width: 0;
|
padding: 10px 12px;
|
background: #fafafa;
|
border-radius: 6px;
|
}
|
.cross-template-details h3 {
|
margin: 0 0 6px;
|
font-size: 14px;
|
font-weight: 500;
|
line-height: 22px;
|
}
|
.cross-template-details dl {
|
display: grid;
|
gap: 4px;
|
margin: 0;
|
}
|
.cross-template-details dl > div {
|
display: grid;
|
grid-template-columns: 64px minmax(0, 1fr);
|
gap: 8px;
|
}
|
.cross-template-details dt {
|
color: rgb(0 0 0 / 45%);
|
text-align: left;
|
}
|
.cross-template-details dd {
|
min-width: 0;
|
margin: 0;
|
overflow: hidden;
|
}
|
.cross-template-detail-value {
|
display: block;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.field-actions {
|
display: grid;
|
gap: 8px;
|
padding-top: 10px;
|
}
|
.sticky-action {
|
position: sticky;
|
bottom: 0;
|
padding-top: 10px;
|
background: #fff;
|
}
|
.secondary-page-header {
|
display: flex;
|
gap: 4px;
|
align-items: center;
|
min-height: 32px;
|
margin-bottom: 8px;
|
}
|
.secondary-page-back {
|
flex: 0 0 32px;
|
width: 32px;
|
min-width: 32px;
|
height: 32px;
|
padding: 0;
|
}
|
.secondary-page-header h2 {
|
margin: 0;
|
font-size: 16px;
|
line-height: 22px;
|
}
|
@media (max-width: 300px) {
|
.cross-template-footer {
|
gap: 8px;
|
margin-right: -10px;
|
margin-left: -10px;
|
padding: 8px 12px;
|
}
|
.cross-template-details {
|
padding: 8px 10px;
|
}
|
.cross-template-details h3 {
|
margin-bottom: 2px;
|
line-height: 20px;
|
}
|
.cross-template-details dl {
|
gap: 2px;
|
}
|
.cross-template-details dl > div {
|
grid-template-columns: 56px minmax(0, 1fr);
|
gap: 6px;
|
line-height: 20px;
|
}
|
}
|
@media (max-height: 420px) {
|
.cross-template-footer {
|
position: static;
|
}
|
}
|
</style>
|