Compare commits
26 Commits
货到付款版
...
1d69923e06
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -6,3 +6,27 @@
|
||||
- 在 `src/views/shop/dashboard/index.vue` 的欢迎横幅右侧添加新订单提醒开关栏(开关 + 测试按钮),检测到新订单后自动调用 loadData() 刷新 Dashboard 数据。
|
||||
- 在 `src/views/cms/dashboard/index.vue` 的概况卡片标题栏添加新订单提醒控件,同样检测到新订单后刷新数据。
|
||||
- 提取了 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` 同样有此后缀,用户未提及,暂不动。
|
||||
48
.workbuddy/memory/2026-07-16.md
Normal file
48
.workbuddy/memory/2026-07-16.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 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"`,半宽下单列更不拥挤。
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>企业商城</title>
|
||||
<title>小程序商城</title>
|
||||
<!-- TinyMCE 通过本地文件加载,避免 Rolldown 打包 UMD 模块的问题 -->
|
||||
<script src="/tinymce/tinymce.min.js"></script>
|
||||
<style>
|
||||
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -150,3 +150,26 @@ export async function refundShopOrder(data: ShopOrder) {
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ export interface ShopOrder {
|
||||
coachId?: number;
|
||||
// 支付的用户id
|
||||
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;
|
||||
// 代付支付方式, 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;
|
||||
// 0未付款,1已付款
|
||||
payStatus?: number;
|
||||
@@ -129,6 +129,8 @@ export interface ShopOrder {
|
||||
invoiceNo?: string;
|
||||
// 支付时间
|
||||
payTime?: string;
|
||||
// 线下收款支付凭证(图片地址,确认线下收款时上传)
|
||||
paymentVoucher?: string;
|
||||
// 退款时间
|
||||
refundTime?: string;
|
||||
// 申请退款时间
|
||||
@@ -142,9 +144,9 @@ export interface ShopOrder {
|
||||
// 系统版本号 0当前版本 value=其他版本
|
||||
version?: number;
|
||||
// 买家备注
|
||||
buyerRemarks: undefined;
|
||||
buyerRemarks?: string;
|
||||
// 商家备注
|
||||
merchantRemarks: undefined;
|
||||
merchantRemarks?: string;
|
||||
// 用户id
|
||||
userId?: number;
|
||||
// 备注
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
label: '货到付款',
|
||||
key: 'codPay',
|
||||
icon: 'IdcardOutlined'
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '线下付款',
|
||||
key: 'offlinePay',
|
||||
icon: 'IdcardOutlined'
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
||||
/* 主框架 */
|
||||
export default {
|
||||
system: '小程序开发',
|
||||
system: '小程序商城',
|
||||
home: '主页',
|
||||
header: {
|
||||
profile: '个人资料',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<span>{{ item.meta.title }}</span>
|
||||
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
|
||||
<!-- dot 模式:纯红点,不显示数字(用于新订单提醒等) -->
|
||||
<a-badge v-if="item.meta.badge === 'dot'" dot />
|
||||
<!-- 数字徽章模式 -->
|
||||
<a-badge
|
||||
v-else
|
||||
:count="item.meta.badge"
|
||||
:number-style="{ background: item.meta.badgeColor as string }"
|
||||
/>
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
import HeaderTools from './components/header-tools.vue';
|
||||
import PageFooter from './components/page-footer.vue';
|
||||
import MenuTitle from './components/menu-title.vue';
|
||||
import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
||||
import {
|
||||
HIDE_SIDEBARS,
|
||||
HIDE_FOOTERS,
|
||||
@@ -127,6 +128,11 @@
|
||||
const { t, locale } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 全局启动新订单提醒:任意页面都会轮询检测新订单,
|
||||
// 检测到后在「订单管理」菜单显示红点 + 叮声/语音播报
|
||||
const { start: startOrderNotify } = useOrderNotify();
|
||||
startOrderNotify();
|
||||
|
||||
// 是否刷新页面
|
||||
if (localStorage.getItem('Reload')) {
|
||||
window.location.reload();
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ export function getPayType(index?: number): any {
|
||||
{
|
||||
value: 8,
|
||||
label: '货到付款'
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '线下付款'
|
||||
}
|
||||
];
|
||||
if (index != null) {
|
||||
|
||||
@@ -333,16 +333,12 @@
|
||||
const chooseFile = (data: FileRecord) => {
|
||||
images.value.push({
|
||||
uid: data.id,
|
||||
url:
|
||||
data.downloadUrl +
|
||||
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90',
|
||||
url: data.downloadUrl,
|
||||
status: 'done',
|
||||
title: '', // 初始化标题为空
|
||||
path: '' // 初始化链接为空
|
||||
});
|
||||
form.images =
|
||||
data.downloadUrl +
|
||||
'?x-oss-process=image/resize,m_fixed,w_2000/quality,Q_90';
|
||||
form.images = data.downloadUrl;
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = ref({
|
||||
name: '小程序开发',
|
||||
name: '小程序商城',
|
||||
description:
|
||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||
version: '2.0.0',
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
<a-descriptions-item label="技术支持">
|
||||
<span
|
||||
class="cursor-pointer"
|
||||
>企业商城</span
|
||||
>企业官网</span
|
||||
>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
@@ -213,11 +213,14 @@
|
||||
import { openNew } from '@/utils/common';
|
||||
import { useSiteStore } from '@/store/modules/site';
|
||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { getTenantInfo } from '@/api/layout';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
// 使用状态管理
|
||||
const siteStore = useSiteStore();
|
||||
const statisticsStore = useStatisticsStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 从 store 中获取响应式数据
|
||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
||||
@@ -225,7 +228,7 @@
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = ref({
|
||||
name: '小程序开发',
|
||||
name: '小程序商城',
|
||||
description:
|
||||
'基于Spring、SpringBoot、SpringMVC等技术栈构建的前后端分离开发平台',
|
||||
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 orderCount = computed(() => statisticsStore.orderCount);
|
||||
const totalSales = computed(() => statisticsStore.totalSales);
|
||||
@@ -248,6 +257,15 @@
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
// 独立请求租户信息,不受其他请求失败影响
|
||||
getTenantInfo()
|
||||
.then((res) => {
|
||||
tenantCreateTime.value = res?.createTime || '';
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('获取租户信息失败:', e);
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
siteStore.fetchSiteInfo(),
|
||||
@@ -267,11 +285,14 @@
|
||||
await loadData();
|
||||
// 开始自动刷新统计数据(每5分钟)
|
||||
statisticsStore.startAutoRefresh();
|
||||
// 运行天数每小时检查一次(跨天自动更新)
|
||||
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时停止自动刷新
|
||||
statisticsStore.stopAutoRefresh();
|
||||
clearInterval(runDaysTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -313,7 +313,7 @@
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig: Config = {
|
||||
siteName: '企业商城',
|
||||
siteName: '小程序商城',
|
||||
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
||||
domain: '',
|
||||
icpNo: '',
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
<div class="shop-dashboard">
|
||||
<!-- 欢迎横幅 -->
|
||||
<div class="welcome-banner">
|
||||
<div class="welcome-left">
|
||||
<h2 class="welcome-title">🏸 {{ userStore.info?.tenantName }}</h2>
|
||||
<div class="welcome-left flex flex-row gap-2 items-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-right">
|
||||
<div class="welcome-actions">
|
||||
<div class="order-notify-bar">
|
||||
@@ -17,7 +25,6 @@
|
||||
<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">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新数据
|
||||
@@ -72,35 +79,15 @@
|
||||
</div>
|
||||
<RightOutlined class="todo-arrow" />
|
||||
</div>
|
||||
<div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">
|
||||
🎉 暂无待处理事项,一切正常!
|
||||
</div>
|
||||
<!-- <div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">-->
|
||||
<!-- 🎉 暂无待处理事项,一切正常!-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 快速入口 -->
|
||||
<!-- 今日数据概况 -->
|
||||
<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-header">
|
||||
<span class="panel-title">📊 今日数据概况</span>
|
||||
@@ -136,128 +123,538 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 系统基本信息 -->
|
||||
<a-row :gutter="[16, 16]" style="margin-top: 16px">
|
||||
<a-col :span="12">
|
||||
<!-- 今日数据概况 + 基本信息 -->
|
||||
<a-row :gutter="[16, 16]">
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">📋 基本信息</span>
|
||||
</div>
|
||||
<div style="padding: 16px 18px;">
|
||||
<div style="padding: 23px 18px;">
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="系统名称">
|
||||
{{ siteStore.appName }}
|
||||
<a-descriptions-item label="软件名称">
|
||||
<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 label="版本号">
|
||||
{{ systemInfo.version }}
|
||||
<a-descriptions-item label="当前版本">
|
||||
<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 label="运行状态">
|
||||
{{ siteStore.statusText || '正常' }}
|
||||
<a-descriptions-item label="授权编号">
|
||||
<span>{{ currentSubscription?.subscriptionNo || '-' }}</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间">
|
||||
{{ siteInfo?.createTime }}
|
||||
<a-descriptions-item label="开通时间">
|
||||
<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 label="到期时间">
|
||||
{{ siteInfo?.expirationTime }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="系统运行">
|
||||
{{ runDays }} 天
|
||||
<template v-if="subscriptionLoading">
|
||||
<a-skeleton-input :active="true" size="small" style="width: 140px" />
|
||||
</template>
|
||||
<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 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>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<a-col :span="12">
|
||||
<div class="panel" style="min-height: 353px">
|
||||
<!-- 快速入口 -->
|
||||
<a-col :xs="24" :md="12">
|
||||
<div class="panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">⚡ 快捷操作</span>
|
||||
<span class="panel-title">⚡ 快速入口</span>
|
||||
</div>
|
||||
<div style="padding: 16px 18px;">
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
@click="navigateTo('/website/field')"
|
||||
:loading="loading"
|
||||
<div class="quick-grid">
|
||||
<div
|
||||
v-for="item in quickLinks"
|
||||
:key="item.to"
|
||||
class="quick-card"
|
||||
@click="navigateTo(item.to)"
|
||||
>
|
||||
<UngroupOutlined />
|
||||
参数配置
|
||||
</a-button>
|
||||
<a-button block @click="navigateTo('/shop/shopOrder')">
|
||||
<CalendarOutlined />
|
||||
订单管理
|
||||
</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 class="quick-icon" :style="{ background: item.bg }">
|
||||
{{ item.icon }}
|
||||
<span v-if="item.badge" class="quick-icon-dot"></span>
|
||||
</div>
|
||||
<div class="quick-label">{{ item.label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
RightOutlined,
|
||||
UngroupOutlined,
|
||||
CalendarOutlined,
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
FileTextOutlined,
|
||||
ClearOutlined,
|
||||
InfoCircleOutlined
|
||||
InfoCircleOutlined,
|
||||
QrcodeOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { useSiteStore } from '@/store/modules/site';
|
||||
import { useStatisticsStore } from '@/store/modules/statistics';
|
||||
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 { getShopSettingCategoryValues } from '@/api/shop/shopSetting';
|
||||
import { getCompressedImageUrl } from '@/utils/image';
|
||||
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 statisticsStore = useStatisticsStore();
|
||||
const userStore = useUserStore();
|
||||
const tenantStore = useTenantStore();
|
||||
const subscriptionStore = useAppSubscriptionStore();
|
||||
const router = useRouter();
|
||||
|
||||
// 从 store 中获取响应式数据
|
||||
const { siteInfo, loading: siteLoading } = storeToRefs(siteStore);
|
||||
const { loading: siteLoading } = storeToRefs(siteStore);
|
||||
const { loading: statisticsLoading } = storeToRefs(statisticsStore);
|
||||
|
||||
// 系统信息
|
||||
const systemInfo = reactive({
|
||||
name: '小程序商城',
|
||||
version: '2.0.0',
|
||||
environment: '生产环境',
|
||||
database: 'MySQL 8.0',
|
||||
server: 'Linux CentOS 7.9',
|
||||
// ============================================================
|
||||
// 基本信息:数据来源拆分
|
||||
// - 身份信息(系统名称/版本号/运行状态)来自 app_product (productId=65)
|
||||
// - 时间线(创建时间/到期时间/系统运行天数)来自 app_subscription
|
||||
// · 创建时间 = subscription.startTime(订阅生效时间)
|
||||
// · 到期时间 = subscription.expireTime
|
||||
// · 系统运行天数 = 自 subscription.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>;
|
||||
// 系统运行天数:自订阅生效时间起累计
|
||||
// 锚定 currentSubscription.startTime(与"创建时间"行同源,语义统一)。
|
||||
// 注意:若后端续费为新增订阅记录,此处取的是 expireTime 最新的 active 订阅,
|
||||
// startTime 可能是续费生效日,runDays 会从续费日重新计数。
|
||||
// 如需"累计运行天数",应改用最早的 active 订阅 startTime 或后端额外提供字段。
|
||||
const runDays = computed(() => {
|
||||
const startTime = currentSubscription.value?.startTime;
|
||||
if (!startTime) return 0;
|
||||
return Math.floor((now.value - new Date(startTime).getTime()) / (24 * 60 * 60 * 1000));
|
||||
});
|
||||
const userCount = computed(() => statisticsStore.userCount);
|
||||
const orderCount = computed(() => statisticsStore.orderCount);
|
||||
const totalSales = computed(() => statisticsStore.totalSales);
|
||||
@@ -271,21 +668,29 @@ const loading = computed(() => siteLoading.value || statisticsLoading.value);
|
||||
// 核心统计数据(使用computed确保响应式更新)
|
||||
const coreStats = computed(() => [
|
||||
{ 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: totalSales.value || 0, desc: '元', color: 'purple', 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?tab=all' },
|
||||
{ icon: '⏱️', label: '运行天数', value: runDays.value || 0, desc: '系统运行', color: 'green', to: '' },
|
||||
]);
|
||||
|
||||
// 待处理事项数据
|
||||
const pendingPaymentCount = ref(0);
|
||||
const pendingShipmentCount = 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确保响应式更新)
|
||||
const todoItems = computed(() => [
|
||||
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
||||
{ label: '退款申请', value: pendingRefundCount.value, to: '/shop/shopOrder', tagColor: 'orange', dotColor: 'dot-orange', urgent: pendingRefundCount.value > 0 },
|
||||
// { label: '优惠券使用', value: couponUsedCount.value, to: '/market/coupon', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
|
||||
{ label: '待付款订单', value: pendingPaymentCount.value, to: '/shop/shopOrder?tab=unpaid', tagColor: 'gold', dotColor: 'dot-gold', urgent: pendingPaymentCount.value > 0 },
|
||||
{ label: '待发货订单', value: pendingShipmentCount.value, to: '/shop/shopOrder?tab=undelivered', tagColor: 'blue', dotColor: 'dot-blue', urgent: pendingShipmentCount.value > 0 },
|
||||
{ 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确保响应式更新)
|
||||
@@ -296,15 +701,15 @@ const todayStats = computed(() => ({
|
||||
couponUsed: couponUsedCount.value || 0,
|
||||
}));
|
||||
|
||||
// 快速入口
|
||||
const quickLinks = [
|
||||
{ to: '/website/field', icon: '⚙️', label: '参数配置', bg: '#eff6ff' },
|
||||
{ to: '/shop/shopOrder', icon: '📦', label: '订单管理', bg: '#f0fdf4' },
|
||||
{ to: '/system/user', icon: '👥', label: '用户管理', bg: '#fff7ed' },
|
||||
{ to: '/website/index', icon: '🏪', label: '站点管理', bg: '#faf5ff' },
|
||||
{ to: '/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
||||
{ to: '/cmsArticle', icon: '📝', label: '文章管理', bg: '#fefce8' },
|
||||
];
|
||||
// 快速入口(订单管理项随新订单状态显示红点)
|
||||
const quickLinks = computed(() => [
|
||||
{ to: '/shop/shopOrder?tab=all', icon: '📦', label: '订单管理', bg: '#f0fdf4', badge: hasNewOrder.value },
|
||||
{ to: '/shop/shopGoods', icon: '🏸', label: '商品管理', bg: '#ecfdf5' },
|
||||
{ to: '/shop/shopGoodsCategory', icon: '🗂️', label: '商品分类', bg: '#eff6ff' },
|
||||
{ to: '/market/coupon', icon: '🎁', label: '优惠券', bg: '#fff7ed' },
|
||||
{ to: '/system/user', icon: '👥', label: '会员管理', bg: '#faf5ff' },
|
||||
{ to: '/shop/shopSetting', icon: '⚙️', label: '商城设置', bg: '#fefce8' },
|
||||
]);
|
||||
|
||||
// 导航跳转
|
||||
const navigateTo = (path: string) => {
|
||||
@@ -319,19 +724,8 @@ const formatMoney = (value?: number | string) => {
|
||||
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
|
||||
};
|
||||
|
||||
// 清除缓存
|
||||
const handleClearCache = () => {
|
||||
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
|
||||
(msg) => {
|
||||
if (msg) {
|
||||
message.success(msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// 新订单声音提醒
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
// 新订单声音提醒(全局单例:轮询由 layout 启动,此处仅消费状态 + UI)
|
||||
const { enabled: notifyEnabled, hasNewOrder, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
onNewOrder: () => loadData()
|
||||
});
|
||||
|
||||
@@ -348,6 +742,25 @@ const refreshStatistics = 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 {
|
||||
await Promise.all([
|
||||
siteStore.fetchSiteInfo(),
|
||||
@@ -356,8 +769,23 @@ const loadData = async () => {
|
||||
|
||||
// 获取待处理事项数据
|
||||
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({
|
||||
type: 0,
|
||||
statusFilter: 1,
|
||||
page: 1,
|
||||
limit: 1
|
||||
@@ -369,8 +797,9 @@ const loadData = async () => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取退款/售后订单数(使用 statusFilter=6 对应退款/售后)
|
||||
// 获取退款/售后订单数(type=0商城订单, statusFilter=6退款/售后)
|
||||
const refundResult = await pageShopOrder({
|
||||
type: 0,
|
||||
statusFilter: 6,
|
||||
page: 1,
|
||||
limit: 1
|
||||
@@ -391,11 +820,17 @@ onMounted(async () => {
|
||||
|
||||
// 开始自动刷新统计数据(每5分钟)
|
||||
statisticsStore.startAutoRefresh();
|
||||
|
||||
// 运行天数每小时检查一次(跨天自动更新)
|
||||
runDaysTimer = setInterval(() => { now.value = Date.now(); }, 60 * 60 * 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// 组件卸载时停止自动刷新
|
||||
statisticsStore.stopAutoRefresh();
|
||||
clearInterval(runDaysTimer);
|
||||
// 停止支付状态轮询
|
||||
stopPolling();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -419,6 +854,11 @@ onUnmounted(() => {
|
||||
flex-wrap: wrap;
|
||||
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-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
|
||||
|
||||
@@ -475,7 +915,7 @@ onUnmounted(() => {
|
||||
.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 {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
|
||||
@@ -485,6 +925,7 @@ onUnmounted(() => {
|
||||
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
||||
.dot-orange { background: #f97316; }
|
||||
.dot-blue { background: #3b82f6; }
|
||||
.dot-gold { background: #faad14; }
|
||||
.dot-cyan { background: #06b6d4; }
|
||||
.dot-purple { background: #a855f7; }
|
||||
.todo-content { flex: 1; display: flex; align-items: center; }
|
||||
@@ -508,15 +949,33 @@ onUnmounted(() => {
|
||||
.quick-icon {
|
||||
width: 44px; height: 44px; border-radius: 10px;
|
||||
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; }
|
||||
|
||||
/* 今日数据 */
|
||||
.today-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
.today-stat {
|
||||
display: flex;
|
||||
@@ -553,4 +1012,81 @@ onUnmounted(() => {
|
||||
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>
|
||||
|
||||
@@ -588,6 +588,7 @@
|
||||
userId: undefined,
|
||||
money: undefined,
|
||||
payType: undefined,
|
||||
paymentVoucher: '',
|
||||
wechatAccount: '',
|
||||
wechatName: '',
|
||||
alipayName: '',
|
||||
|
||||
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>
|
||||
241
src/views/shop/shopOrder/components/OfflinePaymentModal.vue
Normal file
241
src/views/shop/shopOrder/components/OfflinePaymentModal.vue
Normal file
@@ -0,0 +1,241 @@
|
||||
<!-- 确认线下收款弹窗 -->
|
||||
<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="确认已收到该订单的线下付款(微信转账/银行汇款等),确认后订单将进入待发货状态。"
|
||||
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 } 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
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
paymentVoucher: [
|
||||
{ required: true, message: '请上传支付凭证' }
|
||||
]
|
||||
};
|
||||
|
||||
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('确认收款成功,订单已进入待发货状态');
|
||||
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 = '';
|
||||
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>
|
||||
@@ -71,9 +71,11 @@
|
||||
label="支付状态"
|
||||
: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 == 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" 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-descriptions-item>
|
||||
<!-- 第四排-->
|
||||
@@ -147,7 +149,7 @@
|
||||
</a-tag>
|
||||
<a-tag v-if="form.payType == 9">
|
||||
<IdcardOutlined class="tag-icon" />
|
||||
IC月卡
|
||||
线下付款
|
||||
</a-tag>
|
||||
<a-tag v-if="form.payType == 10">
|
||||
<IdcardOutlined class="tag-icon" />
|
||||
@@ -187,7 +189,16 @@
|
||||
</a-tag>
|
||||
</template>
|
||||
<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>
|
||||
</a-tooltip>
|
||||
</a-descriptions-item>
|
||||
@@ -206,6 +217,15 @@
|
||||
>
|
||||
{{ form.buyerRemarks }}
|
||||
</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-->
|
||||
<!-- label="结算状态"-->
|
||||
<!-- :labelStyle="{ width: '90px', color: '#808080' }"-->
|
||||
@@ -235,7 +255,7 @@
|
||||
<template v-if="column.key === 'goodsName'">
|
||||
<div style="display: flex; align-items: center; gap: 12px">
|
||||
<a-avatar
|
||||
:src="record.image || record.goodsImage"
|
||||
:src="getCompressedImageUrl(record.image, { width: 100 })"
|
||||
shape="square"
|
||||
:size="50"
|
||||
style="flex-shrink: 0"
|
||||
@@ -414,6 +434,7 @@
|
||||
import { updateShopOrder, removeShopOrder, refundShopOrder } from '@/api/shop/shopOrder';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import DeliveryModal from './deliveryModal.vue';
|
||||
import {getCompressedImageUrl, ensureFullUrl} from "@/utils/image";
|
||||
|
||||
const useForm = Form.useForm;
|
||||
|
||||
@@ -581,6 +602,8 @@
|
||||
invoiceNo: undefined,
|
||||
// 支付时间
|
||||
payTime: undefined,
|
||||
// 线下收款支付凭证(图片地址,确认线下收款时上传)
|
||||
paymentVoucher: undefined,
|
||||
// 退款时间
|
||||
refundTime: undefined,
|
||||
// 申请退款时间
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
</div>
|
||||
<a-tabs type="card" v-model:activeKey="activeKey" @change="onTabs">
|
||||
<a-tab-pane key="all" tab="全部" />
|
||||
<a-tab-pane key="unpaid" tab="待付款" />
|
||||
<a-tab-pane key="undelivered" tab="待发货" />
|
||||
<a-tab-pane key="unreceived" tab="待收货" />
|
||||
<a-tab-pane key="completed" tab="已完成" />
|
||||
<!-- <a-tab-pane key="unpaid" tab="待付款" />-->
|
||||
<a-tab-pane key="refunded" tab="退货/售后" />
|
||||
<a-tab-pane key="cancelled" tab="已关闭" />
|
||||
</a-tabs>
|
||||
@@ -93,10 +93,15 @@
|
||||
v-if="record.payType === 8"
|
||||
color="blue"
|
||||
>货到付款</a-tag>
|
||||
<!-- 线下付款标识 -->
|
||||
<a-tag
|
||||
v-if="record.payType === 9"
|
||||
color="orange"
|
||||
>线下付款</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"
|
||||
@click.stop="updatePayStatus(record)"
|
||||
class="cursor-pointer"
|
||||
@@ -109,6 +114,20 @@
|
||||
class="cursor-pointer"
|
||||
>待收货付款</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
|
||||
v-else-if="record.payStatus == 0 || record.payStatus == null"
|
||||
@click.stop="updatePayStatus(record)"
|
||||
@@ -174,7 +193,7 @@
|
||||
<template v-for="(item, index) in record.orderGoods" :key="index">
|
||||
<div class="item py-1">
|
||||
<a-space :id="`g-${index}`">
|
||||
<a-avatar :src="getCompressedImageUrl(item.image)" shape="square" :size="80" />
|
||||
<a-avatar :src="getCompressedImageUrl(item.image,{ width: 100 })" shape="square" :size="50" />
|
||||
<span>{{ item.goodsName }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
@@ -185,6 +204,10 @@
|
||||
<template v-if="record.payType === 8">
|
||||
<a-tag color="blue">货到付款</a-tag>
|
||||
</template>
|
||||
<!-- 线下付款特殊标识 -->
|
||||
<template v-else-if="record.payType === 9">
|
||||
<a-tag color="orange">线下付款</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-for="item in getPayType()">
|
||||
<template v-if="record.payStatus == 1">
|
||||
@@ -230,8 +253,8 @@
|
||||
<!-- 查看详情 - 所有状态都可以查看 -->
|
||||
<a @click.stop="openEdit(record)"> <EyeOutlined /> 详情 </a>
|
||||
|
||||
<!-- 未付款状态的操作 -->
|
||||
<template v-if="!record.payStatus && record.orderStatus === 0">
|
||||
<!-- 未付款状态的操作(排除线下付款) -->
|
||||
<template v-if="!record.payStatus && record.orderStatus === 0 && record.payType !== 9">
|
||||
<a @click.stop="handleEditOrder(record)">
|
||||
<EditOutlined /> 修改
|
||||
</a>
|
||||
@@ -240,6 +263,16 @@
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 线下付款·待确认收款状态的操作 -->
|
||||
<template v-if="!record.payStatus && record.orderStatus === 0 && record.payType === 9">
|
||||
<a @click.stop="handleConfirmOfflinePayment(record)" class="ele-text-success">
|
||||
<CheckCircleOutlined /> 确认收款
|
||||
</a>
|
||||
<a @click.stop="handleCancelOrder(record)">
|
||||
<span class="ele-text-warning"> <CloseOutlined /> 关闭 </span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<!-- 已付款未发货状态的操作 -->
|
||||
<template
|
||||
v-if="
|
||||
@@ -336,11 +369,19 @@
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
|
||||
<!-- 确认线下收款弹窗 -->
|
||||
<OfflinePaymentModal
|
||||
v-model:visible="showOfflinePayment"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
/>
|
||||
</a-page-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { createVNode, ref, onMounted, onActivated } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
@@ -365,6 +406,7 @@
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import OrderInfo from './components/orderInfo.vue';
|
||||
import DeliveryModal from './components/deliveryModal.vue';
|
||||
import OfflinePaymentModal from './components/OfflinePaymentModal.vue';
|
||||
import { ShopOrder, ShopOrderParam } from '@/api/shop/shopOrder/model';
|
||||
import {
|
||||
pageShopOrder,
|
||||
@@ -392,15 +434,49 @@
|
||||
const showMove = ref(false);
|
||||
// 是否显示发货弹窗
|
||||
const showDelivery = ref(false);
|
||||
// 是否显示确认线下收款弹窗
|
||||
const showOfflinePayment = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
// 激活的标签
|
||||
const activeKey = ref<string>('undelivered');
|
||||
// 激活的标签(支持从路由参数初始化,如 /shop/shopOrder?tab=all)
|
||||
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'
|
||||
);
|
||||
|
||||
// ============ 新订单提醒 ============
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
|
||||
const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify, clearBadge } = useOrderNotify({
|
||||
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];
|
||||
};
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
@@ -413,6 +489,10 @@
|
||||
where.status = filters.status;
|
||||
}
|
||||
where.type = 0;
|
||||
// 确保初次加载时也按当前tab筛选(statusFilter未设置时从activeKey推导)
|
||||
if (where.statusFilter === undefined) {
|
||||
where.statusFilter = getStatusFilterByTab(activeKey.value);
|
||||
}
|
||||
return pageShopOrder({
|
||||
...where,
|
||||
...orders,
|
||||
@@ -493,54 +573,11 @@
|
||||
};
|
||||
|
||||
const onTabs = () => {
|
||||
// 使用statusFilter进行筛选,这是后端专门为订单状态筛选设计的字段
|
||||
const filterParams: Record<string, any> = {};
|
||||
|
||||
// 根据后端 statusFilter 的值对应:
|
||||
// undefined全部,0待付款,1待发货,2待核销,3待收货,4待评价,5已完成,6已退款,7已删除
|
||||
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;
|
||||
const sf = getStatusFilterByTab(activeKey.value);
|
||||
if (sf !== undefined) {
|
||||
filterParams.statusFilter = sf;
|
||||
}
|
||||
|
||||
reload(filterParams);
|
||||
};
|
||||
|
||||
@@ -627,6 +664,12 @@
|
||||
});
|
||||
};
|
||||
|
||||
// 确认线下付款收款
|
||||
const handleConfirmOfflinePayment = (record: ShopOrder) => {
|
||||
current.value = record;
|
||||
showOfflinePayment.value = true;
|
||||
};
|
||||
|
||||
// 发货处理
|
||||
const handleDelivery = (record: ShopOrder) => {
|
||||
current.value = record;
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { ref, onBeforeUnmount } from 'vue';
|
||||
import { pageShopOrder } from '@/api/shop/shopOrder';
|
||||
import type { ShopOrder } from '@/api/shop/shopOrder/model';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
/**
|
||||
* 新订单提醒 - 轮询检测 + 声音/语音播报
|
||||
* 新订单提醒 - 全局单例轮询 + 红点徽章 + 声音/语音播报
|
||||
*
|
||||
* 原理:
|
||||
* 1. 定时轮询 pageShopOrder(只查最新 1 条)
|
||||
* 2. 与上次记录的 orderId 比较,不同则说明有新订单
|
||||
* 3. 播放"叮"声(Web Audio API) + 语音播报"您有一条新的订单"(Speech Synthesis API)
|
||||
* 4. 同时触发 onNewOrder 回调,通知调用方刷新页面数据
|
||||
* 设计要点:
|
||||
* 1. **全局单例**:轮询状态(timer、lastOrderId、enabled、hasNewOrder)全部为模块级变量,
|
||||
* 无论多少个组件调用 useOrderNotify(),始终只有一个轮询在运行,避免重复请求。
|
||||
* 2. **红点徽章**:检测到新订单时,调用 userStore.setMenuBadge 给「订单管理」菜单挂红点;
|
||||
* 进入订单列表页时调用 clearBadge() 清除,并同步 lastOrderId 为最新(标记“已查看”)。
|
||||
* 3. **声音/语音**:保留原 Web Audio 叮声 + Speech Synthesis 语音播报。
|
||||
* 4. **回调集合**:多组件可各自注册 onNewOrder 回调(如 dashboard 刷新统计、订单页刷新表格),
|
||||
* 检测到新订单时统一遍历触发。
|
||||
*
|
||||
* 浏览器策略:音频播放需要用户交互后才允许。
|
||||
* 默认开启轮询,音频在用户首次交互后自动解锁。
|
||||
*
|
||||
* **实例安全**:每次调用 useOrderNotify() 创建独立实例,
|
||||
* 多个页面可同时使用而不冲突。AudioContext 在模块级共享。
|
||||
* 生命周期:
|
||||
* - layout 组件挂载时调用 useOrderNotify() 并触发 start(),全局启动一次,常驻不停。
|
||||
* - dashboard / 订单列表页 / cms dashboard 调用 useOrderNotify() 仅消费状态与 UI,不重复启动。
|
||||
*/
|
||||
|
||||
// ============ 配置 ============
|
||||
@@ -32,6 +34,9 @@ const DING_DURATION = 0.3;
|
||||
/** 语音播报文案 */
|
||||
const SPEECH_TEXT = '您有一条新的订单';
|
||||
|
||||
/** 订单管理菜单路径(红点挂在这里) */
|
||||
const ORDER_MENU_PATH = '/shop/shopOrder';
|
||||
|
||||
// ============ 模块级共享状态(音频相关) ============
|
||||
|
||||
/** 音频上下文(模块级共享,延迟创建,需用户交互后) */
|
||||
@@ -40,6 +45,32 @@ let audioCtx: AudioContext | 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();
|
||||
playDingSound();
|
||||
speak(SPEECH_TEXT);
|
||||
if (onNewOrderCallback) {
|
||||
onNewOrderCallback();
|
||||
// 触发所有已注册回调(刷新各页面数据)
|
||||
newOrderCallbacks.forEach((cb) => {
|
||||
try {
|
||||
cb();
|
||||
} catch (e) {
|
||||
console.warn('[订单提醒] 回调执行失败:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 红点徽章控制 ============
|
||||
|
||||
/**
|
||||
* 设置/清除订单管理菜单的红点徽章
|
||||
* @param on true=显示红点,false=清除红点
|
||||
*/
|
||||
function setOrderBadge(on: boolean) {
|
||||
hasNewOrder.value = on;
|
||||
try {
|
||||
const userStore = useUserStore();
|
||||
if (on) {
|
||||
// 'dot' 表示纯红点(不显示数字),由 menu-title.vue 渲染为 a-badge dot
|
||||
userStore.setMenuBadge(ORDER_MENU_PATH, 'dot', '#ff4d4f');
|
||||
} else {
|
||||
// 传 undefined 清除徽章
|
||||
userStore.setMenuBadge(ORDER_MENU_PATH, undefined);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[订单提醒] 设置菜单红点失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 对外接口 ============
|
||||
// ============ 轮询逻辑 ============
|
||||
|
||||
/**
|
||||
* 新订单提醒组合式函数
|
||||
*
|
||||
* 用法:
|
||||
* ```ts
|
||||
* const { enabled, toggle, testNotify } = useOrderNotify({
|
||||
* onNewOrder: () => reload()
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param options.onNewOrder 检测到新订单时的回调(通常用于刷新页面数据)
|
||||
* 检测是否有新订单
|
||||
* 只查最新 1 条,与 lastOrderId 比较:不同且更大则说明有新订单
|
||||
*/
|
||||
export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
// ============ 实例级状态 ============
|
||||
const enabled = ref(true);
|
||||
let polling = false;
|
||||
let timerId: ReturnType<typeof setInterval> | null = null;
|
||||
let lastOrderId: number | undefined = undefined;
|
||||
let initialized = false;
|
||||
let onNewOrderCallback: (() => void) | null = options?.onNewOrder ?? null;
|
||||
|
||||
// ============ 轮询逻辑 ============
|
||||
|
||||
async function checkNewOrder() {
|
||||
async function checkNewOrder() {
|
||||
if (!enabled.value) return;
|
||||
try {
|
||||
const result = await pageShopOrder({
|
||||
page: 1,
|
||||
@@ -184,6 +224,7 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
const currentOrderId = latestOrder.orderId;
|
||||
|
||||
if (!initialized) {
|
||||
// 首次只记录基准,不触发提醒
|
||||
lastOrderId = currentOrderId;
|
||||
initialized = true;
|
||||
return;
|
||||
@@ -193,46 +234,98 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
currentOrderId !== lastOrderId &&
|
||||
(lastOrderId === undefined || (currentOrderId ?? 0) > lastOrderId)
|
||||
) {
|
||||
triggerNotify(onNewOrderCallback);
|
||||
// 有新订单:声音 + 语音 + 回调 + 红点
|
||||
triggerNotify();
|
||||
setOrderBadge(true);
|
||||
}
|
||||
|
||||
lastOrderId = currentOrderId;
|
||||
} catch (error) {
|
||||
console.warn('[订单提醒] 轮询查询失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
function startPolling() {
|
||||
if (polling) return;
|
||||
polling = true;
|
||||
initialized = false;
|
||||
|
||||
checkNewOrder();
|
||||
timerId = setInterval(checkNewOrder, POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
function stopPolling() {
|
||||
polling = false;
|
||||
if (timerId) {
|
||||
clearInterval(timerId);
|
||||
timerId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 全局启动 / 停止 ============
|
||||
|
||||
/**
|
||||
* 全局启动订单提醒(由 layout 组件调用一次即可)
|
||||
* 内部用 started 标志保证幂等,重复调用安全
|
||||
*/
|
||||
function start() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
registerAudioUnlock();
|
||||
if (enabled.value) {
|
||||
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(() => {
|
||||
newOrderCallbacks.delete(cb);
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 生命周期 ============
|
||||
|
||||
onMounted(() => {
|
||||
registerAudioUnlock();
|
||||
startPolling();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopPolling();
|
||||
onNewOrderCallback = null;
|
||||
});
|
||||
|
||||
// ============ 对外方法 ============
|
||||
|
||||
// 开关切换
|
||||
const toggle = (value: boolean) => {
|
||||
enabled.value = value;
|
||||
if (value) {
|
||||
@@ -240,17 +333,28 @@ export function useOrderNotify(options?: { onNewOrder?: () => void }) {
|
||||
startPolling();
|
||||
} else {
|
||||
stopPolling();
|
||||
setOrderBadge(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 测试提醒
|
||||
const testNotify = () => {
|
||||
initAudioContext();
|
||||
triggerNotify(onNewOrderCallback);
|
||||
triggerNotify();
|
||||
};
|
||||
|
||||
return {
|
||||
/** 是否开启提醒(响应式,所有组件共享) */
|
||||
enabled,
|
||||
/** 是否有未查看的新订单(响应式,驱动红点) */
|
||||
hasNewOrder,
|
||||
/** 开关切换 */
|
||||
toggle,
|
||||
testNotify
|
||||
/** 测试提醒(声音 + 语音) */
|
||||
testNotify,
|
||||
/** 清除红点并标记已查看(进入订单页时调用) */
|
||||
clearBadge,
|
||||
/** 全局启动(layout 调用一次) */
|
||||
start
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
<a-tab-pane tab="分销设置" key="dealer">
|
||||
<Dealer />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="支付设置" key="payment">
|
||||
<Payment />
|
||||
</a-tab-pane>
|
||||
<!-- <a-tab-pane tab="支付设置" key="payment">-->
|
||||
<!-- <Payment />-->
|
||||
<!-- </a-tab-pane>-->
|
||||
<a-tab-pane tab="通知设置" key="notify">
|
||||
<Notify />
|
||||
</a-tab-pane>
|
||||
@@ -43,7 +43,7 @@ import Basic from './components/basic.vue';
|
||||
import Order from './components/order.vue';
|
||||
import Points from './components/points.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 Upload from './components/upload.vue';
|
||||
import Sms from './components/sms.vue';
|
||||
|
||||
Reference in New Issue
Block a user