fix: 对齐新版前端旧版布局
This commit is contained in:
@@ -2,13 +2,6 @@
|
||||
<div class="database-workspace">
|
||||
<aside class="database-sidebar-pane">
|
||||
<section class="database-table-pane">
|
||||
<div class="database-table-actions">
|
||||
<n-button size="small" tertiary :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
|
||||
<n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll">
|
||||
删除全部
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div class="database-table-list-wrap">
|
||||
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
|
||||
<n-spin v-else-if="loadingTables" class="database-list-loading" size="small" />
|
||||
@@ -126,12 +119,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import type { DataTableColumns } from 'naive-ui';
|
||||
|
||||
import { apiGet, apiPost, download } from '../api/client';
|
||||
import type { ApiMessage, DatabaseInfo, TableData, TableInfo } from '../types';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type RowData = Record<string, unknown>;
|
||||
type DownloadFormat = 'csv' | 'xlsx';
|
||||
@@ -166,10 +160,27 @@ const columns = computed<DataTableColumns<RowData>>(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setPageHeader({
|
||||
actions: [
|
||||
{
|
||||
key: 'drop-all-tables',
|
||||
label: '删除全部表',
|
||||
icon: '🗑️',
|
||||
type: 'error',
|
||||
variant: 'solid',
|
||||
disabled: computed(() => tables.value.length === 0),
|
||||
onClick: confirmDropAll
|
||||
}
|
||||
]
|
||||
});
|
||||
void testConnection();
|
||||
void loadTables();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetPageHeader();
|
||||
});
|
||||
|
||||
async function testConnection() {
|
||||
testing.value = true;
|
||||
try {
|
||||
@@ -342,9 +353,10 @@ function formatCell(value: unknown): string {
|
||||
.database-workspace {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 92px);
|
||||
min-height: 560px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
.database-sidebar-pane {
|
||||
@@ -358,9 +370,9 @@ function formatCell(value: unknown): string {
|
||||
.database-table-pane,
|
||||
.database-content-pane,
|
||||
.database-server-info {
|
||||
background: #fff;
|
||||
border: 1px solid #dfe4ec;
|
||||
border-radius: 6px;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-default);
|
||||
}
|
||||
|
||||
.database-table-pane {
|
||||
@@ -370,16 +382,6 @@ function formatCell(value: unknown): string {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.database-table-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 46px;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e6eaf0;
|
||||
}
|
||||
|
||||
.database-table-list-wrap {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -403,36 +405,37 @@ function formatCell(value: unknown): string {
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
color: #1f2933;
|
||||
padding: 10px 16px;
|
||||
color: var(--td-text-color-primary);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e8ecf2;
|
||||
border-bottom: 1px solid var(--td-border-color-light);
|
||||
cursor: pointer;
|
||||
transition: all var(--td-transition);
|
||||
}
|
||||
|
||||
.database-table-item:hover {
|
||||
background: #f4f7fb;
|
||||
background: var(--td-bg-color-container-hover);
|
||||
}
|
||||
|
||||
.database-table-item.active {
|
||||
color: #0052d9;
|
||||
font-weight: 600;
|
||||
background: #ecf2fe;
|
||||
color: var(--td-brand-color);
|
||||
font-weight: 500;
|
||||
background: var(--td-brand-color-light);
|
||||
}
|
||||
|
||||
.table-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
flex: 0 0 auto;
|
||||
background: #9aa4b2;
|
||||
background: var(--td-text-color-placeholder);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.database-table-item.active .table-dot {
|
||||
background: #0052d9;
|
||||
background: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.table-name {
|
||||
@@ -447,7 +450,7 @@ function formatCell(value: unknown): string {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 12px 28px;
|
||||
font-size: 12px;
|
||||
background: #f8fafc;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
|
||||
.server-info-item {
|
||||
@@ -459,11 +462,11 @@ function formatCell(value: unknown): string {
|
||||
}
|
||||
|
||||
.server-info-item:not(:last-of-type) {
|
||||
border-bottom: 1px dashed #dfe4ec;
|
||||
border-bottom: 1px dashed var(--td-border-color-light);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #4f5b6b;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
@@ -484,18 +487,18 @@ function formatCell(value: unknown): string {
|
||||
}
|
||||
|
||||
.status-badge.success {
|
||||
color: #00a870;
|
||||
background: #dff7ed;
|
||||
color: var(--td-success-color);
|
||||
background: var(--td-success-color-light);
|
||||
}
|
||||
|
||||
.status-badge.warning {
|
||||
color: #ed7b2f;
|
||||
background: #fff1e6;
|
||||
color: var(--td-warning-color);
|
||||
background: var(--td-warning-color-light);
|
||||
}
|
||||
|
||||
.status-badge.unknown {
|
||||
color: #8b95a4;
|
||||
background: #edf1f6;
|
||||
color: var(--td-text-color-placeholder);
|
||||
background: var(--td-bg-color-container-hover);
|
||||
}
|
||||
|
||||
.connection-test {
|
||||
@@ -518,9 +521,9 @@ function formatCell(value: unknown): string {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 48px;
|
||||
padding: 8px 14px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e6eaf0;
|
||||
padding: 12px 16px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-bottom: 1px solid var(--td-border-color-light);
|
||||
}
|
||||
|
||||
.toolbar-left {
|
||||
@@ -533,8 +536,8 @@ function formatCell(value: unknown): string {
|
||||
.current-table {
|
||||
overflow: hidden;
|
||||
max-width: 42vw;
|
||||
color: #1f2933;
|
||||
font-weight: 700;
|
||||
color: var(--td-text-color-primary);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -542,9 +545,9 @@ function formatCell(value: unknown): string {
|
||||
.row-count {
|
||||
flex: 0 0 auto;
|
||||
padding: 2px 8px;
|
||||
color: #667386;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 12px;
|
||||
background: #edf1f6;
|
||||
background: var(--td-bg-color-component);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -573,7 +576,7 @@ function formatCell(value: unknown): string {
|
||||
.columns-collapse {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 14px 12px;
|
||||
border-top: 1px solid #e6eaf0;
|
||||
border-top: 1px solid var(--td-border-color-light);
|
||||
}
|
||||
|
||||
.database-pagination {
|
||||
@@ -582,15 +585,15 @@ function formatCell(value: unknown): string {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 48px;
|
||||
padding: 8px 14px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid #e6eaf0;
|
||||
padding: 12px 16px;
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-top: 1px solid var(--td-border-color-light);
|
||||
}
|
||||
|
||||
.pagination-total {
|
||||
flex: 0 0 auto;
|
||||
color: #667386;
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
@@ -598,6 +601,7 @@ function formatCell(value: unknown): string {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.database-sidebar-pane {
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
@click="openFilePicker"
|
||||
@keydown.enter.prevent="openFilePicker"
|
||||
@keydown.space.prevent="openFilePicker"
|
||||
@dragenter.prevent="onDragEnter"
|
||||
@dragover.prevent="onDragOver"
|
||||
@dragenter.prevent.stop="onDragEnter"
|
||||
@dragover.prevent.stop="onDragOver"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop"
|
||||
@drop.prevent.stop="onDrop"
|
||||
>
|
||||
<div class="upload-icon">
|
||||
<n-icon size="24"><FolderOpenOutline /></n-icon>
|
||||
|
||||
@@ -1,92 +1,98 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="5">
|
||||
<n-card title="处理历史" size="small">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button size="small" tertiary :loading="loading" @click="loadHistory">刷新</n-button>
|
||||
<n-button size="small" type="error" tertiary :disabled="records.length === 0" @click="confirmClear">
|
||||
清空
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
<div class="history-page-body">
|
||||
<n-spin v-if="loading" class="page-loading" />
|
||||
|
||||
<n-space vertical>
|
||||
<n-alert v-if="cacheSize" type="default" :bordered="false">
|
||||
缓存占用:{{ cacheSize.size_formatted }},目录 {{ cacheSize.dir_count }} 个
|
||||
</n-alert>
|
||||
<div v-else-if="records.length === 0" class="empty-state history-empty-state">
|
||||
<span class="empty-icon">📭</span>
|
||||
<p>暂无处理记录</p>
|
||||
</div>
|
||||
|
||||
<n-empty v-if="records.length === 0 && !loading" description="暂无历史记录" />
|
||||
<n-scrollbar v-else class="history-list">
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="record in records" :key="record.id">
|
||||
<n-thing :title="record.id" :description="recordSummary(record)">
|
||||
<template #header-extra>
|
||||
<n-tag size="small" :type="statusType(record.status)">{{ statusText(record.status) }}</n-tag>
|
||||
</template>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button size="small" @click="openDetail(record.id)">详情</n-button>
|
||||
<n-button size="small" tertiary type="error" @click="confirmDelete(record.id)">删除</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="12" :m="7">
|
||||
<n-card title="任务详情" size="small">
|
||||
<template #header-extra>
|
||||
<n-button v-if="detail" size="small" tertiary @click="loadRecordSize(detail.id)">计算占用</n-button>
|
||||
</template>
|
||||
|
||||
<n-empty v-if="!detail" description="请选择一条历史记录" />
|
||||
<n-space v-else vertical size="large">
|
||||
<n-descriptions bordered size="small" :column="2">
|
||||
<n-descriptions-item label="任务 ID">{{ detail.id }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ statusText(detail.status) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="文件数">{{ detail.file_count }}</n-descriptions-item>
|
||||
<n-descriptions-item label="耗时">{{ formatElapsed(detail.elapsed_time) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ formatDate(detail.timestamp) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="占用">{{ recordSize }}</n-descriptions-item>
|
||||
<n-descriptions-item label="工作目录" :span="2">{{ detail.work_dir }}</n-descriptions-item>
|
||||
<n-descriptions-item v-if="detail.error" label="错误" :span="2">{{ detail.error }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div>
|
||||
<div class="section-label">处理日志</div>
|
||||
<n-log :log="detail.logs.join('\n') || '暂无日志'" language="text" trim class="log-panel" />
|
||||
<div v-else class="history-list">
|
||||
<div v-for="record in records" :key="record.id" class="history-item">
|
||||
<div class="history-status" :class="record.status"></div>
|
||||
<div class="history-info">
|
||||
<div class="history-id">{{ record.id }}</div>
|
||||
<div class="history-meta">
|
||||
{{ formatDate(record.timestamp) }} · {{ record.file_count }} 个文件 · {{ formatElapsed(record.elapsed_time) }}
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</div>
|
||||
<span class="record-size">{{ statusText(record.status) }}</span>
|
||||
<div class="history-actions">
|
||||
<n-button size="small" tertiary @click="openDetail(record.id)">详情</n-button>
|
||||
<n-button size="small" tertiary type="error" @click="confirmDelete(record.id)">删除</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-modal v-model:show="detailVisible" preset="card" class="history-detail-modal" title="任务详情">
|
||||
<n-empty v-if="!detail" description="暂无详情" />
|
||||
<n-space v-else vertical size="large">
|
||||
<n-descriptions bordered size="small" :column="2">
|
||||
<n-descriptions-item label="任务 ID">{{ detail.id }}</n-descriptions-item>
|
||||
<n-descriptions-item label="状态">{{ statusText(detail.status) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="文件数">{{ detail.file_count }}</n-descriptions-item>
|
||||
<n-descriptions-item label="耗时">{{ formatElapsed(detail.elapsed_time) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">{{ formatDate(detail.timestamp) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="占用">
|
||||
<n-button v-if="recordSize === '-'" size="tiny" text @click="loadRecordSize(detail.id)">计算占用</n-button>
|
||||
<span v-else>{{ recordSize }}</span>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="工作目录" :span="2">{{ detail.work_dir }}</n-descriptions-item>
|
||||
<n-descriptions-item v-if="detail.error" label="错误" :span="2">{{ detail.error }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div>
|
||||
<div class="section-label">处理日志</div>
|
||||
<n-log :log="detail.logs.join('\n') || '暂无日志'" language="text" trim class="log-panel" />
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
import type { TagProps } from 'naive-ui';
|
||||
|
||||
import { apiGet, apiPost } from '../api/client';
|
||||
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const records = ref<HistoryRecord[]>([]);
|
||||
const detail = ref<HistoryDetail | null>(null);
|
||||
const detailVisible = ref(false);
|
||||
const cacheSize = ref<CacheSize | null>(null);
|
||||
const recordSize = ref('-');
|
||||
const loading = ref(false);
|
||||
|
||||
const totalSizeText = computed(() => `总占用: ${cacheSize.value?.size_formatted || '计算中...'}`);
|
||||
const hasRecords = computed(() => records.value.length > 0);
|
||||
|
||||
onMounted(() => {
|
||||
setPageHeader({
|
||||
actions: [
|
||||
{ key: 'refresh-history', label: '刷新', icon: '🔄', loading, onClick: loadHistory },
|
||||
{
|
||||
key: 'clear-history',
|
||||
label: '清空历史',
|
||||
icon: '🗑️',
|
||||
type: 'error',
|
||||
variant: 'solid',
|
||||
disabled: computed(() => !hasRecords.value),
|
||||
onClick: confirmClear
|
||||
},
|
||||
{ key: 'history-size', kind: 'text', label: totalSizeText }
|
||||
]
|
||||
});
|
||||
void loadHistory();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetPageHeader();
|
||||
});
|
||||
|
||||
async function loadHistory() {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -107,6 +113,7 @@ async function openDetail(recordId: string) {
|
||||
try {
|
||||
detail.value = await apiPost<HistoryDetail>('/api/history/detail', { record_id: recordId });
|
||||
recordSize.value = '-';
|
||||
detailVisible.value = true;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载任务详情失败');
|
||||
}
|
||||
@@ -138,6 +145,7 @@ async function deleteRecord(recordId: string) {
|
||||
await apiPost('/api/history/delete', { record_id: recordId });
|
||||
if (detail.value?.id === recordId) {
|
||||
detail.value = null;
|
||||
detailVisible.value = false;
|
||||
}
|
||||
message.success('删除成功');
|
||||
await loadHistory();
|
||||
@@ -160,6 +168,7 @@ async function clearHistory() {
|
||||
try {
|
||||
await apiPost('/api/history/clear');
|
||||
detail.value = null;
|
||||
detailVisible.value = false;
|
||||
message.success('已清空历史记录');
|
||||
await loadHistory();
|
||||
} catch (error) {
|
||||
@@ -167,13 +176,6 @@ async function clearHistory() {
|
||||
}
|
||||
}
|
||||
|
||||
function statusType(status: string): TagProps['type'] {
|
||||
if (status === 'completed') return 'success';
|
||||
if (status === 'failed') return 'error';
|
||||
if (status === 'processing') return 'info';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function statusText(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
@@ -184,10 +186,6 @@ function statusText(status: string): string {
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
function recordSummary(record: HistoryRecord): string {
|
||||
return `${formatDate(record.timestamp)} / ${record.file_count} 个文件 / ${formatElapsed(record.elapsed_time)}`;
|
||||
}
|
||||
|
||||
function formatDate(value: string): string {
|
||||
if (!value) return '-';
|
||||
return value.replace('T', ' ').slice(0, 19);
|
||||
|
||||
@@ -1,46 +1,31 @@
|
||||
<template>
|
||||
<div class="script-workspace">
|
||||
<n-card size="small" class="work-card script-editor-pane">
|
||||
<template #header>
|
||||
<div class="script-card-title">
|
||||
<span>脚本编辑</span>
|
||||
<n-tag v-if="isModified" size="small" type="warning">已修改</n-tag>
|
||||
<n-tag v-else size="small" type="success">已保存</n-tag>
|
||||
<div class="script-page-body">
|
||||
<div class="script-editor-container">
|
||||
<div ref="editorHost" class="script-editor" />
|
||||
</div>
|
||||
|
||||
<div class="script-status-bar">
|
||||
<span>{{ cursorText }}</span>
|
||||
<span>{{ editorStatus }}</span>
|
||||
<span>{{ lineCount }} 行</span>
|
||||
</div>
|
||||
|
||||
<div v-if="taskStatus" class="process-section script-process-section">
|
||||
<div class="upload-process-card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-left">
|
||||
<span class="card-title">脚本执行进度</span>
|
||||
<span class="process-status" :class="statusClass">{{ statusText }}</span>
|
||||
</div>
|
||||
<div class="card-header-right">
|
||||
<n-button size="small" tertiary :disabled="!taskId" @click="refreshStatus">刷新</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<n-log :log="logText" language="text" trim class="script-log" />
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button size="small" tertiary :loading="loading" @click="loadScript">重新加载</n-button>
|
||||
<n-button size="small" type="primary" :loading="saving" @click="saveScript">保存</n-button>
|
||||
<n-button size="small" type="warning" :loading="executing" @click="executeScript">执行</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<div class="script-editor-toolbar">
|
||||
<span class="script-path" :title="scriptPath">{{ scriptPath || 'ReportScript.sql' }}</span>
|
||||
<span class="script-modified">修改时间:{{ modified || '-' }}</span>
|
||||
</div>
|
||||
|
||||
<div ref="editorHost" class="monaco-editor-host" />
|
||||
|
||||
<div class="script-status-bar">
|
||||
<span>{{ cursorText }}</span>
|
||||
<span>{{ lineCount }} 行</span>
|
||||
<span>{{ editorStatus }}</span>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-card title="执行状态" size="small" class="work-card script-log-pane">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-tag v-if="taskStatus" :type="statusType">{{ statusText }}</n-tag>
|
||||
<n-button size="small" tertiary :disabled="!taskId" @click="refreshStatus">刷新</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<n-empty v-if="!taskStatus" description="脚本尚未执行" />
|
||||
<n-log v-else :log="logText" language="text" trim class="script-log" />
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +38,7 @@ import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution';
|
||||
|
||||
import { apiGet, apiPost } from '../api/client';
|
||||
import type { ApiMessage, ScriptContent, TaskStatus } from '../types';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type MonacoGlobal = typeof globalThis & {
|
||||
MonacoEnvironment?: {
|
||||
@@ -81,13 +67,14 @@ const executing = ref(false);
|
||||
const taskId = ref('');
|
||||
const taskStatus = ref<TaskStatus | null>(null);
|
||||
let timer: number | undefined;
|
||||
let themeObserver: MutationObserver | undefined;
|
||||
let applyingRemoteContent = false;
|
||||
|
||||
const logText = computed(() => taskStatus.value?.logs?.join('\n') || '等待执行日志');
|
||||
const statusType = computed(() => {
|
||||
if (taskStatus.value?.status === 'completed') return 'success';
|
||||
if (taskStatus.value?.status === 'failed') return 'error';
|
||||
return 'info';
|
||||
const statusClass = computed(() => {
|
||||
if (taskStatus.value?.status === 'completed') return 'completed';
|
||||
if (taskStatus.value?.status === 'failed') return 'failed';
|
||||
return 'processing';
|
||||
});
|
||||
const statusText = computed(() => {
|
||||
const status = taskStatus.value?.status;
|
||||
@@ -102,15 +89,29 @@ const editorStatus = computed(() => {
|
||||
if (saving.value) return '保存中';
|
||||
return isModified.value ? '未保存' : '就绪';
|
||||
});
|
||||
const modifiedText = computed(() => (modified.value ? `最后修改: ${modified.value}` : '最后修改: -'));
|
||||
|
||||
onMounted(async () => {
|
||||
setPageHeader({
|
||||
subtitle: computed(() => scriptPath.value || 'ReportScript.sql'),
|
||||
actions: [
|
||||
{ key: 'script-modified', kind: 'text', label: modifiedText },
|
||||
{ key: 'save-script', label: computed(() => (isModified.value ? '保存 *' : '保存')), icon: '💾', type: 'primary', variant: 'solid', loading: saving, onClick: saveScript },
|
||||
{ key: 'format-script', label: '格式化', icon: '✨', onClick: formatScript },
|
||||
{ key: 'run-script', label: '运行脚本', icon: '▶️', type: 'success', variant: 'solid', loading: executing, onClick: executeScript }
|
||||
]
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
createEditor();
|
||||
observeTheme();
|
||||
await loadScript();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetPageHeader();
|
||||
stopPolling();
|
||||
themeObserver?.disconnect();
|
||||
for (const disposable of disposables) {
|
||||
disposable.dispose();
|
||||
}
|
||||
@@ -123,17 +124,25 @@ function createEditor() {
|
||||
editor.value = monaco.editor.create(editorHost.value, {
|
||||
value: '-- 正在加载 ReportScript.sql',
|
||||
language: 'sql',
|
||||
theme: 'vs',
|
||||
theme: currentEditorTheme(),
|
||||
fontSize: 14,
|
||||
fontFamily: "'Cascadia Code', Consolas, 'Courier New', monospace",
|
||||
fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, 'Courier New', monospace",
|
||||
lineNumbers: 'on',
|
||||
minimap: { enabled: true },
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
roundedSelection: false,
|
||||
roundedSelection: true,
|
||||
renderLineHighlight: 'all',
|
||||
tabSize: 4,
|
||||
wordWrap: 'off'
|
||||
insertSpaces: true,
|
||||
wordWrap: 'on',
|
||||
folding: true,
|
||||
showFoldingControls: 'always',
|
||||
bracketPairColorization: { enabled: true },
|
||||
guides: {
|
||||
bracketPairs: true,
|
||||
indentation: true
|
||||
}
|
||||
});
|
||||
|
||||
disposables.push(
|
||||
@@ -193,6 +202,17 @@ async function saveScript() {
|
||||
}
|
||||
}
|
||||
|
||||
async function formatScript() {
|
||||
const action = editor.value?.getAction('editor.action.formatDocument');
|
||||
if (!action) {
|
||||
message.warning('当前编辑器没有可用的格式化动作');
|
||||
return;
|
||||
}
|
||||
|
||||
await action.run();
|
||||
message.success('格式化完成');
|
||||
}
|
||||
|
||||
async function executeScript() {
|
||||
executing.value = true;
|
||||
try {
|
||||
@@ -238,6 +258,17 @@ function updateCursor() {
|
||||
cursorText.value = position ? `行 ${position.lineNumber},列 ${position.column}` : '行 1,列 1';
|
||||
}
|
||||
|
||||
function observeTheme() {
|
||||
themeObserver = new MutationObserver(() => {
|
||||
monaco.editor.setTheme(currentEditorTheme());
|
||||
});
|
||||
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
}
|
||||
|
||||
function currentEditorTheme() {
|
||||
return document.documentElement.getAttribute('data-theme') === 'dark' ? 'vs-dark' : 'vs';
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
void refreshStatus();
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<template>
|
||||
<div class="settings-workspace">
|
||||
<input ref="configInput" class="hidden-input" type="file" accept=".json" @change="uploadConfigFile" />
|
||||
|
||||
<div class="settings-column">
|
||||
<n-card title="数据库配置" size="small" class="work-card">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button size="small" tertiary :loading="loading" @click="loadConfig">刷新</n-button>
|
||||
<n-button size="small" tertiary :loading="testingDb" @click="testDatabase">测试连接</n-button>
|
||||
</n-space>
|
||||
<span class="muted-line">更新时间:{{ configUpdate || '-' }}</span>
|
||||
</template>
|
||||
|
||||
<n-form label-placement="top">
|
||||
@@ -37,15 +36,19 @@
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div class="card-actions">
|
||||
<span class="muted-line">更新时间:{{ configUpdate || '-' }}</span>
|
||||
<n-button type="primary" :loading="savingMysql" @click="saveMysql">保存配置</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingMysql" @click="saveMysql">保存配置</n-button>
|
||||
<n-button :loading="testingDb" @click="testDatabase">测试连接</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"
|
||||
@@ -65,14 +68,16 @@
|
||||
/>
|
||||
<n-button @click="addSheetFilter">添加</n-button>
|
||||
</n-input-group>
|
||||
<div class="card-actions">
|
||||
<span class="muted-line">匹配这些关键词的 Sheet 会跳过处理</span>
|
||||
<n-button type="primary" :loading="savingSheetFilter" @click="saveSheetFilter">保存规则</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingSheetFilter" @click="saveSheetFilter">保存规则</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="登录密码" size="small" class="work-card">
|
||||
<n-card title="修改登录密码" size="small" class="work-card">
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="当前密码">
|
||||
<n-input v-model:value="passwordForm.current_password" type="password" show-password-on="click" />
|
||||
@@ -80,61 +85,39 @@
|
||||
<n-form-item label="新密码">
|
||||
<n-input v-model:value="passwordForm.new_password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-form-item label="确认密码">
|
||||
<n-form-item label="确认新密码">
|
||||
<n-input v-model:value="passwordForm.confirm_password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<div class="card-actions">
|
||||
<span />
|
||||
<n-button type="primary" :loading="changingPassword" @click="changePassword">修改密码</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card title="配置文件与服务" size="small" class="work-card">
|
||||
<n-space vertical>
|
||||
<n-space>
|
||||
<n-button @click="downloadConfig">下载配置</n-button>
|
||||
<n-button @click="configInput?.click()">上传配置</n-button>
|
||||
<input ref="configInput" class="hidden-input" type="file" accept=".json" @change="uploadConfigFile" />
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="changingPassword" @click="changePassword">修改密码</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-descriptions v-if="serviceStatus" bordered size="small" :column="2">
|
||||
<n-descriptions-item label="状态">{{ serviceStatus.status }}</n-descriptions-item>
|
||||
<n-descriptions-item label="版本">{{ serviceStatus.version }}</n-descriptions-item>
|
||||
<n-descriptions-item label="系统">{{ serviceStatus.platform }}</n-descriptions-item>
|
||||
<n-descriptions-item label="进程">{{ serviceStatus.pid }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<div class="card-actions">
|
||||
<n-button size="small" tertiary @click="loadServiceStatus">刷新状态</n-button>
|
||||
<n-button type="warning" tertiary @click="confirmRestart">重启服务</n-button>
|
||||
</div>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
|
||||
<n-card size="small" class="work-card field-mapping-card">
|
||||
<template #header>
|
||||
<div class="field-card-title">
|
||||
<span>字段提取配置</span>
|
||||
<n-tag size="small" round>{{ extractFields.length }} 个字段</n-tag>
|
||||
<div class="field-card-header">
|
||||
<div class="field-card-title-row">
|
||||
<div class="field-card-title">
|
||||
<span>字段映射配置</span>
|
||||
<n-tag size="small" round>{{ extractFields.length }} 个字段</n-tag>
|
||||
</div>
|
||||
<n-input
|
||||
v-model:value="fieldSearch"
|
||||
size="small"
|
||||
clearable
|
||||
class="field-search"
|
||||
placeholder="搜索字段..."
|
||||
/>
|
||||
</div>
|
||||
<p class="form-hint">定义 Excel 列名到数据库字段的映射规则,提取来源匹配任意一个即可</p>
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-space align="center">
|
||||
<n-input
|
||||
v-model:value="fieldSearch"
|
||||
size="small"
|
||||
clearable
|
||||
class="field-search"
|
||||
placeholder="搜索字段或提取来源"
|
||||
/>
|
||||
<n-button size="small" @click="addFieldMapping">添加字段</n-button>
|
||||
<n-button size="small" type="primary" :loading="savingExtractFields" @click="saveExtractFields">
|
||||
保存映射
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<n-scrollbar class="field-mapping-scroll">
|
||||
<n-empty v-if="visibleFieldMappings.length === 0" description="没有匹配的字段配置" />
|
||||
@@ -189,16 +172,26 @@
|
||||
</div>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="addFieldMapping">添加字段</n-button>
|
||||
<n-button type="primary" :loading="savingExtractFields" @click="saveExtractFields">
|
||||
保存映射
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage, type SelectOption } from 'naive-ui';
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage, type SelectOption } from 'naive-ui';
|
||||
|
||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||
import type { ApiMessage, AppConfig, ServiceStatus } from '../types';
|
||||
import type { ApiMessage, AppConfig } from '../types';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
interface ExtractFieldConfig {
|
||||
Field: string;
|
||||
@@ -208,7 +201,6 @@ interface ExtractFieldConfig {
|
||||
}
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const configInput = ref<HTMLInputElement | null>(null);
|
||||
const configUpdate = ref('');
|
||||
const loading = ref(false);
|
||||
@@ -217,7 +209,6 @@ const savingMysql = ref(false);
|
||||
const savingSheetFilter = ref(false);
|
||||
const savingExtractFields = ref(false);
|
||||
const changingPassword = ref(false);
|
||||
const serviceStatus = ref<ServiceStatus | null>(null);
|
||||
const sheetFilters = ref<string[]>([]);
|
||||
const newSheetFilter = ref('');
|
||||
const extractFields = ref<ExtractFieldConfig[]>([]);
|
||||
@@ -258,8 +249,17 @@ const visibleFieldMappings = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
setPageHeader({
|
||||
actions: [
|
||||
{ key: 'download-config', label: '下载配置', icon: '📥', onClick: downloadConfig },
|
||||
{ key: 'upload-config', label: '上传配置', icon: '📤', onClick: () => configInput.value?.click() }
|
||||
]
|
||||
});
|
||||
void loadConfig();
|
||||
void loadServiceStatus();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resetPageHeader();
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
@@ -454,33 +454,6 @@ async function uploadConfigFile(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServiceStatus() {
|
||||
try {
|
||||
serviceStatus.value = await apiGet<ServiceStatus>('/api/service/status');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '读取运行状态失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmRestart() {
|
||||
dialog.warning({
|
||||
title: '重启服务',
|
||||
content: '确认重启后端服务?',
|
||||
positiveText: '重启',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: restartService
|
||||
});
|
||||
}
|
||||
|
||||
async function restartService() {
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/service/restart');
|
||||
message.success(result.message || '服务正在重启');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '重启服务失败');
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExtractFields(fields: Array<Record<string, unknown>>): ExtractFieldConfig[] {
|
||||
return fields.map(field => {
|
||||
const extract = Array.isArray(field.Extract) ? uniqueStrings(field.Extract) : [];
|
||||
|
||||
Reference in New Issue
Block a user