修正bug

master
cysamurai 1 month ago
parent 0533c2ccb7
commit e07d0caa9f

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

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

@ -6,6 +6,7 @@ import axios, {
} from "axios";
import { Body, fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { showErrorNative } from "../host/dialog";
import { log } from "../host/logger";
import { getSession, setSession } from "../host/session";
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 AUTH_LOGIN_PATH = "/auth/login";
const AUTH_REFRESH_PATH = "/auth/refresh";
const AUTH_LOGOUT_PATH = "/auth/logout";
type ApiEnvelope<T = unknown> = {
code: number;
@ -29,6 +31,8 @@ type RefreshTokenResponse = {
type RetryableAxiosRequestConfig = AxiosRequestConfig & {
_retry?: boolean;
_skipAuthRefresh?: boolean;
/** 不弹原生错误框(用于退出前 logout 等) */
_suppressErrorUi?: boolean;
};
let refreshTokenPromise: Promise<string | null> | null = null;
@ -294,23 +298,29 @@ instance.interceptors.response.use(
async (response) => {
const { data } = response;
const payload = data as ApiEnvelope;
const requestConfig = response.config as RetryableAxiosRequestConfig;
if (!isApiSuccessCode(payload?.code)) {
const requestConfig = response.config as RetryableAxiosRequestConfig;
if (payload?.code === 401 && canRefreshAndRetry(requestConfig)) {
return retryWithRefreshedToken(requestConfig);
}
const message = getApiMessage(payload);
if (!requestConfig._suppressErrorUi) {
await showErrorNative(message);
}
throw new Error(message);
}
return payload.data;
},
async (error) => {
const requestConfig = (error.config ?? {}) as RetryableAxiosRequestConfig;
if (requestConfig._suppressErrorUi) {
throw error;
}
if (error.response) {
const requestConfig = error.config as RetryableAxiosRequestConfig;
if (error.response.status === 401 && canRefreshAndRetry(requestConfig)) {
try {
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 = {
/**
* axios

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

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

Loading…
Cancel
Save