36 Commits

Author SHA1 Message Date
c553a71ea2 feat(dashboard): 新增后台今日数据概况及仪表盘页面
- 新增 shopOrderTotalAll 和 shopOrderOverview 接口,支持含未支付口径数据统计
- 新增 ShopOrderOverview 类型定义,包含今日销售额、订单数、新增用户数和优惠券使用数
- 新增后台专用仪表盘页面,展示用户数、订单数、总营业额和系统运行天数等核心数据
- 仪表盘页面实现新订单提醒功能,支持声音和语音播报
- 提供待付款、待发货订单数和退款申请动态展示,带点击跳转功能
- 新增今日数据概况区域,显示销售额、订单数、新增用户和优惠券使用情况
- 集成订阅和续费功能,支持支付二维码扫码支付及状态轮询
- 基本信息展示系统版本、授权状态以及订阅到期提醒和续费入口
- 快速入口模块方便访问订单管理、商品管理、优惠券管理等功能页
- 实现数据自动刷新机制,包括统计数据和含未支付订单的特殊统计
- 优化了界面样式和交互体验,提升了后台管理的可用性和操作便捷性
2026-08-01 22:11:27 +08:00
a284a83b86 feat(dashboard): 新增后台今日数据概况及仪表盘页面
- 新增 shopOrderTotalAll 和 shopOrderOverview 接口,支持含未支付口径数据统计
- 新增 ShopOrderOverview 类型定义,包含今日销售额、订单数、新增用户数和优惠券使用数
- 新增后台专用仪表盘页面,展示用户数、订单数、总营业额和系统运行天数等核心数据
- 仪表盘页面实现新订单提醒功能,支持声音和语音播报
- 提供待付款、待发货订单数和退款申请动态展示,带点击跳转功能
- 新增今日数据概况区域,显示销售额、订单数、新增用户和优惠券使用情况
- 集成订阅和续费功能,支持支付二维码扫码支付及状态轮询
- 基本信息展示系统版本、授权状态以及订阅到期提醒和续费入口
- 快速入口模块方便访问订单管理、商品管理、优惠券管理等功能页
- 实现数据自动刷新机制,包括统计数据和含未支付订单的特殊统计
- 优化了界面样式和交互体验,提升了后台管理的可用性和操作便捷性
2026-08-01 22:10:28 +08:00
bf60ba1d04 refactor(shop): 将商品价格相关文本统一修改为“商城价”
- 修改shopGoods、shopGoodsBrowse和shopGoodsSku模型中price字段注释
- 更新shopGoodsBrowse和shopGoodsSku页面表格中价格列标题
- 调整shopGoodsEdit组件中表单标签、输入框占位符和校验提示信息
- 同步修改shopGoodsSkuEdit组件中价格表单项的标签和占位符文本
- 保证所有相关文本表述一致,提升用户界面和代码的统一性
2026-07-30 12:45:27 +08:00
eb26594f41 refactor(user): 优化用户列表展示和排序功能
- 表格大小设置为 small,提升视觉紧凑度
- 用户名称显示优先 realName,其次 nickname,缺失时显示 "-"
- 邮箱列添加排序功能,支持用户交互排序
- 移除创建时间列的文字省略,完整显示时间信息
2026-07-29 00:52:48 +08:00
e87e60fd6a feat(shopOrder): 添加订单商品明细展开功能并优化订单状态显示
- 在订单列表中增加商品明细的展开/收起功能,默认显示2条商品信息
- 支持点击“展开全部 N 件”切换显示全部或折叠商品明细
- 优化订单状态栏支付状态、发货状态、开票状态及操作按钮中的分隔线统一风格
- 将shop/dashboard运行天数改为基于租户创建时间,而非订阅生效时间,保证准确性
- 调整订单列表表格列宽及显示顺序,改善界面布局和信息展示
- 注释和代码格式微调,删除无用的组件显示及注释代码,提升代码整洁度
- 修复了部分变量定义和模板标签排版,完善代码可维护性
2026-07-29 00:44:54 +08:00
4ab90c9d89 style(shop): 优化商品和订单列表界面细节
- 添加商品列表表格横向滚动支持,避免列溢出
- 设置商品图片高度,保持缩略图统一尺寸
- 修改商品状态标签文案为“出售中”
- 调整订单商品明细列,支持展开与收起全部商品
- 在订单列表操作列中新增竖线分隔符,提升按钮分隔清晰度
- 统一订单操作按钮文本,去除图标依赖
- 注释掉商品列表中排序号列
- 微调表格中部分列宽及对齐方式提高视觉体验
2026-07-28 19:44:12 +08:00
a92248bff7 fix(api): 修复根据code查询租户时类型定义错误
- 修改getByCode函数中请求返回类型为ApiResult<Tenant>
- 修正对返回数据的访问以匹配新的类型结构
- 确保类型安全和避免潜在运行时错误
2026-07-27 14:30:56 +08:00
dd094f8769 feat(cmsAd): 优化广告图片展示和引入图片压缩工具
- 使用压缩后的图片 URL 显示广告图片,提升加载性能
- 添加图片预览功能,支持点击查看原图
- 调整图片展示宽度参数,保证视觉效果
- 引入图片压缩工具方法 getCompressedImageUrl 以统一处理图片请求
2026-07-27 14:25:11 +08:00
4a8b0a957c refactor(api): 统一CMS接口URL配置
- 在环境变量中新增CMS接口URL配置项 VITE_CMS_API_URL
- 将所有CMS相关API接口调用的基地址由MODULES_API_URL或SERVER_API_URL替换为CMS_API_BASE_URL
- 更新cmsAd、cmsAdRecord、cmsArticle及相关cms模块的接口地址引用
- 统一管理CMS API基础路径,提高代码维护性和可配置性
2026-07-27 12:42:51 +08:00
45fc45680f feat(passport): 增加多租户手机号登录支持并修复邮箱验证码校验
- 修复邮箱登录验证码未校验问题,恢复输入错误提示及刷新验证码逻辑
- 新增多租户选项登录流程,支持单租户直接登录及多租户选择弹窗
- 扩展登录接口及模型,新增 TenantOption 类型和 LoginOutcome 联合结果类型
- 新增 loginBySelectTenant 二次登录接口及对应组件逻辑
- 统一保存登录态,包括 token、租户ID和用户ID到本地存储
- 调整前端短信登录逻辑,根据返回状态决定流程,支持多租户弹窗选择
- 确保新增代码无类型错误并通过构建验证
2026-07-24 22:33:39 +08:00
55e90c2434 feat(login): 优化手机号验证码发送逻辑,前3次免图形验证码
- 新增本地存储计数,记录每个手机号发送次数
- 前3次发送验证码无需弹出图形验证码弹窗,提升用户体验
- 第4次及以后需通过图形验证码验证后才发送短信
- 发送失败时自动刷新图形验证码并显示错误信息
- 补充异常处理,确保发送按钮状态正确重置
- 代码无类型错误,构建验证通过
2026-07-24 22:25:30 +08:00
f9bf3c6df3 feat(passport): 优化忘记密码页发送验证码体验
- 引入图形验证码弹窗,风格与注册页面保持一致
- 使用 localStorage 记录每个账号发送验证码次数,前三次免图形验证码
- 第四次及以后发送验证码需输入正确图形验证码才能发送
- 失败时自动刷新验证码,关闭弹窗时重置加载状态
- 新增相关国际化文案支持中英文
- 调整登录页邮箱登录相关占位符文案提升用户体验
2026-07-24 22:14:29 +08:00
d6b56d99ce feat(passport): 新增忘记密码功能及相关接口
- 新增忘记密码页面,支持邮箱找回和手机找回两种方式,包含验证码倒计时
- 添加密码强度和一致性校验,确保新密码安全性
- 开启对应路由 /forget,恢复已注释的忘记密码入口
- 新增发送短信验证码和邮箱验证码接口,适配未登录场景
- 新增重置密码接口,支持手机短信码及邮箱验证码方式
- 请求拦截器支持 X-Tenant-Skip 头,跳过租户信息注入,避免跨租户串扰
- 国际化新增忘记密码相关英文和中文文案
- 页面样式与登录页视觉风格一致,跨屏幕响应式布局优化
2026-07-24 22:07:53 +08:00
0bfd5e667b docs(shop): 补充商品分类多级分类调研内容
- 说明分类管理页支持多级层级及其实现方式
- 指出商品端选分类组件限制在2~3级的问题
- 提醒后端是否限制层级需查后端仓库确认

feat(shopGoods): 新增批量上架含图片商品SQL脚本

- 提供批量上架分类ID为10332且含有图片商品的SQL预览查询
- 包含更新商品状态为上架的SQL语句及执行说明
- 约定商品状态、图片判定及删除标记等业务规则
2026-07-24 11:19:50 +08:00
a831dce61a feat(shopGoods): 优化商品列表和后端排序逻辑
- 商品列表「编号」列移除,改为名称下方灰色小字显示编号
- 市场价、价格、会员价三列合并为单列,按编辑弹窗顺序堆叠显示
- 后端新增 is_top 字段,实现置顶商品排序优先级调整
- pageRel 和 listRel 默认排序改为置顶优先、排序号次之、创建时间最后
- 采集默认设置 isTop 字段为 0,保持一致性
- 删除单独「编号」列节省列表横向空间,布局更紧凑
- 后端排序调整影响所有调用分页接口的端,置顶商品浮至最前
- 编辑弹窗「套餐价格」更名「进货价格」,对应接口字段注释同步修改
2026-07-24 11:17:35 +08:00
e2f6dce7a1 feat(shopGoods): 新增商品置顶功能及批量上下架操作
- 在商品模型中新增 isTop 字段支持置顶状态(1置顶,0不置顶)
- 商品列表新增置顶列,支持列表内联切换置顶状态
- 启用排序号列排序功能,置顶列参与前端排序
- 新增单个商品上架/下架切换接口及批量上下架操作
- 优化商品列表工具栏和选中操作栏,加入批量上架、下架按钮
- 商品分类及图片列表调用 OSS 图片压缩接口处理展示图片
- 新增删除商品导入模板及图片导入模板下载链接与使用说明
- 优化图片压缩工具,新增去除 URL 中 x-oss-process 参数功能,确保调用参数生效
2026-07-24 10:57:23 +08:00
44e416525d fix(shopOrder): 修复导出商品总数量和完善导出字段
- 新增 getGoodsTotalNum 函数,基于订单商品明细求总数量,修正商品总数量计算错误
- 修改导出商品总数量列,避免使用订单级 totalNum 字段,确保数量正确
- 补齐导出字段,从8列扩展至15列,包含收货人、联系电话、配送方式等物流关键字段
- 导出数据信息同步商品明细,通过 Promise.all 调用逐单获取商品详情
- 支持按当前标签筛选导出,确保导出数据符合当前视图筛选条件
- 增加辅助函数优化代码结构,如 getShopName、getDeliveryTypeLabel 等
2026-07-23 08:00:34 +08:00
708bc7fc80 feat(shopOrder): 补充订单商品明细显示并优化列表数据加载
- 引入订单商品明细接口listShopOrderGoods以获取详细商品数据
- 修改商品汇总显示,增加规格信息展示(商品名(规格) x 数量)
- 优化订单列表加载,针对每笔订单单独请求商品明细数据补全
- 解决订单列表接口不返回orderGoods字段的问题,保证商品明细完整显示
- 确保异步获取商品详情失败时,使用空数组避免页面错误
2026-07-23 07:56:19 +08:00
a4107e212a feat(shopOrder): 按当前标签筛选状态导出订单
- 搜索组件增加 statusFilter 属性以支持导出时传入筛选状态
- 在 shopOrder 页面通过计算属性传递当前标签的筛选状态给搜索组件
- 导出时订单列表请求中带入当前标签对应的筛选状态,避免混入其它状态订单
- 优化导出功能的状态一致性,提升使用体验
2026-07-23 07:51:50 +08:00
c9a4e4c229 feat(shopOrder): 优化订单导出数据格式与内容
- 新增配送方式、发货状态等字段的中文标签转换函数
- 修改导出表头,调整字段顺序及名称,使信息更完整
- 导出内容增加收货人、联系电话、收货地址、商品明细等详细信息
- 优化商品明细汇总,显示商品名及数量
- 区分自提与快递的发货店铺名称展示
- 设置导出Excel表格列宽,更加美观规整
- 增加配送时间、自提码、买家备注等字段导出内容
2026-07-23 07:45:35 +08:00
f65b8a945d fix(login): 互换登录页右上角扫码与手机图标
- 调整扫码模式下显示手机图标,账号/手机模式显示二维码图标
- 解决图标显示与目标模式相反的问题
- 优化用户界面切换按钮的视觉表达
2026-07-18 19:43:54 +08:00
9395e63f20 feat(passport): 优化登录页账号/手机号切换和指示器动画
- 调整默认登录类型为账号登录,支持账号和手机号登录切换
- 添加底部蓝色下划线指示器,动态显示当前选中标签位置和宽度
- 实现指示器收缩滑动和展开的两阶段动画效果,提升交互体验
- 增加窗口尺寸变化时自动调整指示器位置和宽度
- 修改扫码登录切换逻辑,避免意外切换至手机号登录
- 在账号登录和手机号登录页底部添加提示“还没有账号?联系管理员开通”
2026-07-18 17:10:08 +08:00
2037b0699a feat(passport): 支持邮箱和用户ID登录合并账号登录
- LoginParam 接口新增 email 和 userId 字段
- 新增 loginByEmail 和 loginByUserId 接口调用,逻辑与 login 类似
- 登录页账号输入框改为 account,支持输入邮箱或用户ID
- 提交时根据账号格式(邮箱或纯数字)调用对应登录接口
- 校验规则改为校验 account 字段,格式不符时提示错误
- 手机号短信登录改用 loginBySuperAdminSms 接口替代 loginBySms
- 登录成功后保存租户ID和用户ID至 localStorage
- 优化登录页相关导入及逻辑,提升登录体验
2026-07-18 17:00:39 +08:00
bef50c2f26 feat(order): 支持线下付款先发货后结款流程
- 后端移除确认收款时订单状态限制,已发货订单确认收款后自动完成订单
- 管理后台订单列表和操作按钮区分线下付款未付款不同发货状态
- 离线付款弹窗新增发货状态字段及提示文案动态展示
- 订单详情支付状态标签区分已发货待收款状态,更新步骤条显示逻辑
- 小程序端订单状态文本和操作按钮支持线下付款先发货后结款场景
2026-07-17 15:09:51 +08:00
57be388a65 fix(xinlong-shop-taro): 修复订单详情页配送方式判断逻辑
- 纠正了判断快递配送的条件,避免误判商家送货为快递配送
- 订单收货信息卡片显示不同配送方式对应的图标和门店名称
- 订单信息区新增配送方式字段,映射 DELIVERY_TYPE_MAP
- 规范了 deliveryType 的值范围及对应含义
- 修复时纠正了误改项目,将改动应用到商家端项目 xinlong-shop-taro
2026-07-17 12:10:10 +08:00
bd814d00b0 feat(shop): 优化发货弹窗文案及修复小程序配送方式判断
- deliveryModal.vue 中发货门店、发货人标签及提示文案,根据快递配送与商家送货动态显示
- 快递配送场景下新增寄件人信息同步至快递面单的小字说明和验证文案动态调整
- 修正小程序订单详情页 deliveryType 判断,新增 DELIVERY_TYPE_MAP 支持快递及商家送货标识
- 订单详情页收货信息卡片图标和配送门店名称根据实际配送方式动态显示
- 订单信息区新增配送方式字段,清晰展示快递配送、自提及商家送货三种状态
2026-07-17 12:05:22 +08:00
3edbb13a8d fix(orders): 修正发货功能关联的项目错误导致 storeId 为 0
- 发现并纠正之前误操作在错误项目 guilixu-taro 上的改动
- 恢复 guilixu-taro 项目中误改的文件,删除新增文件
- 明确实际工作项目为 xinlong-shop-taro,该项目已内置发货功能
- 解决 xinlong-shop-taro 中 openShipModal 调用 listShopStoreUser 未传 storeId 导致默认 0 的问题
- 修改 openShipModal 先调用 getMyClerk 获取 storeId,再传递给 listShopStoreUser 拉取本门店店员
- 默认选中当前登录人对应的店员,完善发货弹窗功能
- 警示跨项目操作时需确认准确项目路径,避免混淆同名 API 和项目目录
2026-07-17 11:36:36 +08:00
b406973039 feat(store): 修正 storeId 获取并新增发货人员选择功能
- 排查 orders 页请求 storeId=0 的根因,确认旧版 H5 构建发出该请求
- 明确门店 storeId 来源于 getMyClerk() 返回的 ShopStoreUser.storeId 字段
- 实现全局 useClerk 上下文,管理店员及门店信息,避免局部状态冗余
- 重构用户中心与门店中心页面为调用 useClerk,移除局部 storeInfo 状态
- 补全 ShopStoreUser 模型字段,支持完整店员信息展示
- 在 orders 页新增“确认发货”功能,支持选择发货人员并提交发货请求
- 发货单提交包含 sendStoreId 和 deliveryUserId,确保后端一致落库
- 全项目类型检查无新增报错,优化门店相关接口调用逻辑和状态管理
2026-07-17 11:31:42 +08:00
2745f4b0db feat(shopOrder): 增加发货门店与配送店员选择功能
- 在发货类型非“无需发货”时新增发货门店下拉选择,支持加载门店列表
- 选择门店后自动回填发货地址及快递配送发货人信息
- 商家送货时新增配送店员下拉筛选,支持根据门店联动加载配送人员
- 配送店员选择后自动回填发货人姓名和电话
- 表单校验新增发货门店必填及配送店员在商家送货时必填规则
- 更新提交数据结构,包含发货门店ID与配送店员ID字段
- 订单详情页增加发货门店及配送店员展示字段
- 调整配送方式单选按钮顺序,快递配送与商家送货前置,无需发货后置
2026-07-17 04:17:57 +08:00
e9249c910f feat(shopOrder): 增加发货门店与配送店员选择功能
- 在发货类型非“无需发货”时新增发货门店下拉选择,支持加载门店列表
- 选择门店后自动回填发货地址及快递配送发货人信息
- 商家送货时新增配送店员下拉筛选,支持根据门店联动加载配送人员
- 配送店员选择后自动回填发货人姓名和电话
- 表单校验新增发货门店必填及配送店员在商家送货时必填规则
- 更新提交数据结构,包含发货门店ID与配送店员ID字段
- 订单详情页增加发货门店及配送店员展示字段
- 调整配送方式单选按钮顺序,快递配送与商家送货前置,无需发货后置
2026-07-17 04:11:45 +08:00
6b57dd2fed fix(shopOrder): 修正确认收货状态码
- 将 deliveryStatus 状态码由 30 修改为 20,保持与已收货状态一致
- 确认收货操作后订单状态设置为已完成
- 更新相关组件中确认收货的逻辑状态码展示
2026-07-17 00:05:17 +08:00
c9e074366f =========== V3.0.1 ==========
- 稳定版
- 可以基于此版本作为其他项目的底座
2026-07-16 22:16:53 +08:00
b07ceaa8f1 =========== V3.0.1 ========== 2026-07-16 22:08:07 +08:00
1662daf436 fix(dashboard): 修复刷新统计数据未更新问题
- 交换基本信息和今日数据概况面板位置,基本信息放左侧,今日数据概况放右侧
- 修改刷新统计数据方法调用,强制传入参数跳过缓存
- 删除测试通知按钮,简化界面
- 确保刷新数据按钮可用且显示加载状态
- 添加刷新成功提示,增强用户体验
2026-07-16 21:32:19 +08:00
1d69923e06 refactor(shop-dashboard): 调整商城后台Dashboard布局及面板结构
- 基本信息面板改为左右两列并排显示,响应式列数调整为2列
- 今日数据概况与基本信息合并至同一行,分别占用半宽空间
- 今日数据概况面板栅格调整为2列,样式和间距优化
- 删除快捷操作面板及相关图标导入和缓存清理功能
- 快速入口功能调整为动态渲染卡片布局取代原按键列表
- 优化部分样式,如待办任务列表的内边距和字体大小调整
- 头像显示增加默认图片占位,避免空白显示
2026-07-16 21:25:22 +08:00
0b6563aad9 refactor(cms): 更新商城名称及相关展示文案
- 将网页标题由“企业商城”修改为“小程序商城”
- 修改仪表盘技术支持项显示名称为“企业官网”
- 更新登录页默认配置中站点名称为“小程序商城”
2026-07-16 14:13:33 +08:00
83 changed files with 5556 additions and 1430 deletions

