<script setup lang="ts">
|
import type { DraftField } from '../domain/draft';
|
import type { FieldConflict } from '../domain/conflict';
|
import type { FillFieldValue } from '../domain/types';
|
|
import { AimOutlined } from '@ant-design/icons-vue';
|
import dayjs from 'dayjs';
|
import { computed, onBeforeUnmount, ref, watch } from 'vue';
|
|
import { isReferenceTag } from '../domain/draft';
|
|
const props = withDefaults(
|
defineProps<{
|
compact?: boolean;
|
conflicts?: Record<string, FieldConflict>;
|
disabled: boolean;
|
errors: Record<string, string>;
|
fields: DraftField[];
|
filterInput?: string;
|
focusedTag?: null | string;
|
}>(),
|
{
|
compact: false,
|
conflicts: () => ({}),
|
filterInput: '',
|
focusedTag: null,
|
},
|
);
|
const emit = defineEmits<{
|
locate: [tag: string];
|
resolve: [tag: string, resolution: 'document' | 'draft'];
|
update: [tag: string, value: FillFieldValue];
|
}>();
|
const filterQuery = ref('');
|
let filterTimer: ReturnType<typeof setTimeout> | undefined;
|
|
const filteredFields = computed(() => {
|
const query = filterQuery.value.trim().toLocaleLowerCase();
|
if (!query) return props.fields;
|
return props.fields.filter((field) => (field.alias || '').toLocaleLowerCase().includes(query) || field.tag.toLocaleLowerCase().includes(query));
|
});
|
|
watch(
|
() => props.filterInput,
|
(value) => {
|
if (filterTimer) clearTimeout(filterTimer);
|
filterTimer = setTimeout(() => {
|
filterQuery.value = value;
|
}, 300);
|
},
|
{ immediate: true },
|
);
|
|
onBeforeUnmount(() => {
|
if (filterTimer) clearTimeout(filterTimer);
|
});
|
|
function updateDate(tag: string, value: null | string) {
|
emit('update', tag, value || '');
|
}
|
|
function isDateTime(field: DraftField): boolean {
|
return /h/iu.test(field.dateFormat || '');
|
}
|
|
function pickerFormat(field: DraftField): string {
|
return isDateTime(field) ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD';
|
}
|
|
function displayValue(value: FillFieldValue): string {
|
if (typeof value === 'boolean') return value ? '已勾选' : '未勾选';
|
return value || '(空)';
|
}
|
|
function isReadOnlyField(field: DraftField): boolean {
|
return field.readOnly === true || isReferenceTag(field.tag);
|
}
|
</script>
|
<template>
|
<div class="field-list" :class="{ 'field-list-compact': compact }">
|
<a-empty v-if="!fields.length" description="文档中没有已标注字段" />
|
<a-empty v-else-if="!filteredFields.length" :image="false" description="没有匹配的字段" />
|
<div
|
v-for="field in filteredFields"
|
:key="field.tag"
|
class="field-row"
|
:class="{ 'field-row-focused': field.tag === focusedTag }"
|
:data-field-tag="field.tag">
|
<div class="field-label">
|
<label>
|
{{ field.alias || field.tag }}<span v-if="field.required" class="required-mark" aria-label="必填"> *</span>
|
<a-tag v-if="field.valueSource === 'CROSS_TEMPLATE'" color="blue" data-test="cross-template-value-source">跨模板字段值</a-tag>
|
</label>
|
<a-tooltip title="在文档中定位" placement="topRight">
|
<a-button type="text" size="small" :aria-label="`在文档中定位 ${field.alias || field.tag}`" @click="emit('locate', field.tag)">
|
<template #icon><AimOutlined /></template>
|
</a-button>
|
</a-tooltip>
|
</div>
|
<a-input
|
v-if="field.type === 'text'"
|
:disabled="disabled || isReadOnlyField(field)"
|
placeholder="请输入"
|
:value="field.draftValue"
|
@update:value="emit('update', field.tag, $event)" />
|
<a-textarea
|
v-else-if="field.type === 'multiline'"
|
:disabled="disabled || isReadOnlyField(field)"
|
placeholder="请输入"
|
:value="field.draftValue"
|
@update:value="emit('update', field.tag, $event)" />
|
<a-select
|
v-else-if="field.type === 'select'"
|
:disabled="disabled || isReadOnlyField(field)"
|
:options="field.options.map((item) => ({ label: item.display, value: item.value }))"
|
placeholder="请选择"
|
:value="field.draftValue === '' ? undefined : field.draftValue"
|
@update:value="emit('update', field.tag, $event)" />
|
<a-checkbox
|
v-else-if="field.type === 'checkbox'"
|
:checked="Boolean(field.draftValue)"
|
:disabled="disabled || isReadOnlyField(field)"
|
@update:checked="emit('update', field.tag, $event)" />
|
<a-date-picker
|
v-else
|
:disabled="disabled || isReadOnlyField(field)"
|
:format="pickerFormat(field)"
|
:placeholder="isDateTime(field) ? '请选择日期时间' : '请选择日期'"
|
:show-time="isDateTime(field) ? { format: 'HH:mm:ss' } : false"
|
:value="field.draftValue ? dayjs(String(field.draftValue)) : null"
|
:value-format="pickerFormat(field)"
|
@change="updateDate(field.tag, $event)" />
|
<div v-if="conflicts[field.tag]" class="field-conflict" role="alert">
|
<div class="field-conflict-value">
|
<span>文档</span><strong>{{ displayValue(conflicts[field.tag]!.documentValue) }}</strong>
|
</div>
|
<div class="field-conflict-value">
|
<span>草稿</span><strong>{{ displayValue(conflicts[field.tag]!.draftValue) }}</strong>
|
</div>
|
<a-space>
|
<a-button size="small" data-resolution="document" @click="emit('resolve', field.tag, 'document')">使用文档值</a-button>
|
<a-button size="small" type="primary" data-resolution="draft" @click="emit('resolve', field.tag, 'draft')">保留草稿</a-button>
|
</a-space>
|
</div>
|
<div v-if="errors[field.tag] || field.formulaStatus === 'ERROR'" class="field-error">
|
{{ errors[field.tag] || field.formulaMessage || '公式计算失败' }}
|
</div>
|
</div>
|
</div>
|
</template>
|