feat: 统一数据目录映射配置
This commit is contained in:
@@ -337,36 +337,73 @@
|
||||
|
||||
<n-tab-pane name="rules" tab="规则映射">
|
||||
<div class="settings-rules-grid">
|
||||
<n-card title="Sheet 过滤规则" size="small" class="work-card">
|
||||
<n-space vertical>
|
||||
<p class="form-hint">匹配这些关键词的 Sheet 将被跳过处理</p>
|
||||
<div class="filter-tags">
|
||||
<n-tag
|
||||
v-for="(filter, index) in sheetFilters"
|
||||
:key="`${filter}-${index}`"
|
||||
closable
|
||||
@close="removeSheetFilter(index)"
|
||||
>
|
||||
{{ filter }}
|
||||
</n-tag>
|
||||
<n-empty v-if="sheetFilters.length === 0" size="small" description="暂无过滤规则" />
|
||||
</div>
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="newSheetFilter"
|
||||
placeholder="输入需要跳过的 Sheet 关键词"
|
||||
@keydown.enter.prevent="addSheetFilter"
|
||||
/>
|
||||
<n-button @click="addSheetFilter">添加</n-button>
|
||||
</n-input-group>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingSheetFilter" @click="saveSheetFilter">保存规则</n-button>
|
||||
<div class="settings-rules-side">
|
||||
<n-card title="数据目录映射" size="small" class="work-card">
|
||||
<n-space vertical>
|
||||
<p class="form-hint">每行配置一个源目录到暂存表的映射。就绪规则选择“每日7天”时按目标周 7 天检查;选择“自动日/周”时按目录最新文件自动识别日粒度或周粒度。</p>
|
||||
<div class="directory-mapping-list">
|
||||
<div
|
||||
v-for="(item, index) in dataMappingsForm.directories"
|
||||
:key="`${item.path}-${item.table}-${item.ready_rule}-${index}`"
|
||||
class="directory-mapping-row"
|
||||
>
|
||||
<n-input v-model:value="item.path" size="small" placeholder="目录,例如 4G 或 RJ/700M/700RJYD" />
|
||||
<n-input v-model:value="item.table" size="small" placeholder="暂存表,例如 4G_UD" />
|
||||
<n-select v-model:value="item.ready_rule" size="small" :options="readyRuleOptions" />
|
||||
<n-button quaternary circle size="small" type="error" @click="removeDirectoryMapping(index)">
|
||||
<template #icon>
|
||||
<n-icon><CloseOutline /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
<n-empty v-if="dataMappingsForm.directories.length === 0" size="small" description="暂无目录映射" />
|
||||
</div>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="newDirectoryMapping.path" placeholder="目录路径" @keydown.enter.prevent="addDirectoryMapping" />
|
||||
<n-input v-model:value="newDirectoryMapping.table" placeholder="暂存表名" @keydown.enter.prevent="addDirectoryMapping" />
|
||||
<n-select v-model:value="newDirectoryMapping.ready_rule" class="directory-ready-select" :options="readyRuleOptions" />
|
||||
<n-button @click="addDirectoryMapping">添加</n-button>
|
||||
</n-input-group>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingDirectoryMappings" @click="saveDirectoryMappings">保存目录映射</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="Sheet 过滤规则" size="small" class="work-card">
|
||||
<n-space vertical>
|
||||
<p class="form-hint">匹配这些关键词的 Sheet 将被跳过处理</p>
|
||||
<div class="filter-tags">
|
||||
<n-tag
|
||||
v-for="(filter, index) in sheetFilters"
|
||||
:key="`${filter}-${index}`"
|
||||
closable
|
||||
@close="removeSheetFilter(index)"
|
||||
>
|
||||
{{ filter }}
|
||||
</n-tag>
|
||||
<n-empty v-if="sheetFilters.length === 0" size="small" description="暂无过滤规则" />
|
||||
</div>
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="newSheetFilter"
|
||||
placeholder="输入需要跳过的 Sheet 关键词"
|
||||
@keydown.enter.prevent="addSheetFilter"
|
||||
/>
|
||||
<n-button @click="addSheetFilter">添加</n-button>
|
||||
</n-input-group>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingSheetFilter" @click="saveSheetFilter">保存规则</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-card size="small" class="work-card field-mapping-card">
|
||||
<template #header>
|
||||
@@ -494,6 +531,8 @@ import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||
import type {
|
||||
ApiMessage,
|
||||
AppConfig,
|
||||
DataDirectoryMapping,
|
||||
DataMappingsConfig,
|
||||
HistoryRetentionConfig,
|
||||
MetrixConfig,
|
||||
RemoteAutoSchedulerConfig,
|
||||
@@ -526,6 +565,7 @@ const savingBackend = ref(false);
|
||||
const savingMetrix = ref(false);
|
||||
const sourceType = ref<'ftp' | 'sftp' | 'metrix'>('sftp');
|
||||
const warehouseType = ref<'mysql' | 'metrix'>('mysql');
|
||||
const savingDirectoryMappings = ref(false);
|
||||
const savingHistoryRetention = ref(false);
|
||||
const savingSheetFilter = ref(false);
|
||||
const savingExtractFields = ref(false);
|
||||
@@ -571,8 +611,21 @@ const metrixForm = reactive<MetrixConfig>({
|
||||
storage_id: '',
|
||||
database_conn_id: '',
|
||||
target_database: '',
|
||||
recent_days: 7,
|
||||
data_dir_to_table: { '4G': '4G_UD', '5G': '5G_UD' }
|
||||
recent_days: 7
|
||||
});
|
||||
|
||||
const dataMappingsForm = reactive<DataMappingsConfig>({
|
||||
directories: [
|
||||
{ path: '4G', table: '4G_UD', ready_rule: 'daily' },
|
||||
{ path: '5G', table: '5G_UD', ready_rule: 'daily' }
|
||||
],
|
||||
table_field_mappings: {}
|
||||
});
|
||||
|
||||
const newDirectoryMapping = reactive<DataDirectoryMapping>({
|
||||
path: '',
|
||||
table: '',
|
||||
ready_rule: 'daily'
|
||||
});
|
||||
|
||||
const historyRetentionForm = reactive<HistoryRetentionConfig>({
|
||||
@@ -600,6 +653,10 @@ const schedulerWeekOptions: SelectOption[] = [
|
||||
{ label: '上周', value: 0 },
|
||||
{ label: '上上周', value: -1 }
|
||||
];
|
||||
const readyRuleOptions: SelectOption[] = [
|
||||
{ label: '每日7天', value: 'daily' },
|
||||
{ label: '自动日/周', value: 'auto' }
|
||||
];
|
||||
|
||||
const visibleFieldMappings = computed(() => {
|
||||
const keyword = fieldSearch.value.trim().toLowerCase();
|
||||
@@ -696,8 +753,8 @@ async function loadConfig() {
|
||||
metrixForm.database_conn_id = config.metrix.database_conn_id || '';
|
||||
metrixForm.target_database = config.metrix.target_database || '';
|
||||
metrixForm.recent_days = Number(config.metrix.recent_days) || 7;
|
||||
metrixForm.data_dir_to_table = config.metrix.data_dir_to_table || { '4G': '4G_UD', '5G': '5G_UD' };
|
||||
}
|
||||
Object.assign(dataMappingsForm, normalizeDataMappingsConfig(config.data_mappings));
|
||||
mysqlForm.host = config.mysql.host;
|
||||
mysqlForm.port = config.mysql.port;
|
||||
mysqlForm.user = config.mysql.user;
|
||||
@@ -824,6 +881,51 @@ async function saveRemote() {
|
||||
}
|
||||
}
|
||||
|
||||
function addDirectoryMapping() {
|
||||
const path = normalizeDirectoryText(newDirectoryMapping.path);
|
||||
const table = newDirectoryMapping.table.trim();
|
||||
const readyRule = newDirectoryMapping.ready_rule === 'auto' ? 'auto' : 'daily';
|
||||
if (!path || !table) {
|
||||
message.warning('请填写目录路径和暂存表名');
|
||||
return;
|
||||
}
|
||||
const exists = dataMappingsForm.directories.some(
|
||||
item => normalizeDirectoryText(item.path) === path && item.table.trim() === table && item.ready_rule === readyRule
|
||||
);
|
||||
if (exists) {
|
||||
message.warning('该目录映射已存在');
|
||||
return;
|
||||
}
|
||||
dataMappingsForm.directories.push({ path, table, ready_rule: readyRule });
|
||||
newDirectoryMapping.path = '';
|
||||
newDirectoryMapping.table = '';
|
||||
newDirectoryMapping.ready_rule = 'daily';
|
||||
}
|
||||
|
||||
function removeDirectoryMapping(index: number) {
|
||||
dataMappingsForm.directories.splice(index, 1);
|
||||
}
|
||||
|
||||
async function saveDirectoryMappings() {
|
||||
const config = normalizeDataMappingsConfig(dataMappingsForm);
|
||||
if (config.directories.length === 0) {
|
||||
message.warning('请至少保留一个数据目录映射');
|
||||
return;
|
||||
}
|
||||
|
||||
savingDirectoryMappings.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/config/data-mappings', config);
|
||||
Object.assign(dataMappingsForm, config);
|
||||
configUpdate.value = result.update || configUpdate.value;
|
||||
message.success('数据目录映射已保存');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存数据目录映射失败');
|
||||
} finally {
|
||||
savingDirectoryMappings.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function addSchedulerDirectory() {
|
||||
const value = normalizeDirectoryText(newSchedulerDirectory.value);
|
||||
if (!value) return;
|
||||
@@ -1072,6 +1174,31 @@ function normalizeRemoteConfig(config: RemoteDataConfig | undefined): RemoteData
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDataMappingsConfig(config: DataMappingsConfig | undefined): DataMappingsConfig {
|
||||
const directories: DataDirectoryMapping[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of config?.directories || []) {
|
||||
const path = normalizeDirectoryText(item.path || '');
|
||||
const table = String(item.table || '').trim();
|
||||
const readyRule = item.ready_rule === 'auto' ? 'auto' : 'daily';
|
||||
if (!path || !table) continue;
|
||||
const key = `${path}\n${table}\n${readyRule}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
directories.push({ path, table, ready_rule: readyRule });
|
||||
}
|
||||
if (directories.length === 0) {
|
||||
directories.push(
|
||||
{ path: '4G', table: '4G_UD', ready_rule: 'daily' },
|
||||
{ path: '5G', table: '5G_UD', ready_rule: 'daily' }
|
||||
);
|
||||
}
|
||||
return {
|
||||
directories,
|
||||
table_field_mappings: config?.table_field_mappings || {}
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAutoSchedulerConfig(config: RemoteAutoSchedulerConfig | undefined): RemoteAutoSchedulerConfig {
|
||||
return {
|
||||
enabled: Boolean(config?.enabled),
|
||||
|
||||
+34
-4
@@ -1483,14 +1483,13 @@ select {
|
||||
}
|
||||
|
||||
.settings-rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr);
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding-right: 6px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overflow: hidden;
|
||||
scrollbar-color: var(--td-scrollbar-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
@@ -1509,10 +1508,22 @@ select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.settings-rules-grid > .work-card {
|
||||
.settings-rules-grid > .work-card,
|
||||
.settings-rules-side > .work-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-rules-side {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
padding-right: 6px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.settings-security-panel {
|
||||
max-width: 560px;
|
||||
}
|
||||
@@ -1532,6 +1543,23 @@ select {
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.directory-mapping-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.directory-mapping-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 104px auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.directory-ready-select {
|
||||
width: 112px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin: 0;
|
||||
color: var(--td-text-color-placeholder);
|
||||
@@ -1991,6 +2019,8 @@ select {
|
||||
}
|
||||
|
||||
.field-mapping-card {
|
||||
grid-row: auto;
|
||||
grid-column: auto;
|
||||
height: min(760px, calc(100vh - 260px));
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
@@ -98,9 +98,9 @@ export interface AppConfig {
|
||||
dbname: string;
|
||||
};
|
||||
metrix: MetrixConfig;
|
||||
data_mappings: DataMappingsConfig;
|
||||
remote_data: RemoteDataConfig;
|
||||
history_retention: HistoryRetentionConfig;
|
||||
rj_data?: RJDataConfig;
|
||||
sheet_filter: string[];
|
||||
extract_fields: Array<Record<string, unknown>>;
|
||||
}
|
||||
@@ -112,12 +112,16 @@ export interface MetrixConfig {
|
||||
database_conn_id: string;
|
||||
target_database: string;
|
||||
recent_days: number;
|
||||
data_dir_to_table: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RJDataConfig {
|
||||
enabled: boolean;
|
||||
weekly_directories: string[];
|
||||
export interface DataDirectoryMapping {
|
||||
path: string;
|
||||
table: string;
|
||||
ready_rule: 'daily' | 'auto';
|
||||
}
|
||||
|
||||
export interface DataMappingsConfig {
|
||||
directories: DataDirectoryMapping[];
|
||||
table_field_mappings: Record<string, Array<Record<string, unknown>>>;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user