feat: 优化远程配置和调试启动

This commit is contained in:
2026-06-01 17:58:41 +08:00
parent fa8a1fff18
commit 8ba562e63e
4 changed files with 283 additions and 181 deletions
+58
View File
@@ -0,0 +1,58 @@
@echo off
setlocal
title CapacityReport Debug
cd /d "%~dp0"
set PYTHON_EXE=.venv\Scripts\python.exe
if not exist "%PYTHON_EXE%" (
echo [ERROR] Python virtual environment not found: %PYTHON_EXE%
echo [HINT] Run this command in the project root first: uv venv
pause
exit /b 1
)
"%PYTHON_EXE%" -c "import fastapi, uvicorn, pandas, openpyxl, pymysql, chardet" >nul 2>&1
if errorlevel 1 (
echo [INFO] Installing or repairing Python dependencies...
uv pip install -r requirements.txt
if errorlevel 1 (
echo [ERROR] Failed to install Python dependencies.
pause
exit /b 1
)
)
where npm >nul 2>&1
if errorlevel 1 (
echo [ERROR] npm not found. Install Node.js first.
pause
exit /b 1
)
pushd frontend
if not exist "node_modules" (
echo [INFO] Installing frontend dependencies...
call npm ci
if errorlevel 1 (
echo [ERROR] Failed to install frontend dependencies.
popd
pause
exit /b 1
)
)
popd
echo ========================================
echo CapacityReport - debug mode
echo ========================================
echo [INFO] Backend: http://127.0.0.1:9081
echo [INFO] Frontend: http://127.0.0.1:5173
echo [INFO] Close both debug windows to stop.
echo.
start "CapacityReport Backend" cmd /k ""%PYTHON_EXE%" -m app.main --host 127.0.0.1 --port 9081"
start "CapacityReport Frontend" cmd /k "pushd "%~dp0frontend" && npm run dev -- --host 127.0.0.1 --port 5173"
echo [OK] Debug services are starting.
pause
+7
View File
@@ -619,3 +619,10 @@
- Database-related settings now stay together with `数据库配置` and `处理历史保留`; remote automation settings now own the FTP/SFTP fields, auto-delete toggle, scheduler configuration, and scheduler status panel. - Database-related settings now stay together with `数据库配置` and `处理历史保留`; remote automation settings now own the FTP/SFTP fields, auto-delete toggle, scheduler configuration, and scheduler status panel.
- `frontend/src/styles.css` now uses dedicated layout classes for the database and remote settings panes, with independent scrolling and responsive single-column stacking on narrower screens. - `frontend/src/styles.css` now uses dedicated layout classes for the database and remote settings panes, with independent scrolling and responsive single-column stacking on narrower screens.
- Verification performed: `cd frontend && npm run build` passed. Browser inspection on `http://127.0.0.1:9081/settings` confirmed the two new tabs render correctly, the remote panel shows its own configuration and scheduler cards, and horizontal overflow remained at `0`. - Verification performed: `cd frontend && npm run build` passed. Browser inspection on `http://127.0.0.1:9081/settings` confirmed the two new tabs render correctly, the remote panel shows its own configuration and scheduler cards, and horizontal overflow remained at `0`.
## 2026-06-01: Settings remote scheduler refinement
- `frontend/src/components/SettingsPanel.vue` keeps `远程数据源` focused on FTP/SFTP connection fields. Protocol, host, port, timeout, and FTP passive mode render on one row when width allows; remote automation and delete-source toggles now sit below `远程目录`.
- `自动调度` is now a separate settings tab. Scheduler controls are disabled unless `启用远程自动化` is on, and turning off remote automation automatically turns off scheduler enablement in the current form state and saved payload.
- `debug.bat` starts the Python backend on `127.0.0.1:9081` and the Vite frontend on `127.0.0.1:5173` for source-level debugging without requiring `frontend/dist`.
- Verification performed: `cmd /c debug.bat` started the debug backend/frontend, `cd frontend && npm run build` passed, and browser inspection confirmed the remote connection fields align in one row with no horizontal overflow. Debug processes and `frontend/dist/` were removed after verification.
+160 -162
View File
@@ -82,57 +82,30 @@
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="remote" tab="远程数据源"> <n-tab-pane name="remote" tab="远程数据源">
<div class="settings-remote-grid"> <div class="settings-remote-panel">
<n-card title="远程数据源配置" size="small" class="work-card settings-remote-card"> <n-card title="远程数据源配置" size="small" class="work-card settings-remote-card">
<n-form label-placement="top"> <n-form label-placement="top">
<n-grid :cols="12" :x-gap="12"> <div class="remote-connection-row">
<n-gi :span="6"> <n-form-item label="协议">
<n-form-item label="启用远程自动化"> <n-select
<n-switch v-model:value="remoteForm.enabled" :disabled="remoteForm.auto_scheduler.enabled" /> v-model:value="remoteForm.protocol"
</n-form-item> :options="remoteProtocolOptions"
</n-gi> @update:value="updateRemoteProtocol"
<n-gi :span="6"> />
<n-form-item label="处理成功后删除源文件"> </n-form-item>
<n-switch <n-form-item label="服务器地址">
v-model:value="remoteForm.auto_delete_source" <n-input v-model:value="remoteForm.host" placeholder="192.168.1.10" />
:disabled="remoteForm.auto_scheduler.enabled" </n-form-item>
/> <n-form-item label="端口">
</n-form-item> <n-input-number v-model:value="remoteForm.port" class="full-width" :min="1" :max="65535" />
</n-gi> </n-form-item>
</n-grid> <n-form-item label="超时秒数">
<n-grid :cols="12" :x-gap="12"> <n-input-number v-model:value="remoteForm.timeout" class="full-width" :min="1" :max="600" />
<n-gi :span="4"> </n-form-item>
<n-form-item label="协议"> <n-form-item label="FTP 被动模式" class="remote-passive-field">
<n-select <n-switch v-model:value="remoteForm.passive" :disabled="remoteForm.protocol !== 'ftp'" />
v-model:value="remoteForm.protocol" </n-form-item>
:options="remoteProtocolOptions" </div>
@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-grid :cols="12" :x-gap="12">
<n-gi :span="6"> <n-gi :span="6">
<n-form-item label="用户名"> <n-form-item label="用户名">
@@ -148,6 +121,17 @@
<n-form-item label="远程目录"> <n-form-item label="远程目录">
<n-input v-model:value="remoteForm.remote_dir" placeholder="/CapacityReportData" /> <n-input v-model:value="remoteForm.remote_dir" placeholder="/CapacityReportData" />
</n-form-item> </n-form-item>
<div class="remote-toggle-row">
<n-form-item label="启用远程自动化">
<n-switch v-model:value="remoteForm.enabled" />
</n-form-item>
<n-form-item label="处理成功后删除源文件">
<n-switch
v-model:value="remoteForm.auto_delete_source"
:disabled="remoteForm.auto_scheduler.enabled"
/>
</n-form-item>
</div>
<p class="form-hint">自动化执行会递归下载该目录下的全部文件和文件夹到本地缓存,再按现有处理流程入库和执行脚本;自动删除源文件只会在处理成功后删除远程文件,保留目录结构。</p> <p class="form-hint">自动化执行会递归下载该目录下的全部文件和文件夹到本地缓存,再按现有处理流程入库和执行脚本;自动删除源文件只会在处理成功后删除远程文件,保留目录结构。</p>
</n-form> </n-form>
@@ -158,122 +142,124 @@
</n-space> </n-space>
</template> </template>
</n-card> </n-card>
</div>
</n-tab-pane>
<div class="settings-remote-stack"> <n-tab-pane name="scheduler" tab="自动调度">
<n-card title="自动调度" size="small" class="work-card settings-scheduler-card"> <div class="settings-scheduler-panel">
<n-form label-placement="top"> <n-card title="自动调度" size="small" class="work-card settings-scheduler-card">
<n-grid :cols="12" :x-gap="12"> <n-form label-placement="top">
<n-gi :span="4"> <n-grid :cols="12" :x-gap="12">
<n-form-item label="启用自动调度"> <n-gi :span="4">
<n-switch v-model:value="remoteForm.auto_scheduler.enabled" /> <n-form-item label="启用自动调度">
</n-form-item> <n-switch v-model:value="remoteForm.auto_scheduler.enabled" :disabled="!remoteForm.enabled" />
</n-gi> </n-form-item>
<n-gi :span="4"> </n-gi>
<n-form-item label="检查间隔(小时)"> <n-gi :span="4">
<n-input-number <n-form-item label="检查间隔(小时)">
v-model:value="remoteForm.auto_scheduler.check_interval_hours" <n-input-number
class="full-width" v-model:value="remoteForm.auto_scheduler.check_interval_hours"
:min="1" class="full-width"
:precision="0" :min="1"
:disabled="!remoteForm.auto_scheduler.enabled" :precision="0"
/> :disabled="schedulerControlsDisabled"
</n-form-item> />
</n-gi> </n-form-item>
<n-gi :span="4"> </n-gi>
<n-form-item label="目标周期"> <n-gi :span="4">
<n-select <n-form-item label="目标周期">
v-model:value="remoteForm.auto_scheduler.week_offset" <n-select
:options="schedulerWeekOptions" v-model:value="remoteForm.auto_scheduler.week_offset"
:disabled="!remoteForm.auto_scheduler.enabled" :options="schedulerWeekOptions"
/> :disabled="schedulerControlsDisabled"
</n-form-item> />
</n-gi> </n-form-item>
</n-grid> </n-gi>
<n-form-item label="预期目录"> </n-grid>
<div class="scheduler-dir-editor"> <n-form-item label="预期目录">
<n-input-group> <div class="scheduler-dir-editor">
<n-input <n-input-group>
v-model:value="newSchedulerDirectory" <n-input
placeholder="例如 4G/FDD" v-model:value="newSchedulerDirectory"
:disabled="!remoteForm.auto_scheduler.enabled" placeholder="例如 4G/FDD"
@keydown.enter.prevent="addSchedulerDirectory" :disabled="schedulerControlsDisabled"
/> @keydown.enter.prevent="addSchedulerDirectory"
<n-button :disabled="!remoteForm.auto_scheduler.enabled" @click="addSchedulerDirectory"> />
添加 <n-button :disabled="schedulerControlsDisabled" @click="addSchedulerDirectory">
</n-button> 添加
</n-input-group> </n-button>
<div class="scheduler-dir-tags"> </n-input-group>
<n-tag <div class="scheduler-dir-tags">
v-for="(directory, index) in remoteForm.auto_scheduler.expected_directories" <n-tag
:key="`${directory}-${index}`" v-for="(directory, index) in remoteForm.auto_scheduler.expected_directories"
closable :key="`${directory}-${index}`"
:disabled="!remoteForm.auto_scheduler.enabled" closable
@close="removeSchedulerDirectory(index)" :disabled="schedulerControlsDisabled"
> @close="removeSchedulerDirectory(index)"
{{ directory }} >
</n-tag> {{ directory }}
<n-tag v-if="remoteForm.auto_scheduler.expected_directories.length === 0" type="default"> </n-tag>
按实际 ZIP 目录检测 <n-tag v-if="remoteForm.auto_scheduler.expected_directories.length === 0" type="default">
</n-tag> 按实际 ZIP 目录检测
</div>
</div>
</n-form-item>
<p class="form-hint">自动调度会按文件名日期检查目标自然周 7 天;开启后会强制启用远程自动化和处理成功后删除源文件。</p>
</n-form>
<div class="scheduler-status-panel">
<div class="scheduler-status-header">
<span>调度状态</span>
<n-tag size="small" :type="schedulerStatusTagType">
{{ schedulerStatusLabel }}
</n-tag>
</div>
<div class="scheduler-status-grid">
<div>
<span>目标周</span>
<strong>{{ schedulerTargetWeekText }}</strong>
</div>
<div>
<span>下次检查</span>
<strong>{{ schedulerStatus?.next_check_at || '-' }}</strong>
</div>
<div>
<span>就绪标识</span>
<strong>{{ schedulerStatus?.ready_flag?.exists ? '已存在' : '无' }}</strong>
</div>
</div>
<p class="form-hint">{{ schedulerStatus?.last_message || '自动调度状态尚未加载' }}</p>
<div v-if="schedulerDirectoryRows.length > 0" class="scheduler-directory-list">
<div v-for="row in schedulerDirectoryRows" :key="row.name" class="scheduler-directory-row">
<span class="scheduler-directory-name">{{ row.displayName }}</span>
<n-tag size="small" :type="row.skipped ? 'info' : row.ready ? 'success' : 'warning'">
{{ row.skipped ? '已停推' : row.ready ? '就绪' : `缺 ${row.missing_days.length} 天` }}
</n-tag> </n-tag>
</div> </div>
</div> </div>
<n-space justify="end" size="small"> </n-form-item>
<n-button size="small" :loading="loadingSchedulerStatus" @click="loadSchedulerStatus"> <p class="form-hint">自动调度依赖远程自动化;关闭远程自动化时会自动关闭调度。启用调度后会按文件名日期检查目标自然周 7 天,并强制开启处理成功后删除源文件。</p>
刷新状态 </n-form>
</n-button>
<n-button
size="small"
type="primary"
:loading="triggeringScheduler"
:disabled="!remoteForm.auto_scheduler.enabled"
@click="triggerSchedulerCheck"
>
立即检查
</n-button>
</n-space>
</div>
<template #footer> <div class="scheduler-status-panel">
<n-space justify="end"> <div class="scheduler-status-header">
<n-button type="primary" :loading="savingRemote" @click="saveRemote">保存调度</n-button> <span>调度状态</span>
</n-space> <n-tag size="small" :type="schedulerStatusTagType">
</template> {{ schedulerStatusLabel }}
</n-card> </n-tag>
</div> </div>
<div class="scheduler-status-grid">
<div>
<span>目标周</span>
<strong>{{ schedulerTargetWeekText }}</strong>
</div>
<div>
<span>下次检查</span>
<strong>{{ schedulerStatus?.next_check_at || '-' }}</strong>
</div>
<div>
<span>就绪标识</span>
<strong>{{ schedulerStatus?.ready_flag?.exists ? '已存在' : '无' }}</strong>
</div>
</div>
<p class="form-hint">{{ schedulerStatus?.last_message || '自动调度状态尚未加载' }}</p>
<div v-if="schedulerDirectoryRows.length > 0" class="scheduler-directory-list">
<div v-for="row in schedulerDirectoryRows" :key="row.name" class="scheduler-directory-row">
<span class="scheduler-directory-name">{{ row.displayName }}</span>
<n-tag size="small" :type="row.skipped ? 'info' : row.ready ? 'success' : 'warning'">
{{ row.skipped ? '已停推' : row.ready ? '就绪' : `缺 ${row.missing_days.length} 天` }}
</n-tag>
</div>
</div>
<n-space justify="end" size="small">
<n-button size="small" :loading="loadingSchedulerStatus" @click="loadSchedulerStatus">
刷新状态
</n-button>
<n-button
size="small"
type="primary"
:loading="triggeringScheduler"
:disabled="!remoteForm.enabled || !remoteForm.auto_scheduler.enabled"
@click="triggerSchedulerCheck"
>
立即检查
</n-button>
</n-space>
</div>
<template #footer>
<n-space justify="end">
<n-button type="primary" :loading="savingRemote" @click="saveRemote">保存调度</n-button>
</n-space>
</template>
</n-card>
</div> </div>
</n-tab-pane> </n-tab-pane>
@@ -579,12 +565,21 @@ const schedulerDirectoryRows = computed(() => {
...status ...status
})); }));
}); });
const schedulerControlsDisabled = computed(() => !remoteForm.enabled || !remoteForm.auto_scheduler.enabled);
watch(
() => remoteForm.enabled,
enabled => {
if (!enabled && remoteForm.auto_scheduler.enabled) {
remoteForm.auto_scheduler.enabled = false;
}
}
);
watch( watch(
() => remoteForm.auto_scheduler.enabled, () => remoteForm.auto_scheduler.enabled,
enabled => { enabled => {
if (enabled) { if (enabled) {
remoteForm.enabled = true;
remoteForm.auto_delete_source = true; remoteForm.auto_delete_source = true;
} }
} }
@@ -959,8 +954,11 @@ function normalizeAutoSchedulerConfig(config: RemoteAutoSchedulerConfig | undefi
} }
function enforceRemoteSchedulerRules() { function enforceRemoteSchedulerRules() {
if (!remoteForm.enabled) {
remoteForm.auto_scheduler.enabled = false;
return;
}
if (!remoteForm.auto_scheduler.enabled) return; if (!remoteForm.auto_scheduler.enabled) return;
remoteForm.enabled = true;
remoteForm.auto_delete_source = true; remoteForm.auto_delete_source = true;
remoteForm.auto_scheduler.check_interval_hours = Math.max(Number(remoteForm.auto_scheduler.check_interval_hours) || 1, 1); remoteForm.auto_scheduler.check_interval_hours = Math.max(Number(remoteForm.auto_scheduler.check_interval_hours) || 1, 1);
remoteForm.auto_scheduler.expected_directories = uniqueStrings(remoteForm.auto_scheduler.expected_directories) remoteForm.auto_scheduler.expected_directories = uniqueStrings(remoteForm.auto_scheduler.expected_directories)
+58 -19
View File
@@ -1383,14 +1383,17 @@ select {
} }
.settings-database-panel, .settings-database-panel,
.settings-remote-grid, .settings-remote-panel,
.settings-scheduler-panel,
.settings-rules-grid { .settings-rules-grid {
display: grid; display: grid;
align-items: start; align-items: start;
gap: 18px; gap: 18px;
} }
.settings-database-panel { .settings-database-panel,
.settings-remote-panel,
.settings-scheduler-panel {
grid-template-columns: minmax(420px, 680px); grid-template-columns: minmax(420px, 680px);
height: 100%; height: 100%;
min-height: 0; min-height: 0;
@@ -1401,39 +1404,30 @@ select {
scrollbar-width: thin; scrollbar-width: thin;
} }
.settings-database-stack, .settings-database-stack {
.settings-remote-stack {
display: flex; display: flex;
min-width: 0; min-width: 0;
flex-direction: column; flex-direction: column;
gap: 18px; gap: 18px;
} }
.settings-remote-grid {
grid-template-columns: minmax(380px, 0.9fr) minmax(360px, 1.1fr);
height: 100%;
min-height: 0;
padding-right: 6px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-color: var(--td-scrollbar-color) transparent;
scrollbar-width: thin;
}
.settings-database-panel::-webkit-scrollbar, .settings-database-panel::-webkit-scrollbar,
.settings-remote-grid::-webkit-scrollbar { .settings-remote-panel::-webkit-scrollbar,
.settings-scheduler-panel::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px; height: 8px;
} }
.settings-database-panel::-webkit-scrollbar-thumb, .settings-database-panel::-webkit-scrollbar-thumb,
.settings-remote-grid::-webkit-scrollbar-thumb { .settings-remote-panel::-webkit-scrollbar-thumb,
.settings-scheduler-panel::-webkit-scrollbar-thumb {
background: var(--td-scrollbar-color); background: var(--td-scrollbar-color);
border-radius: 8px; border-radius: 8px;
} }
.settings-database-panel::-webkit-scrollbar-track, .settings-database-panel::-webkit-scrollbar-track,
.settings-remote-grid::-webkit-scrollbar-track { .settings-remote-panel::-webkit-scrollbar-track,
.settings-scheduler-panel::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
@@ -1442,6 +1436,36 @@ select {
min-width: 0; min-width: 0;
} }
.settings-remote-panel {
grid-template-columns: minmax(720px, 1fr);
}
.settings-scheduler-panel {
grid-template-columns: minmax(420px, 760px);
}
.remote-connection-row {
display: grid;
grid-template-columns: minmax(112px, 0.8fr) minmax(220px, 1.8fr) minmax(110px, 0.8fr) minmax(120px, 0.8fr) minmax(116px, 0.7fr);
gap: 12px;
align-items: start;
}
.remote-passive-field .n-form-item-blank {
align-items: center;
min-height: 34px;
}
.remote-toggle-row {
display: flex;
flex-wrap: wrap;
gap: 12px 56px;
}
.remote-toggle-row .n-form-item {
margin-bottom: 0;
}
.settings-rules-grid { .settings-rules-grid {
grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr); grid-template-columns: minmax(280px, 0.72fr) minmax(0, 1.4fr);
grid-template-rows: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr);
@@ -1935,11 +1959,16 @@ select {
/* ==================== Responsive ==================== */ /* ==================== Responsive ==================== */
@media (max-width: 1200px) { @media (max-width: 1200px) {
.settings-database-panel, .settings-database-panel,
.settings-remote-grid, .settings-remote-panel,
.settings-scheduler-panel,
.settings-rules-grid { .settings-rules-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.remote-connection-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.settings-rules-grid { .settings-rules-grid {
grid-template-rows: auto minmax(520px, 1fr); grid-template-rows: auto minmax(520px, 1fr);
align-items: start; align-items: start;
@@ -2032,3 +2061,13 @@ select {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
@media (max-width: 700px) {
.remote-connection-row {
grid-template-columns: 1fr;
}
.remote-toggle-row {
gap: 12px 24px;
}
}