import { DEMO_ACCOUNT, DEMO_PASSWORD, getAuthCredentials, logout, saveCurrentUser, saveLoginSession, savePermissionData, saveRememberedCredentials, } from "./storage"; import { clearAttendanceCache } from "@/api/attendance"; const SERVER_KEY = "apiInfo"; const DEFAULT_SERVER = { mode: "demo", protocol: "http://", hostname: "", port: "", }; export function getServerConfig() { const saved = uni.getStorageSync(SERVER_KEY) || {}; return { ...DEFAULT_SERVER, ...saved, protocol: saved.protocol || saved.protocal || DEFAULT_SERVER.protocol, }; } export function saveServerConfig(config) { const normalized = { mode: config.mode === "server" ? "server" : "demo", protocol: config.protocol === "https://" ? "https://" : "http://", hostname: String(config.hostname || "") .trim() .replace(/^https?:\/\//, "") .replace(/\/$/, ""), port: String(config.port || "").trim(), }; uni.setStorageSync(SERVER_KEY, normalized); return normalized; } // H5 dev(pnpm dev:h5)走 Vite 代理避开浏览器 CORS;其他场景仍用配置里的真实地址 const useDevProxy = typeof window !== "undefined" && typeof process !== "undefined" && process.env?.NODE_ENV !== "production"; export function getApiBaseUrl(config = getServerConfig()) { if (config.mode !== "server" || !config.hostname) return ""; if (useDevProxy) return "/api"; const port = config.port ? `:${config.port}` : ""; return `${config.protocol}${config.hostname}${port}/api`; } export function request({ url, method = "GET", data, token, sessionId, timeout = 15000, }) { return new Promise((resolve, reject) => { // 未显式传 token 时自动从登录态读取,登录后所有请求自动带上凭证 let effectiveToken = token; let effectiveSessionId = sessionId; if (!effectiveToken) { const stored = getAuthCredentials(); effectiveToken = stored.token; if (!effectiveSessionId) effectiveSessionId = stored.sessionId; } const header = effectiveToken ? { "content-type": "application/json", "zoomwin-token": effectiveToken, Authorization: effectiveToken, "zoomwin-sid": effectiveSessionId || "", } : { "content-type": "application/json", platform: "wxapp" }; uni.request({ url, method, data, header, timeout, success: (response) => { if (response.statusCode === 401) { // 服务端认为凭证过期:清本地会话 + 清考勤缓存 + 跳登录页 logout(); clearAttendanceCache(); uni.showToast({ title: "登录已过期,请重新登录", icon: "none", duration: 1500, }); setTimeout(() => uni.reLaunch({ url: "/pages/login/index" }), 1200); reject(new Error("身份验证已过期,请重新登录")); return; } const body = response.data || {}; if (response.statusCode < 200 || response.statusCode >= 300) { reject( new Error( body.message || `服务器响应异常(${response.statusCode})`, ), ); return; } if (body.code !== undefined && Number(body.code) !== 0) { reject(new Error(body.message || "请求失败")); return; } resolve(body); }, fail: (error) => reject(new Error(error.errMsg || "无法连接服务器")), }); }); } /** * multipart/form-data 上传封装,对应 /main/file/uploadFile 等需要走 uni.uploadFile 的接口。 * 与 request() 的差异: * - 不写 content-type(uni-app 会自动设 multipart/form-data + boundary) * - 走 uni.uploadFile,文件路径用 filePath,表单字段名用 name(默认 multiPartFile) * - H5 端响应体可能是 JSON 字符串,需要 typeof === 'string' 时 JSON.parse * 401、code !== 0 等失败语义与 request() 完全一致,调用方可统一处理。 */ export function uploadFile({ url, filePath, name = "multiPartFile", formData, timeout = 30000, }) { return new Promise((resolve, reject) => { if (!filePath) { reject(new Error("缺少文件路径")); return; } const { token, sessionId } = getAuthCredentials(); const header = token ? { "zoomwin-token": token, Authorization: token, "zoomwin-sid": sessionId || "", } : { platform: "wxapp" }; uni.uploadFile({ url, filePath, name, formData, header, timeout, success: (response) => { if (response.statusCode === 401) { logout(); clearAttendanceCache(); uni.showToast({ title: "登录已过期,请重新登录", icon: "none", duration: 1500, }); setTimeout(() => uni.reLaunch({ url: "/pages/login/index" }), 1200); reject(new Error("身份验证已过期,请重新登录")); return; } let body = response.data; if (typeof body === "string") { try { body = JSON.parse(body); } catch { // 不是 JSON:保持原字符串,下方按非 2xx/message 处理 } } const payload = body && typeof body === "object" ? body : {}; if ( response.statusCode < 200 || response.statusCode >= 300 ) { reject( new Error( payload.message || `服务器响应异常(${response.statusCode})`, ), ); return; } if (payload.code !== undefined && Number(payload.code) !== 0) { reject(new Error(payload.message || "请求失败")); return; } resolve(payload); }, fail: (error) => reject(new Error(error.errMsg || "文件上传失败")), }); }); } export async function testServerConnection(config) { const normalized = { mode: "server", protocol: config.protocol === "https://" ? "https://" : "http://", hostname: String(config.hostname || "") .trim() .replace(/^https?:\/\//, "") .replace(/\/$/, ""), port: String(config.port || "").trim(), }; const baseUrl = getApiBaseUrl(normalized); if (!baseUrl) throw new Error("请填写服务器地址"); await request({ url: `${baseUrl}/main/connection/getConnectionTest`, timeout: 5000, }); return true; } function collectAuthorities(tree) { const authorities = []; const walk = (list) => { for (const item of Array.isArray(list) ? list : []) { if (item && item.menuType === 2) authorities.push(item); if (item?.children?.length) walk(item.children); } }; walk(tree); return authorities; } async function loadPermissionTree(baseUrl) { const response = await request({ url: `${baseUrl}/system/resources/getResourcesTreePDA`, }); const tree = Array.isArray(response.data) ? response.data : []; savePermissionData(tree, collectAuthorities(tree)); return tree; } // 拉取当前登录人的扩展资料(部门、岗位、角色等),登录后调用一次写入本地 export async function fetchCurrentUser(baseUrl) { if (!baseUrl) throw new Error("缺少 baseUrl"); const response = await request({ url: `${baseUrl}/system/account/getLoginUser`, }); return response.data || null; } // 拉取员工完整档案(含 deptName / postId / secondLinkName / secondLinkPhone), // 与 getLoginUser 合并补全部门岗位与紧急联系人 // profile 响应把用户数据放在 user 子对象里,内部摊平到顶层 // 岗位名称不在 profile 直接返回,而是 postId(逗号分隔的 id 串), // 内部再调 /hr/position/page 用 id 匹配出 positionName 后回填到返回对象的 position 字段 export async function fetchUserDetail(baseUrl, userId) { if (!baseUrl) throw new Error("缺少 baseUrl"); if (!userId && userId !== 0) throw new Error("缺少用户ID"); const response = await request({ url: `${baseUrl}/main/users/${userId}/profile`, }); const data = response.data || null; if (!data) return null; // 嵌套 user 包装时,把 user 的字段提到顶层(deptName / secondLinkName 等), // 同时保留顶层其他字段(postId 等);mapCurrentUser 不必关心嵌套结构 const profile = data.user && typeof data.user === "object" ? { ...data, ...data.user } : data; try { const names = await resolvePostIds(baseUrl, profile.postId); if (names) profile.position = names; } catch (error) { // 岗位解析失败不影响其他字段 } return profile; } // 把 profile.postId("1111,2222")解析成岗位名称字符串,用 / 拼接多个岗位 async function resolvePostIds(baseUrl, postIdString) { if (!postIdString) return ""; const ids = String(postIdString) .split(",") .map((s) => s.trim()) .filter(Boolean); if (!ids.length) return ""; const response = await request({ url: `${baseUrl}/hr/position/page`, method: "POST", data: { pageNum: 1, size: 1000, status: 1 }, }); const pageData = response.data || {}; const records = Array.isArray(pageData.list) ? pageData.list : Array.isArray(pageData) ? pageData : []; const names = ids .map((id) => { const match = records.find((r) => String(r.id) === id); return match ? String(match.positionName || match.name || "") : ""; }) .filter(Boolean); return names.join("、"); } export async function performLogin({ account, password, rememberPassword }) { const config = getServerConfig(); const loginName = String(account || "").trim(); const loginPwd = String(password || ""); if (config.mode !== "server") { await new Promise((resolve) => setTimeout(resolve, 450)); if ( loginName.toUpperCase() !== DEMO_ACCOUNT || loginPwd !== DEMO_PASSWORD ) { throw new Error("工号或密码错误"); } const data = { token: `demo-token-${Date.now()}`, sessionId: "demo-session", userId: DEMO_ACCOUNT, loginName: DEMO_ACCOUNT, userName: "陈晓雨", }; saveLoginSession(data, loginName); savePermissionData([], []); saveRememberedCredentials({ account: loginName, password: loginPwd, rememberPassword, }); return data; } const baseUrl = getApiBaseUrl(config); if (!baseUrl) throw new Error("请先配置服务器地址"); const response = await request({ url: `${baseUrl}/main/user/login`, method: "POST", data: { loginName, loginPwd }, }); const data = response.data || {}; if (!data.token) throw new Error("登录响应中缺少 Token"); saveLoginSession(data, loginName); saveRememberedCredentials({ account: loginName, password: loginPwd, rememberPassword, }); try { await loadPermissionTree(baseUrl); } catch (error) { savePermissionData([], []); uni.showToast({ title: error.message || "权限加载失败", icon: "none" }); } try { // 部门/岗位在 getLoginUser 里没有,单独调 getById 拿,再合并 const [basic, detail] = await Promise.allSettled([ fetchCurrentUser(baseUrl), fetchUserDetail(baseUrl, data.userId), ]); const basicData = basic.status === "fulfilled" ? basic.value : null; const detailData = detail.status === "fulfilled" ? detail.value : null; // getById 字段覆盖基础信息(deptName / postName / joinDate 等) const merged = detailData ? { ...(basicData || {}), ...detailData } : basicData; if (merged) saveCurrentUser(merged); if (detail.status === "rejected") { uni.showToast({ title: detail.reason?.message || "员工档案加载失败", icon: "none", }); } } catch (error) { uni.showToast({ title: error.message || "用户信息加载失败", icon: "none" }); } return data; } export async function getCompanyBranding(config = getServerConfig()) { if (config.mode !== "server") return null; const baseUrl = getApiBaseUrl(config); if (!baseUrl) return null; const response = await request({ url: `${baseUrl}/pda/mes/us/indexName`, timeout: 5000, }); return response.data || null; } // 服务器模式下退出登录:通知后端作废 token,再由页面清理本地 storage 并跳登录页 // 演示模式或未配置服务器时直接返回(不抛错),由调用方继续清理本地 export async function serverLogout() { if (getServerConfig().mode !== "server") return; const baseUrl = getApiBaseUrl(); if (!baseUrl) return; try { await request({ url: `${baseUrl}/main/user/logout`, method: "POST", timeout: 5000, }); } catch (error) { // 服务器登出失败(网络/401/token 过期等)不阻塞本地清理 console.warn("[auth] server logout failed:", error?.message || error); } }