refactor: ♻️ 优化存储
This commit is contained in:
parent
8137d414da
commit
13f92f79b6
|
|
@ -3,7 +3,8 @@ fetch("https://crm.yunvip123.com/api/DemandManage/GetWorkOrderListPage", {
|
||||||
accept: "application/json, text/javascript, */*; q=0.01",
|
accept: "application/json, text/javascript, */*; q=0.01",
|
||||||
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||||
"sec-ch-ua": '"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
"sec-ch-ua":
|
||||||
|
'"Not:A-Brand";v="99", "Google Chrome";v="145", "Chromium";v="145"',
|
||||||
"sec-ch-ua-mobile": "?0",
|
"sec-ch-ua-mobile": "?0",
|
||||||
"sec-ch-ua-platform": '"Windows"',
|
"sec-ch-ua-platform": '"Windows"',
|
||||||
"sec-fetch-dest": "empty",
|
"sec-fetch-dest": "empty",
|
||||||
|
|
@ -11,7 +12,8 @@ fetch("https://crm.yunvip123.com/api/DemandManage/GetWorkOrderListPage", {
|
||||||
"sec-fetch-site": "same-origin",
|
"sec-fetch-site": "same-origin",
|
||||||
"x-requested-with": "XMLHttpRequest",
|
"x-requested-with": "XMLHttpRequest",
|
||||||
},
|
},
|
||||||
referrer: "https://crm.yunvip123.com/WebUI/DemandManagement/DemandListNew.html?v=2.0.4.12",
|
referrer:
|
||||||
|
"https://crm.yunvip123.com/WebUI/DemandManagement/DemandListNew.html?v=2.0.4.12",
|
||||||
body: "DL_ProductID=&DL_ID=&isSelect=8&Solution=&OrderType=&DL_TaskName=&CreatGID=&CreatTimeStart=&CreatTimeEnd=&SloveGID=&SloveTimeStart=&SloveTimeEnd=&ShowTable=DemandList&isShow=3&IsExport=0&PageIndex=1&PageSize=20",
|
body: "DL_ProductID=&DL_ID=&isSelect=8&Solution=&OrderType=&DL_TaskName=&CreatGID=&CreatTimeStart=&CreatTimeEnd=&SloveGID=&SloveTimeStart=&SloveTimeEnd=&ShowTable=DemandList&isShow=3&IsExport=0&PageIndex=1&PageSize=20",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
mode: "cors",
|
mode: "cors",
|
||||||
|
|
@ -6,7 +6,8 @@
|
||||||
* 所有接口都要带 cookie: ASP.NET_SessionId=td0mirxws4klqpm0bydhvgjj
|
* 所有接口都要带 cookie: ASP.NET_SessionId=td0mirxws4klqpm0bydhvgjj
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const requestData = "UserAcount=&PassWord=123456&VerifyCode=&Account=18688886666_777&ValidateCode=";
|
const requestData =
|
||||||
|
"UserAcount=&PassWord=123456&VerifyCode=&Account=18688886666_777&ValidateCode=";
|
||||||
|
|
||||||
const responseBody = {
|
const responseBody = {
|
||||||
success: true,
|
success: true,
|
||||||
45
src/App.vue
45
src/App.vue
|
|
@ -14,11 +14,13 @@ import MonitorControl from "@/components/MonitorControl.vue";
|
||||||
import SettingsPanel from "@/components/SettingsPanel.vue";
|
import SettingsPanel from "@/components/SettingsPanel.vue";
|
||||||
import StatusHeader from "@/components/StatusHeader.vue";
|
import StatusHeader from "@/components/StatusHeader.vue";
|
||||||
import UpdateDialog from "@/components/UpdateDialog.vue";
|
import UpdateDialog from "@/components/UpdateDialog.vue";
|
||||||
|
import { useTray } from "@/composables/useTray";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useLogStore } from "@/stores/log";
|
import { useLogStore } from "@/stores/log";
|
||||||
import { useMonitorStore } from "@/stores/monitor";
|
import { useMonitorStore } from "@/stores/monitor";
|
||||||
import { useNetworkStore } from "@/stores/network";
|
import { useNetworkStore } from "@/stores/network";
|
||||||
import { useUpdaterStore } from "@/stores/updater";
|
import { useUpdaterStore } from "@/stores/updater";
|
||||||
|
import { migrateFromLocalStorage } from "@/utils/storage";
|
||||||
|
|
||||||
const activeTab = ref("monitor");
|
const activeTab = ref("monitor");
|
||||||
const store = useMonitorStore();
|
const store = useMonitorStore();
|
||||||
|
|
@ -26,6 +28,8 @@ const appStore = useAppStore();
|
||||||
const logStore = useLogStore();
|
const logStore = useLogStore();
|
||||||
const updater = useUpdaterStore();
|
const updater = useUpdaterStore();
|
||||||
const networkStore = useNetworkStore();
|
const networkStore = useNetworkStore();
|
||||||
|
const { setupTray } = useTray();
|
||||||
|
const closeDialogRef = ref<InstanceType<typeof CloseDialog> | null>(null);
|
||||||
|
|
||||||
/** 通知动作事件的取消监听函数 */
|
/** 通知动作事件的取消监听函数 */
|
||||||
let unlistenNotification: UnlistenFn | null = null;
|
let unlistenNotification: UnlistenFn | null = null;
|
||||||
|
|
@ -43,18 +47,19 @@ const statusItems = computed<StatusBarItem[]>(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
appStore.loadConfig();
|
const migrated = await migrateFromLocalStorage();
|
||||||
store.loadCredentials();
|
if (migrated) {
|
||||||
await store.setupTray();
|
logStore.addLog("INFO", "已将本地 localStorage 配置迁移到文件存储", "CONFIG");
|
||||||
|
|
||||||
try {
|
|
||||||
const enabled = await isAutostartEnabled();
|
|
||||||
appStore.autoStartEnabled = enabled;
|
|
||||||
appStore.config.auto_start = enabled;
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await appStore.loadConfig();
|
||||||
|
await store.loadCredentials();
|
||||||
|
await setupTray();
|
||||||
|
|
||||||
|
const enabled = await isAutostartEnabled();
|
||||||
|
appStore.autoStartEnabled = enabled;
|
||||||
|
appStore.config.auto_start = enabled;
|
||||||
|
|
||||||
unlistenNotification = await listen<string>("notification-action", (event) => {
|
unlistenNotification = await listen<string>("notification-action", (event) => {
|
||||||
if (event.payload === "postpone_1h") store.postponeMonitoring();
|
if (event.payload === "postpone_1h") store.postponeMonitoring();
|
||||||
else if (event.payload === "go_handle") store.pauseForHandle();
|
else if (event.payload === "go_handle") store.pauseForHandle();
|
||||||
|
|
@ -67,30 +72,24 @@ onMounted(async () => {
|
||||||
} else if (action === "close") {
|
} else if (action === "close") {
|
||||||
await getCurrentWindow().destroy();
|
await getCurrentWindow().destroy();
|
||||||
} else {
|
} else {
|
||||||
appStore.showCloseDialog = true;
|
closeDialogRef.value?.openDialog();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
logStore.addLog("INFO", "应用程序启动完成", "SYSTEM");
|
logStore.addLog("INFO", "应用程序启动完成", "SYSTEM");
|
||||||
|
|
||||||
if (appStore.config.silent_start) {
|
if (appStore.config.silent_start) {
|
||||||
try {
|
await getCurrentWindow().hide();
|
||||||
await getCurrentWindow().hide();
|
logStore.addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
|
||||||
logStore.addLog("INFO", "静默启动:窗口已隐藏到托盘", "SYSTEM");
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
getCurrentWindow().setTitle(import.meta.env.VITE_APP_NAME);
|
getCurrentWindow().setTitle(import.meta.env.VITE_APP_NAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion()
|
getVersion().then((v) => {
|
||||||
.then((v) => {
|
updater.currentVersion = v;
|
||||||
updater.currentVersion = v;
|
});
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
|
|
||||||
networkStore.checkNetworkStatus();
|
networkStore.checkNetworkStatus();
|
||||||
updater.checkForUpdates();
|
updater.checkForUpdates();
|
||||||
|
|
@ -152,7 +151,7 @@ onUnmounted(() => {
|
||||||
<AppStatusBar :items="statusItems" />
|
<AppStatusBar :items="statusItems" />
|
||||||
</a-layout>
|
</a-layout>
|
||||||
|
|
||||||
<CloseDialog />
|
<CloseDialog ref="closeDialogRef" />
|
||||||
<UpdateDialog />
|
<UpdateDialog />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
// 使用示例:展示新的泛型 request 函数和 HTTP 方法辅助函数如何使用
|
||||||
|
|
||||||
|
import {
|
||||||
|
request,
|
||||||
|
get,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
del,
|
||||||
|
patch,
|
||||||
|
type RequestOptions,
|
||||||
|
} from "@/api/client";
|
||||||
|
import type { ApiResponse, LoginResponseData } from "@/api/types";
|
||||||
|
|
||||||
|
// 示例1:GET 请求
|
||||||
|
interface UserProfile {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getUserProfile(userId: number) {
|
||||||
|
// 只需要指定响应类型
|
||||||
|
const response = await request<UserProfile>(`/users/${userId}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例2:POST 请求,带请求数据和响应类型
|
||||||
|
interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(username: string, password: string) {
|
||||||
|
// 指定响应类型和请求类型
|
||||||
|
const response = await request<ApiResponse<LoginResponseData>, LoginRequest>(
|
||||||
|
"/login",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
data: { username, password },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例3:PUT 请求
|
||||||
|
interface UpdateUserRequest {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UpdateUserResponse {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUser(userId: number, name: string, email: string) {
|
||||||
|
const response = await request<UpdateUserResponse, UpdateUserRequest>(
|
||||||
|
`/users/${userId}`,
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
data: { name, email },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例4:不指定类型(使用默认 any)
|
||||||
|
async function genericRequest() {
|
||||||
|
const response = await request("/some-endpoint");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例5:使用 RequestOptions 类型定义
|
||||||
|
interface SearchParams {
|
||||||
|
keyword: string;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchResult {
|
||||||
|
items: any[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createSearchOptions(
|
||||||
|
keyword: string,
|
||||||
|
page: number,
|
||||||
|
limit: number,
|
||||||
|
): RequestOptions<SearchParams> {
|
||||||
|
return {
|
||||||
|
method: "POST",
|
||||||
|
data: { keyword, page, limit },
|
||||||
|
headers: {
|
||||||
|
"Custom-Header": "search-request",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchUsers(keyword: string, page: number, limit: number) {
|
||||||
|
const options: RequestOptions<SearchParams> = createSearchOptions(
|
||||||
|
keyword,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
);
|
||||||
|
const response = await request<SearchResult, SearchParams>(
|
||||||
|
"/search",
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 示例6:使用 HTTP 方法辅助函数
|
||||||
|
|
||||||
|
async function getUserById(userId: number) {
|
||||||
|
// 使用 get 辅助函数
|
||||||
|
const user = await get<UserProfile>(`/users/${userId}`);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUser(name: string, email: string) {
|
||||||
|
// 使用 post 辅助函数
|
||||||
|
const newUser = await post<UserProfile, { name: string; email: string }>(
|
||||||
|
"/users",
|
||||||
|
{ name, email },
|
||||||
|
);
|
||||||
|
return newUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateUserProfile(
|
||||||
|
userId: number,
|
||||||
|
updates: Partial<UserProfile>,
|
||||||
|
) {
|
||||||
|
// 使用 put 辅助函数
|
||||||
|
const updatedUser = await put<UserProfile, Partial<UserProfile>>(
|
||||||
|
`/users/${userId}`,
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteUser(userId: number) {
|
||||||
|
// 使用 del 辅助函数
|
||||||
|
const result = await del<{ success: boolean }>(`/users/${userId}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function partialUpdateUser(
|
||||||
|
userId: number,
|
||||||
|
updates: Partial<UserProfile>,
|
||||||
|
) {
|
||||||
|
// 使用 patch 辅助函数
|
||||||
|
const updatedUser = await patch<UserProfile, Partial<UserProfile>>(
|
||||||
|
`/users/${userId}`,
|
||||||
|
updates,
|
||||||
|
);
|
||||||
|
return updatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出函数以避免未使用变量的警告
|
||||||
|
export {
|
||||||
|
getUserProfile,
|
||||||
|
login,
|
||||||
|
updateUser,
|
||||||
|
genericRequest,
|
||||||
|
searchUsers,
|
||||||
|
getUserById,
|
||||||
|
createUser,
|
||||||
|
updateUserProfile,
|
||||||
|
deleteUser,
|
||||||
|
partialUpdateUser,
|
||||||
|
};
|
||||||
|
|
@ -1,18 +1,49 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
import { useLogStore } from "@/stores/log";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const { addLog } = useLogStore();
|
||||||
|
const isOpen = ref(false);
|
||||||
const rememberClose = ref(false);
|
const rememberClose = ref(false);
|
||||||
|
|
||||||
function handleClose(action: "minimize" | "close") {
|
function openDialog() {
|
||||||
appStore.handleCloseAction(action, rememberClose.value);
|
isOpen.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDialog() {
|
||||||
|
isOpen.value = false;
|
||||||
rememberClose.value = false;
|
rememberClose.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openDialog,
|
||||||
|
closeDialog,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleClose(action: "minimize" | "close") {
|
||||||
|
isOpen.value = false;
|
||||||
|
try {
|
||||||
|
if (rememberClose.value) {
|
||||||
|
appStore.config.close_action = action;
|
||||||
|
await appStore.saveConfigToStorage();
|
||||||
|
addLog("INFO", `关闭行为已设为: ${action === "minimize" ? "最小化到托盘" : "直接退出"}`, "SYSTEM");
|
||||||
|
}
|
||||||
|
if (action === "minimize") {
|
||||||
|
await getCurrentWindow().hide();
|
||||||
|
} else {
|
||||||
|
await getCurrentWindow().destroy();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
rememberClose.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-modal v-model:open="appStore.showCloseDialog" title="关闭窗口" :width="340" centered>
|
<a-modal v-model:open="isOpen" title="关闭窗口" :width="340" centered @cancel="closeDialog">
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<p class="dialog-tip">请选择关闭窗口时的操作:</p>
|
<p class="dialog-tip">请选择关闭窗口时的操作:</p>
|
||||||
<a-checkbox v-model:checked="rememberClose"> 记住我的选择 </a-checkbox>
|
<a-checkbox v-model:checked="rememberClose"> 记住我的选择 </a-checkbox>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useMonitorStore } from "@/stores/monitor";
|
import { useMonitorStore } from "@/stores/monitor";
|
||||||
|
|
||||||
const store = useMonitorStore();
|
const store = useMonitorStore();
|
||||||
|
const { loginMode, isMonitoring, username, password, tokenSessionId, rememberPassword } = storeToRefs(store);
|
||||||
/** 表单操作加载状态 */
|
/** 表单操作加载状态 */
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
|
@ -20,7 +22,7 @@ async function handleStartMonitoring() {
|
||||||
|
|
||||||
async function handleStopMonitoring() {
|
async function handleStopMonitoring() {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
await store.stopMonitoring();
|
store.stopMonitoring();
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -28,25 +30,25 @@ async function handleStopMonitoring() {
|
||||||
<template>
|
<template>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-radio-group v-model:value="store.loginMode" :disabled="store.isMonitoring">
|
<a-radio-group v-model:value="loginMode" :disabled="isMonitoring">
|
||||||
<a-radio-button value="password"> 账号密码登录 </a-radio-button>
|
<a-radio-button value="password"> 账号密码登录 </a-radio-button>
|
||||||
<a-radio-button value="token"> Token 登录 </a-radio-button>
|
<a-radio-button value="token"> Token 登录 </a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<template v-if="store.loginMode === 'password'">
|
<template v-if="loginMode === 'password'">
|
||||||
<a-form-item label="用户名">
|
<a-form-item label="用户名">
|
||||||
<a-input v-model:value="store.username" placeholder="请输入用户名" :disabled="isLoading" />
|
<a-input v-model:value="username" placeholder="请输入用户名" :disabled="isLoading" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="密码">
|
<a-form-item label="密码">
|
||||||
<a-input-password v-model:value="store.password" placeholder="请输入密码" :disabled="isLoading" />
|
<a-input-password v-model:value="password" placeholder="请输入密码" :disabled="isLoading" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a-form-item label="ASP.NET_SessionId">
|
<a-form-item label="ASP.NET_SessionId">
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="store.tokenSessionId"
|
v-model:value="tokenSessionId"
|
||||||
placeholder="请输入 ASP.NET_SessionId"
|
placeholder="请输入 ASP.NET_SessionId"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
allow-clear
|
allow-clear
|
||||||
|
|
@ -58,8 +60,8 @@ async function handleStopMonitoring() {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-checkbox v-model:checked="store.rememberPassword">
|
<a-checkbox v-model:checked="rememberPassword">
|
||||||
记住{{ store.loginMode === "password" ? "密码" : "Token" }}
|
记住{{ loginMode === "password" ? "密码" : "Token" }}
|
||||||
</a-checkbox>
|
</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
|
|
@ -68,15 +70,13 @@ async function handleStopMonitoring() {
|
||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:disabled="store.isMonitoring"
|
:disabled="isMonitoring"
|
||||||
style="background: #52c41a; border-color: #52c41a"
|
style="background: #52c41a; border-color: #52c41a"
|
||||||
@click="handleStartMonitoring()"
|
@click="handleStartMonitoring()"
|
||||||
>
|
>
|
||||||
开始监测
|
开始监测
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button danger :disabled="isLoading || !store.isMonitoring" @click="handleStopMonitoring()">
|
<a-button danger :disabled="isLoading || !isMonitoring" @click="handleStopMonitoring()"> 停止监测 </a-button>
|
||||||
停止监测
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import duration from "dayjs/plugin/duration";
|
import duration from "dayjs/plugin/duration";
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
import { useTray } from "@/composables/useTray";
|
||||||
import { useMonitorStore } from "@/stores/monitor";
|
import { useMonitorStore } from "@/stores/monitor";
|
||||||
import { useNetworkStore } from "@/stores/network";
|
import { useNetworkStore } from "@/stores/network";
|
||||||
|
|
||||||
|
|
@ -16,6 +17,7 @@ dayjs.extend(duration);
|
||||||
|
|
||||||
const store = useMonitorStore();
|
const store = useMonitorStore();
|
||||||
const networkStore = useNetworkStore();
|
const networkStore = useNetworkStore();
|
||||||
|
const { minimizeToTray } = useTray();
|
||||||
/** 手动检查加载状态 */
|
/** 手动检查加载状态 */
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
|
@ -25,6 +27,10 @@ async function handleManualCheck() {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleMinimizeToTray() {
|
||||||
|
await minimizeToTray();
|
||||||
|
}
|
||||||
|
|
||||||
const countdown = ref("");
|
const countdown = ref("");
|
||||||
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
|
@ -113,7 +119,7 @@ onUnmounted(() => {
|
||||||
</template>
|
</template>
|
||||||
测试通知
|
测试通知
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="store.minimizeToTray()">
|
<a-button @click="handleMinimizeToTray()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<MinusCircleOutlined />
|
<MinusCircleOutlined />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,19 @@
|
||||||
import { CloudSyncOutlined } from "@ant-design/icons-vue";
|
import { CloudSyncOutlined } from "@ant-design/icons-vue";
|
||||||
import dayjs, { type Dayjs } from "dayjs";
|
import dayjs, { type Dayjs } from "dayjs";
|
||||||
import { debounce } from "lodash-es";
|
import { debounce } from "lodash-es";
|
||||||
import { computed, watch } from "vue";
|
import { storeToRefs } from "pinia";
|
||||||
|
import { computed, onBeforeUnmount, watch } from "vue";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useUpdaterStore } from "@/stores/updater";
|
import { useUpdaterStore } from "@/stores/updater";
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
const { config } = storeToRefs(appStore);
|
||||||
const { currentVersion, isCheckingUpdates, isUpdating, checkForUpdates } = useUpdaterStore();
|
const { currentVersion, isCheckingUpdates, isUpdating, checkForUpdates } = useUpdaterStore();
|
||||||
|
|
||||||
// ===== 检查间隔:内部用秒,TimePicker 用 Dayjs =====
|
// ===== 检查间隔:内部用秒,TimePicker 用 Dayjs =====
|
||||||
const intervalTime = computed<Dayjs>({
|
const intervalTime = computed<Dayjs>({
|
||||||
get() {
|
get() {
|
||||||
const t = appStore.config.check_interval;
|
const t = config.value.check_interval;
|
||||||
const hours = Math.floor(t / 3600);
|
const hours = Math.floor(t / 3600);
|
||||||
const minutes = Math.floor((t % 3600) / 60);
|
const minutes = Math.floor((t % 3600) / 60);
|
||||||
const seconds = t % 60;
|
const seconds = t % 60;
|
||||||
|
|
@ -21,59 +23,64 @@ const intervalTime = computed<Dayjs>({
|
||||||
set(val: Dayjs | null) {
|
set(val: Dayjs | null) {
|
||||||
if (!val) return;
|
if (!val) return;
|
||||||
const totalSeconds = val.hour() * 3600 + val.minute() * 60 + val.second();
|
const totalSeconds = val.hour() * 3600 + val.minute() * 60 + val.second();
|
||||||
appStore.config.check_interval = Math.max(10, totalSeconds);
|
config.value.check_interval = Math.max(10, totalSeconds);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 自动保存:config 变化后防抖 500ms 触发保存
|
// 自动保存:config 变化后防抖 500ms 触发保存(写入文件存储)
|
||||||
const saveConfig = debounce(() => appStore.updateConfig(), 500);
|
const saveConfig = debounce(() => appStore.updateConfig(), 500);
|
||||||
watch(() => appStore.config, saveConfig, { deep: true });
|
watch(config, saveConfig, { deep: true });
|
||||||
|
|
||||||
|
// 离开页面时立即执行未触发的防抖保存
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
saveConfig.flush();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="检查间隔">
|
<a-form-item label="检查间隔">
|
||||||
<a-time-picker v-model:value="intervalTime" format="HH:mm:ss" :allow-clear="false" style="width: 100%" />
|
<a-time-picker v-model:value="intervalTime" format="HH:mm:ss" :allow-clear="false" style="width: 100%" />
|
||||||
<div class="form-hint">最短 10 秒,当前 {{ appStore.config.check_interval }} 秒</div>
|
<div class="form-hint">最短 10 秒,当前 {{ config.check_interval }} 秒</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="网络超时(秒)">
|
<a-form-item label="网络超时(秒)">
|
||||||
<a-input-number v-model:value="appStore.config.network_timeout" :min="5" :max="120" style="width: 100%" />
|
<a-input-number v-model:value="config.network_timeout" :min="5" :max="120" style="width: 100%" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider />
|
<a-divider />
|
||||||
<div class="switch-row">
|
<div class="switch-row">
|
||||||
<div class="switch-label">开机自启动</div>
|
<div class="switch-label">开机自启动</div>
|
||||||
<a-switch v-model:checked="appStore.config.auto_start" />
|
<a-switch v-model:checked="config.auto_start" />
|
||||||
</div>
|
</div>
|
||||||
<div class="switch-row">
|
<div class="switch-row">
|
||||||
<div class="switch-label">
|
<div class="switch-label">
|
||||||
启动后自动监测
|
启动后自动监测
|
||||||
<div class="form-hint">需同时开启「记住密码」</div>
|
<div class="form-hint">需同时开启「记住密码」</div>
|
||||||
</div>
|
</div>
|
||||||
<a-switch v-model:checked="appStore.config.auto_monitor" />
|
<a-switch v-model:checked="config.auto_monitor" />
|
||||||
</div>
|
</div>
|
||||||
<div class="switch-row">
|
<div class="switch-row">
|
||||||
<div class="switch-label">
|
<div class="switch-label">
|
||||||
静默启动
|
静默启动
|
||||||
<div class="form-hint">启动时直接最小化到托盘</div>
|
<div class="form-hint">启动时直接最小化到托盘</div>
|
||||||
</div>
|
</div>
|
||||||
<a-switch v-model:checked="appStore.config.silent_start" />
|
<a-switch v-model:checked="config.silent_start" />
|
||||||
</div>
|
</div>
|
||||||
<a-divider />
|
<a-divider />
|
||||||
<div class="switch-row">
|
<div class="switch-row">
|
||||||
<div class="switch-label">显示系统通知</div>
|
<div class="switch-label">显示系统通知</div>
|
||||||
<a-switch v-model:checked="appStore.config.show_notifications" />
|
<a-switch v-model:checked="config.show_notifications" />
|
||||||
</div>
|
</div>
|
||||||
<div class="switch-row">
|
<div class="switch-row">
|
||||||
<div class="switch-label">通知提示音</div>
|
<div class="switch-label">通知提示音</div>
|
||||||
<a-switch v-model:checked="appStore.config.notification_sound" :disabled="!appStore.config.show_notifications" />
|
<a-switch v-model:checked="config.notification_sound" :disabled="!config.show_notifications" />
|
||||||
</div>
|
</div>
|
||||||
<a-divider />
|
<a-divider />
|
||||||
<a-form-item label="点击「去处理」后暂停时长(分钟)">
|
<a-form-item label="点击「去处理」后暂停时长(分钟)">
|
||||||
<a-input-number v-model:value="appStore.config.handle_pause_minutes" :min="1" :max="120" style="width: 100%" />
|
<a-input-number v-model:value="config.handle_pause_minutes" :min="1" :max="120" style="width: 100%" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider />
|
<a-divider />
|
||||||
<a-form-item label="关闭窗口行为">
|
<a-form-item label="关闭窗口行为">
|
||||||
<a-radio-group v-model:value="appStore.config.close_action">
|
<a-radio-group v-model:value="config.close_action">
|
||||||
<a-radio value="ask"> 每次询问 </a-radio>
|
<a-radio value="ask"> 每次询问 </a-radio>
|
||||||
<a-radio value="minimize"> 最小化到托盘 </a-radio>
|
<a-radio value="minimize"> 最小化到托盘 </a-radio>
|
||||||
<a-radio value="close"> 直接退出 </a-radio>
|
<a-radio value="close"> 直接退出 </a-radio>
|
||||||
|
|
|
||||||
|
|
@ -13,27 +13,26 @@ import { Menu, MenuItem } from "@tauri-apps/api/menu";
|
||||||
import { TrayIcon } from "@tauri-apps/api/tray";
|
import { TrayIcon } from "@tauri-apps/api/tray";
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
import { type Ref } from "vue";
|
import { storeToRefs } from "pinia";
|
||||||
import { type TicketCounts } from "@/api/types";
|
import { watch } from "vue";
|
||||||
import { CRM_URL } from "@/constants/app";
|
import { CRM_URL } from "@/constants/app";
|
||||||
import { useLogStore } from "@/stores/log";
|
import { useLogStore } from "@/stores/log";
|
||||||
|
import { useMonitorStore } from "@/stores/monitor";
|
||||||
import { getErrorMessage } from "@/utils/common";
|
import { getErrorMessage } from "@/utils/common";
|
||||||
|
|
||||||
|
/** 隐藏主窗口(最小化到系统托盘)。 */
|
||||||
|
async function minimizeToTray() {
|
||||||
|
await getCurrentWindow().hide();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param isMonitoring 是否正在监测
|
* 状态与动作直接从 monitor store 获取。
|
||||||
* @param isPostponed 是否已推迟/暂停
|
|
||||||
* @param ticketCounts 当前工单计数
|
|
||||||
* @param startMonitoring 开始监测(托盘菜单调用)
|
|
||||||
* @param stopMonitoring 停止监测(托盘菜单调用)
|
|
||||||
*/
|
*/
|
||||||
export function useTray(
|
export function useTray() {
|
||||||
isMonitoring: Ref<boolean>,
|
|
||||||
isPostponed: Ref<boolean>,
|
|
||||||
ticketCounts: Ref<TicketCounts>,
|
|
||||||
startMonitoring: () => Promise<void>,
|
|
||||||
stopMonitoring: () => void,
|
|
||||||
) {
|
|
||||||
const { addLog } = useLogStore();
|
const { addLog } = useLogStore();
|
||||||
|
const monitorStore = useMonitorStore();
|
||||||
|
const { isMonitoring, isPostponed, ticketCounts } = storeToRefs(monitorStore);
|
||||||
|
const { startMonitoring, stopMonitoring } = monitorStore;
|
||||||
const appName = import.meta.env.VITE_APP_NAME;
|
const appName = import.meta.env.VITE_APP_NAME;
|
||||||
// 当前托盘图标实例,null 表示尚未初始化
|
// 当前托盘图标实例,null 表示尚未初始化
|
||||||
let trayIconInstance: TrayIcon | null = null;
|
let trayIconInstance: TrayIcon | null = null;
|
||||||
|
|
@ -46,7 +45,7 @@ export function useTray(
|
||||||
async function loadSvgImage(): Promise<HTMLImageElement> {
|
async function loadSvgImage(): Promise<HTMLImageElement> {
|
||||||
if (cachedSvgImg) return cachedSvgImg;
|
if (cachedSvgImg) return cachedSvgImg;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const img = new window.Image();
|
const img = new globalThis.Image();
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
cachedSvgImg = img;
|
cachedSvgImg = img;
|
||||||
resolve(img);
|
resolve(img);
|
||||||
|
|
@ -234,6 +233,15 @@ export function useTray(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[isMonitoring, isPostponed, ticketCounts],
|
||||||
|
() => {
|
||||||
|
updateTrayIcon();
|
||||||
|
updateTrayMenu();
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化系统托盘:创建图标(初始灰色)、注册右键菜单、绑定左键单击事件。
|
* 初始化系统托盘:创建图标(初始灰色)、注册右键菜单、绑定左键单击事件。
|
||||||
* 应在应用启动时调用一次。
|
* 应在应用启动时调用一次。
|
||||||
|
|
@ -270,10 +278,5 @@ export function useTray(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 隐藏主窗口(最小化到系统托盘)。 */
|
|
||||||
async function minimizeToTray() {
|
|
||||||
await getCurrentWindow().hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray };
|
return { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const STORE_FILE_NAME = "config.json";
|
||||||
|
|
||||||
|
export const APP_CONFIG_STORAGE_KEY = "crm_config";
|
||||||
|
export const CREDENTIALS_STORAGE_KEY = "crm_credentials";
|
||||||
|
|
@ -1,29 +1,43 @@
|
||||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
|
||||||
import {
|
import {
|
||||||
enable as enableAutostart,
|
enable as enableAutostart,
|
||||||
disable as disableAutostart,
|
disable as disableAutostart,
|
||||||
} from "@tauri-apps/plugin-autostart";
|
} from "@tauri-apps/plugin-autostart";
|
||||||
import { defineStore, acceptHMRUpdate } from "pinia";
|
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
|
import { APP_CONFIG_STORAGE_KEY } from "@/constants/store";
|
||||||
import { useLogStore } from "@/stores/log";
|
import { useLogStore } from "@/stores/log";
|
||||||
import { getErrorMessage } from "@/utils/common";
|
import { getErrorMessage } from "@/utils/common";
|
||||||
|
import { getItem, setItem } from "@/utils/storage";
|
||||||
|
|
||||||
|
type CloseAction = "ask" | "minimize" | "close";
|
||||||
|
|
||||||
|
interface AppConfig {
|
||||||
|
check_interval: number; // 轮询间隔(秒)
|
||||||
|
auto_start: boolean; // 开机自启
|
||||||
|
auto_monitor: boolean; // 启动后自动开始监测
|
||||||
|
silent_start: boolean; // 静默启动(最小化到托盘)
|
||||||
|
show_notifications: boolean; // 是否显示系统通知
|
||||||
|
notification_sound: boolean; // 通知是否播放声音
|
||||||
|
network_timeout: number; // 网络请求超时时间(秒)
|
||||||
|
close_action: CloseAction; // 点击关闭按钮的行为
|
||||||
|
handle_pause_minutes: number; // 点击"去处理"后的暂停时长(分钟)
|
||||||
|
}
|
||||||
|
|
||||||
export const useAppStore = defineStore("app", () => {
|
export const useAppStore = defineStore("app", () => {
|
||||||
const message = ref(""); // 界面状态提示消息
|
const message = ref(""); // 界面状态提示消息
|
||||||
const isLoading = ref(false); // 是否处于加载/请求中
|
const isLoading = ref(false); // 是否处于加载/请求中
|
||||||
const autoStartEnabled = ref(false); // 系统自启动是否已启用
|
const autoStartEnabled = ref(false); // 系统自启动是否已启用
|
||||||
const config = ref({
|
const config = ref<AppConfig>({
|
||||||
check_interval: 60, // 轮询间隔(秒)
|
check_interval: 60 * 10,
|
||||||
auto_start: false, // 开机自启
|
auto_start: false,
|
||||||
auto_monitor: false, // 启动后自动开始监测
|
auto_monitor: false,
|
||||||
silent_start: false, // 静默启动(最小化到托盘)
|
silent_start: false,
|
||||||
show_notifications: true, // 是否显示系统通知
|
show_notifications: true,
|
||||||
notification_sound: true, // 通知是否播放声音
|
notification_sound: true,
|
||||||
network_timeout: 30, // 网络请求超时时间(秒)
|
network_timeout: 30,
|
||||||
close_action: "ask" as "ask" | "minimize" | "close", // 点击关闭按钮的行为
|
close_action: "ask",
|
||||||
handle_pause_minutes: 20, // 点击"去处理"后的暂停时长(分钟)
|
handle_pause_minutes: 20,
|
||||||
});
|
});
|
||||||
const showCloseDialog = ref(false); // 是否显示关闭确认对话框
|
|
||||||
|
|
||||||
const { addLog } = useLogStore();
|
const { addLog } = useLogStore();
|
||||||
|
|
||||||
|
|
@ -37,26 +51,27 @@ export const useAppStore = defineStore("app", () => {
|
||||||
message.value = msg;
|
message.value = msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从 localStorage 加载已保存的运行配置,与默认值合并 */
|
/** 从文件存储加载配置(localStorage 迁移在应用启动阶段统一执行) */
|
||||||
function loadConfig() {
|
async function loadConfig() {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("crm_config");
|
const savedFromFile = await getItem<Partial<AppConfig>>(
|
||||||
if (saved) {
|
APP_CONFIG_STORAGE_KEY,
|
||||||
const data = JSON.parse(saved);
|
);
|
||||||
config.value = { ...config.value, ...data };
|
if (savedFromFile && typeof savedFromFile === "object") {
|
||||||
addLog("INFO", "已加载保存的配置", "CONFIG");
|
config.value = { ...config.value, ...savedFromFile };
|
||||||
|
addLog("INFO", "已从文件加载配置", "CONFIG");
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
addLog("ERROR", `加载配置失败: ${getErrorMessage(e)}`, "CONFIG");
|
addLog("ERROR", `加载配置失败: ${getErrorMessage(e)}`, "CONFIG");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 将当前配置序列化写入 localStorage */
|
/** 将当前配置写入文件存储 */
|
||||||
function saveConfigToStorage() {
|
async function saveConfigToStorage() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem("crm_config", JSON.stringify(config.value));
|
await setItem(APP_CONFIG_STORAGE_KEY, config.value);
|
||||||
} catch {
|
} catch (e: unknown) {
|
||||||
/* ignore */
|
addLog("ERROR", `保存配置失败: ${getErrorMessage(e)}`, "CONFIG");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,45 +90,21 @@ export const useAppStore = defineStore("app", () => {
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
addLog("ERROR", `自启动设置失败: ${getErrorMessage(e)}`, "CONFIG");
|
addLog("ERROR", `自启动设置失败: ${getErrorMessage(e)}`, "CONFIG");
|
||||||
}
|
}
|
||||||
saveConfigToStorage();
|
await saveConfigToStorage();
|
||||||
setMessage("配置更新成功");
|
setMessage("配置更新成功");
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 处理用户关闭对话框的确认操作,可选记住选择;minimize 隐藏窗口,close 销毁窗口 */
|
|
||||||
async function handleCloseAction(
|
|
||||||
action: "minimize" | "close",
|
|
||||||
remember: boolean,
|
|
||||||
) {
|
|
||||||
showCloseDialog.value = false;
|
|
||||||
if (remember) {
|
|
||||||
config.value.close_action = action;
|
|
||||||
saveConfigToStorage();
|
|
||||||
addLog(
|
|
||||||
"INFO",
|
|
||||||
`关闭行为已设为: ${action === "minimize" ? "最小化到托盘" : "直接退出"}`,
|
|
||||||
"SYSTEM",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (action === "minimize") {
|
|
||||||
await getCurrentWindow().hide();
|
|
||||||
} else {
|
|
||||||
await getCurrentWindow().destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message,
|
message,
|
||||||
isLoading,
|
isLoading,
|
||||||
autoStartEnabled,
|
autoStartEnabled,
|
||||||
config,
|
config,
|
||||||
showCloseDialog,
|
|
||||||
clearMessage,
|
clearMessage,
|
||||||
setMessage,
|
setMessage,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveConfigToStorage,
|
saveConfigToStorage,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
handleCloseAction,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,21 @@ import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { formatTime } from "@/utils/common";
|
import { formatTime } from "@/utils/common";
|
||||||
|
|
||||||
|
export const LOG_LEVELS = {
|
||||||
|
INFO: "INFO",
|
||||||
|
WARN: "WARN",
|
||||||
|
ERROR: "ERROR",
|
||||||
|
DEBUG: "DEBUG",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type LogLevel = (typeof LOG_LEVELS)[keyof typeof LOG_LEVELS];
|
||||||
|
|
||||||
/** 单条日志记录 */
|
/** 单条日志记录 */
|
||||||
export interface LogEntry {
|
export interface LogEntry {
|
||||||
/** 日志记录时间,格式:YYYY-MM-DD HH:mm:ss */
|
/** 日志记录时间,格式:YYYY-MM-DD HH:mm:ss */
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
/** 日志级别,如 INFO / WARN / ERROR / DEBUG */
|
/** 日志级别,如 INFO / WARN / ERROR / DEBUG */
|
||||||
level: string;
|
level: LogLevel;
|
||||||
/** 日志正文 */
|
/** 日志正文 */
|
||||||
message: string;
|
message: string;
|
||||||
/** 日志来源分类,如 LOGIN / SCHEDULER / SYSTEM */
|
/** 日志来源分类,如 LOGIN / SCHEDULER / SYSTEM */
|
||||||
|
|
@ -19,7 +28,7 @@ export const useLogStore = defineStore("log", () => {
|
||||||
const logs = ref<LogEntry[]>([]);
|
const logs = ref<LogEntry[]>([]);
|
||||||
|
|
||||||
/** 追加一条日志,超过 1000 条时自动丢弃最旧一条 */
|
/** 追加一条日志,超过 1000 条时自动丢弃最旧一条 */
|
||||||
function addLog(level: string, msg: string, category: string) {
|
function addLog(level: LogLevel, msg: string, category: string) {
|
||||||
logs.value.push({
|
logs.value.push({
|
||||||
timestamp: formatTime(new Date()),
|
timestamp: formatTime(new Date()),
|
||||||
level,
|
level,
|
||||||
|
|
@ -29,6 +38,22 @@ export const useLogStore = defineStore("log", () => {
|
||||||
if (logs.value.length > 1000) logs.value.shift();
|
if (logs.value.length > 1000) logs.value.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addInfoLog(msg: string, category: string) {
|
||||||
|
addLog(LOG_LEVELS.INFO, msg, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addWarnLog(msg: string, category: string) {
|
||||||
|
addLog(LOG_LEVELS.WARN, msg, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addErrorLog(msg: string, category: string) {
|
||||||
|
addLog(LOG_LEVELS.ERROR, msg, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDebugLog(msg: string, category: string) {
|
||||||
|
addLog(LOG_LEVELS.DEBUG, msg, category);
|
||||||
|
}
|
||||||
|
|
||||||
/** 清空所有日志 */
|
/** 清空所有日志 */
|
||||||
function clearLogs() {
|
function clearLogs() {
|
||||||
logs.value = [];
|
logs.value = [];
|
||||||
|
|
@ -51,9 +76,18 @@ export const useLogStore = defineStore("log", () => {
|
||||||
a.download = `monitor-logs-${formatTime(new Date()).replaceAll(/[: ]/g, "-")}.txt`;
|
a.download = `monitor-logs-${formatTime(new Date()).replaceAll(/[: ]/g, "-")}.txt`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
addLog("INFO", `已导出 ${logs.value.length} 条日志`, "SYSTEM");
|
addInfoLog(`已导出 ${logs.value.length} 条日志`, "SYSTEM");
|
||||||
return "日志导出成功";
|
return "日志导出成功";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { logs, addLog, clearLogs, exportLogs };
|
return {
|
||||||
|
logs,
|
||||||
|
addLog,
|
||||||
|
addInfoLog,
|
||||||
|
addWarnLog,
|
||||||
|
addErrorLog,
|
||||||
|
addDebugLog,
|
||||||
|
clearLogs,
|
||||||
|
exportLogs,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,26 @@ import { defineStore, acceptHMRUpdate, storeToRefs } from "pinia";
|
||||||
import { ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { login, checkWorkOrders, checkStatus } from "@/api/monitor";
|
import { login, checkWorkOrders, checkStatus } from "@/api/monitor";
|
||||||
import { useNotification } from "@/composables/useNotification";
|
import { useNotification } from "@/composables/useNotification";
|
||||||
import { useTray } from "@/composables/useTray";
|
import { CREDENTIALS_STORAGE_KEY } from "@/constants/store";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useLogStore } from "@/stores/log";
|
import { useLogStore } from "@/stores/log";
|
||||||
import { formatTime, getErrorMessage } from "@/utils/common";
|
import { formatTime, getErrorMessage } from "@/utils/common";
|
||||||
|
import { getItem, removeItem, setItem } from "@/utils/storage";
|
||||||
import type { TicketCounts, CheckStatusResponse } from "@/api/types";
|
import type { TicketCounts, CheckStatusResponse } from "@/api/types";
|
||||||
|
|
||||||
|
/** 判断 API 返回 code 是否属于登录过期 */
|
||||||
|
function isLoginTimeoutCode(code: string | null | undefined): boolean {
|
||||||
|
return code === "LoginTimeout";
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StoredCredentials {
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
remember?: boolean;
|
||||||
|
loginMode?: "password" | "token";
|
||||||
|
tokenSessionId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const useMonitorStore = defineStore("monitor", () => {
|
export const useMonitorStore = defineStore("monitor", () => {
|
||||||
// ===== 响应式数据 =====
|
// ===== 响应式数据 =====
|
||||||
const username = ref(""); // 登录用户名
|
const username = ref(""); // 登录用户名
|
||||||
|
|
@ -159,7 +173,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
} else {
|
} else {
|
||||||
// 3. 处理业务失败
|
// 3. 处理业务失败
|
||||||
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
|
addLog("WARN", `API 返回异常: ${result.msg}`, "SCHEDULER");
|
||||||
if (!isRetry && isSessionExpiredMsg(result.msg)) {
|
if (!isRetry && isSessionExpired(result.code, result.msg)) {
|
||||||
await tryReLogin();
|
await tryReLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +183,16 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断 API 返回消息是否属于会话过期类错误 */
|
/** 判断 API 返回是否属于会话过期类错误(优先 code,msg 兜底) */
|
||||||
|
function isSessionExpired(
|
||||||
|
code: string | null | undefined,
|
||||||
|
msg: string | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (isLoginTimeoutCode(code)) return true;
|
||||||
|
return isSessionExpiredMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 判断 API 返回消息是否属于会话过期类错误(兼容历史逻辑) */
|
||||||
function isSessionExpiredMsg(msg: string | undefined): boolean {
|
function isSessionExpiredMsg(msg: string | undefined): boolean {
|
||||||
if (!msg) return false;
|
if (!msg) return false;
|
||||||
const keywords = [
|
const keywords = [
|
||||||
|
|
@ -213,12 +236,11 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 认证与凭据 =====
|
// ===== 认证与凭据 =====
|
||||||
/** 从 localStorage 加载已保存的登录凭据 */
|
/** 从文件存储加载已保存的登录凭据 */
|
||||||
function loadCredentials() {
|
async function loadCredentials() {
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem("crm_credentials");
|
const data = await getItem<StoredCredentials>(CREDENTIALS_STORAGE_KEY);
|
||||||
if (saved) {
|
if (data && typeof data === "object") {
|
||||||
const data = JSON.parse(saved);
|
|
||||||
username.value = data.username || "";
|
username.value = data.username || "";
|
||||||
password.value = data.password || "";
|
password.value = data.password || "";
|
||||||
rememberPassword.value = data.remember || false;
|
rememberPassword.value = data.remember || false;
|
||||||
|
|
@ -231,23 +253,20 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 根据 rememberPassword 决定写入或清除 localStorage 中的凭据 */
|
/** 根据 rememberPassword 决定写入或清除文件存储中的凭据 */
|
||||||
function saveCredentials() {
|
async function saveCredentials() {
|
||||||
try {
|
try {
|
||||||
if (rememberPassword.value) {
|
if (rememberPassword.value) {
|
||||||
localStorage.setItem(
|
await setItem(CREDENTIALS_STORAGE_KEY, {
|
||||||
"crm_credentials",
|
username: username.value,
|
||||||
JSON.stringify({
|
password: password.value,
|
||||||
username: username.value,
|
remember: true,
|
||||||
password: password.value,
|
loginMode: loginMode.value,
|
||||||
remember: true,
|
tokenSessionId: tokenSessionId.value,
|
||||||
loginMode: loginMode.value,
|
});
|
||||||
tokenSessionId: tokenSessionId.value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
addLog("INFO", "凭据已保存到本地", "CONFIG");
|
addLog("INFO", "凭据已保存到本地", "CONFIG");
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem("crm_credentials");
|
await removeItem(CREDENTIALS_STORAGE_KEY);
|
||||||
addLog("INFO", "已清除本地凭据", "CONFIG");
|
addLog("INFO", "已清除本地凭据", "CONFIG");
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
|
|
@ -256,7 +275,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 校验凭据不为空后保存,同时记录日志并更新状态消息 */
|
/** 校验凭据不为空后保存,同时记录日志并更新状态消息 */
|
||||||
function saveSettings() {
|
async function saveSettings() {
|
||||||
if (loginMode.value === "password") {
|
if (loginMode.value === "password") {
|
||||||
if (!username.value.trim() || !password.value.trim()) {
|
if (!username.value.trim() || !password.value.trim()) {
|
||||||
appStore.setMessage("请输入用户名和密码");
|
appStore.setMessage("请输入用户名和密码");
|
||||||
|
|
@ -268,7 +287,7 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
saveCredentials();
|
await saveCredentials();
|
||||||
appStore.setMessage("设置保存成功");
|
appStore.setMessage("设置保存成功");
|
||||||
addLog("INFO", "用户凭据已保存", "CONFIG");
|
addLog("INFO", "用户凭据已保存", "CONFIG");
|
||||||
}
|
}
|
||||||
|
|
@ -434,15 +453,6 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 系统托盘 =====
|
|
||||||
const { setupTray, updateTrayIcon, updateTrayMenu, minimizeToTray } = useTray(
|
|
||||||
isMonitoring,
|
|
||||||
isPostponed,
|
|
||||||
ticketCounts,
|
|
||||||
startMonitoring,
|
|
||||||
stopMonitoring,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 检查间隔变化时重启定时器
|
// 检查间隔变化时重启定时器
|
||||||
watch(
|
watch(
|
||||||
() => config.value.check_interval,
|
() => config.value.check_interval,
|
||||||
|
|
@ -455,16 +465,6 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听状态变化,自动更新托盘图标和菜单
|
|
||||||
watch(
|
|
||||||
[isMonitoring, isPostponed, ticketCounts],
|
|
||||||
() => {
|
|
||||||
updateTrayIcon();
|
|
||||||
updateTrayMenu();
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
username,
|
username,
|
||||||
|
|
@ -480,7 +480,6 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
// 方法
|
// 方法
|
||||||
saveSettings,
|
saveSettings,
|
||||||
loadCredentials,
|
loadCredentials,
|
||||||
setupTray,
|
|
||||||
startMonitoring,
|
startMonitoring,
|
||||||
stopMonitoring,
|
stopMonitoring,
|
||||||
postponeMonitoring,
|
postponeMonitoring,
|
||||||
|
|
@ -488,7 +487,6 @@ export const useMonitorStore = defineStore("monitor", () => {
|
||||||
resumeMonitoring,
|
resumeMonitoring,
|
||||||
manualCheck,
|
manualCheck,
|
||||||
testNotification,
|
testNotification,
|
||||||
minimizeToTray,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { ref } from "vue";
|
||||||
import { API_BASE } from "@/api/client";
|
import { API_BASE } from "@/api/client";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useLogStore } from "@/stores/log";
|
import { useLogStore } from "@/stores/log";
|
||||||
import { formatTime } from "@/utils/common";
|
import { formatTime, getErrorMessage } from "@/utils/common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 网络状态 store。
|
* 网络状态 store。
|
||||||
|
|
@ -42,8 +42,8 @@ export const useNetworkStore = defineStore("network", () => {
|
||||||
connectTimeout: appStore.config.network_timeout * 1000,
|
connectTimeout: appStore.config.network_timeout * 1000,
|
||||||
});
|
});
|
||||||
connected = resp.ok;
|
connected = resp.ok;
|
||||||
} catch {
|
} catch (e: unknown) {
|
||||||
/* 网络不通 */
|
addLog("ERROR", `公网连通性检测失败: ${getErrorMessage(e)}`, "NETWORK");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connected) {
|
if (connected) {
|
||||||
|
|
@ -55,8 +55,8 @@ export const useNetworkStore = defineStore("network", () => {
|
||||||
});
|
});
|
||||||
respTime = Date.now() - apiStart;
|
respTime = Date.now() - apiStart;
|
||||||
reachable = resp.status < 500;
|
reachable = resp.status < 500;
|
||||||
} catch {
|
} catch (e: unknown) {
|
||||||
/* API 不可达 */
|
addLog("ERROR", `API 可达性检测失败: ${getErrorMessage(e)}`, "NETWORK");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import { load, type Store } from "@tauri-apps/plugin-store";
|
import { load, type Store } from "@tauri-apps/plugin-store";
|
||||||
|
import {
|
||||||
|
APP_CONFIG_STORAGE_KEY,
|
||||||
|
CREDENTIALS_STORAGE_KEY,
|
||||||
|
STORE_FILE_NAME,
|
||||||
|
} from "@/constants/store";
|
||||||
|
|
||||||
let storeInstance: Store | null = null;
|
let storeInstance: Store | null = null;
|
||||||
|
|
||||||
async function getStore(): Promise<Store> {
|
async function getStore(): Promise<Store> {
|
||||||
if (!storeInstance) {
|
storeInstance ??= await load(STORE_FILE_NAME, {
|
||||||
storeInstance = await load("config.json", { defaults: {}, autoSave: true });
|
defaults: {},
|
||||||
}
|
autoSave: true,
|
||||||
|
});
|
||||||
return storeInstance;
|
return storeInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +43,7 @@ export async function migrateFromLocalStorage(): Promise<boolean> {
|
||||||
if (migrated) return false;
|
if (migrated) return false;
|
||||||
|
|
||||||
let hasMigrated = false;
|
let hasMigrated = false;
|
||||||
for (const key of ["crm_credentials", "crm_config"]) {
|
for (const key of [CREDENTIALS_STORAGE_KEY, APP_CONFIG_STORAGE_KEY]) {
|
||||||
const raw = localStorage.getItem(key);
|
const raw = localStorage.getItem(key);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -46,6 +52,7 @@ export async function migrateFromLocalStorage(): Promise<boolean> {
|
||||||
hasMigrated = true;
|
hasMigrated = true;
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore invalid JSON */
|
/* ignore invalid JSON */
|
||||||
|
// TODO 换成r-utils解析JSON
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||||
|
|
||||||
|
export async function hideCurrentWindow() {
|
||||||
|
await getCurrentWindow().hide();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue