fix: 优化日志显示和下载完成反馈
This commit is contained in:
@@ -1,4 +1,12 @@
|
||||
# 项目上下文记录
|
||||
## 2026-05-22:统一日志框与桌面端下载路径反馈
|
||||
|
||||
- 数据处理页菜单从“数据上传”改为“数据处理”,路由标题同步改为“数据处理”。
|
||||
- 上传/处理日志、历史详情日志和脚本执行日志统一使用分级日志渲染:默认/INFO 跟随主题文字色,SUCCESS 绿色,WARNING 黄色,ERROR 红色;日志框背景和滚动条颜色跟随主题,支持横纵向滚动。
|
||||
- 数据处理页日志高度改为响应式上限,避免长任务日志把外层页面撑出纵向滚动条。
|
||||
- 脚本编辑页的手动运行日志移到编辑器下方,编辑器与日志框共享页面高度;脚本运行状态保存在前端全局状态中,切换页面回来可继续轮询或查看上次运行结果,运行结束不自动隐藏日志。
|
||||
- 桌面端下载完成弹窗会显示保存路径,并提供“打开所在文件夹”;新增 Tauri 命令 `open_path_in_file_manager` 通过系统文件管理器打开下载目录。
|
||||
|
||||
## 2026-05-21:桌面端 sidecar 进程改为跨平台管理
|
||||
|
||||
- Tauri 桌面端启动/关闭后端 sidecar 不再调用 Windows `netstat`、`tasklist`、`taskkill` 等控制台命令,避免启动和退出时闪过 DOS 窗口。
|
||||
|
||||
@@ -178,7 +178,7 @@ type MenuKey = (typeof menuKeys)[number];
|
||||
let brandClickResetTimer: number | undefined;
|
||||
|
||||
const menuOptions: MenuOption[] = [
|
||||
{ label: '数据上传', key: 'workflow', icon: renderIcon(CloudUploadOutline) },
|
||||
{ label: '数据处理', key: 'workflow', icon: renderIcon(CloudUploadOutline) },
|
||||
{ label: '处理历史', key: 'history', icon: renderIcon(FileTrayFullOutline) },
|
||||
{ label: '数据管理', key: 'database', icon: renderIcon(ServerOutline) },
|
||||
{ label: '脚本编辑', key: 'script', icon: renderIcon(ConstructOutline) },
|
||||
|
||||
@@ -11,6 +11,11 @@ let cachedTauriInvoke: TauriInvoke | null = null;
|
||||
|
||||
type TauriInvoke = <T>(command: string, args?: Record<string, unknown>) => Promise<T>;
|
||||
|
||||
export interface DownloadResult {
|
||||
saved: boolean;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
interface TauriDownloadHeader {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -149,7 +154,7 @@ export function upload<T>(
|
||||
});
|
||||
}
|
||||
|
||||
export async function download(url: string, body: unknown, filename: string): Promise<boolean> {
|
||||
export async function download(url: string, body: unknown, filename: string): Promise<DownloadResult> {
|
||||
const headers = new Headers({ 'Content-Type': 'application/json' });
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
@@ -180,7 +185,7 @@ export async function download(url: string, body: unknown, filename: string): Pr
|
||||
return await saveBlobResponse(response, parseFilename(response.headers.get('content-disposition')) || filename);
|
||||
}
|
||||
|
||||
export async function downloadGet(url: string, fallbackFilename: string): Promise<boolean> {
|
||||
export async function downloadGet(url: string, fallbackFilename: string): Promise<DownloadResult> {
|
||||
const headers = new Headers();
|
||||
const token = getToken();
|
||||
if (token) {
|
||||
@@ -247,7 +252,7 @@ function parseApiError(data: ApiError, fallback: string): { message: string; det
|
||||
};
|
||||
}
|
||||
|
||||
async function saveBlobResponse(response: Response, filename: string): Promise<boolean> {
|
||||
async function saveBlobResponse(response: Response, filename: string): Promise<DownloadResult> {
|
||||
const blob = await response.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -255,19 +260,25 @@ async function saveBlobResponse(response: Response, filename: string): Promise<b
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
return true;
|
||||
return { saved: true };
|
||||
}
|
||||
|
||||
async function downloadWithTauri(request: TauriDownloadRequest): Promise<boolean> {
|
||||
async function downloadWithTauri(request: TauriDownloadRequest): Promise<DownloadResult> {
|
||||
try {
|
||||
const invoke = await getTauriInvoke();
|
||||
const result = await invoke<TauriDownloadResult>('download_to_file', { request });
|
||||
return result.saved;
|
||||
return { saved: result.saved, path: result.path };
|
||||
} catch (error) {
|
||||
throw parseTauriDownloadError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openPathInFileManager(path: string): Promise<void> {
|
||||
if (!isTauriRuntime()) return;
|
||||
const invoke = await getTauriInvoke();
|
||||
await invoke('open_path_in_file_manager', { path });
|
||||
}
|
||||
|
||||
async function getTauriInvoke(): Promise<TauriInvoke> {
|
||||
if (!cachedTauriInvoke) {
|
||||
const api = await import('@tauri-apps/api/core');
|
||||
|
||||
@@ -487,14 +487,14 @@ async function downloadSelectedCsv() {
|
||||
const filename = `${selectedCsvTable.value}.csv`;
|
||||
downloadingFormat.value = 'csv';
|
||||
try {
|
||||
const completed = await download(
|
||||
const result = await download(
|
||||
'/api/download',
|
||||
{ table_name: selectedCsvTable.value, format: 'csv' },
|
||||
filename
|
||||
);
|
||||
if (completed) {
|
||||
if (result.saved) {
|
||||
csvDialogVisible.value = false;
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
showDownloadCompleteDialog(dialog, filename, result.path);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
@@ -510,14 +510,14 @@ async function downloadSelectedXlsx() {
|
||||
downloadingFormat.value = 'xlsx';
|
||||
const filename = selectedXlsxTables.value.length === 1 ? `${selectedXlsxTables.value[0]}.xlsx` : 'tables.xlsx';
|
||||
try {
|
||||
const completed = await download(
|
||||
const result = await download(
|
||||
'/api/download',
|
||||
{ table_names: selectedXlsxTables.value, format: 'xlsx' },
|
||||
filename
|
||||
);
|
||||
if (completed) {
|
||||
if (result.saved) {
|
||||
xlsxDialogVisible.value = false;
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
showDownloadCompleteDialog(dialog, filename, result.path);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '导出失败');
|
||||
|
||||
@@ -141,7 +141,12 @@
|
||||
{{ taskStatus.error }}
|
||||
</n-alert>
|
||||
<div ref="logContainer" class="log-container">
|
||||
<pre class="log-content">{{ logText }}</pre>
|
||||
<pre class="colored-log-content"><span
|
||||
v-for="(line, index) in logLines"
|
||||
:key="index"
|
||||
class="colored-log-line"
|
||||
:class="`log-level-${line.level}`"
|
||||
>{{ line.text }}</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,6 +194,7 @@ import {
|
||||
|
||||
import { ApiRequestError, apiGet, apiPost, upload } from '../api/client';
|
||||
import type { ActiveTask, ApiErrorDetail, LicenseStatus, TaskStatus } from '../types';
|
||||
import { toColoredLogLines } from '../composables/logLines';
|
||||
|
||||
type FileStatus = 'pending' | 'uploading' | 'uploaded' | 'error';
|
||||
|
||||
@@ -248,6 +254,7 @@ const stageLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
const logText = computed(() => taskStatus.value?.logs?.join('\n') || '等待任务开始');
|
||||
const logLines = computed(() => toColoredLogLines(logText.value, '等待任务开始'));
|
||||
const processVisible = computed(() => Boolean(taskStatus.value || activeTask.value?.has_active));
|
||||
const taskInProgress = computed(() => {
|
||||
if (activeTask.value?.has_active) {
|
||||
|
||||
@@ -101,10 +101,9 @@ import { CopyOutline, DownloadOutline, FileTrayOutline, RefreshOutline, TrashOut
|
||||
import { apiGet, apiPost, download as downloadFile } from '../api/client';
|
||||
import type { CacheSize, HistoryDetail, HistoryRecord } from '../types';
|
||||
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||
import { toColoredLogLines } from '../composables/logLines';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const records = ref<HistoryRecord[]>([]);
|
||||
@@ -120,13 +119,7 @@ let recordSizeToken = 0;
|
||||
const totalSizeText = computed(() => `总占用: ${cacheSize.value?.size_formatted || '计算中...'}`);
|
||||
const hasRecords = computed(() => records.value.length > 0);
|
||||
const detailLogText = computed(() => detail.value?.logs?.join('\n') || '');
|
||||
const detailLogLines = computed(() => {
|
||||
const lines = detailLogText.value ? detailLogText.value.split(/\r?\n/) : ['暂无日志'];
|
||||
return lines.map(line => ({
|
||||
text: line,
|
||||
level: resolveLogLevel(line)
|
||||
}));
|
||||
});
|
||||
const detailLogLines = computed(() => toColoredLogLines(detailLogText.value));
|
||||
|
||||
onMounted(() => {
|
||||
setPageHeader({
|
||||
@@ -186,9 +179,9 @@ async function downloadHistory(record: HistoryRecord) {
|
||||
downloadingRecordId.value = record.id;
|
||||
try {
|
||||
const filename = `${record.id}.zip`;
|
||||
const completed = await downloadFile('/api/history/download', { record_id: record.id }, filename);
|
||||
if (completed) {
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
const result = await downloadFile('/api/history/download', { record_id: record.id }, filename);
|
||||
if (result.saved) {
|
||||
showDownloadCompleteDialog(dialog, filename, result.path);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '下载历史数据失败');
|
||||
@@ -256,14 +249,6 @@ async function writeClipboardText(text: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveLogLevel(line: string): LogLevel {
|
||||
if (/\b(ERROR|FAILED|FAILURE)\b|错误|失败/i.test(line)) return 'error';
|
||||
if (/\b(WARN|WARNING)\b|警告/i.test(line)) return 'warning';
|
||||
if (/\b(SUCCESS|SUCCEEDED|COMPLETED)\b|成功|完成/i.test(line)) return 'success';
|
||||
if (/\bINFO\b|信息/i.test(line)) return 'info';
|
||||
return 'default';
|
||||
}
|
||||
|
||||
function confirmDelete(recordId: string) {
|
||||
dialog.warning({
|
||||
title: '删除历史记录',
|
||||
|
||||
@@ -4,14 +4,8 @@
|
||||
<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 v-if="taskStatus || taskId" class="script-process-section">
|
||||
<div class="upload-process-card script-process-card">
|
||||
<div class="card-header">
|
||||
<div class="card-header-left">
|
||||
<span class="card-title">脚本执行进度</span>
|
||||
@@ -22,10 +16,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<n-log :log="logText" language="text" trim class="script-log" />
|
||||
<div class="colored-log-panel script-log" role="log" aria-label="脚本执行日志">
|
||||
<pre class="colored-log-content"><span
|
||||
v-for="(line, index) in logLines"
|
||||
:key="index"
|
||||
class="colored-log-line"
|
||||
:class="`log-level-${line.level}`"
|
||||
>{{ line.text }}</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="script-status-bar">
|
||||
<span>{{ cursorText }}</span>
|
||||
<span>{{ editorStatus }}</span>
|
||||
<span>{{ lineCount }} 行</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -39,12 +46,17 @@ import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution';
|
||||
|
||||
import { apiGet, apiPost } from '../api/client';
|
||||
import type { ApiMessage, ScriptContent, TaskStatus } from '../types';
|
||||
import { toColoredLogLines } from '../composables/logLines';
|
||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||
|
||||
type MonacoGlobal = typeof globalThis & {
|
||||
MonacoEnvironment?: {
|
||||
getWorker: (_moduleId: string, _label: string) => Worker;
|
||||
};
|
||||
__capacityReportScriptTaskState?: {
|
||||
taskId: ReturnType<typeof ref<string>>;
|
||||
taskStatus: ReturnType<typeof ref<TaskStatus | null>>;
|
||||
};
|
||||
};
|
||||
|
||||
(globalThis as MonacoGlobal).MonacoEnvironment = {
|
||||
@@ -65,13 +77,21 @@ const isModified = ref(false);
|
||||
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;
|
||||
let themeObserver: MutationObserver | undefined;
|
||||
let applyingRemoteContent = false;
|
||||
|
||||
const globalWindow = window as MonacoGlobal;
|
||||
const scriptTaskState = globalWindow.__capacityReportScriptTaskState || {
|
||||
taskId: ref(''),
|
||||
taskStatus: ref<TaskStatus | null>(null)
|
||||
};
|
||||
globalWindow.__capacityReportScriptTaskState = scriptTaskState;
|
||||
const taskId = scriptTaskState.taskId;
|
||||
const taskStatus = scriptTaskState.taskStatus;
|
||||
|
||||
const logText = computed(() => taskStatus.value?.logs?.join('\n') || '等待执行日志');
|
||||
const logLines = computed(() => toColoredLogLines(logText.value, '等待执行日志'));
|
||||
const statusClass = computed(() => {
|
||||
if (taskStatus.value?.status === 'completed') return 'completed';
|
||||
if (taskStatus.value?.status === 'failed') return 'failed';
|
||||
@@ -123,6 +143,9 @@ onMounted(async () => {
|
||||
createEditor();
|
||||
observeTheme();
|
||||
await loadScript();
|
||||
if (taskId.value && taskStatus.value && !['completed', 'failed'].includes(taskStatus.value.status)) {
|
||||
startPolling();
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
||||
@@ -659,9 +659,9 @@ async function changePassword() {
|
||||
async function downloadConfig() {
|
||||
try {
|
||||
const filename = 'Configure.json';
|
||||
const completed = await downloadGet('/api/config/download', filename);
|
||||
if (completed) {
|
||||
showDownloadCompleteDialog(dialog, filename);
|
||||
const result = await downloadGet('/api/config/download', filename);
|
||||
if (result.saved) {
|
||||
showDownloadCompleteDialog(dialog, filename, result.path);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '下载配置失败');
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
interface DownloadDialog {
|
||||
success(options: {
|
||||
title: string;
|
||||
content: string;
|
||||
positiveText?: string;
|
||||
closable?: boolean;
|
||||
maskClosable?: boolean;
|
||||
}): unknown;
|
||||
}
|
||||
import { h, type VNodeChild } from 'vue';
|
||||
import type { useDialog } from 'naive-ui';
|
||||
|
||||
export function showDownloadCompleteDialog(dialog: DownloadDialog, filename?: string): void {
|
||||
import { openPathInFileManager } from '../api/client';
|
||||
|
||||
type DownloadDialog = ReturnType<typeof useDialog>;
|
||||
|
||||
export function showDownloadCompleteDialog(dialog: DownloadDialog, filename?: string, path?: string): void {
|
||||
const safeName = filename?.trim();
|
||||
const safePath = path?.trim();
|
||||
const content = safePath
|
||||
? (): VNodeChild => h('div', { class: 'download-complete-content' }, [
|
||||
h('p', safeName ? `文件「${safeName}」已下载完成。` : '文件已下载完成。'),
|
||||
h('button', {
|
||||
class: 'download-path-link',
|
||||
type: 'button',
|
||||
title: safePath,
|
||||
onClick: () => void openPathInFileManager(safePath)
|
||||
}, safePath)
|
||||
])
|
||||
: safeName ? `文件「${safeName}」已下载完成。` : '文件已下载完成。';
|
||||
|
||||
dialog.success({
|
||||
title: '下载完成',
|
||||
content: safeName ? `文件「${safeName}」已下载完成。` : '文件已下载完成。',
|
||||
positiveText: '知道了',
|
||||
content,
|
||||
positiveText: safePath ? '打开所在文件夹' : '知道了',
|
||||
negativeText: safePath ? '关闭' : undefined,
|
||||
closable: true,
|
||||
maskClosable: true
|
||||
maskClosable: true,
|
||||
onPositiveClick: safePath ? () => openPathInFileManager(safePath) : undefined
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export type LogLevel = 'default' | 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
export interface ColoredLogLine {
|
||||
text: string;
|
||||
level: LogLevel;
|
||||
}
|
||||
|
||||
export function toColoredLogLines(text: string, fallback = '暂无日志'): ColoredLogLine[] {
|
||||
const lines = text ? text.split(/\r?\n/) : [fallback];
|
||||
return lines.map(line => ({
|
||||
text: line,
|
||||
level: resolveLogLevel(line)
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveLogLevel(line: string): LogLevel {
|
||||
const marker = line.match(/\[(ERROR|WARN|WARNING|SUCCESS|INFO)\]/i)?.[1]?.toUpperCase();
|
||||
if (marker === 'ERROR') return 'error';
|
||||
if (marker === 'WARN' || marker === 'WARNING') return 'warning';
|
||||
if (marker === 'SUCCESS') return 'success';
|
||||
if (marker === 'INFO') return 'info';
|
||||
if (/\b(ERROR|FAILED|FAILURE)\b|错误|失败/i.test(line)) return 'error';
|
||||
if (/\b(WARN|WARNING)\b|警告/i.test(line)) return 'warning';
|
||||
if (/\b(SUCCESS|SUCCEEDED|COMPLETED)\b|成功|已完成|处理完成|下载完成|执行完成/i.test(line)) {
|
||||
return 'success';
|
||||
}
|
||||
return 'default';
|
||||
}
|
||||
@@ -9,7 +9,7 @@ const ScriptPanel = () => import('./components/ScriptPanel.vue');
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{ path: '/', redirect: { name: 'workflow' } },
|
||||
{ path: '/upload', name: 'workflow', component: FileWorkflow, meta: { title: '数据上传与处理' } },
|
||||
{ path: '/upload', name: 'workflow', component: FileWorkflow, meta: { title: '数据处理' } },
|
||||
{ path: '/history', name: 'history', component: HistoryPanel, meta: { title: '处理历史' } },
|
||||
{ path: '/database', name: 'database', component: DatabasePanel, meta: { title: '数据管理' } },
|
||||
{ path: '/script', name: 'script', component: ScriptPanel, meta: { title: '脚本编辑' } },
|
||||
|
||||
+103
-31
@@ -26,6 +26,7 @@
|
||||
--td-bg-color-component-disabled: #eeeeee;
|
||||
--td-border-color: #dcdcdc;
|
||||
--td-border-color-light: #e7e7e7;
|
||||
--td-scrollbar-color: rgba(0, 0, 0, 0.2);
|
||||
--td-mask-color: rgba(0, 0, 0, 0.6);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.05), 0 4px 5px rgba(0, 0, 0, 0.08), 0 2px 4px -1px rgba(0, 0, 0, 0.12);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.05), 0 8px 10px 1px rgba(0, 0, 0, 0.06), 0 5px 5px -3px rgba(0, 0, 0, 0.1);
|
||||
@@ -61,6 +62,7 @@
|
||||
--td-bg-color-component-disabled: #383838;
|
||||
--td-border-color: #5e5e5e;
|
||||
--td-border-color-light: #4c4c4c;
|
||||
--td-scrollbar-color: rgba(255, 255, 255, 0.28);
|
||||
--td-shadow-1: 0 1px 10px rgba(0, 0, 0, 0.15), 0 4px 5px rgba(0, 0, 0, 0.22), 0 2px 4px -1px rgba(0, 0, 0, 0.32);
|
||||
--td-shadow-2: 0 3px 14px 2px rgba(0, 0, 0, 0.15), 0 8px 10px 1px rgba(0, 0, 0, 0.18), 0 5px 5px -3px rgba(0, 0, 0, 0.28);
|
||||
--td-shadow-3: 0 6px 30px 5px rgba(0, 0, 0, 0.15), 0 16px 24px 2px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.24);
|
||||
@@ -473,6 +475,12 @@ select {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
||||
.upload-page-body {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-zone {
|
||||
display: flex;
|
||||
position: relative;
|
||||
@@ -797,9 +805,14 @@ select {
|
||||
}
|
||||
|
||||
.process-section {
|
||||
min-height: 0;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.upload-page-body > .process-section {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.upload-process-card {
|
||||
background: var(--td-bg-color-container);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
@@ -829,6 +842,7 @@ select {
|
||||
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.process-status {
|
||||
@@ -857,20 +871,40 @@ select {
|
||||
}
|
||||
|
||||
.log-container {
|
||||
height: calc(100vh - 280px);
|
||||
min-height: 300px;
|
||||
height: clamp(200px, calc(100vh - 400px), 520px);
|
||||
min-height: 180px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
overflow: auto;
|
||||
background: var(--td-bg-color-page);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-default);
|
||||
scrollbar-color: var(--td-scrollbar-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.process-section.finished .log-container {
|
||||
height: auto;
|
||||
min-height: 180px;
|
||||
max-height: clamp(220px, calc(100vh - 620px), 460px);
|
||||
height: clamp(180px, calc(100vh - 460px), 380px);
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.upload-page-body > .process-section .upload-process-card {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-page-body > .process-section .card-body {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.upload-page-body > .process-section .log-container {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
@@ -1063,13 +1097,15 @@ select {
|
||||
}
|
||||
|
||||
.colored-log-panel {
|
||||
height: 360px;
|
||||
overflow: scroll;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
color: #d7dde7;
|
||||
background: #111820;
|
||||
color: var(--td-text-color-primary);
|
||||
background: var(--td-bg-color-page);
|
||||
border: 1px solid var(--td-border-color-light);
|
||||
border-radius: var(--td-radius-default);
|
||||
scrollbar-color: #5f6f83 #0b1118;
|
||||
scrollbar-color: var(--td-scrollbar-color) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.colored-log-panel::-webkit-scrollbar {
|
||||
@@ -1078,22 +1114,22 @@ select {
|
||||
}
|
||||
|
||||
.colored-log-panel::-webkit-scrollbar-track {
|
||||
background: #0b1118;
|
||||
background: transparent;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.colored-log-panel::-webkit-scrollbar-thumb {
|
||||
background: #5f6f83;
|
||||
border: 2px solid #0b1118;
|
||||
background: var(--td-scrollbar-color);
|
||||
border: 2px solid var(--td-bg-color-page);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.colored-log-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #7b8da3;
|
||||
background: var(--td-text-color-placeholder);
|
||||
}
|
||||
|
||||
.colored-log-panel::-webkit-scrollbar-corner {
|
||||
background: #0b1118;
|
||||
background: var(--td-bg-color-page);
|
||||
}
|
||||
|
||||
.colored-log-content {
|
||||
@@ -1108,32 +1144,57 @@ select {
|
||||
|
||||
.colored-log-line {
|
||||
display: block;
|
||||
color: #d7dde7;
|
||||
color: var(--td-text-color-primary);
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.colored-log-line.log-level-info {
|
||||
color: #62a8ff;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.colored-log-line.log-level-success {
|
||||
color: #38d996;
|
||||
color: var(--td-success-color);
|
||||
}
|
||||
|
||||
.colored-log-line.log-level-warning {
|
||||
color: #f6c453;
|
||||
color: var(--td-warning-color);
|
||||
}
|
||||
|
||||
.colored-log-line.log-level-error {
|
||||
color: #ff6b7a;
|
||||
color: var(--td-error-color);
|
||||
}
|
||||
|
||||
.script-log {
|
||||
min-height: 320px;
|
||||
padding: 12px;
|
||||
color: #d7dde7;
|
||||
background: #111820;
|
||||
border-radius: var(--td-radius-default);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.download-complete-content {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.download-complete-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.download-path-link {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
color: var(--td-brand-color);
|
||||
font: inherit;
|
||||
font-family: var(--td-font-family-mono);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.download-path-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ==================== Settings ==================== */
|
||||
@@ -1550,7 +1611,7 @@ select {
|
||||
}
|
||||
|
||||
.script-editor-container {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
margin: 16px 16px 0;
|
||||
overflow: hidden;
|
||||
@@ -1588,17 +1649,28 @@ select {
|
||||
}
|
||||
|
||||
.script-process-section {
|
||||
flex: 0 0 auto;
|
||||
margin: 0 16px 16px;
|
||||
flex: 0 1 clamp(168px, 26vh, 280px);
|
||||
min-height: 148px;
|
||||
margin: 8px 16px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.script-process-card {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.script-process-section .card-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
|
||||
.script-log {
|
||||
height: 240px;
|
||||
min-height: 240px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* ==================== Responsive ==================== */
|
||||
|
||||
Generated
+1
@@ -237,6 +237,7 @@ dependencies = [
|
||||
name = "capacity-report-desktop"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"open",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"sysinfo",
|
||||
|
||||
@@ -9,6 +9,7 @@ edition = "2021"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
open = "5.3.5"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["stream"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sysinfo = { version = "0.38.4", default-features = false, features = ["system"] }
|
||||
|
||||
+19
-1
@@ -267,6 +267,21 @@ async fn download_to_file(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_path_in_file_manager(path: String) -> Result<(), String> {
|
||||
let target = PathBuf::from(path);
|
||||
let directory = if target.is_dir() {
|
||||
target
|
||||
} else {
|
||||
target
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.ok_or_else(|| "Unable to resolve download directory".to_string())?
|
||||
};
|
||||
|
||||
open::that(directory).map_err(|error| format!("Unable to open download directory: {error}"))
|
||||
}
|
||||
|
||||
async fn write_download(request: DownloadFileRequest, file_path: &Path) -> Result<(), String> {
|
||||
let method = reqwest::Method::from_bytes(request.method.as_bytes())
|
||||
.map_err(|error| format!("Invalid download method: {error}"))?;
|
||||
@@ -332,7 +347,10 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![download_to_file])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
download_to_file,
|
||||
open_path_in_file_manager
|
||||
])
|
||||
.manage(ServerState(Mutex::new(None)))
|
||||
.setup(|app| {
|
||||
start_server(app)?;
|
||||
|
||||
Reference in New Issue
Block a user