扩展用户管理字段与组织编码生成,顶部改为仓库下拉;Nana 分 AI 与专属顾问 Tab,仓库页移除客服字段,并完善扫描语音与订单相关页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
798 lines
21 KiB
Vue
798 lines
21 KiB
Vue
<script lang="ts" setup>
|
||
import { computed, ref, watch } from 'vue';
|
||
import { Message, Modal } from '@arco-design/web-vue';
|
||
import { IconUpload, IconFile } from '@arco-design/web-vue/es/icon';
|
||
import * as XLSX from 'xlsx';
|
||
import {
|
||
getOrderTemplateCustomDetail,
|
||
getOrderTemplateFieldSchema,
|
||
postOrderTemplateCustomUpload,
|
||
postOrderTemplateSaveMapping,
|
||
type OrderTemplateCustomDetail,
|
||
type OrderTemplateFieldSchemaItem,
|
||
} from '@/api/order';
|
||
|
||
const DEFAULT_HEADER_ROW_INDEX = 1;
|
||
const DEFAULT_DATA_START_ROW_INDEX = 2;
|
||
|
||
export interface StandardTemplateOption {
|
||
templateKey: string;
|
||
label: string;
|
||
id?: number;
|
||
}
|
||
|
||
const props = defineProps<{
|
||
visible: boolean;
|
||
templateId?: number;
|
||
standardTemplateOptions: StandardTemplateOption[];
|
||
}>();
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:visible', value: boolean): void;
|
||
(e: 'success', payload?: { templateId: number }): void;
|
||
}>();
|
||
|
||
const loading = ref(false);
|
||
const saving = ref(false);
|
||
const fieldsLoading = ref(false);
|
||
const detail = ref<OrderTemplateCustomDetail | null>(null);
|
||
|
||
const name = ref('');
|
||
const file = ref<File | null>(null);
|
||
const dragActive = ref(false);
|
||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||
const sourceHeaders = ref<string[]>([]);
|
||
|
||
const standardTemplateKey = ref('dedicated_small_parcel_v1');
|
||
const standardFields = ref<OrderTemplateFieldSchemaItem[]>([]);
|
||
const mappingMap = ref<Record<string, string>>({});
|
||
const baselineSnapshot = ref('');
|
||
|
||
const isCreateMode = computed(() => !props.templateId || props.templateId <= 0);
|
||
const modalTitle = computed(() => {
|
||
if (isCreateMode.value) return '添加自定义模板';
|
||
const templateName = detail.value?.name || name.value;
|
||
return templateName ? `编辑自定义模板 - ${templateName}` : '编辑自定义模板';
|
||
});
|
||
|
||
const standardSelectOptions = computed(() =>
|
||
props.standardTemplateOptions.map((item) => ({
|
||
label: item.label,
|
||
value: item.templateKey,
|
||
})),
|
||
);
|
||
|
||
const mappingFieldsVisible = computed(() => {
|
||
if (isCreateMode.value) {
|
||
return standardFields.value.length > 0;
|
||
}
|
||
return !!detail.value && standardFields.value.length > 0;
|
||
});
|
||
|
||
const canSubmitSave = computed(() => {
|
||
if (isCreateMode.value) {
|
||
return (
|
||
!!name.value.trim() &&
|
||
!!file.value &&
|
||
sourceHeaders.value.length > 0 &&
|
||
standardFields.value.length > 0
|
||
);
|
||
}
|
||
return !!detail.value && sourceHeaders.value.length > 0 && standardFields.value.length > 0;
|
||
});
|
||
|
||
const isDirty = computed(() => baselineSnapshot.value !== serializeFormState());
|
||
|
||
const groupLabels: Record<string, string> = {
|
||
basic: '基本信息',
|
||
sender: '发件人',
|
||
receiver: '收件人',
|
||
declaration: '申报信息',
|
||
};
|
||
|
||
const groupedFields = computed(() => {
|
||
const groups = new Map<string, OrderTemplateFieldSchemaItem[]>();
|
||
standardFields.value.forEach((field) => {
|
||
const group = field.group || 'basic';
|
||
if (!groups.has(group)) groups.set(group, []);
|
||
groups.get(group)!.push(field);
|
||
});
|
||
return Array.from(groups.entries()).map(([key, items]) => ({
|
||
key,
|
||
label: groupLabels[key] || key,
|
||
items,
|
||
}));
|
||
});
|
||
|
||
const sourceHeaderOptions = computed(() => [
|
||
{ label: '不映射', value: '' },
|
||
...sourceHeaders.value.map((h) => ({ label: h, value: h })),
|
||
]);
|
||
|
||
const missingRequiredLabels = computed(() =>
|
||
standardFields.value
|
||
.filter((f) => f.required)
|
||
.filter((f) => !String(mappingMap.value[f.fieldKey] ?? '').trim())
|
||
.map((f) => f.label),
|
||
);
|
||
|
||
const unmappedSourceHeaders = computed(() => {
|
||
const used = new Set(Object.values(mappingMap.value).filter(Boolean));
|
||
return sourceHeaders.value.filter((h) => !used.has(h));
|
||
});
|
||
|
||
function defaultStandardKey() {
|
||
return props.standardTemplateOptions[0]?.templateKey || 'dedicated_small_parcel_v1';
|
||
}
|
||
|
||
function serializeFormState() {
|
||
return JSON.stringify({
|
||
name: name.value.trim(),
|
||
standardTemplateKey: standardTemplateKey.value,
|
||
fileName: file.value?.name || '',
|
||
sourceHeaders: sourceHeaders.value,
|
||
mappingMap: mappingMap.value,
|
||
});
|
||
}
|
||
|
||
function captureBaseline() {
|
||
baselineSnapshot.value = serializeFormState();
|
||
}
|
||
|
||
function normalizeHeader(text: string) {
|
||
return String(text || '')
|
||
.trim()
|
||
.replace(/[((].*?[))]/g, '')
|
||
.replace(/[\s*]/g, '')
|
||
.toLowerCase();
|
||
}
|
||
|
||
function buildSuggestedMapping(
|
||
fields: OrderTemplateFieldSchemaItem[],
|
||
headers: string[],
|
||
existing?: Record<string, string>,
|
||
) {
|
||
const next: Record<string, string> = {};
|
||
const used = new Set<string>();
|
||
fields.forEach((field) => {
|
||
const prev = existing?.[field.fieldKey];
|
||
if (prev && headers.includes(prev) && !used.has(prev)) {
|
||
next[field.fieldKey] = prev;
|
||
used.add(prev);
|
||
return;
|
||
}
|
||
|
||
const normalizedLabel = normalizeHeader(field.label);
|
||
const matched = headers.find((header) => {
|
||
if (used.has(header)) return false;
|
||
const normalizedHeader = normalizeHeader(header);
|
||
return (
|
||
normalizedHeader === normalizedLabel ||
|
||
normalizedHeader.includes(normalizedLabel) ||
|
||
normalizedLabel.includes(normalizedHeader)
|
||
);
|
||
});
|
||
|
||
next[field.fieldKey] = matched ?? '';
|
||
if (matched) used.add(matched);
|
||
});
|
||
return next;
|
||
}
|
||
|
||
function resetCreateForm() {
|
||
name.value = '';
|
||
file.value = null;
|
||
sourceHeaders.value = [];
|
||
mappingMap.value = {};
|
||
dragActive.value = false;
|
||
if (fileInputRef.value) fileInputRef.value.value = '';
|
||
standardTemplateKey.value = defaultStandardKey();
|
||
}
|
||
|
||
function resetAll() {
|
||
detail.value = null;
|
||
standardFields.value = [];
|
||
baselineSnapshot.value = '';
|
||
resetCreateForm();
|
||
}
|
||
|
||
async function parseFileHeaders(picked: File) {
|
||
const buffer = await picked.arrayBuffer();
|
||
const workbook = XLSX.read(buffer, { type: 'array' });
|
||
const sheetName = workbook.SheetNames[0];
|
||
if (!sheetName) {
|
||
sourceHeaders.value = [];
|
||
return;
|
||
}
|
||
const sheet = workbook.Sheets[sheetName];
|
||
const table = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '' }) as any[][];
|
||
const row = table[0] || [];
|
||
sourceHeaders.value = row
|
||
.map((cell) => String(cell ?? '').trim())
|
||
.filter(Boolean);
|
||
mappingMap.value = buildSuggestedMapping(standardFields.value, sourceHeaders.value, mappingMap.value);
|
||
}
|
||
|
||
function acceptFile(picked: File | null | undefined) {
|
||
if (!picked) return;
|
||
const ext = picked.name.toLowerCase();
|
||
if (!ext.endsWith('.xlsx') && !ext.endsWith('.xls')) {
|
||
Message.warning('仅支持 .xlsx / .xls 文件');
|
||
return;
|
||
}
|
||
file.value = picked;
|
||
if (!name.value.trim()) {
|
||
name.value = picked.name.replace(/\.(xlsx|xls)$/i, '');
|
||
}
|
||
void parseFileHeaders(picked);
|
||
}
|
||
|
||
function triggerPicker() {
|
||
if (!isCreateMode.value) return;
|
||
fileInputRef.value?.click();
|
||
}
|
||
|
||
function onFileChange(e: Event) {
|
||
const input = e.target as HTMLInputElement;
|
||
acceptFile(input.files?.[0]);
|
||
}
|
||
|
||
function onDragOver() {
|
||
if (!isCreateMode.value) return;
|
||
dragActive.value = true;
|
||
}
|
||
|
||
function onDragLeave() {
|
||
dragActive.value = false;
|
||
}
|
||
|
||
function onDrop(e: DragEvent) {
|
||
if (!isCreateMode.value) return;
|
||
dragActive.value = false;
|
||
acceptFile(e.dataTransfer?.files?.[0]);
|
||
}
|
||
|
||
async function loadStandardFields(preserveMapping = true) {
|
||
if (!standardTemplateKey.value) return;
|
||
fieldsLoading.value = true;
|
||
try {
|
||
const schema = await getOrderTemplateFieldSchema(standardTemplateKey.value);
|
||
standardFields.value = schema.fields ?? [];
|
||
const existing = preserveMapping ? { ...mappingMap.value } : undefined;
|
||
mappingMap.value = buildSuggestedMapping(standardFields.value, sourceHeaders.value, existing);
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '加载标准模板字段失败');
|
||
} finally {
|
||
fieldsLoading.value = false;
|
||
}
|
||
}
|
||
|
||
function applySavedMappings(data: OrderTemplateCustomDetail) {
|
||
sourceHeaders.value = data.sourceHeaders ?? [];
|
||
const saved: Record<string, string> = {};
|
||
(data.mapping?.mappings ?? []).forEach((item) => {
|
||
if (item.standardFieldKey && item.sourceColumn) {
|
||
saved[item.standardFieldKey] = item.sourceColumn;
|
||
}
|
||
});
|
||
mappingMap.value = buildSuggestedMapping(standardFields.value, sourceHeaders.value, saved);
|
||
}
|
||
|
||
async function loadDetail() {
|
||
if (!props.templateId || props.templateId <= 0) return;
|
||
loading.value = true;
|
||
try {
|
||
const data = await getOrderTemplateCustomDetail(props.templateId);
|
||
detail.value = data;
|
||
name.value = data.name;
|
||
standardTemplateKey.value = data.standardTemplateKey || data.mapping?.standardTemplateKey || defaultStandardKey();
|
||
await loadStandardFields(false);
|
||
applySavedMappings(data);
|
||
captureBaseline();
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '加载模板详情失败');
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
async function handleStandardTemplateChange() {
|
||
await loadStandardFields(true);
|
||
}
|
||
|
||
function buildSaveMappings() {
|
||
return Object.entries(mappingMap.value)
|
||
.filter(([, sourceColumn]) => String(sourceColumn ?? '').trim())
|
||
.map(([standardFieldKey, sourceColumn]) => ({ standardFieldKey, sourceColumn }));
|
||
}
|
||
|
||
async function saveMappingForTemplate(templateId: number) {
|
||
return postOrderTemplateSaveMapping({
|
||
templateId,
|
||
standardTemplateKey: standardTemplateKey.value,
|
||
headerRowIndex: DEFAULT_HEADER_ROW_INDEX,
|
||
dataStartRowIndex: DEFAULT_DATA_START_ROW_INDEX,
|
||
mappings: buildSaveMappings(),
|
||
});
|
||
}
|
||
|
||
async function handleSave(): Promise<boolean> {
|
||
if (isCreateMode.value) {
|
||
const templateName = name.value.trim();
|
||
if (!templateName) {
|
||
Message.warning('请输入模板名称');
|
||
return false;
|
||
}
|
||
if (!file.value) {
|
||
Message.warning('请上传 Excel 样例文件');
|
||
return false;
|
||
}
|
||
if (!standardTemplateKey.value) {
|
||
Message.warning('请选择映射的标准模板');
|
||
return false;
|
||
}
|
||
if (!canSubmitSave.value) {
|
||
Message.warning('请完成字段映射配置');
|
||
return false;
|
||
}
|
||
} else if (!props.templateId) {
|
||
return false;
|
||
}
|
||
|
||
saving.value = true;
|
||
try {
|
||
let targetId = props.templateId ?? 0;
|
||
if (isCreateMode.value && file.value) {
|
||
const upload = await postOrderTemplateCustomUpload(name.value.trim(), file.value);
|
||
targetId = upload.templateId;
|
||
}
|
||
if (!Number.isFinite(targetId) || targetId <= 0) {
|
||
Message.error('模板 ID 无效,请重新上传样例文件');
|
||
return false;
|
||
}
|
||
const result = await saveMappingForTemplate(targetId);
|
||
if (result.missingRequiredFields?.length) {
|
||
Message.warning(`仍有必填字段未映射:${result.missingRequiredFields.join('、')}`);
|
||
if (!isCreateMode.value) {
|
||
await loadDetail();
|
||
}
|
||
return false;
|
||
}
|
||
Message.success(isCreateMode.value ? '自定义模板已创建' : '映射已保存');
|
||
emit('success', { templateId: targetId });
|
||
forceClose();
|
||
return true;
|
||
} catch (e: any) {
|
||
Message.error(e?.message || '保存失败');
|
||
return false;
|
||
} finally {
|
||
saving.value = false;
|
||
}
|
||
}
|
||
|
||
function forceClose() {
|
||
emit('update:visible', false);
|
||
}
|
||
|
||
function handleCloseRequest() {
|
||
if (!isDirty.value) {
|
||
forceClose();
|
||
return;
|
||
}
|
||
Modal.confirm({
|
||
title: '是否保存?',
|
||
content: isCreateMode.value
|
||
? '关闭后未保存的内容将不会创建模板及映射关联。'
|
||
: '关闭后未保存的映射修改将丢失。',
|
||
okText: '保存',
|
||
cancelText: '不保存',
|
||
maskClosable: false,
|
||
onOk: async () => {
|
||
const saved = await handleSave();
|
||
if (!saved) {
|
||
throw new Error('save failed');
|
||
}
|
||
},
|
||
onCancel: () => {
|
||
forceClose();
|
||
},
|
||
});
|
||
}
|
||
|
||
watch(
|
||
() => [props.visible, props.templateId] as const,
|
||
async ([open, templateId]) => {
|
||
if (!open) {
|
||
resetAll();
|
||
return;
|
||
}
|
||
if (templateId && templateId > 0) {
|
||
await loadDetail();
|
||
} else {
|
||
standardTemplateKey.value = defaultStandardKey();
|
||
await loadStandardFields(false);
|
||
captureBaseline();
|
||
}
|
||
},
|
||
);
|
||
</script>
|
||
|
||
<template>
|
||
<a-modal
|
||
:visible="visible"
|
||
:title="modalTitle"
|
||
:width="1320"
|
||
:mask-closable="false"
|
||
unmount-on-close
|
||
class="custom-template-modal"
|
||
@cancel="handleCloseRequest"
|
||
>
|
||
<a-spin :loading="loading || fieldsLoading" class="modal-spin">
|
||
<div class="modal-layout">
|
||
<aside class="panel-left">
|
||
<div class="panel-title">模板信息</div>
|
||
|
||
<a-form layout="vertical" class="left-form">
|
||
<a-form-item label="映射标准模板" required>
|
||
<a-select
|
||
v-model="standardTemplateKey"
|
||
:options="standardSelectOptions"
|
||
placeholder="选择标准模板"
|
||
@change="handleStandardTemplateChange"
|
||
/>
|
||
</a-form-item>
|
||
|
||
<a-form-item label="模板名称" :required="isCreateMode">
|
||
<a-input
|
||
v-if="isCreateMode"
|
||
v-model="name"
|
||
placeholder="例如:客户A导入模板"
|
||
allow-clear
|
||
/>
|
||
<div v-else class="readonly-text">{{ detail?.name || name }}</div>
|
||
</a-form-item>
|
||
|
||
<a-form-item v-if="isCreateMode" label="Excel 样例" required>
|
||
<div
|
||
class="upload-zone"
|
||
:class="{ 'has-file': !!file, 'is-dragover': dragActive }"
|
||
@click="triggerPicker"
|
||
@dragover.prevent="onDragOver"
|
||
@dragleave.prevent="onDragLeave"
|
||
@drop.prevent="onDrop"
|
||
>
|
||
<icon-upload v-if="!file" class="upload-icon" />
|
||
<icon-file v-else class="upload-icon is-file" />
|
||
<div v-if="file" class="upload-file-name">{{ file.name }}</div>
|
||
<div v-else class="upload-placeholder">点击或拖拽上传 Excel</div>
|
||
<div class="upload-tip">第一行需为列名表头,支持 .xlsx / .xls</div>
|
||
</div>
|
||
<input
|
||
ref="fileInputRef"
|
||
type="file"
|
||
accept=".xlsx,.xls"
|
||
class="hidden-input"
|
||
@change="onFileChange"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
|
||
<div v-if="!isCreateMode" class="status-row">
|
||
<span class="status-label">映射状态</span>
|
||
<a-tag v-if="detail?.mappingStatus === 'ready'" color="green" size="small">已就绪</a-tag>
|
||
<a-tag v-else color="orangered" size="small">待配置</a-tag>
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="panel-right">
|
||
<div class="panel-title panel-title-right">
|
||
<span>字段映射</span>
|
||
<span v-if="sourceHeaders.length" class="header-count">已识别 {{ sourceHeaders.length }} 列</span>
|
||
</div>
|
||
|
||
<div v-if="!mappingFieldsVisible" class="mapping-empty">
|
||
<a-empty description="正在加载标准字段…" />
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div v-if="isCreateMode && !sourceHeaders.length" class="mapping-hint">
|
||
标准字段已加载,上传 Excel 样例后可选择各列映射
|
||
</div>
|
||
|
||
<div v-for="group in groupedFields" :key="group.key" class="mapping-group">
|
||
<div class="mapping-group-title">{{ group.label }}</div>
|
||
<div class="mapping-table-wrap">
|
||
<table class="mapping-table">
|
||
<thead>
|
||
<tr>
|
||
<th>标准字段</th>
|
||
<th>上传模板列</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="field in group.items" :key="field.fieldKey">
|
||
<td class="field-label-col">
|
||
<span v-if="field.required" class="required-star">*</span>
|
||
{{ field.label }}
|
||
<span class="field-key">{{ field.fieldKey }}</span>
|
||
</td>
|
||
<td>
|
||
<a-select
|
||
v-model="mappingMap[field.fieldKey]"
|
||
allow-search
|
||
allow-clear
|
||
:disabled="!sourceHeaders.length"
|
||
:placeholder="sourceHeaders.length ? '选择上传模板列' : '请先上传 Excel'"
|
||
:options="sourceHeaderOptions"
|
||
/>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mapping-summary">
|
||
<div v-if="missingRequiredLabels.length" class="summary-warn">
|
||
缺失必填映射:{{ missingRequiredLabels.join('、') }}
|
||
</div>
|
||
<div v-if="unmappedSourceHeaders.length" class="summary-muted">
|
||
未映射的上传列:{{ unmappedSourceHeaders.join('、') }}
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</section>
|
||
</div>
|
||
</a-spin>
|
||
|
||
<template #footer>
|
||
<a-button @click="handleCloseRequest">取消</a-button>
|
||
<a-button type="primary" :loading="saving" @click="handleSave">
|
||
{{ isCreateMode ? '创建并保存' : '保存映射' }}
|
||
</a-button>
|
||
</template>
|
||
</a-modal>
|
||
</template>
|
||
|
||
<style scoped lang="less">
|
||
.modal-spin {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
.modal-layout {
|
||
display: flex;
|
||
gap: 0;
|
||
min-height: 560px;
|
||
max-height: 78vh;
|
||
}
|
||
|
||
.panel-left {
|
||
width: 300px;
|
||
flex-shrink: 0;
|
||
border-right: 1px solid #eef0f3;
|
||
padding-right: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.panel-right {
|
||
flex: 1;
|
||
min-width: 0;
|
||
padding-left: 20px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.panel-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
|
||
&::before {
|
||
content: '';
|
||
width: 3px;
|
||
height: 14px;
|
||
border-radius: 2px;
|
||
background: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.panel-title-right {
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.header-count {
|
||
font-size: 12px;
|
||
font-weight: 400;
|
||
color: #86909c;
|
||
|
||
&::before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.left-form {
|
||
:deep(.arco-form-item) {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
:deep(.arco-form-item-label) {
|
||
font-size: 13px;
|
||
color: #4e5969;
|
||
}
|
||
}
|
||
|
||
.readonly-text {
|
||
min-height: 32px;
|
||
line-height: 32px;
|
||
font-size: 14px;
|
||
color: #1d2129;
|
||
}
|
||
|
||
.upload-zone {
|
||
border: 1px dashed #c9cdd4;
|
||
border-radius: 4px;
|
||
padding: 20px 16px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
background: #f7f8fa;
|
||
transition: border-color 0.2s, background 0.2s;
|
||
|
||
&:hover,
|
||
&.is-dragover {
|
||
border-color: var(--app-primary-color);
|
||
background: rgba(var(--primary-6), 0.04);
|
||
}
|
||
|
||
&.has-file {
|
||
border-style: solid;
|
||
border-color: rgba(var(--primary-6), 0.35);
|
||
background: #fff;
|
||
}
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 28px;
|
||
color: #86909c;
|
||
margin-bottom: 8px;
|
||
|
||
&.is-file {
|
||
color: var(--app-primary-color);
|
||
}
|
||
}
|
||
|
||
.upload-placeholder {
|
||
font-size: 13px;
|
||
color: #4e5969;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.upload-file-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #1d2129;
|
||
margin-bottom: 4px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.upload-tip {
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
}
|
||
|
||
.hidden-input {
|
||
display: none;
|
||
}
|
||
|
||
.status-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding-top: 4px;
|
||
border-top: 1px solid #eef0f3;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.status-label {
|
||
font-size: 13px;
|
||
color: #4e5969;
|
||
}
|
||
|
||
.mapping-empty {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-height: 420px;
|
||
}
|
||
|
||
.mapping-hint {
|
||
margin-bottom: 12px;
|
||
padding: 8px 12px;
|
||
font-size: 13px;
|
||
color: #4e5969;
|
||
background: #f7f8fa;
|
||
border-radius: 4px;
|
||
border: 1px solid #eef0f3;
|
||
}
|
||
|
||
.mapping-group {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.mapping-group-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #1d2129;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.mapping-table-wrap {
|
||
border: 1px solid #e8eaed;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mapping-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
|
||
th,
|
||
td {
|
||
border-bottom: 1px solid #eef0f3;
|
||
padding: 8px 12px;
|
||
text-align: left;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
th {
|
||
background: #f7f8fa;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #4e5969;
|
||
}
|
||
|
||
tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
:deep(.arco-select) {
|
||
width: 100%;
|
||
min-width: 180px;
|
||
}
|
||
}
|
||
|
||
.field-label-col {
|
||
width: 40%;
|
||
min-width: 200px;
|
||
}
|
||
|
||
.required-star {
|
||
color: #f53f3f;
|
||
margin-right: 4px;
|
||
}
|
||
|
||
.field-key {
|
||
display: block;
|
||
margin-top: 2px;
|
||
font-size: 12px;
|
||
color: #86909c;
|
||
}
|
||
|
||
.mapping-summary {
|
||
margin-top: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.summary-warn {
|
||
color: #f53f3f;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.summary-muted {
|
||
color: #86909c;
|
||
}
|
||
</style>
|