feat(): 优化状态栏

This commit is contained in:
tsl 2026-03-10 15:20:06 +08:00
parent da677a033e
commit 17618ec3a0
11 changed files with 135 additions and 52 deletions

View File

@ -37,7 +37,8 @@
"Bash(pnpm run tauri build)",
"Bash(start \"\" \"D:\\\\Projects\\\\tauri\\\\tauri-app\\\\src-tauri\\\\target\\\\release\\\\bundle\\\\nsis\\\\tauri-app_0.1.0_x64-setup.exe\")",
"Bash(pnpm run build)",
"Bash(pnpm add pinia)"
"Bash(pnpm add pinia)",
"Bash(pnpm add dayjs)"
]
}
}

1
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2.5.3",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.19",
"pinia": "^3.0.4",
"vue": "^3.5.29"
},

View File

@ -19,6 +19,7 @@
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2.5.3",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.19",
"pinia": "^3.0.4",
"vue": "^3.5.29"
},

View File

@ -29,6 +29,9 @@ importers:
ant-design-vue:
specifier: ^4.2.6
version: 4.2.6(vue@3.5.29(typescript@5.9.3))
dayjs:
specifier: ^1.11.19
version: 1.11.19
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.29(typescript@5.9.3))

View File

@ -2,7 +2,7 @@ use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::atomic::{AtomicU16, Ordering};
use tauri::{Emitter, Manager};
use tauri::Emitter;
use tauri_plugin_autostart::MacosLauncher;
mod notification;
@ -25,34 +25,42 @@ fn start_notification_callback_server(app_handle: tauri::AppHandle) {
});
}
fn handle_callback_request(app_handle: &tauri::AppHandle, mut stream: std::net::TcpStream) {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);
let (action, html) = if request.contains("GET /postpone") {
let _ = app_handle.emit("notification-action", "postpone_1h");
if let Some(w) = app_handle.get_webview_window("main") {
let _ = w.unminimize();
let _ = w.show();
let _ = w.set_focus();
}
("postpone", "已推迟1小时监控将在1小时后恢复")
} else {
("unknown", "未知操作")
};
log::info!("Notification callback: {}", action);
let body = format!(
fn simple_page(msg: &str) -> String {
format!(
"<!DOCTYPE html><html><head><meta charset='utf-8'></head>\
<body style='display:flex;justify-content:center;align-items:center;\
height:80vh;font-family:system-ui;color:#333'>\
<div style='text-align:center'><h2>{}</h2>\
<p style='color:#888'></p></div></body>\
<script>setTimeout(()=>window.close(),1500)</script></html>",
html
);
msg
)
}
fn redirect_page(url: &str) -> String {
format!(
"<!DOCTYPE html><html><head><meta charset='utf-8'>\
<meta http-equiv='refresh' content='0;url={url}'></head>\
<body>...</body></html>"
)
}
fn handle_callback_request(app_handle: &tauri::AppHandle, mut stream: std::net::TcpStream) {
let mut buf = [0u8; 1024];
let n = stream.read(&mut buf).unwrap_or(0);
let request = String::from_utf8_lossy(&buf[..n]);
let (action, body) = if request.contains("GET /postpone") {
let _ = app_handle.emit("notification-action", "postpone_1h");
("postpone", simple_page("已推迟1小时监控将在1小时后恢复"))
} else if request.contains("GET /handle") {
let _ = app_handle.emit("notification-action", "go_handle");
("handle", redirect_page("https://crm.yunvip123.com/index.html"))
} else {
("unknown", simple_page("未知操作"))
};
log::info!("Notification callback: {}", action);
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",

View File

@ -37,7 +37,7 @@ pub fn send_interactive_toast(
</visual>
<actions>
<action content="推迟1小时" arguments="http://127.0.0.1:{port}/postpone" activationType="protocol"/>
<action content="去处理" arguments="https://crm.yunvip123.com/index.html" activationType="protocol"/>
<action content="去处理" arguments="http://127.0.0.1:{port}/handle" activationType="protocol"/>
</actions>
</toast>"#,
port = port,

View File

@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "工单系统监测",
"version": "0.1.1",
"version": "0.1.2",
"identifier": "com.administrator.tauri-app",
"build": {
"beforeDevCommand": "pnpm dev",

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from "vue";
import { ref, computed, onMounted, onUnmounted } from "vue";
import {
SettingOutlined,
DashboardOutlined,
@ -12,13 +12,23 @@ import LoginForm from "./components/LoginForm.vue";
import MonitorControl from "./components/MonitorControl.vue";
import SettingsPanel from "./components/SettingsPanel.vue";
import LogViewer from "./components/LogViewer.vue";
import AppStatusBar from "./components/AppStatusBar.vue";
import AppStatusBar, { type StatusBarItem } from "./components/AppStatusBar.vue";
import CloseDialog from "./components/CloseDialog.vue";
import { useMonitorStore } from "./stores/monitor";
const activeTab = ref("monitor");
const store = useMonitorStore();
const statusItems = computed<StatusBarItem[]>(() => {
const items: StatusBarItem[] = [];
if (store.isPostponed) {
items.push({ key: "postponed", type: "warning", content: store.message });
} else if (store.message) {
items.push({ key: "message", type: "info", content: store.message });
}
return items;
});
onMounted(() => store.initialize());
onUnmounted(() => store.cleanup());
</script>
@ -61,7 +71,7 @@ onUnmounted(() => store.cleanup());
</a-tabs>
</a-layout-content>
<AppStatusBar />
<AppStatusBar :items="statusItems" />
</a-layout>
<CloseDialog />

View File

@ -1,22 +1,43 @@
<script setup lang="ts">
import {
ExclamationCircleOutlined,
InfoCircleOutlined,
ExclamationCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
} from "@ant-design/icons-vue";
import { useMonitorStore } from "../stores/monitor";
const store = useMonitorStore();
export interface StatusBarItem {
key: string;
content: string;
type?: "info" | "warning" | "success" | "error";
}
defineProps<{ items?: StatusBarItem[] }>();
const colorMap: Record<string, string> = {
info: "rgba(255,255,255,0.85)",
warning: "#ffe58f",
success: "#95de64",
error: "#ff7875",
};
const iconMap: Record<string, unknown> = {
info: InfoCircleOutlined,
warning: ExclamationCircleOutlined,
success: CheckCircleOutlined,
error: CloseCircleOutlined,
};
</script>
<template>
<div class="app-statusbar">
<span v-if="store.isPostponed" class="statusbar-item statusbar-item--warning">
<ExclamationCircleOutlined /> 监控已推迟1小时后自动恢复
</span>
<span v-if="store.isPostponed && store.message" class="statusbar-sep" />
<span v-if="store.message" class="statusbar-item statusbar-item--info">
<InfoCircleOutlined /> {{ store.message }}
<template v-for="(item, index) in items" :key="item.key">
<span v-if="index > 0" class="statusbar-sep" />
<span class="statusbar-item" :style="{ color: colorMap[item.type ?? 'info'] }">
<component :is="iconMap[item.type ?? 'info']" />
{{ item.content }}
</span>
</template>
</div>
</template>
@ -38,12 +59,6 @@ const store = useMonitorStore();
padding: 0 10px;
height: 100%;
}
.statusbar-item--warning {
color: #ffe58f;
}
.statusbar-item--info {
color: rgba(255, 255, 255, 0.85);
}
.statusbar-sep {
width: 1px;
height: 14px;

View File

@ -1,18 +1,33 @@
<script setup lang="ts">
import { computed } from "vue";
import dayjs, { type Dayjs } from "dayjs";
import { useMonitorStore } from "../stores/monitor";
const store = useMonitorStore();
// ===== TimePicker Dayjs =====
const intervalTime = computed<Dayjs>({
get() {
const t = store.config.check_interval;
return dayjs().hour(Math.floor(t / 3600)).minute(Math.floor((t % 3600) / 60)).second(t % 60).millisecond(0);
},
set(val: Dayjs | null) {
if (!val) return;
store.config.check_interval = Math.max(10, val.hour() * 3600 + val.minute() * 60 + val.second());
},
});
</script>
<template>
<a-form layout="vertical">
<a-form-item label="检查间隔(秒)">
<a-input-number
v-model:value="store.config.check_interval"
:min="10"
:max="3600"
<a-form-item label="检查间隔">
<a-time-picker
v-model:value="intervalTime"
format="HH:mm:ss"
:allow-clear="false"
style="width: 100%"
/>
<div class="form-hint">最短 10 当前 {{ store.config.check_interval }} </div>
</a-form-item>
<a-form-item label="网络超时(秒)">
<a-input-number
@ -50,6 +65,14 @@ const store = useMonitorStore();
</a-form-item>
</a-col>
</a-row>
<a-form-item label="点击「去处理」后暂停时长(分钟)">
<a-input-number
v-model:value="store.config.handle_pause_minutes"
:min="1"
:max="120"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="关闭窗口行为">
<a-radio-group v-model:value="store.config.close_action">
<a-radio value="ask">每次询问</a-radio>

View File

@ -68,6 +68,7 @@ export const useMonitorStore = defineStore("monitor", () => {
notification_sound: true,
network_timeout: 30,
close_action: "ask" as "ask" | "minimize" | "close",
handle_pause_minutes: 20,
});
const showCloseDialog = ref(false);
@ -233,8 +234,9 @@ export const useMonitorStore = defineStore("monitor", () => {
async function tryReLogin(): Promise<void> {
if (loginMode.value === "token") {
addLog("WARN", "Token 模式下无法自动重登录,请更换新的 Token", "SESSION");
message.value = "Token 可能已过期,请更换新的 Token";
addLog("ERROR", "Token 已失效,监控已停止,请更换新的 Token 后重新开始", "SESSION");
message.value = "Token 已失效,监控已停止";
stopMonitoring();
return;
}
if (!username.value || !password.value) {
@ -453,6 +455,24 @@ export const useMonitorStore = defineStore("monitor", () => {
}, 60 * 60 * 1000);
}
function pauseForHandle() {
if (!isMonitoring.value) return;
if (monitorTimer) { clearInterval(monitorTimer); monitorTimer = null; }
if (postponeTimer) { clearTimeout(postponeTimer); }
isPostponed.value = true;
const minutes = config.value.handle_pause_minutes;
addLog("INFO", `点击"去处理",监控已暂停 ${minutes} 分钟`, "MONITOR");
message.value = `监控已暂停 ${minutes} 分钟,将在 ${minutes} 分钟后自动恢复`;
postponeTimer = setTimeout(() => {
isPostponed.value = false;
postponeTimer = null;
apiCheckStatus();
monitorTimer = setInterval(() => apiCheckStatus(), config.value.check_interval * 1000);
addLog("INFO", "暂停结束,监控已自动恢复", "MONITOR");
message.value = "暂停结束,监控已自动恢复";
}, minutes * 60 * 1000);
}
async function manualCheck() {
if (!isLoggedIn.value) { message.value = "请先开始监控(登录)"; return; }
isLoading.value = true;
@ -564,6 +584,7 @@ export const useMonitorStore = defineStore("monitor", () => {
} catch { /* ignore */ }
unlistenNotification = await listen<string>("notification-action", (event) => {
if (event.payload === "postpone_1h") postponeMonitoring();
else if (event.payload === "go_handle") pauseForHandle();
});
unlistenClose = await listen<void>("close-requested", async () => {
const action = config.value.close_action;