本地长期开发成果的整体入库快照,包含后端鉴权/权限、AI 接入、合同解析、 SKU 目录等模块,以及前端相应页面与样式改版。 后端 - 新增鉴权与会话:scrypt 密码哈希、HttpOnly cookie(happy_sid)、登录失败锁定 - 新增「角色 → 权限组 → 权限点」三级权限模型与审计日志 - 新增 AI 提供商接入与预设(兼容 OpenAI 协议,内置 DeepSeek / 通义千问等) - 新增合同解析、合同文件保留策略、客户归属与访问控制 - 新增 SKU 目录、组织品牌、新客开发、系统日志等路由 前端 - 新增登录页与用户 / 权限组 / 系统日志管理页 - 新增客户详情、客户公海、询盘详情、报价编辑器、合同清洗中心 - 新增 AI 配置、组织品牌、包装参数、SKU 选择器等组件 - 新增英文大写金额、未保存改动拦截、抽屉与骨架屏 - 新增 stage1 样式层;产品、供应商、订单等页面改版 - EmbeddedSku 页面并入现有产品体系后删除 开源准备 - 新增 AGPL-3.0 LICENSE - README:移除未启用的 SQLCipher 加密声明(改为如实的「已知限制」)、 补充环境变量说明、许可证由「私有项目」改为 AGPL-3.0 - 移除历史遗留的空文件 _index_backup.txt - 文档中的项目路径更新为 happy-export
104 lines
3.1 KiB
React
104 lines
3.1 KiB
React
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||
import axios from 'axios'
|
||
|
||
/**
|
||
* 会话上下文:全局唯一的「当前用户是谁」答案来源。
|
||
*
|
||
* 为什么不把用户信息塞进 localStorage:
|
||
* 改了个人资料 / 换了角色之后,前端必须立刻拿到新值。所以只依赖后端的会话 cookie,
|
||
* 启动时打一次 /api/auth/me,之后由各处的 setUser / refresh 回写。
|
||
*/
|
||
const AuthContext = createContext(null)
|
||
|
||
export function AuthProvider({ children }) {
|
||
const [user, setUser] = useState(null)
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
/* 启动时确定登录态 */
|
||
useEffect(() => {
|
||
let alive = true
|
||
;(async () => {
|
||
try {
|
||
const { data } = await axios.get('/api/auth/me')
|
||
if (alive) setUser(data?.user || null)
|
||
} catch {
|
||
if (alive) setUser(null)
|
||
} finally {
|
||
if (alive) setLoading(false)
|
||
}
|
||
})()
|
||
return () => { alive = false }
|
||
}, [])
|
||
|
||
/* 任何接口返回 401(会话过期 / 被踢),就地清空登录态,界面自然回登录页 */
|
||
useEffect(() => {
|
||
const id = axios.interceptors.response.use(
|
||
response => response,
|
||
error => {
|
||
const status = error?.response?.status
|
||
const url = String(error?.config?.url || '')
|
||
const isAuthProbe = url.includes('/api/auth/login') || url.includes('/api/auth/me')
|
||
if (status === 401 && !isAuthProbe) setUser(null)
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
return () => axios.interceptors.response.eject(id)
|
||
}, [])
|
||
|
||
const refresh = useCallback(async () => {
|
||
try {
|
||
const { data } = await axios.get('/api/auth/me')
|
||
setUser(data?.user || null)
|
||
return data?.user || null
|
||
} catch {
|
||
setUser(null)
|
||
return null
|
||
}
|
||
}, [])
|
||
|
||
const login = useCallback(async (username, password) => {
|
||
const { data } = await axios.post('/api/auth/login', { username, password })
|
||
setUser(data.user)
|
||
return data.user
|
||
}, [])
|
||
|
||
const logout = useCallback(async () => {
|
||
try {
|
||
await axios.post('/api/auth/logout')
|
||
} catch { /* 会话可能已过期,忽略 */ }
|
||
setUser(null)
|
||
}, [])
|
||
|
||
const value = useMemo(
|
||
() => ({ user, loading, login, logout, refresh, setUser }),
|
||
[user, loading, login, logout, refresh]
|
||
)
|
||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||
}
|
||
|
||
export function useAuth() {
|
||
const context = useContext(AuthContext)
|
||
if (!context) throw new Error('useAuth 必须在 AuthProvider 内使用')
|
||
return context
|
||
}
|
||
|
||
/** 权限判断:系统管理员兜底全通过,其余按角色所属权限组的权限点判断 */
|
||
export function hasPermission(user, key) {
|
||
if (!user) return false
|
||
if (user.role === 'admin') return true
|
||
return Array.isArray(user.permissions) && user.permissions.includes(key)
|
||
}
|
||
|
||
/** 角色英文值 → 中文标签 */
|
||
export const ROLE_LABELS = {
|
||
admin: '系统管理员',
|
||
chairman: '董事长',
|
||
general_manager: '总经理',
|
||
sales_manager: '销售经理',
|
||
sales_rep: '销售业务员'
|
||
}
|
||
|
||
export function roleLabel(role) {
|
||
return ROLE_LABELS[role] || role || '—'
|
||
}
|