Compare commits
79 Commits
v1.0
..
4a7b217c2b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a7b217c2b | |||
| b5c67988a6 | |||
| 053b776d9a | |||
| 780606f05a | |||
| a04a8baec7 | |||
| 6a5b7ff711 | |||
| 9b5875e6fb | |||
| 07add26fe6 | |||
| cf7d065379 | |||
| b6ae3d71ef | |||
| 8ab3326f26 | |||
| c85b1351e3 | |||
| 9de1df84ca | |||
| 706db16337 | |||
| 40bcc77c50 | |||
| a228dcb96b | |||
| ae2ea4d841 | |||
| aae0c03dd8 | |||
| 3bc856fc95 | |||
| c553a71ea2 | |||
| a284a83b86 | |||
| bf60ba1d04 | |||
| eb26594f41 | |||
| e87e60fd6a | |||
| 4ab90c9d89 | |||
| a92248bff7 | |||
| dd094f8769 | |||
| 4a8b0a957c | |||
| 45fc45680f | |||
| 55e90c2434 | |||
| f9bf3c6df3 | |||
| d6b56d99ce | |||
| 0bfd5e667b | |||
| a831dce61a | |||
| e2f6dce7a1 | |||
| 44e416525d | |||
| 708bc7fc80 | |||
| a4107e212a | |||
| c9a4e4c229 | |||
| f65b8a945d | |||
| 9395e63f20 | |||
| 2037b0699a | |||
| bef50c2f26 | |||
| 57be388a65 | |||
| bd814d00b0 | |||
| 3edbb13a8d | |||
| b406973039 | |||
| 2745f4b0db | |||
| e9249c910f | |||
| 6b57dd2fed | |||
| c9e074366f | |||
| b07ceaa8f1 | |||
| 1662daf436 | |||
| 1d69923e06 | |||
| 0b6563aad9 | |||
| 6c50ffa58b | |||
| 6fcc120e91 | |||
| 015cd50c08 | |||
| 5b5bb3b0e5 | |||
| ace46ffea6 | |||
| a2c4f49ee6 | |||
| 00346d96d8 | |||
| 071a3bf110 | |||
| d5e040b1e6 | |||
| 3a951c70f5 | |||
| 2abd7e3372 | |||
| 7c73f9f821 | |||
| 09af76e4ea | |||
| 29a18ffbc6 | |||
| e0d27f67d5 | |||
| a59d428e78 | |||
| 9e65007e65 | |||
| e2dd8dbc18 | |||
| 08b697556b | |||
| b0ff82599a | |||
| 57b584df1b | |||
| 6350a9b5a9 | |||
| 614e843673 | |||
| 894592c290 |
@@ -1,2 +1,3 @@
|
||||
VITE_APP_NAME=小程序商城
|
||||
VITE_API_URL=/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
VITE_APP_NAME=后台管理系统
|
||||
#VITE_API_URL=http://127.0.0.1:9200/api
|
||||
VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_API_URL=http://127.0.0.1:9200/api
|
||||
#VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
VITE_APP_NAME=后台管理系统
|
||||
VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
dist.zip
|
||||
.verify-build
|
||||
.DS_Store
|
||||
*.local
|
||||
@@ -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` 中同步更新引用(该引用当前被注释,但保持一致性)。
|
||||
|
||||
|
||||
@@ -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` 同样有此后缀,用户未提及,暂不动。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 2026-07-16
|
||||
|
||||
## 续费扫码支付链路修复(前后端联动)
|
||||
|
||||
### 问题1:小程序 pay 页扫码后误显"支付成功"
|
||||
|
||||
- 文件:`websopy-taro/src/passport/pay/index.tsx`
|
||||
- 根因:`fetchDetail` 用 `subscription.status === 'active'` 判定已支付。但后端 `renewPay` 为了避免放弃支付导致服务中断,**故意保留** `status='active'`、仅设 `payStatus=0`。
|
||||
- 修复:判断改为 `payStatus === 1`;同时处理 `cancelled`/`expired` 状态。详见 `websopy-taro/.workbuddy/memory/2026-07-16.md`。
|
||||
|
||||
### 问题2:用户点"立即支付"后端返回"该订阅已激活"
|
||||
|
||||
- 文件:`/Users/gxwebsoft/JAVA/websopy-java/src/main/java/com/gxwebsoft/app/controller/AppSubscriptionController.java`
|
||||
- 根因:`pay/{id}`(line 339-341)和 `mp-prepay`(line 539-541)两处拦截只看 `status='active'`,与上次 `mp-confirm` 同样的坑——续费的 `active+payStatus=0` 被误拦。
|
||||
- 修复:两处拦截条件对齐 `mp-confirm`,改为 `status='active' && payStatus==1` 才拦截,放行续费的 `active+payStatus=0`。
|
||||
- commit:`7c95f8e` (本地 main,待 push + 服务器部署)。
|
||||
|
||||
### 关键规则(防回归)
|
||||
|
||||
判断"是否已支付"必须用 `payStatus`(0=未支付 / 1=已支付),**不能**用 `status`(订阅生命周期 active/pending/expired/cancelled)。续费场景下这两个字段完全解耦:后端 `renewPay` 保留 `status='active'` + 设 `payStatus=0`。
|
||||
|
||||
后端三处拦截条件现在全部对齐:
|
||||
- `pay/{id}` line 339-343
|
||||
- `mp-prepay` line 541-545
|
||||
- `mp-confirm` line 573(之前已修)
|
||||
|
||||
### 部署待办(用户操作)
|
||||
|
||||
1. 后端:`cd /Users/gxwebsoft/JAVA/websopy-java && git push`,然后远程服务器 `git pull && mvn clean package -DskipTests && docker-compose up -d --build cms-api`(或对应重启方式)。
|
||||
2. 小程序端:`websopy-taro` 重新编译上传体验版/正式版。
|
||||
3. 联调验证:admin 点"立即续费" → 弹码 → 微信扫码 → 进小程序应显示"确认支付"页(不再误显成功)→ 点"立即支付" → 微信支付 → 显示"支付成功" → 跳转 `/user/apps/index`。
|
||||
|
||||
## 商城后台 Dashboard 布局调整
|
||||
|
||||
- 文件:`src/views/shop/dashboard/index.vue`
|
||||
- 需求:今日数据概况页,「基本信息」改为左右两列并排;删除「快捷操作」面板。
|
||||
- 改动:
|
||||
- `a-descriptions` 由 `:column="1"` 改为 `:column="{ xs: 1, sm: 2 }"`(响应式两列并排)。
|
||||
- 基本信息 `a-col` 由 `:span="12"` 改为 `:span="24"`(占满整行)。
|
||||
- 删除「快捷操作」`a-col` 整块,并清理其专属的图标导入(ShoppingCartOutlined/ShopOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined/ClearOutlined)、`removeSiteInfoCache` 导入及 `handleClearCache` 函数。
|
||||
|
||||
### 今日数据概况 + 基本信息改为左右两栏布局
|
||||
|
||||
- 需求:参考「待处理事项 + 快速入口」的左右结构,将「今日数据概况」和「基本信息」也并排为左右各半。
|
||||
- 改动:
|
||||
- 原来各自独占整行的两个 panel,合并到一个 `<a-row>` 中,各占 `:md="12"`。
|
||||
- 今日数据概况栅格从 4 列改为 2 列(`grid-template-columns: repeat(2, 1fr)`),适配半宽空间。
|
||||
- 基本信息 `a-descriptions` 列数改为 `:column="1"`,半宽下单列更不拥挤。
|
||||
- 基本信息(左)和今日数据概况(右)位置对调,基本信息在左、今日数据概况在右。
|
||||
|
||||
### 刷新统计数据修复
|
||||
|
||||
- 问题:点「刷新数据」按钮提示成功,但实际数据没更新。
|
||||
- 根因:`refreshStatistics` 调用 `statisticsStore.fetchStatistics()` 未传 `forceRefresh=true`,5分钟缓存有效期内直接返回旧数据。
|
||||
- 修复:改为 `fetchStatistics(true)` 强制跳过缓存。
|
||||
|
||||
## README.md 修改
|
||||
|
||||
- 项目名称从"WebSoftAdmin"改为"小程序商城后台管理系统"。
|
||||
- 项目域名更新为 https://shop.websoft.top。
|
||||
- 去掉 CMS 内容管理系统的介绍部分(文章管理、一键排版、媒体库等)。
|
||||
- 更新技术栈版本号对齐 package.json(AntDv 4.x、Vite 8.x、TS 5.x、Pinia 2.x)。
|
||||
- 补充门店/经销商/会员管理等商城核心功能描述,对齐实际 views/shop 目录结构。
|
||||
@@ -0,0 +1,122 @@
|
||||
# 2026-07-17 工作日志
|
||||
|
||||
## 订单发货功能改进(shopOrder / deliveryModal)
|
||||
|
||||
分析了订单发货弹窗(`src/views/shop/shopOrder/components/deliveryModal.vue`)三种配送方式,按用户确认的改进方案实施了前端改动:
|
||||
|
||||
### 改动文件
|
||||
1. `deliveryModal.vue`(核心)
|
||||
- 新增「发货门店」下拉(复用 `listShopStore`),选中后自动回填:快递配送→发货人=门店名/电话=门店电话/地址=门店地址;商家送货→地址=门店地址。
|
||||
- 商家送货(`deliveryType===2`)新增「配送店员」二级联动下拉(复用 `listShopStoreUser({storeId})`),选中店员自动回填发货人=店员名/联系方式=店员电话。
|
||||
- 校验:发货门店必填(`deliveryType!==1`);配送店员必填(仅 `deliveryType===2`)。
|
||||
- 提交时附带 `sendStoreId` / `deliveryUserId`。
|
||||
2. `api/shop/shopOrder/model/index.ts`:新增 `sendStoreId`、`deliveryUserId` 字段(为后端落库铺路)。
|
||||
3. `orderInfo.vue`:发货信息卡片增加「发货门店」「配送店员」展示(可选链,后端返回 `storeName`/`deliveryUserName` 后自动生效,否则显示「未填写」)。
|
||||
|
||||
### 关键结论 / 待后端配合
|
||||
- 门店、店员 list API 已存在,前端改动零后端依赖即可落地交互。
|
||||
- **后端缺口(需补充)**:`shop_order` 表需新增 `send_store_id`、`delivery_user_id` 字段存储门店/店员关联;否则下拉选了也存不进去。
|
||||
- 登录用户 `User` 模型无 `storeId`,无法默认锁定当前门店,门店下拉需用户手动选择。
|
||||
- 原「发货地址模板」需求因无现成后端接口,改用「门店作为发货地址模板」方案满足(复用门店 name/phone/address)。
|
||||
|
||||
## 配送方式排序调整
|
||||
|
||||
- 修改 `deliveryModal.vue` 配送方式 radio 按钮顺序:快递配送→商家送货→无需发货(原:快递配送→无需发货→商家送货),仅调换模板中 radio 排列顺序,value 值不变。
|
||||
|
||||
## 后端 Java 改动(guilixu-java,分两轮)
|
||||
|
||||
### 第一轮:shop_order 主表
|
||||
- `ShopOrder.java` 实体加持久化字段 `sendStoreId`、`deliveryUserId`(→`send_store_id`/`delivery_user_id`),及虚拟展示字段 `sendStoreName`/`deliveryUserName`(exist=false)。
|
||||
- `ShopOrderMapper.xml` 的 `selectSql` 加 `LEFT JOIN shop_store f` / `LEFT JOIN shop_store_user g` 并 SELECT `f.name as sendStoreName, g.name as deliveryUserName`,让订单详情联带回门店/店员名称。
|
||||
- 前端 `orderInfo.vue` 展示路径从 `form.shopOrderDelivery.xxx` 改为 `form.sendStoreName`/`form.deliveryUserName`(名称落在订单主对象上)。
|
||||
- SQL:`ALTER TABLE shop_order ADD send_store_id INT, delivery_user_id INT;`
|
||||
|
||||
### 第二轮:shop_order_delivery 发货单表(用户要求"也写进发货记录表")
|
||||
- `ShopOrderDelivery.java` 实体加持久化字段 `sendStoreId`、`deliveryUserId`。
|
||||
- `ShopOrderController.update()` 的发货写入分支(无需物流分支 deliveryMethod=20、实物快递分支 deliveryMethod=30、以及补录运单号分支)新建 `ShopOrderDelivery` 时都 `setSendStoreId`/`setDeliveryUserId`(值取自 `shopOrder` 对象,兜底取 `shopOrderNow`)。
|
||||
- 说明:前端发货只调 `PUT /shop/shop-order`,该接口内部 `new ShopOrderDelivery()` 并 save 落 `shop_order_delivery`;主表靠 `updateById(shopOrder)` 自动落库,发货单表需显式 set 字段。
|
||||
- SQL:`ALTER TABLE shop_order_delivery ADD send_store_id INT, delivery_user_id INT;`
|
||||
- 验证:`vue-tsc` 前端零报错;Java 用 `./mvnw -o compile` 后台验证中。
|
||||
|
||||
## Taro 商家端 storeId=0 排查(guilixu-taro,跨项目)
|
||||
- 用户反馈 orders 页请求 `/shop/shop-store-user?storeId=0`。排查结论:
|
||||
- `guilixu-taro/src/pages/store/orders/index.tsx` **未调用** `shop-store-user`;全项目 `listShopStoreUser` 仅定义、无调用方。该请求非当前源码发出,应为线上旧版 H5 构建。
|
||||
- **当前门店 storeId 的真实来源**:`getMyClerk()`(`GET /shop/shop-store-user/my`)返回的 `ShopStoreUser` 对象,其 `.storeId` 字段(shop_dealer.id)才是门店 id。注意 `user.tsx:103` 用 `storeInfo`(=getMyClerk 结果)判断"是否显示门店中心",所以那里就能拿到 `storeId = storeInfo.storeId`。
|
||||
- **易错点**:`ShopStoreUser` 有 `id`(店员自身主键)和 `storeId`(门店 id)两个字段,二者不同。误用 `storeInfo.id` 当门店 id 是 bug。`booking/index.tsx:186` 的 `storeId: storeInfo!.id || 0` 中 `storeInfo` 恰为 `ShopStore` 类型故 `.id` 碰巧正确,但 `|| 0` 兜底在未加载时仍会变 0。
|
||||
- **根因模式**:项目无全局门店/店员上下文,`storeInfo` 是各页面局部 useState(user/booking/center 各一份)。需在 storeId 就绪前发请求 + `|| 0` 兜底 → `storeId=0`。
|
||||
- **已实施全局 `useClerk`**(用户确认后开工):
|
||||
- 新增 `src/contexts/ClerkContext.tsx`:`ClerkProvider` + `useClerkContext`。登录态(`useUserContext().isLoggedIn`)变化即拉取/清空 `getMyClerk()`;对外暴露 `clerk`(ShopStoreUser|null)、`storeId`(=clerk?.storeId)、`loading`、`refreshClerk`、`clearClerk`。
|
||||
- 新增 `src/hooks/useClerk.ts`:镜像 `useUser` 的"优先 Context、未包裹则降级本地"范式,文件头注释写明用法与 `storeId>0` 守卫约定。
|
||||
- `src/app.tsx`:在 `UserProvider` 内挂载 `<ClerkProvider>`(使其能响应登录/登出)。
|
||||
- 重构 `src/pages/user/user.tsx` 与 `src/pages/store/center/index.tsx`:移除各自的局部 `storeInfo`/`getMyClerk` 调用,改为 `const { clerk: storeInfo, refreshClerk } = useClerk()`。
|
||||
- 补全 `src/api/shop/shopStoreUser/model/index.ts` 的 `ShopStoreUser` 模型:`getMyClerk`(/my) 联表返回的 `name`/`phone`/`avatar`/`storeName` 补入(原模型缺失,旧代码靠 `useState<any>` 掩盖)。
|
||||
- **验证**:裸 `tsc --noEmit --types webpack-env` 全量检查——新建/改动的 `ClerkContext.tsx`/`useClerk.ts`/`app.tsx`/`user.tsx`/`center/index.tsx` 及模型 **零新增报错**;`center` 页仅剩 4 个**既有**报错(75 的 `finally`、82 的 `applyStatus`、89 的 `total`、106 的 `Option`,均在未改动的 `loadBadgeCounts`/`subscribe` 原函数里,属项目原有类型问题 / 裸 tsc 的 lib 配置 artifact)。注意裸 `tsc` 默认会因 tsconfig `types:["@tarojs/taro"]`+`typeRoots` 报 `TS2688`,需用 `--types webpack-env` 覆盖才能跑真实业务检查。
|
||||
- **未动**:`booking/index.tsx` 的 `storeInfo` 是用户所选门店(ShopStore,来自 `getShopStore(id)`/选择),与店员所属门店是两回事,不在本次范围(其 `storeId: storeInfo!.id || 0` 属不同接口的兜底问题)。orders 页现可随时从 `useClerk().storeId` 取当前门店 id。
|
||||
|
||||
## Taro 商家端 orders 页「选择发货人员」发货功能(guilixu-taro)
|
||||
- 用户截图显示线上旧版 orders 页有「选择发货人员」弹窗且请求 `?storeId=0` 显示"暂无可选择的店员"。当前源码 `orders/index.tsx` 原本**没有**该功能(只有确认收款/确认完成),确认是旧构建。
|
||||
- 在 `src/pages/store/orders/index.tsx` 新增「确认发货」功能:
|
||||
- 引入 `useClerk()` 取 `storeId`(杜绝 storeId=0)、`listShopStoreUser`、`ShopStoreUser` 类型。
|
||||
- `OpType` 增加 `'deliver'`;`getOrderActions` 中「待发货」订单(`payStatus && deliveryStatus===10`)显示"确认发货"按钮。
|
||||
- `openModal('deliver')` 时调 `loadClerkList()`:`storeId>0` 才 `listShopStoreUser({storeId})`,否则 toast 提示门店未就绪、不发请求。
|
||||
- 弹窗内新增店员单选列表(selectedClerk),deliver 模式隐藏凭证上传区。
|
||||
- 提交发货:`deliveryType=2`(商家送货)、`deliveryStatus=20`(待收货)、`deliveryTime`、`sendName/sendPhone`(店员)、`sendStoreId=storeId`、`deliveryUserId=店员id` → `PUT /shop/shop-order`。
|
||||
- 后端已在上一轮把 `sendStoreId`/`deliveryUserId` 落 `shop_order` + `shop_order_delivery`,此处字段对齐。
|
||||
- 验证:`tsc --noEmit --types webpack-env` 总报错仍 387(零新增);orders 页仅剩 2 个既有报错(165 `fetchLatestOrders` 返回类型、169 `count` 隐式 any,均在未改的 `useNewOrderDetector` 调用块)。
|
||||
|
||||
---
|
||||
## ⚠️ 重大纠正:正确项目是 xinlong-shop-taro,不是 guilixu-taro
|
||||
- 用户最终明确:商家端发货/门店相关功能在 **`/Users/gxwebsoft/VUE/xinlong-shop-taro`**,前面整段记的 `guilixu-taro` 改动**全改错项目**了。
|
||||
- **已在 guilixu-taro 还原**(git 仓库):`git checkout --` 还原 5 个跟踪文件(model/index.ts、app.tsx、center/index.tsx、orders/index.tsx、user.tsx);`rm` 删除 2 个新建文件(contexts/ClerkContext.tsx、hooks/useClerk.ts)。未动的 `pages/index/index.scss` 是仓库原有未提交改动,保留。
|
||||
- **xinlong-shop-taro 才是真项目,且早已内置发货功能**:`orders/index.tsx` 已有 `OpType='ship'` 的「发货」+ `saveShopOrderDelivery` + `listShopStoreUser` + 发货弹窗;`user.tsx` 已有 `getMyClerk()` 取 `storeInfo`(含 `.storeId`)用于"门店中心"判断。
|
||||
- **真正的 `storeId=0` bug 根因**:`xinlong-shop-taro/src/pages/store/orders/index.tsx:465` 的 `openShipModal` 调 `listShopStoreUser()` **没传 storeId**(默认 0),即用户最初报的 `?storeId=0`。
|
||||
- **已修(xinlong-shop-taro)**:`openShipModal` 内先 `getMyClerk()` 取 `storeId`,`listShopStoreUser({storeId})` 拉本门店店员;并默认选中 `userId === 当前登录人 userId` 的店员(`setSelectedClerkId(match.id)`)。`tsc --noEmit --types webpack-env` 该文件零新增报错(仅 162/208/212 三个项目既有错误,均在我未改动区域)。
|
||||
- **教训**:跨项目时先用 glob/find 在多个候选项目里确认"哪个项目有该功能源码",再动手;不要凭域名/习惯默认锁定一个项目。本次因 `guilixu-taro` 与 `xinlong-shop-taro` 命名相近、且前者恰有 `getMyClerk` 等同名 API 而误判。
|
||||
|
||||
## 发货弹窗文案优化(deliveryModal.vue)
|
||||
- 用户反馈"快递配送时发货人/联系人选门店是否不合理",分析结论:业务逻辑正确(发货人=寄件方=门店),问题在文案未区分"寄件方"与"承运方"。
|
||||
- 改动 `src/views/shop/shopOrder/components/deliveryModal.vue`:
|
||||
- 「发货门店」标签动态化:快递配送→「发货门店」,商家送货→「配送门店」(placeholder 同步动态)。
|
||||
- 「发货人」标签动态化:快递配送→「寄件人」,商家送货→「发货人」(联系方式标签同理)。
|
||||
- 提示文案动态化:快递配送说明"寄件人即为发货门店",商家送货说明"回填配送店员信息"。
|
||||
- 快递配送时在寄件人字段下方加小字说明"寄件人信息将同步至快递面单"。
|
||||
- 验证规则的 reject 文案同步动态化。
|
||||
- 不涉及数据结构/接口变更,纯前端文案优化。
|
||||
|
||||
## 修复小程序订单详情页配送方式判断逻辑(xinlong-shop-taro)⚠️ 注意正确项目
|
||||
|
||||
- **正确项目**:`/Users/gxwebsoft/VUE/xinlong-shop-taro`(非 guilixu-taro)
|
||||
- 文件:`src/pages/order/detail.tsx`
|
||||
- 问题:原代码 `isExpress = order.deliveryType === 0 || undefined`,无法区分 deliveryType=2(商家送货)
|
||||
- 修复:
|
||||
1. 新增 `DELIVERY_TYPE_MAP`:`0=快递配送, 1=自提, 2=商家送货`
|
||||
2. 修正 `isExpress` 判断:`deliveryType === 0 || 2 || undefined` → 快递/商家送货都显示收货人信息
|
||||
3. 收货信息卡片:商家送货显示🛵图标+配送门店名,快递配送显示📦图标
|
||||
4. 订单信息区新增「配送方式」字段展示(使用 DELIVERY_TYPE_MAP)
|
||||
- deliveryType 值范围来自管理后台 deliveryModal.vue:0=快递配送, 1=无需发货(自提), 2=商家送货
|
||||
- ⚠️ 初次改错了项目(guilixu-taro),已用 git checkout 还原,正确改动在 xinlong-shop-taro
|
||||
|
||||
## 方案A:线下付款先发货后结款(三端同步改动)
|
||||
|
||||
### 后端(guilixu-java)
|
||||
1. `ShopOrderServiceImpl.confirmOfflinePayment()`:移除 `.eq(ShopOrder::getOrderStatus, 0)` 约束,允许已发货订单确认收款。新增逻辑:若 `deliveryStatus=20`(已发货),确认收款后自动 `setOrderStatus=1` 完成订单。
|
||||
2. `ShopOrderController.confirmOfflinePayment()`:更新 `@Operation` 描述和成功消息(不再固定写"进入待发货状态")。
|
||||
3. `ShopOrderService.java`:更新 Javadoc 描述。
|
||||
4. **注意**:Controller 的 `PUT /shop/shop-order`(update)接口发货时**不校验 payStatus**,仅判断 `deliveryStatus 10→20`,所以未付款的线下订单可以直接通过 update 接口发货(前端改 deliveryStatus 即可)。
|
||||
|
||||
### 管理后台(guilixu-admin)
|
||||
1. `src/views/shop/shopOrder/index.vue`:线下付款未付款订单(payType=9, payStatus=0, orderStatus=0)分两种情况:
|
||||
- `deliveryStatus=10`:同时显示「确认收款」+「发货」+「关闭」
|
||||
- `deliveryStatus=20`:仅显示「确认收款」(先发货后结款,已发货状态)
|
||||
2. `src/views/shop/shopOrder/components/OfflinePaymentModal.vue`:
|
||||
- 新增 `deliveryStatus` 表单字段 + `alertMessage` 计算属性:已发货时提示"确认后订单将自动完成",未发货时提示"进入待发货状态"。
|
||||
- 成功消息动态化:`deliveryStatus===20 ? '已自动完成' : '已进入待发货状态'`
|
||||
3. `src/views/shop/shopOrder/components/orderInfo.vue`:
|
||||
- 支付状态标签:`payType=9 && payStatus=0 && deliveryStatus=20` → 蓝色「已发货待收款」
|
||||
- `loadSteps()` 步骤条:线下付款未付款但已发货时,步骤条显示「下单→待收款→已发货」而非停留在「下单」
|
||||
|
||||
### 小程序端(xinlong-shop-taro)
|
||||
1. `src/pages/store/orders/index.tsx`:
|
||||
- `getStatusText()`:新增「已发货待收款」状态(`payType===9 && !payStatus && deliveryStatus===20`)
|
||||
- `getOrderActions()`:发货按钮条件放宽——线下付款未付款(`payType===9`)也能显示发货按钮(支持先发货后结款)
|
||||
- `OP_DESC`:确认收款描述移除"订单将进入待发货状态"(已不是唯一路径)
|
||||
@@ -0,0 +1,49 @@
|
||||
# 2026-07-18 工作日志
|
||||
|
||||
## 手机号登录改用 loginBySuperAdminSms 接口
|
||||
|
||||
### 改动文件
|
||||
1. `src/api/passport/login/index.ts`
|
||||
- 新增 `loginBySuperAdminSms()` 方法,调用 `/loginBySuperAdminSms` 接口
|
||||
- 保留原有 `loginBySms()` 方法(其他页面如 register/dealer 仍在使用)
|
||||
- 新方法逻辑与 `loginBySms` 一致:成功后 setToken + 存 TenantId/UserId
|
||||
|
||||
2. `src/views/passport/login/index.vue`
|
||||
- import 替换:`loginBySms` → `loginBySuperAdminSms`
|
||||
- `onLoginBySms()` 函数中调用替换:`loginBySms(form)` → `loginBySuperAdminSms(form)`
|
||||
- 多租户选择逻辑(`listAdminsByPhoneAll`)保持不变
|
||||
|
||||
### 注意
|
||||
- 仅改了管理后台登录页,注册页面(register/dealer)仍用 `loginBySms`
|
||||
|
||||
## 账号登录合并邮箱/用户ID登录
|
||||
|
||||
### 需求
|
||||
账号登录输入框同时支持「邮箱」和「用户ID」:邮箱 → `https://server.websoft.top/api/loginByEmail`,纯数字 → `https://server.websoft.top/api/loginByUserId`。
|
||||
|
||||
### 改动文件
|
||||
1. `src/api/passport/login/model/index.ts`
|
||||
- `LoginParam` 新增 `email?: string` 和 `userId?: string | number`
|
||||
|
||||
2. `src/api/passport/login/index.ts`
|
||||
- 新增 `loginByEmail()` / `loginByUserId()`,逻辑同 `login()`(setToken + 存 TenantId/UserId)
|
||||
|
||||
3. `src/views/passport/login/index.vue`
|
||||
- 表单字段 `userId` → `account`(新增 `LoginForm = User & { account?: string }` 类型)
|
||||
- 输入框 placeholder 改为「邮箱 / 用户ID」,form-item name 改为 `account`
|
||||
- 校验规则 `userId` → `account`
|
||||
- `submit()` 用正则判断邮箱(`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`) / 纯数字(`/^\d+$/`),路由到对应接口;格式不符提示「请输入正确的邮箱或用户ID」
|
||||
- 移除未使用的 `login` 导入
|
||||
|
||||
### 注意
|
||||
- 外部接口用绝对 URL(`https://server.websoft.top/api/...`),未走 SERVER_API_URL 代理
|
||||
- `vue-tsc` 全量检查仍有大量历史错误(ByteMdEditor、shopActivity 等),本次改动无新增错误
|
||||
|
||||
## 登录页右上角切换图标互换
|
||||
|
||||
### 需求
|
||||
`src/views/passport/login/index.vue` 右上角扫码/手机切换按钮,图标原本与「目标模式」相反,需互换。
|
||||
|
||||
### 改动
|
||||
- 第 65-66 行:`v-if="loginType === 'scan'"` 显示 `QrcodeOutlined` → 改为 `MobileOutlined`;`v-else` 显示 `MobileOutlined` → 改为 `QrcodeOutlined`
|
||||
- 即:扫码模式下显示手机图标(切回账号/手机),账号/手机模式显示二维码图标(切到扫码)
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-07-23
|
||||
|
||||
## 订单导出字段补齐(物流部发货用)
|
||||
- 文件:`src/views/shop/shopOrder/components/search.vue` 的 `handleExport`
|
||||
- 原来只导出 8 列(订单编号/订单标题[实际是 comments]/买家姓名/手机/实付金额/支付方式/付款时间/下单时间),且缺物流发货关键字段。
|
||||
- 新增并补齐到 15 列:收货人、联系电话、收货地址、配送方式(0快递/1自提/2商家送货)、商品明细(商品名x数量)、商品总数量、实付金额、配送时间、自提码、发货/自提店铺、买家备注、支付方式、发货状态(10未发/20已发/30部分)、下单时间。
|
||||
- 新增辅助函数:`getDeliveryTypeLabel` / `getDeliveryStatusLabel` / `getGoodsSummary` / `getShopName`。
|
||||
- 注意:枚举值与 `deliveryModal.vue` 一致(deliveryType 0快递 1无需发货/自提 2商家送货)。
|
||||
- 环境提示:本机 Grep 工具因缺 libpcre2 不可用,搜代码改用 Python 脚本遍历。
|
||||
|
||||
## 导出跟随当前标签筛选(避免混入非待发货订单)
|
||||
- index.vue:新增 `statusFilterForExport` computed = `getStatusFilterByTab(activeKey)`,并通过 `:status-filter` 传给 `<search>`。
|
||||
- search.vue:给 props 加 `statusFilter?`,导出请求改为 `listShopOrder({ ...where, statusFilter: props.statusFilter })`。
|
||||
- 效果:导出所见即所得——切到「待发货」标签再导出,只导出待发货;其它标签同理。全部标签下 statusFilter=undefined 不额外过滤。
|
||||
|
||||
## 修复:导出的商品明细为空
|
||||
- 根因:`listShopOrder`(非分页列表接口)不返回 `orderGoods` 嵌套;页面表格/详情用的是 `pageShopOrder` 和单独的 `listShopOrderGoods` 取明细。
|
||||
- 修复:导出时在 `search.vue` 中用 `Promise.all` 按 `orderId` 逐个调 `listShopOrderGoods({orderId})` 补齐商品明细,再写入「商品明细」列。
|
||||
- 商品明细格式升级为 `商品名(规格) x数量`,规格取自 ShopOrderGoods.spec,便于物流发对型号。
|
||||
- 单订单请求失败时 `.catch(() => [])` 兜底,不影响其它订单。
|
||||
|
||||
## 修复:商品总数量计算不对
|
||||
- 根因:「商品总数量」列原本用订单级 `d.totalNum`,但 `listShopOrder` 列表接口未返回该字段(与 orderGoods 同理),导致算错/为空。
|
||||
- 修复:新增 `getGoodsTotalNum(goods)`,对 `listShopOrderGoods` 取到的明细 `goodsList[idx]` 逐行 `totalNum` 求和。改动后「商品总数量」与「商品明细」列同源,必然对得上。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 2026-07-24 工作日志
|
||||
|
||||
## shopGoods 商品列表排序机制分析
|
||||
- 现状:列表「排序号」列(`src/views/shop/shopGoods/index.vue` 约 273-279 行)只做展示,**未启用 `sorter`**,列表默认也不按 `sortNumber` 排序(默认排序由后端决定,前端 `orders` 仅来自「创建时间」列的排序)。
|
||||
- 编辑表单里「排序号」是 `a-input-number`(0-9999,默认 100),且在 `rules` 里是 **必填**。
|
||||
- 已存在类似的布尔开关 `recommend`(列表内联切换,index.vue 约 47-58 行),「置顶」可复用同一 UI 模式。
|
||||
- 结论倾向:纯排序号在商品多时难用,建议「置顶开关 + 少量置顶项内排序 + 其余按默认(创建时间/销量)」的混合方案,而非完全替换。
|
||||
|
||||
## 已实现:方案 B(商品置顶功能)
|
||||
- 数据模型 `src/api/shop/shopGoods/model/index.ts`:新增 `isTop?: number`(置顶 1/不置顶 0)。
|
||||
- 列表 `src/views/shop/shopGoods/index.vue`:
|
||||
- 新增「置顶」列,列表内联一键切换(复用 recommend 的 CheckOutlined/CloseOutlined 模式),新增 `onTop` 处理函数。
|
||||
- 「排序号」列开启 `sorter: true`(之前只展示、不可排序)。
|
||||
- 后端迁移脚本 `add-is-top-shop-goods.sql`:新增 `is_top` TINYINT 字段;分页默认排序需改为 `ORDER BY is_top DESC, sort_number ASC, create_time DESC`(后端不在本仓库,需手动改 page 接口 ORDER BY / 放开 orders 白名单)。
|
||||
- 注意:置顶真正"排到最前"依赖后端默认排序改动;点击列排序也需后端放开 is_top / sort_number。
|
||||
|
||||
## 后端改动(/Users/gxwebsoft/JAVA/guilixu-java,Spring Boot + MyBatis-Plus)
|
||||
- `entity/ShopGoods.java`:新增 `private Integer isTop;`(映射列 is_top,沿用 camelCase→下划线策略;mapper 用 SELECT a.*,无需改 XML)。
|
||||
- `service/impl/ShopGoodsServiceImpl.java`:
|
||||
- `pageRel` 与 `listRel` 默认排序由 `sort_number asc, create_time desc` 改为 `is_top desc, sort_number asc, create_time desc`(置顶优先,置顶内按排序号,其余按创建时间倒序)。
|
||||
- `collectFromUrl` 采集默认值补 `goods.setIsTop(0)`(与 recommend 一致)。
|
||||
- 排序字段无白名单:`PageParam` 直接按列名拼 ORDER BY,前端点「置顶」「排序号」列排序无需额外放开。
|
||||
- DB 仍需执行 `add-is-top-shop-goods.sql`(在 admin 仓库根目录):`ALTER TABLE shop_goods ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0`。
|
||||
- 副作用提醒:默认排序改为置顶优先会影响所有调用 pageRel/listRel 的端(含小程序商品列表),置顶商品会浮到最前——通常正是期望行为。
|
||||
|
||||
## shopGoods 列表:商品编号移到名称下方
|
||||
- `src/views/shop/shopGoods/index.vue`:删除独立的「编号」列(原 `code` 列),改为在「商品」列的名称下方显示「编号:{{ record.code }}」(灰色小字),节省一列横向空间。无 code 时不渲染该行。
|
||||
|
||||
## shopGoods 列表:三个价格合并为一列 + 与编辑弹窗对齐
|
||||
- 原「市场价/价格/会员价」三列(字段 salePrice/price/dealerPrice)合并为单个「价格」列(`key: 'priceGroup'`,width 150)。
|
||||
- 单元格内堆叠显示,顺序与命名对齐编辑弹窗(`shopGoodsEdit.vue` 基本信息 Tab):价格(price,加粗头条)→ 市场价(salePrice)→ 会员价(dealerPrice);空值显示 `-`。
|
||||
- 列表展示用 priceGroup 自定义 `#bodyCell` 渲染;导出 Excel(`handleExport`)仍保留三列独立价格,未改动。
|
||||
|
||||
## 商品分类(ShopGoodsCategory)多级分类调研
|
||||
- 分类管理页 `/shop/shopGoodsCategory` 本身**支持**多级:模型有 parentId/children,编辑弹窗用 a-tree-select(toTreeData) 不限层级,列表 computeDepth 递归缩进。
|
||||
- 但商品端选分类的级联组件 `src/components/SelectGoodsCategory/index.vue` 的 formatData/filterData 只映射 2~3 级;shopGoodsEdit 的 `chooseGoodsCategory` 写死 `value[1].value`/`value[0].label`(仅 2 级),3 级及以上会选错/选不中。
|
||||
- 后端 `/shop/shop-goods-category` 是否限层级需查后端仓库确认(本仓库看不到)。
|
||||
|
||||
## 新增:忘记密码(找回密码)功能
|
||||
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`(已验证可用)。
|
||||
- 新增页面 `src/views/passport/forget/index.vue`:复用登录页视觉风格,支持「邮箱找回 / 手机找回」两种 Tab,含验证码 60s 倒计时、密码强度校验(≥8 位且含字母+数字)、两次密码一致性校验。
|
||||
- 路由:在 `src/router/routes.ts` 启用 `/forget`(登录页「忘记密码」链接 `push('/forget')` 原本就指向它,路由此前被注释)。
|
||||
- 新增 API:
|
||||
- `src/api/passport/login/index.ts`:`sendEmailCaptcha(data,{skipTenant})` → POST /sendEmailCaptcha;`sendSmsCaptchaByAdmin(data,{skipTenant})` → POST /sendSmsCaptchaByAdmin。
|
||||
- `src/api/layout/index.ts`:`resetPassword(data)` → POST /resetPassword(带 X-Tenant-Skip:1);`src/api/layout/model/index.ts` 新增 `ResetPasswordParam`。
|
||||
- 关键改动:`src/utils/request.ts` 请求拦截器在请求头含 `X-Tenant-Skip` 时跳过注入 TenantId/CompanyId/MerchantId/Domain(登录前找回密码场景,避免串租户)。`vite build --outDir dist-verify` 验证通过(真实 `vite build` 因沙箱 safe-delete 守卫删 dist 拦截,非代码问题)。
|
||||
- 后端同址 server.websoft.top/api,与 website-admin 共用一套找回密码接口。
|
||||
|
||||
## 优化:忘记密码页发送验证码前 3 次免图形验证
|
||||
- 修改 `src/views/passport/forget/index.vue`:引入 `getCaptcha`,新增图形验证码弹窗(与 `register2/index.vue` 风格一致)。
|
||||
- 用 `localStorage` 按 `forget_send_count_${activeTab}_${account}` 记录每个邮箱/手机号的发送次数。
|
||||
- 同一个账号前 3 次点击「获取验证码」直接调用 `sendEmailCaptcha`/`sendSmsCaptchaByAdmin`;第 4 次起弹出图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
|
||||
- 失败时自动刷新图形验证码;弹窗关闭时重置 `codeLoading`。
|
||||
- 新增 i18n 键 `login.forgetPage.captchaTitle`、`captchaPlaceholder`、`sendNow`(zh_CN/en)。
|
||||
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-captcha` 编译通过。
|
||||
|
||||
## 优化:登录页手机号验证码前 3 次免图形验证
|
||||
- 修改 `src/views/passport/login/index.vue`:手机号登录 Tab 下,「发送验证码」按钮由直接弹窗改为先计数判断。
|
||||
- 用 `localStorage` 按 `login_sms_send_count_${phone}` 记录每个手机号的发送次数。
|
||||
- 同一个手机号前 3 次直接调用 `sendSmsCaptcha`;第 4 次起弹出原有图形验证码弹窗,输入正确并点击「立即发送」后才真正发送。
|
||||
- 失败时自动刷新图形验证码;补充了 `.catch`/`.finally`,避免发送异常时按钮一直 loading。
|
||||
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-login` 编译通过。
|
||||
|
||||
## 修复:邮箱登录图形验证码未校验
|
||||
- **问题**:`src/views/passport/login/index.vue` 第 651-555 行的验证码比对逻辑被注释掉,导致用户输入任意非空字符串即可通过前端校验。
|
||||
- **修复**:取消注释 `form.code?.toLowerCase() !== text.value` 的校验块,恢复「输入错误则提示"验证码错误"并刷新验证码」的逻辑。
|
||||
- **说明**:当前后端 `/captcha` 接口返回明文答案 `data.text`,前端直接比对即可。后续若后端改为返回 key 模式,需同步改为传 key 给后端校验。
|
||||
|
||||
## 新增:手机号登录多租户选择
|
||||
- 参考项目:`/Users/gxwebsoft/VUE/website-admin/app/pages/login.vue`。
|
||||
- `src/api/passport/login/model/index.ts`:
|
||||
- `LoginResult` 新增 `tenants?: TenantOption[]`;
|
||||
- 新增 `TenantOption` 类型与 `LoginOutcome` 联合类型(`ok` / `selectTenant`)。
|
||||
- `src/api/passport/login/index.ts`:
|
||||
- 新增 `saveLoginState` 统一保存 token、TenantId、UserId;
|
||||
- `loginBySuperAdminSms` 改为返回 `LoginOutcome`,检测 `data.tenants` 时返回 `selectTenant`;
|
||||
- 新增 `loginBySelectTenant({ phone, code, tenantId, remember })` 二次登录接口。
|
||||
- `src/views/passport/login/index.vue`:
|
||||
- `onLoginBySms` 根据 `outcome.status` 处理:单租户直接登录成功;多租户弹出「请选择要登录的租户」弹窗。
|
||||
- 新增弹窗与 `onSelectTenant`:选择租户后调用 `loginBySelectTenant` 完成二次登录并跳转。
|
||||
- `vue-tsc` 本次新增代码无类型错误;`vite build --outDir dist-verify-tenant` 编译通过。
|
||||
@@ -0,0 +1,16 @@
|
||||
# 2026-07-25 工作日志
|
||||
|
||||
## CMS 模块复用方案分析(mp-java vs guilixu-java)
|
||||
- 结论:**guilixu 应直接调用 mp-java 的 cms(modules 服务),不要整体复制到 guilixu-java。**
|
||||
- 证据:guilixu 的 152 个 cms 类是 mp-java 235 类的子集分叉且落后 1.5 个月;前端 cms 接口已全部走 `MODULES_API_URL`(指向 mp-java),guilixu-java 的 cms 包是死代码;"整体复制"因缺 `project` 模块无法编译。
|
||||
- 产出报告:`docs/cms-module-reuse-analysis.md`
|
||||
- 已同步架构结论到 MEMORY.md(Java 后端架构 / MODULES_API_URL 模式)
|
||||
|
||||
## 商品分类列表:二级/三级前加树形连接线(方案A)
|
||||
- 文件:`src/views/shop/shopGoodsCategory/index.vue`
|
||||
- 需求:用户要二级/三级分类前加图标区分层级,先看 mockup 选方案,选了「方案A 树形连接线」。
|
||||
- 实现:移除原 `a-space` 的 `paddingLeft` 纯缩进,改为用 SVG 绘制树形连接线(`renderTreePrefix` 函数)。
|
||||
- 新增 `nodeMap`、`isLastMap`,`buildTreeMeta` 在 datasource 加载时构建(是否末位子节点)。
|
||||
- 连接线逻辑:祖先节点非末位→贯穿引导竖线;末段→竖线+横线拐角(末位用 └,否则 ├)。每级宽 20px,颜色 #d0d0d0。
|
||||
- 顶级(depth=0)不画;二级/三级按祖先链回溯绘制。缩进完全由 SVG 宽度承担。
|
||||
- 验证:vite 模块编译通过(localhost:5580 dev server 已起)。
|
||||
@@ -0,0 +1,24 @@
|
||||
# 2026-07-28
|
||||
|
||||
## 订单列表操作列分隔线对齐 shopGoods
|
||||
- 文件:`src/views/shop/shopOrder/index.vue` 的 `column.key === 'action'` 操作列模板。
|
||||
- 改动:原本操作列按钮只用 `<a-space>` 包裹、无分隔线;现按 `shop/shopGoods` 的写法,在「详情」与各条件块之间、以及块内多个按钮之间加入 `<a-divider type="vertical" />`。
|
||||
- 分隔线放在各 `<template v-if>` 条件块内部,仅当对应操作出现时才渲染,避免多余竖线。
|
||||
|
||||
## 订单列表商品明细展开/收起(方案一已落地)
|
||||
- 文件:`src/views/shop/shopOrder/index.vue`。
|
||||
- 需求:商品明细较多时列表行被撑高,加「展开/收起」。采用方案一(单元格内展开),已确认执行。
|
||||
- 实现:
|
||||
- 新增 `expandedOrderIds: ref<Set<string>>`(用 `String(record.orderId)` 作 key,因 orderId 为 number|undefined)。
|
||||
- 新增 `collapsedGoodsCount = 2`、`isOrderGoodsExpanded(record)`、`toggleOrderGoodsExpanded(record)`。
|
||||
- 改写 `column.key === 'orderGoods'` 模板:默认 `record.orderGoods.slice(0, 2)`,超过 2 件显示「展开全部 N 件」,点击切换,展开后文案变「收起」。
|
||||
- 类型检查:`vue-tsc --noEmit` 仅有的 3 处报错(line 2 `$router`/page-header、line 696/734 未用变量)均为项目既有,与本次改动无关。
|
||||
|
||||
## shop/dashboard 运行天数改为取租户创建时间
|
||||
- 文件:`src/views/shop/dashboard/index.vue`。
|
||||
- 需求:/dashboard 的"运行天数"要改用「租户创建时间」而不是订阅生效时间。
|
||||
- 现状调查:
|
||||
- `cms/dashboard`(站点控制台,标题"系统运行天数")已在提交 894592c 改为 `getTenantInfo().createTime`(租户创建时间),已是目标行为。
|
||||
- `shop/dashboard`(商城后台,标签"运行天数")原用 `currentSubscription.startTime`(订阅生效时间)。
|
||||
- 改动:将 shop/dashboard 的 `runDays` computed 改为取 `tenantStore.company?.createTime`(loadData 已 `tenantStore.fetchTenantInfo()` 写入 company)。同步更新顶部数据来源注释。
|
||||
- 数据源说明:`getTenantInfo()` 返回 `Company`,其 `createTime` 字段即租户创建时间(`src/api/system/company/model/index.ts` line 164);`site.ts` 的 `runDays` getter 仍用 `siteInfo.createTime`(站点创建时间),目前 cms/dashboard 已不依赖它。
|
||||
@@ -0,0 +1,16 @@
|
||||
# 2026-07-29
|
||||
|
||||
## websopy-java:cms 模块数据源路由修复(重要跨项目)
|
||||
- 报错:`cms-template/current` 报 `Table 'db_websopy.cms_website' doesn't exist`;jar 为 `websopy-api-1.5.0.jar`(= `/Users/gxwebsoft/JAVA/websopy-java`)。
|
||||
- 根因:websopy-java 原只有单一数据源,dev/prod/websopy 三个 profile 全指向 `db_websopy`;而 cms 模块(23 张 cms_* 表)实际在 `modules` 库。test profile 本就指向 `modules`(印证 cms 表在 modules)。
|
||||
- 已确认方案 B(用户选「独立库 db_websopy」):多数据源,websopy 自有数据留 `db_websopy`,仅 cms 包走 `modules`。
|
||||
- 改动(在 websopy-java):
|
||||
1. `pom.xml` 增加 `com.baomidou:dynamic-datasource-spring-boot-starter:3.4.1`(匹配 MP 3.4.3.3;**不要用 3.5.x**,其对 MP 3.5,运行期可能 NoSuchMethod)。
|
||||
2. `application-dev/prod/websopy/test.yml` 的单一 `spring.datasource` 改为 `spring.datasource.dynamic`:`primary: websopy`(test 为 `modules`),`datasource.websopy` + `datasource.modules` 双库;modules 用库 `modules`、用户 `modules`、密码 `P7KsAyDXG8YdLnkA`(取自 test profile)。
|
||||
3. 给 `com/gxwebsoft/cms/mapper` 下全部 23 个 Mapper 接口加 `@DS("modules")`(import `com.baomidou.dynamic.datasource.annotation.DS`),仅把 cms SQL 路由到 modules,不影响 websopy 自有表。
|
||||
- 调用链:`cms-template/current` → `CmsWebsiteServiceImpl.getByTenantId()` → `baseMapper.getByTenantId()`(已注解)→ modules。✅
|
||||
- 重新构建:websopy-java 本地无 mvn/docker,需在其环境 `mvn clean package` 或重 build docker 镜像后重启容器(prod profile `SPRING_PROFILES_ACTIVE=prod`)。
|
||||
- ⚠️ 遗留问题(未处理,需确认):
|
||||
- `shop/mapper/xml/ShopGoodsMapper.xml` 第 9 行 `LEFT JOIN cms_navigation`(无 schema 前缀),该 shop 查询走默认 websopy → 若 `cms_navigation` 不在 `db_websopy` 会失败;dynamic-datasource 无法跨单条 SQL 双库,需把 cms_navigation 复制到 db_websopy 或改成 `modules.cms_navigation`/`gxwebsoft_core.cms_navigation` 前缀。
|
||||
- 前提假设:所有 cms_* 表都在 modules(依 test profile 整库连 modules 推断)。若实际只有部分 cms 表在 modules,应改为只给缺失的那几张 mapper 加 @DS。
|
||||
- 跨库写事务:cms 写操作经 @DS 走 modules 独立连接,与 websopy 事务不在一个事务里,写一致性需后续评估。
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-08-01 工作记录
|
||||
|
||||
## 订单自动取消改造:超时时间改为后台「订单设置」驱动
|
||||
|
||||
### 需求
|
||||
自动取消未付款订单的超时时间,需由管理后台「商城设置 → 订单设置 → 未付款自动关闭」控制;设置为 0 分钟 = 不自动取消。
|
||||
|
||||
### 改造要点(已全部落地)
|
||||
- 后台设置存于 `ShopSetting` 表 `setting_key='shop_order'` 的 JSON 中,字段 `orderAutoCloseMins`(前端 order.vue 已支持保存)。
|
||||
- 新增 `ShopSettingService.getOrderAutoCloseMinutes()`:参照 `getSalesAccumulateType()` 写法,从 `getCategoryValues("order")` 读 `orderAutoCloseMins`;0=禁用(合法返回);负数/读取失败兜底 30(常量 `DEFAULT_AUTO_CLOSE_MINUTES=30`)。
|
||||
- `OrderAutoCancelTask.cancelExpiredOrders()`:先读 DB,`==0` 直接 return(全局禁用,含租户特殊配置 tenantConfigs 一起跳过);`>0` 用它替换原先 YAML 的 `defaultTimeoutMinutes` 传给 `findExpiredUnpaidOrders`。
|
||||
- `OrderConfigProperties.AutoCancel.defaultTimeoutMinutes` 标 `@Deprecated`,仅作兜底。
|
||||
- 前端 `order.vue`:`orderAutoCloseMins` 的 `min` 由 1 改 0,placeholder/文案/校验提示改为「0 表示不自动取消」。
|
||||
|
||||
### 设计决策(用户已确认)
|
||||
1. DB 未配置/异常兜底 = 30 分钟(与历史默认一致)
|
||||
2. 现状 YAML `tenantConfigs`(租户特殊超时)保留不做改造;0=全局禁用时 tenantConfigs 也不执行
|
||||
3. 不引入缓存,定时任务每次查一次 DB(开销极小),改后台 1~5 分钟内生效
|
||||
4. 前端交互全部调整(min=0 + 文案 + 校验)
|
||||
|
||||
### 涉及文件
|
||||
- JAVA: `ShopSettingService.java`、`ShopSettingServiceImpl.java`、`task/OrderAutoCancelTask.java`、`config/OrderConfigProperties.java`
|
||||
- 前端: `src/views/shop/shopSetting/components/order.vue`
|
||||
|
||||
### 未做
|
||||
- 未编译验证(本地无 maven/mvnw 不可用);改动为标准增量,已逐文件核对语法。
|
||||
- SQL / 取消逻辑 / 库存回退 / 优惠券退还 / 销量回滚 全部未动。
|
||||
@@ -0,0 +1,90 @@
|
||||
# 2026-08-05 商品管理页(shopGoods)加载优化分析
|
||||
|
||||
## 关键结论(修正 2026-08-04 误判)
|
||||
- 后端 `mp-java` 的 `ShopGoodsMapper.xml` 已支持 `sceneType` 参数:
|
||||
- `on_sale` → `AND a.status = 0`
|
||||
- `pending` → `AND a.status != 0`
|
||||
- `sold_out` → `AND a.stock = 0`
|
||||
- 前端 `search.vue` 传 `where.sceneType` 给 `pageShopGoods` 是**有效的**(JS 运行时携带,TS 类型未声明但不影响运行)。
|
||||
- 结论:「出售中/待上架/已售罄」状态筛选功能**正常**,**不是卡顿根因**。上轮判定"sceneType 后端不识别"为误判。
|
||||
|
||||
## getCount 合并进 pageShopGoods 的影响面
|
||||
- 商品模块 `getCount`(`/shop/shop-goods/data`)仅被 `src/views/shop/shopGoods/components/search.vue` **一处**调用。
|
||||
- `cms/cmsArticle` 的 `getCount` 是独立接口(`/cms/cms-article/data`),互不影响。
|
||||
- `guilixu-taro`(用户端小程序)未调用 `/shop/shop-goods/data`。
|
||||
- 合并到 page 响应、保留 `/data` 接口 → 向后兼容,零影响其他端。
|
||||
- 收益有限:当前 `getCount` 与 `page` 在 `reload()` 里已是并发触发,合并仅省一次 HTTP 往返,非量级差异。
|
||||
|
||||
## 首屏请求并发情况
|
||||
- 首屏同一帧发 3 个请求:pageShopGoods(datasource)、getCount(search.vue reload)、listShopGoodsCategory(index.vue 顶层 query())。
|
||||
- `search.vue` 的 `watch(() => props.merchantId, ..., { immediate: true })` 与 `index.vue` 顶层 `query()` 存在时序竞争,但单次进入只触发一次,未构成死循环。
|
||||
|
||||
## 卡顿真因(待 profiling 确认,未执行改动)
|
||||
- 头号嫌疑:商品封面 `<a-image>`(Ant Design Vue)不支持原生 `loading="lazy"`,首屏多行图片一次性并发请求;另 `getCompressedImageUrl` 每次 v-for 渲染重算。
|
||||
- 次因:首屏 3 并发 + watch immediate 竞争。
|
||||
- 建议:让用户开 DevTools → Network 看 `pageShopGoods` 耗时与是否有重复/慢请求,再定改动优先级。
|
||||
|
||||
## 用户决策(本轮)
|
||||
- 已售罄=库存为0(与后端一致,确认)。
|
||||
- getCount 合并影响面小但收益有限,待用户定。
|
||||
- 导出(listShopGoods 全量)本轮先不做。
|
||||
- 澄清:要替换的是商品封面 `<a-image>`,**不是 ele-pro-table 整体**;仅当前模块。
|
||||
|
||||
## 后端 /data(getCount)实现细节(影响合并决策)
|
||||
- `ShopGoodsController.data()` 实际执行:
|
||||
1. `count(status=0 AND stock>0)` → totalNum(出售中)
|
||||
2. `count(status>0)` → totalNum2(待上架)
|
||||
3. `count(stock=0)` → totalNum3(已售罄)
|
||||
4. **`update shop_goods SET status=1 WHERE stock=0`** ← 写副作用!每次调 /data 都会把已售罄商品自动下架。
|
||||
- `/page` 用 MyBatis-Plus `selectPageRel`,内部已含 1 次 count(total) + 1 次 select,共 2 SQL。
|
||||
- 现状进入/筛选:page(2 SQL) + data(3 count + 1 update = 4 SQL) 并发。
|
||||
- 翻页/排序:只调 page(2 SQL),**不触发 data 的 update**。
|
||||
|
||||
## getCount 合并进 /page 的后端消耗结论(负优化,不建议)
|
||||
- 合并后每次 page 调用(含翻页/排序)都多跑 3 次 count + 1 次 update → 翻页/排序 SQL 从 2 飙到 6。
|
||||
- 列表返回变慢:原来 page 的 2 SQL 快速回,data 的 4 SQL 异步在后;合并后列表要等额外 3 count + 1 update 算完才返回。
|
||||
- 写副作用被高频触发:翻页/排序也跑 `UPDATE ... WHERE stock=0`,幂等但带来行锁竞争,且写操作混在读链路是反模式。
|
||||
- 总 DB 读次数在「进入/筛选」场景不变,但「翻页/排序」场景陡增;净收益为负(仅省一次 HTTP 往返)。
|
||||
- **结论:单纯合并是负优化,不执行。** 真要优化后端统计应:① 3 次 count 合成 1 次 GROUP BY 条件聚合;② 把 update 副作用从读接口剥离(移到定时任务/下单扣库存时);③ 再谈是否挂到 page 响应。前端侧保持并发(Promise.all)即可,零后端改动。
|
||||
|
||||
## 已执行的前端优化(2026-08-05)
|
||||
用户确认执行,已改 3 个文件:
|
||||
|
||||
### 1. 商品封面图懒加载(index.vue + image.ts)★ 最可能见效
|
||||
- `src/views/shop/shopGoods/index.vue` 第 39 行附近:`<a-image :src :preview=false :width :height>` 替换为原生
|
||||
`<img class="goods-thumb" loading="lazy" width="50" height="50" alt="">`。
|
||||
- 原因:antdv `a-image` 不支持转发原生 `loading="lazy"`,且即使 `:preview=false` 仍包一层容器/事件;换成原生 `<img>` 后由浏览器 IntersectionObserver 懒加载,首屏仅可视区~10 张图请求。
|
||||
- 新增 `.goods-thumb` 样式(object-fit:cover、圆角 4px、暗色占位背景)。
|
||||
- `src/utils/image.ts`:`getCompressedImageUrl` 加 `Map<string,string>` 缓存,避免列表重渲染(勾选/hover/滚动)时重复拼接 OSS URL 字符串。全局受益,纯函数无副作用。
|
||||
|
||||
### 2. 首屏请求收敛(index.vue + search.vue)
|
||||
- `index.vue`:`<ele-pro-table>` 加 `:init-load="false"`(关闭表格自身自动首屏加载)。
|
||||
- `search.vue`:去掉 `watch(merchantId, {...immediate:true})`,改为 `onMounted` 主动触发一次 `reload()`(设置 where.merchantId + 发 getCount + emit search 触发表格首次加载)。
|
||||
- 原因:原 `initLoad`(默认 true) + `watch immediate` 导致首屏重复发一次 `pageShopGoods`(自动加载一次 + watch 又 reload 一次);改后首屏只有 1 次 page + 1 次 getCount(并发),无重复。
|
||||
- `index.vue`:`query()`(加载分类)从顶层同步调用改为 `onMounted(query)`,分类数据延迟到列表首屏渲染后异步加载,不阻塞首屏;去掉 `query()` 内无用的 `loading.value = true` 写入。
|
||||
- 注:`loading` ref 仍保留(handleExport 用它做防重入,非死变量,上轮"loading 死变量"判断有误)。
|
||||
|
||||
### 验证
|
||||
- 已跑 `vite build` 后台验证可编译(待结果)。
|
||||
|
||||
## 后端优化已执行(2026-08-05,用户确认"现在改")
|
||||
改 mp-java 共 6 处:
|
||||
1. `ShopGoodsMapper.xml`:新增 `selectCountSummary` —— `CASE WHEN` 聚合一条 SQL 返回 totalNum(出售中=status=0且stock>0)/totalNum2(待上架=status>0)/totalNum3(已售罄=stock=0),替代原 3 次 count;含 `param.merchantId` 可选条件,多租户插件自动加 tenant_id。
|
||||
2. `ShopGoodsMapper.java`:加 `selectCountSummary` + `downSoldOutGoods`(`@InterceptorIgnore(tenantLine=true)` 跨租户 `UPDATE shop_goods SET status=1 WHERE stock=0 AND status!=1`,加 `status!=1` 避免重复写)。
|
||||
3. `ShopGoodsService.java`/`Impl`:`getCountSummary(param)`(聚合统计,Map→int 安全转换)+ `downSoldOutGoods()`。
|
||||
4. `ShopGoodsController.data()`:改为调 `getCountSummary(param)`,删除 3 次 count 与 update 写操作(data 接口不再写库)。
|
||||
5. 新建 `shop/task/GoodsSoldOutTask.java`:`@Component` + `@Scheduled(cron="${shop.goods.sold-out.cron:0 */5 * * * ?}")`,每 5 分钟全局下架已售罄商品(仿 CouponExpireTask 写法,@EnableScheduling 已在 WebSoftApplication 启用)。
|
||||
- 编译验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper.class)。
|
||||
- **语义变化(需用户知晓)**:
|
||||
- 下架时机:原"每次打开商品列表才下架当前租户" → 现"定时任务每 5 分钟全局下架所有租户"。5 分钟窗口内新售罄商品会短暂留在"出售中"列表(on_sale 只查 status=0),直至定时任务执行。
|
||||
- 范围:原依赖请求线程租户上下文(只下当前租户)→ 现 `@InterceptorIgnore` 跨租户(所有商户已售罄都下架),更彻底一致。
|
||||
- 频率可调:`shop.goods.sold-out.cron` 配置项。
|
||||
- 若要求"出售中"列表立即可不显示 stock=0,可给 on_sale SQL 加 `AND a.stock > 0`(未做,超出剥离范围,待用户决定)。
|
||||
- 注:Controller 改造后 `LambdaQueryWrapper`/`LambdaUpdateWrapper` import 可能 unused,但 Java 编译允许 unused import 且其他接口可能仍用,未清理(编译已通过)。
|
||||
|
||||
## ⚠️ 重要修正:guilixu-java 才是本次后端优化的目标端(2026-08-05 用户指出"改错路径")
|
||||
- 用户明确:后端改动**应该**落在 `/Users/gxwebsoft/JAVA/guilixu-java`,且 **mp-java 也保留**(两端都要有)。
|
||||
- 原因澄清:shopGoods 的 `ShopGoodsController.data()` / Service / Mapper / task 在 **mp-java 与 guilixu-java 都存在且结构一致(同 `com.gxwebsoft.shop` 包)**,所以"共享 shop 业务模块"的改动要**两端同步**。
|
||||
- 已对 guilixu-java 等价移植上述 6 处改动(selectCountSummary SQL、selectCountSummary/downSoldOutGoods 方法、getCountSummary/downSoldOutGoods 方法体、data() 剥离写、新建 GoodsSoldOutTask),与 mp-java 完全一致。
|
||||
- 验证:IntelliJ 内置 Maven `compile` 通过(target/classes 已生成 GoodsSoldOutTask/ServiceImpl/Mapper/Controller.class)。
|
||||
- 教训:之前 MEMORY.md 说"guilixu-java 的 cms 是死代码、前端不调 guilixu-java"是**针对 cms 模块**的;**不能推广到所有 shop 模块**——shopGoods 等共享业务在两端都有活代码,改动需两端同步。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 2026-08-06 工作日志
|
||||
|
||||
## 运费模板「不发货地区」需求分析
|
||||
- 客户需求:运费模板要支持"排除地区/不发货地区"(黑名单模式),而非仅"指定配送"(白名单)。
|
||||
- 现状问题:当前 `ShopExpressTemplateDetail` 仅 `provinceId`+`cityId` 单值,级联选择器只能选 1 个省市,无排除逻辑。截图里的双栏 Transfer modal 前端代码库里不存在,需从零开发。
|
||||
- 产出接口方案文档:`docs/运费模板-不发货地区-接口方案.md`,含表字段变更(region_mode/region_ids)、下单校验逻辑、6 个待后端确认问题。
|
||||
- 后端在 mp-java 的 shop 模块(前端走 MODULES_API_URL)。用户下一步:先与后端对齐接口方案,再做前端改造。
|
||||
- 地区 ID 为行政区划编码字符串(省110000/市110100/区110101),下单时可前缀匹配。
|
||||
- 存储方案确认:region_ids 采用**逗号分隔单字段**(VARCHAR(2000)),不新建关联表。理由:查询方向固定为"模板→明细"、排除地区少、改动最小可快速上线。已写入方案文档。
|
||||
- 用户确认"分两步"策略:第1步接口方案已产出(docs/运费模板-不发货地区-接口方案.md),待后端回复 6 个确认问题后做第2步前端改造。
|
||||
|
||||
## 后端实现检查(用户要求检查 guilixu-java 是否实现完)
|
||||
- **guilixu-java 已实现完整**:entity 加 regionMode/regionIds、param 加查询字段、迁移 SQL(ALTER+历史数据迁移+索引+回滚)、DeliveryRegionChecker(黑名单命中抛"暂不配送")、RegionCodeResolver(名称→编码)、OrderBusinessService 第 662/668 行已接入校验。
|
||||
- **用户纠正(18:09):** guilixu-admin 的**商城接口实际调 `https://shop-api.websoft.top`(= guilixu-java)**,不是 mp-java。前端 shopExpressTemplateDetail API 用默认 `@/utils/request`,baseURL = `MODULES_API_URL` = `localStorage ApiUrl || VITE_API_URL`;该部署的 ApiUrl 配的就是 shop-api.websoft.top/api,即 **guilixu-java 才是生效后端**。
|
||||
- **结论反转:guilixu-java 这套实现正确且生效,不是死代码,后端已完整,无需改 mp-java。** mp-java 与此功能无关(本部署 商城不走 mp-java)。
|
||||
- 后端 `region/regions-data.json` 用 v/l/c 精简格式(与 RegionCodeResolver 读取一致),无字段名 bug。
|
||||
- 后端完成度:entity/param/SQL/DeliveryRegionChecker/RegionCodeResolver/下单接入(OrderBusinessService 第 87 行调用 validateDeliveryRegionIfNeeded)全齐。
|
||||
- **剩余工作 = 纯前端(guilixu-admin)**:① model 加 regionMode/regionIds;② 新建双栏多选地区组件(替换单路径级联 `RegionsSelect`,支持"全国/指定配送/指定不发货"模式切换+多选多省);③ 编辑表单加模式切换+多选;④ 列表回显模式与地区汇总。截图里的 Transfer Modal 前端尚不存在。
|
||||
- 教训:判断"改了哪个后端才生效"要以**运行时的 ApiUrl(localStorage/localStorage 配置的 ApiUrl,本部署=guilixu-java)**为准,不能默认 `MODULES_API_URL`=mp-java。guilixu-admin 的 商城走 guilixu-java。
|
||||
@@ -0,0 +1,130 @@
|
||||
# 2026-08-11 工作记录
|
||||
|
||||
## 运费模板配送地区改为「省+市两级多选穿梭弹窗」
|
||||
|
||||
### 需求背景
|
||||
客户要求运费模板的配送地区选择改成仿截图的双栏穿梭弹窗(左侧候选 + 右侧已选),
|
||||
选择粒度从原「省+市单选(级联)」升级为「多条规则覆盖多城市」,并保留「不选=全国」规则。
|
||||
|
||||
### 改动清单(纯前端,后端字段由用户确认可改)
|
||||
1. **新建组件** `src/components/RegionMultiSelect/index.vue`
|
||||
- 树形可展开:省级可展开看市级;省级"选择"=选该省全部市,市级可单独选。
|
||||
- 左栏搜索(按省/市名过滤)、右栏已选列表(单项是"省/市"+取消)。
|
||||
- 底部:全部选择 / 取消 / 确认。
|
||||
- `v-model:value` 绑定已选**城市ID数组**;额外 emit `update:provinceIds`(由城市推导的省份ID数组)。
|
||||
- 复用 `src/components/RegionsSelect/load-data.ts` 的 getRegionsData()(public/json/regions-data.json,取省+市两级,丢弃区级)。
|
||||
2. **model** `src/api/shop/shopExpressTemplateDetail/model/index.ts`
|
||||
- 新增 `provinceIds?: string`(逗号分隔)、`cityIds?: string`(逗号分隔);保留旧 `provinceId/cityId` 兼容历史数据。
|
||||
3. **编辑弹窗** `src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue`
|
||||
- 移除 `RegionsSelect type="provinceCity"`,改用 `RegionMultiSelect`。
|
||||
- 打开时从 `record.cityIds`(兼容旧 `cityId`)回填;保存时数组→逗号串提交 `cityIds/provinceIds`。
|
||||
4. **列表页** `src/views/shop/shopExpressTemplateDetail/index.vue`
|
||||
- "配送地区"列 customRender 改为遍历 `cityIds` 多值反查 regionMap;超22字截断+tooltip;旧单值数据兼容。
|
||||
- 新增 `h` 函数 import。
|
||||
|
||||
### 待办 / 风险
|
||||
- antd 为 v4,`var(--ant-color-border)` 在暗色下可能回退浅灰(边框略浅,不影响功能);如要完美暗色适配可后续改 less 变量。
|
||||
- 构建验证(vite build)已通过。
|
||||
|
||||
## ⚠️ 重大修订(同日):前端字段名与后端不匹配,导致"没存进库"
|
||||
|
||||
### 根因
|
||||
- 后端 `ShopExpressTemplateDetail` 实体**已有**配送区域多选机制:`regionMode`(0=全国 / 1=指定配送白名单 / 2=不发货黑名单)+ `regionIds`(逗号分隔的 6 位城市编码串)。
|
||||
- 旧代码 `provinceId`/`cityId` 是单值历史字段,已被迁移脚本 `sql/shop_express_template_detail_region.sql` 迁成 `regionMode=1, regionIds=城市编码`。
|
||||
- 我第一版前端提交的是 `provinceIds`/`cityIds`,后端实体无此字段 → MyBatis-Plus 自动忽略未知字段 → 看起来"没存没变化"。
|
||||
- ORM 是 MyBatis-Plus,add/update 用 `save()`/`updateById()` 自动生成 SQL,接收的是实体类本身(非 Param);新增字段只需在**实体加属性 + 表加列**,无需改 Mapper XML。
|
||||
|
||||
### 修复(对接后端已有的 regionMode/regionIds,无需后端改动)
|
||||
1. model:`provinceIds`/`cityIds` → 改为 `regionMode?`、`regionIds?`。
|
||||
2. `RegionMultiSelect` 组件:去掉 `update:provinceIds` emit,只 emit `update:value`(城市编码数组)。
|
||||
3. 编辑弹窗:城市选择变化 → `form.regionIds = 逗号串`、`form.regionMode = 1`(有选)或 `0`(全国);回显从 `form.regionIds` 拆出。
|
||||
4. 列表页:customRender 读 `record.regionIds` 反查城市名。
|
||||
- 结论:**本需求纯前端即可闭环,后端无需改**。城市编码(`regions-data.json` 的 city value,6位)与 `regionIds` 完全一致,下单校验 `DeliveryRegionChecker` 读 `regionIds` 直接生效。
|
||||
|
||||
## 修复:多规格SKU表格缺少库存列
|
||||
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
|
||||
- 问题:`columns` 数组缺少 `stock`(库存) 和 `salePrice`(划线价) 列定义,导致多规格SKU明细表不显示库存输入框(批量设置栏有,但表格列没有)
|
||||
- 修复:在 columns 数组中补齐 `salePrice` 和 `stock` 两列(bodyCell 模板已有渲染逻辑,只需加列定义)
|
||||
|
||||
## 修复:商品保存「快递配送时运费模板必选」前端校验缺失
|
||||
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
|
||||
- 问题:后端有校验规则——`canExpress=1` 时要求 `expressTemplateId` 必填,但前端无对应校验,用户开启快递配送后直接提交被后端拦住,报错"运费模板必选"。
|
||||
- 修复:在 `save()` 的 validate 通过后、请求前增加前端拦截:`canExpress=1 && !expressTemplateId` → `message.error('请选择运费模板')`。
|
||||
- UI 优化:运费模板 `<a-form-item>` 加 `:required="form.canExpress === 1"` 动态必填标记 + placeholder 提示文字。
|
||||
|
||||
## 修复:多规格SKU会员价(buyingPrice)保存失败
|
||||
- **根因**:后端 `ShopGoodsSku` 实体类(guilixu-java + mp-java)和数据库表 `shop_goods_sku` 均无 `buying_price` 字段。前端提交的 buyingPrice 被 MyBatis-Plus 忽略 → 保存后丢失。
|
||||
- **改动(三端同步)**:
|
||||
1. **guilixu-java** `entity/ShopGoodsSku.java`:加 `private BigDecimal buyingPrice;`
|
||||
2. **mp-java** `entity/ShopGoodsSku.java`:同上(shop 模块两端同步)
|
||||
3. **前端 TS 接口** `api/shop/shopGoodsSku/model/index.ts`:加 `buyingPrice?: string;`
|
||||
- **需手动执行 SQL**:`ALTER TABLE shop_goods_sku ADD COLUMN buying_price DECIMAL(10,2) DEFAULT NULL COMMENT '会员价' AFTER cost;`
|
||||
|
||||
## 修复:多规格SKU图片删不掉
|
||||
- 文件:`src/views/shop/shopGoods/components/shopGoodsEdit.vue`
|
||||
- 问题:`onDeleteSkuItem(index)` 误删 `images.value`(主商品图片数组),而非 `skuList.value[index].images`(对应SKU行图片)→ 点击 × 无反应
|
||||
- 修复:改为清空 `skuList.value[index].images = []` + `image = ''`
|
||||
|
||||
## 商品列表「价格」列支持多规格价格区间(方案A)
|
||||
- 需求:多规格商品各 SKU 价格不同,原列表价格列只显示主表 `price/salePrice/dealerPrice`(占位值),与 SKU 实际价格对不上。确认采用方案A:**多规格显示 ¥最低价 ~ ¥最高价,单规格保持单值**。
|
||||
- 后端(**guilixu-java**,生效后端):
|
||||
1. `entity/ShopGoods.java`:加 3 个**非持久化**字段 `priceMax / salePriceMax / dealerPriceMax`(BigDecimal,`@TableField(exist = false)`)。
|
||||
2. `service/impl/ShopGoodsServiceImpl.java`:`pageRel`/`listRel` 查完后调 `fillSkuPriceRange(list)`——收集 `specs==1` 的商品,`shopGoodsSkuService.lambdaQuery().in(goodsId, ids)` 一次批量查 SKU,按 `price/salePrice/buyingPrice` 算 min/max 回填(主表 `price=min`、`priceMax=max`;市场价/会员价同理)。只填内存对象、不落库。
|
||||
- 前端(**guilixu-admin**):
|
||||
1. `api/shop/shopGoods/model/index.ts`:ShopGoods 加 `priceMax? / salePriceMax? / dealerPriceMax?`。
|
||||
2. `views/shop/shopGoods/index.vue`:priceGroup 列渲染改为「区间不相等时 ¥min ~ ¥max,否则单值」;导出 Excel 新增 `fmtPrice()` 同步处理。
|
||||
- 验证:`guilixu-java` `mvn -o compile` 通过(本机无全局 mvn,临时用 /tmp/apache-maven-3.9.6 离线编译);`guilixu-admin` `vite build` 通过。
|
||||
- 待确认:**mp-java 是否需要同步**——本部署 admin 商城走 guilixu-java,mp-java 未参与;若 mp-java 侧也有管理端/列表调用 ShopGoods.listRel,则需同步 `fillSkuPriceRange` + 实体字段。
|
||||
|
||||
## 修复:商品「是否可以快递配送 / 运费模板」保存不成功(同根因)
|
||||
- **根因**:`ShopGoods` 实体(guilixu-java)与 `shop_goods` 表**都没有** `can_express` / `express_template_id` 字段。前端提交这两个值被 MyBatis-Plus 静默丢弃(与 8-11 多规格 buyingPrice 同一类问题)。
|
||||
- **改动**:
|
||||
1. **guilixu-java** `entity/ShopGoods.java`:加 `private Integer canExpress;`(是否可以快递配送 0/1)+ `private Integer expressTemplateId;`(运费模板ID,关联 shop_express_template.id)。`map-underscore-to-camel-case=true` 已开,自动映射 `can_express`/`express_template_id`。
|
||||
2. **迁移 SQL**(新建)`guilixu-java/sql/add_shop_goods_express_fields.sql`:`ALTER TABLE shop_goods ADD can_express TINYINT NOT NULL DEFAULT 1, ADD express_template_id INT NULL;`
|
||||
3. **前端** `shopGoodsEdit.vue`:把"运费模板"表单项从「基本信息」页签移到「营销设置」页签内、"是否可以快递配送"开关**正下方**(用户要求)。
|
||||
- **⚠️ 上线前置动作(否则不生效)**:必须在 db_guilixu 执行该 SQL 加列,且 guilixu-java 需重新编译部署(实体加了字段)。
|
||||
- 此修复同时解决"运费模板"也存不进库的问题(同一根因,用户只报了 canExpress)。
|
||||
- 前端 运费模板 必填校验(save 内 `canExpress===1 && !expressTemplateId`)与动态 label 已早先存在,逻辑无需改。
|
||||
|
||||
## 商品管理编辑弹窗:Markdown 编辑器换成 WangEditor 富文本
|
||||
|
||||
### 需求
|
||||
用户反馈商品「编辑弹窗」的 Markdown 编辑器不好用,要求换成 website-admin 的 `app/pages/articles.vue` 那套编辑器。
|
||||
|
||||
### 方案(经评估后采用)
|
||||
- articles.vue 用的是 `app/components/RichTextEditor.vue`(WangEditor `@wangeditor/editor-for-vue`,输出 HTML),并非 Markdown。
|
||||
- 采用「编辑时按需转换」策略:**老商品 content 是 Markdown,编辑回显时用 markdown-it 转成 HTML 再进富文本编辑器;已是 HTML 则原样返回**。零 DB 风险,存储格式新旧混存。
|
||||
|
||||
### 改动清单
|
||||
1. **新增依赖**(guilixu-admin,pnpm):`@wangeditor/editor@^5`、`@wangeditor/editor-for-vue@^5`、`markdown-it@^14`、`@types/markdown-it@^14`(已写 package.json + node_modules)。
|
||||
2. **新建组件** `src/components/RichTextEditor.vue`:从 website-admin 的同名组件复制,**删除 Nuxt 专属 `<ClientOnly>` 包装**(guilixu-admin 是 Vite SPA 无此组件);图片上传用 `@/api/system/file` 的 `uploadFile`(返回 `res.url || res.path`,admin 已有)。
|
||||
3. **改造** `src/views/shop/shopGoods/components/shopGoodsEdit.vue`:
|
||||
- 移除 `MdEditor`(`md-editor-v3`)+ 相关 import / `markdownToolbars` / `onMarkdownUploadImg` / `editorVisible` watch / `disabled` ref / `editorRef` / 文件库 `SelectData` 弹窗(无打开入口,死代码)/ `:deep(.md-editor-*)` 样式。
|
||||
- 商品详情 tab 内改用 `<RichTextEditor v-model="content" :height="520" v-if="active==='content'"/>`(仅在 tab 激活时挂载,规避隐藏容器初始化宽度 0 问题)。
|
||||
- 新增 `md = markdownit()` 与 `toEditorContent(raw)`,编辑回显 `content.value = toEditorContent(props.data.content)`。
|
||||
- 补回被误删的 `FileRecord` import(chooseImage 等函数仍用)。
|
||||
4. **保存逻辑不变**:`save()` 已读 `content.value`,直接提交 HTML。
|
||||
|
||||
### 验证
|
||||
- `pnpm build`(vite build)通过,exit 0,dist 正常产出。
|
||||
|
||||
### 风险 / 待确认
|
||||
- **消费端(小程序 `guilixu-taro/src/pages/shop/product-detail.tsx`)已兼容两种格式**:`parseContent` 把 `` 转 `<img>`,其余 HTML 直接 `RichText nodes` 渲染 → 新旧 content 都能展示,不崩。
|
||||
- **潜在坑**:若 HTML content 里含裸 `.jpg` 链接(非 `<img>` 包裹,如 `<a href="https://x/x.jpg">`),`parseContent` 的兜底会把该 URL 误转成 `<img>` 破坏链接。WangEditor 产出的图片都是 `<img>`,正常商品详情极少触发,低概率。
|
||||
- `uploadFile` 返回的 `url` 是否为小程序域名白名单内完整 URL 需上线后在真机确认(影响商品详情页图片显示)。
|
||||
- 老商品仅在「重新编辑」时转 HTML;从未编辑过的老 Markdown 商品保持 Markdown,仍由小程序正确渲染。
|
||||
|
||||
## 文章管理编辑弹窗:统一换成 WangEditor 富文本(双模式→单模式)
|
||||
- 用户要求文章模块编辑器"也换了",与商品模块一致。经确认采用「统一 WangEditor 单模式」,**删除原有的 TinyMCE 富文本 + MdEditor Markdown 双模式切换**。
|
||||
- 改动文件:`src/views/cms/cmsArticle/components/articleEdit.vue`(复用已建的 `src/components/RichTextEditor.vue`)。
|
||||
- 删除:双模式 `v-if="editor==1/2"` 渲染分支、`editor` ref、`onEditorTypeChange/performEditorSwitch/convertContentFormat/onEditor` 切换逻辑、`markdownToolbars`、`onMarkdownUploadImg`、`openMarkdownImageSelector/openMarkdownVideoSelector`、`insertMarkdownText`、TinyMCE `config`(含 custom_image_selector/quick_upload/auto_format/toggle_indent 等自定义按钮 setup)、`.mce-content-body` 样式、`uploadOss` import(仅在删块中使用)。
|
||||
- 新增/改写:
|
||||
- 内容详情改用 `<RichTextEditor ref="editorRef" v-model="content" :height="500">`。
|
||||
- 编辑器下方按钮区:`📷从图片库选择`/`🎬从视频库选择`(调 `editorRef.value.insertHtml` 插入 img/video,复用 SelectData 弹窗)、`🎨一键排版`/`📐首行缩进`(直接操作 `content.value` HTML 字符串,底层 smartFormatContent/addIndent/removeIndent 纯函数保留)。
|
||||
- `md = markdownit()` + `toEditorContent(raw)`:回显时 Markdown→HTML,已是 HTML 原样返回。
|
||||
- `save()` 固定 `editor: 1`(统一 HTML)。
|
||||
- 验证:`pnpm build` 通过(exit 0)。
|
||||
- 风险/待办:
|
||||
- `src/views/cms/cmsWebsiteSettingEdit.vue` 仍有"编辑器类型 1 md-editor-v3, 2 tinymce-editor"设置字段,文章编辑已不再读取,**该设置现在对文章编辑无效**,建议后续清理(改动涉及系统设置暂未动)。
|
||||
- 展示端 `guilixu-taro/src/pages/index/article-detail.tsx` 用 Taro `RichText` 渲染 HTML(目前是占位页),统一 HTML 安全。
|
||||
- `uploadFile` 的 url 域名白名单同商品模块,需真机确认。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 2026-08-12 工作记录
|
||||
|
||||
## 偏远地区不发货 — 如何让远端用户无法下单(tenant 10606)
|
||||
|
||||
- 用户已 `UPDATE shop_goods set express_template_id=11 WHERE express_template_id IS NULL and tenant_id=10606`,以为这就禁了偏远地区下单。实际结论:**这条只绑运费模板(算运费用),没有生成「不发货黑名单」数据,不会拦截下单**。
|
||||
- 后端拦截已端到端接通:`OrderBusinessService.createOrder` → `validateDeliveryRegionIfNeeded`(第87行) → `DeliveryRegionChecker`(按 tenant_id 查 `shop_express_template_detail` 中 region_mode=2 且 status=0 的明细,收货地址行政区划码命中即抛"XX暂不配送,请更换收货地址")。
|
||||
- 链路确认:guilixu-taro `BaseUrl=https://shop-api.websoft.top/api`、`TenantId=10606`,下单走 guilixu-java(db_guilixu);单租户下买家 tenantId=10606,校验能命中黑名单。✅
|
||||
- **admin 前端明细编辑表单未暴露 regionMode=2(只 0/1)**,故黑名单只能 SQL 直插 `shop_express_template_detail`(region_mode=2, region_ids=偏远省6位码逗号串, status=0, tenant_id=10606, template_id=11)。已给 INSERT 模板与省码清单。
|
||||
- 注意:黑名单是**租户级、全店生效**(不区分绑定哪个模板);自提(deliveryType=1)/selfTakeMerchantId 下单跳过校验。
|
||||
|
||||
## 补上后台「不发货黑名单」UI 模式(已交付)
|
||||
- 改 `src/views/shop/shopExpressTemplateDetail/components/shopExpressTemplateDetailEdit.vue`:新增「配送模式」三态 radio(0全国/1指定配送/2指定不发货);mode=2 复用 `RegionMultiSelect` 选地区,保存写 region_mode=2 + region_ids(城市码逗号串);回显按 region_mode 初始化。
|
||||
- 改 `src/views/shop/shopExpressTemplateDetail/index.vue`:「配送地区」列按 region_mode 显示「不发货:xxx / 配送:xxx / 默认全国」,城市码聚合为省名显示。
|
||||
- 类型检查(vue-tsc)确认本次改动无新错误(唯一相关报错是预存的 `$router` 用法,原文件即有,未引入)。
|
||||
- 结论:黑名单现可经后台 UI 配置;SQL 直插仍作为备选(二者后台存城市码、SQL 存省份码,后端比对为编码集合 contains,都生效、互不冲突)。
|
||||
- 已生成插偏远八省黑名单完整 SQL:`offline-remote-areas-blacklist-10606.sql`(template_id=11, region_mode=2, region_ids=新疆650000/西藏540000/青海630000/宁夏640000/内蒙古150000/海南460000/黑龙江230000/甘肃620000, status=0, tenant_id=10606),含前置查重 + 后置验证语句。
|
||||
|
||||
## 微信支付报错 APPID_MCHID_NOT_MATCH(tenant 10606 下单失败)
|
||||
|
||||
- 现象:用户下单报 `创建微信支付订单失败:Wrong HttpStatusCode[400] ... APPID_MCHID_NOT_MATCH`。服务商模式 JSAPI,请求体 sp_appid=`wx541db955e7a62709`/sp_mchid=`1720296183`、sub_appid=`wxcad054ee427aaca1`/sub_mchid=`1744894035`。
|
||||
- 根因:传给微信的 `sub_appid` 与 `sub_mchid` 在微信侧不匹配(该子商户应用ID未绑定到该子商户号)。代码无 bug。
|
||||
- 4 个值来源:`gxwebsoft_core.sys_payment`(tenant 10606 微信JSAPI记录)字段 `app_id`/`mch_id`/`sub_app_id`/`sub_mch_id`;拼装在 `ShopOrderServiceImpl.java:809-912`(服务商模式)。
|
||||
- 已验证 `sub_app_id=wxcad054ee427aaca1` 就是 `guilixu-taro` 用户端小程序真实 appid(project.config.json),故 sub_appid 是对的、与买家 openid 一致。
|
||||
- 结论:问题在 `sub_app_id↔sub_mchid` 绑定被破坏——最可能(1)商户平台子商户 `1744894035` 对 appid 的授权被移除/失效;(2)`sys_payment.sub_mch_id` 被误改。
|
||||
- 修复:微信商户平台(服务商 `1720296183` 服务商平台→特约商户管理→开发配置)为子商户 `1744894035` 绑定 appid `wxcad054ee427aaca1`;并 `SELECT` 核对 `sys_payment` 的 sub_mch_id 仍为 `1744894035`。无需改代码,新订单号直接读最新配置。
|
||||
@@ -0,0 +1,26 @@
|
||||
# 2026-08-14 工作日志
|
||||
|
||||
## 修复商品分类层级(10409 → 10421)
|
||||
|
||||
- 问题:guilixu-admin 后台 `/shop/shopGoodsCategory` 页面中,10409「二级分类」与 10421「特别专区」显示为同级。
|
||||
- 原因:前端按 `parentId` 渲染树形层级,说明 10409 的 `parent_id` 不是 10421(实际可能为 0)。
|
||||
- 处理:
|
||||
- 确认后端表为 `shop_goods_category`(guilixu-java / db_guilixu),关键字段 `category_id`、`parent_id`。
|
||||
- 生成修复 SQL:`fix-category-parent-10409.sql`,将 10409 的 `parent_id` 更新为 10421。
|
||||
- 同时提供了后台 UI 的临时操作方式:编辑 10409 把上级分类改为 10421。
|
||||
- 文件:`/Users/gxwebsoft/VUE/guilixu-admin/fix-category-parent-10409.sql`
|
||||
|
||||
## 商品分类编辑弹窗新增「链接地址」字段
|
||||
|
||||
- 需求:在 `shopGoodsCategoryEdit.vue` 的「分类标识」下方新增「链接地址」输入项,存到 `path` 字段。
|
||||
- 改动:`src/views/shop/shopGoodsCategory/components/shopGoodsCategoryEdit.vue`
|
||||
- 在「分类标识」form-item 之后新增「链接地址」项,绑定 `form.path`(form 已有该字段,无需改 model/后端)。
|
||||
- 重置逻辑 Object.assign 补上 `path: undefined`,保证新增时清空。
|
||||
- 注意:仅前端 UI 改动,后端表 `shop_goods_category.path` 字段已存在,无需迁移。
|
||||
|
||||
## 商品分类列表树形连线高度调紧凑
|
||||
|
||||
- 问题:后台 `/shop/shopGoodsCategory` 列表中,树形层级连线 SVG 高度 40px,导致行显得太高、不紧凑。
|
||||
- 改动:`src/views/shop/shopGoodsCategory/index.vue`
|
||||
- `renderTreePrefix` 中 SVG 高度 `H` 从 `40` 改为 `28`,`mid` 自动为 `14`。
|
||||
- 效果:二级/三级分类前的树形引导线高度降低,整行更紧凑。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 2026-08-15 工作日志
|
||||
|
||||
## 专区功能(ShopHomeSection)接口报错修复
|
||||
|
||||
- 现象:访问专区列表接口报 `NoSuchPropertyException: ShopHomeSectionParam.deleted`。
|
||||
- 根因:模板生成的 `ShopHomeSectionParam`(guilixu-java `src/main/java/com/gxwebsoft/shop/param/ShopHomeSectionParam.java`)**漏了 `deleted` 字段**;同项目 Mapper XML(`ShopHomeSectionMapper.xml`)按模板带 `<if test="param.deleted == null"> AND a.deleted = 0</if>` 逻辑删除过滤,OGNL 求值时属性不存在即抛异常。
|
||||
- 对比:`ShopGoodsParam` 有 `deleted` 字段(@QueryField EQ + @Schema),为标准写法;`ShopHomeSection` 实体本身有 `@TableLogic private Integer deleted;`,表启用逻辑删除。
|
||||
- 修复:给 `ShopHomeSectionParam` 末尾补 `private Integer deleted;`(带 @QueryField + @Schema),与模板一致。
|
||||
- 验证:重新编译部署 guilixu-java(端口 9200 本地实例)后接口恢复正常。
|
||||
- ⚠️ 同类预警:其他新生成的 Param 若同样漏 `deleted`,会报一模一样的错,补字段即可。
|
||||
|
||||
## 专区功能菜单未显示(待办)
|
||||
|
||||
- 前端页面 `src/views/special/zone/index.vue` 与 API 层 `src/api/shop/shopZone/` 已完成。
|
||||
- 但因 guilixu-admin 为动态路由(菜单由后端接口拉取),后端菜单表尚未插入「专区管理」记录,故左侧导航看不到入口。
|
||||
- 待处理:在后端菜单表(sys_menu 类)插入菜单记录(component=special/zone,tenant_id=10606),刷新/重登后生效。
|
||||
|
||||
## 专区功能交互入口约定(答疑结论)
|
||||
|
||||
- 白名单用户:专区列表每行「白名单」按钮 → 弹窗勾选用户 → 确定(PUT /{id}/users 覆盖式保存到 shop_home_section_user 表)。前端 UserSelectModal 链路完整。
|
||||
- 商品关联:专区页「商品」按钮只是**查看**该专区下商品(GET /{id}/goods,后端用 FIND_IN_SET(section_id, section_ids) 查询);**往专区加商品不在此页操作**,要去「商品管理 → 编辑商品 → 所属专区(多选)」勾选专区并保存,写入 goods.section_ids。
|
||||
- 后端接口齐全:users/goods 查询与设置、check-permission 权限校验均已实现。
|
||||
- ⚠️ 待确认:白名单弹窗当前按 isStaff=true 查后台员工用户(@/api/system/user 的 pageUsers);若受限专区面向小程序买家,需调整用户来源。
|
||||
@@ -0,0 +1,30 @@
|
||||
# 2026-08-16 工作日志
|
||||
|
||||
## 专区白名单用户弹窗修复
|
||||
|
||||
### 1. 去掉 isStaff 限制
|
||||
- 文件:`src/views/special/zone/components/UserSelectModal.vue`
|
||||
- 改动:`pageUsers` 调用移除 `isStaff: true` 参数,使白名单搜索覆盖全部用户(不再只查后台员工)。
|
||||
|
||||
### 2. 修复勾选框不显示
|
||||
- 现象:弹窗表格数据正常但无 checkbox 勾选列,用户无法选择。
|
||||
- 根因:`rowSelection` 用 `reactive()` 包整个对象的方式在 `ele-pro-table` 中不生效;且缺少 `columnWidth` 导致勾选列无宽度不渲染。
|
||||
- 对比:项目内 `shopCoupon/index.vue` 用 `computed(() => ({ columnWidth, selectedRowKeys, onChange }))` 模式可正常工作;字典页用 `{ columnWidth: 32 }` 最简写法也可。
|
||||
- 修复:
|
||||
- `reactive` → `computed` 返回 rowSelection 对象(与 shopCoupon 一致)
|
||||
- 补 `columnWidth: 48`
|
||||
- `selectedRowKeys` 独立为 `ref<number[]>`,通过 computed 注入 rowSelection
|
||||
- 所有引用点同步更新(模板计数、confirm、watch 初始化)
|
||||
|
||||
## 专区商品抽屉改造为可编辑
|
||||
|
||||
- 需求:专区「商品」按钮打开的抽屉从只读列表升级为支持添加/移除商品。
|
||||
- 后端(guilixu-java `ShopHomeSectionController.java`)新增两个接口:
|
||||
- `POST /{id}/goods` — 添加商品到专区(接收 goodsId[],将 section_id 追加到每个 goods 的 section_ids 逗号串中,用 LinkedHashSet 去重)
|
||||
- `DELETE /{id}/goods` — 从专区移除商品(接收 goodsId[],从 section_ids 中移除该 section_id,空则置 null)
|
||||
- 前端 API(`src/api/shop/shopZone/index.ts`)新增 `addSectionGoods`、`removeSectionGoods`。
|
||||
- 前端 UI(`src/views/special/zone/index.vue`)商品抽屉改造:
|
||||
- 顶部:搜索框 + 「搜索」按钮 + 「全部添加(N)」确认按钮
|
||||
- 搜索结果区:虚线框展示候选商品列表,每行有「+添加」单条操作(自动过滤已在专区内的商品)
|
||||
- 已关联商品表格:每行加「移除」按钮(二次确认后调用 remove 接口)
|
||||
- 数据源:搜索商品复用已有 `pageShopGoods` API(@/api/shop/shopGoods)
|
||||
@@ -0,0 +1,67 @@
|
||||
# 项目长期记忆
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 关键项目路径
|
||||
- **管理后台**:`/Users/gxwebsoft/VUE/guilixu-admin`(Vue3 + Ant Design Vue)
|
||||
- **用户端小程序**:`/Users/gxwebsoft/VUE/guilixu-taro`(Taro + React)
|
||||
- **商家端小程序**:`/Users/gxwebsoft/VUE/xinlong-shop-taro`(Taro + React)
|
||||
|
||||
### ⚠️ 跨项目教训(重要)
|
||||
- 商家端发货/门店/订单相关功能在 **xinlong-shop-taro**,不要改到 guilixu-taro
|
||||
- 用户端订单详情等功能在 **guilixu-taro**
|
||||
- 跨项目改动前,先用 grep/glob 在多个候选项目里确认"哪个项目有该功能源码",再动手
|
||||
- 已发生两次误改 guilixu-taro 的教训(useClerk 全局上下文、订单详情 deliveryType 修复)
|
||||
|
||||
## 业务约定
|
||||
|
||||
### deliveryType 配送方式枚举
|
||||
- `0` = 快递配送
|
||||
- `1` = 无需发货 / 自提
|
||||
- `2` = 商家送货
|
||||
|
||||
来源:管理后台 `src/views/shop/shopOrder/components/deliveryModal.vue`
|
||||
|
||||
## Java 后端架构(重要)
|
||||
|
||||
### 双后端 / MODULES_API_URL 模式(2026-07-25 分析结论)
|
||||
- **mp-java**(`/Users/gxwebsoft/JAVA/mp-java`)= 独立的「modules 服务」,用 `modules` 库;cms(235类)、shop 等业务模块都在它这里,持续高频开发。
|
||||
- **guilixu-java**(`/Users/gxwebsoft/JAVA/guilixu-java`)= 桂礼序主后端,用 `db_guilixu` 库;是 mp-java 的「子集分叉」。
|
||||
- **guilixu-admin 前端已预设双基地址**:`SERVER_API_URL`(主后端) + `MODULES_API_URL`(localStorage ApiUrl / VITE_API_URL)。
|
||||
- ⚠️ **运行时实际指向以 `localStorage ApiUrl` 为准**(2026-08-06 用户纠正):**guilixu-admin 的「商城」接口实际调 `https://shop-api.websoft.top`(= guilixu-java)**,并非 mp-java。**判断"改哪个后端才生效"不能默认 MODULES_API_URL=mp-java,必须查 ApiUrl 运行时配置。** cms 接口另走 `VITE_CMS_API_URL`=cms-api.websoft.top(mp-java)。
|
||||
- **结论(仅限 cms 模块):guilixu-java 里的 cms 包(152类)是 mp-java cms 的旧分叉子集、且前端并不调它(前端调 modules 服务),属重复/死代码,应删除而非复制或扩展。**
|
||||
- 复制「整个 cms 到 guilixu-java」不可行:mp-java 完整 cms 的 CmsApp/CmsWebsiteService 依赖 `project` 模块(guilixu-java 没有),会编译失败。
|
||||
- 注意:mp-java 与 guilixu-java 是独立 git 仓库(`git.websoft.top/gxwebsoft/mp-java.git` 与 `guilixu-java.git`),各自独立部署、独立库。
|
||||
|
||||
### ⚠️ 双后端改动同步规则(2026-08-05 修正)
|
||||
- **cms 模块**改动:仅 mp-java(guilixu-java 的 cms 是死代码)。
|
||||
- **shop 等共享业务模块**(如 `ShopGoodsController` / Service / Mapper / task):**mp-java 与 guilixu-java 都有活代码且结构一致(同 `com.gxwebsoft.shop` 包)**,用户确认改动要**两端同步**(例:2026-08-05 shopGoods `/data` 统计优化,两端各改 6 处)。
|
||||
- 改动前先 grep 两端确认文件是否存在,避免只改一处导致另一端行为不一致。
|
||||
|
||||
### ⚠️ 运费模板不发货地区需求(2026-08-06 结论,已按用户纠正修正)
|
||||
- 需求:运费模板支持「不发货地区(黑名单)」,ShopExpressTemplateDetail 加 regionMode/regionIds 字段 + DeliveryRegionChecker 下单校验。
|
||||
- **后端(guilixu-java)已完整实现**:entity/param/迁移SQL/DeliveryRegionChecker/RegionCodeResolver/OrderBusinessService 接入(第 87 行调用 validateDeliveryRegionIfNeeded)均就位。
|
||||
- **关键事实(用户 2026-08-06 18:09 纠正):guilixu-admin 的「商城」接口实际调 `https://shop-api.websoft.top`(= guilixu-java)**。前端 shopExpressTemplateDetail 用默认 `@/utils/request`,baseURL = MODULES_API_URL = localStorage ApiUrl || VITE_API_URL;本部署的 ApiUrl 即 shop-api.websoft.top/api。故 guilixu-java 是生效后端,其实现正确生效,不是死代码。
|
||||
- **结论:本功能(admin 配置)无需改 mp-java**(本部署 商城不走 mp-java)。
|
||||
- **端到端链路已确认打通(2026-08-12)**:用户端 `guilixu-taro` 的 `BaseUrl = https://shop-api.websoft.top/api`(config/env.js 全环境),`TenantId='10606'`,下单 `POST /shop/shop-order` 即走 guilixu-java(db_guilixu);且单租户下买家 tenantId=10606(微信支付证书也依赖此值),故 `DeliveryRegionChecker` 按 `tenant_id=10606` 能命中黑名单。✅ 功能完整闭环。
|
||||
- **✅ 后台 UI 已支持「不发货黑名单」模式(2026-08-12 完成)**:`shopExpressTemplateDetailEdit.vue` 已加「配送模式」三态 radio(0全国/1指定配送/2指定不发货);mode=2 复用 `RegionMultiSelect` 选地区,保存写 `region_mode=2` + `region_ids`(城市码逗号串);回显按 `region_mode` 初始化。列表页 `index.vue` 的「配送地区」列按 `region_mode` 显示「不发货:xxx / 配送:xxx / 默认全国」,并把城市码聚合为省名。后端比对是编码集合 contains,故后台存城市码与 SQL 存省份码都正确拦截、互不冲突。
|
||||
- 仍可用 SQL 直接 INSERT 黑名单明细(region_mode=2, region_ids=偏远省码, status=0, tenant_id=10606)作为备选,无需经过 UI。
|
||||
- 接口方案文档:`docs/运费模板-不发货地区-接口方案.md`。
|
||||
|
||||
### shopGoods status 字段(上架/下架)
|
||||
- `0` = 已上架 / 上架
|
||||
- `1` = 待上架 / 下架
|
||||
- `2` = 待审核,`3` = 审核不通过
|
||||
- 编辑表单「状态」radio 绑定 `form.status`:0=上架、1=下架。
|
||||
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
|
||||
- 列表 tag 仍显示:0=已上架、1=待上架、2=待审核、3=审核不通过。
|
||||
|
||||
### 运费模板配送地区多选(2026-08-11 已实现前端)
|
||||
- 需求:配送地区选择从「省+市单选级联」升级为「省+市两级多选穿梭弹窗」(仿客户截图双栏:左候选/右已选)。
|
||||
- 前端组件:`src/components/RegionMultiSelect/index.vue`(树形可展开,省级"选择"=选该省全部市,市级可单独选;@update:value 城市编码数组)。
|
||||
- **直接对接后端已有的 `regionMode`(0=全国/1=指定配送白名单/2=不发货黑名单) + `regionIds`(逗号分隔城市编码串) 机制,无需后端改动**。城市编码即 `regions-data.json` 的 city value(6位),与 `regionIds` 完全一致。
|
||||
- 数据字段:model 用 `regionMode` / `regionIds`(已有字段,非新建);旧 `provinceId`/`cityId` 仅作回显兼容。
|
||||
- 列表多值展示:`index.vue` customRender 遍历 `regionIds` 反查 regionMap,超 22 字截断 + tooltip。
|
||||
- 下单校验 `DeliveryRegionChecker` 读 `regionIds` 直接生效(regionMode=2 为不发货黑名单)。
|
||||
- ⚠️ 教训:第一版误用 `provinceIds`/`cityIds` 字段名,与后端 `regionMode`/`regionIds` 不匹配,MyBatis-Plus 自动忽略未知字段导致"没存"。改字段名对接即可,不必新增后端字段。
|
||||
|
||||
@@ -1,84 +1,89 @@
|
||||
<div align="center">
|
||||
<h1>🌐 WebSoftAdmin</h1>
|
||||
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级后台管理系统</strong></p>
|
||||
<h1>🛒 小程序商城后台管理系统</h1>
|
||||
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级小程序商城后台</strong></p>
|
||||
|
||||
<p>
|
||||
<img src="https://img.shields.io/badge/Vue-3.x-4FC08D" alt="Vue">
|
||||
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-3.x-1890FF" alt="Ant Design Vue">
|
||||
<img src="https://img.shields.io/badge/TypeScript-4.x-3178C6" alt="TypeScript">
|
||||
<img src="https://img.shields.io/badge/Vite-4.x-646CFF" alt="Vite">
|
||||
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-4.x-1890FF" alt="Ant Design Vue">
|
||||
<img src="https://img.shields.io/badge/TypeScript-5.x-3178C6" alt="TypeScript">
|
||||
<img src="https://img.shields.io/badge/Vite-8.x-646CFF" alt="Vite">
|
||||
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
|
||||
</p>
|
||||
</div>
|
||||
|
||||
## 📖 项目简介
|
||||
|
||||
WebSoftAdmin 是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级后台管理系统,采用最新的前端技术栈:
|
||||
小程序商城后台管理系统是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级商城管理平台,为小程序商城提供完整的后台管理能力,涵盖商品、订单、会员、经销商、门店、营销活动等核心业务模块。
|
||||
|
||||
- **前端框架**:Vue 3 + TypeScript + Vite
|
||||
- **UI 组件库**:Ant Design Vue 3.x
|
||||
- **富文本编辑器**:TinyMCE(支持图片/视频上传、一键排版)
|
||||
- **图表库**:ECharts + G2
|
||||
- **工具库**:Lodash、Day.js、CryptoJS
|
||||
|
||||
|
||||
|
||||
## 项目演示
|
||||
| 后台管理系统 | https://mp.websoft.top |
|
||||
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 账号密码 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
|
||||
| 关注公众号 |  |
|
||||
|
||||
- **UI 组件库**:Ant Design Vue 4.x + EleAdmin Pro
|
||||
- **状态管理**:Pinia
|
||||
- **图表库**:ECharts + @antv/g2
|
||||
- **工具库**:Lodash、Day.js、CryptoJS、ExcelJS
|
||||
|
||||
**项目域名**:[https://shop.websoft.top](https://shop.websoft.top)
|
||||
|
||||
**官方产品介绍**:[https://websoft.top/shop](https://websoft.top/shop)
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 核心技术
|
||||
|
||||
| 技术 | 版本 | 说明 |
|
||||
|------|------|------|
|
||||
| Vue | 3.x | 渐进式 JavaScript 框架 |
|
||||
| TypeScript | 4.x | JavaScript 的超集 |
|
||||
| Vite | 4.x | 下一代前端构建工具 |
|
||||
| Ant Design Vue | 3.2.11 | 企业级 UI 设计语言 |
|
||||
| EleAdmin Pro | 1.10.1 | 企业级组件库 |
|
||||
| TypeScript | 5.x | JavaScript 的超集 |
|
||||
| Vite | 8.x | 下一代前端构建工具 |
|
||||
| Ant Design Vue | 4.x | 企业级 UI 设计语言 |
|
||||
| EleAdmin Pro | 1.11+ | 企业级组件库 |
|
||||
| Pinia | 2.x | Vue 3 状态管理库 |
|
||||
|
||||
### 功能组件
|
||||
|
||||
- **TinyMCE** - 富文本编辑器,支持图片/视频上传
|
||||
- **ECharts** - 数据可视化图表库
|
||||
- **CropperJS** - 图片裁剪组件
|
||||
- **ExcelJS** - Excel 文件处理
|
||||
- **ExcelJS / xlsx** - Excel 文件导入导出
|
||||
- **Ali OSS** - 阿里云对象存储
|
||||
- **qrcode / jsbarcode** - 二维码与条形码生成
|
||||
|
||||
## 📋 环境要求
|
||||
|
||||
### 基础环境
|
||||
- 🟢 **Node.js 16+**
|
||||
- 📦 **npm 8+ / yarn 1.22+**
|
||||
|
||||
- 🟢 **Node.js 18+**
|
||||
- 📦 **npm 8+ / yarn 1.22+ / pnpm 8+**
|
||||
- 🌐 **现代浏览器**(Chrome 63+、Firefox、Safari、Edge)
|
||||
|
||||
### 开发工具
|
||||
|
||||
- **推荐**:VS Code / WebStorm
|
||||
- **插件**:Vetur / Volar(Vue 3 支持)
|
||||
- **插件**:Volar(Vue 3 支持)
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/websoft-top/paopao-vue.git
|
||||
cd paopao-vue
|
||||
git clone <仓库地址>
|
||||
cd guilixu-admin
|
||||
```
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 使用 npm
|
||||
npm install
|
||||
|
||||
# 或使用 yarn
|
||||
yarn install
|
||||
|
||||
# 或使用 pnpm(推荐)
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
```bash
|
||||
# 复制环境变量示例文件
|
||||
cp .env.example .env
|
||||
@@ -88,6 +93,7 @@ cp .env.example .env
|
||||
```
|
||||
|
||||
### 4. 启动开发服务器
|
||||
|
||||
```bash
|
||||
# 开发模式
|
||||
npm run dev
|
||||
@@ -99,6 +105,7 @@ yarn dev
|
||||
访问 `http://localhost:3000` 即可看到管理后台。
|
||||
|
||||
### 5. 构建生产版本
|
||||
|
||||
```bash
|
||||
# 生产构建
|
||||
npm run build
|
||||
@@ -112,6 +119,7 @@ npm run serve
|
||||
项目使用环境变量来管理敏感信息和配置。请按照以下步骤配置:
|
||||
|
||||
### 必需配置
|
||||
|
||||
```bash
|
||||
# API 配置
|
||||
VITE_API_URL=https://your-api.com/api # 后端 API 地址
|
||||
@@ -124,51 +132,54 @@ VITE_TENANT_ID=your_tenant_id # 租户 ID
|
||||
```
|
||||
|
||||
### 可选配置
|
||||
|
||||
```bash
|
||||
# 高德地图 (如需使用地图功能)
|
||||
VITE_MAP_KEY=your_map_key # 高德地图 Key
|
||||
VITE_MAP_CODE=your_map_security_code # 高德地图安全密钥
|
||||
|
||||
# WebSoftAdmin 授权 (商业版功能)
|
||||
# EleAdmin Pro 授权 (商业版功能)
|
||||
VITE_LICENSE_CODE=your_license_code # 授权码
|
||||
```
|
||||
|
||||
### 获取配置信息
|
||||
- **高德地图密钥**:访问 [高德开放平台](https://lbs.amap.com/) 申请
|
||||
- **WebSoftAdmin 授权码**:联系 [官方网站](https://websoft.top/) 获取
|
||||
- **其他 API 配置**:根据您的后端服务配置
|
||||
|
||||
## 🎯 核心功能
|
||||
|
||||
### 📝 内容管理系统
|
||||
- **文章管理**:支持富文本编辑、图片/视频上传
|
||||
- **一键排版**:智能文章格式优化,包含10种专业排版样式
|
||||
- **媒体库**:图片/视频文件管理,支持分组和搜索
|
||||
- **首行缩进**:中文段落格式智能切换
|
||||
|
||||
### 🛒 商城管理
|
||||
- **商品管理**:商品信息编辑、规格设置
|
||||
- **订单管理**:订单流程跟踪
|
||||
|
||||
- **商品管理**:商品信息编辑、规格设置、SKU 管理、商品分类
|
||||
- **订单管理**:订单流程跟踪、订单商品管理
|
||||
- **库存管理**:商品库存监控
|
||||
- **优惠券管理**:优惠券创建与发放、用户优惠券管理
|
||||
- **营销活动**:活动管理、活动报名、积分商品
|
||||
- **礼品管理**:商城礼品配置
|
||||
|
||||
### 👥 用户权限
|
||||
- **用户管理**:用户信息维护
|
||||
- **角色权限**:基于角色的访问控制
|
||||
### 🏪 门店与经销商
|
||||
|
||||
- **门店管理**:门店信息、门店围栏、门店骑手、门店仓库
|
||||
- **经销商体系**:经销商申请、经销商用户、经销商订单、经销商资金与提现
|
||||
- **分销设置**:经销商推荐人、分销海报、分销配置
|
||||
|
||||
### 👥 会员管理
|
||||
|
||||
- **会员注册**:会员注册管理
|
||||
- **会员权益**:权益包管理、权益兑换
|
||||
- **会员推荐**:推荐人关系管理
|
||||
|
||||
### 👤 用户权限
|
||||
|
||||
- **用户管理**:用户信息维护、用户分组
|
||||
- **角色权限**:基于角色的访问控制(RBAC)
|
||||
- **菜单管理**:动态菜单配置
|
||||
- **组织架构**:组织与部门管理
|
||||
|
||||
## 🎨 富文本编辑器特色功能
|
||||
### ⚙️ 系统管理
|
||||
|
||||
### 📸 媒体上传
|
||||
- **图片上传**:支持拖拽、粘贴、文件选择
|
||||
- **视频上传**:支持多种视频格式
|
||||
- **媒体库**:统一的媒体文件管理
|
||||
- **OSS 存储**:阿里云对象存储集成
|
||||
|
||||
### ✨ 智能排版
|
||||
- **一键排版**:10种专业排版优化
|
||||
- **首行缩进**:中文段落格式切换
|
||||
- **样式优化**:标题、段落、列表、表格等元素美化
|
||||
- **响应式**:适配不同屏幕尺寸
|
||||
- **系统配置**:商城设置、提现配置
|
||||
- **支付管理**:支付配置、订单支付
|
||||
- **字典管理**:数据字典维护
|
||||
- **缓存管理**:缓存清理
|
||||
- **域名管理**:域名白名单配置
|
||||
- **操作记录**:登录记录、操作日志
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
@@ -176,11 +187,17 @@ VITE_LICENSE_CODE=your_license_code # 授权码
|
||||
src/
|
||||
├── components/ # 公共组件
|
||||
├── views/ # 页面组件
|
||||
│ ├── cms/ # 内容管理
|
||||
│ ├── shop/ # 商城管理
|
||||
│ └── system/ # 系统管理
|
||||
├── router/ # 路由配置
|
||||
├── store/ # 状态管理
|
||||
├── utils/ # 工具函数
|
||||
└── assets/ # 静态资源
|
||||
│ ├── shop/ # 商城管理(商品、订单、门店、经销商、会员等)
|
||||
│ ├── system/ # 系统管理(用户、角色、菜单、字典、支付等)
|
||||
│ ├── passport/ # 登录认证
|
||||
│ ├── user/ # 用户中心
|
||||
│ └── result/ # 结果页
|
||||
├── router/ # 路由配置
|
||||
├── store/ # Pinia 状态管理
|
||||
├── utils/ # 工具函数
|
||||
└── assets/ # 静态资源
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
-- ============================================================
|
||||
-- 商品表新增「置顶」字段 is_top,并调整默认排序(方案 B)
|
||||
-- ------------------------------------------------------------
|
||||
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||
-- 1. is_top = 1 表示置顶,0(默认)表示不置顶
|
||||
-- 2. 表名/字段名 snake_case 约定(Node + TypeORM 常见);
|
||||
-- 若你的库列名是驼峰(goodsId / isTop),请自行替换
|
||||
-- 3. 与 recommend(推荐)同为「列表内联一键切换」的布尔开关
|
||||
-- 4. 混合排序思路:置顶的永远在最前;置顶内部按排序号升序;
|
||||
-- 其余商品按创建时间倒序(绝大多数商品无需手动排)
|
||||
-- ============================================================
|
||||
|
||||
-- 第一步:新增字段(已存在则跳过;MySQL 5.7 / 8 通用)
|
||||
ALTER TABLE shop_goods
|
||||
ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶:1置顶 0不置顶';
|
||||
|
||||
-- (可选)联合索引,置顶 + 排序号分页查询更稳
|
||||
-- ALTER TABLE shop_goods
|
||||
-- ADD INDEX idx_is_top_sort (is_top, sort_number);
|
||||
|
||||
-- 第二步:调整分页查询(page 接口)默认排序
|
||||
-- 原(示例):ORDER BY create_time DESC
|
||||
-- 改: ORDER BY is_top DESC, sort_number ASC, create_time DESC
|
||||
-- 说明:若后端排序字段来自前端 orders 参数,需放开 is_top / sort_number 参与排序
|
||||
-- (前端「置顶」「排序号」列已开启 sorter,会向后端传 orders)
|
||||
|
||||
-- 第三步(验证):确认字段已加、数据默认 0,且排序符合预期
|
||||
SELECT goods_id,
|
||||
name,
|
||||
is_top,
|
||||
sort_number,
|
||||
create_time
|
||||
FROM shop_goods
|
||||
ORDER BY is_top DESC, sort_number ASC, create_time DESC
|
||||
LIMIT 20;
|
||||
@@ -0,0 +1,73 @@
|
||||
# 桂礼序 CMS 模块复用方案分析
|
||||
|
||||
> 分析对象:`mp-java`(modules 服务,cms 235 类) vs `guilixu-java`(桂礼序主后端,cms 152 类)
|
||||
> 分析日期:2026-07-25
|
||||
|
||||
## 一、现状事实(已核实)
|
||||
|
||||
| 维度 | mp-java | guilixu-java |
|
||||
|------|---------|--------------|
|
||||
| 形态 | 单体 Spring Boot 应用(单 pom,@MapperScan `com.gxwebsoft.**`) | 同左 |
|
||||
| cms 类数 | **235**(controller 37) | **152**(controller 25) |
|
||||
| cms 数据库 | `modules` 库 | `db_guilixu`(prod/dev)、`modules`(test) |
|
||||
| cms 建表/迁移 | 由 `src/main/java/com/gxwebsoft/cms/sql/` + `docs/sql` 管理,高频更新 | `sql/` 目录**无 cms 建表脚本** |
|
||||
| git 引入时间 | 2025-07-24("2.0版本分离"),持续开发 | 2026-06-01("桂礼序"),已落后 ~1.5 个月 |
|
||||
| 最近提交 | 2026-07-24/25(仍在改 cms) | 最近为 shop/order 相关,cms 停滞 |
|
||||
|
||||
**关键证据:**
|
||||
|
||||
1. **guilixu-java 的 152 个 cms 类,全部能在 mp-java 的 cms 里找到同名类**;mp-java 额外多出 83 个(CmsApp / CmsBanner / CmsCase 等系列)。→ guilixu 的 cms 是 mp-java cms 的**子集分叉**。
|
||||
2. **前端(guilixu-admin)的 cms 接口早已使用 `MODULES_API_URL`**,路径如 `MODULES_API_URL + '/cms/cms-ad/page'`。`MODULES_API_URL` = `localStorage.ApiUrl || VITE_API_URL`,即指向 modules 服务(mp-java)。shop 的商户/优惠券接口同样走 `MODULES_API_URL`。→ **前端本就假定 cms/shop 由 mp-java 提供,guilixu-java 自己的 cms 包前端并不调用(重复/死代码)。**
|
||||
3. **mp-java 与 guilixu-java 的 cms 控制器路径完全一致**(`/api/cms/cms-ad`、`/api/cms/cms-article-like` …),说明两者是同一套接口契约。
|
||||
4. **完整复制不可编译**:mp-java 完整 cms 的 `CmsApp*`/`CmsWebsiteService*` 依赖 `com.gxwebsoft.project`(Project/ProjectService),而 guilixu-java **没有 project 模块**。把 235 类整体搬过去会编译失败。
|
||||
|
||||
## 二、方案对比
|
||||
|
||||
### 方案 A:guilixu 直接调用 mp-java 的 cms(modules 服务作为 cms 提供方)
|
||||
|
||||
**优点**
|
||||
- ✅ 单一事实来源:cms 逻辑只维护一份,guilixu 永远拿到最新功能(含 83 个新增类)。
|
||||
- ✅ 前端几乎零改动:guilixu-admin 的 cms 接口**已经**指向 `MODULES_API_URL`,契约路径与 mp-java 一致。
|
||||
- ✅ 消除重复:可删除 guilixu-java 里 152 个冗余 cms 类,降低维护与同步负担。
|
||||
- ✅ cms 数据/建表/迁移由 mp-java 集中管理(modules 库),guilixu 无需自建 cms 表。
|
||||
- ✅ 符合现有"主后端 + modules 服务"双基地址架构(shop 已实现)。
|
||||
|
||||
**缺点 / 风险**
|
||||
- ⚠️ 运行期耦合:guilixu 的 cms 功能可用性依赖 mp-java 在线(mp-java 宕机/升级 → cms 不可用)。
|
||||
- ⚠️ 跨库一致:cms 在 `modules`,guilixu 业务在 `db_guilixu`;若需"同事务写 cms+shop"无法用本地事务,需走事件/Saga。
|
||||
- ⚠️ 鉴权边界:mp-java 与 guilixu 各自用户体系(不同库),需确保 modules 服务对 guilixu-admin 的 cms 调用有合法鉴权(现有 TENANT_ID/APP_SECRET/ApiUrl 机制应已覆盖)。
|
||||
- ⚠️ 接口微小差异:guilixu 是旧分叉,个别字段/路径若 mp-java 已重构,需对一遍(如 CmsWebsite 已删手机号脱敏方法)。
|
||||
|
||||
### 方案 B:把 cms 模块整个复制到 guilixu-java
|
||||
|
||||
**优点**
|
||||
- ✅ 自包含:cms 随 guilixu-java 独立运行,不依赖 mp-java 在线。
|
||||
- ✅ 接口契约稳定:沿用现有 guilixu-java cms 路径,前端无需改 baseURL。
|
||||
- ✅ 可做 guilixu 专属深度定制,不受 mp-java 限制。
|
||||
|
||||
**缺点 / 风险**
|
||||
- ❌ **语义上"整个复制"不可行**:完整 235 类依赖 guilixu 没有的 `project` 模块,编译失败;只能裁剪到子集,等于失去"完整"意义。
|
||||
- ❌ 固化分叉:现在已落后 83 类/1.5 个月;mp-java cms 仍高频演进,复制后会陷入"复制→分叉→再复制"循环,双份维护成本最高。
|
||||
- ❌ 数据迁移:需把 cms 表从 `modules` 迁到 `db_guilixu`,并自补建表/迁移脚本(目前 guilixu 无 cms 建表脚本)。
|
||||
- ❌ 与前端现状冲突:前端 cms 调的是 `MODULES_API_URL`(mp-java),复制后这套 guilixu cms 实际无人调用(死代码),反而制造两套真相。
|
||||
|
||||
### (补充)方案 C:抽独立 cms 共享库/微服务(长期最优,成本高)
|
||||
把 cms 抽成 `cms-spring-boot-starter`(或独立 cms 微服务 + 客户端 SDK),mp-java 与 guilixu-java 都依赖。能根治重复与分叉,但需重构打包、版本发布流程,短期落地成本高于 A。
|
||||
|
||||
## 三、结论与落地建议
|
||||
|
||||
**结论:选方案 A(guilixu 直接调用 mp-java 的 cms),不要整体复制到 guilixu-java。**
|
||||
|
||||
理由(按重要性):
|
||||
1. 前端早已按"modules 服务(mp-java)提供 cms"设计(cms 接口全走 `MODULES_API_URL`,路径契约一致)——方案 A 是顺延既有架构,方案 B 反而与现状冲突。
|
||||
2. guilixu 的 cms 是 mp-java 的旧分叉子集且前端不调它,复制/扩展只会把分叉永久化;"整体复制"还因缺 `project` 模块无法编译。
|
||||
3. cms 数据、建表、迁移本就在 mp-java(modules 库)集中管理,远程调用天然复用。
|
||||
|
||||
**落地步骤建议:**
|
||||
1. 确认 `guilixu-admin` 的 `MODULES_API_URL` / `localStorage.ApiUrl` 指向 mp-java 部署实例(modules 服务),cms 调用即生效(前端基本不用动)。
|
||||
2. **删除 guilixu-java 中重复的 `com/gxwebsoft/cms` 包(152 类)**,消除死代码与未来同步负担。
|
||||
3. 对一遍 mp-java 与 guilixu 旧 cms 的接口字段差异(重点 CmsWebsite 等已重构类),确保前端无误。
|
||||
4. 明确可用性边界:在运维/部署文档标注"cms 依赖 modules 服务",并为 mp-java 配置高可用/健康检查。
|
||||
5. 若后续确实需要 guilixu 专属、与自身业务同事务的 cms 能力,再走方案 C(抽共享库),不要走方案 B。
|
||||
|
||||
> 备注:若你的真实诉求是"让 guilixu 完全独立、不依赖 mp-java 在线",则需在方案 B 基础上额外补 `project` 模块依赖 + cms 表迁移到 `db_guilixu` + 长期同步机制,综合成本最高,除非有强隔离需求否则不推荐。
|
||||
@@ -0,0 +1,129 @@
|
||||
# 运费模板「不发货地区(排除地区)」接口方案
|
||||
|
||||
> 目标:支持商家配置"除部分偏远地区外,全国均可配送"的场景,避免逐个添加可配送省份。
|
||||
> 前端代码库:guilixu-admin(shop 模块接口走 MODULES_API_URL,即 mp-java 的 shop 模块)
|
||||
|
||||
---
|
||||
|
||||
## 一、现状(改造前)
|
||||
|
||||
- 一条 `shop_express_template_detail` = 一种配送区域 + 一套运费规则。
|
||||
- 当前**只能选 1 个省市组合**,字段为 `province_id` + `city_id`(单值)。
|
||||
- 不选 = 默认全国。
|
||||
- **无任何排除/不发货逻辑**。
|
||||
|
||||
## 二、需求(改造后)
|
||||
|
||||
配送区域支持两种模式:
|
||||
|
||||
| 模式 | 含义 | 典型场景 |
|
||||
|------|------|----------|
|
||||
| 白名单(指定配送) | 只发列表中的地区 | 仅发江浙沪 |
|
||||
| 黑名单(指定不发货) | 发列表外的所有地区 | 除新疆/西藏/港澳台外全国发 |
|
||||
|
||||
新增「不发货地区」配置:商家框选不发货的省/市,保存后这些地区下单时报"暂不配送"。
|
||||
|
||||
## 三、数据库变更
|
||||
|
||||
`shop_express_template_detail` 表新增两个字段(存储方案选型见下方说明):
|
||||
|
||||
> **存储方案选型:推荐逗号分隔单字段,不新建关联表。**
|
||||
> 理由:① 本业务查询模式是「模板→明细」,非「地区→模板」,无需按地区反查;② 排除地区通常仅几个省,VARCHAR(2000) 足够;③ 改动最小、前端直接透传编码字符串、可快速上线满足客户急迫配置需求。若未来需「按地区统计运费模板」等报表,再迁关联表。
|
||||
|
||||
```sql
|
||||
ALTER TABLE shop_express_template_detail
|
||||
ADD COLUMN region_mode TINYINT NOT NULL DEFAULT 0 COMMENT '配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单)',
|
||||
ADD COLUMN region_ids VARCHAR(2000) DEFAULT NULL COMMENT '地区编码列表(与regions-data.json一致),逗号分隔,如 110000,120000。region_mode=1或2时有效';
|
||||
```
|
||||
|
||||
- `region_mode=0`(全国):`region_ids` 为空,兼容旧"不选=全国"行为。
|
||||
- `region_mode=1/2`:`region_ids` 存选中的地区编码(省/市/区 6 位编码,与前端 `regions-data.json` 的 `value` 一致)。
|
||||
|
||||
### 旧数据兼容
|
||||
|
||||
历史数据 `province_id` / `city_id` 保留不删。迁移建议:
|
||||
- 旧数据 `region_mode` 默认置 `1`,`region_ids` = `province_id[,city_id]`(有 city 才拼)。
|
||||
- 前端编辑时若检测到旧字段(无 `region_mode`),按 `province_id/city_id` 回显到白名单模式。
|
||||
|
||||
## 四、region_ids 存储与匹配规则
|
||||
|
||||
- 存**最小选中粒度**编码:用户选到省只存省码,选到市存市码(省码可推导,不必冗余存)。
|
||||
- 编码前缀关系:区码 `110101` ⊃ 市码 `110100` ⊃ 省码 `110000`。
|
||||
- 匹配算法(下单时使用):
|
||||
- 收货地址解析出 省码 / 市码 / 区码。
|
||||
- 命中判断:`region_ids` 中包含收货地的 省码 **或** 市码 **或** 区码 即视为命中该条明细。
|
||||
- 推荐:前端提交时统一存**省+市两级**(不存区),简化逻辑;如需区县级排除再加。
|
||||
|
||||
## 五、下单配送校验逻辑(后端 + 小程序端双重校验)
|
||||
|
||||
```
|
||||
收货地址 → 解析 {provinceCode, cityCode, districtCode}
|
||||
|
||||
1. 查该商品运费模板下所有 detail 明细
|
||||
2. 遍历所有 region_mode=2(黑名单) 明细:
|
||||
若 收货地省/市/区码 任一 ∈ 该明细.region_ids
|
||||
→ 直接判定【不配送】,返回 "该地区暂不配送"
|
||||
3. 若无黑名单命中,按现有逻辑选运费明细:
|
||||
- region_mode=0(全国) 明细 → 匹配
|
||||
- region_mode=1(白名单) 明细 → 收货地码 ∈ region_ids 才匹配
|
||||
4. 无任何明细匹配 → 不配送(或走默认模板,按业务定)
|
||||
```
|
||||
|
||||
> 优先级:**黑名单排除 > 白名单/全国匹配**。即一旦落入不发货列表,直接不可下单。
|
||||
|
||||
## 六、接口变更
|
||||
|
||||
涉及接口(mp-java `ShopExpressTemplateDetailController`):
|
||||
|
||||
| 接口 | METHOD & PATH | 变更 |
|
||||
|------|---------------|------|
|
||||
| 新增明细 | POST `/shop/shop-express-template-detail` | 入参加 `regionMode`、`regionIds` |
|
||||
| 修改明细 | PUT `/shop/shop-express-template-detail` | 入参加 `regionMode`、`regionIds` |
|
||||
| 明细详情 | GET `/shop/shop-express-template-detail/{id}` | 出参加 `regionMode`、`regionIds` |
|
||||
| 明细列表 | GET `/shop/shop-express-template-detail/page` | 出参加 `regionMode`、`regionIds` |
|
||||
|
||||
入参示例(新增):
|
||||
```json
|
||||
{
|
||||
"templateId": 12,
|
||||
"type": true,
|
||||
"regionMode": 2,
|
||||
"regionIds": "650000,540000,810000,820000,710000",
|
||||
"firstNum": "1",
|
||||
"firstAmount": "0",
|
||||
"extraNum": "1",
|
||||
"extraAmount": "0",
|
||||
"status": 0,
|
||||
"sortNumber": 100
|
||||
}
|
||||
```
|
||||
|
||||
## 七、需后端确认的问题 —— 已确认(2026-08-06 后端已实现)
|
||||
|
||||
> 落地仓库:**guilixu-java**(不是 mp-java;两边 shop 模块原本字节一致,本次只改 guilixu-java)。
|
||||
|
||||
| # | 问题 | 结论 |
|
||||
|---|------|------|
|
||||
| 1 | 编码 vs 名称 | **存编码**。6 位行政区划码,与 `regions-data.json` 的 `value` 一致。 |
|
||||
| 2 | 粒度 | **省/市/区三级都支持**。前端建议只提交省+市两级;后端匹配时省/市/区任一命中即算命中。 |
|
||||
| 3 | 黑白名单优先级 | **确认:黑名单命中即不可下单**,优先级高于白名单/全国。允许同一模板下多种模式共存。 |
|
||||
| 4 | 旧数据迁移 | **由后端 SQL 一次性迁移**,见 `sql/shop_express_template_detail_region.sql`。经核查现网 `province_id/city_id` 存的就是 6 位区划码(如 150000/150200),可直接拼接,无需映射表。 |
|
||||
| 5 | 下单校验落点 | **后端统一做**,已接入 `OrderBusinessService.createOrder()` 第 3.2 步,防绕过。小程序端可另做前置提示。 |
|
||||
| 6 | 多模板 | **按租户维度跨模板取并集**。因 `shop_goods` 目前未与运费模板建立关联字段,按"本店除偏远地区外全国发货"的语义处理;后续商品绑定模板后可加 `templateId` 过滤。 |
|
||||
|
||||
### 后端实现要点(前端需知)
|
||||
|
||||
- **新增字段**:`regionMode`(Integer) + `regionIds`(String),已贯通 page / list / get / save / update 全部接口,直接传即可。
|
||||
- **`regionIds` 格式**:逗号分隔的 6 位编码字符串,如 `"650000,540000,810000,820000,710000"`。
|
||||
- **`regionMode` 取值**:`0`=全国、`1`=指定配送(白名单)、`2`=指定不发货(黑名单)。**默认 0**。
|
||||
- **列表筛选**:`regionMode` 支持精确筛选;`regionIds` 支持模糊包含筛选。
|
||||
- **旧数据回显**:迁移 SQL 已把历史行刷成 `regionMode=1` + `regionIds`,前端**无需**再兼容 `province_id/city_id` 回显逻辑。
|
||||
- **地址名称→编码**:后端 `RegionCodeResolver` 负责把 `shop_user_address` 里的省/市/区**名称**翻译成编码(该表只存名称不存编码)。已处理 `新疆维吾尔自治区`↔`新疆`、`北京市`↔`北京` 等后缀归一化。
|
||||
- **拦截文案**:命中时后端抛 `BusinessException`,接口返回形如 `"新疆维吾尔自治区暂不配送,请更换收货地址"`,前端直接展示 message 即可。
|
||||
- **放行兜底**:地址解析不出行政区划编码时(境外地址等)**放行不拦截**,避免误伤。
|
||||
|
||||
> ⚠️ 上线顺序:必须**先执行** `sql/shop_express_template_detail_region.sql`,再发布后端,否则查询会报未知列。
|
||||
|
||||
---
|
||||
|
||||
*前端侧对应改造:新建 `RegionsTransfer` 双栏选择组件 + 改造 `shopExpressTemplateDetailEdit.vue` 增加模式切换 + 列表回显。*
|
||||
+1
-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>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- ============================================================
|
||||
-- 下架「分类ID = 10332 且 没有图片」的商品
|
||||
-- ------------------------------------------------------------
|
||||
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||
-- 1. 下架 = status = 1(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio)
|
||||
-- 2. 「没有图片」以封面图 image 是否为空判断(封面图为必填项)
|
||||
-- 3. 已删除商品(deleted = 1)不参与,避免误改
|
||||
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
|
||||
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
|
||||
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
|
||||
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
|
||||
-- ============================================================
|
||||
|
||||
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
|
||||
SELECT goods_id,
|
||||
name,
|
||||
image,
|
||||
status
|
||||
FROM shop_goods
|
||||
WHERE category_id = 10332
|
||||
AND (image IS NULL OR image = '')
|
||||
AND (deleted = 0 OR deleted IS NULL);
|
||||
|
||||
-- 第二步:确认无误后执行下架
|
||||
UPDATE shop_goods
|
||||
SET status = 1,
|
||||
update_time = NOW()
|
||||
WHERE category_id = 10332
|
||||
AND (image IS NULL OR image = '')
|
||||
AND (deleted = 0 OR deleted IS NULL);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- (可选)若你认为「没有图片」应同时要求轮播图也为空,
|
||||
-- 即封面图和轮播图都没有才算没图,使用下面更严格的条件:
|
||||
-- AND (image IS NULL OR image = '')
|
||||
-- AND (files IS NULL OR files = '' OR files = '[]')
|
||||
-- ------------------------------------------------------------
|
||||
@@ -0,0 +1,32 @@
|
||||
-- ============================================================
|
||||
-- 偏远地区不发货黑名单(租户 10606,全店生效)
|
||||
-- ============================================================
|
||||
-- 作用:下单时 guilixu-java 的 DeliveryRegionChecker 按 tenant_id=10606
|
||||
-- 取 region_mode=2 且 status=0 的明细,将收货地址行政区划码与
|
||||
-- region_ids 比对,命中即拦截并提示「XX暂不配送,请更换收货地址」。
|
||||
-- 注意:黑名单是租户级、全店生效,不区分商品绑定的运费模板。
|
||||
-- 自提/无需发货(deliveryType=1) 不参与此校验。
|
||||
-- 省份码(与 regions-data.json 完全一致):
|
||||
-- 新疆650000 西藏540000 青海630000 宁夏640000 内蒙古150000
|
||||
-- 海南460000 黑龙江230000 甘肃620000
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 先确认是否已有黑名单(避免重复插入)
|
||||
SELECT id, template_id, region_mode, region_ids, status, tenant_id
|
||||
FROM shop_express_template_detail
|
||||
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
|
||||
|
||||
-- 2) 若上一步无记录,再执行插入(template_id=11 即「偏远地区不发」模板)
|
||||
INSERT INTO shop_express_template_detail
|
||||
(template_id, type, province_id, city_id, region_mode, region_ids,
|
||||
first_num, first_amount, extra_amount, extra_num, status, deleted,
|
||||
tenant_id, create_time, update_time, sort_number)
|
||||
VALUES
|
||||
(11, 0, NULL, NULL, 2,
|
||||
'650000,540000,630000,640000,150000,460000,230000,620000',
|
||||
0, 0, 0, 0, 0, 0, 10606, NOW(), NOW(), 0);
|
||||
|
||||
-- 3) 验证(应能看到刚插入的这条,且 region_ids 非空)
|
||||
SELECT id, template_id, region_mode, region_ids, status, tenant_id
|
||||
FROM shop_express_template_detail
|
||||
WHERE tenant_id = 10606 AND region_mode = 2 AND status = 0;
|
||||
@@ -0,0 +1,38 @@
|
||||
-- ============================================================
|
||||
-- 批量上架「分类ID = 10332 且 含有图片」的商品
|
||||
-- ------------------------------------------------------------
|
||||
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin):
|
||||
-- 1. 上架 = status = 0(项目约定:0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio)
|
||||
-- 2. 「含有图片」以封面图 image 是否非空判断(image IS NOT NULL AND image <> '')
|
||||
-- 3. 已删除商品(deleted = 1)不参与,避免误改
|
||||
-- 4. 表名/字段名按 snake_case 约定(Node + TypeORM 常见);
|
||||
-- 若你的库列名是驼峰(goodsId / categoryId),请自行替换
|
||||
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
|
||||
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
|
||||
-- ============================================================
|
||||
|
||||
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE)
|
||||
SELECT goods_id,
|
||||
name,
|
||||
image,
|
||||
status
|
||||
FROM shop_goods
|
||||
WHERE category_id = 10332
|
||||
AND image IS NOT NULL
|
||||
AND image <> ''
|
||||
AND (deleted = 0 OR deleted IS NULL);
|
||||
|
||||
-- 第二步:确认无误后执行上架
|
||||
UPDATE shop_goods
|
||||
SET status = 0,
|
||||
update_time = NOW()
|
||||
WHERE category_id = 10332
|
||||
AND image IS NOT NULL
|
||||
AND image <> ''
|
||||
AND (deleted = 0 OR deleted IS NULL);
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- (可选)若只想把「当前已下架」的商品重新上架,
|
||||
-- 而不动「待审核(2) / 审核不通过(3)」的商品,加上 status 条件:
|
||||
-- AND status = 1
|
||||
-- ------------------------------------------------------------
|
||||
@@ -32,6 +32,8 @@
|
||||
"@bytemd/plugin-gfm": "^1.17.2",
|
||||
"@bytemd/plugin-highlight": "^1.17.4",
|
||||
"@bytemd/plugin-highlight-ssr": "^1.20.2",
|
||||
"@wangeditor/editor": "^5",
|
||||
"@wangeditor/editor-for-vue": "^5",
|
||||
"@wecom/jssdk": "^1.3.1",
|
||||
"ali-oss": "^6.18.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
@@ -53,6 +55,7 @@
|
||||
"js-md5": "^0.7.3",
|
||||
"jsbarcode": "^3.11.5",
|
||||
"lodash-es": "^4.17.21",
|
||||
"markdown-it": "^14",
|
||||
"md-editor-v3": "^5.2.3",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -73,6 +76,7 @@
|
||||
"@types/dompurify": "^3.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.15.0",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
|
||||
Generated
+395
@@ -34,6 +34,12 @@ importers:
|
||||
'@bytemd/plugin-highlight-ssr':
|
||||
specifier: ^1.20.2
|
||||
version: 1.22.0(bytemd@1.22.0)
|
||||
'@wangeditor/editor':
|
||||
specifier: ^5
|
||||
version: 5.1.23
|
||||
'@wangeditor/editor-for-vue':
|
||||
specifier: ^5
|
||||
version: 5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.3(typescript@5.9.3))
|
||||
'@wecom/jssdk':
|
||||
specifier: ^1.3.1
|
||||
version: 1.4.5
|
||||
@@ -97,6 +103,9 @@ importers:
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
markdown-it:
|
||||
specifier: ^14
|
||||
version: 14.1.0
|
||||
md-editor-v3:
|
||||
specifier: ^5.2.3
|
||||
version: 5.8.5(vue@3.5.3(typescript@5.9.3))
|
||||
@@ -152,6 +161,9 @@ importers:
|
||||
'@types/lodash-es':
|
||||
specifier: ^4.17.6
|
||||
version: 4.17.12
|
||||
'@types/markdown-it':
|
||||
specifier: ^14.1.2
|
||||
version: 14.1.2
|
||||
'@types/node':
|
||||
specifier: ^22.15.0
|
||||
version: 22.19.18
|
||||
@@ -1371,6 +1383,9 @@ packages:
|
||||
'@socket.io/component-emitter@3.1.2':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
'@transloadit/prettier-bytes@0.0.7':
|
||||
resolution: {integrity: sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==}
|
||||
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||
|
||||
@@ -1444,6 +1459,9 @@ packages:
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
'@types/event-emitter@0.3.5':
|
||||
resolution: {integrity: sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==}
|
||||
|
||||
'@types/file-saver@2.0.7':
|
||||
resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==}
|
||||
|
||||
@@ -1566,6 +1584,23 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
resolution: {integrity: sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==}
|
||||
|
||||
'@uppy/core@2.3.4':
|
||||
resolution: {integrity: sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==}
|
||||
|
||||
'@uppy/store-default@2.1.1':
|
||||
resolution: {integrity: sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==}
|
||||
|
||||
'@uppy/utils@4.1.3':
|
||||
resolution: {integrity: sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==}
|
||||
|
||||
'@uppy/xhr-upload@2.1.3':
|
||||
resolution: {integrity: sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.3.3
|
||||
|
||||
'@vavt/copy2clipboard@1.0.3':
|
||||
resolution: {integrity: sha512-HtG48r2FBYp9eRvGB3QGmtRBH1zzRRAVvFbGgFstOwz4/DDaNiX0uZc3YVKPydqgOav26pibr9MtoCaWxn7aeA==}
|
||||
|
||||
@@ -1654,6 +1689,93 @@ packages:
|
||||
'@vue/shared@3.5.34':
|
||||
resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==}
|
||||
|
||||
'@wangeditor/basic-modules@1.1.7':
|
||||
resolution: {integrity: sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/code-highlight@1.0.3':
|
||||
resolution: {integrity: sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/core@1.1.19':
|
||||
resolution: {integrity: sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.1.1
|
||||
'@uppy/xhr-upload': ^2.0.3
|
||||
dom7: ^3.0.0
|
||||
is-hotkey: ^0.2.0
|
||||
lodash.camelcase: ^4.3.0
|
||||
lodash.clonedeep: ^4.5.0
|
||||
lodash.debounce: ^4.0.8
|
||||
lodash.foreach: ^4.5.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
lodash.toarray: ^4.4.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12':
|
||||
resolution: {integrity: sha512-0Ds3D8I+xnpNWezAeO7HmPRgTfUxHLMd9JKcIw+QzvSmhC5xUHbpCcLU+KLmeBKTR/zffnS5GQo6qi3GhTMJWQ==}
|
||||
peerDependencies:
|
||||
'@wangeditor/editor': '>=5.1.0'
|
||||
vue: ^3.0.5
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
resolution: {integrity: sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==}
|
||||
|
||||
'@wangeditor/list-module@1.0.5':
|
||||
resolution: {integrity: sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/table-module@1.1.4':
|
||||
resolution: {integrity: sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==}
|
||||
peerDependencies:
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/upload-image-module@1.0.2':
|
||||
resolution: {integrity: sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.0.3
|
||||
'@uppy/xhr-upload': ^2.0.3
|
||||
'@wangeditor/basic-modules': 1.x
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.foreach: ^4.5.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wangeditor/video-module@1.1.4':
|
||||
resolution: {integrity: sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==}
|
||||
peerDependencies:
|
||||
'@uppy/core': ^2.1.4
|
||||
'@uppy/xhr-upload': ^2.0.7
|
||||
'@wangeditor/core': 1.x
|
||||
dom7: ^3.0.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
|
||||
'@wecom/jssdk@1.4.5':
|
||||
resolution: {integrity: sha512-1SSmcDUid7QOQNNFaM4d884NXJQYuoO+oLZtM9TRO5uMXYj2s69hmuAoPUr3D3br6VDgv4H+tbIG5A5A+uB9lA==}
|
||||
|
||||
@@ -2256,6 +2378,9 @@ packages:
|
||||
dom-scroll-into-view@2.0.1:
|
||||
resolution: {integrity: sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==}
|
||||
|
||||
dom7@3.0.0:
|
||||
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
|
||||
|
||||
dompurify@3.4.5:
|
||||
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
|
||||
|
||||
@@ -2728,6 +2853,9 @@ packages:
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
i18next@20.6.1:
|
||||
resolution: {integrity: sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2751,6 +2879,9 @@ packages:
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
immer@9.0.21:
|
||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2809,6 +2940,9 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-hotkey@0.2.0:
|
||||
resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==}
|
||||
|
||||
is-number@7.0.0:
|
||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
@@ -2825,9 +2959,16 @@ packages:
|
||||
resolution: {integrity: sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-plain-object@5.0.0:
|
||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-type-of@1.4.0:
|
||||
resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==}
|
||||
|
||||
is-url@1.2.4:
|
||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||
|
||||
is-what@3.14.1:
|
||||
resolution: {integrity: sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==}
|
||||
|
||||
@@ -3024,6 +3165,12 @@ packages:
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
lodash.camelcase@4.3.0:
|
||||
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
|
||||
|
||||
lodash.clonedeep@4.5.0:
|
||||
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
|
||||
|
||||
lodash.debounce@4.0.8:
|
||||
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
|
||||
|
||||
@@ -3039,6 +3186,9 @@ packages:
|
||||
lodash.flatten@4.4.0:
|
||||
resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==}
|
||||
|
||||
lodash.foreach@4.5.0:
|
||||
resolution: {integrity: sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==}
|
||||
|
||||
lodash.groupby@4.6.0:
|
||||
resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==}
|
||||
|
||||
@@ -3064,6 +3214,12 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash.throttle@4.1.1:
|
||||
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
|
||||
|
||||
lodash.toarray@4.4.0:
|
||||
resolution: {integrity: sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==}
|
||||
|
||||
lodash.union@4.6.0:
|
||||
resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==}
|
||||
|
||||
@@ -3278,6 +3434,9 @@ packages:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-match@1.0.2:
|
||||
resolution: {integrity: sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3332,6 +3491,9 @@ packages:
|
||||
mz@2.7.0:
|
||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||
|
||||
namespace-emitter@2.0.1:
|
||||
resolution: {integrity: sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==}
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
@@ -3584,6 +3746,14 @@ packages:
|
||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
preact@10.29.8:
|
||||
resolution: {integrity: sha512-ej2aVZ+vZ8WO7tvlQWRM9N63A0KzF9q4mWJfDUHgYaIofWY9hu74QdnQrjoPMmZi2/nZ5gN0bJCQF49xQqx09Q==}
|
||||
peerDependencies:
|
||||
preact-render-to-string: '>=5'
|
||||
peerDependenciesMeta:
|
||||
preact-render-to-string:
|
||||
optional: true
|
||||
|
||||
prelude-ls@1.2.1:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -3597,6 +3767,10 @@ packages:
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
prismjs@1.30.0:
|
||||
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
@@ -3841,6 +4015,18 @@ packages:
|
||||
simple-swizzle@0.2.4:
|
||||
resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==}
|
||||
|
||||
slate-history@0.66.0:
|
||||
resolution: {integrity: sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==}
|
||||
peerDependencies:
|
||||
slate: '>=0.65.3'
|
||||
|
||||
slate@0.72.8:
|
||||
resolution: {integrity: sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==}
|
||||
|
||||
snabbdom@3.6.4:
|
||||
resolution: {integrity: sha512-VmxEfuw1/Y/eFj5VtMhYnukExpYiPkNzoo3+N3qwAOUDMl8wXgbli5ebR+j0knE3lZ/0eYskLxNcX64uy10N9w==}
|
||||
engines: {node: '>=12.17.0'}
|
||||
|
||||
socket.io-client@4.8.1:
|
||||
resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -3881,6 +4067,9 @@ packages:
|
||||
resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==}
|
||||
engines: {node: '>=0.8'}
|
||||
|
||||
ssr-window@3.0.0:
|
||||
resolution: {integrity: sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==}
|
||||
|
||||
statuses@1.5.0:
|
||||
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -3973,6 +4162,9 @@ packages:
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
tiny-warning@1.0.3:
|
||||
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -4311,6 +4503,9 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wildcard@1.1.2:
|
||||
resolution: {integrity: sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==}
|
||||
|
||||
win-release@1.1.1:
|
||||
resolution: {integrity: sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5993,6 +6188,8 @@ snapshots:
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@transloadit/prettier-bytes@0.0.7': {}
|
||||
|
||||
'@tybys/wasm-util@0.10.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -6060,6 +6257,8 @@ snapshots:
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/event-emitter@0.3.5': {}
|
||||
|
||||
'@types/file-saver@2.0.7': {}
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
@@ -6211,6 +6410,37 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@uppy/companion-client@2.2.2':
|
||||
dependencies:
|
||||
'@uppy/utils': 4.1.3
|
||||
namespace-emitter: 2.0.1
|
||||
|
||||
'@uppy/core@2.3.4':
|
||||
dependencies:
|
||||
'@transloadit/prettier-bytes': 0.0.7
|
||||
'@uppy/store-default': 2.1.1
|
||||
'@uppy/utils': 4.1.3
|
||||
lodash.throttle: 4.1.1
|
||||
mime-match: 1.0.2
|
||||
namespace-emitter: 2.0.1
|
||||
nanoid: 3.3.11
|
||||
preact: 10.29.8
|
||||
transitivePeerDependencies:
|
||||
- preact-render-to-string
|
||||
|
||||
'@uppy/store-default@2.1.1': {}
|
||||
|
||||
'@uppy/utils@4.1.3':
|
||||
dependencies:
|
||||
lodash.throttle: 4.1.1
|
||||
|
||||
'@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4)':
|
||||
dependencies:
|
||||
'@uppy/companion-client': 2.2.2
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/utils': 4.1.3
|
||||
nanoid: 3.3.11
|
||||
|
||||
'@vavt/copy2clipboard@1.0.3': {}
|
||||
|
||||
'@vavt/util@2.1.1': {}
|
||||
@@ -6365,6 +6595,116 @@ snapshots:
|
||||
|
||||
'@vue/shared@3.5.34': {}
|
||||
|
||||
'@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
is-url: 1.2.4
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 3.3.11
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/code-highlight@1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
prismjs: 1.30.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@types/event-emitter': 0.3.5
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
dom7: 3.0.0
|
||||
event-emitter: 0.3.5
|
||||
html-void-elements: 2.0.1
|
||||
i18next: 20.6.1
|
||||
is-hotkey: 0.2.0
|
||||
lodash.camelcase: 4.3.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
lodash.debounce: 4.0.8
|
||||
lodash.foreach: 4.5.0
|
||||
lodash.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
lodash.toarray: 4.4.0
|
||||
nanoid: 3.3.11
|
||||
scroll-into-view-if-needed: 2.2.31
|
||||
slate: 0.72.8
|
||||
slate-history: 0.66.0(slate@0.72.8)
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/editor-for-vue@5.1.12(@wangeditor/editor@5.1.23)(vue@3.5.3(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@wangeditor/editor': 5.1.23
|
||||
vue: 3.5.3(typescript@5.9.3)
|
||||
|
||||
'@wangeditor/editor@5.1.23':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/code-highlight': 1.0.3(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/list-module': 1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/table-module': 1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/upload-image-module': 1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/video-module': 1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
is-hotkey: 0.2.0
|
||||
lodash.camelcase: 4.3.0
|
||||
lodash.clonedeep: 4.5.0
|
||||
lodash.debounce: 4.0.8
|
||||
lodash.foreach: 4.5.0
|
||||
lodash.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
lodash.toarray: 4.4.0
|
||||
nanoid: 3.3.11
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
transitivePeerDependencies:
|
||||
- preact-render-to-string
|
||||
|
||||
'@wangeditor/list-module@1.0.5(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/table-module@1.1.4(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
lodash.isequal: 4.5.0
|
||||
lodash.throttle: 4.1.1
|
||||
nanoid: 3.3.11
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/upload-image-module@1.0.2(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/basic-modules@1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.foreach@4.5.0)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/basic-modules': 1.1.7(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(lodash.throttle@4.1.1)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
lodash.foreach: 4.5.0
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wangeditor/video-module@1.1.4(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(@wangeditor/core@1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4))(dom7@3.0.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)':
|
||||
dependencies:
|
||||
'@uppy/core': 2.3.4
|
||||
'@uppy/xhr-upload': 2.1.3(@uppy/core@2.3.4)
|
||||
'@wangeditor/core': 1.1.19(@uppy/core@2.3.4)(@uppy/xhr-upload@2.1.3(@uppy/core@2.3.4))(dom7@3.0.0)(is-hotkey@0.2.0)(lodash.camelcase@4.3.0)(lodash.clonedeep@4.5.0)(lodash.debounce@4.0.8)(lodash.foreach@4.5.0)(lodash.isequal@4.5.0)(lodash.throttle@4.1.1)(lodash.toarray@4.4.0)(nanoid@3.3.11)(slate@0.72.8)(snabbdom@3.6.4)
|
||||
dom7: 3.0.0
|
||||
nanoid: 3.3.11
|
||||
slate: 0.72.8
|
||||
snabbdom: 3.6.4
|
||||
|
||||
'@wecom/jssdk@1.4.5': {}
|
||||
|
||||
acorn-jsx@5.3.2(acorn@8.15.0):
|
||||
@@ -7006,6 +7346,10 @@ snapshots:
|
||||
|
||||
dom-scroll-into-view@2.0.1: {}
|
||||
|
||||
dom7@3.0.0:
|
||||
dependencies:
|
||||
ssr-window: 3.0.0
|
||||
|
||||
dompurify@3.4.5:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
@@ -7556,6 +7900,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
i18next@20.6.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
@@ -7571,6 +7919,8 @@ snapshots:
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
immer@9.0.21: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -7613,6 +7963,8 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-hotkey@0.2.0: {}
|
||||
|
||||
is-number@7.0.0: {}
|
||||
|
||||
is-path-inside@3.0.3: {}
|
||||
@@ -7621,12 +7973,16 @@ snapshots:
|
||||
|
||||
is-plain-object@3.0.1: {}
|
||||
|
||||
is-plain-object@5.0.0: {}
|
||||
|
||||
is-type-of@1.4.0:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
is-class-hotfix: 0.0.6
|
||||
isstream: 0.1.2
|
||||
|
||||
is-url@1.2.4: {}
|
||||
|
||||
is-what@3.14.1: {}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
@@ -7792,6 +8148,10 @@ snapshots:
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash.camelcase@4.3.0: {}
|
||||
|
||||
lodash.clonedeep@4.5.0: {}
|
||||
|
||||
lodash.debounce@4.0.8: {}
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
@@ -7802,6 +8162,8 @@ snapshots:
|
||||
|
||||
lodash.flatten@4.4.0: {}
|
||||
|
||||
lodash.foreach@4.5.0: {}
|
||||
|
||||
lodash.groupby@4.6.0: {}
|
||||
|
||||
lodash.isboolean@3.0.3: {}
|
||||
@@ -7818,6 +8180,10 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash.throttle@4.1.1: {}
|
||||
|
||||
lodash.toarray@4.4.0: {}
|
||||
|
||||
lodash.union@4.6.0: {}
|
||||
|
||||
lodash.uniq@4.5.0: {}
|
||||
@@ -8215,6 +8581,10 @@ snapshots:
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-match@1.0.2:
|
||||
dependencies:
|
||||
wildcard: 1.1.2
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
@@ -8265,6 +8635,8 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
thenify-all: 1.6.0
|
||||
|
||||
namespace-emitter@2.0.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
@@ -8469,6 +8841,8 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
preact@10.29.8: {}
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.0:
|
||||
@@ -8477,6 +8851,8 @@ snapshots:
|
||||
|
||||
prettier@2.8.8: {}
|
||||
|
||||
prismjs@1.30.0: {}
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
property-information@6.5.0: {}
|
||||
@@ -8784,6 +9160,19 @@ snapshots:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.4
|
||||
|
||||
slate-history@0.66.0(slate@0.72.8):
|
||||
dependencies:
|
||||
is-plain-object: 5.0.0
|
||||
slate: 0.72.8
|
||||
|
||||
slate@0.72.8:
|
||||
dependencies:
|
||||
immer: 9.0.21
|
||||
is-plain-object: 5.0.0
|
||||
tiny-warning: 1.0.3
|
||||
|
||||
snabbdom@3.6.4: {}
|
||||
|
||||
socket.io-client@4.8.1:
|
||||
dependencies:
|
||||
'@socket.io/component-emitter': 3.1.2
|
||||
@@ -8827,6 +9216,8 @@ snapshots:
|
||||
dependencies:
|
||||
frac: 1.1.2
|
||||
|
||||
ssr-window@3.0.0: {}
|
||||
|
||||
statuses@1.5.0: {}
|
||||
|
||||
stream-http@2.8.2:
|
||||
@@ -8949,6 +9340,8 @@ snapshots:
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
tiny-warning@1.0.3: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -9284,6 +9677,8 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wildcard@1.1.2: {}
|
||||
|
||||
win-release@1.1.1:
|
||||
dependencies:
|
||||
semver: 5.7.2
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 订阅状态: pending-待支付 active-已激活 expired-已过期 cancelled-已取消
|
||||
*/
|
||||
export type SubscriptionStatus =
|
||||
| 'pending'
|
||||
| 'active'
|
||||
| 'expired'
|
||||
| 'cancelled';
|
||||
|
||||
/**
|
||||
* 价格类型: free-免费 one_time-买断 subscription-订阅
|
||||
*/
|
||||
export type PriceType = 'free' | 'one_time' | 'subscription';
|
||||
|
||||
/**
|
||||
* 订阅周期: month-月 quarter-季 year-年
|
||||
*/
|
||||
export type SubscriptionPeriod = 'month' | 'quarter' | 'year';
|
||||
|
||||
/**
|
||||
* 支付方式: 0-余额 1-微信 2-支付宝 12-免费
|
||||
*/
|
||||
export type PayType = 0 | 1 | 2 | 12;
|
||||
|
||||
/**
|
||||
* 应用订阅实体(对应表 app_subscription)
|
||||
* 字段对照后端 com.gxwebsoft.app.entity.AppSubscription
|
||||
*/
|
||||
export interface AppSubscription {
|
||||
// 主键ID
|
||||
id?: number;
|
||||
// 订阅编号(业务唯一)
|
||||
subscriptionNo?: string;
|
||||
// 购买用户ID
|
||||
userId?: number;
|
||||
// 应用产品ID
|
||||
productId?: number;
|
||||
// 租户ID
|
||||
tenantId?: number;
|
||||
// 订阅状态
|
||||
status?: SubscriptionStatus;
|
||||
// 价格类型
|
||||
priceType?: PriceType;
|
||||
// 原价(单位:元)
|
||||
originalPrice?: number;
|
||||
// 实付金额(单位:元)
|
||||
payPrice?: number;
|
||||
// 支付方式
|
||||
payType?: PayType;
|
||||
// 支付状态: 0-未支付 1-已支付
|
||||
payStatus?: number;
|
||||
// 支付时间
|
||||
payTime?: string;
|
||||
// 第三方交易号
|
||||
transactionId?: string;
|
||||
// 订阅周期
|
||||
subscriptionPeriod?: SubscriptionPeriod;
|
||||
// 生效时间
|
||||
startTime?: string;
|
||||
// 到期时间(订阅型)
|
||||
expireTime?: string;
|
||||
// 是否自动续费 0-否 1-是
|
||||
autoRenew?: number;
|
||||
// 分配的域名
|
||||
instanceDomain?: string;
|
||||
// 实例管理后台URL
|
||||
instanceAdminUrl?: string;
|
||||
// 实例配置(JSON)
|
||||
instanceConfig?: string;
|
||||
// 关联的支付订单号
|
||||
orderNo?: string;
|
||||
// 关联的支付订单ID
|
||||
orderId?: number;
|
||||
// 备注
|
||||
remark?: string;
|
||||
// 排序
|
||||
sortNumber?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 更新时间
|
||||
updateTime?: string;
|
||||
// ===== 关联查询字段(非数据库字段) =====
|
||||
productName?: string;
|
||||
productIcon?: string;
|
||||
productLogo?: string;
|
||||
productAppType?: number;
|
||||
productDescription?: string;
|
||||
developerName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的订阅分页查询参数
|
||||
*/
|
||||
export interface AppSubscriptionQueryParam extends PageParam {
|
||||
// 订阅状态过滤
|
||||
status?: SubscriptionStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订阅参数
|
||||
*/
|
||||
export interface SubscribeParam {
|
||||
// 应用产品ID
|
||||
productId: number;
|
||||
// 订阅周期,默认 month
|
||||
subscriptionPeriod?: SubscriptionPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付小程序码参数
|
||||
*/
|
||||
export interface GeneratePayQrcodeParam {
|
||||
// 应用产品ID
|
||||
productId: number;
|
||||
// 订阅周期,默认 month
|
||||
subscriptionPeriod?: SubscriptionPeriod;
|
||||
// 小程序版本:develop / trial / release,默认 trial
|
||||
envVersion?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订阅返回结果
|
||||
*/
|
||||
export interface SubscribeResult {
|
||||
subscriptionId: number;
|
||||
subscriptionNo: string;
|
||||
status: SubscriptionStatus;
|
||||
message: string;
|
||||
payPrice?: number;
|
||||
orderNo?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付小程序码返回结果
|
||||
*/
|
||||
export interface GeneratePayQrcodeResult {
|
||||
subscriptionNo: string;
|
||||
subscriptionId: number;
|
||||
qrcodeBase64: string;
|
||||
productName: string;
|
||||
payPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询支付状态返回结果
|
||||
*/
|
||||
export interface CheckStatusResult {
|
||||
paid: boolean;
|
||||
payStatus: number;
|
||||
status: SubscriptionStatus;
|
||||
payTime?: string;
|
||||
transactionId?: string;
|
||||
id: number;
|
||||
subscriptionNo: string;
|
||||
productId: number;
|
||||
productName?: string;
|
||||
productLogo?: string;
|
||||
priceType?: PriceType;
|
||||
payPrice?: number;
|
||||
subscriptionPeriod?: SubscriptionPeriod;
|
||||
}
|
||||
|
||||
/**
|
||||
* 余额支付返回结果
|
||||
*/
|
||||
export interface PayResult {
|
||||
paid: boolean;
|
||||
balance?: number;
|
||||
subscriptionNo: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信 Native 支付(Web 端)返回结果
|
||||
*/
|
||||
export interface WechatNativePayResult {
|
||||
subscriptionId: number;
|
||||
subscriptionNo: string;
|
||||
miniappQrcode: string;
|
||||
miniappPagePath: string;
|
||||
payPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序 JSAPI 预支付返回结果
|
||||
*/
|
||||
export interface MpPrepayResult {
|
||||
subscriptionId: number;
|
||||
subscriptionNo: string;
|
||||
outTradeNo: string;
|
||||
timeStamp: string;
|
||||
nonceStr: string;
|
||||
package: string;
|
||||
signType: string;
|
||||
paySign: string;
|
||||
payPrice: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取余额返回结果
|
||||
*/
|
||||
export interface BalanceResult {
|
||||
balance: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序预支付参数
|
||||
*/
|
||||
export interface MpPrepayParam {
|
||||
openid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 小程序支付确认参数
|
||||
*/
|
||||
export interface MpConfirmParam {
|
||||
transactionId?: string;
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsAd, CmsAdParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询广告位
|
||||
*/
|
||||
export async function pageCmsAd(params: CmsAdParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsAd>>>(
|
||||
MODULES_API_URL + '/cms/cms-ad/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsAd(params: CmsAdParam) {
|
||||
*/
|
||||
export async function listCmsAd(params?: CmsAdParam) {
|
||||
const res = await request.get<ApiResult<CmsAd[]>>(
|
||||
MODULES_API_URL + '/cms/cms-ad',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsAd(params?: CmsAdParam) {
|
||||
*/
|
||||
export async function addCmsAd(data: CmsAd) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsAd(data: CmsAd) {
|
||||
*/
|
||||
export async function updateCmsAd(data: CmsAd) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsAd(data: CmsAd) {
|
||||
*/
|
||||
export async function removeCmsAd(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-ad/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsAd(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsAd(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsAd(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsAd(id: number) {
|
||||
const res = await request.get<ApiResult<CmsAd>>(
|
||||
MODULES_API_URL + '/cms/cms-ad/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-ad/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsAdRecord, CmsAdRecordParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询广告图片
|
||||
*/
|
||||
export async function pageCmsAdRecord(params: CmsAdRecordParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsAdRecord>>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsAdRecord(params: CmsAdRecordParam) {
|
||||
*/
|
||||
export async function listCmsAdRecord(params?: CmsAdRecordParam) {
|
||||
const res = await request.get<ApiResult<CmsAdRecord[]>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsAdRecord(params?: CmsAdRecordParam) {
|
||||
*/
|
||||
export async function addCmsAdRecord(data: CmsAdRecord) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsAdRecord(data: CmsAdRecord) {
|
||||
*/
|
||||
export async function updateCmsAdRecord(data: CmsAdRecord) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsAdRecord(data: CmsAdRecord) {
|
||||
*/
|
||||
export async function removeCmsAdRecord(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsAdRecord(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsAdRecord(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsAdRecord(id: number) {
|
||||
const res = await request.get<ApiResult<CmsAdRecord>>(
|
||||
MODULES_API_URL + '/cms/cms-ad-record/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-ad-record/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticle, CmsArticleParam } from './model';
|
||||
|
||||
@@ -7,7 +8,7 @@ import type { CmsArticle, CmsArticleParam } from './model';
|
||||
*/
|
||||
export async function pageCmsArticle(params: CmsArticleParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticle>>>(
|
||||
'/cms/cms-article/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -22,7 +23,7 @@ export async function pageCmsArticle(params: CmsArticleParam) {
|
||||
* 查询文章列表
|
||||
*/
|
||||
export async function listCmsArticle(params?: CmsArticleParam) {
|
||||
const res = await request.get<ApiResult<CmsArticle[]>>('/cms/cms-article', {
|
||||
const res = await request.get<ApiResult<CmsArticle[]>>(CMS_API_BASE_URL + '/cms/cms-article', {
|
||||
params
|
||||
});
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
@@ -35,7 +36,7 @@ export async function listCmsArticle(params?: CmsArticleParam) {
|
||||
* 添加文章
|
||||
*/
|
||||
export async function addCmsArticle(data: CmsArticle) {
|
||||
const res = await request.post<ApiResult<unknown>>('/cms/cms-article', data);
|
||||
const res = await request.post<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
@@ -46,7 +47,7 @@ export async function addCmsArticle(data: CmsArticle) {
|
||||
* 修改文章
|
||||
*/
|
||||
export async function updateCmsArticle(data: CmsArticle) {
|
||||
const res = await request.put<ApiResult<unknown>>('/cms/cms-article', data);
|
||||
const res = await request.put<ApiResult<unknown>>(CMS_API_BASE_URL + '/cms/cms-article', data);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
@@ -58,7 +59,7 @@ export async function updateCmsArticle(data: CmsArticle) {
|
||||
*/
|
||||
export async function updateBatchCmsArticle(data: any) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/cms/cms-article/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article/batch',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -72,7 +73,7 @@ export async function updateBatchCmsArticle(data: any) {
|
||||
*/
|
||||
export async function removeCmsArticle(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
'/cms/cms-article/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -85,7 +86,7 @@ export async function removeCmsArticle(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsArticle(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
'/cms/cms-article/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -101,7 +102,7 @@ export async function removeBatchCmsArticle(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsArticle(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticle>>(
|
||||
'/cms/cms-article/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -114,7 +115,7 @@ export async function getCmsArticle(id: number) {
|
||||
*/
|
||||
export async function getByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsArticle>>(
|
||||
'/cms/cms-article/getByCode/' + code
|
||||
CMS_API_BASE_URL + '/cms/cms-article/getByCode/' + code
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -123,7 +124,7 @@ export async function getByCode(code: string) {
|
||||
}
|
||||
|
||||
export async function getCount(params: CmsArticleParam) {
|
||||
const res = await request.get('/cms/cms-article/data', {
|
||||
const res = await request.get(CMS_API_BASE_URL + '/cms/cms-article/data', {
|
||||
params
|
||||
});
|
||||
if (res.data.code === 0) {
|
||||
@@ -139,7 +140,7 @@ export async function importArticles(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/cms/cms-article/import',
|
||||
CMS_API_BASE_URL + '/cms/cms-article/import',
|
||||
formData
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticleCategory, CmsArticleCategoryParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询文章分类表
|
||||
*/
|
||||
export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticleCategory>>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleCategory(params: CmsArticleCategoryParam) {
|
||||
*/
|
||||
export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
|
||||
const res = await request.get<ApiResult<CmsArticleCategory[]>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsArticleCategory(params?: CmsArticleCategoryParam) {
|
||||
*/
|
||||
export async function addCmsArticleCategory(data: CmsArticleCategory) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsArticleCategory(data: CmsArticleCategory) {
|
||||
*/
|
||||
export async function updateCmsArticleCategory(data: CmsArticleCategory) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleCategory(data: CmsArticleCategory) {
|
||||
*/
|
||||
export async function removeCmsArticleCategory(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleCategory(
|
||||
data: (number | undefined)[]
|
||||
) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleCategory(
|
||||
*/
|
||||
export async function getCmsArticleCategory(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticleCategory>>(
|
||||
MODULES_API_URL + '/cms/cms-article-category/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-category/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticleComment, CmsArticleCommentParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询文章评论表
|
||||
*/
|
||||
export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticleComment>>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleComment(params: CmsArticleCommentParam) {
|
||||
*/
|
||||
export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
|
||||
const res = await request.get<ApiResult<CmsArticleComment[]>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsArticleComment(params?: CmsArticleCommentParam) {
|
||||
*/
|
||||
export async function addCmsArticleComment(data: CmsArticleComment) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsArticleComment(data: CmsArticleComment) {
|
||||
*/
|
||||
export async function updateCmsArticleComment(data: CmsArticleComment) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleComment(data: CmsArticleComment) {
|
||||
*/
|
||||
export async function removeCmsArticleComment(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleComment(
|
||||
data: (number | undefined)[]
|
||||
) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleComment(
|
||||
*/
|
||||
export async function getCmsArticleComment(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticleComment>>(
|
||||
MODULES_API_URL + '/cms/cms-article-comment/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-comment/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticleContent, CmsArticleContentParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询文章记录表
|
||||
*/
|
||||
export async function pageCmsArticleContent(params: CmsArticleContentParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticleContent>>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleContent(params: CmsArticleContentParam) {
|
||||
*/
|
||||
export async function listCmsArticleContent(params?: CmsArticleContentParam) {
|
||||
const res = await request.get<ApiResult<CmsArticleContent[]>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsArticleContent(params?: CmsArticleContentParam) {
|
||||
*/
|
||||
export async function addCmsArticleContent(data: CmsArticleContent) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsArticleContent(data: CmsArticleContent) {
|
||||
*/
|
||||
export async function updateCmsArticleContent(data: CmsArticleContent) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleContent(data: CmsArticleContent) {
|
||||
*/
|
||||
export async function removeCmsArticleContent(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsArticleContent(
|
||||
data: (number | undefined)[]
|
||||
) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsArticleContent(
|
||||
*/
|
||||
export async function getCmsArticleContent(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticleContent>>(
|
||||
MODULES_API_URL + '/cms/cms-article-content/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-content/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticleCount, CmsArticleCountParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询点赞文章
|
||||
*/
|
||||
export async function pageCmsArticleCount(params: CmsArticleCountParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticleCount>>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleCount(params: CmsArticleCountParam) {
|
||||
*/
|
||||
export async function listCmsArticleCount(params?: CmsArticleCountParam) {
|
||||
const res = await request.get<ApiResult<CmsArticleCount[]>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsArticleCount(params?: CmsArticleCountParam) {
|
||||
*/
|
||||
export async function addCmsArticleCount(data: CmsArticleCount) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsArticleCount(data: CmsArticleCount) {
|
||||
*/
|
||||
export async function updateCmsArticleCount(data: CmsArticleCount) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleCount(data: CmsArticleCount) {
|
||||
*/
|
||||
export async function removeCmsArticleCount(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsArticleCount(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleCount(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsArticleCount(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticleCount>>(
|
||||
MODULES_API_URL + '/cms/cms-article-count/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-count/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsArticleLike, CmsArticleLikeParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询点赞文章
|
||||
*/
|
||||
export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsArticleLike>>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsArticleLike(params: CmsArticleLikeParam) {
|
||||
*/
|
||||
export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
|
||||
const res = await request.get<ApiResult<CmsArticleLike[]>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsArticleLike(params?: CmsArticleLikeParam) {
|
||||
*/
|
||||
export async function addCmsArticleLike(data: CmsArticleLike) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsArticleLike(data: CmsArticleLike) {
|
||||
*/
|
||||
export async function updateCmsArticleLike(data: CmsArticleLike) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsArticleLike(data: CmsArticleLike) {
|
||||
*/
|
||||
export async function removeCmsArticleLike(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsArticleLike(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsArticleLike(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsArticleLike(id: number) {
|
||||
const res = await request.get<ApiResult<CmsArticleLike>>(
|
||||
MODULES_API_URL + '/cms/cms-article-like/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-article-like/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsDesign, CmsDesignParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询页面管理记录表
|
||||
*/
|
||||
export async function pageCmsDesign(params: CmsDesignParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsDesign>>>(
|
||||
MODULES_API_URL + '/cms/cms-design/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-design/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsDesign(params: CmsDesignParam) {
|
||||
*/
|
||||
export async function listCmsDesign(params?: CmsDesignParam) {
|
||||
const res = await request.get<ApiResult<CmsDesign[]>>(
|
||||
MODULES_API_URL + '/cms/cms-design',
|
||||
CMS_API_BASE_URL + '/cms/cms-design',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsDesign(params?: CmsDesignParam) {
|
||||
*/
|
||||
export async function addCmsDesign(data: CmsDesign) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-design',
|
||||
CMS_API_BASE_URL + '/cms/cms-design',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsDesign(data: CmsDesign) {
|
||||
*/
|
||||
export async function updateCmsDesign(data: CmsDesign) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-design',
|
||||
CMS_API_BASE_URL + '/cms/cms-design',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsDesign(data: CmsDesign) {
|
||||
*/
|
||||
export async function removeCmsDesign(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-design/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-design/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsDesign(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsDesign(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-design/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-design/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsDesign(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsDesign(id: number) {
|
||||
const res = await request.get<ApiResult<CmsDesign>>(
|
||||
MODULES_API_URL + '/cms/cms-design/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-design/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsDomain, CmsDomainParam } from './model';
|
||||
import { SERVER_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询网站域名记录表
|
||||
*/
|
||||
export async function pageCmsDomain(params: CmsDomainParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsDomain>>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsDomain(params: CmsDomainParam) {
|
||||
*/
|
||||
export async function listCmsDomain(params?: CmsDomainParam) {
|
||||
const res = await request.get<ApiResult<CmsDomain[]>>(
|
||||
SERVER_API_URL + '/cms/cms-domain',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsDomain(params?: CmsDomainParam) {
|
||||
*/
|
||||
export async function addCmsDomain(data: CmsDomain) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/cms/cms-domain',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsDomain(data: CmsDomain) {
|
||||
*/
|
||||
export async function updateCmsDomain(data: CmsDomain) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/domain',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/domain',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsDomain(data: CmsDomain) {
|
||||
*/
|
||||
export async function removeCmsDomain(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsDomain(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsDomain(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsDomain(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsDomain(id: number) {
|
||||
const res = await request.get<ApiResult<CmsDomain>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -114,7 +114,7 @@ export async function checkExistence(
|
||||
id?: number
|
||||
) {
|
||||
const res = await request.get<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/existence',
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/existence',
|
||||
{
|
||||
params: { field, value, id }
|
||||
}
|
||||
@@ -127,7 +127,7 @@ export async function checkExistence(
|
||||
|
||||
export async function resolvable(id: number) {
|
||||
const res = await request.get<ApiResult<CmsDomain>>(
|
||||
SERVER_API_URL + '/cms/cms-domain/resolvable/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-domain/resolvable/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data) {
|
||||
return res.data;
|
||||
@@ -143,7 +143,7 @@ export async function getAuthorizedDomain() {
|
||||
return false;
|
||||
}
|
||||
const res = await request.get<ApiResult<CmsDomain>>(
|
||||
SERVER_API_URL +
|
||||
CMS_API_BASE_URL +
|
||||
'/cms/cms-domain/getAuthorizedDomain/' +
|
||||
localStorage.getItem('WebsiteId')
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsForm, CmsFormParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询表单设计表
|
||||
*/
|
||||
export async function pageCmsForm(params: CmsFormParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsForm>>>(
|
||||
MODULES_API_URL + '/cms/cms-form/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-form/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsForm(params: CmsFormParam) {
|
||||
*/
|
||||
export async function listCmsForm(params?: CmsFormParam) {
|
||||
const res = await request.get<ApiResult<CmsForm[]>>(
|
||||
MODULES_API_URL + '/cms/cms-form',
|
||||
CMS_API_BASE_URL + '/cms/cms-form',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsForm(params?: CmsFormParam) {
|
||||
*/
|
||||
export async function addCmsForm(data: CmsForm) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form',
|
||||
CMS_API_BASE_URL + '/cms/cms-form',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsForm(data: CmsForm) {
|
||||
*/
|
||||
export async function updateCmsForm(data: CmsForm) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form',
|
||||
CMS_API_BASE_URL + '/cms/cms-form',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsForm(data: CmsForm) {
|
||||
*/
|
||||
export async function removeCmsForm(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-form/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsForm(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsForm(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-form/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsForm(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsForm(id: number) {
|
||||
const res = await request.get<ApiResult<CmsForm>>(
|
||||
MODULES_API_URL + '/cms/cms-form/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-form/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsFormRecord, CmsFormRecordParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询表单数据记录表
|
||||
*/
|
||||
export async function pageCmsFormRecord(params: CmsFormRecordParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsFormRecord>>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsFormRecord(params: CmsFormRecordParam) {
|
||||
*/
|
||||
export async function listCmsFormRecord(params?: CmsFormRecordParam) {
|
||||
const res = await request.get<ApiResult<CmsFormRecord[]>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsFormRecord(params?: CmsFormRecordParam) {
|
||||
*/
|
||||
export async function addCmsFormRecord(data: CmsFormRecord) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsFormRecord(data: CmsFormRecord) {
|
||||
*/
|
||||
export async function updateCmsFormRecord(data: CmsFormRecord) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record',
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsFormRecord(data: CmsFormRecord) {
|
||||
*/
|
||||
export async function removeCmsFormRecord(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsFormRecord(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsFormRecord(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsFormRecord(id: number) {
|
||||
const res = await request.get<ApiResult<CmsFormRecord>>(
|
||||
MODULES_API_URL + '/cms/cms-form-record/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-form-record/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsLang, CmsLangParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询国际化
|
||||
*/
|
||||
export async function pageCmsLang(params: CmsLangParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsLang>>>(
|
||||
MODULES_API_URL + '/cms/cms-lang/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsLang(params: CmsLangParam) {
|
||||
*/
|
||||
export async function listCmsLang(params?: CmsLangParam) {
|
||||
const res = await request.get<ApiResult<CmsLang[]>>(
|
||||
MODULES_API_URL + '/cms/cms-lang',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsLang(params?: CmsLangParam) {
|
||||
*/
|
||||
export async function addCmsLang(data: CmsLang) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsLang(data: CmsLang) {
|
||||
*/
|
||||
export async function updateCmsLang(data: CmsLang) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsLang(data: CmsLang) {
|
||||
*/
|
||||
export async function removeCmsLang(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-lang/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsLang(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsLang(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsLang(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsLang(id: number) {
|
||||
const res = await request.get<ApiResult<CmsLang>>(
|
||||
MODULES_API_URL + '/cms/cms-lang/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-lang/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsLangLog, CmsLangLogParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询国际化记录启用
|
||||
*/
|
||||
export async function pageCmsLangLog(params: CmsLangLogParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsLangLog>>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsLangLog(params: CmsLangLogParam) {
|
||||
*/
|
||||
export async function listCmsLangLog(params?: CmsLangLogParam) {
|
||||
const res = await request.get<ApiResult<CmsLangLog[]>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsLangLog(params?: CmsLangLogParam) {
|
||||
*/
|
||||
export async function addCmsLangLog(data: CmsLangLog) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsLangLog(data: CmsLangLog) {
|
||||
*/
|
||||
export async function updateCmsLangLog(data: CmsLangLog) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsLangLog(data: CmsLangLog) {
|
||||
*/
|
||||
export async function removeCmsLangLog(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsLangLog(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsLangLog(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsLangLog(id: number) {
|
||||
const res = await request.get<ApiResult<CmsLangLog>>(
|
||||
MODULES_API_URL + '/cms/cms-lang-log/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-lang-log/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsLink, CmsLinkParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询常用链接
|
||||
*/
|
||||
export async function pageCmsLink(params: CmsLinkParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsLink>>>(
|
||||
MODULES_API_URL + '/cms/cms-link/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-link/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsLink(params: CmsLinkParam) {
|
||||
*/
|
||||
export async function listCmsLink(params?: CmsLinkParam) {
|
||||
const res = await request.get<ApiResult<CmsLink[]>>(
|
||||
MODULES_API_URL + '/cms/cms-link',
|
||||
CMS_API_BASE_URL + '/cms/cms-link',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsLink(params?: CmsLinkParam) {
|
||||
*/
|
||||
export async function addCmsLink(data: CmsLink) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-link',
|
||||
CMS_API_BASE_URL + '/cms/cms-link',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsLink(data: CmsLink) {
|
||||
*/
|
||||
export async function updateCmsLink(data: CmsLink) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-link',
|
||||
CMS_API_BASE_URL + '/cms/cms-link',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsLink(data: CmsLink) {
|
||||
*/
|
||||
export async function removeCmsLink(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-link/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-link/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsLink(id?: number) {
|
||||
*/
|
||||
export async function updateBatchCmsLink(data: any) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-link/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-link/batch',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -95,7 +95,7 @@ export async function updateBatchCmsLink(data: any) {
|
||||
*/
|
||||
export async function removeBatchCmsLink(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-link/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-link/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export async function removeBatchCmsLink(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsLink(id: number) {
|
||||
const res = await request.get<ApiResult<CmsLink>>(
|
||||
MODULES_API_URL + '/cms/cms-link/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-link/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsModel, CmsModelParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询模型
|
||||
*/
|
||||
export async function pageCmsModel(params: CmsModelParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsModel>>>(
|
||||
MODULES_API_URL + '/cms/cms-model/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-model/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsModel(params: CmsModelParam) {
|
||||
*/
|
||||
export async function listCmsModel(params?: CmsModelParam) {
|
||||
const res = await request.get<ApiResult<CmsModel[]>>(
|
||||
MODULES_API_URL + '/cms/cms-model',
|
||||
CMS_API_BASE_URL + '/cms/cms-model',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsModel(params?: CmsModelParam) {
|
||||
*/
|
||||
export async function addCmsModel(data: CmsModel) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-model',
|
||||
CMS_API_BASE_URL + '/cms/cms-model',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsModel(data: CmsModel) {
|
||||
*/
|
||||
export async function updateCmsModel(data: CmsModel) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-model',
|
||||
CMS_API_BASE_URL + '/cms/cms-model',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsModel(data: CmsModel) {
|
||||
*/
|
||||
export async function removeCmsModel(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-model/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-model/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsModel(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsModel(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-model/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-model/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsModel(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsModel(id: number) {
|
||||
const res = await request.get<ApiResult<CmsModel>>(
|
||||
MODULES_API_URL + '/cms/cms-model/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-model/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsNavigation, CmsNavigationParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询网站导航记录表
|
||||
*/
|
||||
export async function pageCmsNavigation(params: CmsNavigationParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsNavigation>>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsNavigation(params: CmsNavigationParam) {
|
||||
*/
|
||||
export async function listCmsNavigation(params?: CmsNavigationParam) {
|
||||
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsNavigation(params?: CmsNavigationParam) {
|
||||
*/
|
||||
export async function treeNavigation(params?: CmsNavigationParam) {
|
||||
const res = await request.get<ApiResult<CmsNavigation[]>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/tree',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/tree',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export async function treeNavigation(params?: CmsNavigationParam) {
|
||||
*/
|
||||
export async function addCmsNavigation(data: CmsNavigation) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -69,7 +69,7 @@ export async function addCmsNavigation(data: CmsNavigation) {
|
||||
*/
|
||||
export async function updateCmsNavigation(data: CmsNavigation) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -83,7 +83,7 @@ export async function updateCmsNavigation(data: CmsNavigation) {
|
||||
*/
|
||||
export async function removeCmsNavigation(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -96,7 +96,7 @@ export async function removeCmsNavigation(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export async function removeBatchCmsNavigation(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsNavigation(id: number) {
|
||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -125,7 +125,7 @@ export async function getCmsNavigation(id: number) {
|
||||
*/
|
||||
export async function getByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsNavigation>>(
|
||||
'/cms/cms-navigation/getByCode/' + code
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/getByCode/' + code
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -140,7 +140,7 @@ export async function importCmsNavigation(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-navigation/import',
|
||||
CMS_API_BASE_URL + '/cms/cms-navigation/import',
|
||||
formData
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import request from '@/utils/request';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsOrder, CmsOrderParam } from './model';
|
||||
|
||||
@@ -7,7 +8,7 @@ import type { CmsOrder, CmsOrderParam } from './model';
|
||||
*/
|
||||
export async function pageCmsOrder(params: CmsOrderParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsOrder>>>(
|
||||
'/cms/cms-order/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-order/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -23,7 +24,7 @@ export async function pageCmsOrder(params: CmsOrderParam) {
|
||||
*/
|
||||
export async function listCmsOrder(params?: CmsOrderParam) {
|
||||
const res = await request.get<ApiResult<CmsOrder[]>>(
|
||||
'/cms/cms-order',
|
||||
CMS_API_BASE_URL + '/cms/cms-order',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -39,7 +40,7 @@ export async function listCmsOrder(params?: CmsOrderParam) {
|
||||
*/
|
||||
export async function addCmsOrder(data: CmsOrder) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
'/cms/cms-order',
|
||||
CMS_API_BASE_URL + '/cms/cms-order',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -53,7 +54,7 @@ export async function addCmsOrder(data: CmsOrder) {
|
||||
*/
|
||||
export async function updateCmsOrder(data: CmsOrder) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
'/cms/cms-order',
|
||||
CMS_API_BASE_URL + '/cms/cms-order',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -67,7 +68,7 @@ export async function updateCmsOrder(data: CmsOrder) {
|
||||
*/
|
||||
export async function removeCmsOrder(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
'/cms/cms-order/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-order/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -80,7 +81,7 @@ export async function removeCmsOrder(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsOrder(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
'/cms/cms-order/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-order/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -96,7 +97,7 @@ export async function removeBatchCmsOrder(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsOrder(id: number) {
|
||||
const res = await request.get<ApiResult<CmsOrder>>(
|
||||
'/cms/cms-order/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-order/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsStatistics, CmsStatisticsParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询站点统计信息表
|
||||
*/
|
||||
export async function pageCmsStatistics(params: CmsStatisticsParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsStatistics>>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsStatistics(params: CmsStatisticsParam) {
|
||||
*/
|
||||
export async function listCmsStatistics(params?: CmsStatisticsParam) {
|
||||
const res = await request.get<ApiResult<CmsStatistics[]>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics',
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsStatistics(params?: CmsStatisticsParam) {
|
||||
*/
|
||||
export async function addCmsStatistics(data: CmsStatistics) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics',
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsStatistics(data: CmsStatistics) {
|
||||
*/
|
||||
export async function updateCmsStatistics(data: CmsStatistics) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics',
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsStatistics(data: CmsStatistics) {
|
||||
*/
|
||||
export async function removeCmsStatistics(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsStatistics(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsStatistics(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsStatistics(id: number) {
|
||||
const res = await request.get<ApiResult<CmsStatistics>>(
|
||||
MODULES_API_URL + '/cms/cms-statistics/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-statistics/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsTemplate, CmsTemplateParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询网站模版
|
||||
*/
|
||||
export async function pageCmsTemplate(params: CmsTemplateParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsTemplate>>>(
|
||||
MODULES_API_URL + '/cms/cms-template/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-template/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsTemplate(params: CmsTemplateParam) {
|
||||
*/
|
||||
export async function listCmsTemplate(params?: CmsTemplateParam) {
|
||||
const res = await request.get<ApiResult<CmsTemplate[]>>(
|
||||
MODULES_API_URL + '/cms/cms-template',
|
||||
CMS_API_BASE_URL + '/cms/cms-template',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsTemplate(params?: CmsTemplateParam) {
|
||||
*/
|
||||
export async function addCmsTemplate(data: CmsTemplate) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-template',
|
||||
CMS_API_BASE_URL + '/cms/cms-template',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsTemplate(data: CmsTemplate) {
|
||||
*/
|
||||
export async function updateCmsTemplate(data: CmsTemplate) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-template',
|
||||
CMS_API_BASE_URL + '/cms/cms-template',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsTemplate(data: CmsTemplate) {
|
||||
*/
|
||||
export async function removeCmsTemplate(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-template/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-template/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsTemplate(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-template/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-template/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsTemplate(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsTemplate(id: number) {
|
||||
const res = await request.get<ApiResult<CmsTemplate>>(
|
||||
MODULES_API_URL + '/cms/cms-template/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-template/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsWebsite, CmsWebsiteParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询网站信息记录表
|
||||
*/
|
||||
export async function pageCmsWebsite(params: CmsWebsiteParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsWebsite>>>(
|
||||
MODULES_API_URL + '/cms/cms-website/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-website/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsite(params: CmsWebsiteParam) {
|
||||
*/
|
||||
export async function listCmsWebsite(params?: CmsWebsiteParam) {
|
||||
const res = await request.get<ApiResult<CmsWebsite[]>>(
|
||||
MODULES_API_URL + '/cms/cms-website',
|
||||
CMS_API_BASE_URL + '/cms/cms-website',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsWebsite(params?: CmsWebsiteParam) {
|
||||
*/
|
||||
export async function addCmsWebsite(data: CmsWebsite) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website',
|
||||
CMS_API_BASE_URL + '/cms/cms-website',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsWebsite(data: CmsWebsite) {
|
||||
*/
|
||||
export async function updateCmsWebsite(data: CmsWebsite) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website',
|
||||
CMS_API_BASE_URL + '/cms/cms-website',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsite(data: CmsWebsite) {
|
||||
*/
|
||||
export async function removeCmsWebsite(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsWebsite(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-website/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsite(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsWebsite(id: number) {
|
||||
const res = await request.get<ApiResult<CmsWebsite>>(
|
||||
MODULES_API_URL + '/cms/cms-website/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -110,7 +110,7 @@ export async function getCmsWebsite(id: number) {
|
||||
*/
|
||||
export async function removeSiteInfoCache(key?: string) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website/clearSiteInfo/' + key
|
||||
CMS_API_BASE_URL + '/cms/cms-website/clearSiteInfo/' + key
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsWebsiteField, CmsWebsiteFieldParam, Config } from './model';
|
||||
import { MODULES_API_URL, TEMPLATE_ID } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL, TEMPLATE_ID } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询应用参数
|
||||
*/
|
||||
export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsWebsiteField>>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteField(params: CmsWebsiteFieldParam) {
|
||||
*/
|
||||
export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
|
||||
const res = await request.get<ApiResult<CmsWebsiteField[]>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsWebsiteField(params?: CmsWebsiteFieldParam) {
|
||||
*/
|
||||
export async function addCmsWebsiteField(data: CmsWebsiteField) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsWebsiteField(data: CmsWebsiteField) {
|
||||
*/
|
||||
export async function updateCmsWebsiteField(data: CmsWebsiteField) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteField(data: CmsWebsiteField) {
|
||||
*/
|
||||
export async function removeCmsWebsiteField(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeCmsWebsiteField(id?: number) {
|
||||
*/
|
||||
export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export async function removeBatchCmsWebsiteField(data: (number | undefined)[]) {
|
||||
*/
|
||||
export async function getCmsWebsiteField(id: number) {
|
||||
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -110,7 +110,7 @@ export async function getCmsWebsiteField(id: number) {
|
||||
*/
|
||||
export async function getCmsWebsiteFieldByCode(code: string) {
|
||||
const res = await request.get<ApiResult<CmsWebsiteField>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/getByCode/' + code
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/getByCode/' + code
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
@@ -123,7 +123,7 @@ export async function getCmsWebsiteFieldByCode(code: string) {
|
||||
*/
|
||||
export async function undeleteWebsiteField(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/website-field/undelete/' + id
|
||||
CMS_API_BASE_URL + '/cms/website-field/undelete/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -138,7 +138,7 @@ export async function importWebsiteField(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/import',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/import',
|
||||
formData
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -152,7 +152,7 @@ export async function importWebsiteField(file: File) {
|
||||
*/
|
||||
export async function configWebsiteField(params?: CmsWebsiteFieldParam) {
|
||||
const res = await request.get<ApiResult<Config>>(
|
||||
MODULES_API_URL + '/cms/cms-website-field/config',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-field/config',
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { CmsWebsiteSetting, CmsWebsiteSettingParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询网站设置
|
||||
*/
|
||||
export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
|
||||
const res = await request.get<ApiResult<PageResult<CmsWebsiteSetting>>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting/page',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageCmsWebsiteSetting(params: CmsWebsiteSettingParam) {
|
||||
*/
|
||||
export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
|
||||
const res = await request.get<ApiResult<CmsWebsiteSetting[]>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listCmsWebsiteSetting(params?: CmsWebsiteSettingParam) {
|
||||
*/
|
||||
export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||
*/
|
||||
export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateCmsWebsiteSetting(data: CmsWebsiteSetting) {
|
||||
*/
|
||||
export async function removeCmsWebsiteSetting(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -83,7 +83,7 @@ export async function removeBatchCmsWebsiteSetting(
|
||||
data: (number | undefined)[]
|
||||
) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting/batch',
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export async function removeBatchCmsWebsiteSetting(
|
||||
*/
|
||||
export async function getCmsWebsiteSetting(id: number) {
|
||||
const res = await request.get<ApiResult<CmsWebsiteSetting>>(
|
||||
MODULES_API_URL + '/cms/cms-website-setting/' + id
|
||||
CMS_API_BASE_URL + '/cms/cms-website-setting/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { Link, LinkParam } from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import { CMS_API_BASE_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询链接
|
||||
*/
|
||||
export async function pageLink(params: LinkParam) {
|
||||
const res = await request.get<ApiResult<PageResult<Link>>>(
|
||||
MODULES_API_URL + '/oa/link/page',
|
||||
CMS_API_BASE_URL + '/oa/link/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export async function pageLink(params: LinkParam) {
|
||||
*/
|
||||
export async function listLink(params?: LinkParam) {
|
||||
const res = await request.get<ApiResult<Link[]>>(
|
||||
MODULES_API_URL + '/oa/link',
|
||||
CMS_API_BASE_URL + '/oa/link',
|
||||
{
|
||||
params
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export async function listLink(params?: LinkParam) {
|
||||
*/
|
||||
export async function addLink(data: Link) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/oa/link',
|
||||
CMS_API_BASE_URL + '/oa/link',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -54,7 +54,7 @@ export async function addLink(data: Link) {
|
||||
*/
|
||||
export async function updateLink(data: Link) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/oa/link',
|
||||
CMS_API_BASE_URL + '/oa/link',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
@@ -68,7 +68,7 @@ export async function updateLink(data: Link) {
|
||||
*/
|
||||
export async function removeLink(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/oa/link/' + id
|
||||
CMS_API_BASE_URL + '/oa/link/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
@@ -81,7 +81,7 @@ export async function removeLink(id?: number) {
|
||||
*/
|
||||
export async function removeBatchLink(data: (number | undefined)[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/oa/link/batch',
|
||||
CMS_API_BASE_URL + '/oa/link/batch',
|
||||
{
|
||||
data
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export async function checkExistence(
|
||||
id?: number
|
||||
) {
|
||||
const res = await request.get<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/oa/link/existence',
|
||||
CMS_API_BASE_URL + '/oa/link/existence',
|
||||
{
|
||||
params: { field, value, id }
|
||||
}
|
||||
|
||||
+19
-1
@@ -1,7 +1,7 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import type { User } from '@/api/system/user/model';
|
||||
import type { UpdatePasswordParam, NoticeResult } from './model';
|
||||
import type { UpdatePasswordParam, ResetPasswordParam, NoticeResult } from './model';
|
||||
import { SERVER_API_URL } from '@/config/setting';
|
||||
import { Company } from '@/api/system/company/model';
|
||||
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
|
||||
@@ -108,6 +108,24 @@ export async function updatePassword(
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 找回/重置密码(未登录场景,登录页"忘记密码"入口)
|
||||
* 后端对应 POST /resetPassword,支持手机或邮箱两种方式
|
||||
*/
|
||||
export async function resetPassword(
|
||||
data: ResetPasswordParam
|
||||
): Promise<string> {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/resetPassword',
|
||||
data,
|
||||
{ headers: { 'X-Tenant-Skip': '1' } }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message ?? '密码重置成功';
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建完整网站并初始化
|
||||
*/
|
||||
|
||||
@@ -24,6 +24,23 @@ export interface UpdatePasswordParam {
|
||||
oldPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 找回/重置密码参数
|
||||
* 手机找回时传 phone + smsCode,邮箱找回时传 email + emailCode
|
||||
*/
|
||||
export interface ResetPasswordParam {
|
||||
/** 手机找回时必填 */
|
||||
phone?: string;
|
||||
smsCode?: string;
|
||||
/** 邮箱找回时必填 */
|
||||
email?: string;
|
||||
emailCode?: string;
|
||||
/** 新密码 */
|
||||
newPassword: string;
|
||||
/** 确认新密码 */
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知数据格式
|
||||
*/
|
||||
|
||||
@@ -9,6 +9,30 @@ import type {
|
||||
} from './model';
|
||||
import { User } from '@/api/system/user/model';
|
||||
import { SERVER_API_URL } from '@/config/setting';
|
||||
import type { LoginOutcome, TenantOption } from './model';
|
||||
|
||||
/**
|
||||
* 登录态落地:把后端返回的 access_token / 租户 / 用户 持久化到本地
|
||||
*/
|
||||
function saveLoginState(result: LoginResult | undefined, remember?: boolean) {
|
||||
if (!result) return;
|
||||
if (result.access_token) {
|
||||
setToken(result.access_token, remember);
|
||||
}
|
||||
const user = result.user;
|
||||
if (user) {
|
||||
if (user.tenantId != null) {
|
||||
try {
|
||||
localStorage.setItem('TenantId', String(user.tenantId));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
if (user.userId != null) {
|
||||
try {
|
||||
localStorage.setItem('UserId', String(user.userId));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
@@ -31,6 +55,48 @@ export async function login(data: LoginParam) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱登录
|
||||
*/
|
||||
export async function loginByEmail(data: LoginParam) {
|
||||
const res = await request.post<ApiResult<LoginResult>>(
|
||||
'https://server.websoft.top/api/loginByEmail',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
setToken(res.data.data?.access_token, data.remember);
|
||||
if (res.data.data?.user) {
|
||||
const user = res.data.data?.user;
|
||||
localStorage.setItem('TenantId', String(user.tenantId));
|
||||
localStorage.setItem('UserId', String(user.userId));
|
||||
}
|
||||
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户ID登录
|
||||
*/
|
||||
export async function loginByUserId(data: LoginParam) {
|
||||
const res = await request.post<ApiResult<LoginResult>>(
|
||||
'https://server.websoft.top/api/loginByUserId',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
setToken(res.data.data?.access_token, data.remember);
|
||||
if (res.data.data?.user) {
|
||||
const user = res.data.data?.user;
|
||||
localStorage.setItem('TenantId', String(user.tenantId));
|
||||
localStorage.setItem('UserId', String(user.userId));
|
||||
}
|
||||
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
@@ -62,6 +128,62 @@ export async function loginBySms(data: LoginParam) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 超管手机号短信登录
|
||||
* 后端对应 POST /loginBySuperAdminSms,参数为 { phone, code, tenantId }
|
||||
* - 单租户:直接返回 ok
|
||||
* - 多租户:返回 selectTenant,前端需调用 loginBySelectTenant 二次登录
|
||||
*/
|
||||
export async function loginBySuperAdminSms(
|
||||
data: LoginParam
|
||||
): Promise<LoginOutcome> {
|
||||
const res = await request.post<ApiResult<LoginResult>>(
|
||||
SERVER_API_URL + '/loginBySuperAdminSms',
|
||||
{ phone: data.phone, code: data.smsCode, smsCode: data.smsCode, tenantId: data.tenantId }
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
const result = res.data.data;
|
||||
if (result?.tenants && result.tenants.length > 0) {
|
||||
return {
|
||||
status: 'selectTenant',
|
||||
tenants: result.tenants as TenantOption[],
|
||||
message: res.data.message || '请选择要登录的租户'
|
||||
};
|
||||
}
|
||||
if (result?.user) {
|
||||
saveLoginState(result, data.remember);
|
||||
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message || '登录失败'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择租户后登录(多租户短信登录专用二次登录接口)
|
||||
* 后端对应 POST /loginBySelectTenant,参数为 { phone, code, tenantId }
|
||||
*/
|
||||
export async function loginBySelectTenant(data: {
|
||||
phone: string;
|
||||
code: string;
|
||||
tenantId: number;
|
||||
remember?: boolean;
|
||||
}): Promise<LoginOutcome> {
|
||||
const res = await request.post<ApiResult<LoginResult>>(
|
||||
SERVER_API_URL + '/loginBySelectTenant',
|
||||
{ phone: data.phone, code: data.code, smsCode: data.code, tenantId: data.tenantId }
|
||||
);
|
||||
if (res.data.code !== 0) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
const result = res.data.data;
|
||||
if (result?.user) {
|
||||
saveLoginState(result, data.remember);
|
||||
return { status: 'ok', user: result.user, message: res.data.message || '登录成功' };
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message || '登录失败'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*/
|
||||
@@ -76,6 +198,46 @@ export async function sendSmsCaptcha(data: LoginParam) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送短信验证码(后台管理员,跨租户:只要该手机号是超管或管理员即发送)
|
||||
* 后端对应 POST /sendSmsCaptchaByAdmin,参数为 { phone }
|
||||
* @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用)
|
||||
*/
|
||||
export async function sendSmsCaptchaByAdmin(
|
||||
data: LoginParam,
|
||||
options?: { skipTenant?: boolean }
|
||||
) {
|
||||
const res = await request.post<ApiResult<SmsCaptchaResult>>(
|
||||
SERVER_API_URL + '/sendSmsCaptchaByAdmin',
|
||||
data,
|
||||
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码(找回密码等登录前场景使用)
|
||||
* 后端对应 POST /sendEmailCaptcha,参数为 { email }
|
||||
* @param options.skipTenant 为 true 时不携带 tenantId 请求头(登录前无法确定租户时使用)
|
||||
*/
|
||||
export async function sendEmailCaptcha(
|
||||
data: LoginParam,
|
||||
options?: { skipTenant?: boolean }
|
||||
) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/sendEmailCaptcha',
|
||||
data,
|
||||
options?.skipTenant ? { headers: { 'X-Tenant-Skip': '1' } } : undefined
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,10 @@ import type { User } from '@/api/system/user/model';
|
||||
export interface LoginParam {
|
||||
// 账号
|
||||
username?: string;
|
||||
// 邮箱
|
||||
email?: string;
|
||||
// 用户ID
|
||||
userId?: string | number;
|
||||
// 密码
|
||||
password?: string;
|
||||
// 租户id
|
||||
@@ -15,6 +19,8 @@ export interface LoginParam {
|
||||
phone?: string;
|
||||
// 短信验证码
|
||||
code?: string;
|
||||
// 短信验证码(别名,与后端 LoginParam.code 对应)
|
||||
smsCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,8 +31,30 @@ export interface LoginResult {
|
||||
access_token?: string;
|
||||
// 用户信息
|
||||
user?: User;
|
||||
// 多租户场景下返回,此时 access_token / user 为 null
|
||||
tenants?: TenantOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 多租户登录时后端返回的租户选项
|
||||
*/
|
||||
export interface TenantOption {
|
||||
tenantId?: number;
|
||||
tenantName?: string;
|
||||
userId?: number;
|
||||
username?: string;
|
||||
nickname?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录接口统一返回结果
|
||||
* - ok:单租户直接登录成功
|
||||
* - selectTenant:多租户,需要前端弹出选择租户列表
|
||||
*/
|
||||
export type LoginOutcome =
|
||||
| { status: 'ok'; user: NonNullable<LoginResult['user']>; message: string }
|
||||
| { status: 'selectTenant'; tenants: TenantOption[]; message: string };
|
||||
|
||||
/**
|
||||
* 图形验证码返回结果
|
||||
*/
|
||||
|
||||
@@ -12,10 +12,14 @@ export interface ShopExpressTemplateDetail {
|
||||
templateId?: number;
|
||||
/** 计费方式: true=按件, false=按重量 */
|
||||
type?: boolean;
|
||||
/** 省份ID */
|
||||
/** 省份ID(旧单值字段,兼容历史数据) */
|
||||
provinceId?: number;
|
||||
/** 城市ID */
|
||||
/** 城市ID(旧单值字段,兼容历史数据) */
|
||||
cityId?: number;
|
||||
/** 配送区域模式: 0=全国, 1=指定配送(白名单), 2=指定不发货(黑名单) */
|
||||
regionMode?: number;
|
||||
/** 地区编码列表(6位行政区划码,逗号分隔),regionMode=1或2时有效 */
|
||||
regionIds?: string;
|
||||
/** 首件数量/重量 */
|
||||
firstNum?: string;
|
||||
/** 首件价格 */
|
||||
|
||||
@@ -31,8 +31,10 @@ export interface ShopGoods {
|
||||
canExpress?: number;
|
||||
// 商品分类
|
||||
category?: string;
|
||||
// 商品分类ID
|
||||
// 商品分类ID(主分类,向后兼容)
|
||||
categoryId?: number;
|
||||
// 商品分类ID列表(方案B:可归属多个分类)
|
||||
categoryIds?: number[];
|
||||
parentName?: string;
|
||||
categoryName?: string;
|
||||
// 一级分类
|
||||
@@ -44,9 +46,9 @@ export interface ShopGoods {
|
||||
commissionRole?: number;
|
||||
// 货架
|
||||
position?: string;
|
||||
// 套餐价格
|
||||
// 进货价格
|
||||
buyingPrice?: string;
|
||||
// 商品价格
|
||||
// 商城价
|
||||
price?: string;
|
||||
originPrice?: string;
|
||||
// 销售价格
|
||||
@@ -62,6 +64,10 @@ export interface ShopGoods {
|
||||
memberMarketPrice?: string;
|
||||
// 经销商价格
|
||||
dealerPrice?: string;
|
||||
// 价格区间最大值(多规格列表展示用,由后端按 SKU 计算;单规格/区间相等时为 undefined)
|
||||
priceMax?: string;
|
||||
salePriceMax?: string;
|
||||
dealerPriceMax?: string;
|
||||
// 有赠品
|
||||
buyingGift?: boolean;
|
||||
// 有赠品
|
||||
@@ -87,6 +93,8 @@ export interface ShopGoods {
|
||||
gainIntegral?: number;
|
||||
// 推荐
|
||||
recommend?: number;
|
||||
// 置顶(1置顶 0不置顶)
|
||||
isTop?: number;
|
||||
// 商户ID
|
||||
merchantId?: number;
|
||||
// 商户名称
|
||||
@@ -143,6 +151,8 @@ export interface ShopGoods {
|
||||
activityType?: number;
|
||||
// 配送方式:0送上门 1限自提
|
||||
deliveryMode?: number;
|
||||
// 所属专区(逗号分隔的 sectionId 字符串,对应 shop_goods.section_ids 列)
|
||||
sectionIds?: string;
|
||||
}
|
||||
|
||||
export interface BathSet {
|
||||
@@ -168,6 +178,8 @@ export interface ShopGoodsCollectParam {
|
||||
export interface ShopGoodsParam extends PageParam {
|
||||
parentId?: number;
|
||||
categoryId?: number;
|
||||
// 方案B:按多个分类查询,与单值 categoryId 二选一,多值时优先
|
||||
categoryIds?: number[];
|
||||
goodsId?: number;
|
||||
status?: number;
|
||||
goodsName?: string;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 用户商品浏览记录
|
||||
*/
|
||||
export interface ShopGoodsBrowse {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
goodsId?: number;
|
||||
tenantId?: number;
|
||||
merchantId?: number;
|
||||
/** 浏览次数 */
|
||||
visitCount?: number;
|
||||
/** 最后浏览时间 */
|
||||
lastVisitTime?: string;
|
||||
/** 浏览来源 */
|
||||
browseSource?: string;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
/** 商品名称(关联查询) */
|
||||
goodsName?: string;
|
||||
/** 商品封面图(关联查询) */
|
||||
goodsImage?: string;
|
||||
/** 商城价(关联查询) */
|
||||
price?: string;
|
||||
/** 市场价(关联查询) */
|
||||
salePrice?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览记录查询参数
|
||||
*/
|
||||
export interface ShopGoodsBrowseParam extends PageParam {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
goodsId?: number;
|
||||
merchantId?: number;
|
||||
tenantId?: number;
|
||||
browseSource?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
@@ -12,12 +12,14 @@ export interface ShopGoodsSku {
|
||||
sku?: string;
|
||||
// 商品图片
|
||||
image?: string;
|
||||
// 商品价格
|
||||
// 商城价
|
||||
price?: string;
|
||||
// 市场价格
|
||||
salePrice?: string;
|
||||
// 成本价
|
||||
cost?: string;
|
||||
// 会员价
|
||||
buyingPrice?: string;
|
||||
// 库存
|
||||
stock?: number;
|
||||
// sku编码
|
||||
|
||||
@@ -150,3 +150,72 @@ 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台今日数据概况(含未支付口径)
|
||||
* 字段对齐后端 ShopOrderOverviewVo:
|
||||
* - todaySales 今日销售额(元,含未支付,按 total_price 求和)
|
||||
* - todayOrders 今日订单数(含未支付)
|
||||
* - todayUsers 今日新增用户数
|
||||
* - couponUsedCount 今日使用优惠券的订单数
|
||||
*/
|
||||
export interface ShopOrderOverview {
|
||||
/** 今日销售额(元),含未支付,按订单面额 total_price 求和 */
|
||||
todaySales: number;
|
||||
/** 今日订单数,含未支付 */
|
||||
todayOrders: number;
|
||||
/** 今日新增用户数 */
|
||||
todayUsers: number;
|
||||
/** 今日使用优惠券的订单数 */
|
||||
couponUsedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计订单总金额(含未支付,按订单面额 total_price 求和)
|
||||
* 对标 /shop/shop-order/total,但不限制 pay_status,返回所有未删除订单的 total_price 之和
|
||||
*/
|
||||
export async function shopOrderTotalAll() {
|
||||
const res = await request.get<ApiResult<number>>(
|
||||
MODULES_API_URL + '/shop/shop-order/total-all'
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data ?? 0;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后台今日数据概况(含未支付口径)
|
||||
*/
|
||||
export async function shopOrderOverview() {
|
||||
const res = await request.get<ApiResult<ShopOrderOverview>>(
|
||||
MODULES_API_URL + '/shop/shop-order/overview'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
@@ -85,9 +85,9 @@ export interface ShopOrder {
|
||||
coachId?: number;
|
||||
// 支付的用户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;
|
||||
@@ -111,6 +111,14 @@ export interface ShopOrder {
|
||||
sendPhone?: string;
|
||||
// 发货地址
|
||||
sendAddress?: string;
|
||||
// 发货门店id(快递配送=发货仓库,商家送货=配送门店)
|
||||
sendStoreId?: number;
|
||||
// 配送店员id(商家送货时记录具体配送人员)
|
||||
deliveryUserId?: number;
|
||||
// 发货门店名称(后端联表返回)
|
||||
sendStoreName?: string;
|
||||
// 配送店员名称(后端联表返回)
|
||||
deliveryUserName?: string;
|
||||
// 优惠类型:0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡,5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
|
||||
couponType?: number;
|
||||
// 优惠说明
|
||||
@@ -129,6 +137,8 @@ export interface ShopOrder {
|
||||
invoiceNo?: string;
|
||||
// 支付时间
|
||||
payTime?: string;
|
||||
// 线下收款支付凭证(图片地址,确认线下收款时上传)
|
||||
paymentVoucher?: string;
|
||||
// 退款时间
|
||||
refundTime?: string;
|
||||
// 申请退款时间
|
||||
@@ -142,9 +152,9 @@ export interface ShopOrder {
|
||||
// 系统版本号 0当前版本 value=其他版本
|
||||
version?: number;
|
||||
// 买家备注
|
||||
buyerRemarks: undefined;
|
||||
buyerRemarks?: string;
|
||||
// 商家备注
|
||||
merchantRemarks: undefined;
|
||||
merchantRemarks?: string;
|
||||
// 用户id
|
||||
userId?: number;
|
||||
// 备注
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface ShopSetting {
|
||||
payAlipayPrivateKey?: string;
|
||||
payAlipayPublicKey?: string;
|
||||
payBalanceEnabled?: boolean | number;
|
||||
|
||||
salesAccumulateType?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type {
|
||||
HomeSection,
|
||||
HomeSectionParam,
|
||||
SectionPermission,
|
||||
SectionGoods,
|
||||
SectionUser
|
||||
} from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
* 分页查询首页专区
|
||||
*/
|
||||
export async function pageHomeSections(params: HomeSectionParam) {
|
||||
const res = await request.get<ApiResult<PageResult<HomeSection>>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/page',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询首页专区列表
|
||||
*/
|
||||
export async function listHomeSections(params?: HomeSectionParam) {
|
||||
const res = await request.get<ApiResult<HomeSection[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
{
|
||||
params
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据id查询首页专区
|
||||
*/
|
||||
export async function getHomeSection(id: number) {
|
||||
const res = await request.get<ApiResult<HomeSection>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加首页专区
|
||||
*/
|
||||
export async function addHomeSection(data: HomeSection) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改首页专区
|
||||
*/
|
||||
export async function updateHomeSection(data: HomeSection) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section',
|
||||
data
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除首页专区
|
||||
*/
|
||||
export async function removeHomeSection(id?: number) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询专区白名单用户
|
||||
*/
|
||||
export async function listSectionUsers(id: number) {
|
||||
const res = await request.get<ApiResult<SectionUser[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存专区白名单用户(覆盖式)
|
||||
*/
|
||||
export async function setSectionUsers(id: number, userIds: number[]) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
|
||||
userIds
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询专区商品分页
|
||||
*/
|
||||
export async function listSectionGoods(
|
||||
id: number,
|
||||
params?: { page?: number; limit?: number }
|
||||
) {
|
||||
const res = await request.get<ApiResult<PageResult<SectionGoods>>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
{
|
||||
params: { page: 1, limit: 10, ...params }
|
||||
}
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加商品到专区
|
||||
*/
|
||||
export async function addSectionGoods(id: number, goodsIds: number[]) {
|
||||
const res = await request.post<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
goodsIds
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从专区移除商品
|
||||
*/
|
||||
export async function removeSectionGoods(id: number, goodsIds: number[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/goods',
|
||||
{ data: goodsIds }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.message;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验专区访问权限
|
||||
*/
|
||||
export async function checkSectionPermission(sectionIds: number[]) {
|
||||
const res = await request.post<ApiResult<SectionPermission[]>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/check-permission',
|
||||
{ sectionIds }
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { PageParam } from '@/api';
|
||||
|
||||
/**
|
||||
* 首页专区
|
||||
*/
|
||||
export interface HomeSection {
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 租户id
|
||||
tenantId?: number;
|
||||
// 专区标题
|
||||
title?: string;
|
||||
// 专区副标题
|
||||
subtitle?: string;
|
||||
// banner 图片URL
|
||||
banner?: string;
|
||||
// 关联分类ID(逗号分隔字符串,可选)
|
||||
categoryIds?: string;
|
||||
// 样式类型 0默认
|
||||
styleType?: number;
|
||||
// 是否受限专区 0否 1是
|
||||
restricted?: number;
|
||||
// 排序号(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 状态 0正常 1禁用
|
||||
status?: number;
|
||||
// 生效开始时间
|
||||
startTime?: string;
|
||||
// 生效结束时间
|
||||
endTime?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 是否删除 0否 1是
|
||||
deleted?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页专区搜索条件
|
||||
*/
|
||||
export interface HomeSectionParam extends PageParam {
|
||||
// 专区标题(模糊)
|
||||
title?: string;
|
||||
// 状态 0正常 1禁用
|
||||
status?: number;
|
||||
// 是否受限专区 0/1
|
||||
restricted?: number;
|
||||
// 排序号
|
||||
sortNumber?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区权限校验结果
|
||||
*/
|
||||
export interface SectionPermission {
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 是否受限专区
|
||||
restricted?: boolean;
|
||||
// 当前用户是否允许访问
|
||||
allowed?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区商品(只读展示,字段按需取用)
|
||||
*/
|
||||
export interface SectionGoods {
|
||||
goodsId?: number;
|
||||
name?: string;
|
||||
image?: string;
|
||||
price?: string;
|
||||
salePrice?: string;
|
||||
stock?: number;
|
||||
status?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区白名单用户
|
||||
*/
|
||||
export interface SectionUser {
|
||||
id?: number;
|
||||
sectionId?: number;
|
||||
userId?: number;
|
||||
}
|
||||
@@ -181,7 +181,7 @@ export async function initialization(roleId?: number) {
|
||||
* 根据code查询租户
|
||||
*/
|
||||
export async function getByCode(code: string) {
|
||||
const res = await request.get<Tenant>(
|
||||
const res = await request.get<ApiResult<Tenant>>(
|
||||
SERVER_API_URL + '/system/tenant/getByCode/' + code
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
|
||||
@@ -170,7 +170,7 @@ export async function updateUserStatus(userId?: number, status?: number) {
|
||||
/**
|
||||
* 修改推荐状态
|
||||
*/
|
||||
export async function updateUserRecommend(form) {
|
||||
export async function updateUserRecommend(form: { userId: number; recommend: number }) {
|
||||
const res = await request.put<ApiResult<unknown>>(
|
||||
SERVER_API_URL + '/system/user/recommend',
|
||||
form
|
||||
|
||||
@@ -84,6 +84,12 @@
|
||||
label: '货到付款',
|
||||
key: 'codPay',
|
||||
icon: 'IdcardOutlined'
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '线下付款',
|
||||
key: 'offlinePay',
|
||||
icon: 'IdcardOutlined'
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
<!-- 省+市两级多选穿梭弹窗 -->
|
||||
<template>
|
||||
<div class="region-multi-select">
|
||||
<!-- 触发区 -->
|
||||
<div class="rms-trigger">
|
||||
<a-button class="rms-trigger-btn" @click="openModal">
|
||||
{{ triggerText }}
|
||||
</a-button>
|
||||
<span v-if="modelValue.length" class="rms-summary">
|
||||
已选 {{ modelValue.length }} 个城市
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="区域选择"
|
||||
width="760px"
|
||||
:mask-closable="false"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="rms-body">
|
||||
<!-- 左侧候选 -->
|
||||
<div class="rms-panel">
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
placeholder="输入地区关键字"
|
||||
allow-clear
|
||||
class="rms-search"
|
||||
/>
|
||||
<div class="rms-scroll">
|
||||
<div
|
||||
v-for="p in filteredProvinces"
|
||||
:key="p.value"
|
||||
class="rms-province"
|
||||
>
|
||||
<div class="rms-province-row">
|
||||
<span class="rms-expand" @click="toggleExpand(p.value)">
|
||||
{{ isExpanded(p.value) ? '▾' : '▸' }}
|
||||
</span>
|
||||
<span class="rms-label" @click="toggleExpand(p.value)">
|
||||
{{ p.label }}
|
||||
</span>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:disabled="isProvinceSelected(p)"
|
||||
@click="selectProvince(p)"
|
||||
>
|
||||
{{ isProvinceSelected(p) ? '已选' : '选择' }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="isExpanded(p.value)" class="rms-city-list">
|
||||
<div
|
||||
v-for="c in p.cities"
|
||||
:key="c.value"
|
||||
class="rms-city-row"
|
||||
>
|
||||
<span class="rms-label">{{ c.label }}</span>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
:disabled="isCitySelected(c.value)"
|
||||
@click="toggleCity(c.value)"
|
||||
>
|
||||
{{ isCitySelected(c.value) ? '已选' : '选择' }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧已选 -->
|
||||
<div class="rms-panel">
|
||||
<div class="rms-selected-title">
|
||||
已选择 ({{ selectedCities.length }})
|
||||
</div>
|
||||
<div class="rms-scroll">
|
||||
<div
|
||||
v-for="item in selectedList"
|
||||
:key="item.cityValue"
|
||||
class="rms-selected-item"
|
||||
>
|
||||
<span class="rms-selected-label">
|
||||
{{ item.provinceLabel }} / {{ item.cityLabel }}
|
||||
</span>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
danger
|
||||
@click="removeCity(item.cityValue)"
|
||||
>
|
||||
取消
|
||||
</a-button>
|
||||
</div>
|
||||
<a-empty
|
||||
v-if="!selectedCities.length"
|
||||
:image="simpleImage"
|
||||
description="暂未选择"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="selectAll">全部选择</a-button>
|
||||
<span class="rms-footer-spacer"></span>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" @click="handleConfirm">确认</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { Empty } from 'ant-design-vue';
|
||||
import { getRegionsData } from '@/components/RegionsSelect/load-data';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 已选城市ID数组(逗号分隔的行政区码) */
|
||||
value?: string[];
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{ value: () => [], placeholder: '选择配送地区' }
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', value: string[]): void;
|
||||
}>();
|
||||
|
||||
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const visible = ref(false);
|
||||
const keyword = ref('');
|
||||
const expandedSet = ref<Set<string>>(new Set());
|
||||
const selectedSet = ref<Set<string>>(new Set());
|
||||
|
||||
interface CityItem {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
interface ProvinceItem {
|
||||
value: string;
|
||||
label: string;
|
||||
cities: CityItem[];
|
||||
}
|
||||
|
||||
const provinceList = ref<ProvinceItem[]>([]);
|
||||
const cityToProvince = ref<Record<string, string>>({});
|
||||
const labelMap = ref<Record<string, string>>({});
|
||||
|
||||
// 加载省+市两级数据(丢弃区级)
|
||||
getRegionsData().then((data) => {
|
||||
const list: ProvinceItem[] = [];
|
||||
const c2p: Record<string, string> = {};
|
||||
const lm: Record<string, string> = {};
|
||||
(data ?? []).forEach((p) => {
|
||||
lm[p.value] = p.label;
|
||||
const cities: CityItem[] = (p.children ?? []).map((c) => {
|
||||
c2p[c.value] = p.value;
|
||||
lm[c.value] = c.label;
|
||||
return { value: c.value, label: c.label };
|
||||
});
|
||||
list.push({ value: p.value, label: p.label, cities });
|
||||
});
|
||||
provinceList.value = list;
|
||||
cityToProvince.value = c2p;
|
||||
labelMap.value = lm;
|
||||
});
|
||||
|
||||
const modelValue = computed(() => props.value ?? []);
|
||||
|
||||
const triggerText = computed(() =>
|
||||
modelValue.value.length
|
||||
? `已选 ${modelValue.value.length} 个城市`
|
||||
: props.placeholder
|
||||
);
|
||||
|
||||
const isExpanded = (v: string) => expandedSet.value.has(v);
|
||||
const toggleExpand = (v: string) => {
|
||||
const s = new Set(expandedSet.value);
|
||||
if (s.has(v)) {
|
||||
s.delete(v);
|
||||
} else {
|
||||
s.add(v);
|
||||
}
|
||||
expandedSet.value = s;
|
||||
};
|
||||
|
||||
const isCitySelected = (v: string) => selectedSet.value.has(v);
|
||||
const isProvinceSelected = (p: ProvinceItem) =>
|
||||
p.cities.length > 0 && p.cities.every((c) => selectedSet.value.has(c.value));
|
||||
|
||||
const filteredProvinces = computed(() => {
|
||||
const kw = keyword.value.trim();
|
||||
if (!kw) {
|
||||
return provinceList.value;
|
||||
}
|
||||
return provinceList.value
|
||||
.map((p) => {
|
||||
const matchProvince = p.label.includes(kw);
|
||||
if (matchProvince) {
|
||||
return p;
|
||||
}
|
||||
const cities = p.cities.filter((c) => c.label.includes(kw));
|
||||
if (cities.length) {
|
||||
return { ...p, cities };
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as ProvinceItem[];
|
||||
});
|
||||
|
||||
const selectedCities = computed(() => Array.from(selectedSet.value));
|
||||
|
||||
const selectedList = computed(() =>
|
||||
selectedCities.value.map((cv) => ({
|
||||
cityValue: cv,
|
||||
cityLabel: labelMap.value[cv] || cv,
|
||||
provinceValue: cityToProvince.value[cv],
|
||||
provinceLabel: labelMap.value[cityToProvince.value[cv]] || ''
|
||||
}))
|
||||
);
|
||||
|
||||
const selectProvince = (p: ProvinceItem) => {
|
||||
const s = new Set(selectedSet.value);
|
||||
p.cities.forEach((c) => s.add(c.value));
|
||||
selectedSet.value = s;
|
||||
};
|
||||
|
||||
const toggleCity = (cv: string) => {
|
||||
const s = new Set(selectedSet.value);
|
||||
if (s.has(cv)) {
|
||||
s.delete(cv);
|
||||
} else {
|
||||
s.add(cv);
|
||||
}
|
||||
selectedSet.value = s;
|
||||
};
|
||||
|
||||
const removeCity = (cv: string) => {
|
||||
const s = new Set(selectedSet.value);
|
||||
s.delete(cv);
|
||||
selectedSet.value = s;
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
const s = new Set<string>();
|
||||
provinceList.value.forEach((p) =>
|
||||
p.cities.forEach((c) => s.add(c.value))
|
||||
);
|
||||
selectedSet.value = s;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
selectedSet.value = new Set(modelValue.value);
|
||||
keyword.value = '';
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('update:value', selectedCities.value);
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 外部清空时同步内部选中态
|
||||
watch(
|
||||
() => props.value,
|
||||
(v) => {
|
||||
if (!visible.value) {
|
||||
selectedSet.value = new Set(v ?? []);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.region-multi-select {
|
||||
width: 100%;
|
||||
}
|
||||
.rms-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.rms-trigger-btn {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
.rms-summary {
|
||||
color: var(--ant-color-text-secondary, #999);
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.rms-body {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 420px;
|
||||
}
|
||||
.rms-panel {
|
||||
flex: 1;
|
||||
border: 1px solid var(--ant-color-border, #f0f0f0);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rms-search {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.rms-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.rms-province-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.rms-expand {
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--ant-color-text-secondary, #999);
|
||||
}
|
||||
.rms-label {
|
||||
flex: 1;
|
||||
cursor: default;
|
||||
}
|
||||
.rms-city-list {
|
||||
padding-left: 24px;
|
||||
}
|
||||
.rms-city-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.rms-selected-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.rms-selected-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px dashed var(--ant-color-border, #f5f5f5);
|
||||
}
|
||||
.rms-selected-label {
|
||||
flex: 1;
|
||||
}
|
||||
.rms-footer-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,205 @@
|
||||
<template>
|
||||
<div class="rich-text-editor">
|
||||
<!-- 编辑器尚未初始化时的占位 -->
|
||||
<div
|
||||
v-if="!editorReady"
|
||||
:style="{ height: editorHeight + 'px', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#999' }"
|
||||
>
|
||||
编辑器加载中...
|
||||
</div>
|
||||
<template v-else>
|
||||
<component
|
||||
:is="Toolbar"
|
||||
v-if="editorRef"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
style="border-bottom: 1px solid #d9d9d9"
|
||||
/>
|
||||
<component
|
||||
:is="Editor"
|
||||
:defaultConfig="editorConfig"
|
||||
:modelValue="modelValue"
|
||||
@onCreated="handleCreated"
|
||||
@onChange="handleChange"
|
||||
:style="{ height: editorHeight + 'px', overflowY: 'hidden' }"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref, shallowRef } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const editorReady = ref(false)
|
||||
const editorRef = shallowRef<any>()
|
||||
const Toolbar = shallowRef<any>(null)
|
||||
const Editor = shallowRef<any>(null)
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
/** 编辑器高度(px),默认 380 */
|
||||
height?: number
|
||||
/** 图片上传大小限制(MB),默认 5 */
|
||||
maxImageSize?: number
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
placeholder: '请输入正文...',
|
||||
height: 380,
|
||||
maxImageSize: 5
|
||||
}
|
||||
)
|
||||
|
||||
const editorHeight = computed(() => props.height)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
// 工具栏配置
|
||||
const toolbarConfig: Record<string, any> = {
|
||||
// 视频上传未配置,仍排除;全屏按钮保留(更自然)
|
||||
excludeKeys: ['group-video']
|
||||
}
|
||||
|
||||
/** 上传图片并返回 URL */
|
||||
async function doUploadImage(file: File): Promise<string> {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error('只能上传图片文件')
|
||||
return ''
|
||||
}
|
||||
if (file.size / 1024 / 1024 > props.maxImageSize) {
|
||||
message.error(`图片大小不能超过 ${props.maxImageSize}MB`)
|
||||
return ''
|
||||
}
|
||||
// 动态导入 uploadFile 避免循环依赖
|
||||
const { uploadFile } = await import('@/api/system/file')
|
||||
const res = await uploadFile(file)
|
||||
return res.url || res.path || ''
|
||||
}
|
||||
|
||||
/** 在编辑器当前光标处插入图片 */
|
||||
function insertEditorImage(editor: any, url: string, alt = '') {
|
||||
if (!editor || !url) return
|
||||
try {
|
||||
if (typeof editor.focus === 'function') editor.focus()
|
||||
if (typeof editor.insertNode === 'function') {
|
||||
editor.insertNode({
|
||||
type: 'image',
|
||||
src: url,
|
||||
alt,
|
||||
href: url,
|
||||
children: [{ text: '' }]
|
||||
})
|
||||
} else if (typeof editor.dangerouslyInsertHtml === 'function') {
|
||||
editor.dangerouslyInsertHtml(`<img src="${url}" alt="${alt}" style="max-width:100%;"/>`)
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('图片插入失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器配置
|
||||
const editorConfig: Record<string, any> = {
|
||||
placeholder: props.placeholder,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
async customUpload(file: File, insertFn: (url: string, alt?: string, href?: string) => void) {
|
||||
try {
|
||||
const url = await doUploadImage(file)
|
||||
if (url) insertFn(url, file.name || 'image', url)
|
||||
} catch (e: any) {
|
||||
message.error(e?.message || '图片上传失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
customPaste(editor: any, event: ClipboardEvent) {
|
||||
const items = event.clipboardData?.items
|
||||
if (!items || items.length === 0) return true
|
||||
|
||||
const imageFiles: File[] = []
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (!item) continue
|
||||
if (item.kind === 'file' && item.type.startsWith('image/')) {
|
||||
const file = item.getAsFile()
|
||||
if (file) imageFiles.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (imageFiles.length === 0) return true
|
||||
|
||||
// 阻止默认粘贴,避免把 base64 大图片直接塞进编辑器
|
||||
event.preventDefault()
|
||||
|
||||
// 异步上传所有粘贴的图片并插入
|
||||
Promise.all(
|
||||
imageFiles.map(async (file) => {
|
||||
const url = await doUploadImage(file)
|
||||
return { file, url }
|
||||
})
|
||||
).then((results) => {
|
||||
results.forEach(({ url }) => {
|
||||
if (url) insertEditorImage(editor, url, '粘贴图片')
|
||||
})
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreated(editor: any) {
|
||||
editorRef.value = editor
|
||||
}
|
||||
|
||||
function handleChange(editor: any) {
|
||||
emit('update:modelValue', editor.getHtml())
|
||||
}
|
||||
|
||||
/** 在光标处插入 HTML(供「插入附件」等外部入口调用) */
|
||||
function insertHtml(html: string) {
|
||||
const editor = editorRef.value
|
||||
if (editor && typeof editor.dangerouslyInsertHtml === 'function') {
|
||||
editor.dangerouslyInsertHtml(html)
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ insertHtml })
|
||||
|
||||
// 组件销毁时销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor && typeof editor.destroy === 'function') {
|
||||
try {
|
||||
editor.destroy()
|
||||
} catch {
|
||||
// 已销毁或实例无效时忽略
|
||||
}
|
||||
}
|
||||
editorRef.value = null
|
||||
})
|
||||
|
||||
// 客户端才加载 WangEditor(CSS + 组件)
|
||||
onMounted(async () => {
|
||||
// 并行加载 CSS 和组件
|
||||
const [cssModule, wangEditorModule] = await Promise.all([
|
||||
import('@wangeditor/editor/dist/css/style.css'),
|
||||
import('@wangeditor/editor-for-vue')
|
||||
])
|
||||
Toolbar.value = wangEditorModule.Toolbar
|
||||
Editor.value = wangEditorModule.Editor
|
||||
editorReady.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-text-editor {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -21,6 +21,9 @@ export const FILE_SERVER =
|
||||
*/
|
||||
// 接口地址
|
||||
export const API_BASE_URL: string = import.meta.env.VITE_API_URL;
|
||||
// CMS 模块接口地址(cms 已迁移到 mp-java 并独立部署,调用指向远程服务)
|
||||
export const CMS_API_BASE_URL: string =
|
||||
import.meta.env.VITE_CMS_API_URL || 'https://cms-api.websoft.top/api';
|
||||
export const PROJECT_NAME: string = import.meta.env.VITE_APP_NAME;
|
||||
// 不显示侧栏的路由
|
||||
export const HIDE_SIDEBARS: string[] = ['/home'];
|
||||
|
||||
@@ -10,5 +10,36 @@ export default {
|
||||
loading: 'loading',
|
||||
oldPassword: 'Old',
|
||||
newPassword: 'New',
|
||||
confirm: 'Confirm'
|
||||
confirm: 'Confirm',
|
||||
forgetPage: {
|
||||
title: 'Forgot Password',
|
||||
subTitle: 'Enter your account info to reset the password',
|
||||
tabEmail: 'By Email',
|
||||
tabPhone: 'By Phone',
|
||||
email: 'Email',
|
||||
phone: 'Phone',
|
||||
emailCode: 'Email Code',
|
||||
code: 'SMS Code',
|
||||
getCode: 'Get Code',
|
||||
sentEmail: 'Verification code sent, please check your email',
|
||||
sentPhone: 'Verification code sent',
|
||||
newPassword: 'New Password',
|
||||
confirmPassword: 'Confirm Password',
|
||||
reset: 'Reset Password',
|
||||
backLogin: 'Back to Login',
|
||||
success: 'Password reset successfully, please login with the new password',
|
||||
emailPlaceholder: 'Please enter your registered email',
|
||||
phonePlaceholder: 'Please enter your registered phone',
|
||||
codePlaceholder: 'Please enter the code',
|
||||
passwordPlaceholder: 'At least 8 chars, must contain letters and numbers',
|
||||
confirmPlaceholder: 'Please enter the password again',
|
||||
newPasswordRule: 'Password must be at least 8 chars with letters and numbers',
|
||||
confirmRule: 'The two passwords do not match',
|
||||
emailRule: 'Please enter a valid email address',
|
||||
phoneRule: 'Please enter a valid phone number',
|
||||
codeRule: 'Please enter the code',
|
||||
captchaTitle: 'Send Code',
|
||||
captchaPlaceholder: 'Please enter the captcha',
|
||||
sendNow: 'Send Now'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const storeName = localStorage.getItem('StoreName') || 'WebSoft Inc';
|
||||
/* 主框架 */
|
||||
export default {
|
||||
system: '小程序开发',
|
||||
system: '小程序商城',
|
||||
home: '主页',
|
||||
header: {
|
||||
profile: '个人资料',
|
||||
|
||||
@@ -10,5 +10,36 @@ export default {
|
||||
loading: '登录中',
|
||||
oldPassword: '旧密码',
|
||||
newPassword: '新密码',
|
||||
confirm: '确认密码'
|
||||
confirm: '确认密码',
|
||||
forgetPage: {
|
||||
title: '忘记密码',
|
||||
subTitle: '请输入账号信息以重置密码',
|
||||
tabEmail: '邮箱找回',
|
||||
tabPhone: '手机找回',
|
||||
email: '邮箱',
|
||||
phone: '手机号',
|
||||
emailCode: '邮箱验证码',
|
||||
code: '短信验证码',
|
||||
getCode: '获取验证码',
|
||||
sentEmail: '验证码已发送,请查收邮箱',
|
||||
sentPhone: '验证码已发送',
|
||||
newPassword: '新密码',
|
||||
confirmPassword: '确认新密码',
|
||||
reset: '重置密码',
|
||||
backLogin: '返回登录',
|
||||
success: '密码重置成功,请使用新密码登录',
|
||||
emailPlaceholder: '请输入注册邮箱',
|
||||
phonePlaceholder: '请输入注册手机号',
|
||||
codePlaceholder: '请输入验证码',
|
||||
passwordPlaceholder: '至少 8 位,需包含字母和数字',
|
||||
confirmPlaceholder: '请再次输入新密码',
|
||||
newPasswordRule: '新密码至少 8 位,需包含字母和数字',
|
||||
confirmRule: '两次输入的密码不一致',
|
||||
emailRule: '请输入正确的邮箱地址',
|
||||
phoneRule: '请输入正确的手机号',
|
||||
codeRule: '请输入验证码',
|
||||
captchaTitle: '发送验证码',
|
||||
captchaPlaceholder: '请输入图形验证码',
|
||||
sendNow: '立即发送'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<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();
|
||||
|
||||
@@ -50,11 +50,11 @@ export const routes = [
|
||||
component: () => import('@/views/passport/merchant/success.vue'),
|
||||
meta: { title: '申请提交成功' }
|
||||
},
|
||||
// {
|
||||
// path: '/forget',
|
||||
// component: () => import('@/views/passport/forget/index.vue'),
|
||||
// meta: { title: '忘记密码' }
|
||||
// },
|
||||
{
|
||||
path: '/forget',
|
||||
component: () => import('@/views/passport/forget/index.vue'),
|
||||
meta: { title: '忘记密码' }
|
||||
},
|
||||
// {
|
||||
// path: '/wx-work-login',
|
||||
// component: () => import('@/views/passport/wx-work/index.vue'),
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
+49
-7
@@ -41,6 +41,26 @@ export function ensureFullUrl(url: string): string {
|
||||
return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除 URL 中已有的 x-oss-process 参数(无论其在 query 中的位置)
|
||||
*
|
||||
* 阿里云 OSS 的图片处理参数通常以 `x-oss-process=image/...` 形式附加在 query 中,
|
||||
* 可能位于首个或后续参数位。本函数精确剔除该参数,保留其余 query 参数。
|
||||
*
|
||||
* @param url 完整图片 URL
|
||||
* @returns 已剥离 x-oss-process 的 URL
|
||||
*/
|
||||
function removeOssProcess(url: string): string {
|
||||
const qIndex = url.indexOf('?');
|
||||
if (qIndex === -1) return url;
|
||||
const base = url.slice(0, qIndex);
|
||||
const params = url
|
||||
.slice(qIndex + 1)
|
||||
.split('&')
|
||||
.filter((p) => !p.startsWith('x-oss-process='));
|
||||
return params.length ? `${base}?${params.join('&')}` : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OSS 压缩后的图片 URL
|
||||
*
|
||||
@@ -49,28 +69,50 @@ export function ensureFullUrl(url: string): string {
|
||||
*
|
||||
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
|
||||
*
|
||||
* 优先级:以调用方指定的 width/quality 为准。若 URL 已自带 x-oss-process
|
||||
* (接口已返回压缩参数),会先剥离再按本函数参数重新拼接,确保调用方参数生效。
|
||||
*
|
||||
* @param url 原始图片 URL
|
||||
* @param options 压缩选项
|
||||
* @param options 压缩选项(width/quality 不传则使用默认值)
|
||||
* @returns 处理后的图片 URL
|
||||
*/
|
||||
// 缓存已计算的压缩 URL,避免列表重渲染时(勾选、hover、滚动)重复拼接字符串
|
||||
const compressedUrlCache = new Map<string, string>();
|
||||
|
||||
export function getCompressedImageUrl(
|
||||
url: string,
|
||||
options?: ImageCompressOptions
|
||||
): string {
|
||||
if (!url) return '';
|
||||
|
||||
// 命中缓存直接返回,省去重复计算
|
||||
const cacheKey = options ? `${url}|${JSON.stringify(options)}` : url;
|
||||
const cached = compressedUrlCache.get(cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
// 先补全为完整 URL
|
||||
const fullUrl = ensureFullUrl(url);
|
||||
if (!fullUrl) return '';
|
||||
if (!fullUrl) {
|
||||
compressedUrlCache.set(cacheKey, '');
|
||||
return '';
|
||||
}
|
||||
|
||||
const { width, quality, enabled } = { ...DEFAULT_OPTIONS, ...options };
|
||||
|
||||
// 未启用压缩,原样返回
|
||||
if (!enabled) return fullUrl;
|
||||
if (!enabled) {
|
||||
compressedUrlCache.set(cacheKey, fullUrl);
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
// 已包含 OSS 处理参数,避免重复拼接
|
||||
if (fullUrl.includes('x-oss-process')) return fullUrl;
|
||||
// 优先使用调用方指定的 width/quality:
|
||||
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
|
||||
const baseUrl = removeOssProcess(fullUrl);
|
||||
|
||||
// 已有其他 query 参数用 & 拼接,否则用 ?
|
||||
const separator = fullUrl.includes('?') ? '&' : '?';
|
||||
const separator = baseUrl.includes('?') ? '&' : '?';
|
||||
|
||||
return `${fullUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
||||
const result = `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
|
||||
compressedUrlCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ service.interceptors.request.use(
|
||||
}
|
||||
// 获取租户ID
|
||||
if (config.headers) {
|
||||
// 登录前场景(找回密码等)由调用方显式设置 X-Tenant-Skip,
|
||||
// 此时不做任何租户/企业/商户/域名头注入,避免串租户
|
||||
if (config.headers['X-Tenant-Skip']) {
|
||||
return config;
|
||||
}
|
||||
// 附加企业ID
|
||||
const companyId = localStorage.getItem('CompanyId');
|
||||
if (companyId) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -47,7 +47,11 @@
|
||||
v-for="(item, index) in record.imageList"
|
||||
:key="index"
|
||||
>
|
||||
<a-image :src="item.url" :width="80" />
|
||||
<a-image
|
||||
:src="getCompressedImageUrl(item.url, { width: 160, quality: 90 })"
|
||||
:preview="{ src: item.url }"
|
||||
:width="80"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="record.type == 4">
|
||||
{{ record.comments }}
|
||||
@@ -113,6 +117,7 @@
|
||||
import { listCmsNavigation } from '@/api/cms/cmsNavigation';
|
||||
import { getPageTitle } from '@/utils/common';
|
||||
import { getTenantId } from '@/utils/domain';
|
||||
import { getCompressedImageUrl } from '@/utils/image';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
@@ -80,58 +80,22 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容详情" name="content">
|
||||
<!-- 富文本编辑器 -->
|
||||
<div v-if="editor == 1">
|
||||
<tinymce-editor
|
||||
ref="editorRef"
|
||||
class="editor-content"
|
||||
v-model:value="content"
|
||||
:disabled="disabled"
|
||||
:init="config"
|
||||
placeholder="支持直接粘贴或拖拽图片,也可点击工具栏图片按钮从文件库选择"
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡
|
||||
提示:工具栏"图片"按钮从图片库选择,"上传"按钮快速上传图片;"视频"按钮从视频库选择,"上传视频"按钮快速上传视频;"一键排版"按钮自动优化文章格式;"首行缩进"按钮切换段落缩进
|
||||
</div>
|
||||
<RichTextEditor
|
||||
ref="editorRef"
|
||||
v-model="content"
|
||||
:disabled="disabled"
|
||||
:height="500"
|
||||
placeholder="支持直接粘贴或拖拽图片,也可点击下方按钮从图片库选择"
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡 提示:支持直接粘贴/拖拽上传图片;点击下方按钮从图片库/视频库选择,或使用「排版」「首行缩进」优化文章
|
||||
</div>
|
||||
|
||||
<!-- Markdown编辑器 -->
|
||||
<div v-if="editor == 2">
|
||||
<!-- 📝 Markdown编辑器工具栏扩展 -->
|
||||
<div class="markdown-toolbar-extension">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openMarkdownImageSelector"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
📷 从图片库选择
|
||||
</a-button>
|
||||
<a-button
|
||||
type="default"
|
||||
size="small"
|
||||
@click="openMarkdownVideoSelector"
|
||||
style="margin-right: 8px"
|
||||
>
|
||||
🎬 从视频库选择
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<MdEditor
|
||||
v-model="content"
|
||||
:disabled="disabled"
|
||||
height="500px"
|
||||
:placeholder="'请输入Markdown内容...'"
|
||||
:toolbars="markdownToolbars"
|
||||
:onUploadImg="onMarkdownUploadImg"
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡
|
||||
提示:支持Markdown语法,可以使用工具栏按钮从文件库选择图片/视频,也可以直接拖拽上传文件
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-space class="editor-toolbar-ext mt-2" wrap>
|
||||
<a-button size="small" @click="openImageSelector">📷 从图片库选择</a-button>
|
||||
<a-button size="small" @click="openVideoSelector">🎬 从视频库选择</a-button>
|
||||
<a-button size="small" @click="handleAutoFormat">🎨 一键排版</a-button>
|
||||
<a-button size="small" @click="toggleParagraphIndent">📐 首行缩进</a-button>
|
||||
</a-space>
|
||||
<a-space
|
||||
class="py-2 flex items-center text-gray-400"
|
||||
v-if="lang == 'zh_CN'"
|
||||
@@ -317,11 +281,8 @@
|
||||
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
|
||||
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
import TinymceEditor from '@/components/TinymceEditor/index.vue';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import { uploadOss } from '@/api/system/file';
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue';
|
||||
import markdownit from 'markdown-it';
|
||||
import { CmsArticleCategory } from '@/api/cms/cmsArticleCategory/model';
|
||||
import { CmsNavigation } from '@/api/cms/cmsNavigation/model';
|
||||
import SourceSelect from '@/views/cms/cmsArticle/dictionary/source-select.vue';
|
||||
@@ -335,8 +296,6 @@
|
||||
const useForm = Form.useForm;
|
||||
const setting = useWebsiteSettingStore();
|
||||
const { locale } = useI18n();
|
||||
const editor = ref<number>(1);
|
||||
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
@@ -486,138 +445,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 📝 编辑器类型切换处理
|
||||
const onEditorTypeChange = (e: any) => {
|
||||
const newEditorType = e.target.value;
|
||||
const oldEditorType = editor.value;
|
||||
|
||||
// 如果编辑器类型没有变化,直接返回
|
||||
if (newEditorType === oldEditorType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果当前有内容,提示用户确认切换
|
||||
if (
|
||||
content.value &&
|
||||
content.value.trim() !== '' &&
|
||||
content.value !== '<p><br></p>'
|
||||
) {
|
||||
Modal.confirm({
|
||||
title: '🔄 切换编辑器类型',
|
||||
content: '切换编辑器类型可能会影响内容格式,是否继续?',
|
||||
okText: '确认切换',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
performEditorSwitch(newEditorType, oldEditorType);
|
||||
},
|
||||
onCancel: () => {
|
||||
// 恢复原来的编辑器类型
|
||||
editor.value = oldEditorType;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 没有内容直接切换
|
||||
performEditorSwitch(newEditorType, oldEditorType);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行编辑器切换
|
||||
const performEditorSwitch = (newType: number, oldType: number) => {
|
||||
try {
|
||||
editor.value = newType;
|
||||
|
||||
// 保存编辑器类型偏好到本地存储
|
||||
localStorage.setItem('cms_article_editor_preference', newType.toString());
|
||||
|
||||
// 显示切换成功提示
|
||||
const editorName = newType === 1 ? '富文本编辑器' : 'Markdown编辑器';
|
||||
message.success({
|
||||
content: `✅ 已切换到${editorName}`,
|
||||
duration: 2
|
||||
});
|
||||
|
||||
// 如果有内容,尝试进行格式转换
|
||||
if (content.value && content.value.trim() !== '') {
|
||||
convertContentFormat(oldType, newType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('编辑器切换失败:', error);
|
||||
message.error('编辑器切换失败,请重试');
|
||||
// 恢复原来的编辑器类型
|
||||
editor.value = oldType;
|
||||
}
|
||||
};
|
||||
|
||||
// 内容格式转换
|
||||
const convertContentFormat = (fromType: number, toType: number) => {
|
||||
if (!content.value) return;
|
||||
|
||||
try {
|
||||
if (fromType === 1 && toType === 2) {
|
||||
// 富文本转Markdown(简单转换)
|
||||
let markdownContent = content.value
|
||||
.replace(/<h1[^>]*>(.*?)<\/h1>/gi, '# $1\n\n')
|
||||
.replace(/<h2[^>]*>(.*?)<\/h2>/gi, '## $1\n\n')
|
||||
.replace(/<h3[^>]*>(.*?)<\/h3>/gi, '### $1\n\n')
|
||||
.replace(/<h4[^>]*>(.*?)<\/h4>/gi, '#### $1\n\n')
|
||||
.replace(/<h5[^>]*>(.*?)<\/h5>/gi, '##### $1\n\n')
|
||||
.replace(/<h6[^>]*>(.*?)<\/h6>/gi, '###### $1\n\n')
|
||||
.replace(/<strong[^>]*>(.*?)<\/strong>/gi, '**$1**')
|
||||
.replace(/<b[^>]*>(.*?)<\/b>/gi, '**$1**')
|
||||
.replace(/<em[^>]*>(.*?)<\/em>/gi, '*$1*')
|
||||
.replace(/<i[^>]*>(.*?)<\/i>/gi, '*$1*')
|
||||
.replace(/<code[^>]*>(.*?)<\/code>/gi, '`$1`')
|
||||
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '[$2]($1)')
|
||||
.replace(
|
||||
/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi,
|
||||
''
|
||||
)
|
||||
.replace(/<img[^>]*src="([^"]*)"[^>]*>/gi, '')
|
||||
.replace(/<p[^>]*>(.*?)<\/p>/gi, '$1\n\n')
|
||||
.replace(/<br[^>]*>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '') // 移除其他HTML标签
|
||||
.replace(/\n{3,}/g, '\n\n') // 清理多余换行
|
||||
.trim();
|
||||
|
||||
content.value = markdownContent;
|
||||
message.info('已尝试将富文本内容转换为Markdown格式');
|
||||
} else if (fromType === 2 && toType === 1) {
|
||||
// Markdown转富文本(基本转换)
|
||||
let htmlContent = content.value
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^#### (.*$)/gim, '<h4>$1</h4>')
|
||||
.replace(/^##### (.*$)/gim, '<h5>$1</h5>')
|
||||
.replace(/^###### (.*$)/gim, '<h6>$1</h6>')
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(
|
||||
/!\[([^\]]*)\]\(([^)]+)\)/g,
|
||||
'<img src="$2" alt="$1" style="max-width: 100%;" />'
|
||||
)
|
||||
.replace(/\n\n/g, '</p><p>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
// 包装在段落标签中
|
||||
if (htmlContent && !htmlContent.startsWith('<')) {
|
||||
htmlContent = '<p>' + htmlContent + '</p>';
|
||||
}
|
||||
|
||||
content.value = htmlContent;
|
||||
message.info('已尝试将Markdown内容转换为富文本格式');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('内容格式转换失败:', error);
|
||||
message.warning('内容格式转换失败,请手动调整内容格式');
|
||||
}
|
||||
};
|
||||
|
||||
const onEditor = (value: any) => {
|
||||
editor.value = value.target.value;
|
||||
};
|
||||
|
||||
const onComments = () => {
|
||||
if (form.comments == undefined) {
|
||||
@@ -737,7 +564,16 @@
|
||||
});
|
||||
};
|
||||
|
||||
const editorRef = ref<InstanceType<typeof TinymceEditor> | null>(null);
|
||||
const editorRef = ref<any>(null);
|
||||
|
||||
// markdown-it 实例:编辑回显时把老 Markdown 内容转成 HTML 喂给富文本编辑器
|
||||
const md = markdownit();
|
||||
const toEditorContent = (val: string): string => {
|
||||
if (!val) return '';
|
||||
// 已是 HTML(含标签)则原样返回,否则用 markdown-it 转 HTML
|
||||
if (/<[a-z][\s\S]*>/i.test(val)) return val;
|
||||
return md.render(val);
|
||||
};
|
||||
|
||||
// 文件库选择弹窗状态
|
||||
const showFileSelector = ref(false);
|
||||
@@ -747,324 +583,6 @@
|
||||
const showVideoSelector = ref(false);
|
||||
const videoSelectCallback = ref<((url: string) => void) | null>(null);
|
||||
|
||||
// 📝 Markdown编辑器配置
|
||||
const markdownToolbars = [
|
||||
'bold',
|
||||
'underline',
|
||||
'italic',
|
||||
'-',
|
||||
'title',
|
||||
'strikeThrough',
|
||||
'sub',
|
||||
'sup',
|
||||
'quote',
|
||||
'unorderedList',
|
||||
'orderedList',
|
||||
'task',
|
||||
'-',
|
||||
'codeRow',
|
||||
'code',
|
||||
'link',
|
||||
'image',
|
||||
'table',
|
||||
'mermaid',
|
||||
'katex',
|
||||
'-',
|
||||
'revoke',
|
||||
'next',
|
||||
'save',
|
||||
'=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
'catalog'
|
||||
] as any;
|
||||
|
||||
// 📝 Markdown编辑器图片上传处理
|
||||
const onMarkdownUploadImg = async (
|
||||
files: File[],
|
||||
callback: (urls: string[]) => void
|
||||
) => {
|
||||
try {
|
||||
const uploadPromises = files.map(async (file) => {
|
||||
// 检查文件大小(限制为10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error(`图片 ${file.name} 大小超过10MB,请选择更小的文件`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
message.error(`文件 ${file.name} 不是有效的图片格式`);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await uploadOss(file);
|
||||
return result.url || result.path;
|
||||
} catch (error) {
|
||||
console.error('图片上传失败:', error);
|
||||
message.error(`图片 ${file.name} 上传失败`);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(uploadPromises);
|
||||
const successUrls = results.filter((url) => url !== null) as string[];
|
||||
|
||||
if (successUrls.length > 0) {
|
||||
callback(successUrls);
|
||||
message.success(`成功上传 ${successUrls.length} 张图片`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error);
|
||||
message.error('图片上传失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 📝 Markdown编辑器图片选择器
|
||||
const openMarkdownImageSelector = () => {
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
// 在当前光标位置插入Markdown图片语法
|
||||
const imageMarkdown = ``;
|
||||
insertMarkdownText(imageMarkdown);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
};
|
||||
|
||||
// 📝 Markdown编辑器视频选择器
|
||||
const openMarkdownVideoSelector = () => {
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
// 在当前光标位置插入Markdown视频语法(使用HTML标签)
|
||||
const videoMarkdown = `<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${url}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>`;
|
||||
insertMarkdownText(videoMarkdown);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
};
|
||||
|
||||
// 📝 在Markdown编辑器中插入文本
|
||||
const insertMarkdownText = (text: string) => {
|
||||
// 简单的文本插入,在内容末尾添加
|
||||
if (content.value) {
|
||||
content.value += '\n\n' + text;
|
||||
} else {
|
||||
content.value = text;
|
||||
}
|
||||
};
|
||||
|
||||
const config = ref({
|
||||
height: 420,
|
||||
paste_data_images: true,
|
||||
automatic_uploads: true,
|
||||
|
||||
// 自定义工具栏,移除默认的image和media按钮,添加自定义按钮 (视频、上传视频按钮 custom_video_selector quick_video_upload)(首行缩进 )
|
||||
toolbar: [
|
||||
'fullscreen preview code codesample emoticons auto_format custom_image_selector quick_upload',
|
||||
'undo redo | forecolor backcolor',
|
||||
'bold italic underline strikethrough',
|
||||
'alignleft aligncenter alignright alignjustify',
|
||||
'outdent indent | numlist bullist',
|
||||
'formatselect fontselect fontsizeselect',
|
||||
'link charmap anchor pagebreak | ltr rtl'
|
||||
].join(' | '),
|
||||
|
||||
// 图片上传处理器 - 支持拖拽和粘贴上传
|
||||
images_upload_handler: (blobInfo, success, error) => {
|
||||
const file = blobInfo.blob();
|
||||
|
||||
// 检查文件大小(限制为10MB)
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
error('图片大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
error('只能上传图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示上传进度提示
|
||||
const loadingMsg = message.loading('图片上传中...', 0);
|
||||
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
success(res.url || res.path);
|
||||
message.success('图片上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
error(msg || '图片上传失败');
|
||||
message.error('图片上传失败:' + msg);
|
||||
});
|
||||
},
|
||||
|
||||
// 图片工具栏
|
||||
image_toolbar:
|
||||
'alignleft aligncenter alignright | rotateleft rotateright | imageoptions',
|
||||
|
||||
// 图片标题
|
||||
image_title: true,
|
||||
|
||||
// 图片描述
|
||||
image_description: true,
|
||||
|
||||
// 图片尺寸
|
||||
image_dimensions: true,
|
||||
|
||||
// 图片类别
|
||||
image_class_list: [
|
||||
{ title: '无样式', value: '' },
|
||||
{ title: '响应式图片', value: 'img-responsive' },
|
||||
{ title: '圆角图片', value: 'img-rounded' },
|
||||
{ title: '圆形图片', value: 'img-circle' }
|
||||
],
|
||||
|
||||
// 自定义按钮设置
|
||||
setup: (editor: any) => {
|
||||
// 添加自定义图片选择按钮
|
||||
editor.ui.registry.addButton('custom_image_selector', {
|
||||
text: '图片',
|
||||
icon: 'image',
|
||||
tooltip: '插入图片(从文件库选择或上传)',
|
||||
onAction: () => {
|
||||
// 打开文件库选择弹窗
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
editor.insertContent(
|
||||
`<img src="${url}" alt="图片" style="max-width: 100%;" />`
|
||||
);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加快速上传按钮
|
||||
editor.ui.registry.addButton('quick_upload', {
|
||||
text: '上传',
|
||||
icon: 'upload',
|
||||
tooltip: '快速上传图片',
|
||||
onAction: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/*';
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// 检查文件大小
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error('图片大小不能超过10MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMsg = message.loading('图片上传中...', 0);
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
const imageUrl = res.url || res.path;
|
||||
editor.insertContent(
|
||||
`<img src="${imageUrl}" alt="${file.name}" style="max-width: 100%;" />`
|
||||
);
|
||||
message.success('图片上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
message.error('图片上传失败:' + msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加自定义视频选择按钮
|
||||
editor.ui.registry.addButton('custom_video_selector', {
|
||||
text: '视频',
|
||||
icon: 'embed',
|
||||
tooltip: '插入视频(从视频库选择)',
|
||||
onAction: () => {
|
||||
// 打开视频库选择弹窗
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
// 插入视频标签,使用HTML5 video元素
|
||||
editor.insertContent(`
|
||||
<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${url}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
`);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加快速视频上传按钮
|
||||
editor.ui.registry.addButton('quick_video_upload', {
|
||||
text: '上传视频',
|
||||
icon: 'upload',
|
||||
tooltip: '快速上传视频',
|
||||
onAction: () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'video/*';
|
||||
input.onchange = (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
// 检查文件大小(限制为100MB)
|
||||
if (file.size > 100 * 1024 * 1024) {
|
||||
message.error('视频大小不能超过100MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const loadingMsg = message.loading('视频上传中...', 0);
|
||||
uploadOss(file)
|
||||
.then((res) => {
|
||||
loadingMsg();
|
||||
const videoUrl = res.path || res.downloadUrl;
|
||||
editor.insertContent(`
|
||||
<video controls style="max-width: 100%; height: auto;">
|
||||
<source src="${videoUrl}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
`);
|
||||
message.success('视频上传成功');
|
||||
})
|
||||
.catch((msg) => {
|
||||
loadingMsg();
|
||||
message.error('视频上传失败:' + msg);
|
||||
});
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
});
|
||||
|
||||
// 添加一键排版按钮
|
||||
editor.ui.registry.addButton('auto_format', {
|
||||
text: '排版',
|
||||
icon: 'template',
|
||||
tooltip: '智能优化文章格式和排版',
|
||||
onAction: () => {
|
||||
// 直接在这里处理排版,因为此时编辑器肯定已经初始化完成
|
||||
handleAutoFormat(editor);
|
||||
}
|
||||
});
|
||||
|
||||
// 添加段落首行缩进切换按钮
|
||||
editor.ui.registry.addButton('toggle_indent', {
|
||||
icon: 'indent',
|
||||
tooltip: '切换段落首行缩进(适合中文排版)',
|
||||
onAction: () => {
|
||||
toggleParagraphIndent(editor);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 从文件库选择图片的回调
|
||||
const onFileSelected = (data: FileRecord) => {
|
||||
if (fileSelectCallback.value) {
|
||||
@@ -1093,11 +611,31 @@
|
||||
showVideoSelector.value = false;
|
||||
};
|
||||
|
||||
// 从图片库选择图片:插入到 WangEditor 当前光标处
|
||||
const openImageSelector = () => {
|
||||
fileSelectCallback.value = (url: string) => {
|
||||
editorRef.value?.insertHtml(
|
||||
`<img src="${url}" alt="图片" style="max-width:100%;"/>`
|
||||
);
|
||||
};
|
||||
showFileSelector.value = true;
|
||||
};
|
||||
|
||||
// 从视频库选择视频:插入到 WangEditor 当前光标处
|
||||
const openVideoSelector = () => {
|
||||
videoSelectCallback.value = (url: string) => {
|
||||
editorRef.value?.insertHtml(
|
||||
`<video controls style="max-width:100%;height:auto;"><source src="${url}" type="video/mp4"></video>`
|
||||
);
|
||||
};
|
||||
showVideoSelector.value = true;
|
||||
};
|
||||
|
||||
// 🎨 智能一键排版 - 人性化设计
|
||||
const handleAutoFormat = (editor: any) => {
|
||||
const handleAutoFormat = () => {
|
||||
try {
|
||||
// 1. 检查内容
|
||||
const content = editor.getContent();
|
||||
const content = content.value;
|
||||
if (
|
||||
!content ||
|
||||
content.trim() === '' ||
|
||||
@@ -1121,7 +659,7 @@
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const optimizedContent = smartFormatContent(content);
|
||||
editor.setContent(optimizedContent);
|
||||
content.value = optimizedContent;
|
||||
|
||||
loadingMsg();
|
||||
|
||||
@@ -1316,9 +854,9 @@
|
||||
};
|
||||
|
||||
// 🔄 段落首行缩进切换功能
|
||||
const toggleParagraphIndent = (editor: any) => {
|
||||
const toggleParagraphIndent = () => {
|
||||
try {
|
||||
const content = editor.getContent();
|
||||
const content = content.value;
|
||||
|
||||
if (
|
||||
!content ||
|
||||
@@ -1351,7 +889,7 @@
|
||||
actionText = '已添加段落首行缩进';
|
||||
}
|
||||
|
||||
editor.setContent(newContent);
|
||||
content.value = newContent;
|
||||
|
||||
message.success({
|
||||
content: `📐 ${actionText}`,
|
||||
@@ -1449,7 +987,7 @@
|
||||
}
|
||||
const formData = {
|
||||
...form,
|
||||
editor: editor.value || 1,
|
||||
editor: 1,
|
||||
status: setting.setting?.articleReview ? 1 : 0,
|
||||
content: content.value
|
||||
};
|
||||
@@ -1480,12 +1018,6 @@
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (localStorage.getItem('Editor')) {
|
||||
editor.value = Number(localStorage.getItem('Editor'));
|
||||
} else {
|
||||
editor.value = 1;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
images.value = [];
|
||||
category.value = [];
|
||||
@@ -1499,7 +1031,7 @@
|
||||
// 文章详情
|
||||
assignObject(form, data);
|
||||
if (data.content) {
|
||||
content.value = data.content;
|
||||
content.value = toEditorContent(data.content);
|
||||
}
|
||||
if (!data.source) {
|
||||
form.source = undefined;
|
||||
@@ -1509,9 +1041,6 @@
|
||||
} else {
|
||||
form.tags = undefined;
|
||||
}
|
||||
if (data.editor) {
|
||||
editor.value = data.editor;
|
||||
}
|
||||
if (data.files) {
|
||||
const arr = JSON.parse(data.files);
|
||||
arr.map((url: string) => {
|
||||
@@ -1561,26 +1090,6 @@
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
// 编辑器内图片样式
|
||||
:deep(.mce-content-body) {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
&.img-responsive {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
&.img-rounded {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&.img-circle {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择提示
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<div
|
||||
class="login-wrapper"
|
||||
:style="{
|
||||
backgroundImage: 'url(https://oss.wsdns.cn/20250105/314d2a3da10048b09ef0f0464fdacbff.jpg)'
|
||||
}"
|
||||
>
|
||||
<div class="logo-login" v-if="config?.siteName">
|
||||
<img :src="config.siteLogo" class="logo" />
|
||||
<h4>{{ config.siteName }}</h4>
|
||||
</div>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
class="login-form ele-bg-white"
|
||||
>
|
||||
<div class="login-title flex justify-center items-center px-12">
|
||||
<h4 class="title-btn active">{{ t('login.forgetPage.title') }}</h4>
|
||||
</div>
|
||||
<p class="forget-subtitle">{{ t('login.forgetPage.subTitle') }}</p>
|
||||
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
class="forget-tabs"
|
||||
@change="onTabChange"
|
||||
>
|
||||
<!-- 邮箱找回 -->
|
||||
<a-tab-pane key="email" :tab="t('login.forgetPage.tabEmail')">
|
||||
<a-form-item name="email">
|
||||
<a-input
|
||||
allow-clear
|
||||
size="large"
|
||||
v-model:value="form.email"
|
||||
:placeholder="t('login.forgetPage.emailPlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<MailOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="emailCode">
|
||||
<div class="login-input-group">
|
||||
<a-input
|
||||
allow-clear
|
||||
size="large"
|
||||
:maxlength="6"
|
||||
v-model:value="form.emailCode"
|
||||
:placeholder="t('login.forgetPage.codePlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<SafetyOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
class="login-captcha"
|
||||
:disabled="!!countdownTime"
|
||||
@click="sendCode"
|
||||
>
|
||||
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
|
||||
<span v-else>{{ countdownTime }} s</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 手机找回 -->
|
||||
<a-tab-pane key="phone" :tab="t('login.forgetPage.tabPhone')">
|
||||
<a-form-item name="phone">
|
||||
<a-input
|
||||
allow-clear
|
||||
size="large"
|
||||
:maxlength="11"
|
||||
v-model:value="form.phone"
|
||||
:placeholder="t('login.forgetPage.phonePlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<MobileOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item name="code">
|
||||
<div class="login-input-group">
|
||||
<a-input
|
||||
allow-clear
|
||||
size="large"
|
||||
:maxlength="6"
|
||||
v-model:value="form.code"
|
||||
:placeholder="t('login.forgetPage.codePlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<SafetyOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button
|
||||
class="login-captcha"
|
||||
:disabled="!!countdownTime"
|
||||
@click="sendCode"
|
||||
>
|
||||
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
|
||||
<span v-else>{{ countdownTime }} s</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
<a-form-item name="newPassword">
|
||||
<a-input-password
|
||||
size="large"
|
||||
v-model:value="form.newPassword"
|
||||
:placeholder="t('login.forgetPage.passwordPlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item name="confirmPassword">
|
||||
<a-input-password
|
||||
size="large"
|
||||
v-model:value="form.confirmPassword"
|
||||
:placeholder="t('login.forgetPage.confirmPlaceholder')"
|
||||
>
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
block
|
||||
size="large"
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="submit"
|
||||
>
|
||||
{{ loading ? t('login.loading') : t('login.forgetPage.reset') }}
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<div class="text-center pt-2">
|
||||
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
|
||||
</div>
|
||||
</a-form>
|
||||
|
||||
<!-- 图形验证码弹窗 -->
|
||||
<a-modal
|
||||
:width="340"
|
||||
:footer="null"
|
||||
:title="t('login.forgetPage.captchaTitle')"
|
||||
v-model:visible="captchaVisible"
|
||||
@cancel="codeLoading = false"
|
||||
>
|
||||
<div class="login-input-group" style="margin-bottom: 16px">
|
||||
<a-input
|
||||
v-model:value="imgCode"
|
||||
:maxlength="5"
|
||||
size="large"
|
||||
:placeholder="t('login.forgetPage.captchaPlaceholder')"
|
||||
allow-clear
|
||||
@pressEnter="() => doSendCode(imgCode)"
|
||||
/>
|
||||
<a-button class="login-captcha">
|
||||
<img alt="" :src="captchaImage" @click="refreshCaptcha" />
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button
|
||||
block
|
||||
size="large"
|
||||
type="primary"
|
||||
:loading="codeLoading"
|
||||
@click="() => doSendCode(imgCode)"
|
||||
>
|
||||
{{ t('login.forgetPage.sendNow') }}
|
||||
</a-button>
|
||||
</a-modal>
|
||||
|
||||
<div class="login-copyright">
|
||||
<a-space>
|
||||
<span>© {{ new Date().getFullYear() }}</span>
|
||||
<span>{{ config?.copyright || 'websoft.top Inc.' }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, onUnmounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
LockOutlined,
|
||||
MobileOutlined,
|
||||
SafetyOutlined,
|
||||
MailOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import {
|
||||
sendSmsCaptchaByAdmin,
|
||||
sendEmailCaptcha,
|
||||
getCaptcha
|
||||
} from '@/api/passport/login';
|
||||
import { resetPassword } from '@/api/layout';
|
||||
import useFormData from '@/utils/use-form-data';
|
||||
import { Config } from '@/api/cms/cmsWebsiteField/model';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
// 默认站点配置(与登录页保持一致)
|
||||
const defaultConfig: Config = {
|
||||
siteName: '小程序商城',
|
||||
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
||||
domain: '',
|
||||
icpNo: '',
|
||||
copyright: '',
|
||||
loginBgImg: '',
|
||||
address: '',
|
||||
tel: '',
|
||||
kefu2: '',
|
||||
kefu1: '',
|
||||
email: '',
|
||||
loginTitle: '',
|
||||
sysLogo: ''
|
||||
};
|
||||
const config = ref<Config>({ ...defaultConfig });
|
||||
|
||||
// 找回方式:email(邮箱) / phone(手机),默认邮箱
|
||||
const activeTab = ref<'email' | 'phone'>('email');
|
||||
|
||||
const { form } = useFormData<{
|
||||
email: string;
|
||||
emailCode: string;
|
||||
phone: string;
|
||||
code: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}>({
|
||||
email: '',
|
||||
emailCode: '',
|
||||
phone: '',
|
||||
code: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const loading = ref(false);
|
||||
|
||||
// 验证码倒计时
|
||||
const countdownTime = ref(0);
|
||||
let countdownTimer: number | null = null;
|
||||
|
||||
// 图形验证码弹窗
|
||||
const captchaVisible = ref(false);
|
||||
const captchaImage = ref('');
|
||||
const captchaText = ref('');
|
||||
const imgCode = ref('');
|
||||
const codeLoading = ref(false);
|
||||
|
||||
// 同一个账号前 3 次发送不需要图形验证码
|
||||
const FREE_SEND_TIMES = 3;
|
||||
|
||||
// 根据当前找回方式和账号生成发送次数缓存 key
|
||||
const getSendCountKey = () => {
|
||||
const account = activeTab.value === 'email' ? form.email : form.phone;
|
||||
return `forget_send_count_${activeTab.value}_${account}`;
|
||||
};
|
||||
|
||||
// 读取当前账号已发送次数
|
||||
const getSendCount = () => {
|
||||
const key = getSendCountKey();
|
||||
if (!key) return 0;
|
||||
const count = localStorage.getItem(key);
|
||||
return count ? Number(count) || 0 : 0;
|
||||
};
|
||||
|
||||
// 增加发送次数
|
||||
const increaseSendCount = () => {
|
||||
const key = getSendCountKey();
|
||||
if (!key) return;
|
||||
localStorage.setItem(key, String(getSendCount() + 1));
|
||||
};
|
||||
|
||||
// 刷新图形验证码
|
||||
const refreshCaptcha = () => {
|
||||
getCaptcha()
|
||||
.then((data) => {
|
||||
captchaImage.value = data.base64;
|
||||
captchaText.value = data.text;
|
||||
})
|
||||
.catch((e: Error) => message.error(e.message));
|
||||
};
|
||||
|
||||
// 打开图形验证码弹窗
|
||||
const openCaptchaModal = () => {
|
||||
imgCode.value = '';
|
||||
refreshCaptcha();
|
||||
captchaVisible.value = true;
|
||||
};
|
||||
|
||||
// 密码强度:至少8位,且包含字母和数字
|
||||
const passwordRules = [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.forgetPage.newPasswordRule'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ min: 8, message: t('login.forgetPage.newPasswordRule'), trigger: 'blur' },
|
||||
{
|
||||
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/,
|
||||
message: t('login.forgetPage.newPasswordRule'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
const confirmPasswordRules = [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.forgetPage.confirmRule'),
|
||||
trigger: 'blur'
|
||||
},
|
||||
{
|
||||
validator: (_rule: unknown, value: string) =>
|
||||
value === form.newPassword
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error(t('login.forgetPage.confirmRule'))),
|
||||
trigger: 'blur'
|
||||
}
|
||||
];
|
||||
|
||||
// 根据当前找回方式动态生成校验规则
|
||||
const rules = computed(() => {
|
||||
if (activeTab.value === 'email') {
|
||||
return {
|
||||
email: [
|
||||
{ required: true, message: t('login.forgetPage.emailRule'), trigger: 'blur' },
|
||||
{ type: 'email', message: t('login.forgetPage.emailRule'), trigger: 'blur' }
|
||||
],
|
||||
emailCode: [
|
||||
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
|
||||
],
|
||||
newPassword: passwordRules,
|
||||
confirmPassword: confirmPasswordRules
|
||||
};
|
||||
}
|
||||
return {
|
||||
phone: [
|
||||
{ required: true, message: t('login.forgetPage.phoneRule'), trigger: 'blur' },
|
||||
{ pattern: /^1\d{10}$/, message: t('login.forgetPage.phoneRule'), trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
|
||||
],
|
||||
newPassword: passwordRules,
|
||||
confirmPassword: confirmPasswordRules
|
||||
};
|
||||
});
|
||||
|
||||
const onTabChange = () => {
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
// 发送验证码(邮箱/手机)
|
||||
const sendCode = () => {
|
||||
if (activeTab.value === 'email') {
|
||||
if (!form.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
|
||||
message.error(t('login.forgetPage.emailRule'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
|
||||
message.error(t('login.forgetPage.phoneRule'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (getSendCount() >= FREE_SEND_TIMES) {
|
||||
openCaptchaModal();
|
||||
} else {
|
||||
doSendCode();
|
||||
}
|
||||
};
|
||||
|
||||
// 真正调用接口发送验证码
|
||||
const doSendCode = (captchaCode?: string) => {
|
||||
if (captchaCode !== undefined) {
|
||||
if (!captchaCode) {
|
||||
message.error('请输入图形验证码');
|
||||
return;
|
||||
}
|
||||
if (captchaText.value !== captchaCode.toLowerCase()) {
|
||||
message.error('图形验证码不正确');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
codeLoading.value = true;
|
||||
const sendPromise =
|
||||
activeTab.value === 'email'
|
||||
? sendEmailCaptcha({ email: form.email }, { skipTenant: true })
|
||||
: sendSmsCaptchaByAdmin({ phone: form.phone }, { skipTenant: true });
|
||||
|
||||
sendPromise
|
||||
.then(() => {
|
||||
message.success(
|
||||
activeTab.value === 'email'
|
||||
? t('login.forgetPage.sentEmail')
|
||||
: t('login.forgetPage.getCode')
|
||||
);
|
||||
captchaVisible.value = false;
|
||||
increaseSendCount();
|
||||
startCountdown();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
message.error(e.message);
|
||||
refreshCaptcha();
|
||||
})
|
||||
.finally(() => {
|
||||
codeLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const startCountdown = () => {
|
||||
countdownTime.value = 60;
|
||||
countdownTimer = window.setInterval(() => {
|
||||
if (countdownTime.value <= 1) {
|
||||
countdownTimer && clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
countdownTime.value--;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const goLogin = () => {
|
||||
router.push('/login');
|
||||
};
|
||||
|
||||
// 提交重置
|
||||
const submit = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const params: Record<string, unknown> = {
|
||||
newPassword: form.newPassword,
|
||||
confirmPassword: form.confirmPassword
|
||||
};
|
||||
if (activeTab.value === 'email') {
|
||||
params.email = form.email;
|
||||
params.emailCode = form.emailCode;
|
||||
} else {
|
||||
params.phone = form.phone;
|
||||
params.smsCode = form.code;
|
||||
}
|
||||
resetPassword(params as any)
|
||||
.then((msg) => {
|
||||
message.success(msg || t('login.forgetPage.success'));
|
||||
loading.value = false;
|
||||
router.push('/login');
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
message.error(e.message);
|
||||
loading.value = false;
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-wrapper {
|
||||
padding: 48px 16px 0 16px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--grey-5);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
min-height: 100vh;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.forget-subtitle {
|
||||
text-align: center;
|
||||
color: #8c8c8c;
|
||||
margin: -8px 0 16px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 380px;
|
||||
margin: 0 auto;
|
||||
max-width: 100%;
|
||||
padding: 0 28px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
h4 {
|
||||
padding: 22px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title-btn {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
line-height: 50px;
|
||||
|
||||
.active {
|
||||
color: #007dff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.forget-tabs {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-repeat: no-repeat;
|
||||
|
||||
:deep(.ant-input-affix-wrapper) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.login-captcha {
|
||||
width: 102px;
|
||||
height: 40px;
|
||||
margin-left: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.login-copyright {
|
||||
color: #eee;
|
||||
text-align: center;
|
||||
padding: 48px 0 22px 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.logo-login {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-left: 6px;
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-height: 640px) {
|
||||
.login-wrapper {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-top: -260px;
|
||||
}
|
||||
|
||||
.login-copyright {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.login-form {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -32,18 +32,21 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<h4
|
||||
ref="accountTabRef"
|
||||
class="title-btn"
|
||||
:class="loginType === 'account' ? 'active' : ''"
|
||||
@click="onLoginType('account')"
|
||||
>邮箱登录</h4
|
||||
>
|
||||
<a-divider type="vertical" style="height: 20px" />
|
||||
<h4
|
||||
ref="smsTabRef"
|
||||
class="title-btn"
|
||||
:class="loginType === 'sms' ? 'active' : ''"
|
||||
@click="onLoginType('sms')"
|
||||
>手机号登录</h4
|
||||
>
|
||||
<a-divider type="vertical" style="height: 20px" />
|
||||
<h4
|
||||
class="title-btn"
|
||||
:class="loginType === 'account' ? 'active' : ''"
|
||||
@click="onLoginType('account')"
|
||||
>账号登录</h4
|
||||
>
|
||||
<div class="tab-indicator" :style="indicatorStyle"></div>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
@@ -59,8 +62,8 @@
|
||||
<div
|
||||
class="absolute top-2 right-2 text-lg text-white font-bold cursor-pointer"
|
||||
>
|
||||
<QrcodeOutlined v-if="loginType === 'sms'" />
|
||||
<MobileOutlined v-else />
|
||||
<MobileOutlined v-if="loginType === 'scan'" />
|
||||
<QrcodeOutlined v-else />
|
||||
</div>
|
||||
<!-- <span class="absolute top-3 right-1.5 text-sm text-white font-bold cursor-pointer">{{ '登录' }}</span>-->
|
||||
</div>
|
||||
@@ -77,12 +80,12 @@
|
||||
<!-- </template>-->
|
||||
<!-- </a-input>-->
|
||||
<!-- </a-form-item>-->
|
||||
<a-form-item name="username">
|
||||
<a-form-item name="account">
|
||||
<a-input
|
||||
allow-clear
|
||||
size="large"
|
||||
v-model:value="form.userId"
|
||||
:placeholder="`用户ID`"
|
||||
v-model:value="form.account"
|
||||
placeholder="请输入邮箱"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
@@ -93,7 +96,7 @@
|
||||
<a-input-password
|
||||
size="large"
|
||||
v-model:value="form.password"
|
||||
placeholder="登录密码"
|
||||
placeholder="请输入登录密码"
|
||||
@pressEnter="submit"
|
||||
>
|
||||
<template #prefix>
|
||||
@@ -141,6 +144,9 @@
|
||||
>
|
||||
{{ loading ? t('login.loading') : t('login.login') }}
|
||||
</a-button>
|
||||
<div class="text-center pt-5 text-gray-500"
|
||||
>还没有账号?联系管理员开通</div
|
||||
>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="loginType === 'sms'">
|
||||
@@ -168,7 +174,7 @@
|
||||
<a-button
|
||||
class="login-captcha"
|
||||
:disabled="!!countdownTime"
|
||||
@click="openImgCodeModal"
|
||||
@click="handleSendCode"
|
||||
>
|
||||
<span v-if="!countdownTime">发送验证码</span>
|
||||
<span v-else>已发送 {{ countdownTime }} s</span>
|
||||
@@ -185,6 +191,9 @@
|
||||
>
|
||||
{{ loading ? t('login.loading') : t('login.login') }}
|
||||
</a-button>
|
||||
<div class="text-center pt-5 text-gray-500"
|
||||
>还没有账号?联系管理员开通</div
|
||||
>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="loginType == 'scan'">
|
||||
@@ -214,7 +223,7 @@
|
||||
size="large"
|
||||
placeholder="请输入图形验证码"
|
||||
allow-clear
|
||||
@pressEnter="sendCode"
|
||||
@pressEnter="() => sendCode(imgCode.value)"
|
||||
/>
|
||||
<a-button class="login-captcha">
|
||||
<img alt="" :src="captcha" @click="changeCaptcha" />
|
||||
@@ -225,7 +234,7 @@
|
||||
size="large"
|
||||
type="primary"
|
||||
:loading="codeLoading"
|
||||
@click="sendCode"
|
||||
@click="() => sendCode(imgCode.value)"
|
||||
>
|
||||
立即发送
|
||||
</a-button>
|
||||
@@ -261,11 +270,40 @@
|
||||
</a-list>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 多租户选择登录 -->
|
||||
<a-modal
|
||||
:width="420"
|
||||
:footer="null"
|
||||
:mask-closable="false"
|
||||
title="请选择要登录的租户"
|
||||
v-model:visible="showSelectTenantModal"
|
||||
>
|
||||
<div class="flex flex-col justify-start">
|
||||
<a-list item-layout="horizontal" :data-source="pendingTenants">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item
|
||||
class="cursor-pointer hover:border-gray-100"
|
||||
@click="onSelectTenant(item)"
|
||||
>
|
||||
<a-list-item-meta :description="`租户ID: ${item.tenantId}`">
|
||||
<template #title>
|
||||
{{ item.tenantName }}
|
||||
</template>
|
||||
</a-list-item-meta>
|
||||
<template #actions>
|
||||
<RightOutlined />
|
||||
</template>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, unref, watch } from 'vue';
|
||||
import { ref, reactive, unref, watch, nextTick, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
@@ -278,7 +316,14 @@
|
||||
RightOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { goHomeRoute, cleanPageTabs } from '@/utils/page-tab-util';
|
||||
import { login, loginBySms, getCaptcha } from '@/api/passport/login';
|
||||
import {
|
||||
loginBySuperAdminSms,
|
||||
getCaptcha,
|
||||
loginByEmail,
|
||||
loginByUserId,
|
||||
loginBySelectTenant
|
||||
} from '@/api/passport/login';
|
||||
import type { TenantOption } from '@/api/passport/login/model';
|
||||
import QrLogin from '@/components/QrLogin/index.vue';
|
||||
|
||||
import { User } from '@/api/system/user/model';
|
||||
@@ -308,12 +353,66 @@
|
||||
const direction = ref(0);
|
||||
// 加载状态
|
||||
const loading = ref(false);
|
||||
// 是否显示tenantId填写输入框
|
||||
const loginType = ref('sms');
|
||||
// 是否显示tenantId填写输入框,默认进入账号登录/手机号登录切换模式
|
||||
const loginType = ref('account');
|
||||
// 标签下划线指示器
|
||||
const accountTabRef = ref<HTMLElement>();
|
||||
const smsTabRef = ref<HTMLElement>();
|
||||
const indicatorStyle = ref({ left: '0px', width: '0px', transition: 'none' });
|
||||
// 收缩/展开两阶段动画时长(ms),模拟阿里云"先收一下再展开"的弹性效果
|
||||
const SHRINK_MS = 150;
|
||||
const EXPAND_MS = 220;
|
||||
let indicatorTimer: number | null = null;
|
||||
const updateIndicator = (animate = true) => {
|
||||
if (indicatorTimer != null) {
|
||||
clearTimeout(indicatorTimer);
|
||||
indicatorTimer = null;
|
||||
}
|
||||
nextTick(() => {
|
||||
const activeEl =
|
||||
loginType.value === 'account' ? accountTabRef.value : smsTabRef.value;
|
||||
if (!activeEl) {
|
||||
return;
|
||||
}
|
||||
const targetLeft = activeEl.offsetLeft;
|
||||
const targetWidth = activeEl.offsetWidth;
|
||||
// 非动画:直接定位(首次渲染 / 窗口缩放)
|
||||
if (!animate) {
|
||||
indicatorStyle.value = {
|
||||
left: `${targetLeft}px`,
|
||||
width: `${targetWidth}px`,
|
||||
transition: 'none'
|
||||
};
|
||||
return;
|
||||
}
|
||||
// 阶段一:收缩成一条短线,并滑向目标中心
|
||||
const targetCenter = targetLeft + targetWidth / 2;
|
||||
const smallWidth = 24;
|
||||
indicatorStyle.value = {
|
||||
left: `${targetCenter - smallWidth / 2}px`,
|
||||
width: `${smallWidth}px`,
|
||||
transition: `left ${SHRINK_MS}ms ease-in, width ${SHRINK_MS}ms ease-in`
|
||||
};
|
||||
// 阶段二:展开到目标标签的宽度
|
||||
indicatorTimer = window.setTimeout(() => {
|
||||
indicatorStyle.value = {
|
||||
left: `${targetLeft}px`,
|
||||
width: `${targetWidth}px`,
|
||||
transition: `left ${EXPAND_MS}ms ease-out, width ${EXPAND_MS}ms ease-out`
|
||||
};
|
||||
indicatorTimer = null;
|
||||
}, SHRINK_MS);
|
||||
});
|
||||
};
|
||||
watch(loginType, () => updateIndicator(true), { immediate: true });
|
||||
onMounted(() => {
|
||||
updateIndicator(false);
|
||||
window.addEventListener('resize', () => updateIndicator(false));
|
||||
});
|
||||
|
||||
// 默认配置
|
||||
const defaultConfig: Config = {
|
||||
siteName: '企业商城',
|
||||
siteName: '小程序商城',
|
||||
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
|
||||
domain: '',
|
||||
icpNo: '',
|
||||
@@ -346,8 +445,10 @@
|
||||
console.warn('获取网站配置失败,使用默认配置:', e.message);
|
||||
}
|
||||
};
|
||||
const { form } = useFormData<User>({
|
||||
userId: undefined,
|
||||
type LoginForm = User & { account?: string };
|
||||
|
||||
const { form } = useFormData<LoginForm>({
|
||||
account: '',
|
||||
username: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
@@ -373,9 +474,34 @@
|
||||
const countdownTime = ref(0);
|
||||
// 验证码倒计时定时器
|
||||
let countdownTimer: number | null = null;
|
||||
|
||||
// 同一个手机号前 3 次发送不需要图形验证码
|
||||
const FREE_SEND_TIMES = 3;
|
||||
|
||||
// 根据手机号生成发送次数缓存 key
|
||||
const getSendCountKey = () => `login_sms_send_count_${form.phone}`;
|
||||
|
||||
// 读取当前手机号已发送次数
|
||||
const getSendCount = () => {
|
||||
const key = getSendCountKey();
|
||||
if (!key) return 0;
|
||||
const count = localStorage.getItem(key);
|
||||
return count ? Number(count) || 0 : 0;
|
||||
};
|
||||
|
||||
// 增加发送次数
|
||||
const increaseSendCount = () => {
|
||||
const key = getSendCountKey();
|
||||
if (!key) return;
|
||||
localStorage.setItem(key, String(getSendCount() + 1));
|
||||
};
|
||||
// 多用户选择账号登录
|
||||
const showSelectLoginUser = ref<boolean>(false);
|
||||
const admins = ref<User[]>([]);
|
||||
// 多租户选择登录
|
||||
const showSelectTenantModal = ref<boolean>(false);
|
||||
const pendingTenants = ref<TenantOption[]>([]);
|
||||
const selectingTenant = ref<boolean>(false);
|
||||
// const tenantId = getTenantId();
|
||||
|
||||
// 表格选中数据
|
||||
@@ -383,7 +509,7 @@
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
userId: [
|
||||
account: [
|
||||
{
|
||||
required: true,
|
||||
message: t('login.username'),
|
||||
@@ -422,42 +548,56 @@
|
||||
]
|
||||
});
|
||||
|
||||
/* 显示发送短信验证码弹窗 */
|
||||
const openImgCodeModal = () => {
|
||||
/* 处理发送短信验证码按钮点击 */
|
||||
const handleSendCode = () => {
|
||||
if (!form.phone) {
|
||||
message.error('请输入手机号码');
|
||||
return;
|
||||
}
|
||||
imgCode.value = '';
|
||||
changeCaptcha();
|
||||
visible.value = true;
|
||||
if (getSendCount() >= FREE_SEND_TIMES) {
|
||||
imgCode.value = '';
|
||||
changeCaptcha();
|
||||
visible.value = true;
|
||||
} else {
|
||||
sendCode();
|
||||
}
|
||||
};
|
||||
|
||||
/* 发送短信验证码 */
|
||||
const sendCode = () => {
|
||||
if (!imgCode.value) {
|
||||
message.error('请输入图形验证码');
|
||||
return;
|
||||
}
|
||||
if (text.value !== imgCode.value.toLowerCase()) {
|
||||
message.error('图形验证码不正确');
|
||||
return;
|
||||
const sendCode = (captchaCode?: string) => {
|
||||
if (captchaCode !== undefined) {
|
||||
if (!captchaCode) {
|
||||
message.error('请输入图形验证码');
|
||||
return;
|
||||
}
|
||||
if (text.value !== captchaCode.toLowerCase()) {
|
||||
message.error('图形验证码不正确');
|
||||
return;
|
||||
}
|
||||
}
|
||||
codeLoading.value = true;
|
||||
sendSmsCaptcha({ phone: form.phone }).then(() => {
|
||||
message.success('短信验证码发送成功, 请注意查收!');
|
||||
visible.value = false;
|
||||
codeLoading.value = false;
|
||||
countdownTime.value = 30;
|
||||
// 开始对按钮进行倒计时
|
||||
countdownTimer = window.setInterval(() => {
|
||||
if (countdownTime.value <= 1) {
|
||||
countdownTimer && clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
countdownTime.value--;
|
||||
}, 1000);
|
||||
});
|
||||
sendSmsCaptcha({ phone: form.phone })
|
||||
.then(() => {
|
||||
message.success('短信验证码发送成功, 请注意查收!');
|
||||
visible.value = false;
|
||||
increaseSendCount();
|
||||
countdownTime.value = 30;
|
||||
// 开始对按钮进行倒计时
|
||||
countdownTimer = window.setInterval(() => {
|
||||
if (countdownTime.value <= 1) {
|
||||
countdownTimer && clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
countdownTime.value--;
|
||||
}, 1000);
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
message.error(e.message);
|
||||
changeCaptcha();
|
||||
})
|
||||
.finally(() => {
|
||||
codeLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// const { clearValidate, validate, validateInfos } = useForm(form, rules);
|
||||
@@ -479,24 +619,19 @@
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
form.code = form.smsCode?.toLowerCase();
|
||||
loginBySms(form)
|
||||
.then((msg) => {
|
||||
if (msg == '请选择登录用户') {
|
||||
showSelectLoginUser.value = true;
|
||||
listAdminsByPhoneAll({
|
||||
phone: form.phone,
|
||||
templateId: TEMPLATE_ID
|
||||
}).then((data) => {
|
||||
admins.value = data;
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
message.success(msg);
|
||||
loginBySuperAdminSms(form)
|
||||
.then((outcome) => {
|
||||
if (outcome.status === 'selectTenant') {
|
||||
pendingTenants.value = outcome.tenants;
|
||||
showSelectTenantModal.value = true;
|
||||
loading.value = false;
|
||||
resetFields();
|
||||
cleanPageTabs();
|
||||
goHome();
|
||||
return;
|
||||
}
|
||||
message.success(outcome.message);
|
||||
loading.value = false;
|
||||
resetFields();
|
||||
cleanPageTabs();
|
||||
goHome();
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
message.error(e.message);
|
||||
@@ -511,6 +646,29 @@
|
||||
onLoginBySms();
|
||||
};
|
||||
|
||||
/* 选择租户后二次登录 */
|
||||
const onSelectTenant = (item: TenantOption) => {
|
||||
if (selectingTenant.value || item.tenantId == null) return;
|
||||
selectingTenant.value = true;
|
||||
loginBySelectTenant({
|
||||
phone: form.phone,
|
||||
code: form.smsCode || '',
|
||||
tenantId: item.tenantId,
|
||||
remember: form.remember
|
||||
})
|
||||
.then((outcome) => {
|
||||
showSelectTenantModal.value = false;
|
||||
message.success(outcome.message);
|
||||
resetFields();
|
||||
cleanPageTabs();
|
||||
goHome();
|
||||
})
|
||||
.catch((e: Error) => message.error(e.message))
|
||||
.finally(() => {
|
||||
selectingTenant.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const submit = () => {
|
||||
if (!formRef.value) {
|
||||
@@ -519,15 +677,30 @@
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
// if (form.code?.toLowerCase() !== text.value) {
|
||||
// message.error('验证码错误');
|
||||
// changeCaptcha();
|
||||
// return;
|
||||
// }
|
||||
if (form.code?.toLowerCase() !== text.value) {
|
||||
message.error('验证码错误');
|
||||
changeCaptcha();
|
||||
return;
|
||||
}
|
||||
const account = (form.account || '').trim();
|
||||
// 邮箱格式判断
|
||||
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(account);
|
||||
// 纯数字判断(用户ID)
|
||||
const isPureNumber = /^\d+$/.test(account);
|
||||
if (!isEmail && !isPureNumber) {
|
||||
message.error('请输入正确的邮箱或用户ID');
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
form.code = form.code?.toLowerCase();
|
||||
form.phone = undefined;
|
||||
login(form)
|
||||
const payload = {
|
||||
password: form.password,
|
||||
code: form.code?.toLowerCase(),
|
||||
remember: form.remember
|
||||
};
|
||||
const loginApi = isEmail
|
||||
? loginByEmail({ ...payload, email: account })
|
||||
: loginByUserId({ ...payload, userId: account });
|
||||
loginApi
|
||||
.then((msg) => {
|
||||
if (msg == '请选择登录用户') {
|
||||
showSelectLoginUser.value = true;
|
||||
@@ -566,7 +739,7 @@
|
||||
|
||||
const onScan = () => {
|
||||
if (loginType.value == 'scan') {
|
||||
loginType.value = 'sms';
|
||||
loginType.value = 'account';
|
||||
} else {
|
||||
loginType.value = 'scan';
|
||||
}
|
||||
@@ -691,9 +864,11 @@
|
||||
|
||||
.title-btn {
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
line-height: 50px;
|
||||
@@ -701,6 +876,14 @@
|
||||
.active {
|
||||
color: #007dff;
|
||||
}
|
||||
|
||||
.tab-indicator {
|
||||
position: absolute;
|
||||
bottom: -10px;
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
background-color: #007dff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+710
-178
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user