style: 收纳 CellData 远程规则设置
This commit is contained in:
@@ -778,3 +778,9 @@
|
|||||||
- 容量数据和 CellData 卡片左上角均标注数据类型,右上角均有说明按钮;容量数据说明弹窗明确启用 CellData 时会在处理容量数据前先更新 CellData。
|
- 容量数据和 CellData 卡片左上角均标注数据类型,右上角均有说明按钮;容量数据说明弹窗明确启用 CellData 时会在处理容量数据前先更新 CellData。
|
||||||
- CellData 卡片点击后选择文件夹,拖拽也只接受文件夹;不支持单个 ZIP 文件直接拖入,避免缺少频段目录导致无法映射。
|
- CellData 卡片点击后选择文件夹,拖拽也只接受文件夹;不支持单个 ZIP 文件直接拖入,避免缺少频段目录导致无法映射。
|
||||||
- 验证:`frontend` `npm run build` 通过;构建产物已清理。
|
- 验证:`frontend` `npm run build` 通过;构建产物已清理。
|
||||||
|
|
||||||
|
## 2026-06-25:远程数据源页收纳 CellData 规则设置
|
||||||
|
|
||||||
|
- 远程数据源页只保留容量数据源与 CellData 数据源两张连接配置卡片,宽屏下并排展示;CellData 的扫描路径、正则和映射 JSON 收纳到「规则设置」弹窗。
|
||||||
|
- CellData 规则弹窗包含扫描路径列表、路径说明入口、高级正则和 Monaco JSON 编辑器,支持格式化、校验、恢复默认映射和保存规则。
|
||||||
|
- 验证:`frontend` `npm run build` 通过;构建产物已清理。
|
||||||
|
|||||||
@@ -293,62 +293,6 @@
|
|||||||
<n-form-item label="远程目录">
|
<n-form-item label="远程目录">
|
||||||
<n-input v-model:value="cellDataRemoteForm.remote_dir" placeholder="/" />
|
<n-input v-model:value="cellDataRemoteForm.remote_dir" placeholder="/" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="扫描路径">
|
|
||||||
<div class="cell-data-path-editor">
|
|
||||||
<div class="cell-data-path-header">
|
|
||||||
<span class="form-hint">支持路径模板和多目录组合</span>
|
|
||||||
<n-button quaternary circle size="small" title="查看说明" @click="cellDataHelpVisible = true">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon><InformationCircleOutline /></n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div class="cell-data-path-list">
|
|
||||||
<div v-for="(path, index) in cellDataScanPaths" :key="`${path}-${index}`" class="cell-data-path-row">
|
|
||||||
<n-input v-model:value="cellDataScanPaths[index]" size="small" placeholder="/.../{maxyear}年/300表" />
|
|
||||||
<n-button quaternary circle size="small" type="error" @click="removeCellDataScanPath(index)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon><CloseOutline /></n-icon>
|
|
||||||
</template>
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="newCellDataScanPath" placeholder="扫描路径模板" @keydown.enter.prevent="addCellDataScanPath" />
|
|
||||||
<n-button @click="addCellDataScanPath">添加</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</div>
|
|
||||||
</n-form-item>
|
|
||||||
<n-grid :cols="12" :x-gap="12">
|
|
||||||
<n-gi :span="4">
|
|
||||||
<n-form-item label="年份目录正则">
|
|
||||||
<n-input v-model:value="cellDataRegexForm.year_dir_regex" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="4">
|
|
||||||
<n-form-item label="文件名正则">
|
|
||||||
<n-input v-model:value="cellDataRegexForm.file_name_regex" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
<n-gi :span="4">
|
|
||||||
<n-form-item label="时间戳正则">
|
|
||||||
<n-input v-model:value="cellDataRegexForm.file_time_regex" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-gi>
|
|
||||||
</n-grid>
|
|
||||||
<n-form-item label="映射规则 JSON">
|
|
||||||
<n-input
|
|
||||||
v-model:value="cellDataMappingText"
|
|
||||||
type="textarea"
|
|
||||||
:autosize="{ minRows: 8, maxRows: 16 }"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</n-form-item>
|
|
||||||
<n-space justify="end" size="small">
|
|
||||||
<n-button :loading="validatingCellDataSettings" @click="validateCellDataSettings">校验 JSON</n-button>
|
|
||||||
<n-button @click="restoreDefaultCellDataMapping">恢复默认映射</n-button>
|
|
||||||
<n-button type="primary" :loading="savingCellDataSettings" @click="saveCellDataSettings">保存规则</n-button>
|
|
||||||
</n-space>
|
|
||||||
<n-form-item label="启用 CellData 数据源">
|
<n-form-item label="启用 CellData 数据源">
|
||||||
<n-switch v-model:value="cellDataRemoteForm.enabled" />
|
<n-switch v-model:value="cellDataRemoteForm.enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -359,6 +303,7 @@
|
|||||||
<n-space justify="end">
|
<n-space justify="end">
|
||||||
<n-button type="primary" :loading="savingCellDataRemote" @click="saveCellDataRemote">保存配置</n-button>
|
<n-button type="primary" :loading="savingCellDataRemote" @click="saveCellDataRemote">保存配置</n-button>
|
||||||
<n-button :loading="testingCellDataRemote" @click="testCellDataRemote">测试连接</n-button>
|
<n-button :loading="testingCellDataRemote" @click="testCellDataRemote">测试连接</n-button>
|
||||||
|
<n-button @click="openCellDataSettingsModal">规则设置</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -701,13 +646,95 @@
|
|||||||
</section>
|
</section>
|
||||||
</n-scrollbar>
|
</n-scrollbar>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="cellDataSettingsVisible"
|
||||||
|
preset="card"
|
||||||
|
class="cell-data-settings-modal"
|
||||||
|
title="CellData 规则设置"
|
||||||
|
style="width: min(860px, calc(100vw - 32px))"
|
||||||
|
@after-enter="layoutCellDataEditor"
|
||||||
|
@after-leave="disposeCellDataEditor"
|
||||||
|
>
|
||||||
|
<div class="cell-data-settings-body">
|
||||||
|
<section class="cell-data-settings-section">
|
||||||
|
<div class="cell-data-section-title">
|
||||||
|
<strong>扫描路径</strong>
|
||||||
|
<n-button quaternary circle size="small" title="查看说明" @click="cellDataHelpVisible = true">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><InformationCircleOutline /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="cell-data-path-list">
|
||||||
|
<div v-for="(path, index) in cellDataScanPaths" :key="`${path}-${index}`" class="cell-data-path-row">
|
||||||
|
<n-input v-model:value="cellDataScanPaths[index]" size="small" placeholder="/.../{maxyear}年/300表" />
|
||||||
|
<n-button quaternary circle size="small" type="error" @click="removeCellDataScanPath(index)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon><CloseOutline /></n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input v-model:value="newCellDataScanPath" placeholder="扫描路径模板" @keydown.enter.prevent="addCellDataScanPath" />
|
||||||
|
<n-button @click="addCellDataScanPath">添加</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cell-data-settings-section">
|
||||||
|
<div class="cell-data-section-title">
|
||||||
|
<strong>高级匹配</strong>
|
||||||
|
</div>
|
||||||
|
<n-grid :cols="12" :x-gap="12">
|
||||||
|
<n-gi :span="4">
|
||||||
|
<n-form-item label="年份目录正则">
|
||||||
|
<n-input v-model:value="cellDataRegexForm.year_dir_regex" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="4">
|
||||||
|
<n-form-item label="文件名正则">
|
||||||
|
<n-input v-model:value="cellDataRegexForm.file_name_regex" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
<n-gi :span="4">
|
||||||
|
<n-form-item label="时间戳正则">
|
||||||
|
<n-input v-model:value="cellDataRegexForm.file_time_regex" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="cell-data-settings-section">
|
||||||
|
<div class="cell-data-section-title">
|
||||||
|
<strong>映射规则 JSON</strong>
|
||||||
|
<n-space size="small">
|
||||||
|
<n-button size="small" @click="formatCellDataMapping">格式化</n-button>
|
||||||
|
<n-button size="small" :loading="validatingCellDataSettings" @click="validateCellDataSettings">校验</n-button>
|
||||||
|
<n-button size="small" @click="restoreDefaultCellDataMapping">恢复默认</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div ref="cellDataEditorHost" class="cell-data-json-editor"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="cellDataSettingsVisible = false">取消</n-button>
|
||||||
|
<n-button type="primary" :loading="savingCellDataSettings" @click="saveCellDataSettings">保存规则</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue';
|
||||||
import { useDialog, useMessage, type SelectOption } from 'naive-ui';
|
import { useDialog, useMessage, type SelectOption } from 'naive-ui';
|
||||||
import { CloseOutline, CloudDownloadOutline, CloudUploadOutline, InformationCircleOutline } from '@vicons/ionicons5';
|
import { CloseOutline, CloudDownloadOutline, CloudUploadOutline, InformationCircleOutline } from '@vicons/ionicons5';
|
||||||
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
|
||||||
|
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||||||
|
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
|
||||||
|
|
||||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||||
import type {
|
import type {
|
||||||
@@ -732,6 +759,16 @@ interface ExtractFieldConfig {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonacoGlobal = typeof globalThis & {
|
||||||
|
MonacoEnvironment?: {
|
||||||
|
getWorker: (_moduleId: string, _label: string) => Worker;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(globalThis as MonacoGlobal).MonacoEnvironment = {
|
||||||
|
getWorker: () => new editorWorker()
|
||||||
|
};
|
||||||
|
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const configInput = ref<HTMLInputElement | null>(null);
|
const configInput = ref<HTMLInputElement | null>(null);
|
||||||
@@ -766,8 +803,11 @@ const fieldSearch = ref('');
|
|||||||
const newSchedulerDirectory = ref('');
|
const newSchedulerDirectory = ref('');
|
||||||
const newCellDataScanPath = ref('');
|
const newCellDataScanPath = ref('');
|
||||||
const cellDataHelpVisible = ref(false);
|
const cellDataHelpVisible = ref(false);
|
||||||
|
const cellDataSettingsVisible = ref(false);
|
||||||
const cellDataScanPaths = ref<string[]>([]);
|
const cellDataScanPaths = ref<string[]>([]);
|
||||||
const cellDataMappingText = ref('');
|
const cellDataMappingText = ref('');
|
||||||
|
const cellDataEditorHost = ref<HTMLDivElement | null>(null);
|
||||||
|
const cellDataEditor = shallowRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
||||||
const schedulerStatus = ref<RemoteSchedulerStatus | null>(null);
|
const schedulerStatus = ref<RemoteSchedulerStatus | null>(null);
|
||||||
const newExtractValues = reactive<Record<number, string>>({});
|
const newExtractValues = reactive<Record<number, string>>({});
|
||||||
|
|
||||||
@@ -963,6 +1003,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
resetPageHeader();
|
resetPageHeader();
|
||||||
|
disposeCellDataEditor();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadConfig() {
|
async function loadConfig() {
|
||||||
@@ -1206,7 +1247,7 @@ function removeCellDataScanPath(index: number) {
|
|||||||
function getCellDataSettingsPayload() {
|
function getCellDataSettingsPayload() {
|
||||||
let mapping: Record<string, unknown>;
|
let mapping: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
mapping = JSON.parse(cellDataMappingText.value || '{}') as Record<string, unknown>;
|
mapping = JSON.parse(getCellDataMappingText() || '{}') as Record<string, unknown>;
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('映射规则不是有效 JSON');
|
throw new Error('映射规则不是有效 JSON');
|
||||||
}
|
}
|
||||||
@@ -1234,7 +1275,7 @@ async function validateCellDataSettings() {
|
|||||||
async function restoreDefaultCellDataMapping() {
|
async function restoreDefaultCellDataMapping() {
|
||||||
try {
|
try {
|
||||||
const mapping = await apiGet<Record<string, unknown>>('/api/config/cell-data/mapping/default');
|
const mapping = await apiGet<Record<string, unknown>>('/api/config/cell-data/mapping/default');
|
||||||
cellDataMappingText.value = JSON.stringify(mapping, null, 2);
|
setCellDataMappingText(JSON.stringify(mapping, null, 2));
|
||||||
message.success('已恢复默认映射');
|
message.success('已恢复默认映射');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '恢复默认映射失败');
|
message.error(error instanceof Error ? error.message : '恢复默认映射失败');
|
||||||
@@ -1247,7 +1288,7 @@ async function saveCellDataSettings() {
|
|||||||
const payload = getCellDataSettingsPayload();
|
const payload = getCellDataSettingsPayload();
|
||||||
const result = await apiPost<ApiMessage>('/api/config/cell-data/settings', payload);
|
const result = await apiPost<ApiMessage>('/api/config/cell-data/settings', payload);
|
||||||
cellDataScanPaths.value = normalizeCellDataScanPaths(payload.scan_paths);
|
cellDataScanPaths.value = normalizeCellDataScanPaths(payload.scan_paths);
|
||||||
cellDataMappingText.value = JSON.stringify(payload.mapping, null, 2);
|
setCellDataMappingText(JSON.stringify(payload.mapping, null, 2));
|
||||||
configUpdate.value = result.update || configUpdate.value;
|
configUpdate.value = result.update || configUpdate.value;
|
||||||
message.success(result.message || 'CellData 规则已保存');
|
message.success(result.message || 'CellData 规则已保存');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1257,6 +1298,71 @@ async function saveCellDataSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCellDataSettingsModal() {
|
||||||
|
cellDataSettingsVisible.value = true;
|
||||||
|
void nextTick(createCellDataEditor);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCellDataEditor() {
|
||||||
|
if (!cellDataEditorHost.value || cellDataEditor.value) {
|
||||||
|
layoutCellDataEditor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cellDataEditor.value = monaco.editor.create(cellDataEditorHost.value, {
|
||||||
|
value: cellDataMappingText.value || '{}',
|
||||||
|
language: 'json',
|
||||||
|
theme: currentEditorTheme(),
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Consolas, 'Courier New', monospace",
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
tabSize: 2,
|
||||||
|
insertSpaces: true,
|
||||||
|
formatOnPaste: true,
|
||||||
|
formatOnType: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function disposeCellDataEditor() {
|
||||||
|
if (cellDataEditor.value) {
|
||||||
|
cellDataMappingText.value = cellDataEditor.value.getValue();
|
||||||
|
cellDataEditor.value.dispose();
|
||||||
|
cellDataEditor.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function layoutCellDataEditor() {
|
||||||
|
void nextTick(() => cellDataEditor.value?.layout());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellDataMappingText(): string {
|
||||||
|
return cellDataEditor.value?.getValue() ?? cellDataMappingText.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCellDataMappingText(value: string) {
|
||||||
|
cellDataMappingText.value = value;
|
||||||
|
if (cellDataEditor.value) {
|
||||||
|
cellDataEditor.value.setValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function formatCellDataMapping() {
|
||||||
|
try {
|
||||||
|
const formatted = JSON.stringify(JSON.parse(getCellDataMappingText() || '{}'), null, 2);
|
||||||
|
setCellDataMappingText(formatted);
|
||||||
|
await nextTick();
|
||||||
|
await cellDataEditor.value?.getAction('editor.action.formatDocument')?.run();
|
||||||
|
} catch {
|
||||||
|
message.error('映射规则不是有效 JSON');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentEditorTheme(): string {
|
||||||
|
return document.documentElement.dataset.theme === 'dark' ? 'vs-dark' : 'vs';
|
||||||
|
}
|
||||||
|
|
||||||
function addDirectoryMapping() {
|
function addDirectoryMapping() {
|
||||||
const path = normalizeDirectoryText(newDirectoryMapping.path);
|
const path = normalizeDirectoryText(newDirectoryMapping.path);
|
||||||
const table = newDirectoryMapping.table.trim();
|
const table = newDirectoryMapping.table.trim();
|
||||||
|
|||||||
+33
-1
@@ -1547,7 +1547,7 @@ select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-remote-panel {
|
.settings-remote-panel {
|
||||||
grid-template-columns: minmax(720px, 1fr);
|
grid-template-columns: repeat(2, minmax(420px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-scheduler-panel {
|
.settings-scheduler-panel {
|
||||||
@@ -1626,6 +1626,34 @@ select {
|
|||||||
border-radius: var(--td-radius-small);
|
border-radius: var(--td-radius-small);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cell-data-settings-body {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
max-height: 68vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-data-settings-section {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-data-section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-data-json-editor {
|
||||||
|
height: 360px;
|
||||||
|
min-height: 260px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--td-border-color-light);
|
||||||
|
border-radius: var(--td-radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
.settings-rules-grid {
|
.settings-rules-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr);
|
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr);
|
||||||
@@ -2158,6 +2186,10 @@ select {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-remote-panel {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-tabs .n-tab-pane {
|
.settings-tabs .n-tab-pane {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user