1
.env
View File

@@ -1,2 +1,3 @@
VITE_APP_NAME=小程序商城
VITE_API_URL=/api
VITE_CMS_API_URL=https://cms-api.websoft.top/api

View File

@@ -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_CMS_API_URL=https://cms-api.websoft.top/api

View File

@@ -1,2 +1,3 @@
VITE_APP_NAME=后台管理系统
VITE_API_URL=https://shop-api.websoft.top/api
VITE_CMS_API_URL=https://cms-api.websoft.top/api

View File

@@ -29,3 +29,35 @@
1. 后端:`cd /Users/gxwebsoft/JAVA/websopy-java && git push`,然后远程服务器 `git pull && mvn clean package -DskipTests && docker-compose up -d --build cms-api`(或对应重启方式)。
2. 小程序端:`websopy-taro` 重新编译上传体验版/正式版。
3. 联调验证admin 点"立即续费" → 弹码 → 微信扫码 → 进小程序应显示"确认支付"页(不再误显成功)→ 点"立即支付" → 微信支付 → 显示"支付成功" → 跳转 `/user/apps/index`
## 商城后台 Dashboard 布局调整
- 文件:`src/views/shop/dashboard/index.vue`
- 需求:今日数据概况页,「基本信息」改为左右两列并排;删除「快捷操作」面板。
- 改动:
- `a-descriptions``:column="1"` 改为 `:column="{ xs: 1, sm: 2 }"`(响应式两列并排)。
- 基本信息 `a-col``:span="12"` 改为 `:span="24"`(占满整行)。
- 删除「快捷操作」`a-col` 整块并清理其专属的图标导入ShoppingCartOutlined/ShopOutlined/AppstoreOutlined/GiftOutlined/TeamOutlined/SettingOutlined/ClearOutlined`removeSiteInfoCache` 导入及 `handleClearCache` 函数。
### 今日数据概况 + 基本信息改为左右两栏布局
- 需求:参考「待处理事项 + 快速入口」的左右结构,将「今日数据概况」和「基本信息」也并排为左右各半。
- 改动:
- 原来各自独占整行的两个 panel合并到一个 `<a-row>` 中,各占 `:md="12"`
- 今日数据概况栅格从 4 列改为 2 列(`grid-template-columns: repeat(2, 1fr)`),适配半宽空间。
- 基本信息 `a-descriptions` 列数改为 `:column="1"`,半宽下单列更不拥挤。
- 基本信息(左)和今日数据概况(右)位置对调,基本信息在左、今日数据概况在右。
### 刷新统计数据修复
- 问题:点「刷新数据」按钮提示成功,但实际数据没更新。
- 根因:`refreshStatistics` 调用 `statisticsStore.fetchStatistics()` 未传 `forceRefresh=true`5分钟缓存有效期内直接返回旧数据。
- 修复:改为 `fetchStatistics(true)` 强制跳过缓存。
## README.md 修改
- 项目名称从"WebSoftAdmin"改为"小程序商城后台管理系统"。
- 项目域名更新为 https://shop.websoft.top。
- 去掉 CMS 内容管理系统的介绍部分(文章管理、一键排版、媒体库等)。
- 更新技术栈版本号对齐 package.jsonAntDv 4.x、Vite 8.x、TS 5.x、Pinia 2.x
- 补充门店/经销商/会员管理等商城核心功能描述,对齐实际 views/shop 目录结构。

View File

@@ -0,0 +1,122 @@
# 2026-07-17 工作日志
## 订单发货功能改进shopOrder / deliveryModal
分析了订单发货弹窗(`src/views/shop/shopOrder/components/deliveryModal.vue`)三种配送方式,按用户确认的改进方案实施了前端改动:
### 改动文件
1. `deliveryModal.vue`(核心)
- 新增「发货门店」下拉(复用 `listShopStore`),选中后自动回填:快递配送→发货人=门店名/电话=门店电话/地址=门店地址;商家送货→地址=门店地址。
- 商家送货(`deliveryType===2`)新增「配送店员」二级联动下拉(复用 `listShopStoreUser({storeId})`),选中店员自动回填发货人=店员名/联系方式=店员电话。
- 校验:发货门店必填(`deliveryType!==1`);配送店员必填(仅 `deliveryType===2`)。
- 提交时附带 `sendStoreId` / `deliveryUserId`
2. `api/shop/shopOrder/model/index.ts`:新增 `sendStoreId``deliveryUserId` 字段(为后端落库铺路)。
3. `orderInfo.vue`:发货信息卡片增加「发货门店」「配送店员」展示(可选链,后端返回 `storeName`/`deliveryUserName` 后自动生效,否则显示「未填写」)。
### 关键结论 / 待后端配合
- 门店、店员 list API 已存在,前端改动零后端依赖即可落地交互。
- **后端缺口(需补充)**`shop_order` 表需新增 `send_store_id``delivery_user_id` 字段存储门店/店员关联;否则下拉选了也存不进去。
- 登录用户 `User` 模型无 `storeId`,无法默认锁定当前门店,门店下拉需用户手动选择。
- 原「发货地址模板」需求因无现成后端接口,改用「门店作为发货地址模板」方案满足(复用门店 name/phone/address
## 配送方式排序调整
- 修改 `deliveryModal.vue` 配送方式 radio 按钮顺序:快递配送→商家送货→无需发货(原:快递配送→无需发货→商家送货),仅调换模板中 radio 排列顺序value 值不变。
## 后端 Java 改动guilixu-java分两轮
### 第一轮shop_order 主表
- `ShopOrder.java` 实体加持久化字段 `sendStoreId``deliveryUserId`(→`send_store_id`/`delivery_user_id`),及虚拟展示字段 `sendStoreName`/`deliveryUserName`exist=false
- `ShopOrderMapper.xml``selectSql``LEFT JOIN shop_store f` / `LEFT JOIN shop_store_user g` 并 SELECT `f.name as sendStoreName, g.name as deliveryUserName`,让订单详情联带回门店/店员名称。
- 前端 `orderInfo.vue` 展示路径从 `form.shopOrderDelivery.xxx` 改为 `form.sendStoreName`/`form.deliveryUserName`(名称落在订单主对象上)。
- SQL`ALTER TABLE shop_order ADD send_store_id INT, delivery_user_id INT;`
### 第二轮shop_order_delivery 发货单表(用户要求"也写进发货记录表"
- `ShopOrderDelivery.java` 实体加持久化字段 `sendStoreId``deliveryUserId`
- `ShopOrderController.update()` 的发货写入分支(无需物流分支 deliveryMethod=20、实物快递分支 deliveryMethod=30、以及补录运单号分支新建 `ShopOrderDelivery` 时都 `setSendStoreId`/`setDeliveryUserId`(值取自 `shopOrder` 对象,兜底取 `shopOrderNow`)。
- 说明:前端发货只调 `PUT /shop/shop-order`,该接口内部 `new ShopOrderDelivery()` 并 save 落 `shop_order_delivery`;主表靠 `updateById(shopOrder)` 自动落库,发货单表需显式 set 字段。
- SQL`ALTER TABLE shop_order_delivery ADD send_store_id INT, delivery_user_id INT;`
- 验证:`vue-tsc` 前端零报错Java 用 `./mvnw -o compile` 后台验证中。
## Taro 商家端 storeId=0 排查guilixu-taro跨项目
- 用户反馈 orders 页请求 `/shop/shop-store-user?storeId=0`。排查结论:
- `guilixu-taro/src/pages/store/orders/index.tsx` **未调用** `shop-store-user`;全项目 `listShopStoreUser` 仅定义、无调用方。该请求非当前源码发出,应为线上旧版 H5 构建。
- **当前门店 storeId 的真实来源**`getMyClerk()``GET /shop/shop-store-user/my`)返回的 `ShopStoreUser` 对象,其 `.storeId` 字段shop_dealer.id才是门店 id。注意 `user.tsx:103``storeInfo`=getMyClerk 结果)判断"是否显示门店中心",所以那里就能拿到 `storeId = storeInfo.storeId`
- **易错点**`ShopStoreUser``id`(店员自身主键)和 `storeId`(门店 id两个字段二者不同。误用 `storeInfo.id` 当门店 id 是 bug。`booking/index.tsx:186``storeId: storeInfo!.id || 0``storeInfo` 恰为 `ShopStore` 类型故 `.id` 碰巧正确,但 `|| 0` 兜底在未加载时仍会变 0。
- **根因模式**:项目无全局门店/店员上下文,`storeInfo` 是各页面局部 useStateuser/booking/center 各一份)。需在 storeId 就绪前发请求 + `|| 0` 兜底 → `storeId=0`
- **已实施全局 `useClerk`**(用户确认后开工):
- 新增 `src/contexts/ClerkContext.tsx``ClerkProvider` + `useClerkContext`。登录态(`useUserContext().isLoggedIn`)变化即拉取/清空 `getMyClerk()`;对外暴露 `clerk`(ShopStoreUser|null)、`storeId`(=clerk?.storeId)、`loading``refreshClerk``clearClerk`
- 新增 `src/hooks/useClerk.ts`:镜像 `useUser` 的"优先 Context、未包裹则降级本地"范式,文件头注释写明用法与 `storeId>0` 守卫约定。
- `src/app.tsx`:在 `UserProvider` 内挂载 `<ClerkProvider>`(使其能响应登录/登出)。
- 重构 `src/pages/user/user.tsx``src/pages/store/center/index.tsx`:移除各自的局部 `storeInfo`/`getMyClerk` 调用,改为 `const { clerk: storeInfo, refreshClerk } = useClerk()`
- 补全 `src/api/shop/shopStoreUser/model/index.ts``ShopStoreUser` 模型:`getMyClerk`(/my) 联表返回的 `name`/`phone`/`avatar`/`storeName` 补入(原模型缺失,旧代码靠 `useState<any>` 掩盖)。
- **验证**:裸 `tsc --noEmit --types webpack-env` 全量检查——新建/改动的 `ClerkContext.tsx`/`useClerk.ts`/`app.tsx`/`user.tsx`/`center/index.tsx` 及模型 **零新增报错**`center` 页仅剩 4 个**既有**报错75 的 `finally`、82 的 `applyStatus`、89 的 `total`、106 的 `Option`,均在未改动的 `loadBadgeCounts`/`subscribe` 原函数里,属项目原有类型问题 / 裸 tsc 的 lib 配置 artifact。注意裸 `tsc` 默认会因 tsconfig `types:["@tarojs/taro"]`+`typeRoots``TS2688`,需用 `--types webpack-env` 覆盖才能跑真实业务检查。
- **未动**`booking/index.tsx``storeInfo` 是用户所选门店ShopStore来自 `getShopStore(id)`/选择),与店员所属门店是两回事,不在本次范围(其 `storeId: storeInfo!.id || 0` 属不同接口的兜底问题。orders 页现可随时从 `useClerk().storeId` 取当前门店 id。
## Taro 商家端 orders 页「选择发货人员」发货功能guilixu-taro
- 用户截图显示线上旧版 orders 页有「选择发货人员」弹窗且请求 `?storeId=0` 显示"暂无可选择的店员"。当前源码 `orders/index.tsx` 原本**没有**该功能(只有确认收款/确认完成),确认是旧构建。
-`src/pages/store/orders/index.tsx` 新增「确认发货」功能:
- 引入 `useClerk()``storeId`(杜绝 storeId=0`listShopStoreUser``ShopStoreUser` 类型。
- `OpType` 增加 `'deliver'``getOrderActions` 中「待发货」订单(`payStatus && deliveryStatus===10`)显示"确认发货"按钮。
- `openModal('deliver')` 时调 `loadClerkList()``storeId>0``listShopStoreUser({storeId})`,否则 toast 提示门店未就绪、不发请求。
- 弹窗内新增店员单选列表selectedClerkdeliver 模式隐藏凭证上传区。
- 提交发货:`deliveryType=2`(商家送货)、`deliveryStatus=20`(待收货)、`deliveryTime``sendName/sendPhone`(店员)、`sendStoreId=storeId``deliveryUserId=店员id``PUT /shop/shop-order`
- 后端已在上一轮把 `sendStoreId`/`deliveryUserId``shop_order` + `shop_order_delivery`,此处字段对齐。
- 验证:`tsc --noEmit --types webpack-env` 总报错仍 387零新增orders 页仅剩 2 个既有报错165 `fetchLatestOrders` 返回类型、169 `count` 隐式 any均在未改的 `useNewOrderDetector` 调用块)。
---
## ⚠️ 重大纠正:正确项目是 xinlong-shop-taro不是 guilixu-taro
- 用户最终明确:商家端发货/门店相关功能在 **`/Users/gxwebsoft/VUE/xinlong-shop-taro`**,前面整段记的 `guilixu-taro` 改动**全改错项目**了。
- **已在 guilixu-taro 还原**git 仓库):`git checkout --` 还原 5 个跟踪文件model/index.ts、app.tsx、center/index.tsx、orders/index.tsx、user.tsx`rm` 删除 2 个新建文件contexts/ClerkContext.tsx、hooks/useClerk.ts。未动的 `pages/index/index.scss` 是仓库原有未提交改动,保留。
- **xinlong-shop-taro 才是真项目,且早已内置发货功能**`orders/index.tsx` 已有 `OpType='ship'` 的「发货」+ `saveShopOrderDelivery` + `listShopStoreUser` + 发货弹窗;`user.tsx` 已有 `getMyClerk()``storeInfo`(含 `.storeId`)用于"门店中心"判断。
- **真正的 `storeId=0` bug 根因**`xinlong-shop-taro/src/pages/store/orders/index.tsx:465``openShipModal``listShopStoreUser()` **没传 storeId**(默认 0即用户最初报的 `?storeId=0`
- **已修xinlong-shop-taro**`openShipModal` 内先 `getMyClerk()``storeId``listShopStoreUser({storeId})` 拉本门店店员;并默认选中 `userId === 当前登录人 userId` 的店员(`setSelectedClerkId(match.id)`)。`tsc --noEmit --types webpack-env` 该文件零新增报错(仅 162/208/212 三个项目既有错误,均在我未改动区域)。
- **教训**:跨项目时先用 glob/find 在多个候选项目里确认"哪个项目有该功能源码",再动手;不要凭域名/习惯默认锁定一个项目。本次因 `guilixu-taro``xinlong-shop-taro` 命名相近、且前者恰有 `getMyClerk` 等同名 API 而误判。
## 发货弹窗文案优化deliveryModal.vue
- 用户反馈"快递配送时发货人/联系人选门店是否不合理",分析结论:业务逻辑正确(发货人=寄件方=门店),问题在文案未区分"寄件方"与"承运方"。
- 改动 `src/views/shop/shopOrder/components/deliveryModal.vue`
- 「发货门店」标签动态化快递配送→「发货门店」商家送货→「配送门店」placeholder 同步动态)。
- 「发货人」标签动态化:快递配送→「寄件人」,商家送货→「发货人」(联系方式标签同理)。
- 提示文案动态化:快递配送说明"寄件人即为发货门店",商家送货说明"回填配送店员信息"。
- 快递配送时在寄件人字段下方加小字说明"寄件人信息将同步至快递面单"。
- 验证规则的 reject 文案同步动态化。
- 不涉及数据结构/接口变更,纯前端文案优化。
## 修复小程序订单详情页配送方式判断逻辑xinlong-shop-taro 注意正确项目
- **正确项目**`/Users/gxwebsoft/VUE/xinlong-shop-taro`(非 guilixu-taro
- 文件:`src/pages/order/detail.tsx`
- 问题:原代码 `isExpress = order.deliveryType === 0 || undefined`,无法区分 deliveryType=2商家送货
- 修复:
1. 新增 `DELIVERY_TYPE_MAP``0=快递配送, 1=自提, 2=商家送货`
2. 修正 `isExpress` 判断:`deliveryType === 0 || 2 || undefined` → 快递/商家送货都显示收货人信息
3. 收货信息卡片:商家送货显示🛵图标+配送门店名,快递配送显示📦图标
4. 订单信息区新增「配送方式」字段展示(使用 DELIVERY_TYPE_MAP
- deliveryType 值范围来自管理后台 deliveryModal.vue0=快递配送, 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`:确认收款描述移除"订单将进入待发货状态"(已不是唯一路径)

View File

@@ -0,0 +1,49 @@
# 2026-07-18 工作日志
## 手机号登录改用 loginBySuperAdminSms 接口
### 改动文件
1. `src/api/passport/login/index.ts`
- 新增 `loginBySuperAdminSms()` 方法,调用 `/loginBySuperAdminSms` 接口
- 保留原有 `loginBySms()` 方法(其他页面如 register/dealer 仍在使用)
- 新方法逻辑与 `loginBySms` 一致:成功后 setToken + 存 TenantId/UserId
2. `src/views/passport/login/index.vue`
- import 替换:`loginBySms``loginBySuperAdminSms`
- `onLoginBySms()` 函数中调用替换:`loginBySms(form)``loginBySuperAdminSms(form)`
- 多租户选择逻辑(`listAdminsByPhoneAll`)保持不变
### 注意
- 仅改了管理后台登录页注册页面register/dealer仍用 `loginBySms`
## 账号登录合并邮箱/用户ID登录
### 需求
账号登录输入框同时支持「邮箱」和「用户ID」邮箱 → `https://server.websoft.top/api/loginByEmail`,纯数字 → `https://server.websoft.top/api/loginByUserId`
### 改动文件
1. `src/api/passport/login/model/index.ts`
- `LoginParam` 新增 `email?: string``userId?: string | number`
2. `src/api/passport/login/index.ts`
- 新增 `loginByEmail()` / `loginByUserId()`,逻辑同 `login()`setToken + 存 TenantId/UserId
3. `src/views/passport/login/index.vue`
- 表单字段 `userId``account`(新增 `LoginForm = User & { account?: string }` 类型)
- 输入框 placeholder 改为「邮箱 / 用户ID」form-item name 改为 `account`
- 校验规则 `userId``account`
- `submit()` 用正则判断邮箱(`/^[^\s@]+@[^\s@]+\.[^\s@]+$/`) / 纯数字(`/^\d+$/`)路由到对应接口格式不符提示「请输入正确的邮箱或用户ID」
- 移除未使用的 `login` 导入
### 注意
- 外部接口用绝对 URL`https://server.websoft.top/api/...`),未走 SERVER_API_URL 代理
- `vue-tsc` 全量检查仍有大量历史错误ByteMdEditor、shopActivity 等),本次改动无新增错误
## 登录页右上角切换图标互换
### 需求
`src/views/passport/login/index.vue` 右上角扫码/手机切换按钮,图标原本与「目标模式」相反,需互换。
### 改动
- 第 65-66 行:`v-if="loginType === 'scan'"` 显示 `QrcodeOutlined` → 改为 `MobileOutlined``v-else` 显示 `MobileOutlined` → 改为 `QrcodeOutlined`
- 即:扫码模式下显示手机图标(切回账号/手机),账号/手机模式显示二维码图标(切到扫码)

