From 7f8f88c1484759f160ad9eda063f4b39c7c3f598 Mon Sep 17 00:00:00 2001 From: liuwenxin <1009266660@qq.com> Date: Thu, 15 Oct 2026 09:22:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E9=94=99=E8=AF=AF=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E5=8D=95=E4=B8=80=E5=8C=96/=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E5=90=8E=E7=BD=AE/=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=A1=B5=E9=A6=96=E6=AC=A1=E6=88=90=E5=8A=9F=E6=8B=89=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - request.ts blob 失败分支:先解析真实文案再统一提示——此前先弹兜底 「导出失败」再异步补弹真实 message(双提示),401 时跳转提示与 实际原因错位;401 语义文案在导出场景不再失真 - useConfirmAction:收尾(列表刷新)成功后才弹成功提示——此前先弹 「已删除」再刷新,after 失败被吞时成功/失败提示同屏矛盾 - 日志页登录 tab 首次成功加载后不再重复拉取(loginLoadedOk 由 fetcher 成功返回置位,失败切回可重拉,不复活「失败停空表」旧坑) - login_flow.py 密码改从 E2E_ADMIN_PASSWORD 读(与 system_management 同一约定),硬编码默认值在他机上会假绿为「凭据错误」 - e2e 冒烟:登录 8 项 + 系统管理 13 项全过(拦截器全局改动验证) Co-Authored-By: Claude Code --- frontend/e2e/login_flow.py | 6 ++++- frontend/src/api/request.ts | 25 ++++++++++---------- frontend/src/composables/useConfirmAction.ts | 4 +++- frontend/src/views/system/logs/index.vue | 22 ++++++++++++----- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/frontend/e2e/login_flow.py b/frontend/e2e/login_flow.py index e1f60f3..110e4db 100644 --- a/frontend/e2e/login_flow.py +++ b/frontend/e2e/login_flow.py @@ -23,6 +23,8 @@ 公共工具见 `e2e/common.py`。 """ +import os + from playwright.sync_api import Page, sync_playwright from common import ( @@ -36,7 +38,9 @@ from common import ( ) USERNAME = "admin" -PASSWORD = "Admin@2026" +# 与 system_management 同一约定:开发库 admin 密码从环境变量传入(各库不同, +# 硬编码会在他机上假绿为「凭据错误」),缺省保留历史默认值兜底本机老库 +PASSWORD = os.environ.get("E2E_ADMIN_PASSWORD", "Admin@2026") def run(page: Page) -> None: diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts index a973eea..d9f5e50 100644 --- a/frontend/src/api/request.ts +++ b/frontend/src/api/request.ts @@ -63,20 +63,21 @@ service.interceptors.response.use( } return response }, - (error: AxiosError) => { + async (error: AxiosError) => { const status = error.response?.status - let message = error.response?.data?.message ?? '网络异常,请稍后重试' - // blob 请求失败时 data 是 Blob(如导出接口返回的 JSON 信封错误),需读出真实 message - if (error.response?.data instanceof Blob) { + const raw = error.response?.data + let message = (raw as ApiResult | undefined)?.message ?? '网络异常,请稍后重试' + // blob 请求失败时 data 是 Blob(如导出接口返回的 JSON 信封错误): + // 先解析出真实文案再统一提示 —— 先弹兜底再异步补弹会造成双提示, + // 401 时跳转提示也会与实际原因错位 + if (raw instanceof Blob) { message = '导出失败,请稍后重试' - void error.response.data.text().then((text) => { - try { - const body = JSON.parse(text) as { message?: string } - if (body.message) ElMessage.error(body.message) - } catch { - /* 非 JSON 响应,保持兜底提示 */ - } - }) + try { + const body = JSON.parse(await raw.text()) as { message?: string } + if (body.message) message = body.message + } catch { + /* 非 JSON 响应,保持兜底提示 */ + } } if (status === 401) { diff --git a/frontend/src/composables/useConfirmAction.ts b/frontend/src/composables/useConfirmAction.ts index 00943e5..e1d9832 100644 --- a/frontend/src/composables/useConfirmAction.ts +++ b/frontend/src/composables/useConfirmAction.ts @@ -39,8 +39,10 @@ export function useConfirmAction() { try { await options.run() - ElMessage.success(options.successText) await options.after?.() + // 收尾(列表刷新)成功后才提示:先提示后收尾会让用户看到「已删除」 + // 而列表仍留旧行(after 抛错被吞),成功/失败提示同屏矛盾 + ElMessage.success(options.successText) return true } catch (error) { handledError(error) // 失败提示由请求拦截器统一完成 diff --git a/frontend/src/views/system/logs/index.vue b/frontend/src/views/system/logs/index.vue index f3f8747..fad721f 100644 --- a/frontend/src/views/system/logs/index.vue +++ b/frontend/src/views/system/logs/index.vue @@ -9,6 +9,7 @@ import type { OperationLogItem, OperationLogQueryParams, } from '@/api/logs' +import type { PageData } from '@/api/types' import StatusTag from '@/components/table/StatusTag.vue' import AppPagination from '@/components/table/AppPagination.vue' import { usePagedList } from '@/composables/usePagedList' @@ -120,6 +121,15 @@ const loginFilters = reactive({ }) const loginTimeRange = ref<[string, string] | null>(null) +// fetcher 成功返回即置位(抛错不会走到置位行):供 watch 判断「首次成功」, +// 失败后切回 tab 会重拉,不会停在空表 +const loginLoadedOk = ref(false) +async function loginFetcher(params: LoginLogQueryParams): Promise> { + const data = await logApi.listLoginLogs(params) + loginLoadedOk.value = true + return data +} + const { records: loginRecords, total: loginTotal, @@ -130,7 +140,7 @@ const { search: searchLoginLogs, onPageChange: onLoginPageChange, onSizeChange: onLoginSizeChange, -} = usePagedList(logApi.listLoginLogs, () => ({ +} = usePagedList(loginFetcher, () => ({ username: loginFilters.username, ip: loginFilters.ip, success: loginFilters.success, @@ -150,12 +160,12 @@ function resetLoginFilters(): void { onMounted(loadOperationLogs) -// 登录日志 tab 每次激活都刷新(首屏不打多余请求;此前"只加载一次"的 -// 标志在首次失败后不会重置,切回时停在空表且无从自查) +// 登录日志 tab 首次成功加载后不再重复拉取(来回切 tab 的重复请求是纯冗余, +// 筛选/翻页各自触发刷新)。loginLoadedOk 由 fetcher 成功返回时置位: +// 此前「每次激活都刷新」与「无条件只拉一次」两个版本各有代价 —— 前者 +// 冗余请求、后者首次失败后停在空表且无从自查。 watch(activeTab, (tab) => { - if (tab === 'login') { - void loadLoginLogs() - } + if (tab === 'login' && !loginLoadedOk.value) void loadLoginLogs() })