Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d8080c7bf | |||
| 5943f1536d | |||
| bec3a6f1b8 | |||
| 65926a35b6 | |||
| ced8c7cff7 | |||
| fe97f49afe | |||
| 6556944ffc | |||
| 3c4e30c227 | |||
| 47fb8d1bd0 | |||
| 4ba0427417 | |||
| 6e57fba192 | |||
| d458115da9 | |||
| f986efce8c | |||
| 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 |
@@ -1,2 +1,3 @@
|
||||
VITE_APP_NAME=小程序商城
|
||||
VITE_API_URL=/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
+2
-1
@@ -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=https://guilixu-api.websoft.top/api
|
||||
VITE_CMS_API_URL=https://cms-api.websoft.top/api
|
||||
|
||||
+2
-1
@@ -1,2 +1,3 @@
|
||||
VITE_APP_NAME=后台管理系统
|
||||
VITE_API_URL=https://shop-api.websoft.top/api
|
||||
VITE_API_URL=https://guilixu-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
|
||||
@@ -259,7 +259,7 @@
|
||||
- `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)。
|
||||
- 前端订阅 API 域名硬编码 `https://websopy-api.websoft.top/api/app/subscription`(api/index.ts WEBSOPY_API_BASE),主站 API 走 guilixu-api.websoft.top。用户对比的 `websoft.top/api/_app/...` 是另一个入口(疑 nginx 转发 _app→app)。
|
||||
|
||||
## 续费"弹二维码自动完成支付"修复:清除旧 Redis 支付标记
|
||||
|
||||
|
||||
@@ -72,3 +72,51 @@
|
||||
- **真正的 `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://guilixu-api.websoft.top`(= guilixu-java)**,不是 mp-java。前端 shopExpressTemplateDetail API 用默认 `@/utils/request`,baseURL = `MODULES_API_URL` = `localStorage ApiUrl || VITE_API_URL`;该部署的 ApiUrl 配的就是 guilixu-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://guilixu-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,32 @@
|
||||
# 2026-08-17 工作日志
|
||||
|
||||
## 专区销量统计功能(/special/zone)
|
||||
- 调研:专区与商品靠 `ShopGoods.section_ids`(逗号串)关联;`ShopOrderGoods` 无 sectionId,有 `goodsId/totalNum/price/payStatus/orderStatus/tenantId`;商品有 `sales` 累计字段。
|
||||
- 确认方案:方案B(订单真实成交聚合)+ 列表汇总列 + 独立统计弹窗,指标=销量件数+销售额。
|
||||
- 后端 guilixu-java(shop 模块)改动:
|
||||
- ShopHomeSection 实体加 salesNum/salesAmount(@TableField(exist=false))
|
||||
- 新建 VO:SectionSalesSummaryVO / ShopSectionGoodsRankVO / SectionSalesStatsVO(vo 包)
|
||||
- ShopHomeSectionMapper 加 selectSectionSalesSummary(批量)/ selectSectionGoodsRank(TOP50)
|
||||
- ShopHomeSectionServiceImpl.pageRel 批量填充汇总 + 新增 getSectionSalesStats
|
||||
- ShopHomeSectionController 新增 GET /{id}/stats?start=&end=
|
||||
- 口径:payStatus=1 且 orderStatus NOT IN(2,6),tenantId 隔离,时间对应 create_time
|
||||
- 前端 guilixu-admin:
|
||||
- shopZone API 加 getSectionSalesStats;model 加字段与 VO 类型
|
||||
- zone/index.vue 列表加「销量件数」「销售额」列 + 操作列「销量」按钮
|
||||
- 新建 SectionSalesStatsModal.vue(时间筛选 + 汇总卡片 + 商品排行)
|
||||
- vite build 通过(exit 0)
|
||||
- 待办:后端需 maven 编译并部署到 guilixu-api.websoft.top 才生效(本环境未部署)。
|
||||
|
||||
## 修复专区商品抽屉分页点击无效
|
||||
- 文件:`src/views/special/zone/index.vue`
|
||||
- 问题:抽屉里 `a-table` 的 `:pagination` 直接内联写 `{ current: goodsPage, pageSize: goodsPageSize, total: goodsTotal }`,对象属性中的 ref 不会被 Vue 自动解包,Pagination 拿到的是 ref 对象而非 number,导致分页按钮点不动。
|
||||
- 修复:
|
||||
- 提取 `goodsPagination = computed(() => ({ total: goodsTotal.value, current: goodsPage.value, pageSize: goodsPageSize.value, showSizeChanger: true, onChange: ... }))`
|
||||
- `:pagination="goodsPagination"`
|
||||
- `onChange` 里 `loadGoods(currentSectionId.value)` 修正为 `.value`
|
||||
- 验证:`vite build` 通过(exit 0)。
|
||||
|
||||
## 专区商品图片走压缩 URL
|
||||
- 文件:`src/views/special/zone/index.vue`
|
||||
- 改动:专区商品抽屉的已关联商品列表「图片」列,从直接 `:src="text"` 改为 `:src="getCompressedImageUrl(text, { width: 120 })"`,缩小缩略图体积并统一走 OSS 图片处理。
|
||||
- 说明:`getCompressedImageUrl` 已导入,无需额外引入。
|
||||
@@ -0,0 +1,40 @@
|
||||
# 2026-08-18 工作日志
|
||||
|
||||
## 专区商城价 = 市场价 × 折扣系数 功能(已实现)
|
||||
|
||||
客户要求:专区的商城价显示为市场价的 0.78(并按此价成交)。已与用户确认:展示+成交价都按系数、每专区单独配(默认1.00)、基数=市场价 salePrice。
|
||||
|
||||
### 后端 guilixu-java(仅此,商城走 guilixu-java)
|
||||
- `ShopHomeSection` + `ShopHomeSectionParam` 增加 `discountRate`(BigDecimal, 默认1.00)。
|
||||
- 迁移 SQL:`guilixu-admin/zone-discount-rate-10606.sql`(`ALTER TABLE shop_home_section ADD discount_rate DECIMAL(10,2) NOT NULL DEFAULT 1.00`)。需在生产库执行。
|
||||
- `OrderCreateRequest.OrderGoodsItem` 增加 `sectionId`(每商品项携带专区ID)。
|
||||
- `OrderBusinessService` **两处**价格解析(①`validateAndCalculateTotal` 算总额 ②`createOrder` 落库订单商品价)都加了专区折扣逻辑:若 `item.sectionId` 命中且商品 `section_ids` 含它、且专区 `discountRate≠1`,则 `actualPrice = goods.getSalePrice() × rate`(保留两位小数,HALF_UP)。后端不信任前端传价,按 sectionId 重算,防篡改。
|
||||
|
||||
### 后台 guilixu-admin
|
||||
- `src/api/shop/shopZone/model/index.ts` 的 `HomeSection` 增加 `discountRate?: number`。
|
||||
- `src/views/special/zone/components/zoneEdit.vue`:表单加「折扣系数」输入框(a-input-number,0.01~1,默认1,提示"专区商城价=市场价×系数"),表单默认值 `discountRate: 1`。
|
||||
|
||||
### 小程序 guilixu-taro(仅「专区→立即购买」链路打通)
|
||||
- `src/api/shop/shopHomeSection/index.ts` 增加 `getHomeSection(id)`。
|
||||
- `src/api/shop/shopHomeSection/model/index.ts` 的 `HomeSection` 增加 `discountRate?`。
|
||||
- `section-detail.tsx`:进入时 `getHomeSection` 取折扣系数并展示(价格=市场价×系数,划线价显示市场价),跳转详情带 `sectionId`+`discountRate`。
|
||||
- `product-detail.tsx`:读 `sectionId`/`discountRate`,按系数展示;立即购买时把系数写入 `buy_now` 存储并带参跳结算。
|
||||
- `src/api/shop/shopOrder/model/index.ts` 的 `OrderGoodsItem` 增加 `sectionId?`。
|
||||
- `checkout.tsx`:增加 `getItemDisplayPrice`/`getItemSectionId`,按系数估价并随下单提交 `sectionId`。
|
||||
|
||||
### ⚠️ 已知限制
|
||||
- **购物车链路不支持专区折扣**:服务端 `shop_cart` 表无 `sectionId` 字段,从专区「加入购物车」再结算会走原价(仅「立即购买」享受折扣)。若要购物车也生效,需给 `shop_cart` 表加 `section_id` 并在下单时透传——后续需求。
|
||||
- 未执行迁移 SQL、未重新部署后端、未构建发布前端,需按环境操作。
|
||||
|
||||
### 验证
|
||||
- 前端 `tsc --noEmit` 因环境配置(taro 缺 @tarojs/taro 类型、admin 大量既存错误)未完整跑通,但本次改动文件未产生任何新报错。
|
||||
- 后端 Maven 不在本环境,未编译;改动为标准 Java,已逐行核对。
|
||||
|
||||
## 购物车链路打通专区折扣(补充实现)
|
||||
|
||||
用户要求把"购物车也支持专区折扣"补上。在「立即购买」已实现基础上,打通购物车链路:
|
||||
|
||||
- **后端 guilixu-java**:`ShopCart` 实体加 `sectionId`(真实列)+`discountRate`(exist=false 关联字段);`ShopCartMapper.xml` 的 selectSql 增加 `LEFT JOIN shop_home_section hs ON a.section_id = hs.section_id` 并选 `hs.discount_rate AS discount_rate`(section_id 为 NULL 时不影响普通商品);`ShopCartServiceImpl.addToCart` 在"已存在累加"分支若本次新传 sectionId 非空则刷新 `existCart.sectionId`(让专区加购覆盖普通加购的 null)。迁移SQL:`zone-cart-section-id-10606.sql`(`ALTER TABLE shop_cart ADD section_id INT NULL`)。
|
||||
- **小程序 guilixu-taro**:`CartContext.addItem` 增可选 `sectionId` 参数并透传给加购接口;`refresh` 映射带 `sectionId`/`discountRate`;`CartItem` 接口加两字段;`calcPrice` 在 `discountRate≠1` 时用 `salePrice×rate`;`section-detail`/`product-detail` 加购调用传 `sectionId`;`cart.tsx` 单价展示用折扣价;`checkout.tsx` 的 `getItemDisplayPrice` 折扣分支改读顶层 `salePrice`(购物车项 product 为空,否则算不出折扣价)。
|
||||
- 下单成交价仍由 `OrderBusinessService` 按 `sectionId` 重算,购物车项带 sectionId 后自动生效,无需改下单逻辑。
|
||||
- 验证:taro `tsc --noEmit` 对本次改动 5 个文件零新报错。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 项目长期记忆
|
||||
|
||||
## 专区折扣系数功能(2026-08-18 新增)
|
||||
- 需求:专区商城价 = 市场价(salePrice) × discountRate(每专区可配,默认 1.00;本专区用 0.78)。
|
||||
- **后端 guilixu-java**:`ShopHomeSection.discountRate` 字段 + 迁移SQL(`guilixu-admin/zone-discount-rate-10606.sql`);`OrderCreateRequest.OrderGoodsItem.sectionId`;`OrderBusinessService` 两处价格解析按 sectionId 重算成交价= salePrice×rate(后端算,不信任前端价)。
|
||||
- **后台 guilixu-admin**:`zoneEdit.vue` 有「折扣系数」输入;`shopZone/model` HomeSection 有 `discountRate`。
|
||||
- **小程序 guilixu-taro**:`section-detail`→`product-detail`→`checkout` 透传 sectionId+discountRate,「立即购买」与「购物车」两条链路均按系数生效(购物车补充见下)。
|
||||
- **购物车链路补充(同日)**:`shop_cart` 表加 `section_id` 列(迁移SQL见 `zone-cart-section-id-10606.sql`);`ShopCart` 实体加 `sectionId`(真实列)+`discountRate`(exist=false)、`ShopCartMapper.xml` LEFT JOIN 专区取 `discount_rate`;`addToCart` 在"已存在累加"分支若新传 sectionId 非空则刷新 `existCart.sectionId`(专区加购覆盖普通加购的 null);`CartContext.addItem` 透传 sectionId、`calcPrice` 与 `cart.tsx` 按系数展示;`checkout.tsx` 的 `getItemDisplayPrice` 折扣分支改读顶层 `salePrice`(购物车项 product 为空)。两条链路现在都生效。
|
||||
- 生效前置:执行迁移SQL、重新部署 guilixu-java、构建发布 taro。
|
||||
|
||||
## 双后端事实(沿用)
|
||||
- 商城接口实际调 `https://guilixu-api.websoft.top`(= guilixu-java, db_guilixu);cms 走 mp-java。改商城相关先确认运行时 ApiUrl。详见历史 MEMORY。
|
||||
@@ -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` 增加模式切换 + 列表回显。*
|
||||
@@ -0,0 +1,22 @@
|
||||
-- ============================================================
|
||||
-- 修复历史已支付订单的订单商品支付状态(专区销量统计数据补偿)
|
||||
-- 适用后端:guilixu-java(本部署商城实际后端,db_guilixu)
|
||||
-- 租户:10606
|
||||
-- 背景:下单时 shop_order_goods.pay_status 默认 0,支付成功链路此前只更新
|
||||
-- 主订单 shop_order.pay_status,不会同步订单商品表。专区销量统计按
|
||||
-- shop_order_goods.pay_status=1 聚合,导致历史已支付订单从未被统计。
|
||||
-- 作用:把主订单已支付(pay_status=1)且订单商品仍为未支付(pay_status=0)的记录,
|
||||
-- 同步为已支付,并把 order_status 对齐主订单(退款订单 order_status=6 会被统计口径排除)。
|
||||
-- 执行前请先备份,并在低峰期执行;可先 SELECT 预览影响行数:
|
||||
-- SELECT COUNT(*) FROM shop_order_goods og
|
||||
-- JOIN shop_order o ON og.order_id = o.order_id AND o.tenant_id = 10606
|
||||
-- WHERE og.tenant_id = 10606 AND o.pay_status = 1 AND og.pay_status = 0;
|
||||
-- ============================================================
|
||||
|
||||
UPDATE shop_order_goods og
|
||||
JOIN shop_order o ON og.order_id = o.order_id AND o.tenant_id = 10606
|
||||
SET og.pay_status = 1,
|
||||
og.order_status = o.order_status
|
||||
WHERE og.tenant_id = 10606
|
||||
AND o.pay_status = 1
|
||||
AND og.pay_status = 0;
|
||||
@@ -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,52 @@
|
||||
-- ============================================================
|
||||
-- 报表统计菜单(tenant_id=10606,挂在「商城」顶级菜单下)
|
||||
-- 作用:让前端动态路由 /statistics 自动生效(component=shop/statistics)
|
||||
-- 说明:sys_menu 在 gxwebsoft_core 库;本 SQL 用全限定名,可在任意有权限的库执行。
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 插入报表统计菜单
|
||||
-- parent_id / app_id 自动取自「商城」顶级菜单(title 含"商城",parent_id=0)
|
||||
-- NOT EXISTS 防止重复插入(重复执行安全)
|
||||
INSERT INTO gxwebsoft_core.sys_menu
|
||||
(parent_id, title, path, component, menu_type, sort_number, authority, icon, hide, tenant_id, app_id, deleted)
|
||||
SELECT
|
||||
p.menu_id, -- 商城父级 menu_id
|
||||
'报表统计',
|
||||
'/statistics',
|
||||
'shop/statistics',
|
||||
0, -- 0=菜单
|
||||
60, -- 排序号(放商城菜单末尾)
|
||||
'shop:statistics:view',
|
||||
'BarChartOutlined',
|
||||
0, -- 0=显示
|
||||
10606,
|
||||
p.app_id,
|
||||
0
|
||||
FROM gxwebsoft_core.sys_menu p
|
||||
WHERE p.tenant_id = 10606
|
||||
AND p.parent_id = 0
|
||||
AND p.title LIKE '%商城%'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM gxwebsoft_core.sys_menu s
|
||||
WHERE s.tenant_id = 10606 AND s.component = 'shop/statistics'
|
||||
)
|
||||
LIMIT 1;
|
||||
|
||||
-- 2) 把新菜单分配给「商城」父级已有的角色(否则角色没有该菜单权限,左侧导航看不到)
|
||||
INSERT INTO gxwebsoft_core.sys_role_menu (role_id, menu_id)
|
||||
SELECT rm.role_id, m.menu_id
|
||||
FROM gxwebsoft_core.sys_menu m
|
||||
JOIN gxwebsoft_core.sys_menu p ON p.menu_id = m.parent_id
|
||||
JOIN gxwebsoft_core.sys_role_menu rm ON rm.menu_id = p.menu_id
|
||||
WHERE m.tenant_id = 10606
|
||||
AND m.component = 'shop/statistics'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM gxwebsoft_core.sys_role_menu x
|
||||
WHERE x.role_id = rm.role_id AND x.menu_id = m.menu_id
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- 排查辅助:若第1步未插入(商城父级标题不含"商城"),先用下面语句确认父级 menu_id
|
||||
-- SELECT menu_id, title, app_id FROM gxwebsoft_core.sys_menu
|
||||
-- WHERE tenant_id=10606 AND parent_id=0 AND title LIKE '%商城%';
|
||||
-- ============================================================
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface ShopGoodsBrowse {
|
||||
goodsName?: string;
|
||||
/** 商品封面图(关联查询) */
|
||||
goodsImage?: string;
|
||||
/** 商品价格(关联查询) */
|
||||
/** 商城价(关联查询) */
|
||||
price?: string;
|
||||
/** 市场价(关联查询) */
|
||||
salePrice?: string;
|
||||
|
||||
@@ -12,12 +12,14 @@ export interface ShopGoodsSku {
|
||||
sku?: string;
|
||||
// 商品图片
|
||||
image?: string;
|
||||
// 商品价格
|
||||
// 商城价
|
||||
price?: string;
|
||||
// 市场价格
|
||||
salePrice?: string;
|
||||
// 成本价
|
||||
cost?: string;
|
||||
// 会员价
|
||||
buyingPrice?: string;
|
||||
// 库存
|
||||
stock?: number;
|
||||
// sku编码
|
||||
|
||||
@@ -173,3 +173,49 @@ export async function confirmOfflinePayment(
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult } from '@/api';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
import type {
|
||||
ShopOrderStatsOverview,
|
||||
ShopOrderTrendItem,
|
||||
ShopGoodsRankItem,
|
||||
ShopOrderStatusDist,
|
||||
StatsRangeParams
|
||||
} from './model';
|
||||
|
||||
/**
|
||||
* 经营概览(KPI 卡片)
|
||||
*/
|
||||
export async function getShopOrderStatsOverview(
|
||||
params: StatsRangeParams
|
||||
): Promise<ShopOrderStatsOverview> {
|
||||
const res = await request.get<ApiResult<ShopOrderStatsOverview>>(
|
||||
MODULES_API_URL + '/shop/shop-order/stats/overview',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as ShopOrderStatsOverview;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 销售趋势(type=day|week|month)
|
||||
*/
|
||||
export async function getShopOrderStatsTrend(
|
||||
params: StatsRangeParams & { type?: string }
|
||||
): Promise<ShopOrderTrendItem[]> {
|
||||
const res = await request.get<ApiResult<ShopOrderTrendItem[]>>(
|
||||
MODULES_API_URL + '/shop/shop-order/stats/trend',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as ShopOrderTrendItem[];
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 商品销量排行
|
||||
*/
|
||||
export async function getShopOrderGoodsRank(
|
||||
params: StatsRangeParams & { limit?: number }
|
||||
): Promise<ShopGoodsRankItem[]> {
|
||||
const res = await request.get<ApiResult<ShopGoodsRankItem[]>>(
|
||||
MODULES_API_URL + '/shop/shop-order/stats/goods-rank',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as ShopGoodsRankItem[];
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单/退款分布
|
||||
*/
|
||||
export async function getShopOrderStatsStatusDist(
|
||||
params: StatsRangeParams
|
||||
): Promise<ShopOrderStatusDist> {
|
||||
const res = await request.get<ApiResult<ShopOrderStatusDist>>(
|
||||
MODULES_API_URL + '/shop/shop-order/stats/status-dist',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0) {
|
||||
return res.data.data as ShopOrderStatusDist;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 商城报表统计 - 类型定义
|
||||
*/
|
||||
|
||||
/** 经营概览(GMV + 实收双口径) */
|
||||
export interface ShopOrderStatsOverview {
|
||||
/** GMV(含未支付),按订单面额 total_price 求和 */
|
||||
gmvSales: number;
|
||||
/** 实收金额(仅已支付),按 pay_price 求和 */
|
||||
paidSales: number;
|
||||
/** 订单总数(含未支付) */
|
||||
orderCount: number;
|
||||
/** 已支付订单数 */
|
||||
paidOrderCount: number;
|
||||
/** 客单价 = 实收 / 已支付订单数 */
|
||||
customerUnitPrice: number;
|
||||
/** 退款金额 */
|
||||
refundAmount: number;
|
||||
/** 区间新增会员数 */
|
||||
newUserCount: number;
|
||||
/** 使用优惠券的订单数 */
|
||||
couponUsedCount: number;
|
||||
}
|
||||
|
||||
/** 销售趋势周期项 */
|
||||
export interface ShopOrderTrendItem {
|
||||
/** 周期:day=yyyy-MM-dd / week=yyyy-ww / month=yyyy-MM */
|
||||
period: string;
|
||||
gmvSales: number;
|
||||
paidSales: number;
|
||||
orderCount: number;
|
||||
paidOrderCount: number;
|
||||
}
|
||||
|
||||
/** 商品销量排行项 */
|
||||
export interface ShopGoodsRankItem {
|
||||
goodsId: number;
|
||||
goodsName: string;
|
||||
totalNum: number;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
/** 订单状态分布项 */
|
||||
export interface ShopOrderStatusItem {
|
||||
status: number;
|
||||
statusName: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** 订单/退款分布 */
|
||||
export interface ShopOrderStatusDist {
|
||||
statusCounts: ShopOrderStatusItem[];
|
||||
orderCount: number;
|
||||
paidOrderCount: number;
|
||||
refundCount: number;
|
||||
refundAmount: number;
|
||||
/** 退款率(%) */
|
||||
refundRate: number;
|
||||
}
|
||||
|
||||
/** 区间参数(start/end 格式 yyyy-MM-dd HH:mm:ss) */
|
||||
export interface StatsRangeParams {
|
||||
start: string;
|
||||
end: string;
|
||||
}
|
||||
@@ -77,6 +77,8 @@ export interface ShopSetting {
|
||||
payAlipayPrivateKey?: string;
|
||||
payAlipayPublicKey?: string;
|
||||
payBalanceEnabled?: boolean | number;
|
||||
|
||||
salesAccumulateType?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type {
|
||||
HomeSection,
|
||||
HomeSectionParam,
|
||||
SectionPermission,
|
||||
SectionGoods,
|
||||
SectionSalesStatsVO
|
||||
} from './model';
|
||||
import type { User } from '@/api/system/user/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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询专区白名单用户(返回用户详情:userId/realName/mobile/nickname/organizationName)
|
||||
*/
|
||||
export async function listSectionUsers(id: number) {
|
||||
const res = await request.get<ApiResult<User[]>>(
|
||||
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 addSectionUsers(id: number, userIds: number[]) {
|
||||
const res = await request.post<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 removeSectionUsers(id: number, userIds: number[]) {
|
||||
const res = await request.delete<ApiResult<unknown>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/users',
|
||||
{ data: 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取专区小程序码(图片流),返回可用于 <img src> 的 objectURL
|
||||
* @param id 专区ID
|
||||
* @param envVersion release(正式版) / trial(体验版) / develop(开发版)
|
||||
*/
|
||||
export async function getSectionQrcode(
|
||||
id: number,
|
||||
envVersion: string = 'release'
|
||||
): Promise<string> {
|
||||
const res = await request.get(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/qrcode',
|
||||
{ responseType: 'blob', params: { envVersion } } as any
|
||||
);
|
||||
const blob = res.data as Blob;
|
||||
// 微信/后端出错时会返回 JSON 而非图片,这里解析出错误信息
|
||||
if (blob && blob.type && blob.type.indexOf('image') === -1) {
|
||||
let msg = '生成小程序码失败';
|
||||
try {
|
||||
const json = JSON.parse(await blob.text());
|
||||
if (json && json.error) msg = json.error;
|
||||
} catch (e) {
|
||||
/* 忽略解析失败 */
|
||||
}
|
||||
return Promise.reject(new Error(msg));
|
||||
}
|
||||
return window.URL.createObjectURL(blob);
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区销量统计(销量件数 + 销售额 + 商品排行), 支持时间范围 start/end(yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
export async function getSectionSalesStats(
|
||||
id: number,
|
||||
params?: { start?: string; end?: string }
|
||||
) {
|
||||
const res = await request.get<ApiResult<SectionSalesStatsVO>>(
|
||||
MODULES_API_URL + '/shop/shop-home-section/' + id + '/stats',
|
||||
{ params }
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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;
|
||||
// 专区商城价折扣系数, 成交价=市场价×系数, 默认1.00不打折
|
||||
discountRate?: number;
|
||||
// 排序号(数字越小越靠前)
|
||||
sortNumber?: number;
|
||||
// 状态 0正常 1禁用
|
||||
status?: number;
|
||||
// 生效开始时间
|
||||
startTime?: string;
|
||||
// 生效结束时间
|
||||
endTime?: string;
|
||||
// 用户ID
|
||||
userId?: number;
|
||||
// 是否删除 0否 1是
|
||||
deleted?: number;
|
||||
// 创建时间
|
||||
createTime?: string;
|
||||
// 修改时间
|
||||
updateTime?: string;
|
||||
// 销量件数(统计, 非DB字段)
|
||||
salesNum?: number;
|
||||
// 销售额(统计, 非DB字段)
|
||||
salesAmount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页专区搜索条件
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区销量统计(含商品排行)
|
||||
*/
|
||||
export interface SectionSalesStatsVO {
|
||||
// 专区ID
|
||||
sectionId?: number;
|
||||
// 销量件数
|
||||
salesNum?: number;
|
||||
// 销售额
|
||||
salesAmount?: number;
|
||||
// 统计开始时间
|
||||
startTime?: string;
|
||||
// 统计结束时间
|
||||
endTime?: string;
|
||||
// 商品销量排行
|
||||
goodsRank?: SectionGoodsRankItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 专区商品销量排行项
|
||||
*/
|
||||
export interface SectionGoodsRankItem {
|
||||
goodsId?: number;
|
||||
goodsName?: string;
|
||||
salesNum?: number;
|
||||
salesAmount?: 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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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: '立即发送'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+60
-268
@@ -1,21 +1,26 @@
|
||||
/**
|
||||
* 统计数据 store
|
||||
* 说明:今日概况(销售额/订单数/新增会员/用券数)改为调用后端聚合接口
|
||||
* /shop/shop-order/stats/overview,不再前端拉全量订单求和,也不再依赖 cms_statistics 表。
|
||||
*/
|
||||
import { defineStore } from 'pinia';
|
||||
import dayjs from 'dayjs';
|
||||
import { pageUsers } from '@/api/system/user';
|
||||
import { pageShopOrder, shopOrderTotal, listShopOrder } from '@/api/shop/shopOrder';
|
||||
import {
|
||||
addCmsStatistics,
|
||||
listCmsStatistics,
|
||||
updateCmsStatistics
|
||||
} from '@/api/cms/cmsStatistics';
|
||||
import { CmsStatistics } from '@/api/cms/cmsStatistics/model';
|
||||
import { safeNumber, hasValidId } from '@/utils/type-guards';
|
||||
import { pageShopOrder, shopOrderTotal } from '@/api/shop/shopOrder';
|
||||
import { getShopOrderStatsOverview } from '@/api/shop/shopOrderStats';
|
||||
import { safeNumber } from '@/utils/type-guards';
|
||||
|
||||
export interface StatisticsState {
|
||||
// 统计数据
|
||||
statistics: CmsStatistics | null;
|
||||
statistics: {
|
||||
userCount: number;
|
||||
orderCount: number;
|
||||
totalSales: number;
|
||||
todaySales: number;
|
||||
monthSales: number;
|
||||
todayOrders: number;
|
||||
todayUsers: number;
|
||||
} | null;
|
||||
// 加载状态
|
||||
loading: boolean;
|
||||
// 最后更新时间
|
||||
@@ -40,65 +45,14 @@ export const useStatisticsStore = defineStore('statistics', {
|
||||
}),
|
||||
|
||||
getters: {
|
||||
/**
|
||||
* 获取用户总数
|
||||
*/
|
||||
userCount: (state): number => {
|
||||
return safeNumber(state.statistics?.userCount);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取订单总数
|
||||
*/
|
||||
orderCount: (state): number => {
|
||||
return safeNumber(state.statistics?.orderCount);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取总销售额
|
||||
*/
|
||||
totalSales: (state): number => {
|
||||
return safeNumber(state.statistics?.totalSales);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取今日销售额
|
||||
*/
|
||||
todaySales: (state): number => {
|
||||
return safeNumber(state.statistics?.todaySales);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取本月销售额
|
||||
*/
|
||||
monthSales: (state): number => {
|
||||
return safeNumber(state.statistics?.monthSales);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取今日订单数
|
||||
*/
|
||||
todayOrders: (state): number => {
|
||||
return safeNumber(state.statistics?.todayOrders);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取今日新增用户
|
||||
*/
|
||||
todayUsers: (state): number => {
|
||||
return safeNumber(state.statistics?.todayUsers);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取今日使用优惠券数量(安全取值)
|
||||
*/
|
||||
safeCouponUsedCount: (state): number => {
|
||||
return safeNumber(state.couponUsedCount);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查缓存是否有效
|
||||
*/
|
||||
userCount: (state): number => safeNumber(state.statistics?.userCount),
|
||||
orderCount: (state): number => safeNumber(state.statistics?.orderCount),
|
||||
totalSales: (state): number => safeNumber(state.statistics?.totalSales),
|
||||
todaySales: (state): number => safeNumber(state.statistics?.todaySales),
|
||||
monthSales: (state): number => safeNumber(state.statistics?.monthSales),
|
||||
todayOrders: (state): number => safeNumber(state.statistics?.todayOrders),
|
||||
todayUsers: (state): number => safeNumber(state.statistics?.todayUsers),
|
||||
safeCouponUsedCount: (state): number => safeNumber(state.couponUsedCount),
|
||||
isCacheValid: (state): boolean => {
|
||||
if (!state.lastUpdateTime) return false;
|
||||
const now = Date.now();
|
||||
@@ -107,40 +61,35 @@ export const useStatisticsStore = defineStore('statistics', {
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 获取统计数据
|
||||
* @param forceRefresh 是否强制刷新
|
||||
*/
|
||||
async fetchStatistics(forceRefresh = false) {
|
||||
// 如果缓存有效且不强制刷新,直接返回缓存数据
|
||||
if (!forceRefresh && this.isCacheValid && this.statistics) {
|
||||
return this.statistics;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
// 并行获取各种统计数据,使用Promise.allSettled确保部分失败不影响整体
|
||||
const [usersResult, ordersResult, totalResult, statisticsResult] =
|
||||
// 今日区间(与后端 dayjs startOf/endOf('day') 对齐)
|
||||
const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 并行获取:累计用户数 / 累计订单数 / 累计实收 / 今日概览(后端聚合)
|
||||
const [usersResult, ordersResult, totalResult, overviewResult] =
|
||||
await Promise.allSettled([
|
||||
pageUsers({ page: 1, limit: 1 }),
|
||||
pageShopOrder({ page: 1, limit: 1 }),
|
||||
shopOrderTotal(),
|
||||
listCmsStatistics({})
|
||||
getShopOrderStatsOverview({ start: todayStart, end: todayEnd })
|
||||
]);
|
||||
|
||||
// 安全提取结果
|
||||
const users =
|
||||
usersResult.status === 'fulfilled' ? usersResult.value : null;
|
||||
const orders =
|
||||
ordersResult.status === 'fulfilled' ? ordersResult.value : null;
|
||||
const total =
|
||||
totalResult.status === 'fulfilled' ? totalResult.value : null;
|
||||
const statisticsData =
|
||||
statisticsResult.status === 'fulfilled'
|
||||
? statisticsResult.value
|
||||
: null;
|
||||
const overview =
|
||||
overviewResult.status === 'fulfilled' ? overviewResult.value : null;
|
||||
|
||||
// 记录失败的API调用
|
||||
if (usersResult.status === 'rejected') {
|
||||
console.error('❌ 用户API调用失败:', usersResult.reason);
|
||||
}
|
||||
@@ -150,178 +99,41 @@ export const useStatisticsStore = defineStore('statistics', {
|
||||
if (totalResult.status === 'rejected') {
|
||||
console.error('❌ 订单总额API调用失败:', totalResult.reason);
|
||||
}
|
||||
if (statisticsResult.status === 'rejected') {
|
||||
console.error('❌ 统计数据API调用失败:', statisticsResult.reason);
|
||||
if (overviewResult.status === 'rejected') {
|
||||
console.error('❌ 今日概览API调用失败:', overviewResult.reason);
|
||||
}
|
||||
|
||||
// 添加调试日志
|
||||
console.log('🔍 统计数据获取结果:', {
|
||||
users: users,
|
||||
orders: orders,
|
||||
total: total,
|
||||
statisticsData: statisticsData
|
||||
});
|
||||
const userCount =
|
||||
users && typeof users === 'object' && 'count' in users
|
||||
? safeNumber((users as any).count)
|
||||
: 0;
|
||||
const orderCount =
|
||||
orders && typeof orders === 'object' && 'count' in orders
|
||||
? safeNumber((orders as any).count)
|
||||
: 0;
|
||||
const totalSales = safeNumber(total);
|
||||
|
||||
let statistics: CmsStatistics;
|
||||
// 今日数据走后端聚合接口(实收 + 总数 + 新增会员 + 用券数)
|
||||
const todaySales = overview ? safeNumber(overview.paidSales) : 0;
|
||||
const todayOrders = overview ? safeNumber(overview.orderCount) : 0;
|
||||
const todayUsers = overview ? safeNumber(overview.newUserCount) : 0;
|
||||
const couponUsedCount = overview
|
||||
? safeNumber(overview.couponUsedCount)
|
||||
: 0;
|
||||
|
||||
// 安全获取用户数量,添加更详细的验证
|
||||
const userCount = (() => {
|
||||
if (!users) {
|
||||
console.warn('⚠️ 用户API返回空数据');
|
||||
return 0;
|
||||
}
|
||||
if (typeof users === 'object' && 'count' in users) {
|
||||
const count = users.count;
|
||||
console.log('✅ 用户数量:', count);
|
||||
return safeNumber(count);
|
||||
}
|
||||
console.warn('⚠️ 用户API返回数据格式不正确:', users);
|
||||
return 0;
|
||||
})();
|
||||
|
||||
// 安全获取订单数量
|
||||
const orderCount = (() => {
|
||||
if (!orders) {
|
||||
console.warn('⚠️ 订单API返回空数据');
|
||||
return 0;
|
||||
}
|
||||
if (typeof orders === 'object' && 'count' in orders) {
|
||||
const count = orders.count;
|
||||
console.log('✅ 订单数量:', count);
|
||||
return safeNumber(count);
|
||||
}
|
||||
console.warn('⚠️ 订单API返回数据格式不正确:', orders);
|
||||
return 0;
|
||||
})();
|
||||
|
||||
// 实时计算今日数据(不依赖可能未更新的统计表)
|
||||
const todayStart = dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
const todayEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
|
||||
|
||||
// 安全获取今日订单列表、销售额和优惠券使用量
|
||||
let todayOrders = 0;
|
||||
let todaySales = 0;
|
||||
let couponUsedCount = 0;
|
||||
try {
|
||||
const todayOrderList = await listShopOrder({
|
||||
createTimeStart: todayStart,
|
||||
createTimeEnd: todayEnd
|
||||
});
|
||||
if (Array.isArray(todayOrderList)) {
|
||||
todayOrders = todayOrderList.length;
|
||||
todaySales = todayOrderList.reduce((acc, order) => {
|
||||
return acc + (order.payStatus ? safeNumber(order.payPrice) : 0);
|
||||
}, 0);
|
||||
couponUsedCount = todayOrderList.filter((order) => {
|
||||
const couponType = order.couponType;
|
||||
return couponType !== undefined && couponType !== null && couponType !== 0;
|
||||
}).length;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ 获取今日订单列表失败:', e);
|
||||
}
|
||||
|
||||
// 安全获取今日新增用户
|
||||
let todayUsers = 0;
|
||||
try {
|
||||
const todayUsersResult = await pageUsers({
|
||||
page: 1,
|
||||
limit: 1,
|
||||
createTimeStart: todayStart,
|
||||
createTimeEnd: todayEnd
|
||||
});
|
||||
if (todayUsersResult && typeof todayUsersResult === 'object' && 'count' in todayUsersResult) {
|
||||
todayUsers = safeNumber(todayUsersResult.count);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ 获取今日新增用户失败:', e);
|
||||
}
|
||||
|
||||
const totalSales = (() => {
|
||||
if (!total) {
|
||||
console.warn('⚠️ 订单总额API返回空数据');
|
||||
return 0;
|
||||
}
|
||||
if (Array.isArray(total)) {
|
||||
// 如果是数组,计算总金额
|
||||
const sum = total.reduce((acc, order) => {
|
||||
const amount = order.payPrice || order.totalPrice || 0;
|
||||
return acc + safeNumber(amount);
|
||||
}, 0);
|
||||
console.log('✅ 总销售额(数组计算):', sum);
|
||||
return sum;
|
||||
}
|
||||
const amount = safeNumber(total);
|
||||
console.log('✅ 总销售额(直接值):', amount);
|
||||
return amount;
|
||||
})();
|
||||
|
||||
if (statisticsData && statisticsData.length > 0) {
|
||||
// 更新现有统计数据
|
||||
const existingStatistics = statisticsData[0];
|
||||
|
||||
// 确保数据存在且有有效的 ID
|
||||
if (hasValidId(existingStatistics)) {
|
||||
const updateData: Partial<CmsStatistics> = {
|
||||
id: existingStatistics.id,
|
||||
userCount: userCount,
|
||||
orderCount: orderCount,
|
||||
totalSales: totalSales,
|
||||
todaySales: todaySales,
|
||||
todayOrders: todayOrders,
|
||||
todayUsers: todayUsers
|
||||
};
|
||||
|
||||
// 异步更新数据库
|
||||
setTimeout(() => {
|
||||
updateCmsStatistics(updateData).catch((error) => {
|
||||
console.error('更新统计数据失败:', error);
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
// 更新本地数据
|
||||
statistics = {
|
||||
...existingStatistics,
|
||||
...updateData,
|
||||
todaySales,
|
||||
todayOrders,
|
||||
todayUsers
|
||||
};
|
||||
} else {
|
||||
// 如果现有数据无效,使用基础数据
|
||||
statistics = {
|
||||
userCount: userCount,
|
||||
orderCount: orderCount,
|
||||
totalSales: totalSales,
|
||||
todaySales: todaySales,
|
||||
todayOrders: todayOrders,
|
||||
todayUsers: todayUsers
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// 创建新的统计数据
|
||||
statistics = {
|
||||
userCount: userCount,
|
||||
orderCount: orderCount,
|
||||
totalSales: totalSales,
|
||||
todaySales: todaySales,
|
||||
todayOrders: todayOrders,
|
||||
todayUsers: todayUsers
|
||||
};
|
||||
|
||||
// 异步保存到数据库
|
||||
setTimeout(() => {
|
||||
addCmsStatistics(statistics).catch((error) => {
|
||||
console.error('保存统计数据失败:', error);
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
this.statistics = statistics;
|
||||
this.statistics = {
|
||||
userCount,
|
||||
orderCount,
|
||||
totalSales,
|
||||
todaySales,
|
||||
monthSales: todaySales,
|
||||
todayOrders,
|
||||
todayUsers
|
||||
};
|
||||
this.couponUsedCount = couponUsedCount;
|
||||
this.lastUpdateTime = Date.now();
|
||||
|
||||
return statistics;
|
||||
return this.statistics;
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
throw error;
|
||||
@@ -330,44 +142,27 @@ export const useStatisticsStore = defineStore('statistics', {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新统计数据
|
||||
*/
|
||||
updateStatistics(statistics: Partial<CmsStatistics>) {
|
||||
updateStatistics(statistics: Partial<StatisticsState['statistics']>) {
|
||||
if (this.statistics) {
|
||||
this.statistics = { ...this.statistics, ...statistics };
|
||||
this.lastUpdateTime = Date.now();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
clearCache() {
|
||||
this.statistics = null;
|
||||
this.lastUpdateTime = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 强制刷新统计数据
|
||||
*/
|
||||
async forceRefresh() {
|
||||
console.log('🔄 强制刷新统计数据...');
|
||||
this.clearCache();
|
||||
return await this.fetchStatistics(true);
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置缓存有效期
|
||||
*/
|
||||
setCacheExpiry(expiry: number) {
|
||||
this.cacheExpiry = expiry;
|
||||
},
|
||||
|
||||
/**
|
||||
* 开始自动刷新
|
||||
* @param interval 刷新间隔(毫秒),默认5分钟
|
||||
*/
|
||||
startAutoRefresh(interval = 5 * 60 * 1000) {
|
||||
this.stopAutoRefresh();
|
||||
this.refreshTimer = window.setInterval(() => {
|
||||
@@ -375,9 +170,6 @@ export const useStatisticsStore = defineStore('statistics', {
|
||||
}, interval);
|
||||
},
|
||||
|
||||
/**
|
||||
* 停止自动刷新
|
||||
*/
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
|
||||
+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) {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文件选择提示
|
||||
|
||||
@@ -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,8 +353,62 @@
|
||||
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 = {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -344,10 +344,10 @@ const { loading: statisticsLoading } = storeToRefs(statisticsStore);
|
||||
// ============================================================
|
||||
// 基本信息:数据来源拆分
|
||||
// - 身份信息(系统名称/版本号/运行状态)来自 app_product (productId=65)
|
||||
// - 时间线(创建时间/到期时间/系统运行天数)来自 app_subscription
|
||||
// · 创建时间 = subscription.startTime(订阅生效时间)
|
||||
// - 时间线(开通时间/到期时间)来自 app_subscription
|
||||
// · 开通时间 = subscription.startTime(订阅生效时间)
|
||||
// · 到期时间 = subscription.expireTime
|
||||
// · 系统运行天数 = 自 subscription.createTime 起累计
|
||||
// - 系统运行天数 = 自租户创建时间起累计(tenantStore.company.createTime)
|
||||
// ============================================================
|
||||
// 当前应用产品(按固定 productId=65 查询)
|
||||
const currentProduct = ref<AppProduct | null>(null);
|
||||
@@ -643,15 +643,13 @@ const onPayModalClose = () => {
|
||||
// 计算属性
|
||||
const now = ref(Date.now());
|
||||
let runDaysTimer: ReturnType<typeof setInterval>;
|
||||
// 系统运行天数:自订阅生效时间起累计
|
||||
// 锚定 currentSubscription.startTime(与"创建时间"行同源,语义统一)。
|
||||
// 注意:若后端续费为新增订阅记录,此处取的是 expireTime 最新的 active 订阅,
|
||||
// startTime 可能是续费生效日,runDays 会从续费日重新计数。
|
||||
// 如需"累计运行天数",应改用最早的 active 订阅 startTime 或后端额外提供字段。
|
||||
// 系统运行天数:自租户创建时间起累计
|
||||
// 锚定 tenantStore.company.createTime(租户建档时间),与后台租户创建时间一致。
|
||||
// 数据来源:loadData 中已调用 tenantStore.fetchTenantInfo() 写入 tenantStore.company。
|
||||
const runDays = computed(() => {
|
||||
const startTime = currentSubscription.value?.startTime;
|
||||
if (!startTime) return 0;
|
||||
return Math.floor((now.value - new Date(startTime).getTime()) / (24 * 60 * 60 * 1000));
|
||||
const createTime = tenantStore.company?.createTime;
|
||||
if (!createTime) return 0;
|
||||
return Math.floor((now.value - new Date(createTime).getTime()) / (24 * 60 * 60 * 1000));
|
||||
});
|
||||
const userCount = computed(() => statisticsStore.userCount);
|
||||
const orderCount = computed(() => statisticsStore.orderCount);
|
||||
|
||||
+84
-27
@@ -20,13 +20,31 @@
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="配送地区" name="regions">
|
||||
<RegionsSelect
|
||||
v-model:value="regions"
|
||||
type="provinceCity"
|
||||
:show-search="true"
|
||||
placeholder="请选择配送省份/城市(不选则默认全国)"
|
||||
/>
|
||||
<a-form-item label="配送模式" name="regionMode">
|
||||
<a-radio-group v-model:value="form.regionMode" @change="onModeChange">
|
||||
<a-radio :value="0">全国</a-radio>
|
||||
<a-radio :value="1">指定配送</a-radio>
|
||||
<a-radio :value="2">指定不发货</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="配送地区">
|
||||
<template v-if="form.regionMode === 0">
|
||||
<span class="rms-tip">全国发货,无需选择地区</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<RegionMultiSelect
|
||||
:value="cityIds"
|
||||
:placeholder="form.regionMode === 2 ? '选择不发货地区' : '选择配送地区'"
|
||||
@update:value="onCityIdsChange"
|
||||
/>
|
||||
<div class="rms-tip">
|
||||
{{
|
||||
form.regionMode === 2
|
||||
? '选中地区将不配送,用户下单会被拦截'
|
||||
: '不选则默认全国(建议切换到"全国"模式)'
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item :label="templateType ? '首件数量' : '首重重量'" name="firstNum">
|
||||
<a-input-number
|
||||
@@ -94,7 +112,7 @@
|
||||
import {
|
||||
ShopExpressTemplateDetail
|
||||
} from '@/api/shop/shopExpressTemplateDetail/model';
|
||||
import RegionsSelect from '@/components/RegionsSelect/index.vue';
|
||||
import RegionMultiSelect from '@/components/RegionMultiSelect/index.vue';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
@@ -128,8 +146,8 @@
|
||||
const maxable = ref(true);
|
||||
// 表单实例
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
// 级联选择器选中值(字符串ID数组)
|
||||
const regions = ref<string[]>([]);
|
||||
// 已选城市编码数组(对应后端的 regionIds)
|
||||
const cityIds = ref<string[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive<ShopExpressTemplateDetail>({
|
||||
@@ -138,6 +156,8 @@
|
||||
type: true,
|
||||
provinceId: undefined,
|
||||
cityId: undefined,
|
||||
regionMode: undefined,
|
||||
regionIds: undefined,
|
||||
firstNum: undefined,
|
||||
firstAmount: undefined,
|
||||
extraAmount: undefined,
|
||||
@@ -186,16 +206,20 @@
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 级联选择器值变化时,同步到 form.provinceId / form.cityId */
|
||||
watch(regions, (val) => {
|
||||
if (val && val.length > 0) {
|
||||
form.provinceId = Number(val[0]);
|
||||
form.cityId = val.length > 1 ? Number(val[1]) : undefined;
|
||||
} else {
|
||||
form.provinceId = undefined;
|
||||
form.cityId = undefined;
|
||||
/* 城市选中变化,同步到 form.regionIds(模式由 radio 决定:1=指定配送,2=指定不发货) */
|
||||
const onCityIdsChange = (val: string[]) => {
|
||||
cityIds.value = val;
|
||||
form.regionIds = val.length ? val.join(',') : undefined;
|
||||
};
|
||||
|
||||
/* 切换配送模式 */
|
||||
const onModeChange = () => {
|
||||
// 切到「全国」时清空已选地区
|
||||
if (form.regionMode === 0) {
|
||||
cityIds.value = [];
|
||||
form.regionIds = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
@@ -205,11 +229,28 @@
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
// 指定配送/不发货模式下必须选择地区
|
||||
if (form.regionMode === 1 || form.regionMode === 2) {
|
||||
if (!cityIds.value.length) {
|
||||
message.error(
|
||||
form.regionMode === 2 ? '请选择不发货地区' : '请选择配送地区'
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
loading.value = true;
|
||||
const formData = { ...form };
|
||||
// 确保templateId和type从父级继承
|
||||
formData.templateId = props.templateId;
|
||||
formData.type = props.templateType ?? true;
|
||||
// 配送模式:0=全国(无地区);1=指定配送;2=指定不发货;后两者 regionIds 存城市编码逗号串
|
||||
if (form.regionMode === 0) {
|
||||
formData.regionIds = undefined;
|
||||
formData.regionMode = 0;
|
||||
} else {
|
||||
formData.regionIds = cityIds.value.join(',');
|
||||
formData.regionMode = form.regionMode;
|
||||
}
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateShopExpressTemplateDetail
|
||||
: addShopExpressTemplateDetail;
|
||||
@@ -234,27 +275,43 @@
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
if (form.regionMode == null) {
|
||||
form.regionMode = 0;
|
||||
}
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
form.templateId = props.templateId;
|
||||
form.type = props.templateType ?? true;
|
||||
form.regionMode = 0;
|
||||
}
|
||||
// 回显级联选择器
|
||||
if (form.provinceId) {
|
||||
const arr = [String(form.provinceId)];
|
||||
if (form.cityId) {
|
||||
arr.push(String(form.cityId));
|
||||
// 回显多选配送区域(仅 1=指定配送 / 2=指定不发货 带地区,regionIds 存城市编码)
|
||||
if (form.regionMode === 1 || form.regionMode === 2) {
|
||||
if (form.regionIds) {
|
||||
cityIds.value = form.regionIds.split(',').filter(Boolean);
|
||||
} else if (form.cityId) {
|
||||
// 兼容旧单值数据
|
||||
cityIds.value = [String(form.cityId)];
|
||||
} else {
|
||||
cityIds.value = [];
|
||||
}
|
||||
regions.value = arr;
|
||||
} else {
|
||||
regions.value = [];
|
||||
cityIds.value = [];
|
||||
form.regionIds = undefined;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
regions.value = [];
|
||||
cityIds.value = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rms-tip {
|
||||
margin-top: 4px;
|
||||
color: var(--ant-color-text-secondary, #999);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref, computed, onMounted } from 'vue';
|
||||
import { createVNode, ref, computed, onMounted, h } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
@@ -92,6 +92,8 @@
|
||||
|
||||
// 地区ID→名称映射
|
||||
const regionMap = ref<Record<string, string>>({});
|
||||
// 城市ID→省份ID映射(用于把城市码聚合显示为省名)
|
||||
const cityToProvince = ref<Record<string, string>>({});
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ShopExpressTemplateDetail[]>([]);
|
||||
@@ -128,6 +130,25 @@
|
||||
align: 'center',
|
||||
width: 150,
|
||||
customRender: ({ record }) => {
|
||||
const mode = record.regionMode;
|
||||
const ids = (record.regionIds || '')
|
||||
.split(',')
|
||||
.filter(Boolean);
|
||||
if (mode === 2) {
|
||||
// 指定不发货(黑名单)
|
||||
if (!ids.length) {
|
||||
return h('span', { style: { color: '#bbb' } }, '不发货(未配置地区)');
|
||||
}
|
||||
const text = '不发货:' + aggregateProvinces(ids).join('、');
|
||||
return renderRegionText(text, 26);
|
||||
}
|
||||
if (mode === 1) {
|
||||
// 指定配送(白名单)
|
||||
if (!ids.length) return '默认全国';
|
||||
const text = aggregateProvinces(ids).join('、');
|
||||
return renderRegionText(text, 22);
|
||||
}
|
||||
// 兼容旧数据 / 全国
|
||||
const province = record.provinceId
|
||||
? regionMap.value[String(record.provinceId)]
|
||||
: '';
|
||||
@@ -266,19 +287,44 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* 把城市编码数组聚合为去重的省份名称数组(兼容直接存省份码的场景) */
|
||||
const aggregateProvinces = (ids: string[]) => {
|
||||
const set = new Set<string>();
|
||||
ids.forEach((id) => {
|
||||
const prov = cityToProvince.value[id];
|
||||
if (prov) {
|
||||
set.add(regionMap.value[prov] || prov);
|
||||
} else {
|
||||
set.add(regionMap.value[id] || id);
|
||||
}
|
||||
});
|
||||
return [...set];
|
||||
};
|
||||
|
||||
/* 过长截断并支持 tooltip 悬浮查看全部 */
|
||||
const renderRegionText = (text: string, max = 22) => {
|
||||
if (text.length > max) {
|
||||
return h('span', { title: text }, text.slice(0, max) + '…');
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
/* 加载地区数据,建立ID→名称映射 */
|
||||
onMounted(() => {
|
||||
getRegionsData().then((data) => {
|
||||
const map: Record<string, string> = {};
|
||||
const c2p: Record<string, string> = {};
|
||||
data.forEach((province) => {
|
||||
map[province.value] = province.label;
|
||||
if (province.children) {
|
||||
province.children.forEach((city) => {
|
||||
map[city.value] = city.label;
|
||||
c2p[city.value] = province.value;
|
||||
});
|
||||
}
|
||||
});
|
||||
regionMap.value = map;
|
||||
cityToProvince.value = c2p;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,89 +1,114 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
danger
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
:disabled="selection?.length === 0"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="openImport">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="openImageImport">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入图片</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="openDetailImport">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入详情</span>
|
||||
</a-button>
|
||||
<a-button
|
||||
type="dashed"
|
||||
class="ele-btn-icon"
|
||||
:disabled="selection?.length === 0"
|
||||
@click="convertContent"
|
||||
>
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
<span>转换详情</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
|
||||
导出xls
|
||||
</a-button>
|
||||
<a-radio-group v-model:value="type" @change="handleSearch">
|
||||
<a-radio-button value="出售中"
|
||||
>出售中({{ goodsCount?.totalNum }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="待上架"
|
||||
>待上架({{ goodsCount?.totalNum2 }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="已售罄"
|
||||
>已售罄({{ goodsCount?.totalNum3 }})
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 180px"
|
||||
:listHeight="700"
|
||||
placeholder="请选择分类"
|
||||
:value="where.categoryId || undefined"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (where.categoryId = value)"
|
||||
@change="onCategoryId"
|
||||
/>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
placeholder="请输入关键词"
|
||||
style="width: 220px"
|
||||
v-model:value="where.keywords"
|
||||
@pressEnter="reload"
|
||||
@search="reload"
|
||||
/>
|
||||
<a-button type="text" @click="reset">重置</a-button>
|
||||
</a-space>
|
||||
<div class="goods-toolbar">
|
||||
<!-- 常驻工具栏 -->
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
|
||||
<!-- 导入(合并为下拉菜单) -->
|
||||
<a-dropdown>
|
||||
<a-button type="dashed" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
<DownOutlined class="ele-btn-icon-arrow" />
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="goods" @click="openImport">
|
||||
导入商品
|
||||
</a-menu-item>
|
||||
<a-menu-item key="image" @click="openImageImport">
|
||||
导入图片
|
||||
</a-menu-item>
|
||||
<a-menu-item key="detail" @click="openDetailImport">
|
||||
导入详情
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
|
||||
导出xls
|
||||
</a-button>
|
||||
|
||||
<a-radio-group v-model:value="type" @change="handleSearch">
|
||||
<a-radio-button value="出售中"
|
||||
>出售中({{ goodsCount?.totalNum }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="待上架"
|
||||
>待上架({{ goodsCount?.totalNum2 }})
|
||||
</a-radio-button>
|
||||
<a-radio-button value="已售罄"
|
||||
>已售罄({{ goodsCount?.totalNum3 }})
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
multiple
|
||||
:max-tag-count="2"
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 200px"
|
||||
:listHeight="700"
|
||||
placeholder="请选择分类(可多选)"
|
||||
:value="where.categoryIds && where.categoryIds.length ? where.categoryIds : undefined"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number[]) => (where.categoryIds = value || [])"
|
||||
@change="onCategoryIds"
|
||||
/>
|
||||
<a-input-search
|
||||
allow-clear
|
||||
placeholder="请输入关键词"
|
||||
style="width: 220px"
|
||||
v-model:value="where.keywords"
|
||||
@pressEnter="reload"
|
||||
@search="reload"
|
||||
/>
|
||||
<a-button type="text" @click="reset">重置</a-button>
|
||||
</a-space>
|
||||
|
||||
<!-- 选中操作栏:勾选商品后才显示 -->
|
||||
<div v-if="selection && selection.length > 0" class="selection-bar">
|
||||
<span class="selection-bar-count">已选 {{ selection.length }} 项</span>
|
||||
<a-button
|
||||
danger
|
||||
type="primary"
|
||||
class="ele-btn-icon"
|
||||
@click="removeBatch"
|
||||
>
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
<span>批量删除</span>
|
||||
</a-button>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="batchUp">
|
||||
<template #icon>
|
||||
<ArrowUpOutlined />
|
||||
</template>
|
||||
<span>批量上架</span>
|
||||
</a-button>
|
||||
<a-button danger class="ele-btn-icon" @click="batchDown">
|
||||
<template #icon>
|
||||
<ArrowDownOutlined />
|
||||
</template>
|
||||
<span>批量下架</span>
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="convertContent">
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
<span>转换详情</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@@ -91,9 +116,12 @@
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
UploadOutlined,
|
||||
SyncOutlined
|
||||
SyncOutlined,
|
||||
ArrowUpOutlined,
|
||||
ArrowDownOutlined,
|
||||
DownOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { ref, watch } from 'vue';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { getCount } from '@/api/shop/shopGoods';
|
||||
import type { GoodsCount, ShopGoodsParam } from '@/api/shop/shopGoods/model';
|
||||
import useSearch from '@/utils/use-search';
|
||||
@@ -102,8 +130,8 @@
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
// 选中的商品
|
||||
selection?: any[];
|
||||
merchantId?: number;
|
||||
navigationList?: ShopGoodsCategory[];
|
||||
}>(),
|
||||
@@ -123,6 +151,7 @@
|
||||
status: undefined,
|
||||
stock: undefined,
|
||||
categoryId: undefined,
|
||||
categoryIds: [],
|
||||
merchantId: undefined,
|
||||
keywords: ''
|
||||
});
|
||||
@@ -131,12 +160,14 @@
|
||||
(e: 'search', where?: ShopGoodsParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchUp'): void;
|
||||
(e: 'batchDown'): void;
|
||||
(e: 'batchMove'): void;
|
||||
(e: 'import'): void;
|
||||
(e: 'imageImport'): void;
|
||||
(e: 'detailImport'): void;
|
||||
(e: 'convertContent'): void;
|
||||
(e: 'export'): void;
|
||||
(e: 'export', where?: ShopGoodsParam): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
@@ -149,6 +180,16 @@
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
// 批量上架
|
||||
const batchUp = () => {
|
||||
emit('batchUp');
|
||||
};
|
||||
|
||||
// 批量下架
|
||||
const batchDown = () => {
|
||||
emit('batchDown');
|
||||
};
|
||||
|
||||
// 打开导入
|
||||
const openImport = () => {
|
||||
emit('import');
|
||||
@@ -166,9 +207,9 @@
|
||||
emit('convertContent');
|
||||
};
|
||||
|
||||
// 导出
|
||||
// 导出(携带当前筛选条件 where)
|
||||
const handleExport = () => {
|
||||
emit('export');
|
||||
emit('export', where);
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
@@ -200,23 +241,58 @@
|
||||
reload();
|
||||
};
|
||||
|
||||
// 按分类查询
|
||||
const onCategoryId = (id: number) => {
|
||||
where.categoryId = id;
|
||||
// 按分类查询(方案B:支持多选,主分类取首个)
|
||||
const onCategoryIds = (ids: number[]) => {
|
||||
where.categoryIds = ids || [];
|
||||
where.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||
emit('search', where);
|
||||
};
|
||||
|
||||
// 首次进入:设置商户条件并触发统计 + 列表加载。
|
||||
// 用 onMounted 替代 watch 的 immediate,避免与 ele-pro-table 的自动加载(initLoad)重复发请求。
|
||||
onMounted(() => {
|
||||
const id = props.merchantId;
|
||||
where.merchantId = Number(id) > 0 ? id : undefined;
|
||||
reload();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.merchantId,
|
||||
(id) => {
|
||||
if (Number(id) > 0) {
|
||||
where.merchantId = id;
|
||||
reload();
|
||||
} else {
|
||||
where.merchantId = undefined;
|
||||
reload();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
where.merchantId = Number(id) > 0 ? id : undefined;
|
||||
reload();
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.goods-toolbar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* 选中后浮现的操作栏 */
|
||||
.selection-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #e6f4ff;
|
||||
border: 1px solid #91caff;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.selection-bar-count {
|
||||
font-weight: 600;
|
||||
color: #1677ff;
|
||||
margin-right: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ele-btn-icon-arrow {
|
||||
font-size: 12px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,16 +20,53 @@
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
<div class="ele-text-center">
|
||||
<span>只能上传 xls、xlsx 文件</span>
|
||||
<span>只能上传 xls、xlsx 文件,</span>
|
||||
<a :href="templateUrl" download="商品详情导入模板.xlsx">下载导入模板</a>
|
||||
</div>
|
||||
|
||||
<!-- 使用说明 -->
|
||||
<a-alert type="info" show-icon :style="{ marginTop: '16px' }">
|
||||
<template #message>
|
||||
<div class="import-tips">
|
||||
<div class="import-tips-title">使用说明:</div>
|
||||
<ul class="import-tips-list">
|
||||
<li>Excel 中每行对应一个商品的详情内容</li>
|
||||
<li>图片按<strong>文件夹</strong>组织,<strong>文件夹名称为商品编号</strong></li>
|
||||
<li>文件夹内的图片按<strong>数字序号</strong>命名排序(如 1.jpg、2.png、3.jpg …)</li>
|
||||
<li>图片序号对应 Excel 中的图片展示顺序</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</a-alert>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.import-tips {
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
color: #555;
|
||||
}
|
||||
.import-tips-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
}
|
||||
.import-tips-list {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
li {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { importShopGoodsDetails } from '@/api/shop/shopGoods';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
@@ -42,6 +79,10 @@
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const templateUrl = computed(() => {
|
||||
return MODULES_API_URL + '/shop/shop-goods/import/details/template';
|
||||
});
|
||||
|
||||
const doUpload = ({ file }) => {
|
||||
if (
|
||||
![
|
||||
|
||||
@@ -20,16 +20,18 @@
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
<div class="ele-text-center">
|
||||
<span>只能上传 zip 文件,图片文件名需与商品编号对应</span>
|
||||
<span>只能上传 zip 文件,图片文件名需与商品编号对应,</span>
|
||||
<a :href="templateUrl" download="商品图片导入示例.zip">下载导入模板</a>
|
||||
</div>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||
import { importShopGoodsImages } from '@/api/shop/shopGoods';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
@@ -42,6 +44,10 @@
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const templateUrl = computed(() => {
|
||||
return MODULES_API_URL + '/shop/shop-goods/import/images/template';
|
||||
});
|
||||
|
||||
const doUpload = ({ file }) => {
|
||||
const isZip =
|
||||
file.type === 'application/zip' ||
|
||||
|
||||
@@ -32,18 +32,31 @@
|
||||
v-model:value="form.name"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品分类" name="categoryId">
|
||||
<a-form-item label="商品分类" name="categoryIds">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
multiple
|
||||
:max-tag-count="3"
|
||||
:tree-data="navigationList"
|
||||
tree-default-expand-all
|
||||
style="width: 320px"
|
||||
placeholder="请选择分类"
|
||||
:value="form.categoryId || undefined"
|
||||
placeholder="请选择分类(可多选)"
|
||||
:value="form.categoryIds && form.categoryIds.length ? form.categoryIds : undefined"
|
||||
:listHeight="700"
|
||||
:dropdown-style="{ overflow: 'auto' }"
|
||||
@update:value="(value?: number) => (form.categoryId = value)"
|
||||
@change="onCategoryId"
|
||||
@update:value="(value?: number[]) => (form.categoryIds = value || [])"
|
||||
@change="onCategoryIds"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属专区" name="sectionIds">
|
||||
<a-select
|
||||
v-model:value="sectionIdList"
|
||||
mode="multiple"
|
||||
:max-tag-count="3"
|
||||
:options="zoneOptions"
|
||||
style="width: 320px"
|
||||
placeholder="请选择所属专区(可多选)"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品编码" name="code">
|
||||
@@ -72,10 +85,10 @@
|
||||
v-model:value="form.unitName"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品价格" name="price">
|
||||
<a-form-item label="商城价" name="price">
|
||||
<a-space>
|
||||
<a-input-number
|
||||
:placeholder="`商品价格`"
|
||||
:placeholder="`商城价`"
|
||||
style="width: 240px"
|
||||
:min="0.01"
|
||||
v-model:value="form.price"
|
||||
@@ -98,18 +111,19 @@
|
||||
v-model:value="form.dealerPrice"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="套餐价格" name="originPrice">
|
||||
<a-form-item label="进货价格" name="originPrice">
|
||||
<a-input-number
|
||||
:placeholder="`套餐价格`"
|
||||
:placeholder="`进货价格`"
|
||||
style="width: 240px"
|
||||
:min="0.01"
|
||||
v-model:value="form.buyingPrice"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="运费模板" v-if="!merchantId">
|
||||
<a-form-item :label="form.canExpress === 1 ? '运费模板' : '运费模板'" v-if="!merchantId" :required="form.canExpress === 1">
|
||||
<a-select
|
||||
v-model:value="form.expressTemplateId"
|
||||
style="width: 240px"
|
||||
:placeholder="form.canExpress === 1 ? '请选择运费模板' : '请选择运费模板'"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(item, index) in expressTemplateList"
|
||||
@@ -211,18 +225,12 @@
|
||||
</a-form-item>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="商品详情" key="content">
|
||||
<!-- Markdown 编辑器 -->
|
||||
<MdEditor
|
||||
<RichTextEditor
|
||||
v-if="active === 'content'"
|
||||
v-model="content"
|
||||
:disabled="disabled"
|
||||
height="650px"
|
||||
placeholder="支持 Markdown 语法,可直接粘贴或拖拽图片..."
|
||||
:toolbars="markdownToolbars"
|
||||
:onUploadImg="onMarkdownUploadImg"
|
||||
:height="520"
|
||||
placeholder="请输入商品详情,支持图文混排..."
|
||||
/>
|
||||
<div class="file-selector-tip">
|
||||
💡 提示:支持 Markdown 语法,可直接拖拽上传图片,也可以使用工具栏按钮从文件库选择图片
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane tab="商品规格" key="spec">
|
||||
<a-form-item label="规格类型" name="specs">
|
||||
@@ -308,7 +316,7 @@
|
||||
:precision="2"
|
||||
:min="0"
|
||||
style="width: 110px"
|
||||
placeholder="商品价格"
|
||||
placeholder="商城价"
|
||||
/>
|
||||
<a-input-number
|
||||
v-model:value="batchSalePrice"
|
||||
@@ -355,7 +363,7 @@
|
||||
:data="record.images || []"
|
||||
:index="index"
|
||||
@done="chooseSkuImage"
|
||||
@del="onDeleteSkuItem"
|
||||
@del="() => onDeleteSkuItem(index)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'price'">
|
||||
@@ -473,7 +481,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="配送方式" name="deliveryMode">
|
||||
<a-radio-group v-model:value="form.deliveryMode">
|
||||
<a-radio :value="0">送上门</a-radio>
|
||||
<a-radio :value="0">快递配送</a-radio>
|
||||
<a-radio :value="1">限自提</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
@@ -673,23 +681,6 @@
|
||||
</a-tabs>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
<!-- 文件库选择弹窗 -->
|
||||
<SelectData
|
||||
v-model:visible="showFileSelector"
|
||||
title="选择图片"
|
||||
type="image"
|
||||
class="file-selector-modal"
|
||||
@done="onFileSelected"
|
||||
/>
|
||||
|
||||
<!-- 视频库选择弹窗 -->
|
||||
<SelectData
|
||||
v-model:visible="showVideoSelector"
|
||||
title="选择视频"
|
||||
type="video"
|
||||
class="file-selector-modal"
|
||||
@done="onVideoSelected"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
@@ -702,7 +693,7 @@ import {
|
||||
SettingOutlined,
|
||||
UploadOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import {ref, reactive, watch, nextTick} from 'vue';
|
||||
import {ref, reactive, watch} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
import {Form, message} from 'ant-design-vue';
|
||||
import {assignObject, messageLoading, uuid} from 'ele-admin-pro';
|
||||
@@ -712,12 +703,11 @@ import {useThemeStore} from '@/store/modules/theme';
|
||||
import {storeToRefs} from 'pinia';
|
||||
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 {ShopMerchant} from '@/api/shop/shopMerchant/model';
|
||||
import {uploadFile, uploadOss} from '@/api/system/file';
|
||||
import { MdEditor } from 'md-editor-v3';
|
||||
import 'md-editor-v3/lib/style.css';
|
||||
import 'md-editor-v3/lib/preview.css';
|
||||
import {FileRecord} from '@/api/system/file/model';
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue';
|
||||
import markdownit from 'markdown-it';
|
||||
import {ShopSpec} from '@/api/shop/shopSpec/model';
|
||||
import {ShopGoodsSku} from '@/api/shop/shopGoodsSku/model';
|
||||
import {ShopGoodsSpec} from '@/api/shop/shopGoodsSpec/model';
|
||||
@@ -730,7 +720,7 @@ import {ShopExpressTemplate} from '@/api/shop/shopExpressTemplate/model';
|
||||
import {listShopExpressTemplate} from '@/api/shop/shopExpressTemplate';
|
||||
import {listShopCommissionRole} from '@/api/shop/shopCommissionRole';
|
||||
import {listShopGoodsRoleCommission} from '@/api/shop/shopGoodsRoleCommission';
|
||||
import SelectData from "@/components/SelectFile/components/select-data.vue";
|
||||
import {listHomeSections} from '@/api/shop/shopZone';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
@@ -761,7 +751,21 @@ const maxable = ref(true);
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const images = ref<ItemType[]>([]);
|
||||
const content = ref('');
|
||||
const disabled = ref(false);
|
||||
|
||||
// Markdown -> HTML 转换(编辑回显旧数据时,把 Markdown 内容转成富文本可识别的 HTML)
|
||||
const md = markdownit();
|
||||
/**
|
||||
* 把后端 content 转为富文本编辑器可用的 HTML:
|
||||
* - 已是 HTML(含标签)则原样返回,避免二次转义;
|
||||
* - 否则按 Markdown 渲染为 HTML。
|
||||
*/
|
||||
const toEditorContent = (raw?: string): string => {
|
||||
if (!raw) return '';
|
||||
const s = raw.trim();
|
||||
if (/<[a-z][\s\S]*>/i.test(s)) return raw;
|
||||
return md.render(s);
|
||||
};
|
||||
|
||||
// 当前选项卡
|
||||
const active = ref('base');
|
||||
|
||||
@@ -867,6 +871,9 @@ const skuList = ref<ShopGoodsSku[]>([]);
|
||||
const files = ref<ItemType[]>([]);
|
||||
const goodsSpec = ref<ShopGoodsSpec>();
|
||||
const category = ref<string[]>([]);
|
||||
// 所属专区(多选)
|
||||
const sectionIdList = ref<number[]>([]);
|
||||
const zoneOptions = ref<{ label: string; value: number }[]>([]);
|
||||
|
||||
// 批量设置
|
||||
const batchPrice = ref<number | undefined>(undefined);
|
||||
@@ -895,17 +902,29 @@ const columns = [
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品价格',
|
||||
title: '商城价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '划线价',
|
||||
dataIndex: 'salePrice',
|
||||
key: 'salePrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '会员价',
|
||||
dataIndex: 'buyingPrice',
|
||||
key: 'buyingPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '库存',
|
||||
dataIndex: 'stock',
|
||||
key: 'stock',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品编号',
|
||||
dataIndex: 'skuNo',
|
||||
@@ -926,12 +945,13 @@ const addEnsureTag = (item = '') => {
|
||||
ensureTagItem.value = '';
|
||||
};
|
||||
|
||||
// 选择栏目
|
||||
const onCategoryId = (id: number) => {
|
||||
form.categoryId = id;
|
||||
// 选择栏目(方案B:支持多选,主分类取列表首个,向后兼容旧字段)
|
||||
const onCategoryIds = (ids: number[]) => {
|
||||
form.categoryIds = ids || [];
|
||||
form.categoryId = ids && ids.length ? ids[0] : undefined;
|
||||
// 💾 在新增模式下,用户手动选择栏目时也保存到本地存储
|
||||
if (!isUpdate.value && id) {
|
||||
saveLastCategory(id);
|
||||
if (!isUpdate.value && ids && ids.length) {
|
||||
saveLastCategory(ids[0]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -956,34 +976,6 @@ const onDeleteEnsureTag = (tag: string) => {
|
||||
ensureTag.value.splice(index, 1);
|
||||
};
|
||||
|
||||
// 从文件库选择图片的回调
|
||||
const onFileSelected = (data: FileRecord) => {
|
||||
if (fileSelectCallback.value) {
|
||||
// 使用文件的完整URL,确保有值
|
||||
const imageUrl = data.url || data.path || '';
|
||||
if (imageUrl) {
|
||||
fileSelectCallback.value(imageUrl);
|
||||
message.success('图片插入成功');
|
||||
}
|
||||
fileSelectCallback.value = null;
|
||||
}
|
||||
showFileSelector.value = false;
|
||||
};
|
||||
|
||||
// 从视频库选择视频的回调
|
||||
const onVideoSelected = (data: FileRecord) => {
|
||||
if (videoSelectCallback.value) {
|
||||
// 使用文件的完整URL,确保有值
|
||||
const videoUrl = data.path || data.downloadUrl || '';
|
||||
if (videoUrl) {
|
||||
videoSelectCallback.value(videoUrl);
|
||||
message.success('视频插入成功');
|
||||
}
|
||||
videoSelectCallback.value = null;
|
||||
}
|
||||
showVideoSelector.value = false;
|
||||
};
|
||||
|
||||
// 🎨 智能一键排版 - 人性化设计
|
||||
const handleAutoFormat = (editor: any) => {
|
||||
try {
|
||||
@@ -1230,6 +1222,7 @@ const form = reactive<ShopGoods>({
|
||||
canExpress: 1,
|
||||
unitName: '',
|
||||
categoryId: undefined,
|
||||
categoryIds: [],
|
||||
parentName: undefined,
|
||||
categoryName: undefined,
|
||||
specs: 0,
|
||||
@@ -1337,7 +1330,7 @@ const rules = reactive({
|
||||
price: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写会员价',
|
||||
message: '请填写商城价',
|
||||
type: 'number',
|
||||
trigger: 'blur'
|
||||
}
|
||||
@@ -1353,7 +1346,7 @@ const rules = reactive({
|
||||
buyingPrice: [
|
||||
{
|
||||
required: true,
|
||||
message: '请填写会员店/总仓批发价',
|
||||
message: '请填写进货价',
|
||||
type: 'number',
|
||||
trigger: 'blur'
|
||||
}
|
||||
@@ -1475,14 +1468,18 @@ const chooseSupplier = (item: ShopMerchant) => {
|
||||
};
|
||||
|
||||
const chooseGoodsCategory = (item: ShopGoodsCategory, value: any) => {
|
||||
form.categoryId = value[1].value;
|
||||
const cid = value[1].value as number;
|
||||
form.categoryId = cid;
|
||||
form.categoryIds = [cid];
|
||||
form.parentName = value[0].label;
|
||||
form.categoryName = value[1].label;
|
||||
};
|
||||
const chooseTakeawayCategory = (item: ShopGoodsCategory, value: any) => {
|
||||
const cid = item[0] as number;
|
||||
form.categoryParent = '店铺分类';
|
||||
form.categoryChildren = value[0].label;
|
||||
form.categoryId = item[0];
|
||||
form.categoryId = cid;
|
||||
form.categoryIds = [cid];
|
||||
};
|
||||
|
||||
const chooseImage = (data: FileRecord) => {
|
||||
@@ -1505,7 +1502,8 @@ const chooseSkuImage = (data: FileRecord) => {
|
||||
};
|
||||
|
||||
const onDeleteSkuItem = (index: number) => {
|
||||
images.value.splice(index, 1);
|
||||
skuList.value[index].images = [];
|
||||
skuList.value[index].image = '';
|
||||
};
|
||||
|
||||
const videos = ref<ItemType[]>([]);
|
||||
@@ -1726,88 +1724,6 @@ const batchApply = () => {
|
||||
};
|
||||
|
||||
|
||||
const editorRef = ref(null);
|
||||
|
||||
// 文件库选择弹窗状态
|
||||
const showFileSelector = ref(false);
|
||||
const fileSelectCallback = ref<((url: string) => void) | null>(null);
|
||||
|
||||
// 视频库选择弹窗状态
|
||||
const showVideoSelector = ref(false);
|
||||
const videoSelectCallback = ref<((url: string) => void) | null>(null);
|
||||
|
||||
// 编辑器可见性控制
|
||||
// 删除了 TinyMCE 相关代码,改用 MdEditor
|
||||
const editorVisible = ref(false);
|
||||
|
||||
// 当 Tab 切换到商品详情时,启用编辑器
|
||||
watch(
|
||||
() => active.value,
|
||||
(key) => {
|
||||
if (key === 'content') {
|
||||
// 切换到商品详情 Tab 时,等待 DOM 渲染完成后启用编辑器
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
editorVisible.value = true;
|
||||
console.log('[shopGoodsEdit] Switched to content tab, editorVisible = true');
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const markdownToolbars = [
|
||||
'bold',
|
||||
'underline',
|
||||
'italic',
|
||||
'-',
|
||||
'title',
|
||||
'strikeThrough',
|
||||
'quote',
|
||||
'unorderedList',
|
||||
'orderedList',
|
||||
'task',
|
||||
'-',
|
||||
'codeRow',
|
||||
'code',
|
||||
'link',
|
||||
'image',
|
||||
'table',
|
||||
'-',
|
||||
'revoke',
|
||||
'next',
|
||||
'=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
'preview',
|
||||
'previewOnly'
|
||||
];
|
||||
|
||||
// Markdown 图片上传
|
||||
const onMarkdownUploadImg = async (
|
||||
files: File[],
|
||||
callback: (urls: string[]) => void
|
||||
) => {
|
||||
try {
|
||||
const uploadPromises = files.map(async (file) => {
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
message.error(`图片 ${file.name} 大小超过10MB`);
|
||||
return null;
|
||||
}
|
||||
const res = await uploadOss(file);
|
||||
return res.url || res.path;
|
||||
});
|
||||
const results = await Promise.all(uploadPromises);
|
||||
callback(results.filter(Boolean) as string[]);
|
||||
} catch (err: any) {
|
||||
message.error(err.message || '图片上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
// TinyMCE config 已移除,改用 MdEditor
|
||||
const config = ref({});
|
||||
|
||||
const {resetFields} = useForm(form, rules);
|
||||
|
||||
const COMMISSION_PERCENT_FIELDS = [
|
||||
@@ -1846,6 +1762,11 @@ const save = () => {
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
// 快递配送时运费模板必选(后端也有此校验,前端提前拦截给出友好提示)
|
||||
if (form.canExpress === 1 && !form.expressTemplateId && !merchantId) {
|
||||
loading.value = false;
|
||||
return message.error('请选择运费模板');
|
||||
}
|
||||
if (form.priceGift && !form.priceGiftNum)
|
||||
return message.error('请输入会员赠品数量');
|
||||
if (ensureTag.value.length) form.ensureTag = ensureTag.value.join();
|
||||
@@ -1890,8 +1811,17 @@ const save = () => {
|
||||
if (form.specs === 1 && goodsSpec.value && spec.value.length) {
|
||||
goodsSpec.value.specValue = JSON.stringify(spec.value);
|
||||
}
|
||||
// 方案B:主分类兜底取多分类第一个,保证旧字段/旧逻辑兼容
|
||||
if (form.categoryIds && form.categoryIds.length && !form.categoryId) {
|
||||
form.categoryId = form.categoryIds[0];
|
||||
}
|
||||
|
||||
const formData: any = {
|
||||
...form,
|
||||
// 所属专区:数字数组 -> 逗号分隔字符串
|
||||
sectionIds: sectionIdList.value.length
|
||||
? sectionIdList.value.join(',')
|
||||
: '',
|
||||
content: content.value,
|
||||
category: JSON.stringify(category.value),
|
||||
files: JSON.stringify(files.value),
|
||||
@@ -2055,8 +1985,20 @@ watch(
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
await getExpressTemplateList();
|
||||
// 加载所属专区选项
|
||||
listHomeSections({})
|
||||
.then((list) => {
|
||||
zoneOptions.value = (list || []).map((z) => ({
|
||||
label: z.title || '',
|
||||
value: z.sectionId as number
|
||||
}));
|
||||
})
|
||||
.catch(() => {
|
||||
zoneOptions.value = [];
|
||||
});
|
||||
|
||||
images.value = [];
|
||||
sectionIdList.value = [];
|
||||
category.value = [];
|
||||
files.value = [];
|
||||
videos.value = [];
|
||||
@@ -2065,6 +2007,13 @@ watch(
|
||||
ensureTagItem.value = '';
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
// 所属专区:逗号分隔字符串 -> 数字数组
|
||||
sectionIdList.value = form.sectionIds
|
||||
? form.sectionIds
|
||||
.split(',')
|
||||
.map((s) => Number(s.trim()))
|
||||
.filter((n) => !Number.isNaN(n))
|
||||
: [];
|
||||
if (form.commissionType === undefined || form.commissionType === null) {
|
||||
form.commissionType = 10;
|
||||
}
|
||||
@@ -2145,22 +2094,28 @@ watch(
|
||||
category.value = JSON.parse(props.data.category);
|
||||
}
|
||||
if (props.data.content) {
|
||||
content.value = props.data.content;
|
||||
content.value = toEditorContent(props.data.content);
|
||||
}
|
||||
|
||||
isUpdate.value = true;
|
||||
if (form.ensureTag) {
|
||||
ensureTag.value = form.ensureTag.split(',');
|
||||
}
|
||||
// 🎯 优先级设置栏目:
|
||||
// 1. 如果传入了 categoryId(从栏目页面点击添加),使用传入的
|
||||
// 2. 否则使用上次保存的栏目
|
||||
if (props.data.categoryId) {
|
||||
// 🎯 优先级设置栏目(方案B:支持多分类回填):
|
||||
// 1. 如果传入了 categoryIds,使用多分类
|
||||
// 2. 否则如果传入了 categoryId,兼容单分类
|
||||
// 3. 否则使用上次保存的栏目
|
||||
if (props.data.categoryIds && props.data.categoryIds.length) {
|
||||
form.categoryIds = props.data.categoryIds;
|
||||
form.categoryId = props.data.categoryId ?? props.data.categoryIds[0];
|
||||
} else if (props.data.categoryId) {
|
||||
form.categoryId = props.data?.categoryId;
|
||||
form.categoryIds = [props.data.categoryId];
|
||||
} else {
|
||||
const lastCategory = getLastCategory();
|
||||
if (lastCategory) {
|
||||
form.categoryId = lastCategory;
|
||||
form.categoryIds = [lastCategory];
|
||||
}
|
||||
}
|
||||
await getRoleList();
|
||||
@@ -2181,17 +2136,4 @@ watch(
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 修复 MdEditor 工具栏图标过大 */
|
||||
:deep(.md-editor-toolbar-wrapper .md-editor-icon) {
|
||||
font-size: 16px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
:deep(.md-editor-toolbar-wrapper svg) {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
:deep(.md-editor-toolbar-item) {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
:init-load="false"
|
||||
:scroll="{ x: 1800 }"
|
||||
size="small"
|
||||
v-model:selection="selection"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
@@ -21,6 +24,8 @@
|
||||
:navigationList="navigationList"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchUp="batchUpShelf"
|
||||
@batchDown="batchDownShelf"
|
||||
@batchMove="openMove"
|
||||
@import="openImport"
|
||||
@imageImport="openImageImport"
|
||||
@@ -32,15 +37,60 @@
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'name'">
|
||||
<a-space class="flex items-center cursor-pointer">
|
||||
<a-image
|
||||
:src="getCompressedImageUrl(record.image)"
|
||||
<img
|
||||
v-if="record.image"
|
||||
:preview="false"
|
||||
:width="50"
|
||||
:src="getCompressedImageUrl(record.image)"
|
||||
class="goods-thumb"
|
||||
loading="lazy"
|
||||
alt=""
|
||||
width="50"
|
||||
height="50"
|
||||
/>
|
||||
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
||||
<div class="flex flex-col justify-center leading-tight">
|
||||
<span class="text-gray-700 font-bold">{{ record.name }}</span>
|
||||
<span
|
||||
v-if="record.code"
|
||||
class="text-gray-400 text-xs mt-1"
|
||||
>编号:{{ record.code }}</span
|
||||
>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'priceGroup'">
|
||||
<div class="flex flex-col leading-tight">
|
||||
<span class="text-gray-800 font-bold"
|
||||
>¥{{ record.price
|
||||
}}<span
|
||||
v-if="record.priceMax != null && record.priceMax !== record.price"
|
||||
> ~ ¥{{ record.priceMax }}</span
|
||||
></span
|
||||
>
|
||||
<span class="text-gray-400 text-xs"
|
||||
>市场价:<template v-if="record.salePrice"
|
||||
>¥{{ record.salePrice
|
||||
}}<span
|
||||
v-if="
|
||||
record.salePriceMax != null &&
|
||||
record.salePriceMax !== record.salePrice
|
||||
"
|
||||
> ~ ¥{{ record.salePriceMax }}</span
|
||||
></template
|
||||
><template v-else>-</template></span
|
||||
>
|
||||
<span class="text-gray-400 text-xs"
|
||||
>会员价:<template v-if="record.dealerPrice"
|
||||
>¥{{ record.dealerPrice
|
||||
}}<span
|
||||
v-if="
|
||||
record.dealerPriceMax != null &&
|
||||
record.dealerPriceMax !== record.dealerPrice
|
||||
"
|
||||
> ~ ¥{{ record.dealerPriceMax }}</span
|
||||
></template
|
||||
><template v-else>-</template></span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'recommend'">
|
||||
<a-space @click.stop="onRecommend(record)">
|
||||
<span
|
||||
@@ -53,8 +103,20 @@
|
||||
/></span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'isTop'">
|
||||
<a-space @click.stop="onTop(record)">
|
||||
<span
|
||||
v-if="record.isTop === 1"
|
||||
class="ele-text-success cursor-pointer"
|
||||
><CheckOutlined
|
||||
/></span>
|
||||
<span v-else class="ele-text-placeholder cursor-pointer"
|
||||
><CloseOutlined
|
||||
/></span>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0" color="green">已上架</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="green">出售中</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
|
||||
<a-tag v-if="record.status === 2" color="purple">待审核</a-tag>
|
||||
<a-tag v-if="record.status === 3" color="red">审核不通过</a-tag>
|
||||
@@ -63,6 +125,19 @@
|
||||
<a-space>
|
||||
<a @click.stop="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a
|
||||
v-if="record.status === 0"
|
||||
class="ele-text-warning"
|
||||
@click.stop="toggleShelf(record, 1)"
|
||||
>下架</a
|
||||
>
|
||||
<a
|
||||
v-else
|
||||
class="ele-text-success"
|
||||
@click.stop="toggleShelf(record, 0)"
|
||||
>上架</a
|
||||
>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm.stop="remove(record)"
|
||||
@@ -102,7 +177,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { createVNode, ref, onMounted } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import {
|
||||
ExclamationCircleOutlined,
|
||||
@@ -136,6 +211,7 @@
|
||||
import { ShopGoodsCategory } from '@/api/shop/shopGoodsCategory/model';
|
||||
import { listShopGoodsCategory } from '@/api/shop/shopGoodsCategory';
|
||||
import { utils, writeFile } from 'xlsx';
|
||||
import { ensureFullUrl } from '@/utils/image';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 表格实例
|
||||
@@ -158,7 +234,7 @@
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
@@ -195,34 +271,18 @@
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
title: '编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
title: '分类',
|
||||
dataIndex: 'categoryName',
|
||||
key: 'categoryName',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '市场价',
|
||||
dataIndex: 'salePrice',
|
||||
key: 'salePrice',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }) => (text ? `¥${text}` : '-')
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => text || '--'
|
||||
},
|
||||
{
|
||||
title: '价格',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }) => `¥${text}`
|
||||
},
|
||||
{
|
||||
title: '会员价',
|
||||
dataIndex: 'dealerPrice',
|
||||
key: 'dealerPrice',
|
||||
align: 'center',
|
||||
width: 110,
|
||||
customRender: ({ text }) => (text ? `¥${text}` : '-')
|
||||
key: 'priceGroup',
|
||||
dataIndex: 'priceGroup'
|
||||
},
|
||||
{
|
||||
title: '销量',
|
||||
@@ -242,6 +302,12 @@
|
||||
key: 'recommend',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '置顶',
|
||||
dataIndex: 'isTop',
|
||||
key: 'isTop',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -254,13 +320,14 @@
|
||||
// key: 'comments',
|
||||
// align: 'center',
|
||||
// },
|
||||
{
|
||||
title: '排序号',
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 120
|
||||
},
|
||||
// {
|
||||
// title: '排序号',
|
||||
// dataIndex: 'sortNumber',
|
||||
// key: 'sortNumber',
|
||||
// align: 'center',
|
||||
// width: 120,
|
||||
// sorter: true
|
||||
// },
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
@@ -270,15 +337,15 @@
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
// {
|
||||
// title: '操作',
|
||||
// key: 'action',
|
||||
// width: 180,
|
||||
// fixed: 'right',
|
||||
// align: 'center',
|
||||
// hideInSetting: true
|
||||
// }
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
@@ -352,8 +419,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
/* 导出商品列表 */
|
||||
const handleExport = async () => {
|
||||
/* 价格区间文本:多规格且区间不相等时显示 ¥min ~ ¥max,否则显示单值 */
|
||||
const fmtPrice = (min?: string | number, max?: string | number) => {
|
||||
if (min == null || min === '') {
|
||||
return max != null && max !== '' ? `¥${max}` : '';
|
||||
}
|
||||
const base = `¥${min}`;
|
||||
return max != null && max !== '' && `${max}` !== `${min}`
|
||||
? `${base} ~ ¥${max}`
|
||||
: base;
|
||||
};
|
||||
|
||||
/* 导出商品列表(可携带当前筛选条件) */
|
||||
const handleExport = async (where?: ShopGoodsParam) => {
|
||||
if (loading.value) {
|
||||
return;
|
||||
}
|
||||
@@ -371,11 +449,12 @@
|
||||
'库存',
|
||||
'状态',
|
||||
'推荐',
|
||||
'创建时间'
|
||||
'创建时间',
|
||||
'封面图'
|
||||
]
|
||||
];
|
||||
|
||||
const list = await listShopGoods({});
|
||||
const list = await listShopGoods(where || {});
|
||||
if (!list || list.length === 0) {
|
||||
message.warning('没有数据可以导出');
|
||||
loading.value = false;
|
||||
@@ -387,14 +466,15 @@
|
||||
array.push([
|
||||
`${goods.goodsId || ''}`,
|
||||
`${goods.name || ''}`,
|
||||
`¥${goods.price || 0}`,
|
||||
`${goods.salePrice ? `¥${goods.salePrice}` : ''}`,
|
||||
`${goods.dealerPrice ? `¥${goods.dealerPrice}` : ''}`,
|
||||
fmtPrice(goods.price, goods.priceMax),
|
||||
fmtPrice(goods.salePrice, goods.salePriceMax),
|
||||
fmtPrice(goods.dealerPrice, goods.dealerPriceMax),
|
||||
`${goods.sales || 0}`,
|
||||
`${goods.stock || 0}`,
|
||||
statusMap[goods.status || 0] || '',
|
||||
goods.recommend === 1 ? '是' : '否',
|
||||
`${goods.createTime || ''}`
|
||||
`${goods.createTime || ''}`,
|
||||
ensureFullUrl(`${goods.image || ''}`)
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -416,7 +496,8 @@
|
||||
{ wch: 10 },
|
||||
{ wch: 12 },
|
||||
{ wch: 8 },
|
||||
{ wch: 20 }
|
||||
{ wch: 20 },
|
||||
{ wch: 50 }
|
||||
];
|
||||
|
||||
message.destroy();
|
||||
@@ -477,9 +558,8 @@
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
/* 查询:分类数据延迟到列表首屏渲染完成后再加载,不阻塞首屏 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
// 加载分类数据
|
||||
if (!navigationList.value) {
|
||||
listShopGoodsCategory({}).then((res) => {
|
||||
@@ -506,20 +586,90 @@
|
||||
});
|
||||
};
|
||||
|
||||
/* 单个商品置顶/取消置顶(isTop: 1 置顶 0 不置顶) */
|
||||
const onTop = (row: ShopGoods) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
updateShopGoods({
|
||||
...row,
|
||||
isTop: row.isTop == 1 ? 0 : 1
|
||||
})
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 单个商品上/下架切换(status: 0 上架 1 下架) */
|
||||
const toggleShelf = (row: ShopGoods, status: number) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
updateShopGoods({ ...row, status })
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量上架 */
|
||||
const batchUpShelf = () => {
|
||||
batchSetShelf(0, '上架');
|
||||
};
|
||||
|
||||
/* 批量下架 */
|
||||
const batchDownShelf = () => {
|
||||
batchSetShelf(1, '下架');
|
||||
};
|
||||
|
||||
/* 批量设置上下架 */
|
||||
const batchSetShelf = (status: number, label: string) => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条商品');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: `确定要批量${label}选中的 ${selection.value.length} 个商品吗?`,
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
Promise.all(selection.value.map((d) => updateShopGoods({ ...d, status })))
|
||||
.then(() => {
|
||||
hide();
|
||||
message.success(`已批量${label} ${selection.value.length} 个商品`);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ShopGoods) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
openEdit(record);
|
||||
// openEdit(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
// openEdit(record);
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
onMounted(query);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -528,4 +678,14 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.goods-thumb {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(255, 255, 255, 0.06);
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '商品价格',
|
||||
title: '商城价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center',
|
||||
|
||||
@@ -90,6 +90,14 @@
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="链接地址" name="path">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="可选,如:/pages/goods/list?categoryId=1"
|
||||
v-model:value="form.path"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="0">正常</a-radio>
|
||||
@@ -286,6 +294,7 @@
|
||||
image: undefined,
|
||||
adImage: undefined,
|
||||
parentId: undefined,
|
||||
path: undefined,
|
||||
sortNumber: 100,
|
||||
comments: undefined,
|
||||
status: 0
|
||||
|
||||
@@ -23,10 +23,11 @@
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
<a-space :style="{ paddingLeft: ((depthMap.get(record.categoryId || 0) || 0) * 20) + 'px' }">
|
||||
<img v-if="record.image" :src="record.image" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<span v-html="renderTreePrefix(record)"></span>
|
||||
<img v-if="record.image" :src="getCompressedImageUrl(record.image, { width: 48, quality: 90 })" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
|
||||
<span class="font-bold">{{ record.title }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="record.type === 0 ? 'blue' : 'orange'">
|
||||
@@ -36,8 +37,8 @@
|
||||
<template v-if="column.key === 'adImage'">
|
||||
<img
|
||||
v-if="record.adImage"
|
||||
:src="record.adImage"
|
||||
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
|
||||
:src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
|
||||
style="max-width: 56px; height: 28px; object-fit: cover; border-radius: 4px;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
@@ -91,6 +92,7 @@
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
|
||||
import { getCompressedImageUrl } from '@/utils/image';
|
||||
import {
|
||||
listShopGoodsCategory,
|
||||
removeShopGoodsCategory,
|
||||
@@ -112,6 +114,10 @@
|
||||
const flatCategoryList = ref<ShopGoodsCategory[]>([]);
|
||||
// 分类层级深度映射
|
||||
const depthMap = ref<Map<number, number>>(new Map());
|
||||
// 分类节点映射(categoryId -> 节点),用于回溯祖先链
|
||||
const nodeMap = ref<Map<number, ShopGoodsCategory>>(new Map());
|
||||
// 是否为父级最后一个子节点(用于绘制树形连接线的拐角 / 引导竖线)
|
||||
const isLastMap = ref<Map<number, boolean>>(new Map());
|
||||
|
||||
// 计算每个分类的层级深度
|
||||
const computeDepth = (list: ShopGoodsCategory[]) => {
|
||||
@@ -132,16 +138,76 @@
|
||||
depthMap.value = map;
|
||||
};
|
||||
|
||||
// 构建树形元信息:节点映射 + 是否末位子节点
|
||||
const buildTreeMeta = (list: ShopGoodsCategory[]) => {
|
||||
const byParent = new Map<number, ShopGoodsCategory[]>();
|
||||
const nMap = new Map<number, ShopGoodsCategory>();
|
||||
list.forEach((c) => {
|
||||
if (c.categoryId != null) nMap.set(c.categoryId, c);
|
||||
const pid = c.parentId || 0;
|
||||
if (!byParent.has(pid)) byParent.set(pid, []);
|
||||
byParent.get(pid)!.push(c);
|
||||
});
|
||||
const lastMap = new Map<number, boolean>();
|
||||
byParent.forEach((children) => {
|
||||
children.forEach((c, idx) => {
|
||||
if (c.categoryId != null) {
|
||||
lastMap.set(c.categoryId, idx === children.length - 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
nodeMap.value = nMap;
|
||||
isLastMap.value = lastMap;
|
||||
};
|
||||
|
||||
// 表格数据源(不分页,全量展示便于查看层级)
|
||||
const datasource: DatasourceFunction = ({ where, orders }) => {
|
||||
return listShopGoodsCategory({ ...where, ...orders }).then((res) => {
|
||||
const list = res || [];
|
||||
flatCategoryList.value = list;
|
||||
computeDepth(list);
|
||||
buildTreeMeta(list);
|
||||
return { list, count: list.length };
|
||||
});
|
||||
};
|
||||
|
||||
// 生成树形连接线 SVG:顶级不画;二级/三级根据祖先链绘制引导竖线与拐角
|
||||
const renderTreePrefix = (record: ShopGoodsCategory): string => {
|
||||
const depth = depthMap.value.get(record.categoryId || 0) || 0;
|
||||
if (depth === 0) return '';
|
||||
const W = 20;
|
||||
const H = 20;
|
||||
const mid = H / 2;
|
||||
// 回溯祖先链:chain[0]=顶级 ... chain[depth]=当前节点
|
||||
const chain: number[] = [];
|
||||
let cur: number | undefined = record.categoryId;
|
||||
while (cur != null) {
|
||||
chain.unshift(cur);
|
||||
cur = nodeMap.value.get(cur)?.parentId || undefined;
|
||||
if (chain.length > 30) break;
|
||||
}
|
||||
let svg = `<svg width="${depth * W}" height="${H}" viewBox="0 0 ${depth * W} ${H}" fill="none" style="flex-shrink:0;display:block;">`;
|
||||
const stroke = '#d0d0d0';
|
||||
for (let k = 0; k < depth; k++) {
|
||||
const x = k * W + Math.floor(W / 2);
|
||||
if (k < depth - 1) {
|
||||
// 祖先节点不是末位子节点时,画贯穿引导竖线
|
||||
if (!isLastMap.value.get(chain[k])) {
|
||||
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
|
||||
}
|
||||
} else {
|
||||
// 末段:竖线到中线 + 横线到右侧
|
||||
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
|
||||
if (!isLastMap.value.get(record.categoryId || 0)) {
|
||||
svg += `<line x1="${x}" y1="${mid}" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
|
||||
}
|
||||
svg += `<line x1="${x}" y1="${mid}" x2="${(k + 1) * W}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
|
||||
}
|
||||
}
|
||||
svg += `</svg>`;
|
||||
return svg;
|
||||
};
|
||||
|
||||
// 获取父级分类名称
|
||||
const getParentName = (parentId?: number) => {
|
||||
if (!parentId || parentId === 0) return '顶级分类';
|
||||
|
||||
@@ -45,10 +45,10 @@
|
||||
@del="onDeleteItem"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="商品价格" name="price">
|
||||
<a-form-item label="商城价" name="price">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入商品价格"
|
||||
placeholder="请输入商城价"
|
||||
v-model:value="form.price"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '商品价格',
|
||||
title: '商城价',
|
||||
dataIndex: 'price',
|
||||
key: 'price',
|
||||
align: 'center'
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<a-alert
|
||||
type="info"
|
||||
show-icon
|
||||
message="确认已收到该订单的线下付款(微信转账/银行汇款等),确认后订单将进入待发货状态。"
|
||||
:message="alertMessage"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { UploadFile, UploadProps } from 'ant-design-vue';
|
||||
@@ -99,16 +99,25 @@
|
||||
orderId: undefined as number | undefined,
|
||||
orderNo: '' as string,
|
||||
paymentVoucher: '' as string,
|
||||
remarks: '' as string
|
||||
remarks: '' as string,
|
||||
deliveryStatus: undefined as number | undefined
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
paymentVoucher: [
|
||||
{ required: true, message: '请上传支付凭证' }
|
||||
{ required: false, message: '请上传支付凭证' }
|
||||
]
|
||||
};
|
||||
|
||||
// 根据发货状态显示不同的提示文案
|
||||
const alertMessage = computed(() => {
|
||||
if (form.deliveryStatus === 20) {
|
||||
return '该订单已发货,确认收到线下付款(微信转账/银行汇款等)后,订单将自动完成。';
|
||||
}
|
||||
return '确认已收到该订单的线下付款(微信转账/银行汇款等),确认后订单将进入待发货状态。';
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const { resetFields, validate } = useForm(form, rules);
|
||||
|
||||
@@ -197,7 +206,7 @@
|
||||
form.remarks || undefined,
|
||||
form.paymentVoucher || undefined
|
||||
);
|
||||
message.success('确认收款成功,订单已进入待发货状态');
|
||||
message.success(form.deliveryStatus === 20 ? '确认收款成功,订单已自动完成' : '确认收款成功,订单已进入待发货状态');
|
||||
emit('done');
|
||||
updateVisible(false);
|
||||
} catch (error: any) {
|
||||
@@ -218,6 +227,7 @@
|
||||
form.orderNo = props.data?.orderNo || '';
|
||||
form.paymentVoucher = '';
|
||||
form.remarks = '';
|
||||
form.deliveryStatus = props.data?.deliveryStatus;
|
||||
fileList.value = [];
|
||||
} else {
|
||||
resetFields();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user