Compare commits
60 Commits
v1.0
...
guilixu-sh
| Author | SHA1 | Date | |
|---|---|---|---|
| c553a71ea2 | |||
| a284a83b86 | |||
| bf60ba1d04 | |||
| eb26594f41 | |||
| e87e60fd6a | |||
| 4ab90c9d89 | |||
| a92248bff7 | |||
| dd094f8769 | |||
| 4a8b0a957c | |||
| 45fc45680f | |||
| 55e90c2434 | |||
| f9bf3c6df3 | |||
| d6b56d99ce | |||
| 0bfd5e667b | |||
| a831dce61a | |||
| e2f6dce7a1 | |||
| 44e416525d | |||
| 708bc7fc80 | |||
| a4107e212a | |||
| c9a4e4c229 | |||
| f65b8a945d | |||
| 9395e63f20 | |||
| 2037b0699a | |||
| bef50c2f26 | |||
| 57be388a65 | |||
| bd814d00b0 | |||
| 3edbb13a8d | |||
| b406973039 | |||
| 2745f4b0db | |||
| e9249c910f | |||
| 6b57dd2fed | |||
| c9e074366f | |||
| b07ceaa8f1 | |||
| 1662daf436 | |||
| 1d69923e06 | |||
| 0b6563aad9 | |||
| 6c50ffa58b | |||
| 6fcc120e91 | |||
| 015cd50c08 | |||
| 5b5bb3b0e5 | |||
| ace46ffea6 | |||
| a2c4f49ee6 | |||
| 00346d96d8 | |||
| 071a3bf110 | |||
| d5e040b1e6 | |||
| 3a951c70f5 | |||
| 2abd7e3372 | |||
| 7c73f9f821 | |||
| 09af76e4ea | |||
| 29a18ffbc6 | |||
| e0d27f67d5 | |||
| a59d428e78 | |||
| 9e65007e65 | |||
| e2dd8dbc18 | |||
| 08b697556b | |||
| b0ff82599a | |||
| 57b584df1b | |||
| 6350a9b5a9 | |||
| 614e843673 | |||
| 894592c290 |
1
.env
1
.env
@@ -1,2 +1,3 @@
|
|||||||
VITE_APP_NAME=小程序商城
|
VITE_APP_NAME=小程序商城
|
||||||
VITE_API_URL=/api
|
VITE_API_URL=/api
|
||||||
|
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
VITE_APP_NAME=后台管理系统
|
VITE_APP_NAME=后台管理系统
|
||||||
#VITE_API_URL=http://127.0.0.1:9200/api
|
#VITE_API_URL=http://127.0.0.1:9200/api
|
||||||
VITE_API_URL=https://shop-api.websoft.top/api
|
VITE_API_URL=https://shop-api.websoft.top/api
|
||||||
|
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
VITE_APP_NAME=后台管理系统
|
VITE_APP_NAME=后台管理系统
|
||||||
VITE_API_URL=https://shop-api.websoft.top/api
|
VITE_API_URL=https://shop-api.websoft.top/api
|
||||||
|
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||||
|
|||||||
@@ -6,3 +6,27 @@
|
|||||||
- 在 `src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
|
- 在 `src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
|
||||||
- 在 `src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
|
- 在 `src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
|
||||||
- 提取了 CMS Dashboard 的 loadData 函数使其可复用。
|
- 提取了 CMS Dashboard 的 loadData 函数使其可复用。
|
||||||
|
|
||||||
|
## 更新 shop/dashboard 快捷操作按钮为商城常用功能
|
||||||
|
|
||||||
|
- 快捷操作按钮从原来的通用功能(参数配置/用户管理/站点管理/登录日志)改为商城系统常用功能:订单管理、商品管理、商品分类、优惠券管理、会员管理、商城设置、清除缓存。
|
||||||
|
- 路由路径统一使用 `/shop/shopXxx` 格式(与 `/shop/shopOrder` 等已确认路径一致),修复了快速入口中 `/shopGoods` → `/shop/shopGoods` 的路径不一致问题。
|
||||||
|
- 快速入口九宫格也同步改为商城相关入口。
|
||||||
|
- 图标导入更新:移除 UngroupOutlined/CalendarOutlined/UserOutlined/FileTextOutlined,新增 ShoppingCartOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined。
|
||||||
|
|
||||||
|
## 修复 shop/dashboard 待发货订单数量统计不准确
|
||||||
|
|
||||||
|
- 根因:订单列表页 datasource 中 `where.type = 0`(只查商城订单),但 Dashboard 统计未传 `type: 0`,导致把预定订单/外卖(type=1)和会员卡订单(type=2)也计入。
|
||||||
|
- 修复:为 pendingShipmentCount 和 pendingRefundCount 查询都加上 `type: 0`。
|
||||||
|
|
||||||
|
## 修复 Dashboard 运行天数为 0 的问题
|
||||||
|
|
||||||
|
- 根因:`getTenantInfo()` 被放在 `await Promise.all([...])` 之后,如果 `statisticsStore.fetchStatistics()` 抛异常(`couponUsedCount` 错误),`Promise.all` reject 后直接跳到 `catch`,`getTenantInfo` 根本不会执行,`tenantCreateTime` 永远是空值。
|
||||||
|
- 修复:在 `shop/dashboard/index.vue` 和 `cms/dashboard/index.vue` 的 `loadData` 中,把 `getTenantInfo()` 放到 `Promise.all` 外面,用 `.then` 独立执行,不受其他请求失败影响。
|
||||||
|
- 运行天数计算基于 `getTenantInfo()` 返回的 `Company.createTime`(租户创建时间)。
|
||||||
|
|
||||||
|
## 修复 statisticsStore couponUsedCount 冲突
|
||||||
|
|
||||||
|
- 根因:`couponUsedCount` 同时被定义成 state 属性和 getter,Pinia 中同名冲突导致 action 里 `this.couponUsedCount = ...` 报错:`'set' on proxy: trap returned falsish`。
|
||||||
|
- 修复:将 getter 改名为 `safeCouponUsedCount`,`shop/dashboard/index.vue` 中同步更新引用(该引用当前被注释,但保持一致性)。
|
||||||
|
|
||||||
|
|||||||
279
.workbuddy/memory/2026-07-15.md
Normal file
279
.workbuddy/memory/2026-07-15.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# 2026-07-15
|
||||||
|
|
||||||
|
## shopOrder 选项卡默认改为全部
|
||||||
|
|
||||||
|
- 需求:`/shop/shopOrder` 选项卡默认从"待发货"改为"全部"。
|
||||||
|
- 改动 `src/views/shop/shopOrder/index.vue`:`activeKey` 初始值在路由参数 `tab` 无效时的回退值由 `'undelivered'` 改为 `'all'`。
|
||||||
|
- `datasource` 中 `getStatusFilterByTab('all')` 返回 `undefined`,即不传 `statusFilter`,查全部订单,符合预期。
|
||||||
|
|
||||||
|
## 修复 shopOrder 页面默认待发货tab数据不正确
|
||||||
|
|
||||||
|
- 根因:`activeKey` 默认为 `'undelivered'`,但 `datasource` 函数只设了 `where.type = 0`,没有设 `statusFilter`。`statusFilter` 只在用户点击 tab 触发 `onTabs` 时才设置。所以页面初次加载时查的是全部订单而非待发货订单。
|
||||||
|
- 修复:提取 `getStatusFilterByTab(key)` 公共映射函数,在 `datasource` 中当 `where.statusFilter` 未设置时从 `activeKey` 自动推导。`onTabs` 也简化为复用同一函数。
|
||||||
|
- 文件:`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 订单总数点击跳转支持指定 tab
|
||||||
|
|
||||||
|
- Dashboard 中"订单总数"/"总营业额"跳转链接改为 `/shop/shopOrder?tab=all`,"待发货订单"改为 `?tab=undelivered`,"退款申请"改为 `?tab=refunded`。
|
||||||
|
- shopOrder 页面读取 `route.query.tab` 初始化 `activeKey`,无效值回退到 `'undelivered'`。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 待处理事项新增"待付款订单"
|
||||||
|
|
||||||
|
- 在待发货订单上方新增"待付款订单"统计项,使用 `statusFilter=0` 查询,点击跳转 `/shop/shopOrder?tab=unpaid`。
|
||||||
|
- shopOrder 页面取消注释"待付款"tab,`validTabs` 加入 `unpaid`。
|
||||||
|
- 新增 `dot-gold` 样式。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`、`src/views/shop/shopOrder/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 引入 useTenantStore,创建时间读租户信息
|
||||||
|
|
||||||
|
- 引入 `useTenantStore`,`loadData` 中调用 `tenantStore.fetchTenantInfo()` 替代直接调用 `getTenantInfo()` API。
|
||||||
|
- 基本信息"创建时间"改为读取 `tenantStore.company?.createTime`。
|
||||||
|
- 运行天数也改为从 `tenantStore.company?.createTime` 计算,移除独立的 `tenantCreateTime` ref。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`
|
||||||
|
|
||||||
|
## Dashboard 欢迎横幅新增商城Logo
|
||||||
|
|
||||||
|
- 通过 `getShopSettingCategoryValues('basic')` 获取商城设置,读取 `shopLogo` 字段。
|
||||||
|
- 使用 `getCompressedImageUrl(shopLogo, { width: 200, quality: 90 })` 压缩图片。
|
||||||
|
- 在 welcome-banner 左侧用 `a-avatar` (64px, square) 展示,无 Logo 时不显示。
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`
|
||||||
|
|
||||||
|
## 新增 useAppSubscriptionStore(websopy 应用订阅特殊接口)
|
||||||
|
|
||||||
|
- 新增 `src/api/app/appSubscription/model.ts`:AppSubscription 实体及参数/返回类型,对照后端 `com.gxwebsoft.app.entity.AppSubscription`。
|
||||||
|
- 新增 `src/api/app/appSubscription/index.ts`:封装 `AppSubscriptionController` 全部 14 个接口。
|
||||||
|
- 特殊接口域名 `https://websopy-api.websoft.top/api`(与项目主 API 域名不同),复用 `@/utils/request`(共享登录态 token、401 处理),传绝对 URL。
|
||||||
|
- 后端无 context-path;baseURL=`https://websopy-api.websoft.top/api`,接口路径用 `/app/subscription/xxx`。
|
||||||
|
- 后端 `BaseController` 成功 code=0;`success(IPage)` 转成 `PageResult{list,count}`,与项目 `@/api` 的 PageResult 结构一致。
|
||||||
|
- 新增 `src/store/modules/appSubscription.ts`:`useAppSubscriptionStore`(Pinia Options API 风格,对齐 site.ts/statistics.ts),聚合列表/详情/支付状态/订阅管理方法。列表/详情缓存5分钟;`checkStatus`/`checkPurchased` 实时请求;订阅/支付/管理类操作成功后调 `invalidateCache()` 失效缓存。
|
||||||
|
|
||||||
|
## 确认线下收款新增上传凭证功能
|
||||||
|
|
||||||
|
- 背景:原"确认线下收款"用 `Modal.confirm` + `createVNode` 动态拼一个 input,只有备注,没有上传凭证。用户需要加凭证上传。
|
||||||
|
- 字段决策:新增 `paymentVoucher?: string` 字段(不复用 comments/merchantRemarks,因为凭证是图片URL非文本)。顺带修复 `buyerRemarks`/`merchantRemarks` 类型 bug(`undefined` → `string?`)。
|
||||||
|
- 重构为独立组件 `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`:
|
||||||
|
- 用 `<a-modal>` + `<a-form>` + `<a-upload list-type="picture-card">` 单张图片上传。
|
||||||
|
- 上传走 `uploadOss`(`src/api/system/file`),存储返回的 `path`(与项目 `UploadCert` 组件约定一致),预览用 `getUrl(path)` 补全。
|
||||||
|
- 凭证必填,备注可选;校验 jpg/png、≤10MB;支持点击预览大图。
|
||||||
|
- API `confirmOfflinePayment(id, remarks?, paymentVoucher?)` 新增第三参数,通过 query params 传给后端。
|
||||||
|
- `index.vue`:移除原 `Modal.confirm`/`createVNode` 逻辑,改为打开新组件(`v-model:visible` + `:data="current"` + `@done="reload"`),与 `DeliveryModal` 模式一致。
|
||||||
|
- 类型检查:新增 0 错误,顺带消掉原 `okType: 'success'` 的类型错误(antdv4 ButtonType 不含 success,新组件去掉了 ok-type)。
|
||||||
|
- 改动文件:`src/api/shop/shopOrder/model/index.ts`、`src/api/shop/shopOrder/index.ts`、`src/views/shop/shopOrder/components/OfflinePaymentModal.vue`(新增)、`src/views/shop/shopOrder/index.vue`
|
||||||
|
- 后端待办(已完成):后端项目 `/Users/gxwebsoft/JAVA/guilixu-java`(MyBatis-Plus 3.4.3.3,无 Flyway/Liquibase,DDL 手动管理,SQL 脚本放 `sql/` 目录):
|
||||||
|
- `ShopOrder.java` 实体新增 `paymentVoucher` 字段(payTime 之后)。
|
||||||
|
- `ShopOrderController.confirmOfflinePayment` 新增 `@RequestParam paymentVoucher`。
|
||||||
|
- `ShopOrderService` 接口 + `ShopOrderServiceImpl` 实现方法签名加 `paymentVoucher` 参数,lambdaUpdate 加 `.set(paymentVoucher 非空, ShopOrder::getPaymentVoucher, paymentVoucher)`。
|
||||||
|
- DDL 脚本 `sql/shop_order_payment_voucher.sql`:`ALTER TABLE shop_order ADD COLUMN payment_voucher VARCHAR(500) NULL ... AFTER pay_time`。
|
||||||
|
- 注意:方法签名从 2 参变 3 参,全项目仅 Controller 一处调用,无断裂。
|
||||||
|
|
||||||
|
## Dashboard 到期时间显示订阅信息 + 立即订阅/扫码支付
|
||||||
|
|
||||||
|
- 改造 `src/views/shop/dashboard/index.vue` 基本信息「到期时间」行:三态渲染(active 显示 expireTime + 已激活 tag + ≤7天到期提醒;pending 显示待支付 + 去支付;无订阅显示立即订阅)。
|
||||||
|
- **productId 关联条件**:`app_product.tenantId = userStore.info.tenantId`(非 siteInfo.appId)。通过 `pageProducts({tenantId,current:1,size:1})` 取该租户应用。
|
||||||
|
- 新增 `src/api/app/appProduct/{model.ts,index.ts}`:AppProduct 类型 + `pageProducts` 接口(GET /api/app/product/page,分页参数 current/size,注意与项目 page/limit 不同)。
|
||||||
|
- 「立即订阅」流程:选 month/year → 免费应用调 `subscribe()` 直接激活;付费应用调 `generatePayQrcode()` 生成小程序码 → Modal 展示二维码 → 每 2.5s 轮询 `checkStatus(subscriptionNo)`,paid 后关闭+刷新(最多 5 分钟)。envVersion 按 `import.meta.env.DEV` 自动判断 trial/release。
|
||||||
|
- pending 订阅「去支付」复用 `subscriptionStore.pay(id,'wechat')`(用 `'miniappQrcode' in result` 类型守卫窄化)生成小程序码,不重复创建订阅。
|
||||||
|
- 小程序端支付页 `websopy-taro/src/passport/pay/index.tsx`:扫码 scene=subscriptionNo → detail-by-no → mp-prepay → requestPayment → mp-confirm,后端写 Redis `wxpay:paid:{no}=1`,Web 端 checkStatus 据此感知。
|
||||||
|
- 移除 dashboard 不再使用的 `siteInfo` 解构(原到期时间读 `siteInfo.expirationTime`,已改为订阅 expireTime),修复 noUnusedLocals 报错。
|
||||||
|
|
||||||
|
## 修复后端 generatePayQrcode 报错 "Field 'price_type' doesn't have a default value"
|
||||||
|
|
||||||
|
- 现象:dashboard 点「立即订阅」付费应用调 `generatePayQrcode` 时,后端 insert app_subscription 报 `price_type` 无默认值。
|
||||||
|
- 根因:后端 `AppSubscriptionController.generatePayQrcode`(websopy-java)创建订阅时漏了 `setPriceType()`,而 DB `price_type` 为 NOT NULL 无默认值。对比 `subscribe` 方法有 `setPriceType`。
|
||||||
|
- 参考 `websopy-pc/app/pages/console/pay/[subscriptionNo].vue`:它用 `subscribe` 创建订阅 + `pay(id,'wechat')` 生成二维码(两步),避开了 `generatePayQrcode`,所以没遇到此 bug。
|
||||||
|
- 修复(后端 `AppSubscriptionController.java` generatePayQrcode 方法):补全 `setPriceType(product.getPriceType())` + `setOriginalPrice`/`setPayStatus(0)`/`setTenantId`,对齐 subscribe。**前端无需改动**,重启后端即可。
|
||||||
|
- 注意:`subscribe` 价格计算用 `price/100`(当分转元),`generatePayQrcode` 直接用 `price`(当元),两者不一致;schema 标注 price 为「元」,故 `generatePayQrcode` 价格逻辑更合理,保留不动。
|
||||||
|
|
||||||
|
## 修复价格多了100倍(product.price 实际单位是分)
|
||||||
|
|
||||||
|
- 用户反馈:`generatePayQrcode` 算出的金额多了100倍。确认 `product.price` 实际存的是**分**(非 schema 标注的元),故 `subscribe` 的 `/100` 是对的,`generatePayQrcode` 直接用 price 是错的。
|
||||||
|
- 后端 `AppSubscriptionController.generatePayQrcode` 价格计算改为 `price/100` 转元(对齐 subscribe),年付按10个月(*10,对齐 subscribe 年付优惠),季付*3,月付*1。
|
||||||
|
- 前端 `dashboard/index.vue` 参考价同步修正:`monthPrice = price/100`,`yearPrice = monthPrice*10`(原为 price 直接 + *12)。
|
||||||
|
- 结论:product.price 实际单位是**分**,所有价格展示/计算都需 /100 转元。
|
||||||
|
|
||||||
|
## 改用 subscribe+pay 避开 generatePayQrcode 的 price_type 报错
|
||||||
|
|
||||||
|
- 现象:用户反馈 `generatePayQrcode` 仍报 `price_type` 无默认值(报错 SQL 字段集是修复前的,说明后端未重启/未重编译,`setPriceType` 修复未生效)。
|
||||||
|
- 关键认知:`generatePayQrcode` 后端**不接收 priceType 参数**(从 `product.getPriceType()` 取),所以前端传 priceType 给它也没用,无法绕过。
|
||||||
|
- 修复(前端方案,不依赖后端重启):dashboard `confirmSubscribe` 改用 `subscribe` + `pay` 两步(参考 websopy-pc 的 `pay/[subscriptionNo].vue` 模式):
|
||||||
|
- `subscribe` 创建订阅(后端有 `setPriceType`,不报错;价格 `/100` 正确)→ 免费 `status=active` 直接成功
|
||||||
|
- 付费 `status=pending` → `pay(subscriptionId,'wechat')` 生成小程序码 → Modal 展示 + 轮询 `checkStatus`
|
||||||
|
- 移除 `confirmSubscribe` 里的 `isFreeProduct` 分支(改用 `subResult.status` 判断);`isFreeProduct` 仍用于订阅 Modal 的免费提示。
|
||||||
|
- vue-tsc 验证 dashboard 零 error TS(总错误 1778 不变)。
|
||||||
|
- 后端 `generatePayQrcode` 的 `setPriceType` 修复仍保留(重启后端后该接口也能用),但前端已不依赖它。
|
||||||
|
|
||||||
|
## 订阅 productId 改为固定 65(小程序商城)
|
||||||
|
|
||||||
|
- 需求:dashboard 立即订阅请求的 productId 从动态(按 `tenantId` 查 `app_product`)改为写死 `65`。
|
||||||
|
- 改动 `src/views/shop/dashboard/index.vue`:
|
||||||
|
- imports:`pageProducts` → `getProductDetail`。
|
||||||
|
- `loadSubscriptionInfo`:移除 `userStore.info.tenantId` 判断和 `pageProducts({tenantId,current:1,size:1})` 调用,改为 `getProductDetail(65)` 查固定产品详情;定义 `FIXED_PRODUCT_ID = 65` 常量,订阅列表过滤和 `currentProduct.productId` 统一使用该常量。
|
||||||
|
- `confirmSubscribe` 无需改动(用 `currentProduct.productId`,已自动是 65)。
|
||||||
|
- `getProductDetail(65)` 返回的 product 含 name/priceType/price,前端参考价 `monthPrice = price/100`、`yearPrice = monthPrice*10` 仍适用。
|
||||||
|
- vue-tsc 验证 dashboard 零 error TS(总错误 1778 不变)。
|
||||||
|
|
||||||
|
## shopGoodsBrowse 列表商品图片改用 OSS 压缩函数
|
||||||
|
|
||||||
|
- 文件:`src/views/shop/shopGoodsBrowse/index.vue`
|
||||||
|
- 引入 `getCompressedImageUrl`(来自 `src/utils/image.ts`),将商品图片 `:src="record.goodsImage"` 改为 `:src="getCompressedImageUrl(record.goodsImage)"`。
|
||||||
|
- 与 `shopGoods`、`shopOrder`、`dashboard` 等页面保持一致:默认宽度 240px、质量 90、自动补全 OSS 域名、跳过已含 `x-oss-process` 的 URL。
|
||||||
|
- 预览行为未改(仍走 a-image 默认 src 预览)。
|
||||||
|
|
||||||
|
## 电脑版后台新订单红点提示(全局)
|
||||||
|
|
||||||
|
- 需求:电脑版后台有新订单时显示红点,让管理员知道有新订单。
|
||||||
|
- 关键发现:系统已有菜单红点基础设施(`menu-title.vue` 读 `item.meta.badge` 渲染徽章;`user.ts` `setMenuBadge(path,value,color)`;`layout/index.vue` 有 `.ele-menu-badge` 样式),但 `useOrderNotify` 原为组件级(dashboard/cms dashboard/shopOrder 三处各自独立轮询),未接通红点。
|
||||||
|
- 改造 `src/views/shop/shopOrder/useOrderNotify.ts` 为**模块级全局单例**:
|
||||||
|
- 轮询/状态/红点全部模块级变量(enabled、hasNewOrder、lastOrderId、timer),多组件调用只一个轮询。
|
||||||
|
- 检测到新订单 → triggerNotify(叮声+语音+遍历回调Set)+ `setOrderBadge(true)` 调 `userStore.setMenuBadge('/shop/shopOrder','dot','#ff4d4f')`。
|
||||||
|
- 新增 `clearBadge()`:立即清红点 + 重查最新订单同步 lastOrderId(标记"已查看")。
|
||||||
|
- `onNewOrder` 回调用 Set 管理,组件 onBeforeUnmount 自动移除;`start()` 幂等,由 layout 调一次。
|
||||||
|
- `src/layout/components/menu-title.vue`:`badge==='dot'` 时用 `<a-badge dot />` 纯红点,否则原数字徽章逻辑。
|
||||||
|
- `src/layout/index.vue`:setup 调 `useOrderNotify().start()` 全局启动,后台任意页面生效。
|
||||||
|
- `src/views/shop/shopOrder/index.vue`:`onMounted` + `onActivated` 调 `clearBadge()`(兼容 keep-alive `cache-key`,用 `orderBadgeMounted` 标志防首次双触发)。
|
||||||
|
- `src/views/shop/dashboard/index.vue`:`quickLinks` 改 computed,订单管理项 `badge: hasNewOrder.value`;`quick-icon` 加红点 span + pulse 动画样式。
|
||||||
|
- 决策(用户确认):红点=新订单未查看(进订单页清除);位置=订单管理菜单+dashboard快速入口订单管理图标;声音保留并全局化。
|
||||||
|
- 构建验证:`vite build --outDir dist_verify` 通过(EXIT:0)。原 `pnpm build` 失败仅因 vite 清空 `dist/assets` 被安全删除保护拦截(83文件>50阈值),与代码无关。
|
||||||
|
|
||||||
|
## shopOrder 订单详情页(orderInfo.vue)支付方式下增加支付凭证图
|
||||||
|
|
||||||
|
- 背景:之前 `OfflinePaymentModal` 确认线下收款时已支持上传 `paymentVoucher`(OSS path),但订单详情页只展示「线下付款」tag,看不到凭证图。
|
||||||
|
- 改动 `src/views/shop/shopOrder/components/orderInfo.vue`:
|
||||||
|
- 模板:在「支付方式」`<a-descriptions-item>` 之后、「开票状态」之前新增「支付凭证」项,`v-if="form.paymentVoucher"`,`:span="3"` 占满整行。
|
||||||
|
- 缩略图用 `a-image`,`:width="120"`,`getCompressedImageUrl(form.paymentVoucher, { width: 750 })`(按用户要求宽度 750),`preview-src-list` 用 `ensureFullUrl(form.paymentVoucher)` 给原图预览。
|
||||||
|
- 引入 `ensureFullUrl` 来自 `@/utils/image`(已有 `getCompressedImageUrl`)。
|
||||||
|
- form reactive 默认值新增 `paymentVoucher: undefined`,对齐 `ShopOrder` model。
|
||||||
|
- `ShopOrder` model 中 `paymentVoucher?: string` 字段早就存在(payTime 之后),无需改 model/API。
|
||||||
|
- 用户场景:仅线下付款(payType=9)的订单才有此凭证图,条件渲染自然处理。
|
||||||
|
|
||||||
|
## Dashboard 基本信息 6 字段数据源重构(product + subscription)
|
||||||
|
|
||||||
|
- 需求:dashboard「基本信息」面板原先混用硬编码/tenant/siteStore,改为统一从 `app_product.product_id=65` + `app_subscription` 读取,语义更准确。
|
||||||
|
- 字段映射决策(用户确认):
|
||||||
|
- 系统名称 → `currentProduct.productName`(原硬编码 '小程序商城')
|
||||||
|
- 版本号 → `currentProduct.version`(原硬编码 '2.0.0')
|
||||||
|
- 运行状态 → `currentProduct.publishStatus` + 中文映射 + a-tag 颜色(原 `siteStore.statusText`)
|
||||||
|
- 创建时间 → `currentSubscription.startTime`(原 `tenantStore.company?.createTime`)
|
||||||
|
- 到期时间 → `currentSubscription.expireTime`(保持不变,原本就是这个)
|
||||||
|
- 系统运行天数 → 自 `currentSubscription.startTime` 起累计(原自 `tenantStore.company?.createTime`;后改为 startTime,与"创建时间"行同源,语义统一)
|
||||||
|
- 改动 `src/views/shop/dashboard/index.vue`:
|
||||||
|
- 删除硬编码 `systemInfo` reactive(已确认 cms 那两个 systemInfo 是独立 const,互不影响)。
|
||||||
|
- import 移除 `reactive`。
|
||||||
|
- 新增 `productStatusText` computed:publishStatus → {text,color} 映射(published=已发布/green、pending_review=审核中/orange、developing=开发中/blue、rejected=已驳回/red、deprecated=已下架/red),未知状态显示原始值,无 product 显示 '-'。
|
||||||
|
- 重写 `runDays` computed:数据源由 `tenantStore.company?.createTime` 改为 `currentSubscription.value?.startTime`(与"创建时间"行同源),注释说明续费新增订阅记录会重置计数的边界行为。
|
||||||
|
- 模板 6 个字段全部重绑,每个字段加 `subscriptionLoading` 骨架屏分支(避免空白/老数据闪现)。
|
||||||
|
- 「系统运行」加 `a-tooltip title="自您首次开通小程序商城起"`(用户要求)。
|
||||||
|
- 数据流复用:`loadSubscriptionInfo()` 原本就调 `getProductDetail(65)` 填充 `currentProduct`,无需新增接口调用。
|
||||||
|
- 类型校验:vue-tsc 对 `shop/dashboard/index.vue` 行级错误 0 条;cms 那批历史错误与本次无关。
|
||||||
|
|
||||||
|
## Dashboard 到期时间续费功能 + 标签矛盾修复
|
||||||
|
|
||||||
|
- 背景:dashboard「到期时间」行原先在订阅已过期时同时显示绿色「已激活」标签 + 「(已过期)」红字,状态自相矛盾;且即将到期/已过期时无续费入口(用户反馈"没有按钮呢")。
|
||||||
|
- 改动 `src/views/shop/dashboard/index.vue`:
|
||||||
|
- 标签动态化:`a-tag :color="isExpired ? 'red' : 'green'"`,文案 `已过期/已激活`;已过期时显示「(已过期N天)」。
|
||||||
|
- 新增续费按钮:`showRenewButton`(`daysToExpire <= 7` 即即将到期或已过期时显示),点击 `onRenew`。
|
||||||
|
- 复用订阅 Modal:新增 `subscribeMode`('subscribe'|'renew')+ `renewing` 状态,Modal 的 title/okText/confirm-loading/@ok 按 mode 动态切换;免费应用 alert 文案也按 mode 区分。
|
||||||
|
- `confirmRenew` 调 `subscriptionStore.renew(sub.id, selectedPeriod)`(POST /app/subscription/renew/{id}?period=month|year),成功后 `refreshSubscription()`。
|
||||||
|
- `onSubscribe` 补设 `subscribeMode='subscribe'`。
|
||||||
|
- 续费 API 语义:`renewSubscription` 返回 string(同步完成,疑似余额扣款/免费续期)。若后端续费需走微信扫码支付,需改为返回支付二维码结构并接 pay 流程——待后端确认。
|
||||||
|
- 关键字段再确认(用户):开始时间用 `appSubscription.startTime`,到期时间用 `appSubscription.expireTime`,daysToExpire 与 `dayjs()`(浏览器本地时间)对比。
|
||||||
|
|
||||||
|
## Dashboard 续费改走 subscribe+pay 扫码(后端 renew 接口未实现)
|
||||||
|
|
||||||
|
- 触发:用户截图显示 `POST /api/app/subscription/renew/90` 返回 404,确认后端没有实现 renew 接口。
|
||||||
|
- 方案:续费不再调 `store.renew` / `renewSubscription`,改为复用现有 `subscribe + pay` 扫码支付链路(与首次订阅完全一致)。后端 subscribe 对已有 active 订阅会处理续期/创建新记录。
|
||||||
|
- 改动 `src/views/shop/dashboard/index.vue` 的 `confirmRenew`:
|
||||||
|
- 删掉 `await subscriptionStore.renew(sub.id, ...)` 改为 `subscribe({ productId, subscriptionPeriod })`。
|
||||||
|
- 免费应用走 `subResult.status === 'active'` 分支,提示「续费成功」并刷新。
|
||||||
|
- 付费应用走 `pay(subscriptionId, 'wechat', envVersion)` → 拿到 `miniappQrcode` → 弹支付 Modal + `startPolling`。
|
||||||
|
- `payProductName` fallback 文案改为「应用续费」(与订阅的「应用订阅」区分)。
|
||||||
|
- `onRenew` 入口、`subscribeMode='renew'`、续费 Modal title/按钮复用不变,UI 上用户感知不到底层调的是 subscribe。
|
||||||
|
- 教训/记录:调后端接口前最好先确认接口是否真实存在;前端 `api/app/appSubscription/index.ts` 的 `renewSubscription` 函数可保留(防止后端后续补上接口),但 dashboard 暂不调用。
|
||||||
|
|
||||||
|
## Dashboard 续费最终定稿:方案1 renew+pay(前端已就绪,等后端实现)
|
||||||
|
|
||||||
|
- 触发:上一步用 subscribe 走续费,后端返回「您已订阅该应用,无需重复购买」——subscribe 对已有 active 订阅是拒绝的,续费不能复用 subscribe。
|
||||||
|
- 定稿方案(用户确认):方案1 —— 后端实现 `/renew/{id}?period=xxx` 返回 `SubscribeResult`(含 subscriptionId),前端再调 `pay` 生成小程序码。与 subscribe+pay 对称,职责清晰。
|
||||||
|
- 三处改动(前端已全部就绪,等后端补 renew 接口即可联调通):
|
||||||
|
1. `src/api/app/appSubscription/index.ts` `renewSubscription`:返回类型 `string` → `SubscribeResult`;取 `res.data.data`(原取 `res.data.message`)。
|
||||||
|
2. `src/store/modules/appSubscription.ts` `renew`:返回类型 `string` → `SubscribeResult`,返回 result 而非 msg。
|
||||||
|
3. `src/views/shop/dashboard/index.vue` `confirmRenew`:改回调 `subscriptionStore.renew(sub.id, period)` 拿 `renewResult` → 免费(status=active)直接成功;付费走 `pay(renewResult.subscriptionId, 'wechat', envVersion)` → 弹码 + `startPolling`。与 confirmSubscribe 完全对称,仅第一步 subscribe→renew、传 sub.id 而非 productId。
|
||||||
|
- **后端接口约定(待实现)**:`POST /api/app/subscription/renew/{id}?period=month|year`
|
||||||
|
- 入参:id=原订阅ID,period=month/year
|
||||||
|
- 逻辑:校验原订阅归属 → 创建 pending 续费记录(startTime=原expireTime或now,expireTime=新到期)→ 返回 `{ subscriptionId, subscriptionNo, status, message, payPrice?, orderNo? }`(同 SubscribeResult)
|
||||||
|
- 前端拿到后调 `pay/{subscriptionId}` 生成微信小程序码,与订阅流程一致
|
||||||
|
- 续费按钮触发条件不变:`showRenewButton`(daysToExpire<=7,即将到期或已过期)。
|
||||||
|
|
||||||
|
## 续费最终对齐:后端已有 /renew-pay(路径不匹配导致 404,改前端即可)
|
||||||
|
|
||||||
|
- 关键发现:后端 `AppSubscriptionController` 其实**已经实现了续费**,方法是 `renewPay`(line 849-910),路径是 `/api/app/subscription/renew-pay/{id}`(带连字符),不是前端调的 `/renew/{id}`,所以之前 404。
|
||||||
|
- 后端 renew-pay 逻辑(已实现,无需改动):
|
||||||
|
- 校验订阅存在 + priceType=subscription + 产品存在
|
||||||
|
- 计算续费价格(年付按10个月,对齐 subscribe)
|
||||||
|
- 计算新到期:`baseTime = max(原expireTime, now)`,`newExpireTime = baseTime + period`(未过期从原到期延后,已过期从现在延后,不丢时长)
|
||||||
|
- 原订阅状态改 pending、覆盖 payPrice/period/expireTime
|
||||||
|
- method=balance → handleBalancePay(直接扣款激活);method=wechat → handleWechatPay(返回 miniappQrcode)
|
||||||
|
- 前端三处改动(对齐后端,从方案1改为方案2"renew一步返码"):
|
||||||
|
1. `api/.../index.ts` `renewSubscription`:路径 `/renew/{id}` → `/renew-pay/{id}`;参数加 `method/envVersion`;返回类型 `SubscribeResult` → `PayResult | WechatNativePayResult`。
|
||||||
|
2. `store/.../appSubscription.ts` `renew`:参数加 `method/envVersion`;返回类型 `PayResult | WechatNativePayResult`。
|
||||||
|
3. `dashboard/index.vue` `confirmRenew`:`renew(sub.id, period, 'wechat', envVersion)` 直接拿支付结果 → `'paid' in result` 余额成功分支;`'miniappQrcode' in result` 弹码+轮询。不再二次调 pay。
|
||||||
|
- 教训:前端 404 时应先 grep 后端 Controller 确认接口是否真实存在及准确路径(带连字符的 `/renew-pay` 容易被误写为 `/renew`)。后端项目位于 `/Users/gxwebsoft/JAVA/websopy-java/`,Controller 全路径 `com.gxwebsoft.app.controller.AppSubscriptionController`。
|
||||||
|
|
||||||
|
## 续费报错"微信小程序配置不存在或解析失败"根因排查
|
||||||
|
|
||||||
|
- 报错:`POST /renew-pay/90` 返回"生成小程序码失败: 微信小程序配置不存在或解析失败 — 请检查 app_setting 表中 key='platform_miniprogram' 的记录"。用户说"之前订阅还可以"。
|
||||||
|
- 根因机制(`WxMiniprogramUtil.getAccessToken`):
|
||||||
|
- access_token 有 Redis 缓存,key=`WX_ACCESS_TOKEN:5`(DEFAULT_TENANT_ID=5),TTL=7000秒(约2小时)。
|
||||||
|
- **缓存命中时直接返回 token,不读 app_setting 配置**;缓存未命中才调 `getMiniprogramSetting()` 读 `app_setting` 表 key='platform_miniprogram'。
|
||||||
|
- 所以"之前订阅可以"= access_token 缓存命中掩盖了配置读取问题;现在续费失败=缓存过期,走到读配置那步发现读不到。
|
||||||
|
- 这**不是续费特有 bug**,现在去测订阅(subscribe+pay)也会同样失败。
|
||||||
|
- `AppSettingServiceImpl.getByKey` 只按 setting_key 查、不按租户过滤(注释"平台配置不按租户隔离"),排除租户隔离问题。
|
||||||
|
- 排查方向(按可能性):
|
||||||
|
1. app_setting 表 platform_miniprogram 记录被误删/setting_value 被清空。
|
||||||
|
2. setting_value JSON 格式损坏(多/少逗号引号),`JSON.parseObject` 抛异常 → catch return null。
|
||||||
|
3. 表里有多条 setting_key='platform_miniprogram' 记录,MyBatis-Plus getOne 抛 TooManyResultsException → catch return null。
|
||||||
|
- 后端 handleWechatPay 开头会调 `diagnoseConfig()` 打印详细日志(app_setting 是否存在、setting_value 原始内容、解析出的 appId/appSecret),看后端日志最快定位。
|
||||||
|
- 修复:配置丢了去小程序配置页重新保存(batchSave 会写入);多条记录则清理只留一条。
|
||||||
|
- 关键文件:`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/common/core/utils/WxMiniprogramUtil.java`(getAccessToken line 182-276,getMiniprogramSetting line 291-316)。
|
||||||
|
|
||||||
|
## 后端 renew-pay 隐患修复(放弃支付不中断服务 + 续费不丢时长)
|
||||||
|
|
||||||
|
- 隐患:原 renewPay 把原订阅 status 改 pending + 提前改 expireTime,用户放弃支付会服务中断、到期时间错乱;且 handleBalancePay/mpConfirm 激活时 expireTime=now+period 覆盖了续费应得的"原到期+period",未过期续费用户损失剩余时长。
|
||||||
|
- 后端改动 `AppSubscriptionController.java`(4 处):
|
||||||
|
1. `renewPay`:删 `setStatus("pending")` 和 `setExpireTime(newExpireTime)`;只设 payStatus=0 + payPrice/originalPrice/period。原订阅 status 保持 active(或 expired),expireTime 不变 → 放弃支付零影响。newExpireTime 仍计算,仅用于日志"预计到期(支付后)"。
|
||||||
|
2. `handleBalancePay` 激活:`sub.setStartTime(now)` → `if (startTime==null) setStartTime(now)`(续费保留首次生效时间);expireTime 由 `now+period` → `max(原expireTime, now)+period`。
|
||||||
|
3. `mpConfirm` 拦截:`if (status=="active")` → `if (status=="active" && payStatus==1)`,放行续费的 active+payStatus=0。
|
||||||
|
4. `mpConfirm` 激活:同 handleBalancePay,startTime 仅 null 时设、expireTime 用 max(原expireTime, now)+period。
|
||||||
|
- max 逻辑对新订阅和续费都正确:新订阅 expireTime 为 null → expireBase=now → now+period;续费未过期 → 原+period(不丢时长);续费已过期 → now+period。
|
||||||
|
- 放弃支付现状:订阅保持 active+payStatus=0,服务不中断;用户可再次点"立即续费"重新 renew-pay(会覆盖 payPrice/period 并重新生成码)。无清理 pending 续费订单的需求(不再产生 pending 记录)。
|
||||||
|
- 前端无需改动(renew-pay 调用不变)。
|
||||||
|
|
||||||
|
## 续费"配置不存在"真正根因:app_setting 未加入租户忽略列表(已修复)
|
||||||
|
|
||||||
|
- 纠正之前猜测:app_setting 表 platform_miniprogram 配置**完全正常**(pymysql 直查 db_websopy 确认:setting_id=12, tenant_id=5, 169字节, JSON 合法, appId=wx541db955e7a62709, appSecret 齐全)。
|
||||||
|
- 真正根因:`MybatisPlusConfig.java` 的 `TenantLineHandler.ignoreTable` 列表里有 `app_subscription` 但**没有 `app_setting`**。
|
||||||
|
- app_setting 查询受租户拦截器隔离 → `getByKey('platform_miniprogram')` 被自动加 `AND tenant_id=当前租户`。
|
||||||
|
- 配置记录 tenant_id=5;续费用户租户 ≠ 5(或请求头/域名取不到租户)→ 查询过滤掉这条记录 → 返回 null → 报"配置不存在或解析失败"。
|
||||||
|
- 订阅 pay 能用 = access_token 缓存命中(Redis key 固定 `WX_ACCESS_TOKEN:5` 不分租户,TTL 7000秒)没走 app_setting 查询;续费报错 = 缓存过期走了查询被拦截。与"之前还可以、现在不行"的时间差完全吻合。
|
||||||
|
- `AppSettingServiceImpl` 代码注释明说"平台配置不按租户隔离"、getByKey 不带租户条件,但拦截器在 SQL 层强制隔离,两者矛盾——这是历史遗留 bug。
|
||||||
|
- 修复(后端 `MybatisPlusConfig.java`):ignoreTable 列表在 `"app_subscription"` 后加 `"app_setting"`(一行改动)。改完 app_setting 所有查询忽略租户,与代码设计意图一致。
|
||||||
|
- 排查方法值得记录:pymysql 直连远程库(47.119.165.234:13308 db_websopy / redis 16379 db0)验证数据;mysql 客户端未装时用 managed python venv 装 pymysql 查。dev profile 连此库。
|
||||||
|
- 前端订阅 API 域名硬编码 `https://websopy-api.websoft.top/api/app/subscription`(api/index.ts WEBSOPY_API_BASE),主站 API 走 shop-api.websoft.top。用户对比的 `websoft.top/api/_app/...` 是另一个入口(疑 nginx 转发 _app→app)。
|
||||||
|
|
||||||
|
## 续费"弹二维码自动完成支付"修复:清除旧 Redis 支付标记
|
||||||
|
|
||||||
|
- 现象:续费 renew-pay 弹出小程序码后几秒,前端轮询 checkStatus 返回 paid=true,自动判定支付成功(用户没扫码)。
|
||||||
|
- 根因(pymysql+redis 直查确认):续费复用原订阅记录的 subscriptionNo,原订阅支付时 mpConfirm/handleBalancePay 写入的 Redis 标记 `wxpay:paid:{subscriptionNo}=1`(TTL 30分钟)未清除。checkStatus 逻辑 `paidViaRedis || paidViaDb`,pay_status 维度(renewPay 设 0)已返回 false,但 Redis 维度残留 → 误判。
|
||||||
|
- 数据证据:id=94 status=active pay_status=0 expire_time=2027-06-15(订阅支付成功→renewPay 改 pay_status=0);Redis wxpay:paid:SUB202607152237396107=1 残留。
|
||||||
|
- 修复(方案A,用户确认):`AppSubscriptionController.renewPay` 在 updateById 后加 `redisUtil.delete("wxpay:paid:" + sub.getSubscriptionNo())`(RedisUtil.delete 在 line 214)。配合 pay_status=0,checkStatus 两个维度都 false → paid=false。
|
||||||
|
- 次要问题(未解决):续费把原订阅 pay_status 1→0,若放弃支付,原订阅变 active+pay_status=0(语义"已激活但未支付"),不影响功能。彻底解决需方案C(renewPay 新建独立订阅记录,原订阅不动),暂不做。
|
||||||
|
- 排查手段:managed python venv 装 pymysql+redis 直连远程库(47.119.165.234:13308/16379)查 app_subscription 表 + Redis scan wxpay:paid:*,比看日志更直接。
|
||||||
|
|
||||||
|
## cmsAd 编辑保存不再拼 OSS 压缩后缀
|
||||||
|
|
||||||
|
- 需求:广告编辑保存时图片 URL 不再拼接 `?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90` 后缀,因为图片压缩已统一在小程序端处理。
|
||||||
|
- 改动 `src/views/cms/cmsAd/components/cmsAdEdit.vue` 的 `chooseFile` 函数:
|
||||||
|
- `images.value.push` 的 `url` 由 `data.downloadUrl + '?x-oss-process=...'` 改为 `data.downloadUrl`。
|
||||||
|
- `form.images` 同步去掉后缀,直接用 `data.downloadUrl`。
|
||||||
|
- 注意:本次只改广告(cmsAd);`cmsArticle/articleEdit.vue`、`cmsModel/cmsModelEdit.vue` 同样有此后缀,用户未提及,暂不动。
|
||||||
63
.workbuddy/memory/2026-07-16.md
Normal file
63
.workbuddy/memory/2026-07-16.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 2026-07-16
|
||||||
|
|
||||||
|
## 续费扫码支付链路修复(前后端联动)
|
||||||
|
|
||||||
|
### 问题1:小程序 pay 页扫码后误显"支付成功"
|
||||||
|
|
||||||
|
- 文件:`websopy-taro/src/passport/pay/index.tsx`
|
||||||
|
- 根因:`fetchDetail` 用 `subscription.status === 'active'` 判定已支付。但后端 `renewPay` 为了避免放弃支付导致服务中断,**故意保留** `status='active'`、仅设 `payStatus=0`。
|
||||||
|
- 修复:判断改为 `payStatus === 1`;同时处理 `cancelled`/`expired` 状态。详见 `websopy-taro/.workbuddy/memory/2026-07-16.md`。
|
||||||
|
|
||||||
|
### 问题2:用户点"立即支付"后端返回"该订阅已激活"
|
||||||
|
|
||||||
|
- 文件:`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppSubscriptionController.java`
|
||||||
|
- 根因:`pay/{id}`(line 339-341)和 `mp-prepay`(line 539-541)两处拦截只看 `status='active'`,与上次 `mp-confirm` 同样的坑——续费的 `active+payStatus=0` 被误拦。
|
||||||
|
- 修复:两处拦截条件对齐 `mp-confirm`,改为 `status='active' && payStatus==1` 才拦截,放行续费的 `active+payStatus=0`。
|
||||||
|
- commit:`7c95f8e` (本地 main,待 push + 服务器部署)。
|
||||||
|
|
||||||
|
### 关键规则(防回归)
|
||||||
|
|
||||||
|
判断"是否已支付"必须用 `payStatus`(0=未支付 / 1=已支付),**不能**用 `status`(订阅生命周期 active/pending/expired/cancelled)。续费场景下这两个字段完全解耦:后端 `renewPay` 保留 `status='active'` + 设 `payStatus=0`。
|
||||||
|
|
||||||
|
后端三处拦截条件现在全部对齐:
|
||||||
|
- `pay/{id}` line 339-343
|
||||||
|
- `mp-prepay` line 541-545
|
||||||
|
- `mp-confirm` line 573(之前已修)
|
||||||
|
|
||||||
|
### 部署待办(用户操作)
|
||||||
|
|
||||||
|
1. 后端:`cd /Users/gxwebsoft/JAVA/websopy-java && git push`,然后远程服务器 `git pull && mvn clean package -DskipTests && docker-compose up -d --build cms-api`(或对应重启方式)。
|
||||||
|
2. 小程序端:`websopy-taro` 重新编译上传体验版/正式版。
|
||||||
|
3. 联调验证:admin 点"立即续费" → 弹码 → 微信扫码 → 进小程序应显示"确认支付"页(不再误显成功)→ 点"立即支付" → 微信支付 → 显示"支付成功" → 跳转 `/user/apps/index`。
|
||||||
|
|
||||||
|
## 商城后台 Dashboard 布局调整
|
||||||
|
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`
|
||||||
|
- 需求:今日数据概况页,「基本信息」改为左右两列并排;删除「快捷操作」面板。
|
||||||
|
- 改动:
|
||||||
|
- `a-descriptions` 由 `:column="1"` 改为 `:column="{ xs: 1, sm: 2 }"`(响应式两列并排)。
|
||||||
|
- 基本信息 `a-col` 由 `:span="12"` 改为 `:span="24"`(占满整行)。
|
||||||
|
- 删除「快捷操作」`a-col` 整块,并清理其专属的图标导入(ShoppingCartOutlined/ShopOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined/ClearOutlined)、`removeSiteInfoCache` 导入及 `handleClearCache` 函数。
|
||||||
|
|
||||||
|
### 今日数据概况 + 基本信息改为左右两栏布局
|
||||||
|
|
||||||
|
- 需求:参考「待处理事项 + 快速入口」的左右结构,将「今日数据概况」和「基本信息」也并排为左右各半。
|
||||||
|
- 改动:
|
||||||
|
- 原来各自独占整行的两个 panel,合并到一个 `<a-row>` 中,各占 `:md="12"`。
|
||||||
|
- 今日数据概况栅格从 4 列改为 2 列(`grid-template-columns: repeat(2, 1fr)`),适配半宽空间。
|
||||||
|
- 基本信息 `a-descriptions` 列数改为 `:column="1"`,半宽下单列更不拥挤。
|
||||||
|
- 基本信息(左)和今日数据概况(右)位置对调,基本信息在左、今日数据概况在右。
|
||||||
|
|
||||||
|
### 刷新统计数据修复
|
||||||
|
|
||||||
|
- 问题:点「刷新数据」按钮提示成功,但实际数据没更新。
|
||||||
|
- 根因:`refreshStatistics` 调用 `statisticsStore.fetchStatistics()` 未传 `forceRefresh=true`,5分钟缓存有效期内直接返回旧数据。
|
||||||
|
- 修复:改为 `fetchStatistics(true)` 强制跳过缓存。
|
||||||
|
|
||||||
|
## README.md 修改
|
||||||
|
|
||||||
|
- 项目名称从"WebSoftAdmin"改为"小程序商城后台管理系统"。
|
||||||
|
- 项目域名更新为 https://shop.websoft.top。
|
||||||
|
- 去掉 CMS 内容管理系统的介绍部分(文章管理、一键排版、媒体库等)。
|
||||||
|
- 更新技术栈版本号对齐 package.json(AntDv 4.x、Vite 8.x、TS 5.x、Pinia 2.x)。
|
||||||
|
- 补充门店/经销商/会员管理等商城核心功能描述,对齐实际 views/shop 目录结构。
|
||||||
122
.workbuddy/memory/2026-07-17.md
Normal file
122
.workbuddy/memory/2026-07-17.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 2026-07-17 工作日志
|
||||||
|
|
||||||
|
## 订单发货功能改进(shopOrder / deliveryModal)
|
||||||
|
|
||||||
|
分析了订单发货弹窗(`src/views/shop/shopOrder/components/deliveryModal.vue`)三种配送方式,按用户确认的改进方案实施了前端改动:
|
||||||
|
|
||||||
|
### 改动文件
|
||||||
|
1. `deliveryModal.vue`(核心)
|
||||||
|
- 新增「发货门店」下拉(复用 `listShopStore`),选中后自动回填:快递配送→发货人=门店名/电话=门店电话/地址=门店地址;商家送货→地址=门店地址。
|
||||||
|
- 商家送货(`deliveryType===2`)新增「配送店员」二级联动下拉(复用 `listShopStoreUser({storeId})`),选中店员自动回填发货人=店员名/联系方式=店员电话。
|
||||||
|
- 校验:发货门店必填(`deliveryType!==1`);配送店员必填(仅 `deliveryType===2`)。
|
||||||
|
- 提交时附带 `sendStoreId` / `deliveryUserId`。
|
||||||
|
2. `api/shop/shopOrder/model/index.ts`:新增 `sendStoreId`、`deliveryUserId` 字段(为后端落库铺路)。
|
||||||
|
3. `orderInfo.vue`:发货信息卡片增加「发货门店」「配送店员」展示(可选链,后端返回 `storeName`/`deliveryUserName` 后自动生效,否则显示「未填写」)。
|
||||||
|
|
||||||
|
### 关键结论 / 待后端配合
|
||||||
|
- 门店、店员 list API 已存在,前端改动零后端依赖即可落地交互。
|
||||||
|
- **后端缺口(需补充)**:`shop_order` 表需新增 `send_store_id`、`delivery_user_id` 字段存储门店/店员关联;否则下拉选了也存不进去。
|
||||||
|
- 登录用户 `User` 模型无 `storeId`,无法默认锁定当前门店,门店下拉需用户手动选择。
|
||||||
|
- 原「发货地址模板」需求因无现成后端接口,改用「门店作为发货地址模板」方案满足(复用门店 name/phone/address)。
|
||||||
|
|
||||||
|
## 配送方式排序调整
|
||||||
|
|
||||||
|
- 修改 `deliveryModal.vue` 配送方式 radio 按钮顺序:快递配送→商家送货→无需发货(原:快递配送→无需发货→商家送货),仅调换模板中 radio 排列顺序,value 值不变。
|
||||||
|
|
||||||
|
## 后端 Java 改动(guilixu-java,分两轮)
|
||||||
|
|
||||||
|
### 第一轮:shop_order 主表
|
||||||
|
- `ShopOrder.java` 实体加持久化字段 `sendStoreId`、`deliveryUserId`(→`send_store_id`/`delivery_user_id`),及虚拟展示字段 `sendStoreName`/`deliveryUserName`(exist=false)。
|
||||||
|
- `ShopOrderMapper.xml` 的 `selectSql` 加 `LEFT JOIN shop_store f` / `LEFT JOIN shop_store_user g` 并 SELECT `f.name as sendStoreName, g.name as deliveryUserName`,让订单详情联带回门店/店员名称。
|
||||||
|
- 前端 `orderInfo.vue` 展示路径从 `form.shopOrderDelivery.xxx` 改为 `form.sendStoreName`/`form.deliveryUserName`(名称落在订单主对象上)。
|
||||||
|
- SQL:`ALTER TABLE shop_order ADD send_store_id INT, delivery_user_id INT;`
|
||||||
|
|
||||||
|
### 第二轮:shop_order_delivery 发货单表(用户要求"也写进发货记录表")
|
||||||
|
- `ShopOrderDelivery.java` 实体加持久化字段 `sendStoreId`、`deliveryUserId`。
|
||||||
|
- `ShopOrderController.update()` 的发货写入分支(无需物流分支 deliveryMethod=20、实物快递分支 deliveryMethod=30、以及补录运单号分支)新建 `ShopOrderDelivery` 时都 `setSendStoreId`/`setDeliveryUserId`(值取自 `shopOrder` 对象,兜底取 `shopOrderNow`)。
|
||||||
|
- 说明:前端发货只调 `PUT /shop/shop-order`,该接口内部 `new ShopOrderDelivery()` 并 save 落 `shop_order_delivery`;主表靠 `updateById(shopOrder)` 自动落库,发货单表需显式 set 字段。
|
||||||
|
- SQL:`ALTER TABLE shop_order_delivery ADD send_store_id INT, delivery_user_id INT;`
|
||||||
|
- 验证:`vue-tsc` 前端零报错;Java 用 `./mvnw -o compile` 后台验证中。
|
||||||
|
|
||||||
|
## Taro 商家端 storeId=0 排查(guilixu-taro,跨项目)
|
||||||
|
- 用户反馈 orders 页请求 `/shop/shop-store-user?storeId=0`。排查结论:
|
||||||
|
- `guilixu-taro/src/pages/store/orders/index.tsx` **未调用** `shop-store-user`;全项目 `listShopStoreUser` 仅定义、无调用方。该请求非当前源码发出,应为线上旧版 H5 构建。
|
||||||
|
- **当前门店 storeId 的真实来源**:`getMyClerk()`(`GET /shop/shop-store-user/my`)返回的 `ShopStoreUser` 对象,其 `.storeId` 字段(shop_dealer.id)才是门店 id。注意 `user.tsx:103` 用 `storeInfo`(=getMyClerk 结果)判断"是否显示门店中心",所以那里就能拿到 `storeId = storeInfo.storeId`。
|
||||||
|
- **易错点**:`ShopStoreUser` 有 `id`(店员自身主键)和 `storeId`(门店 id)两个字段,二者不同。误用 `storeInfo.id` 当门店 id 是 bug。`booking/index.tsx:186` 的 `storeId: storeInfo!.id || 0` 中 `storeInfo` 恰为 `ShopStore` 类型故 `.id` 碰巧正确,但 `|| 0` 兜底在未加载时仍会变 0。
|
||||||
|
- **根因模式**:项目无全局门店/店员上下文,`storeInfo` 是各页面局部 useState(user/booking/center 各一份)。需在 storeId 就绪前发请求 + `|| 0` 兜底 → `storeId=0`。
|
||||||
|
- **已实施全局 `useClerk`**(用户确认后开工):
|
||||||
|
- 新增 `src/contexts/ClerkContext.tsx`:`ClerkProvider` + `useClerkContext`。登录态(`useUserContext().isLoggedIn`)变化即拉取/清空 `getMyClerk()`;对外暴露 `clerk`(ShopStoreUser|null)、`storeId`(=clerk?.storeId)、`loading`、`refreshClerk`、`clearClerk`。
|
||||||
|
- 新增 `src/hooks/useClerk.ts`:镜像 `useUser` 的"优先 Context、未包裹则降级本地"范式,文件头注释写明用法与 `storeId>0` 守卫约定。
|
||||||
|
- `src/app.tsx`:在 `UserProvider` 内挂载 `<ClerkProvider>`(使其能响应登录/登出)。
|
||||||
|
- 重构 `src/pages/user/user.tsx` 与 `src/pages/store/center/index.tsx`:移除各自的局部 `storeInfo`/`getMyClerk` 调用,改为 `const { clerk: storeInfo, refreshClerk } = useClerk()`。
|
||||||
|
- 补全 `src/api/shop/shopStoreUser/model/index.ts` 的 `ShopStoreUser` 模型:`getMyClerk`(/my) 联表返回的 `name`/`phone`/`avatar`/`storeName` 补入(原模型缺失,旧代码靠 `useState<any>` 掩盖)。
|
||||||
|
- **验证**:裸 `tsc --noEmit --types webpack-env` 全量检查——新建/改动的 `ClerkContext.tsx`/`useClerk.ts`/`app.tsx`/`user.tsx`/`center/index.tsx` 及模型 **零新增报错**;`center` 页仅剩 4 个**既有**报错(75 的 `finally`、82 的 `applyStatus`、89 的 `total`、106 的 `Option`,均在未改动的 `loadBadgeCounts`/`subscribe` 原函数里,属项目原有类型问题 / 裸 tsc 的 lib 配置 artifact)。注意裸 `tsc` 默认会因 tsconfig `types:["@tarojs/taro"]`+`typeRoots` 报 `TS2688`,需用 `--types webpack-env` 覆盖才能跑真实业务检查。
|
||||||
|
- **未动**:`booking/index.tsx` 的 `storeInfo` 是用户所选门店(ShopStore,来自 `getShopStore(id)`/选择),与店员所属门店是两回事,不在本次范围(其 `storeId: storeInfo!.id || 0` 属不同接口的兜底问题)。orders 页现可随时从 `useClerk().storeId` 取当前门店 id。
|
||||||
|
|
||||||
|
## Taro 商家端 orders 页「选择发货人员」发货功能(guilixu-taro)
|
||||||
|
- 用户截图显示线上旧版 orders 页有「选择发货人员」弹窗且请求 `?storeId=0` 显示"暂无可选择的店员"。当前源码 `orders/index.tsx` 原本**没有**该功能(只有确认收款/确认完成),确认是旧构建。
|
||||||
|
- 在 `src/pages/store/orders/index.tsx` 新增「确认发货」功能:
|
||||||
|
- 引入 `useClerk()` 取 `storeId`(杜绝 storeId=0)、`listShopStoreUser`、`ShopStoreUser` 类型。
|
||||||
|
- `OpType` 增加 `'deliver'`;`getOrderActions` 中「待发货」订单(`payStatus && deliveryStatus===10`)显示"确认发货"按钮。
|
||||||
|
- `openModal('deliver')` 时调 `loadClerkList()`:`storeId>0` 才 `listShopStoreUser({storeId})`,否则 toast 提示门店未就绪、不发请求。
|
||||||
|
- 弹窗内新增店员单选列表(selectedClerk),deliver 模式隐藏凭证上传区。
|
||||||
|
- 提交发货:`deliveryType=2`(商家送货)、`deliveryStatus=20`(待收货)、`deliveryTime`、`sendName/sendPhone`(店员)、`sendStoreId=storeId`、`deliveryUserId=店员id` → `PUT /shop/shop-order`。
|
||||||
|
- 后端已在上一轮把 `sendStoreId`/`deliveryUserId` 落 `shop_order` + `shop_order_delivery`,此处字段对齐。
|
||||||
|
- 验证:`tsc --noEmit --types webpack-env` 总报错仍 387(零新增);orders 页仅剩 2 个既有报错(165 `fetchLatestOrders` 返回类型、169 `count` 隐式 any,均在未改的 `useNewOrderDetector` 调用块)。
|
||||||
|
|
||||||
|
---
|
||||||
|
## ⚠️ 重大纠正:正确项目是 xinlong-shop-taro,不是 guilixu-taro
|
||||||
|
- 用户最终明确:商家端发货/门店相关功能在 **`/Users/gxwebsoft/VUE/xinlong-shop-taro`**,前面整段记的 `guilixu-taro` 改动**全改错项目**了。
|
||||||
|
- **已在 guilixu-taro 还原**(git 仓库):`git checkout --` 还原 5 个跟踪文件(model/index.ts、app.tsx、center/index.tsx、orders/index.tsx、user.tsx);`rm` 删除 2 个新建文件(contexts/ClerkContext.tsx、hooks/useClerk.ts)。未动的 `pages/index/index.scss` 是仓库原有未提交改动,保留。
|
||||||
|
- **xinlong-shop-taro 才是真项目,且早已内置发货功能**:`orders/index.tsx` 已有 `OpType='ship'` 的「发货」+ `saveShopOrderDelivery` + `listShopStoreUser` + 发货弹窗;`user.tsx` 已有 `getMyClerk()` 取 `storeInfo`(含 `.storeId`)用于"门店中心"判断。
|
||||||
|
- **真正的 `storeId=0` bug 根因**:`xinlong-shop-taro/src/pages/store/orders/index.tsx:465` 的 `openShipModal` 调 `listShopStoreUser()` **没传 storeId**(默认 0),即用户最初报的 `?storeId=0`。
|
||||||
|
- **已修(xinlong-shop-taro)**:`openShipModal` 内先 `getMyClerk()` 取 `storeId`,`listShopStoreUser({storeId})` 拉本门店店员;并默认选中 `userId === 当前登录人 userId` 的店员(`setSelectedClerkId(match.id)`)。`tsc --noEmit --types webpack-env` 该文件零新增报错(仅 162/208/212 三个项目既有错误,均在我未改动区域)。
|
||||||
|
- **教训**:跨项目时先用 glob/find 在多个候选项目里确认"哪个项目有该功能源码",再动手;不要凭域名/习惯默认锁定一个项目。本次因 `guilixu-taro` 与 `xinlong-shop-taro` 命名相近、且前者恰有 `getMyClerk` 等同名 API 而误判。
|
||||||
|
|
||||||
|
## 发货弹窗文案优化(deliveryModal.vue)
|
||||||
|
- 用户反馈"快递配送时发货人/联系人选门店是否不合理",分析结论:业务逻辑正确(发货人=寄件方=门店),问题在文案未区分"寄件方"与"承运方"。
|
||||||
|
- 改动 `src/views/shop/shopOrder/components/deliveryModal.vue`:
|
||||||
|
- 「发货门店」标签动态化:快递配送→「发货门店」,商家送货→「配送门店」(placeholder 同步动态)。
|
||||||
|
- 「发货人」标签动态化:快递配送→「寄件人」,商家送货→「发货人」(联系方式标签同理)。
|
||||||
|
- 提示文案动态化:快递配送说明"寄件人即为发货门店",商家送货说明"回填配送店员信息"。
|
||||||
|
- 快递配送时在寄件人字段下方加小字说明"寄件人信息将同步至快递面单"。
|
||||||
|
- 验证规则的 reject 文案同步动态化。
|
||||||
|
- 不涉及数据结构/接口变更,纯前端文案优化。
|
||||||
|
|
||||||
|
## 修复小程序订单详情页配送方式判断逻辑(xinlong-shop-taro)⚠️ 注意正确项目
|
||||||
|
|
||||||
|
- **正确项目**:`/Users/gxwebsoft/VUE/xinlong-shop-taro`(非 guilixu-taro)
|
||||||
|
- 文件:`src/pages/order/detail.tsx`
|
||||||
|
- 问题:原代码 `isExpress = order.deliveryType === 0 || undefined`,无法区分 deliveryType=2(商家送货)
|
||||||
|
- 修复:
|
||||||
|
1. 新增 `DELIVERY_TYPE_MAP`:`0=快递配送, 1=自提, 2=商家送货`
|
||||||
|
2. 修正 `isExpress` 判断:`deliveryType === 0 || 2 || undefined` → 快递/商家送货都显示收货人信息
|
||||||
|
3. 收货信息卡片:商家送货显示🛵图标+配送门店名,快递配送显示📦图标
|
||||||
|
4. 订单信息区新增「配送方式」字段展示(使用 DELIVERY_TYPE_MAP)
|
||||||
|
- deliveryType 值范围来自管理后台 deliveryModal.vue:0=快递配送, 1=无需发货(自提), 2=商家送货
|
||||||
|
- ⚠️ 初次改错了项目(guilixu-taro),已用 git checkout 还原,正确改动在 xinlong-shop-taro
|
||||||
|
|
||||||
|
## 方案A:线下付款先发货后结款(三端同步改动)
|
||||||
|
|
||||||
|
### 后端(guilixu-java)
|
||||||
|
1. `ShopOrderServiceImpl.confirmOfflinePayment()`:移除 `.eq(ShopOrder::getOrderStatus, 0)` 约束,允许已发货订单确认收款。新增逻辑:若 `deliveryStatus=20`(已发货),确认收款后自动 `setOrderStatus=1` 完成订单。
|
||||||
|
2. `ShopOrderController.confirmOfflinePayment()`:更新 `@Operation` 描述和成功消息(不再固定写"进入待发货状态")。
|
||||||
|
3. `ShopOrderService.java`:更新 Javadoc 描述。
|
||||||
|
4. **注意**:Controller 的 `PUT /shop/shop-order`(update)接口发货时**不校验 payStatus**,仅判断 `deliveryStatus 10→20`,所以未付款的线下订单可以直接通过 update 接口发货(前端改 deliveryStatus 即可)。
|
||||||
|
|
||||||
|
### 管理后台(guilixu-admin)
|
||||||
|
1. `src/views/shop/shopOrder/index.vue`:线下付款未付款订单(payType=9, payStatus=0, orderStatus=0)分两种情况:
|
||||||
|
- `deliveryStatus=10`:同时显示「确认收款」+「发货」+「关闭」
|
||||||
|
- `deliveryStatus=20`:仅显示「确认收款」(先发货后结款,已发货状态)
|
||||||
|
2. `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`:
|
||||||
|
- 新增 `deliveryStatus` 表单字段 + `alertMessage` 计算属性:已发货时提示"确认后订单将自动完成",未发货时提示"进入待发货状态"。
|
||||||
|
- 成功消息动态化:`deliveryStatus===20 ? '已自动完成' : '已进入待发货状态'`
|
||||||
|
3. `src/views/shop/shopOrder/components/orderInfo.vue`:
|
||||||
|
- 支付状态标签:`payType=9 && payStatus=0 && deliveryStatus=20` → 蓝色「已发货待收款」
|
||||||
|
- `loadSteps()` 步骤条:线下付款未付款但已发货时,步骤条显示「下单→待收款→已发货」而非停留在「下单」
|
||||||
|
|
||||||
|
### 小程序端(xinlong-shop-taro)
|
||||||
|
1. `src/pages/store/orders/index.tsx`:
|
||||||
|
- `getStatusText()`:新增「已发货待收款」状态(`payType===9 && !payStatus && deliveryStatus===20`)
|
||||||
|
- `getOrderActions()`:发货按钮条件放宽——线下付款未付款(`payType===9`)也能显示发货按钮(支持先发货后结款)
|
||||||
|
- `OP_DESC`:确认收款描述移除"订单将进入待发货状态"(已不是唯一路径)
|
||||||
49
.workbuddy/memory/2026-07-18.md
Normal file
49
.workbuddy/memory/2026-07-18.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 2026-07-18 工作日志
|
||||||
|
|
||||||
|
## 手机号登录改用 loginBySuperAdminSms 接口
|
||||||
|
|
||||||
|
### 改动文件
|
||||||
|
1. `src/api/passport/login/index.ts`
|
||||||
|
- 新增 `loginBySuperAdminSms()` 方法,调用 `/loginBySuperAdminSms` 接口
|
||||||
|
- 保留原有 `loginBySms()` 方法(其他页面如 register/dealer 仍在使用)
|
||||||
|
- 新方法逻辑与 `loginBySms` 一致:成功后 setToken + 存 TenantId/UserId
|
||||||
|
|
||||||
|
2. `src/views/passport/login/index.vue`
|
||||||
|
- import 替换:`loginBySms` → `loginBySuperAdminSms`
|
||||||
|
- `onLoginBySms()` 函数中调用替换:`loginBySms(form)` → `loginBySuperAdminSms(form)`
|
||||||
|
- 多租户选择逻辑(`listAdminsByPhoneAll`)保持不变
|
||||||
|
|
||||||
|
### 注意
|
||||||
|
- 仅改了管理后台登录页,注册页面(register/dealer)仍用 `loginBySms`
|
||||||
|
|
||||||
|
## 账号登录合并邮箱/用户ID登录
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
账号登录输入框同时支持「邮箱」和「用户ID」:邮箱 → `https://server.websoft.top/api/loginByEmail`,纯数字 → `https://server.websoft.top/api/loginByUserId`。
|
||||||
|
|
||||||
|
### 改动文件
|
||||||
|
1. `src/api/passport/login/model/index.ts`
|
||||||
|
- `LoginParam` 新增 `email?: string` 和 `userId?: string | number`
|
||||||
|
|
||||||
|
2. `src/api/passport/login/index.ts`
|
||||||
|
- 新增 `loginByEmail()` / `loginByUserId()`,逻辑同 `login()`(setToken + 存 TenantId/UserId)
|
||||||
|
|
||||||
|
3. `src/views/passport/login/index.vue`
|
||||||
|
- 表单字段 `userId` → `account`(新增 `LoginForm = User & { account?: string }` 类型)
|
||||||
|
- 输入框 placeholder 改为「邮箱 / 用户ID」,form-item name 改为 `account`
|
||||||
|
- 校验规则 `userId` → `account`
|
||||||
|
- `submit()` 用正则判断邮箱(`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`) / 纯数字(`/^\d+$/`),路由到对应接口;格式不符提示「请输入正确的邮箱或用户ID」
|
||||||
|
- 移除未使用的 `login` 导入
|
||||||
|
|
||||||
|
### 注意
|
||||||
|
- 外部接口用绝对 URL(`https://server.websoft.top/api/...`),未走 SERVER_API_URL 代理
|
||||||
|
- `vue-tsc` 全量检查仍有大量历史错误(ByteMdEditor、shopActivity 等),本次改动无新增错误
|
||||||
|
|
||||||
|
## 登录页右上角切换图标互换
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
`src/views/passport/login/index.vue` 右上角扫码/手机切换按钮,图标原本与「目标模式」相反,需互换。
|
||||||
|
|
||||||
|
### 改动
|
||||||
|
- 第 65-66 行:`v-if="loginType === 'scan'"` 显示 `QrcodeOutlined` → 改为 `MobileOutlined`;`v-else` 显示 `MobileOutlined` → 改为 `QrcodeOutlined`
|
||||||
|
- 即:扫码模式下显示手机图标(切回账号/手机),账号/手机模式显示二维码图标(切到扫码)
|
||||||
24
.workbuddy/memory/2026-07-23.md
Normal file
24
.workbuddy/memory/2026-07-23.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 2026-07-23
|
||||||
|
|
||||||
|
## 订单导出字段补齐(物流部发货用)
|
||||||
|
- 文件:`src/views/shop/shopOrder/components/search.vue` 的 `handleExport`
|
||||||
|
- 原来只导出 8 列(订单编号/订单标题[实际是 comments]/买家姓名/手机/实付金额/支付方式/付款时间/下单时间),且缺物流发货关键字段。
|
||||||
|
- 新增并补齐到 15 列:收货人、联系电话、收货地址、配送方式(0快递/1自提/2商家送货)、商品明细(商品名x数量)、商品总数量、实付金额、配送时间、自提码、发货/自提店铺、买家备注、支付方式、发货状态(10未发/20已发/30部分)、下单时间。
|
||||||
|
- 新增辅助函数:`getDeliveryTypeLabel` / `getDeliveryStatusLabel` / `getGoodsSummary` / `getShopName`。
|
||||||
|
- 注意:枚举值与 `deliveryModal.vue` 一致(deliveryType 0快递 1无需发货/自提 2商家送货)。
|
||||||
|
- 环境提示:本机 Grep 工具因缺 libpcre2 不可用,搜代码改用 Python 脚本遍历。
|
||||||
|
|
||||||
|
## 导出跟随当前标签筛选(避免混入非待发货订单)
|
||||||
|
- index.vue:新增 `statusFilterForExport` computed = `getStatusFilterByTab(activeKey)`,并通过 `:status-filter` 传给 `<search>`。
|
||||||
|
- search.vue:给 props 加 `statusFilter?`,导出请求改为 `listShopOrder({ ...where, statusFilter: props.statusFilter })`。
|
||||||
|
- 效果:导出所见即所得——切到「待发货」标签再导出,只导出待发货;其它标签同理。全部标签下 statusFilter=undefined 不额外过滤。
|
||||||
|
|
||||||
|
## 修复:导出的商品明细为空
|
||||||
|
- 根因:`listShopOrder`(非分页列表接口)不返回 `orderGoods` 嵌套;页面表格/详情用的是 `pageShopOrder` 和单独的 `listShopOrderGoods` 取明细。
|
||||||
|
- 修复:导出时在 `search.vue` 中用 `Promise.all` 按 `orderId` 逐个调 `listShopOrderGoods({orderId})` 补齐商品明细,再写入「商品明细」列。
|
||||||
|
- 商品明细格式升级为 `商品名(规格) x数量`,规格取自 ShopOrderGoods.spec,便于物流发对型号。
|
||||||
|
- 单订单请求失败时 `.catch(() => [])` 兜底,不影响其它订单。
|
||||||
|
|
||||||
|
## 修复:商品总数量计算不对
|
||||||
|
- 根因:「商品总数量」列原本用订单级 `d.totalNum`,但 `listShopOrder` 列表接口未返回该字段(与 orderGoods 同理),导致算错/为空。
|
||||||
|
- 修复:新增 `getGoodsTotalNum(goods)`,对 `listShopOrderGoods` 取到的明细 `goodsList[idx]` 逐行 `totalNum` 求和。改动后「商品总数量」与「商品明细」列同源,必然对得上。
|
||||||
81
.workbuddy/memory/2026-07-24.md
Normal file
81
.workbuddy/memory/2026-07-24.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# 2026-07-24 工作日志
|
||||||
|
|
||||||
|
## shopGoods 商品列表排序机制分析
|
||||||
|
- 现状:列表「排序号」列(`src/views/shop/shopGoods/index.vue` 约 273-279 行)只做展示,**未启用 `sorter`**,列表默认也不按 `sortNumber` 排序(默认排序由后端决定,前端 `orders` 仅来自「创建时间」列的排序)。
|
||||||
|
- 编辑表单里「排序号」是 `a-input-number`(0-9999,默认 100),且在 `rules` 里是 **必填**。
|
||||||
|
- 已存在类似的布尔开关 `recommend`(列表内联切换,index.vue 约 47-58 行),「置顶」可复用同一 UI 模式。
|
||||||
|
- 结论倾向:纯排序号在商品多时难用,建议「置顶开关 + 少量置顶项内排序 + 其余按默认(创建时间/销量)」的混合方案,而非完全替换。
|
||||||
|
|
||||||
|
## 已实现:方案 B(商品置顶功能)
|
||||||
|
- 数据模型 `src/api/shop/shopGoods/model/index.ts`:新增 `isTop?: number`(置顶 1/不置顶 0)。
|
||||||
|
- 列表 `src/views/shop/shopGoods/index.vue`:
|
||||||
|
- 新增「置顶」列,列表内联一键切换(复用 recommend 的 CheckOutlined/CloseOutlined 模式),新增 `onTop` 处理函数。
|
||||||
|
- 「排序号」列开启 `sorter: true`(之前只展示、不可排序)。
|
||||||
|
- 后端迁移脚本 `add-is-top-shop-goods.sql`:新增 `is_top` TINYINT 字段;分页默认排序需改为 `ORDER BY is_top DESC, sort_number ASC, create_time DESC`(后端不在本仓库,需手动改 page 接口 ORDER BY / 放开 orders 白名单)。
|
||||||
|
- 注意:置顶真正"排到最前"依赖后端默认排序改动;点击列排序也需后端放开 is_top / sort_number。
|
||||||
|
|
||||||
|
## 后端改动(/Users/gxwebsoft/JAVA/guilixu-java,Spring Boot + MyBatis-Plus)
|
||||||
|
- `entity/ShopGoods.java`:新增 `private Integer isTop;`(映射列 is_top,沿用 camelCase→下划线策略;mapper 用 SELECT a.*,无需改 XML)。
|
||||||
|
- `service/impl/ShopGoodsServiceImpl.java`:
|
||||||
|
- `pageRel` 与 `listRel` 默认排序由 `sort_number asc, create_time desc` 改为 `is_top desc, sort_number asc, create_time desc`(置顶优先,置顶内按排序号,其余按创建时间倒序)。
|
||||||
|
- `collectFromUrl` 采集默认值补 `goods.setIsTop(0)`(与 recommend 一致)。
|
||||||
|
- 排序字段无白名单:`PageParam` 直接按列名拼 ORDER BY,前端点「置顶」「排序号」列排序无需额外放开。
|
||||||
|
- DB 仍需执行 `add-is-top-shop-goods.sql`(在 admin 仓库根目录):`ALTER TABLE shop_goods ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0`。
|
||||||
|
- 副作用提醒:默认排序改为置顶优先会影响所有调用 pageRel/listRel 的端(含小程序商品列表),置顶商品会浮到最前——通常正是期望行为。
|
||||||
|
|
||||||
|
## shopGoods 列表:商品编号移到名称下方
|
||||||
|
- `src/views/shop/shopGoods/index.vue`:删除独立的「编号」列(原 `code` 列),改为在「商品」列的名称下方显示「编号:{{ record.code }}」(灰色小字),节省一列横向空间。无 code 时不渲染该行。
|
||||||
|
|
||||||
|
## shopGoods 列表:三个价格合并为一列 + 与编辑弹窗对齐
|
||||||
|
- 原「市场价/价格/会员价」三列(字段 salePrice/price/dealerPrice)合并为单个「价格」列(`key: 'priceGroup'`,width 150)。
|
||||||
|
- 单元格内堆叠显示,顺序与命名对齐编辑弹窗(`shopGoodsEdit.vue` 基本信息 Tab):价格(price,加粗头条)→ 市场价(salePrice)→ 会员价(dealerPrice);空值显示 `-`。
|
||||||
|
- 列表展示用 priceGroup 自定义 `#bodyCell` 渲染;导出 Excel(`handleExport`)仍保留三列独立价格,未改动。
|
||||||
|
|
||||||
|
## 商品分类(ShopGoodsCategory)多级分类调研
|
||||||
|
- 分类管理页 `/shop/shopGoodsCategory` 本身**支持**多级:模型有 parentId/children,编辑弹窗用 a-tree-select(toTreeData) 不限层级,列表 computeDepth 递归缩进。
|
||||||
|
- 但商品端选分类的级联组件 `src/components/SelectGoodsCategory/index.vue` 的 formatData/filterData 只映射 2~3 级;shopGoodsEdit 的 `chooseGoodsCategory` 写死 `value[1].value`/`value[0].label`(仅 2 级),3 级及以上会选错/选不中。
|
||||||
|
- 后端 `/shop/shop-goods-category` 是否限层级需查后端仓库确认(本仓库看不到)。
|
||||||
|
|
||||||
|
## 新增:忘记密码(找回密码)功能
|
||||||
|
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`(已验证可用)。
|
||||||
|
- 新增页面 `src/views/passport/forget/index.vue`:复用登录页视觉风格,支持「邮箱找回 / 手机找回」两种 Tab,含验证码 60s 倒计时、密码强度校验(≥8 位且含字母+数字)、两次密码一致性校验。
|
||||||
|
- 路由:在 `src/router/routes.ts` 启用 `/forget`(登录页「忘记密码」链接 `push('/forget')` 原本就指向它,路由此前被注释)。
|
||||||
|
- 新增 API:
|
||||||
|
- `src/api/passport/login/index.ts`:`sendEmailCaptcha(data,{skipTenant})` → POST /sendEmailCaptcha;`sendSmsCaptchaByAdmin(data,{skipTenant})` → POST /sendSmsCaptchaByAdmin。
|
||||||
|
- `src/api/layout/index.ts`:`resetPassword(data)` → POST /resetPassword(带 X-Tenant-Skip:1);`src/api/layout/model/index.ts` 新增 `ResetPasswordParam`。
|
||||||
|
- 关键改动:`src/utils/request.ts` 请求拦截器在请求头含 `X-Tenant-Skip` 时跳过注入 TenantId/CompanyId/MerchantId/Domain(登录前找回密码场景,避免串租户)。`vite build --outDir dist-verify` 验证通过(真实 `vite build` 因沙箱 safe-delete 守卫删 dist 拦截,非代码问题)。
|
||||||
|
- 后端同址 server.websoft.top/api,与 website-admin 共用一套找回密码接口。
|
||||||
|
|
||||||
|
## 优化:忘记密码页发送验证码前 3 次免图形验证
|
||||||
|
- 修改 `src/views/passport/forget/index.vue`:引入 `getCaptcha`,新增图形验证码弹窗(与 `register2/index.vue` 风格一致)。
|
||||||
|
- 用 `localStorage` 按 `forget_send_count_${activeTab}_${account}` 记录每个邮箱/手机号的发送次数。
|
||||||
|
- 同一个账号前 3 次点击「获取验证码」直接调用 `sendEmailCaptcha`/`sendSmsCaptchaByAdmin`;第 4 次起弹出图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
|
||||||
|
- 失败时自动刷新图形验证码;弹窗关闭时重置 `codeLoading`。
|
||||||
|
- 新增 i18n 键 `login.forgetPage.captchaTitle`、`captchaPlaceholder`、`sendNow`(zh_CN/en)。
|
||||||
|
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-captcha` 编译通过。
|
||||||
|
|
||||||
|
## 优化:登录页手机号验证码前 3 次免图形验证
|
||||||
|
- 修改 `src/views/passport/login/index.vue`:手机号登录 Tab 下,「发送验证码」按钮由直接弹窗改为先计数判断。
|
||||||
|
- 用 `localStorage` 按 `login_sms_send_count_${phone}` 记录每个手机号的发送次数。
|
||||||
|
- 同一个手机号前 3 次直接调用 `sendSmsCaptcha`;第 4 次起弹出原有图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
|
||||||
|
- 失败时自动刷新图形验证码;补充了 `.catch`/`.finally`,避免发送异常时按钮一直 loading。
|
||||||
|
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-login` 编译通过。
|
||||||
|
|
||||||
|
## 修复:邮箱登录图形验证码未校验
|
||||||
|
- **问题**:`src/views/passport/login/index.vue` 第 651-555 行的验证码比对逻辑被注释掉,导致用户输入任意非空字符串即可通过前端校验。
|
||||||
|
- **修复**:取消注释 `form.code?.toLowerCase() !== text.value` 的校验块,恢复「输入错误则提示"验证码错误"并刷新验证码」的逻辑。
|
||||||
|
- **说明**:当前后端 `/captcha` 接口返回明文答案 `data.text`,前端直接比对即可。后续若后端改为返回 key 模式,需同步改为传 key 给后端校验。
|
||||||
|
|
||||||
|
## 新增:手机号登录多租户选择
|
||||||
|
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`。
|
||||||
|
- `src/api/passport/login/model/index.ts`:
|
||||||
|
- `LoginResult` 新增 `tenants?: TenantOption[]`;
|
||||||
|
- 新增 `TenantOption` 类型与 `LoginOutcome` 联合类型(`ok` / `selectTenant`)。
|
||||||
|
- `src/api/passport/login/index.ts`:
|
||||||
|
- 新增 `saveLoginState` 统一保存 token、TenantId、UserId;
|
||||||
|
- `loginBySuperAdminSms` 改为返回 `LoginOutcome`,检测 `data.tenants` 时返回 `selectTenant`;
|
||||||
|
- 新增 `loginBySelectTenant({ phone, code, tenantId, remember })` 二次登录接口。
|
||||||
|
- `src/views/passport/login/index.vue`:
|
||||||
|
- `onLoginBySms` 根据 `outcome.status` 处理:单租户直接登录成功;多租户弹出「请选择要登录的租户」弹窗。
|
||||||
|
- 新增弹窗与 `onSelectTenant`:选择租户后调用 `loginBySelectTenant` 完成二次登录并跳转。
|
||||||
|
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-tenant` 编译通过。
|
||||||
16
.workbuddy/memory/2026-07-25.md
Normal file
16
.workbuddy/memory/2026-07-25.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 2026-07-25 工作日志
|
||||||
|
|
||||||
|
## CMS 模块复用方案分析(mp-java vs guilixu-java)
|
||||||
|
- 结论:**guilixu 应直接调用 mp-java 的 cms(modules 服务),不要整体复制到 guilixu-java。**
|
||||||
|
- 证据:guilixu 的 152 个 cms 类是 mp-java 235 类的子集分叉且落后 1.5 个月;前端 cms 接口已全部走 `MODULES_API_URL`(指向 mp-java),guilixu-java 的 cms 包是死代码;"整体复制"因缺 `project` 模块无法编译。
|
||||||
|
- 产出报告:`docs/cms-module-reuse-analysis.md`
|
||||||
|
- 已同步架构结论到 MEMORY.md(Java 后端架构 / MODULES_API_URL 模式)
|
||||||
|
|
||||||
|
## 商品分类列表:二级/三级前加树形连接线(方案A)
|
||||||
|
- 文件:`src/views/shop/shopGoodsCategory/index.vue`
|
||||||
|
- 需求:用户要二级/三级分类前加图标区分层级,先看 mockup 选方案,选了「方案A 树形连接线」。
|
||||||
|
- 实现:移除原 `a-space` 的 `paddingLeft` 纯缩进,改为用 SVG 绘制树形连接线(`renderTreePrefix` 函数)。
|
||||||
|
- 新增 `nodeMap`、`isLastMap`,`buildTreeMeta` 在 datasource 加载时构建(是否末位子节点)。
|
||||||
|
- 连接线逻辑:祖先节点非末位→贯穿引导竖线;末段→竖线+横线拐角(末位用 └,否则 ├)。每级宽 20px,颜色 #d0d0d0。
|
||||||
|
- 顶级(depth=0)不画;二级/三级按祖先链回溯绘制。缩进完全由 SVG 宽度承担。
|
||||||
|
- 验证:vite 模块编译通过(localhost:5580 dev server 已起)。
|
||||||
24
.workbuddy/memory/2026-07-28.md
Normal file
24
.workbuddy/memory/2026-07-28.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# 2026-07-28
|
||||||
|
|
||||||
|
## 订单列表操作列分隔线对齐 shopGoods
|
||||||
|
- 文件:`src/views/shop/shopOrder/index.vue` 的 `column.key === 'action'` 操作列模板。
|
||||||
|
- 改动:原本操作列按钮只用 `<a-space>` 包裹、无分隔线;现按 `shop/shopGoods` 的写法,在「详情」与各条件块之间、以及块内多个按钮之间加入 `<a-divider type="vertical" />`。
|
||||||
|
- 分隔线放在各 `<template v-if>` 条件块内部,仅当对应操作出现时才渲染,避免多余竖线。
|
||||||
|
|
||||||
|
## 订单列表商品明细展开/收起(方案一已落地)
|
||||||
|
- 文件:`src/views/shop/shopOrder/index.vue`。
|
||||||
|
- 需求:商品明细较多时列表行被撑高,加「展开/收起」。采用方案一(单元格内展开),已确认执行。
|
||||||
|
- 实现:
|
||||||
|
- 新增 `expandedOrderIds: ref<Set<string>>`(用 `String(record.orderId)` 作 key,因 orderId 为 number|undefined)。
|
||||||
|
- 新增 `collapsedGoodsCount = 2`、`isOrderGoodsExpanded(record)`、`toggleOrderGoodsExpanded(record)`。
|
||||||
|
- 改写 `column.key === 'orderGoods'` 模板:默认 `record.orderGoods.slice(0, 2)`,超过 2 件显示「展开全部 N 件」,点击切换,展开后文案变「收起」。
|
||||||
|
- 类型检查:`vue-tsc --noEmit` 仅有的 3 处报错(line 2 `$router`/page-header、line 696/734 未用变量)均为项目既有,与本次改动无关。
|
||||||
|
|
||||||
|
## shop/dashboard 运行天数改为取租户创建时间
|
||||||
|
- 文件:`src/views/shop/dashboard/index.vue`。
|
||||||
|
- 需求:/dashboard 的"运行天数"要改用「租户创建时间」而不是订阅生效时间。
|
||||||
|
- 现状调查:
|
||||||
|
- `cms/dashboard`(站点控制台,标题"系统运行天数")已在提交 894592c 改为 `getTenantInfo().createTime`(租户创建时间),已是目标行为。
|
||||||
|
- `shop/dashboard`(商城后台,标签"运行天数")原用 `currentSubscription.startTime`(订阅生效时间)。
|
||||||
|
- 改动:将 shop/dashboard 的 `runDays` computed 改为取 `tenantStore.company?.createTime`(loadData 已 `tenantStore.fetchTenantInfo()` 写入 company)。同步更新顶部数据来源注释。
|
||||||
|
- 数据源说明:`getTenantInfo()` 返回 `Company`,其 `createTime` 字段即租户创建时间(`src/api/system/company/model/index.ts` line 164);`site.ts` 的 `runDays` getter 仍用 `siteInfo.createTime`(站点创建时间),目前 cms/dashboard 已不依赖它。
|
||||||
16
.workbuddy/memory/2026-07-29.md
Normal file
16
.workbuddy/memory/2026-07-29.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 2026-07-29
|
||||||
|
|
||||||
|
## websopy-java:cms 模块数据源路由修复(重要跨项目)
|
||||||
|
- 报错:`cms-template/current` 报 `Table 'db_websopy.cms_website' doesn't exist`;jar 为 `websopy-api-1.5.0.jar`(= `/Users/gxwebsoft/JAVA/websopy-java`)。
|
||||||
|
- 根因:websopy-java 原只有单一数据源,dev/prod/websopy 三个 profile 全指向 `db_websopy`;而 cms 模块(23 张 cms_* 表)实际在 `modules` 库。test profile 本就指向 `modules`(印证 cms 表在 modules)。
|
||||||
|
- 已确认方案 B(用户选「独立库 db_websopy」):多数据源,websopy 自有数据留 `db_websopy`,仅 cms 包走 `modules`。
|
||||||
|
- 改动(在 websopy-java):
|
||||||
|
1. `pom.xml` 增加 `com.baomidou:dynamic-datasource-spring-boot-starter:3.4.1`(匹配 MP 3.4.3.3;**不要用 3.5.x**,其对 MP 3.5,运行期可能 NoSuchMethod)。
|
||||||
|
2. `application-dev/prod/websopy/test.yml` 的单一 `spring.datasource` 改为 `spring.datasource.dynamic`:`primary: websopy`(test 为 `modules`),`datasource.websopy` + `datasource.modules` 双库;modules 用库 `modules`、用户 `modules`、密码 `P7KsAyDXG8YdLnkA`(取自 test profile)。
|
||||||
|
3. 给 `com/gxwebsoft/cms/mapper` 下全部 23 个 Mapper 接口加 `@DS("modules")`(import `com.baomidou.dynamic.datasource.annotation.DS`),仅把 cms SQL 路由到 modules,不影响 websopy 自有表。
|
||||||
|
- 调用链:`cms-template/current` → `CmsWebsiteServiceImpl.getByTenantId()` → `baseMapper.getByTenantId()`(已注解)→ modules。✅
|
||||||
|
- 重新构建:websopy-java 本地无 mvn/docker,需在其环境 `mvn clean package` 或重 build docker 镜像后重启容器(prod profile `SPRING_PROFILES_ACTIVE=prod`)。
|
||||||
|
- ⚠️ 遗留问题(未处理,需确认):
|
||||||
|
- `shop/mapper/xml/ShopGoodsMapper.xml` 第 9 行 `LEFT JOIN cms_navigation`(无 schema 前缀),该 shop 查询走默认 websopy → 若 `cms_navigation` 不在 `db_websopy` 会失败;dynamic-datasource 无法跨单条 SQL 双库,需把 cms_navigation 复制到 db_websopy 或改成 `modules.cms_navigation`/`gxwebsoft_core.cms_navigation` 前缀。
|
||||||
|
- 前提假设:所有 cms_* 表都在 modules(依 test profile 整库连 modules 推断)。若实际只有部分 cms 表在 modules,应改为只给缺失的那几张 mapper 加 @DS。
|
||||||
|
- 跨库写事务:cms 写操作经 @DS 走 modules 独立连接,与 websopy 事务不在一个事务里,写一致性需后续评估。
|
||||||
27
.workbuddy/memory/2026-08-01.md
Normal file
27
.workbuddy/memory/2026-08-01.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# 2026-08-01 工作记录
|
||||||
|
|
||||||
|
## 订单自动取消改造:超时时间改为后台「订单设置」驱动
|
||||||
|
|
||||||
|
### 需求
|
||||||
|
自动取消未付款订单的超时时间,需由管理后台「商城设置 → 订单设置 → 未付款自动关闭」控制;设置为 0 分钟 = 不自动取消。
|
||||||
|
|
||||||
|
### 改造要点(已全部落地)
|
||||||
|
- 后台设置存于 `ShopSetting` 表 `setting_key='shop_order'` 的 JSON 中,字段 `orderAutoCloseMins`(前端 order.vue 已支持保存)。
|
||||||
|
- 新增 `ShopSettingService.getOrderAutoCloseMinutes()`:参照 `getSalesAccumulateType()` 写法,从 `getCategoryValues("order")` 读 `orderAutoCloseMins`;0=禁用(合法返回);负数/读取失败兜底 30(常量 `DEFAULT_AUTO_CLOSE_MINUTES=30`)。
|
||||||
|
- `OrderAutoCancelTask.cancelExpiredOrders()`:先读 DB,`==0` 直接 return(全局禁用,含租户特殊配置 tenantConfigs 一起跳过);`>0` 用它替换原先 YAML 的 `defaultTimeoutMinutes` 传给 `findExpiredUnpaidOrders`。
|
||||||
|
- `OrderConfigProperties.AutoCancel.defaultTimeoutMinutes` 标 `@Deprecated`,仅作兜底。
|
||||||
|
- 前端 `order.vue`:`orderAutoCloseMins` 的 `min` 由 1 改 0,placeholder/文案/校验提示改为「0 表示不自动取消」。
|
||||||
|
|
||||||
|
### 设计决策(用户已确认)
|
||||||
|
1. DB 未配置/异常兜底 = 30 分钟(与历史默认一致)
|
||||||
|
2. 现状 YAML `tenantConfigs`(租户特殊超时)保留不做改造;0=全局禁用时 tenantConfigs 也不执行
|
||||||
|
3. 不引入缓存,定时任务每次查一次 DB(开销极小),改后台 1~5 分钟内生效
|
||||||
|
4. 前端交互全部调整(min=0 + 文案 + 校验)
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
- JAVA: `ShopSettingService.java`、`ShopSettingServiceImpl.java`、`task/OrderAutoCancelTask.java`、`config/OrderConfigProperties.java`
|
||||||
|
- 前端: `src/views/shop/shopSetting/components/order.vue`
|
||||||
|
|
||||||
|
### 未做
|
||||||
|
- 未编译验证(本地无 maven/mvnw 不可用);改动为标准增量,已逐文件核对语法。
|
||||||
|
- SQL / 取消逻辑 / 库存回退 / 优惠券退还 / 销量回滚 全部未动。
|
||||||
42
.workbuddy/memory/MEMORY.md
Normal file
42
.workbuddy/memory/MEMORY.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# 项目长期记忆
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
### 关键项目路径
|
||||||
|
- **管理后台**:`/Users/gxwebsoft/VUE/guilixu-admin`(Vue3 + Ant Design Vue)
|
||||||
|
- **用户端小程序**:`/Users/gxwebsoft/VUE/guilixu-taro`(Taro + React)
|
||||||
|
- **商家端小程序**:`/Users/gxwebsoft/VUE/xinlong-shop-taro`(Taro + React)
|
||||||
|
|
||||||
|
### ⚠️ 跨项目教训(重要)
|
||||||
|
- 商家端发货/门店/订单相关功能在 **xinlong-shop-taro**,不要改到 guilixu-taro
|
||||||
|
- 用户端订单详情等功能在 **guilixu-taro**
|
||||||
|
- 跨项目改动前,先用 grep/glob 在多个候选项目里确认"哪个项目有该功能源码",再动手
|
||||||
|
- 已发生两次误改 guilixu-taro 的教训(useClerk 全局上下文、订单详情 deliveryType 修复)
|
||||||
|
|
||||||
|
## 业务约定
|
||||||
|
|
||||||
|
### deliveryType 配送方式枚举
|
||||||
|
- `0` = 快递配送
|
||||||
|
- `1` = 无需发货 / 自提
|
||||||
|
- `2` = 商家送货
|
||||||
|
|
||||||
|
来源:管理后台 `src/views/shop/shopOrder/components/deliveryModal.vue`
|
||||||
|
|
||||||
|
## Java 后端架构(重要)
|
||||||
|
|
||||||
|
### 双后端 / MODULES_API_URL 模式(2026-07-25 分析结论)
|
||||||
|
- **mp-java**(`/Users/gxwebsoft/JAVA/mp-java`)= 独立的「modules 服务」,用 `modules` 库;cms(235类)、shop 等业务模块都在它这里,持续高频开发。
|
||||||
|
- **guilixu-java**(`/Users/gxwebsoft/JAVA/guilixu-java`)= 桂礼序主后端,用 `db_guilixu` 库;是 mp-java 的「子集分叉」。
|
||||||
|
- **guilixu-admin 前端已预设双基地址**:`SERVER_API_URL`(主后端) + `MODULES_API_URL`(localStorage ApiUrl / VITE_API_URL)。shop 商户/优惠券、cms 全部接口**都已走 MODULES_API_URL**,即前端本就假定 cms/shop 由 mp-java(modules 服务)提供。
|
||||||
|
- **结论:guilixu-java 里的 cms 包(152类)是 mp-java cms 的旧分叉子集、且前端并不调它(前端调 modules 服务),属重复/死代码,应删除而非复制或扩展。**
|
||||||
|
- 复制「整个 cms 到 guilixu-java」不可行:mp-java 完整 cms 的 CmsApp/CmsWebsiteService 依赖 `project` 模块(guilixu-java 没有),会编译失败。
|
||||||
|
- 注意:mp-java 与 guilixu-java 是独立 git 仓库(`git.websoft.top/gxwebsoft/mp-java.git` 与 `guilixu-java.git`),各自独立部署、独立库。
|
||||||
|
|
||||||
|
### shopGoods status 字段(上架/下架)
|
||||||
|
- `0` = 已上架 / 上架
|
||||||
|
- `1` = 待上架 / 下架
|
||||||
|
- `2` = 待审核,`3` = 审核不通过
|
||||||
|
- 编辑表单「状态」radio 绑定 `form.status`:0=上架、1=下架。
|
||||||
|
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
|
||||||
|
- 列表 tag 仍显示:0=已上架、1=待上架、2=待审核、3=审核不通过。
|
||||||
|
|
||||||
141
README.md
141
README.md
@@ -1,84 +1,89 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<h1>🌐 WebSoftAdmin</h1>
|
<h1>🛒 小程序商城后台管理系统</h1>
|
||||||
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级后台管理系统</strong></p>
|
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级小程序商城后台</strong></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<img src="https://img.shields.io/badge/Vue-3.x-4FC08D" alt="Vue">
|
<img src="https://img.shields.io/badge/Vue-3.x-4FC08D" alt="Vue">
|
||||||
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-3.x-1890FF" alt="Ant Design Vue">
|
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-4.x-1890FF" alt="Ant Design Vue">
|
||||||
<img src="https://img.shields.io/badge/TypeScript-4.x-3178C6" alt="TypeScript">
|
<img src="https://img.shields.io/badge/TypeScript-5.x-3178C6" alt="TypeScript">
|
||||||
<img src="https://img.shields.io/badge/Vite-4.x-646CFF" alt="Vite">
|
<img src="https://img.shields.io/badge/Vite-8.x-646CFF" alt="Vite">
|
||||||
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
|
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 📖 项目简介
|
## 📖 项目简介
|
||||||
|
|
||||||
WebSoftAdmin 是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级后台管理系统,采用最新的前端技术栈:
|
小程序商城后台管理系统是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级商城管理平台,为小程序商城提供完整的后台管理能力,涵盖商品、订单、会员、经销商、门店、营销活动等核心业务模块。
|
||||||
|
|
||||||
- **前端框架**:Vue 3 + TypeScript + Vite
|
- **前端框架**:Vue 3 + TypeScript + Vite
|
||||||
- **UI 组件库**:Ant Design Vue 3.x
|
- **UI 组件库**:Ant Design Vue 4.x + EleAdmin Pro
|
||||||
- **富文本编辑器**:TinyMCE(支持图片/视频上传、一键排版)
|
- **状态管理**:Pinia
|
||||||
- **图表库**:ECharts + G2
|
- **图表库**:ECharts + @antv/g2
|
||||||
- **工具库**:Lodash、Day.js、CryptoJS
|
- **工具库**:Lodash、Day.js、CryptoJS、ExcelJS
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 项目演示
|
|
||||||
| 后台管理系统 | https://mp.websoft.top |
|
|
||||||
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
| 账号密码 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
|
|
||||||
| 关注公众号 |  |
|
|
||||||
|
|
||||||
|
|
||||||
|
**项目域名**:[https://shop.websoft.top](https://shop.websoft.top)
|
||||||
|
|
||||||
|
**官方产品介绍**:[https://websoft.top/shop](https://websoft.top/shop)
|
||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
### 核心技术
|
### 核心技术
|
||||||
|
|
||||||
| 技术 | 版本 | 说明 |
|
| 技术 | 版本 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| Vue | 3.x | 渐进式 JavaScript 框架 |
|
| Vue | 3.x | 渐进式 JavaScript 框架 |
|
||||||
| TypeScript | 4.x | JavaScript 的超集 |
|
| TypeScript | 5.x | JavaScript 的超集 |
|
||||||
| Vite | 4.x | 下一代前端构建工具 |
|
| Vite | 8.x | 下一代前端构建工具 |
|
||||||
| Ant Design Vue | 3.2.11 | 企业级 UI 设计语言 |
|
| Ant Design Vue | 4.x | 企业级 UI 设计语言 |
|
||||||
| EleAdmin Pro | 1.10.1 | 企业级组件库 |
|
| EleAdmin Pro | 1.11+ | 企业级组件库 |
|
||||||
|
| Pinia | 2.x | Vue 3 状态管理库 |
|
||||||
|
|
||||||
### 功能组件
|
### 功能组件
|
||||||
|
|
||||||
- **TinyMCE** - 富文本编辑器,支持图片/视频上传
|
- **TinyMCE** - 富文本编辑器,支持图片/视频上传
|
||||||
- **ECharts** - 数据可视化图表库
|
- **ECharts** - 数据可视化图表库
|
||||||
- **CropperJS** - 图片裁剪组件
|
- **CropperJS** - 图片裁剪组件
|
||||||
- **ExcelJS** - Excel 文件处理
|
- **ExcelJS / xlsx** - Excel 文件导入导出
|
||||||
- **Ali OSS** - 阿里云对象存储
|
- **Ali OSS** - 阿里云对象存储
|
||||||
|
- **qrcode / jsbarcode** - 二维码与条形码生成
|
||||||
|
|
||||||
## 📋 环境要求
|
## 📋 环境要求
|
||||||
|
|
||||||
### 基础环境
|
### 基础环境
|
||||||
- 🟢 **Node.js 16+**
|
|
||||||
- 📦 **npm 8+ / yarn 1.22+**
|
- 🟢 **Node.js 18+**
|
||||||
|
- 📦 **npm 8+ / yarn 1.22+ / pnpm 8+**
|
||||||
- 🌐 **现代浏览器**(Chrome 63+、Firefox、Safari、Edge)
|
- 🌐 **现代浏览器**(Chrome 63+、Firefox、Safari、Edge)
|
||||||
|
|
||||||
### 开发工具
|
### 开发工具
|
||||||
|
|
||||||
- **推荐**:VS Code / WebStorm
|
- **推荐**:VS Code / WebStorm
|
||||||
- **插件**:Vetur / Volar(Vue 3 支持)
|
- **插件**:Volar(Vue 3 支持)
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 1. 克隆项目
|
### 1. 克隆项目
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/websoft-top/paopao-vue.git
|
git clone <仓库地址>
|
||||||
cd paopao-vue
|
cd guilixu-admin
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 安装依赖
|
### 2. 安装依赖
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用 npm
|
# 使用 npm
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 或使用 yarn
|
# 或使用 yarn
|
||||||
yarn install
|
yarn install
|
||||||
|
|
||||||
|
# 或使用 pnpm(推荐)
|
||||||
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置环境变量
|
### 3. 配置环境变量
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 复制环境变量示例文件
|
# 复制环境变量示例文件
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
@@ -88,6 +93,7 @@ cp .env.example .env
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 4. 启动开发服务器
|
### 4. 启动开发服务器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开发模式
|
# 开发模式
|
||||||
npm run dev
|
npm run dev
|
||||||
@@ -99,6 +105,7 @@ yarn dev
|
|||||||
访问 `http://localhost:3000` 即可看到管理后台。
|
访问 `http://localhost:3000` 即可看到管理后台。
|
||||||
|
|
||||||
### 5. 构建生产版本
|
### 5. 构建生产版本
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 生产构建
|
# 生产构建
|
||||||
npm run build
|
npm run build
|
||||||
@@ -112,6 +119,7 @@ npm run serve
|
|||||||
项目使用环境变量来管理敏感信息和配置。请按照以下步骤配置:
|
项目使用环境变量来管理敏感信息和配置。请按照以下步骤配置:
|
||||||
|
|
||||||
### 必需配置
|
### 必需配置
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# API 配置
|
# API 配置
|
||||||
VITE_API_URL=https://your-api.com/api # 后端 API 地址
|
VITE_API_URL=https://your-api.com/api # 后端 API 地址
|
||||||
@@ -124,51 +132,54 @@ VITE_TENANT_ID=your_tenant_id # 租户 ID
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 可选配置
|
### 可选配置
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 高德地图 (如需使用地图功能)
|
# 高德地图 (如需使用地图功能)
|
||||||
VITE_MAP_KEY=your_map_key # 高德地图 Key
|
VITE_MAP_KEY=your_map_key # 高德地图 Key
|
||||||
VITE_MAP_CODE=your_map_security_code # 高德地图安全密钥
|
VITE_MAP_CODE=your_map_security_code # 高德地图安全密钥
|
||||||
|
|
||||||
# WebSoftAdmin 授权 (商业版功能)
|
# EleAdmin Pro 授权 (商业版功能)
|
||||||
VITE_LICENSE_CODE=your_license_code # 授权码
|
VITE_LICENSE_CODE=your_license_code # 授权码
|
||||||
```
|
```
|
||||||
|
|
||||||
### 获取配置信息
|
|
||||||
- **高德地图密钥**:访问 [高德开放平台](https://lbs.amap.com/) 申请
|
|
||||||
- **WebSoftAdmin 授权码**:联系 [官方网站](https://websoft.top/) 获取
|
|
||||||
- **其他 API 配置**:根据您的后端服务配置
|
|
||||||
|
|
||||||
## 🎯 核心功能
|
## 🎯 核心功能
|
||||||
|
|
||||||
### 📝 内容管理系统
|
|
||||||
- **文章管理**:支持富文本编辑、图片/视频上传
|
|
||||||
- **一键排版**:智能文章格式优化,包含10种专业排版样式
|
|
||||||
- **媒体库**:图片/视频文件管理,支持分组和搜索
|
|
||||||
- **首行缩进**:中文段落格式智能切换
|
|
||||||
|
|
||||||
### 🛒 商城管理
|
### 🛒 商城管理
|
||||||
- **商品管理**:商品信息编辑、规格设置
|
|
||||||
- **订单管理**:订单流程跟踪
|
- **商品管理**:商品信息编辑、规格设置、SKU 管理、商品分类
|
||||||
|
- **订单管理**:订单流程跟踪、订单商品管理
|
||||||
- **库存管理**:商品库存监控
|
- **库存管理**:商品库存监控
|
||||||
|
- **优惠券管理**:优惠券创建与发放、用户优惠券管理
|
||||||
|
- **营销活动**:活动管理、活动报名、积分商品
|
||||||
|
- **礼品管理**:商城礼品配置
|
||||||
|
|
||||||
### 👥 用户权限
|
### 🏪 门店与经销商
|
||||||
- **用户管理**:用户信息维护
|
|
||||||
- **角色权限**:基于角色的访问控制
|
- **门店管理**:门店信息、门店围栏、门店骑手、门店仓库
|
||||||
|
- **经销商体系**:经销商申请、经销商用户、经销商订单、经销商资金与提现
|
||||||
|
- **分销设置**:经销商推荐人、分销海报、分销配置
|
||||||
|
|
||||||
|
### 👥 会员管理
|
||||||
|
|
||||||
|
- **会员注册**:会员注册管理
|
||||||
|
- **会员权益**:权益包管理、权益兑换
|
||||||
|
- **会员推荐**:推荐人关系管理
|
||||||
|
|
||||||
|
### 👤 用户权限
|
||||||
|
|
||||||
|
- **用户管理**:用户信息维护、用户分组
|
||||||
|
- **角色权限**:基于角色的访问控制(RBAC)
|
||||||
- **菜单管理**:动态菜单配置
|
- **菜单管理**:动态菜单配置
|
||||||
|
- **组织架构**:组织与部门管理
|
||||||
|
|
||||||
## 🎨 富文本编辑器特色功能
|
### ⚙️ 系统管理
|
||||||
|
|
||||||
### 📸 媒体上传
|
- **系统配置**:商城设置、提现配置
|
||||||
- **图片上传**:支持拖拽、粘贴、文件选择
|
- **支付管理**:支付配置、订单支付
|
||||||
- **视频上传**:支持多种视频格式
|
- **字典管理**:数据字典维护
|
||||||
- **媒体库**:统一的媒体文件管理
|
- **缓存管理**:缓存清理
|
||||||
- **OSS 存储**:阿里云对象存储集成
|
- **域名管理**:域名白名单配置
|
||||||
|
- **操作记录**:登录记录、操作日志
|
||||||
### ✨ 智能排版
|
|
||||||
- **一键排版**:10种专业排版优化
|
|
||||||
- **首行缩进**:中文段落格式切换
|
|
||||||
- **样式优化**:标题、段落、列表、表格等元素美化
|
|
||||||
- **响应式**:适配不同屏幕尺寸
|
|
||||||
|
|
||||||
## 🏗️ 项目结构
|
## 🏗️ 项目结构
|
||||||
|
|
||||||
@@ -176,11 +187,17 @@ VITE_LICENSE_CODE=your_license_code # 授权码
|
|||||||
src/
|
src/
|
||||||
├── components/ # 公共组件
|
├── components/ # 公共组件
|
||||||
├── views/ # 页面组件
|
├── views/ # 页面组件
|
||||||
│ ├── cms/ # 内容管理
|
│ ├── shop/ # 商城管理(商品、订单、门店、经销商、会员等)
|
||||||
│ ├── shop/ # 商城管理
|
│ ├── system/ # 系统管理(用户、角色、菜单、字典、支付等)
|
||||||
│ └── system/ # 系统管理
|
│ ├── passport/ # 登录认证
|
||||||
|
│ ├── user/ # 用户中心
|
||||||
|
│ └── result/ # 结果页
|
||||||
├── router/ # 路由配置
|
├── router/ # 路由配置
|
||||||
├── store/ # 状态管理
|
├── store/ # Pinia 状态管理
|
||||||
├── utils/ # 工具函数
|
├── utils/ # 工具函数
|
||||||
└── assets/ # 静态资源
|
└── assets/ # 静态资源
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
|
|||||||
35
add-is-top-shop-goods.sql
Normal file
35
add-is-top-shop-goods.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 商品表新增「置顶」字段 is_top,并调整默认排序(方案 B)
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||||
|
-- 1. is_top = 1 表示置顶,0(默认)表示不置顶
|
||||||
|
-- 2. 表名/字段名 snake_case 约定(Node + TypeORM 常见);
|
||||||
|
-- 若你的库列名是驼峰(goodsId / isTop),请自行替换
|
||||||
|
-- 3. 与 recommend(推荐)同为「列表内联一键切换」的布尔开关
|
||||||
|
-- 4. 混合排序思路:置顶的永远在最前;置顶内部按排序号升序;
|
||||||
|
-- 其余商品按创建时间倒序(绝大多数商品无需手动排)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 第一步:新增字段(已存在则跳过;MySQL 5.7 / 8 通用)
|
||||||
|
ALTER TABLE shop_goods
|
||||||
|
ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:1置顶 0不置顶';
|
||||||
|
|
||||||
|
-- (可选)联合索引,置顶 + 排序号分页查询更稳
|
||||||
|
-- ALTER TABLE shop_goods
|
||||||
|
-- ADD INDEX idx_is_top_sort (is_top, sort_number);
|
||||||
|
|
||||||
|
-- 第二步:调整分页查询(page 接口)默认排序
|
||||||
|
-- 原(示例):ORDER BY create_time DESC
|
||||||
|
-- 改: ORDER BY is_top DESC, sort_number ASC, create_time DESC
|
||||||
|
-- 说明:若后端排序字段来自前端 orders 参数,需放开 is_top / sort_number 参与排序
|
||||||
|
-- (前端「置顶」「排序号」列已开启 sorter,会向后端传 orders)
|
||||||
|
|
||||||
|
-- 第三步(验证):确认字段已加、数据默认 0,且排序符合预期
|
||||||
|
SELECT goods_id,
|
||||||
|
name,
|
||||||
|
is_top,
|
||||||
|
sort_number,
|
||||||
|
create_time
|
||||||
|
FROM shop_goods
|
||||||
|
ORDER BY is_top DESC, sort_number ASC, create_time DESC
|
||||||
|
LIMIT 20;
|
||||||
73
docs/cms-module-reuse-analysis.md
Normal file
73
docs/cms-module-reuse-analysis.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# 桂礼序 CMS 模块复用方案分析
|
||||||
|
|
||||||
|
> 分析对象:`mp-java`(modules 服务,cms 235 类) vs `guilixu-java`(桂礼序主后端,cms 152 类)
|
||||||
|
> 分析日期:2026-07-25
|
||||||
|
|
||||||
|
## 一、现状事实(已核实)
|
||||||
|
|
||||||
|
| 维度 | mp-java | guilixu-java |
|
||||||
|
|------|---------|--------------|
|
||||||
|
| 形态 | 单体 Spring Boot 应用(单 pom,@MapperScan `com.gxwebsoft.**`) | 同左 |
|
||||||
|
| cms 类数 | **235**(controller 37) | **152**(controller 25) |
|
||||||
|
| cms 数据库 | `modules` 库 | `db_guilixu`(prod/dev)、`modules`(test) |
|
||||||
|
| cms 建表/迁移 | 由 `src/main/java/com/gxwebsoft/cms/sql/` + `docs/sql` 管理,高频更新 | `sql/` 目录**无 cms 建表脚本** |
|
||||||
|
| git 引入时间 | 2025-07-24("2.0版本分离"),持续开发 | 2026-06-01("桂礼序"),已落后 ~1.5 个月 |
|
||||||
|
| 最近提交 | 2026-07-24/25(仍在改 cms) | 最近为 shop/order 相关,cms 停滞 |
|
||||||
|
|
||||||
|
**关键证据:**
|
||||||
|
|
||||||
|
1. **guilixu-java 的 152 个 cms 类,全部能在 mp-java 的 cms 里找到同名类**;mp-java 额外多出 83 个(CmsApp / CmsBanner / CmsCase 等系列)。→ guilixu 的 cms 是 mp-java cms 的**子集分叉**。
|
||||||
|
2. **前端(guilixu-admin)的 cms 接口早已使用 `MODULES_API_URL`**,路径如 `MODULES_API_URL + '/cms/cms-ad/page'`。`MODULES_API_URL` = `localStorage.ApiUrl || VITE_API_URL`,即指向 modules 服务(mp-java)。shop 的商户/优惠券接口同样走 `MODULES_API_URL`。→ **前端本就假定 cms/shop 由 mp-java 提供,guilixu-java 自己的 cms 包前端并不调用(重复/死代码)。**
|
||||||
|
3. **mp-java 与 guilixu-java 的 cms 控制器路径完全一致**(`/api/cms/cms-ad`、`/api/cms/cms-article-like` …),说明两者是同一套接口契约。
|
||||||
|
4. **完整复制不可编译**:mp-java 完整 cms 的 `CmsApp*`/`CmsWebsiteService*` 依赖 `com.gxwebsoft.project`(Project/ProjectService),而 guilixu-java **没有 project 模块**。把 235 类整体搬过去会编译失败。
|
||||||
|
|
||||||
|
## 二、方案对比
|
||||||
|
|
||||||
|
### 方案 A:guilixu 直接调用 mp-java 的 cms(modules 服务作为 cms 提供方)
|
||||||
|
|
||||||
|
**优点**
|
||||||
|
- ✅ 单一事实来源:cms 逻辑只维护一份,guilixu 永远拿到最新功能(含 83 个新增类)。
|
||||||
|
- ✅ 前端几乎零改动:guilixu-admin 的 cms 接口**已经**指向 `MODULES_API_URL`,契约路径与 mp-java 一致。
|
||||||
|
- ✅ 消除重复:可删除 guilixu-java 里 152 个冗余 cms 类,降低维护与同步负担。
|
||||||
|
- ✅ cms 数据/建表/迁移由 mp-java 集中管理(modules 库),guilixu 无需自建 cms 表。
|
||||||
|
- ✅ 符合现有"主后端 + modules 服务"双基地址架构(shop 已实现)。
|
||||||
|
|
||||||
|
**缺点 / 风险**
|
||||||
|
- ⚠️ 运行期耦合:guilixu 的 cms 功能可用性依赖 mp-java 在线(mp-java 宕机/升级 → cms 不可用)。
|
||||||
|
- ⚠️ 跨库一致:cms 在 `modules`,guilixu 业务在 `db_guilixu`;若需"同事务写 cms+shop"无法用本地事务,需走事件/Saga。
|
||||||
|
- ⚠️ 鉴权边界:mp-java 与 guilixu 各自用户体系(不同库),需确保 modules 服务对 guilixu-admin 的 cms 调用有合法鉴权(现有 TENANT_ID/APP_SECRET/ApiUrl 机制应已覆盖)。
|
||||||
|
- ⚠️ 接口微小差异:guilixu 是旧分叉,个别字段/路径若 mp-java 已重构,需对一遍(如 CmsWebsite 已删手机号脱敏方法)。
|
||||||
|
|
||||||
|
### 方案 B:把 cms 模块整个复制到 guilixu-java
|
||||||
|
|
||||||
|
**优点**
|
||||||
|
- ✅ 自包含:cms 随 guilixu-java 独立运行,不依赖 mp-java 在线。
|
||||||
|
- ✅ 接口契约稳定:沿用现有 guilixu-java cms 路径,前端无需改 baseURL。
|
||||||
|
- ✅ 可做 guilixu 专属深度定制,不受 mp-java 限制。
|
||||||
|
|
||||||
|
**缺点 / 风险**
|
||||||
|
- ❌ **语义上"整个复制"不可行**:完整 235 类依赖 guilixu 没有的 `project` 模块,编译失败;只能裁剪到子集,等于失去"完整"意义。
|
||||||
|
- ❌ 固化分叉:现在已落后 83 类/1.5 个月;mp-java cms 仍高频演进,复制后会陷入"复制→分叉→再复制"循环,双份维护成本最高。
|
||||||
|
- ❌ 数据迁移:需把 cms 表从 `modules` 迁到 `db_guilixu`,并自补建表/迁移脚本(目前 guilixu 无 cms 建表脚本)。
|
||||||
|
- ❌ 与前端现状冲突:前端 cms 调的是 `MODULES_API_URL`(mp-java),复制后这套 guilixu cms 实际无人调用(死代码),反而制造两套真相。
|
||||||
|
|
||||||
|
### (补充)方案 C:抽独立 cms 共享库/微服务(长期最优,成本高)
|
||||||
|
把 cms 抽成 `cms-spring-boot-starter`(或独立 cms 微服务 + 客户端 SDK),mp-java 与 guilixu-java 都依赖。能根治重复与分叉,但需重构打包、版本发布流程,短期落地成本高于 A。
|
||||||
|
|
||||||
|
## 三、结论与落地建议
|
||||||
|
|
||||||
|
**结论:选方案 A(guilixu 直接调用 mp-java 的 cms),不要整体复制到 guilixu-java。**
|
||||||
|
|
||||||
|
理由(按重要性):
|
||||||
|
1. 前端早已按"modules 服务(mp-java)提供 cms"设计(cms 接口全走 `MODULES_API_URL`,路径契约一致)——方案 A 是顺延既有架构,方案 B 反而与现状冲突。
|
||||||
|
2. guilixu 的 cms 是 mp-java 的旧分叉子集且前端不调它,复制/扩展只会把分叉永久化;"整体复制"还因缺 `project` 模块无法编译。
|
||||||
|
3. cms 数据、建表、迁移本就在 mp-java(modules 库)集中管理,远程调用天然复用。
|
||||||
|
|
||||||
|
**落地步骤建议:**
|
||||||
|
1. 确认 `guilixu-admin` 的 `MODULES_API_URL` / `localStorage.ApiUrl` 指向 mp-java 部署实例(modules 服务),cms 调用即生效(前端基本不用动)。
|
||||||
|
2. **删除 guilixu-java 中重复的 `com/gxwebsoft/cms` 包(152 类)**,消除死代码与未来同步负担。
|
||||||
|
3. 对一遍 mp-java 与 guilixu 旧 cms 的接口字段差异(重点 CmsWebsite 等已重构类),确保前端无误。
|
||||||
|
4. 明确可用性边界:在运维/部署文档标注"cms 依赖 modules 服务",并为 mp-java 配置高可用/健康检查。
|
||||||
|
5. 若后续确实需要 guilixu 专属、与自身业务同事务的 cms 能力,再走方案 C(抽共享库),不要走方案 B。
|
||||||
|
|
||||||
|
> 备注:若你的真实诉求是"让 guilixu 完全独立、不依赖 mp-java 在线",则需在方案 B 基础上额外补 `project` 模块依赖 + cms 表迁移到 `db_guilixu` + 长期同步机制,综合成本最高,除非有强隔离需求否则不推荐。
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>企业商城</title>
|
<title>小程序商城</title>
|
||||||
<!-- TinyMCE 通过本地文件加载,避免 Rolldown 打包 UMD 模块的问题 -->
|
<!-- TinyMCE 通过本地文件加载,避免 Rolldown 打包 UMD 模块的问题 -->
|
||||||
<script src="/tinymce/tinymce.min.js"></script>
|
<script src="/tinymce/tinymce.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
37
offline-noimage-goods-10332.sql
Normal file
37
offline-noimage-goods-10332.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 下架「分类ID = 10332 且 没有图片」的商品
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||||
|
-- 1. 下架 = status = 1(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio)
|
||||||
|
-- 2. 「没有图片」以封面图 image 是否为空判断(封面图为必填项)
|
||||||
|
-- 3. 已删除商品(deleted = 1)不参与,避免误改
|
||||||
|
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
|
||||||
|
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
|
||||||
|
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
|
||||||
|
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
|
||||||
|
SELECT goods_id,
|
||||||
|
name,
|
||||||
|
image,
|
||||||
|
status
|
||||||
|
FROM shop_goods
|
||||||
|
WHERE category_id = 10332
|
||||||
|
AND (image IS NULL OR image = '')
|
||||||
|
AND (deleted = 0 OR deleted IS NULL);
|
||||||
|
|
||||||
|
-- 第二步:确认无误后执行下架
|
||||||
|
UPDATE shop_goods
|
||||||
|
SET status = 1,
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE category_id = 10332
|
||||||
|
AND (image IS NULL OR image = '')
|
||||||
|
AND (deleted = 0 OR deleted IS NULL);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- (可选)若你认为「没有图片」应同时要求轮播图也为空,
|
||||||
|
-- 即封面图和轮播图都没有才算没图,使用下面更严格的条件:
|
||||||
|
-- AND (image IS NULL OR image = '')
|
||||||
|
-- AND (files IS NULL OR files = '' OR files = '[]')
|
||||||
|
-- ------------------------------------------------------------
|
||||||
38
online-withimage-goods-10332.sql
Normal file
38
online-withimage-goods-10332.sql
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 批量上架「分类ID = 10332 且 含有图片」的商品
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||||
|
-- 1. 上架 = status = 0(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio)
|
||||||
|
-- 2. 「含有图片」以封面图 image 是否非空判断(image IS NOT NULL AND image <> '')
|
||||||
|
-- 3. 已删除商品(deleted = 1)不参与,避免误改
|
||||||
|
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
|
||||||
|
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
|
||||||
|
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
|
||||||
|
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
|
||||||
|
SELECT goods_id,
|
||||||
|
name,
|
||||||
|
image,
|
||||||
|
status
|
||||||
|
FROM shop_goods
|
||||||
|
WHERE category_id = 10332
|
||||||
|
AND image IS NOT NULL
|
||||||
|
AND image <> ''
|
||||||
|
AND (deleted = 0 OR deleted IS NULL);
|
||||||
|
|
||||||
|
-- 第二步:确认无误后执行上架
|
||||||
|
UPDATE shop_goods
|
||||||
|
SET status = 0,
|
||||||
|
update_time = NOW()
|
||||||
|
WHERE category_id = 10332
|
||||||
|
AND image IS NOT NULL
|
||||||
|
AND image <> ''
|
||||||
|
AND (deleted = 0 OR deleted IS NULL);
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------
|
||||||
|
-- (可选)若只想把「当前已下架」的商品重新上架,
|
||||||
|
-- 而不动「待审核(2) / 审核不通过(3)」的商品,加上 status 条件:
|
||||||
|
-- AND status = 1
|
||||||
|
-- ------------------------------------------------------------
|
||||||
43
src/api/app/appProduct/index.ts
Normal file
43
src/api/app/appProduct/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { AppProduct, AppProductQueryParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websopy 特殊接口域名
|
||||||
|
* 后端 Controller: com.gxwebsoft.app.controller.AppProductController
|
||||||
|
* @RequestMapping("/api/app/product"),后端无 context-path
|
||||||
|
* 完整 URL = https://websopy-api.websoft.top/api/app/product/xxx
|
||||||
|
*/
|
||||||
|
const WEBSOPY_API_BASE = 'https://websopy-api.websoft.top/api';
|
||||||
|
const BASE = `${WEBSOPY_API_BASE}/app/product`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询应用列表
|
||||||
|
* GET /app/product/page
|
||||||
|
* 支持按 tenantId 过滤(管理后台按租户查询应用)
|
||||||
|
* 注意:分页参数为 current/size(非 page/limit)
|
||||||
|
*/
|
||||||
|
export async function pageProducts(
|
||||||
|
params: AppProductQueryParam
|
||||||
|
): Promise<PageResult<AppProduct>> {
|
||||||
|
const res = await request.get<ApiResult<PageResult<AppProduct>>>(
|
||||||
|
`${BASE}/page`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data || { list: [], count: 0 };
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用详情
|
||||||
|
* GET /app/product/detail/{id}
|
||||||
|
*/
|
||||||
|
export async function getProductDetail(id: number): Promise<AppProduct> {
|
||||||
|
const res = await request.get<ApiResult<AppProduct>>(`${BASE}/detail/${id}`);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as AppProduct;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
143
src/api/app/appProduct/model.ts
Normal file
143
src/api/app/appProduct/model.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/**
|
||||||
|
* 应用产品实体(对应表 app_product)
|
||||||
|
* 字段对照后端 com.gxwebsoft.app.entity.AppProduct
|
||||||
|
*/
|
||||||
|
export interface AppProduct {
|
||||||
|
// 应用ID(主键)
|
||||||
|
productId?: number;
|
||||||
|
// 应用名称
|
||||||
|
productName?: string;
|
||||||
|
// 应用标识(唯一)
|
||||||
|
productCode?: string;
|
||||||
|
// 应用密钥
|
||||||
|
productSecret?: string;
|
||||||
|
// 应用类型: 10网站 20微信小程序 30抖音小程序 40百度小程序 50支付宝小程序 60Android 70iOS 80macOS 90Windows 100插件
|
||||||
|
appType?: number;
|
||||||
|
// 应用类型名称(关联查询)
|
||||||
|
appTypeName?: string;
|
||||||
|
// 分类ID
|
||||||
|
categoryId?: number;
|
||||||
|
// 行业类型(父级)
|
||||||
|
industryParent?: string;
|
||||||
|
// 行业类型(子级)
|
||||||
|
industryChild?: string;
|
||||||
|
// 应用Logo
|
||||||
|
logo?: string;
|
||||||
|
// 应用图标
|
||||||
|
icon?: string;
|
||||||
|
// 二维码
|
||||||
|
qrcode?: string;
|
||||||
|
// 应用截图(JSON数组)
|
||||||
|
screenshots?: string;
|
||||||
|
// 应用简介
|
||||||
|
description?: string;
|
||||||
|
// 详细说明
|
||||||
|
content?: string;
|
||||||
|
// 关键词
|
||||||
|
keywords?: string;
|
||||||
|
// 域名
|
||||||
|
domain?: string;
|
||||||
|
// 域名前缀
|
||||||
|
prefix?: string;
|
||||||
|
// 包名/AppID
|
||||||
|
packageName?: string;
|
||||||
|
// 后台地址
|
||||||
|
adminUrl?: string;
|
||||||
|
// API地址
|
||||||
|
apiUrl?: string;
|
||||||
|
// 下载地址
|
||||||
|
downloadUrl?: string;
|
||||||
|
// 版本号
|
||||||
|
version?: string;
|
||||||
|
// 版本: standard标准版 professional专业版 perpetual永久授权
|
||||||
|
edition?: string;
|
||||||
|
// 最低版本要求
|
||||||
|
minVersion?: string;
|
||||||
|
// 定价: free免费 one_time一次性 subscription订阅
|
||||||
|
priceType?: string;
|
||||||
|
// 价格(元)
|
||||||
|
price?: number;
|
||||||
|
// 划线价格
|
||||||
|
linePrice?: number;
|
||||||
|
// 续费价格
|
||||||
|
renewPrice?: number;
|
||||||
|
// 交付方式: 1源码 2托管 3授权
|
||||||
|
deliveryMethod?: number;
|
||||||
|
// 计费方式: 1按年 2按月 3一次性
|
||||||
|
chargingMethod?: number;
|
||||||
|
// 订阅周期: month/year
|
||||||
|
subscriptionPeriod?: string;
|
||||||
|
// 发布状态: developing pending_review published rejected deprecated
|
||||||
|
publishStatus?: string;
|
||||||
|
// 发布时间
|
||||||
|
publishTime?: string;
|
||||||
|
// 审核时间
|
||||||
|
reviewTime?: string;
|
||||||
|
// 审核人ID
|
||||||
|
reviewerId?: number;
|
||||||
|
// 拒绝原因
|
||||||
|
rejectReason?: string;
|
||||||
|
// 浏览次数
|
||||||
|
clicks?: number;
|
||||||
|
// 安装次数
|
||||||
|
installs?: number;
|
||||||
|
// 下载次数
|
||||||
|
downloads?: number;
|
||||||
|
// 评分(1-5)
|
||||||
|
rating?: number;
|
||||||
|
// 点赞数
|
||||||
|
likes?: number;
|
||||||
|
// 开发者
|
||||||
|
developer?: string;
|
||||||
|
// 开发者电话
|
||||||
|
developerPhone?: string;
|
||||||
|
// 开发者邮箱
|
||||||
|
developerEmail?: string;
|
||||||
|
// 是否推荐: 0否 1是
|
||||||
|
recommend?: number;
|
||||||
|
// 是否官方: 0否 1是
|
||||||
|
official?: number;
|
||||||
|
// 是否上架市场: 0否 1是
|
||||||
|
market?: number;
|
||||||
|
// 是否显示首页: 0否 1是
|
||||||
|
showIndex?: number;
|
||||||
|
// 是否可搜索: 0否 1是
|
||||||
|
searchEnabled?: number;
|
||||||
|
// 模板ID
|
||||||
|
templateId?: number;
|
||||||
|
// 租户ID
|
||||||
|
tenantId?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 更新时间
|
||||||
|
updateTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用产品分页查询参数
|
||||||
|
* 注意:后端 AppProductController.page 的分页参数为 current/size(非 page/limit)
|
||||||
|
*/
|
||||||
|
export interface AppProductQueryParam {
|
||||||
|
// 页码
|
||||||
|
current?: number;
|
||||||
|
// 每页条数
|
||||||
|
size?: number;
|
||||||
|
// 应用名称
|
||||||
|
productName?: string;
|
||||||
|
// 应用标识
|
||||||
|
productCode?: string;
|
||||||
|
// 应用类型
|
||||||
|
appType?: number;
|
||||||
|
// 分类ID
|
||||||
|
categoryId?: number;
|
||||||
|
// 发布状态
|
||||||
|
publishStatus?: string;
|
||||||
|
// 状态
|
||||||
|
status?: number;
|
||||||
|
// 用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 租户ID(按租户查询应用)
|
||||||
|
tenantId?: number;
|
||||||
|
// 关键词搜索(同时搜索应用名称和应用标识)
|
||||||
|
keywords?: string;
|
||||||
|
}
|
||||||
276
src/api/app/appSubscription/index.ts
Normal file
276
src/api/app/appSubscription/index.ts
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type {
|
||||||
|
AppSubscription,
|
||||||
|
AppSubscriptionQueryParam,
|
||||||
|
SubscribeParam,
|
||||||
|
SubscribeResult,
|
||||||
|
GeneratePayQrcodeParam,
|
||||||
|
GeneratePayQrcodeResult,
|
||||||
|
CheckStatusResult,
|
||||||
|
PayResult,
|
||||||
|
WechatNativePayResult,
|
||||||
|
MpPrepayParam,
|
||||||
|
MpPrepayResult,
|
||||||
|
MpConfirmParam,
|
||||||
|
BalanceResult
|
||||||
|
} from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websopy 特殊接口域名(与项目主 API 域名不同)
|
||||||
|
* 后端 Controller: com.gxwebsoft.app.controller.AppSubscriptionController
|
||||||
|
* @RequestMapping("/api/app/subscription"),后端无 context-path
|
||||||
|
* 因此完整 URL = https://websopy-api.websoft.top + /api/app/subscription/xxx
|
||||||
|
*/
|
||||||
|
const WEBSOPY_API_BASE = 'https://websopy-api.websoft.top/api';
|
||||||
|
const BASE = `${WEBSOPY_API_BASE}/app/subscription`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的订阅列表(分页)
|
||||||
|
* GET /app/subscription/my/page
|
||||||
|
*/
|
||||||
|
export async function pageMySubscriptions(
|
||||||
|
params: AppSubscriptionQueryParam
|
||||||
|
): Promise<PageResult<AppSubscription>> {
|
||||||
|
const res = await request.get<ApiResult<PageResult<AppSubscription>>>(
|
||||||
|
`${BASE}/my/page`,
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { list: [], count: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅详情(按ID)
|
||||||
|
* GET /app/subscription/detail/{id}
|
||||||
|
*/
|
||||||
|
export async function getSubscriptionDetail(
|
||||||
|
id: number
|
||||||
|
): Promise<AppSubscription> {
|
||||||
|
const res = await request.get<ApiResult<AppSubscription>>(
|
||||||
|
`${BASE}/detail/${id}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as AppSubscription;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订阅编号查询详情(小程序入口用)
|
||||||
|
* GET /app/subscription/detail-by-no/{subscriptionNo}
|
||||||
|
*/
|
||||||
|
export async function getSubscriptionDetailByNo(
|
||||||
|
subscriptionNo: string
|
||||||
|
): Promise<AppSubscription> {
|
||||||
|
const res = await request.get<ApiResult<AppSubscription>>(
|
||||||
|
`${BASE}/detail-by-no/${subscriptionNo}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as AppSubscription;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态(前端轮询用)
|
||||||
|
* GET /app/subscription/check-status/{subscriptionNo}
|
||||||
|
*/
|
||||||
|
export async function checkSubscriptionStatus(
|
||||||
|
subscriptionNo: string
|
||||||
|
): Promise<CheckStatusResult> {
|
||||||
|
const res = await request.get<ApiResult<CheckStatusResult>>(
|
||||||
|
`${BASE}/check-status/${subscriptionNo}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as CheckStatusResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已购买某应用
|
||||||
|
* GET /app/subscription/check-purchased/{productId}
|
||||||
|
* 返回 boolean
|
||||||
|
*/
|
||||||
|
export async function checkPurchased(
|
||||||
|
productId: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
const res = await request.get<ApiResult<boolean>>(
|
||||||
|
`${BASE}/check-purchased/${productId}`
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data === true;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户余额
|
||||||
|
* GET /app/subscription/balance
|
||||||
|
*/
|
||||||
|
export async function getBalance(): Promise<BalanceResult> {
|
||||||
|
const res = await request.get<ApiResult<BalanceResult>>(`${BASE}/balance`);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { balance: 0 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅
|
||||||
|
* POST /app/subscription/subscribe
|
||||||
|
* 免费应用直接激活,付费应用创建待支付记录
|
||||||
|
*/
|
||||||
|
export async function subscribe(
|
||||||
|
data: SubscribeParam
|
||||||
|
): Promise<SubscribeResult> {
|
||||||
|
const res = await request.post<ApiResult<SubscribeResult>>(
|
||||||
|
`${BASE}/subscribe`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as SubscribeResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码(同时创建订阅记录)
|
||||||
|
* POST /app/subscription/generate-pay-qrcode
|
||||||
|
*/
|
||||||
|
export async function generatePayQrcode(
|
||||||
|
data: GeneratePayQrcodeParam
|
||||||
|
): Promise<GeneratePayQrcodeResult> {
|
||||||
|
const res = await request.post<ApiResult<GeneratePayQrcodeResult>>(
|
||||||
|
`${BASE}/generate-pay-qrcode`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as GeneratePayQrcodeResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付
|
||||||
|
* POST /app/subscription/pay/{id}?method=balance|wechat&envVersion=xxx
|
||||||
|
* - method=balance:余额支付,返回 PayResult
|
||||||
|
* - method=wechat:微信 Native 支付,返回小程序码 WechatNativePayResult
|
||||||
|
*/
|
||||||
|
export async function paySubscription(
|
||||||
|
id: number,
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
const res = await request.post<ApiResult<PayResult | WechatNativePayResult>>(
|
||||||
|
`${BASE}/pay/${id}`,
|
||||||
|
undefined,
|
||||||
|
{ params: { method, envVersion } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as PayResult | WechatNativePayResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付下单
|
||||||
|
* POST /app/subscription/mp-prepay/{id}
|
||||||
|
* Body: { openid }
|
||||||
|
*/
|
||||||
|
export async function mpPrepay(
|
||||||
|
id: number,
|
||||||
|
data: MpPrepayParam
|
||||||
|
): Promise<MpPrepayResult> {
|
||||||
|
const res = await request.post<ApiResult<MpPrepayResult>>(
|
||||||
|
`${BASE}/mp-prepay/${id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as MpPrepayResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付成功确认
|
||||||
|
* POST /app/subscription/mp-confirm/{subscriptionNo}
|
||||||
|
* Body: { transactionId? }
|
||||||
|
*/
|
||||||
|
export async function mpConfirm(
|
||||||
|
subscriptionNo: string,
|
||||||
|
data?: MpConfirmParam
|
||||||
|
): Promise<PayResult> {
|
||||||
|
const res = await request.post<ApiResult<PayResult>>(
|
||||||
|
`${BASE}/mp-confirm/${subscriptionNo}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return (
|
||||||
|
res.data.data || { paid: true, subscriptionNo }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续费:基于已有订阅创建续费订单并生成支付码(一步完成,对齐后端 /renew-pay)
|
||||||
|
* POST /app/subscription/renew-pay/{id}?period=month&method=wechat&envVersion=trial
|
||||||
|
* - method=balance:余额支付,返回 PayResult(paid/balance)
|
||||||
|
* - method=wechat:微信支付,返回小程序码 WechatNativePayResult(miniappQrcode)
|
||||||
|
* 后端会基于原 expireTime 或 now(取较大者)延长到期时间
|
||||||
|
*/
|
||||||
|
export async function renewSubscription(
|
||||||
|
id: number,
|
||||||
|
period: 'month' | 'year' = 'month',
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
const res = await request.post<ApiResult<PayResult | WechatNativePayResult>>(
|
||||||
|
`${BASE}/renew-pay/${id}`,
|
||||||
|
undefined,
|
||||||
|
{ params: { period, method, envVersion } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data as PayResult | WechatNativePayResult;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退订/取消
|
||||||
|
* POST /app/subscription/cancel/{id}
|
||||||
|
*/
|
||||||
|
export async function cancelSubscription(id: number): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<string>>(`${BASE}/cancel/${id}`);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message || '退订成功';
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用
|
||||||
|
* POST /app/subscription/toggle-enable/{id}?enabled=true|false
|
||||||
|
*/
|
||||||
|
export async function toggleEnable(
|
||||||
|
id: number,
|
||||||
|
enabled: boolean
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<string>>(
|
||||||
|
`${BASE}/toggle-enable/${id}`,
|
||||||
|
undefined,
|
||||||
|
{ params: { enabled } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message || (enabled ? '已启用' : '已禁用');
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
219
src/api/app/appSubscription/model.ts
Normal file
219
src/api/app/appSubscription/model.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅状态: pending-待支付 active-已激活 expired-已过期 cancelled-已取消
|
||||||
|
*/
|
||||||
|
export type SubscriptionStatus =
|
||||||
|
| 'pending'
|
||||||
|
| 'active'
|
||||||
|
| 'expired'
|
||||||
|
| 'cancelled';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格类型: free-免费 one_time-买断 subscription-订阅
|
||||||
|
*/
|
||||||
|
export type PriceType = 'free' | 'one_time' | 'subscription';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅周期: month-月 quarter-季 year-年
|
||||||
|
*/
|
||||||
|
export type SubscriptionPeriod = 'month' | 'quarter' | 'year';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支付方式: 0-余额 1-微信 2-支付宝 12-免费
|
||||||
|
*/
|
||||||
|
export type PayType = 0 | 1 | 2 | 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用订阅实体(对应表 app_subscription)
|
||||||
|
* 字段对照后端 com.gxwebsoft.app.entity.AppSubscription
|
||||||
|
*/
|
||||||
|
export interface AppSubscription {
|
||||||
|
// 主键ID
|
||||||
|
id?: number;
|
||||||
|
// 订阅编号(业务唯一)
|
||||||
|
subscriptionNo?: string;
|
||||||
|
// 购买用户ID
|
||||||
|
userId?: number;
|
||||||
|
// 应用产品ID
|
||||||
|
productId?: number;
|
||||||
|
// 租户ID
|
||||||
|
tenantId?: number;
|
||||||
|
// 订阅状态
|
||||||
|
status?: SubscriptionStatus;
|
||||||
|
// 价格类型
|
||||||
|
priceType?: PriceType;
|
||||||
|
// 原价(单位:元)
|
||||||
|
originalPrice?: number;
|
||||||
|
// 实付金额(单位:元)
|
||||||
|
payPrice?: number;
|
||||||
|
// 支付方式
|
||||||
|
payType?: PayType;
|
||||||
|
// 支付状态: 0-未支付 1-已支付
|
||||||
|
payStatus?: number;
|
||||||
|
// 支付时间
|
||||||
|
payTime?: string;
|
||||||
|
// 第三方交易号
|
||||||
|
transactionId?: string;
|
||||||
|
// 订阅周期
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
// 生效时间
|
||||||
|
startTime?: string;
|
||||||
|
// 到期时间(订阅型)
|
||||||
|
expireTime?: string;
|
||||||
|
// 是否自动续费 0-否 1-是
|
||||||
|
autoRenew?: number;
|
||||||
|
// 分配的域名
|
||||||
|
instanceDomain?: string;
|
||||||
|
// 实例管理后台URL
|
||||||
|
instanceAdminUrl?: string;
|
||||||
|
// 实例配置(JSON)
|
||||||
|
instanceConfig?: string;
|
||||||
|
// 关联的支付订单号
|
||||||
|
orderNo?: string;
|
||||||
|
// 关联的支付订单ID
|
||||||
|
orderId?: number;
|
||||||
|
// 备注
|
||||||
|
remark?: string;
|
||||||
|
// 排序
|
||||||
|
sortNumber?: number;
|
||||||
|
// 创建时间
|
||||||
|
createTime?: string;
|
||||||
|
// 更新时间
|
||||||
|
updateTime?: string;
|
||||||
|
// ===== 关联查询字段(非数据库字段) =====
|
||||||
|
productName?: string;
|
||||||
|
productIcon?: string;
|
||||||
|
productLogo?: string;
|
||||||
|
productAppType?: number;
|
||||||
|
productDescription?: string;
|
||||||
|
developerName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 我的订阅分页查询参数
|
||||||
|
*/
|
||||||
|
export interface AppSubscriptionQueryParam extends PageParam {
|
||||||
|
// 订阅状态过滤
|
||||||
|
status?: SubscriptionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅参数
|
||||||
|
*/
|
||||||
|
export interface SubscribeParam {
|
||||||
|
// 应用产品ID
|
||||||
|
productId: number;
|
||||||
|
// 订阅周期,默认 month
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码参数
|
||||||
|
*/
|
||||||
|
export interface GeneratePayQrcodeParam {
|
||||||
|
// 应用产品ID
|
||||||
|
productId: number;
|
||||||
|
// 订阅周期,默认 month
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
// 小程序版本:develop / trial / release,默认 trial
|
||||||
|
envVersion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅返回结果
|
||||||
|
*/
|
||||||
|
export interface SubscribeResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
message: string;
|
||||||
|
payPrice?: number;
|
||||||
|
orderNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码返回结果
|
||||||
|
*/
|
||||||
|
export interface GeneratePayQrcodeResult {
|
||||||
|
subscriptionNo: string;
|
||||||
|
subscriptionId: number;
|
||||||
|
qrcodeBase64: string;
|
||||||
|
productName: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态返回结果
|
||||||
|
*/
|
||||||
|
export interface CheckStatusResult {
|
||||||
|
paid: boolean;
|
||||||
|
payStatus: number;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
payTime?: string;
|
||||||
|
transactionId?: string;
|
||||||
|
id: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
productId: number;
|
||||||
|
productName?: string;
|
||||||
|
productLogo?: string;
|
||||||
|
priceType?: PriceType;
|
||||||
|
payPrice?: number;
|
||||||
|
subscriptionPeriod?: SubscriptionPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 余额支付返回结果
|
||||||
|
*/
|
||||||
|
export interface PayResult {
|
||||||
|
paid: boolean;
|
||||||
|
balance?: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 微信 Native 支付(Web 端)返回结果
|
||||||
|
*/
|
||||||
|
export interface WechatNativePayResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
miniappQrcode: string;
|
||||||
|
miniappPagePath: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付返回结果
|
||||||
|
*/
|
||||||
|
export interface MpPrepayResult {
|
||||||
|
subscriptionId: number;
|
||||||
|
subscriptionNo: string;
|
||||||
|
outTradeNo: string;
|
||||||
|
timeStamp: string;
|
||||||
|
nonceStr: string;
|
||||||
|
package: string;
|
||||||
|
signType: string;
|
||||||
|
paySign: string;
|
||||||
|
payPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取余额返回结果
|
||||||
|
*/
|
||||||
|
export interface BalanceResult {
|
||||||
|
balance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序预支付参数
|
||||||
|
*/
|
||||||
|
export interface MpPrepayParam {
|
||||||
|
openid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付确认参数
|
||||||
|
*/
|
||||||
|
export interface MpConfirmParam {
|
||||||
|
transactionId?: string;
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsAd, CmsAdParam } from './model';
|
import type { CmsAd, CmsAdParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询广告位
|
* 分页查询广告位
|
||||||
*/
|
*/
|
||||||
export async function pageCmsAd(params: CmsAdParam) {
|
export async function pageCmsAd(params: CmsAdParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsAd>>>(
|
const res = await request.get<ApiResult<PageResult<CmsAd>>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad/page',
|
CMS_API_BASE_URL + '/cms/cms-ad/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsAd(params: CmsAdParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsAd(params?: CmsAdParam) {
|
export async function listCmsAd(params?: CmsAdParam) {
|
||||||
const res = await request.get<ApiResult<CmsAd[]>>(
|
const res = await request.get<ApiResult<CmsAd[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad',
|
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsAd(params?: CmsAdParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsAd(data: CmsAd) {
|
export async function addCmsAd(data: CmsAd) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad',
|
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsAd(data: CmsAd) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsAd(data: CmsAd) {
|
export async function updateCmsAd(data: CmsAd) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad',
|
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsAd(data: CmsAd) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsAd(id?: number) {
|
export async function removeCmsAd(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad/' + id
|
CMS_API_BASE_URL + '/cms/cms-ad/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsAd(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsAd(data: (number | undefined)[]) {
|
export async function removeBatchCmsAd(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad/batch',
|
CMS_API_BASE_URL + '/cms/cms-ad/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsAd(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsAd(id: number) {
|
export async function getCmsAd(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsAd>>(
|
const res = await request.get<ApiResult<CmsAd>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad/' + id
|
CMS_API_BASE_URL + '/cms/cms-ad/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsAdRecord, CmsAdRecordParam } from './model';
|
import type { CmsAdRecord, CmsAdRecordParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询广告图片
|
* 分页查询广告图片
|
||||||
*/
|
*/
|
||||||
export async function pageCmsAdRecord(params: CmsAdRecordParam) {
|
export async function pageCmsAdRecord(params: CmsAdRecordParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsAdRecord>>>(
|
const res = await request.get<ApiResult<PageResult<CmsAdRecord>>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record/page',
|
CMS_API_BASE_URL + '/cms/cms-ad-record/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsAdRecord(params: CmsAdRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsAdRecord(params?: CmsAdRecordParam) {
|
export async function listCmsAdRecord(params?: CmsAdRecordParam) {
|
||||||
const res = await request.get<ApiResult<CmsAdRecord[]>>(
|
const res = await request.get<ApiResult<CmsAdRecord[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record',
|
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsAdRecord(params?: CmsAdRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsAdRecord(data: CmsAdRecord) {
|
export async function addCmsAdRecord(data: CmsAdRecord) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record',
|
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsAdRecord(data: CmsAdRecord) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsAdRecord(data: CmsAdRecord) {
|
export async function updateCmsAdRecord(data: CmsAdRecord) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record',
|
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsAdRecord(data: CmsAdRecord) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsAdRecord(id?: number) {
|
export async function removeCmsAdRecord(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record/' + id
|
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsAdRecord(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
|
export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record/batch',
|
CMS_API_BASE_URL + '/cms/cms-ad-record/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsAdRecord(id: number) {
|
export async function getCmsAdRecord(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsAdRecord>>(
|
const res = await request.get<ApiResult<CmsAdRecord>>(
|
||||||
MODULES_API_URL + '/cms/cms-ad-record/' + id
|
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticle, CmsArticleParam } from './model';
|
import type { CmsArticle, CmsArticleParam } from './model';
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ import type { CmsArticle, CmsArticleParam } from './model';
|
|||||||
*/
|
*/
|
||||||
export async function pageCmsArticle(params: CmsArticleParam) {
|
export async function pageCmsArticle(params: CmsArticleParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticle>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticle>>>(
|
||||||
'/cms/cms-article/page',
|
CMS_API_BASE_URL + '/cms/cms-article/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -22,7 +23,7 @@ export async function pageCmsArticle(params: CmsArticleParam) {
|
|||||||
* 查询文章列表
|
* 查询文章列表
|
||||||
*/
|
*/
|
||||||
export async function listCmsArticle(params?: CmsArticleParam) {
|
export async function listCmsArticle(params?: CmsArticleParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticle[]>>('/cms/cms-article', {
|
const res = await request.get<ApiResult<CmsArticle[]>>(CMS_API_BASE_URL + '/cms/cms-article', {
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
@@ -35,7 +36,7 @@ export async function listCmsArticle(params?: CmsArticleParam) {
|
|||||||
* 添加文章
|
* 添加文章
|
||||||
*/
|
*/
|
||||||
export async function addCmsArticle(data: CmsArticle) {
|
export async function addCmsArticle(data: CmsArticle) {
|
||||||
const res = await request.post<ApiResult<unknown>>('/cms/cms-article', data);
|
const res = await request.post<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
}
|
}
|
||||||
@@ -46,7 +47,7 @@ export async function addCmsArticle(data: CmsArticle) {
|
|||||||
* 修改文章
|
* 修改文章
|
||||||
*/
|
*/
|
||||||
export async function updateCmsArticle(data: CmsArticle) {
|
export async function updateCmsArticle(data: CmsArticle) {
|
||||||
const res = await request.put<ApiResult<unknown>>('/cms/cms-article', data);
|
const res = await request.put<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
}
|
}
|
||||||
@@ -58,7 +59,7 @@ export async function updateCmsArticle(data: CmsArticle) {
|
|||||||
*/
|
*/
|
||||||
export async function updateBatchCmsArticle(data: any) {
|
export async function updateBatchCmsArticle(data: any) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/cms/cms-article/batch',
|
CMS_API_BASE_URL + '/cms/cms-article/batch',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -72,7 +73,7 @@ export async function updateBatchCmsArticle(data: any) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticle(id?: number) {
|
export async function removeCmsArticle(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
'/cms/cms-article/' + id
|
CMS_API_BASE_URL + '/cms/cms-article/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -85,7 +86,7 @@ export async function removeCmsArticle(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsArticle(data: (number | undefined)[]) {
|
export async function removeBatchCmsArticle(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
'/cms/cms-article/batch',
|
CMS_API_BASE_URL + '/cms/cms-article/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ export async function removeBatchCmsArticle(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticle(id: number) {
|
export async function getCmsArticle(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticle>>(
|
const res = await request.get<ApiResult<CmsArticle>>(
|
||||||
'/cms/cms-article/' + id
|
CMS_API_BASE_URL + '/cms/cms-article/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -114,7 +115,7 @@ export async function getCmsArticle(id: number) {
|
|||||||
*/
|
*/
|
||||||
export async function getByCode(code: string) {
|
export async function getByCode(code: string) {
|
||||||
const res = await request.get<ApiResult<CmsArticle>>(
|
const res = await request.get<ApiResult<CmsArticle>>(
|
||||||
'/cms/cms-article/getByCode/' + code
|
CMS_API_BASE_URL + '/cms/cms-article/getByCode/' + code
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -123,7 +124,7 @@ export async function getByCode(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCount(params: CmsArticleParam) {
|
export async function getCount(params: CmsArticleParam) {
|
||||||
const res = await request.get('/cms/cms-article/data', {
|
const res = await request.get(CMS_API_BASE_URL + '/cms/cms-article/data', {
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -139,7 +140,7 @@ export async function importArticles(file: File) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
'/cms/cms-article/import',
|
CMS_API_BASE_URL + '/cms/cms-article/import',
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticleCategory, CmsArticleCategoryParam } from './model';
|
import type { CmsArticleCategory, CmsArticleCategoryParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询文章分类表
|
* 分页查询文章分类表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
|
export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticleCategory>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticleCategory>>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category/page',
|
CMS_API_BASE_URL + '/cms/cms-article-category/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
|
export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticleCategory[]>>(
|
const res = await request.get<ApiResult<CmsArticleCategory[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category',
|
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsArticleCategory(data: CmsArticleCategory) {
|
export async function addCmsArticleCategory(data: CmsArticleCategory) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category',
|
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsArticleCategory(data: CmsArticleCategory) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsArticleCategory(data: CmsArticleCategory) {
|
export async function updateCmsArticleCategory(data: CmsArticleCategory) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category',
|
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleCategory(data: CmsArticleCategory) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticleCategory(id?: number) {
|
export async function removeCmsArticleCategory(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleCategory(
|
|||||||
data: (number | undefined)[]
|
data: (number | undefined)[]
|
||||||
) {
|
) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category/batch',
|
CMS_API_BASE_URL + '/cms/cms-article-category/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleCategory(
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticleCategory(id: number) {
|
export async function getCmsArticleCategory(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticleCategory>>(
|
const res = await request.get<ApiResult<CmsArticleCategory>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-category/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticleComment, CmsArticleCommentParam } from './model';
|
import type { CmsArticleComment, CmsArticleCommentParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询文章评论表
|
* 分页查询文章评论表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
|
export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticleComment>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticleComment>>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment/page',
|
CMS_API_BASE_URL + '/cms/cms-article-comment/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
|
export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticleComment[]>>(
|
const res = await request.get<ApiResult<CmsArticleComment[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment',
|
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsArticleComment(data: CmsArticleComment) {
|
export async function addCmsArticleComment(data: CmsArticleComment) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment',
|
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsArticleComment(data: CmsArticleComment) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsArticleComment(data: CmsArticleComment) {
|
export async function updateCmsArticleComment(data: CmsArticleComment) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment',
|
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleComment(data: CmsArticleComment) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticleComment(id?: number) {
|
export async function removeCmsArticleComment(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleComment(
|
|||||||
data: (number | undefined)[]
|
data: (number | undefined)[]
|
||||||
) {
|
) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment/batch',
|
CMS_API_BASE_URL + '/cms/cms-article-comment/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleComment(
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticleComment(id: number) {
|
export async function getCmsArticleComment(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticleComment>>(
|
const res = await request.get<ApiResult<CmsArticleComment>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-comment/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticleContent, CmsArticleContentParam } from './model';
|
import type { CmsArticleContent, CmsArticleContentParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询文章记录表
|
* 分页查询文章记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsArticleContent(params: CmsArticleContentParam) {
|
export async function pageCmsArticleContent(params: CmsArticleContentParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticleContent>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticleContent>>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content/page',
|
CMS_API_BASE_URL + '/cms/cms-article-content/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleContent(params: CmsArticleContentParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsArticleContent(params?: CmsArticleContentParam) {
|
export async function listCmsArticleContent(params?: CmsArticleContentParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticleContent[]>>(
|
const res = await request.get<ApiResult<CmsArticleContent[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content',
|
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsArticleContent(params?: CmsArticleContentParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsArticleContent(data: CmsArticleContent) {
|
export async function addCmsArticleContent(data: CmsArticleContent) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content',
|
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsArticleContent(data: CmsArticleContent) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsArticleContent(data: CmsArticleContent) {
|
export async function updateCmsArticleContent(data: CmsArticleContent) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content',
|
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleContent(data: CmsArticleContent) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticleContent(id?: number) {
|
export async function removeCmsArticleContent(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleContent(
|
|||||||
data: (number | undefined)[]
|
data: (number | undefined)[]
|
||||||
) {
|
) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content/batch',
|
CMS_API_BASE_URL + '/cms/cms-article-content/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleContent(
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticleContent(id: number) {
|
export async function getCmsArticleContent(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticleContent>>(
|
const res = await request.get<ApiResult<CmsArticleContent>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-content/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticleCount, CmsArticleCountParam } from './model';
|
import type { CmsArticleCount, CmsArticleCountParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询点赞文章
|
* 分页查询点赞文章
|
||||||
*/
|
*/
|
||||||
export async function pageCmsArticleCount(params: CmsArticleCountParam) {
|
export async function pageCmsArticleCount(params: CmsArticleCountParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticleCount>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticleCount>>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count/page',
|
CMS_API_BASE_URL + '/cms/cms-article-count/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleCount(params: CmsArticleCountParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsArticleCount(params?: CmsArticleCountParam) {
|
export async function listCmsArticleCount(params?: CmsArticleCountParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticleCount[]>>(
|
const res = await request.get<ApiResult<CmsArticleCount[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count',
|
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsArticleCount(params?: CmsArticleCountParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsArticleCount(data: CmsArticleCount) {
|
export async function addCmsArticleCount(data: CmsArticleCount) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count',
|
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsArticleCount(data: CmsArticleCount) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsArticleCount(data: CmsArticleCount) {
|
export async function updateCmsArticleCount(data: CmsArticleCount) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count',
|
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleCount(data: CmsArticleCount) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticleCount(id?: number) {
|
export async function removeCmsArticleCount(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsArticleCount(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
|
export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count/batch',
|
CMS_API_BASE_URL + '/cms/cms-article-count/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticleCount(id: number) {
|
export async function getCmsArticleCount(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticleCount>>(
|
const res = await request.get<ApiResult<CmsArticleCount>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-count/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsArticleLike, CmsArticleLikeParam } from './model';
|
import type { CmsArticleLike, CmsArticleLikeParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询点赞文章
|
* 分页查询点赞文章
|
||||||
*/
|
*/
|
||||||
export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
|
export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsArticleLike>>>(
|
const res = await request.get<ApiResult<PageResult<CmsArticleLike>>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like/page',
|
CMS_API_BASE_URL + '/cms/cms-article-like/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
|
export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
|
||||||
const res = await request.get<ApiResult<CmsArticleLike[]>>(
|
const res = await request.get<ApiResult<CmsArticleLike[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like',
|
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsArticleLike(data: CmsArticleLike) {
|
export async function addCmsArticleLike(data: CmsArticleLike) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like',
|
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsArticleLike(data: CmsArticleLike) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsArticleLike(data: CmsArticleLike) {
|
export async function updateCmsArticleLike(data: CmsArticleLike) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like',
|
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleLike(data: CmsArticleLike) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsArticleLike(id?: number) {
|
export async function removeCmsArticleLike(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsArticleLike(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
|
export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like/batch',
|
CMS_API_BASE_URL + '/cms/cms-article-like/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsArticleLike(id: number) {
|
export async function getCmsArticleLike(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsArticleLike>>(
|
const res = await request.get<ApiResult<CmsArticleLike>>(
|
||||||
MODULES_API_URL + '/cms/cms-article-like/' + id
|
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsDesign, CmsDesignParam } from './model';
|
import type { CmsDesign, CmsDesignParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询页面管理记录表
|
* 分页查询页面管理记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsDesign(params: CmsDesignParam) {
|
export async function pageCmsDesign(params: CmsDesignParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsDesign>>>(
|
const res = await request.get<ApiResult<PageResult<CmsDesign>>>(
|
||||||
MODULES_API_URL + '/cms/cms-design/page',
|
CMS_API_BASE_URL + '/cms/cms-design/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsDesign(params: CmsDesignParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsDesign(params?: CmsDesignParam) {
|
export async function listCmsDesign(params?: CmsDesignParam) {
|
||||||
const res = await request.get<ApiResult<CmsDesign[]>>(
|
const res = await request.get<ApiResult<CmsDesign[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-design',
|
CMS_API_BASE_URL + '/cms/cms-design',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsDesign(params?: CmsDesignParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsDesign(data: CmsDesign) {
|
export async function addCmsDesign(data: CmsDesign) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-design',
|
CMS_API_BASE_URL + '/cms/cms-design',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsDesign(data: CmsDesign) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsDesign(data: CmsDesign) {
|
export async function updateCmsDesign(data: CmsDesign) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-design',
|
CMS_API_BASE_URL + '/cms/cms-design',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsDesign(data: CmsDesign) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsDesign(id?: number) {
|
export async function removeCmsDesign(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-design/' + id
|
CMS_API_BASE_URL + '/cms/cms-design/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsDesign(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsDesign(data: (number | undefined)[]) {
|
export async function removeBatchCmsDesign(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-design/batch',
|
CMS_API_BASE_URL + '/cms/cms-design/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsDesign(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsDesign(id: number) {
|
export async function getCmsDesign(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsDesign>>(
|
const res = await request.get<ApiResult<CmsDesign>>(
|
||||||
MODULES_API_URL + '/cms/cms-design/' + id
|
CMS_API_BASE_URL + '/cms/cms-design/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsDomain, CmsDomainParam } from './model';
|
import type { CmsDomain, CmsDomainParam } from './model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询网站域名记录表
|
* 分页查询网站域名记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsDomain(params: CmsDomainParam) {
|
export async function pageCmsDomain(params: CmsDomainParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsDomain>>>(
|
const res = await request.get<ApiResult<PageResult<CmsDomain>>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/page',
|
CMS_API_BASE_URL + '/cms/cms-domain/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsDomain(params: CmsDomainParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsDomain(params?: CmsDomainParam) {
|
export async function listCmsDomain(params?: CmsDomainParam) {
|
||||||
const res = await request.get<ApiResult<CmsDomain[]>>(
|
const res = await request.get<ApiResult<CmsDomain[]>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain',
|
CMS_API_BASE_URL + '/cms/cms-domain',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsDomain(params?: CmsDomainParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsDomain(data: CmsDomain) {
|
export async function addCmsDomain(data: CmsDomain) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain',
|
CMS_API_BASE_URL + '/cms/cms-domain',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsDomain(data: CmsDomain) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsDomain(data: CmsDomain) {
|
export async function updateCmsDomain(data: CmsDomain) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/domain',
|
CMS_API_BASE_URL + '/cms/cms-domain/domain',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsDomain(data: CmsDomain) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsDomain(id?: number) {
|
export async function removeCmsDomain(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/' + id
|
CMS_API_BASE_URL + '/cms/cms-domain/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsDomain(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsDomain(data: (number | undefined)[]) {
|
export async function removeBatchCmsDomain(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/batch',
|
CMS_API_BASE_URL + '/cms/cms-domain/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsDomain(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsDomain(id: number) {
|
export async function getCmsDomain(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsDomain>>(
|
const res = await request.get<ApiResult<CmsDomain>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/' + id
|
CMS_API_BASE_URL + '/cms/cms-domain/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -114,7 +114,7 @@ export async function checkExistence(
|
|||||||
id?: number
|
id?: number
|
||||||
) {
|
) {
|
||||||
const res = await request.get<ApiResult<unknown>>(
|
const res = await request.get<ApiResult<unknown>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/existence',
|
CMS_API_BASE_URL + '/cms/cms-domain/existence',
|
||||||
{
|
{
|
||||||
params: { field, value, id }
|
params: { field, value, id }
|
||||||
}
|
}
|
||||||
@@ -127,7 +127,7 @@ export async function checkExistence(
|
|||||||
|
|
||||||
export async function resolvable(id: number) {
|
export async function resolvable(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsDomain>>(
|
const res = await request.get<ApiResult<CmsDomain>>(
|
||||||
SERVER_API_URL + '/cms/cms-domain/resolvable/' + id
|
CMS_API_BASE_URL + '/cms/cms-domain/resolvable/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data) {
|
if (res.data.code === 0 && res.data) {
|
||||||
return res.data;
|
return res.data;
|
||||||
@@ -143,7 +143,7 @@ export async function getAuthorizedDomain() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const res = await request.get<ApiResult<CmsDomain>>(
|
const res = await request.get<ApiResult<CmsDomain>>(
|
||||||
SERVER_API_URL +
|
CMS_API_BASE_URL +
|
||||||
'/cms/cms-domain/getAuthorizedDomain/' +
|
'/cms/cms-domain/getAuthorizedDomain/' +
|
||||||
localStorage.getItem('WebsiteId')
|
localStorage.getItem('WebsiteId')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsForm, CmsFormParam } from './model';
|
import type { CmsForm, CmsFormParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询表单设计表
|
* 分页查询表单设计表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsForm(params: CmsFormParam) {
|
export async function pageCmsForm(params: CmsFormParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsForm>>>(
|
const res = await request.get<ApiResult<PageResult<CmsForm>>>(
|
||||||
MODULES_API_URL + '/cms/cms-form/page',
|
CMS_API_BASE_URL + '/cms/cms-form/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsForm(params: CmsFormParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsForm(params?: CmsFormParam) {
|
export async function listCmsForm(params?: CmsFormParam) {
|
||||||
const res = await request.get<ApiResult<CmsForm[]>>(
|
const res = await request.get<ApiResult<CmsForm[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-form',
|
CMS_API_BASE_URL + '/cms/cms-form',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsForm(params?: CmsFormParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsForm(data: CmsForm) {
|
export async function addCmsForm(data: CmsForm) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form',
|
CMS_API_BASE_URL + '/cms/cms-form',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsForm(data: CmsForm) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsForm(data: CmsForm) {
|
export async function updateCmsForm(data: CmsForm) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form',
|
CMS_API_BASE_URL + '/cms/cms-form',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsForm(data: CmsForm) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsForm(id?: number) {
|
export async function removeCmsForm(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form/' + id
|
CMS_API_BASE_URL + '/cms/cms-form/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsForm(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsForm(data: (number | undefined)[]) {
|
export async function removeBatchCmsForm(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form/batch',
|
CMS_API_BASE_URL + '/cms/cms-form/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsForm(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsForm(id: number) {
|
export async function getCmsForm(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsForm>>(
|
const res = await request.get<ApiResult<CmsForm>>(
|
||||||
MODULES_API_URL + '/cms/cms-form/' + id
|
CMS_API_BASE_URL + '/cms/cms-form/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsFormRecord, CmsFormRecordParam } from './model';
|
import type { CmsFormRecord, CmsFormRecordParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询表单数据记录表
|
* 分页查询表单数据记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsFormRecord(params: CmsFormRecordParam) {
|
export async function pageCmsFormRecord(params: CmsFormRecordParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsFormRecord>>>(
|
const res = await request.get<ApiResult<PageResult<CmsFormRecord>>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record/page',
|
CMS_API_BASE_URL + '/cms/cms-form-record/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsFormRecord(params: CmsFormRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsFormRecord(params?: CmsFormRecordParam) {
|
export async function listCmsFormRecord(params?: CmsFormRecordParam) {
|
||||||
const res = await request.get<ApiResult<CmsFormRecord[]>>(
|
const res = await request.get<ApiResult<CmsFormRecord[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record',
|
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsFormRecord(params?: CmsFormRecordParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsFormRecord(data: CmsFormRecord) {
|
export async function addCmsFormRecord(data: CmsFormRecord) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record',
|
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsFormRecord(data: CmsFormRecord) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsFormRecord(data: CmsFormRecord) {
|
export async function updateCmsFormRecord(data: CmsFormRecord) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record',
|
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsFormRecord(data: CmsFormRecord) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsFormRecord(id?: number) {
|
export async function removeCmsFormRecord(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record/' + id
|
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsFormRecord(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
|
export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record/batch',
|
CMS_API_BASE_URL + '/cms/cms-form-record/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsFormRecord(id: number) {
|
export async function getCmsFormRecord(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsFormRecord>>(
|
const res = await request.get<ApiResult<CmsFormRecord>>(
|
||||||
MODULES_API_URL + '/cms/cms-form-record/' + id
|
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsLang, CmsLangParam } from './model';
|
import type { CmsLang, CmsLangParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询国际化
|
* 分页查询国际化
|
||||||
*/
|
*/
|
||||||
export async function pageCmsLang(params: CmsLangParam) {
|
export async function pageCmsLang(params: CmsLangParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsLang>>>(
|
const res = await request.get<ApiResult<PageResult<CmsLang>>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang/page',
|
CMS_API_BASE_URL + '/cms/cms-lang/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsLang(params: CmsLangParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsLang(params?: CmsLangParam) {
|
export async function listCmsLang(params?: CmsLangParam) {
|
||||||
const res = await request.get<ApiResult<CmsLang[]>>(
|
const res = await request.get<ApiResult<CmsLang[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang',
|
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsLang(params?: CmsLangParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsLang(data: CmsLang) {
|
export async function addCmsLang(data: CmsLang) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang',
|
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsLang(data: CmsLang) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsLang(data: CmsLang) {
|
export async function updateCmsLang(data: CmsLang) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang',
|
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsLang(data: CmsLang) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsLang(id?: number) {
|
export async function removeCmsLang(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang/' + id
|
CMS_API_BASE_URL + '/cms/cms-lang/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsLang(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsLang(data: (number | undefined)[]) {
|
export async function removeBatchCmsLang(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang/batch',
|
CMS_API_BASE_URL + '/cms/cms-lang/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsLang(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsLang(id: number) {
|
export async function getCmsLang(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsLang>>(
|
const res = await request.get<ApiResult<CmsLang>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang/' + id
|
CMS_API_BASE_URL + '/cms/cms-lang/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsLangLog, CmsLangLogParam } from './model';
|
import type { CmsLangLog, CmsLangLogParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询国际化记录启用
|
* 分页查询国际化记录启用
|
||||||
*/
|
*/
|
||||||
export async function pageCmsLangLog(params: CmsLangLogParam) {
|
export async function pageCmsLangLog(params: CmsLangLogParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsLangLog>>>(
|
const res = await request.get<ApiResult<PageResult<CmsLangLog>>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log/page',
|
CMS_API_BASE_URL + '/cms/cms-lang-log/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsLangLog(params: CmsLangLogParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsLangLog(params?: CmsLangLogParam) {
|
export async function listCmsLangLog(params?: CmsLangLogParam) {
|
||||||
const res = await request.get<ApiResult<CmsLangLog[]>>(
|
const res = await request.get<ApiResult<CmsLangLog[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log',
|
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsLangLog(params?: CmsLangLogParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsLangLog(data: CmsLangLog) {
|
export async function addCmsLangLog(data: CmsLangLog) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log',
|
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsLangLog(data: CmsLangLog) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsLangLog(data: CmsLangLog) {
|
export async function updateCmsLangLog(data: CmsLangLog) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log',
|
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsLangLog(data: CmsLangLog) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsLangLog(id?: number) {
|
export async function removeCmsLangLog(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log/' + id
|
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsLangLog(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
|
export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log/batch',
|
CMS_API_BASE_URL + '/cms/cms-lang-log/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsLangLog(id: number) {
|
export async function getCmsLangLog(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsLangLog>>(
|
const res = await request.get<ApiResult<CmsLangLog>>(
|
||||||
MODULES_API_URL + '/cms/cms-lang-log/' + id
|
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsLink, CmsLinkParam } from './model';
|
import type { CmsLink, CmsLinkParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询常用链接
|
* 分页查询常用链接
|
||||||
*/
|
*/
|
||||||
export async function pageCmsLink(params: CmsLinkParam) {
|
export async function pageCmsLink(params: CmsLinkParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsLink>>>(
|
const res = await request.get<ApiResult<PageResult<CmsLink>>>(
|
||||||
MODULES_API_URL + '/cms/cms-link/page',
|
CMS_API_BASE_URL + '/cms/cms-link/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsLink(params: CmsLinkParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsLink(params?: CmsLinkParam) {
|
export async function listCmsLink(params?: CmsLinkParam) {
|
||||||
const res = await request.get<ApiResult<CmsLink[]>>(
|
const res = await request.get<ApiResult<CmsLink[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-link',
|
CMS_API_BASE_URL + '/cms/cms-link',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsLink(params?: CmsLinkParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsLink(data: CmsLink) {
|
export async function addCmsLink(data: CmsLink) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-link',
|
CMS_API_BASE_URL + '/cms/cms-link',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsLink(data: CmsLink) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsLink(data: CmsLink) {
|
export async function updateCmsLink(data: CmsLink) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-link',
|
CMS_API_BASE_URL + '/cms/cms-link',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsLink(data: CmsLink) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsLink(id?: number) {
|
export async function removeCmsLink(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-link/' + id
|
CMS_API_BASE_URL + '/cms/cms-link/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsLink(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function updateBatchCmsLink(data: any) {
|
export async function updateBatchCmsLink(data: any) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-link/batch',
|
CMS_API_BASE_URL + '/cms/cms-link/batch',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -95,7 +95,7 @@ export async function updateBatchCmsLink(data: any) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsLink(data: (number | undefined)[]) {
|
export async function removeBatchCmsLink(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-link/batch',
|
CMS_API_BASE_URL + '/cms/cms-link/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export async function removeBatchCmsLink(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsLink(id: number) {
|
export async function getCmsLink(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsLink>>(
|
const res = await request.get<ApiResult<CmsLink>>(
|
||||||
MODULES_API_URL + '/cms/cms-link/' + id
|
CMS_API_BASE_URL + '/cms/cms-link/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsModel, CmsModelParam } from './model';
|
import type { CmsModel, CmsModelParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询模型
|
* 分页查询模型
|
||||||
*/
|
*/
|
||||||
export async function pageCmsModel(params: CmsModelParam) {
|
export async function pageCmsModel(params: CmsModelParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsModel>>>(
|
const res = await request.get<ApiResult<PageResult<CmsModel>>>(
|
||||||
MODULES_API_URL + '/cms/cms-model/page',
|
CMS_API_BASE_URL + '/cms/cms-model/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsModel(params: CmsModelParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsModel(params?: CmsModelParam) {
|
export async function listCmsModel(params?: CmsModelParam) {
|
||||||
const res = await request.get<ApiResult<CmsModel[]>>(
|
const res = await request.get<ApiResult<CmsModel[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-model',
|
CMS_API_BASE_URL + '/cms/cms-model',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsModel(params?: CmsModelParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsModel(data: CmsModel) {
|
export async function addCmsModel(data: CmsModel) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-model',
|
CMS_API_BASE_URL + '/cms/cms-model',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsModel(data: CmsModel) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsModel(data: CmsModel) {
|
export async function updateCmsModel(data: CmsModel) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-model',
|
CMS_API_BASE_URL + '/cms/cms-model',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsModel(data: CmsModel) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsModel(id?: number) {
|
export async function removeCmsModel(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-model/' + id
|
CMS_API_BASE_URL + '/cms/cms-model/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsModel(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsModel(data: (number | undefined)[]) {
|
export async function removeBatchCmsModel(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-model/batch',
|
CMS_API_BASE_URL + '/cms/cms-model/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsModel(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsModel(id: number) {
|
export async function getCmsModel(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsModel>>(
|
const res = await request.get<ApiResult<CmsModel>>(
|
||||||
MODULES_API_URL + '/cms/cms-model/' + id
|
CMS_API_BASE_URL + '/cms/cms-model/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsNavigation, CmsNavigationParam } from './model';
|
import type { CmsNavigation, CmsNavigationParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询网站导航记录表
|
* 分页查询网站导航记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsNavigation(params: CmsNavigationParam) {
|
export async function pageCmsNavigation(params: CmsNavigationParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsNavigation>>>(
|
const res = await request.get<ApiResult<PageResult<CmsNavigation>>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/page',
|
CMS_API_BASE_URL + '/cms/cms-navigation/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsNavigation(params: CmsNavigationParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsNavigation(params?: CmsNavigationParam) {
|
export async function listCmsNavigation(params?: CmsNavigationParam) {
|
||||||
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation',
|
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsNavigation(params?: CmsNavigationParam) {
|
|||||||
*/
|
*/
|
||||||
export async function treeNavigation(params?: CmsNavigationParam) {
|
export async function treeNavigation(params?: CmsNavigationParam) {
|
||||||
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/tree',
|
CMS_API_BASE_URL + '/cms/cms-navigation/tree',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ export async function treeNavigation(params?: CmsNavigationParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsNavigation(data: CmsNavigation) {
|
export async function addCmsNavigation(data: CmsNavigation) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation',
|
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -69,7 +69,7 @@ export async function addCmsNavigation(data: CmsNavigation) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsNavigation(data: CmsNavigation) {
|
export async function updateCmsNavigation(data: CmsNavigation) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation',
|
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -83,7 +83,7 @@ export async function updateCmsNavigation(data: CmsNavigation) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsNavigation(id?: number) {
|
export async function removeCmsNavigation(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/' + id
|
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -96,7 +96,7 @@ export async function removeCmsNavigation(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
|
export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/batch',
|
CMS_API_BASE_URL + '/cms/cms-navigation/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsNavigation(id: number) {
|
export async function getCmsNavigation(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/' + id
|
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -125,7 +125,7 @@ export async function getCmsNavigation(id: number) {
|
|||||||
*/
|
*/
|
||||||
export async function getByCode(code: string) {
|
export async function getByCode(code: string) {
|
||||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||||
'/cms/cms-navigation/getByCode/' + code
|
CMS_API_BASE_URL + '/cms/cms-navigation/getByCode/' + code
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -140,7 +140,7 @@ export async function importCmsNavigation(file: File) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-navigation/import',
|
CMS_API_BASE_URL + '/cms/cms-navigation/import',
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsOrder, CmsOrderParam } from './model';
|
import type { CmsOrder, CmsOrderParam } from './model';
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ import type { CmsOrder, CmsOrderParam } from './model';
|
|||||||
*/
|
*/
|
||||||
export async function pageCmsOrder(params: CmsOrderParam) {
|
export async function pageCmsOrder(params: CmsOrderParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsOrder>>>(
|
const res = await request.get<ApiResult<PageResult<CmsOrder>>>(
|
||||||
'/cms/cms-order/page',
|
CMS_API_BASE_URL + '/cms/cms-order/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -23,7 +24,7 @@ export async function pageCmsOrder(params: CmsOrderParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsOrder(params?: CmsOrderParam) {
|
export async function listCmsOrder(params?: CmsOrderParam) {
|
||||||
const res = await request.get<ApiResult<CmsOrder[]>>(
|
const res = await request.get<ApiResult<CmsOrder[]>>(
|
||||||
'/cms/cms-order',
|
CMS_API_BASE_URL + '/cms/cms-order',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -39,7 +40,7 @@ export async function listCmsOrder(params?: CmsOrderParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsOrder(data: CmsOrder) {
|
export async function addCmsOrder(data: CmsOrder) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
'/cms/cms-order',
|
CMS_API_BASE_URL + '/cms/cms-order',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -53,7 +54,7 @@ export async function addCmsOrder(data: CmsOrder) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsOrder(data: CmsOrder) {
|
export async function updateCmsOrder(data: CmsOrder) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
'/cms/cms-order',
|
CMS_API_BASE_URL + '/cms/cms-order',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -67,7 +68,7 @@ export async function updateCmsOrder(data: CmsOrder) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsOrder(id?: number) {
|
export async function removeCmsOrder(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
'/cms/cms-order/' + id
|
CMS_API_BASE_URL + '/cms/cms-order/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -80,7 +81,7 @@ export async function removeCmsOrder(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsOrder(data: (number | undefined)[]) {
|
export async function removeBatchCmsOrder(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
'/cms/cms-order/batch',
|
CMS_API_BASE_URL + '/cms/cms-order/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -96,7 +97,7 @@ export async function removeBatchCmsOrder(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsOrder(id: number) {
|
export async function getCmsOrder(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsOrder>>(
|
const res = await request.get<ApiResult<CmsOrder>>(
|
||||||
'/cms/cms-order/' + id
|
CMS_API_BASE_URL + '/cms/cms-order/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsStatistics, CmsStatisticsParam } from './model';
|
import type { CmsStatistics, CmsStatisticsParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询站点统计信息表
|
* 分页查询站点统计信息表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsStatistics(params: CmsStatisticsParam) {
|
export async function pageCmsStatistics(params: CmsStatisticsParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsStatistics>>>(
|
const res = await request.get<ApiResult<PageResult<CmsStatistics>>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics/page',
|
CMS_API_BASE_URL + '/cms/cms-statistics/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsStatistics(params: CmsStatisticsParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsStatistics(params?: CmsStatisticsParam) {
|
export async function listCmsStatistics(params?: CmsStatisticsParam) {
|
||||||
const res = await request.get<ApiResult<CmsStatistics[]>>(
|
const res = await request.get<ApiResult<CmsStatistics[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics',
|
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsStatistics(params?: CmsStatisticsParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsStatistics(data: CmsStatistics) {
|
export async function addCmsStatistics(data: CmsStatistics) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics',
|
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsStatistics(data: CmsStatistics) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsStatistics(data: CmsStatistics) {
|
export async function updateCmsStatistics(data: CmsStatistics) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics',
|
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsStatistics(data: CmsStatistics) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsStatistics(id?: number) {
|
export async function removeCmsStatistics(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics/' + id
|
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsStatistics(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
|
export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics/batch',
|
CMS_API_BASE_URL + '/cms/cms-statistics/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsStatistics(id: number) {
|
export async function getCmsStatistics(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsStatistics>>(
|
const res = await request.get<ApiResult<CmsStatistics>>(
|
||||||
MODULES_API_URL + '/cms/cms-statistics/' + id
|
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsTemplate, CmsTemplateParam } from './model';
|
import type { CmsTemplate, CmsTemplateParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询网站模版
|
* 分页查询网站模版
|
||||||
*/
|
*/
|
||||||
export async function pageCmsTemplate(params: CmsTemplateParam) {
|
export async function pageCmsTemplate(params: CmsTemplateParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsTemplate>>>(
|
const res = await request.get<ApiResult<PageResult<CmsTemplate>>>(
|
||||||
MODULES_API_URL + '/cms/cms-template/page',
|
CMS_API_BASE_URL + '/cms/cms-template/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsTemplate(params: CmsTemplateParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsTemplate(params?: CmsTemplateParam) {
|
export async function listCmsTemplate(params?: CmsTemplateParam) {
|
||||||
const res = await request.get<ApiResult<CmsTemplate[]>>(
|
const res = await request.get<ApiResult<CmsTemplate[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-template',
|
CMS_API_BASE_URL + '/cms/cms-template',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsTemplate(params?: CmsTemplateParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsTemplate(data: CmsTemplate) {
|
export async function addCmsTemplate(data: CmsTemplate) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-template',
|
CMS_API_BASE_URL + '/cms/cms-template',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsTemplate(data: CmsTemplate) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsTemplate(data: CmsTemplate) {
|
export async function updateCmsTemplate(data: CmsTemplate) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-template',
|
CMS_API_BASE_URL + '/cms/cms-template',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsTemplate(data: CmsTemplate) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsTemplate(id?: number) {
|
export async function removeCmsTemplate(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-template/' + id
|
CMS_API_BASE_URL + '/cms/cms-template/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsTemplate(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
|
export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-template/batch',
|
CMS_API_BASE_URL + '/cms/cms-template/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsTemplate(id: number) {
|
export async function getCmsTemplate(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsTemplate>>(
|
const res = await request.get<ApiResult<CmsTemplate>>(
|
||||||
MODULES_API_URL + '/cms/cms-template/' + id
|
CMS_API_BASE_URL + '/cms/cms-template/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsWebsite, CmsWebsiteParam } from './model';
|
import type { CmsWebsite, CmsWebsiteParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询网站信息记录表
|
* 分页查询网站信息记录表
|
||||||
*/
|
*/
|
||||||
export async function pageCmsWebsite(params: CmsWebsiteParam) {
|
export async function pageCmsWebsite(params: CmsWebsiteParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsWebsite>>>(
|
const res = await request.get<ApiResult<PageResult<CmsWebsite>>>(
|
||||||
MODULES_API_URL + '/cms/cms-website/page',
|
CMS_API_BASE_URL + '/cms/cms-website/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsite(params: CmsWebsiteParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsWebsite(params?: CmsWebsiteParam) {
|
export async function listCmsWebsite(params?: CmsWebsiteParam) {
|
||||||
const res = await request.get<ApiResult<CmsWebsite[]>>(
|
const res = await request.get<ApiResult<CmsWebsite[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-website',
|
CMS_API_BASE_URL + '/cms/cms-website',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsWebsite(params?: CmsWebsiteParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsWebsite(data: CmsWebsite) {
|
export async function addCmsWebsite(data: CmsWebsite) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website',
|
CMS_API_BASE_URL + '/cms/cms-website',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsWebsite(data: CmsWebsite) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsWebsite(data: CmsWebsite) {
|
export async function updateCmsWebsite(data: CmsWebsite) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website',
|
CMS_API_BASE_URL + '/cms/cms-website',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsite(data: CmsWebsite) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsWebsite(id?: number) {
|
export async function removeCmsWebsite(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website/' + id
|
CMS_API_BASE_URL + '/cms/cms-website/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsWebsite(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
|
export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website/batch',
|
CMS_API_BASE_URL + '/cms/cms-website/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsWebsite(id: number) {
|
export async function getCmsWebsite(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsWebsite>>(
|
const res = await request.get<ApiResult<CmsWebsite>>(
|
||||||
MODULES_API_URL + '/cms/cms-website/' + id
|
CMS_API_BASE_URL + '/cms/cms-website/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -110,7 +110,7 @@ export async function getCmsWebsite(id: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeSiteInfoCache(key?: string) {
|
export async function removeSiteInfoCache(key?: string) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website/clearSiteInfo/' + key
|
CMS_API_BASE_URL + '/cms/cms-website/clearSiteInfo/' + key
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsWebsiteField, CmsWebsiteFieldParam, Config } from './model';
|
import type { CmsWebsiteField, CmsWebsiteFieldParam, Config } from './model';
|
||||||
import { MODULES_API_URL, TEMPLATE_ID } from '@/config/setting';
|
import { CMS_API_BASE_URL, TEMPLATE_ID } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询应用参数
|
* 分页查询应用参数
|
||||||
*/
|
*/
|
||||||
export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
|
export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsWebsiteField>>>(
|
const res = await request.get<ApiResult<PageResult<CmsWebsiteField>>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/page',
|
CMS_API_BASE_URL + '/cms/cms-website-field/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
|
export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
|
||||||
const res = await request.get<ApiResult<CmsWebsiteField[]>>(
|
const res = await request.get<ApiResult<CmsWebsiteField[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field',
|
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsWebsiteField(data: CmsWebsiteField) {
|
export async function addCmsWebsiteField(data: CmsWebsiteField) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field',
|
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsWebsiteField(data: CmsWebsiteField) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsWebsiteField(data: CmsWebsiteField) {
|
export async function updateCmsWebsiteField(data: CmsWebsiteField) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field',
|
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteField(data: CmsWebsiteField) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsWebsiteField(id?: number) {
|
export async function removeCmsWebsiteField(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/' + id
|
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeCmsWebsiteField(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
|
export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/batch',
|
CMS_API_BASE_URL + '/cms/cms-website-field/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsWebsiteField(id: number) {
|
export async function getCmsWebsiteField(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/' + id
|
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -110,7 +110,7 @@ export async function getCmsWebsiteField(id: number) {
|
|||||||
*/
|
*/
|
||||||
export async function getCmsWebsiteFieldByCode(code: string) {
|
export async function getCmsWebsiteFieldByCode(code: string) {
|
||||||
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/getByCode/' + code
|
CMS_API_BASE_URL + '/cms/cms-website-field/getByCode/' + code
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
@@ -123,7 +123,7 @@ export async function getCmsWebsiteFieldByCode(code: string) {
|
|||||||
*/
|
*/
|
||||||
export async function undeleteWebsiteField(id?: number) {
|
export async function undeleteWebsiteField(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/website-field/undelete/' + id
|
CMS_API_BASE_URL + '/cms/website-field/undelete/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -138,7 +138,7 @@ export async function importWebsiteField(file: File) {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/import',
|
CMS_API_BASE_URL + '/cms/cms-website-field/import',
|
||||||
formData
|
formData
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -152,7 +152,7 @@ export async function importWebsiteField(file: File) {
|
|||||||
*/
|
*/
|
||||||
export async function configWebsiteField(params?: CmsWebsiteFieldParam) {
|
export async function configWebsiteField(params?: CmsWebsiteFieldParam) {
|
||||||
const res = await request.get<ApiResult<Config>>(
|
const res = await request.get<ApiResult<Config>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-field/config',
|
CMS_API_BASE_URL + '/cms/cms-website-field/config',
|
||||||
{
|
{
|
||||||
params,
|
params,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { CmsWebsiteSetting, CmsWebsiteSettingParam } from './model';
|
import type { CmsWebsiteSetting, CmsWebsiteSettingParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询网站设置
|
* 分页查询网站设置
|
||||||
*/
|
*/
|
||||||
export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
|
export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<CmsWebsiteSetting>>>(
|
const res = await request.get<ApiResult<PageResult<CmsWebsiteSetting>>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting/page',
|
CMS_API_BASE_URL + '/cms/cms-website-setting/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
|
export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
|
||||||
const res = await request.get<ApiResult<CmsWebsiteSetting[]>>(
|
const res = await request.get<ApiResult<CmsWebsiteSetting[]>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting',
|
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting',
|
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
|||||||
*/
|
*/
|
||||||
export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting',
|
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
|||||||
*/
|
*/
|
||||||
export async function removeCmsWebsiteSetting(id?: number) {
|
export async function removeCmsWebsiteSetting(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting/' + id
|
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsWebsiteSetting(
|
|||||||
data: (number | undefined)[]
|
data: (number | undefined)[]
|
||||||
) {
|
) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting/batch',
|
CMS_API_BASE_URL + '/cms/cms-website-setting/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsWebsiteSetting(
|
|||||||
*/
|
*/
|
||||||
export async function getCmsWebsiteSetting(id: number) {
|
export async function getCmsWebsiteSetting(id: number) {
|
||||||
const res = await request.get<ApiResult<CmsWebsiteSetting>>(
|
const res = await request.get<ApiResult<CmsWebsiteSetting>>(
|
||||||
MODULES_API_URL + '/cms/cms-website-setting/' + id
|
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
return res.data.data;
|
return res.data.data;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
import type { Link, LinkParam } from './model';
|
import type { Link, LinkParam } from './model';
|
||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询链接
|
* 分页查询链接
|
||||||
*/
|
*/
|
||||||
export async function pageLink(params: LinkParam) {
|
export async function pageLink(params: LinkParam) {
|
||||||
const res = await request.get<ApiResult<PageResult<Link>>>(
|
const res = await request.get<ApiResult<PageResult<Link>>>(
|
||||||
MODULES_API_URL + '/oa/link/page',
|
CMS_API_BASE_URL + '/oa/link/page',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ export async function pageLink(params: LinkParam) {
|
|||||||
*/
|
*/
|
||||||
export async function listLink(params?: LinkParam) {
|
export async function listLink(params?: LinkParam) {
|
||||||
const res = await request.get<ApiResult<Link[]>>(
|
const res = await request.get<ApiResult<Link[]>>(
|
||||||
MODULES_API_URL + '/oa/link',
|
CMS_API_BASE_URL + '/oa/link',
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export async function listLink(params?: LinkParam) {
|
|||||||
*/
|
*/
|
||||||
export async function addLink(data: Link) {
|
export async function addLink(data: Link) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/oa/link',
|
CMS_API_BASE_URL + '/oa/link',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -54,7 +54,7 @@ export async function addLink(data: Link) {
|
|||||||
*/
|
*/
|
||||||
export async function updateLink(data: Link) {
|
export async function updateLink(data: Link) {
|
||||||
const res = await request.put<ApiResult<unknown>>(
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/oa/link',
|
CMS_API_BASE_URL + '/oa/link',
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
@@ -68,7 +68,7 @@ export async function updateLink(data: Link) {
|
|||||||
*/
|
*/
|
||||||
export async function removeLink(id?: number) {
|
export async function removeLink(id?: number) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/oa/link/' + id
|
CMS_API_BASE_URL + '/oa/link/' + id
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
@@ -81,7 +81,7 @@ export async function removeLink(id?: number) {
|
|||||||
*/
|
*/
|
||||||
export async function removeBatchLink(data: (number | undefined)[]) {
|
export async function removeBatchLink(data: (number | undefined)[]) {
|
||||||
const res = await request.delete<ApiResult<unknown>>(
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/oa/link/batch',
|
CMS_API_BASE_URL + '/oa/link/batch',
|
||||||
{
|
{
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export async function checkExistence(
|
|||||||
id?: number
|
id?: number
|
||||||
) {
|
) {
|
||||||
const res = await request.get<ApiResult<unknown>>(
|
const res = await request.get<ApiResult<unknown>>(
|
||||||
MODULES_API_URL + '/oa/link/existence',
|
CMS_API_BASE_URL + '/oa/link/existence',
|
||||||
{
|
{
|
||||||
params: { field, value, id }
|
params: { field, value, id }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult } from '@/api';
|
import type { ApiResult } from '@/api';
|
||||||
import type { User } from '@/api/system/user/model';
|
import type { User } from '@/api/system/user/model';
|
||||||
import type { UpdatePasswordParam, NoticeResult } from './model';
|
import type { UpdatePasswordParam, ResetPasswordParam, NoticeResult } from './model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import { SERVER_API_URL } from '@/config/setting';
|
||||||
import { Company } from '@/api/system/company/model';
|
import { Company } from '@/api/system/company/model';
|
||||||
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
|
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
|
||||||
@@ -108,6 +108,24 @@ export async function updatePassword(
|
|||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找回/重置密码(未登录场景,登录页"忘记密码"入口)
|
||||||
|
* 后端对应 POST /resetPassword,支持手机或邮箱两种方式
|
||||||
|
*/
|
||||||
|
export async function resetPassword(
|
||||||
|
data: ResetPasswordParam
|
||||||
|
): Promise<string> {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
SERVER_API_URL + '/resetPassword',
|
||||||
|
data,
|
||||||
|
{ headers: { 'X-Tenant-Skip': '1' } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message ?? '密码重置成功';
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建完整网站并初始化
|
* 创建完整网站并初始化
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,6 +24,23 @@ export interface UpdatePasswordParam {
|
|||||||
oldPassword: string;
|
oldPassword: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找回/重置密码参数
|
||||||
|
* 手机找回时传 phone + smsCode,邮箱找回时传 email + emailCode
|
||||||
|
*/
|
||||||
|
export interface ResetPasswordParam {
|
||||||
|
/** 手机找回时必填 */
|
||||||
|
phone?: string;
|
||||||
|
smsCode?: string;
|
||||||
|
/** 邮箱找回时必填 */
|
||||||
|
email?: string;
|
||||||
|
emailCode?: string;
|
||||||
|
/** 新密码 */
|
||||||
|
newPassword: string;
|
||||||
|
/** 确认新密码 */
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知数据格式
|
* 通知数据格式
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,30 @@ import type {
|
|||||||
} from './model';
|
} from './model';
|
||||||
import { User } from '@/api/system/user/model';
|
import { User } from '@/api/system/user/model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import { SERVER_API_URL } from '@/config/setting';
|
||||||
|
import type { LoginOutcome, TenantOption } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录态落地:把后端返回的 access_token / 租户 / 用户 持久化到本地
|
||||||
|
*/
|
||||||
|
function saveLoginState(result: LoginResult | undefined, remember?: boolean) {
|
||||||
|
if (!result) return;
|
||||||
|
if (result.access_token) {
|
||||||
|
setToken(result.access_token, remember);
|
||||||
|
}
|
||||||
|
const user = result.user;
|
||||||
|
if (user) {
|
||||||
|
if (user.tenantId != null) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('TenantId', String(user.tenantId));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (user.userId != null) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem('UserId', String(user.userId));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
@@ -31,6 +55,48 @@ export async function login(data: LoginParam) {
|
|||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱登录
|
||||||
|
*/
|
||||||
|
export async function loginByEmail(data: LoginParam) {
|
||||||
|
const res = await request.post<ApiResult<LoginResult>>(
|
||||||
|
'https://server.websoft.top/api/loginByEmail',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
setToken(res.data.data?.access_token, data.remember);
|
||||||
|
if (res.data.data?.user) {
|
||||||
|
const user = res.data.data?.user;
|
||||||
|
localStorage.setItem('TenantId', String(user.tenantId));
|
||||||
|
localStorage.setItem('UserId', String(user.userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID登录
|
||||||
|
*/
|
||||||
|
export async function loginByUserId(data: LoginParam) {
|
||||||
|
const res = await request.post<ApiResult<LoginResult>>(
|
||||||
|
'https://server.websoft.top/api/loginByUserId',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
setToken(res.data.data?.access_token, data.remember);
|
||||||
|
if (res.data.data?.user) {
|
||||||
|
const user = res.data.data?.user;
|
||||||
|
localStorage.setItem('TenantId', String(user.tenantId));
|
||||||
|
localStorage.setItem('UserId', String(user.userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取验证码
|
* 获取验证码
|
||||||
*/
|
*/
|
||||||
@@ -62,6 +128,62 @@ export async function loginBySms(data: LoginParam) {
|
|||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 超管手机号短信登录
|
||||||
|
* 后端对应 POST /loginBySuperAdminSms,参数为 { phone, code, tenantId }
|
||||||
|
* - 单租户:直接返回 ok
|
||||||
|
* - 多租户:返回 selectTenant,前端需调用 loginBySelectTenant 二次登录
|
||||||
|
*/
|
||||||
|
export async function loginBySuperAdminSms(
|
||||||
|
data: LoginParam
|
||||||
|
): Promise<LoginOutcome> {
|
||||||
|
const res = await request.post<ApiResult<LoginResult>>(
|
||||||
|
SERVER_API_URL + '/loginBySuperAdminSms',
|
||||||
|
{ phone: data.phone, code: data.smsCode, smsCode: data.smsCode, tenantId: data.tenantId }
|
||||||
|
);
|
||||||
|
if (res.data.code !== 0) {
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
const result = res.data.data;
|
||||||
|
if (result?.tenants && result.tenants.length > 0) {
|
||||||
|
return {
|
||||||
|
status: 'selectTenant',
|
||||||
|
tenants: result.tenants as TenantOption[],
|
||||||
|
message: res.data.message || '请选择要登录的租户'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (result?.user) {
|
||||||
|
saveLoginState(result, data.remember);
|
||||||
|
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message || '登录失败'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择租户后登录(多租户短信登录专用二次登录接口)
|
||||||
|
* 后端对应 POST /loginBySelectTenant,参数为 { phone, code, tenantId }
|
||||||
|
*/
|
||||||
|
export async function loginBySelectTenant(data: {
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
tenantId: number;
|
||||||
|
remember?: boolean;
|
||||||
|
}): Promise<LoginOutcome> {
|
||||||
|
const res = await request.post<ApiResult<LoginResult>>(
|
||||||
|
SERVER_API_URL + '/loginBySelectTenant',
|
||||||
|
{ phone: data.phone, code: data.code, smsCode: data.code, tenantId: data.tenantId }
|
||||||
|
);
|
||||||
|
if (res.data.code !== 0) {
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
const result = res.data.data;
|
||||||
|
if (result?.user) {
|
||||||
|
saveLoginState(result, data.remember);
|
||||||
|
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message || '登录失败'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送短信验证码
|
* 发送短信验证码
|
||||||
*/
|
*/
|
||||||
@@ -76,6 +198,46 @@ export async function sendSmsCaptcha(data: LoginParam) {
|
|||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码(后台管理员,跨租户:只要该手机号是超管或管理员即发送)
|
||||||
|
* 后端对应 POST /sendSmsCaptchaByAdmin,参数为 { phone }
|
||||||
|
* @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用)
|
||||||
|
*/
|
||||||
|
export async function sendSmsCaptchaByAdmin(
|
||||||
|
data: LoginParam,
|
||||||
|
options?: { skipTenant?: boolean }
|
||||||
|
) {
|
||||||
|
const res = await request.post<ApiResult<SmsCaptchaResult>>(
|
||||||
|
SERVER_API_URL + '/sendSmsCaptchaByAdmin',
|
||||||
|
data,
|
||||||
|
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码(找回密码等登录前场景使用)
|
||||||
|
* 后端对应 POST /sendEmailCaptcha,参数为 { email }
|
||||||
|
* @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用)
|
||||||
|
*/
|
||||||
|
export async function sendEmailCaptcha(
|
||||||
|
data: LoginParam,
|
||||||
|
options?: { skipTenant?: boolean }
|
||||||
|
) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
SERVER_API_URL + '/sendEmailCaptcha',
|
||||||
|
data,
|
||||||
|
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录
|
* 登录
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import type { User } from '@/api/system/user/model';
|
|||||||
export interface LoginParam {
|
export interface LoginParam {
|
||||||
// 账号
|
// 账号
|
||||||
username?: string;
|
username?: string;
|
||||||
|
// 邮箱
|
||||||
|
email?: string;
|
||||||
|
// 用户ID
|
||||||
|
userId?: string | number;
|
||||||
// 密码
|
// 密码
|
||||||
password?: string;
|
password?: string;
|
||||||
// 租户id
|
// 租户id
|
||||||
@@ -15,6 +19,8 @@ export interface LoginParam {
|
|||||||
phone?: string;
|
phone?: string;
|
||||||
// 短信验证码
|
// 短信验证码
|
||||||
code?: string;
|
code?: string;
|
||||||
|
// 短信验证码(别名,与后端 LoginParam.code 对应)
|
||||||
|
smsCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,8 +31,30 @@ export interface LoginResult {
|
|||||||
access_token?: string;
|
access_token?: string;
|
||||||
// 用户信息
|
// 用户信息
|
||||||
user?: User;
|
user?: User;
|
||||||
|
// 多租户场景下返回,此时 access_token / user 为 null
|
||||||
|
tenants?: TenantOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多租户登录时后端返回的租户选项
|
||||||
|
*/
|
||||||
|
export interface TenantOption {
|
||||||
|
tenantId?: number;
|
||||||
|
tenantName?: string;
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
nickname?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录接口统一返回结果
|
||||||
|
* - ok:单租户直接登录成功
|
||||||
|
* - selectTenant:多租户,需要前端弹出选择租户列表
|
||||||
|
*/
|
||||||
|
export type LoginOutcome =
|
||||||
|
| { status: 'ok'; user: NonNullable<LoginResult['user']>; message: string }
|
||||||
|
| { status: 'selectTenant'; tenants: TenantOption[]; message: string };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图形验证码返回结果
|
* 图形验证码返回结果
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ export interface ShopGoods {
|
|||||||
commissionRole?: number;
|
commissionRole?: number;
|
||||||
// 货架
|
// 货架
|
||||||
position?: string;
|
position?: string;
|
||||||
// 套餐价格
|
// 进货价格
|
||||||
buyingPrice?: string;
|
buyingPrice?: string;
|
||||||
// 商品价格
|
// 商城价
|
||||||
price?: string;
|
price?: string;
|
||||||
originPrice?: string;
|
originPrice?: string;
|
||||||
// 销售价格
|
// 销售价格
|
||||||
@@ -87,6 +87,8 @@ export interface ShopGoods {
|
|||||||
gainIntegral?: number;
|
gainIntegral?: number;
|
||||||
// 推荐
|
// 推荐
|
||||||
recommend?: number;
|
recommend?: number;
|
||||||
|
// 置顶(1置顶 0不置顶)
|
||||||
|
isTop?: number;
|
||||||
// 商户ID
|
// 商户ID
|
||||||
merchantId?: number;
|
merchantId?: number;
|
||||||
// 商户名称
|
// 商户名称
|
||||||
|
|||||||
30
src/api/shop/shopGoodsBrowse/index.ts
Normal file
30
src/api/shop/shopGoodsBrowse/index.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult, PageResult } from '@/api';
|
||||||
|
import type { ShopGoodsBrowse, ShopGoodsBrowseParam } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台管理:分页查询浏览记录
|
||||||
|
*/
|
||||||
|
export async function pageShopGoodsBrowse(params: ShopGoodsBrowseParam) {
|
||||||
|
const res = await request.get<ApiResult<PageResult<ShopGoodsBrowse>>>(
|
||||||
|
'/shop/shop-goods-browse/admin/page',
|
||||||
|
{ params }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台管理:删除单条浏览记录
|
||||||
|
*/
|
||||||
|
export async function removeShopGoodsBrowse(id?: number) {
|
||||||
|
const res = await request.delete<ApiResult<unknown>>(
|
||||||
|
'/shop/shop-goods-browse/admin/' + id
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
42
src/api/shop/shopGoodsBrowse/model/index.ts
Normal file
42
src/api/shop/shopGoodsBrowse/model/index.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { PageParam } from '@/api';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户商品浏览记录
|
||||||
|
*/
|
||||||
|
export interface ShopGoodsBrowse {
|
||||||
|
id?: number;
|
||||||
|
userId?: number;
|
||||||
|
goodsId?: number;
|
||||||
|
tenantId?: number;
|
||||||
|
merchantId?: number;
|
||||||
|
/** 浏览次数 */
|
||||||
|
visitCount?: number;
|
||||||
|
/** 最后浏览时间 */
|
||||||
|
lastVisitTime?: string;
|
||||||
|
/** 浏览来源 */
|
||||||
|
browseSource?: string;
|
||||||
|
createTime?: string;
|
||||||
|
updateTime?: string;
|
||||||
|
/** 商品名称(关联查询) */
|
||||||
|
goodsName?: string;
|
||||||
|
/** 商品封面图(关联查询) */
|
||||||
|
goodsImage?: string;
|
||||||
|
/** 商城价(关联查询) */
|
||||||
|
price?: string;
|
||||||
|
/** 市场价(关联查询) */
|
||||||
|
salePrice?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 浏览记录查询参数
|
||||||
|
*/
|
||||||
|
export interface ShopGoodsBrowseParam extends PageParam {
|
||||||
|
id?: number;
|
||||||
|
userId?: number;
|
||||||
|
goodsId?: number;
|
||||||
|
merchantId?: number;
|
||||||
|
tenantId?: number;
|
||||||
|
browseSource?: string;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ export interface ShopGoodsSku {
|
|||||||
sku?: string;
|
sku?: string;
|
||||||
// 商品图片
|
// 商品图片
|
||||||
image?: string;
|
image?: string;
|
||||||
// 商品价格
|
// 商城价
|
||||||
price?: string;
|
price?: string;
|
||||||
// 市场价格
|
// 市场价格
|
||||||
salePrice?: string;
|
salePrice?: string;
|
||||||
|
|||||||
@@ -150,3 +150,72 @@ export async function refundShopOrder(data: ShopOrder) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认线下付款收款
|
||||||
|
* 商家确认已收到线下转账(微信转账/银行汇款等),确认后订单进入待发货状态
|
||||||
|
* @param id 订单ID
|
||||||
|
* @param remarks 备注(可选)
|
||||||
|
* @param paymentVoucher 支付凭证图片地址(可选)
|
||||||
|
*/
|
||||||
|
export async function confirmOfflinePayment(
|
||||||
|
id: number,
|
||||||
|
remarks?: string,
|
||||||
|
paymentVoucher?: string
|
||||||
|
) {
|
||||||
|
const res = await request.put<ApiResult<unknown>>(
|
||||||
|
MODULES_API_URL + '/shop/shop-order/confirm-offline-payment/' + id,
|
||||||
|
null,
|
||||||
|
{ params: { remarks, paymentVoucher } }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台今日数据概况(含未支付口径)
|
||||||
|
* 字段对齐后端 ShopOrderOverviewVo:
|
||||||
|
* - todaySales 今日销售额(元,含未支付,按 total_price 求和)
|
||||||
|
* - todayOrders 今日订单数(含未支付)
|
||||||
|
* - todayUsers 今日新增用户数
|
||||||
|
* - couponUsedCount 今日使用优惠券的订单数
|
||||||
|
*/
|
||||||
|
export interface ShopOrderOverview {
|
||||||
|
/** 今日销售额(元),含未支付,按订单面额 total_price 求和 */
|
||||||
|
todaySales: number;
|
||||||
|
/** 今日订单数,含未支付 */
|
||||||
|
todayOrders: number;
|
||||||
|
/** 今日新增用户数 */
|
||||||
|
todayUsers: number;
|
||||||
|
/** 今日使用优惠券的订单数 */
|
||||||
|
couponUsedCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计订单总金额(含未支付,按订单面额 total_price 求和)
|
||||||
|
* 对标 /shop/shop-order/total,但不限制 pay_status,返回所有未删除订单的 total_price 之和
|
||||||
|
*/
|
||||||
|
export async function shopOrderTotalAll() {
|
||||||
|
const res = await request.get<ApiResult<number>>(
|
||||||
|
MODULES_API_URL + '/shop/shop-order/total-all'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.data ?? 0;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取后台今日数据概况(含未支付口径)
|
||||||
|
*/
|
||||||
|
export async function shopOrderOverview() {
|
||||||
|
const res = await request.get<ApiResult<ShopOrderOverview>>(
|
||||||
|
MODULES_API_URL + '/shop/shop-order/overview'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ export interface ShopOrder {
|
|||||||
coachId?: number;
|
coachId?: number;
|
||||||
// 支付的用户id
|
// 支付的用户id
|
||||||
payUserId?: number;
|
payUserId?: number;
|
||||||
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
|
// 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
|
||||||
payType?: number;
|
payType?: number;
|
||||||
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9~18 已废弃
|
// 代付支付方式, 0余额支付, 1微信支付, 2支付宝, 3银联支付, 4现金支付, 5POS机支付, 6免费, 7积分支付, 8货到付款, 9线下付款, 10~18 已废弃
|
||||||
friendPayType?: number;
|
friendPayType?: number;
|
||||||
// 0未付款,1已付款
|
// 0未付款,1已付款
|
||||||
payStatus?: number;
|
payStatus?: number;
|
||||||
@@ -111,6 +111,14 @@ export interface ShopOrder {
|
|||||||
sendPhone?: string;
|
sendPhone?: string;
|
||||||
// 发货地址
|
// 发货地址
|
||||||
sendAddress?: string;
|
sendAddress?: string;
|
||||||
|
// 发货门店id(快递配送=发货仓库,商家送货=配送门店)
|
||||||
|
sendStoreId?: number;
|
||||||
|
// 配送店员id(商家送货时记录具体配送人员)
|
||||||
|
deliveryUserId?: number;
|
||||||
|
// 发货门店名称(后端联表返回)
|
||||||
|
sendStoreName?: string;
|
||||||
|
// 配送店员名称(后端联表返回)
|
||||||
|
deliveryUserName?: string;
|
||||||
// 优惠类型:0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡,5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
|
// 优惠类型:0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡,5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
|
||||||
couponType?: number;
|
couponType?: number;
|
||||||
// 优惠说明
|
// 优惠说明
|
||||||
@@ -129,6 +137,8 @@ export interface ShopOrder {
|
|||||||
invoiceNo?: string;
|
invoiceNo?: string;
|
||||||
// 支付时间
|
// 支付时间
|
||||||
payTime?: string;
|
payTime?: string;
|
||||||
|
// 线下收款支付凭证(图片地址,确认线下收款时上传)
|
||||||
|
paymentVoucher?: string;
|
||||||
// 退款时间
|
// 退款时间
|
||||||
refundTime?: string;
|
refundTime?: string;
|
||||||
// 申请退款时间
|
// 申请退款时间
|
||||||
@@ -142,9 +152,9 @@ export interface ShopOrder {
|
|||||||
// 系统版本号 0当前版本 value=其他版本
|
// 系统版本号 0当前版本 value=其他版本
|
||||||
version?: number;
|
version?: number;
|
||||||
// 买家备注
|
// 买家备注
|
||||||
buyerRemarks: undefined;
|
buyerRemarks?: string;
|
||||||
// 商家备注
|
// 商家备注
|
||||||
merchantRemarks: undefined;
|
merchantRemarks?: string;
|
||||||
// 用户id
|
// 用户id
|
||||||
userId?: number;
|
userId?: number;
|
||||||
// 备注
|
// 备注
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ export interface ShopSetting {
|
|||||||
payAlipayPrivateKey?: string;
|
payAlipayPrivateKey?: string;
|
||||||
payAlipayPublicKey?: string;
|
payAlipayPublicKey?: string;
|
||||||
payBalanceEnabled?: boolean | number;
|
payBalanceEnabled?: boolean | number;
|
||||||
|
|
||||||
|
salesAccumulateType?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export async function initialization(roleId?: number) {
|
|||||||
* 根据code查询租户
|
* 根据code查询租户
|
||||||
*/
|
*/
|
||||||
export async function getByCode(code: string) {
|
export async function getByCode(code: string) {
|
||||||
const res = await request.get<Tenant>(
|
const res = await request.get<ApiResult<Tenant>>(
|
||||||
SERVER_API_URL + '/system/tenant/getByCode/' + code
|
SERVER_API_URL + '/system/tenant/getByCode/' + code
|
||||||
);
|
);
|
||||||
if (res.data.code === 0 && res.data.data) {
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
|||||||
@@ -84,6 +84,12 @@
|
|||||||
label: '货到付款',
|
label: '货到付款',
|
||||||
key: 'codPay',
|
key: 'codPay',
|
||||||
icon: 'IdcardOutlined'
|
icon: 'IdcardOutlined'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '线下付款',
|
||||||
|
key: 'offlinePay',
|
||||||
|
icon: 'IdcardOutlined'
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const FILE_SERVER =
|
|||||||
*/
|
*/
|
||||||
// 接口地址
|
// 接口地址
|
||||||
export const API_BASE_URL: string = import.meta.env.VITE_API_URL;
|
export const API_BASE_URL: string = import.meta.env.VITE_API_URL;
|
||||||
|
// CMS 模块接口地址(cms 已迁移到 mp-java 并独立部署,调用指向远程服务)
|
||||||
|
export const CMS_API_BASE_URL: string =
|
||||||
|
import.meta.env.VITE_CMS_API_URL || 'https://cms-api.websoft.top/api';
|
||||||
export const PROJECT_NAME: string = import.meta.env.VITE_APP_NAME;
|
export const PROJECT_NAME: string = import.meta.env.VITE_APP_NAME;
|
||||||
// 不显示侧栏的路由
|
// 不显示侧栏的路由
|
||||||
export const HIDE_SIDEBARS: string[] = ['/home'];
|
export const HIDE_SIDEBARS: string[] = ['/home'];
|
||||||
|
|||||||
@@ -10,5 +10,36 @@ export default {
|
|||||||
loading: 'loading',
|
loading: 'loading',
|
||||||
oldPassword: 'Old',
|
oldPassword: 'Old',
|
||||||
newPassword: 'New',
|
newPassword: 'New',
|
||||||
confirm: 'Confirm'
|
confirm: 'Confirm',
|
||||||
|
forgetPage: {
|
||||||
|
title: 'Forgot Password',
|
||||||
|
subTitle: 'Enter your account info to reset the password',
|
||||||
|
tabEmail: 'By Email',
|
||||||
|
tabPhone: 'By Phone',
|
||||||
|
email: 'Email',
|
||||||
|
phone: 'Phone',
|
||||||
|
emailCode: 'Email Code',
|
||||||
|
code: 'SMS Code',
|
||||||
|
getCode: 'Get Code',
|
||||||
|
sentEmail: 'Verification code sent, please check your email',
|
||||||
|
sentPhone: 'Verification code sent',
|
||||||
|
newPassword: 'New Password',
|
||||||
|
confirmPassword: 'Confirm Password',
|
||||||
|
reset: 'Reset Password',
|
||||||
|
backLogin: 'Back to Login',
|
||||||
|
success: 'Password reset successfully, please login with the new password',
|
||||||
|
emailPlaceholder: 'Please enter your registered email',
|
||||||
|
phonePlaceholder: 'Please enter your registered phone',
|
||||||
|
codePlaceholder: 'Please enter the code',
|
||||||
|
passwordPlaceholder: 'At least 8 chars, must contain letters and numbers',
|
||||||
|
confirmPlaceholder: 'Please enter the password again',
|
||||||
|
newPasswordRule: 'Password must be at least 8 chars with letters and numbers',
|
||||||
|
confirmRule: 'The two passwords do not match',
|
||||||
|
emailRule: 'Please enter a valid email address',
|
||||||
|
phoneRule: 'Please enter a valid phone number',
|
||||||
|
codeRule: 'Please enter the code',
|
||||||
|
captchaTitle: 'Send Code',
|
||||||
|
captchaPlaceholder: 'Please enter the captcha',
|
||||||
|
sendNow: 'Send Now'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
||||||
/* 主框架 */
|
/* 主框架 */
|
||||||
export default {
|
export default {
|
||||||
system: '小程序开发',
|
system: '小程序商城',
|
||||||
home: '主页',
|
home: '主页',
|
||||||
header: {
|
header: {
|
||||||
profile: '个人资料',
|
profile: '个人资料',
|
||||||
|
|||||||
@@ -10,5 +10,36 @@ export default {
|
|||||||
loading: '登录中',
|
loading: '登录中',
|
||||||
oldPassword: '旧密码',
|
oldPassword: '旧密码',
|
||||||
newPassword: '新密码',
|
newPassword: '新密码',
|
||||||
confirm: '确认密码'
|
confirm: '确认密码',
|
||||||
|
forgetPage: {
|
||||||
|
title: '忘记密码',
|
||||||
|
subTitle: '请输入账号信息以重置密码',
|
||||||
|
tabEmail: '邮箱找回',
|
||||||
|
tabPhone: '手机找回',
|
||||||
|
email: '邮箱',
|
||||||
|
phone: '手机号',
|
||||||
|
emailCode: '邮箱验证码',
|
||||||
|
code: '短信验证码',
|
||||||
|
getCode: '获取验证码',
|
||||||
|
sentEmail: '验证码已发送,请查收邮箱',
|
||||||
|
sentPhone: '验证码已发送',
|
||||||
|
newPassword: '新密码',
|
||||||
|
confirmPassword: '确认新密码',
|
||||||
|
reset: '重置密码',
|
||||||
|
backLogin: '返回登录',
|
||||||
|
success: '密码重置成功,请使用新密码登录',
|
||||||
|
emailPlaceholder: '请输入注册邮箱',
|
||||||
|
phonePlaceholder: '请输入注册手机号',
|
||||||
|
codePlaceholder: '请输入验证码',
|
||||||
|
passwordPlaceholder: '至少 8 位,需包含字母和数字',
|
||||||
|
confirmPlaceholder: '请再次输入新密码',
|
||||||
|
newPasswordRule: '新密码至少 8 位,需包含字母和数字',
|
||||||
|
confirmRule: '两次输入的密码不一致',
|
||||||
|
emailRule: '请输入正确的邮箱地址',
|
||||||
|
phoneRule: '请输入正确的手机号',
|
||||||
|
codeRule: '请输入验证码',
|
||||||
|
captchaTitle: '发送验证码',
|
||||||
|
captchaPlaceholder: '请输入图形验证码',
|
||||||
|
sendNow: '立即发送'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<span>{{ item.meta.title }}</span>
|
<span>{{ item.meta.title }}</span>
|
||||||
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
|
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
|
||||||
|
<!-- dot 模式:纯红点,不显示数字(用于新订单提醒等) -->
|
||||||
|
<a-badge v-if="item.meta.badge === 'dot'" dot />
|
||||||
|
<!-- 数字徽章模式 -->
|
||||||
<a-badge
|
<a-badge
|
||||||
|
v-else
|
||||||
:count="item.meta.badge"
|
:count="item.meta.badge"
|
||||||
:number-style="{ background: item.meta.badgeColor as string }"
|
:number-style="{ background: item.meta.badgeColor as string }"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
import HeaderTools from './components/header-tools.vue';
|
import HeaderTools from './components/header-tools.vue';
|
||||||
import PageFooter from './components/page-footer.vue';
|
import PageFooter from './components/page-footer.vue';
|
||||||
import MenuTitle from './components/menu-title.vue';
|
import MenuTitle from './components/menu-title.vue';
|
||||||
|
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
||||||
import {
|
import {
|
||||||
HIDE_SIDEBARS,
|
HIDE_SIDEBARS,
|
||||||
HIDE_FOOTERS,
|
HIDE_FOOTERS,
|
||||||
@@ -127,6 +128,11 @@
|
|||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 全局启动新订单提醒:任意页面都会轮询检测新订单,
|
||||||
|
// 检测到后在「订单管理」菜单显示红点 + 叮声/语音播报
|
||||||
|
const { start: startOrderNotify } = useOrderNotify();
|
||||||
|
startOrderNotify();
|
||||||
|
|
||||||
// 是否刷新页面
|
// 是否刷新页面
|
||||||
if (localStorage.getItem('Reload')) {
|
if (localStorage.getItem('Reload')) {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ export const routes = [
|
|||||||
component: () => import('@/views/passport/merchant/success.vue'),
|
component: () => import('@/views/passport/merchant/success.vue'),
|
||||||
meta: { title: '申请提交成功' }
|
meta: { title: '申请提交成功' }
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// path: '/forget',
|
path: '/forget',
|
||||||
// component: () => import('@/views/passport/forget/index.vue'),
|
component: () => import('@/views/passport/forget/index.vue'),
|
||||||
// meta: { title: '忘记密码' }
|
meta: { title: '忘记密码' }
|
||||||
// },
|
},
|
||||||
// {
|
// {
|
||||||
// path: '/wx-work-login',
|
// path: '/wx-work-login',
|
||||||
// component: () => import('@/views/passport/wx-work/index.vue'),
|
// component: () => import('@/views/passport/wx-work/index.vue'),
|
||||||
|
|||||||
375
src/store/modules/appSubscription.ts
Normal file
375
src/store/modules/appSubscription.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
/**
|
||||||
|
* 应用订阅 store
|
||||||
|
* 数据来源:websopy 特殊接口(db_websopy.app_subscription 表)
|
||||||
|
* 接口域名:https://websopy-api.websoft.top/api
|
||||||
|
* 后端:com.gxwebsoft.app.controller.AppSubscriptionController
|
||||||
|
*/
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import {
|
||||||
|
pageMySubscriptions,
|
||||||
|
getSubscriptionDetail,
|
||||||
|
getSubscriptionDetailByNo,
|
||||||
|
checkSubscriptionStatus,
|
||||||
|
checkPurchased,
|
||||||
|
getBalance,
|
||||||
|
subscribe,
|
||||||
|
generatePayQrcode,
|
||||||
|
paySubscription,
|
||||||
|
mpPrepay,
|
||||||
|
mpConfirm,
|
||||||
|
renewSubscription,
|
||||||
|
cancelSubscription,
|
||||||
|
toggleEnable
|
||||||
|
} from '@/api/app/appSubscription';
|
||||||
|
import type {
|
||||||
|
AppSubscription,
|
||||||
|
AppSubscriptionQueryParam,
|
||||||
|
SubscribeParam,
|
||||||
|
SubscribeResult,
|
||||||
|
GeneratePayQrcodeParam,
|
||||||
|
GeneratePayQrcodeResult,
|
||||||
|
CheckStatusResult,
|
||||||
|
PayResult,
|
||||||
|
WechatNativePayResult,
|
||||||
|
MpPrepayResult
|
||||||
|
} from '@/api/app/appSubscription/model';
|
||||||
|
import type { PageResult } from '@/api';
|
||||||
|
|
||||||
|
export interface AppSubscriptionState {
|
||||||
|
// 我的订阅列表
|
||||||
|
subscriptionList: AppSubscription[];
|
||||||
|
// 总数量
|
||||||
|
total: number;
|
||||||
|
// 当前查看的订阅详情
|
||||||
|
currentSubscription: AppSubscription | null;
|
||||||
|
// 用户余额
|
||||||
|
balance: number;
|
||||||
|
// 加载状态
|
||||||
|
loading: boolean;
|
||||||
|
// 最后更新时间
|
||||||
|
lastUpdateTime: number | null;
|
||||||
|
// 缓存有效期(毫秒)
|
||||||
|
cacheExpiry: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppSubscriptionStore = defineStore('appSubscription', {
|
||||||
|
state: (): AppSubscriptionState => ({
|
||||||
|
subscriptionList: [],
|
||||||
|
total: 0,
|
||||||
|
currentSubscription: null,
|
||||||
|
balance: 0,
|
||||||
|
loading: false,
|
||||||
|
lastUpdateTime: null,
|
||||||
|
// 默认缓存5分钟(订阅涉及支付状态,不宜过长)
|
||||||
|
cacheExpiry: 5 * 60 * 1000
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
/**
|
||||||
|
* 已激活订阅
|
||||||
|
*/
|
||||||
|
activeSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'active');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 待支付订阅
|
||||||
|
*/
|
||||||
|
pendingSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'pending');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已过期订阅
|
||||||
|
*/
|
||||||
|
expiredSubscriptions: (state): AppSubscription[] => {
|
||||||
|
return state.subscriptionList.filter((s) => s.status === 'expired');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查缓存是否有效
|
||||||
|
*/
|
||||||
|
isCacheValid: (state): boolean => {
|
||||||
|
if (!state.lastUpdateTime) return false;
|
||||||
|
const now = Date.now();
|
||||||
|
return now - state.lastUpdateTime < state.cacheExpiry;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// ============================================================
|
||||||
|
// 查询类
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取我的订阅列表(分页,带缓存)
|
||||||
|
* @param params 查询参数(page/limit/status)
|
||||||
|
* @param forceRefresh 是否强制刷新(切换 status 过滤时建议传 true)
|
||||||
|
*/
|
||||||
|
async fetchMySubscriptions(
|
||||||
|
params: AppSubscriptionQueryParam = { page: 1, limit: 10 },
|
||||||
|
forceRefresh = false
|
||||||
|
): Promise<PageResult<AppSubscription>> {
|
||||||
|
// 缓存有效且不强制刷新,直接返回缓存列表
|
||||||
|
if (!forceRefresh && this.isCacheValid && this.subscriptionList.length > 0) {
|
||||||
|
return { list: this.subscriptionList, count: this.total };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
const data = await pageMySubscriptions(params);
|
||||||
|
this.subscriptionList = data.list || [];
|
||||||
|
this.total = data.count || 0;
|
||||||
|
this.lastUpdateTime = Date.now();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取我的订阅列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订阅详情(按ID)
|
||||||
|
*/
|
||||||
|
async fetchDetail(id: number): Promise<AppSubscription> {
|
||||||
|
try {
|
||||||
|
const data = await getSubscriptionDetail(id);
|
||||||
|
this.currentSubscription = data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取订阅详情失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订阅编号查询详情(小程序入口用)
|
||||||
|
*/
|
||||||
|
async fetchDetailByNo(subscriptionNo: string): Promise<AppSubscription> {
|
||||||
|
try {
|
||||||
|
const data = await getSubscriptionDetailByNo(subscriptionNo);
|
||||||
|
this.currentSubscription = data;
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('根据订阅编号查询详情失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询支付状态(前端轮询用,强制实时请求,不走缓存)
|
||||||
|
*/
|
||||||
|
async checkStatus(subscriptionNo: string): Promise<CheckStatusResult> {
|
||||||
|
return await checkSubscriptionStatus(subscriptionNo);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已购买某应用(强制实时请求,不走缓存)
|
||||||
|
*/
|
||||||
|
async checkPurchased(productId: number): Promise<boolean> {
|
||||||
|
return await checkPurchased(productId);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户余额
|
||||||
|
*/
|
||||||
|
async fetchBalance(): Promise<number> {
|
||||||
|
try {
|
||||||
|
const data = await getBalance();
|
||||||
|
this.balance = data.balance;
|
||||||
|
return data.balance;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户余额失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 订阅与支付操作
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订阅
|
||||||
|
* 免费应用直接激活,付费应用创建待支付记录
|
||||||
|
*/
|
||||||
|
async subscribe(data: SubscribeParam): Promise<SubscribeResult> {
|
||||||
|
try {
|
||||||
|
const result = await subscribe(data);
|
||||||
|
// 订阅状态变化,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建订阅失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成支付小程序码(同时创建订阅记录)
|
||||||
|
*/
|
||||||
|
async generatePayQrcode(
|
||||||
|
data: GeneratePayQrcodeParam
|
||||||
|
): Promise<GeneratePayQrcodeResult> {
|
||||||
|
try {
|
||||||
|
const result = await generatePayQrcode(data);
|
||||||
|
// 已创建订阅记录,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成支付小程序码失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发起支付
|
||||||
|
* @param id 订阅ID
|
||||||
|
* @param method 支付方式:balance-余额支付 / wechat-微信支付
|
||||||
|
* @param envVersion 小程序版本(微信支付时使用):develop/trial/release
|
||||||
|
*/
|
||||||
|
async pay(
|
||||||
|
id: number,
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
try {
|
||||||
|
const result = await paySubscription(id, method, envVersion);
|
||||||
|
// 支付成功后余额/订阅状态变化,失效缓存并刷新余额
|
||||||
|
this.invalidateCache();
|
||||||
|
if (method === 'balance') {
|
||||||
|
const payResult = result as PayResult;
|
||||||
|
if (typeof payResult.balance === 'number') {
|
||||||
|
this.balance = payResult.balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('发起支付失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序 JSAPI 预支付下单
|
||||||
|
*/
|
||||||
|
async mpPrepay(id: number, openid: string): Promise<MpPrepayResult> {
|
||||||
|
try {
|
||||||
|
return await mpPrepay(id, { openid });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('小程序预支付失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序支付成功确认
|
||||||
|
*/
|
||||||
|
async mpConfirm(
|
||||||
|
subscriptionNo: string,
|
||||||
|
transactionId?: string
|
||||||
|
): Promise<PayResult> {
|
||||||
|
try {
|
||||||
|
const result = await mpConfirm(subscriptionNo, { transactionId });
|
||||||
|
// 支付确认后订阅状态变化,失效缓存
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('小程序支付确认失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 订阅管理
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 续费:基于已有订阅创建续费订单并生成支付码(一步完成,对齐后端 /renew-pay)
|
||||||
|
* @param id 订阅ID
|
||||||
|
* @param period 周期:month/year
|
||||||
|
* @param method 支付方式:balance/wechat
|
||||||
|
* @param envVersion 小程序版本(微信支付时使用)
|
||||||
|
*/
|
||||||
|
async renew(
|
||||||
|
id: number,
|
||||||
|
period: 'month' | 'year' = 'month',
|
||||||
|
method: 'balance' | 'wechat' = 'wechat',
|
||||||
|
envVersion?: string
|
||||||
|
): Promise<PayResult | WechatNativePayResult> {
|
||||||
|
try {
|
||||||
|
const result = await renewSubscription(id, period, method, envVersion);
|
||||||
|
this.invalidateCache();
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('续费失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退订/取消
|
||||||
|
*/
|
||||||
|
async cancel(id: number): Promise<string> {
|
||||||
|
try {
|
||||||
|
const msg = await cancelSubscription(id);
|
||||||
|
this.invalidateCache();
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退订失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用/禁用
|
||||||
|
*/
|
||||||
|
async toggleEnable(id: number, enabled: boolean): Promise<string> {
|
||||||
|
try {
|
||||||
|
const msg = await toggleEnable(id, enabled);
|
||||||
|
this.invalidateCache();
|
||||||
|
return msg;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('启用/禁用失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 缓存控制
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 失效缓存(仅重置时间标记,保留已有数据,下次拉取时更新)
|
||||||
|
* 用于订阅/支付/管理操作后,确保下次查询获取最新数据
|
||||||
|
*/
|
||||||
|
invalidateCache() {
|
||||||
|
this.lastUpdateTime = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除缓存(清空数据并重置时间标记)
|
||||||
|
*/
|
||||||
|
clearCache() {
|
||||||
|
this.subscriptionList = [];
|
||||||
|
this.total = 0;
|
||||||
|
this.currentSubscription = null;
|
||||||
|
this.lastUpdateTime = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制刷新订阅列表
|
||||||
|
*/
|
||||||
|
async refresh() {
|
||||||
|
return await this.fetchMySubscriptions({ page: 1, limit: 10 }, true);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置缓存有效期
|
||||||
|
*/
|
||||||
|
setCacheExpiry(expiry: number) {
|
||||||
|
this.cacheExpiry = expiry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -90,9 +90,9 @@ export const useStatisticsStore = defineStore('statistics', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取今日使用优惠券数量
|
* 获取今日使用优惠券数量(安全取值)
|
||||||
*/
|
*/
|
||||||
couponUsedCount: (state): number => {
|
safeCouponUsedCount: (state): number => {
|
||||||
return safeNumber(state.couponUsedCount);
|
return safeNumber(state.couponUsedCount);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,26 @@ export function ensureFullUrl(url: string): string {
|
|||||||
return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
|
return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 URL 中已有的 x-oss-process 参数(无论其在 query 中的位置)
|
||||||
|
*
|
||||||
|
* 阿里云 OSS 的图片处理参数通常以 `x-oss-process=image/...` 形式附加在 query 中,
|
||||||
|
* 可能位于首个或后续参数位。本函数精确剔除该参数,保留其余 query 参数。
|
||||||
|
*
|
||||||
|
* @param url 完整图片 URL
|
||||||
|
* @returns 已剥离 x-oss-process 的 URL
|
||||||
|
*/
|
||||||
|
function removeOssProcess(url: string): string {
|
||||||
|
const qIndex = url.indexOf('?');
|
||||||
|
if (qIndex === -1) return url;
|
||||||
|
const base = url.slice(0, qIndex);
|
||||||
|
const params = url
|
||||||
|
.slice(qIndex + 1)
|
||||||
|
.split('&')
|
||||||
|
.filter((p) => !p.startsWith('x-oss-process='));
|
||||||
|
return params.length ? `${base}?${params.join('&')}` : base;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 OSS 压缩后的图片 URL
|
* 获取 OSS 压缩后的图片 URL
|
||||||
*
|
*
|
||||||
@@ -49,8 +69,11 @@ export function ensureFullUrl(url: string): string {
|
|||||||
*
|
*
|
||||||
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
|
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
|
||||||
*
|
*
|
||||||
|
* 优先级:以调用方指定的 width/quality 为准。若 URL 已自带 x-oss-process
|
||||||
|
* (接口已返回压缩参数),会先剥离再按本函数参数重新拼接,确保调用方参数生效。
|
||||||
|
*
|
||||||
* @param url 原始图片 URL
|
* @param url 原始图片 URL
|
||||||
* @param options 压缩选项
|
* @param options 压缩选项(width/quality 不传则使用默认值)
|
||||||
* @returns 处理后的图片 URL
|
* @returns 处理后的图片 URL
|
||||||
*/
|
*/
|
||||||
export function getCompressedImageUrl(
|
export function getCompressedImageUrl(
|
||||||
@@ -66,11 +89,12 @@ export function getCompressedImageUrl(
|
|||||||
// 未启用压缩,原样返回
|
// 未启用压缩,原样返回
|
||||||
if (!enabled) return fullUrl;
|
if (!enabled) return fullUrl;
|
||||||
|
|
||||||
// 已包含 OSS 处理参数,避免重复拼接
|
// 优先使用调用方指定的 width/quality:
|
||||||
if (fullUrl.includes('x-oss-process')) return fullUrl;
|
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
|
||||||
|
const baseUrl = removeOssProcess(fullUrl);
|
||||||
|
|
||||||
// 已有其他 query 参数用 & 拼接,否则用 ?
|
// 已有其他 query 参数用 & 拼接,否则用 ?
|
||||||
const separator = fullUrl.includes('?') ? '&' : '?';
|
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||||
|
|
||||||
return `${fullUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
return `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ service.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
// 获取租户ID
|
// 获取租户ID
|
||||||
if (config.headers) {
|
if (config.headers) {
|
||||||
|
// 登录前场景(找回密码等)由调用方显式设置 X-Tenant-Skip,
|
||||||
|
// 此时不做任何租户/企业/商户/域名头注入,避免串租户
|
||||||
|
if (config.headers['X-Tenant-Skip']) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
// 附加企业ID
|
// 附加企业ID
|
||||||
const companyId = localStorage.getItem('CompanyId');
|
const companyId = localStorage.getItem('CompanyId');
|
||||||
if (companyId) {
|
if (companyId) {
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export function getPayType(index?: number): any {
|
|||||||
{
|
{
|
||||||
value: 8,
|
value: 8,
|
||||||
label: '货到付款'
|
label: '货到付款'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 9,
|
||||||
|
label: '线下付款'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
if (index != null) {
|
if (index != null) {
|
||||||
|
|||||||
@@ -333,16 +333,12 @@
|
|||||||
const chooseFile = (data: FileRecord) => {
|
const chooseFile = (data: FileRecord) => {
|
||||||
images.value.push({
|
images.value.push({
|
||||||
uid: data.id,
|
uid: data.id,
|
||||||
url:
|
url: data.downloadUrl,
|
||||||
data.downloadUrl +
|
|
||||||
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90',
|
|
||||||
status: 'done',
|
status: 'done',
|
||||||
title: '', // 初始化标题为空
|
title: '', // 初始化标题为空
|
||||||
path: '' // 初始化链接为空
|
path: '' // 初始化链接为空
|
||||||
});
|
});
|
||||||
form.images =
|
form.images = data.downloadUrl;
|
||||||
data.downloadUrl +
|
|
||||||
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeleteItem = (index: number) => {
|
const onDeleteItem = (index: number) => {
|
||||||
|
|||||||
@@ -47,7 +47,11 @@
|
|||||||
v-for="(item, index) in record.imageList"
|
v-for="(item, index) in record.imageList"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<a-image :src="item.url" :width="80" />
|
<a-image
|
||||||
|
:src="getCompressedImageUrl(item.url, { width: 160, quality: 90 })"
|
||||||
|
:preview="{ src: item.url }"
|
||||||
|
:width="80"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="record.type == 4">
|
<template v-if="record.type == 4">
|
||||||
{{ record.comments }}
|
{{ record.comments }}
|
||||||
@@ -113,6 +117,7 @@
|
|||||||
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
|
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
|
||||||
import { getPageTitle } from '@/utils/common';
|
import { getPageTitle } from '@/utils/common';
|
||||||
import { getTenantId } from '@/utils/domain';
|
import { getTenantId } from '@/utils/domain';
|
||||||
|
import { getCompressedImageUrl } from '@/utils/image';
|
||||||
|
|
||||||
// 表格实例
|
// 表格实例
|
||||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
const systemInfo = ref({
|
const systemInfo = ref({
|
||||||
name: '小程序开发',
|
name: '小程序商城',
|
||||||
description:
|
description:
|
||||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
|
|||||||
@@ -153,7 +153,7 @@
|
|||||||
<a-descriptions-item label="技术支持">
|
<a-descriptions-item label="技术支持">
|
||||||
<span
|
<span
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>企业商城</span
|
>企业官网</span
|
||||||
>
|
>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
@@ -213,11 +213,14 @@
|
|||||||
import { openNew } from '@/utils/common';
|
import { openNew } from '@/utils/common';
|
||||||
import { useSiteStore } from '@/store/modules/site';
|
import { useSiteStore } from '@/store/modules/site';
|
||||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import { getTenantInfo } from '@/api/layout';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|
||||||
// 使用状态管理
|
// 使用状态管理
|
||||||
const siteStore = useSiteStore();
|
const siteStore = useSiteStore();
|
||||||
const statisticsStore = useStatisticsStore();
|
const statisticsStore = useStatisticsStore();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 从 store 中获取响应式数据
|
// 从 store 中获取响应式数据
|
||||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
||||||
@@ -225,7 +228,7 @@
|
|||||||
|
|
||||||
// 系统信息
|
// 系统信息
|
||||||
const systemInfo = ref({
|
const systemInfo = ref({
|
||||||
name: '小程序开发',
|
name: '小程序商城',
|
||||||
description:
|
description:
|
||||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||||
version: '2.0.0',
|
version: '2.0.0',
|
||||||
@@ -238,7 +241,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const runDays = computed(() => siteStore.runDays);
|
const now = ref(Date.now());
|
||||||
|
const tenantCreateTime = ref<string>('');
|
||||||
|
let runDaysTimer: ReturnType<typeof setInterval>;
|
||||||
|
const runDays = computed(() => {
|
||||||
|
if (!tenantCreateTime.value) return 0;
|
||||||
|
return Math.floor((now.value - new Date(tenantCreateTime.value).getTime()) / (24 * 60 * 60 * 1000));
|
||||||
|
});
|
||||||
const userCount = computed(() => statisticsStore.userCount);
|
const userCount = computed(() => statisticsStore.userCount);
|
||||||
const orderCount = computed(() => statisticsStore.orderCount);
|
const orderCount = computed(() => statisticsStore.orderCount);
|
||||||
const totalSales = computed(() => statisticsStore.totalSales);
|
const totalSales = computed(() => statisticsStore.totalSales);
|
||||||
@@ -248,6 +257,15 @@
|
|||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
// 独立请求租户信息,不受其他请求失败影响
|
||||||
|
getTenantInfo()
|
||||||
|
.then((res) => {
|
||||||
|
tenantCreateTime.value = res?.createTime || '';
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('获取租户信息失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
siteStore.fetchSiteInfo(),
|
siteStore.fetchSiteInfo(),
|
||||||
@@ -267,11 +285,14 @@
|
|||||||
await loadData();
|
await loadData();
|
||||||
// 开始自动刷新统计数据(每5分钟)
|
// 开始自动刷新统计数据(每5分钟)
|
||||||
statisticsStore.startAutoRefresh();
|
statisticsStore.startAutoRefresh();
|
||||||
|
// 运行天数每小时检查一次(跨天自动更新)
|
||||||
|
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时停止自动刷新
|
// 组件卸载时停止自动刷新
|
||||||
statisticsStore.stopAutoRefresh();
|
statisticsStore.stopAutoRefresh();
|
||||||
|
clearInterval(runDaysTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
621
src/views/passport/forget/index.vue
Normal file
621
src/views/passport/forget/index.vue
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="login-wrapper"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: 'url(https://oss.wsdns.cn/20250105/314d2a3da10048b09ef0f0464fdacbff.jpg)'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="logo-login" v-if="config?.siteName">
|
||||||
|
<img :src="config.siteLogo" class="logo" />
|
||||||
|
<h4>{{ config.siteName }}</h4>
|
||||||
|
</div>
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
class="login-form ele-bg-white"
|
||||||
|
>
|
||||||
|
<div class="login-title flex justify-center items-center px-12">
|
||||||
|
<h4 class="title-btn active">{{ t('login.forgetPage.title') }}</h4>
|
||||||
|
</div>
|
||||||
|
<p class="forget-subtitle">{{ t('login.forgetPage.subTitle') }}</p>
|
||||||
|
|
||||||
|
<a-tabs
|
||||||
|
v-model:activeKey="activeTab"
|
||||||
|
class="forget-tabs"
|
||||||
|
@change="onTabChange"
|
||||||
|
>
|
||||||
|
<!-- 邮箱找回 -->
|
||||||
|
<a-tab-pane key="email" :tab="t('login.forgetPage.tabEmail')">
|
||||||
|
<a-form-item name="email">
|
||||||
|
<a-input
|
||||||
|
allow-clear
|
||||||
|
size="large"
|
||||||
|
v-model:value="form.email"
|
||||||
|
:placeholder="t('login.forgetPage.emailPlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<MailOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item name="emailCode">
|
||||||
|
<div class="login-input-group">
|
||||||
|
<a-input
|
||||||
|
allow-clear
|
||||||
|
size="large"
|
||||||
|
:maxlength="6"
|
||||||
|
v-model:value="form.emailCode"
|
||||||
|
:placeholder="t('login.forgetPage.codePlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SafetyOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<a-button
|
||||||
|
class="login-captcha"
|
||||||
|
:disabled="!!countdownTime"
|
||||||
|
@click="sendCode"
|
||||||
|
>
|
||||||
|
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
|
||||||
|
<span v-else>{{ countdownTime }} s</span>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-tab-pane>
|
||||||
|
|
||||||
|
<!-- 手机找回 -->
|
||||||
|
<a-tab-pane key="phone" :tab="t('login.forgetPage.tabPhone')">
|
||||||
|
<a-form-item name="phone">
|
||||||
|
<a-input
|
||||||
|
allow-clear
|
||||||
|
size="large"
|
||||||
|
:maxlength="11"
|
||||||
|
v-model:value="form.phone"
|
||||||
|
:placeholder="t('login.forgetPage.phonePlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<MobileOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item name="code">
|
||||||
|
<div class="login-input-group">
|
||||||
|
<a-input
|
||||||
|
allow-clear
|
||||||
|
size="large"
|
||||||
|
:maxlength="6"
|
||||||
|
v-model:value="form.code"
|
||||||
|
:placeholder="t('login.forgetPage.codePlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<SafetyOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
<a-button
|
||||||
|
class="login-captcha"
|
||||||
|
:disabled="!!countdownTime"
|
||||||
|
@click="sendCode"
|
||||||
|
>
|
||||||
|
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
|
||||||
|
<span v-else>{{ countdownTime }} s</span>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
</a-tab-pane>
|
||||||
|
</a-tabs>
|
||||||
|
|
||||||
|
<a-form-item name="newPassword">
|
||||||
|
<a-input-password
|
||||||
|
size="large"
|
||||||
|
v-model:value="form.newPassword"
|
||||||
|
:placeholder="t('login.forgetPage.passwordPlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<LockOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item name="confirmPassword">
|
||||||
|
<a-input-password
|
||||||
|
size="large"
|
||||||
|
v-model:value="form.confirmPassword"
|
||||||
|
:placeholder="t('login.forgetPage.confirmPlaceholder')"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<LockOutlined />
|
||||||
|
</template>
|
||||||
|
</a-input-password>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-button
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
{{ loading ? t('login.loading') : t('login.forgetPage.reset') }}
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
<div class="text-center pt-2">
|
||||||
|
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<!-- 图形验证码弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
:width="340"
|
||||||
|
:footer="null"
|
||||||
|
:title="t('login.forgetPage.captchaTitle')"
|
||||||
|
v-model:visible="captchaVisible"
|
||||||
|
@cancel="codeLoading = false"
|
||||||
|
>
|
||||||
|
<div class="login-input-group" style="margin-bottom: 16px">
|
||||||
|
<a-input
|
||||||
|
v-model:value="imgCode"
|
||||||
|
:maxlength="5"
|
||||||
|
size="large"
|
||||||
|
:placeholder="t('login.forgetPage.captchaPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
@pressEnter="() => doSendCode(imgCode)"
|
||||||
|
/>
|
||||||
|
<a-button class="login-captcha">
|
||||||
|
<img alt="" :src="captchaImage" @click="refreshCaptcha" />
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<a-button
|
||||||
|
block
|
||||||
|
size="large"
|
||||||
|
type="primary"
|
||||||
|
:loading="codeLoading"
|
||||||
|
@click="() => doSendCode(imgCode)"
|
||||||
|
>
|
||||||
|
{{ t('login.forgetPage.sendNow') }}
|
||||||
|
</a-button>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<div class="login-copyright">
|
||||||
|
<a-space>
|
||||||
|
<span>© {{ new Date().getFullYear() }}</span>
|
||||||
|
<span>{{ config?.copyright || 'websoft.top Inc.' }}</span>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, computed, onUnmounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
LockOutlined,
|
||||||
|
MobileOutlined,
|
||||||
|
SafetyOutlined,
|
||||||
|
MailOutlined
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
import { FormInstance } from 'ant-design-vue/es/form';
|
||||||
|
import {
|
||||||
|
sendSmsCaptchaByAdmin,
|
||||||
|
sendEmailCaptcha,
|
||||||
|
getCaptcha
|
||||||
|
} from '@/api/passport/login';
|
||||||
|
import { resetPassword } from '@/api/layout';
|
||||||
|
import useFormData from '@/utils/use-form-data';
|
||||||
|
import { Config } from '@/api/cms/cmsWebsiteField/model';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 默认站点配置(与登录页保持一致)
|
||||||
|
const defaultConfig: Config = {
|
||||||
|
siteName: '小程序商城',
|
||||||
|
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
||||||
|
domain: '',
|
||||||
|
icpNo: '',
|
||||||
|
copyright: '',
|
||||||
|
loginBgImg: '',
|
||||||
|
address: '',
|
||||||
|
tel: '',
|
||||||
|
kefu2: '',
|
||||||
|
kefu1: '',
|
||||||
|
email: '',
|
||||||
|
loginTitle: '',
|
||||||
|
sysLogo: ''
|
||||||
|
};
|
||||||
|
const config = ref<Config>({ ...defaultConfig });
|
||||||
|
|
||||||
|
// 找回方式:email(邮箱) / phone(手机),默认邮箱
|
||||||
|
const activeTab = ref<'email' | 'phone'>('email');
|
||||||
|
|
||||||
|
const { form } = useFormData<{
|
||||||
|
email: string;
|
||||||
|
emailCode: string;
|
||||||
|
phone: string;
|
||||||
|
code: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}>({
|
||||||
|
email: '',
|
||||||
|
emailCode: '',
|
||||||
|
phone: '',
|
||||||
|
code: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance | null>(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 验证码倒计时
|
||||||
|
const countdownTime = ref(0);
|
||||||
|
let countdownTimer: number | null = null;
|
||||||
|
|
||||||
|
// 图形验证码弹窗
|
||||||
|
const captchaVisible = ref(false);
|
||||||
|
const captchaImage = ref('');
|
||||||
|
const captchaText = ref('');
|
||||||
|
const imgCode = ref('');
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
|
||||||
|
// 同一个账号前 3 次发送不需要图形验证码
|
||||||
|
const FREE_SEND_TIMES = 3;
|
||||||
|
|
||||||
|
// 根据当前找回方式和账号生成发送次数缓存 key
|
||||||
|
const getSendCountKey = () => {
|
||||||
|
const account = activeTab.value === 'email' ? form.email : form.phone;
|
||||||
|
return `forget_send_count_${activeTab.value}_${account}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 读取当前账号已发送次数
|
||||||
|
const getSendCount = () => {
|
||||||
|
const key = getSendCountKey();
|
||||||
|
if (!key) return 0;
|
||||||
|
const count = localStorage.getItem(key);
|
||||||
|
return count ? Number(count) || 0 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 增加发送次数
|
||||||
|
const increaseSendCount = () => {
|
||||||
|
const key = getSendCountKey();
|
||||||
|
if (!key) return;
|
||||||
|
localStorage.setItem(key, String(getSendCount() + 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新图形验证码
|
||||||
|
const refreshCaptcha = () => {
|
||||||
|
getCaptcha()
|
||||||
|
.then((data) => {
|
||||||
|
captchaImage.value = data.base64;
|
||||||
|
captchaText.value = data.text;
|
||||||
|
})
|
||||||
|
.catch((e: Error) => message.error(e.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开图形验证码弹窗
|
||||||
|
const openCaptchaModal = () => {
|
||||||
|
imgCode.value = '';
|
||||||
|
refreshCaptcha();
|
||||||
|
captchaVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 密码强度:至少8位,且包含字母和数字
|
||||||
|
const passwordRules = [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('login.forgetPage.newPasswordRule'),
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{ min: 8, message: t('login.forgetPage.newPasswordRule'), trigger: 'blur' },
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/,
|
||||||
|
message: t('login.forgetPage.newPasswordRule'),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const confirmPasswordRules = [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: t('login.forgetPage.confirmRule'),
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: (_rule: unknown, value: string) =>
|
||||||
|
value === form.newPassword
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error(t('login.forgetPage.confirmRule'))),
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 根据当前找回方式动态生成校验规则
|
||||||
|
const rules = computed(() => {
|
||||||
|
if (activeTab.value === 'email') {
|
||||||
|
return {
|
||||||
|
email: [
|
||||||
|
{ required: true, message: t('login.forgetPage.emailRule'), trigger: 'blur' },
|
||||||
|
{ type: 'email', message: t('login.forgetPage.emailRule'), trigger: 'blur' }
|
||||||
|
],
|
||||||
|
emailCode: [
|
||||||
|
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
|
||||||
|
],
|
||||||
|
newPassword: passwordRules,
|
||||||
|
confirmPassword: confirmPasswordRules
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
phone: [
|
||||||
|
{ required: true, message: t('login.forgetPage.phoneRule'), trigger: 'blur' },
|
||||||
|
{ pattern: /^1\d{10}$/, message: t('login.forgetPage.phoneRule'), trigger: 'blur' }
|
||||||
|
],
|
||||||
|
code: [
|
||||||
|
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
|
||||||
|
],
|
||||||
|
newPassword: passwordRules,
|
||||||
|
confirmPassword: confirmPasswordRules
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const onTabChange = () => {
|
||||||
|
formRef.value?.clearValidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送验证码(邮箱/手机)
|
||||||
|
const sendCode = () => {
|
||||||
|
if (activeTab.value === 'email') {
|
||||||
|
if (!form.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
|
||||||
|
message.error(t('login.forgetPage.emailRule'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
|
||||||
|
message.error(t('login.forgetPage.phoneRule'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (getSendCount() >= FREE_SEND_TIMES) {
|
||||||
|
openCaptchaModal();
|
||||||
|
} else {
|
||||||
|
doSendCode();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 真正调用接口发送验证码
|
||||||
|
const doSendCode = (captchaCode?: string) => {
|
||||||
|
if (captchaCode !== undefined) {
|
||||||
|
if (!captchaCode) {
|
||||||
|
message.error('请输入图形验证码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (captchaText.value !== captchaCode.toLowerCase()) {
|
||||||
|
message.error('图形验证码不正确');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
codeLoading.value = true;
|
||||||
|
const sendPromise =
|
||||||
|
activeTab.value === 'email'
|
||||||
|
? sendEmailCaptcha({ email: form.email }, { skipTenant: true })
|
||||||
|
: sendSmsCaptchaByAdmin({ phone: form.phone }, { skipTenant: true });
|
||||||
|
|
||||||
|
sendPromise
|
||||||
|
.then(() => {
|
||||||
|
message.success(
|
||||||
|
activeTab.value === 'email'
|
||||||
|
? t('login.forgetPage.sentEmail')
|
||||||
|
: t('login.forgetPage.getCode')
|
||||||
|
);
|
||||||
|
captchaVisible.value = false;
|
||||||
|
increaseSendCount();
|
||||||
|
startCountdown();
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
message.error(e.message);
|
||||||
|
refreshCaptcha();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
codeLoading.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdownTime.value = 60;
|
||||||
|
countdownTimer = window.setInterval(() => {
|
||||||
|
if (countdownTime.value <= 1) {
|
||||||
|
countdownTimer && clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
|
}
|
||||||
|
countdownTime.value--;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goLogin = () => {
|
||||||
|
router.push('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交重置
|
||||||
|
const submit = () => {
|
||||||
|
if (!formRef.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formRef.value
|
||||||
|
.validate()
|
||||||
|
.then(() => {
|
||||||
|
loading.value = true;
|
||||||
|
const params: Record<string, unknown> = {
|
||||||
|
newPassword: form.newPassword,
|
||||||
|
confirmPassword: form.confirmPassword
|
||||||
|
};
|
||||||
|
if (activeTab.value === 'email') {
|
||||||
|
params.email = form.email;
|
||||||
|
params.emailCode = form.emailCode;
|
||||||
|
} else {
|
||||||
|
params.phone = form.phone;
|
||||||
|
params.smsCode = form.code;
|
||||||
|
}
|
||||||
|
resetPassword(params as any)
|
||||||
|
.then((msg) => {
|
||||||
|
message.success(msg || t('login.forgetPage.success'));
|
||||||
|
loading.value = false;
|
||||||
|
router.push('/login');
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
message.error(e.message);
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.login-wrapper {
|
||||||
|
padding: 48px 16px 0 16px;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--grey-5);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
min-height: 100vh;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forget-subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin: -8px 0 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 380px;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 28px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 2px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
padding: 22px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
line-height: 50px;
|
||||||
|
|
||||||
|
.active {
|
||||||
|
color: #007dff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forget-tabs {
|
||||||
|
:deep(.ant-tabs-nav) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-input-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
|
||||||
|
:deep(.ant-input-affix-wrapper) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-captcha {
|
||||||
|
width: 102px;
|
||||||
|
height: 40px;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-copyright {
|
||||||
|
color: #eee;
|
||||||
|
text-align: center;
|
||||||
|
padding: 48px 0 22px 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-login {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-left: 6px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-height: 640px) {
|
||||||
|
.login-wrapper {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin-top: -260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-copyright {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.login-form {
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: auto;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,18 +32,21 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h4
|
<h4
|
||||||
|
ref="accountTabRef"
|
||||||
|
class="title-btn"
|
||||||
|
:class="loginType === 'account' ? 'active' : ''"
|
||||||
|
@click="onLoginType('account')"
|
||||||
|
>邮箱登录</h4
|
||||||
|
>
|
||||||
|
<a-divider type="vertical" style="height: 20px" />
|
||||||
|
<h4
|
||||||
|
ref="smsTabRef"
|
||||||
class="title-btn"
|
class="title-btn"
|
||||||
:class="loginType === 'sms' ? 'active' : ''"
|
:class="loginType === 'sms' ? 'active' : ''"
|
||||||
@click="onLoginType('sms')"
|
@click="onLoginType('sms')"
|
||||||
>手机号登录</h4
|
>手机号登录</h4
|
||||||
>
|
>
|
||||||
<a-divider type="vertical" style="height: 20px" />
|
<div class="tab-indicator" :style="indicatorStyle"></div>
|
||||||
<h4
|
|
||||||
class="title-btn"
|
|
||||||
:class="loginType === 'account' ? 'active' : ''"
|
|
||||||
@click="onLoginType('account')"
|
|
||||||
>账号登录</h4
|
|
||||||
>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -59,8 +62,8 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute top-2 right-2 text-lg text-white font-bold cursor-pointer"
|
class="absolute top-2 right-2 text-lg text-white font-bold cursor-pointer"
|
||||||
>
|
>
|
||||||
<QrcodeOutlined v-if="loginType === 'sms'" />
|
<MobileOutlined v-if="loginType === 'scan'" />
|
||||||
<MobileOutlined v-else />
|
<QrcodeOutlined v-else />
|
||||||
</div>
|
</div>
|
||||||
<!-- <span class="absolute top-3 right-1.5 text-sm text-white font-bold cursor-pointer">{{ '登录' }}</span>-->
|
<!-- <span class="absolute top-3 right-1.5 text-sm text-white font-bold cursor-pointer">{{ '登录' }}</span>-->
|
||||||
</div>
|
</div>
|
||||||
@@ -77,12 +80,12 @@
|
|||||||
<!-- </template>-->
|
<!-- </template>-->
|
||||||
<!-- </a-input>-->
|
<!-- </a-input>-->
|
||||||
<!-- </a-form-item>-->
|
<!-- </a-form-item>-->
|
||||||
<a-form-item name="username">
|
<a-form-item name="account">
|
||||||
<a-input
|
<a-input
|
||||||
allow-clear
|
allow-clear
|
||||||
size="large"
|
size="large"
|
||||||
v-model:value="form.userId"
|
v-model:value="form.account"
|
||||||
:placeholder="`用户ID`"
|
placeholder="请输入邮箱"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<UserOutlined />
|
<UserOutlined />
|
||||||
@@ -93,7 +96,7 @@
|
|||||||
<a-input-password
|
<a-input-password
|
||||||
size="large"
|
size="large"
|
||||||
v-model:value="form.password"
|
v-model:value="form.password"
|
||||||
placeholder="登录密码"
|
placeholder="请输入登录密码"
|
||||||
@pressEnter="submit"
|
@pressEnter="submit"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -141,6 +144,9 @@
|
|||||||
>
|
>
|
||||||
{{ loading ? t('login.loading') : t('login.login') }}
|
{{ loading ? t('login.loading') : t('login.login') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<div class="text-center pt-5 text-gray-500"
|
||||||
|
>还没有账号?联系管理员开通</div
|
||||||
|
>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="loginType === 'sms'">
|
<template v-if="loginType === 'sms'">
|
||||||
@@ -168,7 +174,7 @@
|
|||||||
<a-button
|
<a-button
|
||||||
class="login-captcha"
|
class="login-captcha"
|
||||||
:disabled="!!countdownTime"
|
:disabled="!!countdownTime"
|
||||||
@click="openImgCodeModal"
|
@click="handleSendCode"
|
||||||
>
|
>
|
||||||
<span v-if="!countdownTime">发送验证码</span>
|
<span v-if="!countdownTime">发送验证码</span>
|
||||||
<span v-else>已发送 {{ countdownTime }} s</span>
|
<span v-else>已发送 {{ countdownTime }} s</span>
|
||||||
@@ -185,6 +191,9 @@
|
|||||||
>
|
>
|
||||||
{{ loading ? t('login.loading') : t('login.login') }}
|
{{ loading ? t('login.loading') : t('login.login') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<div class="text-center pt-5 text-gray-500"
|
||||||
|
>还没有账号?联系管理员开通</div
|
||||||
|
>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="loginType == 'scan'">
|
<template v-if="loginType == 'scan'">
|
||||||
@@ -214,7 +223,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
placeholder="请输入图形验证码"
|
placeholder="请输入图形验证码"
|
||||||
allow-clear
|
allow-clear
|
||||||
@pressEnter="sendCode"
|
@pressEnter="() => sendCode(imgCode.value)"
|
||||||
/>
|
/>
|
||||||
<a-button class="login-captcha">
|
<a-button class="login-captcha">
|
||||||
<img alt="" :src="captcha" @click="changeCaptcha" />
|
<img alt="" :src="captcha" @click="changeCaptcha" />
|
||||||
@@ -225,7 +234,7 @@
|
|||||||
size="large"
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="codeLoading"
|
:loading="codeLoading"
|
||||||
@click="sendCode"
|
@click="() => sendCode(imgCode.value)"
|
||||||
>
|
>
|
||||||
立即发送
|
立即发送
|
||||||
</a-button>
|
</a-button>
|
||||||
@@ -261,11 +270,40 @@
|
|||||||
</a-list>
|
</a-list>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 多租户选择登录 -->
|
||||||
|
<a-modal
|
||||||
|
:width="420"
|
||||||
|
:footer="null"
|
||||||
|
:mask-closable="false"
|
||||||
|
title="请选择要登录的租户"
|
||||||
|
v-model:visible="showSelectTenantModal"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col justify-start">
|
||||||
|
<a-list item-layout="horizontal" :data-source="pendingTenants">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<a-list-item
|
||||||
|
class="cursor-pointer hover:border-gray-100"
|
||||||
|
@click="onSelectTenant(item)"
|
||||||
|
>
|
||||||
|
<a-list-item-meta :description="`租户ID: ${item.tenantId}`">
|
||||||
|
<template #title>
|
||||||
|
{{ item.tenantName }}
|
||||||
|
</template>
|
||||||
|
</a-list-item-meta>
|
||||||
|
<template #actions>
|
||||||
|
<RightOutlined />
|
||||||
|
</template>
|
||||||
|
</a-list-item>
|
||||||
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, reactive, unref, watch } from 'vue';
|
import { ref, reactive, unref, watch, nextTick, onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Form, message } from 'ant-design-vue';
|
import { Form, message } from 'ant-design-vue';
|
||||||
@@ -278,7 +316,14 @@
|
|||||||
RightOutlined
|
RightOutlined
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
|
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
|
||||||
import { login, loginBySms, getCaptcha } from '@/api/passport/login';
|
import {
|
||||||
|
loginBySuperAdminSms,
|
||||||
|
getCaptcha,
|
||||||
|
loginByEmail,
|
||||||
|
loginByUserId,
|
||||||
|
loginBySelectTenant
|
||||||
|
} from '@/api/passport/login';
|
||||||
|
import type { TenantOption } from '@/api/passport/login/model';
|
||||||
import QrLogin from '@/components/QrLogin/index.vue';
|
import QrLogin from '@/components/QrLogin/index.vue';
|
||||||
|
|
||||||
import { User } from '@/api/system/user/model';
|
import { User } from '@/api/system/user/model';
|
||||||
@@ -308,12 +353,66 @@
|
|||||||
const direction = ref(0);
|
const direction = ref(0);
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
// 是否显示tenantId填写输入框
|
// 是否显示tenantId填写输入框,默认进入账号登录/手机号登录切换模式
|
||||||
const loginType = ref('sms');
|
const loginType = ref('account');
|
||||||
|
// 标签下划线指示器
|
||||||
|
const accountTabRef = ref<HTMLElement>();
|
||||||
|
const smsTabRef = ref<HTMLElement>();
|
||||||
|
const indicatorStyle = ref({ left: '0px', width: '0px', transition: 'none' });
|
||||||
|
// 收缩/展开两阶段动画时长(ms),模拟阿里云"先收一下再展开"的弹性效果
|
||||||
|
const SHRINK_MS = 150;
|
||||||
|
const EXPAND_MS = 220;
|
||||||
|
let indicatorTimer: number | null = null;
|
||||||
|
const updateIndicator = (animate = true) => {
|
||||||
|
if (indicatorTimer != null) {
|
||||||
|
clearTimeout(indicatorTimer);
|
||||||
|
indicatorTimer = null;
|
||||||
|
}
|
||||||
|
nextTick(() => {
|
||||||
|
const activeEl =
|
||||||
|
loginType.value === 'account' ? accountTabRef.value : smsTabRef.value;
|
||||||
|
if (!activeEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetLeft = activeEl.offsetLeft;
|
||||||
|
const targetWidth = activeEl.offsetWidth;
|
||||||
|
// 非动画:直接定位(首次渲染 / 窗口缩放)
|
||||||
|
if (!animate) {
|
||||||
|
indicatorStyle.value = {
|
||||||
|
left: `${targetLeft}px`,
|
||||||
|
width: `${targetWidth}px`,
|
||||||
|
transition: 'none'
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 阶段一:收缩成一条短线,并滑向目标中心
|
||||||
|
const targetCenter = targetLeft + targetWidth / 2;
|
||||||
|
const smallWidth = 24;
|
||||||
|
indicatorStyle.value = {
|
||||||
|
left: `${targetCenter - smallWidth / 2}px`,
|
||||||
|
width: `${smallWidth}px`,
|
||||||
|
transition: `left ${SHRINK_MS}ms ease-in, width ${SHRINK_MS}ms ease-in`
|
||||||
|
};
|
||||||
|
// 阶段二:展开到目标标签的宽度
|
||||||
|
indicatorTimer = window.setTimeout(() => {
|
||||||
|
indicatorStyle.value = {
|
||||||
|
left: `${targetLeft}px`,
|
||||||
|
width: `${targetWidth}px`,
|
||||||
|
transition: `left ${EXPAND_MS}ms ease-out, width ${EXPAND_MS}ms ease-out`
|
||||||
|
};
|
||||||
|
indicatorTimer = null;
|
||||||
|
}, SHRINK_MS);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
watch(loginType, () => updateIndicator(true), { immediate: true });
|
||||||
|
onMounted(() => {
|
||||||
|
updateIndicator(false);
|
||||||
|
window.addEventListener('resize', () => updateIndicator(false));
|
||||||
|
});
|
||||||
|
|
||||||
// 默认配置
|
// 默认配置
|
||||||
const defaultConfig: Config = {
|
const defaultConfig: Config = {
|
||||||
siteName: '企业商城',
|
siteName: '小程序商城',
|
||||||
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
||||||
domain: '',
|
domain: '',
|
||||||
icpNo: '',
|
icpNo: '',
|
||||||
@@ -346,8 +445,10 @@
|
|||||||
console.warn('获取网站配置失败,使用默认配置:', e.message);
|
console.warn('获取网站配置失败,使用默认配置:', e.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const { form } = useFormData<User>({
|
type LoginForm = User & { account?: string };
|
||||||
userId: undefined,
|
|
||||||
|
const { form } = useFormData<LoginForm>({
|
||||||
|
account: '',
|
||||||
username: '',
|
username: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -373,9 +474,34 @@
|
|||||||
const countdownTime = ref(0);
|
const countdownTime = ref(0);
|
||||||
// 验证码倒计时定时器
|
// 验证码倒计时定时器
|
||||||
let countdownTimer: number | null = null;
|
let countdownTimer: number | null = null;
|
||||||
|
|
||||||
|
// 同一个手机号前 3 次发送不需要图形验证码
|
||||||
|
const FREE_SEND_TIMES = 3;
|
||||||
|
|
||||||
|
// 根据手机号生成发送次数缓存 key
|
||||||
|
const getSendCountKey = () => `login_sms_send_count_${form.phone}`;
|
||||||
|
|
||||||
|
// 读取当前手机号已发送次数
|
||||||
|
const getSendCount = () => {
|
||||||
|
const key = getSendCountKey();
|
||||||
|
if (!key) return 0;
|
||||||
|
const count = localStorage.getItem(key);
|
||||||
|
return count ? Number(count) || 0 : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 增加发送次数
|
||||||
|
const increaseSendCount = () => {
|
||||||
|
const key = getSendCountKey();
|
||||||
|
if (!key) return;
|
||||||
|
localStorage.setItem(key, String(getSendCount() + 1));
|
||||||
|
};
|
||||||
// 多用户选择账号登录
|
// 多用户选择账号登录
|
||||||
const showSelectLoginUser = ref<boolean>(false);
|
const showSelectLoginUser = ref<boolean>(false);
|
||||||
const admins = ref<User[]>([]);
|
const admins = ref<User[]>([]);
|
||||||
|
// 多租户选择登录
|
||||||
|
const showSelectTenantModal = ref<boolean>(false);
|
||||||
|
const pendingTenants = ref<TenantOption[]>([]);
|
||||||
|
const selectingTenant = ref<boolean>(false);
|
||||||
// const tenantId = getTenantId();
|
// const tenantId = getTenantId();
|
||||||
|
|
||||||
// 表格选中数据
|
// 表格选中数据
|
||||||
@@ -383,7 +509,7 @@
|
|||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
userId: [
|
account: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: t('login.username'),
|
message: t('login.username'),
|
||||||
@@ -422,32 +548,39 @@
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
/* 显示发送短信验证码弹窗 */
|
/* 处理发送短信验证码按钮点击 */
|
||||||
const openImgCodeModal = () => {
|
const handleSendCode = () => {
|
||||||
if (!form.phone) {
|
if (!form.phone) {
|
||||||
message.error('请输入手机号码');
|
message.error('请输入手机号码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (getSendCount() >= FREE_SEND_TIMES) {
|
||||||
imgCode.value = '';
|
imgCode.value = '';
|
||||||
changeCaptcha();
|
changeCaptcha();
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
|
} else {
|
||||||
|
sendCode();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 发送短信验证码 */
|
/* 发送短信验证码 */
|
||||||
const sendCode = () => {
|
const sendCode = (captchaCode?: string) => {
|
||||||
if (!imgCode.value) {
|
if (captchaCode !== undefined) {
|
||||||
|
if (!captchaCode) {
|
||||||
message.error('请输入图形验证码');
|
message.error('请输入图形验证码');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (text.value !== imgCode.value.toLowerCase()) {
|
if (text.value !== captchaCode.toLowerCase()) {
|
||||||
message.error('图形验证码不正确');
|
message.error('图形验证码不正确');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
codeLoading.value = true;
|
codeLoading.value = true;
|
||||||
sendSmsCaptcha({ phone: form.phone }).then(() => {
|
sendSmsCaptcha({ phone: form.phone })
|
||||||
|
.then(() => {
|
||||||
message.success('短信验证码发送成功, 请注意查收!');
|
message.success('短信验证码发送成功, 请注意查收!');
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
codeLoading.value = false;
|
increaseSendCount();
|
||||||
countdownTime.value = 30;
|
countdownTime.value = 30;
|
||||||
// 开始对按钮进行倒计时
|
// 开始对按钮进行倒计时
|
||||||
countdownTimer = window.setInterval(() => {
|
countdownTimer = window.setInterval(() => {
|
||||||
@@ -457,6 +590,13 @@
|
|||||||
}
|
}
|
||||||
countdownTime.value--;
|
countdownTime.value--;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
})
|
||||||
|
.catch((e: Error) => {
|
||||||
|
message.error(e.message);
|
||||||
|
changeCaptcha();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
codeLoading.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -479,24 +619,19 @@
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
form.code = form.smsCode?.toLowerCase();
|
form.code = form.smsCode?.toLowerCase();
|
||||||
loginBySms(form)
|
loginBySuperAdminSms(form)
|
||||||
.then((msg) => {
|
.then((outcome) => {
|
||||||
if (msg == '请选择登录用户') {
|
if (outcome.status === 'selectTenant') {
|
||||||
showSelectLoginUser.value = true;
|
pendingTenants.value = outcome.tenants;
|
||||||
listAdminsByPhoneAll({
|
showSelectTenantModal.value = true;
|
||||||
phone: form.phone,
|
loading.value = false;
|
||||||
templateId: TEMPLATE_ID
|
return;
|
||||||
}).then((data) => {
|
}
|
||||||
admins.value = data;
|
message.success(outcome.message);
|
||||||
});
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
message.success(msg);
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
resetFields();
|
resetFields();
|
||||||
cleanPageTabs();
|
cleanPageTabs();
|
||||||
goHome();
|
goHome();
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
message.error(e.message);
|
message.error(e.message);
|
||||||
@@ -511,6 +646,29 @@
|
|||||||
onLoginBySms();
|
onLoginBySms();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* 选择租户后二次登录 */
|
||||||
|
const onSelectTenant = (item: TenantOption) => {
|
||||||
|
if (selectingTenant.value || item.tenantId == null) return;
|
||||||
|
selectingTenant.value = true;
|
||||||
|
loginBySelectTenant({
|
||||||
|
phone: form.phone,
|
||||||
|
code: form.smsCode || '',
|
||||||
|
tenantId: item.tenantId,
|
||||||
|
remember: form.remember
|
||||||
|
})
|
||||||
|
.then((outcome) => {
|
||||||
|
showSelectTenantModal.value = false;
|
||||||
|
message.success(outcome.message);
|
||||||
|
resetFields();
|
||||||
|
cleanPageTabs();
|
||||||
|
goHome();
|
||||||
|
})
|
||||||
|
.catch((e: Error) => message.error(e.message))
|
||||||
|
.finally(() => {
|
||||||
|
selectingTenant.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/* 保存编辑 */
|
/* 保存编辑 */
|
||||||
const submit = () => {
|
const submit = () => {
|
||||||
if (!formRef.value) {
|
if (!formRef.value) {
|
||||||
@@ -519,15 +677,30 @@
|
|||||||
formRef.value
|
formRef.value
|
||||||
.validate()
|
.validate()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// if (form.code?.toLowerCase() !== text.value) {
|
if (form.code?.toLowerCase() !== text.value) {
|
||||||
// message.error('验证码错误');
|
message.error('验证码错误');
|
||||||
// changeCaptcha();
|
changeCaptcha();
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
const account = (form.account || '').trim();
|
||||||
|
// 邮箱格式判断
|
||||||
|
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(account);
|
||||||
|
// 纯数字判断(用户ID)
|
||||||
|
const isPureNumber = /^\d+$/.test(account);
|
||||||
|
if (!isEmail && !isPureNumber) {
|
||||||
|
message.error('请输入正确的邮箱或用户ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
form.code = form.code?.toLowerCase();
|
const payload = {
|
||||||
form.phone = undefined;
|
password: form.password,
|
||||||
login(form)
|
code: form.code?.toLowerCase(),
|
||||||
|
remember: form.remember
|
||||||
|
};
|
||||||
|
const loginApi = isEmail
|
||||||
|
? loginByEmail({ ...payload, email: account })
|
||||||
|
: loginByUserId({ ...payload, userId: account });
|
||||||
|
loginApi
|
||||||
.then((msg) => {
|
.then((msg) => {
|
||||||
if (msg == '请选择登录用户') {
|
if (msg == '请选择登录用户') {
|
||||||
showSelectLoginUser.value = true;
|
showSelectLoginUser.value = true;
|
||||||
@@ -566,7 +739,7 @@
|
|||||||
|
|
||||||
const onScan = () => {
|
const onScan = () => {
|
||||||
if (loginType.value == 'scan') {
|
if (loginType.value == 'scan') {
|
||||||
loginType.value = 'sms';
|
loginType.value = 'account';
|
||||||
} else {
|
} else {
|
||||||
loginType.value = 'scan';
|
loginType.value = 'scan';
|
||||||
}
|
}
|
||||||
@@ -691,9 +864,11 @@
|
|||||||
|
|
||||||
.title-btn {
|
.title-btn {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-title {
|
.login-title {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
@@ -701,6 +876,14 @@
|
|||||||
.active {
|
.active {
|
||||||
color: #007dff;
|
color: #007dff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tab-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -10px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: #007dff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,18 @@
|
|||||||
<div class="shop-dashboard">
|
<div class="shop-dashboard">
|
||||||
<!-- 欢迎横幅 -->
|
<!-- 欢迎横幅 -->
|
||||||
<div class="welcome-banner">
|
<div class="welcome-banner">
|
||||||
<div class="welcome-left">
|
<div class="welcome-left flex flex-row gap-2 items-center">
|
||||||
<h2 class="welcome-title">🏸 {{ userStore.info?.tenantName }}</h2>
|
<a-avatar
|
||||||
|
:src="shopLogoUrl || 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg'"
|
||||||
|
:size="64"
|
||||||
|
shape="square"
|
||||||
|
class="welcome-logo"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 class="welcome-title">{{ userStore.info?.tenantName }}</h2>
|
||||||
<p class="welcome-sub">欢迎回来,管理员,今日数据已更新</p>
|
<p class="welcome-sub">欢迎回来,管理员,今日数据已更新</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="welcome-right">
|
<div class="welcome-right">
|
||||||
<div class="welcome-actions">
|
<div class="welcome-actions">
|
||||||
<div class="order-notify-bar">
|
<div class="order-notify-bar">
|
||||||
@@ -14,15 +22,12 @@
|
|||||||
<a-tooltip title="开启后每60秒自动检测新订单,检测到后播放叮声并语音播报">
|
<a-tooltip title="开启后每60秒自动检测新订单,检测到后播放叮声并语音播报">
|
||||||
<InfoCircleOutlined class="order-notify-info" />
|
<InfoCircleOutlined class="order-notify-info" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-button size="small" :disabled="!notifyEnabled" @click="onTestNotify">测试</a-button>
|
<!-- <a-button size="small" :disabled="!notifyEnabled" @click="onTestNotify">测试</a-button>-->
|
||||||
</div>
|
|
||||||
<a-space>
|
|
||||||
<a-tag color="green" style="font-size:13px;padding:4px 12px">{{ siteStore.statusText || '正常' }}</a-tag>
|
|
||||||
<a-button size="small" @click="refreshStatistics" :loading="loading">
|
<a-button size="small" @click="refreshStatistics" :loading="loading">
|
||||||
<template #icon><ReloadOutlined /></template>
|
<template #icon><ReloadOutlined /></template>
|
||||||
刷新数据
|
刷新数据
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,35 +77,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<RightOutlined class="todo-arrow" />
|
<RightOutlined class="todo-arrow" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">
|
<!-- <div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">-->
|
||||||
🎉 暂无待处理事项,一切正常!
|
<!-- 🎉 暂无待处理事项,一切正常!-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
||||||
<!-- 快速入口 -->
|
<!-- 今日数据概况 -->
|
||||||
<a-col :xs="24" :md="12">
|
<a-col :xs="24" :md="12">
|
||||||
<div class="panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<span class="panel-title">⚡ 快速入口</span>
|
|
||||||
</div>
|
|
||||||
<div class="quick-grid">
|
|
||||||
<div
|
|
||||||
v-for="item in quickLinks"
|
|
||||||
:key="item.to"
|
|
||||||
class="quick-card"
|
|
||||||
@click="navigateTo(item.to)"
|
|
||||||
>
|
|
||||||
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
|
|
||||||
<div class="quick-label">{{ item.label }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
|
|
||||||
<!-- 今日数据趋势 -->
|
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title">📊 今日数据概况</span>
|
<span class="panel-title">📊 今日数据概况</span>
|
||||||
@@ -136,128 +121,536 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
<!-- 系统基本信息 -->
|
<!-- 今日数据概况 + 基本信息 -->
|
||||||
<a-row :gutter="[16, 16]" style="margin-top: 16px">
|
<a-row :gutter="[16, 16]">
|
||||||
<a-col :span="12">
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<a-col :xs="24" :md="12">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title">📋 基本信息</span>
|
<span class="panel-title">📋 基本信息</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 16px 18px;">
|
<div style="padding: 23px 18px;">
|
||||||
<a-descriptions :column="1" size="small">
|
<a-descriptions :column="1" size="small">
|
||||||
<a-descriptions-item label="系统名称">
|
<a-descriptions-item label="软件名称">
|
||||||
{{ siteStore.appName }}
|
<template v-if="subscriptionLoading">
|
||||||
|
<a-skeleton-input :active="true" size="small" style="width: 160px" />
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ currentProduct?.productName || '-' }}</template>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="版本号">
|
<a-descriptions-item label="当前版本">
|
||||||
{{ systemInfo.version }}
|
<template v-if="subscriptionLoading">
|
||||||
|
<a-skeleton-input :active="true" size="small" style="width: 160px" />
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ currentProduct?.version || '-' }}</template>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="运行状态">
|
<a-descriptions-item label="授权编号">
|
||||||
{{ siteStore.statusText || '正常' }}
|
<span>{{ currentSubscription?.subscriptionNo || '-' }}</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="创建时间">
|
<a-descriptions-item label="开通时间">
|
||||||
{{ siteInfo?.createTime }}
|
<template v-if="subscriptionLoading">
|
||||||
|
<a-skeleton-input :active="true" size="small" style="width: 160px" />
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ formatDateTime(currentSubscription?.startTime) }}</template>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="到期时间">
|
<a-descriptions-item label="到期时间">
|
||||||
{{ siteInfo?.expirationTime }}
|
<template v-if="subscriptionLoading">
|
||||||
</a-descriptions-item>
|
<a-skeleton-input :active="true" size="small" style="width: 140px" />
|
||||||
<a-descriptions-item label="系统运行">
|
</template>
|
||||||
{{ runDays }} 天
|
<template v-else-if="currentSubscription && currentSubscription.status === 'active' && currentSubscription.expireTime">
|
||||||
|
<span>{{ formatDateTime(currentSubscription.expireTime) }}</span>
|
||||||
|
<a-tag v-if="isExpired" :color="isExpired ? 'red' : 'green'" style="margin-left: 8px">
|
||||||
|
{{ isExpired ? '已过期' : '已激活' }}
|
||||||
|
</a-tag>
|
||||||
|
<span v-if="daysToExpire !== null && daysToExpire >= 0 && daysToExpire <= 7" class="expire-warn">
|
||||||
|
({{ daysToExpire }}天后到期)
|
||||||
|
</span>
|
||||||
|
<span v-else-if="daysToExpire !== null && daysToExpire < 0" class="expire-warn">
|
||||||
|
(已过期{{ Math.abs(daysToExpire) }}天)
|
||||||
|
</span>
|
||||||
|
<!-- 续费按钮:即将到期(7天内)或已过期时显示 -->
|
||||||
|
<a-button v-if="showRenewButton" type="link" size="small" @click="onRenew" style="padding: 0 0 0 8px">
|
||||||
|
立即续费
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="currentSubscription && currentSubscription.status === 'pending'">
|
||||||
|
<span class="text-orange">待支付 ¥{{ Number(currentSubscription.payPrice || 0).toFixed(2) }}</span>
|
||||||
|
<a-button type="link" size="small" @click="repayPending" style="padding: 0 0 0 8px">去支付</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-muted">未订阅</span>
|
||||||
|
<a-button type="link" size="small" @click="onSubscribe" style="padding: 0 0 0 8px">立即订阅</a-button>
|
||||||
|
</template>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
<!-- <a-descriptions-item label="系统运行">-->
|
||||||
|
<!-- <template v-if="subscriptionLoading">-->
|
||||||
|
<!-- <a-skeleton-input :active="true" size="small" style="width: 60px" />-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- <template v-else>-->
|
||||||
|
<!-- <a-tooltip title="自您首次开通小程序商城起">-->
|
||||||
|
<!-- <span>{{ runDays }} 天</span>-->
|
||||||
|
<!-- </a-tooltip>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </a-descriptions-item>-->
|
||||||
|
<!-- <a-descriptions-item label="绑定小程序">-->
|
||||||
|
<!-- {{ currentSubscription?.miniProgramId || 'wx75cbc65759eb981a' }}-->
|
||||||
|
<!-- </a-descriptions-item>-->
|
||||||
|
<!-- <a-descriptions-item label="售后服务">-->
|
||||||
|
<!-- <a href="https://websoft.top/login" class="text-gray-500">提交售后工单</a>-->
|
||||||
|
<!-- </a-descriptions-item>-->
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
<!-- 快速入口 -->
|
||||||
<!-- 快捷操作 -->
|
<a-col :xs="24" :md="12">
|
||||||
<a-col :span="12">
|
<div class="panel">
|
||||||
<div class="panel" style="min-height: 353px">
|
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<span class="panel-title">⚡ 快捷操作</span>
|
<span class="panel-title">⚡ 快速入口</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 16px 18px;">
|
<div class="quick-grid">
|
||||||
<a-space direction="vertical" style="width: 100%">
|
<div
|
||||||
<a-button
|
v-for="item in quickLinks"
|
||||||
type="primary"
|
:key="item.to"
|
||||||
block
|
class="quick-card"
|
||||||
@click="navigateTo('/website/field')"
|
@click="navigateTo(item.to)"
|
||||||
:loading="loading"
|
|
||||||
>
|
>
|
||||||
<UngroupOutlined />
|
<div class="quick-icon" :style="{ background: item.bg }">
|
||||||
参数配置
|
{{ item.icon }}
|
||||||
</a-button>
|
<span v-if="item.badge" class="quick-icon-dot"></span>
|
||||||
<a-button block @click="navigateTo('/shop/shopOrder')">
|
</div>
|
||||||
<CalendarOutlined />
|
<div class="quick-label">{{ item.label }}</div>
|
||||||
订单管理
|
</div>
|
||||||
</a-button>
|
|
||||||
<a-button block @click="navigateTo('/system/user')">
|
|
||||||
<UserOutlined />
|
|
||||||
用户管理
|
|
||||||
</a-button>
|
|
||||||
<a-button block @click="navigateTo('/website/index')">
|
|
||||||
<ShopOutlined />
|
|
||||||
站点管理
|
|
||||||
</a-button>
|
|
||||||
<a-button block @click="navigateTo('/system/login-record')">
|
|
||||||
<FileTextOutlined />
|
|
||||||
登录日志
|
|
||||||
</a-button>
|
|
||||||
<a-button block @click="handleClearCache">
|
|
||||||
<ClearOutlined />
|
|
||||||
清除缓存
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
|
<!-- 订阅/续费 Modal:选择订阅周期(subscribe-新订阅 / renew-续费) -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="subscribeModalVisible"
|
||||||
|
:title="subscribeMode === 'renew' ? '续费应用' : '订阅应用'"
|
||||||
|
:confirm-loading="subscribeMode === 'renew' ? renewing : subscribing"
|
||||||
|
:ok-text="subscribeMode === 'renew' ? '确认续费' : '确认订阅'"
|
||||||
|
cancel-text="取消"
|
||||||
|
:mask-closable="false"
|
||||||
|
@ok="subscribeMode === 'renew' ? confirmRenew() : confirmSubscribe()"
|
||||||
|
>
|
||||||
|
<div class="subscribe-modal-body">
|
||||||
|
<div class="subscribe-product-name">
|
||||||
|
{{ currentProduct?.productName || '应用订阅' }}
|
||||||
|
</div>
|
||||||
|
<a-alert
|
||||||
|
v-if="isFreeProduct"
|
||||||
|
type="success"
|
||||||
|
:message="subscribeMode === 'renew' ? '该应用为免费应用,续费后立即激活' : '该应用为免费应用,订阅后立即激活'"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
/>
|
||||||
|
<div v-else class="period-options">
|
||||||
|
<div
|
||||||
|
class="period-option"
|
||||||
|
:class="{ active: selectedPeriod === 'month' }"
|
||||||
|
@click="selectedPeriod = 'month'"
|
||||||
|
>
|
||||||
|
<div class="period-option-label">月付</div>
|
||||||
|
<div class="period-option-price">¥{{ monthPrice.toFixed(2) }}/月</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="period-option"
|
||||||
|
:class="{ active: selectedPeriod === 'year' }"
|
||||||
|
@click="selectedPeriod = 'year'"
|
||||||
|
>
|
||||||
|
<div class="period-option-label">年付</div>
|
||||||
|
<div class="period-option-price">¥{{ yearPrice.toFixed(2) }}/年</div>
|
||||||
|
<div class="period-option-tag">更划算</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 支付 Modal:微信扫码支付 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:open="payModalVisible"
|
||||||
|
:footer="null"
|
||||||
|
:mask-closable="false"
|
||||||
|
@cancel="onPayModalClose"
|
||||||
|
>
|
||||||
|
<div class="pay-modal-body">
|
||||||
|
<div class="pay-product-name">{{ payProductName }}</div>
|
||||||
|
<div class="pay-amount">
|
||||||
|
<span class="pay-amount-label">支付金额</span>
|
||||||
|
<span class="pay-amount-value">¥{{ payPrice.toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="pay-qrcode-wrap">
|
||||||
|
<img
|
||||||
|
v-if="payQrcode"
|
||||||
|
:src="payQrcode"
|
||||||
|
alt="支付二维码"
|
||||||
|
class="pay-qrcode-img"
|
||||||
|
/>
|
||||||
|
<a-spin v-else />
|
||||||
|
</div>
|
||||||
|
<div class="pay-tip">
|
||||||
|
<QrcodeOutlined />
|
||||||
|
请使用微信扫描上方二维码完成支付
|
||||||
|
</div>
|
||||||
|
<div class="pay-order-no">订单号:{{ paySubscriptionNo }}</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { reactive, ref, computed, onMounted, onUnmounted } from 'vue';
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
||||||
import {
|
import {
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
RightOutlined,
|
RightOutlined,
|
||||||
UngroupOutlined,
|
InfoCircleOutlined,
|
||||||
CalendarOutlined,
|
QrcodeOutlined
|
||||||
UserOutlined,
|
|
||||||
ShopOutlined,
|
|
||||||
FileTextOutlined,
|
|
||||||
ClearOutlined,
|
|
||||||
InfoCircleOutlined
|
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { message } from 'ant-design-vue/es';
|
import { message } from 'ant-design-vue/es';
|
||||||
import { useSiteStore } from '@/store/modules/site';
|
import { useSiteStore } from '@/store/modules/site';
|
||||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||||
import { useUserStore } from '@/store/modules/user';
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
import { useTenantStore } from '@/store/modules/tenant';
|
||||||
|
import { useAppSubscriptionStore } from '@/store/modules/appSubscription';
|
||||||
import { pageShopOrder } from '@/api/shop/shopOrder';
|
import { pageShopOrder } from '@/api/shop/shopOrder';
|
||||||
|
import { getShopSettingCategoryValues } from '@/api/shop/shopSetting';
|
||||||
|
import { getCompressedImageUrl } from '@/utils/image';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
|
import { getProductDetail } from '@/api/app/appProduct';
|
||||||
|
import type { AppProduct } from '@/api/app/appProduct/model';
|
||||||
|
import type { AppSubscription } from '@/api/app/appSubscription/model';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
// 使用状态管理
|
// 使用状态管理
|
||||||
const siteStore = useSiteStore();
|
const siteStore = useSiteStore();
|
||||||
const statisticsStore = useStatisticsStore();
|
const statisticsStore = useStatisticsStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const tenantStore = useTenantStore();
|
||||||
|
const subscriptionStore = useAppSubscriptionStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
// 从 store 中获取响应式数据
|
// 从 store 中获取响应式数据
|
||||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
const { loading: siteLoading } = storeToRefs(siteStore);
|
||||||
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
|
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
|
||||||
|
|
||||||
// 系统信息
|
// ============================================================
|
||||||
const systemInfo = reactive({
|
// 基本信息:数据来源拆分
|
||||||
name: '小程序商城',
|
// - 身份信息(系统名称/版本号/运行状态)来自 app_product (productId=65)
|
||||||
version: '2.0.0',
|
// - 时间线(开通时间/到期时间)来自 app_subscription
|
||||||
environment: '生产环境',
|
// · 开通时间 = subscription.startTime(订阅生效时间)
|
||||||
database: 'MySQL 8.0',
|
// · 到期时间 = subscription.expireTime
|
||||||
server: 'Linux CentOS 7.9',
|
// - 系统运行天数 = 自租户创建时间起累计(tenantStore.company.createTime)
|
||||||
|
// ============================================================
|
||||||
|
// 当前应用产品(按固定 productId=65 查询)
|
||||||
|
const currentProduct = ref<AppProduct | null>(null);
|
||||||
|
|
||||||
|
// 应用发布状态映射(运行状态)
|
||||||
|
const productStatusText = computed(() => {
|
||||||
|
const map: Record<string, { text: string; color: string }> = {
|
||||||
|
published: { text: '已发布', color: 'green' },
|
||||||
|
pending_review: { text: '审核中', color: 'orange' },
|
||||||
|
developing: { text: '开发中', color: 'blue' },
|
||||||
|
rejected: { text: '已驳回', color: 'red' },
|
||||||
|
deprecated: { text: '已下架', color: 'red' },
|
||||||
|
};
|
||||||
|
const status = currentProduct.value?.publishStatus;
|
||||||
|
if (!status) return { text: '-', color: 'default' };
|
||||||
|
return map[status] || { text: status, color: 'default' };
|
||||||
|
});
|
||||||
|
// 当前应用的有效订阅
|
||||||
|
const currentSubscription = ref<AppSubscription | null>(null);
|
||||||
|
// 订阅信息加载中
|
||||||
|
const subscriptionLoading = ref(false);
|
||||||
|
|
||||||
|
// 订阅 Modal(选择周期)
|
||||||
|
const subscribeModalVisible = ref(false);
|
||||||
|
const subscribing = ref(false);
|
||||||
|
const selectedPeriod = ref<'month' | 'year'>('month');
|
||||||
|
// Modal 模式:subscribe-新订阅 / renew-续费(复用同一 Modal)
|
||||||
|
const subscribeMode = ref<'subscribe' | 'renew'>('subscribe');
|
||||||
|
const renewing = ref(false);
|
||||||
|
|
||||||
|
// 支付 Modal(小程序码扫码支付)
|
||||||
|
const payModalVisible = ref(false);
|
||||||
|
const payQrcode = ref('');
|
||||||
|
const payPrice = ref(0);
|
||||||
|
const paySubscriptionNo = ref('');
|
||||||
|
const payProductName = ref('');
|
||||||
|
const pollingTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const pollCount = ref(0);
|
||||||
|
const MAX_POLL_COUNT = 120; // 2.5s * 120 = 5分钟
|
||||||
|
|
||||||
|
// 微信小程序环境版本:开发用 trial,生产用 release
|
||||||
|
const envVersion = import.meta.env.DEV ? 'trial' : 'release';
|
||||||
|
|
||||||
|
// 是否免费应用
|
||||||
|
const isFreeProduct = computed(() => {
|
||||||
|
const p = currentProduct.value;
|
||||||
|
if (!p) return false;
|
||||||
|
return p.priceType === 'free' || !p.price || Number(p.price) === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 月付参考价格(product.price 单位为分,除以100转为元)
|
||||||
|
const monthPrice = computed(() => (Number(currentProduct.value?.price) || 0) / 100);
|
||||||
|
// 年付参考价格(按10个月计费,对齐后端 subscribe/generatePayQrcode 年付优惠)
|
||||||
|
const yearPrice = computed(() => monthPrice.value * 10);
|
||||||
|
|
||||||
|
// 距到期天数(负数表示已过期),null 表示无到期时间
|
||||||
|
const daysToExpire = computed(() => {
|
||||||
|
const expire = currentSubscription.value?.expireTime;
|
||||||
|
if (!expire) return null;
|
||||||
|
return dayjs(expire).startOf('day').diff(dayjs().startOf('day'), 'day');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否已过期(expireTime 早于今天)
|
||||||
|
const isExpired = computed(
|
||||||
|
() => daysToExpire.value !== null && daysToExpire.value < 0
|
||||||
|
);
|
||||||
|
|
||||||
|
// 续费按钮显示条件:即将到期(7天内)或已过期
|
||||||
|
const showRenewButton = computed(() => {
|
||||||
|
if (daysToExpire.value === null) return false;
|
||||||
|
return daysToExpire.value <= 7;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatDateTime = (dt?: string) => {
|
||||||
|
if (!dt) return '-';
|
||||||
|
return dayjs(dt).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载当前应用的订阅信息
|
||||||
|
const loadSubscriptionInfo = async () => {
|
||||||
|
// 固定订阅产品:productId=65(小程序商城)
|
||||||
|
const FIXED_PRODUCT_ID = 65;
|
||||||
|
subscriptionLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 1. 按固定 productId 查询产品详情
|
||||||
|
const product = await getProductDetail(FIXED_PRODUCT_ID);
|
||||||
|
if (!product || !product.productId) {
|
||||||
|
console.warn(`未找到产品 ${FIXED_PRODUCT_ID}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentProduct.value = product;
|
||||||
|
|
||||||
|
// 2. 拉取我的订阅列表,过滤出该产品的订阅
|
||||||
|
const subRes = await subscriptionStore.fetchMySubscriptions(
|
||||||
|
{ page: 1, limit: 50 },
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const subs = (subRes.list || []).filter(
|
||||||
|
(s) => s.productId === FIXED_PRODUCT_ID
|
||||||
|
);
|
||||||
|
// 优先取 active 中 expireTime 最新;否则取 pending 最新
|
||||||
|
const active = subs
|
||||||
|
.filter((s) => s.status === 'active')
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
dayjs(b.expireTime).valueOf() - dayjs(a.expireTime).valueOf()
|
||||||
|
)[0];
|
||||||
|
const pending = subs
|
||||||
|
.filter((s) => s.status === 'pending')
|
||||||
|
.sort(
|
||||||
|
(a, b) => dayjs(b.createTime).valueOf() - dayjs(a.createTime).valueOf()
|
||||||
|
)[0];
|
||||||
|
currentSubscription.value = active || pending || null;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('获取订阅信息失败:', e);
|
||||||
|
} finally {
|
||||||
|
subscriptionLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新订阅信息(支付成功/订阅成功后调用)
|
||||||
|
const refreshSubscription = () => {
|
||||||
|
loadSubscriptionInfo().catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开订阅 Modal
|
||||||
|
const onSubscribe = () => {
|
||||||
|
if (!currentProduct.value?.productId) {
|
||||||
|
message.warning('应用信息加载中,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedPeriod.value = 'month';
|
||||||
|
subscribeMode.value = 'subscribe';
|
||||||
|
subscribeModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开续费 Modal(复用订阅 Modal)
|
||||||
|
const onRenew = () => {
|
||||||
|
if (!currentSubscription.value?.id) {
|
||||||
|
message.warning('订阅信息加载中,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedPeriod.value = 'month';
|
||||||
|
subscribeMode.value = 'renew';
|
||||||
|
subscribeModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认续费(后端 renew-pay 一步完成:创建续费订单 + 生成支付码,无需再调 pay)
|
||||||
|
const confirmRenew = async () => {
|
||||||
|
const sub = currentSubscription.value;
|
||||||
|
if (!sub?.id) return;
|
||||||
|
renewing.value = true;
|
||||||
|
try {
|
||||||
|
// renew-pay 直接返回支付结果(微信支付返回小程序码,余额支付返回 paid)
|
||||||
|
const result = await subscriptionStore.renew(
|
||||||
|
sub.id,
|
||||||
|
selectedPeriod.value,
|
||||||
|
'wechat',
|
||||||
|
envVersion
|
||||||
|
);
|
||||||
|
// 余额支付:直接成功
|
||||||
|
if ('paid' in result && result.paid) {
|
||||||
|
message.success('续费成功');
|
||||||
|
subscribeModalVisible.value = false;
|
||||||
|
refreshSubscription();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 微信支付:弹小程序码扫码
|
||||||
|
if ('miniappQrcode' in result) {
|
||||||
|
paySubscriptionNo.value = result.subscriptionNo;
|
||||||
|
payQrcode.value = result.miniappQrcode;
|
||||||
|
payPrice.value = Number(result.payPrice) || 0;
|
||||||
|
payProductName.value = currentProduct.value?.productName || '应用续费';
|
||||||
|
subscribeModalVisible.value = false;
|
||||||
|
payModalVisible.value = true;
|
||||||
|
// 开始轮询支付状态
|
||||||
|
startPolling(paySubscriptionNo.value);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('续费失败:', e);
|
||||||
|
message.error(e?.message || '续费失败,请重试');
|
||||||
|
} finally {
|
||||||
|
renewing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认订阅
|
||||||
|
const confirmSubscribe = async () => {
|
||||||
|
const product = currentProduct.value;
|
||||||
|
if (!product?.productId) return;
|
||||||
|
subscribing.value = true;
|
||||||
|
try {
|
||||||
|
// 1. 创建订阅(subscribe 后端会 setPriceType,避免 generatePayQrcode 的 price_type 报错)
|
||||||
|
const subResult = await subscriptionStore.subscribe({
|
||||||
|
productId: product.productId,
|
||||||
|
subscriptionPeriod: selectedPeriod.value
|
||||||
|
});
|
||||||
|
// 免费应用:subscribe 直接激活,status=active
|
||||||
|
if (subResult.status === 'active') {
|
||||||
|
message.success('订阅成功');
|
||||||
|
subscribeModalVisible.value = false;
|
||||||
|
refreshSubscription();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 2. 付费应用:用 pay 生成支付小程序码(参考 websopy-pc 的 subscribe + pay 模式)
|
||||||
|
const payResult = await subscriptionStore.pay(
|
||||||
|
subResult.subscriptionId,
|
||||||
|
'wechat',
|
||||||
|
envVersion
|
||||||
|
);
|
||||||
|
if ('miniappQrcode' in payResult) {
|
||||||
|
paySubscriptionNo.value =
|
||||||
|
payResult.subscriptionNo || subResult.subscriptionNo;
|
||||||
|
payQrcode.value = payResult.miniappQrcode;
|
||||||
|
payPrice.value = Number(payResult.payPrice) || 0;
|
||||||
|
payProductName.value = product.productName || '应用订阅';
|
||||||
|
subscribeModalVisible.value = false;
|
||||||
|
payModalVisible.value = true;
|
||||||
|
// 开始轮询支付状态
|
||||||
|
startPolling(paySubscriptionNo.value);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('创建订阅失败:', e);
|
||||||
|
message.error(e?.message || '创建订阅失败,请重试');
|
||||||
|
} finally {
|
||||||
|
subscribing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重新支付(pending 订阅,复用已有订阅记录生成小程序码)
|
||||||
|
const repayPending = async () => {
|
||||||
|
const sub = currentSubscription.value;
|
||||||
|
if (!sub?.id || !sub.subscriptionNo) return;
|
||||||
|
try {
|
||||||
|
const result = await subscriptionStore.pay(sub.id, 'wechat', envVersion);
|
||||||
|
if ('miniappQrcode' in result) {
|
||||||
|
paySubscriptionNo.value = sub.subscriptionNo;
|
||||||
|
payQrcode.value = result.miniappQrcode;
|
||||||
|
payPrice.value = Number(result.payPrice) || 0;
|
||||||
|
payProductName.value =
|
||||||
|
sub.productName || currentProduct.value?.productName || '应用订阅';
|
||||||
|
payModalVisible.value = true;
|
||||||
|
startPolling(sub.subscriptionNo);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
message.error(e?.message || '生成支付码失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始轮询支付状态(每 2.5s 查一次,最多 5 分钟)
|
||||||
|
const startPolling = (subscriptionNo: string) => {
|
||||||
|
stopPolling();
|
||||||
|
pollCount.value = 0;
|
||||||
|
pollingTimer.value = setInterval(async () => {
|
||||||
|
pollCount.value++;
|
||||||
|
if (pollCount.value > MAX_POLL_COUNT) {
|
||||||
|
stopPolling();
|
||||||
|
message.warning('支付状态查询超时,如已支付请刷新页面');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const status = await subscriptionStore.checkStatus(subscriptionNo);
|
||||||
|
if (status.paid) {
|
||||||
|
stopPolling();
|
||||||
|
payModalVisible.value = false;
|
||||||
|
message.success('支付成功');
|
||||||
|
refreshSubscription();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('查询支付状态失败:', e);
|
||||||
|
}
|
||||||
|
}, 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止轮询
|
||||||
|
const stopPolling = () => {
|
||||||
|
if (pollingTimer.value) {
|
||||||
|
clearInterval(pollingTimer.value);
|
||||||
|
pollingTimer.value = null;
|
||||||
|
}
|
||||||
|
pollCount.value = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 关闭支付 Modal
|
||||||
|
const onPayModalClose = () => {
|
||||||
|
stopPolling();
|
||||||
|
payModalVisible.value = false;
|
||||||
|
payQrcode.value = '';
|
||||||
|
paySubscriptionNo.value = '';
|
||||||
|
};
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const runDays = computed(() => siteStore.runDays);
|
const now = ref(Date.now());
|
||||||
|
let runDaysTimer: ReturnType<typeof setInterval>;
|
||||||
|
// 系统运行天数:自租户创建时间起累计
|
||||||
|
// 锚定 tenantStore.company.createTime(租户建档时间),与后台租户创建时间一致。
|
||||||
|
// 数据来源:loadData 中已调用 tenantStore.fetchTenantInfo() 写入 tenantStore.company。
|
||||||
|
const runDays = computed(() => {
|
||||||
|
const createTime = tenantStore.company?.createTime;
|
||||||
|
if (!createTime) return 0;
|
||||||
|
return Math.floor((now.value - new Date(createTime).getTime()) / (24 * 60 * 60 * 1000));
|
||||||
|
});
|
||||||
const userCount = computed(() => statisticsStore.userCount);
|
const userCount = computed(() => statisticsStore.userCount);
|
||||||
const orderCount = computed(() => statisticsStore.orderCount);
|
const orderCount = computed(() => statisticsStore.orderCount);
|
||||||
const totalSales = computed(() => statisticsStore.totalSales);
|
const totalSales = computed(() => statisticsStore.totalSales);
|
||||||
@@ -271,21 +664,29 @@ const loading = computed(() => siteLoading.value || statisticsLoading.value);
|
|||||||
// 核心统计数据(使用computed确保响应式更新)
|
// 核心统计数据(使用computed确保响应式更新)
|
||||||
const coreStats = computed(() => [
|
const coreStats = computed(() => [
|
||||||
{ icon: '🏸', label: '用户总数', value: userCount.value || 0, desc: '注册用户', color: 'blue', to: '/system/user' },
|
{ icon: '🏸', label: '用户总数', value: userCount.value || 0, desc: '注册用户', color: 'blue', to: '/system/user' },
|
||||||
{ icon: '📦', label: '订单总数', value: orderCount.value || 0, desc: '全部订单', color: 'orange', to: '/shop/shopOrder' },
|
{ icon: '📦', label: '订单总数', value: orderCount.value || 0, desc: '全部订单', color: 'orange', to: '/shop/shopOrder?tab=all' },
|
||||||
{ icon: '💰', label: '总营业额', value: totalSales.value || 0, desc: '元', color: 'purple', to: '/shop/shopOrder' },
|
{ icon: '💰', label: '总营业额', value: totalSales.value || 0, desc: '元', color: 'purple', to: '/shop/shopOrder?tab=all' },
|
||||||
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
|
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 待处理事项数据
|
// 待处理事项数据
|
||||||
|
const pendingPaymentCount = ref(0);
|
||||||
const pendingShipmentCount = ref(0);
|
const pendingShipmentCount = ref(0);
|
||||||
const pendingRefundCount = ref(0);
|
const pendingRefundCount = ref(0);
|
||||||
const couponUsedCount = computed(() => statisticsStore.couponUsedCount);
|
const couponUsedCount = computed(() => statisticsStore.safeCouponUsedCount);
|
||||||
|
|
||||||
|
// 商城Logo
|
||||||
|
const shopLogo = ref('');
|
||||||
|
const shopLogoUrl = computed(() =>
|
||||||
|
shopLogo.value ? getCompressedImageUrl(shopLogo.value, { width: 200, quality: 90 }) : ''
|
||||||
|
);
|
||||||
|
|
||||||
// 待处理事项(使用computed确保响应式更新)
|
// 待处理事项(使用computed确保响应式更新)
|
||||||
const todoItems = computed(() => [
|
const todoItems = computed(() => [
|
||||||
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
{ label: '待付款订单', value: pendingPaymentCount.value, to: '/shop/shopOrder?tab=unpaid', tagColor: 'gold', dotColor: 'dot-gold', urgent: pendingPaymentCount.value > 0 },
|
||||||
{ label: '退款申请', value: pendingRefundCount.value, to: '/shop/shopOrder', tagColor: 'orange', dotColor: 'dot-orange', urgent: pendingRefundCount.value > 0 },
|
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder?tab=undelivered', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
||||||
// { label: '优惠券使用', value: couponUsedCount.value, to: '/market/coupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
|
{ label: '退款申请', value: pendingRefundCount.value, to: '/shop/shopOrder?tab=refunded', tagColor: 'orange', dotColor: 'dot-orange', urgent: pendingRefundCount.value > 0 },
|
||||||
|
{ label: '优惠券使用', value: couponUsedCount.value, to: '/market/coupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 今日统计(使用computed确保响应式更新)
|
// 今日统计(使用computed确保响应式更新)
|
||||||
@@ -296,15 +697,15 @@ const todayStats = computed(() => ({
|
|||||||
couponUsed: couponUsedCount.value || 0,
|
couponUsed: couponUsedCount.value || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 快速入口
|
// 快速入口(订单管理项随新订单状态显示红点)
|
||||||
const quickLinks = [
|
const quickLinks = computed(() => [
|
||||||
{ to: '/website/field', icon: '⚙️', label: '参数配置', bg: '#eff6ff' },
|
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4', badge: hasNewOrder.value },
|
||||||
{ to: '/shop/shopOrder', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
|
{ to: '/shop/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
||||||
{ to: '/system/user', icon: '👥', label: '用户管理', bg: '#fff7ed' },
|
{ to: '/shop/shopGoodsCategory', icon: '🗂️', label: '商品分类', bg: '#eff6ff' },
|
||||||
{ to: '/website/index', icon: '🏪', label: '站点管理', bg: '#faf5ff' },
|
{ to: '/market/coupon', icon: '🎁', label: '优惠券', bg: '#fff7ed' },
|
||||||
{ to: '/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
{ to: '/system/user', icon: '👥', label: '会员管理', bg: '#faf5ff' },
|
||||||
{ to: '/cmsArticle', icon: '📝', label: '文章管理', bg: '#fefce8' },
|
{ to: '/shop/shopSetting', icon: '⚙️', label: '商城设置', bg: '#fefce8' },
|
||||||
];
|
]);
|
||||||
|
|
||||||
// 导航跳转
|
// 导航跳转
|
||||||
const navigateTo = (path: string) => {
|
const navigateTo = (path: string) => {
|
||||||
@@ -319,26 +720,15 @@ const formatMoney = (value?: number | string) => {
|
|||||||
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
|
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清除缓存
|
// 新订单声音提醒(全局单例:轮询由 layout 启动,此处仅消费状态 + UI)
|
||||||
const handleClearCache = () => {
|
const { enabled: notifyEnabled, hasNewOrder, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||||
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
|
|
||||||
(msg) => {
|
|
||||||
if (msg) {
|
|
||||||
message.success(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新订单声音提醒
|
|
||||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
|
||||||
onNewOrder: () => loadData()
|
onNewOrder: () => loadData()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 刷新统计数据
|
// 刷新统计数据(强制刷新,跳过缓存)
|
||||||
const refreshStatistics = async () => {
|
const refreshStatistics = async () => {
|
||||||
try {
|
try {
|
||||||
await statisticsStore.fetchStatistics();
|
await statisticsStore.fetchStatistics(true);
|
||||||
message.success('统计数据刷新成功');
|
message.success('统计数据刷新成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('刷新统计数据失败:', error);
|
console.error('刷新统计数据失败:', error);
|
||||||
@@ -348,6 +738,25 @@ const refreshStatistics = async () => {
|
|||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
|
// 独立请求租户信息,不受其他请求失败影响
|
||||||
|
tenantStore.fetchTenantInfo().catch((e) => {
|
||||||
|
console.warn('获取租户信息失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 独立加载订阅信息(到期时间),不受其他请求失败影响
|
||||||
|
loadSubscriptionInfo().catch((e) => {
|
||||||
|
console.warn('加载订阅信息失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 独立请求商城Logo,不受其他请求失败影响
|
||||||
|
getShopSettingCategoryValues('basic')
|
||||||
|
.then((values) => {
|
||||||
|
shopLogo.value = values?.shopLogo || '';
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('获取商城设置失败:', e);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
siteStore.fetchSiteInfo(),
|
siteStore.fetchSiteInfo(),
|
||||||
@@ -356,8 +765,23 @@ const loadData = async () => {
|
|||||||
|
|
||||||
// 获取待处理事项数据
|
// 获取待处理事项数据
|
||||||
try {
|
try {
|
||||||
// 获取待发货订单数(使用 statusFilter=1 对应待发货)
|
// 获取待付款订单数(type=0商城订单, statusFilter=0待付款)
|
||||||
|
const paymentResult = await pageShopOrder({
|
||||||
|
type: 0,
|
||||||
|
statusFilter: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 1
|
||||||
|
});
|
||||||
|
pendingPaymentCount.value = paymentResult?.count || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('获取待付款订单数失败:', e);
|
||||||
|
pendingPaymentCount.value = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取待发货订单数(type=0商城订单, statusFilter=1待发货)
|
||||||
const shipmentResult = await pageShopOrder({
|
const shipmentResult = await pageShopOrder({
|
||||||
|
type: 0,
|
||||||
statusFilter: 1,
|
statusFilter: 1,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 1
|
limit: 1
|
||||||
@@ -369,8 +793,9 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取退款/售后订单数(使用 statusFilter=6 对应退款/售后)
|
// 获取退款/售后订单数(type=0商城订单, statusFilter=6退款/售后)
|
||||||
const refundResult = await pageShopOrder({
|
const refundResult = await pageShopOrder({
|
||||||
|
type: 0,
|
||||||
statusFilter: 6,
|
statusFilter: 6,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 1
|
limit: 1
|
||||||
@@ -391,11 +816,17 @@ onMounted(async () => {
|
|||||||
|
|
||||||
// 开始自动刷新统计数据(每5分钟)
|
// 开始自动刷新统计数据(每5分钟)
|
||||||
statisticsStore.startAutoRefresh();
|
statisticsStore.startAutoRefresh();
|
||||||
|
|
||||||
|
// 运行天数每小时检查一次(跨天自动更新)
|
||||||
|
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 组件卸载时停止自动刷新
|
// 组件卸载时停止自动刷新
|
||||||
statisticsStore.stopAutoRefresh();
|
statisticsStore.stopAutoRefresh();
|
||||||
|
clearInterval(runDaysTimer);
|
||||||
|
// 停止支付状态轮询
|
||||||
|
stopPolling();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -419,6 +850,11 @@ onUnmounted(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
.welcome-logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 2px solid rgba(255,255,255,0.2);
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
|
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
|
||||||
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
|
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
|
||||||
|
|
||||||
@@ -475,7 +911,7 @@ onUnmounted(() => {
|
|||||||
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
|
||||||
|
|
||||||
/* 待办 */
|
/* 待办 */
|
||||||
.todo-list { padding: 8px 0; }
|
.todo-list { padding: 2px 0; }
|
||||||
.todo-item {
|
.todo-item {
|
||||||
display: flex; align-items: center; gap: 12px;
|
display: flex; align-items: center; gap: 12px;
|
||||||
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
|
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
|
||||||
@@ -485,6 +921,7 @@ onUnmounted(() => {
|
|||||||
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||||
.dot-orange { background: #f97316; }
|
.dot-orange { background: #f97316; }
|
||||||
.dot-blue { background: #3b82f6; }
|
.dot-blue { background: #3b82f6; }
|
||||||
|
.dot-gold { background: #faad14; }
|
||||||
.dot-cyan { background: #06b6d4; }
|
.dot-cyan { background: #06b6d4; }
|
||||||
.dot-purple { background: #a855f7; }
|
.dot-purple { background: #a855f7; }
|
||||||
.todo-content { flex: 1; display: flex; align-items: center; }
|
.todo-content { flex: 1; display: flex; align-items: center; }
|
||||||
@@ -508,15 +945,33 @@ onUnmounted(() => {
|
|||||||
.quick-icon {
|
.quick-icon {
|
||||||
width: 44px; height: 44px; border-radius: 10px;
|
width: 44px; height: 44px; border-radius: 10px;
|
||||||
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
display: flex; align-items: center; justify-content: center; font-size: 22px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
/* 快速入口新订单红点 */
|
||||||
|
.quick-icon-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -3px;
|
||||||
|
right: -3px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 77, 79, 0.3);
|
||||||
|
animation: quick-dot-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes quick-dot-pulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 1; }
|
||||||
|
50% { transform: scale(1.25); opacity: 0.85; }
|
||||||
}
|
}
|
||||||
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
|
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
|
||||||
|
|
||||||
/* 今日数据 */
|
/* 今日数据 */
|
||||||
.today-stats {
|
.today-stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 16px;
|
gap: 12px;
|
||||||
padding: 20px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.today-stat {
|
.today-stat {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -553,4 +1008,81 @@ onUnmounted(() => {
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 到期时间状态 */
|
||||||
|
.text-muted { color: rgba(0,0,0,0.45); }
|
||||||
|
.text-orange { color: #fa8c16; }
|
||||||
|
.expire-warn { color: #ff4d4f; font-size: 12px; margin-left: 4px; }
|
||||||
|
|
||||||
|
/* 订阅 Modal */
|
||||||
|
.subscribe-modal-body { padding: 8px 0; }
|
||||||
|
.subscribe-product-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: rgba(0,0,0,0.85);
|
||||||
|
}
|
||||||
|
.period-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.period-option {
|
||||||
|
flex: 1;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.period-option:hover { border-color: #69b1ff; }
|
||||||
|
.period-option.active {
|
||||||
|
border-color: #00704A;
|
||||||
|
background: #f0fdf4;
|
||||||
|
}
|
||||||
|
.period-option-label { font-size: 14px; color: rgba(0,0,0,0.65); margin-bottom: 8px; }
|
||||||
|
.period-option-price { font-size: 18px; font-weight: 700; color: #00704A; }
|
||||||
|
.period-option-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 8px;
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 支付 Modal */
|
||||||
|
.pay-modal-body { text-align: center; padding: 8px 0; }
|
||||||
|
.pay-product-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: rgba(0,0,0,0.85);
|
||||||
|
}
|
||||||
|
.pay-amount { margin-bottom: 16px; }
|
||||||
|
.pay-amount-label { font-size: 13px; color: rgba(0,0,0,0.45); margin-right: 8px; }
|
||||||
|
.pay-amount-value { font-size: 24px; font-weight: 800; color: #ff4d4f; }
|
||||||
|
.pay-qrcode-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.pay-qrcode-img {
|
||||||
|
width: 220px;
|
||||||
|
height: 220px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.pay-tip {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(0,0,0,0.55);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.pay-order-no {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0,0,0,0.35);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -588,6 +588,7 @@
|
|||||||
userId: undefined,
|
userId: undefined,
|
||||||
money: undefined,
|
money: undefined,
|
||||||
payType: undefined,
|
payType: undefined,
|
||||||
|
paymentVoucher: '',
|
||||||
wechatAccount: '',
|
wechatAccount: '',
|
||||||
wechatName: '',
|
wechatName: '',
|
||||||
alipayName: '',
|
alipayName: '',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<template>
|
<template>
|
||||||
|
<div class="goods-toolbar">
|
||||||
|
<!-- 常驻工具栏 -->
|
||||||
<a-space :size="10" style="flex-wrap: wrap">
|
<a-space :size="10" style="flex-wrap: wrap">
|
||||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
@@ -7,50 +9,35 @@
|
|||||||
</template>
|
</template>
|
||||||
<span>添加</span>
|
<span>添加</span>
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button
|
|
||||||
danger
|
<!-- 导入(合并为下拉菜单) -->
|
||||||
type="primary"
|
<a-dropdown>
|
||||||
class="ele-btn-icon"
|
<a-button type="dashed" class="ele-btn-icon">
|
||||||
:disabled="selection?.length === 0"
|
|
||||||
@click="removeBatch"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</template>
|
|
||||||
<span>批量删除</span>
|
|
||||||
</a-button>
|
|
||||||
<a-button type="dashed" class="ele-btn-icon" @click="openImport">
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
</template>
|
</template>
|
||||||
<span>导入</span>
|
<span>导入</span>
|
||||||
|
<DownOutlined class="ele-btn-icon-arrow" />
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="dashed" class="ele-btn-icon" @click="openImageImport">
|
<template #overlay>
|
||||||
<template #icon>
|
<a-menu>
|
||||||
<UploadOutlined />
|
<a-menu-item key="goods" @click="openImport">
|
||||||
|
导入商品
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="image" @click="openImageImport">
|
||||||
|
导入图片
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="detail" @click="openDetailImport">
|
||||||
|
导入详情
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
<span>导入图片</span>
|
</a-dropdown>
|
||||||
</a-button>
|
|
||||||
<a-button type="dashed" class="ele-btn-icon" @click="openDetailImport">
|
|
||||||
<template #icon>
|
|
||||||
<UploadOutlined />
|
|
||||||
</template>
|
|
||||||
<span>导入详情</span>
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
type="dashed"
|
|
||||||
class="ele-btn-icon"
|
|
||||||
:disabled="selection?.length === 0"
|
|
||||||
@click="convertContent"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<SyncOutlined />
|
|
||||||
</template>
|
|
||||||
<span>转换详情</span>
|
|
||||||
</a-button>
|
|
||||||
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
|
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
|
||||||
导出xls
|
导出xls
|
||||||
</a-button>
|
</a-button>
|
||||||
|
|
||||||
<a-radio-group v-model:value="type" @change="handleSearch">
|
<a-radio-group v-model:value="type" @change="handleSearch">
|
||||||
<a-radio-button value="出售中"
|
<a-radio-button value="出售中"
|
||||||
>出售中({{ goodsCount?.totalNum }})
|
>出售中({{ goodsCount?.totalNum }})
|
||||||
@@ -62,6 +49,7 @@
|
|||||||
>已售罄({{ goodsCount?.totalNum3 }})
|
>已售罄({{ goodsCount?.totalNum3 }})
|
||||||
</a-radio-button>
|
</a-radio-button>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
|
|
||||||
<a-tree-select
|
<a-tree-select
|
||||||
allow-clear
|
allow-clear
|
||||||
:tree-data="navigationList"
|
:tree-data="navigationList"
|
||||||
@@ -84,6 +72,41 @@
|
|||||||
/>
|
/>
|
||||||
<a-button type="text" @click="reset">重置</a-button>
|
<a-button type="text" @click="reset">重置</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
|
||||||
|
<!-- 选中操作栏:勾选商品后才显示 -->
|
||||||
|
<div v-if="selection && selection.length > 0" class="selection-bar">
|
||||||
|
<span class="selection-bar-count">已选 {{ selection.length }} 项</span>
|
||||||
|
<a-button
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
class="ele-btn-icon"
|
||||||
|
@click="removeBatch"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</template>
|
||||||
|
<span>批量删除</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="primary" class="ele-btn-icon" @click="batchUp">
|
||||||
|
<template #icon>
|
||||||
|
<ArrowUpOutlined />
|
||||||
|
</template>
|
||||||
|
<span>批量上架</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button danger class="ele-btn-icon" @click="batchDown">
|
||||||
|
<template #icon>
|
||||||
|
<ArrowDownOutlined />
|
||||||
|
</template>
|
||||||
|
<span>批量下架</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="dashed" class="ele-btn-icon" @click="convertContent">
|
||||||
|
<template #icon>
|
||||||
|
<SyncOutlined />
|
||||||
|
</template>
|
||||||
|
<span>转换详情</span>
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -91,7 +114,10 @@
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
SyncOutlined
|
SyncOutlined,
|
||||||
|
ArrowUpOutlined,
|
||||||
|
ArrowDownOutlined,
|
||||||
|
DownOutlined
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { getCount } from '@/api/shop/shopGoods';
|
import { getCount } from '@/api/shop/shopGoods';
|
||||||
@@ -102,8 +128,8 @@
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
// 选中的角色
|
// 选中的商品
|
||||||
selection?: [];
|
selection?: any[];
|
||||||
merchantId?: number;
|
merchantId?: number;
|
||||||
navigationList?: ShopGoodsCategory[];
|
navigationList?: ShopGoodsCategory[];
|
||||||
}>(),
|
}>(),
|
||||||
@@ -131,6 +157,8 @@
|
|||||||
(e: 'search', where?: ShopGoodsParam): void;
|
(e: 'search', where?: ShopGoodsParam): void;
|
||||||
(e: 'add'): void;
|
(e: 'add'): void;
|
||||||
(e: 'remove'): void;
|
(e: 'remove'): void;
|
||||||
|
(e: 'batchUp'): void;
|
||||||
|
(e: 'batchDown'): void;
|
||||||
(e: 'batchMove'): void;
|
(e: 'batchMove'): void;
|
||||||
(e: 'import'): void;
|
(e: 'import'): void;
|
||||||
(e: 'imageImport'): void;
|
(e: 'imageImport'): void;
|
||||||
@@ -149,6 +177,16 @@
|
|||||||
emit('remove');
|
emit('remove');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 批量上架
|
||||||
|
const batchUp = () => {
|
||||||
|
emit('batchUp');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量下架
|
||||||
|
const batchDown = () => {
|
||||||
|
emit('batchDown');
|
||||||
|
};
|
||||||
|
|
||||||
// 打开导入
|
// 打开导入
|
||||||
const openImport = () => {
|
const openImport = () => {
|
||||||
emit('import');
|
emit('import');
|
||||||
@@ -220,3 +258,35 @@
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.goods-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中后浮现的操作栏 */
|
||||||
|
.selection-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: #e6f4ff;
|
||||||
|
border: 1px solid #91caff;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-bar-count {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1677ff;
|
||||||
|
margin-right: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ele-btn-icon-arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -20,16 +20,53 @@
|
|||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
<div class="ele-text-center">
|
<div class="ele-text-center">
|
||||||
<span>只能上传 xls、xlsx 文件</span>
|
<span>只能上传 xls、xlsx 文件,</span>
|
||||||
|
<a :href="templateUrl" download="商品详情导入模板.xlsx">下载导入模板</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 使用说明 -->
|
||||||
|
<a-alert type="info" show-icon :style="{ marginTop: '16px' }">
|
||||||
|
<template #message>
|
||||||
|
<div class="import-tips">
|
||||||
|
<div class="import-tips-title">使用说明:</div>
|
||||||
|
<ul class="import-tips-list">
|
||||||
|
<li>Excel 中每行对应一个商品的详情内容</li>
|
||||||
|
<li>图片按<strong>文件夹</strong>组织,<strong>文件夹名称为商品编号</strong></li>
|
||||||
|
<li>文件夹内的图片按<strong>数字序号</strong>命名排序(如 1.jpg、2.png、3.jpg …)</li>
|
||||||
|
<li>图片序号对应 Excel 中的图片展示顺序</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
</ele-modal>
|
</ele-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.import-tips {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.import-tips-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.import-tips-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
li {
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { message } from 'ant-design-vue/es';
|
import { message } from 'ant-design-vue/es';
|
||||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||||
import { importShopGoodsDetails } from '@/api/shop/shopGoods';
|
import { importShopGoodsDetails } from '@/api/shop/shopGoods';
|
||||||
|
import { MODULES_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -42,6 +79,10 @@
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const templateUrl = computed(() => {
|
||||||
|
return MODULES_API_URL + '/shop/shop-goods/import/details/template';
|
||||||
|
});
|
||||||
|
|
||||||
const doUpload = ({ file }) => {
|
const doUpload = ({ file }) => {
|
||||||
if (
|
if (
|
||||||
![
|
![
|
||||||
|
|||||||
@@ -20,16 +20,18 @@
|
|||||||
</a-upload-dragger>
|
</a-upload-dragger>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
<div class="ele-text-center">
|
<div class="ele-text-center">
|
||||||
<span>只能上传 zip 文件,图片文件名需与商品编号对应</span>
|
<span>只能上传 zip 文件,图片文件名需与商品编号对应,</span>
|
||||||
|
<a :href="templateUrl" download="商品图片导入示例.zip">下载导入模板</a>
|
||||||
</div>
|
</div>
|
||||||
</ele-modal>
|
</ele-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { message } from 'ant-design-vue/es';
|
import { message } from 'ant-design-vue/es';
|
||||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||||
import { importShopGoodsImages } from '@/api/shop/shopGoods';
|
import { importShopGoodsImages } from '@/api/shop/shopGoods';
|
||||||
|
import { MODULES_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -42,6 +44,10 @@
|
|||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const templateUrl = computed(() => {
|
||||||
|
return MODULES_API_URL + '/shop/shop-goods/import/images/template';
|
||||||
|
});
|
||||||
|
|
||||||
const doUpload = ({ file }) => {
|
const doUpload = ({ file }) => {
|
||||||
const isZip =
|
const isZip =
|
||||||
file.type === 'application/zip' ||
|
file.type === 'application/zip' ||
|
||||||
|
|||||||
@@ -72,10 +72,10 @@
|
|||||||
v-model:value="form.unitName"
|
v-model:value="form.unitName"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="商品价格" name="price">
|
<a-form-item label="商城价" name="price">
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:placeholder="`商品价格`"
|
:placeholder="`商城价`"
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
:min="0.01"
|
:min="0.01"
|
||||||
v-model:value="form.price"
|
v-model:value="form.price"
|
||||||
@@ -98,9 +98,9 @@
|
|||||||
v-model:value="form.dealerPrice"
|
v-model:value="form.dealerPrice"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="套餐价格" name="originPrice">
|
<a-form-item label="进货价格" name="originPrice">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:placeholder="`套餐价格`"
|
:placeholder="`进货价格`"
|
||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
:min="0.01"
|
:min="0.01"
|
||||||
v-model:value="form.buyingPrice"
|
v-model:value="form.buyingPrice"
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
:precision="2"
|
:precision="2"
|
||||||
:min="0"
|
:min="0"
|
||||||
style="width: 110px"
|
style="width: 110px"
|
||||||
placeholder="商品价格"
|
placeholder="商城价"
|
||||||
/>
|
/>
|
||||||
<a-input-number
|
<a-input-number
|
||||||
v-model:value="batchSalePrice"
|
v-model:value="batchSalePrice"
|
||||||
@@ -895,7 +895,7 @@ const columns = [
|
|||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品价格',
|
title: '商城价',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
key: 'price',
|
key: 'price',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
@@ -1337,7 +1337,7 @@ const rules = reactive({
|
|||||||
price: [
|
price: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请填写会员价',
|
message: '请填写商城价',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
@@ -1353,7 +1353,7 @@ const rules = reactive({
|
|||||||
buyingPrice: [
|
buyingPrice: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请填写会员店/总仓批发价',
|
message: '请填写进货价',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:datasource="datasource"
|
:datasource="datasource"
|
||||||
:customRow="customRow"
|
:customRow="customRow"
|
||||||
|
:scroll="{ x: 1800 }"
|
||||||
|
size="small"
|
||||||
v-model:selection="selection"
|
v-model:selection="selection"
|
||||||
tool-class="ele-toolbar-form"
|
tool-class="ele-toolbar-form"
|
||||||
class="sys-org-table"
|
class="sys-org-table"
|
||||||
@@ -21,6 +23,8 @@
|
|||||||
:navigationList="navigationList"
|
:navigationList="navigationList"
|
||||||
@add="openEdit"
|
@add="openEdit"
|
||||||
@remove="removeBatch"
|
@remove="removeBatch"
|
||||||
|
@batchUp="batchUpShelf"
|
||||||
|
@batchDown="batchDownShelf"
|
||||||
@batchMove="openMove"
|
@batchMove="openMove"
|
||||||
@import="openImport"
|
@import="openImport"
|
||||||
@imageImport="openImageImport"
|
@imageImport="openImageImport"
|
||||||
@@ -37,10 +41,33 @@
|
|||||||
v-if="record.image"
|
v-if="record.image"
|
||||||
:preview="false"
|
:preview="false"
|
||||||
:width="50"
|
:width="50"
|
||||||
|
:height="50"
|
||||||
/>
|
/>
|
||||||
|
<div class="flex flex-col justify-center leading-tight">
|
||||||
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
||||||
|
<span
|
||||||
|
v-if="record.code"
|
||||||
|
class="text-gray-400 text-xs mt-1"
|
||||||
|
>编号:{{ record.code }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="column.key === 'priceGroup'">
|
||||||
|
<div class="flex flex-col leading-tight">
|
||||||
|
<span class="text-gray-800 font-bold">¥{{ record.price }}</span>
|
||||||
|
<span class="text-gray-400 text-xs"
|
||||||
|
>市场价:{{
|
||||||
|
record.salePrice ? `¥${record.salePrice}` : '-'
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
<span class="text-gray-400 text-xs"
|
||||||
|
>会员价:{{
|
||||||
|
record.dealerPrice ? `¥${record.dealerPrice}` : '-'
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-if="column.key === 'recommend'">
|
<template v-if="column.key === 'recommend'">
|
||||||
<a-space @click.stop="onRecommend(record)">
|
<a-space @click.stop="onRecommend(record)">
|
||||||
<span
|
<span
|
||||||
@@ -53,8 +80,20 @@
|
|||||||
/></span>
|
/></span>
|
||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="column.key === 'isTop'">
|
||||||
|
<a-space @click.stop="onTop(record)">
|
||||||
|
<span
|
||||||
|
v-if="record.isTop === 1"
|
||||||
|
class="ele-text-success cursor-pointer"
|
||||||
|
><CheckOutlined
|
||||||
|
/></span>
|
||||||
|
<span v-else class="ele-text-placeholder cursor-pointer"
|
||||||
|
><CloseOutlined
|
||||||
|
/></span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'status'">
|
||||||
<a-tag v-if="record.status === 0" color="green">已上架</a-tag>
|
<a-tag v-if="record.status === 0" color="green">出售中</a-tag>
|
||||||
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
|
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
|
||||||
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
|
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
|
||||||
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>
|
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>
|
||||||
@@ -63,6 +102,19 @@
|
|||||||
<a-space>
|
<a-space>
|
||||||
<a @click.stop="openEdit(record)">修改</a>
|
<a @click.stop="openEdit(record)">修改</a>
|
||||||
<a-divider type="vertical" />
|
<a-divider type="vertical" />
|
||||||
|
<a
|
||||||
|
v-if="record.status === 0"
|
||||||
|
class="ele-text-warning"
|
||||||
|
@click.stop="toggleShelf(record, 1)"
|
||||||
|
>下架</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
class="ele-text-success"
|
||||||
|
@click.stop="toggleShelf(record, 0)"
|
||||||
|
>上架</a
|
||||||
|
>
|
||||||
|
<a-divider type="vertical" />
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
title="确定要删除此记录吗?"
|
title="确定要删除此记录吗?"
|
||||||
@confirm.stop="remove(record)"
|
@confirm.stop="remove(record)"
|
||||||
@@ -194,35 +246,10 @@
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
width: 300
|
width: 300
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: '编号',
|
|
||||||
dataIndex: 'code',
|
|
||||||
key: 'code',
|
|
||||||
align: 'center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '市场价',
|
|
||||||
dataIndex: 'salePrice',
|
|
||||||
key: 'salePrice',
|
|
||||||
align: 'center',
|
|
||||||
width: 110,
|
|
||||||
customRender: ({ text }) => (text ? `¥${text}` : '-')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '价格',
|
title: '价格',
|
||||||
dataIndex: 'price',
|
key: 'priceGroup',
|
||||||
key: 'price',
|
dataIndex: 'priceGroup'
|
||||||
align: 'center',
|
|
||||||
width: 110,
|
|
||||||
customRender: ({ text }) => `¥${text}`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '会员价',
|
|
||||||
dataIndex: 'dealerPrice',
|
|
||||||
key: 'dealerPrice',
|
|
||||||
align: 'center',
|
|
||||||
width: 110,
|
|
||||||
customRender: ({ text }) => (text ? `¥${text}` : '-')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '销量',
|
title: '销量',
|
||||||
@@ -242,6 +269,12 @@
|
|||||||
key: 'recommend',
|
key: 'recommend',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '置顶',
|
||||||
|
dataIndex: 'isTop',
|
||||||
|
key: 'isTop',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
@@ -254,13 +287,14 @@
|
|||||||
// key: 'comments',
|
// key: 'comments',
|
||||||
// align: 'center',
|
// align: 'center',
|
||||||
// },
|
// },
|
||||||
{
|
// {
|
||||||
title: '排序号',
|
// title: '排序号',
|
||||||
dataIndex: 'sortNumber',
|
// dataIndex: 'sortNumber',
|
||||||
key: 'sortNumber',
|
// key: 'sortNumber',
|
||||||
align: 'center',
|
// align: 'center',
|
||||||
width: 120
|
// width: 120,
|
||||||
},
|
// sorter: true
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
@@ -270,15 +304,15 @@
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
width: 180,
|
width: 180,
|
||||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 180,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
hideInSetting: true
|
||||||
}
|
}
|
||||||
// {
|
|
||||||
// title: '操作',
|
|
||||||
// key: 'action',
|
|
||||||
// width: 180,
|
|
||||||
// fixed: 'right',
|
|
||||||
// align: 'center',
|
|
||||||
// hideInSetting: true
|
|
||||||
// }
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/* 搜索 */
|
/* 搜索 */
|
||||||
@@ -506,16 +540,86 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* 单个商品置顶/取消置顶(isTop: 1 置顶 0 不置顶) */
|
||||||
|
const onTop = (row: ShopGoods) => {
|
||||||
|
const hide = message.loading('请求中..', 0);
|
||||||
|
updateShopGoods({
|
||||||
|
...row,
|
||||||
|
isTop: row.isTop == 1 ? 0 : 1
|
||||||
|
})
|
||||||
|
.then((msg) => {
|
||||||
|
hide();
|
||||||
|
message.success(msg);
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
hide();
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 单个商品上/下架切换(status: 0 上架 1 下架) */
|
||||||
|
const toggleShelf = (row: ShopGoods, status: number) => {
|
||||||
|
const hide = message.loading('请求中..', 0);
|
||||||
|
updateShopGoods({ ...row, status })
|
||||||
|
.then((msg) => {
|
||||||
|
hide();
|
||||||
|
message.success(msg);
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
hide();
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 批量上架 */
|
||||||
|
const batchUpShelf = () => {
|
||||||
|
batchSetShelf(0, '上架');
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 批量下架 */
|
||||||
|
const batchDownShelf = () => {
|
||||||
|
batchSetShelf(1, '下架');
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 批量设置上下架 */
|
||||||
|
const batchSetShelf = (status: number, label: string) => {
|
||||||
|
if (!selection.value.length) {
|
||||||
|
message.error('请至少选择一条商品');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: `确定要批量${label}选中的 ${selection.value.length} 个商品吗?`,
|
||||||
|
icon: createVNode(ExclamationCircleOutlined),
|
||||||
|
maskClosable: true,
|
||||||
|
onOk: () => {
|
||||||
|
const hide = message.loading('请求中..', 0);
|
||||||
|
Promise.all(selection.value.map((d) => updateShopGoods({ ...d, status })))
|
||||||
|
.then(() => {
|
||||||
|
hide();
|
||||||
|
message.success(`已批量${label} ${selection.value.length} 个商品`);
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
hide();
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/* 自定义行属性 */
|
/* 自定义行属性 */
|
||||||
const customRow = (record: ShopGoods) => {
|
const customRow = (record: ShopGoods) => {
|
||||||
return {
|
return {
|
||||||
// 行点击事件
|
// 行点击事件
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
openEdit(record);
|
// openEdit(record);
|
||||||
},
|
},
|
||||||
// 行双击事件
|
// 行双击事件
|
||||||
onDblclick: () => {
|
onDblclick: () => {
|
||||||
// openEdit(record);
|
openEdit(record);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
87
src/views/shop/shopGoodsBrowse/components/search.vue
Normal file
87
src/views/shop/shopGoodsBrowse/components/search.vue
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!-- 搜索表单 -->
|
||||||
|
<template>
|
||||||
|
<a-space :size="10" style="flex-wrap: wrap">
|
||||||
|
<a-input
|
||||||
|
v-model:value="where.userId"
|
||||||
|
placeholder="用户ID"
|
||||||
|
allow-clear
|
||||||
|
style="width: 130px"
|
||||||
|
@press-enter="search"
|
||||||
|
/>
|
||||||
|
<a-input
|
||||||
|
v-model:value="where.goodsId"
|
||||||
|
placeholder="商品ID"
|
||||||
|
allow-clear
|
||||||
|
style="width: 130px"
|
||||||
|
@press-enter="search"
|
||||||
|
/>
|
||||||
|
<a-select
|
||||||
|
v-model:value="where.browseSource"
|
||||||
|
placeholder="浏览来源"
|
||||||
|
allow-clear
|
||||||
|
style="width: 130px"
|
||||||
|
>
|
||||||
|
<a-select-option value="detail">商品详情</a-select-option>
|
||||||
|
<a-select-option value="home">首页</a-select-option>
|
||||||
|
<a-select-option value="category">分类</a-select-option>
|
||||||
|
<a-select-option value="search">搜索</a-select-option>
|
||||||
|
<a-select-option value="share">分享</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<a-range-picker
|
||||||
|
v-model:value="dateRange"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
:placeholder="['开始日期', '结束日期']"
|
||||||
|
@change="onDateChange"
|
||||||
|
/>
|
||||||
|
<a-button type="primary" class="ele-btn-icon" @click="search">
|
||||||
|
<template #icon>
|
||||||
|
<SearchOutlined />
|
||||||
|
</template>
|
||||||
|
<span>查询</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="reset">重置</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons-vue';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
import type { ShopGoodsBrowseParam } from '@/api/shop/shopGoodsBrowse/model';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
where?: ShopGoodsBrowseParam;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'search', where?: ShopGoodsBrowseParam): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 搜索条件(双向绑定)
|
||||||
|
const where = computed(() => props.where ?? {});
|
||||||
|
|
||||||
|
// 日期范围
|
||||||
|
const dateRange = ref<[Dayjs, Dayjs] | undefined>();
|
||||||
|
|
||||||
|
// 日期变化
|
||||||
|
const onDateChange = (_dates: any, formatStrings: [string, string]) => {
|
||||||
|
where.value.startTime = formatStrings[0] || undefined;
|
||||||
|
where.value.endTime = formatStrings[1] || undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const search = () => {
|
||||||
|
emit('search', { ...where.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const reset = () => {
|
||||||
|
where.value.userId = undefined;
|
||||||
|
where.value.goodsId = undefined;
|
||||||
|
where.value.browseSource = undefined;
|
||||||
|
where.value.startTime = undefined;
|
||||||
|
where.value.endTime = undefined;
|
||||||
|
dateRange.value = undefined;
|
||||||
|
emit('search', {});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
196
src/views/shop/shopGoodsBrowse/index.vue
Normal file
196
src/views/shop/shopGoodsBrowse/index.vue
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<template>
|
||||||
|
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||||
|
<ele-pro-table
|
||||||
|
ref="tableRef"
|
||||||
|
row-key="id"
|
||||||
|
:columns="columns"
|
||||||
|
:datasource="datasource"
|
||||||
|
tool-class="ele-toolbar-form"
|
||||||
|
class="shop-goods-browse-table"
|
||||||
|
>
|
||||||
|
<template #toolbar>
|
||||||
|
<search @search="reload" />
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'goodsImage'">
|
||||||
|
<a-image
|
||||||
|
v-if="record.goodsImage"
|
||||||
|
:src="getCompressedImageUrl(record.goodsImage)"
|
||||||
|
:width="50"
|
||||||
|
:height="50"
|
||||||
|
style="border-radius: 4px; object-fit: cover"
|
||||||
|
/>
|
||||||
|
<span v-else class="ele-text-placeholder">无图</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'browseSource'">
|
||||||
|
<a-tag v-if="record.browseSource === 'detail'" color="blue">商品详情</a-tag>
|
||||||
|
<a-tag v-else-if="record.browseSource === 'home'" color="green">首页</a-tag>
|
||||||
|
<a-tag v-else-if="record.browseSource === 'category'" color="cyan">分类</a-tag>
|
||||||
|
<a-tag v-else-if="record.browseSource === 'search'" color="orange">搜索</a-tag>
|
||||||
|
<a-tag v-else-if="record.browseSource === 'share'" color="purple">分享</a-tag>
|
||||||
|
<span v-else>{{ record.browseSource || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'visitCount'">
|
||||||
|
<a-tag color="red">{{ record.visitCount || 1 }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'lastVisitTime'">
|
||||||
|
{{ toDateString(record.lastVisitTime, 'yyyy-MM-dd HH:mm') }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'createTime'">
|
||||||
|
{{ toDateString(record.createTime, 'yyyy-MM-dd HH:mm') }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-popconfirm
|
||||||
|
title="确定要删除此浏览记录吗?"
|
||||||
|
@confirm="remove(record)"
|
||||||
|
>
|
||||||
|
<a class="ele-text-danger">删除</a>
|
||||||
|
</a-popconfirm>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</ele-pro-table>
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import type { EleProTable } from 'ele-admin-pro';
|
||||||
|
import { toDateString } from 'ele-admin-pro';
|
||||||
|
import { getCompressedImageUrl } from '@/utils/image';
|
||||||
|
import type {
|
||||||
|
DatasourceFunction,
|
||||||
|
ColumnItem
|
||||||
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
|
import Search from './components/search.vue';
|
||||||
|
import {
|
||||||
|
pageShopGoodsBrowse,
|
||||||
|
removeShopGoodsBrowse
|
||||||
|
} from '@/api/shop/shopGoodsBrowse';
|
||||||
|
import type {
|
||||||
|
ShopGoodsBrowse,
|
||||||
|
ShopGoodsBrowseParam
|
||||||
|
} from '@/api/shop/shopGoodsBrowse/model';
|
||||||
|
|
||||||
|
// 表格实例
|
||||||
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
|
|
||||||
|
// 表格数据源
|
||||||
|
const datasource: DatasourceFunction = ({
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
where,
|
||||||
|
orders
|
||||||
|
}) => {
|
||||||
|
return pageShopGoodsBrowse({
|
||||||
|
...where,
|
||||||
|
...orders,
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格列配置
|
||||||
|
const columns = ref<ColumnItem[]>([
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
align: 'center',
|
||||||
|
width: 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户ID',
|
||||||
|
dataIndex: 'userId',
|
||||||
|
key: 'userId',
|
||||||
|
align: 'center',
|
||||||
|
width: 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品图片',
|
||||||
|
key: 'goodsImage',
|
||||||
|
align: 'center',
|
||||||
|
width: 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商品名称',
|
||||||
|
dataIndex: 'goodsName',
|
||||||
|
key: 'goodsName',
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '商城价',
|
||||||
|
dataIndex: 'price',
|
||||||
|
key: 'price',
|
||||||
|
align: 'center',
|
||||||
|
width: 100,
|
||||||
|
customRender: ({ text }) => (text ? `¥${text}` : '-')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '浏览次数',
|
||||||
|
dataIndex: 'visitCount',
|
||||||
|
key: 'visitCount',
|
||||||
|
align: 'center',
|
||||||
|
width: 90,
|
||||||
|
sorter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '浏览来源',
|
||||||
|
dataIndex: 'browseSource',
|
||||||
|
key: 'browseSource',
|
||||||
|
align: 'center',
|
||||||
|
width: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后浏览时间',
|
||||||
|
dataIndex: 'lastVisitTime',
|
||||||
|
key: 'lastVisitTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 160,
|
||||||
|
sorter: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '首次浏览',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
align: 'center',
|
||||||
|
width: 160
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'right',
|
||||||
|
align: 'center',
|
||||||
|
hideInSetting: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
/* 搜索 */
|
||||||
|
const reload = (where?: ShopGoodsBrowseParam) => {
|
||||||
|
tableRef?.value?.reload({ where: where });
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 删除单条 */
|
||||||
|
const remove = (row: ShopGoodsBrowse) => {
|
||||||
|
const hide = message.loading('请求中..', 0);
|
||||||
|
removeShopGoodsBrowse(row.id)
|
||||||
|
.then((msg) => {
|
||||||
|
hide();
|
||||||
|
message.success(msg);
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
hide();
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
name: 'ShopGoodsBrowse'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped></style>
|
||||||
@@ -23,10 +23,11 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'title'">
|
<template v-if="column.key === 'title'">
|
||||||
<a-space :style="{ paddingLeft: ((depthMap.get(record.categoryId || 0) || 0) * 20) + 'px' }">
|
<div style="display: flex; align-items: center; gap: 6px;">
|
||||||
<img v-if="record.image" :src="record.image" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
|
<span v-html="renderTreePrefix(record)"></span>
|
||||||
|
<img v-if="record.image" :src="getCompressedImageUrl(record.image, { width: 48, quality: 90 })" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
|
||||||
<span class="font-bold">{{ record.title }}</span>
|
<span class="font-bold">{{ record.title }}</span>
|
||||||
</a-space>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'type'">
|
<template v-if="column.key === 'type'">
|
||||||
<a-tag :color="record.type === 0 ? 'blue' : 'orange'">
|
<a-tag :color="record.type === 0 ? 'blue' : 'orange'">
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
<template v-if="column.key === 'adImage'">
|
<template v-if="column.key === 'adImage'">
|
||||||
<img
|
<img
|
||||||
v-if="record.adImage"
|
v-if="record.adImage"
|
||||||
:src="record.adImage"
|
:src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
|
||||||
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
|
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
|
||||||
/>
|
/>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
import Search from './components/search.vue';
|
import Search from './components/search.vue';
|
||||||
import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
|
import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
|
||||||
|
import { getCompressedImageUrl } from '@/utils/image';
|
||||||
import {
|
import {
|
||||||
listShopGoodsCategory,
|
listShopGoodsCategory,
|
||||||
removeShopGoodsCategory,
|
removeShopGoodsCategory,
|
||||||
@@ -112,6 +114,10 @@
|
|||||||
const flatCategoryList = ref<ShopGoodsCategory[]>([]);
|
const flatCategoryList = ref<ShopGoodsCategory[]>([]);
|
||||||
// 分类层级深度映射
|
// 分类层级深度映射
|
||||||
const depthMap = ref<Map<number, number>>(new Map());
|
const depthMap = ref<Map<number, number>>(new Map());
|
||||||
|
// 分类节点映射(categoryId -> 节点),用于回溯祖先链
|
||||||
|
const nodeMap = ref<Map<number, ShopGoodsCategory>>(new Map());
|
||||||
|
// 是否为父级最后一个子节点(用于绘制树形连接线的拐角 / 引导竖线)
|
||||||
|
const isLastMap = ref<Map<number, boolean>>(new Map());
|
||||||
|
|
||||||
// 计算每个分类的层级深度
|
// 计算每个分类的层级深度
|
||||||
const computeDepth = (list: ShopGoodsCategory[]) => {
|
const computeDepth = (list: ShopGoodsCategory[]) => {
|
||||||
@@ -132,16 +138,76 @@
|
|||||||
depthMap.value = map;
|
depthMap.value = map;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 构建树形元信息:节点映射 + 是否末位子节点
|
||||||
|
const buildTreeMeta = (list: ShopGoodsCategory[]) => {
|
||||||
|
const byParent = new Map<number, ShopGoodsCategory[]>();
|
||||||
|
const nMap = new Map<number, ShopGoodsCategory>();
|
||||||
|
list.forEach((c) => {
|
||||||
|
if (c.categoryId != null) nMap.set(c.categoryId, c);
|
||||||
|
const pid = c.parentId || 0;
|
||||||
|
if (!byParent.has(pid)) byParent.set(pid, []);
|
||||||
|
byParent.get(pid)!.push(c);
|
||||||
|
});
|
||||||
|
const lastMap = new Map<number, boolean>();
|
||||||
|
byParent.forEach((children) => {
|
||||||
|
children.forEach((c, idx) => {
|
||||||
|
if (c.categoryId != null) {
|
||||||
|
lastMap.set(c.categoryId, idx === children.length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
nodeMap.value = nMap;
|
||||||
|
isLastMap.value = lastMap;
|
||||||
|
};
|
||||||
|
|
||||||
// 表格数据源(不分页,全量展示便于查看层级)
|
// 表格数据源(不分页,全量展示便于查看层级)
|
||||||
const datasource: DatasourceFunction = ({ where, orders }) => {
|
const datasource: DatasourceFunction = ({ where, orders }) => {
|
||||||
return listShopGoodsCategory({ ...where, ...orders }).then((res) => {
|
return listShopGoodsCategory({ ...where, ...orders }).then((res) => {
|
||||||
const list = res || [];
|
const list = res || [];
|
||||||
flatCategoryList.value = list;
|
flatCategoryList.value = list;
|
||||||
computeDepth(list);
|
computeDepth(list);
|
||||||
|
buildTreeMeta(list);
|
||||||
return { list, count: list.length };
|
return { list, count: list.length };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 生成树形连接线 SVG:顶级不画;二级/三级根据祖先链绘制引导竖线与拐角
|
||||||
|
const renderTreePrefix = (record: ShopGoodsCategory): string => {
|
||||||
|
const depth = depthMap.value.get(record.categoryId || 0) || 0;
|
||||||
|
if (depth === 0) return '';
|
||||||
|
const W = 20;
|
||||||
|
const H = 40;
|
||||||
|
const mid = H / 2;
|
||||||
|
// 回溯祖先链:chain[0]=顶级 ... chain[depth]=当前节点
|
||||||
|
const chain: number[] = [];
|
||||||
|
let cur: number | undefined = record.categoryId;
|
||||||
|
while (cur != null) {
|
||||||
|
chain.unshift(cur);
|
||||||
|
cur = nodeMap.value.get(cur)?.parentId || undefined;
|
||||||
|
if (chain.length > 30) break;
|
||||||
|
}
|
||||||
|
let svg = `<svg width="${depth * W}" height="${H}" viewBox="0 0 ${depth * W} ${H}" fill="none" style="flex-shrink:0;display:block;">`;
|
||||||
|
const stroke = '#d0d0d0';
|
||||||
|
for (let k = 0; k < depth; k++) {
|
||||||
|
const x = k * W + Math.floor(W / 2);
|
||||||
|
if (k < depth - 1) {
|
||||||
|
// 祖先节点不是末位子节点时,画贯穿引导竖线
|
||||||
|
if (!isLastMap.value.get(chain[k])) {
|
||||||
|
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 末段:竖线到中线 + 横线到右侧
|
||||||
|
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
|
||||||
|
if (!isLastMap.value.get(record.categoryId || 0)) {
|
||||||
|
svg += `<line x1="${x}" y1="${mid}" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
|
||||||
|
}
|
||||||
|
svg += `<line x1="${x}" y1="${mid}" x2="${(k + 1) * W}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
svg += `</svg>`;
|
||||||
|
return svg;
|
||||||
|
};
|
||||||
|
|
||||||
// 获取父级分类名称
|
// 获取父级分类名称
|
||||||
const getParentName = (parentId?: number) => {
|
const getParentName = (parentId?: number) => {
|
||||||
if (!parentId || parentId === 0) return '顶级分类';
|
if (!parentId || parentId === 0) return '顶级分类';
|
||||||
|
|||||||
@@ -45,10 +45,10 @@
|
|||||||
@del="onDeleteItem"
|
@del="onDeleteItem"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="商品价格" name="price">
|
<a-form-item label="商城价" name="price">
|
||||||
<a-input
|
<a-input
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="请输入商品价格"
|
placeholder="请输入商城价"
|
||||||
v-model:value="form.price"
|
v-model:value="form.price"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|||||||
@@ -137,7 +137,7 @@
|
|||||||
align: 'center'
|
align: 'center'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品价格',
|
title: '商城价',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
key: 'price',
|
key: 'price',
|
||||||
align: 'center'
|
align: 'center'
|
||||||
|
|||||||
251
src/views/shop/shopOrder/components/OfflinePaymentModal.vue
Normal file
251
src/views/shop/shopOrder/components/OfflinePaymentModal.vue
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<!-- 确认线下收款弹窗 -->
|
||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:visible="visible"
|
||||||
|
title="确认线下收款"
|
||||||
|
:width="520"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
ok-text="确认收款"
|
||||||
|
@update:visible="updateVisible"
|
||||||
|
@ok="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<a-alert
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
:message="alertMessage"
|
||||||
|
style="margin-bottom: 16px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="form"
|
||||||
|
:rules="rules"
|
||||||
|
:label-col="{ span: 5 }"
|
||||||
|
:wrapper-col="{ span: 18 }"
|
||||||
|
>
|
||||||
|
<!-- 订单号 -->
|
||||||
|
<a-form-item label="订单号">
|
||||||
|
<span style="color: #999">{{ form.orderNo || '-' }}</span>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 支付凭证 -->
|
||||||
|
<a-form-item label="支付凭证" name="paymentVoucher">
|
||||||
|
<a-upload
|
||||||
|
list-type="picture-card"
|
||||||
|
:max-count="1"
|
||||||
|
:file-list="fileList"
|
||||||
|
:custom-request="handleUpload"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
@remove="handleRemove"
|
||||||
|
@preview="handlePreview"
|
||||||
|
>
|
||||||
|
<div v-if="!fileList.length">
|
||||||
|
<PlusOutlined />
|
||||||
|
<div class="ant-upload-text">上传凭证</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
<div class="voucher-tip">支持 jpg/png 图片,单张不超过 10MB</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<a-form-item label="备注" name="remarks">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="form.remarks"
|
||||||
|
placeholder="可填写备注(如:微信转账已收到)"
|
||||||
|
:rows="3"
|
||||||
|
:maxlength="200"
|
||||||
|
show-count
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<a-modal
|
||||||
|
:visible="previewVisible"
|
||||||
|
:footer="null"
|
||||||
|
title="凭证预览"
|
||||||
|
@cancel="previewVisible = false"
|
||||||
|
>
|
||||||
|
<img :src="previewImage" alt="支付凭证" style="width: 100%" />
|
||||||
|
</a-modal>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, reactive, watch, computed } from 'vue';
|
||||||
|
import { Form, message } from 'ant-design-vue';
|
||||||
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
|
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||||
|
import { ShopOrder } from '@/api/shop/shopOrder/model';
|
||||||
|
import { confirmOfflinePayment } from '@/api/shop/shopOrder';
|
||||||
|
import { uploadOss } from '@/api/system/file';
|
||||||
|
import { getUrl } from '@/utils/common';
|
||||||
|
|
||||||
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean;
|
||||||
|
data?: ShopOrder | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
(e: 'done'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const form = reactive({
|
||||||
|
orderId: undefined as number | undefined,
|
||||||
|
orderNo: '' as string,
|
||||||
|
paymentVoucher: '' as string,
|
||||||
|
remarks: '' as string,
|
||||||
|
deliveryStatus: undefined as number | undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const rules = {
|
||||||
|
paymentVoucher: [
|
||||||
|
{ required: false, message: '请上传支付凭证' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据发货状态显示不同的提示文案
|
||||||
|
const alertMessage = computed(() => {
|
||||||
|
if (form.deliveryStatus === 20) {
|
||||||
|
return '该订单已发货,确认收到线下付款(微信转账/银行汇款等)后,订单将自动完成。';
|
||||||
|
}
|
||||||
|
return '确认已收到该订单的线下付款(微信转账/银行汇款等),确认后订单将进入待发货状态。';
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref();
|
||||||
|
const { resetFields, validate } = useForm(form, rules);
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const loading = ref(false);
|
||||||
|
const fileList = ref<UploadFile[]>([]);
|
||||||
|
const previewVisible = ref(false);
|
||||||
|
const previewImage = ref('');
|
||||||
|
|
||||||
|
// 上传前校验
|
||||||
|
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
|
||||||
|
const isImage = ['image/jpeg', 'image/png', 'image/jpg'].includes(
|
||||||
|
file.type
|
||||||
|
);
|
||||||
|
if (!isImage) {
|
||||||
|
message.error('仅支持 jpg/png 格式图片');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isLt10M = file.size / 1024 / 1024 < 10;
|
||||||
|
if (!isLt10M) {
|
||||||
|
message.error('图片大小不能超过 10MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义上传
|
||||||
|
const handleUpload: UploadProps['customRequest'] = (options) => {
|
||||||
|
const { file, onSuccess, onError } = options;
|
||||||
|
uploadOss(file as File)
|
||||||
|
.then((data) => {
|
||||||
|
// 存储返回的 path(与项目 UploadCert 组件约定一致)
|
||||||
|
form.paymentVoucher = data.path || '';
|
||||||
|
fileList.value = [
|
||||||
|
{
|
||||||
|
uid: String(data.id || Date.now()),
|
||||||
|
name: data.name || '支付凭证',
|
||||||
|
status: 'done',
|
||||||
|
url: data.url || getUrl(data.path || '')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
onSuccess?.(data, file as any);
|
||||||
|
// 触发表单校验清除错误
|
||||||
|
formRef.value?.validateFields(['paymentVoucher']);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
message.error(e.message || '上传失败');
|
||||||
|
onError?.(e as any);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除凭证
|
||||||
|
const handleRemove = () => {
|
||||||
|
form.paymentVoucher = '';
|
||||||
|
fileList.value = [];
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预览凭证
|
||||||
|
const handlePreview = (file: UploadFile) => {
|
||||||
|
previewImage.value = file.url || '';
|
||||||
|
previewVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新弹窗显示状态
|
||||||
|
const updateVisible = (visible: boolean) => {
|
||||||
|
emit('update:visible', visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消
|
||||||
|
const handleCancel = () => {
|
||||||
|
updateVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交确认收款
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
await validate();
|
||||||
|
if (!form.orderId) {
|
||||||
|
message.error('订单信息缺失');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
await confirmOfflinePayment(
|
||||||
|
form.orderId,
|
||||||
|
form.remarks || undefined,
|
||||||
|
form.paymentVoucher || undefined
|
||||||
|
);
|
||||||
|
message.success(form.deliveryStatus === 20 ? '确认收款成功,订单已自动完成' : '确认收款成功,订单已进入待发货状态');
|
||||||
|
emit('done');
|
||||||
|
updateVisible(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.errorFields) return; // 表单校验失败,不弹错误
|
||||||
|
message.error(error.message || '确认收款失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听弹窗显示状态
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible) {
|
||||||
|
// 初始化表单
|
||||||
|
form.orderId = props.data?.orderId;
|
||||||
|
form.orderNo = props.data?.orderNo || '';
|
||||||
|
form.paymentVoucher = '';
|
||||||
|
form.remarks = '';
|
||||||
|
form.deliveryStatus = props.data?.deliveryStatus;
|
||||||
|
fileList.value = [];
|
||||||
|
} else {
|
||||||
|
resetFields();
|
||||||
|
fileList.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.voucher-tip {
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-upload-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<a-radio :value="0">
|
<a-radio :value="0">
|
||||||
<span style="color: #1890ff">快递配送</span>
|
<span style="color: #1890ff">快递配送</span>
|
||||||
</a-radio>
|
</a-radio>
|
||||||
<a-radio :value="1">无需发货</a-radio>
|
|
||||||
<a-radio :value="2">商家送货</a-radio>
|
<a-radio :value="2">商家送货</a-radio>
|
||||||
|
<a-radio :value="1">无需发货</a-radio>
|
||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
@@ -87,17 +87,79 @@
|
|||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<template v-if="form.deliveryType !== 1">
|
<template v-if="form.deliveryType !== 1">
|
||||||
<a-form-item label="发货人" name="sendName">
|
<!-- 发货门店:快递配送=发货仓库,商家送货=配送门店 -->
|
||||||
|
<a-form-item
|
||||||
|
:label="form.deliveryType === 0 ? '发货门店' : '配送门店'"
|
||||||
|
name="sendStoreId"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="form.sendStoreId"
|
||||||
|
:placeholder="form.deliveryType === 0 ? '请选择发货门店' : '请选择配送门店'"
|
||||||
|
@change="onStoreChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="store in storeList"
|
||||||
|
:key="store.id"
|
||||||
|
:value="store.id"
|
||||||
|
>
|
||||||
|
{{ store.name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<!-- 商家送货:配送店员,依赖门店二级联动 -->
|
||||||
|
<a-form-item
|
||||||
|
label="配送店员"
|
||||||
|
name="deliveryUserId"
|
||||||
|
v-if="form.deliveryType === 2"
|
||||||
|
>
|
||||||
|
<a-select
|
||||||
|
v-model:value="form.deliveryUserId"
|
||||||
|
placeholder="请先选择门店"
|
||||||
|
:disabled="!form.sendStoreId"
|
||||||
|
@change="onUserChange"
|
||||||
|
>
|
||||||
|
<a-select-option
|
||||||
|
v-for="user in userList"
|
||||||
|
:key="user.id"
|
||||||
|
:value="user.id"
|
||||||
|
>
|
||||||
|
{{ user.name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<div style="color: #999; font-size: 12px; margin: 4px 0 12px">
|
||||||
|
{{
|
||||||
|
form.deliveryType === 0
|
||||||
|
? '选择发货门店后,将自动回填寄件人信息(快递配送时,寄件人即为发货门店)'
|
||||||
|
: '选择配送门店后,将自动回填配送店员信息'
|
||||||
|
}},可手动修改
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-form-item
|
||||||
|
:label="form.deliveryType === 0 ? '寄件人' : '发货人'"
|
||||||
|
name="sendName"
|
||||||
|
>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.sendName"
|
v-model:value="form.sendName"
|
||||||
placeholder="请输入发货人"
|
:placeholder="form.deliveryType === 0 ? '请输入寄件人' : '请输入发货人'"
|
||||||
:maxlength="50"
|
:maxlength="50"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-if="form.deliveryType === 0"
|
||||||
|
style="color: #999; font-size: 12px; margin-top: 4px"
|
||||||
|
>
|
||||||
|
快递配送场景下,寄件人信息将同步至快递面单
|
||||||
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="发货人联系方式" name="sendPhone">
|
<a-form-item
|
||||||
|
:label="form.deliveryType === 0 ? '寄件人联系方式' : '发货人联系方式'"
|
||||||
|
name="sendPhone"
|
||||||
|
>
|
||||||
<a-input
|
<a-input
|
||||||
v-model:value="form.sendPhone"
|
v-model:value="form.sendPhone"
|
||||||
placeholder="请输入发货人联系方式"
|
:placeholder="form.deliveryType === 0 ? '请输入寄件人联系方式' : '请输入发货人联系方式'"
|
||||||
:maxlength="50"
|
:maxlength="50"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -156,6 +218,10 @@
|
|||||||
import { updateShopOrder } from '@/api/shop/shopOrder';
|
import { updateShopOrder } from '@/api/shop/shopOrder';
|
||||||
import { listShopExpress } from '@/api/shop/shopExpress';
|
import { listShopExpress } from '@/api/shop/shopExpress';
|
||||||
import { ShopExpress } from '@/api/shop/shopExpress/model';
|
import { ShopExpress } from '@/api/shop/shopExpress/model';
|
||||||
|
import { listShopStore } from '@/api/shop/shopStore';
|
||||||
|
import { ShopStore } from '@/api/shop/shopStore/model';
|
||||||
|
import { listShopStoreUser } from '@/api/shop/shopStoreUser';
|
||||||
|
import { ShopStoreUser } from '@/api/shop/shopStoreUser/model';
|
||||||
import { toDateString } from 'ele-admin-pro';
|
import { toDateString } from 'ele-admin-pro';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
import ExpressSettingModal from './expressSettingModal.vue';
|
import ExpressSettingModal from './expressSettingModal.vue';
|
||||||
@@ -184,6 +250,8 @@
|
|||||||
sendName: '',
|
sendName: '',
|
||||||
sendPhone: '',
|
sendPhone: '',
|
||||||
sendAddress: '',
|
sendAddress: '',
|
||||||
|
sendStoreId: undefined as number | undefined, // 发货门店/配送门店
|
||||||
|
deliveryUserId: undefined as number | undefined, // 配送店员
|
||||||
deliveryTime: dayjs() as Dayjs
|
deliveryTime: dayjs() as Dayjs
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,7 +293,9 @@
|
|||||||
{
|
{
|
||||||
validator: (_: any, value: any) => {
|
validator: (_: any, value: any) => {
|
||||||
if (form.deliveryType !== 1 && !value) {
|
if (form.deliveryType !== 1 && !value) {
|
||||||
return Promise.reject('请输入发货人');
|
return Promise.reject(
|
||||||
|
form.deliveryType === 0 ? '请输入寄件人' : '请输入发货人'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -235,7 +305,11 @@
|
|||||||
{
|
{
|
||||||
validator: (_: any, value: any) => {
|
validator: (_: any, value: any) => {
|
||||||
if (form.deliveryType !== 1 && !value) {
|
if (form.deliveryType !== 1 && !value) {
|
||||||
return Promise.reject('请输入发货人联系方式');
|
return Promise.reject(
|
||||||
|
form.deliveryType === 0
|
||||||
|
? '请输入寄件人联系方式'
|
||||||
|
: '请输入发货人联系方式'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@@ -250,6 +324,28 @@
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
sendStoreId: [
|
||||||
|
{
|
||||||
|
validator: (_: any, value: any) => {
|
||||||
|
if (form.deliveryType !== 1 && !value) {
|
||||||
|
return Promise.reject(
|
||||||
|
form.deliveryType === 0 ? '请选择发货门店' : '请选择配送门店'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
deliveryUserId: [
|
||||||
|
{
|
||||||
|
validator: (_: any, value: any) => {
|
||||||
|
if (form.deliveryType === 2 && !value) {
|
||||||
|
return Promise.reject('请选择配送店员');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -259,6 +355,8 @@
|
|||||||
// 状态
|
// 状态
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const expressList = ref<ShopExpress[]>([]);
|
const expressList = ref<ShopExpress[]>([]);
|
||||||
|
const storeList = ref<ShopStore[]>([]);
|
||||||
|
const userList = ref<ShopStoreUser[]>([]);
|
||||||
const expressModalVisible = ref(false);
|
const expressModalVisible = ref(false);
|
||||||
|
|
||||||
// 加载快递公司列表
|
// 加载快递公司列表
|
||||||
@@ -271,6 +369,58 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载门店列表
|
||||||
|
const loadStoreList = async () => {
|
||||||
|
try {
|
||||||
|
const data = await listShopStore({});
|
||||||
|
storeList.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载门店失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载指定门店下的店员
|
||||||
|
const loadUserList = async (storeId?: number) => {
|
||||||
|
if (!storeId) {
|
||||||
|
userList.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await listShopStoreUser({ storeId });
|
||||||
|
userList.value = data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载店员失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 门店选择变化
|
||||||
|
const onStoreChange = (storeId: number) => {
|
||||||
|
const store = storeList.value.find((item) => item.id === storeId);
|
||||||
|
if (store) {
|
||||||
|
// 发货地址统一回填写门店地址
|
||||||
|
form.sendAddress = store.address || '';
|
||||||
|
if (form.deliveryType === 0) {
|
||||||
|
// 快递配送:门店作为发货方,直接回填发货人与联系方式
|
||||||
|
form.sendName = store.name || '';
|
||||||
|
form.sendPhone = store.phone || '';
|
||||||
|
}
|
||||||
|
if (form.deliveryType === 2) {
|
||||||
|
// 商家送货:切换门店后清空已选店员并重新加载
|
||||||
|
form.deliveryUserId = undefined;
|
||||||
|
loadUserList(storeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 店员选择变化
|
||||||
|
const onUserChange = (userId: number) => {
|
||||||
|
const user = userList.value.find((item) => item.id === userId);
|
||||||
|
if (user) {
|
||||||
|
form.sendName = user.name || '';
|
||||||
|
form.sendPhone = user.phone || '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 快递公司筛选
|
// 快递公司筛选
|
||||||
const filterExpressOption = (input: string, option: any) => {
|
const filterExpressOption = (input: string, option: any) => {
|
||||||
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||||
@@ -303,6 +453,9 @@
|
|||||||
form.sendName = '';
|
form.sendName = '';
|
||||||
form.sendPhone = '';
|
form.sendPhone = '';
|
||||||
form.sendAddress = '';
|
form.sendAddress = '';
|
||||||
|
form.sendStoreId = undefined;
|
||||||
|
form.deliveryUserId = undefined;
|
||||||
|
userList.value = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -345,6 +498,8 @@
|
|||||||
updateData.sendName = form.sendName;
|
updateData.sendName = form.sendName;
|
||||||
updateData.sendPhone = form.sendPhone;
|
updateData.sendPhone = form.sendPhone;
|
||||||
updateData.sendAddress = form.sendAddress;
|
updateData.sendAddress = form.sendAddress;
|
||||||
|
updateData.sendStoreId = form.sendStoreId;
|
||||||
|
updateData.deliveryUserId = undefined;
|
||||||
updateData.expressName = form.expressName || express?.expressName;
|
updateData.expressName = form.expressName || express?.expressName;
|
||||||
updateData.expressNo = form.expressNo;
|
updateData.expressNo = form.expressNo;
|
||||||
} else if (form.deliveryType === 2) {
|
} else if (form.deliveryType === 2) {
|
||||||
@@ -352,6 +507,8 @@
|
|||||||
updateData.sendName = form.sendName;
|
updateData.sendName = form.sendName;
|
||||||
updateData.sendPhone = form.sendPhone;
|
updateData.sendPhone = form.sendPhone;
|
||||||
updateData.sendAddress = form.sendAddress;
|
updateData.sendAddress = form.sendAddress;
|
||||||
|
updateData.sendStoreId = form.sendStoreId;
|
||||||
|
updateData.deliveryUserId = form.deliveryUserId;
|
||||||
updateData.expressId = undefined;
|
updateData.expressId = undefined;
|
||||||
updateData.expressName = undefined;
|
updateData.expressName = undefined;
|
||||||
updateData.expressNo = undefined;
|
updateData.expressNo = undefined;
|
||||||
@@ -399,10 +556,14 @@
|
|||||||
form.sendName = '';
|
form.sendName = '';
|
||||||
form.sendPhone = '';
|
form.sendPhone = '';
|
||||||
form.sendAddress = '';
|
form.sendAddress = '';
|
||||||
|
form.sendStoreId = undefined;
|
||||||
|
form.deliveryUserId = undefined;
|
||||||
|
userList.value = [];
|
||||||
form.deliveryTime = dayjs();
|
form.deliveryTime = dayjs();
|
||||||
|
|
||||||
// 加载快递公司列表
|
// 加载快递公司列表与门店列表
|
||||||
loadExpressList();
|
loadExpressList();
|
||||||
|
loadStoreList();
|
||||||
} else {
|
} else {
|
||||||
resetFields();
|
resetFields();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,9 +71,12 @@
|
|||||||
label="支付状态"
|
label="支付状态"
|
||||||
:labelStyle="{ width: '90px', color: '#808080' }"
|
:labelStyle="{ width: '90px', color: '#808080' }"
|
||||||
>
|
>
|
||||||
<a-tag v-if="form.payStatus == 1 && form.payType !== 8" color="green">已付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType !== 8 && form.payType !== 9" color="green">已付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 1 && form.payType === 8" color="blue">待收货付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType === 8" color="blue">待收货付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 0">未付款</a-tag>
|
<a-tag v-if="form.payStatus == 1 && form.payType === 9" color="green">已确认收款</a-tag>
|
||||||
|
<a-tag v-if="form.payStatus == 0 && form.payType === 9 && form.deliveryStatus === 20" color="blue">已发货待收款</a-tag>
|
||||||
|
<a-tag v-if="form.payStatus == 0 && form.payType === 9 && form.deliveryStatus !== 20" color="orange">待确认收款</a-tag>
|
||||||
|
<a-tag v-if="form.payStatus == 0 && form.payType !== 9">未付款</a-tag>
|
||||||
<a-tag v-if="form.payStatus == 3">未付款,占场中</a-tag>
|
<a-tag v-if="form.payStatus == 3">未付款,占场中</a-tag>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<!-- 第四排-->
|
<!-- 第四排-->
|
||||||
@@ -147,7 +150,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="form.payType == 9">
|
<a-tag v-if="form.payType == 9">
|
||||||
<IdcardOutlined class="tag-icon" />
|
<IdcardOutlined class="tag-icon" />
|
||||||
IC月卡
|
线下付款
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<a-tag v-if="form.payType == 10">
|
<a-tag v-if="form.payType == 10">
|
||||||
<IdcardOutlined class="tag-icon" />
|
<IdcardOutlined class="tag-icon" />
|
||||||
@@ -187,7 +190,16 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<span class="text-gray-400">未支付</span>
|
<!-- 线下付款未确认时也显示支付方式 -->
|
||||||
|
<a-tag v-if="form.payType == 9">
|
||||||
|
<IdcardOutlined class="tag-icon" />
|
||||||
|
线下付款
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-if="form.payType == 8">
|
||||||
|
<IdcardOutlined class="tag-icon" />
|
||||||
|
货到付款
|
||||||
|
</a-tag>
|
||||||
|
<span v-if="form.payType !== 9 && form.payType !== 8" class="text-gray-400">未支付</span>
|
||||||
</template>
|
</template>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
@@ -206,6 +218,15 @@
|
|||||||
>
|
>
|
||||||
{{ form.buyerRemarks }}
|
{{ form.buyerRemarks }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
|
||||||
|
<a-descriptions-item v-if="form.paymentVoucher" label="支付凭证" :labelStyle="{ width: '90px', color: '#808080' }">
|
||||||
|
<a-image
|
||||||
|
:width="70"
|
||||||
|
:src="getCompressedImageUrl(form.paymentVoucher, { width: 750 })"
|
||||||
|
:preview-src-list="[ensureFullUrl(form.paymentVoucher)]"
|
||||||
|
style="cursor: pointer"
|
||||||
|
/>
|
||||||
|
</a-descriptions-item>
|
||||||
<!-- <a-descriptions-item-->
|
<!-- <a-descriptions-item-->
|
||||||
<!-- label="结算状态"-->
|
<!-- label="结算状态"-->
|
||||||
<!-- :labelStyle="{ width: '90px', color: '#808080' }"-->
|
<!-- :labelStyle="{ width: '90px', color: '#808080' }"-->
|
||||||
@@ -235,7 +256,7 @@
|
|||||||
<template v-if="column.key === 'goodsName'">
|
<template v-if="column.key === 'goodsName'">
|
||||||
<div style="display: flex; align-items: center; gap: 12px">
|
<div style="display: flex; align-items: center; gap: 12px">
|
||||||
<a-avatar
|
<a-avatar
|
||||||
:src="record.image || record.goodsImage"
|
:src="getCompressedImageUrl(record.image, { width: 100 })"
|
||||||
shape="square"
|
shape="square"
|
||||||
:size="50"
|
:size="50"
|
||||||
style="flex-shrink: 0"
|
style="flex-shrink: 0"
|
||||||
@@ -342,6 +363,24 @@
|
|||||||
>
|
>
|
||||||
<a-spin :spinning="loading">
|
<a-spin :spinning="loading">
|
||||||
<a-descriptions :column="2">
|
<a-descriptions :column="2">
|
||||||
|
<a-descriptions-item
|
||||||
|
label="发货门店"
|
||||||
|
v-if="form.deliveryType === 0 || form.deliveryType === 2"
|
||||||
|
:labelStyle="{ width: '90px', color: '#808080' }"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
form.sendStoreName ||
|
||||||
|
form.expressMerchantName ||
|
||||||
|
'未填写'
|
||||||
|
}}
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label="配送店员"
|
||||||
|
v-if="form.deliveryType === 2"
|
||||||
|
:labelStyle="{ width: '90px', color: '#808080' }"
|
||||||
|
>
|
||||||
|
{{ form.deliveryUserName || '未填写' }}
|
||||||
|
</a-descriptions-item>
|
||||||
<a-descriptions-item
|
<a-descriptions-item
|
||||||
label="发货人"
|
label="发货人"
|
||||||
:labelStyle="{ width: '90px', color: '#808080' }"
|
:labelStyle="{ width: '90px', color: '#808080' }"
|
||||||
@@ -414,6 +453,7 @@
|
|||||||
import { updateShopOrder, removeShopOrder, refundShopOrder } from '@/api/shop/shopOrder';
|
import { updateShopOrder, removeShopOrder, refundShopOrder } from '@/api/shop/shopOrder';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { message, Modal } from 'ant-design-vue';
|
||||||
import DeliveryModal from './deliveryModal.vue';
|
import DeliveryModal from './deliveryModal.vue';
|
||||||
|
import {getCompressedImageUrl, ensureFullUrl} from "@/utils/image";
|
||||||
|
|
||||||
const useForm = Form.useForm;
|
const useForm = Form.useForm;
|
||||||
|
|
||||||
@@ -581,6 +621,8 @@
|
|||||||
invoiceNo: undefined,
|
invoiceNo: undefined,
|
||||||
// 支付时间
|
// 支付时间
|
||||||
payTime: undefined,
|
payTime: undefined,
|
||||||
|
// 线下收款支付凭证(图片地址,确认线下收款时上传)
|
||||||
|
paymentVoucher: undefined,
|
||||||
// 退款时间
|
// 退款时间
|
||||||
refundTime: undefined,
|
refundTime: undefined,
|
||||||
// 申请退款时间
|
// 申请退款时间
|
||||||
@@ -713,7 +755,21 @@
|
|||||||
];
|
];
|
||||||
} else if (order.payStatus === 0) {
|
} else if (order.payStatus === 0) {
|
||||||
// 未付款
|
// 未付款
|
||||||
|
// 线下付款先发货后结款:已发货时步骤条显示发货状态
|
||||||
|
if (order.payType === 9 && order.deliveryStatus === 20) {
|
||||||
|
active.value = 2;
|
||||||
|
steps.value[1].description = '待收款';
|
||||||
|
steps.value[2].description = order.deliveryTime
|
||||||
|
? toDateString(order.deliveryTime, 'MM-dd HH:mm')
|
||||||
|
: '已发货';
|
||||||
|
if (order.orderStatus === 1) {
|
||||||
|
active.value = 4;
|
||||||
|
steps.value[3].description = '已收货';
|
||||||
|
steps.value[4].description = '订单完成';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
active.value = 0;
|
active.value = 0;
|
||||||
|
}
|
||||||
} else if (order.payStatus === 1) {
|
} else if (order.payStatus === 1) {
|
||||||
// 已付款
|
// 已付款
|
||||||
active.value = 1;
|
active.value = 1;
|
||||||
@@ -846,7 +902,7 @@
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
await updateShopOrder({
|
await updateShopOrder({
|
||||||
...form,
|
...form,
|
||||||
deliveryStatus: 30, // 已收货
|
deliveryStatus: 20, // 已收货
|
||||||
orderStatus: 1 // 已完成
|
orderStatus: 1 // 已完成
|
||||||
});
|
});
|
||||||
message.success('确认收货成功');
|
message.success('确认收货成功');
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
allow-clear
|
allow-clear
|
||||||
v-model:value="where.orderNo"
|
v-model:value="where.orderNo"
|
||||||
placeholder="订单编号"
|
placeholder="订单编号"
|
||||||
style="width: 260px"
|
style="width: 240px"
|
||||||
@search="reload"
|
@search="reload"
|
||||||
@pressEnter="reload"
|
@pressEnter="reload"
|
||||||
/>
|
/>
|
||||||
@@ -31,39 +31,37 @@
|
|||||||
<!-- <a-select-option :value="1">普通订单</a-select-option>-->
|
<!-- <a-select-option :value="1">普通订单</a-select-option>-->
|
||||||
<!-- <a-select-option :value="2">秒杀订单</a-select-option>-->
|
<!-- <a-select-option :value="2">秒杀订单</a-select-option>-->
|
||||||
<!-- <a-select-option :value="3">拼团订单</a-select-option>-->
|
<!-- <a-select-option :value="3">拼团订单</a-select-option>-->
|
||||||
<!-- </a-select>-->
|
|
||||||
<!-- <a-select-->
|
|
||||||
<!-- v-model:value="where.payStatus"-->
|
|
||||||
<!-- style="width: 150px"-->
|
|
||||||
<!-- placeholder="付款状态"-->
|
|
||||||
<!-- @change="search"-->
|
|
||||||
<!-- >-->
|
|
||||||
<!-- <a-select-option value="">全部</a-select-option>-->
|
|
||||||
<!-- <a-select-option :value="1">已付款</a-select-option>-->
|
|
||||||
<!-- <a-select-option :value="0">未付款</a-select-option>-->
|
|
||||||
<!-- </a-select>-->
|
<!-- </a-select>-->
|
||||||
<a-select
|
<a-select
|
||||||
v-model:value="where.orderStatus"
|
v-model:value="where.payStatus"
|
||||||
style="width: 150px"
|
style="width: 150px"
|
||||||
placeholder="订单状态"
|
placeholder="付款状态"
|
||||||
@change="search"
|
@change="search"
|
||||||
>
|
>
|
||||||
<a-select-option value="">全部</a-select-option>
|
<a-select-option value="">全部</a-select-option>
|
||||||
<a-select-option :value="1">已完成</a-select-option>
|
<a-select-option :value="1">已付款</a-select-option>
|
||||||
<!-- <a-select-option :value="0">未完成</a-select-option>-->
|
<a-select-option :value="0">未付款</a-select-option>
|
||||||
<!-- <a-select-option :value="2">未使用</a-select-option>-->
|
|
||||||
<a-select-option :value="3">已取消</a-select-option>
|
|
||||||
<a-select-option :value="4">退款中</a-select-option>
|
|
||||||
<a-select-option :value="5">退款被拒</a-select-option>
|
|
||||||
<a-select-option :value="6">退款成功</a-select-option>
|
|
||||||
</a-select>
|
</a-select>
|
||||||
<!-- <a-select-->
|
<!-- <a-select-->
|
||||||
<!-- :options="getPayType()"-->
|
<!-- v-model:value="where.orderStatus"-->
|
||||||
<!-- v-model:value="where.payType"-->
|
|
||||||
<!-- style="width: 150px"-->
|
<!-- style="width: 150px"-->
|
||||||
<!-- placeholder="付款方式"-->
|
<!-- placeholder="订单状态"-->
|
||||||
<!-- @change="search"-->
|
<!-- @change="search"-->
|
||||||
<!-- />-->
|
<!-- >-->
|
||||||
|
<!-- <a-select-option value="">全部</a-select-option>-->
|
||||||
|
<!-- <a-select-option :value="1">已完成</a-select-option>-->
|
||||||
|
<!-- <a-select-option :value="3">已取消</a-select-option>-->
|
||||||
|
<!-- <a-select-option :value="4">退款中</a-select-option>-->
|
||||||
|
<!-- <a-select-option :value="5">退款被拒</a-select-option>-->
|
||||||
|
<!-- <a-select-option :value="6">退款成功</a-select-option>-->
|
||||||
|
<!-- </a-select>-->
|
||||||
|
<a-select
|
||||||
|
:options="getPayType()"
|
||||||
|
v-model:value="where.payType"
|
||||||
|
style="width: 150px"
|
||||||
|
placeholder="付款方式"
|
||||||
|
@change="search"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-range-picker
|
<a-range-picker
|
||||||
v-model:value="dateRange"
|
v-model:value="dateRange"
|
||||||
@@ -98,12 +96,15 @@ import {message} from 'ant-design-vue';
|
|||||||
import useSearch from '@/utils/use-search';
|
import useSearch from '@/utils/use-search';
|
||||||
import {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model';
|
import {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model';
|
||||||
import {listShopOrder} from '@/api/shop/shopOrder';
|
import {listShopOrder} from '@/api/shop/shopOrder';
|
||||||
|
import {listShopOrderGoods} from '@/api/shop/shopOrderGoods';
|
||||||
import {getPayType} from '@/utils/shop';
|
import {getPayType} from '@/utils/shop';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
// 选中的订单
|
// 选中的订单
|
||||||
selection?: ShopOrder[];
|
selection?: ShopOrder[];
|
||||||
|
// 当前列表标签对应的筛选状态(如待发货=1),导出时一并带入
|
||||||
|
statusFilter?: number;
|
||||||
}>(),
|
}>(),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
@@ -188,35 +189,112 @@ const orders = ref<ShopOrder[]>([]);
|
|||||||
const xlsFileName = ref<string>();
|
const xlsFileName = ref<string>();
|
||||||
const type = ref('');
|
const type = ref('');
|
||||||
|
|
||||||
|
// 配送方式中文(0快递配送 1无需发货/自提 2商家送货)
|
||||||
|
const getDeliveryTypeLabel = (t?: number) => {
|
||||||
|
switch (t) {
|
||||||
|
case 0:
|
||||||
|
return '快递配送';
|
||||||
|
case 1:
|
||||||
|
return '无需发货(自提)';
|
||||||
|
case 2:
|
||||||
|
return '商家送货';
|
||||||
|
default:
|
||||||
|
return '未设置';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发货状态中文(10未发货 20已发货 30部分发货)
|
||||||
|
const getDeliveryStatusLabel = (s?: number) => {
|
||||||
|
switch (s) {
|
||||||
|
case 10:
|
||||||
|
return '未发货';
|
||||||
|
case 20:
|
||||||
|
return '已发货';
|
||||||
|
case 30:
|
||||||
|
return '部分发货';
|
||||||
|
default:
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 商品明细汇总(商品名(规格) x 数量)
|
||||||
|
const getGoodsSummary = (goods?: any[]) => {
|
||||||
|
if (!goods || !goods.length) return '';
|
||||||
|
return goods
|
||||||
|
.map((g) => {
|
||||||
|
const spec = g.spec ? `(${g.spec})` : '';
|
||||||
|
return `${g.goodsName || '未知商品'}${spec} x${g.totalNum || 1}`;
|
||||||
|
})
|
||||||
|
.join(';');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 商品总数量:基于实际取到的商品明细求和(订单级 totalNum 列表接口可能不返回,以明细为准)
|
||||||
|
const getGoodsTotalNum = (goods?: any[]) => {
|
||||||
|
if (!goods || !goods.length) return 0;
|
||||||
|
return goods.reduce((sum, g) => sum + (Number(g.totalNum) || 0), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自提/发货店铺名称
|
||||||
|
const getShopName = (d: ShopOrder) => {
|
||||||
|
if (d.deliveryType === 1) return d.selfTakeMerchantName || '';
|
||||||
|
return d.expressMerchantName || '';
|
||||||
|
};
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const array: (string | number)[][] = [
|
const array: (string | number)[][] = [
|
||||||
[
|
[
|
||||||
'订单编号',
|
'订单编号',
|
||||||
'订单标题',
|
'收货人',
|
||||||
'买家姓名',
|
'联系电话',
|
||||||
'手机号码',
|
'收货地址',
|
||||||
|
'配送方式',
|
||||||
|
'商品明细',
|
||||||
|
'商品总数量',
|
||||||
'实付金额(元)',
|
'实付金额(元)',
|
||||||
|
'配送时间',
|
||||||
|
'自提码',
|
||||||
|
'发货/自提店铺',
|
||||||
|
'买家备注',
|
||||||
'支付方式',
|
'支付方式',
|
||||||
'付款时间',
|
'发货状态',
|
||||||
'下单时间'
|
'下单时间'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
await listShopOrder(where)
|
await listShopOrder({...where, statusFilter: props.statusFilter})
|
||||||
.then((list) => {
|
.then(async (list) => {
|
||||||
orders.value = list;
|
orders.value = list;
|
||||||
list?.forEach((d: ShopOrder) => {
|
// 列表接口不返回 orderGoods,按订单逐个补齐商品明细(含名称/规格/数量)
|
||||||
|
const goodsList = await Promise.all(
|
||||||
|
(list ?? []).map((d) =>
|
||||||
|
d.orderId
|
||||||
|
? listShopOrderGoods({orderId: d.orderId}).catch(() => [])
|
||||||
|
: Promise.resolve([])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
list?.forEach((d: ShopOrder, idx: number) => {
|
||||||
array.push([
|
array.push([
|
||||||
`${d.orderNo}`,
|
`${d.orderNo || ''}`,
|
||||||
`${d.comments}`,
|
`${d.realName || ''}`,
|
||||||
`${d.realName}`,
|
`${d.phone || d.mobile || ''}`,
|
||||||
`${d.phone}`,
|
`${d.address || ''}`,
|
||||||
`${d.payPrice}`,
|
`${getDeliveryTypeLabel(d.deliveryType)}`,
|
||||||
|
`${getGoodsSummary(goodsList[idx])}`,
|
||||||
|
`${getGoodsTotalNum(goodsList[idx])}`,
|
||||||
|
`${d.payPrice || ''}`,
|
||||||
|
`${
|
||||||
|
d.sendStartTime && d.sendEndTime
|
||||||
|
? `${d.sendStartTime} - ${d.sendEndTime}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
`${d.selfTakeCode || ''}`,
|
||||||
|
`${getShopName(d)}`,
|
||||||
|
`${d.buyerRemarks || ''}`,
|
||||||
`${getPayType(d.payType)}`,
|
`${getPayType(d.payType)}`,
|
||||||
`${d.payTime || ''}`,
|
`${getDeliveryStatusLabel(d.deliveryStatus)}`,
|
||||||
`${d.createTime}`
|
`${d.createTime || ''}`
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
const sheetName = `订单数据`;
|
const sheetName = `订单数据`;
|
||||||
@@ -226,19 +304,23 @@ const handleExport = async () => {
|
|||||||
};
|
};
|
||||||
const sheet = utils.aoa_to_sheet(array);
|
const sheet = utils.aoa_to_sheet(array);
|
||||||
workbook.Sheets[sheetName] = sheet;
|
workbook.Sheets[sheetName] = sheet;
|
||||||
// 设置列宽
|
// 设置列宽(与上方表头一一对应)
|
||||||
sheet['!cols'] = [
|
sheet['!cols'] = [
|
||||||
{wch: 10},
|
{wch: 22}, // 订单编号
|
||||||
{wch: 40},
|
{wch: 12}, // 收货人
|
||||||
{wch: 20},
|
{wch: 16}, // 联系电话
|
||||||
{wch: 20},
|
{wch: 40}, // 收货地址
|
||||||
{wch: 60},
|
{wch: 14}, // 配送方式
|
||||||
{wch: 15},
|
{wch: 40}, // 商品明细
|
||||||
{wch: 10},
|
{wch: 12}, // 商品总数量
|
||||||
{wch: 10},
|
{wch: 12}, // 实付金额(元)
|
||||||
{wch: 20},
|
{wch: 28}, // 配送时间
|
||||||
{wch: 10},
|
{wch: 12}, // 自提码
|
||||||
{wch: 20}
|
{wch: 20}, // 发货/自提店铺
|
||||||
|
{wch: 30}, // 买家备注
|
||||||
|
{wch: 12}, // 支付方式
|
||||||
|
{wch: 10}, // 发货状态
|
||||||
|
{wch: 20} // 下单时间
|
||||||
];
|
];
|
||||||
message.loading('正在导出...');
|
message.loading('正在导出...');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,45 +1,46 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
|
||||||
<a-card style="margin-bottom: 20px">
|
<div class="p-3 mb-2 bg-white">
|
||||||
<search
|
<search
|
||||||
@search="reload"
|
@search="reload"
|
||||||
:selection="selection"
|
:selection="selection"
|
||||||
|
:status-filter="statusFilterForExport"
|
||||||
@add="openEdit"
|
@add="openEdit"
|
||||||
@remove="removeBatch"
|
@remove="removeBatch"
|
||||||
@batchMove="openMove"
|
@batchMove="openMove"
|
||||||
/>
|
/>
|
||||||
</a-card>
|
|
||||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
|
||||||
<div class="order-notify-bar">
|
|
||||||
<a-space>
|
|
||||||
<span class="order-notify-label">新订单提醒</span>
|
|
||||||
<a-switch
|
|
||||||
v-model:checked="notifyEnabled"
|
|
||||||
checked-children="开"
|
|
||||||
un-checked-children="关"
|
|
||||||
@change="onNotifyToggle"
|
|
||||||
/>
|
|
||||||
<a-tooltip
|
|
||||||
title="开启后每60秒自动检测新订单,检测到后播放叮声并语音播报"
|
|
||||||
>
|
|
||||||
<InfoCircleOutlined class="order-notify-tip" />
|
|
||||||
</a-tooltip>
|
|
||||||
<a-button
|
|
||||||
type="link"
|
|
||||||
size="small"
|
|
||||||
:disabled="!notifyEnabled"
|
|
||||||
@click="onTestNotify"
|
|
||||||
>
|
|
||||||
测试提醒
|
|
||||||
</a-button>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||||
|
<!-- <div class="order-notify-bar">-->
|
||||||
|
<!-- <a-space>-->
|
||||||
|
<!-- <span class="order-notify-label">新订单提醒</span>-->
|
||||||
|
<!-- <a-switch-->
|
||||||
|
<!-- v-model:checked="notifyEnabled"-->
|
||||||
|
<!-- checked-children="开"-->
|
||||||
|
<!-- un-checked-children="关"-->
|
||||||
|
<!-- @change="onNotifyToggle"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- <a-tooltip-->
|
||||||
|
<!-- title="开启后每60秒自动检测新订单,检测到后播放叮声并语音播报"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- <InfoCircleOutlined class="order-notify-tip" />-->
|
||||||
|
<!-- </a-tooltip>-->
|
||||||
|
<!-- <a-button-->
|
||||||
|
<!-- type="link"-->
|
||||||
|
<!-- size="small"-->
|
||||||
|
<!-- :disabled="!notifyEnabled"-->
|
||||||
|
<!-- @click="onTestNotify"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- 测试提醒-->
|
||||||
|
<!-- </a-button>-->
|
||||||
|
<!-- </a-space>-->
|
||||||
|
<!-- </div>-->
|
||||||
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
||||||
<a-tab-pane key="all" tab="全部"/>
|
<a-tab-pane key="all" tab="全部"/>
|
||||||
|
<a-tab-pane key="unpaid" tab="待付款"/>
|
||||||
<a-tab-pane key="undelivered" tab="待发货"/>
|
<a-tab-pane key="undelivered" tab="待发货"/>
|
||||||
<a-tab-pane key="unreceived" tab="待收货"/>
|
<a-tab-pane key="unreceived" tab="待收货"/>
|
||||||
<a-tab-pane key="completed" tab="已完成"/>
|
<a-tab-pane key="completed" tab="已完成"/>
|
||||||
<!-- <a-tab-pane key="unpaid" tab="待付款" />-->
|
|
||||||
<a-tab-pane key="refunded" tab="退货/售后"/>
|
<a-tab-pane key="refunded" tab="退货/售后"/>
|
||||||
<a-tab-pane key="cancelled" tab="已关闭"/>
|
<a-tab-pane key="cancelled" tab="已关闭"/>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@@ -49,6 +50,9 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:datasource="datasource"
|
:datasource="datasource"
|
||||||
:customRow="customRow"
|
:customRow="customRow"
|
||||||
|
v-model:selection="selection"
|
||||||
|
:scroll="{ x: 1600 }"
|
||||||
|
size="small"
|
||||||
cache-key="proShopOrderTable"
|
cache-key="proShopOrderTable"
|
||||||
:toolbar="false"
|
:toolbar="false"
|
||||||
tool-class="ele-toolbar-form"
|
tool-class="ele-toolbar-form"
|
||||||
@@ -67,10 +71,7 @@
|
|||||||
<div class="leading-tight">
|
<div class="leading-tight">
|
||||||
<div class="cursor-pointer" @click.stop="onSearch(record)">
|
<div class="cursor-pointer" @click.stop="onSearch(record)">
|
||||||
{{
|
{{
|
||||||
record.nickname ||
|
record.realName
|
||||||
record.realName ||
|
|
||||||
record.name ||
|
|
||||||
'匿名'
|
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -88,52 +89,70 @@
|
|||||||
<template v-if="column.key === 'statusInfo'">
|
<template v-if="column.key === 'statusInfo'">
|
||||||
<div class="py-1">
|
<div class="py-1">
|
||||||
<a-space :size="6" wrap>
|
<a-space :size="6" wrap>
|
||||||
<!-- 货到付款标识 -->
|
|
||||||
<a-tag
|
|
||||||
v-if="record.payType === 8"
|
|
||||||
color="blue"
|
|
||||||
>货到付款</a-tag>
|
|
||||||
|
|
||||||
<!-- 支付状态 -->
|
<!-- 支付状态 -->
|
||||||
<a-tag
|
<a-tag
|
||||||
v-if="record.payStatus == 1 && record.payType !== 8"
|
v-if="record.payStatus == 1 && record.payType !== 8 && record.payType !== 9"
|
||||||
color="green"
|
color="green"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>已付款</a-tag
|
>已付款
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-else-if="record.payStatus == 1 && record.payType === 8"
|
v-else-if="record.payStatus == 1 && record.payType === 8"
|
||||||
color="green"
|
color="green"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>待收货付款</a-tag
|
>待收货付款
|
||||||
|
</a-tag
|
||||||
|
>
|
||||||
|
<a-tag
|
||||||
|
v-else-if="record.payStatus == 1 && record.payType === 9"
|
||||||
|
color="green"
|
||||||
|
@click.stop="updatePayStatus(record)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>已确认收款
|
||||||
|
</a-tag
|
||||||
|
>
|
||||||
|
<a-tag
|
||||||
|
v-else-if="record.payStatus == 0 && record.payType === 9"
|
||||||
|
color="orange"
|
||||||
|
@click.stop="updatePayStatus(record)"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>待确认收款
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-else-if="record.payStatus == 0 || record.payStatus == null"
|
v-else-if="record.payStatus == 0 || record.payStatus == null"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>未付款</a-tag
|
>未付款
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag
|
<a-tag
|
||||||
v-else-if="record.payStatus == 3"
|
v-else-if="record.payStatus == 3"
|
||||||
color="orange"
|
color="orange"
|
||||||
@click.stop="updatePayStatus(record)"
|
@click.stop="updatePayStatus(record)"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>占场中</a-tag
|
>占场中
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- 发货状态 -->
|
<!-- 发货状态 -->
|
||||||
<a-tag
|
<a-tag
|
||||||
v-if="record.deliveryStatus == 10"
|
v-if="record.deliveryStatus == 10"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
>未发货</a-tag
|
>未发货
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.deliveryStatus == 20" color="green"
|
<a-tag v-if="record.deliveryStatus == 20" color="green"
|
||||||
>已发货</a-tag
|
>已发货
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.deliveryStatus == 30" color="blue"
|
<a-tag v-if="record.deliveryStatus == 30" color="blue"
|
||||||
>部分发货</a-tag
|
>部分发货
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- 开票状态 -->
|
<!-- 开票状态 -->
|
||||||
@@ -144,23 +163,29 @@
|
|||||||
<!-- 订单状态 -->
|
<!-- 订单状态 -->
|
||||||
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
|
<a-tag v-if="record.orderStatus === 0">未完成</a-tag>
|
||||||
<a-tag v-if="record.orderStatus === 1" color="green"
|
<a-tag v-if="record.orderStatus === 1" color="green"
|
||||||
>已完成</a-tag
|
>已完成
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
|
<a-tag v-if="record.orderStatus === 2">已关闭</a-tag>
|
||||||
<a-tag v-if="record.orderStatus === 3" color="red"
|
<a-tag v-if="record.orderStatus === 3" color="red"
|
||||||
>关闭中</a-tag
|
>关闭中
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.orderStatus === 4" color="red"
|
<a-tag v-if="record.orderStatus === 4" color="red"
|
||||||
>退款申请中</a-tag
|
>退款申请中
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.orderStatus === 5" color="red"
|
<a-tag v-if="record.orderStatus === 5" color="red"
|
||||||
>退款被拒绝</a-tag
|
>退款被拒绝
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.orderStatus === 6" color="orange"
|
<a-tag v-if="record.orderStatus === 6" color="orange"
|
||||||
>已退款</a-tag
|
>已退款
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<a-tag v-if="record.orderStatus === 7" color="pink"
|
<a-tag v-if="record.orderStatus === 7" color="pink"
|
||||||
>客户端申请退款</a-tag
|
>客户端申请退款
|
||||||
|
</a-tag
|
||||||
>
|
>
|
||||||
<!-- 结算状态 -->
|
<!-- 结算状态 -->
|
||||||
<!-- <a-tag v-if="record.isSettled === 0">未结算</a-tag>-->
|
<!-- <a-tag v-if="record.isSettled === 0">未结算</a-tag>-->
|
||||||
@@ -171,19 +196,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'orderGoods'">
|
<template v-if="column.key === 'orderGoods'">
|
||||||
<template v-for="(item, index) in record.orderGoods" :key="index">
|
<a-space
|
||||||
<div class="item py-1">
|
v-for="(item, index) in isOrderGoodsExpanded(record)
|
||||||
<a-space :id="`g-${index}`">
|
? record.orderGoods
|
||||||
<a-avatar :src="getCompressedImageUrl(item.image)" shape="square" :size="80" />
|
: (record.orderGoods || []).slice(0, collapsedGoodsCount)"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a-avatar :src="getCompressedImageUrl(item.image,{ width: 100 })" shape="square" :size="50"/>
|
||||||
|
<div class="flex flex-col">
|
||||||
<span>{{ item.goodsName }}</span>
|
<span>{{ item.goodsName }}</span>
|
||||||
</a-space>
|
<span class="text-gray-400 text-xs"> x {{ item.totalNum }} </span>
|
||||||
|
</div>
|
||||||
|
</a-space>
|
||||||
|
<div
|
||||||
|
v-if="record.orderGoods && record.orderGoods.length > collapsedGoodsCount"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
|
<a @click.stop="toggleOrderGoodsExpanded(record)" class="text-xs">
|
||||||
|
<template v-if="isOrderGoodsExpanded(record)">收起</template>
|
||||||
|
<template v-else>展开全部 {{ record.orderGoods.length }} 件</template>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'payType'">
|
<template v-if="column.key === 'payType'">
|
||||||
<!-- 货到付款特殊标识 -->
|
<!-- 货到付款特殊标识 -->
|
||||||
<template v-if="record.payType === 8">
|
<template v-if="record.payType === 8">
|
||||||
<a-tag color="blue">货到付款</a-tag>
|
<span>货到付款</span>
|
||||||
|
</template>
|
||||||
|
<!-- 线下付款特殊标识 -->
|
||||||
|
<template v-else-if="record.payType === 9">
|
||||||
|
<span class="text-sm">线下付款</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-for="item in getPayType()">
|
<template v-for="item in getPayType()">
|
||||||
@@ -228,15 +270,43 @@
|
|||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<a-space>
|
<a-space>
|
||||||
<!-- 查看详情 - 所有状态都可以查看 -->
|
<!-- 查看详情 - 所有状态都可以查看 -->
|
||||||
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
|
<a @click.stop="openEdit(record)"> 详情 </a>
|
||||||
|
|
||||||
<!-- 未付款状态的操作 -->
|
<!-- 未付款状态的操作(排除线下付款) -->
|
||||||
<template v-if="!record.payStatus && record.orderStatus === 0">
|
<template v-if="!record.payStatus && record.orderStatus === 0 && record.payType !== 9">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a @click.stop="handleEditOrder(record)">
|
<a @click.stop="handleEditOrder(record)">
|
||||||
<EditOutlined /> 修改
|
修改
|
||||||
</a>
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a @click.stop="handleCancelOrder(record)">
|
<a @click.stop="handleCancelOrder(record)">
|
||||||
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
|
<span class="ele-text-warning"> 关闭 </span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 线下付款·待确认收款状态的操作(支持先发货后结款) -->
|
||||||
|
<template
|
||||||
|
v-if="!record.payStatus && record.orderStatus === 0 && record.payType === 9 && record.deliveryStatus === 10">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
<a @click.stop="handleConfirmOfflinePayment(record)" class="ele-text-success">
|
||||||
|
确认收款
|
||||||
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
<a @click.stop="handleDelivery(record)" class="ele-text-primary">
|
||||||
|
发货
|
||||||
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
<a @click.stop="handleCancelOrder(record)">
|
||||||
|
<span class="ele-text-warning"> 关闭 </span>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 线下付款·已发货未收款状态的操作(先发货后结款) -->
|
||||||
|
<template
|
||||||
|
v-if="!record.payStatus && record.orderStatus === 0 && record.payType === 9 && record.deliveryStatus === 20">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
|
<a @click.stop="handleConfirmOfflinePayment(record)" class="ele-text-success">
|
||||||
|
确认收款
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -250,11 +320,13 @@
|
|||||||
!isRefundStatus(record.orderStatus)
|
!isRefundStatus(record.orderStatus)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a @click.stop="handleDelivery(record)" class="ele-text-primary">
|
<a @click.stop="handleDelivery(record)" class="ele-text-primary">
|
||||||
<SendOutlined /> 发货
|
发货
|
||||||
</a>
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
||||||
<UndoOutlined /> 退款
|
退款
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -266,14 +338,16 @@
|
|||||||
record.orderStatus === 0
|
record.orderStatus === 0
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a
|
<a
|
||||||
@click.stop="handleConfirmReceive(record)"
|
@click.stop="handleConfirmReceive(record)"
|
||||||
class="ele-text-primary"
|
class="ele-text-primary"
|
||||||
>
|
>
|
||||||
<CheckOutlined /> 确认收货
|
确认收货
|
||||||
</a>
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
||||||
<UndoOutlined /> 退款
|
退款
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -282,42 +356,47 @@
|
|||||||
<template
|
<template
|
||||||
v-if="record.orderStatus === 4 || record.orderStatus === 7"
|
v-if="record.orderStatus === 4 || record.orderStatus === 7"
|
||||||
>
|
>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a
|
<a
|
||||||
@click.stop="handleApproveRefund(record)"
|
@click.stop="handleApproveRefund(record)"
|
||||||
class="ele-text-success"
|
class="ele-text-success"
|
||||||
>
|
>
|
||||||
<CheckCircleOutlined /> 同意退款
|
同意退款
|
||||||
</a>
|
</a>
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a
|
<a
|
||||||
@click.stop="handleRejectRefund(record)"
|
@click.stop="handleRejectRefund(record)"
|
||||||
class="ele-text-danger"
|
class="ele-text-danger"
|
||||||
>
|
>
|
||||||
<CloseCircleOutlined /> 拒绝退款
|
拒绝退款
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="record.orderStatus === 5">
|
<template v-if="record.orderStatus === 5">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a @click.stop="handleRetryRefund(record)">
|
<a @click.stop="handleRetryRefund(record)">
|
||||||
<RedoOutlined /> 重新处理
|
重新处理
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 已完成状态的操作 -->
|
<!-- 已完成状态的操作 -->
|
||||||
<template v-if="record.orderStatus === 1">
|
<template v-if="record.orderStatus === 1">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
<a v-permission="'shop:shopOrder:refund'" @click.stop="handleApplyRefund(record)">
|
||||||
<UndoOutlined /> 退款
|
退款
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 -->
|
<!-- 删除操作 - 已完成、已关闭、退款成功的订单可以删除 -->
|
||||||
<template v-if="canDeleteOrder(record)">
|
<template v-if="canDeleteOrder(record)">
|
||||||
|
<a-divider type="vertical"/>
|
||||||
<a-popconfirm
|
<a-popconfirm
|
||||||
title="确定要删除此订单吗?删除后无法恢复。"
|
title="确定要删除此订单吗?删除后无法恢复。"
|
||||||
@confirm="remove(record)"
|
@confirm="remove(record)"
|
||||||
>
|
>
|
||||||
<a class="ele-text-danger" @click.stop>
|
<a class="ele-text-danger" @click.stop>
|
||||||
<DeleteOutlined /> 删除
|
删除
|
||||||
</a>
|
</a>
|
||||||
</a-popconfirm>
|
</a-popconfirm>
|
||||||
</template>
|
</template>
|
||||||
@@ -336,11 +415,19 @@
|
|||||||
:data="current"
|
:data="current"
|
||||||
@done="reload"
|
@done="reload"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- 确认线下收款弹窗 -->
|
||||||
|
<OfflinePaymentModal
|
||||||
|
v-model:visible="showOfflinePayment"
|
||||||
|
:data="current"
|
||||||
|
@done="reload"
|
||||||
|
/>
|
||||||
</a-page-header>
|
</a-page-header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createVNode, ref } from 'vue';
|
import {createVNode, ref, onMounted, onActivated, computed} from 'vue';
|
||||||
|
import {useRoute} from 'vue-router';
|
||||||
import type {EleProTable} from 'ele-admin-pro';
|
import type {EleProTable} from 'ele-admin-pro';
|
||||||
import type {
|
import type {
|
||||||
DatasourceFunction,
|
DatasourceFunction,
|
||||||
@@ -348,23 +435,13 @@
|
|||||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
import {
|
import {
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
EyeOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
CloseOutlined,
|
|
||||||
SendOutlined,
|
|
||||||
UndoOutlined,
|
|
||||||
CheckOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
CloseCircleOutlined,
|
|
||||||
RedoOutlined,
|
|
||||||
DeleteOutlined,
|
|
||||||
InfoCircleOutlined
|
|
||||||
} from '@ant-design/icons-vue';
|
} from '@ant-design/icons-vue';
|
||||||
import Search from './components/search.vue';
|
import Search from './components/search.vue';
|
||||||
import {getPageTitle} from '@/utils/common';
|
import {getPageTitle} from '@/utils/common';
|
||||||
import {toDateString} from 'ele-admin-pro';
|
import {toDateString} from 'ele-admin-pro';
|
||||||
import OrderInfo from './components/orderInfo.vue';
|
import OrderInfo from './components/orderInfo.vue';
|
||||||
import DeliveryModal from './components/deliveryModal.vue';
|
import DeliveryModal from './components/deliveryModal.vue';
|
||||||
|
import OfflinePaymentModal from './components/OfflinePaymentModal.vue';
|
||||||
import {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model';
|
import {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model';
|
||||||
import {
|
import {
|
||||||
pageShopOrder,
|
pageShopOrder,
|
||||||
@@ -392,15 +469,72 @@
|
|||||||
const showMove = ref(false);
|
const showMove = ref(false);
|
||||||
// 是否显示发货弹窗
|
// 是否显示发货弹窗
|
||||||
const showDelivery = ref(false);
|
const showDelivery = ref(false);
|
||||||
|
// 是否显示确认线下收款弹窗
|
||||||
|
const showOfflinePayment = ref(false);
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
// 激活的标签
|
// 激活的标签(支持从路由参数初始化,如 /shop/shopOrder?tab=all)
|
||||||
const activeKey = ref<string>('undelivered');
|
const route = useRoute();
|
||||||
|
const validTabs = ['all', 'unpaid', 'undelivered', 'unreceived', 'completed', 'refunded', 'cancelled'];
|
||||||
|
const tabFromQuery = route.query.tab as string;
|
||||||
|
const activeKey = ref<string>(
|
||||||
|
validTabs.includes(tabFromQuery) ? tabFromQuery : 'all'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 商品明细展开状态:记录已展开商品明细的订单 orderId
|
||||||
|
const expandedOrderIds = ref<Set<string>>(new Set());
|
||||||
|
// 商品明细默认折叠时显示的条数
|
||||||
|
const collapsedGoodsCount = 2;
|
||||||
|
// 当前订单的商品明细是否已展开
|
||||||
|
const isOrderGoodsExpanded = (record: ShopOrder) =>
|
||||||
|
expandedOrderIds.value.has(String(record.orderId));
|
||||||
|
// 切换订单商品明细的展开/收起
|
||||||
|
const toggleOrderGoodsExpanded = (record: ShopOrder) => {
|
||||||
|
const next = new Set(expandedOrderIds.value);
|
||||||
|
if (next.has(String(record.orderId))) {
|
||||||
|
next.delete(String(record.orderId));
|
||||||
|
} else {
|
||||||
|
next.add(String(record.orderId));
|
||||||
|
}
|
||||||
|
expandedOrderIds.value = next;
|
||||||
|
};
|
||||||
|
|
||||||
// ============ 新订单提醒 ============
|
// ============ 新订单提醒 ============
|
||||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
const {enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify, clearBadge} = useOrderNotify({
|
||||||
onNewOrder: () => reload()
|
onNewOrder: () => reload()
|
||||||
});
|
});
|
||||||
|
// 进入订单列表页时清除菜单红点(标记已查看),并同步 lastOrderId 为当前最新订单
|
||||||
|
// 兼容 keep-alive:onMounted 处理首次进入,onActivated 处理缓存再次激活
|
||||||
|
let orderBadgeMounted = false;
|
||||||
|
onMounted(() => {
|
||||||
|
clearBadge();
|
||||||
|
orderBadgeMounted = true;
|
||||||
|
});
|
||||||
|
onActivated(() => {
|
||||||
|
if (orderBadgeMounted) clearBadge();
|
||||||
|
});
|
||||||
|
// 根据tab key获取对应的statusFilter值
|
||||||
|
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除,8已关闭
|
||||||
|
const getStatusFilterByTab = (key: string): number | undefined => {
|
||||||
|
const filterMap: Record<string, number> = {
|
||||||
|
unpaid: 0,
|
||||||
|
undelivered: 1,
|
||||||
|
unverified: 2,
|
||||||
|
unreceived: 3,
|
||||||
|
unevaluated: 4,
|
||||||
|
completed: 5,
|
||||||
|
refunded: 6,
|
||||||
|
deleted: 7,
|
||||||
|
cancelled: 8
|
||||||
|
};
|
||||||
|
return filterMap[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出时跟随当前标签的筛选状态(如「待发货」=1),避免导出混入其它状态的订单
|
||||||
|
const statusFilterForExport = computed(() =>
|
||||||
|
getStatusFilterByTab(activeKey.value)
|
||||||
|
);
|
||||||
|
|
||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({
|
const datasource: DatasourceFunction = ({
|
||||||
page,
|
page,
|
||||||
@@ -413,6 +547,10 @@
|
|||||||
where.status = filters.status;
|
where.status = filters.status;
|
||||||
}
|
}
|
||||||
where.type = 0;
|
where.type = 0;
|
||||||
|
// 确保初次加载时也按当前tab筛选(statusFilter未设置时从activeKey推导)
|
||||||
|
if (where.statusFilter === undefined) {
|
||||||
|
where.statusFilter = getStatusFilterByTab(activeKey.value);
|
||||||
|
}
|
||||||
return pageShopOrder({
|
return pageShopOrder({
|
||||||
...where,
|
...where,
|
||||||
...orders,
|
...orders,
|
||||||
@@ -427,61 +565,53 @@
|
|||||||
title: '订单编号',
|
title: '订单编号',
|
||||||
dataIndex: 'orderNo',
|
dataIndex: 'orderNo',
|
||||||
key: 'orderNo',
|
key: 'orderNo',
|
||||||
width: 200,
|
width: 150
|
||||||
align: 'center'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '用户信息',
|
title: '买家信息',
|
||||||
dataIndex: 'userId',
|
dataIndex: 'userId',
|
||||||
key: 'userInfo'
|
key: 'userInfo',
|
||||||
|
width: 150
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品信息',
|
title: '商品信息',
|
||||||
dataIndex: 'orderGoods',
|
dataIndex: 'orderGoods',
|
||||||
key: 'orderGoods'
|
key: 'orderGoods',
|
||||||
|
width: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '实付金额',
|
title: '实付金额',
|
||||||
dataIndex: 'payPrice',
|
dataIndex: 'payPrice',
|
||||||
key: 'payPrice',
|
key: 'payPrice',
|
||||||
align: 'center',
|
|
||||||
width: 120,
|
|
||||||
customRender: ({text}) => '¥' + text
|
customRender: ({text}) => '¥' + text
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: '支付方式',
|
title: '支付方式',
|
||||||
// dataIndex: 'payType',
|
dataIndex: 'payType',
|
||||||
// key: 'payType',
|
key: 'payType'
|
||||||
// align: 'center',
|
},
|
||||||
// width: 120,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'orderStatus',
|
dataIndex: 'orderStatus',
|
||||||
key: 'statusInfo',
|
key: 'statusInfo',
|
||||||
align: 'center'
|
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// title: '备注',
|
// title: '备注',
|
||||||
// dataIndex: 'comments',
|
// dataIndex: 'comments',
|
||||||
// key: 'comments',
|
// key: 'comments',
|
||||||
// align: 'center',
|
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
title: '创建时间',
|
title: '创建时间',
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
key: 'createTime',
|
key: 'createTime',
|
||||||
width: 180,
|
|
||||||
align: 'center',
|
|
||||||
sorter: true,
|
sorter: true,
|
||||||
customRender: ({text}) => toDateString(text)
|
customRender: ({text}) => toDateString(text)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 240,
|
width: 220,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
align: 'center',
|
|
||||||
hideInSetting: true
|
hideInSetting: true
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
@@ -493,54 +623,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onTabs = () => {
|
const onTabs = () => {
|
||||||
// 使用statusFilter进行筛选,这是后端专门为订单状态筛选设计的字段
|
|
||||||
const filterParams: Record<string, any> = {};
|
const filterParams: Record<string, any> = {};
|
||||||
|
const sf = getStatusFilterByTab(activeKey.value);
|
||||||
// 根据后端 statusFilter 的值对应:
|
if (sf !== undefined) {
|
||||||
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除
|
filterParams.statusFilter = sf;
|
||||||
switch (activeKey.value) {
|
|
||||||
case 'all':
|
|
||||||
// 全部订单:不传statusFilter参数
|
|
||||||
// filterParams.statusFilter = undefined; // 不设置该字段
|
|
||||||
break;
|
|
||||||
case 'unpaid':
|
|
||||||
// 待付款:pay_status = false
|
|
||||||
filterParams.statusFilter = 0;
|
|
||||||
break;
|
|
||||||
case 'undelivered':
|
|
||||||
// 待发货:pay_status = true AND delivery_status = 10
|
|
||||||
filterParams.statusFilter = 1;
|
|
||||||
break;
|
|
||||||
case 'unverified':
|
|
||||||
// 待核销:pay_status = true AND delivery_status = 10 (与待发货相同)
|
|
||||||
filterParams.statusFilter = 2;
|
|
||||||
break;
|
|
||||||
case 'unreceived':
|
|
||||||
// 待收货:pay_status = true AND delivery_status = 20
|
|
||||||
filterParams.statusFilter = 3;
|
|
||||||
break;
|
|
||||||
case 'unevaluated':
|
|
||||||
// 待评价:order_status = 1 (与已完成相同)
|
|
||||||
filterParams.statusFilter = 4;
|
|
||||||
break;
|
|
||||||
case 'completed':
|
|
||||||
// 已完成:order_status = 1
|
|
||||||
filterParams.statusFilter = 5;
|
|
||||||
break;
|
|
||||||
case 'cancelled':
|
|
||||||
// 已关闭:order_status = 2
|
|
||||||
filterParams.statusFilter = 8;
|
|
||||||
break;
|
|
||||||
case 'refunded':
|
|
||||||
// 退款/售后:order_status = 6
|
|
||||||
filterParams.statusFilter = 6;
|
|
||||||
break;
|
|
||||||
case 'deleted':
|
|
||||||
// 已删除:deleted = 1
|
|
||||||
filterParams.statusFilter = 7;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reload(filterParams);
|
reload(filterParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -572,7 +659,8 @@
|
|||||||
updateUser({
|
updateUser({
|
||||||
userId: record.userId,
|
userId: record.userId,
|
||||||
realName: record.realName
|
realName: record.realName
|
||||||
}).then(() => {});
|
}).then(() => {
|
||||||
|
});
|
||||||
}
|
}
|
||||||
reload();
|
reload();
|
||||||
});
|
});
|
||||||
@@ -627,6 +715,12 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 确认线下付款收款
|
||||||
|
const handleConfirmOfflinePayment = (record: ShopOrder) => {
|
||||||
|
current.value = record;
|
||||||
|
showOfflinePayment.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
// 发货处理
|
// 发货处理
|
||||||
const handleDelivery = (record: ShopOrder) => {
|
const handleDelivery = (record: ShopOrder) => {
|
||||||
current.value = record;
|
current.value = record;
|
||||||
@@ -663,7 +757,7 @@
|
|||||||
try {
|
try {
|
||||||
await updateShopOrder({
|
await updateShopOrder({
|
||||||
...record,
|
...record,
|
||||||
deliveryStatus: 30, // 已收货
|
deliveryStatus: 20, // 已收货
|
||||||
orderStatus: 1 // 已完成
|
orderStatus: 1 // 已完成
|
||||||
});
|
});
|
||||||
message.success('确认收货成功');
|
message.success('确认收货成功');
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
import { ref, onBeforeUnmount } from 'vue';
|
||||||
import { pageShopOrder } from '@/api/shop/shopOrder';
|
import { pageShopOrder } from '@/api/shop/shopOrder';
|
||||||
import type { ShopOrder } from '@/api/shop/shopOrder/model';
|
import type { ShopOrder } from '@/api/shop/shopOrder/model';
|
||||||
|
import { useUserStore } from '@/store/modules/user';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新订单提醒 - 轮询检测 + 声音/语音播报
|
* 新订单提醒 - 全局单例轮询 + 红点徽章 + 声音/语音播报
|
||||||
*
|
*
|
||||||
* 原理:
|
* 设计要点:
|
||||||
* 1. 定时轮询 pageShopOrder(只查最新 1 条)
|
* 1. **全局单例**:轮询状态(timer、lastOrderId、enabled、hasNewOrder)全部为模块级变量,
|
||||||
* 2. 与上次记录的 orderId 比较,不同则说明有新订单
|
* 无论多少个组件调用 useOrderNotify(),始终只有一个轮询在运行,避免重复请求。
|
||||||
* 3. 播放"叮"声(Web Audio API) + 语音播报"您有一条新的订单"(Speech Synthesis API)
|
* 2. **红点徽章**:检测到新订单时,调用 userStore.setMenuBadge 给「订单管理」菜单挂红点;
|
||||||
* 4. 同时触发 onNewOrder 回调,通知调用方刷新页面数据
|
* 进入订单列表页时调用 clearBadge() 清除,并同步 lastOrderId 为最新(标记“已查看”)。
|
||||||
|
* 3. **声音/语音**:保留原 Web Audio 叮声 + Speech Synthesis 语音播报。
|
||||||
|
* 4. **回调集合**:多组件可各自注册 onNewOrder 回调(如 dashboard 刷新统计、订单页刷新表格),
|
||||||
|
* 检测到新订单时统一遍历触发。
|
||||||
*
|
*
|
||||||
* 浏览器策略:音频播放需要用户交互后才允许。
|
* 生命周期:
|
||||||
* 默认开启轮询,音频在用户首次交互后自动解锁。
|
* - layout 组件挂载时调用 useOrderNotify() 并触发 start(),全局启动一次,常驻不停。
|
||||||
*
|
* - dashboard / 订单列表页 / cms dashboard 调用 useOrderNotify() 仅消费状态与 UI,不重复启动。
|
||||||
* **实例安全**:每次调用 useOrderNotify() 创建独立实例,
|
|
||||||
* 多个页面可同时使用而不冲突。AudioContext 在模块级共享。
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ============ 配置 ============
|
// ============ 配置 ============
|
||||||
@@ -32,6 +34,9 @@ const DING_DURATION = 0.3;
|
|||||||
/** 语音播报文案 */
|
/** 语音播报文案 */
|
||||||
const SPEECH_TEXT = '您有一条新的订单';
|
const SPEECH_TEXT = '您有一条新的订单';
|
||||||
|
|
||||||
|
/** 订单管理菜单路径(红点挂在这里) */
|
||||||
|
const ORDER_MENU_PATH = '/shop/shopOrder';
|
||||||
|
|
||||||
// ============ 模块级共享状态(音频相关) ============
|
// ============ 模块级共享状态(音频相关) ============
|
||||||
|
|
||||||
/** 音频上下文(模块级共享,延迟创建,需用户交互后) */
|
/** 音频上下文(模块级共享,延迟创建,需用户交互后) */
|
||||||
@@ -40,6 +45,32 @@ let audioCtx: AudioContext | null = null;
|
|||||||
/** 用户交互解锁音频的监听器(模块级,全局只注册一次) */
|
/** 用户交互解锁音频的监听器(模块级,全局只注册一次) */
|
||||||
let unlockHandler: (() => void) | null = null;
|
let unlockHandler: (() => void) | null = null;
|
||||||
|
|
||||||
|
// ============ 模块级单例状态(轮询 + 红点) ============
|
||||||
|
|
||||||
|
/** 是否开启提醒(所有组件共享同一开关) */
|
||||||
|
const enabled = ref(true);
|
||||||
|
|
||||||
|
/** 是否有未查看的新订单(驱动红点 / 快速入口标记) */
|
||||||
|
const hasNewOrder = ref(false);
|
||||||
|
|
||||||
|
/** 轮询是否进行中 */
|
||||||
|
let polling = false;
|
||||||
|
|
||||||
|
/** 轮询定时器 */
|
||||||
|
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
/** 上次记录的最新订单 ID(用于判断是否有新订单) */
|
||||||
|
let lastOrderId: number | undefined = undefined;
|
||||||
|
|
||||||
|
/** 是否已完成首次初始化(首次只记录基准,不触发提醒) */
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
/** 是否已全局启动(避免重复 start) */
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
/** 新订单回调集合(多组件注册,统一触发) */
|
||||||
|
const newOrderCallbacks = new Set<() => void>();
|
||||||
|
|
||||||
// ============ 声音播放 ============
|
// ============ 声音播放 ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,43 +161,52 @@ function speak(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发提醒:叮声 + 语音播报 + 通知回调
|
* 触发提醒:叮声 + 语音播报 + 通知所有已注册回调
|
||||||
*/
|
*/
|
||||||
function triggerNotify(onNewOrderCallback: (() => void) | null) {
|
function triggerNotify() {
|
||||||
initAudioContext();
|
initAudioContext();
|
||||||
playDingSound();
|
playDingSound();
|
||||||
speak(SPEECH_TEXT);
|
speak(SPEECH_TEXT);
|
||||||
if (onNewOrderCallback) {
|
// 触发所有已注册回调(刷新各页面数据)
|
||||||
onNewOrderCallback();
|
newOrderCallbacks.forEach((cb) => {
|
||||||
|
try {
|
||||||
|
cb();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[订单提醒] 回调执行失败:', e);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 对外接口 ============
|
// ============ 红点徽章控制 ============
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 新订单提醒组合式函数
|
* 设置/清除订单管理菜单的红点徽章
|
||||||
*
|
* @param on true=显示红点,false=清除红点
|
||||||
* 用法:
|
|
||||||
* ```ts
|
|
||||||
* const { enabled, toggle, testNotify } = useOrderNotify({
|
|
||||||
* onNewOrder: () => reload()
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @param options.onNewOrder 检测到新订单时的回调(通常用于刷新页面数据)
|
|
||||||
*/
|
*/
|
||||||
export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
function setOrderBadge(on: boolean) {
|
||||||
// ============ 实例级状态 ============
|
hasNewOrder.value = on;
|
||||||
const enabled = ref(true);
|
try {
|
||||||
let polling = false;
|
const userStore = useUserStore();
|
||||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
if (on) {
|
||||||
let lastOrderId: number | undefined = undefined;
|
// 'dot' 表示纯红点(不显示数字),由 menu-title.vue 渲染为 a-badge dot
|
||||||
let initialized = false;
|
userStore.setMenuBadge(ORDER_MENU_PATH, 'dot', '#ff4d4f');
|
||||||
let onNewOrderCallback: (() => void) | null = options?.onNewOrder ?? null;
|
} else {
|
||||||
|
// 传 undefined 清除徽章
|
||||||
|
userStore.setMenuBadge(ORDER_MENU_PATH, undefined);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[订单提醒] 设置菜单红点失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 轮询逻辑 ============
|
// ============ 轮询逻辑 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否有新订单
|
||||||
|
* 只查最新 1 条,与 lastOrderId 比较:不同且更大则说明有新订单
|
||||||
|
*/
|
||||||
async function checkNewOrder() {
|
async function checkNewOrder() {
|
||||||
|
if (!enabled.value) return;
|
||||||
try {
|
try {
|
||||||
const result = await pageShopOrder({
|
const result = await pageShopOrder({
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -184,6 +224,7 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
|||||||
const currentOrderId = latestOrder.orderId;
|
const currentOrderId = latestOrder.orderId;
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
|
// 首次只记录基准,不触发提醒
|
||||||
lastOrderId = currentOrderId;
|
lastOrderId = currentOrderId;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
return;
|
return;
|
||||||
@@ -193,7 +234,9 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
|||||||
currentOrderId !== lastOrderId &&
|
currentOrderId !== lastOrderId &&
|
||||||
(lastOrderId === undefined || (currentOrderId ?? 0) > lastOrderId)
|
(lastOrderId === undefined || (currentOrderId ?? 0) > lastOrderId)
|
||||||
) {
|
) {
|
||||||
triggerNotify(onNewOrderCallback);
|
// 有新订单:声音 + 语音 + 回调 + 红点
|
||||||
|
triggerNotify();
|
||||||
|
setOrderBadge(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastOrderId = currentOrderId;
|
lastOrderId = currentOrderId;
|
||||||
@@ -219,20 +262,70 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ 生命周期 ============
|
// ============ 全局启动 / 停止 ============
|
||||||
|
|
||||||
onMounted(() => {
|
/**
|
||||||
|
* 全局启动订单提醒(由 layout 组件调用一次即可)
|
||||||
|
* 内部用 started 标志保证幂等,重复调用安全
|
||||||
|
*/
|
||||||
|
function start() {
|
||||||
|
if (started) return;
|
||||||
|
started = true;
|
||||||
registerAudioUnlock();
|
registerAudioUnlock();
|
||||||
|
if (enabled.value) {
|
||||||
startPolling();
|
startPolling();
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除红点并同步 lastOrderId 为当前最新订单(标记“已查看”)
|
||||||
|
* 进入订单列表页时调用
|
||||||
|
*/
|
||||||
|
async function clearBadge() {
|
||||||
|
// 立即清除红点(同步,体验即时)
|
||||||
|
setOrderBadge(false);
|
||||||
|
// 重新查询最新订单,把 lastOrderId 同步为最新,标记“已查看到此订单”
|
||||||
|
// 这样后续只有比这个更新的订单才会再次触发红点
|
||||||
|
try {
|
||||||
|
const result = await pageShopOrder({ page: 1, limit: 1, type: 0 });
|
||||||
|
const latest: ShopOrder | undefined = result?.list?.[0];
|
||||||
|
lastOrderId = latest?.orderId;
|
||||||
|
initialized = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[订单提醒] 同步最新订单失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============ 对外接口 ============
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新订单提醒组合式函数(全局单例)
|
||||||
|
*
|
||||||
|
* 用法:
|
||||||
|
* ```ts
|
||||||
|
* // layout 全局启动(仅一次)
|
||||||
|
* const { start } = useOrderNotify();
|
||||||
|
* start();
|
||||||
|
*
|
||||||
|
* // dashboard / 订单页消费状态与 UI
|
||||||
|
* const { enabled, hasNewOrder, toggle, testNotify, clearBadge } = useOrderNotify({
|
||||||
|
* onNewOrder: () => reload()
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param options.onNewOrder 检测到新订单时的回调(组件卸载时自动移除)
|
||||||
|
*/
|
||||||
|
export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||||
|
// 注册回调(组件卸载时自动移除,避免内存泄漏)
|
||||||
|
const cb = options?.onNewOrder;
|
||||||
|
if (cb) {
|
||||||
|
newOrderCallbacks.add(cb);
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopPolling();
|
newOrderCallbacks.delete(cb);
|
||||||
onNewOrderCallback = null;
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============ 对外方法 ============
|
// 开关切换
|
||||||
|
|
||||||
const toggle = (value: boolean) => {
|
const toggle = (value: boolean) => {
|
||||||
enabled.value = value;
|
enabled.value = value;
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -240,17 +333,28 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
|||||||
startPolling();
|
startPolling();
|
||||||
} else {
|
} else {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
setOrderBadge(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 测试提醒
|
||||||
const testNotify = () => {
|
const testNotify = () => {
|
||||||
initAudioContext();
|
initAudioContext();
|
||||||
triggerNotify(onNewOrderCallback);
|
triggerNotify();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
/** 是否开启提醒(响应式,所有组件共享) */
|
||||||
enabled,
|
enabled,
|
||||||
|
/** 是否有未查看的新订单(响应式,驱动红点) */
|
||||||
|
hasNewOrder,
|
||||||
|
/** 开关切换 */
|
||||||
toggle,
|
toggle,
|
||||||
testNotify
|
/** 测试提醒(声音 + 语音) */
|
||||||
|
testNotify,
|
||||||
|
/** 清除红点并标记已查看(进入订单页时调用) */
|
||||||
|
clearBadge,
|
||||||
|
/** 全局启动(layout 调用一次) */
|
||||||
|
start
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,12 +20,12 @@
|
|||||||
<a-form-item label="未付款自动关闭" name="orderAutoCloseMins">
|
<a-form-item label="未付款自动关闭" name="orderAutoCloseMins">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
v-model:value="form.orderAutoCloseMins"
|
v-model:value="form.orderAutoCloseMins"
|
||||||
:min="1"
|
:min="0"
|
||||||
:max="10080"
|
:max="10080"
|
||||||
placeholder="请输入分钟数"
|
placeholder="0 表示不自动取消"
|
||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
<span style="margin-left: 8px; color: #888">分钟</span>
|
<span style="margin-left: 8px; color: #888">分钟(0 表示不自动取消)</span>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="自动好评天数" name="orderAutoCommentDays">
|
<a-form-item label="自动好评天数" name="orderAutoCommentDays">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
@@ -54,6 +54,15 @@
|
|||||||
un-checked-children="否"
|
un-checked-children="否"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="销量累加方式" name="salesAccumulateType">
|
||||||
|
<a-radio-group v-model:value="form.salesAccumulateType">
|
||||||
|
<a-radio :value="10">支付成功后累加(默认)</a-radio>
|
||||||
|
<a-radio :value="20">下单成功后累加</a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
<div style="color: #888; font-size: 12px; margin-top: 4px">
|
||||||
|
支付累加:用户付款后才计入销量;下单累加:生成订单即计入销量(取消订单会回滚)。
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="保存">
|
<a-form-item label="保存">
|
||||||
<a-button type="primary" :loading="loading" @click="save">保存</a-button>
|
<a-button type="primary" :loading="loading" @click="save">保存</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
@@ -84,7 +93,8 @@ const { form, assignFields } = useFormData<ShopSetting>({
|
|||||||
orderAutoCloseMins: 30,
|
orderAutoCloseMins: 30,
|
||||||
orderAutoCommentDays: 15,
|
orderAutoCommentDays: 15,
|
||||||
orderFreightFreeAmount: 0,
|
orderFreightFreeAmount: 0,
|
||||||
orderRefundEnabled: true
|
orderRefundEnabled: true,
|
||||||
|
salesAccumulateType: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
@@ -99,7 +109,7 @@ const rules = reactive({
|
|||||||
orderAutoCloseMins: [
|
orderAutoCloseMins: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
message: '请输入未付款自动关闭分钟数',
|
message: '请输入未付款自动关闭分钟数(0 表示不自动取消)',
|
||||||
type: 'number',
|
type: 'number',
|
||||||
trigger: 'blur'
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
@@ -135,6 +145,9 @@ const load = async () => {
|
|||||||
if (values.orderFreightFreeAmount !== undefined) {
|
if (values.orderFreightFreeAmount !== undefined) {
|
||||||
form.orderFreightFreeAmount = Number(values.orderFreightFreeAmount);
|
form.orderFreightFreeAmount = Number(values.orderFreightFreeAmount);
|
||||||
}
|
}
|
||||||
|
if (values.salesAccumulateType !== undefined) {
|
||||||
|
form.salesAccumulateType = Number(values.salesAccumulateType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// 未配置时忽略
|
// 未配置时忽略
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<a-tab-pane tab="分销设置" key="dealer">
|
<a-tab-pane tab="分销设置" key="dealer">
|
||||||
<Dealer />
|
<Dealer />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane tab="支付设置" key="payment">
|
<!-- <a-tab-pane tab="支付设置" key="payment">-->
|
||||||
<Payment />
|
<!-- <Payment />-->
|
||||||
</a-tab-pane>
|
<!-- </a-tab-pane>-->
|
||||||
<a-tab-pane tab="通知设置" key="notify">
|
<a-tab-pane tab="通知设置" key="notify">
|
||||||
<Notify />
|
<Notify />
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
@@ -43,7 +43,7 @@ import Basic from './components/basic.vue';
|
|||||||
import Order from './components/order.vue';
|
import Order from './components/order.vue';
|
||||||
import Points from './components/points.vue';
|
import Points from './components/points.vue';
|
||||||
import Dealer from './components/dealer.vue';
|
import Dealer from './components/dealer.vue';
|
||||||
import Payment from './components/payment.vue';
|
// import Payment from './components/payment.vue';
|
||||||
import Notify from './components/notify.vue';
|
import Notify from './components/notify.vue';
|
||||||
import Upload from './components/upload.vue';
|
import Upload from './components/upload.vue';
|
||||||
import Sms from './components/sms.vue';
|
import Sms from './components/sms.vue';
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user