修正初始化错误

master
cysamurai 1 month ago
parent 41fc488225
commit 57b32c54aa

@ -1,38 +1,40 @@
use tauri::State; use std::fs;
use std::path::PathBuf;
use crate::state::{AppState, SessionState}; use tauri::AppHandle;
#[tauri::command] const RUNTIME_SESSION_FILE: &str = "runtime_session.json";
pub fn session_get(state: State<'_, AppState>) -> Result<SessionState, String> {
let session = state
.session
.lock()
.map_err(|error| format!("读取会话锁失败: {error}"))?;
Ok(session.clone()) fn runtime_session_path(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
.path_resolver()
.app_data_dir()
.ok_or_else(|| "无法获取应用数据目录".to_string())?;
fs::create_dir_all(&dir).map_err(|e| format!("创建应用数据目录失败: {e}"))?;
Ok(dir.join(RUNTIME_SESSION_FILE))
} }
/// 读取完整运行时会话 JSON与前端 `SessionState` 一致camelCase
/// 多窗口下各 WebView 的 localStorage 互不共享,因此会话必须落盘,由所有窗口共用。
#[tauri::command] #[tauri::command]
pub fn session_set( pub fn session_get(app: AppHandle) -> Result<String, String> {
payload: SessionState, let path = runtime_session_path(&app)?;
state: State<'_, AppState>, if !path.exists() {
) -> Result<SessionState, String> { return Ok("{}".to_string());
let mut session = state }
.session fs::read_to_string(&path).map_err(|e| format!("读取会话文件失败: {e}"))
.lock()
.map_err(|error| format!("写入会话锁失败: {error}"))?;
*session = payload.clone();
Ok(payload)
} }
#[tauri::command] #[tauri::command]
pub fn session_clear(state: State<'_, AppState>) -> Result<SessionState, String> { pub fn session_set(app: AppHandle, json: String) -> Result<(), String> {
let mut session = state let _: serde_json::Value =
.session serde_json::from_str(&json).map_err(|e| format!("会话 JSON 无效: {e}"))?;
.lock() let path = runtime_session_path(&app)?;
.map_err(|error| format!("清空会话锁失败: {error}"))?; fs::write(&path, json).map_err(|e| format!("写入会话文件失败: {e}"))?;
Ok(())
}
*session = SessionState::default(); #[tauri::command]
Ok(session.clone()) pub fn session_clear(app: AppHandle) -> Result<(), String> {
session_set(app, "{}".to_string())
} }

