-
-
- 检测中...
- 404 Not Found
- 200 OK
- Error
-
-
+
+
+
+
+
+
+
+
+
+
+ 查询
+ 重置
+
+
-
-
预期字段 (参考老 /vue 后台)
-
-
-
-
+
+
+
+
+ 已选 {{ selection.length }} 项
+ 批量审核
+ 批量驳回
+ 批量上下架
+ 批量改分类
+ 批量删除
+
+
+
+
+
+
+
+
+
+
+
{{ (row.title || '?').charAt(0) }}
+
{{ row.title }}
+
+
+
+
+
+
+
+ ¥ {{ row.price.toLocaleString() }} / {{ row.unit }}
+
+
+
+ {{ ONSHELF_STATUS_MAP[row.onShelf]?.label }}
+
+
+
+
+
+ {{ CHECK_STATUS_MAP[row.checkStatus]?.label }}
+
+
+
+
+
+ 详情
+ 审核
+ {{ row.onShelf === 1 ? '下架' : '上架' }}
+ 改分类
+ 删除
+
+
+
+
+
+
+ {{ detailData.serviceGoodId }}
+ {{ detailData.title }}
+ {{ detailData.enterpriseName }}
+ {{ detailData.categoryName }}
+ {{ detailData.serviceArea }}
+ ¥ {{ detailData.price.toLocaleString() }} / {{ detailData.unit }}
+
+ {{ CHECK_STATUS_MAP[detailData.checkStatus]?.label }}
+
+
+ {{ ONSHELF_STATUS_MAP[detailData.onShelf]?.label }}
+
+ {{ detailData.checkOpinion || '--' }}
+
+
+
+
+
+ {{ checkData.title }}
+
+
+ 通过
+ 驳回
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+ {{ categoryData.title }}
+ {{ categoryData.categoryName }}
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+ 共 {{ selection.length }} 个
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+ 共 {{ selection.length }} 个
+
+
+
+ 取消
+ 确定
+
+
diff --git a/src/modules/sys/DeptListView.vue b/src/modules/sys/DeptListView.vue
new file mode 100644
index 0000000..eb3c0dd
--- /dev/null
+++ b/src/modules/sys/DeptListView.vue
@@ -0,0 +1,232 @@
+
+
+
+
+
+
+
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/DictDataListView.vue b/src/modules/sys/DictDataListView.vue
new file mode 100644
index 0000000..c491e82
--- /dev/null
+++ b/src/modules/sys/DictDataListView.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ row.dictValue }}
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/DictTypeListView.vue b/src/modules/sys/DictTypeListView.vue
new file mode 100644
index 0000000..67376a5
--- /dev/null
+++ b/src/modules/sys/DictTypeListView.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ row.dictType }}
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/FileListView.vue b/src/modules/sys/FileListView.vue
new file mode 100644
index 0000000..1ad3e45
--- /dev/null
+++ b/src/modules/sys/FileListView.vue
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+ {{ row.id }}
+
+ {{ row.url }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 复制链接
+ 删除
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/LogErrorView.vue b/src/modules/sys/LogErrorView.vue
new file mode 100644
index 0000000..b67d90d
--- /dev/null
+++ b/src/modules/sys/LogErrorView.vue
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ row.module }}
+
+ {{ row.requestMethod }}
+
+ {{ row.requestUri }}
+
+ {{ previewError(row.errorInfo) }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 详情
+
+
+
+
+
+
+
+
模块
+
{{ currentRow.module }}
+
+
+
请求
+
+ {{ currentRow.requestMethod }}
+ {{ currentRow.requestUri }}
+
+
+
+
IP
+
{{ currentRow.ip || '--' }}
+
+
+
User Agent
+
{{ currentRow.userAgent || '--' }}
+
+
+
时间
+
{{ fmtDate(currentRow.createDate) }}
+
+
+
错误堆栈
+
+
{{ currentRow.errorInfo || '--' }}
+
+
+
+
+ 关闭
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/LogLoginView.vue b/src/modules/sys/LogLoginView.vue
new file mode 100644
index 0000000..40662d7
--- /dev/null
+++ b/src/modules/sys/LogLoginView.vue
@@ -0,0 +1,180 @@
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ OPERATION_LABEL[row.operation ?? -1] || '--' }}
+
+
+ {{ STATUS_LABEL[row.status].label }}
+ --
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/LogOperationView.vue b/src/modules/sys/LogOperationView.vue
new file mode 100644
index 0000000..a7fb89b
--- /dev/null
+++ b/src/modules/sys/LogOperationView.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ row.module }}
+
+
+
+ {{ row.requestMethod }}
+
+
+
+ {{ row.requestUri }}
+
+
+ {{ STATUS_LABEL[row.status].label }}
+ --
+
+
+ {{ row.requestTime ?? '--' }}
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/MenuListView.vue b/src/modules/sys/MenuListView.vue
new file mode 100644
index 0000000..bd77c21
--- /dev/null
+++ b/src/modules/sys/MenuListView.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/NoticeListView.vue b/src/modules/sys/NoticeListView.vue
new file mode 100644
index 0000000..e967f2f
--- /dev/null
+++ b/src/modules/sys/NoticeListView.vue
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ TYPE_MAP[row.type] || '其他' }}
+
+
+ {{ previewContent(row.content) }}
+
+
+ {{ STATUS_MAP[row.status].label }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+ 系统通知
+ 公告
+ 活动
+
+
+
+
+
+
+
+ 发布
+ 草稿
+
+
+
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/ParamsListView.vue b/src/modules/sys/ParamsListView.vue
new file mode 100644
index 0000000..47f8d8d
--- /dev/null
+++ b/src/modules/sys/ParamsListView.vue
@@ -0,0 +1,418 @@
+
+
+
+
+
+
+
+
+
+ 权限不足
+
+ 当前账号没有 sys:params:page 权限,无法查看参数列表。
+ 请联系管理员在老后台 /vue/sys/role 给当前角色分配此权限。
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ row.paramCode }}
+
+
+
+ {{ previewValue(row.paramValue) }}
+
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+
+ {{ fmtDate(row.updateDate) }}
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/PostListView.vue b/src/modules/sys/PostListView.vue
new file mode 100644
index 0000000..8775923
--- /dev/null
+++ b/src/modules/sys/PostListView.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ row.postCode }}
+
+ {{ STATUS_MAP[row.status].label }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 正常
+ 停用
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/ProductListView.vue b/src/modules/sys/ProductListView.vue
new file mode 100644
index 0000000..3db323a
--- /dev/null
+++ b/src/modules/sys/ProductListView.vue
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ fmtPrice(row.originPrice) }}
+ {{ fmtPrice(row.price) }}
+
+ {{ STATUS_MAP[row.status].label }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 上架
+ 下架
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/RoleGroupListView.vue b/src/modules/sys/RoleGroupListView.vue
new file mode 100644
index 0000000..df0aafd
--- /dev/null
+++ b/src/modules/sys/RoleGroupListView.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ fmtDate(row.createDate) }}
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/RoleListView.vue b/src/modules/sys/RoleListView.vue
new file mode 100644
index 0000000..d85f353
--- /dev/null
+++ b/src/modules/sys/RoleListView.vue
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 菜单权限和数据范围在编辑弹窗里只能修改基础信息
+ 完整菜单/部门配置请在老后台(/vue/sys/role)操作
+
+
+ 取消
+
+ {{ editMode === 'create' ? '确认新增' : '确认修改' }}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/ScheduleListView.vue b/src/modules/sys/ScheduleListView.vue
new file mode 100644
index 0000000..b681137
--- /dev/null
+++ b/src/modules/sys/ScheduleListView.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+ {{ row.beanName }}
+ {{ row.cron }}
+
+ {{ STATUS_MAP[row.status].label }}
+
+ {{ fmtDate(row.createDate) }}
+
+ 执行
+ 暂停
+ 恢复
+ 删除
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/sys/UserListView.vue b/src/modules/sys/UserListView.vue
new file mode 100644
index 0000000..a40399a
--- /dev/null
+++ b/src/modules/sys/UserListView.vue
@@ -0,0 +1,455 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ {{ row.username }}
+
+
+
+ {{ USER_TYPE_MAP[row.userType].label }}
+ --
+
+
+
+ {{ STATUS_MAP[row.status].label }}
+
+
+
+ {{ fmtDate(row.createDate) }}
+
+
+
+ 停用
+ 启用
+ 重置密码
+ 删除
+
+
+
+
+
+
+
+ 用户: {{ resetPwdTarget.username }}
+ ({{ resetPwdTarget.realName }})
+
+
+
+
+
+
+
+ 重置后请通过安全渠道告知用户
+ 用户下次登录需使用新密码
+
+
+ 取消
+ 确认重置
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/router/index.ts b/src/router/index.ts
index 48bba63..94295c2 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -215,15 +215,15 @@ const routes: RouteRecordRaw[] = [
// job 1 个
{ path: '/job/schedule', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'job-schedule' }), meta: { title: '定时任务' } },
// order 9 个
- { path: '/order/ordergood', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-ordergood' }), meta: { title: '商品订单' } },
- { path: '/order/serviceorder', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-serviceorder' }), meta: { title: '服务订单' } },
- { path: '/order/groupsupplierorder', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-group-supplier' }), meta: { title: '商品拼团订单' } },
- { path: '/order/groupserviceorder', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-group-service' }), meta: { title: '服务拼团订单' } },
- { path: '/order/ordergood/after', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-ordergood-after' }), meta: { title: '商品售后订单' } },
- { path: '/order/orderserviceafter', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-orderserviceafter' }), meta: { title: '服务售后订单' } },
- { path: '/order/ordergood/commission', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-ordergood-commission' }), meta: { title: '实物订单分佣统计' } },
- { path: '/order/serviceorder/commission', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-serviceorder-commission' }), meta: { title: '服务订单分佣统计' } },
- { path: '/order/base-price-analysis', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'order-base-price-analysis' }), meta: { title: '基础价格分析表' } },
+ { path: '/order/ordergood', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品订单' } },
+ { path: '/order/serviceorder', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单' } },
+ { path: '/order/groupsupplierorder', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品拼团订单' } },
+ { path: '/order/groupserviceorder', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务拼团订单' } },
+ { path: '/order/ordergood/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '实物订单分佣统计' } },
+ { path: '/order/serviceorder/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单分佣统计' } },
+ { path: '/order/ordergood/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品售后订单' } },
+ { path: '/order/orderserviceafter', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
+ { path: '/order/base-price-analysis', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '基础价格分析表' } },
// other 7 个
{ path: '/other/brand', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-brand' }), meta: { title: '品牌管理' } },
{ path: '/other/category/good', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-category-good' }), meta: { title: '平台分类' } },
diff --git a/src/stores/inquiry.ts b/src/stores/inquiry.ts
new file mode 100644
index 0000000..171b2af
--- /dev/null
+++ b/src/stores/inquiry.ts
@@ -0,0 +1,155 @@
+import { defineStore } from 'pinia'
+import { ref, computed } from 'vue'
+import { httpGet, httpPost } from '@/utils/request'
+
+/**
+ * 询价 Store
+ * W3.7 改用 httpGet 统一封装
+ *
+ * F002 W2 起调真 API:
+ * - GET /sys/openapi/inquiry/list?page=&limit=&status=
+ * - GET /sys/openapi/inquiry/info?id=
+ * - POST /sys/openapi/inquiry/publish
+ * - POST /sys/openapi/inquiry/quote
+ * - POST /sys/openapi/inquiry/cancel
+ *
+ * 字段映射(后端 db 字段 -> 前端展示):
+ * - id -> 单号
+ * - customerName -> 客户
+ * - productName -> 商品
+ * - purchaseQuantity+unit -> 数量
+ * - status -> 状态
+ * - createDate -> 时间
+ */
+
+export type InquiryStatus = 'pending' | 'quoting' | 'done' | 'cancelled' | string
+
+export interface Inquiry {
+ id: string
+ customer: string
+ product: string
+ qty: string
+ status: InquiryStatus
+ time: string
+ raw?: any
+}
+
+export interface InquiryListResult {
+ list: Inquiry[]
+ total: number
+ page: number
+ limit: number
+ isLive: boolean
+ error?: string
+}
+
+const STATUS_MAP: Record
= {
+ pending: { label: '待报价', type: 'primary' },
+ quoting: { label: '报价中', type: 'warning' },
+ done: { label: '已成交', type: 'success' },
+ cancelled: { label: '已取消', type: 'danger' }
+}
+
+export const STATUS_OPTIONS = [
+ { value: '', label: '全部' },
+ { value: 'pending', label: '待报价' },
+ { value: 'quoting', label: '报价中' },
+ { value: 'done', label: '已成交' },
+ { value: 'cancelled', label: '已取消' }
+]
+
+export function statusLabel(s: string) {
+ return STATUS_MAP[s]?.label || s || '未知'
+}
+export function statusType(s: string): any {
+ return (STATUS_MAP[s]?.type || 'info') as any
+}
+
+function normalizeInquiry(it: any): Inquiry {
+ return {
+ id: String(it.id || it.rfqId || it.inquiryId || 'RFQ-?'),
+ customer: it.customerName || it.customer || it.customerCode || '--',
+ product: it.productName || it.product || it.productCode || '--',
+ qty: it.purchaseQuantity
+ ? `${it.purchaseQuantity}${it.unit ? ' ' + it.unit : ''}`
+ : it.qty || '--',
+ status: it.status || 'pending',
+ time: (it.createDate || it.createTime || '').toString().slice(0, 16),
+ raw: it
+ }
+}
+
+export const useInquiryStore = defineStore('inquiry', () => {
+ const list = ref([])
+ const total = ref(0)
+ const page = ref(1)
+ const limit = ref(20)
+ const statusFilter = ref('')
+ const loading = ref(false)
+ const error = ref(null)
+ const lastFetchedAt = ref(0)
+ const isLive = ref(false)
+
+ const pageCount = computed(() => Math.max(1, Math.ceil(total.value / limit.value)))
+
+ async function fetchList(force = false) {
+ if (loading.value) return
+ if (!force && lastFetchedAt.value && Date.now() - lastFetchedAt.value < 60 * 1000) {
+ return
+ }
+ loading.value = true
+ error.value = null
+ try {
+ const data = await httpGet('/sys/openapi/inquiry/list', {
+ page: page.value,
+ limit: limit.value,
+ ...(statusFilter.value ? { status: statusFilter.value } : {})
+ })
+ list.value = (Array.isArray(data) ? data : []).map(normalizeInquiry)
+ total.value = list.value.length + (page.value - 1) * limit.value // 简化 total(后端不返)
+ isLive.value = true
+ lastFetchedAt.value = Date.now()
+ } catch (e: any) {
+ error.value = e?.msg || e?.message || '询价列表加载失败'
+ isLive.value = false
+ } finally {
+ loading.value = false
+ }
+ }
+
+ /**
+ * 详情优先复用 store.list 缓存(W2 D4 踩坑:后端 /info 期望 Long 不接受雪花 ID 字符串)
+ * 不命中再调 fetchOne
+ */
+ async function fetchOne(id: string): Promise {
+ try {
+ const data = await httpGet(`/sys/openapi/inquiry/${id}`)
+ return data ? normalizeInquiry(data) : null
+ } catch {
+ return null
+ }
+ }
+
+ /**
+ * W3.4 报价:POST /sys/openapi/inquiry/quote
+ * 入参 id + price + remark
+ * 业务异常交给调用方处理(用 try/catch,err.msg 给 toast)
+ */
+ async function quote(id: string, price: number, remark: string): Promise {
+ return await httpPost('/sys/openapi/inquiry/quote', { id, price, remark })
+ }
+
+ function setStatusFilter(s: string) {
+ statusFilter.value = s
+ page.value = 1
+ }
+ function setPage(p: number) {
+ page.value = p
+ }
+
+ return {
+ list, total, page, limit, statusFilter, loading, error, isLive, pageCount,
+ lastFetchedAt,
+ fetchList, fetchOne, quote, setStatusFilter, setPage
+ }
+})
diff --git a/src/stores/user.ts b/src/stores/user.ts
index fa8b7a8..1c12745 100644
--- a/src/stores/user.ts
+++ b/src/stores/user.ts
@@ -1,41 +1,114 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
+import { httpGet } from '@/utils/request'
/**
* 用户 Store
- * 架构:token 存 localStorage(同源 cookie 受限),Pinia 只存 user info
- * 后期可加 token 自动刷新逻辑
+ * W3.7:改用 httpGet 统一封装(自动 token + 401 跳登录)
*/
const STORAGE_KEY = 'access_token'
+function readStorage(key: string, fallback = ''): string {
+ try {
+ return localStorage.getItem(key) || fallback
+ } catch {
+ return fallback
+ }
+}
+
+function safeParseArray(raw: string): string[] {
+ if (!raw) return []
+ try {
+ const v = JSON.parse(raw)
+ return Array.isArray(v) ? v : []
+ } catch {
+ return []
+ }
+}
+
+function writeStorage(key: string, val: string) {
+ try {
+ if (val) localStorage.setItem(key, val)
+ else localStorage.removeItem(key)
+ } catch {}
+}
+
export const useUserStore = defineStore('user', () => {
- const token = ref(localStorage.getItem(STORAGE_KEY))
- const username = ref(localStorage.getItem('username') || '')
- const permissions = ref(JSON.parse(localStorage.getItem('permissions') || '[]'))
+ const token = ref(readStorage(STORAGE_KEY) || null)
+ const username = ref(readStorage('username'))
+ const realName = ref(readStorage('realName'))
+ const headUrl = ref(readStorage('headUrl'))
+ const superAdmin = ref(Number(readStorage('superAdmin')) || 0)
+ const permissions = ref(safeParseArray(readStorage('permissions')))
const isLoggedIn = computed(() => !!token.value)
+ const displayName = computed(() => realName.value || username.value)
+ const initial = computed(() => (realName.value || username.value || '?').slice(0, 1).toUpperCase())
- function setAuth(payload: { token: string; username: string; permissions?: string[] }) {
+ function setAuth(payload: {
+ token: string
+ username: string
+ realName?: string
+ headUrl?: string
+ superAdmin?: number
+ permissions?: string[]
+ }) {
token.value = payload.token
username.value = payload.username
+ realName.value = payload.realName || ''
+ headUrl.value = payload.headUrl || ''
+ superAdmin.value = payload.superAdmin || 0
permissions.value = payload.permissions || []
- localStorage.setItem(STORAGE_KEY, payload.token)
- localStorage.setItem('username', payload.username)
- localStorage.setItem('permissions', JSON.stringify(payload.permissions || []))
+ writeStorage(STORAGE_KEY, payload.token)
+ writeStorage('username', payload.username)
+ writeStorage('realName', payload.realName || '')
+ writeStorage('headUrl', payload.headUrl || '')
+ writeStorage('superAdmin', String(payload.superAdmin || 0))
+ writeStorage('permissions', JSON.stringify(payload.permissions || []))
}
function logout() {
token.value = null
username.value = ''
+ realName.value = ''
+ headUrl.value = ''
+ superAdmin.value = 0
permissions.value = []
- localStorage.removeItem(STORAGE_KEY)
- localStorage.removeItem('username')
- localStorage.removeItem('permissions')
+ ;['access_token', 'username', 'realName', 'headUrl', 'superAdmin', 'permissions'].forEach(k =>
+ writeStorage(k, '')
+ )
}
function hasPermission(code: string): boolean {
+ if (superAdmin.value === 1) return true
return permissions.value.includes('*:*:*') || permissions.value.includes(code)
}
- return { token, username, permissions, isLoggedIn, setAuth, logout, hasPermission }
+ /**
+ * 登录后调 /sys/user/info 拿真实用户信息
+ * 失败静默:头像/姓名是装饰,不影响后续菜单/看板
+ */
+ async function fetchUserInfo() {
+ if (!token.value) return null
+ try {
+ const u = await httpGet('/sys/user/info')
+ realName.value = u.realName || ''
+ username.value = u.username || username.value
+ headUrl.value = u.headUrl || ''
+ superAdmin.value = u.superAdmin || 0
+ writeStorage('realName', realName.value)
+ writeStorage('username', username.value)
+ writeStorage('headUrl', headUrl.value)
+ writeStorage('superAdmin', String(superAdmin.value))
+ return u
+ } catch {
+ return null
+ }
+ }
+
+ return {
+ token, username, realName, headUrl, superAdmin, permissions,
+ isLoggedIn, displayName, initial,
+ setAuth, logout, hasPermission, fetchUserInfo
+ }
})
diff --git a/src/theme/presets.ts b/src/theme/presets.ts
index e0425f7..01c813b 100644
--- a/src/theme/presets.ts
+++ b/src/theme/presets.ts
@@ -13,7 +13,7 @@ export interface ThemePreset {
export const THEME_PRESETS: ThemePreset[] = [
{ value: 'tech-blue', name: '科技蓝', color: '#2563EB' },
{ value: 'red', name: '中国红', color: '#DC2626' },
- { value: 'purple', name: 'Notion 紫', color: '#5645D4' },
+ { value: 'purple', name: '深空紫', color: '#5645D4' },
{ value: 'dark', name: '暗夜黑', color: '#0F172A' }
]
diff --git a/src/utils/request.ts b/src/utils/request.ts
new file mode 100644
index 0000000..05a85e5
--- /dev/null
+++ b/src/utils/request.ts
@@ -0,0 +1,114 @@
+/**
+ * F006 统一 HTTP 客户端
+ * W3.7 基础:所有 store 用这个 http 实例,自动带 token,401 跳登录,错误 toast
+ *
+ * 用法:
+ * import { http } from '@/utils/request'
+ * const res = await http.get('/sys/menu/nav')
+ * // 业务错误已 reject(用 try/catch 处理),成功自动返回 data.data
+ */
+import axios, { type AxiosInstance, type InternalAxiosRequestConfig } from 'axios'
+import { ElMessage } from 'element-plus'
+
+export interface ApiEnvelope {
+ code: number
+ msg?: string | null
+ data: T
+}
+
+const STORAGE_KEY = 'access_token'
+
+export const http: AxiosInstance = axios.create({
+ baseURL: '',
+ timeout: 15000
+})
+
+http.interceptors.request.use((config: InternalAxiosRequestConfig) => {
+ // 跳过 OAuth2 登录请求(用 Basic auth 不加 Bearer)
+ if (config.url?.includes('/auth/oauth/token')) return config
+ const token = localStorage.getItem(STORAGE_KEY)
+ if (token) {
+ config.headers = config.headers ?? {}
+ config.headers.Authorization = `Bearer ${token}`
+ }
+ return config
+})
+
+http.interceptors.response.use(
+ (response) => {
+ const body = response.data as ApiEnvelope | undefined
+ if (body && typeof body === 'object' && 'code' in body) {
+ if (body.code === 0) return response
+ // 业务错:401 token 过期 / 其它业务错
+ if (body.code === 401 || body.code === 401000) {
+ handleAuthExpired()
+ return Promise.reject(body)
+ }
+ // 业务错(不弹 toast,留给调用方决定)
+ return Promise.reject(body)
+ }
+ return response
+ },
+ (error) => {
+ const status = error.response?.status
+ if (status === 401) {
+ handleAuthExpired()
+ } else {
+ const msg =
+ error.response?.data?.msg ||
+ error.response?.data?.message ||
+ error.message ||
+ '请求失败'
+ ElMessage.error(msg)
+ }
+ return Promise.reject(error)
+ }
+)
+
+function handleAuthExpired() {
+ ElMessage.warning('登录已过期,请重新登录')
+ localStorage.removeItem(STORAGE_KEY)
+ localStorage.removeItem('username')
+ localStorage.removeItem('realName')
+ localStorage.removeItem('headUrl')
+ localStorage.removeItem('superAdmin')
+ localStorage.removeItem('permissions')
+ // 用 location 跳,避免 router 状态被污染
+ const base = import.meta.env.BASE_URL || '/admin/'
+ window.location.href = base + 'login'
+}
+
+/**
+ * 简化业务调用:
+ * const data = await httpGet