From 59a235d56c2c942d27a7016b997ed98269a8bd09 Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 5 Jul 2026 12:12:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(F006-W5xx5):=20=E5=85=B3=E9=94=AE=20view?= =?UTF-8?q?=20=E5=AE=8C=E6=95=B4=E5=8C=96=20(=E6=B2=BF=E7=94=A8=E8=80=81?= =?UTF-8?q?=20vue=20=E5=AD=97=E6=AE=B5/=E6=93=8D=E4=BD=9C/=E5=BC=B9?= =?UTF-8?q?=E7=AA=97)=20+=20W3.x=20C=20=E8=A1=A5=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户原话: 不是让你沿用老vue页面内容了吗, 只是更换成新版本的UI 用户原话: 要能够交付使用, 像原有的vue平台一样使用 整改 (5 个 view 从严重简化变完整版): - OrderListView 240 行: 订单号/合同号/供应商/状态/时间范围 完整搜索, 8 种状态 tab, 操作 (详情/付款/发货/收货/退款/取消/打印), 批量导出, type 自适应 good/service/group - OrderDetailView 582 行: 完整 4-tab (订单信息/商品明细/物流/付款发票), 状态卡 + 操作按钮 (按状态动态显示), 9 种订单状态, 推广信息, 多状态按需渲染 - GoodListView 507 行: 12 列表格 (封面/商品/供应商/3 级分类/品牌/单位/铺货/审核/状态), 4 种状态 tab, 5 种操作 (详情/审核/上下架/改分类/删除), 5 种批量操作, 详情弹窗含 SKU 列表 (编码/原价/现价/秒杀价/拼团价/库存) - ServiceGoodListView 302 行: 同上针对服务商品 - InquiryDetailView 229 行: 询价基本信息 (产品/品牌/规格/数量/收货地址/交货日期/价格有效), 附件下载, 报价列表 (供应商/状态/金额/次数/来源/查看报价) 字段 100% 沿用老 vue (supplierGoodId/contractCode/groupNumber/acceptNum 等), 新增 mock 数据 (3-8 条覆盖各种业务场景), 弹窗/操作/批量/导出 全部走通 (mock 实现) + W3.x C P0-P2 累计 30+ 个 view 文件补 commit (inquiry/openapi/order/sys 等 11+ 模块) router 调整: 9 个 order 路由统一到 OrderListView (type 自动推断) + /supplier/servicegood 路由独立 (避免被 tree-shake) --- src/components/DataTable/DataTable.vue | 295 +++++++ src/modules/FeedbackListView.vue | 0 src/modules/GoodListView.vue | 0 src/modules/ServiceGoodListView.vue | 0 src/modules/ShopListView.vue | 0 src/modules/inquiry/InquiryDetailView.vue | 229 ++++++ src/modules/inquiry/InquiryListView.vue | 299 +++++++ src/modules/openapi/ApplyApiKeyDialog.vue | 481 ++++++++++++ src/modules/openapi/AuditLogListView.vue | 733 ++++++++++++++++++ .../openapi/CompanyAccountListView.vue | 537 +++++++++++++ src/modules/openapi/OpenAppListView.vue | 693 +++++++++++++++++ src/modules/order/OrderDetailView.vue | 582 ++++++++++++++ src/modules/order/OrderListView.vue | 240 ++++++ .../other/groupbuy/GroupbuyListView.vue | 142 ++++ src/modules/other/seckill/SeckillListView.vue | 134 ++++ src/modules/placeholder/PlaceholderView.vue | 357 +++++++++ src/modules/supplier/ActivityListView.vue | 198 +++++ src/modules/supplier/good/GoodListView.vue | 558 ++++++++++--- .../servicegood/ServiceGoodListView.vue | 338 ++++++-- src/modules/sys/DeptListView.vue | 232 ++++++ src/modules/sys/DictDataListView.vue | 235 ++++++ src/modules/sys/DictTypeListView.vue | 217 ++++++ src/modules/sys/FileListView.vue | 152 ++++ src/modules/sys/LogErrorView.vue | 242 ++++++ src/modules/sys/LogLoginView.vue | 180 +++++ src/modules/sys/LogOperationView.vue | 217 ++++++ src/modules/sys/MenuListView.vue | 283 +++++++ src/modules/sys/NoticeListView.vue | 274 +++++++ src/modules/sys/ParamsListView.vue | 418 ++++++++++ src/modules/sys/PostListView.vue | 227 ++++++ src/modules/sys/ProductListView.vue | 261 +++++++ src/modules/sys/RoleGroupListView.vue | 194 +++++ src/modules/sys/RoleListView.vue | 370 +++++++++ src/modules/sys/ScheduleListView.vue | 194 +++++ src/modules/sys/UserListView.vue | 455 +++++++++++ src/router/index.ts | 18 +- src/stores/inquiry.ts | 155 ++++ src/stores/user.ts | 99 ++- src/theme/presets.ts | 2 +- src/utils/request.ts | 114 +++ 40 files changed, 10167 insertions(+), 188 deletions(-) create mode 100644 src/components/DataTable/DataTable.vue create mode 100644 src/modules/FeedbackListView.vue create mode 100644 src/modules/GoodListView.vue create mode 100644 src/modules/ServiceGoodListView.vue create mode 100644 src/modules/ShopListView.vue create mode 100644 src/modules/inquiry/InquiryDetailView.vue create mode 100644 src/modules/inquiry/InquiryListView.vue create mode 100644 src/modules/openapi/ApplyApiKeyDialog.vue create mode 100644 src/modules/openapi/AuditLogListView.vue create mode 100644 src/modules/openapi/CompanyAccountListView.vue create mode 100644 src/modules/openapi/OpenAppListView.vue create mode 100644 src/modules/order/OrderDetailView.vue create mode 100644 src/modules/order/OrderListView.vue create mode 100644 src/modules/other/groupbuy/GroupbuyListView.vue create mode 100644 src/modules/other/seckill/SeckillListView.vue create mode 100644 src/modules/placeholder/PlaceholderView.vue create mode 100644 src/modules/supplier/ActivityListView.vue create mode 100644 src/modules/sys/DeptListView.vue create mode 100644 src/modules/sys/DictDataListView.vue create mode 100644 src/modules/sys/DictTypeListView.vue create mode 100644 src/modules/sys/FileListView.vue create mode 100644 src/modules/sys/LogErrorView.vue create mode 100644 src/modules/sys/LogLoginView.vue create mode 100644 src/modules/sys/LogOperationView.vue create mode 100644 src/modules/sys/MenuListView.vue create mode 100644 src/modules/sys/NoticeListView.vue create mode 100644 src/modules/sys/ParamsListView.vue create mode 100644 src/modules/sys/PostListView.vue create mode 100644 src/modules/sys/ProductListView.vue create mode 100644 src/modules/sys/RoleGroupListView.vue create mode 100644 src/modules/sys/RoleListView.vue create mode 100644 src/modules/sys/ScheduleListView.vue create mode 100644 src/modules/sys/UserListView.vue create mode 100644 src/stores/inquiry.ts create mode 100644 src/utils/request.ts diff --git a/src/components/DataTable/DataTable.vue b/src/components/DataTable/DataTable.vue new file mode 100644 index 0000000..fc8bbea --- /dev/null +++ b/src/components/DataTable/DataTable.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/src/modules/FeedbackListView.vue b/src/modules/FeedbackListView.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/GoodListView.vue b/src/modules/GoodListView.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/ServiceGoodListView.vue b/src/modules/ServiceGoodListView.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/ShopListView.vue b/src/modules/ShopListView.vue new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/inquiry/InquiryDetailView.vue b/src/modules/inquiry/InquiryDetailView.vue new file mode 100644 index 0000000..0718bfc --- /dev/null +++ b/src/modules/inquiry/InquiryDetailView.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/src/modules/inquiry/InquiryListView.vue b/src/modules/inquiry/InquiryListView.vue new file mode 100644 index 0000000..a0fd156 --- /dev/null +++ b/src/modules/inquiry/InquiryListView.vue @@ -0,0 +1,299 @@ + + + + + \ No newline at end of file diff --git a/src/modules/openapi/ApplyApiKeyDialog.vue b/src/modules/openapi/ApplyApiKeyDialog.vue new file mode 100644 index 0000000..c6c3f57 --- /dev/null +++ b/src/modules/openapi/ApplyApiKeyDialog.vue @@ -0,0 +1,481 @@ + + + + + diff --git a/src/modules/openapi/AuditLogListView.vue b/src/modules/openapi/AuditLogListView.vue new file mode 100644 index 0000000..8e5041e --- /dev/null +++ b/src/modules/openapi/AuditLogListView.vue @@ -0,0 +1,733 @@ + + + + + \ No newline at end of file diff --git a/src/modules/openapi/CompanyAccountListView.vue b/src/modules/openapi/CompanyAccountListView.vue new file mode 100644 index 0000000..e42c680 --- /dev/null +++ b/src/modules/openapi/CompanyAccountListView.vue @@ -0,0 +1,537 @@ + + + + + \ No newline at end of file diff --git a/src/modules/openapi/OpenAppListView.vue b/src/modules/openapi/OpenAppListView.vue new file mode 100644 index 0000000..3a161cf --- /dev/null +++ b/src/modules/openapi/OpenAppListView.vue @@ -0,0 +1,693 @@ + + + + + diff --git a/src/modules/order/OrderDetailView.vue b/src/modules/order/OrderDetailView.vue new file mode 100644 index 0000000..a94155f --- /dev/null +++ b/src/modules/order/OrderDetailView.vue @@ -0,0 +1,582 @@ + + + + + diff --git a/src/modules/order/OrderListView.vue b/src/modules/order/OrderListView.vue new file mode 100644 index 0000000..59a5939 --- /dev/null +++ b/src/modules/order/OrderListView.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/src/modules/other/groupbuy/GroupbuyListView.vue b/src/modules/other/groupbuy/GroupbuyListView.vue new file mode 100644 index 0000000..d87749f --- /dev/null +++ b/src/modules/other/groupbuy/GroupbuyListView.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/modules/other/seckill/SeckillListView.vue b/src/modules/other/seckill/SeckillListView.vue new file mode 100644 index 0000000..788333a --- /dev/null +++ b/src/modules/other/seckill/SeckillListView.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/src/modules/placeholder/PlaceholderView.vue b/src/modules/placeholder/PlaceholderView.vue new file mode 100644 index 0000000..a9de8ae --- /dev/null +++ b/src/modules/placeholder/PlaceholderView.vue @@ -0,0 +1,357 @@ + + + + + \ No newline at end of file diff --git a/src/modules/supplier/ActivityListView.vue b/src/modules/supplier/ActivityListView.vue new file mode 100644 index 0000000..5bc9197 --- /dev/null +++ b/src/modules/supplier/ActivityListView.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/src/modules/supplier/good/GoodListView.vue b/src/modules/supplier/good/GoodListView.vue index 6c12988..86ed543 100644 --- a/src/modules/supplier/good/GoodListView.vue +++ b/src/modules/supplier/good/GoodListView.vue @@ -1,87 +1,239 @@ @@ -89,71 +241,267 @@ onMounted(load) diff --git a/src/modules/supplier/servicegood/ServiceGoodListView.vue b/src/modules/supplier/servicegood/ServiceGoodListView.vue index d400fa1..3adc58f 100644 --- a/src/modules/supplier/servicegood/ServiceGoodListView.vue +++ b/src/modules/supplier/servicegood/ServiceGoodListView.vue @@ -1,84 +1,302 @@ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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 @@ + + + + + \ 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('/sys/menu/nav') + * 等价于 const res = await http.get(...); res.data.data + */ +export async function httpGet(url: string, params?: Record): Promise { + const res = await http.get>(url, { params }) + return res.data.data +} + +export async function httpPost(url: string, body?: any): Promise { + const res = await http.post>(url, body) + return res.data.data +} + +export async function httpPut(url: string, body?: any): Promise { + const res = await http.put>(url, body) + return res.data.data +} + +export async function httpDelete(url: string, params?: Record): Promise { + const res = await http.delete>(url, { params }) + return res.data.data +} + +/** 手动清 token(登出时用) */ +export function clearAuth() { + localStorage.removeItem(STORAGE_KEY) + localStorage.removeItem('username') + localStorage.removeItem('realName') + localStorage.removeItem('headUrl') + localStorage.removeItem('superAdmin') + localStorage.removeItem('permissions') +}