feat: 新增远程数据自动化处理
This commit is contained in:
@@ -40,6 +40,17 @@
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="!taskInProgress" class="remote-run-card">
|
||||
<div class="remote-run-copy">
|
||||
<h4>远程自动化</h4>
|
||||
<p>从已配置的 FTP/SFTP 目录递归下载数据,然后自动开始处理。</p>
|
||||
</div>
|
||||
<n-button type="primary" secondary :loading="remoteStarting" :disabled="working" @click="startRemoteProcessing">
|
||||
<template #icon><n-icon><CloudDownloadOutline /></n-icon></template>
|
||||
远程下载并处理
|
||||
</n-button>
|
||||
</section>
|
||||
|
||||
<div v-if="!taskInProgress && files.length > 0" class="file-list">
|
||||
<div class="file-list-header">
|
||||
<h4>已选择文件</h4>
|
||||
@@ -133,6 +144,7 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import {
|
||||
CloudDownloadOutline,
|
||||
CloudUploadOutline,
|
||||
DocumentTextOutline,
|
||||
FolderOpenOutline,
|
||||
@@ -160,7 +172,9 @@ interface DroppedFile {
|
||||
interface UploadResponse {
|
||||
success: boolean;
|
||||
task_id: string;
|
||||
file_count: number;
|
||||
file_count?: number;
|
||||
message?: string;
|
||||
stage?: string;
|
||||
}
|
||||
|
||||
const validExtensions = new Set(['.zip', '.xlsx', '.xls', '.csv']);
|
||||
@@ -170,6 +184,7 @@ const folderInput = ref<HTMLInputElement | null>(null);
|
||||
const files = ref<PickedFile[]>([]);
|
||||
const uploadProgress = ref(0);
|
||||
const working = ref(false);
|
||||
const remoteStarting = ref(false);
|
||||
const isDragging = ref(false);
|
||||
const taskStatus = ref<TaskStatus | null>(null);
|
||||
const activeTask = ref<ActiveTask | null>(null);
|
||||
@@ -194,6 +209,7 @@ const statusClass = computed(() => {
|
||||
const processStatusText = computed(() => {
|
||||
if (taskStatus.value?.status === 'completed') return '处理完成';
|
||||
if (taskStatus.value?.status === 'failed') return '处理失败';
|
||||
if (activeTask.value?.stage === 'downloading') return '远程下载中...';
|
||||
if (activeTask.value?.stage === 'uploading') return '上传中...';
|
||||
return '处理中...';
|
||||
});
|
||||
@@ -396,7 +412,7 @@ async function uploadAndStart() {
|
||||
item.status = 'uploaded';
|
||||
item.progress = 100;
|
||||
});
|
||||
message.success(`上传完成:${result.file_count} 个文件`);
|
||||
message.success(`上传完成:${result.file_count ?? files.value.length} 个文件`);
|
||||
await apiPost('/api/process/start', { task_id: result.task_id });
|
||||
taskStatus.value = { task_id: result.task_id, status: 'processing', logs: ['任务已提交,等待处理日志...'] };
|
||||
startPolling(result.task_id);
|
||||
@@ -412,6 +428,30 @@ async function uploadAndStart() {
|
||||
}
|
||||
}
|
||||
|
||||
async function startRemoteProcessing() {
|
||||
if (working.value || remoteStarting.value) return;
|
||||
|
||||
remoteStarting.value = true;
|
||||
try {
|
||||
const result = await apiPost<UploadResponse>('/api/remote/start');
|
||||
message.success(result.message || '远程自动化任务已启动');
|
||||
files.value = [];
|
||||
uploadProgress.value = 0;
|
||||
activeTask.value = {
|
||||
has_active: true,
|
||||
task_id: result.task_id,
|
||||
stage: result.stage || 'downloading',
|
||||
started_at: new Date().toISOString()
|
||||
};
|
||||
taskStatus.value = { task_id: result.task_id, status: 'processing', logs: ['远程下载任务已提交,等待处理日志...'] };
|
||||
startPolling(result.task_id);
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '启动远程自动化任务失败');
|
||||
} finally {
|
||||
remoteStarting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateUploadingFiles(progress: number) {
|
||||
files.value.forEach(item => {
|
||||
if (item.status === 'uploading') {
|
||||
|
||||
@@ -46,6 +46,70 @@
|
||||
</template>
|
||||
</n-card>
|
||||
|
||||
<n-card title="远程数据源" size="small" class="work-card">
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="启用远程自动化">
|
||||
<n-switch v-model:value="remoteForm.enabled" />
|
||||
</n-form-item>
|
||||
<n-grid :cols="12" :x-gap="12">
|
||||
<n-gi :span="4">
|
||||
<n-form-item label="协议">
|
||||
<n-select
|
||||
v-model:value="remoteForm.protocol"
|
||||
:options="remoteProtocolOptions"
|
||||
@update:value="updateRemoteProtocol"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="8">
|
||||
<n-form-item label="服务器地址">
|
||||
<n-input v-model:value="remoteForm.host" placeholder="192.168.1.10" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="12" :x-gap="12">
|
||||
<n-gi :span="4">
|
||||
<n-form-item label="端口">
|
||||
<n-input-number v-model:value="remoteForm.port" class="full-width" :min="1" :max="65535" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="4">
|
||||
<n-form-item label="超时秒数">
|
||||
<n-input-number v-model:value="remoteForm.timeout" class="full-width" :min="1" :max="600" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="4">
|
||||
<n-form-item label="FTP 被动模式">
|
||||
<n-switch v-model:value="remoteForm.passive" :disabled="remoteForm.protocol !== 'ftp'" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-grid :cols="12" :x-gap="12">
|
||||
<n-gi :span="6">
|
||||
<n-form-item label="用户名">
|
||||
<n-input v-model:value="remoteForm.user" placeholder="remote user" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi :span="6">
|
||||
<n-form-item label="密码">
|
||||
<n-input v-model:value="remoteForm.passwd" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-form-item label="远程目录">
|
||||
<n-input v-model:value="remoteForm.remote_dir" placeholder="/CapacityReportData" />
|
||||
</n-form-item>
|
||||
<p class="form-hint">自动化执行会递归下载该目录下的全部文件和文件夹到本地缓存,再按现有处理流程入库和执行脚本。</p>
|
||||
</n-form>
|
||||
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button type="primary" :loading="savingRemote" @click="saveRemote">保存配置</n-button>
|
||||
<n-button :loading="testingRemote" @click="testRemote">测试连接</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>
|
||||
@@ -193,7 +257,7 @@ import { useMessage, type SelectOption } from 'naive-ui';
|
||||
import { CloseOutline, CloudDownloadOutline, CloudUploadOutline } from '@vicons/ionicons5';
|
||||
|
||||
import { apiGet, apiPost, downloadGet, upload } from '../api/client';
|
||||
import type { ApiMessage, AppConfig } from '../types';
|
||||
import type { ApiMessage, AppConfig, RemoteDataConfig } from '../types';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
interface ExtractFieldConfig {
|
||||
@@ -208,7 +272,9 @@ const configInput = ref<HTMLInputElement | null>(null);
|
||||
const configUpdate = ref('');
|
||||
const loading = ref(false);
|
||||
const testingDb = ref(false);
|
||||
const testingRemote = ref(false);
|
||||
const savingMysql = ref(false);
|
||||
const savingRemote = ref(false);
|
||||
const savingSheetFilter = ref(false);
|
||||
const savingExtractFields = ref(false);
|
||||
const changingPassword = ref(false);
|
||||
@@ -226,6 +292,18 @@ const mysqlForm = reactive({
|
||||
dbname: ''
|
||||
});
|
||||
|
||||
const remoteForm = reactive<RemoteDataConfig>({
|
||||
enabled: false,
|
||||
protocol: 'sftp',
|
||||
host: '',
|
||||
port: 22,
|
||||
user: '',
|
||||
passwd: '',
|
||||
remote_dir: '/',
|
||||
passive: true,
|
||||
timeout: 30
|
||||
});
|
||||
|
||||
const passwordForm = reactive({
|
||||
current_password: '',
|
||||
new_password: '',
|
||||
@@ -238,6 +316,10 @@ const fieldTypeOptions: SelectOption[] = [
|
||||
{ label: '小数', value: 'float' },
|
||||
{ label: '日期时间', value: 'datetime' }
|
||||
];
|
||||
const remoteProtocolOptions: SelectOption[] = [
|
||||
{ label: 'SFTP', value: 'sftp' },
|
||||
{ label: 'FTP', value: 'ftp' }
|
||||
];
|
||||
|
||||
const visibleFieldMappings = computed(() => {
|
||||
const keyword = fieldSearch.value.trim().toLowerCase();
|
||||
@@ -275,6 +357,7 @@ async function loadConfig() {
|
||||
mysqlForm.user = config.mysql.user;
|
||||
mysqlForm.passwd = config.mysql.passwd || '';
|
||||
mysqlForm.dbname = config.mysql.dbname;
|
||||
Object.assign(remoteForm, normalizeRemoteConfig(config.remote_data));
|
||||
sheetFilters.value = [...config.sheet_filter];
|
||||
extractFields.value = normalizeExtractFields(config.extract_fields);
|
||||
resetExtractInputs();
|
||||
@@ -285,6 +368,15 @@ async function loadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
function updateRemoteProtocol(value: string) {
|
||||
if (value === 'sftp' && (!remoteForm.port || remoteForm.port === 21)) {
|
||||
remoteForm.port = 22;
|
||||
}
|
||||
if (value === 'ftp' && (!remoteForm.port || remoteForm.port === 22)) {
|
||||
remoteForm.port = 21;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMysql() {
|
||||
if (!mysqlForm.host || !mysqlForm.user || !mysqlForm.dbname) {
|
||||
message.warning('请填写完整数据库配置');
|
||||
@@ -306,6 +398,57 @@ async function saveMysql() {
|
||||
}
|
||||
}
|
||||
|
||||
function getRemotePayload(): RemoteDataConfig {
|
||||
return {
|
||||
...remoteForm,
|
||||
protocol: remoteForm.protocol,
|
||||
host: remoteForm.host.trim(),
|
||||
port: remoteForm.port || (remoteForm.protocol === 'sftp' ? 22 : 21),
|
||||
user: remoteForm.user.trim(),
|
||||
passwd: remoteForm.passwd || '',
|
||||
remote_dir: remoteForm.remote_dir.trim() || '/',
|
||||
passive: remoteForm.passive,
|
||||
timeout: remoteForm.timeout || 30
|
||||
};
|
||||
}
|
||||
|
||||
function validateRemoteForm(): boolean {
|
||||
if (!remoteForm.host || !remoteForm.user || !remoteForm.remote_dir) {
|
||||
message.warning('请填写完整远程数据源配置');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveRemote() {
|
||||
if (!validateRemoteForm()) return;
|
||||
|
||||
savingRemote.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/config/remote', getRemotePayload());
|
||||
configUpdate.value = result.update || configUpdate.value;
|
||||
message.success(result.message || '远程数据源配置已保存');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存远程数据源配置失败');
|
||||
} finally {
|
||||
savingRemote.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testRemote() {
|
||||
if (!validateRemoteForm()) return;
|
||||
|
||||
testingRemote.value = true;
|
||||
try {
|
||||
const result = await apiPost<ApiMessage>('/api/remote/test', getRemotePayload());
|
||||
message[result.success ? 'success' : 'error'](result.message || '远程连接测试完成');
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '远程连接测试失败');
|
||||
} finally {
|
||||
testingRemote.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function testDatabase() {
|
||||
testingDb.value = true;
|
||||
try {
|
||||
@@ -457,6 +600,21 @@ async function uploadConfigFile(event: Event) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRemoteConfig(config: RemoteDataConfig | undefined): RemoteDataConfig {
|
||||
const protocol = config?.protocol === 'ftp' ? 'ftp' : 'sftp';
|
||||
return {
|
||||
enabled: Boolean(config?.enabled),
|
||||
protocol,
|
||||
host: config?.host || '',
|
||||
port: config?.port || (protocol === 'sftp' ? 22 : 21),
|
||||
user: config?.user || '',
|
||||
passwd: config?.passwd || '',
|
||||
remote_dir: config?.remote_dir || '/',
|
||||
passive: config?.passive ?? true,
|
||||
timeout: config?.timeout || 30
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeExtractFields(fields: Array<Record<string, unknown>>): ExtractFieldConfig[] {
|
||||
return fields.map(field => {
|
||||
const extract = Array.isArray(field.Extract) ? uniqueStrings(field.Extract) : [];
|
||||
|
||||
@@ -593,6 +593,36 @@ select {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.remote-run-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding: 16px 20px;
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-default);
|
||||
}
|
||||
|
||||
.remote-run-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.remote-run-copy h4 {
|
||||
margin: 0 0 4px;
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remote-run-copy p {
|
||||
margin: 0;
|
||||
color: var(--td-text-color-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@@ -1521,6 +1551,11 @@ select {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.remote-run-card {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-actions,
|
||||
.field-card-title-row,
|
||||
.extract-list-header {
|
||||
|
||||
@@ -57,10 +57,23 @@ export interface AppConfig {
|
||||
passwd?: string;
|
||||
dbname: string;
|
||||
};
|
||||
remote_data: RemoteDataConfig;
|
||||
sheet_filter: string[];
|
||||
extract_fields: Array<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
export interface RemoteDataConfig {
|
||||
enabled: boolean;
|
||||
protocol: 'ftp' | 'sftp';
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
passwd?: string;
|
||||
remote_dir: string;
|
||||
passive: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
name: string;
|
||||
columns: Array<{
|
||||
|
||||
Reference in New Issue
Block a user