feat(): 优化状态栏
This commit is contained in:
parent
da677a033e
commit
17618ec3a0
|
|
@ -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)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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{}",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
16
src/App.vue
16
src/App.vue
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
</span>
|
||||
<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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue