refactor: 重构后端结构并迁移前端到 Vue3
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="4">
|
||||
<n-space vertical size="large">
|
||||
<n-card title="数据库连接" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary :loading="testing" @click="testConnection">测试</n-button>
|
||||
</template>
|
||||
<n-space vertical>
|
||||
<n-alert v-if="databaseInfo" :type="databaseInfo.success ? 'success' : 'error'" :bordered="false">
|
||||
MySQL {{ databaseInfo.version || '-' }} / LOAD DATA:
|
||||
{{ databaseInfo.load_data_infile ? '可用' : '不可用' }}
|
||||
</n-alert>
|
||||
<n-button block :loading="loadingTables" @click="loadTables">刷新数据表</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="数据表" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary type="error" :disabled="tables.length === 0" @click="confirmDropAll">
|
||||
删除全部
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-space vertical>
|
||||
<n-select
|
||||
v-model:value="selectedTable"
|
||||
filterable
|
||||
clearable
|
||||
placeholder="选择数据表"
|
||||
:options="tableOptions"
|
||||
@update:value="handleSelectTable"
|
||||
/>
|
||||
<n-empty v-if="tables.length === 0 && !loadingTables" description="暂无数据表" />
|
||||
<n-scrollbar v-else class="table-list">
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="table in tables" :key="table">
|
||||
<n-button text @click="loadTable(table)">{{ table }}</n-button>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="12" :m="8">
|
||||
<n-card title="表数据" size="small">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="reloadTable">刷新</n-button>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('csv')">CSV</n-button>
|
||||
<n-button size="small" tertiary :disabled="!selectedTable" @click="downloadTable('xlsx')">XLSX</n-button>
|
||||
<n-button size="small" tertiary type="warning" :disabled="!selectedTable" @click="confirmTruncate">
|
||||
清空
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="error" :disabled="!selectedTable" @click="confirmDrop">
|
||||
删除
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<n-empty v-if="!selectedTable" description="请选择数据表" />
|
||||
<n-space v-else vertical size="large">
|
||||
<n-descriptions v-if="tableInfo" bordered size="small" :column="3">
|
||||
<n-descriptions-item label="表名">{{ tableInfo.name }}</n-descriptions-item>
|
||||
<n-descriptions-item label="行数">{{ tableInfo.row_count }}</n-descriptions-item>
|
||||
<n-descriptions-item label="字段数">{{ tableInfo.columns.length }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<n-data-table
|
||||
size="small"
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loadingData"
|
||||
:bordered="false"
|
||||
:single-line="false"
|
||||
max-height="420"
|
||||
/>
|
||||
<div class="table-footer">
|
||||
<span>共 {{ total }} 行</span>
|
||||
<n-pagination
|
||||
:page="page"
|
||||
:page-count="totalPages"
|
||||
:page-size="pageSize"
|
||||
show-size-picker
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
@update:page="changePage"
|
||||
@update:page-size="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n-collapse v-if="tableInfo">
|
||||
<n-collapse-item title="字段结构" name="columns">
|
||||
<n-table size="small" :bordered="false">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>字段</th>
|
||||
<th>类型</th>
|
||||
<th>允许空</th>
|
||||
<th>键</th>
|
||||
<th>默认值</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="column in tableInfo.columns" :key="String(column.Field)">
|
||||
<td>{{ column.Field }}</td>
|
||||
<td>{{ column.Type }}</td>
|
||||
<td>{{ column.Null }}</td>
|
||||
<td>{{ column.Key }}</td>
|
||||
<td>{{ column.Default ?? '' }}</td>
|
||||
<td>{{ column.Extra }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, 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';
|
||||
|
||||
type RowData = Record<string, unknown>;
|
||||
type DownloadFormat = 'csv' | 'xlsx';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const databaseInfo = ref<DatabaseInfo | null>(null);
|
||||
const tables = ref<string[]>([]);
|
||||
const selectedTable = ref<string | null>(null);
|
||||
const tableInfo = ref<TableInfo | null>(null);
|
||||
const rows = ref<RowData[]>([]);
|
||||
const total = ref(0);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(50);
|
||||
const loadingTables = ref(false);
|
||||
const loadingData = ref(false);
|
||||
const testing = ref(false);
|
||||
|
||||
const tableOptions = computed(() => tables.value.map(table => ({ label: table, value: table })));
|
||||
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / pageSize.value)));
|
||||
const columns = computed<DataTableColumns<RowData>>(() => {
|
||||
const keys = rows.value.length
|
||||
? Object.keys(rows.value[0])
|
||||
: tableInfo.value?.columns.map(column => String(column.Field || '')).filter(Boolean) || [];
|
||||
|
||||
return keys.map(key => ({
|
||||
title: key,
|
||||
key,
|
||||
minWidth: 120,
|
||||
ellipsis: { tooltip: true },
|
||||
render: row => formatCell(row[key])
|
||||
}));
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void testConnection();
|
||||
void loadTables();
|
||||
});
|
||||
|
||||
async function testConnection() {
|
||||
testing.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/database/test');
|
||||
message[result.success ? 'success' : 'error'](result.message || '连接测试完成');
|
||||
databaseInfo.value = await apiGet<DatabaseInfo>('/api/database/info');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '数据库连接测试失败');
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTables() {
|
||||
loadingTables.value = true;
|
||||
try {
|
||||
const result = await apiGet<{ tables: string[] }>('/api/database/tables');
|
||||
tables.value = result.tables;
|
||||
if (selectedTable.value && !tables.value.includes(selectedTable.value)) {
|
||||
selectedTable.value = null;
|
||||
tableInfo.value = null;
|
||||
rows.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载数据表失败');
|
||||
} finally {
|
||||
loadingTables.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectTable(value: string | null) {
|
||||
if (value) {
|
||||
void loadTable(value);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTable(table: string) {
|
||||
selectedTable.value = table;
|
||||
page.value = 1;
|
||||
await Promise.all([loadTableInfo(table), loadTableData()]);
|
||||
}
|
||||
|
||||
async function reloadTable() {
|
||||
if (!selectedTable.value) return;
|
||||
await Promise.all([loadTableInfo(selectedTable.value), loadTableData()]);
|
||||
}
|
||||
|
||||
async function loadTableInfo(table: string) {
|
||||
try {
|
||||
tableInfo.value = await apiPost<TableInfo>('/api/database/table/info', { table_name: table });
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载表结构失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTableData() {
|
||||
if (!selectedTable.value) return;
|
||||
loadingData.value = true;
|
||||
try {
|
||||
const result = await apiPost<TableData>('/api/database/table/data', {
|
||||
table_name: selectedTable.value,
|
||||
page: page.value,
|
||||
page_size: pageSize.value
|
||||
});
|
||||
rows.value = result.data;
|
||||
total.value = result.total;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载表数据失败');
|
||||
} finally {
|
||||
loadingData.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function changePage(value: number) {
|
||||
page.value = value;
|
||||
void loadTableData();
|
||||
}
|
||||
|
||||
function changePageSize(value: number) {
|
||||
pageSize.value = value;
|
||||
page.value = 1;
|
||||
void loadTableData();
|
||||
}
|
||||
|
||||
async function downloadTable(format: DownloadFormat) {
|
||||
if (!selectedTable.value) return;
|
||||
try {
|
||||
await download('/api/download', { table_name: selectedTable.value, format }, `${selectedTable.value}.${format}`);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmTruncate() {
|
||||
if (!selectedTable.value) return;
|
||||
dialog.warning({
|
||||
title: '清空数据表',
|
||||
content: `确认清空 ${selectedTable.value} 的全部数据?`,
|
||||
positiveText: '清空',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: truncateTable
|
||||
});
|
||||
}
|
||||
|
||||
async function truncateTable() {
|
||||
if (!selectedTable.value) return;
|
||||
try {
|
||||
await apiPost('/api/database/table/truncate', { table_name: selectedTable.value });
|
||||
message.success('数据表已清空');
|
||||
await reloadTable();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '清空失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDrop() {
|
||||
if (!selectedTable.value) return;
|
||||
dialog.error({
|
||||
title: '删除数据表',
|
||||
content: `确认删除数据表 ${selectedTable.value}?`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: dropTable
|
||||
});
|
||||
}
|
||||
|
||||
async function dropTable() {
|
||||
if (!selectedTable.value) return;
|
||||
try {
|
||||
await apiPost('/api/database/table/drop', { table_name: selectedTable.value });
|
||||
message.success('数据表已删除');
|
||||
selectedTable.value = null;
|
||||
tableInfo.value = null;
|
||||
rows.value = [];
|
||||
total.value = 0;
|
||||
await loadTables();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDropAll() {
|
||||
dialog.error({
|
||||
title: '删除全部数据表',
|
||||
content: '确认删除当前数据库中的全部数据表?',
|
||||
positiveText: '删除全部',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: dropAllTables
|
||||
});
|
||||
}
|
||||
|
||||
async function dropAllTables() {
|
||||
try {
|
||||
const result = await apiPost<ApiMessage & { dropped_count?: number }>('/api/database/table/drop-all');
|
||||
message.success(result.message || `已删除 ${result.dropped_count || 0} 张表`);
|
||||
selectedTable.value = null;
|
||||
tableInfo.value = null;
|
||||
rows.value = [];
|
||||
total.value = 0;
|
||||
await loadTables();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '删除全部数据表失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatCell(value: unknown): string {
|
||||
if (value === null || value === undefined) return '';
|
||||
if (typeof value === 'object') return JSON.stringify(value);
|
||||
return String(value);
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="5">
|
||||
<n-card title="文件上传" size="small">
|
||||
<n-space vertical size="large">
|
||||
<n-space>
|
||||
<n-button @click="fileInput?.click()">
|
||||
<template #icon><n-icon><DocumentAttachOutline /></n-icon></template>
|
||||
选择文件
|
||||
</n-button>
|
||||
<n-button @click="folderInput?.click()">
|
||||
<template #icon><n-icon><FolderOpenOutline /></n-icon></template>
|
||||
选择目录
|
||||
</n-button>
|
||||
<n-button tertiary :disabled="files.length === 0 || working" @click="clearFiles">
|
||||
清空
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<input ref="fileInput" class="hidden-input" type="file" multiple @change="pickFiles" />
|
||||
<input ref="folderInput" class="hidden-input" type="file" multiple @change="pickFiles" />
|
||||
|
||||
<n-statistic label="待上传文件" :value="files.length" />
|
||||
<n-progress v-if="uploadProgress > 0" type="line" :percentage="uploadProgress" />
|
||||
|
||||
<n-scrollbar class="file-list">
|
||||
<n-empty v-if="files.length === 0" description="未选择文件" />
|
||||
<n-list v-else bordered>
|
||||
<n-list-item v-for="item in files" :key="item.path">
|
||||
<n-thing :title="item.path" :description="formatBytes(item.file.size)" />
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-scrollbar>
|
||||
|
||||
<n-button type="primary" block :disabled="files.length === 0" :loading="working" @click="uploadAndStart">
|
||||
上传并处理
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="12" :m="7">
|
||||
<n-card title="处理状态" size="small">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-tag v-if="taskStatus" :type="statusType">{{ taskStatus.status }}</n-tag>
|
||||
<n-button size="small" tertiary @click="checkActiveTask">刷新</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<n-space vertical>
|
||||
<n-alert v-if="activeTask?.has_active" type="info" :bordered="false">
|
||||
当前任务:{{ activeTask.task_id }} / {{ activeTask.stage }}
|
||||
</n-alert>
|
||||
<n-alert v-if="taskStatus?.error" type="error" :bordered="false">
|
||||
{{ taskStatus.error }}
|
||||
</n-alert>
|
||||
<n-log :log="logText" language="text" trim class="log-panel" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { DocumentAttachOutline, FolderOpenOutline } from '@vicons/ionicons5';
|
||||
|
||||
import { apiGet, apiPost, upload } from '../api/client';
|
||||
import type { ActiveTask, TaskStatus } from '../types';
|
||||
|
||||
interface PickedFile {
|
||||
file: File;
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
success: boolean;
|
||||
task_id: string;
|
||||
file_count: number;
|
||||
}
|
||||
|
||||
const message = useMessage();
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const folderInput = ref<HTMLInputElement | null>(null);
|
||||
const files = ref<PickedFile[]>([]);
|
||||
const uploadProgress = ref(0);
|
||||
const working = ref(false);
|
||||
const taskStatus = ref<TaskStatus | null>(null);
|
||||
const activeTask = ref<ActiveTask | null>(null);
|
||||
let timer: number | undefined;
|
||||
|
||||
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';
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const folder = folderInput.value as (HTMLInputElement & { webkitdirectory?: boolean }) | null;
|
||||
if (folder) {
|
||||
folder.webkitdirectory = true;
|
||||
}
|
||||
void checkActiveTask();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
function pickFiles(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const selected = Array.from(input.files || []);
|
||||
for (const file of selected) {
|
||||
const path = (file as File & { webkitRelativePath?: string }).webkitRelativePath || file.name;
|
||||
if (!files.value.some(item => item.path === path)) {
|
||||
files.value.push({ file, path });
|
||||
}
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function clearFiles() {
|
||||
files.value = [];
|
||||
uploadProgress.value = 0;
|
||||
}
|
||||
|
||||
async function uploadAndStart() {
|
||||
const formData = new FormData();
|
||||
for (const item of files.value) {
|
||||
formData.append('files', item.file, item.path);
|
||||
}
|
||||
|
||||
working.value = true;
|
||||
uploadProgress.value = 0;
|
||||
try {
|
||||
const result = await upload<UploadResponse>('/api/upload', formData, value => {
|
||||
uploadProgress.value = value;
|
||||
});
|
||||
message.success(`上传完成:${result.file_count} 个文件`);
|
||||
await apiPost('/api/process/start', { task_id: result.task_id });
|
||||
taskStatus.value = { task_id: result.task_id, status: 'processing', logs: [] };
|
||||
startPolling(result.task_id);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '上传或启动任务失败');
|
||||
} finally {
|
||||
working.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkActiveTask() {
|
||||
try {
|
||||
activeTask.value = await apiGet<ActiveTask>('/api/task/status');
|
||||
if (activeTask.value.has_active && activeTask.value.task_id) {
|
||||
startPolling(activeTask.value.task_id);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '获取任务状态失败');
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling(taskId: string) {
|
||||
stopPolling();
|
||||
void poll(taskId);
|
||||
timer = window.setInterval(() => void poll(taskId), 2000);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function poll(taskId: string) {
|
||||
try {
|
||||
taskStatus.value = await apiPost<TaskStatus>('/api/process/status', { task_id: taskId });
|
||||
if (['completed', 'failed'].includes(taskStatus.value.status)) {
|
||||
stopPolling();
|
||||
await checkActiveTask();
|
||||
}
|
||||
} catch (error) {
|
||||
stopPolling();
|
||||
message.error(error instanceof Error ? error.message : '刷新任务状态失败');
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(size: number): string {
|
||||
if (size === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let value = size;
|
||||
for (const unit of units) {
|
||||
if (value < 1024) return `${value.toFixed(1)} ${unit}`;
|
||||
value /= 1024;
|
||||
}
|
||||
return `${value.toFixed(1)} TB`;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,200 @@
|
||||
<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>
|
||||
|
||||
<n-space vertical>
|
||||
<n-alert v-if="cacheSize" type="default" :bordered="false">
|
||||
缓存占用:{{ cacheSize.size_formatted }},目录 {{ cacheSize.dir_count }} 个
|
||||
</n-alert>
|
||||
|
||||
<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>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { 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';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const records = ref<HistoryRecord[]>([]);
|
||||
const detail = ref<HistoryDetail | null>(null);
|
||||
const cacheSize = ref<CacheSize | null>(null);
|
||||
const recordSize = ref('-');
|
||||
const loading = ref(false);
|
||||
|
||||
onMounted(() => {
|
||||
void loadHistory();
|
||||
});
|
||||
|
||||
async function loadHistory() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [historyResult, sizeResult] = await Promise.all([
|
||||
apiPost<{ records: HistoryRecord[] }>('/api/history', { limit: 50 }),
|
||||
apiGet<CacheSize>('/api/cache/size')
|
||||
]);
|
||||
records.value = historyResult.records;
|
||||
cacheSize.value = sizeResult;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载历史记录失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openDetail(recordId: string) {
|
||||
try {
|
||||
detail.value = await apiPost<HistoryDetail>('/api/history/detail', { record_id: recordId });
|
||||
recordSize.value = '-';
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载任务详情失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRecordSize(recordId: string) {
|
||||
try {
|
||||
const result = await apiPost<{ success: boolean; size_formatted: string }>('/api/history/size', {
|
||||
record_id: recordId
|
||||
});
|
||||
recordSize.value = result.size_formatted;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '计算目录占用失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmDelete(recordId: string) {
|
||||
dialog.warning({
|
||||
title: '删除历史记录',
|
||||
content: `确认删除任务 ${recordId} 及其缓存文件?`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => deleteRecord(recordId)
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteRecord(recordId: string) {
|
||||
try {
|
||||
await apiPost('/api/history/delete', { record_id: recordId });
|
||||
if (detail.value?.id === recordId) {
|
||||
detail.value = null;
|
||||
}
|
||||
message.success('删除成功');
|
||||
await loadHistory();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function confirmClear() {
|
||||
dialog.warning({
|
||||
title: '清空历史记录',
|
||||
content: '确认清空全部历史记录和缓存文件?',
|
||||
positiveText: '清空',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: clearHistory
|
||||
});
|
||||
}
|
||||
|
||||
async function clearHistory() {
|
||||
try {
|
||||
await apiPost('/api/history/clear');
|
||||
detail.value = null;
|
||||
message.success('已清空历史记录');
|
||||
await loadHistory();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '清空失败');
|
||||
}
|
||||
}
|
||||
|
||||
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: '待处理',
|
||||
processing: '处理中',
|
||||
completed: '已完成',
|
||||
failed: '失败'
|
||||
};
|
||||
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);
|
||||
}
|
||||
|
||||
function formatElapsed(value: number): string {
|
||||
if (!value) return '-';
|
||||
return `${value.toFixed(1)} 秒`;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<n-card class="login-card" :bordered="false">
|
||||
<div class="login-brand">
|
||||
<div class="brand-mark large">CR</div>
|
||||
<div>
|
||||
<h1>CapacityReport</h1>
|
||||
<p>容量报表处理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-form ref="formRef" :model="form" :rules="rules" @submit.prevent="submit">
|
||||
<n-form-item label="账号" path="username">
|
||||
<n-input v-model:value="form.username" autofocus />
|
||||
</n-form-item>
|
||||
<n-form-item label="密码" path="password">
|
||||
<n-input v-model:value="form.password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-button type="primary" block :loading="loading" @click="submit">登录</n-button>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { FormInst, FormRules } from 'naive-ui';
|
||||
|
||||
defineProps<{ loading: boolean }>();
|
||||
const emit = defineEmits<{ login: [{ username: string; password: string }] }>();
|
||||
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
const form = reactive({ username: 'root', password: '' });
|
||||
const rules: FormRules = {
|
||||
username: { required: true, message: '请输入账号', trigger: 'blur' },
|
||||
password: { required: true, message: '请输入密码', trigger: 'blur' }
|
||||
};
|
||||
|
||||
async function submit() {
|
||||
await formRef.value?.validate();
|
||||
emit('login', { ...form });
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="8">
|
||||
<n-card title="SQL 脚本" size="small">
|
||||
<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>
|
||||
|
||||
<n-space vertical>
|
||||
<n-alert v-if="scriptPath" type="default" :bordered="false">
|
||||
{{ scriptPath }} / 修改时间:{{ modified || '-' }}
|
||||
</n-alert>
|
||||
<n-input
|
||||
v-model:value="content"
|
||||
type="textarea"
|
||||
class="script-editor"
|
||||
:autosize="{ minRows: 24, maxRows: 32 }"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="12" :m="4">
|
||||
<n-card title="执行状态" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary :disabled="!taskId" @click="refreshStatus">刷新</n-button>
|
||||
</template>
|
||||
|
||||
<n-space vertical>
|
||||
<n-tag v-if="taskStatus" :type="statusType">{{ taskStatus.status }}</n-tag>
|
||||
<n-empty v-if="!taskStatus" description="脚本尚未执行" />
|
||||
<n-log v-else :log="logText" language="text" trim class="log-panel" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
import { apiGet, apiPost } from '../api/client';
|
||||
import type { ApiMessage, ScriptContent, TaskStatus } from '../types';
|
||||
|
||||
const message = useMessage();
|
||||
const content = ref('');
|
||||
const scriptPath = ref('');
|
||||
const modified = ref<string | null>(null);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const executing = ref(false);
|
||||
const taskId = ref('');
|
||||
const taskStatus = ref<TaskStatus | null>(null);
|
||||
let timer: number | undefined;
|
||||
|
||||
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';
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadScript();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
});
|
||||
|
||||
async function loadScript() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await apiGet<ScriptContent>('/api/script/content');
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '加载脚本失败');
|
||||
}
|
||||
content.value = result.content;
|
||||
scriptPath.value = result.path;
|
||||
modified.value = result.modified;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载脚本失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveScript() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage & { modified?: string }>('/api/script/save', {
|
||||
content: content.value
|
||||
});
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '保存失败');
|
||||
}
|
||||
modified.value = result.modified || modified.value;
|
||||
message.success(result.message || '保存成功');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存脚本失败');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeScript() {
|
||||
executing.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/script/execute');
|
||||
if (!result.task_id) {
|
||||
throw new Error(result.message || '脚本任务启动失败');
|
||||
}
|
||||
taskId.value = result.task_id;
|
||||
taskStatus.value = { task_id: result.task_id, status: 'processing', logs: [] };
|
||||
message.success(result.message || '脚本任务已启动');
|
||||
startPolling();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '执行脚本失败');
|
||||
} finally {
|
||||
executing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling();
|
||||
void refreshStatus();
|
||||
timer = window.setInterval(() => void refreshStatus(), 1500);
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (timer) {
|
||||
window.clearInterval(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
if (!taskId.value) return;
|
||||
try {
|
||||
taskStatus.value = await apiPost<TaskStatus>('/api/process/status', { task_id: taskId.value });
|
||||
if (['completed', 'failed'].includes(taskStatus.value.status)) {
|
||||
stopPolling();
|
||||
}
|
||||
} catch (error) {
|
||||
stopPolling();
|
||||
message.error(error instanceof Error ? error.message : '刷新执行状态失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<n-grid :cols="12" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-gi :span="12" :m="6">
|
||||
<n-space vertical size="large">
|
||||
<n-card title="数据库配置" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary :loading="loading" @click="loadConfig">刷新</n-button>
|
||||
</template>
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="地址">
|
||||
<n-input v-model:value="mysqlForm.host" />
|
||||
</n-form-item>
|
||||
<n-form-item label="端口">
|
||||
<n-input-number v-model:value="mysqlForm.port" class="full-width" :min="1" :max="65535" />
|
||||
</n-form-item>
|
||||
<n-form-item label="账号">
|
||||
<n-input v-model:value="mysqlForm.user" />
|
||||
</n-form-item>
|
||||
<n-form-item label="密码">
|
||||
<n-input v-model:value="mysqlForm.passwd" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-form-item label="库名">
|
||||
<n-input v-model:value="mysqlForm.dbname" />
|
||||
</n-form-item>
|
||||
<n-button type="primary" :loading="savingMysql" @click="saveMysql">保存数据库配置</n-button>
|
||||
</n-form>
|
||||
<div class="muted-line">配置更新时间:{{ configUpdate || '-' }}</div>
|
||||
</n-card>
|
||||
|
||||
<n-card title="登录密码" size="small">
|
||||
<n-form label-placement="left" label-width="90">
|
||||
<n-form-item label="当前密码">
|
||||
<n-input v-model:value="passwordForm.current_password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<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-input v-model:value="passwordForm.confirm_password" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-button type="primary" :loading="changingPassword" @click="changePassword">修改密码</n-button>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card title="配置文件" size="small">
|
||||
<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" />
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
|
||||
<n-gi :span="12" :m="6">
|
||||
<n-space vertical size="large">
|
||||
<n-card title="Sheet 过滤规则" size="small">
|
||||
<n-space vertical>
|
||||
<n-input
|
||||
v-model:value="sheetFilterText"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 6, maxRows: 10 }"
|
||||
placeholder="每行一条规则"
|
||||
/>
|
||||
<n-button type="primary" :loading="savingSheetFilter" @click="saveSheetFilter">保存过滤规则</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="字段提取配置" size="small">
|
||||
<n-space vertical>
|
||||
<n-input
|
||||
v-model:value="extractFieldsText"
|
||||
type="textarea"
|
||||
class="json-editor"
|
||||
:autosize="{ minRows: 12, maxRows: 18 }"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<n-space>
|
||||
<n-button :disabled="!extractFieldsText" @click="formatExtractFields">格式化</n-button>
|
||||
<n-button type="primary" :loading="savingExtractFields" @click="saveExtractFields">
|
||||
保存字段配置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card title="运行状态" size="small">
|
||||
<template #header-extra>
|
||||
<n-button size="small" tertiary @click="loadServiceStatus">刷新</n-button>
|
||||
</template>
|
||||
<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-item label="Python">{{ serviceStatus.python_version }}</n-descriptions-item>
|
||||
<n-descriptions-item label="Supervisor">{{ serviceStatus.supervisor ? '是' : '否' }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-button class="restart-button" type="warning" tertiary @click="confirmRestart">重启服务</n-button>
|
||||
</n-card>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useDialog, useMessage } from 'naive-ui';
|
||||
|
||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||
import type { ApiMessage, AppConfig, ServiceStatus } from '../types';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const configInput = ref<HTMLInputElement | null>(null);
|
||||
const configUpdate = ref('');
|
||||
const loading = ref(false);
|
||||
const savingMysql = ref(false);
|
||||
const savingSheetFilter = ref(false);
|
||||
const savingExtractFields = ref(false);
|
||||
const changingPassword = ref(false);
|
||||
const sheetFilterText = ref('');
|
||||
const extractFieldsText = ref('[]');
|
||||
const serviceStatus = ref<ServiceStatus | null>(null);
|
||||
|
||||
const mysqlForm = reactive({
|
||||
host: '',
|
||||
port: 3306 as number | null,
|
||||
user: '',
|
||||
passwd: '',
|
||||
dbname: ''
|
||||
});
|
||||
|
||||
const passwordForm = reactive({
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadConfig();
|
||||
void loadServiceStatus();
|
||||
});
|
||||
|
||||
async function loadConfig() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const config = await apiGet<AppConfig>('/api/config/full');
|
||||
configUpdate.value = config.update;
|
||||
mysqlForm.host = config.mysql.host;
|
||||
mysqlForm.port = config.mysql.port;
|
||||
mysqlForm.user = config.mysql.user;
|
||||
mysqlForm.passwd = config.mysql.passwd || '';
|
||||
mysqlForm.dbname = config.mysql.dbname;
|
||||
sheetFilterText.value = config.sheet_filter.join('\n');
|
||||
extractFieldsText.value = JSON.stringify(config.extract_fields, null, 2);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '加载配置失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMysql() {
|
||||
if (!mysqlForm.host || !mysqlForm.user || !mysqlForm.dbname) {
|
||||
message.warning('请填写完整数据库配置');
|
||||
return;
|
||||
}
|
||||
|
||||
savingMysql.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/config/mysql', {
|
||||
...mysqlForm,
|
||||
port: mysqlForm.port || 3306
|
||||
});
|
||||
configUpdate.value = result.update || configUpdate.value;
|
||||
message.success(result.message || '数据库配置已保存');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存数据库配置失败');
|
||||
} finally {
|
||||
savingMysql.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSheetFilter() {
|
||||
savingSheetFilter.value = true;
|
||||
try {
|
||||
const filters = sheetFilterText.value
|
||||
.split(/\r?\n/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
const result = await apiPost<ApiMessage>('/api/config/sheet-filter', filters);
|
||||
configUpdate.value = result.update || configUpdate.value;
|
||||
message.success(result.message || '过滤规则已保存');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存过滤规则失败');
|
||||
} finally {
|
||||
savingSheetFilter.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatExtractFields() {
|
||||
try {
|
||||
extractFieldsText.value = JSON.stringify(JSON.parse(extractFieldsText.value), null, 2);
|
||||
} catch {
|
||||
message.error('字段配置不是有效 JSON');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveExtractFields() {
|
||||
let fields: Array<Record<string, unknown>>;
|
||||
try {
|
||||
const parsed = JSON.parse(extractFieldsText.value) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('字段配置必须是数组');
|
||||
}
|
||||
fields = parsed as Array<Record<string, unknown>>;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '字段配置不是有效 JSON');
|
||||
return;
|
||||
}
|
||||
|
||||
savingExtractFields.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/config/extract-fields', fields);
|
||||
configUpdate.value = result.update || configUpdate.value;
|
||||
message.success(result.message || '字段配置已保存');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存字段配置失败');
|
||||
} finally {
|
||||
savingExtractFields.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function changePassword() {
|
||||
if (passwordForm.new_password.length < 4) {
|
||||
message.warning('新密码不能少于 4 位');
|
||||
return;
|
||||
}
|
||||
if (passwordForm.new_password !== passwordForm.confirm_password) {
|
||||
message.warning('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
changingPassword.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/change-password', {
|
||||
current_password: passwordForm.current_password,
|
||||
new_password: passwordForm.new_password
|
||||
});
|
||||
passwordForm.current_password = '';
|
||||
passwordForm.new_password = '';
|
||||
passwordForm.confirm_password = '';
|
||||
message.success(result.message || '密码已修改');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '修改密码失败');
|
||||
} finally {
|
||||
changingPassword.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadConfig() {
|
||||
try {
|
||||
await downloadGet('/api/config/download', 'Configure.json');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '下载配置失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadConfigFile(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = '';
|
||||
if (!file) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const result = await upload<ApiMessage>('/api/config/upload', formData);
|
||||
message.success(result.message || '配置文件已上传');
|
||||
await loadConfig();
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '上传配置失败');
|
||||
}
|
||||
}
|
||||
|
||||
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 : '重启服务失败');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user