<script setup lang="ts">
|
import type { DropdownOption, TemplateCheckboxGroup, TemplateFieldDataType, TemplatePrivateFieldDraft } from '../domain/types';
|
|
import { computed, ref, watch } from 'vue';
|
|
import { TranslationOutlined } from '@ant-design/icons-vue';
|
|
import { checkboxGroupCodeValid } from '../domain/checkbox-group';
|
import CodeAssistModal from './CodeAssistModal.vue';
|
|
const props = withDefaults(
|
defineProps<{
|
checkboxGroupOptions?: Array<{ label: string; value: string }>;
|
checkboxGroups?: TemplateCheckboxGroup[];
|
disabled?: boolean;
|
initialName?: string;
|
suggestCode?: (text: string) => Promise<string>;
|
}>(),
|
{
|
checkboxGroupOptions: () => [],
|
checkboxGroups: () => [],
|
disabled: false,
|
initialName: '',
|
},
|
);
|
const emit = defineEmits<{ cancel: []; submit: [draft: TemplatePrivateFieldDraft] }>();
|
|
const name = ref(props.initialName);
|
const type = ref<TemplateFieldDataType>('TEXT');
|
const placeholder = ref('');
|
const defaultValue = ref('');
|
const defaultChecked = ref(false);
|
const checkboxGroupCode = ref('');
|
const checkboxGroupMinSelected = ref(0);
|
const checkboxGroupMaxSelected = ref(1);
|
const checkboxGroupUnlimited = ref(false);
|
const codeAssistVisible = ref(false);
|
const options = ref<DropdownOption[]>([
|
{ display: '', value: '' },
|
{ display: '', value: '' },
|
]);
|
const nameError = ref('');
|
const optionsError = ref('');
|
const typeOptions = [
|
{ label: '单行文本', value: 'TEXT' },
|
{ label: '多行文本', value: 'TEXTAREA' },
|
{ label: '下拉选择', value: 'SELECT' },
|
{ label: '复选框', value: 'CHECKBOX' },
|
{ label: '日期', value: 'DATE' },
|
{ label: '日期时间', value: 'DATETIME' },
|
{ label: '数值', value: 'DECIMAL' },
|
];
|
const isSelect = computed(() => type.value === 'SELECT');
|
const isCheckbox = computed(() => type.value === 'CHECKBOX');
|
const isDate = computed(() => type.value === 'DATE');
|
const isDateTime = computed(() => type.value === 'DATETIME');
|
const isTemporal = computed(() => isDate.value || isDateTime.value);
|
const pickerFormat = computed(() => (isDateTime.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD'));
|
const documentDateFormat = computed(() => (isDateTime.value ? 'yyyy-MM-dd HH:mm:ss' : 'yyyy-MM-dd'));
|
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)),
|
);
|
|
watch(type, (value, previousValue) => {
|
if (value !== 'CHECKBOX') {
|
checkboxGroupCode.value = '';
|
checkboxGroupMinSelected.value = 0;
|
checkboxGroupMaxSelected.value = 1;
|
checkboxGroupUnlimited.value = false;
|
codeAssistVisible.value = false;
|
}
|
if (value === 'CHECKBOX' || value === 'DATE' || value === 'DATETIME') placeholder.value = '';
|
if (value === 'DATE' || value === 'DATETIME' || previousValue === 'DATE' || previousValue === 'DATETIME') {
|
defaultValue.value = '';
|
}
|
});
|
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;
|
}
|
});
|
|
function updateTemporalDefault(value: null | string): void {
|
defaultValue.value = value || '';
|
}
|
|
function openCodeAssist(): void {
|
if (!props.disabled) codeAssistVisible.value = true;
|
}
|
|
function applyCodeSuggestion(code: string): void {
|
checkboxGroupCode.value = code;
|
}
|
|
function submit(): void {
|
if (props.disabled) return;
|
const trimmedName = name.value.trim();
|
nameError.value = '';
|
optionsError.value = '';
|
if (!trimmedName) {
|
nameError.value = '请输入字段名称';
|
return;
|
}
|
if (isCheckbox.value && (!checkboxGroupValid.value || !checkboxGroupLimitsValid.value)) return;
|
const normalizedOptions = options.value.map((item) => ({ display: item.display.trim(), value: item.value.trim() }));
|
if (
|
isSelect.value &&
|
(normalizedOptions.some((item) => !item.display || !item.value) || new Set(normalizedOptions.map((item) => item.value)).size !== normalizedOptions.length)
|
) {
|
optionsError.value = '请填写至少两个完整且业务值唯一的选项';
|
return;
|
}
|
const normalizedPlaceholder = isCheckbox.value || isTemporal.value ? '' : placeholder.value.trim() || trimmedName;
|
emit('submit', {
|
...(isCheckbox.value && checkboxGroupCode.value.trim()
|
? {
|
checkboxGroupCode: checkboxGroupCode.value.trim(),
|
checkboxGroupMaxSelected: checkboxGroupUnlimited.value ? null : checkboxGroupMaxSelected.value,
|
checkboxGroupMinSelected: checkboxGroupMinSelected.value,
|
}
|
: {}),
|
ziduanMingcheng: trimmedName,
|
shujuLeixing: type.value,
|
kongjianPeizhi: {
|
placeholder: normalizedPlaceholder,
|
...(isCheckbox.value ? { defaultChecked: defaultChecked.value } : { defaultValue: defaultValue.value }),
|
...(isTemporal.value ? { dateFormat: documentDateFormat.value } : {}),
|
...(isSelect.value ? { options: normalizedOptions } : {}),
|
},
|
});
|
}
|
</script>
|
|
<template>
|
<section data-test="private-field-form" class="private-field-form">
|
<a-form layout="vertical">
|
<a-form-item label="字段名称" :validate-status="nameError ? 'error' : undefined">
|
<a-input v-model:value="name" data-test="private-field-name" :disabled="disabled" placeholder="请输入字段名称" />
|
<template v-if="nameError" #help>
|
<span data-test="private-field-error">{{ nameError }}</span>
|
</template>
|
</a-form-item>
|
<a-form-item label="类型"><a-select v-model:value="type" data-test="private-field-type" :disabled="disabled" :options="typeOptions" /></a-form-item>
|
<a-form-item v-if="!isCheckbox && !isTemporal" label="占位文本">
|
<a-input v-model:value="placeholder" data-test="private-field-placeholder" :disabled="disabled" placeholder="请输入占位文本" />
|
</a-form-item>
|
<a-form-item :class="{ 'checkbox-default-item': isCheckbox }" label="默认值">
|
<a-checkbox v-if="isCheckbox" v-model:checked="defaultChecked" data-test="private-field-default-checked" :disabled="disabled">默认勾选</a-checkbox>
|
<a-date-picker
|
v-else-if="isTemporal"
|
allow-clear
|
class="private-field-date-picker"
|
data-test="private-field-date-default"
|
:disabled="disabled"
|
:format="pickerFormat"
|
:placeholder="isDateTime ? '请选择日期时间' : '请选择日期'"
|
:show-time="isDateTime ? { format: 'HH:mm:ss' } : false"
|
:value="defaultValue || undefined"
|
:value-format="pickerFormat"
|
@update:value="updateTemporalDefault" />
|
<a-input v-else v-model:value="defaultValue" data-test="private-field-default" :disabled="disabled" placeholder="请输入默认值" />
|
</a-form-item>
|
<section v-if="isCheckbox" class="checkbox-group-settings" aria-label="选择组设置">
|
<a-form-item label="选择组编码(选填)">
|
<a-auto-complete
|
v-model:value="checkboxGroupCode"
|
data-test="private-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="private-checkbox-code-assist"
|
type="button"
|
@click.prevent.stop="openCodeAssist"
|
@mousedown.prevent.stop>
|
<TranslationOutlined />
|
</button>
|
</a-tooltip>
|
</template>
|
</a-auto-complete>
|
<p v-if="!checkboxGroupValid" class="field-error" data-test="private-checkbox-group-error">
|
选择组编码只能使用字母、数字、下划线或连字符,且不超过 80 个字符
|
</p>
|
</a-form-item>
|
<div v-if="checkboxGroupCode.trim()" class="checkbox-group-limits">
|
<a-form-item label="最少选择数">
|
<a-input-number
|
v-model:value="checkboxGroupMinSelected"
|
data-test="private-checkbox-group-min"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
:min="0"
|
:precision="0" />
|
</a-form-item>
|
<a-form-item>
|
<template #label>
|
<span class="checkbox-limit-label">
|
<span>最多选择数</span>
|
<a-switch
|
v-model:checked="checkboxGroupUnlimited"
|
data-test="private-checkbox-group-unlimited"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
checked-children="不限"
|
un-checked-children="限额" />
|
</span>
|
</template>
|
<a-input-number
|
v-if="!checkboxGroupUnlimited"
|
v-model:value="checkboxGroupMaxSelected"
|
class="checkbox-group-max"
|
data-test="private-checkbox-group-max"
|
:disabled="disabled || Boolean(existingCheckboxGroup)"
|
:min="1"
|
:precision="0" />
|
</a-form-item>
|
</div>
|
<p v-if="checkboxGroupCode.trim() && !checkboxGroupLimitsValid" class="field-error checkbox-group-limits-error" data-test="private-checkbox-group-limits-error">
|
最少选择数必须为非负整数,最多选择数必须为正整数且不小于最少选择数
|
</p>
|
</section>
|
<a-form-item v-if="isSelect" label="下拉选项" :validate-status="optionsError ? 'error' : undefined">
|
<div v-for="(option, index) in options" :key="index" class="option-row">
|
<a-input v-model:value="option.display" :data-test="`private-option-display-${index}`" :disabled="disabled" placeholder="显示值" />
|
<a-input v-model:value="option.value" :data-test="`private-option-value-${index}`" :disabled="disabled" placeholder="业务值" />
|
</div>
|
<template v-if="optionsError" #help>
|
<span data-test="private-field-error">{{ optionsError }}</span>
|
</template>
|
</a-form-item>
|
<div class="sticky-action">
|
<a-button
|
block
|
type="primary"
|
data-test="private-field-submit"
|
:disabled="disabled || !checkboxGroupValid || !checkboxGroupLimitsValid"
|
@click="submit">创建并标注</a-button>
|
</div>
|
</a-form>
|
<CodeAssistModal
|
v-model:open="codeAssistVisible"
|
:suggest-code="suggestCode"
|
title="生成选择组编码"
|
@confirm="applyCodeSuggestion" />
|
</section>
|
</template>
|
|
<style scoped>
|
.private-field-form :deep(.ant-form-item) {
|
margin-bottom: 16px;
|
}
|
.private-field-form :deep(.ant-form-item-label) {
|
padding-bottom: 6px;
|
}
|
.option-row {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 8px;
|
margin-bottom: 8px;
|
}
|
.private-field-date-picker {
|
width: 100%;
|
}
|
.checkbox-group-limits {
|
display: grid;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
gap: 12px;
|
}
|
.checkbox-group-limits :deep(.ant-input-number),
|
.checkbox-group-max {
|
width: 100%;
|
}
|
.checkbox-group-settings {
|
margin-top: 2px;
|
padding-top: 16px;
|
border-top: 1px solid #f0f0f0;
|
}
|
.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-limits-error {
|
margin-top: 8px;
|
}
|
.checkbox-default-item :deep(.ant-form-item-control-input) {
|
min-height: 32px;
|
}
|
.private-field-form :deep(.ant-select-arrow) {
|
pointer-events: auto;
|
}
|
.code-assist-trigger {
|
display: inline-flex;
|
align-items: center;
|
justify-content: center;
|
padding: 0;
|
color: #8c8c8c;
|
background: transparent;
|
border: 0;
|
cursor: pointer;
|
}
|
.code-assist-trigger:hover {
|
color: #1677ff;
|
}
|
.sticky-action {
|
position: sticky;
|
bottom: 0;
|
z-index: 1;
|
margin-top: 16px;
|
padding: 12px 0 4px;
|
background: #fff;
|
border-top: 1px solid #f0f0f0;
|
}
|
.field-error {
|
margin: 4px 0 0;
|
color: #ff4d4f;
|
font-size: 12px;
|
line-height: 18px;
|
}
|
</style>
|