修正bug

master
cysamurai 1 month ago
parent 0533c2ccb7
commit e07d0caa9f

@ -32,6 +32,8 @@ export interface CallerInitData {
export const api = { export const api = {
user: { user: {
login: (data: UserRequest) => http.post<UserResponse>("/auth/login", data), login: (data: UserRequest) => http.post<UserResponse>("/auth/login", data),
logout: () =>
http.post<{ success?: boolean; message?: string }>("/auth/logout", {}),
}, },
window: { window: {

@ -1,5 +1,7 @@
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { appWindow } from "@tauri-apps/api/window"; import { appWindow } from "@tauri-apps/api/window";
import { postLogoutBeforeQuit } from "../utils/service";
import { clearSession } from "./session";
/** /**
* *
@ -17,9 +19,18 @@ export async function minimizeWindow(): Promise<void> {
*/ */
export async function closeWindow(): Promise<void> { export async function closeWindow(): Promise<void> {
try { try {
await invoke("quit_app"); await postLogoutBeforeQuit();
} catch (error) { } finally {
throw new Error(`关闭窗口失败: ${String(error)}`); try {
await clearSession();
} catch {
// 忽略
}
try {
await invoke("quit_app");
} catch (error) {
throw new Error(`关闭窗口失败: ${String(error)}`);
}
} }
} }
@ -116,8 +127,17 @@ export async function openTaxerInfoWindow(): Promise<void> {
*/ */
export async function quitApplication(): Promise<void> { export async function quitApplication(): Promise<void> {
try { try {
await invoke("quit_app"); await postLogoutBeforeQuit();
} catch (error) { } finally {
throw new Error(`退出应用失败: ${String(error)}`); try {
await clearSession();
} catch {
// 忽略
}
try {
await invoke("quit_app");
} catch (error) {
throw new Error(`退出应用失败: ${String(error)}`);
}
} }
} }

@ -6,6 +6,7 @@ import axios, {
} from "axios"; } from "axios";
import { Body, fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http"; import { Body, fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { showErrorNative } from "../host/dialog"; import { showErrorNative } from "../host/dialog";
import { log } from "../host/logger";
import { getSession, setSession } from "../host/session"; import { getSession, setSession } from "../host/session";
import type { SessionState } from "../host/types"; import type { SessionState } from "../host/types";
@ -13,6 +14,7 @@ export const API_QUEUE_CALLER_PATH = "/api/queue/caller";
const DEFAULT_API_PORT = 8845; const DEFAULT_API_PORT = 8845;
const AUTH_LOGIN_PATH = "/auth/login"; const AUTH_LOGIN_PATH = "/auth/login";
const AUTH_REFRESH_PATH = "/auth/refresh"; const AUTH_REFRESH_PATH = "/auth/refresh";
const AUTH_LOGOUT_PATH = "/auth/logout";
type ApiEnvelope<T = unknown> = { type ApiEnvelope<T = unknown> = {
code: number; code: number;
@ -29,6 +31,8 @@ type RefreshTokenResponse = {
type RetryableAxiosRequestConfig = AxiosRequestConfig & { type RetryableAxiosRequestConfig = AxiosRequestConfig & {
_retry?: boolean; _retry?: boolean;
_skipAuthRefresh?: boolean; _skipAuthRefresh?: boolean;
/** 不弹原生错误框(用于退出前 logout 等) */
_suppressErrorUi?: boolean;
}; };
let refreshTokenPromise: Promise<string | null> | null = null; let refreshTokenPromise: Promise<string | null> | null = null;
@ -294,23 +298,29 @@ instance.interceptors.response.use(
async (response) => { async (response) => {
const { data } = response; const { data } = response;
const payload = data as ApiEnvelope; const payload = data as ApiEnvelope;
const requestConfig = response.config as RetryableAxiosRequestConfig;
if (!isApiSuccessCode(payload?.code)) { if (!isApiSuccessCode(payload?.code)) {
const requestConfig = response.config as RetryableAxiosRequestConfig;
if (payload?.code === 401 && canRefreshAndRetry(requestConfig)) { if (payload?.code === 401 && canRefreshAndRetry(requestConfig)) {
return retryWithRefreshedToken(requestConfig); return retryWithRefreshedToken(requestConfig);
} }
const message = getApiMessage(payload); const message = getApiMessage(payload);
await showErrorNative(message); if (!requestConfig._suppressErrorUi) {
await showErrorNative(message);
}
throw new Error(message); throw new Error(message);
} }
return payload.data; return payload.data;
}, },
async (error) => { async (error) => {
const requestConfig = (error.config ?? {}) as RetryableAxiosRequestConfig;
if (requestConfig._suppressErrorUi) {
throw error;
}
if (error.response) { if (error.response) {
const requestConfig = error.config as RetryableAxiosRequestConfig;
if (error.response.status === 401 && canRefreshAndRetry(requestConfig)) { if (error.response.status === 401 && canRefreshAndRetry(requestConfig)) {
try { try {
return await retryWithRefreshedToken(requestConfig); return await retryWithRefreshedToken(requestConfig);
@ -355,6 +365,45 @@ instance.interceptors.response.use(
}, },
); );
async function logQuitEvent(level: "info" | "warn" | "error", message: string): Promise<void> {
try {
await log(level, message);
} catch {
// 退出路径不因写日志失败而阻塞
}
}
/**
* 退 `POST .../auth/logout`
*/
export async function postLogoutBeforeQuit(): Promise<void> {
const baseURL = instance.defaults.baseURL;
if (!baseURL || typeof baseURL !== "string" || baseURL.trim() === "") {
await logQuitEvent("warn", "退出前 logout 跳过: 未配置 HTTP baseURL");
return;
}
const logoutUrl = `${String(baseURL).replace(/\/$/, "")}${AUTH_LOGOUT_PATH}`;
await logQuitEvent("info", `退出前调用 logout: url=${logoutUrl}`);
try {
await instance.post(
AUTH_LOGOUT_PATH,
{},
{
timeout: 5000,
_skipAuthRefresh: true,
_suppressErrorUi: true,
headers: { "Content-Type": "application/json" },
} as RetryableAxiosRequestConfig,
);
await logQuitEvent("info", "退出前 logout 调用成功");
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
await logQuitEvent("warn", `退出前 logout 调用失败(仍继续退出): ${detail}`);
}
}
export const http = { export const http = {
/** /**
* axios * axios

@ -4,7 +4,7 @@ import { ElForm, ElMessage, ElMessageBox } from "element-plus";
import { getVersion } from "@tauri-apps/api/app"; import { getVersion } from "@tauri-apps/api/app";
import { writeText } from "@tauri-apps/api/clipboard"; import { writeText } from "@tauri-apps/api/clipboard";
import { invoke } from "@tauri-apps/api/tauri"; import { invoke } from "@tauri-apps/api/tauri";
import { computed, onMounted, onUnmounted, ref } from "vue"; import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { api } from "../api"; import { api } from "../api";
import { getAllConfig, mergeConfig } from "../host/config"; import { getAllConfig, mergeConfig } from "../host/config";
import { log } from "../host/logger"; import { log } from "../host/logger";
@ -17,8 +17,11 @@ import {
startWindowDragging, startWindowDragging,
} from "../host/window"; } from "../host/window";
const username = ref("admin"); /** el-form 需要稳定引用,勿使用每次渲染新建的 `{ username, password }` 对象 */
const password = ref("admin"); const loginForm = reactive({
username: "admin",
password: "admin",
});
const loginType = ref("NSFW"); const loginType = ref("NSFW");
const isLoading = ref(false); const isLoading = ref(false);
const formRef = ref<InstanceType<typeof ElForm>>(); const formRef = ref<InstanceType<typeof ElForm>>();
@ -58,7 +61,7 @@ const rules = {
}; };
const isFormValid = computed( const isFormValid = computed(
() => username.value.trim() !== "" && password.value !== "", () => loginForm.username.trim() !== "" && loginForm.password.trim() !== "",
); );
const isWindowSelected = computed(() => selectedWin.value !== ""); const isWindowSelected = computed(() => selectedWin.value !== "");
@ -78,6 +81,13 @@ function showMessage(
}); });
} }
/**
* 从窗口选择返回账号登录步骤
*/
function backToUserForm(): void {
isLoginSuccessed.value = false;
}
/** /**
* 处理账号登录 * 处理账号登录
*/ */
@ -87,100 +97,113 @@ async function handleLogin(): Promise<void> {
return; return;
} }
await formRef.value?.validate(async (valid) => { const form = formRef.value;
if (!valid) { if (!form) {
return; return;
} }
isLoading.value = true;
try { isLoading.value = true;
const loginRes = await api.user.login({ try {
loginMode: loginType.value, await form.validate();
clientType: "CALLER", } catch {
username: username.value, isLoading.value = false;
password: password.value, return;
hallRegNum: "", }
});
if (!loginRes?.queueToken) {
await log(
"warn",
`登录失败: 接口未返回有效 queueToken, user=${username.value}`,
);
return;
}
sessionState = { try {
empUid: loginRes.operatorProfile.empUid, const loginRes = await api.user.login({
winUid: 0, loginMode: loginType.value,
queueToken: loginRes.queueToken, clientType: "CALLER",
refreshToken: loginRes.refreshToken, username: loginForm.username,
}; password: loginForm.password,
hallRegNum: "",
});
await setSession(sessionState); if (!loginRes?.queueToken) {
await log(
"warn",
`登录失败: 接口未返回有效 queueToken, user=${loginForm.username}`,
);
return;
}
const winList = await api.window.list(); sessionState = {
options.value = winList.windows.map((win) => ({ empUid: loginRes.operatorProfile.empUid,
label: win.windowName, winUid: 0,
value: win.windowUid.toString(), queueToken: loginRes.queueToken,
})); refreshToken: loginRes.refreshToken,
};
if (
cachedWinKey.value &&
options.value.some((item) => item.value === cachedWinKey.value)
) {
selectedWin.value = cachedWinKey.value;
}
const initWindowUid = await setSession(sessionState);
selectedWin.value.trim() !== ""
? Number.parseInt(selectedWin.value, 10)
: Number(sessionState.winUid ?? 0);
const initRes = await api.action.init({
empUid: Number(sessionState.empUid ?? -1),
windowUid: Number.isFinite(initWindowUid) ? initWindowUid : 0,
});
const initSuccess =
((initRes as { data?: { success?: boolean } }).data?.success ??
(initRes as { success?: boolean }).success) === true;
if (!initSuccess) {
await log("warn", "初始化接口调用完成,但返回 success=false");
} else {
sessionState.empUid = Number(sessionState.empUid ?? loginRes.operatorProfile.empUid);
sessionState.winUid = Number.isFinite(initWindowUid) ? initWindowUid : Number(sessionState.winUid ?? 0);
await setSession(sessionState);
}
isLoginSuccessed.value = true; const winList = await api.window.list();
await log( options.value = winList.windows.map((win) => ({
"info", label: win.windowName,
`登录成功: user=${username.value}, empUid=${sessionState.empUid}`, value: win.windowUid.toString(),
); }));
} catch (error) {
const message = error instanceof Error ? error.message : String(error); if (
await log("error", `登录失败: user=${username.value}, ${message}`); cachedWinKey.value &&
} finally { options.value.some((item) => item.value === cachedWinKey.value)
isLoading.value = false; ) {
selectedWin.value = cachedWinKey.value;
} }
});
isLoginSuccessed.value = true;
await log(
"info",
`登录成功: user=${loginForm.username}, empUid=${sessionState.empUid}`,
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await log("error", `登录失败: user=${loginForm.username}, ${message}`);
} finally {
isLoading.value = false;
}
} }
/** /**
* 处理窗口登录确认 * 处理窗口登录确认
*/ */
async function handleWindowLogin(): Promise<void> { async function handleWindowLogin(): Promise<void> {
isLoading.value = true;
try { try {
const winUid = Number.parseInt(selectedWin.value, 10); const winUid = Number.parseInt(selectedWin.value, 10);
if (!Number.isFinite(winUid) || winUid <= 0) {
showMessage("warning", "请选择有效窗口");
return;
}
const selected = options.value.find( const selected = options.value.find(
(item) => item.value === selectedWin.value, (item) => item.value === selectedWin.value,
); );
const initRes = await api.action.init({
empUid: Number(sessionState.empUid ?? -1),
windowUid: winUid,
});
const initSuccess =
((initRes as { data?: { success?: boolean } }).data?.success ??
(initRes as { success?: boolean }).success) === true;
if (!initSuccess) {
const initData = (initRes as { data?: { message?: string } }).data;
const msg =
(typeof initData?.message === "string" && initData.message.trim() !== ""
? initData.message
: null) ??
(typeof (initRes as { message?: string }).message === "string"
? (initRes as { message?: string }).message
: null) ??
"窗口初始化失败,请重试或更换窗口";
showMessage("error", String(msg));
await log("warn", `call-terminal/init 未成功: windowUid=${winUid}`);
return;
}
await mergeConfig({ await mergeConfig({
last_username: username.value.trim(), last_username: loginForm.username.trim(),
selected_win_key: selectedWin.value, selected_win_key: selectedWin.value,
selected_win_value: selected?.label ?? "", selected_win_value: selected?.label ?? "",
selected_win_uid: winUid, selected_win_uid: winUid,
@ -226,6 +249,8 @@ async function handleWindowLogin(): Promise<void> {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
showMessage("error", message || "打开主窗口失败"); showMessage("error", message || "打开主窗口失败");
await log("error", `打开主窗口失败: ${message}`); await log("error", `打开主窗口失败: ${message}`);
} finally {
isLoading.value = false;
} }
} }
@ -380,7 +405,7 @@ onMounted(async () => {
const config = await getAllConfig(); const config = await getAllConfig();
if (typeof config.last_username === "string" && config.last_username.trim()) { if (typeof config.last_username === "string" && config.last_username.trim()) {
username.value = config.last_username; loginForm.username = config.last_username;
} }
if ( if (
typeof config.selected_win_key === "string" && typeof config.selected_win_key === "string" &&
@ -465,7 +490,7 @@ onUnmounted(() => {
<el-form <el-form
ref="formRef" ref="formRef"
:model="{ username, password }" :model="loginForm"
:rules="rules" :rules="rules"
class="login-form" class="login-form"
label-position="top" label-position="top"
@ -473,7 +498,7 @@ onUnmounted(() => {
> >
<el-form-item label="账号" prop="username"> <el-form-item label="账号" prop="username">
<el-input <el-input
v-model="username" v-model="loginForm.username"
:prefix-icon="User" :prefix-icon="User"
placeholder="请输入用户名/手机号" placeholder="请输入用户名/手机号"
size="large" size="large"
@ -483,7 +508,7 @@ onUnmounted(() => {
<el-form-item label="密码" prop="password"> <el-form-item label="密码" prop="password">
<el-input <el-input
v-model="password" v-model="loginForm.password"
:prefix-icon="Lock" :prefix-icon="Lock"
type="password" type="password"
placeholder="请输入登录密码" placeholder="请输入登录密码"
@ -544,7 +569,7 @@ onUnmounted(() => {
type="primary" type="primary"
size="large" size="large"
class="login-button" class="login-button"
@click="isLoginSuccessed = false" @click="backToUserForm"
> >
返回 返回
</el-button> </el-button>

@ -73,7 +73,8 @@ let unlistenMainAction: (() => void) | null = null;
const EVALUATING_COUNTDOWN_SEC = 15; const EVALUATING_COUNTDOWN_SEC = 15;
const pauseReasonOptions = ["午休", "休息一下", "整理资料", "其他"]; const pauseReasonOptions = ["午休", "休息一下", "整理资料", "其他"];
const isMainWindowActive = ref(false); /** 主界面挂载时即视为可轮询;失焦后由 window blur 置为 false 暂停 */
const isMainWindowActive = ref(true);
const buttonPanel = ref<"main" | "more" | "pause">("main"); const buttonPanel = ref<"main" | "more" | "pause">("main");
const isSyncingMainScreen = ref(false); const isSyncingMainScreen = ref(false);
const isActionPending = ref(false); const isActionPending = ref(false);
@ -963,8 +964,17 @@ onMounted(async () => {
await logErr("读取 session 失败", error); await logErr("读取 session 失败", error);
} }
// session setup watch winUid
if (callStatus.value === "idle" && isMainWindowActive.value) {
startQueueCountPolling();
}
try { try {
isMainWindowActive.value = await appWindow.isVisible(); const visible = await appWindow.isVisible();
// show isVisible false false
if (visible) {
isMainWindowActive.value = true;
}
const onFocus = () => { const onFocus = () => {
isMainWindowActive.value = true; isMainWindowActive.value = true;
}; };

Loading…
Cancel
Save