diff --git a/.gitignore b/.gitignore index 594e684ec4ba37215d6510ae2339e102c2282945..ba296ca9b39811492ded34f4ea4c8aaa3959161f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ node_modules dist .vs +docs/* diff --git a/src/api/administratorapi.js b/src/api/administratorapi.js index c64ebd5cfbbab18c1af9e84631b4db6617c69ce2..9e802a652a2b3ce9321f9df5f4e78f1edf7fa944 100644 --- a/src/api/administratorapi.js +++ b/src/api/administratorapi.js @@ -1,9 +1,25 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取管理员列表 +const buildUserNumberPayload = (userNumber) => { + if (userNumber && typeof userNumber === "object") { + return { + UserNumber: userNumber.UserNumber ?? userNumber.userNumber ?? "", + }; + } + + return { UserNumber: userNumber }; +}; + export const fetchAdmins = async (params) => { try { - const response = await api.get("/Admin/GetAllAdminList", { params }); + const response = await api.get( + API_ENDPOINTS.routes.ADMIN_GET_ALL_ADMIN_LIST, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +29,7 @@ export const fetchAdmins = async (params) => { // 添加管理员 export const addAdmin = async (data) => { try { - const response = await api.post("/Admin/AddAdmin", data); + const response = await api.post(API_ENDPOINTS.routes.ADMIN_ADD_ADMIN, data); return response.data; } catch (error) { throw error; @@ -23,7 +39,7 @@ export const addAdmin = async (data) => { // 更新管理员 export const updateAdmin = async (data) => { try { - const response = await api.post(`/Admin/UpdAdmin`, data); + const response = await api.post(API_ENDPOINTS.routes.ADMIN_UPD_ADMIN, data); return response.data; } catch (error) { throw error; @@ -33,7 +49,7 @@ export const updateAdmin = async (data) => { // 删除管理员 export const deleteAdmin = async (data) => { try { - const response = await api.post(`/Admin/DelAdmin`, data); + const response = await api.post(API_ENDPOINTS.routes.ADMIN_DEL_ADMIN, data); return response.data; } catch (error) { throw error; @@ -43,7 +59,10 @@ export const deleteAdmin = async (data) => { // 为用户分配角色(全量覆盖) export const assignUserRoles = async (data) => { try { - const response = await api.post("/Admin/AssignUserRoles", data); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_ASSIGN_USER_ROLES, + data, + ); return response.data; } catch (error) { throw error; @@ -53,9 +72,10 @@ export const assignUserRoles = async (data) => { // 读取指定用户已分配角色编码集合 export const readUserRoles = async (userNumber) => { try { - const response = await api.get("/Admin/ReadUserRoles", { - params: { userNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_READ_USER_ROLES, + buildUserNumberPayload(userNumber), + ); return response.data?.Data?.Items || []; } catch (error) { throw error; @@ -65,9 +85,10 @@ export const readUserRoles = async (userNumber) => { // 读取指定用户的角色-权限明细(来自 RolePermission + Permission) export const readUserRolePermissions = async (userNumber) => { try { - const response = await api.get("/Admin/ReadUserRolePermissions", { - params: { userNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_READ_USER_ROLE_PERMISSIONS, + buildUserNumberPayload(userNumber), + ); return response.data?.Data?.Items || []; } catch (error) { throw error; @@ -77,7 +98,10 @@ export const readUserRolePermissions = async (userNumber) => { // 为用户分配“直接权限”(全量覆盖) export const assignUserPermissions = async (data) => { try { - const response = await api.post("/Admin/AssignUserPermissions", data); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_ASSIGN_USER_PERMISSIONS, + data, + ); return response.data; } catch (error) { throw error; @@ -87,9 +111,10 @@ export const assignUserPermissions = async (data) => { // 读取指定用户的“直接权限”权限编码集合 export const readUserDirectPermissions = async (userNumber) => { try { - const response = await api.get("/Admin/ReadUserDirectPermissions", { - params: { userNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_READ_USER_DIRECT_PERMISSIONS, + buildUserNumberPayload(userNumber), + ); return response.data?.Data?.Items || []; } catch (error) { throw error; diff --git a/src/api/administratortypeapi.js b/src/api/administratortypeapi.js index ab9bec9969359fa5fba8a960c86569765b99d239..1edd1627d16447668721b249b6f77b61bb0f0a27 100644 --- a/src/api/administratortypeapi.js +++ b/src/api/administratortypeapi.js @@ -1,9 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取管理员类型列表 export const fetchAdminTypes = async (params) => { try { - const response = await api.get("/Admin/GetAllAdminTypes", { params }); + const response = await api.get( + API_ENDPOINTS.routes.ADMIN_GET_ALL_ADMIN_TYPES, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +17,10 @@ export const fetchAdminTypes = async (params) => { // 添加管理员类型 export const addAdminType = async (data) => { try { - const response = await api.post("/Admin/AddAdminType", data); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_ADD_ADMIN_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +30,10 @@ export const addAdminType = async (data) => { // 更新管理员类型 export const updateAdminType = async (data) => { try { - const response = await api.post(`/Admin/UpdAdminType`, data); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_UPD_ADMIN_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +43,10 @@ export const updateAdminType = async (data) => { // 删除管理员类型 export const deleteAdminType = async (data) => { try { - const response = await api.post(`/Admin/DelAdminType`, data); + const response = await api.post( + API_ENDPOINTS.routes.ADMIN_DEL_ADMIN_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/baseapi.js b/src/api/baseapi.js index a5035903294b6e065b85cfd0174aafb9e233c2d7..a9a9e9cb359c3ef75f239fc4d6818cf61650a5b5 100644 --- a/src/api/baseapi.js +++ b/src/api/baseapi.js @@ -1,6 +1,24 @@ import axios from "axios"; import { csrfTokenManager } from "@/utils/csrf"; import { handleApiError, handleHttpError } from "@/common/errorHandler"; +import { ERROR_CODES } from "@/common/errorCodes"; +import { getStoredToken } from "@/utils/tokenStorage"; + +const ACTION_PREFIX_REGEX = + /^(Select|GetAll|Get|Build|Read|Insert|Add|Create|Update|Upd|Delete|Del)/i; +const SUFFIX_CLEANUP_REGEX = /(AllCanUse|CanUseAll|All|List)$/i; +const ROW_VERSION_CACHE_TTL_MS = 10 * 60 * 1000; +const ROW_VERSION_CACHE_MAX_ENTITIES = 200; +const ROW_VERSION_CONFLICT_MESSAGE = i18n.global.t("concurrencyConflict"); +const ROW_VERSION_CONFLICT_HINT = i18n.global.t("concurrencyConflictHint"); + +const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key"; +const CSRF_TOKEN_HEADER = "X-CSRF-TOKEN-HEADER"; +const NON_IDEMPOTENT_METHODS = new Set(["post", "put", "patch"]); +const IDEMPOTENT_METHODS = new Set(["get", "head", "options", "delete"]); + +// entityKey -> { rows: Map, touchedAt: number } +const rowVersionCache = new Map(); const api = axios.create({ baseURL: import.meta.env.VITE_API_URL, @@ -24,80 +42,672 @@ export const isApiSuccess = (payload) => { export const getApiMessage = (payload) => payload?.Message || payload?.message || "Request failed"; -// 创建一个函数来设置认证头 +export const clearRowVersionCache = (entityKey) => { + if (entityKey) { + rowVersionCache.delete(entityKey); + return; + } + + rowVersionCache.clear(); +}; + +const touchRowVersionCacheEntity = (entityKey) => { + if (!entityKey) { + return; + } + + const existingEntry = rowVersionCache.get(entityKey); + if (!existingEntry) { + return; + } + + existingEntry.touchedAt = Date.now(); + // Move to tail so Map order reflects LRU (tail = most recently used). + rowVersionCache.delete(entityKey); + rowVersionCache.set(entityKey, existingEntry); +}; + +const pruneRowVersionCache = () => { + const now = Date.now(); + + for (const [entityKey, cacheEntry] of rowVersionCache.entries()) { + const lastTouchedAt = Number(cacheEntry?.touchedAt || 0); + if (!lastTouchedAt || now - lastTouchedAt > ROW_VERSION_CACHE_TTL_MS) { + clearRowVersionCache(entityKey); + } + } + + // Evict from head while overflow exists (head = least recently used). + while (rowVersionCache.size > ROW_VERSION_CACHE_MAX_ENTITIES) { + const lruEntityKey = rowVersionCache.keys().next().value; + if (!lruEntityKey) { + break; + } + clearRowVersionCache(lruEntityKey); + } +}; + +const getEntityKeyFromUrl = (url = "") => { + if (typeof url !== "string") { + return ""; + } + + const trimmedUrl = url.trim(); + if (!trimmedUrl) { + return ""; + } + + const normalizedUrl = trimmedUrl + .split("?")[0] + .replace(/^https?:\/\/[^/]+/i, "") + .trim(); + const segments = normalizedUrl.split("/").filter(Boolean); + const controller = segments[0]?.toLowerCase() || ""; + const action = (segments[1] || "").trim(); + + if (!controller) { + return ""; + } + if (!action) { + return controller; + } + + let resource = action + .replace(ACTION_PREFIX_REGEX, "") + .replace(SUFFIX_CLEANUP_REGEX, "") + .replace(/InfoBy[A-Za-z0-9]+$/i, "") + .replace(/Info$/i, "") + .replace(/Types$/i, "Type"); + + if (/[^s]s$/i.test(resource)) { + resource = resource.slice(0, -1); + } + + resource = resource + .trim() + .replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "") + .toLowerCase(); + + return `${controller}:${resource || controller}`; +}; + +const getRecordId = (record) => { + if (!record || typeof record !== "object") { + return null; + } + return record.Id ?? record.id ?? null; +}; + +const getRecordRowVersion = (record) => { + if (!record || typeof record !== "object") { + return null; + } + return ( + record.RowVersion ?? + record.rowVersion ?? + record.rowversion ?? + record.row_version ?? + null + ); +}; + +const getDeleteItemsFromPayload = (payload) => { + if (!payload || typeof payload !== "object") { + return []; + } + + const candidateList = [payload.DelIds, payload.delIds]; + + for (const candidate of candidateList) { + if (!Array.isArray(candidate)) { + continue; + } + + return candidate + .map((item) => { + if (!item || typeof item !== "object" || Array.isArray(item)) { + if ( + item === undefined || + item === null || + String(item).trim() === "" + ) { + return null; + } + return { Id: item, RowVersion: null }; + } + + const itemId = getRecordId(item); + if (itemId === null) { + return null; + } + + return { + Id: itemId, + RowVersion: getRecordRowVersion(item), + }; + }) + .filter(Boolean); + } + + const scalarCandidateList = [payload.DelId, payload.delId]; + for (const candidate of scalarCandidateList) { + if ( + candidate !== undefined && + candidate !== null && + String(candidate).trim() !== "" + ) { + return [{ Id: candidate, RowVersion: null }]; + } + } + + return []; +}; + +const isEffectiveRowVersion = (value) => { + if (value === undefined || value === null) { + return false; + } + + if (typeof value === "number") { + return Number.isFinite(value) && value > 0; + } + + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) { + return false; + } + const asNumber = Number(trimmed); + if (!Number.isNaN(asNumber)) { + return asNumber > 0; + } + return true; + } + + return true; +}; + +const hasEffectiveRowVersion = (record) => { + if (!record || typeof record !== "object") { + return false; + } + + return ( + isEffectiveRowVersion(record.RowVersion) || + isEffectiveRowVersion(record.rowVersion) || + isEffectiveRowVersion(record.rowversion) || + isEffectiveRowVersion(record.row_version) + ); +}; + +const getCachedRowVersion = (entityKey, id) => { + if (!entityKey || id === null || id === undefined) { + return null; + } + + const cacheEntry = rowVersionCache.get(entityKey); + const rowVersion = cacheEntry?.rows?.get(String(id)); + if (rowVersion === undefined || rowVersion === null) { + return null; + } + + touchRowVersionCacheEntity(entityKey); + return rowVersion; +}; + +const saveRowVersionToCache = (entityKey, record) => { + if (!entityKey || !record || typeof record !== "object") { + return; + } + + const id = getRecordId(record); + const rowVersion = getRecordRowVersion(record); + if (id === null || rowVersion === null) { + return; + } + + if (!rowVersionCache.has(entityKey)) { + rowVersionCache.set(entityKey, { + rows: new Map(), + touchedAt: Date.now(), + }); + } + + rowVersionCache.get(entityKey).rows.set(String(id), rowVersion); + touchRowVersionCacheEntity(entityKey); +}; + +const saveRowVersionsFromResponse = (response) => { + const entityKey = getEntityKeyFromUrl(response?.config?.url); + if (!entityKey) { + return; + } + + const payload = response?.data; + const data = payload?.Data ?? payload?.data; + if (!data) { + return; + } + + const items = data?.Items ?? data?.items; + if (Array.isArray(items)) { + items.forEach((item) => saveRowVersionToCache(entityKey, item)); + pruneRowVersionCache(); + return; + } + + if (Array.isArray(data)) { + data.forEach((item) => saveRowVersionToCache(entityKey, item)); + pruneRowVersionCache(); + return; + } + + if (typeof data === "object") { + saveRowVersionToCache(entityKey, data); + pruneRowVersionCache(); + } +}; + +const shouldAttachRowVersion = (config) => { + const method = (config?.method || "get").toLowerCase(); + if (!["post", "put", "patch"].includes(method)) { + return false; + } + + return true; +}; + +const shouldClearRowVersionCacheAfterMutation = (config) => { + const method = (config?.method || "get").toLowerCase(); + if (["put", "patch", "delete"].includes(method)) { + return true; + } + + if (method !== "post") { + return false; + } + + const normalizedUrl = String(config?.url || "") + .split("?")[0] + .replace(/^https?:\/\/[^/]+/i, "") + .trim(); + const segments = normalizedUrl.split("/").filter(Boolean); + const action = (segments[1] || "").trim(); + + return /^(Insert|Add|Create|Update|Upd|Delete|Del)/i.test(action); +}; + +const clearRowVersionCacheAfterMutation = (response) => { + if (!shouldClearRowVersionCacheAfterMutation(response?.config)) { + return; + } + + const entityKey = getEntityKeyFromUrl(response?.config?.url); + if (!entityKey) { + return; + } + + clearRowVersionCache(entityKey); +}; + +const attachRowVersionFromCache = (config) => { + if (!shouldAttachRowVersion(config)) { + return config; + } + + const data = config?.data; + if (!data || typeof data !== "object" || Array.isArray(data)) { + return config; + } + + if (data instanceof FormData) { + return config; + } + + const entityKey = getEntityKeyFromUrl(config.url); + if (!entityKey) { + return config; + } + + const deleteItems = getDeleteItemsFromPayload(data); + if (deleteItems.length > 0) { + const unresolvedIds = []; + const resolvedDeleteItems = deleteItems + .map((item) => { + let rowVersion = item.RowVersion; + if (!isEffectiveRowVersion(rowVersion)) { + rowVersion = getCachedRowVersion(entityKey, item.Id); + } + + if (!isEffectiveRowVersion(rowVersion)) { + unresolvedIds.push(item.Id); + return null; + } + + return { + Id: item.Id, + RowVersion: rowVersion, + }; + }) + .filter(Boolean); + + if (unresolvedIds.length > 0) { + const conflictError = new Error( + ROW_VERSION_CONFLICT_HINT || ROW_VERSION_CONFLICT_MESSAGE, + ); + conflictError.code = ERROR_CODES.CONCURRENCY_CONFLICT; + conflictError.data = { unresolvedIds }; + throw conflictError; + } + + data.DelIds = resolvedDeleteItems; + delete data.DelId; + delete data.delId; + delete data.DelRows; + delete data.RowVersions; + touchRowVersionCacheEntity(entityKey); + return config; + } + + const id = getRecordId(data); + if (id === null) { + return config; + } + if (hasEffectiveRowVersion(data)) { + return config; + } + + const rowVersion = getCachedRowVersion(entityKey, id); + if (rowVersion === null) { + return config; + } + + touchRowVersionCacheEntity(entityKey); + data.RowVersion = rowVersion; + return config; +}; + +const getNormalizedMethod = (method) => + String(method || "get") + .trim() + .toLowerCase(); + +const isNonIdempotentRequest = (config) => + NON_IDEMPOTENT_METHODS.has(getNormalizedMethod(config?.method)); + +const cloneRequestData = (data) => { + if (data === null || data === undefined) { + return data; + } + + if (typeof FormData !== "undefined" && data instanceof FormData) { + const cloned = new FormData(); + data.forEach((value, key) => { + cloned.append(key, value); + }); + return cloned; + } + + if ( + typeof URLSearchParams !== "undefined" && + data instanceof URLSearchParams + ) { + return new URLSearchParams(data.toString()); + } + + if (typeof structuredClone === "function") { + try { + return structuredClone(data); + } catch { + // Fallback below. + } + } + + if (typeof data === "object") { + try { + return JSON.parse(JSON.stringify(data)); + } catch { + // Keep original when cloning is not possible. + } + } + + return data; +}; + +const cacheRawRequestData = (config) => { + if (!config || Object.prototype.hasOwnProperty.call(config, "__rawRequestData")) { + return config; + } + + config.__rawRequestData = cloneRequestData(config.data); + return config; +}; + +const restoreRawRequestData = (config) => { + if (!config || !Object.prototype.hasOwnProperty.call(config, "__rawRequestData")) { + return config; + } + + config.data = cloneRequestData(config.__rawRequestData); + return config; +}; + +const ensureHeaders = (config) => { + config.headers ||= {}; + return config.headers; +}; + +const setHeaderValue = (config, headerName, headerValue) => { + const headers = ensureHeaders(config); + if (typeof headers.set === "function") { + headers.set(headerName, headerValue); + return; + } + + headers[headerName] = headerValue; +}; + +const getHeaderValue = (config, headerName) => { + const headers = config?.headers; + if (!headers) { + return ""; + } + + if (typeof headers.get === "function") { + return ( + headers.get(headerName) || headers.get(headerName.toLowerCase()) || "" + ); + } + + const direct = + headers[headerName] || + headers[headerName.toLowerCase()] || + headers[headerName.toUpperCase()]; + if (direct) { + return direct; + } + + const matchedKey = Object.keys(headers).find( + (key) => key.toLowerCase() === headerName.toLowerCase(), + ); + return matchedKey ? headers[matchedKey] : ""; +}; + +const generateIdempotencyKey = () => { + if ( + typeof crypto !== "undefined" && + typeof crypto.randomUUID === "function" + ) { + return crypto.randomUUID(); + } + + return `idem-${Date.now()}-${Math.random().toString(16).slice(2)}`; +}; + +const attachIdempotencyKey = (config) => { + if (!isNonIdempotentRequest(config)) { + return config; + } + + const existingKey = String( + config.__idempotencyKey || getHeaderValue(config, IDEMPOTENCY_KEY_HEADER) || "", + ).trim(); + const idempotencyKey = existingKey || generateIdempotencyKey(); + + config.__idempotencyKey = idempotencyKey; + setHeaderValue(config, IDEMPOTENCY_KEY_HEADER, idempotencyKey); + return config; +}; + +const canRetryRequest = (config) => { + const method = getNormalizedMethod(config?.method); + + if (IDEMPOTENT_METHODS.has(method)) { + return true; + } + if (!NON_IDEMPOTENT_METHODS.has(method)) { + return false; + } + + const idempotencyKey = String( + config?.__idempotencyKey || + getHeaderValue(config, IDEMPOTENCY_KEY_HEADER) || + "", + ).trim(); + + // Non-idempotent requests are retried only when a stable idempotency key exists. + return idempotencyKey.length > 0; +}; + +const refreshRetrySensitiveFields = async (config, error) => { + restoreRawRequestData(config); + + const statusCode = Number(error?.response?.status || 0); + const shouldRefreshCsrf = isNonIdempotentRequest(config) || statusCode === 403; + + if (shouldRefreshCsrf) { + try { + await csrfTokenManager.refreshToken({ silent: true }); + } catch { + // Keep fallback path below with existing token/cookie value. + } + } + + const csrfToken = csrfTokenManager.getToken(); + if (csrfToken) { + setHeaderValue(config, CSRF_TOKEN_HEADER, csrfToken); + } +}; + const setAuthorizationHeader = (config) => { - const token = localStorage.getItem("token"); + const token = getStoredToken(); if (token) { - config.headers.Authorization = `Bearer ${token}`; + setHeaderValue(config, "Authorization", `Bearer ${token}`); } return config; }; -// 创建一个重试机制 const retryRequest = async (error) => { - const failedRequest = error.config; + const failedRequest = error?.config; + if (!failedRequest) { + return Promise.reject(error); + } - // 如果已经重试过,则不再重试 if (failedRequest._retry) { return Promise.reject(error); } - // 标记这个请求已经重试过 + if (!canRetryRequest(failedRequest)) { + return Promise.reject(error); + } + failedRequest._retry = true; + await refreshRetrySensitiveFields(failedRequest, error); - // 重新从 localStorage 获取 token - const token = localStorage.getItem("token"); + const token = getStoredToken(); if (token) { - failedRequest.headers.Authorization = `Bearer ${token}`; + setHeaderValue(failedRequest, "Authorization", `Bearer ${token}`); return api(failedRequest); } return Promise.reject(error); }; +const toBusinessErrorPayload = (error) => { + const code = Number(error?.code || error?.Code); + if (!Number.isFinite(code)) { + return null; + } + + return { + Success: false, + Code: code, + Message: error?.message || "", + Data: error?.data, + }; +}; + api.interceptors.request.use( async (config) => { - // 设置认证头 + const token = getStoredToken(); + if (!token && rowVersionCache.size > 0) { + clearRowVersionCache(); + } else { + pruneRowVersionCache(); + } + + config = cacheRawRequestData(config); config = setAuthorizationHeader(config); + config = attachIdempotencyKey(config); + config = attachRowVersionFromCache(config); - // 设置 CSRF Token const csrfToken = csrfTokenManager.getToken(); if (csrfToken) { - config.headers["X-CSRF-TOKEN-HEADER"] = csrfToken; + setHeaderValue(config, CSRF_TOKEN_HEADER, csrfToken); } return config; }, async (error) => { - // 检查是否是 token 相关错误 if (error.isTokenError) { return retryRequest(error); } - // 其他错误直接拒绝 + const localBusinessError = toBusinessErrorPayload(error); + if (localBusinessError) { + return handleApiError(localBusinessError, { + config: error?.config || {}, + }); + } + return Promise.reject(error); }, ); api.interceptors.response.use( (response) => { + saveRowVersionsFromResponse(response); + const resData = response.data; + const hasBusinessFlag = + typeof resData?.Success === "boolean" || + typeof resData?.Code === "number"; + if (response?.config?.skipBusinessErrorHandling) { + if (!hasBusinessFlag || isApiSuccess(resData)) { + clearRowVersionCacheAfterMutation(response); + } return response; } - const resData = response.data; if (typeof resData === "string" && resData.includes("Version")) { return response; } - const hasBusinessFlag = - typeof resData?.Success === "boolean" || - typeof resData?.Code === "number"; - if (hasBusinessFlag && !isApiSuccess(resData)) { return handleApiError(resData, response); } + clearRowVersionCacheAfterMutation(response); return response; }, (error) => { @@ -109,4 +719,45 @@ api.interceptors.response.use( }, ); +const handleBeforeUnload = () => { + clearRowVersionCache(); +}; + +const handleStorageChange = (event) => { + if (event.key === "token" && !event.newValue) { + clearRowVersionCache(); + } +}; + +let hasBoundWindowListeners = false; + +const bindWindowListeners = () => { + if (typeof window === "undefined" || hasBoundWindowListeners) { + return; + } + + window.addEventListener("beforeunload", handleBeforeUnload); + window.addEventListener("storage", handleStorageChange); + hasBoundWindowListeners = true; +}; + +export const unbindWindowListeners = () => { + if (typeof window === "undefined" || !hasBoundWindowListeners) { + return; + } + + window.removeEventListener("beforeunload", handleBeforeUnload); + window.removeEventListener("storage", handleStorageChange); + hasBoundWindowListeners = false; +}; + +bindWindowListeners(); + +if (import.meta.hot) { + import.meta.hot.dispose(() => { + unbindWindowListeners(); + clearRowVersionCache(); + }); +} + export default api; diff --git a/src/api/basicapi.js b/src/api/basicapi.js index 9389e980db0421428af58c7274dd6827daab0a04..b442f33f8d5ad83012891f66f56075355fd6ae0a 100644 --- a/src/api/basicapi.js +++ b/src/api/basicapi.js @@ -1,19 +1,26 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; +import { isTokenInvalidBusinessCode } from "@/common/errorCodes"; const LOGIN_ENDPOINTS = { - admin: "/Admin/Login", - employee: "/Employee/EmployeeLogin", + admin: API_ENDPOINTS.login.admin, + employee: API_ENDPOINTS.login.employee, }; const TWO_FACTOR_CONTROLLERS = { - admin: "Admin", - employee: "Employee", + admin: API_ENDPOINTS.twoFactor.admin, + employee: API_ENDPOINTS.twoFactor.employee, }; -const resolveLoginEndpoint = (loginType = "admin") => +const LOGIN_TYPES = { + admin: API_ENDPOINTS.loginTypes.admin, + employee: API_ENDPOINTS.loginTypes.employee, +}; + +const resolveLoginEndpoint = (loginType = LOGIN_TYPES.admin) => LOGIN_ENDPOINTS[loginType] || LOGIN_ENDPOINTS.admin; -const resolveTwoFactorController = (loginType = "admin") => +const resolveTwoFactorController = (loginType = LOGIN_TYPES.admin) => TWO_FACTOR_CONTROLLERS[loginType] || TWO_FACTOR_CONTROLLERS.admin; const toErrorPayload = (error) => @@ -34,10 +41,11 @@ export const getApiMessage = (payload) => payload?.Message || payload?.message || "Request failed"; export const isTwoFactorChallenge = (payload) => - payload?.Code === 1401 && payload?.Data?.RequiresTwoFactor === true; + isTokenInvalidBusinessCode(payload?.Code) && + payload?.Data?.RequiresTwoFactor === true; // 登录 -export const signIn = async (data, loginType = "admin") => { +export const signIn = async (data, loginType = LOGIN_TYPES.admin) => { try { const response = await api.post(resolveLoginEndpoint(loginType), data, { skipBusinessErrorHandling: true, @@ -52,7 +60,7 @@ export const signIn = async (data, loginType = "admin") => { // 登出 export const signOut = async (data) => { try { - const response = await api.post("/Admin/Logout", data); + const response = await api.post(API_ENDPOINTS.routes.ADMIN_LOGOUT, data); const resData = response.data; if (isApiSuccess(resData)) { return resData; @@ -66,29 +74,32 @@ export const signOut = async (data) => { // 获取服务器版本号 export const getServerVersion = async () => { - const response = await api.get("/version"); + const response = await api.get(API_ENDPOINTS.routes.VERSION); const resData = response.data; return resData; }; -export const fetchTwoFactorStatus = async (loginType = "admin") => { +export const fetchTwoFactorStatus = async (loginType = LOGIN_TYPES.admin) => { try { const controller = resolveTwoFactorController(loginType); - const response = await api.get(`/${controller}/GetTwoFactorStatus`, { - skipBusinessErrorHandling: true, - skipHttpErrorHandling: true, - }); + const response = await api.get( + API_ENDPOINTS.dynamic.getTwoFactorStatus(controller), + { + skipBusinessErrorHandling: true, + skipHttpErrorHandling: true, + }, + ); return response.data; } catch (error) { throw toErrorPayload(error); } }; -export const generateTwoFactorSetup = async (loginType = "admin") => { +export const generateTwoFactorSetup = async (loginType = LOGIN_TYPES.admin) => { try { const controller = resolveTwoFactorController(loginType); const response = await api.post( - `/${controller}/GenerateTwoFactorSetup`, + API_ENDPOINTS.dynamic.generateTwoFactorSetup(controller), {}, { skipBusinessErrorHandling: true, @@ -102,13 +113,13 @@ export const generateTwoFactorSetup = async (loginType = "admin") => { }; export const enableTwoFactor = async ( - loginType = "admin", + loginType = LOGIN_TYPES.admin, verificationCode = "", ) => { try { const controller = resolveTwoFactorController(loginType); const response = await api.post( - `/${controller}/EnableTwoFactor`, + API_ENDPOINTS.dynamic.enableTwoFactor(controller), { VerificationCode: verificationCode }, { skipBusinessErrorHandling: true, @@ -122,13 +133,13 @@ export const enableTwoFactor = async ( }; export const disableTwoFactor = async ( - loginType = "admin", + loginType = LOGIN_TYPES.admin, verificationCode = "", ) => { try { const controller = resolveTwoFactorController(loginType); const response = await api.post( - `/${controller}/DisableTwoFactor`, + API_ENDPOINTS.dynamic.disableTwoFactor(controller), { VerificationCode: verificationCode }, { skipBusinessErrorHandling: true, @@ -142,13 +153,13 @@ export const disableTwoFactor = async ( }; export const regenerateTwoFactorRecoveryCodes = async ( - loginType = "admin", + loginType = LOGIN_TYPES.admin, verificationCode = "", ) => { try { const controller = resolveTwoFactorController(loginType); const response = await api.post( - `/${controller}/RegenerateTwoFactorRecoveryCodes`, + API_ENDPOINTS.dynamic.regenerateTwoFactorRecoveryCodes(controller), { VerificationCode: verificationCode }, { skipBusinessErrorHandling: true, diff --git a/src/api/csrfapi.js b/src/api/csrfapi.js index 8e327bdb64185b37f02ee7172b894e1ba67f0387..bfcc637aa224d32a2cd34a62ecc233e716f4d618 100644 --- a/src/api/csrfapi.js +++ b/src/api/csrfapi.js @@ -1,8 +1,9 @@ import api from "./index"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; export const getCsrfToken = async () => { try { - const response = await api.get("/Login/GetCSRFToken"); + const response = await api.get(API_ENDPOINTS.routes.LOGIN_GET_CSRF_TOKEN); if (!response.data.Success) { throw new Error(response.data.Message); } @@ -14,7 +15,9 @@ export const getCsrfToken = async () => { export const refreshCsrfToken = async () => { try { - const response = await api.get("/Login/RefreshCSRFToken"); + const response = await api.get( + API_ENDPOINTS.routes.LOGIN_REFRESH_CSRF_TOKEN, + ); if (!response.data.Success) { throw new Error(response.data.Message); } diff --git a/src/api/customerapi.js b/src/api/customerapi.js index e45e1fcb61be7182d2317103972e76feb7aa344f..5cf821d401baa0a878233d6891077e731a30c803 100644 --- a/src/api/customerapi.js +++ b/src/api/customerapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取客户列表 export const fetchCustomers = async (params) => { try { - const response = await api.get("/Customer/SelectCustomers", { params }); + const response = await api.get( + API_ENDPOINTS.routes.CUSTOMER_SELECT_CUSTOMERS, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchCustomers = async (params) => { // 添加客户 export const addCustomer = async (data) => { try { - const response = await api.post("/Customer/InsertCustomerInfo", data); + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_INSERT_CUSTOMER_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addCustomer = async (data) => { // 更新客户 export const updateCustomer = async (data) => { try { - const response = await api.post(`/Customer/UpdCustomerInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_UPD_CUSTOMER_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateCustomer = async (data) => { // 删除客户 export const deleteCustomer = async (data) => { try { - const response = await api.post(`/Customer/DelCustomerInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_DEL_CUSTOMER_INFO, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/customerpermissionapi.js b/src/api/customerpermissionapi.js index 2bfdfa84f988e3acc31f312999ac09ba320c1d6e..23b207e926740847e122ba365f625881714d17a8 100644 --- a/src/api/customerpermissionapi.js +++ b/src/api/customerpermissionapi.js @@ -1,11 +1,23 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 读取指定客户已分配的角色(返回角色编码集合) +const buildUserNumberPayload = (customerNumber) => { + if (customerNumber && typeof customerNumber === "object") { + return { + UserNumber: customerNumber.UserNumber ?? customerNumber.userNumber ?? "", + }; + } + + return { UserNumber: customerNumber }; +}; + export const readUserRoles = async (customerNumber) => { try { - const response = await api.get("/CustomerPermission/ReadUserRoles", { - params: { userNumber: customerNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_PERMISSION_READ_USER_ROLES, + buildUserNumberPayload(customerNumber), + ); return response.data?.Data?.Items || []; } catch (error) { throw error; @@ -16,7 +28,7 @@ export const readUserRoles = async (customerNumber) => { export const assignUserRoles = async (data) => { try { const response = await api.post( - "/CustomerPermission/AssignUserRoles", + API_ENDPOINTS.routes.CUSTOMER_PERMISSION_ASSIGN_USER_ROLES, data, ); return response.data; @@ -28,11 +40,9 @@ export const assignUserRoles = async (data) => { // 读取客户(通过角色)所拥有的权限明细(列表) export const readUserRolePermissions = async (customerNumber) => { try { - const response = await api.get( - "/CustomerPermission/ReadUserRolePermissions", - { - params: { userNumber: customerNumber }, - }, + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_PERMISSION_READ_USER_ROLE_PERMISSIONS, + buildUserNumberPayload(customerNumber), ); return response.data?.Data?.Items || []; } catch (error) { @@ -43,11 +53,9 @@ export const readUserRolePermissions = async (customerNumber) => { // 读取客户“直接权限”(非角色继承)权限编码集合 export const readUserDirectPermissions = async (customerNumber) => { try { - const response = await api.get( - "/CustomerPermission/ReadUserDirectPermissions", - { - params: { userNumber: customerNumber }, - }, + const response = await api.post( + API_ENDPOINTS.routes.CUSTOMER_PERMISSION_READ_USER_DIRECT_PERMISSIONS, + buildUserNumberPayload(customerNumber), ); return response.data?.Data?.Items || []; } catch (error) { @@ -59,7 +67,7 @@ export const readUserDirectPermissions = async (customerNumber) => { export const assignUserPermissions = async (data) => { try { const response = await api.post( - "/CustomerPermission/AssignUserPermissions", + API_ENDPOINTS.routes.CUSTOMER_PERMISSION_ASSIGN_USER_PERMISSIONS, data, ); return response.data; diff --git a/src/api/customertypeapi.js b/src/api/customertypeapi.js index 5ccd0f68b59269fb453f5e4b776c3dff0f4469ef..9e184d1e0edb56e42410fece797aed58fd940524 100644 --- a/src/api/customertypeapi.js +++ b/src/api/customertypeapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取客户类型列表 export const fetchCustomerTypes = async (params) => { try { - const response = await api.get("/Base/SelectCustoTypeAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_CUSTO_TYPE_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchCustomerTypes = async (params) => { // 添加客户类型 export const addCustomerType = async (data) => { try { - const response = await api.post("/Base/InsertCustoType", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_INSERT_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addCustomerType = async (data) => { // 更新客户类型 export const updateCustomerType = async (data) => { try { - const response = await api.post(`/Base/UpdateCustoType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPDATE_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateCustomerType = async (data) => { // 删除客户类型 export const deleteCustomerType = async (data) => { try { - const response = await api.post(`/Base/DeleteCustoType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DELETE_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/custotypeapi.js b/src/api/custotypeapi.js index 7e9445f8c4cabbcbb5579138ceb0f5f52773ac6b..5a28df8ae1f2757d1df567cb39d79b235eef5472 100644 --- a/src/api/custotypeapi.js +++ b/src/api/custotypeapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取客户类型列表 export const fetchCustomerTypes = async (params) => { try { - const response = await api.get("/Base/SelectCustoTypeAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_CUSTO_TYPE_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,9 +19,10 @@ export const fetchCustomerTypes = async (params) => { // 获取可用客户类型列表 export const fetchCanUseCustomerTypes = async (params) => { try { - const response = await api.get("/Base/SelectCustoTypeAllCanUse", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_CUSTO_TYPE_ALL_CAN_USE, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -25,7 +32,10 @@ export const fetchCanUseCustomerTypes = async (params) => { // 添加客户类型 export const addCustomerType = async (data) => { try { - const response = await api.post("/Base/InsertCustoType", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_INSERT_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -35,7 +45,10 @@ export const addCustomerType = async (data) => { // 更新客户类型 export const updateCustomerType = async (data) => { try { - const response = await api.post(`/Base/UpdateCustoType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPDATE_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -45,7 +58,10 @@ export const updateCustomerType = async (data) => { // 删除客户类型 export const deleteCustomerType = async (data) => { try { - const response = await api.post(`/Base/DeleteCustoType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DELETE_CUSTO_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/dashboardapi.js b/src/api/dashboardapi.js index 74910e16db319c10f758933633269a4cc7098b54..85bb05aac6c4f0e137679b91118db8b117770743 100644 --- a/src/api/dashboardapi.js +++ b/src/api/dashboardapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取房间统计信息 export const fetchRoomStatistics = async (params) => { try { - const response = await api.get("/Dashboard/RoomStatistics", { params }); + const response = await api.get( + API_ENDPOINTS.routes.DASHBOARD_ROOM_STATISTICS, + { + params, + }, + ); return response.data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchRoomStatistics = async (params) => { // 获取业务统计信息 export const fetchBusinessStatistics = async (params) => { try { - const response = await api.get("/Dashboard/BusinessStatistics", { params }); + const response = await api.get( + API_ENDPOINTS.routes.DASHBOARD_BUSINESS_STATISTICS, + { params }, + ); return response.data; } catch (error) { throw error; @@ -23,9 +32,10 @@ export const fetchBusinessStatistics = async (params) => { // 获取后勤统计信息 export const fetchLogisticsStatistics = async (params) => { try { - const response = await api.get("/Dashboard/LogisticsStatistics", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.DASHBOARD_LOGISTICS_STATISTICS, + { params }, + ); return response.data; } catch (error) { throw error; @@ -35,9 +45,10 @@ export const fetchLogisticsStatistics = async (params) => { // 获取人事统计信息 export const fetchHumanResourcesStatistics = async (params) => { try { - const response = await api.get("/Dashboard/HumanResourcesStatistics", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.DASHBOARD_HUMAN_RESOURCES_STATISTICS, + { params }, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/departmentapi.js b/src/api/departmentapi.js index 060545a433fe638a4aca844e6c510695a27dc894..902710f7e93ded7351b6d8912b5749414c801d7c 100644 --- a/src/api/departmentapi.js +++ b/src/api/departmentapi.js @@ -1,9 +1,12 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取部门列表 export const fetchDepartments = async (params) => { try { - const response = await api.get("/Base/SelectDeptAll", { params }); + const response = await api.get(API_ENDPOINTS.routes.BASE_SELECT_DEPT_ALL, { + params, + }); return response.data.Data; } catch (error) { throw error; @@ -13,7 +16,7 @@ export const fetchDepartments = async (params) => { // 添加部门 export const addDepartment = async (data) => { try { - const response = await api.post("/Base/AddDept", data); + const response = await api.post(API_ENDPOINTS.routes.BASE_ADD_DEPT, data); return response.data; } catch (error) { throw error; @@ -23,7 +26,7 @@ export const addDepartment = async (data) => { // 更新部门 export const updateDepartment = async (data) => { try { - const response = await api.post(`/Base/UpdDept`, data); + const response = await api.post(API_ENDPOINTS.routes.BASE_UPD_DEPT, data); return response.data; } catch (error) { throw error; @@ -33,7 +36,7 @@ export const updateDepartment = async (data) => { // 删除部门 export const deleteDepartment = async (data) => { try { - const response = await api.post(`/Base/DelDept`, { data }); + const response = await api.post(API_ENDPOINTS.routes.BASE_DEL_DEPT, data); return response.data; } catch (error) { throw error; diff --git a/src/api/employeeapi.js b/src/api/employeeapi.js index 6de5c87b11ff225c376e5788a6344f85a4ea12d2..d34d977cf53d89597343fee0a116a7b174c8d093 100644 --- a/src/api/employeeapi.js +++ b/src/api/employeeapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取员工列表 export const fetchEmployees = async (params) => { try { - const response = await api.get("/Employee/SelectEmployeeAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.EMPLOYEE_SELECT_EMPLOYEE_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,9 +19,10 @@ export const fetchEmployees = async (params) => { //获取员工详情 export const fetchEmployeeDetail = async (params) => { try { - const response = await api.get("/Employee/SelectEmployeeInfoByEmployeeId", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.EMPLOYEE_SELECT_EMPLOYEE_INFO_BY_EMPLOYEE_ID, + { params }, + ); return response.data; } catch (error) { throw error; @@ -26,7 +33,7 @@ export const fetchEmployeeDetail = async (params) => { export const fetchEmployeeResume = async (params) => { try { const response = await api.get( - "/EmployeeHistory/SelectHistoryByEmployeeId", + API_ENDPOINTS.routes.EMPLOYEE_HISTORY_SELECT_HISTORY_BY_EMPLOYEE_ID, { params }, ); return response.data.Data; @@ -38,7 +45,10 @@ export const fetchEmployeeResume = async (params) => { // 添加员工 export const addEmployee = async (data) => { try { - const response = await api.post("/Employee/AddEmployee", data); + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_ADD_EMPLOYEE, + data, + ); return response.data; } catch (error) { throw error; @@ -48,7 +58,10 @@ export const addEmployee = async (data) => { // 更新员工 export const updateEmployee = async (data) => { try { - const response = await api.post(`/Employee/UpdateEmployee`, data); + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_UPDATE_EMPLOYEE, + data, + ); return response.data; } catch (error) { throw error; @@ -58,7 +71,10 @@ export const updateEmployee = async (data) => { // 禁用/启用员工账号 export const managerEmployeeAccount = async (data) => { try { - const response = await api.post(`/Employee/ManagerEmployeeAccount`, data); + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_MANAGER_EMPLOYEE_ACCOUNT, + data, + ); return response.data; } catch (error) { throw error; @@ -69,7 +85,8 @@ export const managerEmployeeAccount = async (data) => { export const fetchEmployeeRewardPunishment = async (params) => { try { const response = await api.get( - "/RewardPunishment/SelectAllRewardPunishmentByEmployeeId", + API_ENDPOINTS.routes + .REWARD_PUNISHMENT_SELECT_ALL_REWARD_PUNISHMENT_BY_EMPLOYEE_ID, { params }, ); return response.data.Data; @@ -82,7 +99,7 @@ export const fetchEmployeeRewardPunishment = async (params) => { export const fetchEmployeeAttendance = async (params) => { try { const response = await api.get( - "/EmployeeCheck/SelectCheckInfoByEmployeeId", + API_ENDPOINTS.routes.EMPLOYEE_CHECK_SELECT_CHECK_INFO_BY_EMPLOYEE_ID, { params }, ); return response.data.Data; @@ -95,7 +112,7 @@ export const fetchEmployeeAttendance = async (params) => { export const resetEmployeePassword = async (data) => { try { const response = await api.post( - `/Employee/ResetEmployeeAccountPassword`, + API_ENDPOINTS.routes.EMPLOYEE_RESET_EMPLOYEE_ACCOUNT_PASSWORD, data, ); return response.data; @@ -107,7 +124,10 @@ export const resetEmployeePassword = async (data) => { // 上传员工头像 export const uploadEmployeeAvatar = async (data) => { try { - const response = await api.post(`/EmployeePhoto/InsertWorkerPhoto`, data); + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_PHOTO_INSERT_WORKER_PHOTO, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/employeepermissionapi.js b/src/api/employeepermissionapi.js index 3aaa475d91d3804297cd732fb11d8e272f181cfc..09456d9e147f4a09091801327f4f59bfbb88127f 100644 --- a/src/api/employeepermissionapi.js +++ b/src/api/employeepermissionapi.js @@ -1,11 +1,23 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 读取指定员工已分配的角色(返回角色编码集合) +const buildUserNumberPayload = (employeeId) => { + if (employeeId && typeof employeeId === "object") { + return { + UserNumber: employeeId.UserNumber ?? employeeId.userNumber ?? "", + }; + } + + return { UserNumber: employeeId }; +}; + export const readUserRoles = async (employeeId) => { try { - const response = await api.get("/EmployeePermission/ReadUserRoles", { - params: { userNumber: employeeId }, - }); + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_PERMISSION_READ_USER_ROLES, + buildUserNumberPayload(employeeId), + ); return response.data?.Data?.Items || []; } catch (error) { throw error; @@ -16,7 +28,7 @@ export const readUserRoles = async (employeeId) => { export const assignUserRoles = async (data) => { try { const response = await api.post( - "/EmployeePermission/AssignUserRoles", + API_ENDPOINTS.routes.EMPLOYEE_PERMISSION_ASSIGN_USER_ROLES, data, ); return response.data; @@ -28,11 +40,9 @@ export const assignUserRoles = async (data) => { // 读取员工(通过角色)所拥有的权限明细(列表) export const readUserRolePermissions = async (employeeId) => { try { - const response = await api.get( - "/EmployeePermission/ReadUserRolePermissions", - { - params: { userNumber: employeeId }, - }, + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_PERMISSION_READ_USER_ROLE_PERMISSIONS, + buildUserNumberPayload(employeeId), ); return response.data?.Data?.Items || []; } catch (error) { @@ -43,11 +53,9 @@ export const readUserRolePermissions = async (employeeId) => { // 读取员工“直接权限”(非角色继承)权限编码集合 export const readUserDirectPermissions = async (employeeId) => { try { - const response = await api.get( - "/EmployeePermission/ReadUserDirectPermissions", - { - params: { userNumber: employeeId }, - }, + const response = await api.post( + API_ENDPOINTS.routes.EMPLOYEE_PERMISSION_READ_USER_DIRECT_PERMISSIONS, + buildUserNumberPayload(employeeId), ); return response.data?.Data?.Items || []; } catch (error) { @@ -59,7 +67,7 @@ export const readUserDirectPermissions = async (employeeId) => { export const assignUserPermissions = async (data) => { try { const response = await api.post( - "/EmployeePermission/AssignUserPermissions", + API_ENDPOINTS.routes.EMPLOYEE_PERMISSION_ASSIGN_USER_PERMISSIONS, data, ); return response.data; diff --git a/src/api/goodsapi.js b/src/api/goodsapi.js index 3d568c492651ae980ac66670faf13047375f6341..a55e940c0e68d3756815a84963fe715930c84d43 100644 --- a/src/api/goodsapi.js +++ b/src/api/goodsapi.js @@ -1,9 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取商品列表 export const fetchGoodss = async (params) => { try { - const response = await api.get("/Sellthing/SelectSellthingAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.SELLTHING_SELECT_SELLTHING_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +17,10 @@ export const fetchGoodss = async (params) => { // 添加商品 export const addGoods = async (data) => { try { - const response = await api.post("/Sellthing/InsertSellthing", data); + const response = await api.post( + API_ENDPOINTS.routes.SELLTHING_INSERT_SELLTHING, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +30,10 @@ export const addGoods = async (data) => { // 更新商品 export const updateGoods = async (data) => { try { - const response = await api.post(`/Sellthing/UpdateSellthing`, data); + const response = await api.post( + API_ENDPOINTS.routes.SELLTHING_UPDATE_SELLTHING, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +43,10 @@ export const updateGoods = async (data) => { // 删除商品 export const deleteGoods = async (data) => { try { - const response = await api.post(`/Sellthing/DeleteSellthing`, data); + const response = await api.post( + API_ENDPOINTS.routes.SELLTHING_DELETE_SELLTHING, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/hydroelectricityapi.js b/src/api/hydroelectricityapi.js index f7228aa57afed6fdbee274cc60d59cb3f3ca106e..ec1db7df7e28d616be4e29a9f90342178e8db09b 100644 --- a/src/api/hydroelectricityapi.js +++ b/src/api/hydroelectricityapi.js @@ -1,10 +1,11 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 根据条件查询水电费信息 房间号(可选)使用开始时间(可选)使用结束时间(可选) export const fetchHydroelectricitys = async (params) => { try { const response = await api.get( - "/EnergyManagement/SelectEnergyManagementInfo", + API_ENDPOINTS.routes.ENERGY_MANAGEMENT_SELECT_ENERGY_MANAGEMENT_INFO, { params }, ); return response.data.Data; @@ -17,7 +18,7 @@ export const fetchHydroelectricitys = async (params) => { export const addHydroelectricity = async (data) => { try { const response = await api.post( - "/EnergyManagement/InsertEnergyManagementInfo", + API_ENDPOINTS.routes.ENERGY_MANAGEMENT_INSERT_ENERGY_MANAGEMENT_INFO, data, ); return response.data; @@ -30,7 +31,7 @@ export const addHydroelectricity = async (data) => { export const updateHydroelectricity = async (data) => { try { const response = await api.post( - `/EnergyManagement/UpdateEnergyManagementInfo`, + API_ENDPOINTS.routes.ENERGY_MANAGEMENT_UPDATE_ENERGY_MANAGEMENT_INFO, data, ); return response.data; @@ -43,7 +44,7 @@ export const updateHydroelectricity = async (data) => { export const deleteHydroelectricity = async (data) => { try { const response = await api.post( - `/EnergyManagement/DeleteEnergyManagementInfo`, + API_ENDPOINTS.routes.ENERGY_MANAGEMENT_DELETE_ENERGY_MANAGEMENT_INFO, data, ); return response.data; diff --git a/src/api/internalfinanceapi.js b/src/api/internalfinanceapi.js index 79051291669c26c2b56de63d3f0aaa5541c17e53..afa796ef1bceb68e94ee7e77c4fce5970b7c6b5a 100644 --- a/src/api/internalfinanceapi.js +++ b/src/api/internalfinanceapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取资产列表 export const fetchInternalFinances = async (params) => { try { - const response = await api.get("/Asset/SelectAssetInfoAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.ASSET_SELECT_ASSET_INFO_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchInternalFinances = async (params) => { // 添加资产 export const addInternalFinance = async (data) => { try { - const response = await api.post("/Asset/AddAssetInfo", data); + const response = await api.post( + API_ENDPOINTS.routes.ASSET_ADD_ASSET_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addInternalFinance = async (data) => { // 更新资产 export const updateInternalFinance = async (data) => { try { - const response = await api.post(`/Asset/UpdAssetInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.ASSET_UPD_ASSET_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateInternalFinance = async (data) => { // 删除资产 export const deleteInternalFinance = async (data) => { try { - const response = await api.post(`/Asset/DelAssetInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.ASSET_DEL_ASSET_INFO, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/menuapi.js b/src/api/menuapi.js index 14926a281f6c7488fde8dfc162305a4624287f87..92a4ef7a05e8b658ea06358d623786db6743623d 100644 --- a/src/api/menuapi.js +++ b/src/api/menuapi.js @@ -1,54 +1,88 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; -// 获取菜单树 +// Fetch menu tree. export const fetchMenusTree = async (menu) => { try { - const response = await api.post("/Menu/BuildMenuAll", menu); + const response = await api.post( + API_ENDPOINTS.routes.MENU_BUILD_MENU_ALL, + menu, + { + // BuildMenuAll business failures are handled by caller. + skipBusinessErrorHandling: true, + }, + ); - // 后端 BaseResponse 使用 PascalCase 字段(Success/Code/Message/Data) - if (!response.data.Success) { - throw new Error(response.data.Message || "获取菜单失败"); + const payload = response?.data || {}; + if (!payload.Success) { + const error = new Error( + payload.Message || payload.message || "Failed to load menu data", + ); + error.code = payload.Code || payload.code || 0; + throw error; } - return response.data.Data; + + return payload.Data; } catch (error) { - throw error; + const message = + error?.Message || + error?.message || + error?.response?.data?.Message || + error?.response?.data?.message || + "Failed to load menu data"; + + const wrappedError = new Error(String(message)); + wrappedError.code = + error?.code || error?.Code || error?.response?.data?.Code || 0; + throw wrappedError; } }; -// 获取菜单列表 +// Fetch menu list. export const fetchMenus = async (params) => { try { - const response = await api.get("/Menu/SelectMenuAll", { params }); + const response = await api.get(API_ENDPOINTS.routes.MENU_SELECT_MENU_ALL, { + params, + }); return response.data.Data; } catch (error) { throw error; } }; -// 创建新菜单项 +// Create menu. export const addMenu = async (menu) => { try { - const response = await api.post("/Menu/InsertMenu", menu); + const response = await api.post( + API_ENDPOINTS.routes.MENU_INSERT_MENU, + menu, + ); return response.data; } catch (error) { throw error; } }; -// 更新菜单项 +// Update menu. export const updateMenu = async (menu) => { try { - const response = await api.post(`/Menu/UpdateMenu`, menu); + const response = await api.post( + API_ENDPOINTS.routes.MENU_UPDATE_MENU, + menu, + ); return response.data; } catch (error) { throw error; } }; -// 删除菜单项 +// Delete menu. export const deleteMenu = async (menu) => { try { - const response = await api.post(`/Menu/DeleteMenu`, menu); + const response = await api.post( + API_ENDPOINTS.routes.MENU_DELETE_MENU, + menu, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/nationapi.js b/src/api/nationapi.js index f2f05db24afc7014a878f605fc135574564ca9fa..300db812979012439690f789a4ad5cd484d88d9f 100644 --- a/src/api/nationapi.js +++ b/src/api/nationapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取民族列表 export const fetchNations = async (params) => { try { - const response = await api.get("/Base/SelectNationAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_NATION_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,7 @@ export const fetchNations = async (params) => { // 添加民族 export const addNation = async (data) => { try { - const response = await api.post("/Base/AddNation", data); + const response = await api.post(API_ENDPOINTS.routes.BASE_ADD_NATION, data); return response.data; } catch (error) { throw error; @@ -23,7 +29,7 @@ export const addNation = async (data) => { // 更新民族 export const updateNation = async (data) => { try { - const response = await api.post(`/Base/UpdNation`, data); + const response = await api.post(API_ENDPOINTS.routes.BASE_UPD_NATION, data); return response.data; } catch (error) { throw error; @@ -33,7 +39,7 @@ export const updateNation = async (data) => { // 删除民族 export const deleteNation = async (data) => { try { - const response = await api.post(`/Base/DelNation`, data); + const response = await api.post(API_ENDPOINTS.routes.BASE_DEL_NATION, data); return response.data; } catch (error) { throw error; diff --git a/src/api/noticetypeapi.js b/src/api/noticetypeapi.js index 712c7c79c959706b4138d7ac4896f3087af79a95..fbe53442c528f0d0f41dbc173479d4a7d5400c54 100644 --- a/src/api/noticetypeapi.js +++ b/src/api/noticetypeapi.js @@ -1,11 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取公告类型列表 export const fetchNoticeTypes = async (params) => { try { - const response = await api.get("/Base/SelectAppointmentNoticeTypeAll", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_APPOINTMENT_NOTICE_TYPE_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -15,7 +17,10 @@ export const fetchNoticeTypes = async (params) => { // 添加公告类型 export const addNoticeType = async (data) => { try { - const response = await api.post("/Base/CreateAppointmentNoticeType", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_CREATE_APPOINTMENT_NOTICE_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -25,7 +30,10 @@ export const addNoticeType = async (data) => { // 更新公告类型 export const updateNoticeType = async (data) => { try { - const response = await api.post(`/Base/UpdateAppointmentNoticeType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPDATE_APPOINTMENT_NOTICE_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -35,7 +43,10 @@ export const updateNoticeType = async (data) => { // 删除公告类型 export const deleteNoticeType = async (data) => { try { - const response = await api.post(`/Base/DeleteAppointmentNoticeType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DELETE_APPOINTMENT_NOTICE_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/passportapi.js b/src/api/passportapi.js index e32787aceab8cae7430cdcfd10ecb25e6bb39c05..01fd8a42e4247cfa995afd7de425d43c47c9f529 100644 --- a/src/api/passportapi.js +++ b/src/api/passportapi.js @@ -1,9 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取证件类型列表 export const fetchPassports = async (params) => { try { - const response = await api.get("/Base/SelectPassPortTypeAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_PASS_PORT_TYPE_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,9 +17,10 @@ export const fetchPassports = async (params) => { // 获取可用证件类型列表 export const fetchCanUsePassports = async (params) => { try { - const response = await api.get("/Base/SelectPassPortTypeAllCanUse", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_PASS_PORT_TYPE_ALL_CAN_USE, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -25,7 +30,10 @@ export const fetchCanUsePassports = async (params) => { // 添加证件类型 export const addPassport = async (data) => { try { - const response = await api.post("/Base/InsertPassPortType", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_INSERT_PASS_PORT_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -35,7 +43,10 @@ export const addPassport = async (data) => { // 更新证件类型 export const updatePassport = async (data) => { try { - const response = await api.post(`/Base/UpdatePassPortType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPDATE_PASS_PORT_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -45,7 +56,10 @@ export const updatePassport = async (data) => { // 删除证件类型 export const deletePassport = async (data) => { try { - const response = await api.post(`/Base/DeletePassPortType`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DELETE_PASS_PORT_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/permissionapi.js b/src/api/permissionapi.js index 66c88d60245cb58a0981398667e057da166ace60..f4bbecaf4ccbada17e5f51f5425cea2dc279f42f 100644 --- a/src/api/permissionapi.js +++ b/src/api/permissionapi.js @@ -1,10 +1,22 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; + +const buildReadPermissionPayload = (params = {}) => ({ + PermissionNumber: params.PermissionNumber ?? params.permissionNumber ?? "", + PermissionName: params.PermissionName ?? params.permissionName ?? "", + MenuKey: params.MenuKey ?? params.menuKey ?? "", + Module: params.Module ?? params.module ?? "", + Page: params.Page ?? params.page ?? 1, + PageSize: params.PageSize ?? params.pageSize ?? 15, + IgnorePaging: params.IgnorePaging ?? params.ignorePaging ?? false, +}); export const selectPermissionList = async (params) => { try { - const response = await api.get("/Permission/SelectPermissionList", { - params, - }); + const response = await api.post( + API_ENDPOINTS.routes.PERMISSION_SELECT_PERMISSION_LIST, + buildReadPermissionPayload(params), + ); // 返回标准结构 { Items, TotalCount } return response.data?.Data || { Items: [], TotalCount: 0 }; } catch (error) { diff --git a/src/api/positionapi.js b/src/api/positionapi.js index e4f0501773cef2f3f47da979be62fefdc819d295..0a6992128c2669843875d5f394902ebc2785c1d1 100644 --- a/src/api/positionapi.js +++ b/src/api/positionapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取职位列表 export const fetchPositions = async (params) => { try { - const response = await api.get("/Base/SelectPositionAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_POSITION_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchPositions = async (params) => { // 添加职位 export const addPosition = async (data) => { try { - const response = await api.post("/Base/AddPosition", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_ADD_POSITION, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addPosition = async (data) => { // 更新职位 export const updatePosition = async (data) => { try { - const response = await api.post(`/Base/UpdPosition`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPD_POSITION, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updatePosition = async (data) => { // 删除职位 export const deletePosition = async (data) => { try { - const response = await api.post(`/Base/DelPosition`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DEL_POSITION, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/promotioncontentapi.js b/src/api/promotioncontentapi.js index 224502c94a5531e4b94995cf5398602f731ab761..845a6fe031542bee48827201127c9c034b599a0b 100644 --- a/src/api/promotioncontentapi.js +++ b/src/api/promotioncontentapi.js @@ -1,10 +1,11 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取宣传联动内容列表 export const fetchPromotionContents = async (params) => { try { const response = await api.get( - "/PromotionContent/SelectPromotionContentAll", + API_ENDPOINTS.routes.PROMOTION_CONTENT_SELECT_PROMOTION_CONTENT_ALL, { params }, ); return response.data.Data; @@ -17,7 +18,7 @@ export const fetchPromotionContents = async (params) => { export const addPromotionContent = async (data) => { try { const response = await api.post( - "/PromotionContent/AddPromotionContent", + API_ENDPOINTS.routes.PROMOTION_CONTENT_ADD_PROMOTION_CONTENT, data, ); return response.data; @@ -30,7 +31,7 @@ export const addPromotionContent = async (data) => { export const updatePromotionContent = async (data) => { try { const response = await api.post( - `/PromotionContent/UpdatePromotionContent`, + API_ENDPOINTS.routes.PROMOTION_CONTENT_UPDATE_PROMOTION_CONTENT, data, ); return response.data; @@ -43,7 +44,7 @@ export const updatePromotionContent = async (data) => { export const deletePromotionContent = async (data) => { try { const response = await api.post( - `/PromotionContent/DeletePromotionContent`, + API_ENDPOINTS.routes.PROMOTION_CONTENT_DELETE_PROMOTION_CONTENT, data, ); return response.data; diff --git a/src/api/qualificationapi.js b/src/api/qualificationapi.js index 9276f06c4a3010ce33cfb914de3f7293fa062475..cfa5093c267b3de1d5fe4e920194e2ff5bba9f2f 100644 --- a/src/api/qualificationapi.js +++ b/src/api/qualificationapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取学历列表 export const fetchQualifications = async (params) => { try { - const response = await api.get("/Base/SelectEducationAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_EDUCATION_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchQualifications = async (params) => { // 添加学历 export const addQualification = async (data) => { try { - const response = await api.post("/Base/AddEducation", data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_ADD_EDUCATION, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addQualification = async (data) => { // 更新学历 export const updateQualification = async (data) => { try { - const response = await api.post(`/Base/UpdEducation`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_UPD_EDUCATION, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateQualification = async (data) => { // 删除学历 export const deleteQualification = async (data) => { try { - const response = await api.post(`/Base/DelEducation`, data); + const response = await api.post( + API_ENDPOINTS.routes.BASE_DEL_EDUCATION, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/reserapi.js b/src/api/reserapi.js index 30e2b60138fc1f989a6523f01c0938d6bfe4c1f9..945322a9a6769cde07b469e0f19dd96f3f61a84e 100644 --- a/src/api/reserapi.js +++ b/src/api/reserapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取预约列表 export const fetchResers = async (params) => { try { - const response = await api.get("/Reser/SelectReserAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.RESER_SELECT_RESER_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchResers = async (params) => { // 添加预约 export const addReser = async (data) => { try { - const response = await api.post("/Reser/InserReserInfo", data); + const response = await api.post( + API_ENDPOINTS.routes.RESER_INSER_RESER_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addReser = async (data) => { // 更新预约 export const updateReser = async (data) => { try { - const response = await api.post(`/Reser/UpdateReserInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.RESER_UPDATE_RESER_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateReser = async (data) => { // 删除预约 export const deleteReser = async (data) => { try { - const response = await api.post(`/Reser/DeleteReserInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.RESER_DELETE_RESER_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -43,7 +58,10 @@ export const deleteReser = async (data) => { // 获取预约类型 export const fetchReserTypes = async (params) => { try { - const response = await api.get("/Reser/SelectReserTypeAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.RESER_SELECT_RESER_TYPE_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; diff --git a/src/api/roleapi.js b/src/api/roleapi.js index 4bf02c285b3b31baa9413ebeaa1f357b8cd8f829..f1821a56bb4b8b6d023204d76e996cd65fab7149 100644 --- a/src/api/roleapi.js +++ b/src/api/roleapi.js @@ -1,9 +1,26 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取角色列表 +const buildRoleNumberPayload = (roleNumber) => { + if (roleNumber && typeof roleNumber === "object") { + return { + RoleNumber: + roleNumber.RoleNumber ?? + roleNumber.roleNumber ?? + roleNumber.roleNo ?? + "", + }; + } + + return { RoleNumber: roleNumber }; +}; + export const fetchRoles = async (params) => { try { - const response = await api.get("/Role/SelectRoleList", { params }); + const response = await api.get(API_ENDPOINTS.routes.ROLE_SELECT_ROLE_LIST, { + params, + }); return response.data.Data; } catch (error) { throw error; @@ -13,7 +30,10 @@ export const fetchRoles = async (params) => { // 添加角色 export const addRole = async (data) => { try { - const response = await api.post("/Role/InsertRole", data); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_INSERT_ROLE, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +43,10 @@ export const addRole = async (data) => { // 更新角色 export const updateRole = async (data) => { try { - const response = await api.post(`/Role/UpdateRole`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_UPDATE_ROLE, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +56,10 @@ export const updateRole = async (data) => { // 删除角色 export const deleteRole = async (data) => { try { - const response = await api.post(`/Role/DeleteRole`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_DELETE_ROLE, + data, + ); return response.data; } catch (error) { throw error; @@ -43,7 +69,10 @@ export const deleteRole = async (data) => { // 角色-权限:全量覆盖式授予 export const grantRolePermissions = async (data) => { try { - const response = await api.post("/Role/GrantRolePermissions", data); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_GRANT_ROLE_PERMISSIONS, + data, + ); return response.data; } catch (error) { throw error; @@ -53,9 +82,10 @@ export const grantRolePermissions = async (data) => { // 读取指定角色已授予的权限(返回后端 Data,页面逻辑做进一步兼容处理) export const readRolePermissions = async (roleNumber) => { try { - const response = await api.get("/Role/ReadRolePermissions", { - params: { roleNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_READ_ROLE_PERMISSIONS, + buildRoleNumberPayload(roleNumber), + ); // 后端返回标准结构 { Code, Message, Data: { Items, TotalCount } } // 这里直接返回 Items,便于上层直接按照数组处理 return response.data?.Data?.Items || []; @@ -67,9 +97,10 @@ export const readRolePermissions = async (roleNumber) => { // 角色-用户:读取指定角色下的管理员编码集合 export const readRoleUsers = async (roleNumber) => { try { - const response = await api.get("/Role/ReadRoleUsers", { - params: { roleNumber }, - }); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_READ_ROLE_USERS, + buildRoleNumberPayload(roleNumber), + ); // 标准结构 { Code, Message, Data: { Items, TotalCount } } return response.data?.Data?.Items || []; } catch (error) { @@ -80,7 +111,10 @@ export const readRoleUsers = async (roleNumber) => { // 角色-用户:为角色分配管理员(全量覆盖) export const assignRoleUsers = async (data) => { try { - const response = await api.post("/Role/AssignRoleUsers", data); + const response = await api.post( + API_ENDPOINTS.routes.ROLE_ASSIGN_ROLE_USERS, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/roomapi.js b/src/api/roomapi.js index b3e8aef21c676d03dc3cf3d70620fa0a0db26c03..4818ea6ce498715b8365f2acb7f74f98f7ffe170 100644 --- a/src/api/roomapi.js +++ b/src/api/roomapi.js @@ -1,9 +1,12 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取房间列表 export const fetchRooms = async (params) => { try { - const response = await api.get("/Room/SelectRoomAll", { params }); + const response = await api.get(API_ENDPOINTS.routes.ROOM_SELECT_ROOM_ALL, { + params, + }); return response.data.Data; } catch (error) { throw error; @@ -13,7 +16,12 @@ export const fetchRooms = async (params) => { // 获取可使用房间列表 export const fetchAvailableRooms = async (params) => { try { - const response = await api.get("/Room/SelectCanUseRoomAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.ROOM_SELECT_CAN_USE_ROOM_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -23,7 +31,10 @@ export const fetchAvailableRooms = async (params) => { // 添加房间 export const addRoom = async (data) => { try { - const response = await api.post("/Room/InsertRoom", data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_INSERT_ROOM, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +44,10 @@ export const addRoom = async (data) => { // 更新房间 export const updateRoom = async (data) => { try { - const response = await api.post(`/Room/UpdateRoom`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_UPDATE_ROOM, + data, + ); return response.data; } catch (error) { throw error; @@ -43,7 +57,10 @@ export const updateRoom = async (data) => { // 删除房间 export const deleteRoom = async (data) => { try { - const response = await api.post(`/Room/DeleteRoom`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_DELETE_ROOM, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/roomstateapi.js b/src/api/roomstateapi.js index 0bddc3dbe422b2e9eb08c0d6d4ad794fd239cc85..82a300c7bd6e0db863071b24d5b83314d2ca3b62 100644 --- a/src/api/roomstateapi.js +++ b/src/api/roomstateapi.js @@ -1,9 +1,12 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取房间状态列表 export const fetchRoomStates = async () => { try { - const response = await api.get("/Base/SelectRoomStateAll"); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_ROOM_STATE_ALL, + ); return response.data.Data; } catch (error) { throw error; diff --git a/src/api/roomtypeapi.js b/src/api/roomtypeapi.js index 78a435676205ae625589a0defaad9f21cb164df8..68fae87966738caee5d34bc1c6bcca98570efe97 100644 --- a/src/api/roomtypeapi.js +++ b/src/api/roomtypeapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取房间类型列表 export const fetchRoomTypes = async (params) => { try { - const response = await api.get("/RoomType/SelectRoomTypesAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.ROOM_TYPE_SELECT_ROOM_TYPES_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchRoomTypes = async (params) => { // 添加房间类型 export const addRoomType = async (data) => { try { - const response = await api.post("/RoomType/InsertRoomType", data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_TYPE_INSERT_ROOM_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addRoomType = async (data) => { // 更新房间类型 export const updateRoomType = async (data) => { try { - const response = await api.post(`/RoomType/UpdateRoomType`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_TYPE_UPDATE_ROOM_TYPE, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateRoomType = async (data) => { // 删除房间类型 export const deleteRoomType = async (data) => { try { - const response = await api.post(`/RoomType/DeleteRoomType`, data); + const response = await api.post( + API_ENDPOINTS.routes.ROOM_TYPE_DELETE_ROOM_TYPE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/spendinfoapi.js b/src/api/spendinfoapi.js index f2424d5e6cf266122d3ce0cb9320bf3aecebc40d..f70d57aa6714d80cb7ad4ba7c63a59670d952572 100644 --- a/src/api/spendinfoapi.js +++ b/src/api/spendinfoapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取消费信息列表 export const fetchSpendInfos = async (params) => { try { - const response = await api.get("/Spend/SelectSpendInfoAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.SPEND_SELECT_SPEND_INFO_ALL, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchSpendInfos = async (params) => { // 添加消费信息 export const addSpendInfo = async (data) => { try { - const response = await api.post("/Spend/InsertSpendInfo", data); + const response = await api.post( + API_ENDPOINTS.routes.SPEND_INSERT_SPEND_INFO, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addSpendInfo = async (data) => { // 更新消费信息 export const updateSpendInfo = async (data) => { try { - const response = await api.post(`/Spend/UpdSpendInfo`, data); + const response = await api.post( + API_ENDPOINTS.routes.SPEND_UPD_SPEND_INFO, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/supervisioninfoapi.js b/src/api/supervisioninfoapi.js index b5794b2ab7789ff7a75697cb726ec10c85d0084b..a40412b818840e24898d9baef8e331c5956741e5 100644 --- a/src/api/supervisioninfoapi.js +++ b/src/api/supervisioninfoapi.js @@ -1,10 +1,12 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取监管统计列表 export const fetchSupervisionInfos = async (params) => { try { const response = await api.get( - "/SupervisionStatistics/SelectSupervisionStatisticsAll", + API_ENDPOINTS.routes + .SUPERVISION_STATISTICS_SELECT_SUPERVISION_STATISTICS_ALL, { params }, ); return response.data.Data; @@ -17,7 +19,7 @@ export const fetchSupervisionInfos = async (params) => { export const addSupervisionInfo = async (data) => { try { const response = await api.post( - "/SupervisionStatistics/InsertSupervisionStatistics", + API_ENDPOINTS.routes.SUPERVISION_STATISTICS_INSERT_SUPERVISION_STATISTICS, data, ); return response.data; @@ -30,7 +32,7 @@ export const addSupervisionInfo = async (data) => { export const updateSupervisionInfo = async (data) => { try { const response = await api.post( - `/SupervisionStatistics/UpdateSupervisionStatistics`, + API_ENDPOINTS.routes.SUPERVISION_STATISTICS_UPDATE_SUPERVISION_STATISTICS, data, ); return response.data; @@ -43,7 +45,7 @@ export const updateSupervisionInfo = async (data) => { export const deleteSupervisionInfo = async (data) => { try { const response = await api.post( - `/SupervisionStatistics/DeleteSupervisionStatistics`, + API_ENDPOINTS.routes.SUPERVISION_STATISTICS_DELETE_SUPERVISION_STATISTICS, data, ); return response.data; diff --git a/src/api/utilityapi.js b/src/api/utilityapi.js index 526143818c86a380740993536a67c8b354bba6bb..3f3df4f2669dec8bc13aa72f6ff9db30551eb7f5 100644 --- a/src/api/utilityapi.js +++ b/src/api/utilityapi.js @@ -1,11 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取操作日志列表 export const fetchOperationlogs = async (params) => { try { - const response = await api.get("/Utility/SelectOperationlogAll", { - params, - }); + const response = await api.get( + API_ENDPOINTS.routes.UTILITY_SELECT_OPERATIONLOG_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -15,7 +17,10 @@ export const fetchOperationlogs = async (params) => { // 获取请求日志列表 export const fetchRequestlogs = async (params) => { try { - const response = await api.get("/Utility/SelectRequestlogAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.UTILITY_SELECT_REQUESTLOG_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; @@ -25,7 +30,10 @@ export const fetchRequestlogs = async (params) => { // 获取身份证地区代码 export const fetchCardCode = async (cardcode) => { try { - const response = await api.post("/Utility/SelectCardCode", cardcode); + const response = await api.post( + API_ENDPOINTS.routes.UTILITY_SELECT_CARD_CODE, + cardcode, + ); return response.data; } catch (error) { throw error; @@ -36,7 +44,7 @@ export const fetchCardCode = async (cardcode) => { export const deleteOperationlogByRange = async (operationlog) => { try { const response = await api.post( - "/Utility/DeleteOperationlogByRange", + API_ENDPOINTS.routes.UTILITY_DELETE_OPERATIONLOG_BY_RANGE, operationlog, ); return response.data; @@ -49,7 +57,7 @@ export const deleteOperationlogByRange = async (operationlog) => { export const deleteOperationlog = async (operationlog) => { try { const response = await api.post( - "/Utility/DeleteOperationlog", + API_ENDPOINTS.routes.UTILITY_DELETE_OPERATIONLOG, operationlog, ); return response.data; diff --git a/src/api/vipruleapi.js b/src/api/vipruleapi.js index d96e262590d543d915a2da7d5e39df324b3243d4..8a072aebeca5447de53161d629d56fe4f734eee7 100644 --- a/src/api/vipruleapi.js +++ b/src/api/vipruleapi.js @@ -1,9 +1,15 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取会员规则列表 export const fetchVipRules = async (params) => { try { - const response = await api.get("/VipRule/SelectVipRuleList", { params }); + const response = await api.get( + API_ENDPOINTS.routes.VIP_RULE_SELECT_VIP_RULE_LIST, + { + params, + }, + ); return response.data.Data; } catch (error) { throw error; @@ -13,7 +19,10 @@ export const fetchVipRules = async (params) => { // 添加会员规则 export const addVipRule = async (data) => { try { - const response = await api.post("/VipRule/AddVipRule", data); + const response = await api.post( + API_ENDPOINTS.routes.VIP_RULE_ADD_VIP_RULE, + data, + ); return response.data; } catch (error) { throw error; @@ -23,7 +32,10 @@ export const addVipRule = async (data) => { // 更新会员规则 export const updateVipRule = async (data) => { try { - const response = await api.post(`/VipRule/UpdVipRule`, data); + const response = await api.post( + API_ENDPOINTS.routes.VIP_RULE_UPD_VIP_RULE, + data, + ); return response.data; } catch (error) { throw error; @@ -33,7 +45,10 @@ export const updateVipRule = async (data) => { // 删除会员规则 export const deleteVipRule = async (data) => { try { - const response = await api.post(`/VipRule/DelVipRule`, data); + const response = await api.post( + API_ENDPOINTS.routes.VIP_RULE_DEL_VIP_RULE, + data, + ); return response.data; } catch (error) { throw error; diff --git a/src/api/workerfeatureapi.js b/src/api/workerfeatureapi.js index dd2ecece4f57ee5fdc8329e6b48a64dd2a266aaa..d75993cd2ab531c2e16d3fe36bb30901944fbcfd 100644 --- a/src/api/workerfeatureapi.js +++ b/src/api/workerfeatureapi.js @@ -1,9 +1,13 @@ import api from "../api"; +import { API_ENDPOINTS } from "../config/apiEndpoints"; // 获取面貌列表 export const fetchWorkerFeatures = async (params) => { try { - const response = await api.get("/Base/SelectWorkerFeatureAll", { params }); + const response = await api.get( + API_ENDPOINTS.routes.BASE_SELECT_WORKER_FEATURE_ALL, + { params }, + ); return response.data.Data; } catch (error) { throw error; diff --git a/src/common/errorCodes.js b/src/common/errorCodes.js new file mode 100644 index 0000000000000000000000000000000000000000..64863ba3f387126c36eb480975589c1e770a61bf --- /dev/null +++ b/src/common/errorCodes.js @@ -0,0 +1,39 @@ +export const API_SUCCESS_CODE = 0; + +export const HTTP_STATUS = Object.freeze({ + UNAUTHORIZED: 401, + FORBIDDEN: 403, +}); + +export const ERROR_CODES = Object.freeze({ + AUTH_TOKEN_EXPIRED: 1401, + AUTH_INVALID_CREDENTIAL: 1402, + AUTH_PERMISSION_DENIED: 1403, + RESOURCE_NOT_FOUND: 1404, + CONCURRENCY_CONFLICT: 1409, + VALIDATION_FAILED: 2001, + DUPLICATE_DATA: 3001, + SERVER_ERROR: 1500, + NETWORK_ERROR: 9001, + NETWORK_TIMEOUT: 9002, +}); + +export const normalizeCode = (value) => { + const code = Number(value); + return Number.isFinite(code) ? code : 0; +}; + +export const isTokenInvalidBusinessCode = (code) => + normalizeCode(code) === ERROR_CODES.AUTH_TOKEN_EXPIRED; + +export const isPermissionDeniedBusinessCode = (code) => { + const normalized = normalizeCode(code); + return ( + normalized === ERROR_CODES.AUTH_INVALID_CREDENTIAL || + normalized === ERROR_CODES.AUTH_PERMISSION_DENIED + ); +}; + +export const isPermissionDeniedErrorCode = (code) => + isPermissionDeniedBusinessCode(code) || + normalizeCode(code) === HTTP_STATUS.FORBIDDEN; diff --git a/src/common/errorHandler.js b/src/common/errorHandler.js index 8b43ae2076131879af914aaada8877ab2fc17390..019ec5ba82d71d94a4436dc0acb9b7f0cbdca591 100644 --- a/src/common/errorHandler.js +++ b/src/common/errorHandler.js @@ -1,53 +1,89 @@ import i18n from "@/i18n"; import { showErrorNotification, showWarningNotification } from "@/utils"; import { redirectToLogin, handleLogout } from "@/utils/auth"; +import { getStoredToken } from "@/utils/tokenStorage"; +import { + ERROR_CODES, + HTTP_STATUS, + isTokenInvalidBusinessCode, +} from "@/common/errorCodes"; // 可扩展的错误码映射表 +const DEFAULT_API_ERROR_MESSAGE = i18n.global.t("message.requestFailed"); + +const readApiMessage = (payload) => { + if (!payload || typeof payload !== "object") { + return ""; + } + return payload.Message || payload.message || ""; +}; + +export const resolveErrorMessage = (payload, fallbackMessage) => { + const directMessage = readApiMessage(payload); + if (directMessage && directMessage !== DEFAULT_API_ERROR_MESSAGE) { + return directMessage; + } + + const nestedMessage = readApiMessage(payload?.response?.data); + if (nestedMessage && nestedMessage !== DEFAULT_API_ERROR_MESSAGE) { + return nestedMessage; + } + + const localizedFallback = + fallbackMessage || i18n.global.t("message.requestFailed"); + return localizedFallback || DEFAULT_API_ERROR_MESSAGE; +}; + export const errorCodeMap = { // 认证/授权相关 - 1401: { + [ERROR_CODES.AUTH_TOKEN_EXPIRED]: { // token 失效:通常需要用户重新登录 message: i18n.global.t("message.loginExpired") || "登录已过期,请重新登录", action: "redirect", clearStorage: true, }, - 1402: { + [ERROR_CODES.AUTH_INVALID_CREDENTIAL]: { // 凭证无效(示例) - message: - i18n.global.t("message.invalidCredentials") || "凭证无效,请重新登录", + message: i18n.global.t("message.noPermission") || "无权限执行该操作", action: "notify", }, - 1403: { + [ERROR_CODES.AUTH_PERMISSION_DENIED]: { // 权限不足 message: i18n.global.t("message.noPermission") || "无权限执行该操作", action: "notify", }, // 资源/数据相关 - 1404: { + [ERROR_CODES.RESOURCE_NOT_FOUND]: { // 资源未找到 message: i18n.global.t("message.notFound") || "未找到相关资源", action: "notify", }, - 2001: { + [ERROR_CODES.CONCURRENCY_CONFLICT]: { + message: + i18n.global.t("message.concurrencyConflict") || + "数据已被其他用户修改,请刷新后重试", + action: "notify", + }, + [ERROR_CODES.VALIDATION_FAILED]: { // 参数/校验错误 message: i18n.global.t("message.validationFailed") || "参数校验失败,请检查输入", action: "notify", }, - 3001: { + [ERROR_CODES.DUPLICATE_DATA]: { // 数据已存在/重复 message: i18n.global.t("message.duplicate") || "数据已存在,无法重复创建", action: "notify", }, // 服务端/网络 - 1500: { + [ERROR_CODES.SERVER_ERROR]: { message: i18n.global.t("message.serverError") || "服务器内部错误,请稍后重试", action: "notify", }, - 9001: { + [ERROR_CODES.NETWORK_ERROR]: { // 网络/连接异常(客户端定义) message: i18n.global.t("message.networkError") || "网络连接异常,请检查网络", @@ -68,16 +104,16 @@ export function handleApiError(resData, response) { // Token 相关优先处理,保留重试逻辑:当后端返回 1401 且本地还有 token 时,触发重试机制 if ( - code === 1401 || + isTokenInvalidBusinessCode(code) || (typeof message === "string" && message.includes("Token")) ) { - const token = localStorage.getItem("token"); + const token = getStoredToken(); if (!token) { // 没有 token,直接跳转登录并显示后端消息(如果有) - redirectToLogin(backendMessage || i18n.global.t("message.loginExpired") ); + redirectToLogin(backendMessage || i18n.global.t("message.loginExpired")); return Promise.reject({ success: false, - code: 1401, + code: ERROR_CODES.AUTH_TOKEN_EXPIRED, message: backendMessage || i18n.global.t("message.loginExpired"), }); } @@ -151,20 +187,20 @@ export function handleHttpError(error) { // 忽略解析错误,继续执行后续的 HTTP 层处理 } - if (status === 401) { + if (status === HTTP_STATUS.UNAUTHORIZED) { redirectToLogin(i18n.global.t("message.loginExpired"), true); return Promise.reject({ success: false, - code: 401, + code: HTTP_STATUS.UNAUTHORIZED, message: i18n.global.t("message.loginExpired"), }); } - if (status === 403) { + if (status === HTTP_STATUS.FORBIDDEN) { showErrorNotification(i18n.global.t("message.noPermission") || "无权限"); return Promise.reject({ success: false, - code: 403, + code: HTTP_STATUS.FORBIDDEN, message: i18n.global.t("message.noPermission"), }); } @@ -200,7 +236,7 @@ export function handleHttpError(error) { showErrorNotification(displayMessage); return Promise.reject({ success: false, - code: isTimeout ? 9002 : 9001, + code: isTimeout ? ERROR_CODES.NETWORK_TIMEOUT : ERROR_CODES.NETWORK_ERROR, message: displayMessage, }); } diff --git a/src/common/permissioncode.js b/src/common/permissioncode.js index e0cbe850726748aeae3be49209d4c453342b9d36..bdeaa4a9dfb5309aeee1a15cd4b192e52c63bee2 100644 --- a/src/common/permissioncode.js +++ b/src/common/permissioncode.js @@ -49,62 +49,66 @@ export const NoticeTypePermissions = { // 推广内容 (Promotion Content Management) export const PromotionContentPermissions = { - CREATE: "promotioncontent.create", - UPDATE: "promotioncontent.update", - DELETE: "promotioncontent.delete", + CREATE: "promotioncontent.addpromotioncontent", + UPDATE: "promotioncontent.updatepromotioncontent", + DELETE: "promotioncontent.deletepromotioncontent", }; // ==================== 系统管理模块 ==================== // 管理员类型 (Administrator Type Management) export const AdminTypeManagementPermissions = { - CREATE: "admintypemanagement.create", - UPDATE: "admintypemanagement.update", - DELETE: "admintypemanagement.delete", + CREATE: "system:admintype:addadmintype", + UPDATE: "system:admintype:updadmintype", + DELETE: "system:admintype:deladmintype", }; // 管理员管理 (Administrator Management) export const AdministratorManagementPermissions = { - CREATE: "administratormanagement.create", - UPDATE: "administratormanagement.update", - DELETE: "administratormanagement.delete", - ASSIGN_VIEW: "system:user:assign.view", - GET_TWO_FACTOR: "system:admin:get2fa", - GENERATE_TWO_FACTOR: "system:admin:generate2fa", - ENABLE_TWO_FACTOR: "system:admin:enable2fa", - DISABLE_TWO_FACTOR: "system:admin:disable2fa", - RECOVERY_TWO_FACTOR: "system:admin:recovery2fa", + CREATE: "system:admin:addadmin", + UPDATE: "system:admin:updadmin", + DELETE: "system:admin:deladmin", + ASSIGN_VIEW: "system:user:assign.selectpermissionlist", + GET_TWO_FACTOR: "system:admin:gettwofactorstatus", + GENERATE_TWO_FACTOR: "system:admin:generatetwofactorsetup", + ENABLE_TWO_FACTOR: "system:admin:enabletwofactor", + DISABLE_TWO_FACTOR: "system:admin:disabletwofactor", + RECOVERY_TWO_FACTOR: "system:admin:regeneratetwofactorrecoverycodes", }; // 角色管理 (Role Management) export const RoleManagementPermissions = { - CREATE: "rolemanagement.create", - UPDATE: "rolemanagement.update", - DELETE: "rolemanagement.delete", - GRANT: "rolemanagement.grant", - SYSTEM_ROLE_GRANT: "system:role:grant", + CREATE: "system:role:insertrole", + UPDATE: "system:role:updaterole", + DELETE: "system:role:deleterole", + GRANT: "system:role:grantrolepermissions", + SYSTEM_ROLE_GRANT: "system:role:assignroleusers", }; // 菜单管理 (Menu Management) export const MenuManagementPermissions = { - CREATE: "menumanagement.create", - UPDATE: "menumanagement.update", - DELETE: "menumanagement.delete", + CREATE: "menumanagement.insertmenu", + UPDATE: "menumanagement.updatemenu", + DELETE: "menumanagement.deletemenu", }; // 账户权限管理 (Account Permission Management) export const AccountPermissionPermissions = { - ASSIGN: "system:user:assign", + ASSIGN: [ + "system:user:admin:assignuserpermissions", + "system:user:employee:assignuserpermissions", + "system:user:customer:assignuserpermissions", + ], }; // ==================== 客户管理模块 ==================== // 客户管理 (Customer Management) export const CustomerPermissions = { - CREATE: "customer.create", - UPDATE: "customer.update", - DELETE: "customer.delete", - ASSIGN_VIEW: "system:user:assign.view", + CREATE: "customer.insertcustomerinfo", + UPDATE: "customer.updcustomerinfo", + DELETE: "customer.delcustomerinfo", + ASSIGN_VIEW: "system:user:assign.selectpermissionlist", }; // 客户类型 (Customer Type Management) @@ -116,90 +120,90 @@ export const CustomerTypePermissions = { // 客户消费 (Customer Spend Management) export const CustomerSpendPermissions = { - UPDATE: "customerspend.update", + UPDATE: "customerspend.updspendinfo", }; // VIP等级 (VIP Level Management) export const VipLevelPermissions = { - CREATE: "viplevel.create", - UPDATE: "viplevel.update", - DELETE: "viplevel.delete", + CREATE: "viplevel.addviprule", + UPDATE: "viplevel.updviprule", + DELETE: "viplevel.delviprule", }; // ==================== 人力资源管理模块 ==================== // 员工管理 (Staff Management) export const StaffManagementPermissions = { - CREATE: "staffmanagement.create", - UPDATE: "staffmanagement.update", - DELETE: "staffmanagement.delete", - VIEW: "staffmanagement.view", - STATUS: "staffmanagement.status", - RESET: "staffmanagement.reset", - ASSIGN_VIEW: "system:user:assign.view", - GET_TWO_FACTOR: "staffmanagement.get2fa", - GENERATE_TWO_FACTOR: "staffmanagement.generate2fa", - ENABLE_TWO_FACTOR: "staffmanagement.enable2fa", - DISABLE_TWO_FACTOR: "staffmanagement.disable2fa", - RECOVERY_TWO_FACTOR: "staffmanagement.recovery2fa", + CREATE: "staffmanagement.addemployee", + UPDATE: "staffmanagement.updateemployee", + DELETE: "staffmanagement.deleteworkerphoto", + VIEW: "staffmanagement.selectemployeeinfobyemployeeid", + STATUS: "staffmanagement.manageremployeeaccount", + RESET: "staffmanagement.resetemployeeaccountpassword", + ASSIGN_VIEW: "system:user:assign.selectpermissionlist", + GET_TWO_FACTOR: "staffmanagement.gettwofactorstatus", + GENERATE_TWO_FACTOR: "staffmanagement.generatetwofactorsetup", + ENABLE_TWO_FACTOR: "staffmanagement.enabletwofactor", + DISABLE_TWO_FACTOR: "staffmanagement.disabletwofactor", + RECOVERY_TWO_FACTOR: "staffmanagement.regeneratetwofactorrecoverycodes", }; // ==================== 房间信息管理模块 ==================== // 房间管理 (Room Management) export const RoomManagementPermissions = { - CREATE: "roommanagement.create", - UPDATE: "roommanagement.update", - DELETE: "roommanagement.delete", + CREATE: "roommanagement.insertroom", + UPDATE: "roommanagement.updateroom", + DELETE: "roommanagement.deleteroom", }; // 房间配置 (Room Config Management) export const RoomConfigPermissions = { - CREATE: "roomconfig.create", - UPDATE: "roomconfig.update", - DELETE: "roomconfig.delete", + CREATE: "roomconfig.insertroomtype", + UPDATE: "roomconfig.updateroomtype", + DELETE: "roomconfig.deleteroomtype", }; // 预约管理 (Reservation Management) export const ReservationManagementPermissions = { - CREATE: "resermanagement.create", - UPDATE: "resermanagement.update", - DELETE: "resermanagement.delete", + CREATE: "resermanagement.inserreserinfo", + UPDATE: "resermanagement.updatereserinfo", + DELETE: "resermanagement.deletereserinfo", }; // ==================== 物料管理模块 ==================== // 商品管理 (Goods Management) export const GoodsManagementPermissions = { - CREATE: "goodsmanagement.create", - UPDATE: "goodsmanagement.update", - DELETE: "goodsmanagement.delete", + CREATE: "goodsmanagement.insertsellthing", + UPDATE: "goodsmanagement.updatesellthing", + DELETE: "goodsmanagement.deletesellthing", }; // ==================== 财务管理模块 ==================== // 内部财务 (Internal Finance Management) export const InternalFinancePermissions = { - CREATE: "internalfinance.create", - UPDATE: "internalfinance.update", - DELETE: "internalfinance.delete", + CREATE: "internalfinance.addassetinfo", + UPDATE: "internalfinance.updassetinfo", + DELETE: "internalfinance.delassetinfo", }; // ==================== 监督管理模块 ==================== // 监督信息 (Supervision Information) export const SupervisionPermissions = { - CREATE: "supervisioninfo.create", - UPDATE: "supervisioninfo.update", - DELETE: "supervisioninfo.delete", + CREATE: "supervisioninfo.insertsupervisionstatistics", + UPDATE: "supervisioninfo.updatesupervisionstatistics", + DELETE: "supervisioninfo.deletesupervisionstatistics", }; // ==================== 水电管理模块 ==================== // 水电信息 (Hydroelectricity Information) export const HydroelectricityPermissions = { - UPDATE: "hydroelectricinformation.update", - DELETE: "hydroelectricinformation.delete", + UPDATE: "hydroelectricinformation.updateenergymanagementinfo", + DELETE: "hydroelectricinformation.deleteenergymanagementinfo", }; // ==================== 系统行为管理模块 ==================== diff --git a/src/config/apiEndpoints.js b/src/config/apiEndpoints.js new file mode 100644 index 0000000000000000000000000000000000000000..3f6677e8a3b8f0bacd9b3bf362965ef82bd3824a --- /dev/null +++ b/src/config/apiEndpoints.js @@ -0,0 +1,117 @@ +import { FLAT_API_ROUTES } from "./apiRoutes"; + +const ROOT_ROUTE_OVERRIDES = { + VERSION: { group: "SYSTEM", name: "VERSION" }, +}; + +const toSnakeUpper = (value) => + String(value || "") + .replace(/([a-z0-9])([A-Z])/g, "$1_$2") + .replace(/[^A-Za-z0-9]+/g, "_") + .replace(/^_+|_+$/g, "") + .toUpperCase(); + +const getRouteMeta = (flatKey, path) => { + const override = ROOT_ROUTE_OVERRIDES[flatKey]; + if (override) { + return override; + } + + const segments = String(path || "") + .split("/") + .filter(Boolean); + const controller = segments[0]; + const group = controller ? toSnakeUpper(controller) : "ROOT"; + const groupPrefix = `${group}_`; + + if (flatKey.startsWith(groupPrefix)) { + return { group, name: flatKey.slice(groupPrefix.length) }; + } + + const action = segments[1]; + return { + group, + name: action ? toSnakeUpper(action) : flatKey, + }; +}; + +const validateFlatRoutes = (flatRoutes) => { + const routeByPath = new Map(); + + for (const [flatKey, path] of Object.entries(flatRoutes)) { + if (typeof path !== "string" || !path.startsWith("/")) { + throw new Error(`Invalid API route path for ${flatKey}: ${path}`); + } + + const existingKey = routeByPath.get(path); + if (existingKey && existingKey !== flatKey) { + throw new Error( + `Duplicate API route path detected: ${path} (${existingKey}, ${flatKey})`, + ); + } + + routeByPath.set(path, flatKey); + } +}; + +const buildGroupedRoutes = (flatRoutes) => { + const grouped = {}; + + for (const [flatKey, path] of Object.entries(flatRoutes)) { + const { group, name } = getRouteMeta(flatKey, path); + grouped[group] ||= {}; + + const existingPath = grouped[group][name]; + if (existingPath && existingPath !== path) { + throw new Error( + `Duplicate API route key detected: ${group}.${name} (${existingPath}, ${path})`, + ); + } + + grouped[group][name] = path; + } + + return grouped; +}; + +validateFlatRoutes(FLAT_API_ROUTES); + +const groupedRoutes = buildGroupedRoutes(FLAT_API_ROUTES); + +export const ROUTES = Object.freeze( + Object.fromEntries( + Object.entries(groupedRoutes).map(([group, routes]) => [ + group, + Object.freeze({ ...routes }), + ]), + ), +); + +export const LEGACY_ROUTES = Object.freeze({ ...FLAT_API_ROUTES }); + +export const API_ROUTE_KEYS = Object.freeze(Object.keys(LEGACY_ROUTES)); + +export const API_ENDPOINTS = { + routes: Object.freeze({ ...ROUTES, ...LEGACY_ROUTES }), + login: { + admin: ROUTES.ADMIN.LOGIN, + employee: ROUTES.EMPLOYEE.EMPLOYEE_LOGIN, + }, + twoFactor: { + admin: "Admin", + employee: "Employee", + }, + loginTypes: { + admin: "admin", + employee: "employee", + }, + dynamic: { + getTwoFactorStatus: (controller) => `/${controller}/GetTwoFactorStatus`, + generateTwoFactorSetup: (controller) => + `/${controller}/GenerateTwoFactorSetup`, + enableTwoFactor: (controller) => `/${controller}/EnableTwoFactor`, + disableTwoFactor: (controller) => `/${controller}/DisableTwoFactor`, + regenerateTwoFactorRecoveryCodes: (controller) => + `/${controller}/RegenerateTwoFactorRecoveryCodes`, + }, +}; diff --git a/src/config/apiRoutes.js b/src/config/apiRoutes.js new file mode 100644 index 0000000000000000000000000000000000000000..95f389c2f96eecb4cca277113ab060934539377b --- /dev/null +++ b/src/config/apiRoutes.js @@ -0,0 +1,168 @@ +// Security baseline: +// - Keep only endpoints actually used by the current frontend. +// - Do not expose a full backend endpoint catalog in client bundles. +export const FLAT_API_ROUTES = Object.freeze({ + ADMIN_ADD_ADMIN: "/Admin/AddAdmin", + ADMIN_ADD_ADMIN_TYPE: "/Admin/AddAdminType", + ADMIN_ASSIGN_USER_PERMISSIONS: "/Admin/AssignUserPermissions", + ADMIN_ASSIGN_USER_ROLES: "/Admin/AssignUserRoles", + ADMIN_DEL_ADMIN: "/Admin/DelAdmin", + ADMIN_DEL_ADMIN_TYPE: "/Admin/DelAdminType", + ADMIN_GET_ALL_ADMIN_LIST: "/Admin/GetAllAdminList", + ADMIN_GET_ALL_ADMIN_TYPES: "/Admin/GetAllAdminTypes", + ADMIN_LOGIN: "/Admin/Login", + ADMIN_LOGOUT: "/Admin/Logout", + ADMIN_READ_USER_DIRECT_PERMISSIONS: "/Admin/ReadUserDirectPermissions", + ADMIN_READ_USER_ROLE_PERMISSIONS: "/Admin/ReadUserRolePermissions", + ADMIN_READ_USER_ROLES: "/Admin/ReadUserRoles", + ADMIN_UPD_ADMIN: "/Admin/UpdAdmin", + ADMIN_UPD_ADMIN_TYPE: "/Admin/UpdAdminType", + ASSET_ADD_ASSET_INFO: "/Asset/AddAssetInfo", + ASSET_DEL_ASSET_INFO: "/Asset/DelAssetInfo", + ASSET_SELECT_ASSET_INFO_ALL: "/Asset/SelectAssetInfoAll", + ASSET_UPD_ASSET_INFO: "/Asset/UpdAssetInfo", + BASE_ADD_DEPT: "/Base/AddDept", + BASE_ADD_EDUCATION: "/Base/AddEducation", + BASE_ADD_NATION: "/Base/AddNation", + BASE_ADD_POSITION: "/Base/AddPosition", + BASE_CREATE_APPOINTMENT_NOTICE_TYPE: "/Base/CreateAppointmentNoticeType", + BASE_DEL_DEPT: "/Base/DelDept", + BASE_DEL_EDUCATION: "/Base/DelEducation", + BASE_DEL_NATION: "/Base/DelNation", + BASE_DEL_POSITION: "/Base/DelPosition", + BASE_DELETE_APPOINTMENT_NOTICE_TYPE: "/Base/DeleteAppointmentNoticeType", + BASE_DELETE_CUSTO_TYPE: "/Base/DeleteCustoType", + BASE_DELETE_PASS_PORT_TYPE: "/Base/DeletePassPortType", + BASE_INSERT_CUSTO_TYPE: "/Base/InsertCustoType", + BASE_INSERT_PASS_PORT_TYPE: "/Base/InsertPassPortType", + BASE_SELECT_APPOINTMENT_NOTICE_TYPE_ALL: + "/Base/SelectAppointmentNoticeTypeAll", + BASE_SELECT_CUSTO_TYPE_ALL: "/Base/SelectCustoTypeAll", + BASE_SELECT_CUSTO_TYPE_ALL_CAN_USE: "/Base/SelectCustoTypeAllCanUse", + BASE_SELECT_DEPT_ALL: "/Base/SelectDeptAll", + BASE_SELECT_EDUCATION_ALL: "/Base/SelectEducationAll", + BASE_SELECT_NATION_ALL: "/Base/SelectNationAll", + BASE_SELECT_PASS_PORT_TYPE_ALL: "/Base/SelectPassPortTypeAll", + BASE_SELECT_PASS_PORT_TYPE_ALL_CAN_USE: "/Base/SelectPassPortTypeAllCanUse", + BASE_SELECT_POSITION_ALL: "/Base/SelectPositionAll", + BASE_SELECT_ROOM_STATE_ALL: "/Base/SelectRoomStateAll", + BASE_SELECT_WORKER_FEATURE_ALL: "/Base/SelectWorkerFeatureAll", + BASE_UPD_DEPT: "/Base/UpdDept", + BASE_UPD_EDUCATION: "/Base/UpdEducation", + BASE_UPD_NATION: "/Base/UpdNation", + BASE_UPD_POSITION: "/Base/UpdPosition", + BASE_UPDATE_APPOINTMENT_NOTICE_TYPE: "/Base/UpdateAppointmentNoticeType", + BASE_UPDATE_CUSTO_TYPE: "/Base/UpdateCustoType", + BASE_UPDATE_PASS_PORT_TYPE: "/Base/UpdatePassPortType", + CUSTOMER_DEL_CUSTOMER_INFO: "/Customer/DelCustomerInfo", + CUSTOMER_INSERT_CUSTOMER_INFO: "/Customer/InsertCustomerInfo", + CUSTOMER_PERMISSION_ASSIGN_USER_PERMISSIONS: + "/CustomerPermission/AssignUserPermissions", + CUSTOMER_PERMISSION_ASSIGN_USER_ROLES: "/CustomerPermission/AssignUserRoles", + CUSTOMER_PERMISSION_READ_USER_DIRECT_PERMISSIONS: + "/CustomerPermission/ReadUserDirectPermissions", + CUSTOMER_PERMISSION_READ_USER_ROLE_PERMISSIONS: + "/CustomerPermission/ReadUserRolePermissions", + CUSTOMER_PERMISSION_READ_USER_ROLES: "/CustomerPermission/ReadUserRoles", + CUSTOMER_SELECT_CUSTOMERS: "/Customer/SelectCustomers", + CUSTOMER_UPD_CUSTOMER_INFO: "/Customer/UpdCustomerInfo", + DASHBOARD_BUSINESS_STATISTICS: "/Dashboard/BusinessStatistics", + DASHBOARD_HUMAN_RESOURCES_STATISTICS: "/Dashboard/HumanResourcesStatistics", + DASHBOARD_LOGISTICS_STATISTICS: "/Dashboard/LogisticsStatistics", + DASHBOARD_ROOM_STATISTICS: "/Dashboard/RoomStatistics", + EMPLOYEE_ADD_EMPLOYEE: "/Employee/AddEmployee", + EMPLOYEE_CHECK_SELECT_CHECK_INFO_BY_EMPLOYEE_ID: + "/EmployeeCheck/SelectCheckInfoByEmployeeId", + EMPLOYEE_EMPLOYEE_LOGIN: "/Employee/EmployeeLogin", + EMPLOYEE_HISTORY_SELECT_HISTORY_BY_EMPLOYEE_ID: + "/EmployeeHistory/SelectHistoryByEmployeeId", + EMPLOYEE_MANAGER_EMPLOYEE_ACCOUNT: "/Employee/ManagerEmployeeAccount", + EMPLOYEE_PERMISSION_ASSIGN_USER_PERMISSIONS: + "/EmployeePermission/AssignUserPermissions", + EMPLOYEE_PERMISSION_ASSIGN_USER_ROLES: "/EmployeePermission/AssignUserRoles", + EMPLOYEE_PERMISSION_READ_USER_DIRECT_PERMISSIONS: + "/EmployeePermission/ReadUserDirectPermissions", + EMPLOYEE_PERMISSION_READ_USER_ROLE_PERMISSIONS: + "/EmployeePermission/ReadUserRolePermissions", + EMPLOYEE_PERMISSION_READ_USER_ROLES: "/EmployeePermission/ReadUserRoles", + EMPLOYEE_PHOTO_INSERT_WORKER_PHOTO: "/EmployeePhoto/InsertWorkerPhoto", + EMPLOYEE_RESET_EMPLOYEE_ACCOUNT_PASSWORD: + "/Employee/ResetEmployeeAccountPassword", + EMPLOYEE_SELECT_EMPLOYEE_ALL: "/Employee/SelectEmployeeAll", + EMPLOYEE_SELECT_EMPLOYEE_INFO_BY_EMPLOYEE_ID: + "/Employee/SelectEmployeeInfoByEmployeeId", + EMPLOYEE_UPDATE_EMPLOYEE: "/Employee/UpdateEmployee", + ENERGY_MANAGEMENT_DELETE_ENERGY_MANAGEMENT_INFO: + "/EnergyManagement/DeleteEnergyManagementInfo", + ENERGY_MANAGEMENT_INSERT_ENERGY_MANAGEMENT_INFO: + "/EnergyManagement/InsertEnergyManagementInfo", + ENERGY_MANAGEMENT_SELECT_ENERGY_MANAGEMENT_INFO: + "/EnergyManagement/SelectEnergyManagementInfo", + ENERGY_MANAGEMENT_UPDATE_ENERGY_MANAGEMENT_INFO: + "/EnergyManagement/UpdateEnergyManagementInfo", + LOGIN_GET_CSRF_TOKEN: "/Login/GetCSRFToken", + LOGIN_REFRESH_CSRF_TOKEN: "/Login/RefreshCSRFToken", + MENU_BUILD_MENU_ALL: "/Menu/BuildMenuAll", + MENU_DELETE_MENU: "/Menu/DeleteMenu", + MENU_INSERT_MENU: "/Menu/InsertMenu", + MENU_SELECT_MENU_ALL: "/Menu/SelectMenuAll", + MENU_UPDATE_MENU: "/Menu/UpdateMenu", + PERMISSION_SELECT_PERMISSION_LIST: "/Permission/SelectPermissionList", + PROMOTION_CONTENT_ADD_PROMOTION_CONTENT: + "/PromotionContent/AddPromotionContent", + PROMOTION_CONTENT_DELETE_PROMOTION_CONTENT: + "/PromotionContent/DeletePromotionContent", + PROMOTION_CONTENT_SELECT_PROMOTION_CONTENT_ALL: + "/PromotionContent/SelectPromotionContentAll", + PROMOTION_CONTENT_UPDATE_PROMOTION_CONTENT: + "/PromotionContent/UpdatePromotionContent", + RESER_DELETE_RESER_INFO: "/Reser/DeleteReserInfo", + RESER_INSER_RESER_INFO: "/Reser/InserReserInfo", + RESER_SELECT_RESER_ALL: "/Reser/SelectReserAll", + RESER_SELECT_RESER_TYPE_ALL: "/Reser/SelectReserTypeAll", + RESER_UPDATE_RESER_INFO: "/Reser/UpdateReserInfo", + REWARD_PUNISHMENT_SELECT_ALL_REWARD_PUNISHMENT_BY_EMPLOYEE_ID: + "/RewardPunishment/SelectAllRewardPunishmentByEmployeeId", + ROLE_ASSIGN_ROLE_USERS: "/Role/AssignRoleUsers", + ROLE_DELETE_ROLE: "/Role/DeleteRole", + ROLE_GRANT_ROLE_PERMISSIONS: "/Role/GrantRolePermissions", + ROLE_INSERT_ROLE: "/Role/InsertRole", + ROLE_READ_ROLE_PERMISSIONS: "/Role/ReadRolePermissions", + ROLE_READ_ROLE_USERS: "/Role/ReadRoleUsers", + ROLE_SELECT_ROLE_LIST: "/Role/SelectRoleList", + ROLE_UPDATE_ROLE: "/Role/UpdateRole", + ROOM_DELETE_ROOM: "/Room/DeleteRoom", + ROOM_INSERT_ROOM: "/Room/InsertRoom", + ROOM_SELECT_CAN_USE_ROOM_ALL: "/Room/SelectCanUseRoomAll", + ROOM_SELECT_ROOM_ALL: "/Room/SelectRoomAll", + ROOM_TYPE_DELETE_ROOM_TYPE: "/RoomType/DeleteRoomType", + ROOM_TYPE_INSERT_ROOM_TYPE: "/RoomType/InsertRoomType", + ROOM_TYPE_SELECT_ROOM_TYPES_ALL: "/RoomType/SelectRoomTypesAll", + ROOM_TYPE_UPDATE_ROOM_TYPE: "/RoomType/UpdateRoomType", + ROOM_UPDATE_ROOM: "/Room/UpdateRoom", + SELLTHING_DELETE_SELLTHING: "/Sellthing/DeleteSellthing", + SELLTHING_INSERT_SELLTHING: "/Sellthing/InsertSellthing", + SELLTHING_SELECT_SELLTHING_ALL: "/Sellthing/SelectSellthingAll", + SELLTHING_UPDATE_SELLTHING: "/Sellthing/UpdateSellthing", + SPEND_INSERT_SPEND_INFO: "/Spend/InsertSpendInfo", + SPEND_SELECT_SPEND_INFO_ALL: "/Spend/SelectSpendInfoAll", + SPEND_UPD_SPEND_INFO: "/Spend/UpdSpendInfo", + SUPERVISION_STATISTICS_DELETE_SUPERVISION_STATISTICS: + "/SupervisionStatistics/DeleteSupervisionStatistics", + SUPERVISION_STATISTICS_INSERT_SUPERVISION_STATISTICS: + "/SupervisionStatistics/InsertSupervisionStatistics", + SUPERVISION_STATISTICS_SELECT_SUPERVISION_STATISTICS_ALL: + "/SupervisionStatistics/SelectSupervisionStatisticsAll", + SUPERVISION_STATISTICS_UPDATE_SUPERVISION_STATISTICS: + "/SupervisionStatistics/UpdateSupervisionStatistics", + UTILITY_DELETE_OPERATIONLOG: "/Utility/DeleteOperationlog", + UTILITY_DELETE_OPERATIONLOG_BY_RANGE: "/Utility/DeleteOperationlogByRange", + UTILITY_SELECT_CARD_CODE: "/Utility/SelectCardCode", + UTILITY_SELECT_OPERATIONLOG_ALL: "/Utility/SelectOperationlogAll", + UTILITY_SELECT_REQUESTLOG_ALL: "/Utility/SelectRequestlogAll", + VERSION: "/version", + VIP_RULE_ADD_VIP_RULE: "/VipRule/AddVipRule", + VIP_RULE_DEL_VIP_RULE: "/VipRule/DelVipRule", + VIP_RULE_SELECT_VIP_RULE_LIST: "/VipRule/SelectVipRuleList", + VIP_RULE_UPD_VIP_RULE: "/VipRule/UpdVipRule", +}); diff --git a/src/directives/permission.js b/src/directives/permission.js index d05c1cc2a2cda6c6eef122d5005f6c2c77657994..3bec7221ad50bc3fd52b863b4410b2fd9ce79227 100644 --- a/src/directives/permission.js +++ b/src/directives/permission.js @@ -1,40 +1,93 @@ -import { getAllowedMenuKeys, getAllowedPerms } from "@/utils/permission"; +import { + hasPermission, + isPermissionCacheInitialized, +} from "@/utils/permission"; + +const permStateMap = new WeakMap(); /** - * v-perm 指令:支持菜单键或按钮权限码两种来源 - * - 传入 string 或 string[],例如:v-perm="'system:role:grant'" 或 v-perm="['department.create','department.update']" - * - .all 修饰符表示需要全部命中(默认任意命中其一即可) + * v-perm directive: supports both menu keys and button permission codes. + * - accepts string or string[], e.g. v-perm="'system:role:grantrolepermissions'" + * - .all modifier means all required keys must match. */ function isPermitted(required, needAll = false) { - // 未配置要求时默认放行(方便逐步接入) - if (!required || (Array.isArray(required) && required.length === 0)) + if (!required || (Array.isArray(required) && required.length === 0)) { return true; + } - // 合并“菜单键集合 + 按钮权限码集合” - const allowedSet = new Set([...getAllowedMenuKeys(), ...getAllowedPerms()]); + if (!isPermissionCacheInitialized()) { + return null; + } - // 首屏或尚未拉取权限列表时(两个集合都为空)默认放行,避免按钮被永久隐藏 - if (allowedSet.size === 0) { - return true; + return hasPermission(required, needAll); +} + +function hideElement(el) { + el.style.display = "none"; + el.setAttribute("data-permission-hidden", "true"); +} + +function restoreElement(el) { + const state = permStateMap.get(el); + const originalDisplay = + state && typeof state.originalDisplay === "string" + ? state.originalDisplay + : ""; + el.style.display = originalDisplay; + el.removeAttribute("data-permission-hidden"); +} + +function applyPermission(el, required, needAll) { + const permitted = isPermitted(required, needAll); + if (permitted === null) { + hideElement(el); + el.setAttribute("data-permission-pending", "true"); + return; } - const reqList = Array.isArray(required) ? required : [required]; + el.removeAttribute("data-permission-pending"); + if (!permitted) { + hideElement(el); + return; + } - if (needAll) { - return reqList.every((k) => allowedSet.has(k)); + if (el.getAttribute("data-permission-hidden") === "true") { + restoreElement(el); } - return reqList.some((k) => allowedSet.has(k)); } export default { beforeMount(el, binding) { - const required = binding.value; - const needAll = !!binding.modifiers.all; + const state = { + required: binding.value, + needAll: !!binding.modifiers.all, + originalDisplay: el.style.display || "", + handler: null, + }; + + state.handler = () => { + applyPermission(el, state.required, state.needAll); + }; - const permitted = isPermitted(required, needAll); - if (!permitted) { - el.style.display = "none"; - el.setAttribute("data-permission-hidden", "true"); + permStateMap.set(el, state); + window.addEventListener("permissions-updated", state.handler); + state.handler(); + }, + updated(el, binding) { + const state = permStateMap.get(el); + if (!state) { + return; + } + + state.required = binding.value; + state.needAll = !!binding.modifiers.all; + applyPermission(el, state.required, state.needAll); + }, + unmounted(el) { + const state = permStateMap.get(el); + if (state?.handler) { + window.removeEventListener("permissions-updated", state.handler); } + permStateMap.delete(el); }, }; diff --git a/src/i18n.js b/src/i18n.js index 63248147e1c9972e2c468d4ba6f96949c4ea7379..bba2efcf423d5eeffbdc4a98b88adcc7ff89aea3 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -4,7 +4,7 @@ const messages = { "en-US": { message: { hello: "hello", - welcome: "Welcome to our application!", + welcome: "Welcome to our application", username: "Username", password: "Password", pleaseInputUserAccount: "Please input user account", @@ -28,6 +28,7 @@ const messages = { success: "Success", error: "Error", undefined: "Undefined", + my: "My", passwordEditTip: "Do not change this field if you do not want to change your password.", required: "Please enter {field}", @@ -90,6 +91,8 @@ const messages = { "2FA enabled. Save your recovery codes now (shown only once).", recoveryCodeLoginTip: "Recovery code was used for this login. Rebind your authenticator and regenerate recovery codes immediately.", + permissionCheckError: + "Permission check failed, Please retry or contact administrator.", refreshData: "Refresh Data", addSuccess: "Add Success", updateSuccess: "Update Success", @@ -128,10 +131,38 @@ const messages = { duplicate: "Duplicate Data", serverError: "Server Error", networkError: "Network Error", + concurrencyConflict: + "Data was modified by another user. Please refresh and retry.", requestFailed: "Request Failed", loginExpired: "Login expired, please log in again", unexpectedError: "An unexpected error occurred", serverVersion: "Server Version", + homeAccountLabel: "Account", + homeUpdatedAtLabel: "Updated At", + homeAdminDetailPermissionFallback: + "Current account lacks permission to load admin profile details. Showing local account information.", + homeSystemEnvironmentTitle: "System Environment", + homeApiBaseUrl: "API Base URL", + homeRuntimeMode: "Runtime Mode", + homeTimezone: "Timezone", + homeBrowserLanguage: "Browser Language", + homeAppLanguage: "App Language", + homeTokenIssuedAt: "Token Issued At", + homeTokenExpiresAt: "Token Expires At", + homeTokenRemaining: "Token Remaining", + homeAccountTypeAdmin: "Admin Account", + homeAccountTypeEmployee: "Employee Account", + homeCurrentAdminInfo: "Current Admin Info", + homeCurrentEmployeeInfo: "Current Employee Info", + homeExpired: "Expired", + homeEmployeeIdentityNotFound: + "Current employee identity is missing. Unable to load employee details.", + homeEmployeeDetailEmpty: "Employee detail API returned empty data.", + homeEmployeeProfileLoadFailed: "Failed to load employee profile.", + homeAdminDetailNotFound: + "Admin detail API did not return data for current account.", + homeAdminProfileLoadFailed: "Failed to load admin profile.", + homeEnvironmentLoadFailed: "Failed to load environment configuration.", basic: "Basic Information", finance: "Finance Management", supervisionmanagement: "Supervision Management", @@ -729,7 +760,7 @@ const messages = { "zh-CN": { message: { hello: "你好", - welcome: "欢迎使用我们的应用程序!", + welcome: "欢迎使用我们的应用程序", username: "用户账号", password: "用户密码", pleaseInputUserAccount: "请输入用户账号", @@ -752,6 +783,7 @@ const messages = { success: "成功", error: "错误", id: "编号", + my: "我的", passwordEditTip: "留空表示不修改密码", required: "请输入 {field}", description: "描述", @@ -807,6 +839,7 @@ const messages = { "2FA 已启用,请立即保存恢复备用码(仅展示一次)。", recoveryCodeLoginTip: "本次登录使用了恢复备用码,请立即重新绑定验证器并重置备用码。", + permissionCheckError: "权限检查发生错误,请重试或联系管理员", refreshData: "刷新数据", addSuccess: "添加成功", updateSuccess: "更新成功", @@ -845,6 +878,7 @@ const messages = { duplicate: "重复数据", serverError: "服务器错误", networkError: "网络错误", + concurrencyConflict: "数据已被其他用户修改,请刷新后重试", requestFailed: "请求失败", loginExpired: "登录已过期,请重新登录", unexpectedError: "发生了意外错误", @@ -1478,6 +1512,30 @@ const messages = { present: "正常", late: "迟到", absent: "旷工", + homeAccountLabel: "账号", + homeUpdatedAtLabel: "更新时间", + homeAdminDetailPermissionFallback: + "当前账号缺少管理员详情权限,已降级展示本地账号信息。", + homeSystemEnvironmentTitle: "系统环境配置信息", + homeApiBaseUrl: "API 基址", + homeRuntimeMode: "运行模式", + homeTimezone: "时区", + homeBrowserLanguage: "浏览器语言", + homeAppLanguage: "系统语言", + homeTokenIssuedAt: "Token 生效时间", + homeTokenExpiresAt: "Token 过期时间", + homeTokenRemaining: "Token 剩余有效期", + homeAccountTypeAdmin: "管理员账号", + homeAccountTypeEmployee: "员工账号", + homeCurrentAdminInfo: "当前管理员信息", + homeCurrentEmployeeInfo: "当前员工信息", + homeExpired: "已过期", + homeEmployeeIdentityNotFound: "未找到当前员工标识,无法加载员工详情。", + homeEmployeeDetailEmpty: "员工详情接口未返回有效数据。", + homeEmployeeProfileLoadFailed: "员工信息加载失败", + homeAdminDetailNotFound: "管理员详情接口未返回当前账号数据。", + homeAdminProfileLoadFailed: "管理员信息加载失败", + homeEnvironmentLoadFailed: "系统环境配置加载失败", }, }, }; diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index a3bd4aa8d2f35d5d4915b315c830b013b992aace..66803acb8bb23f5a3e3f62fd85b53d1526ce1702 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -244,9 +244,23 @@ import { formatDateTime, showErrorNotification, showSuccessNotification, + showWarningNotification, } from "@/utils/index"; +import { resolveErrorMessage } from "@/common/errorHandler"; import { fetchMenusTree } from "../api/menuapi"; import { BaseFields } from "@/entities/common.entity"; +import { + readUserDirectPermissions as readAdminUserDirectPermissions, + readUserRolePermissions as readAdminUserRolePermissions, +} from "@/api/administratorapi"; +import { + readUserDirectPermissions as readEmployeeUserDirectPermissions, + readUserRolePermissions as readEmployeeUserRolePermissions, +} from "@/api/employeepermissionapi"; +import { + readUserDirectPermissions as readCustomerUserDirectPermissions, + readUserRolePermissions as readCustomerUserRolePermissions, +} from "@/api/customerpermissionapi"; import { AdministratorManagementPermissions, StaffManagementPermissions, @@ -267,6 +281,14 @@ import { signOut, getServerVersion, } from "../api/basicapi"; +import { clearRowVersionCache } from "@/api/baseapi"; +import { isPermissionDeniedErrorCode } from "@/common/errorCodes"; +import { decodeJwtPayload, getUserIdentity, isTokenExpired } from "@/utils/jwt"; +import { + clearStoredToken, + getStoredToken, + getStoredTokenCacheKey, +} from "@/utils/tokenStorage"; const serverVersion = ref(""); @@ -426,6 +448,152 @@ const parseStoredAllowedPermissions = () => { } }; +const unique = (arr) => Array.from(new Set((arr || []).filter(Boolean))); + +const extractPermissionCodes = (items) => + unique( + (Array.isArray(items) ? items : []) + .map((item) => + typeof item === "string" + ? item + : item?.PermissionNumber || item?.PermissionCode || "", + ) + .filter(Boolean), + ); + +const clearAllowedPathsCache = () => { + localStorage.removeItem("allowedPaths"); + localStorage.removeItem("allowedMenuKeys"); + localStorage.removeItem("allowedPerms"); + localStorage.removeItem("allowedPathsToken"); + localStorage.removeItem("allowedPathsExpire"); + localStorage.removeItem("allowedPathsIssuedAt"); +}; + +const normalizePath = (path) => (path || "").replace(/\/$/, ""); + +const persistAllowedAccessCache = ( + allowedMenuKeys = [], + allowedPaths = [], + allowedPerms = [], +) => { + localStorage.setItem("allowedMenuKeys", JSON.stringify(allowedMenuKeys)); + localStorage.setItem("allowedPaths", JSON.stringify(allowedPaths)); + localStorage.setItem("allowedPerms", JSON.stringify(allowedPerms)); + + const currentToken = getStoredToken(); + const currentTokenCacheKey = getStoredTokenCacheKey(); + localStorage.setItem("allowedPathsToken", currentTokenCacheKey); + + const tokenPayload = decodeJwtPayload(currentToken); + const tokenIssuedAt = Number(tokenPayload?.iat); + const tokenExpireAt = Number(tokenPayload?.exp); + + if (Number.isFinite(tokenIssuedAt)) { + localStorage.setItem("allowedPathsIssuedAt", String(tokenIssuedAt)); + } else { + localStorage.removeItem("allowedPathsIssuedAt"); + } + + if (Number.isFinite(tokenExpireAt)) { + localStorage.setItem("allowedPathsExpire", String(tokenExpireAt)); + } else { + localStorage.removeItem("allowedPathsExpire"); + } + + allowedPermissionCodes.value = allowedPerms; + window.dispatchEvent(new Event("permissions-updated")); +}; + +const isPermissionDeniedError = (error) => { + const code = Number( + error?.code || error?.Code || error?.response?.data?.Code || 0, + ); + if (isPermissionDeniedErrorCode(code)) { + return true; + } + + const message = resolveErrorMessage(error, ""); + return /no\s*permission|forbidden|denied|无权限|缺少权限/i.test(message); +}; + +const getPermissionErrorMessage = () => { + const translated = t("message.permissionCheckError"); + return translated === "message.permissionCheckError" + ? "Permission check failed. Please retry or contact administrator." + : translated; +}; + +const getLocalIdentityFallback = () => { + const candidates = [ + localStorage.getItem("account"), + localStorage.getItem("username"), + ]; + + for (const candidate of candidates) { + const normalized = String(candidate || "").trim(); + if (normalized) { + return normalized; + } + } + + return ""; +}; + +const fetchCurrentUserPermissionCodes = async () => { + const permissionReadersByLoginType = { + admin: { + readRolePermissions: readAdminUserRolePermissions, + readDirectPermissions: readAdminUserDirectPermissions, + }, + employee: { + readRolePermissions: readEmployeeUserRolePermissions, + readDirectPermissions: readEmployeeUserDirectPermissions, + }, + customer: { + readRolePermissions: readCustomerUserRolePermissions, + readDirectPermissions: readCustomerUserDirectPermissions, + }, + }; + + const readers = + permissionReadersByLoginType[loginType.value] || + permissionReadersByLoginType.admin; + + const identity = + getUserIdentity(loginType.value, getStoredToken()) || + getLocalIdentityFallback(); + + if (!identity) { + return { ok: false, reason: "identity_missing", codes: [] }; + } + + try { + const [rolePermissions, directPermissions] = await Promise.all([ + readers.readRolePermissions(identity), + readers.readDirectPermissions(identity), + ]); + + const mergedCodes = unique([ + ...extractPermissionCodes(rolePermissions), + ...extractPermissionCodes(directPermissions), + ]); + + return { ok: true, codes: mergedCodes }; + } catch (error) { + const reason = isPermissionDeniedError(error) + ? "permission_denied" + : "request_failed"; + + if (import.meta.env.DEV && reason === "request_failed") { + // eslint-disable-next-line no-console + console.warn("[Permissions] Failed to sync permission codes:", error); + } + + return { ok: false, reason, codes: [] }; + } +}; + const syncAllowedPermissions = () => { allowedPermissionCodes.value = parseStoredAllowedPermissions(); }; @@ -476,7 +644,7 @@ const selectedKeys = computed(() => { const refreshMenu = async () => { try { const menuItems = await fetchMenusTree({ - [BaseFields.USER_TOKEN]: localStorage.getItem("token"), + [BaseFields.USER_TOKEN]: getStoredToken(), }); const currentOpenKeys = [...openKeys.value]; @@ -511,20 +679,47 @@ const refreshMenu = async () => { }; const { keys, paths, perms } = collect(menuData.value, [], [], []); - const unique = (arr) => Array.from(new Set(arr.filter(Boolean))); const allowedMenuKeys = unique(keys); const allowedPaths = unique(paths).map((p) => (p || "").replace(/\/$/, "")); - const allowedPermList = unique(perms); + const allowedPermListFromMenu = unique(perms); + const syncedPermissionCodes = await fetchCurrentUserPermissionCodes(); + const allowedPermList = syncedPermissionCodes.ok + ? syncedPermissionCodes.codes + : allowedPermListFromMenu; + + const shouldShowPermissionWarning = + !syncedPermissionCodes.ok && + syncedPermissionCodes.reason === "request_failed" && + allowedPermList.length === 0; + + if (shouldShowPermissionWarning) { + showWarningNotification(getPermissionErrorMessage()); + } - localStorage.setItem("allowedMenuKeys", JSON.stringify(allowedMenuKeys)); - localStorage.setItem("allowedPaths", JSON.stringify(allowedPaths)); - localStorage.setItem("allowedPerms", JSON.stringify(allowedPermList)); - allowedPermissionCodes.value = allowedPermList; + persistAllowedAccessCache(allowedMenuKeys, allowedPaths, allowedPermList); + const currentPath = normalizePath(router.currentRoute.value.path); + const hasDashboardPath = allowedPaths.some( + (path) => normalizePath(path) === "/dashboard", + ); + if (currentPath === "/dashboard" && !hasDashboardPath) { + await router.replace("/home"); + } // 3) 还原展开状态 openKeys.value = currentOpenKeys; } catch (error) { - showErrorNotification(error.message); + const message = resolveErrorMessage(error, t("message.fetchMenuError")); + showErrorNotification(message); + + if (isPermissionDeniedError(error)) { + menuData.value = []; + persistAllowedAccessCache([], [], []); + + const currentPath = normalizePath(router.currentRoute.value.path); + if (currentPath !== "/home") { + await router.replace("/home"); + } + } } }; @@ -581,7 +776,12 @@ const updateTwoFactorRecoveryCode = (value) => { twoFactorRecoveryCode.value = normalizeCodeOrRecoveryCode(value); }; -const copyTextWithExecCommand = (text) => { +const copyTextWithClipboard = async (text) => { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } + const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; @@ -589,9 +789,14 @@ const copyTextWithExecCommand = (text) => { document.body.appendChild(textArea); textArea.focus(); textArea.select(); - const copied = document.execCommand("copy"); - document.body.removeChild(textArea); - return copied; + + try { + return document.execCommand("copy"); + } catch (error) { + return false; + } finally { + document.body.removeChild(textArea); + } }; const copyAllRecoveryCodes = async () => { @@ -601,9 +806,8 @@ const copyAllRecoveryCodes = async () => { } try { - if (navigator?.clipboard?.writeText) { - await navigator.clipboard.writeText(content); - } else if (!copyTextWithExecCommand(content)) { + const copied = await copyTextWithClipboard(content); + if (!copied) { throw new Error("Copy failed"); } showSuccessNotification(t("message.recoveryCodesCopiedSuccess")); @@ -767,23 +971,57 @@ const handleDisableTwoFactor = async () => { }; const checkLoginStatus = () => { - const storedToken = localStorage.getItem("token"); - if (storedToken) { + const storedToken = getStoredToken(); + const storedTokenCacheKey = getStoredTokenCacheKey(); + + if (storedToken && !isTokenExpired(storedToken)) { isLoggedIn.value = true; loginType.value = localStorage.getItem("loginType") || "admin"; - syncAllowedPermissions(); - const storedUsername = localStorage.getItem("username"); - if (storedUsername) { - username.value = storedUsername; + + const tokenPayload = decodeJwtPayload(storedToken); + const tokenExpireAt = Number(tokenPayload?.exp); + const cachedAllowedToken = localStorage.getItem("allowedPathsToken") || ""; + const cachedAllowedExpireAt = Number( + localStorage.getItem("allowedPathsExpire"), + ); + const now = Math.floor(Date.now() / 1000); + + const isAllowedTokenMismatch = + cachedAllowedToken.length > 0 && + cachedAllowedToken !== storedTokenCacheKey; + const isAllowedCacheExpired = + Number.isFinite(cachedAllowedExpireAt) && cachedAllowedExpireAt <= now; + const isAllowedExpireMismatch = + Number.isFinite(tokenExpireAt) && + Number.isFinite(cachedAllowedExpireAt) && + cachedAllowedExpireAt !== tokenExpireAt; + + if ( + isAllowedTokenMismatch || + isAllowedCacheExpired || + isAllowedExpireMismatch + ) { + clearAllowedPathsCache(); + allowedPermissionCodes.value = []; } else { - username.value = "User"; + syncAllowedPermissions(); } - } else { - isLoggedIn.value = false; - loginType.value = "admin"; - allowedPermissionCodes.value = []; - username.value = ""; + + const storedUsername = localStorage.getItem("username"); + username.value = storedUsername || "User"; + return; } + + isLoggedIn.value = false; + loginType.value = "admin"; + allowedPermissionCodes.value = []; + username.value = ""; + clearStoredToken(); + localStorage.removeItem("username"); + localStorage.removeItem("account"); + localStorage.removeItem("loginType"); + clearAllowedPathsCache(); + clearRowVersionCache(); }; const logout = async () => { @@ -792,13 +1030,12 @@ const logout = async () => { showErrorNotification(response.Message || t("message.logoutFailed")); return; } - localStorage.removeItem("token"); + clearStoredToken(); localStorage.removeItem("username"); localStorage.removeItem("account"); localStorage.removeItem("loginType"); - localStorage.removeItem("allowedPaths"); - localStorage.removeItem("allowedMenuKeys"); - localStorage.removeItem("allowedPerms"); + clearAllowedPathsCache(); + clearRowVersionCache(); isLoggedIn.value = false; allowedPermissionCodes.value = []; username.value = ""; diff --git a/src/router/index.js b/src/router/index.js index 9d66eee8fb9bfc5a2335a035daa8f9436be55251..42250f6488d4e538c25d6ce0e57812a508a3d37d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -3,6 +3,46 @@ import { checkTokenValidity } from "../utils/auth"; import { getPageTitle } from "@/utils/pageTitle"; import i18n from "@/i18n"; import { hasPermission } from "@/utils/permission"; +import { getStoredTokenCacheKey } from "@/utils/tokenStorage"; + +const normalizePath = (path) => (path || "").replace(/\/$/, ""); + +const canAccessDashboardFromCache = () => { + if (typeof window === "undefined") { + return false; + } + + const currentTokenCacheKey = getStoredTokenCacheKey(); + if (!currentTokenCacheKey) { + return false; + } + + const rawAllowedPaths = localStorage.getItem("allowedPaths"); + const allowedPathsToken = localStorage.getItem("allowedPathsToken") || ""; + const allowedPathsExpire = Number(localStorage.getItem("allowedPathsExpire")); + const now = Math.floor(Date.now() / 1000); + const hasAllowedPathsCache = + rawAllowedPaths !== null && + allowedPathsToken.length > 0 && + allowedPathsToken === currentTokenCacheKey && + Number.isFinite(allowedPathsExpire) && + allowedPathsExpire > now; + + if (!hasAllowedPathsCache) { + return false; + } + + try { + const parsed = rawAllowedPaths ? JSON.parse(rawAllowedPaths) : []; + const allowedPaths = Array.isArray(parsed) ? parsed : []; + return allowedPaths.some((path) => normalizePath(path) === "/dashboard"); + } catch (error) { + return false; + } +}; + +const resolveRootRedirectPath = () => + canAccessDashboardFromCache() ? "/dashboard" : "/home"; // 路由组件改为懒加载,减少首屏体积 const NotFound = () => import("../views/responsepage/NotFound.vue"); @@ -68,6 +108,7 @@ const CustomerAccountPermission = () => const EmployeeAccountPermission = () => import("../views/systemmanagement/EmployeeAccountPermissionView.vue"); +const Home = () => import("../views/home/HomeView.vue"); const Dashboard = () => import("../views/dashboard/DashboardView.vue"); const routes = [ @@ -77,7 +118,7 @@ const routes = [ children: [ { path: "/", - redirect: "/dashboard", + redirect: () => resolveRootRedirectPath(), meta: { requiresAuth: true }, }, { @@ -246,19 +287,34 @@ const routes = [ path: "/accountpermission", name: "accountpermission", component: AccountPermission, - meta: { requiresAuth: true, requiredPerm: "system:user:assign.view" }, + meta: { + requiresAuth: true, + requiredPerm: "system:user:assign.selectpermissionlist", + }, }, { path: "/customeraccountpermission", name: "customeraccountpermission", component: CustomerAccountPermission, - meta: { requiresAuth: true, requiredPerm: "system:user:assign.view" }, + meta: { + requiresAuth: true, + requiredPerm: "system:user:assign.selectpermissionlist", + }, }, { path: "/employeeaccountpermission", name: "employeeaccountpermission", component: EmployeeAccountPermission, - meta: { requiresAuth: true, requiredPerm: "system:user:assign.view" }, + meta: { + requiresAuth: true, + requiredPerm: "system:user:assign.selectpermissionlist", + }, + }, + { + path: "/home", + name: "home", + component: Home, + meta: { requiresAuth: true, ignoreAllowedPaths: true }, }, { path: "/dashboard", @@ -286,13 +342,13 @@ const router = createRouter({ routes, }); -router.beforeEach((to, from, next) => { +router.beforeEach(async (to, from, next) => { if (to.meta.public) { return next(); } if (to.matched.some((record) => record.meta.requiresAuth)) { - if (!checkTokenValidity()) { + if (!(await checkTokenValidity())) { return next({ name: "signin" }); } @@ -303,6 +359,10 @@ router.beforeEach((to, from, next) => { } // 支持基于"权限码或菜单键"直达的路由控制(未出现在菜单里的独立页面) + if (to.meta && to.meta.ignoreAllowedPaths) { + return next(); + } + const requiredPerm = to.meta && to.meta.requiredPerm; if (requiredPerm && hasPermission(requiredPerm)) { return next(); @@ -310,11 +370,31 @@ router.beforeEach((to, from, next) => { // 基于后端返回的菜单(权限)结果做前端路由访问控制(路径白名单) const raw = localStorage.getItem("allowedPaths"); + const currentTokenCacheKey = getStoredTokenCacheKey(); + const allowedPathsToken = localStorage.getItem("allowedPathsToken") || ""; + const allowedPathsExpire = Number( + localStorage.getItem("allowedPathsExpire"), + ); + const now = Math.floor(Date.now() / 1000); + const hasAllowedPathsCache = + raw !== null && + allowedPathsToken.length > 0 && + allowedPathsToken === currentTokenCacheKey && + Number.isFinite(allowedPathsExpire) && + allowedPathsExpire > now; const parsed = raw ? JSON.parse(raw) : []; const allowedPaths = Array.isArray(parsed) ? parsed : []; - // 尚未拉取菜单(例如刚进入系统时),先放行,由主布局内拉取并写入 - if (allowedPaths.length > 0) { + // 尚未初始化权限缓存时(例如刚进入系统),先放行。 + // 已初始化但为空数组时,说明当前账号没有可访问路径,应拦截。 + if (hasAllowedPathsCache) { + if (allowedPaths.length === 0) { + if ((to.path || "").replace(/\/$/, "") === "/dashboard") { + return next({ path: "/home" }); + } + return next({ name: "NotFound" }); + } + const normalize = (p) => (p || "").replace(/\/$/, ""); const current = normalize(to.path); @@ -327,6 +407,9 @@ router.beforeEach((to, from, next) => { .some((p) => matchPath(p, current)); if (!permitted) { + if (current === "/dashboard") { + return next({ path: "/home" }); + } return next({ name: "NotFound" }); } } diff --git a/src/utils/auth.js b/src/utils/auth.js index 5803819903b36609b808a6456c27217a54a44e45..40e59241c0f285421acb16cf84b29b9293538e9a 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,17 +1,176 @@ -import { jwtDecode } from "jwt-decode"; import router from "@/router"; +import api from "@/api"; import { showErrorNotification } from "@/utils/index"; import i18n from "@/i18n"; import { signOut } from "@/api/basicapi"; +import { clearRowVersionCache } from "@/api/baseapi"; +import { API_ENDPOINTS } from "@/config/apiEndpoints"; +import { + API_SUCCESS_CODE, + HTTP_STATUS, + isPermissionDeniedBusinessCode, + isTokenInvalidBusinessCode, + normalizeCode, +} from "@/common/errorCodes"; +import { BaseFields } from "@/entities/common.entity"; +import { isTokenExpired } from "@/utils/jwt"; +import { clearStoredToken, getStoredToken } from "@/utils/tokenStorage"; + +const TOKEN_VALIDATION_CACHE_TTL_MS = 60 * 1000; +const TOKEN_VALIDATION_MAX_ATTEMPTS = 3; +const TOKEN_VALIDATION_RETRY_BASE_DELAY_MS = 300; + +const tokenValidationCache = { + token: "", + valid: false, + validatedAt: 0, + pendingToken: "", + pendingPromise: null, +}; + +const clearTokenValidationCache = () => { + tokenValidationCache.token = ""; + tokenValidationCache.valid = false; + tokenValidationCache.validatedAt = 0; + tokenValidationCache.pendingToken = ""; + tokenValidationCache.pendingPromise = null; +}; + +const saveTokenValidationResult = (token, valid) => { + tokenValidationCache.token = token; + tokenValidationCache.valid = valid; + tokenValidationCache.validatedAt = Date.now(); + return valid; +}; + +const isTokenValidationCacheFresh = (token) => { + if (!token || tokenValidationCache.token !== token) { + return false; + } + + return ( + Date.now() - tokenValidationCache.validatedAt <= + TOKEN_VALIDATION_CACHE_TTL_MS + ); +}; + +const isExplicitInvalidTokenError = (error) => { + const businessCode = normalizeCode( + error?.Code || error?.code || error?.response?.data?.Code, + ); + const httpStatus = normalizeCode(error?.response?.status); + return ( + isTokenInvalidBusinessCode(businessCode) || + httpStatus === HTTP_STATUS.UNAUTHORIZED + ); +}; + +const isRetryableTokenValidationError = (error) => { + if (isExplicitInvalidTokenError(error)) { + return false; + } + + const httpStatus = normalizeCode(error?.response?.status); + if (!httpStatus) { + return true; + } + + return httpStatus >= 500; +}; + +const getTokenValidationRetryDelayMs = (attempt) => + TOKEN_VALIDATION_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1); + +const sleep = (delayMs) => + new Promise((resolve) => { + setTimeout(resolve, delayMs); + }); + +const validateTokenWithServer = async (token) => { + if (!token) { + return false; + } + + if (isTokenValidationCacheFresh(token)) { + return tokenValidationCache.valid; + } + + if ( + tokenValidationCache.pendingPromise && + tokenValidationCache.pendingToken === token + ) { + return tokenValidationCache.pendingPromise; + } + + const pendingPromise = (async () => { + try { + for ( + let attempt = 1; + attempt <= TOKEN_VALIDATION_MAX_ATTEMPTS; + attempt += 1 + ) { + try { + const response = await api.post( + API_ENDPOINTS.routes.MENU_BUILD_MENU_ALL, + { [BaseFields.USER_TOKEN]: token }, + { + skipBusinessErrorHandling: true, + skipHttpErrorHandling: true, + }, + ); + + const payload = response?.data || {}; + const code = normalizeCode(payload?.Code); + const valid = + payload?.Success === true || + code === API_SUCCESS_CODE || + isPermissionDeniedBusinessCode(code); + + return saveTokenValidationResult(token, valid); + } catch (error) { + const retryable = isRetryableTokenValidationError(error); + const hasRemainingAttempts = attempt < TOKEN_VALIDATION_MAX_ATTEMPTS; + + if (retryable && hasRemainingAttempts) { + await sleep(getTokenValidationRetryDelayMs(attempt)); + continue; + } + + // Permission-related failures still indicate a valid login session. + const valid = retryable ? false : !isExplicitInvalidTokenError(error); + return saveTokenValidationResult(token, valid); + } + } + + return saveTokenValidationResult(token, false); + } finally { + tokenValidationCache.pendingToken = ""; + tokenValidationCache.pendingPromise = null; + } + })(); + + tokenValidationCache.pendingToken = token; + tokenValidationCache.pendingPromise = pendingPromise; + + return pendingPromise; +}; export function redirectToLogin( message = i18n.global.t("message.loginExpired"), clearStorage = false, ) { - // 只有在明确指定需要清除存储时才清除 if (clearStorage) { - localStorage.removeItem("token"); + clearStoredToken(); localStorage.removeItem("username"); + localStorage.removeItem("account"); + localStorage.removeItem("allowedPerms"); + localStorage.removeItem("allowedPaths"); + localStorage.removeItem("allowedMenuKeys"); + localStorage.removeItem("allowedPathsToken"); + localStorage.removeItem("allowedPathsExpire"); + localStorage.removeItem("allowedPathsIssuedAt"); + clearTokenValidationCache(); + clearRowVersionCache(); } if (router.currentRoute.value.path !== "/signin") { @@ -20,21 +179,22 @@ export function redirectToLogin( } } -// 处理登出 export async function handleLogout() { try { - const token = localStorage.getItem("token"); + const token = getStoredToken(); if (token) { - // 调用登出接口 await signOut(); - // 清理本地存储 - localStorage.removeItem("token"); + clearStoredToken(); localStorage.removeItem("username"); localStorage.removeItem("account"); localStorage.removeItem("allowedPerms"); localStorage.removeItem("allowedPaths"); localStorage.removeItem("allowedMenuKeys"); - // 重定向到登录页 + localStorage.removeItem("allowedPathsToken"); + localStorage.removeItem("allowedPathsExpire"); + localStorage.removeItem("allowedPathsIssuedAt"); + clearTokenValidationCache(); + clearRowVersionCache(); router.push("/signin"); } } catch (error) { @@ -42,29 +202,19 @@ export async function handleLogout() { } } -export function checkTokenValidity(redirect = false) { - const token = localStorage.getItem("token"); +export async function checkTokenValidity(redirect = false) { + void redirect; + const token = getStoredToken(); if (!token) { + clearTokenValidationCache(); return false; } - try { - const decodedToken = jwtDecode(token); - const currentTime = Math.floor(Date.now() / 1000); - - // 检查 token 是否快要过期(比如还有5分钟过期) - const expiresIn = decodedToken.exp - currentTime; - const isNearExpiration = expiresIn < 300; // 5分钟 = 300秒 - - if (decodedToken.exp < currentTime || isNearExpiration) { - // token 已过期或即将过期 - return false; - } - - return true; - } catch (error) { - // token 解析失败 + if (isTokenExpired(token, 300)) { + clearTokenValidationCache(); return false; } + + return validateTokenWithServer(token); } diff --git a/src/utils/csrf.js b/src/utils/csrf.js index 3d47b405e719694d0a14500bd7b2f92f48a363e7..a4a8cf93637893885330d8fbc0bb300689ca1cf2 100644 --- a/src/utils/csrf.js +++ b/src/utils/csrf.js @@ -76,7 +76,8 @@ class CsrfTokenManager { } } - async refreshToken() { + async refreshToken(options = {}) { + const { silent = false } = options || {}; if (this.isRefreshing) { return new Promise((resolve) => { this.subscribers.push((token) => { @@ -118,10 +119,12 @@ class CsrfTokenManager { return this.token; } catch (error) { - showErrorNotification( - i18n.global.t("message.verifyCsrfTokenFailed"), - error, - ); + if (!silent) { + showErrorNotification( + i18n.global.t("message.verifyCsrfTokenFailed"), + error, + ); + } throw error; } finally { this.isRefreshing = false; diff --git a/src/utils/initialize.js b/src/utils/initialize.js index 8fa4dbf745ccdab06043036e26a2f229cf408075..82cb93c125086a6c34cb9ee6700e030684800e59 100644 --- a/src/utils/initialize.js +++ b/src/utils/initialize.js @@ -1,6 +1,7 @@ import { checkTokenValidity, redirectToLogin } from "./auth"; import { csrfTokenManager } from "./csrf"; import router from "@/router"; +import { clearStoredToken, getStoredToken } from "@/utils/tokenStorage"; export const initializeApplication = async () => { try { @@ -20,7 +21,7 @@ export const initializeApplication = async () => { } // 4. 获取并验证 JWT token - const token = localStorage.getItem("token"); + const token = getStoredToken(); if (!token) { redirectToLogin("请先登录", false); // 不清除存储,因为本来就没有token return; @@ -28,9 +29,9 @@ export const initializeApplication = async () => { try { // 5. 验证 token 有效性 - if (!checkTokenValidity(false)) { + if (!(await checkTokenValidity(false))) { // 不自动重定向,我们要自己控制 - localStorage.removeItem("token"); + clearStoredToken(); localStorage.removeItem("username"); redirectToLogin("登录已过期,请重新登录", true); // 清除无效的token return; diff --git a/src/utils/jwt.js b/src/utils/jwt.js new file mode 100644 index 0000000000000000000000000000000000000000..7d184442a067cff7b16ea1c722cf490f7f93ac97 --- /dev/null +++ b/src/utils/jwt.js @@ -0,0 +1,92 @@ +import { jwtDecode } from "jwt-decode"; + +const IDENTITY_FIELDS_BY_LOGIN_TYPE = { + admin: ["sub", "nameid", "UserNumber", "userNumber", "Account", "account"], + employee: [ + "sub", + "nameid", + "EmployeeId", + "employeeId", + "UserNumber", + "userNumber", + "Account", + "account", + ], + customer: ["sub", "nameid", "CustomerId", "customerId", "Account", "account"], +}; + +const COMMON_IDENTITY_FIELDS = [ + "sub", + "nameid", + "UserNumber", + "userNumber", + "EmployeeId", + "employeeId", + "Account", + "account", +]; + +const normalizeIdentity = (value) => { + if (value === null || value === undefined) { + return ""; + } + + const text = String(value).trim(); + return text.length > 0 ? text : ""; +}; + +const pickIdentity = (payload, keys) => { + for (const key of keys) { + const identity = normalizeIdentity(payload?.[key]); + if (identity) { + return identity; + } + } + return ""; +}; + +export const decodeJwtPayload = (token) => { + const normalizedToken = typeof token === "string" ? token.trim() : ""; + if (!normalizedToken) { + return null; + } + + try { + // SECURITY NOTICE: + // jwt-decode only parses payload and does NOT verify signature. + // Never trust decoded content for authentication/authorization decisions. + return jwtDecode(normalizedToken); + } catch (error) { + return null; + } +}; + +export const getTokenExpiration = (token) => { + const payload = decodeJwtPayload(token); + const exp = Number(payload?.exp); + return Number.isFinite(exp) ? exp : null; +}; + +export const isTokenExpired = (token, minRemainingSeconds = 0) => { + const expiration = getTokenExpiration(token); + if (!Number.isFinite(expiration)) { + return true; + } + + const now = Math.floor(Date.now() / 1000); + return expiration - now <= Number(minRemainingSeconds || 0); +}; + +export const getUserIdentity = (loginType, token) => { + const payload = decodeJwtPayload(token); + if (!payload) { + return null; + } + + const preferredFields = IDENTITY_FIELDS_BY_LOGIN_TYPE[loginType] || []; + const identity = + pickIdentity(payload, preferredFields) || + pickIdentity(payload, COMMON_IDENTITY_FIELDS); + + return identity || null; +}; diff --git a/src/utils/permission.js b/src/utils/permission.js index fc1bff84a5ebe0d69d6847c54979dcdc6f352904..9ad6e5702f9707e2089a08b16973fad1da07ed7f 100644 --- a/src/utils/permission.js +++ b/src/utils/permission.js @@ -1,21 +1,43 @@ -export function getAllowedMenuKeys() { +const PERMISSION_CACHE_KEYS = Object.freeze({ + perms: "allowedPerms", + menuKeys: "allowedMenuKeys", +}); + +const readArrayCache = (key) => { + const raw = localStorage.getItem(key); + if (raw === null) { + return { exists: false, valid: false, value: [] }; + } + try { - const raw = localStorage.getItem("allowedMenuKeys"); - const parsed = raw ? JSON.parse(raw) : []; - return Array.isArray(parsed) ? parsed : []; + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return { exists: true, valid: true, value: parsed }; + } + return { exists: true, valid: false, value: [] }; } catch { - return []; + return { exists: true, valid: false, value: [] }; } +}; + +export function getAllowedMenuKeys() { + return readArrayCache(PERMISSION_CACHE_KEYS.menuKeys).value; } export function getAllowedPerms() { - try { - const raw = localStorage.getItem("allowedPerms"); - const parsed = raw ? JSON.parse(raw) : []; - return Array.isArray(parsed) ? parsed : []; - } catch { - return []; + return readArrayCache(PERMISSION_CACHE_KEYS.perms).value; +} + +export function isPermissionCacheInitialized() { + const perms = readArrayCache(PERMISSION_CACHE_KEYS.perms); + const menuKeys = readArrayCache(PERMISSION_CACHE_KEYS.menuKeys); + + if (!perms.exists && !menuKeys.exists) { + return false; } + + // Valid empty arrays still mean "initialized" (user may legitimately have no permissions). + return perms.valid || menuKeys.valid; } /** @@ -33,7 +55,7 @@ export function hasPermission(required, needAll = false) { const allowedMenuKeys = getAllowedMenuKeys(); // 两者都还未初始化(首次进入系统、尚未拉取菜单)时默认放行,避免首屏闪烁/误隐藏 - if (allowedPerms.length === 0 && allowedMenuKeys.length === 0) { + if (!isPermissionCacheInitialized()) { return true; } diff --git a/src/utils/tokenStorage.js b/src/utils/tokenStorage.js new file mode 100644 index 0000000000000000000000000000000000000000..e44b6f2f4093a7a2ed9e20b64a0d24e2efebf745 --- /dev/null +++ b/src/utils/tokenStorage.js @@ -0,0 +1,133 @@ +const TOKEN_STORAGE_KEY = "token"; +const TOKEN_STORAGE_PREFIX = "obf:v1:"; + +const getLocalStorage = () => { + if (typeof window === "undefined") { + return null; + } + + try { + return window.localStorage; + } catch { + return null; + } +}; + +const normalizeToken = (value) => + typeof value === "string" ? value.trim() : ""; + +const reverseText = (value) => value.split("").reverse().join(""); + +const encodeBase64 = (value) => btoa(value); +const decodeBase64 = (value) => atob(value); + +export const obscureToken = (token) => { + const normalized = normalizeToken(token); + if (!normalized) { + return ""; + } + + return `${TOKEN_STORAGE_PREFIX}${encodeBase64(reverseText(normalized))}`; +}; + +export const unobscureToken = (storedValue) => { + const normalized = normalizeToken(storedValue); + if (!normalized) { + return ""; + } + + if (!normalized.startsWith(TOKEN_STORAGE_PREFIX)) { + return normalized; + } + + const payload = normalized.slice(TOKEN_STORAGE_PREFIX.length); + if (!payload) { + return ""; + } + + try { + return reverseText(decodeBase64(payload)); + } catch { + return ""; + } +}; + +const migrateLegacyTokenIfNeeded = (storage, rawToken, token) => { + if (!storage || !rawToken || !token) { + return; + } + + if (!rawToken.startsWith(TOKEN_STORAGE_PREFIX)) { + storage.setItem(TOKEN_STORAGE_KEY, obscureToken(token)); + } +}; + +export const getStoredToken = () => { + const storage = getLocalStorage(); + if (!storage) { + return ""; + } + + const rawToken = normalizeToken(storage.getItem(TOKEN_STORAGE_KEY)); + if (!rawToken) { + return ""; + } + + const token = unobscureToken(rawToken); + if (!token) { + storage.removeItem(TOKEN_STORAGE_KEY); + return ""; + } + + migrateLegacyTokenIfNeeded(storage, rawToken, token); + return token; +}; + +export const getStoredTokenCacheKey = () => { + const storage = getLocalStorage(); + if (!storage) { + return ""; + } + + const rawToken = normalizeToken(storage.getItem(TOKEN_STORAGE_KEY)); + if (!rawToken) { + return ""; + } + + const token = unobscureToken(rawToken); + if (!token) { + storage.removeItem(TOKEN_STORAGE_KEY); + return ""; + } + + const cacheKey = rawToken.startsWith(TOKEN_STORAGE_PREFIX) + ? rawToken + : obscureToken(token); + migrateLegacyTokenIfNeeded(storage, rawToken, token); + return cacheKey; +}; + +export const setStoredToken = (token) => { + const storage = getLocalStorage(); + if (!storage) { + return; + } + + const normalized = normalizeToken(token); + if (!normalized) { + storage.removeItem(TOKEN_STORAGE_KEY); + return; + } + + storage.setItem(TOKEN_STORAGE_KEY, obscureToken(normalized)); +}; + +export const clearStoredToken = () => { + const storage = getLocalStorage(); + if (!storage) { + return; + } + + storage.removeItem(TOKEN_STORAGE_KEY); +}; + diff --git a/src/views/SignInView.vue b/src/views/SignInView.vue index 60e780f49867fbc628a779fb82e1af96ac2db3e4..a698c24c0964313bbcf2ab1f1983acdcea7ffd85 100644 --- a/src/views/SignInView.vue +++ b/src/views/SignInView.vue @@ -126,7 +126,9 @@ import { isTwoFactorChallenge, signIn, } from "../api/basicapi"; +import { clearRowVersionCache } from "@/api/baseapi"; import { useI18n } from "vue-i18n"; +import { setStoredToken } from "@/utils/tokenStorage"; const { t, locale } = useI18n(); const currentLocale = ref(locale.value); @@ -199,8 +201,19 @@ const clearTwoFactorChallenge = () => { pendingLoginContext.value = null; }; +const clearPermissionCache = () => { + localStorage.removeItem("allowedPaths"); + localStorage.removeItem("allowedMenuKeys"); + localStorage.removeItem("allowedPerms"); + localStorage.removeItem("allowedPathsToken"); + localStorage.removeItem("allowedPathsExpire"); + localStorage.removeItem("allowedPathsIssuedAt"); +}; + const persistLoginData = async (userData, loginType) => { - localStorage.setItem("token", userData.UserToken); + clearPermissionCache(); + clearRowVersionCache(); + setStoredToken(userData.UserToken); localStorage.setItem( "username", userData.Name || userData.UserName || userData.Username || form.Account, diff --git a/src/views/base/DepartmentView.vue b/src/views/base/DepartmentView.vue index 6748cdb5bb88a882f1e9ece78b9eac3648466c74..ec097915e7a06327bcfcd61f948a1fa6317539c4 100644 --- a/src/views/base/DepartmentView.vue +++ b/src/views/base/DepartmentView.vue @@ -487,6 +487,7 @@ const fetchDepartmentData = async () => { if (result && Array.isArray(result.Items)) { departments.value = result.Items.map((item) => ({ [DepartmentFields.ID]: item[DepartmentFields.ID], + [DepartmentFields.ROWVERSION]: item[DepartmentFields.ROWVERSION], [DepartmentFields.NUMBER]: item[DepartmentFields.NUMBER], [DepartmentFields.NAME]: item[DepartmentFields.NAME], [DepartmentFields.DESCRIPTION]: item[DepartmentFields.DESCRIPTION], diff --git a/src/views/base/NationView.vue b/src/views/base/NationView.vue index d20089a78537df1b76049deefb00933163b4db55..db7009eb44e623c35644876d2ecfded92e47a5a6 100644 --- a/src/views/base/NationView.vue +++ b/src/views/base/NationView.vue @@ -372,6 +372,7 @@ const fetchNationData = async () => { if (result?.Items) { nations.value = result.Items.map((item) => ({ [NationFields.ID]: item[NationFields.ID], + [NationFields.ROWVERSION]: item[NationFields.ROWVERSION], [NationFields.NUMBER]: item[NationFields.NUMBER], [NationFields.NAME]: item[NationFields.NAME], [NationFields.IS_DELETED]: item[NationFields.IS_DELETED], diff --git a/src/views/base/NoticeTypeView.vue b/src/views/base/NoticeTypeView.vue index c32b149ef8d87a5d97c4e067249d84a6ddf7fd85..28bef0aeccfc88e979227672d34f070abebfb3e0 100644 --- a/src/views/base/NoticeTypeView.vue +++ b/src/views/base/NoticeTypeView.vue @@ -1,4 +1,4 @@ -