@ -1,17 +1,7 @@
use std::sync::Mutex; use std::sync::Mutex;
use serde::{Deserialize, Serialize}; /// 应用级状态(屏幕同步等)。运行时会话已改为磁盘文件,见 `commands/session.rs`。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionState {
pub emp_uid: Option<i64>,
pub win_uid: Option<i64>,
pub queue_token: Option<String>,
}
pub struct AppState { pub struct AppState {
pub session: Mutex<SessionState>,
pub screen_sync: Mutex<ScreenSyncState>, pub screen_sync: Mutex<ScreenSyncState>,
} }
@ -25,7 +15,6 @@ pub struct ScreenSyncState {
impl Default for AppState { impl Default for AppState {
fn default() -> Self { fn default() -> Self {
Self { Self {
session: Mutex::new(SessionState::default()),
screen_sync: Mutex::new(ScreenSyncState::default()), screen_sync: Mutex::new(ScreenSyncState::default()),
} }
} }

@ -1,3 +1,5 @@
import { invoke } from "@tauri-apps/api/tauri";
import type { SessionState } from "./types"; import type { SessionState } from "./types";
const SESSION_KEY = "runtime_session"; const SESSION_KEY = "runtime_session";
@ -29,17 +31,23 @@ const DEFAULT_SESSION: SessionState = {
taxer_kz: null, taxer_kz: null,
}; };
/** 与登录页 / MainView 一致:支持 number 或可解析为整数的 string避免 localStorage JSON 把数字写成字符串后读成 null */
function normalizeOptionalUid(value: unknown): number | null {
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string" && value.trim() !== "") {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
function normalizeSession(raw: unknown): SessionState { function normalizeSession(raw: unknown): SessionState {
const source = (raw ?? {}) as Partial<SessionState>; const source = (raw ?? {}) as Partial<SessionState>;
const empUid = const empUid = normalizeOptionalUid(source.empUid);
typeof source.empUid === "number" && Number.isFinite(source.empUid) const winUid = normalizeOptionalUid(source.winUid);
? source.empUid
: null;
const winUid =
typeof source.winUid === "number" && Number.isFinite(source.winUid)
? source.winUid
: null;
const queueToken = const queueToken =
typeof source.queueToken === "string" ? source.queueToken : null; typeof source.queueToken === "string" ? source.queueToken : null;
const refreshToken = const refreshToken =
@ -125,41 +133,67 @@ function normalizeSession(raw: unknown): SessionState {
} }
/** /**
* localStorage * Tauri `runtime_session.json`
* Tauri 退 localStorage
*
* localStorage退 WebView
* / token token
*/ */
export async function getSession(): Promise<SessionState> { export async function getSession(): Promise<SessionState> {
try { try {
const value = window.localStorage.getItem(SESSION_KEY); const raw = await invoke<string>("session_get");
if (!value) { return normalizeSession(JSON.parse(raw || "{}"));
return { ...DEFAULT_SESSION };
}
return normalizeSession(JSON.parse(value));
} catch (error) { } catch (error) {
throw new Error(`读取会话失败: ${String(error)}`); try {
const value = window.localStorage.getItem(SESSION_KEY);
if (!value) {
return { ...DEFAULT_SESSION };
}
return normalizeSession(JSON.parse(value));
} catch (inner) {
throw new Error(`读取会话失败: ${String(error)} / ${String(inner)}`);
}
} }
} }
/** /**
* localStorage * Tauri WebView localStorage
* Tauri localStorage
*/ */
export async function setSession(payload: SessionState): Promise<SessionState> { export async function setSession(payload: SessionState): Promise<SessionState> {
const normalized = normalizeSession(payload);
const json = JSON.stringify(normalized);
try { try {
const normalized = normalizeSession(payload); await invoke("session_set", { json });
window.localStorage.setItem(SESSION_KEY, JSON.stringify(normalized)); try {
window.localStorage.removeItem(SESSION_KEY);
} catch {
// 忽略
}
return normalized; return normalized;
} catch (error) { } catch (error) {
throw new Error(`写入会话失败: ${String(error)}`); try {
window.localStorage.setItem(SESSION_KEY, json);
return normalized;
} catch (inner) {
throw new Error(`写入会话失败: ${String(error)} / ${String(inner)}`);
}
} }
} }
/** /**
* localStorage * + localStorage
*/ */
export async function clearSession(): Promise<SessionState> { export async function clearSession(): Promise<SessionState> {
try { try {
window.localStorage.setItem(SESSION_KEY, JSON.stringify(DEFAULT_SESSION)); await invoke("session_clear");
return { ...DEFAULT_SESSION }; } catch {
} catch (error) { // 非 Tauri 或未就绪时忽略
throw new Error(`清空会话失败: ${String(error)}`); }
try {
window.localStorage.removeItem(SESSION_KEY);
} catch {
// 忽略
} }
return { ...DEFAULT_SESSION };
} }

@ -176,19 +176,51 @@ function getApiMessage(payload: unknown): string {
return "请求失败"; return "请求失败";
} }
function isAuthEndpoint(url?: string): boolean { function isLoginOrRefreshUrl(url?: string): boolean {
if (!url) { if (!url) {
return false; return false;
} }
return url.includes(AUTH_LOGIN_PATH) || url.includes(AUTH_REFRESH_PATH); return url.includes(AUTH_LOGIN_PATH) || url.includes(AUTH_REFRESH_PATH);
} }
/** 登录 / 刷新 / 登出 不应在 401 时走 refresh 重试(登出后 token 已作废,刷新无意义) */
function shouldAvoidAuthRefreshRetry(url?: string): boolean {
if (!url) {
return false;
}
return (
url.includes(AUTH_LOGIN_PATH) ||
url.includes(AUTH_REFRESH_PATH) ||
url.includes(AUTH_LOGOUT_PATH)
);
}
function setAuthHeader(config: AxiosRequestConfig, queueToken: string): void { function setAuthHeader(config: AxiosRequestConfig, queueToken: string): void {
const headers = (config.headers ?? {}) as Record<string, string>; const headers = (config.headers ?? {}) as Record<string, string>;
headers.Authorization = `Bearer ${queueToken}`; headers.Authorization = `Bearer ${queueToken}`;
config.headers = headers; config.headers = headers;
} }
/** 本次请求是否已携带 Bearer未携带时 401 不应走 refresh否则会误报「会话已过期」 */
function requestHadBearerAuth(config?: AxiosRequestConfig): boolean {
const headers = config?.headers;
if (!headers || typeof headers !== "object") {
return false;
}
const h = headers as Record<string, unknown> & { get?: (name: string) => unknown };
const auth =
(typeof h.get === "function"
? (h.get("Authorization") ?? h.get("authorization"))
: null) ??
h.Authorization ??
h.authorization;
return (
typeof auth === "string" &&
auth.startsWith("Bearer ") &&
auth.length > "Bearer ".length + 4
);
}
async function refreshQueueToken(): Promise<string | null> { async function refreshQueueToken(): Promise<string | null> {
if (refreshTokenPromise) { if (refreshTokenPromise) {
return refreshTokenPromise; return refreshTokenPromise;
@ -239,12 +271,23 @@ function canRefreshAndRetry(config?: RetryableAxiosRequestConfig): boolean {
if (config._retry || config._skipAuthRefresh) { if (config._retry || config._skipAuthRefresh) {
return false; return false;
} }
return !isAuthEndpoint(config.url); if (!requestHadBearerAuth(config)) {
return false;
}
return !shouldAvoidAuthRefreshRetry(config.url);
} }
async function retryWithRefreshedToken(config: RetryableAxiosRequestConfig): Promise<any> { async function retryWithRefreshedToken(config: RetryableAxiosRequestConfig): Promise<any> {
const nextToken = await refreshQueueToken(); const nextToken = await refreshQueueToken();
if (!nextToken) { if (!nextToken) {
let detail = "refresh 未返回 queueToken";
try {
const s = await getSession();
detail = `refresh 失败: refreshToken存在=${Boolean(s.refreshToken?.trim())}, url=${config.url ?? ""}`;
} catch {
// ignore
}
await log("warn", `auth: 401 后 ${detail}`);
throw new Error("会话已过期,请重新登录"); throw new Error("会话已过期,请重新登录");
} }
@ -283,7 +326,7 @@ instance.interceptors.request.use(
token = null; token = null;
} }
if (token && !isAuthEndpoint(config.url)) { if (token && !isLoginOrRefreshUrl(config.url)) {
setAuthHeader(config, token); setAuthHeader(config, token);
} }

@ -150,7 +150,8 @@ async function handleLogin(): Promise<void> {
sessionState = { sessionState = {
empUid: loginRes.operatorProfile.empUid, empUid: loginRes.operatorProfile.empUid,
winUid: 0, /** 未选窗口前不写占位数字 0避免与「无效窗口」混淆且与 MainView `<=0` 判定一致 */
winUid: null,
queueToken: loginRes.queueToken, queueToken: loginRes.queueToken,
refreshToken: loginRes.refreshToken, refreshToken: loginRes.refreshToken,
}; };
@ -236,10 +237,16 @@ async function handleWindowLogin(): Promise<void> {
// caller-init data session // caller-init data session
try { try {
const callerInit = await api.action.callerInit(); const callerInit = await api.action.callerInit();
const callerWin = Number(callerInit.windowUid);
const resolvedWinUid =
Number.isFinite(callerWin) && callerWin > 0 ? callerWin : winUid;
const callerEmp = Number(callerInit.empUid);
const resolvedEmpUid = Number.isFinite(callerEmp) ? callerEmp : Number(sessionState.empUid ?? -1);
sessionState = { sessionState = {
...sessionState, ...sessionState,
winUid: Number(callerInit.windowUid ?? winUid), winUid: resolvedWinUid,
empUid: Number(callerInit.empUid ?? sessionState.empUid ?? -1), empUid: resolvedEmpUid,
windowName: String(callerInit.windowName ?? ""), windowName: String(callerInit.windowName ?? ""),
empName: String(callerInit.empName ?? ""), empName: String(callerInit.empName ?? ""),
autoCallEnabled: Boolean(callerInit.autoCallEnabled), autoCallEnabled: Boolean(callerInit.autoCallEnabled),

@ -30,7 +30,7 @@ import {
} from "../host/dialog"; } from "../host/dialog";
import { emitTaxerTicketContext, listenMainAction } from "../host/events"; import { emitTaxerTicketContext, listenMainAction } from "../host/events";
import { log } from "../host/logger"; import { log } from "../host/logger";
import { getSession } from "../host/session"; import { getSession, setSession } from "../host/session";
import { startScreenSync, stopScreenSync } from "../host/sync"; import { startScreenSync, stopScreenSync } from "../host/sync";
import type { SessionState } from "../host/types"; import type { SessionState } from "../host/types";
import { import {
@ -60,6 +60,15 @@ const sessionState = ref<SessionState>({
queueToken: "", queueToken: "",
refreshToken: "", refreshToken: "",
}); });
/** 与产品流程一致:账号登录拿到 token + 选窗写入 winUid 之后,才允许等候人数轮询(避免主窗口提前挂载时误启动) */
const sessionReadyForQueueCount = computed(() => {
const t = sessionState.value.queueToken;
const tokenOk = typeof t === "string" && t.trim() !== "";
const w = sessionState.value.winUid;
const winOk = typeof w === "number" && Number.isFinite(w) && w > 0;
return tokenOk && winOk;
});
const textColor = ref("#99ccff"); const textColor = ref("#99ccff");
const iconColor = ref("#dcdfe6"); const iconColor = ref("#dcdfe6");
const message = ref("欢迎使用紫云呼叫终端"); const message = ref("欢迎使用紫云呼叫终端");
@ -72,12 +81,19 @@ let evaluatingCountdownTimer: ReturnType<typeof setInterval> | null = null;
let isRankPollingTimer: ReturnType<typeof setInterval> | null = null; let isRankPollingTimer: ReturnType<typeof setInterval> | null = null;
let isRankPollingBusy = false; let isRankPollingBusy = false;
let queueCountPollingTimer: ReturnType<typeof setInterval> | null = null; let queueCountPollingTimer: ReturnType<typeof setInterval> | null = null;
/** 避免 windowUid 未就绪时每 15s 重复打同一条跳过日志 */
let lastQueueCountSkipInvalidWinLogAt = 0;
/** 避免未登录(无 queueToken时每 15s 重复打同一条跳过日志 */
let lastQueueCountSkipNoTokenLogAt = 0;
let unlistenWindowFocusChanged: (() => void) | null = null; let unlistenWindowFocusChanged: (() => void) | null = null;
let unlistenMainAction: (() => void) | null = null; let unlistenMainAction: (() => void) | null = null;
const EVALUATING_COUNTDOWN_SEC = 15; const EVALUATING_COUNTDOWN_SEC = 15;
const pauseReasonOptions = ["午休", "休息一下", "整理资料", "其他"]; const pauseReasonOptions = ["午休", "休息一下", "整理资料", "其他"];
/** 主界面挂载时即视为可轮询;失焦后由 window blur 置为 false 暂停 */ /**
* 主窗口是否处于前台焦点用于高频 isRank 轮询失焦后暂停以减轻请求
* 等候人数 queue-count 不与该标志绑定待机时后台仍定时拉取
*/
const isMainWindowActive = ref(true); 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);
@ -294,10 +310,11 @@ function clearIsRankPolling(): void {
/** /**
* 清理等待人数轮询 * 清理等待人数轮询
*/ */
function clearQueueCountPolling(): void { function clearQueueCountPolling(reason: string): void {
if (queueCountPollingTimer !== null) { if (queueCountPollingTimer !== null) {
clearInterval(queueCountPollingTimer); clearInterval(queueCountPollingTimer);
queueCountPollingTimer = null; queueCountPollingTimer = null;
void log("info", `queue-count 轮询已停止: ${reason}`);
} }
} }
@ -399,14 +416,59 @@ function startIsRankPolling(): void {
}, 1000); }, 1000);
} }
/**
* 从磁盘会话刷新主界面缓存的 emp/win/token登录在其它窗口完成后焦点回到主窗口时对齐
*/
async function refreshMainSessionStateFromDisk(): Promise<void> {
try {
const session = await getSession();
sessionState.value = {
empUid: parseOptionalNumber(session.empUid),
winUid: parseOptionalNumber(session.winUid),
queueToken:
typeof session.queueToken === "string" && session.queueToken.trim() !== ""
? session.queueToken
: null,
refreshToken:
typeof session.refreshToken === "string" && session.refreshToken.trim() !== ""
? session.refreshToken
: null,
};
} catch (error) {
await logErr("主窗口从磁盘同步 session 失败", error);
}
}
/** /**
* 轮询当前窗口等候人数 * 轮询当前窗口等候人数
*/ */
async function pollQueueCountOnce(): Promise<void> { async function pollQueueCountOnce(): Promise<void> {
try { try {
const winUidFromSession = Number(sessionState.value.winUid ?? -1); const session = await getSession();
const windowUid = winUidFromSession; const token =
if (windowUid <= 0) { typeof session.queueToken === "string" ? session.queueToken.trim() : "";
if (!token) {
const now = Date.now();
if (now - lastQueueCountSkipNoTokenLogAt > 60_000) {
lastQueueCountSkipNoTokenLogAt = now;
await log(
"info",
"GET /call-terminal/queue-count 跳过: 无有效 queueToken可能已登出或未登录不发起请求以免触发 401→refresh→「会话已过期」",
);
}
return;
}
const windowUid = Number(session.winUid ?? -1);
if (!Number.isFinite(windowUid) || windowUid <= 0) {
const now = Date.now();
if (now - lastQueueCountSkipInvalidWinLogAt > 60_000) {
lastQueueCountSkipInvalidWinLogAt = now;
await log(
"info",
`GET /call-terminal/queue-count 跳过: windowUid 无效 (${String(session.winUid ?? "null")}),待 session 就绪后再请求`,
);
}
return; return;
} }
@ -415,6 +477,10 @@ async function pollQueueCountOnce(): Promise<void> {
typeof res.queueCount === "number" typeof res.queueCount === "number"
? res.queueCount ? res.queueCount
: Number(res.count ?? 0); : Number(res.count ?? 0);
await log(
"info",
`GET /call-terminal/queue-count: windowUid=${windowUid}, queueCount=${count}`,
);
message.value = `欢迎使用紫云呼叫终端,当前窗口等候人数:${count}`; message.value = `欢迎使用紫云呼叫终端,当前窗口等候人数:${count}`;
} catch (error) { } catch (error) {
await logErr("查询 getQueueCount 失败", error); await logErr("查询 getQueueCount 失败", error);
@ -425,7 +491,11 @@ async function pollQueueCountOnce(): Promise<void> {
* 开启等待人数轮询 * 开启等待人数轮询
*/ */
function startQueueCountPolling(): void { function startQueueCountPolling(): void {
clearQueueCountPolling(); clearQueueCountPolling("重启前清理");
void log(
"info",
`queue-count 轮询已启动: interval=15000ms, callStatus=${callStatus.value}, winUid=${sessionState.value.winUid ?? "null"}, hasQueueToken=${Boolean(sessionState.value.queueToken && String(sessionState.value.queueToken).trim() !== "")}`,
);
void pollQueueCountOnce(); void pollQueueCountOnce();
queueCountPollingTimer = setInterval(() => { queueCountPollingTimer = setInterval(() => {
void pollQueueCountOnce(); void pollQueueCountOnce();
@ -433,32 +503,28 @@ function startQueueCountPolling(): void {
} }
watch( watch(
callStatus, [callStatus, sessionReadyForQueueCount],
(status) => { ([status, ready]) => {
if (status === "evaluating" && isMainWindowActive.value) { if (status === "evaluating" && isMainWindowActive.value) {
startIsRankPolling(); startIsRankPolling();
} else { } else {
clearIsRankPolling(); clearIsRankPolling();
} }
if (status === "idle" && isMainWindowActive.value) { if (status === "idle" && ready) {
startQueueCountPolling(); startQueueCountPolling();
} else { } else {
clearQueueCountPolling(); clearQueueCountPolling(
status !== "idle"
? `callStatus=${status}`
: "会话未就绪(需 queueToken 且 winUid选窗并打开主窗口后再轮询)",
);
} }
}, },
{ immediate: true }, { immediate: true },
); );
watch(isMainWindowActive, (active) => { watch(isMainWindowActive, (active) => {
if (callStatus.value === "idle") {
if (active) {
startQueueCountPolling();
} else {
clearQueueCountPolling();
}
}
if (callStatus.value === "evaluating") { if (callStatus.value === "evaluating") {
if (active) { if (active) {
startIsRankPolling(); startIsRankPolling();
@ -953,28 +1019,46 @@ async function handleMoreCommand(
onMounted(async () => { onMounted(async () => {
try { try {
const session = await getSession(); const session = await getSession();
let winUid = parseOptionalNumber(session.winUid);
const empUid = parseOptionalNumber(session.empUid);
const queueToken =
typeof session.queueToken === "string" && session.queueToken.trim() !== ""
? session.queueToken
: null;
const refreshToken =
typeof session.refreshToken === "string" && session.refreshToken.trim() !== ""
? session.refreshToken
: null;
/** 登录页会把上次选择的窗口写入 config.json主窗口此前只读 session未做兜底。 */
if (winUid === null || winUid <= 0) {
try {
const config = await getAllConfig();
const fromConfig = parseOptionalNumber(config.selected_win_uid);
if (fromConfig !== null && fromConfig > 0) {
winUid = fromConfig;
await setSession({ ...session, winUid: fromConfig });
await log(
"info",
`MainView 挂载: session 无有效 winUid已从 config.selected_win_uid 补全并写回会话: ${fromConfig}`,
);
}
} catch (error) {
await logErr("读取 config 以补全 winUid 失败", error);
}
}
sessionState.value = { sessionState.value = {
empUid: parseOptionalNumber(session.empUid), empUid,
winUid: parseOptionalNumber(session.winUid), winUid,
queueToken: queueToken,
typeof session.queueToken === "string" && refreshToken,
session.queueToken.trim() !== ""
? session.queueToken
: null,
refreshToken:
typeof session.refreshToken === "string" &&
session.refreshToken.trim() !== ""
? session.refreshToken
: null,
}; };
} catch (error) { } catch (error) {
await logErr("读取 session 失败", error); await logErr("读取 session 失败", error);
} }
// session setup watch winUid // queue-count watch([callStatus, sessionReadyForQueueCount]) idle + token winUid start
if (callStatus.value === "idle" && isMainWindowActive.value) {
startQueueCountPolling();
}
try { try {
const visible = await appWindow.isVisible(); const visible = await appWindow.isVisible();
@ -984,6 +1068,7 @@ onMounted(async () => {
} }
const onFocus = () => { const onFocus = () => {
isMainWindowActive.value = true; isMainWindowActive.value = true;
void refreshMainSessionStateFromDisk();
}; };
const onBlur = () => { const onBlur = () => {
isMainWindowActive.value = false; isMainWindowActive.value = false;
@ -1021,7 +1106,7 @@ onUnmounted(() => {
} }
clearEvaluatingCountdown(); clearEvaluatingCountdown();
clearIsRankPolling(); clearIsRankPolling();
clearQueueCountPolling(); clearQueueCountPolling("组件卸载");
if (unlistenWindowFocusChanged) { if (unlistenWindowFocusChanged) {
unlistenWindowFocusChanged(); unlistenWindowFocusChanged();
} }

@ -340,6 +340,17 @@ async function handleReEvaluate(row: Tkt): Promise<void> {
await log("info", `票池评价按钮点击: ticketUid=${row.ticketUid}, tktNum=${row.tktNum}`); await log("info", `票池评价按钮点击: ticketUid=${row.ticketUid}, tktNum=${row.tktNum}`);
} }
/**
* 与主界面一致账号登录有 token 且已选窗后才自动拉取票池 / 开轮询
*/
async function ticketPoolSessionReady(): Promise<boolean> {
const session = await getSession();
const token =
typeof session.queueToken === "string" ? session.queueToken.trim() : "";
const winUid = Number(session.winUid ?? -1);
return Boolean(token && Number.isFinite(winUid) && winUid > 0);
}
/** /**
* 刷新票池数据 * 刷新票池数据
*/ */
@ -357,6 +368,18 @@ async function refreshData(): Promise<void> {
try { try {
await safeLog("info", "票池刷新: start"); await safeLog("info", "票池刷新: start");
const session = await getSession(); const session = await getSession();
const queueToken =
typeof session.queueToken === "string" ? session.queueToken.trim() : "";
if (!queueToken) {
await safeLog(
"warn",
"票池刷新: 无有效 queueToken已登出或未登录跳过请求以避免 401",
);
allData.value = [];
totalCount.value = 0;
return;
}
const winUid = Number(session.winUid ?? -1); const winUid = Number(session.winUid ?? -1);
if (winUid <= 0) { if (winUid <= 0) {
@ -399,22 +422,31 @@ function scheduleImmediateRefresh(): void {
if (!isTicketWindowActive.value) { if (!isTicketWindowActive.value) {
return; return;
} }
if (focusRefreshTimer !== null) { void (async () => {
clearTimeout(focusRefreshTimer); if (!(await ticketPoolSessionReady())) {
} return;
// show/focus }
focusRefreshTimer = setTimeout(() => { if (focusRefreshTimer !== null) {
void refreshData(); clearTimeout(focusRefreshTimer);
}, 80); }
focusRefreshTimer = setTimeout(() => {
void refreshData();
}, 80);
})();
} }
function startRefreshPolling(): void { function startRefreshPolling(): void {
if (refreshTimer !== null) { void (async () => {
return; if (refreshTimer !== null) {
} return;
refreshTimer = setInterval(() => { }
void refreshData(); if (!(await ticketPoolSessionReady())) {
}, 20000); return;
}
refreshTimer = setInterval(() => {
void refreshData();
}, 20000);
})();
} }
function stopRefreshPolling(): void { function stopRefreshPolling(): void {
@ -424,15 +456,11 @@ function stopRefreshPolling(): void {
} }
} }
function handleWindowFocus(): void {
isTicketWindowActive.value = true;
scheduleImmediateRefresh();
}
function handleVisibilityChange(): void { function handleVisibilityChange(): void {
if (!document.hidden) { if (!document.hidden) {
isTicketWindowActive.value = true; isTicketWindowActive.value = true;
scheduleImmediateRefresh(); scheduleImmediateRefresh();
void startRefreshPolling();
return; return;
} }
isTicketWindowActive.value = false; isTicketWindowActive.value = false;
@ -444,36 +472,39 @@ onMounted(async () => {
await safeLog("info", "TicketListView mounted"); await safeLog("info", "TicketListView mounted");
try { try {
isTicketWindowActive.value = await appWindow.isVisible(); isTicketWindowActive.value = await appWindow.isVisible();
const onFocus = () => {
const onWindowFocus = (): void => {
isTicketWindowActive.value = true; isTicketWindowActive.value = true;
scheduleImmediateRefresh(); scheduleImmediateRefresh();
startRefreshPolling(); void startRefreshPolling();
}; };
const onBlur = () => { const onWindowBlur = (): void => {
isTicketWindowActive.value = false; isTicketWindowActive.value = false;
stopRefreshPolling(); stopRefreshPolling();
}; };
window.addEventListener("focus", onFocus); window.addEventListener("focus", onWindowFocus);
window.addEventListener("blur", onBlur); window.addEventListener("blur", onWindowBlur);
unlistenWindowFocusChanged = () => { unlistenWindowFocusChanged = () => {
window.removeEventListener("focus", onFocus); window.removeEventListener("focus", onWindowFocus);
window.removeEventListener("blur", onBlur); window.removeEventListener("blur", onWindowBlur);
}; };
if (isTicketWindowActive.value) {
scheduleImmediateRefresh();
startRefreshPolling();
}
const session = await getSession(); const session = await getSession();
tableHeight.value = 720 - 220; tableHeight.value = 720 - 220;
if (!session.winUid) { if (!(await ticketPoolSessionReady())) {
await safeLog(
"info",
"票池: 未登录或未选窗,不自动刷新(登录、选窗并打开本窗口后再拉取)",
);
} else if (!session.winUid) {
await safeLog("warn", "票池页面启动时未获取到窗口 UID"); await safeLog("warn", "票池页面启动时未获取到窗口 UID");
} }
window.addEventListener("focus", handleWindowFocus);
document.addEventListener("visibilitychange", handleVisibilityChange); document.addEventListener("visibilitychange", handleVisibilityChange);
if (isTicketWindowActive.value) {
if (isTicketWindowActive.value && (await ticketPoolSessionReady())) {
scheduleImmediateRefresh(); scheduleImmediateRefresh();
startRefreshPolling(); void startRefreshPolling();
} }
} catch (error) { } catch (error) {
await safeLog( await safeLog(
@ -494,7 +525,6 @@ onUnmounted(() => {
if (unlistenWindowFocusChanged) { if (unlistenWindowFocusChanged) {
unlistenWindowFocusChanged(); unlistenWindowFocusChanged();
} }
window.removeEventListener("focus", handleWindowFocus);
document.removeEventListener("visibilitychange", handleVisibilityChange); document.removeEventListener("visibilitychange", handleVisibilityChange);
}); });
</script> </script>

Loading…
Cancel
Save