View File

@@ -0,0 +1,24 @@
# 2026-07-23
## 订单导出字段补齐(物流部发货用)
- 文件:`src/views/shop/shopOrder/components/search.vue``handleExport`
- 原来只导出 8 列(订单编号/订单标题[实际是 comments]/买家姓名/手机/实付金额/支付方式/付款时间/下单时间),且缺物流发货关键字段。
- 新增并补齐到 15 列:收货人、联系电话、收货地址、配送方式(0快递/1自提/2商家送货)、商品明细(商品名x数量)、商品总数量、实付金额、配送时间、自提码、发货/自提店铺、买家备注、支付方式、发货状态(10未发/20已发/30部分)、下单时间。
- 新增辅助函数:`getDeliveryTypeLabel` / `getDeliveryStatusLabel` / `getGoodsSummary` / `getShopName`
- 注意:枚举值与 `deliveryModal.vue` 一致deliveryType 0快递 1无需发货/自提 2商家送货
- 环境提示:本机 Grep 工具因缺 libpcre2 不可用,搜代码改用 Python 脚本遍历。
## 导出跟随当前标签筛选(避免混入非待发货订单)
- index.vue新增 `statusFilterForExport` computed = `getStatusFilterByTab(activeKey)`,并通过 `:status-filter` 传给 `<search>`
- search.vue给 props 加 `statusFilter?`,导出请求改为 `listShopOrder({ ...where, statusFilter: props.statusFilter })`
- 效果:导出所见即所得——切到「待发货」标签再导出,只导出待发货;其它标签同理。全部标签下 statusFilter=undefined 不额外过滤。
## 修复:导出的商品明细为空
- 根因:`listShopOrder`(非分页列表接口)不返回 `orderGoods` 嵌套;页面表格/详情用的是 `pageShopOrder` 和单独的 `listShopOrderGoods` 取明细。
- 修复:导出时在 `search.vue` 中用 `Promise.all``orderId` 逐个调 `listShopOrderGoods({orderId})` 补齐商品明细,再写入「商品明细」列。
- 商品明细格式升级为 `商品名(规格) x数量`,规格取自 ShopOrderGoods.spec便于物流发对型号。
- 单订单请求失败时 `.catch(() => [])` 兜底,不影响其它订单。
## 修复:商品总数量计算不对
- 根因:「商品总数量」列原本用订单级 `d.totalNum`,但 `listShopOrder` 列表接口未返回该字段(与 orderGoods 同理),导致算错/为空。
- 修复:新增 `getGoodsTotalNum(goods)`,对 `listShopOrderGoods` 取到的明细 `goodsList[idx]` 逐行 `totalNum` 求和。改动后「商品总数量」与「商品明细」列同源,必然对得上。

View File

@@ -0,0 +1,81 @@
# 2026-07-24 工作日志
## shopGoods 商品列表排序机制分析
- 现状:列表「排序号」列(`src/views/shop/shopGoods/index.vue` 约 273-279 行)只做展示,**未启用 `sorter`**,列表默认也不按 `sortNumber` 排序(默认排序由后端决定,前端 `orders` 仅来自「创建时间」列的排序)。
- 编辑表单里「排序号」是 `a-input-number`0-9999默认 100且在 `rules` 里是 **必填**
- 已存在类似的布尔开关 `recommend`列表内联切换index.vue 约 47-58 行),「置顶」可复用同一 UI 模式。
- 结论倾向:纯排序号在商品多时难用,建议「置顶开关 + 少量置顶项内排序 + 其余按默认(创建时间/销量)」的混合方案,而非完全替换。
## 已实现:方案 B商品置顶功能
- 数据模型 `src/api/shop/shopGoods/model/index.ts`:新增 `isTop?: number`(置顶 1/不置顶 0
- 列表 `src/views/shop/shopGoods/index.vue`
- 新增「置顶」列,列表内联一键切换(复用 recommend 的 CheckOutlined/CloseOutlined 模式),新增 `onTop` 处理函数。
- 「排序号」列开启 `sorter: true`(之前只展示、不可排序)。
- 后端迁移脚本 `add-is-top-shop-goods.sql`:新增 `is_top` TINYINT 字段;分页默认排序需改为 `ORDER BY is_top DESC, sort_number ASC, create_time DESC`(后端不在本仓库,需手动改 page 接口 ORDER BY / 放开 orders 白名单)。
- 注意:置顶真正"排到最前"依赖后端默认排序改动;点击列排序也需后端放开 is_top / sort_number。
## 后端改动(/Users/gxwebsoft/JAVA/guilixu-javaSpring 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` 编译通过。

View File

@@ -0,0 +1,16 @@
# 2026-07-25 工作日志
## CMS 模块复用方案分析mp-java vs guilixu-java
- 结论:**guilixu 应直接调用 mp-java 的 cmsmodules 服务),不要整体复制到 guilixu-java。**
- 证据guilixu 的 152 个 cms 类是 mp-java 235 类的子集分叉且落后 1.5 个月;前端 cms 接口已全部走 `MODULES_API_URL`(指向 mp-javaguilixu-java 的 cms 包是死代码;"整体复制"因缺 `project` 模块无法编译。
- 产出报告:`docs/cms-module-reuse-analysis.md`
- 已同步架构结论到 MEMORY.mdJava 后端架构 / 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 已起)。

View File

@@ -0,0 +1,24 @@
# 2026-07-28
## 订单列表操作列分隔线对齐 shopGoods
- 文件:`src/views/shop/shopOrder/index.vue``column.key === 'action'` 操作列模板。
- 改动:原本操作列按钮只用 `<a-space>` 包裹、无分隔线;现按 `shop/shopGoods` 的写法,在「详情」与各条件块之间、以及块内多个按钮之间加入 `<a-divider type="vertical" />`
- 分隔线放在各 `<template v-if>` 条件块内部,仅当对应操作出现时才渲染,避免多余竖线。
## 订单列表商品明细展开/收起(方案一已落地)
- 文件:`src/views/shop/shopOrder/index.vue`
- 需求:商品明细较多时列表行被撑高,加「展开/收起」。采用方案一(单元格内展开),已确认执行。
- 实现:
- 新增 `expandedOrderIds: ref<Set<string>>`(用 `String(record.orderId)` 作 key因 orderId 为 number|undefined
- 新增 `collapsedGoodsCount = 2``isOrderGoodsExpanded(record)``toggleOrderGoodsExpanded(record)`
- 改写 `column.key === 'orderGoods'` 模板:默认 `record.orderGoods.slice(0, 2)`,超过 2 件显示「展开全部 N 件」,点击切换,展开后文案变「收起」。
- 类型检查:`vue-tsc --noEmit` 仅有的 3 处报错line 2 `$router`/page-header、line 696/734 未用变量)均为项目既有,与本次改动无关。
## shop/dashboard 运行天数改为取租户创建时间
- 文件:`src/views/shop/dashboard/index.vue`
- 需求:/dashboard 的"运行天数"要改用「租户创建时间」而不是订阅生效时间。
- 现状调查:
- `cms/dashboard`(站点控制台,标题"系统运行天数")已在提交 894592c 改为 `getTenantInfo().createTime`(租户创建时间),已是目标行为。
- `shop/dashboard`(商城后台,标签"运行天数")原用 `currentSubscription.startTime`(订阅生效时间)。
- 改动:将 shop/dashboard 的 `runDays` computed 改为取 `tenantStore.company?.createTime`loadData 已 `tenantStore.fetchTenantInfo()` 写入 company。同步更新顶部数据来源注释。
- 数据源说明:`getTenantInfo()` 返回 `Company`,其 `createTime` 字段即租户创建时间(`src/api/system/company/model/index.ts` line 164`site.ts``runDays` getter 仍用 `siteInfo.createTime`(站点创建时间),目前 cms/dashboard 已不依赖它。

View File

@@ -0,0 +1,16 @@
# 2026-07-29
## websopy-javacms 模块数据源路由修复(重要跨项目)
- 报错:`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 事务不在一个事务里,写一致性需后续评估。

View File

@@ -0,0 +1,27 @@
# 2026-08-01 工作记录
## 订单自动取消改造:超时时间改为后台「订单设置」驱动
### 需求
自动取消未付款订单的超时时间,需由管理后台「商城设置 → 订单设置 → 未付款自动关闭」控制;设置为 0 分钟 = 不自动取消。
### 改造要点(已全部落地)
- 后台设置存于 `ShopSetting``setting_key='shop_order'` 的 JSON 中,字段 `orderAutoCloseMins`(前端 order.vue 已支持保存)。
- 新增 `ShopSettingService.getOrderAutoCloseMinutes()`:参照 `getSalesAccumulateType()` 写法,从 `getCategoryValues("order")``orderAutoCloseMins`0=禁用(合法返回);负数/读取失败兜底 30常量 `DEFAULT_AUTO_CLOSE_MINUTES=30`)。
- `OrderAutoCancelTask.cancelExpiredOrders()`:先读 DB`==0` 直接 return全局禁用含租户特殊配置 tenantConfigs 一起跳过);`>0` 用它替换原先 YAML 的 `defaultTimeoutMinutes` 传给 `findExpiredUnpaidOrders`
- `OrderConfigProperties.AutoCancel.defaultTimeoutMinutes``@Deprecated`,仅作兜底。
- 前端 `order.vue``orderAutoCloseMins``min` 由 1 改 0placeholder/文案/校验提示改为「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 / 取消逻辑 / 库存回退 / 优惠券退还 / 销量回滚 全部未动。

View File

@@ -0,0 +1,42 @@
# 项目长期记忆
## 项目结构
### 关键项目路径
- **管理后台**`/Users/gxwebsoft/VUE/guilixu-admin`Vue3 + Ant Design Vue
- **用户端小程序**`/Users/gxwebsoft/VUE/guilixu-taro`Taro + React
- **商家端小程序**`/Users/gxwebsoft/VUE/xinlong-shop-taro`Taro + React
### ⚠️ 跨项目教训(重要)
- 商家端发货/门店/订单相关功能在 **xinlong-shop-taro**,不要改到 guilixu-taro
- 用户端订单详情等功能在 **guilixu-taro**
- 跨项目改动前,先用 grep/glob 在多个候选项目里确认"哪个项目有该功能源码",再动手
- 已发生两次误改 guilixu-taro 的教训useClerk 全局上下文、订单详情 deliveryType 修复)
## 业务约定
### deliveryType 配送方式枚举
- `0` = 快递配送
- `1` = 无需发货 / 自提
- `2` = 商家送货
来源:管理后台 `src/views/shop/shopOrder/components/deliveryModal.vue`
## Java 后端架构(重要)
### 双后端 / MODULES_API_URL 模式2026-07-25 分析结论)
- **mp-java**`/Users/gxwebsoft/JAVA/mp-java`= 独立的「modules 服务」,用 `modules`cms(235类)、shop 等业务模块都在它这里,持续高频开发。
- **guilixu-java**`/Users/gxwebsoft/JAVA/guilixu-java`= 桂礼序主后端,用 `db_guilixu` 库;是 mp-java 的「子集分叉」。
- **guilixu-admin 前端已预设双基地址**`SERVER_API_URL`(主后端) + `MODULES_API_URL`(localStorage ApiUrl / VITE_API_URL)。shop 商户/优惠券、cms 全部接口**都已走 MODULES_API_URL**,即前端本就假定 cms/shop 由 mp-java(modules 服务)提供。
- **结论guilixu-java 里的 cms 包(152类)是 mp-java cms 的旧分叉子集、且前端并不调它(前端调 modules 服务),属重复/死代码,应删除而非复制或扩展。**
- 复制「整个 cms 到 guilixu-java」不可行mp-java 完整 cms 的 CmsApp/CmsWebsiteService 依赖 `project` 模块guilixu-java 没有),会编译失败。
- 注意mp-java 与 guilixu-java 是独立 git 仓库(`git.websoft.top/gxwebsoft/mp-java.git``guilixu-java.git`),各自独立部署、独立库。
### shopGoods status 字段(上架/下架)
- `0` = 已上架 / 上架
- `1` = 待上架 / 下架
- `2` = 待审核,`3` = 审核不通过
- 编辑表单「状态」radio 绑定 `form.status`0=上架、1=下架。
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
- 列表 tag 仍显示0=已上架、1=待上架、2=待审核、3=审核不通过。

147
README.md
View File

@@ -1,84 +1,89 @@
<div align="center">
<h1>🌐 WebSoftAdmin</h1>
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级后台管理系统</strong></p>
<h1>🛒 小程序商城后台管理系统</h1>
<p><strong>基于 Vue 3 + Ant Design Vue 的企业级小程序商城后台</strong></p>
<p>
<img src="https://img.shields.io/badge/Vue-3.x-4FC08D" alt="Vue">
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-3.x-1890FF" alt="Ant Design Vue">
<img src="https://img.shields.io/badge/TypeScript-4.x-3178C6" alt="TypeScript">
<img src="https://img.shields.io/badge/Vite-4.x-646CFF" alt="Vite">
<img src="https://img.shields.io/badge/Ant%20Design%20Vue-4.x-1890FF" alt="Ant Design Vue">
<img src="https://img.shields.io/badge/TypeScript-5.x-3178C6" alt="TypeScript">
<img src="https://img.shields.io/badge/Vite-8.x-646CFF" alt="Vite">
<img src="https://img.shields.io/badge/License-MIT-blue" alt="License">
</p>
</div>
## 📖 项目简介
WebSoftAdmin 是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级后台管理系统,采用最新的前端技术栈:
小程序商城后台管理系统是一个基于 **Vue 3 + Ant Design Vue** 构建的现代化企业级商城管理平台,为小程序商城提供完整的后台管理能力,涵盖商品、订单、会员、经销商、门店、营销活动等核心业务模块。
- **前端框架**Vue 3 + TypeScript + Vite
- **UI 组件库**Ant Design Vue 3.x
- **富文本编辑器**TinyMCE支持图片/视频上传、一键排版)
- **图表库**ECharts + G2
- **工具库**Lodash、Day.js、CryptoJS
## 项目演示
| 后台管理系统 | https://mp.websoft.top |
|--------|-------------------------------------------------------------------------------------------------------------------------------------|
| 账号密码 | [立即注册](https://mp.websoft.top/register/?inviteCode=github) |
| 关注公众号 | ![输入图片说明](https://oss.wsdns.cn/20240327/f1175cc5aae741d3af05484747270bd5.jpeg?x-oss-process=image/resize,m_fixed,w_150/quality,Q_90) |
- **UI 组件库**Ant Design Vue 4.x + EleAdmin Pro
- **状态管理**Pinia
- **图表库**ECharts + @antv/g2
- **工具库**Lodash、Day.js、CryptoJS、ExcelJS
**项目域名**[https://shop.websoft.top](https://shop.websoft.top)
**官方产品介绍**[https://websoft.top/shop](https://websoft.top/shop)
## 🛠️ 技术栈
### 核心技术
| 技术 | 版本 | 说明 |
|------|------|------|
| Vue | 3.x | 渐进式 JavaScript 框架 |
| TypeScript | 4.x | JavaScript 的超集 |
| Vite | 4.x | 下一代前端构建工具 |
| Ant Design Vue | 3.2.11 | 企业级 UI 设计语言 |
| EleAdmin Pro | 1.10.1 | 企业级组件库 |
| TypeScript | 5.x | JavaScript 的超集 |
| Vite | 8.x | 下一代前端构建工具 |
| Ant Design Vue | 4.x | 企业级 UI 设计语言 |
| EleAdmin Pro | 1.11+ | 企业级组件库 |
| Pinia | 2.x | Vue 3 状态管理库 |
### 功能组件
- **TinyMCE** - 富文本编辑器,支持图片/视频上传
- **ECharts** - 数据可视化图表库
- **CropperJS** - 图片裁剪组件
- **ExcelJS** - Excel 文件处理
- **ExcelJS / xlsx** - Excel 文件导入导出
- **Ali OSS** - 阿里云对象存储
- **qrcode / jsbarcode** - 二维码与条形码生成
## 📋 环境要求
### 基础环境
- 🟢 **Node.js 16+**
- 📦 **npm 8+ / yarn 1.22+**
- 🟢 **Node.js 18+**
- 📦 **npm 8+ / yarn 1.22+ / pnpm 8+**
- 🌐 **现代浏览器**Chrome 63+、Firefox、Safari、Edge
### 开发工具
- **推荐**VS Code / WebStorm
- **插件**Vetur / VolarVue 3 支持)
- **插件**VolarVue 3 支持)
## 🚀 快速开始
### 1. 克隆项目
```bash
git clone https://github.com/websoft-top/paopao-vue.git
cd paopao-vue
git clone <仓库地址>
cd guilixu-admin
```
### 2. 安装依赖
```bash
# 使用 npm
npm install
# 或使用 yarn
yarn install
# 或使用 pnpm推荐
pnpm install
```
### 3. 配置环境变量
```bash
# 复制环境变量示例文件
cp .env.example .env
@@ -88,6 +93,7 @@ cp .env.example .env
```
### 4. 启动开发服务器
```bash
# 开发模式
npm run dev
@@ -99,6 +105,7 @@ yarn dev
访问 `http://localhost:3000` 即可看到管理后台。
### 5. 构建生产版本
```bash
# 生产构建
npm run build
@@ -112,6 +119,7 @@ npm run serve
项目使用环境变量来管理敏感信息和配置。请按照以下步骤配置:
### 必需配置
```bash
# API 配置
VITE_API_URL=https://your-api.com/api # 后端 API 地址
@@ -124,51 +132,54 @@ VITE_TENANT_ID=your_tenant_id # 租户 ID
```
### 可选配置
```bash
# 高德地图 (如需使用地图功能)
VITE_MAP_KEY=your_map_key # 高德地图 Key
VITE_MAP_CODE=your_map_security_code # 高德地图安全密钥
# WebSoftAdmin 授权 (商业版功能)
# EleAdmin Pro 授权 (商业版功能)
VITE_LICENSE_CODE=your_license_code # 授权码
```
### 获取配置信息
- **高德地图密钥**:访问 [高德开放平台](https://lbs.amap.com/) 申请
- **WebSoftAdmin 授权码**:联系 [官方网站](https://websoft.top/) 获取
- **其他 API 配置**:根据您的后端服务配置
## 🎯 核心功能
### 📝 内容管理系统
- **文章管理**:支持富文本编辑、图片/视频上传
- **一键排版**智能文章格式优化包含10种专业排版样式
- **媒体库**:图片/视频文件管理,支持分组和搜索
- **首行缩进**:中文段落格式智能切换
### 🛒 商城管理
- **商品管理**:商品信息编辑、规格设置
- **订单管理**订单流程跟踪
- **商品管理**商品信息编辑、规格设置、SKU 管理、商品分类
- **订单管理**:订单流程跟踪、订单商品管理
- **库存管理**:商品库存监控
- **优惠券管理**:优惠券创建与发放、用户优惠券管理
- **营销活动**:活动管理、活动报名、积分商品
- **礼品管理**:商城礼品配置
### 👥 用户权限
- **用户管理**:用户信息维护
- **角色权限**:基于角色的访问控制
### 🏪 门店与经销商
- **门店管理**:门店信息、门店围栏、门店骑手、门店仓库
- **经销商体系**:经销商申请、经销商用户、经销商订单、经销商资金与提现
- **分销设置**:经销商推荐人、分销海报、分销配置
### 👥 会员管理
- **会员注册**:会员注册管理
- **会员权益**:权益包管理、权益兑换
- **会员推荐**:推荐人关系管理
### 👤 用户权限
- **用户管理**:用户信息维护、用户分组
- **角色权限**基于角色的访问控制RBAC
- **菜单管理**:动态菜单配置
- **组织架构**:组织与部门管理
## 🎨 富文本编辑器特色功能
### ⚙️ 系统管理
### 📸 媒体上传
- **图片上传**:支持拖拽、粘贴、文件选择
- **视频上传**:支持多种视频格式
- **媒体库**:统一的媒体文件管
- **OSS 存储**:阿里云对象存储集成
### ✨ 智能排版
- **一键排版**10种专业排版优化
- **首行缩进**:中文段落格式切换
- **样式优化**:标题、段落、列表、表格等元素美化
- **响应式**:适配不同屏幕尺寸
- **系统配置**:商城设置、提现配置
- **支付管理**:支付配置、订单支付
- **字典管理**:数据字典维护
- **缓存管理**:缓存清
- **域名管理**:域名白名单配置
- **操作记录**:登录记录、操作日志
## 🏗️ 项目结构
@@ -176,11 +187,17 @@ VITE_LICENSE_CODE=your_license_code # 授权码
src/
├── components/ # 公共组件
├── views/ # 页面组件
│ ├── cms/ # 内容管理
│ ├── shop/ # 商城管理
── system/ # 系统管理
├── router/ # 路由配置
├── store/ # 状态管理
├── utils/ # 工具函数
── assets/ # 静态资源
│ ├── shop/ # 商城管理(商品、订单、门店、经销商、会员等)
│ ├── system/ # 系统管理(用户、角色、菜单、字典、支付等)
── passport/ # 登录认证
│ ├── user/ # 用户中心
│ └── result/ # 结果页
├── router/ # 路由配置
── store/ # Pinia 状态管理
├── utils/ # 工具函数
└── assets/ # 静态资源
```
## 📄 License
[MIT](LICENSE)

35
add-is-top-shop-goods.sql Normal file
View File

@@ -0,0 +1,35 @@
-- ============================================================
-- 商品表新增「置顶」字段 is_top并调整默认排序方案 B
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin
-- 1. is_top = 1 表示置顶0默认表示不置顶
-- 2. 表名/字段名 snake_case 约定Node + TypeORM 常见);
-- 若你的库列名是驼峰goodsId / isTop请自行替换
-- 3. 与 recommend推荐同为「列表内联一键切换」的布尔开关
-- 4. 混合排序思路:置顶的永远在最前;置顶内部按排序号升序;
-- 其余商品按创建时间倒序(绝大多数商品无需手动排)
-- ============================================================
-- 第一步新增字段已存在则跳过MySQL 5.7 / 8 通用)
ALTER TABLE shop_goods
ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶1置顶 0不置顶';
-- (可选)联合索引,置顶 + 排序号分页查询更稳
-- ALTER TABLE shop_goods
-- ADD INDEX idx_is_top_sort (is_top, sort_number);
-- 第二步:调整分页查询(page 接口)默认排序
-- 原示例ORDER BY create_time DESC
-- 改: ORDER BY is_top DESC, sort_number ASC, create_time DESC
-- 说明:若后端排序字段来自前端 orders 参数,需放开 is_top / sort_number 参与排序
-- (前端「置顶」「排序号」列已开启 sorter会向后端传 orders
-- 第三步(验证):确认字段已加、数据默认 0且排序符合预期
SELECT goods_id,
name,
is_top,
sort_number,
create_time
FROM shop_goods
ORDER BY is_top DESC, sort_number ASC, create_time DESC
LIMIT 20;

View File

@@ -0,0 +1,73 @@
# 桂礼序 CMS 模块复用方案分析
> 分析对象:`mp-java`modules 服务cms 235 类) vs `guilixu-java`桂礼序主后端cms 152 类)
> 分析日期2026-07-25
## 一、现状事实(已核实)
| 维度 | mp-java | guilixu-java |
|------|---------|--------------|
| 形态 | 单体 Spring Boot 应用(单 pom@MapperScan `com.gxwebsoft.**` | 同左 |
| cms 类数 | **235**controller 37 | **152**controller 25 |
| cms 数据库 | `modules` 库 | `db_guilixu`prod/dev`modules`test |
| cms 建表/迁移 | 由 `src/main/java/com/gxwebsoft/cms/sql/` + `docs/sql` 管理,高频更新 | `sql/` 目录**无 cms 建表脚本** |
| git 引入时间 | 2025-07-24"2.0版本分离"),持续开发 | 2026-06-01"桂礼序"),已落后 ~1.5 个月 |
| 最近提交 | 2026-07-24/25仍在改 cms | 最近为 shop/order 相关cms 停滞 |
**关键证据:**
1. **guilixu-java 的 152 个 cms 类,全部能在 mp-java 的 cms 里找到同名类**mp-java 额外多出 83 个CmsApp / CmsBanner / CmsCase 等系列)。→ guilixu 的 cms 是 mp-java cms 的**子集分叉**。
2. **前端guilixu-admin的 cms 接口早已使用 `MODULES_API_URL`**,路径如 `MODULES_API_URL + '/cms/cms-ad/page'``MODULES_API_URL` = `localStorage.ApiUrl || VITE_API_URL`,即指向 modules 服务mp-java。shop 的商户/优惠券接口同样走 `MODULES_API_URL`。→ **前端本就假定 cms/shop 由 mp-java 提供guilixu-java 自己的 cms 包前端并不调用(重复/死代码)。**
3. **mp-java 与 guilixu-java 的 cms 控制器路径完全一致**`/api/cms/cms-ad``/api/cms/cms-article-like` …),说明两者是同一套接口契约。
4. **完整复制不可编译**mp-java 完整 cms 的 `CmsApp*`/`CmsWebsiteService*` 依赖 `com.gxwebsoft.project`Project/ProjectService而 guilixu-java **没有 project 模块**。把 235 类整体搬过去会编译失败。
## 二、方案对比
### 方案 Aguilixu 直接调用 mp-java 的 cmsmodules 服务作为 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 微服务 + 客户端 SDKmp-java 与 guilixu-java 都依赖。能根治重复与分叉,但需重构打包、版本发布流程,短期落地成本高于 A。
## 三、结论与落地建议
**结论:选方案 Aguilixu 直接调用 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-javamodules 库)集中管理,远程调用天然复用。
**落地步骤建议:**
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` + 长期同步机制,综合成本最高,除非有强隔离需求否则不推荐。

View File

@@ -0,0 +1,37 @@
-- ============================================================
-- 下架「分类ID = 10332 且 没有图片」的商品
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin
-- 1. 下架 = status = 1项目约定0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio
-- 2. 「没有图片」以封面图 image 是否为空判断(封面图为必填项)
-- 3. 已删除商品deleted = 1不参与避免误改
-- 4. 表名/字段名按 snake_case 约定Node + TypeORM 常见);
-- 若你的库列名是驼峰goodsId / categoryId请自行替换
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
-- ============================================================
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE
SELECT goods_id,
name,
image,
status
FROM shop_goods
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- 第二步:确认无误后执行下架
UPDATE shop_goods
SET status = 1,
update_time = NOW()
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- ------------------------------------------------------------
-- (可选)若你认为「没有图片」应同时要求轮播图也为空,
-- 即封面图和轮播图都没有才算没图,使用下面更严格的条件:
-- AND (image IS NULL OR image = '')
-- AND (files IS NULL OR files = '' OR files = '[]')
-- ------------------------------------------------------------

View File

@@ -0,0 +1,38 @@
-- ============================================================
-- 批量上架「分类ID = 10332 且 含有图片」的商品
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin
-- 1. 上架 = status = 0项目约定0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio
-- 2. 「含有图片」以封面图 image 是否非空判断image IS NOT NULL AND image <> ''
-- 3. 已删除商品deleted = 1不参与避免误改
-- 4. 表名/字段名按 snake_case 约定Node + TypeORM 常见);
-- 若你的库列名是驼峰goodsId / categoryId请自行替换
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
-- ============================================================
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE
SELECT goods_id,
name,
image,
status
FROM shop_goods
WHERE category_id = 10332
AND image IS NOT NULL
AND image <> ''
AND (deleted = 0 OR deleted IS NULL);
-- 第二步:确认无误后执行上架
UPDATE shop_goods
SET status = 0,
update_time = NOW()
WHERE category_id = 10332
AND image IS NOT NULL
AND image <> ''
AND (deleted = 0 OR deleted IS NULL);
-- ------------------------------------------------------------
-- (可选)若只想把「当前已下架」的商品重新上架,
-- 而不动「待审核(2) / 审核不通过(3)」的商品,加上 status 条件:
-- AND status = 1
-- ------------------------------------------------------------

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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')
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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: {

View File

@@ -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;

View File

@@ -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 }
}

View File

@@ -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));
}
/**
* 创建完整网站并初始化
*/

View File

@@ -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;
}
/**
* 通知数据格式
*/

View File

@@ -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));
}
/**
* 登录
*/

View File

@@ -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 };
/**
* 图形验证码返回结果
*/

View File

@@ -44,9 +44,9 @@ export interface ShopGoods {
commissionRole?: number;
// 货架
position?: string;
// 套餐价格
// 进货价格
buyingPrice?: string;
// 商品价格
// 商城价
price?: string;
originPrice?: string;
// 销售价格
@@ -87,6 +87,8 @@ export interface ShopGoods {
gainIntegral?: number;
// 推荐
recommend?: number;
// 置顶1置顶 0不置顶
isTop?: number;
// 商户ID
merchantId?: number;
// 商户名称

View File

@@ -21,7 +21,7 @@ export interface ShopGoodsBrowse {
goodsName?: string;
/** 商品封面图(关联查询) */
goodsImage?: string;
/** 商品价格(关联查询) */
/** 商城价(关联查询) */
price?: string;
/** 市场价(关联查询) */
salePrice?: string;

View File

@@ -12,7 +12,7 @@ export interface ShopGoodsSku {
sku?: string;
// 商品图片
image?: string;
// 商品价格
// 商城价
price?: string;
// 市场价格
salePrice?: string;

View File

@@ -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));
}

View File

@@ -111,6 +111,14 @@ export interface ShopOrder {
sendPhone?: string;
// 发货地址
sendAddress?: string;
// 发货门店id快递配送=发货仓库,商家送货=配送门店)
sendStoreId?: number;
// 配送店员id商家送货时记录具体配送人员
deliveryUserId?: number;
// 发货门店名称(后端联表返回)
sendStoreName?: string;
// 配送店员名称(后端联表返回)
deliveryUserName?: string;
// 优惠类型0无、1抵扣优惠券、2折扣优惠券、3、VIP月卡、4VIP年卡5VIP次卡、6VIP会员卡、7IC月卡、8IC年卡、9IC次卡、10IC会员卡、11免费订单、12VIP充值卡、13IC充值卡、14VIP季卡、15IC季卡
couponType?: number;
// 优惠说明

View File

@@ -77,6 +77,8 @@ export interface ShopSetting {
payAlipayPrivateKey?: string;
payAlipayPublicKey?: string;
payBalanceEnabled?: boolean | number;
salesAccumulateType?: number;
}
/**

View File

@@ -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) {

View File

@@ -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'];

View File

@@ -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'
}
};

View File

@@ -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: '立即发送'
}
};

View File

@@ -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'),

View File

@@ -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,8 +69,11 @@ export function ensureFullUrl(url: string): string {
*
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
*
* 优先级:以调用方指定的 width/quality 为准。若 URL 已自带 x-oss-process
* (接口已返回压缩参数),会先剥离再按本函数参数重新拼接,确保调用方参数生效。
*
* @param url 原始图片 URL
* @param options 压缩选项
* @param options 压缩选项width/quality 不传则使用默认值)
* @returns 处理后的图片 URL
*/
export function getCompressedImageUrl(
@@ -66,11 +89,12 @@ export function getCompressedImageUrl(
// 未启用压缩,原样返回
if (!enabled) 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}`;
return `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
}

View File

@@ -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) {

View File

@@ -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);

View File

@@ -0,0 +1,621 @@
<template>
<div
class="login-wrapper"
:style="{
backgroundImage: 'url(https://oss.wsdns.cn/20250105/314d2a3da10048b09ef0f0464fdacbff.jpg)'
}"
>
<div class="logo-login" v-if="config?.siteName">
<img :src="config.siteLogo" class="logo" />
<h4>{{ config.siteName }}</h4>
</div>
<a-form
ref="formRef"
:model="form"
:rules="rules"
class="login-form ele-bg-white"
>
<div class="login-title flex justify-center items-center px-12">
<h4 class="title-btn active">{{ t('login.forgetPage.title') }}</h4>
</div>
<p class="forget-subtitle">{{ t('login.forgetPage.subTitle') }}</p>
<a-tabs
v-model:activeKey="activeTab"
class="forget-tabs"
@change="onTabChange"
>
<!-- 邮箱找回 -->
<a-tab-pane key="email" :tab="t('login.forgetPage.tabEmail')">
<a-form-item name="email">
<a-input
allow-clear
size="large"
v-model:value="form.email"
:placeholder="t('login.forgetPage.emailPlaceholder')"
>
<template #prefix>
<MailOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item name="emailCode">
<div class="login-input-group">
<a-input
allow-clear
size="large"
:maxlength="6"
v-model:value="form.emailCode"
:placeholder="t('login.forgetPage.codePlaceholder')"
>
<template #prefix>
<SafetyOutlined />
</template>
</a-input>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="sendCode"
>
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
<span v-else>{{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
</a-tab-pane>
<!-- 手机找回 -->
<a-tab-pane key="phone" :tab="t('login.forgetPage.tabPhone')">
<a-form-item name="phone">
<a-input
allow-clear
size="large"
:maxlength="11"
v-model:value="form.phone"
:placeholder="t('login.forgetPage.phonePlaceholder')"
>
<template #prefix>
<MobileOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item name="code">
<div class="login-input-group">
<a-input
allow-clear
size="large"
:maxlength="6"
v-model:value="form.code"
:placeholder="t('login.forgetPage.codePlaceholder')"
>
<template #prefix>
<SafetyOutlined />
</template>
</a-input>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="sendCode"
>
<span v-if="!countdownTime">{{ t('login.forgetPage.getCode') }}</span>
<span v-else>{{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
</a-tab-pane>
</a-tabs>
<a-form-item name="newPassword">
<a-input-password
size="large"
v-model:value="form.newPassword"
:placeholder="t('login.forgetPage.passwordPlaceholder')"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item name="confirmPassword">
<a-input-password
size="large"
v-model:value="form.confirmPassword"
:placeholder="t('login.forgetPage.confirmPlaceholder')"
>
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
block
size="large"
type="primary"
:loading="loading"
@click="submit"
>
{{ loading ? t('login.loading') : t('login.forgetPage.reset') }}
</a-button>
</a-form-item>
<div class="text-center pt-2">
<a class="login-forget" @click="goLogin">{{ t('login.forgetPage.backLogin') }}</a>
</div>
</a-form>
<!-- 图形验证码弹窗 -->
<a-modal
:width="340"
:footer="null"
:title="t('login.forgetPage.captchaTitle')"
v-model:visible="captchaVisible"
@cancel="codeLoading = false"
>
<div class="login-input-group" style="margin-bottom: 16px">
<a-input
v-model:value="imgCode"
:maxlength="5"
size="large"
:placeholder="t('login.forgetPage.captchaPlaceholder')"
allow-clear
@pressEnter="() => doSendCode(imgCode)"
/>
<a-button class="login-captcha">
<img alt="" :src="captchaImage" @click="refreshCaptcha" />
</a-button>
</div>
<a-button
block
size="large"
type="primary"
:loading="codeLoading"
@click="() => doSendCode(imgCode)"
>
{{ t('login.forgetPage.sendNow') }}
</a-button>
</a-modal>
<div class="login-copyright">
<a-space>
<span>© {{ new Date().getFullYear() }}</span>
<span>{{ config?.copyright || 'websoft.top Inc.' }}</span>
</a-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import {
LockOutlined,
MobileOutlined,
SafetyOutlined,
MailOutlined
} from '@ant-design/icons-vue';
import { FormInstance } from 'ant-design-vue/es/form';
import {
sendSmsCaptchaByAdmin,
sendEmailCaptcha,
getCaptcha
} from '@/api/passport/login';
import { resetPassword } from '@/api/layout';
import useFormData from '@/utils/use-form-data';
import { Config } from '@/api/cms/cmsWebsiteField/model';
const { t } = useI18n();
const router = useRouter();
// 默认站点配置(与登录页保持一致)
const defaultConfig: Config = {
siteName: '小程序商城',
siteLogo: 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg',
domain: '',
icpNo: '',
copyright: '',
loginBgImg: '',
address: '',
tel: '',
kefu2: '',
kefu1: '',
email: '',
loginTitle: '',
sysLogo: ''
};
const config = ref<Config>({ ...defaultConfig });
// 找回方式email(邮箱) / phone(手机),默认邮箱
const activeTab = ref<'email' | 'phone'>('email');
const { form } = useFormData<{
email: string;
emailCode: string;
phone: string;
code: string;
newPassword: string;
confirmPassword: string;
}>({
email: '',
emailCode: '',
phone: '',
code: '',
newPassword: '',
confirmPassword: ''
});
const formRef = ref<FormInstance | null>(null);
const loading = ref(false);
// 验证码倒计时
const countdownTime = ref(0);
let countdownTimer: number | null = null;
// 图形验证码弹窗
const captchaVisible = ref(false);
const captchaImage = ref('');
const captchaText = ref('');
const imgCode = ref('');
const codeLoading = ref(false);
// 同一个账号前 3 次发送不需要图形验证码
const FREE_SEND_TIMES = 3;
// 根据当前找回方式和账号生成发送次数缓存 key
const getSendCountKey = () => {
const account = activeTab.value === 'email' ? form.email : form.phone;
return `forget_send_count_${activeTab.value}_${account}`;
};
// 读取当前账号已发送次数
const getSendCount = () => {
const key = getSendCountKey();
if (!key) return 0;
const count = localStorage.getItem(key);
return count ? Number(count) || 0 : 0;
};
// 增加发送次数
const increaseSendCount = () => {
const key = getSendCountKey();
if (!key) return;
localStorage.setItem(key, String(getSendCount() + 1));
};
// 刷新图形验证码
const refreshCaptcha = () => {
getCaptcha()
.then((data) => {
captchaImage.value = data.base64;
captchaText.value = data.text;
})
.catch((e: Error) => message.error(e.message));
};
// 打开图形验证码弹窗
const openCaptchaModal = () => {
imgCode.value = '';
refreshCaptcha();
captchaVisible.value = true;
};
// 密码强度至少8位且包含字母和数字
const passwordRules = [
{
required: true,
message: t('login.forgetPage.newPasswordRule'),
trigger: 'blur'
},
{ min: 8, message: t('login.forgetPage.newPasswordRule'), trigger: 'blur' },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*#?&]{8,}$/,
message: t('login.forgetPage.newPasswordRule'),
trigger: 'blur'
}
];
const confirmPasswordRules = [
{
required: true,
message: t('login.forgetPage.confirmRule'),
trigger: 'blur'
},
{
validator: (_rule: unknown, value: string) =>
value === form.newPassword
? Promise.resolve()
: Promise.reject(new Error(t('login.forgetPage.confirmRule'))),
trigger: 'blur'
}
];
// 根据当前找回方式动态生成校验规则
const rules = computed(() => {
if (activeTab.value === 'email') {
return {
email: [
{ required: true, message: t('login.forgetPage.emailRule'), trigger: 'blur' },
{ type: 'email', message: t('login.forgetPage.emailRule'), trigger: 'blur' }
],
emailCode: [
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
],
newPassword: passwordRules,
confirmPassword: confirmPasswordRules
};
}
return {
phone: [
{ required: true, message: t('login.forgetPage.phoneRule'), trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: t('login.forgetPage.phoneRule'), trigger: 'blur' }
],
code: [
{ required: true, message: t('login.forgetPage.codeRule'), trigger: 'blur' }
],
newPassword: passwordRules,
confirmPassword: confirmPasswordRules
};
});
const onTabChange = () => {
formRef.value?.clearValidate();
};
// 发送验证码(邮箱/手机)
const sendCode = () => {
if (activeTab.value === 'email') {
if (!form.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
message.error(t('login.forgetPage.emailRule'));
return;
}
} else {
if (!form.phone || !/^1\d{10}$/.test(form.phone)) {
message.error(t('login.forgetPage.phoneRule'));
return;
}
}
if (getSendCount() >= FREE_SEND_TIMES) {
openCaptchaModal();
} else {
doSendCode();
}
};
// 真正调用接口发送验证码
const doSendCode = (captchaCode?: string) => {
if (captchaCode !== undefined) {
if (!captchaCode) {
message.error('请输入图形验证码');
return;
}
if (captchaText.value !== captchaCode.toLowerCase()) {
message.error('图形验证码不正确');
return;
}
}
codeLoading.value = true;
const sendPromise =
activeTab.value === 'email'
? sendEmailCaptcha({ email: form.email }, { skipTenant: true })
: sendSmsCaptchaByAdmin({ phone: form.phone }, { skipTenant: true });
sendPromise
.then(() => {
message.success(
activeTab.value === 'email'
? t('login.forgetPage.sentEmail')
: t('login.forgetPage.getCode')
);
captchaVisible.value = false;
increaseSendCount();
startCountdown();
})
.catch((e: Error) => {
message.error(e.message);
refreshCaptcha();
})
.finally(() => {
codeLoading.value = false;
});
};
const startCountdown = () => {
countdownTime.value = 60;
countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer);
countdownTimer = null;
}
countdownTime.value--;
}, 1000);
};
const goLogin = () => {
router.push('/login');
};
// 提交重置
const submit = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const params: Record<string, unknown> = {
newPassword: form.newPassword,
confirmPassword: form.confirmPassword
};
if (activeTab.value === 'email') {
params.email = form.email;
params.emailCode = form.emailCode;
} else {
params.phone = form.phone;
params.smsCode = form.code;
}
resetPassword(params as any)
.then((msg) => {
message.success(msg || t('login.forgetPage.success'));
loading.value = false;
router.push('/login');
})
.catch((e: Error) => {
message.error(e.message);
loading.value = false;
});
})
.catch(() => {});
};
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer);
});
</script>
<style lang="less" scoped>
.login-wrapper {
padding: 48px 16px 0 16px;
position: relative;
box-sizing: border-box;
background-color: var(--grey-5);
background-repeat: no-repeat;
background-size: cover;
min-height: 100vh;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.2);
}
}
.forget-subtitle {
text-align: center;
color: #8c8c8c;
margin: -8px 0 16px;
font-size: 13px;
}
.login-form {
width: 380px;
margin: 0 auto;
max-width: 100%;
padding: 0 28px;
box-sizing: border-box;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
border-radius: 2px;
position: relative;
z-index: 2;
h4 {
padding: 22px 0;
text-align: center;
}
.title-btn {
cursor: pointer;
white-space: nowrap;
}
.login-title {
position: relative;
display: flex;
justify-content: space-around;
line-height: 50px;
.active {
color: #007dff;
}
}
}
.forget-tabs {
:deep(.ant-tabs-nav) {
margin-bottom: 20px;
}
}
.login-input-group {
display: flex;
align-items: center;
background-repeat: no-repeat;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
width: 102px;
height: 40px;
margin-left: 10px;
padding: 0;
}
}
.login-copyright {
color: #eee;
text-align: center;
padding: 48px 0 22px 0;
position: relative;
z-index: 1;
}
.logo-login {
position: absolute;
top: 20px;
left: 20px;
display: flex;
align-items: center;
cursor: pointer;
img {
width: 28px;
height: 28px;
border-radius: 4px;
}
h4 {
margin-left: 6px;
color: #ffffff;
font-size: 20px;
}
}
@media screen and (min-height: 640px) {
.login-wrapper {
padding-top: 0;
}
.login-form {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%);
margin-top: -260px;
}
.login-copyright {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
}
@media screen and (max-width: 768px) {
.login-form {
left: 50%;
right: auto;
margin-left: 0;
margin-right: auto;
transform: translateX(-50%);
}
}
</style>

View File

@@ -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;
}
}
}

View File

@@ -4,8 +4,7 @@
<div class="welcome-banner">
<div class="welcome-left flex flex-row gap-2 items-center">
<a-avatar
v-if="shopLogoUrl"
:src="shopLogoUrl"
:src="shopLogoUrl || 'https://oss.wsdns.cn/20240822/0252ad4ed46449cdafe12f8d3d96c2ea.svg'"
:size="64"
shape="square"
class="welcome-logo"
@@ -23,15 +22,12 @@
<a-tooltip title="开启后每60秒自动检测新订单检测到后播放叮声并语音播报">
<InfoCircleOutlined class="order-notify-info" />
</a-tooltip>
<a-button size="small" :disabled="!notifyEnabled" @click="onTestNotify">测试</a-button>
</div>
<a-space>
<a-tag color="green" style="font-size:13px;padding:4px 12px">{{ siteStore.statusText || '正常' }}</a-tag>
<!-- <a-button size="small" :disabled="!notifyEnabled" @click="onTestNotify">测试</a-button>-->
<a-button size="small" @click="refreshStatistics" :loading="loading">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
</div>
</div>
@@ -65,11 +61,11 @@
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
@@ -81,82 +77,63 @@
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
<!-- <div v-if="!loading && todoItems.every(t => t.value === 0)" class="todo-empty">-->
<!-- 🎉 暂无待处理事项一切正常-->
<!-- </div>-->
</div>
</div>
</a-col>
<!-- 快速入口 -->
<!-- 今日数据概况 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
<span class="panel-title">📊 今日数据概况</span>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">
{{ item.icon }}
<span v-if="item.badge" class="quick-icon-dot"></span>
<div class="today-stats">
<div class="today-stat">
<div class="today-stat-icon">💰</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ formatMoney(todayStats.salesAmount) }}</div>
<div class="today-stat-label">今日销售额</div>
</div>
</div>
<div class="quick-label">{{ item.label }}</div>
<div class="today-stat">
<div class="today-stat-icon">📦</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.orderCount || 0 }}</div>
<div class="today-stat-label">今日订单数</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">👥</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.newUsers || 0 }}</div>
<div class="today-stat-label">新增用户</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">🎁</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.couponUsed || 0 }}</div>
<div class="today-stat-label">使用优惠券</div>
</div>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- 今日数据趋势 -->
<div class="panel">
<div class="panel-header">
<span class="panel-title">📊 今日数据概况</span>
</div>
<div class="today-stats">
<div class="today-stat">
<div class="today-stat-icon">💰</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ formatMoney(todayStats.salesAmount) }}</div>
<div class="today-stat-label">今日销售额</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">📦</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.orderCount || 0 }}</div>
<div class="today-stat-label">今日订单数</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">👥</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.newUsers || 0 }}</div>
<div class="today-stat-label">新增用户</div>
</div>
</div>
<div class="today-stat">
<div class="today-stat-icon">🎁</div>
<div class="today-stat-info">
<div class="today-stat-value">{{ todayStats.couponUsed || 0 }}</div>
<div class="today-stat-label">使用优惠券</div>
</div>
</div>
</div>
</div>
<!-- 今日数据概况 + 基本信息 -->
<a-row :gutter="[16, 16]">
<!-- 系统基本信息 -->
<a-row :gutter="[16, 16]" style="margin-top: 16px">
<a-col :span="12">
<!-- 基本信息 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📋 基本信息</span>
</div>
<div style="padding: 16px 18px;">
<div style="padding: 23px 18px;">
<a-descriptions :column="1" size="small">
<a-descriptions-item label="软件名称">
<template v-if="subscriptionLoading">
@@ -164,16 +141,16 @@
</template>
<template v-else>{{ currentProduct?.productName || '-' }}</template>
</a-descriptions-item>
<a-descriptions-item label="版本">
<a-descriptions-item label="当前版本">
<template v-if="subscriptionLoading">
<a-skeleton-input :active="true" size="small" style="width: 80px" />
<a-skeleton-input :active="true" size="small" style="width: 160px" />
</template>
<template v-else>{{ currentProduct?.version || '-' }}</template>
</a-descriptions-item>
<a-descriptions-item label="运行状态">
<a-tag :color="productStatusText.color">{{ productStatusText.text }}</a-tag>
<a-descriptions-item label="授权编号">
<span>{{ currentSubscription?.subscriptionNo || '-' }}</span>
</a-descriptions-item>
<a-descriptions-item label="创建时间">
<a-descriptions-item label="开通时间">
<template v-if="subscriptionLoading">
<a-skeleton-input :active="true" size="small" style="width: 160px" />
</template>
@@ -185,7 +162,7 @@
</template>
<template v-else-if="currentSubscription && currentSubscription.status === 'active' && currentSubscription.expireTime">
<span>{{ formatDateTime(currentSubscription.expireTime) }}</span>
<a-tag :color="isExpired ? 'red' : 'green'" style="margin-left: 8px">
<a-tag v-if="isExpired" :color="isExpired ? 'red' : 'green'" style="margin-left: 8px">
{{ isExpired ? '已过期' : '已激活' }}
</a-tag>
<span v-if="daysToExpire !== null && daysToExpire >= 0 && daysToExpire <= 7" class="expire-warn">
@@ -208,62 +185,45 @@
<a-button type="link" size="small" @click="onSubscribe" style="padding: 0 0 0 8px">立即订阅</a-button>
</template>
</a-descriptions-item>
<a-descriptions-item label="系统运行">
<template v-if="subscriptionLoading">
<a-skeleton-input :active="true" size="small" style="width: 60px" />
</template>
<template v-else>
<a-tooltip title="自您首次开通小程序商城起">
<span>{{ runDays }} </span>
</a-tooltip>
</template>
</a-descriptions-item>
<!-- <a-descriptions-item label="系统运行">-->
<!-- <template v-if="subscriptionLoading">-->
<!-- <a-skeleton-input :active="true" size="small" style="width: 60px" />-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <a-tooltip title="自您首次开通小程序商城起">-->
<!-- <span>{{ runDays }} </span>-->
<!-- </a-tooltip>-->
<!-- </template>-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item label="绑定小程序">-->
<!-- {{ currentSubscription?.miniProgramId || 'wx75cbc65759eb981a' }}-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item label="售后服务">-->
<!-- <a href="https://websoft.top/login" class="text-gray-500">提交售后工单</a>-->
<!-- </a-descriptions-item>-->
</a-descriptions>
</div>
</div>
</a-col>
<!-- 快捷操作 -->
<a-col :span="12">
<div class="panel" style="min-height: 353px">
<!-- 快速入口 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 捷操作</span>
<span class="panel-title"> 速入口</span>
</div>
<div style="padding: 16px 18px;">
<a-space direction="vertical" style="width: 100%">
<a-button
type="primary"
block
@click="navigateTo('/shop/shopOrder')"
>
<ShoppingCartOutlined />
订单管理
</a-button>
<a-button block @click="navigateTo('/shop/shopGoods')">
<ShopOutlined />
商品管理
</a-button>
<a-button block @click="navigateTo('/shop/shopGoodsCategory')">
<AppstoreOutlined />
商品分类
</a-button>
<a-button block @click="navigateTo('/market/coupon')">
<GiftOutlined />
优惠券管理
</a-button>
<a-button block @click="navigateTo('/system/user')">
<TeamOutlined />
会员管理
</a-button>
<a-button block @click="navigateTo('/shop/shopSetting')">
<SettingOutlined />
商城设置
</a-button>
<a-button block @click="handleClearCache">
<ClearOutlined />
清除缓存
</a-button>
</a-space>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">
{{ item.icon }}
<span v-if="item.badge" class="quick-icon-dot"></span>
</div>
<div class="quick-label">{{ item.label }}</div>
</div>
</div>
</div>
</a-col>
@@ -351,13 +311,6 @@ import { useOrderNotify } from '@/views/shop/shopOrder/useOrderNotify';
import {
ReloadOutlined,
RightOutlined,
ShoppingCartOutlined,
ShopOutlined,
AppstoreOutlined,
GiftOutlined,
TeamOutlined,
SettingOutlined,
ClearOutlined,
InfoCircleOutlined,
QrcodeOutlined
} from '@ant-design/icons-vue';
@@ -371,7 +324,6 @@ import { pageShopOrder } from '@/api/shop/shopOrder';
import { getShopSettingCategoryValues } from '@/api/shop/shopSetting';
import { getCompressedImageUrl } from '@/utils/image';
import { storeToRefs } from 'pinia';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
import { getProductDetail } from '@/api/app/appProduct';
import type { AppProduct } from '@/api/app/appProduct/model';
import type { AppSubscription } from '@/api/app/appSubscription/model';
@@ -392,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);
@@ -691,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);
@@ -770,26 +720,15 @@ const formatMoney = (value?: number | string) => {
return Number.isFinite(num) ? num.toFixed(2) : '0.00';
};
// 清除缓存
const handleClearCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId')).then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
// 新订单声音提醒(全局单例:轮询由 layout 启动,此处仅消费状态 + UI
const { enabled: notifyEnabled, hasNewOrder, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify({
onNewOrder: () => loadData()
});
// 刷新统计数据
// 刷新统计数据(强制刷新,跳过缓存)
const refreshStatistics = async () => {
try {
await statisticsStore.fetchStatistics();
await statisticsStore.fetchStatistics(true);
message.success('统计数据刷新成功');
} catch (error) {
console.error('刷新统计数据失败:', error);
@@ -972,7 +911,7 @@ onUnmounted(() => {
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
/* 待办 */
.todo-list { padding: 8px 0; }
.todo-list { padding: 2px 0; }
.todo-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
@@ -1030,9 +969,9 @@ onUnmounted(() => {
/* 今日数据 */
.today-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 20px;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
padding: 16px;
}
.today-stat {
display: flex;

View File

@@ -1,89 +1,112 @@
<!-- 搜索表单 -->
<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
: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 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,7 +114,10 @@
PlusOutlined,
DeleteOutlined,
UploadOutlined,
SyncOutlined
SyncOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DownOutlined
} from '@ant-design/icons-vue';
import { ref, watch } from 'vue';
import { getCount } from '@/api/shop/shopGoods';
@@ -102,8 +128,8 @@
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
// 选中的商品
selection?: any[];
merchantId?: number;
navigationList?: ShopGoodsCategory[];
}>(),
@@ -131,6 +157,8 @@
(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;
@@ -149,6 +177,16 @@
emit('remove');
};
// 批量上架
const batchUp = () => {
emit('batchUp');
};
// 批量下架
const batchDown = () => {
emit('batchDown');
};
// 打开导入
const openImport = () => {
emit('import');
@@ -220,3 +258,35 @@
{ immediate: true }
);
</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>

View File

@@ -20,16 +20,53 @@
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传 xlsxlsx 文件</span>
<span>只能上传 xlsxlsx 文件</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.jpg2.png3.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 (
![

View File

@@ -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' ||

View File

@@ -72,10 +72,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,9 +98,9 @@
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"
@@ -308,7 +308,7 @@
:precision="2"
:min="0"
style="width: 110px"
placeholder="商品价格"
placeholder="商城价"
/>
<a-input-number
v-model:value="batchSalePrice"
@@ -895,7 +895,7 @@ const columns = [
align: 'center'
},
{
title: '商品价格',
title: '商城价',
dataIndex: 'price',
key: 'price',
align: 'center'
@@ -1337,7 +1337,7 @@ const rules = reactive({
price: [
{
required: true,
message: '请填写会员价',
message: '请填写商城价',
type: 'number',
trigger: 'blur'
}
@@ -1353,7 +1353,7 @@ const rules = reactive({
buyingPrice: [
{
required: true,
message: '请填写会员店/总仓批发价',
message: '请填写进货价',
type: 'number',
trigger: 'blur'
}

View File

@@ -10,6 +10,8 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 1800 }"
size="small"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
@@ -21,6 +23,8 @@
:navigationList="navigationList"
@add="openEdit"
@remove="removeBatch"
@batchUp="batchUpShelf"
@batchDown="batchDownShelf"
@batchMove="openMove"
@import="openImport"
@imageImport="openImageImport"
@@ -37,10 +41,33 @@
v-if="record.image"
:preview="false"
: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>
<span class="text-gray-400 text-xs"
>市场价{{
record.salePrice ? `${record.salePrice}` : '-'
}}</span
>
<span class="text-gray-400 text-xs"
>会员价{{
record.dealerPrice ? `${record.dealerPrice}` : '-'
}}</span
>
</div>
</template>
<template v-if="column.key === 'recommend'">
<a-space @click.stop="onRecommend(record)">
<span
@@ -53,8 +80,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 +102,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)"
@@ -194,35 +246,10 @@
key: 'name',
width: 300
},
{
title: '编号',
dataIndex: 'code',
key: 'code',
align: 'center',
},
{
title: '市场价',
dataIndex: 'salePrice',
key: 'salePrice',
align: 'center',
width: 110,
customRender: ({ text }) => (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 +269,12 @@
key: 'recommend',
align: 'center'
},
{
title: '置顶',
dataIndex: 'isTop',
key: 'isTop',
align: 'center'
},
{
title: '状态',
dataIndex: 'status',
@@ -254,13 +287,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 +304,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
// }
]);
/* 搜索 */
@@ -506,16 +540,86 @@
});
};
/* 单个商品置顶/取消置顶isTop: 1 置顶 0 不置顶) */
const onTop = (row: ShopGoods) => {
const hide = message.loading('请求中..', 0);
updateShopGoods({
...row,
isTop: row.isTop == 1 ? 0 : 1
})
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 单个商品上/下架切换status: 0 上架 1 下架) */
const toggleShelf = (row: ShopGoods, status: number) => {
const hide = message.loading('请求中..', 0);
updateShopGoods({ ...row, status })
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量上架 */
const batchUpShelf = () => {
batchSetShelf(0, '上架');
};
/* 批量下架 */
const batchDownShelf = () => {
batchSetShelf(1, '下架');
};
/* 批量设置上下架 */
const batchSetShelf = (status: number, label: string) => {
if (!selection.value.length) {
message.error('请至少选择一条商品');
return;
}
Modal.confirm({
title: '提示',
content: `确定要批量${label}选中的 ${selection.value.length} 个商品吗?`,
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
Promise.all(selection.value.map((d) => updateShopGoods({ ...d, status })))
.then(() => {
hide();
message.success(`已批量${label} ${selection.value.length} 个商品`);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: ShopGoods) => {
return {
// 行点击事件
onClick: () => {
openEdit(record);
// openEdit(record);
},
// 行双击事件
onDblclick: () => {
// openEdit(record);
openEdit(record);
}
};
};

View File

@@ -119,7 +119,7 @@
ellipsis: true
},
{
title: '商品价格',
title: '商城价',
dataIndex: 'price',
key: 'price',
align: 'center',

View File

@@ -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,7 +37,7 @@
<template v-if="column.key === 'adImage'">
<img
v-if="record.adImage"
:src="record.adImage"
:src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
/>
<span v-else>-</span>
@@ -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 = 40;
const mid = H / 2;
// 回溯祖先链chain[0]=顶级 ... chain[depth]=当前节点
const chain: number[] = [];
let cur: number | undefined = record.categoryId;
while (cur != null) {
chain.unshift(cur);
cur = nodeMap.value.get(cur)?.parentId || undefined;
if (chain.length > 30) break;
}
let svg = `<svg width="${depth * W}" height="${H}" viewBox="0 0 ${depth * W} ${H}" fill="none" style="flex-shrink:0;display:block;">`;
const stroke = '#d0d0d0';
for (let k = 0; k < depth; k++) {
const x = k * W + Math.floor(W / 2);
if (k < depth - 1) {
// 祖先节点不是末位子节点时,画贯穿引导竖线
if (!isLastMap.value.get(chain[k])) {
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
}
} else {
// 末段:竖线到中线 + 横线到右侧
svg += `<line x1="${x}" y1="0" x2="${x}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
if (!isLastMap.value.get(record.categoryId || 0)) {
svg += `<line x1="${x}" y1="${mid}" x2="${x}" y2="${H}" stroke="${stroke}" stroke-width="1"/>`;
}
svg += `<line x1="${x}" y1="${mid}" x2="${(k + 1) * W}" y2="${mid}" stroke="${stroke}" stroke-width="1"/>`;
}
}
svg += `</svg>`;
return svg;
};
// 获取父级分类名称
const getParentName = (parentId?: number) => {
if (!parentId || parentId === 0) return '顶级分类';

View File

@@ -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>

View File

@@ -137,7 +137,7 @@
align: 'center'
},
{
title: '商品价格',
title: '商城价',
dataIndex: 'price',
key: 'price',
align: 'center'

View File

@@ -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();

View File

@@ -22,8 +22,8 @@
<a-radio :value="0">
<span style="color: #1890ff">快递配送</span>
</a-radio>
<a-radio :value="1">无需发货</a-radio>
<a-radio :value="2">商家送货</a-radio>
<a-radio :value="1">无需发货</a-radio>
</a-radio-group>
</a-form-item>
@@ -87,17 +87,79 @@
</a-form-item>
<template v-if="form.deliveryType !== 1">
<a-form-item label="发货人" name="sendName">
<!-- 发货门店快递配送=发货仓库商家送货=配送门店 -->
<a-form-item
:label="form.deliveryType === 0 ? '发货门店' : '配送门店'"
name="sendStoreId"
>
<a-select
v-model:value="form.sendStoreId"
:placeholder="form.deliveryType === 0 ? '请选择发货门店' : '请选择配送门店'"
@change="onStoreChange"
>
<a-select-option
v-for="store in storeList"
:key="store.id"
:value="store.id"
>
{{ store.name }}
</a-select-option>
</a-select>
</a-form-item>
<!-- 商家送货配送店员依赖门店二级联动 -->
<a-form-item
label="配送店员"
name="deliveryUserId"
v-if="form.deliveryType === 2"
>
<a-select
v-model:value="form.deliveryUserId"
placeholder="请先选择门店"
:disabled="!form.sendStoreId"
@change="onUserChange"
>
<a-select-option
v-for="user in userList"
:key="user.id"
:value="user.id"
>
{{ user.name }}
</a-select-option>
</a-select>
</a-form-item>
<div style="color: #999; font-size: 12px; margin: 4px 0 12px">
{{
form.deliveryType === 0
? '选择发货门店后,将自动回填寄件人信息(快递配送时,寄件人即为发货门店)'
: '选择配送门店后,将自动回填配送店员信息'
}}可手动修改
</div>
<a-form-item
:label="form.deliveryType === 0 ? '寄件人' : '发货人'"
name="sendName"
>
<a-input
v-model:value="form.sendName"
placeholder="请输入发货人"
:placeholder="form.deliveryType === 0 ? '请输入寄件人' : '请输入发货人'"
:maxlength="50"
/>
<div
v-if="form.deliveryType === 0"
style="color: #999; font-size: 12px; margin-top: 4px"
>
快递配送场景下寄件人信息将同步至快递面单
</div>
</a-form-item>
<a-form-item label="发货人联系方式" name="sendPhone">
<a-form-item
:label="form.deliveryType === 0 ? '寄件人联系方式' : '发货人联系方式'"
name="sendPhone"
>
<a-input
v-model:value="form.sendPhone"
placeholder="请输入发货人联系方式"
:placeholder="form.deliveryType === 0 ? '请输入寄件人联系方式' : '请输入发货人联系方式'"
:maxlength="50"
/>
</a-form-item>
@@ -156,6 +218,10 @@
import { updateShopOrder } from '@/api/shop/shopOrder';
import { listShopExpress } from '@/api/shop/shopExpress';
import { ShopExpress } from '@/api/shop/shopExpress/model';
import { listShopStore } from '@/api/shop/shopStore';
import { ShopStore } from '@/api/shop/shopStore/model';
import { listShopStoreUser } from '@/api/shop/shopStoreUser';
import { ShopStoreUser } from '@/api/shop/shopStoreUser/model';
import { toDateString } from 'ele-admin-pro';
import dayjs, { Dayjs } from 'dayjs';
import ExpressSettingModal from './expressSettingModal.vue';
@@ -184,6 +250,8 @@
sendName: '',
sendPhone: '',
sendAddress: '',
sendStoreId: undefined as number | undefined, // 发货门店/配送门店
deliveryUserId: undefined as number | undefined, // 配送店员
deliveryTime: dayjs() as Dayjs
});
@@ -225,7 +293,9 @@
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject('请输入发货人');
return Promise.reject(
form.deliveryType === 0 ? '请输入寄件人' : '请输入发货人'
);
}
return Promise.resolve();
}
@@ -235,7 +305,11 @@
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject('请输入发货人联系方式');
return Promise.reject(
form.deliveryType === 0
? '请输入寄件人联系方式'
: '请输入发货人联系方式'
);
}
return Promise.resolve();
}
@@ -250,6 +324,28 @@
return Promise.resolve();
}
}
],
sendStoreId: [
{
validator: (_: any, value: any) => {
if (form.deliveryType !== 1 && !value) {
return Promise.reject(
form.deliveryType === 0 ? '请选择发货门店' : '请选择配送门店'
);
}
return Promise.resolve();
}
}
],
deliveryUserId: [
{
validator: (_: any, value: any) => {
if (form.deliveryType === 2 && !value) {
return Promise.reject('请选择配送店员');
}
return Promise.resolve();
}
}
]
};
@@ -259,6 +355,8 @@
// 状态
const loading = ref(false);
const expressList = ref<ShopExpress[]>([]);
const storeList = ref<ShopStore[]>([]);
const userList = ref<ShopStoreUser[]>([]);
const expressModalVisible = ref(false);
// 加载快递公司列表
@@ -271,6 +369,58 @@
}
};
// 加载门店列表
const loadStoreList = async () => {
try {
const data = await listShopStore({});
storeList.value = data || [];
} catch (error) {
console.error('加载门店失败:', error);
}
};
// 加载指定门店下的店员
const loadUserList = async (storeId?: number) => {
if (!storeId) {
userList.value = [];
return;
}
try {
const data = await listShopStoreUser({ storeId });
userList.value = data || [];
} catch (error) {
console.error('加载店员失败:', error);
}
};
// 门店选择变化
const onStoreChange = (storeId: number) => {
const store = storeList.value.find((item) => item.id === storeId);
if (store) {
// 发货地址统一回填写门店地址
form.sendAddress = store.address || '';
if (form.deliveryType === 0) {
// 快递配送:门店作为发货方,直接回填发货人与联系方式
form.sendName = store.name || '';
form.sendPhone = store.phone || '';
}
if (form.deliveryType === 2) {
// 商家送货:切换门店后清空已选店员并重新加载
form.deliveryUserId = undefined;
loadUserList(storeId);
}
}
};
// 店员选择变化
const onUserChange = (userId: number) => {
const user = userList.value.find((item) => item.id === userId);
if (user) {
form.sendName = user.name || '';
form.sendPhone = user.phone || '';
}
};
// 快递公司筛选
const filterExpressOption = (input: string, option: any) => {
return option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0;
@@ -303,6 +453,9 @@
form.sendName = '';
form.sendPhone = '';
form.sendAddress = '';
form.sendStoreId = undefined;
form.deliveryUserId = undefined;
userList.value = [];
}
}
);
@@ -345,6 +498,8 @@
updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress;
updateData.sendStoreId = form.sendStoreId;
updateData.deliveryUserId = undefined;
updateData.expressName = form.expressName || express?.expressName;
updateData.expressNo = form.expressNo;
} else if (form.deliveryType === 2) {
@@ -352,6 +507,8 @@
updateData.sendName = form.sendName;
updateData.sendPhone = form.sendPhone;
updateData.sendAddress = form.sendAddress;
updateData.sendStoreId = form.sendStoreId;
updateData.deliveryUserId = form.deliveryUserId;
updateData.expressId = undefined;
updateData.expressName = undefined;
updateData.expressNo = undefined;
@@ -399,10 +556,14 @@
form.sendName = '';
form.sendPhone = '';
form.sendAddress = '';
form.sendStoreId = undefined;
form.deliveryUserId = undefined;
userList.value = [];
form.deliveryTime = dayjs();
// 加载快递公司列表
// 加载快递公司列表与门店列表
loadExpressList();
loadStoreList();
} else {
resetFields();
}

View File

@@ -74,7 +74,8 @@
<a-tag v-if="form.payStatus == 1 && form.payType !== 8 && form.payType !== 9" color="green">已付款</a-tag>
<a-tag v-if="form.payStatus == 1 && form.payType === 8" color="blue">待收货付款</a-tag>
<a-tag v-if="form.payStatus == 1 && form.payType === 9" color="green">已确认收款</a-tag>
<a-tag v-if="form.payStatus == 0 && form.payType === 9" color="orange">确认收款</a-tag>
<a-tag v-if="form.payStatus == 0 && form.payType === 9 && form.deliveryStatus === 20" color="blue">已发货待收款</a-tag>
<a-tag v-if="form.payStatus == 0 && form.payType === 9 && form.deliveryStatus !== 20" color="orange">待确认收款</a-tag>
<a-tag v-if="form.payStatus == 0 && form.payType !== 9">未付款</a-tag>
<a-tag v-if="form.payStatus == 3">未付款,占场中</a-tag>
</a-descriptions-item>
@@ -362,6 +363,24 @@
>
<a-spin :spinning="loading">
<a-descriptions :column="2">
<a-descriptions-item
label="发货门店"
v-if="form.deliveryType === 0 || form.deliveryType === 2"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{
form.sendStoreName ||
form.expressMerchantName ||
'未填写'
}}
</a-descriptions-item>
<a-descriptions-item
label="配送店员"
v-if="form.deliveryType === 2"
:labelStyle="{ width: '90px', color: '#808080' }"
>
{{ form.deliveryUserName || '未填写' }}
</a-descriptions-item>
<a-descriptions-item
label="发货人"
:labelStyle="{ width: '90px', color: '#808080' }"
@@ -736,7 +755,21 @@
];
} else if (order.payStatus === 0) {
// 未付款
active.value = 0;
// 线下付款先发货后结款:已发货时步骤条显示发货状态
if (order.payType === 9 && order.deliveryStatus === 20) {
active.value = 2;
steps.value[1].description = '待收款';
steps.value[2].description = order.deliveryTime
? toDateString(order.deliveryTime, 'MM-dd HH:mm')
: '已发货';
if (order.orderStatus === 1) {
active.value = 4;
steps.value[3].description = '已收货';
steps.value[4].description = '订单完成';
}
} else {
active.value = 0;
}
} else if (order.payStatus === 1) {
// 已付款
active.value = 1;
@@ -869,7 +902,7 @@
loading.value = true;
await updateShopOrder({
...form,
deliveryStatus: 30, // 已收货
deliveryStatus: 20, // 已收货
orderStatus: 1 // 已完成
});
message.success('确认收货成功');

View File

@@ -14,68 +14,66 @@
<!-- <span>批量删除</span>-->
<!-- </a-button>-->
<a-input-search
allow-clear
v-model:value="where.orderNo"
placeholder="订单编号"
style="width: 260px"
@search="reload"
@pressEnter="reload"
allow-clear
v-model:value="where.orderNo"
placeholder="订单编号"
style="width: 240px"
@search="reload"
@pressEnter="reload"
/>
<!-- <a-select-->
<!-- v-model:value="where.type"-->
<!-- style="width: 150px"-->
<!-- placeholder="订单类型"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">普通订单</a-select-option>-->
<!-- <a-select-option :value="2">秒杀订单</a-select-option>-->
<!-- <a-select-option :value="3">拼团订单</a-select-option>-->
<!-- </a-select>-->
<!-- <a-select-->
<!-- v-model:value="where.payStatus"-->
<!-- style="width: 150px"-->
<!-- placeholder="付款状态"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">已付款</a-select-option>-->
<!-- <a-select-option :value="0">未付款</a-select-option>-->
<!-- </a-select>-->
<!-- <a-select-->
<!-- v-model:value="where.type"-->
<!-- style="width: 150px"-->
<!-- placeholder="订单类型"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">普通订单</a-select-option>-->
<!-- <a-select-option :value="2">秒杀订单</a-select-option>-->
<!-- <a-select-option :value="3">拼团订单</a-select-option>-->
<!-- </a-select>-->
<a-select
v-model:value="where.orderStatus"
style="width: 150px"
placeholder="订单状态"
@change="search"
v-model:value="where.payStatus"
style="width: 150px"
placeholder="付款状态"
@change="search"
>
<a-select-option value="">全部</a-select-option>
<a-select-option :value="1">完成</a-select-option>
<!-- <a-select-option :value="0">完成</a-select-option>-->
<!-- <a-select-option :value="2">未使用</a-select-option>-->
<a-select-option :value="3">已取消</a-select-option>
<a-select-option :value="4">退款中</a-select-option>
<a-select-option :value="5">退款被拒</a-select-option>
<a-select-option :value="6">退款成功</a-select-option>
<a-select-option :value="1">付款</a-select-option>
<a-select-option :value="0">付款</a-select-option>
</a-select>
<!-- <a-select-->
<!-- :options="getPayType()"-->
<!-- v-model:value="where.payType"-->
<!-- style="width: 150px"-->
<!-- placeholder="付款方式"-->
<!-- @change="search"-->
<!-- />-->
<!-- <a-select-->
<!-- v-model:value="where.orderStatus"-->
<!-- style="width: 150px"-->
<!-- placeholder="订单状态"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="">全部</a-select-option>-->
<!-- <a-select-option :value="1">已完成</a-select-option>-->
<!-- <a-select-option :value="3">已取消</a-select-option>-->
<!-- <a-select-option :value="4">退款中</a-select-option>-->
<!-- <a-select-option :value="5">退款被拒</a-select-option>-->
<!-- <a-select-option :value="6">退款成功</a-select-option>-->
<!-- </a-select>-->
<a-select
:options="getPayType()"
v-model:value="where.payType"
style="width: 150px"
placeholder="付款方式"
@change="search"
/>
<a-range-picker
v-model:value="dateRange"
@change="search"
value-format="YYYY-MM-DD"
v-model:value="dateRange"
@change="search"
value-format="YYYY-MM-DD"
/>
<a-input-search
allow-clear
:placeholder="getSearchPlaceholder()"
style="width: 320px"
v-model:value="where.keywords"
@search="reload"
allow-clear
:placeholder="getSearchPlaceholder()"
style="width: 320px"
v-model:value="where.keywords"
@search="reload"
>
<template #addonBefore>
<a-select v-model:value="type" style="width: 88px" @change="onType">
@@ -98,14 +96,17 @@ import {message} from 'ant-design-vue';
import useSearch from '@/utils/use-search';
import {ShopOrder, ShopOrderParam} from '@/api/shop/shopOrder/model';
import {listShopOrder} from '@/api/shop/shopOrder';
import {listShopOrderGoods} from '@/api/shop/shopOrderGoods';
import {getPayType} from '@/utils/shop';
const props = withDefaults(
defineProps<{
// 选中的订单
selection?: ShopOrder[];
}>(),
{}
defineProps<{
// 选中的订单
selection?: ShopOrder[];
// 当前列表标签对应的筛选状态(如待发货=1导出时一并带入
statusFilter?: number;
}>(),
{}
);
const emit = defineEmits<{
@@ -188,80 +189,161 @@ const orders = ref<ShopOrder[]>([]);
const xlsFileName = ref<string>();
const type = ref('');
// 配送方式中文0快递配送 1无需发货/自提 2商家送货
const getDeliveryTypeLabel = (t?: number) => {
switch (t) {
case 0:
return '快递配送';
case 1:
return '无需发货(自提)';
case 2:
return '商家送货';
default:
return '未设置';
}
};
// 发货状态中文10未发货 20已发货 30部分发货
const getDeliveryStatusLabel = (s?: number) => {
switch (s) {
case 10:
return '未发货';
case 20:
return '已发货';
case 30:
return '部分发货';
default:
return '未知';
}
};
// 商品明细汇总(商品名(规格) x 数量)
const getGoodsSummary = (goods?: any[]) => {
if (!goods || !goods.length) return '';
return goods
.map((g) => {
const spec = g.spec ? `(${g.spec})` : '';
return `${g.goodsName || '未知商品'}${spec} x${g.totalNum || 1}`;
})
.join('');
};
// 商品总数量:基于实际取到的商品明细求和(订单级 totalNum 列表接口可能不返回,以明细为准)
const getGoodsTotalNum = (goods?: any[]) => {
if (!goods || !goods.length) return 0;
return goods.reduce((sum, g) => sum + (Number(g.totalNum) || 0), 0);
};
// 自提/发货店铺名称
const getShopName = (d: ShopOrder) => {
if (d.deliveryType === 1) return d.selfTakeMerchantName || '';
return d.expressMerchantName || '';
};
// 导出
const handleExport = async () => {
loading.value = true;
const array: (string | number)[][] = [
[
'订单编号',
'订单标题',
'买家姓名',
'手机号码',
'收货人',
'联系电话',
'收货地址',
'配送方式',
'商品明细',
'商品总数量',
'实付金额(元)',
'配送时间',
'自提码',
'发货/自提店铺',
'买家备注',
'支付方式',
'付款时间',
'发货状态',
'下单时间'
]
];
await listShopOrder(where)
.then((list) => {
orders.value = list;
list?.forEach((d: ShopOrder) => {
array.push([
`${d.orderNo}`,
`${d.comments}`,
`${d.realName}`,
`${d.phone}`,
`${d.payPrice}`,
`${getPayType(d.payType)}`,
`${d.payTime || ''}`,
`${d.createTime}`
]);
});
const sheetName = `订单数据`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽
sheet['!cols'] = [
{wch: 10},
{wch: 40},
{wch: 20},
{wch: 20},
{wch: 60},
{wch: 15},
{wch: 10},
{wch: 10},
{wch: 20},
{wch: 10},
{wch: 20}
];
message.loading('正在导出...');
setTimeout(() => {
writeFile(
workbook,
`${
where.createTimeEnd ? xlsFileName.value + '_' : ''
}${sheetName}.xlsx`
await listShopOrder({...where, statusFilter: props.statusFilter})
.then(async (list) => {
orders.value = list;
// 列表接口不返回 orderGoods按订单逐个补齐商品明细含名称/规格/数量)
const goodsList = await Promise.all(
(list ?? []).map((d) =>
d.orderId
? listShopOrderGoods({orderId: d.orderId}).catch(() => [])
: Promise.resolve([])
)
);
list?.forEach((d: ShopOrder, idx: number) => {
array.push([
`${d.orderNo || ''}`,
`${d.realName || ''}`,
`${d.phone || d.mobile || ''}`,
`${d.address || ''}`,
`${getDeliveryTypeLabel(d.deliveryType)}`,
`${getGoodsSummary(goodsList[idx])}`,
`${getGoodsTotalNum(goodsList[idx])}`,
`${d.payPrice || ''}`,
`${
d.sendStartTime && d.sendEndTime
? `${d.sendStartTime} - ${d.sendEndTime}`
: ''
}`,
`${d.selfTakeCode || ''}`,
`${getShopName(d)}`,
`${d.buyerRemarks || ''}`,
`${getPayType(d.payType)}`,
`${getDeliveryStatusLabel(d.deliveryStatus)}`,
`${d.createTime || ''}`
]);
});
const sheetName = `订单数据`;
const workbook = {
SheetNames: [sheetName],
Sheets: {}
};
const sheet = utils.aoa_to_sheet(array);
workbook.Sheets[sheetName] = sheet;
// 设置列宽(与上方表头一一对应)
sheet['!cols'] = [
{wch: 22}, // 订单编号
{wch: 12}, // 收货人
{wch: 16}, // 联系电话
{wch: 40}, // 收货地址
{wch: 14}, // 配送方式
{wch: 40}, // 商品明细
{wch: 12}, // 商品总数量
{wch: 12}, // 实付金额(元)
{wch: 28}, // 配送时间
{wch: 12}, // 自提码
{wch: 20}, // 发货/自提店铺
{wch: 30}, // 买家备注
{wch: 12}, // 支付方式
{wch: 10}, // 发货状态
{wch: 20} // 下单时间
];
message.loading('正在导出...');
setTimeout(() => {
writeFile(
workbook,
`${
where.createTimeEnd ? xlsFileName.value + '_' : ''
}${sheetName}.xlsx`
);
loading.value = false;
}, 1000);
})
.catch((msg) => {
message.error(msg);
loading.value = false;
}, 1000);
})
.catch((msg) => {
message.error(msg);
loading.value = false;
})
.finally(() => {
});
})
.finally(() => {
});
};
watch(
() => props.selection,
() => {
}
() => props.selection,
() => {
}
);
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,12 @@
<a-form-item label="未付款自动关闭" name="orderAutoCloseMins">
<a-input-number
v-model:value="form.orderAutoCloseMins"
:min="1"
:min="0"
:max="10080"
placeholder="请输入分钟数"
placeholder="0 表示不自动取消"
style="width: 200px"
/>
<span style="margin-left: 8px; color: #888">分钟</span>
<span style="margin-left: 8px; color: #888">分钟0 表示不自动取消</span>
</a-form-item>
<a-form-item label="自动好评天数" name="orderAutoCommentDays">
<a-input-number
@@ -54,6 +54,15 @@
un-checked-children=""
/>
</a-form-item>
<a-form-item label="销量累加方式" name="salesAccumulateType">
<a-radio-group v-model:value="form.salesAccumulateType">
<a-radio :value="10">支付成功后累加默认</a-radio>
<a-radio :value="20">下单成功后累加</a-radio>
</a-radio-group>
<div style="color: #888; font-size: 12px; margin-top: 4px">
支付累加用户付款后才计入销量下单累加生成订单即计入销量取消订单会回滚
</div>
</a-form-item>
<a-form-item label="保存">
<a-button type="primary" :loading="loading" @click="save">保存</a-button>
</a-form-item>
@@ -84,7 +93,8 @@ const { form, assignFields } = useFormData<ShopSetting>({
orderAutoCloseMins: 30,
orderAutoCommentDays: 15,
orderFreightFreeAmount: 0,
orderRefundEnabled: true
orderRefundEnabled: true,
salesAccumulateType: 10
});
const rules = reactive({
@@ -99,7 +109,7 @@ const rules = reactive({
orderAutoCloseMins: [
{
required: true,
message: '请输入未付款自动关闭分钟数',
message: '请输入未付款自动关闭分钟数0 表示不自动取消)',
type: 'number',
trigger: 'blur'
}
@@ -135,6 +145,9 @@ const load = async () => {
if (values.orderFreightFreeAmount !== undefined) {
form.orderFreightFreeAmount = Number(values.orderFreightFreeAmount);
}
if (values.salesAccumulateType !== undefined) {
form.salesAccumulateType = Number(values.salesAccumulateType);
}
}
} catch (e: any) {
// 未配置时忽略

View File

@@ -0,0 +1,71 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap">
<a-button type="text" @click="openUrl(`/website/field`)"
>字段扩展
</a-button>
<a-button type="text" @click="openUrl('/website/dict')">字典管理 </a-button>
<a-button type="text" @click="openUrl('/website/domain')"
>域名管理
</a-button>
<a-button type="text" @click="openUrl('/website/model')"
>模型管理
</a-button>
<a-button type="text" @click="openUrl('/website/form')">表单管理 </a-button>
<a-button type="text" @click="openUrl('/website/lang')">国际化 </a-button>
<a-button type="text" @click="openUrl('/website/setting')"
>网站设置
</a-button>
<a-button type="text" class="ele-btn-icon" @click="clearSiteInfoCache">
清除缓存
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { watch, nextTick } from 'vue';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { openUrl } from '@/utils/common';
import { message } from 'ant-design-vue';
import { removeSiteInfoCache } from '@/api/cms/cmsWebsite';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache(
'SiteInfo:' + localStorage.getItem('TenantId') + '*'
).then((msg) => {
if (msg) {
message.success(msg);
}
});
};
nextTick(() => {
if (localStorage.getItem('NotActive')) {
// IsActive.value = false
}
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,408 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑小程序' : '创建小程序'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
:confirm-loading="loading"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="Logo" name="avatar">
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="账号类型" name="type">
{{ form.type }}
</a-form-item>
<a-form-item label="小程序名称" name="websiteName">
<a-input
allow-clear
placeholder="请输入小程序名称"
v-model:value="form.websiteName"
/>
</a-form-item>
<a-form-item label="网站域名" name="domain" v-if="form.type == 10">
<a-input v-model:value="form.domain" placeholder="huawei.com">
<template #addonBefore>
<a-select v-model:value="form.prefix" style="width: 90px">
<a-select-option value="http://">http://</a-select-option>
<a-select-option value="https://">https://</a-select-option>
</a-select>
</template>
</a-input>
</a-form-item>
<a-form-item label="AppId" name="websiteCode" v-if="form.type == 20">
<a-input
allow-clear
placeholder="请输入AppId"
v-model:value="form.websiteCode"
/>
</a-form-item>
<a-form-item label="小程序描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入小程序描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="SEO关键词" name="keywords">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入SEO关键词"
v-model:value="form.keywords"
/>
</a-form-item>
<!-- <a-form-item label="全局样式" name="style">-->
<!-- <a-textarea-->
<!-- :rows="4"-->
<!-- :maxlength="200"-->
<!-- placeholder="全局样式"-->
<!-- v-model:value="form.style"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="小程序类型" name="websiteType">-->
<!-- <a-select-->
<!-- :options="websiteType"-->
<!-- :value="form.websiteType"-->
<!-- placeholder="请选择主体类型"-->
<!-- @change="onCmsWebsiteType"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="当前版本" name="version">-->
<!-- <a-tag color="red" v-if="form.version === 10">标准版</a-tag>-->
<!-- <a-tag color="green" v-if="form.version === 20">专业版</a-tag>-->
<!-- <a-tag color="cyan" v-if="form.version === 30">永久授权</a-tag>-->
<!-- </a-form-item>-->
<a-form-item label="状态" name="running">
<a-radio-group
v-model:value="form.running"
:disabled="form.running == 4 || form.running == 5"
>
<a-radio :value="1">运行中</a-radio>
<a-radio :value="2">维护中</a-radio>
<a-radio :value="3">已关闭</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="form.running == 2" label="维护说明" name="statusText">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="状态说明"
v-model:value="form.statusText"
/>
</a-form-item>
<!-- <a-divider style="margin-bottom: 24px" />-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addCmsWebsite, updateCmsWebsite } from '@/api/cms/cmsWebsite';
import { CmsWebsite } from '@/api/cms/cmsWebsite/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance, type Rule } from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FileRecord } from '@/api/system/file/model';
import { checkExistence } from '@/api/cms/cmsDomain';
import { updateCmsDomain } from '@/api/cms/cmsDomain';
import { updateTenant } from '@/api/system/tenant';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: CmsWebsite | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const websiteQrcode = ref<ItemType[]>([]);
const oldDomain = ref();
const files = ref<ItemType[]>([]);
// 用户信息
const form = reactive<CmsWebsite>({
websiteId: undefined,
websiteLogo: undefined,
websiteName: undefined,
websiteCode: undefined,
type: 20,
files: undefined,
keywords: '',
prefix: '',
domain: '',
adminUrl: '',
style: '',
icpNo: undefined,
email: undefined,
version: undefined,
websiteType: '',
running: 1,
expirationTime: undefined,
sortNumber: undefined,
comments: undefined,
status: undefined,
statusText: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
// comments: [
// {
// required: true,
// type: 'string',
// message: '请填写小程序描述',
// trigger: 'blur'
// }
// ],
keywords: [
{
required: true,
type: 'string',
message: '请填写SEO关键词',
trigger: 'blur'
}
],
running: [
{
required: true,
type: 'number',
message: '请选择小程序状态',
trigger: 'change'
}
],
domain: [
{
required: true,
type: 'string',
message: '请填写小程序域名',
trigger: 'blur'
}
],
websiteCode: [
{
required: true,
type: 'string',
message: '请填写小程序码',
trigger: 'blur'
}
],
// websiteCode: [
// {
// required: true,
// type: 'string',
// message: '该域名已被使用',
// validator: (_rule: Rule, value: string) => {
// return new Promise<void>((resolve, reject) => {
// if (!value) {
// return reject('请输入二级域名');
// }
// checkExistence('domain', `${value}.wsdns.cn`)
// .then(() => {
// if (value === oldDomain.value) {
// return resolve();
// }
// reject('已存在');
// })
// .catch(() => {
// resolve();
// });
// });
// },
// trigger: 'blur'
// }
// ],
adminUrl: [
{
required: true,
type: 'string',
message: '请填写小程序后台管理地址',
trigger: 'blur'
}
],
icpNo: [
{
required: true,
type: 'string',
message: '请填写ICP备案号',
trigger: 'blur'
}
],
appSecret: [
{
required: true,
type: 'string',
message: '请填写小程序秘钥',
trigger: 'blur'
}
],
websiteName: [
{
required: true,
type: 'string',
message: '请填写小程序信息名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.websiteLogo = data.downloadUrl;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.websiteLogo = '';
};
const chooseFile = (data: FileRecord) => {
form.websiteCode = data.url;
files.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
};
const onDeleteFile = (index: number) => {
files.value.splice(index, 1);
};
// const onWebsiteType = (text: string) => {
// form.websiteType = text;
// };
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateCmsWebsite : addCmsWebsite;
if (!isUpdate.value) {
updateVisible(false);
message.loading('创建过程中请勿刷新页面!', 0);
}
const formData = {
...form,
type: 20,
adminUrl: `mp.websoft.top`,
files: JSON.stringify(files.value)
};
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
updateVisible(false);
updateCmsDomain({
websiteId: form.websiteId,
domain: `${localStorage.getItem('TenantId')}.shoplnk.cn`
});
updateTenant({
tenantName: `${form.websiteName}`
}).then(() => {});
localStorage.setItem('Domain', `${form.websiteCode}.shoplnk.cn`);
localStorage.setItem('WebsiteId', `${form.websiteId}`);
localStorage.setItem('WebsiteName', `${form.websiteName}`);
message.destroy();
message.success(msg);
// window.location.reload();
emit('done');
})
.catch((e) => {
loading.value = false;
message.destroy();
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
files.value = [];
websiteQrcode.value = [];
if (props.data?.websiteId) {
assignObject(form, props.data);
if (props.data.websiteLogo) {
images.value.push({
uid: uuid(),
url: props.data.websiteLogo,
status: 'done'
});
}
if (props.data.files) {
files.value = JSON.parse(props.data.files);
}
if (props.data.websiteCode) {
oldDomain.value = props.data.websiteCode;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<!-- 高级筛选 -->
<a-form layout="inline" class="ele-form-inline" style="margin-bottom: 16px">
<a-form-item label="角色">
<a-form-item>
<a-select
v-model:value="searchWhere.roleId"
placeholder="全部角色"
@@ -17,7 +17,7 @@
>
</a-select>
</a-form-item>
<a-form-item label="注册来源">
<a-form-item>
<a-select
v-model:value="searchWhere.platform"
placeholder="全部来源"
@@ -29,7 +29,7 @@
<a-select-option value="WEB">Web</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="类型">
<a-form-item>
<a-select
v-model:value="searchWhere.isAdmin"
placeholder="全部"
@@ -54,6 +54,7 @@
:datasource="datasource"
v-model:selection="selection"
:scroll="{ x: 1800 }"
size="small"
:where="defaultWhere"
:customRow="customRow"
cache-key="proSystemUserTable"
@@ -125,7 +126,7 @@
</template>
</a-avatar>
<div class="user-info">
<span class="user-name">{{ record.realName || '-' }}</span>
<span class="user-name">{{ record.realName || record.nickname || '-' }}</span>
<span class="user-mobile">{{ hasRole('superAdmin') ? record.phone : record.mobile }}</span>
</div>
</div>
@@ -415,6 +416,7 @@
{
title: '邮箱',
dataIndex: 'email',
sorter: true,
showSorterTooltip: false
},
{
@@ -510,7 +512,6 @@
dataIndex: 'createTime',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{

View File

@@ -0,0 +1,119 @@
=========================================================================
2026-07-17 今日改动问题点总结
=========================================================================
【一】管理后台guilixu-admin
1. 发货弹窗新增「发货门店」「配送店员」选择功能
- 文件src/views/shop/shopOrder/components/deliveryModal.vue
- 新增发货门店下拉(复用 listShopStore选中后自动回填
发货人/电话/地址
- 商家送货(deliveryType=2)新增配送店员二级联动下拉
- 新增校验:发货门店必填(deliveryType≠1)、配送店员必填(deliveryType=2)
- 提交时附带 sendStoreId / deliveryUserId
2. 发货弹窗文案优化
- 文件src/views/shop/shopOrder/components/deliveryModal.vue
- 「发货门店」标签动态化:快递配送→发货门店,商家送货→配送门店
- 「发货人」标签动态化:快递配送→寄件人,商家送货→发货人
- 提示文案、验证规则文案同步动态化
- 快递配送时增加"寄件人信息将同步至快递面单"说明
3. 订单模型字段补充
- 文件src/api/shop/shopOrder/model/index.ts
- 新增 sendStoreId、deliveryUserId 字段
4. 订单详情页展示发货门店/配送店员
- 文件src/views/shop/shopOrder/components/orderInfo.vue
- 发货信息卡片增加「发货门店」「配送店员」展示
- 支付状态标签新增「已发货待收款」(payType=9 && payStatus=0 && deliveryStatus=20)
- 步骤条逻辑:线下付款未付款但已发货时显示「下单→待收款→已发货」
5. 线下付款先发货后结款流程
- 文件src/views/shop/shopOrder/index.vue
- 线下付款未付款订单按发货状态区分操作按钮:
· deliveryStatus=10显示「确认收款」+「发货」+「关闭」
· deliveryStatus=20仅显示「确认收款」
- 文件src/views/shop/shopOrder/components/OfflinePaymentModal.vue
- 新增 deliveryStatus 字段及动态提示文案
- 成功消息动态化:已发货→「已自动完成」,未发货→「已进入待发货状态」
6. 配送方式排序调整
- 文件src/views/shop/shopOrder/components/deliveryModal.vue
- radio 顺序改为:快递配送→商家送货→无需发货
-------------------------------------------------------------------------
【二】后端 Javaguilixu-java
1. shop_order 主表新增字段
- ShopOrder.java新增 sendStoreId、deliveryUserId 持久化字段
及 sendStoreName、deliveryUserName 虚拟展示字段
- ShopOrderMapper.xmlLEFT JOIN shop_store / shop_store_user 联带名称
- SQLALTER TABLE shop_order ADD send_store_id INT, delivery_user_id INT
2. shop_order_delivery 发货单表新增字段
- ShopOrderDelivery.java新增 sendStoreId、deliveryUserId
- ShopOrderController.update():发货写入分支(无需物流/实物快递/补录运单号)
均 setSendStoreId / setDeliveryUserId
- SQLALTER TABLE shop_order_delivery ADD send_store_id INT, delivery_user_id INT
3. 线下付款确认收款逻辑调整
- ShopOrderServiceImpl.confirmOfflinePayment():移除订单状态=0约束
允许已发货订单确认收款;已发货(deliveryStatus=20)时确认后自动完成订单
- ShopOrderController / ShopOrderService更新接口描述和成功消息
-------------------------------------------------------------------------
【三】商家端小程序xinlong-shop-taro
1. 修复 storeId=0 导致发货人员列表为空的 Bug
- 文件src/pages/store/orders/index.tsx
- 问题根因openShipModal 调 listShopStoreUser() 未传 storeId(默认0)
- 修复:先 getMyClerk() 取 storeId再 listShopStoreUser({storeId})
- 默认选中当前登录人对应的店员
2. 修复订单详情页配送方式判断逻辑
- 文件src/pages/order/detail.tsx
- 问题:原 isExpress = deliveryType === 0 || undefined无法区分商家送货
- 修复isExpress 改为 deliveryType === 0 || 2 || undefined
- 新增 DELIVERY_TYPE_MAP0=快递配送, 1=自提, 2=商家送货
- 收货信息卡片:商家送货显示🛵+配送门店名,快递配送显示📦
- 订单信息区新增「配送方式」字段展示
3. 支持线下付款先发货后结款
- 文件src/pages/store/orders/index.tsx
- getStatusText():新增「已发货待收款」状态
- getOrderActions():线下付款未付款(payType=9)也能显示发货按钮
- OP_DESC确认收款描述移除"订单将进入待发货状态"
-------------------------------------------------------------------------
【四】跨项目误改纠正(教训记录)
1. 误将商家端功能改到 guilixu-taro用户端
- 错误改动useClerk 全局上下文、订单详情 deliveryType 修复
- 已用 git checkout 还原 guilixu-taro 的 5 个跟踪文件
- 已删除 guilixu-taro 新建的 2 个文件(ClerkContext.tsx / useClerk.ts)
- 正确项目xinlong-shop-taro商家端
2. 教训:跨项目改动前,先在多个候选项目里确认"哪个项目有该功能源码"
商家端发货/门店/订单 → xinlong-shop-taro
用户端订单详情等 → guilixu-taro
=========================================================================
今日 Git 提交记录
=========================================================================
bef50c2 feat(order): 支持线下付款先发货后结款流程
57be388 fix(xinlong-shop-taro): 修复订单详情页配送方式判断逻辑
bd814d0 feat(shop): 优化发货弹窗文案及修复小程序配送方式判断
3edbb13 fix(orders): 修正发货功能关联的项目错误导致 storeId 为 0
b406973 feat(store): 修正 storeId 获取并新增发货人员选择功能
2745f4b feat(shopOrder): 增加发货门店与配送店员选择功能
e9249c9 feat(shopOrder): 增加发货门店与配送店员选择功能
6b57dd2 fix(shopOrder): 修正确认收货状态码
=========================================================================
生成时间2026-07-17 18:18
=========================================================================