fix(frontend): 错误提示单一化/成功提示后置/日志页首次成功拉取

- 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 <noreply@anthropic.com>
This commit is contained in:
liuwenxin 2026-10-15 09:22:12 +08:00
parent c2b462df3b
commit 7f8f88c148
4 changed files with 37 additions and 20 deletions

View File

@ -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:

View File

@ -63,20 +63,21 @@ service.interceptors.response.use(
}
return response
},
(error: AxiosError<ApiResult>) => {
async (error: AxiosError<ApiResult>) => {
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) {

View File

@ -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) // 失败提示由请求拦截器统一完成

View File

@ -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<PageData<LoginLogItem>> {
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<LoginLogItem, LoginLogQueryParams>(logApi.listLoginLogs, () => ({
} = usePagedList<LoginLogItem, LoginLogQueryParams>(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()
})
</script>