mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-10 02:49:03 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b0d6efc8 | ||
|
|
fd241bc36a | ||
|
|
d799b37f0c | ||
|
|
0232ddeb27 | ||
|
|
2497f33cb4 | ||
|
|
0f08807f00 | ||
|
|
29a6b00a21 | ||
|
|
6be934d00b | ||
|
|
560286b7f6 | ||
|
|
961565149c | ||
|
|
1a457a60f8 | ||
|
|
d521587ad8 |
@@ -1,201 +0,0 @@
|
|||||||
# Lilishop UI — Vue 3 迁移与上线前检查清单
|
|
||||||
|
|
||||||
四个子项目(buyer / seller / manager / im)已完成 **Vue 3 + Element Plus** 迁移,并完成依赖小版本对齐。本文档供上线前统一回归使用。
|
|
||||||
|
|
||||||
## 技术栈(四端一致)
|
|
||||||
|
|
||||||
| 类别 | 包 | 版本(package.json) |
|
|
||||||
|------|-----|---------------------|
|
|
||||||
| 框架 | vue | ^3.5.35 |
|
|
||||||
| 编译 | @vue/compiler-sfc | ^3.5.35 |
|
|
||||||
| 路由 | vue-router | ^4.6.4 |
|
|
||||||
| 状态 | pinia | ^4.0.3(四端已全部从 vuex 迁移完成) |
|
|
||||||
| 组件写法 | `<script setup>` | 四端 597 个组件已全部完成 Options API → 组合式 API 迁移;各端 eslint 已启用 `vue/component-api-style: ['warn', ['script-setup']]` 防止回退 |
|
|
||||||
| UI | element-plus | ^2.14.1 |
|
|
||||||
| 图标 | @element-plus/icons-vue | ^2.3.2 |
|
|
||||||
| 请求 | axios | ^1.7.9 |
|
|
||||||
| 兼容 | core-js | ^3.49.0 |
|
|
||||||
| 构建 | @vue/cli-service | ^5.0.9 |
|
|
||||||
|
|
||||||
> 使用 `^` 范围,实际安装版本以各目录 `yarn.lock` 为准。
|
|
||||||
> 要求 **Node >= 16**,包管理统一 **yarn**。
|
|
||||||
|
|
||||||
## 本地启动
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 买家端
|
|
||||||
cd buyer && yarn install && yarn dev # 端口 10000
|
|
||||||
|
|
||||||
# 商家端
|
|
||||||
cd seller && yarn install && yarn dev # 端口 10002
|
|
||||||
|
|
||||||
# 管理端
|
|
||||||
cd manager && yarn install && yarn dev # 端口 10003
|
|
||||||
|
|
||||||
# IM 客服
|
|
||||||
cd im && yarn install && yarn dev # 端口 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
生产构建(上线前必跑):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd buyer && yarn build
|
|
||||||
cd seller && yarn build
|
|
||||||
cd manager && yarn build
|
|
||||||
cd im && yarn build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、通用检查(四端都要过)
|
|
||||||
|
|
||||||
### 1. 认证与请求(axios 1.x 重点)
|
|
||||||
|
|
||||||
各端请求封装位置:
|
|
||||||
|
|
||||||
| 端 | 文件 |
|
|
||||||
|----|------|
|
|
||||||
| buyer | `buyer/src/plugins/request.js` |
|
|
||||||
| seller | `seller/src/libs/axios.js` |
|
|
||||||
| manager | `manager/src/libs/axios.js` |
|
|
||||||
| im | `im/src/utils/request.js` |
|
|
||||||
|
|
||||||
- [ ] **登录**:账号密码登录成功,token 写入正常
|
|
||||||
- [ ] **登出**:清除 token,跳转登录页
|
|
||||||
- [ ] **401**:token 过期后跳转登录 / 刷新 token(buyer/manager/seller 有刷新逻辑)
|
|
||||||
- [ ] **403**:无权限时提示文案正确、不白屏
|
|
||||||
- [ ] **400 / 5xx**:接口报错能弹出 `Message` / `ElMessage` / `Notification`
|
|
||||||
- [ ] **网络超时**:断网或超时后有错误提示,页面不卡死
|
|
||||||
- [ ] **GET 带参**:列表筛选、分页参数序列化正常
|
|
||||||
- [ ] **POST JSON**:`Content-Type: application/json` 请求体正确
|
|
||||||
- [ ] **POST 表单**:`application/x-www-form-urlencoded` 提交正常(im 等)
|
|
||||||
|
|
||||||
### 2. Element Plus UI(2.6 → 2.14 重点)
|
|
||||||
|
|
||||||
- [ ] **表格** `el-table`:列展示、排序、空数据、加载态
|
|
||||||
- [ ] **分页** `el-pagination`:翻页、每页条数切换
|
|
||||||
- [ ] **表单** `el-form`:必填校验、错误提示位置
|
|
||||||
- [ ] **弹窗** `el-dialog` / `ElMessageBox`:打开、确认、取消、关闭
|
|
||||||
- [ ] **下拉** `el-select` / `el-dropdown`:选项、点击外部关闭
|
|
||||||
- [ ] **消息** `ElMessage` / `$Message` 兼容层:success / error / warning
|
|
||||||
- [ ] **加载** `ElLoading` / `v-loading`:请求期间遮罩正常消失
|
|
||||||
- [ ] **日期** `el-date-picker`:选择、清空、范围选择
|
|
||||||
- [ ] **上传** `el-upload`:选文件、进度、成功 / 失败回调
|
|
||||||
|
|
||||||
### 3. 路由与页面
|
|
||||||
|
|
||||||
- [ ] 浏览器 **刷新** 当前路由不 404(history 模式 + 服务端配置)
|
|
||||||
- [ ] **动态路由** 加载正常(seller / manager 菜单权限路由)
|
|
||||||
- [ ] 浏览器 **前进 / 后退** 正常
|
|
||||||
- [ ] 各端 **404 页** 可回到首页
|
|
||||||
|
|
||||||
### 4. 构建与部署
|
|
||||||
|
|
||||||
- [ ] 四端 `yarn build` 无 error(warning 可记录,不阻断)
|
|
||||||
- [ ] 生产环境 API 地址配置正确(`config` / 环境变量)
|
|
||||||
- [ ] 静态资源 `publicPath` 与 Nginx / CDN 路径一致
|
|
||||||
- [ ] gzip 压缩包(如有)可正常访问
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、分端专项检查
|
|
||||||
|
|
||||||
### Buyer 买家端(10000)
|
|
||||||
|
|
||||||
详见 [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md)
|
|
||||||
|
|
||||||
- [ ] 首页装修模块渲染(轮播、商品列表、秒杀区等)
|
|
||||||
- [ ] 商品详情:规格选择、加购、收藏、`el-image` 预览
|
|
||||||
- [ ] 购物车 → 结算 → 支付流程
|
|
||||||
- [ ] 用户中心:订单列表、地址、优惠券、售后
|
|
||||||
- [ ] 登录 / 注册 / 忘记密码
|
|
||||||
- [ ] 店铺入驻、商户页
|
|
||||||
- [ ] 地图(高德 `@amap/amap-jsapi-loader`)定位与选点
|
|
||||||
- [ ] 侧边栏 `el-menu`、头部 `el-dropdown`、动态图标 `iconMap.js`
|
|
||||||
|
|
||||||
### Seller 商家端(10002)
|
|
||||||
|
|
||||||
详见 [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md)
|
|
||||||
|
|
||||||
- [ ] 商家登录、店铺信息
|
|
||||||
- [ ] 商品发布三步流程(`goodsOperation` 步骤条)
|
|
||||||
- [ ] 订单管理:发货、退款、详情
|
|
||||||
- [ ] 促销 / 分销 / 统计图表(`@antv/g2`)
|
|
||||||
- [ ] 页面装修(wap / modelList,从 manager 同步部分)
|
|
||||||
- [ ] 图片懒加载 `vue-lazyload`
|
|
||||||
- [ ] Excel 导出 `vue-json-excel`
|
|
||||||
|
|
||||||
### Manager 管理端(10003)
|
|
||||||
|
|
||||||
详见 [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md)
|
|
||||||
|
|
||||||
- [ ] 管理员登录、权限菜单、动态路由
|
|
||||||
- [ ] 系统设置(`sys/setting-manage` 各 Tab)
|
|
||||||
- [ ] 菜单 / 部门管理
|
|
||||||
- [ ] 页面装修(PC + H5)
|
|
||||||
- [ ] 文章管理
|
|
||||||
- [ ] 商品 / 订单 / 会员 / 促销等核心列表 CRUD
|
|
||||||
- [ ] 富文本 TinyMCE 编辑、图片上传
|
|
||||||
- [ ] 多语言 `vue-i18n` 切换(如有启用)
|
|
||||||
|
|
||||||
### IM 客服端(8000)
|
|
||||||
|
|
||||||
详见 [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md)
|
|
||||||
|
|
||||||
- [ ] 带 token 参数进入会话页
|
|
||||||
- [ ] 对话列表加载、切换会话、未读数
|
|
||||||
- [ ] 发送文字、表情、图片消息
|
|
||||||
- [ ] 商品 / 订单卡片链接跳转(`linkToGoods` / `linkToOrders`)
|
|
||||||
- [ ] 右键菜单(复制、撤回、删除等)
|
|
||||||
- [ ] WebSocket 连接 / 断线重连 / 新消息通知
|
|
||||||
- [ ] 最近浏览、订单列表侧栏(商家场景)
|
|
||||||
- [ ] 用户名片弹层 `$user()`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、已知可忽略项
|
|
||||||
|
|
||||||
| 项 | 说明 |
|
|
||||||
|----|------|
|
|
||||||
| `vue-awesome-swiper` peer warning | 历史依赖,Vue 3 下仍有 peer 告警,功能正常可暂忽略 |
|
|
||||||
| im 内 `<i class="el-icon-*">` | Element Plus 无字体图标,部分旧图标可能不显示,不影响核心功能 |
|
|
||||||
| `vue-prism-editor@2.0.0-alpha.2` | im 代码块编辑器刻意锁定 alpha 版 |
|
|
||||||
| 打包体积 warning | chunk-vendors 偏大,属 Element Plus 全量引入,可后续做按需优化 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、问题记录模板
|
|
||||||
|
|
||||||
上线测试时建议按端记录:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
### [端名] 问题标题
|
|
||||||
- 页面/路由:
|
|
||||||
- 操作步骤:
|
|
||||||
- 预期:
|
|
||||||
- 实际:
|
|
||||||
- 控制台报错:
|
|
||||||
- 优先级:P0 / P1 / P2
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、子项目文档索引
|
|
||||||
|
|
||||||
| 端 | 迁移说明 |
|
|
||||||
|----|----------|
|
|
||||||
| buyer | [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md) |
|
|
||||||
| seller | [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md) |
|
|
||||||
| manager | [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md) |
|
|
||||||
| im | [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、建议上线顺序
|
|
||||||
|
|
||||||
1. **manager**(后台配置源头)
|
|
||||||
2. **seller**(商家经营)
|
|
||||||
3. **buyer**(C 端流量最大)
|
|
||||||
4. **im**(客服,依赖前后端 WebSocket)
|
|
||||||
|
|
||||||
每上一端,至少完成本文 **第一节通用检查** + 对应 **第二节分端检查** 后再切下一端。
|
|
||||||
46
README.md
46
README.md
@@ -162,18 +162,42 @@ lilishop-ui
|
|||||||
|
|
||||||
## 8. 升级计划
|
## 8. 升级计划
|
||||||
|
|
||||||
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
|
|
||||||
|
|
||||||
### ✅ 已完成特性
|
### ✅ 已完成特性
|
||||||
| 功能模块 | 功能模块 |
|
|
||||||
| :--- | :--- |
|
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
|
||||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
|
||||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
| 时间 | 功能模块 | 完成内容 |
|
||||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
| :--- | :--- | :--- |
|
||||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
| 2026-11-05 | ✅ 付费会员 | 单一付费会员、开通与续费、有效期、关联权益、商家与商品折扣、平台补贴及付费记录。 |
|
||||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
| 2026-11-04 | ✅ 客户等级体系 / 客户权益 | 等级配置、权益介绍、经验值任务与明细、权益发放及重试,提供默认等级与权益数据。 |
|
||||||
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
|
| 2026-11-04 | ✅ 小程序订单管理 | 增加小程序订单查询入口及微信订单自动上报开关。 |
|
||||||
| ✅ 主题色管理 | |
|
| 2026-11-04 | ✅ 立减优惠券 | 新增立减券,完善优惠券校验、领取与订单优惠计算。 |
|
||||||
|
| 2026-11-04 | ✅ 商品定时上下架 | 完善定时上下架计划管理、独立列表及执行校验。 |
|
||||||
|
| 2026-11-04 | ✅ 微信小程序原生能力 | 接入微信地址导入、头像选择和昵称填写。 |
|
||||||
|
| 2026-11-02 | ✅ 消息模板 | 支持多渠道模板矩阵维护,统一站内信、短信、微信公众号及小程序订阅消息的场景配置和异步发送。 |
|
||||||
|
| 2026-10-30 | ✅ PC 页面装修 | 重构楼层组件,支持自适应高度、可视化热区绘制及配置交互优化。 |
|
||||||
|
| 2026-10-29 | ✅ 定时任务管理 | 重构为数据库配置驱动,支持运营后台维护任务。 |
|
||||||
|
| 2026-10-29 | ✅ 敏感词管理 | 支持按业务场景配置过滤开关,并优化词库加载。 |
|
||||||
|
| 2026-10-29 | ✅ PC 店铺搜索 | 新增店铺列表,支持商品与店铺双模式搜索。 |
|
||||||
|
| 2026-10-22 | ✅ 分类设置 | 支持四种分类页模板选择、商品列表展示字段开关及模板图片预览。 |
|
||||||
|
| 2026-10-20 | ✅ 移动端页面装修 | 完善可视化编辑器,新增首页组合组件及优惠券、店铺、文章等楼层。 |
|
||||||
|
| 2026-10-09 | ✅ 主题色管理 | 完善主题设置,支持自定义模式。 |
|
||||||
|
| 2026-10-02 | ✅ 预约服务商品 | 完善预约商品、预约交易及订单详情功能。 |
|
||||||
|
| 2026-08-05 | ✅ 二级分销 | 由一级分销扩展为二级分销,增加节点配置及分销员中心。 |
|
||||||
|
| 2026-08-03 | ✅ 卡密商品 | 完善电子卡券履约、订单状态及卡池管理。 |
|
||||||
|
| 2026-07-23 | ✅ 素材库管理 | 增加文件分类、目录管理及目录归属校验。 |
|
||||||
|
| 2026-07-20 | ✅ 经营统计报表 | 新增店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表。 |
|
||||||
|
| 2026-07-13 | ✅ 腾讯云直播 | 接入并完善直播、聊天与直播商品相关功能。 |
|
||||||
|
| 2026-06-30 | ✅ 移动端技术升级 | 升级 Vue 3 与 uView Plus,适配组件和页面。 |
|
||||||
|
| 2026-06-29 | ✅ 电子面单发货 | 完善快递鸟电子面单,支持自动携带运单号发货。 |
|
||||||
|
| 2026-06-05 | ✅ 后端与管理端框架升级 | 5 月 25 日至 6 月 5 日完成 Spring Boot 4、管理端 Vue 3 与 Element Plus 升级。 |
|
||||||
|
| 2026-05-22 | ✅ 购物卡 | 新增礼品卡接口,以及选择、绑定、激活和列表管理。 |
|
||||||
|
| 2026-05-20 | ✅ 微信提现 | 完善转账状态查询、确认回调和重试处理,后续补充提现规则说明配置。 |
|
||||||
|
| 2026-04-16 | ✅ 置顶评论 | 支持评论置顶及商品评价管理。 |
|
||||||
|
| 2026-04-13 | ✅ 专用发票(发票上传) | 完善发票信息字段、发票详情及管理展示。 |
|
||||||
|
| 2026-04-01 | ✅ 商品虚拟销量 | 支持按商品规格设置虚拟销量,提供单个及批量设置入口。 |
|
||||||
|
| 2026-03-30 | ✅ 商品分组 | 新增商品分组管理及商品关联功能。 |
|
||||||
|
| 2026-03-24 | ✅ 会员分组 | 新增会员分组管理。 |
|
||||||
|
|
||||||
### 🔜 近期规划中
|
### 🔜 近期规划中
|
||||||
- 🔜 微信小店对接
|
- 🔜 微信小店对接
|
||||||
|
|||||||
@@ -45,32 +45,36 @@ export function getRegion (id) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页获取文章列表
|
* 分页获取文章列表
|
||||||
* @param cateId 文章分类id
|
* @param params 分页及分类查询条件
|
||||||
|
* @param options 请求选项,skipMessage 为 true 时由页面展示错误
|
||||||
*/
|
*/
|
||||||
export function articleList (params) {
|
export function articleList (params, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article`,
|
url: `/buyer/other/article`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
params
|
params,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取帮助中心文章分类列表
|
* 获取帮助中心文章分类列表
|
||||||
* @param cateId 文章分类id
|
* @param options 请求选项,skipMessage 为 true 时由页面展示错误
|
||||||
*/
|
*/
|
||||||
export function articleCateList () {
|
export function articleCateList (options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article/articleCategory/list`,
|
url: `/buyer/other/article/articleCategory/list`,
|
||||||
method: Method.GET
|
method: Method.GET,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过id获取文章
|
/** 通过文章 ID 获取正文;可由页面接管错误提示。 */
|
||||||
export function articleDetail (id) {
|
export function articleDetail (id, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article/get/${id}`,
|
url: `/buyer/other/article/get/${id}`,
|
||||||
method: Method.GET
|
method: Method.GET,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
buyer/src/api/pickupCard.js
Normal file
13
buyer/src/api/pickupCard.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 提货卡选品、试算及一次兑换接口。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-05
|
||||||
|
*/
|
||||||
|
import request, { Method } from '@/plugins/request.js';
|
||||||
|
|
||||||
|
/** 查询当前会员的卡规则及候选 SKU。 */
|
||||||
|
export const getPickupCard = id => request({ url: `/buyer/promotion/pickupCard/${encodeURIComponent(id)}`, method: Method.GET, needToken: true });
|
||||||
|
/** 服务端校验商品与地址,返回零现金试算。 */
|
||||||
|
export const previewPickupCard = data => request({ url: '/buyer/promotion/pickupCard/preview', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data: { ...data, client: 'PC' } });
|
||||||
|
/** 一次兑换;重试返回同一卡已创建的交易。 */
|
||||||
|
export const redeemPickupCard = data => request({ url: '/buyer/promotion/pickupCard/redeem', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data: { ...data, client: 'PC' } });
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
|
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
|
||||||
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
|
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
|
||||||
<span class="describe">满{{coupon.consumeThreshold}}元可用</span>
|
<span class="describe">{{ coupon.consumeThreshold > 0 ? '满' + coupon.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>使用范围:{{useScope(coupon.scopeType, coupon.storeName)}}</p>
|
<p>使用范围:{{useScope(coupon.scopeType, coupon.storeName)}}</p>
|
||||||
<p>有效期:{{coupon.endTime}}</p>
|
<p>有效期:{{coupon.endTime}}</p>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
@click="handleReceiveCoupon(item.id)"
|
@click="handleReceiveCoupon(item.id)"
|
||||||
>
|
>
|
||||||
<span v-if="item.couponType == 'PRICE'"
|
<span v-if="item.couponType == 'PRICE'"
|
||||||
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
|
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
|
||||||
<span v-if="item.couponType == 'DISCOUNT'"
|
<span v-if="item.couponType == 'DISCOUNT'"
|
||||||
>满{{ item.consumeThreshold }}打{{
|
>满{{ item.consumeThreshold }}打{{
|
||||||
item.couponDiscount
|
item.couponDiscount
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
@click="handleReceiveCoupon(item.id)"
|
@click="handleReceiveCoupon(item.id)"
|
||||||
>
|
>
|
||||||
<span v-if="item.couponType == 'PRICE'"
|
<span v-if="item.couponType == 'PRICE'"
|
||||||
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
|
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
|
||||||
<span v-if="item.couponType == 'DISCOUNT'"
|
<span v-if="item.couponType == 'DISCOUNT'"
|
||||||
>满{{ item.consumeThreshold }}打{{
|
>满{{ item.consumeThreshold }}打{{
|
||||||
item.couponDiscount
|
item.couponDiscount
|
||||||
@@ -127,26 +127,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 限时折扣 -->
|
|
||||||
<div class="item-price-row" v-if="promotionMap['DISCOUNT']">
|
|
||||||
<p>
|
|
||||||
<span class="item-price-title">促销</span>
|
|
||||||
<span class="item-promotion">限时折扣</span>
|
|
||||||
<span class="item-desc-pintuan" v-if="promotionMap['DISCOUNT'].discount">
|
|
||||||
享 {{ promotionMap["DISCOUNT"].discount }} 折
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- 第N件优惠 -->
|
|
||||||
<div class="item-price-row" v-if="promotionMap['NTH']">
|
|
||||||
<p>
|
|
||||||
<span class="item-price-title">促销</span>
|
|
||||||
<span class="item-promotion">第N件</span>
|
|
||||||
<span class="item-desc-pintuan">
|
|
||||||
第 {{ promotionMap["NTH"].nthNum || promotionMap["NTH"].num }} 件优惠
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- 拼团 -->
|
<!-- 拼团 -->
|
||||||
<div class="item-price-row" v-if="promotionMap['PINTUAN']">
|
<div class="item-price-row" v-if="promotionMap['PINTUAN']">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const brandList = computed(() => {
|
|||||||
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultMenus = [
|
const defaultMenus = [
|
||||||
{ title: '基本信息', url: '/home/UserInfo' },
|
{ title: '基本信息', url: '/home/Profile' },
|
||||||
{ title: '我的订单', url: '/home/MyOrder' },
|
{ title: '我的订单', url: '/home/MyOrder' },
|
||||||
{ title: '账户安全', url: '/home/AccountSafe' },
|
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||||
{ title: '消息', url: '/home/Msg' }
|
{ title: '消息', url: '/home/MsgList' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const username = computed(() => {
|
const username = computed(() => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 1. 用户头像与信息区 -->
|
<!-- 1. 用户头像与信息区 -->
|
||||||
<div class="user-main-info">
|
<div class="user-main-info">
|
||||||
<div class="avatar-box">
|
<div class="avatar-box">
|
||||||
<img v-if="options.avatar" :src="options.avatar" alt="avatar" />
|
<img v-if="displayAvatar" :src="displayAvatar" alt="用户头像" />
|
||||||
<div v-else class="avatar-placeholder">
|
<div v-else class="avatar-placeholder">
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
||||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||||
@@ -12,18 +12,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-greeting">
|
<div class="user-greeting">
|
||||||
<div class="badge-tag">{{ options.badgeText || '请登录' }}</div>
|
<div class="badge-tag">{{ isLoggedIn ? '已登录' : (options.badgeText || '请登录') }}</div>
|
||||||
<div class="welcome-text" :title="options.welcomeText || '欢迎来到商城'">
|
<div class="welcome-text" :title="welcomeText">
|
||||||
{{ options.welcomeText || '欢迎来到商城' }}
|
{{ welcomeText }}
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-text" :title="options.subtitle || '登录后查看专属权益'">
|
<div class="sub-text" :title="subtitle">
|
||||||
{{ options.subtitle || '登录后查看专属权益' }}
|
{{ subtitle }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. 登录 / 注册按钮组 -->
|
<!-- 已登录时提供会员入口,装修中的登录与注册文案仅用于游客。 -->
|
||||||
<div class="action-btn-row">
|
<div v-if="isLoggedIn" class="action-btn-row">
|
||||||
|
<router-link to="/home" class="action-btn login-btn">会员中心</router-link>
|
||||||
|
<router-link to="/home/MyOrder" class="action-btn register-btn">我的订单</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else class="action-btn-row">
|
||||||
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
|
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
|
||||||
{{ options.loginText || '登录' }}
|
{{ options.loginText || '登录' }}
|
||||||
</a>
|
</a>
|
||||||
@@ -35,19 +39,23 @@
|
|||||||
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||||
<div class="metrics-cards-row">
|
<div class="metrics-cards-row">
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
|
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('cart') }}</div>
|
||||||
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(options.couponUrl || '/member/coupon')">
|
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(metricUrl('coupons'))">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('coupons') }}</div>
|
||||||
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(options.orderUrl || '/member/order')">
|
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(metricUrl('orders'))">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('orders') }}</div>
|
||||||
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button v-if="hasMetricError" type="button" class="metrics-retry" @click="loadMetrics">
|
||||||
|
数量加载失败,点击重试
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
||||||
<div class="quick-service-row">
|
<div class="quick-service-row">
|
||||||
<a
|
<a
|
||||||
@@ -65,11 +73,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, reactive, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
||||||
|
import storage from '@/plugins/storage';
|
||||||
|
import { cartCount } from '@/api/cart';
|
||||||
|
import { memberCouponList } from '@/api/member';
|
||||||
|
import { getOrderList } from '@/api/order';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
options: {
|
options: {
|
||||||
@@ -86,6 +100,113 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userInfo = ref({});
|
||||||
|
const isLoggedIn = ref(false);
|
||||||
|
|
||||||
|
/** 读取现有登录缓存;缺少令牌时按游客展示,损坏的资料缓存不阻断首页。 */
|
||||||
|
function syncLoginState() {
|
||||||
|
isLoggedIn.value = Boolean(storage.getItem('accessToken'));
|
||||||
|
userInfo.value = {};
|
||||||
|
if (!isLoggedIn.value) return;
|
||||||
|
try {
|
||||||
|
const cached = JSON.parse(storage.getItem('userInfo') || '{}');
|
||||||
|
userInfo.value = cached && typeof cached === 'object' ? cached : {};
|
||||||
|
} catch {
|
||||||
|
userInfo.value = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLoginState();
|
||||||
|
|
||||||
|
// 购物车沿用顶部购物车的有效 SKU 种数;优惠券统计未使用张数,订单统计全部订单。
|
||||||
|
const metrics = reactive({
|
||||||
|
cart: { status: 'idle', value: 0 },
|
||||||
|
coupons: { status: 'idle', value: 0 },
|
||||||
|
orders: { status: 'idle', value: 0 }
|
||||||
|
});
|
||||||
|
let metricRequestId = 0;
|
||||||
|
let metricsLoading = false;
|
||||||
|
const hasMetricError = computed(() => Object.values(metrics).some(item => item.status === 'error'));
|
||||||
|
|
||||||
|
/** 游客显示 0;已登录时区分加载、失败和真实的零值,购物车随公共状态更新。 */
|
||||||
|
function metricText(key) {
|
||||||
|
if (!isLoggedIn.value) return 0;
|
||||||
|
const metric = metrics[key];
|
||||||
|
if (metric.status === 'loading' || metric.status === 'idle') return '…';
|
||||||
|
if (metric.status === 'error') return '—';
|
||||||
|
return key === 'cart' ? appStore.cartNum : metric.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将装修默认的历史地址映射到现有列表,保留商家配置的其他自定义链接。 */
|
||||||
|
function metricUrl(key) {
|
||||||
|
const isCoupon = key === 'coupons';
|
||||||
|
const configured = isCoupon ? props.options.couponUrl : props.options.orderUrl;
|
||||||
|
const legacy = isCoupon ? '/member/coupon' : '/member/order';
|
||||||
|
return !configured || configured === legacy
|
||||||
|
? (isCoupon ? '/home/Coupons' : '/home/MyOrder')
|
||||||
|
: configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 并行读取汇总;失败独立展示,离开页面后忽略旧请求,重入首页时刷新数量。 */
|
||||||
|
async function loadMetrics() {
|
||||||
|
if (metricsLoading) return;
|
||||||
|
syncLoginState();
|
||||||
|
const requestId = ++metricRequestId;
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
Object.values(metrics).forEach(item => { item.status = 'idle'; item.value = 0; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metricsLoading = true;
|
||||||
|
const page = { pageNumber: 1, pageSize: 1 };
|
||||||
|
const queries = [
|
||||||
|
['cart', props.options.showCart !== false, () => cartCount()],
|
||||||
|
['coupons', props.options.showCoupons !== false, () => memberCouponList({ ...page, memberCouponStatus: 'NEW' })],
|
||||||
|
['orders', props.options.showOrders !== false, () => getOrderList({ ...page, tag: 'ALL' })]
|
||||||
|
];
|
||||||
|
await Promise.all(queries.filter(([, visible]) => visible).map(async ([key, , fetchCount]) => {
|
||||||
|
metrics[key].status = 'loading';
|
||||||
|
try {
|
||||||
|
const response = await fetchCount();
|
||||||
|
const raw = key === 'cart' ? response?.result : response?.result?.total;
|
||||||
|
const count = Number(raw);
|
||||||
|
if (!response?.success || raw == null || !Number.isSafeInteger(count) || count < 0) {
|
||||||
|
throw new Error('无效的数量响应');
|
||||||
|
}
|
||||||
|
if (requestId !== metricRequestId) return;
|
||||||
|
metrics[key].value = count;
|
||||||
|
metrics[key].status = 'ready';
|
||||||
|
if (key === 'cart') {
|
||||||
|
appStore.setCartNum(count);
|
||||||
|
storage.setItem('cartNum', count);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (requestId === metricRequestId) metrics[key].status = 'error';
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if (requestId === metricRequestId) metricsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使离开首页前的异步响应失效,避免覆盖下一次进入后的数据。 */
|
||||||
|
function invalidateMetrics() {
|
||||||
|
metricRequestId += 1;
|
||||||
|
metricsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadMetrics);
|
||||||
|
onActivated(loadMetrics);
|
||||||
|
onDeactivated(invalidateMetrics);
|
||||||
|
onBeforeUnmount(invalidateMetrics);
|
||||||
|
|
||||||
|
const displayAvatar = computed(() => isLoggedIn.value
|
||||||
|
? userInfo.value.face
|
||||||
|
: props.options.avatar);
|
||||||
|
const welcomeText = computed(() => isLoggedIn.value
|
||||||
|
? `你好,${userInfo.value.nickName || userInfo.value.username || '会员'}`
|
||||||
|
: (props.options.welcomeText || '欢迎来到商城'));
|
||||||
|
const subtitle = computed(() => isLoggedIn.value
|
||||||
|
? '查看我的订单与专属权益'
|
||||||
|
: (props.options.subtitle || '登录后查看专属权益'));
|
||||||
|
|
||||||
function handleNavigate(url) {
|
function handleNavigate(url) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
if (/^https?:\/\//i.test(url)) {
|
if (/^https?:\/\//i.test(url)) {
|
||||||
@@ -96,10 +217,10 @@ function handleNavigate(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
@@ -114,12 +235,26 @@ function getServiceIcon(iconName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayServices = computed(() => {
|
const displayServices = computed(() => {
|
||||||
if (Array.isArray(props.options?.services) && props.options.services.length > 0) {
|
const services = Array.isArray(props.options?.services) && props.options.services.length > 0
|
||||||
return props.options.services;
|
? props.options.services
|
||||||
}
|
: defaultServices;
|
||||||
return defaultServices;
|
return services.map(normalizeService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 兼容装修中保存的旧默认入口,仅修正已知地址,不覆盖其他自定义链接。 */
|
||||||
|
function normalizeService(service) {
|
||||||
|
const legacyRoutes = {
|
||||||
|
'/member/favorites': '/home/Favorites',
|
||||||
|
'/member/footprint': '/home/MyTracks',
|
||||||
|
'/couponCenter': '/coupon'
|
||||||
|
};
|
||||||
|
const url = legacyRoutes[service.url] || service.url;
|
||||||
|
let name = service.name;
|
||||||
|
if (name === '红包' && url === '/coupon') name = '领券';
|
||||||
|
if (name === '客服' && url === '/article') name = '帮助';
|
||||||
|
return { ...service, url, name };
|
||||||
|
}
|
||||||
|
|
||||||
const customStyle = computed(() => {
|
const customStyle = computed(() => {
|
||||||
const s = props.componentStyle || {};
|
const s = props.componentStyle || {};
|
||||||
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||||
@@ -135,6 +270,14 @@ const customStyle = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.metrics-retry {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.decor-user-benefits {
|
.decor-user-benefits {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const E_COUPON_ORDER_TYPE = "E_COUPON";
|
|||||||
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
||||||
export const E_COUPON_MAX_BUY_NUM = 999;
|
export const E_COUPON_MAX_BUY_NUM = 999;
|
||||||
|
|
||||||
/** 常规立即购买(秒杀/满减/券/限时/第N件等) */
|
/** 常规立即购买(秒杀/满减/券等) */
|
||||||
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
|
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
|
||||||
export const E_COUPON_CART_PINTUAN = "PINTUAN";
|
export const E_COUPON_CART_PINTUAN = "PINTUAN";
|
||||||
export const E_COUPON_CART_POINTS = "POINTS";
|
export const E_COUPON_CART_POINTS = "POINTS";
|
||||||
@@ -72,8 +72,6 @@ export function createEmptyPromotionMap() {
|
|||||||
SECKILL: null,
|
SECKILL: null,
|
||||||
FULL_DISCOUNT: null,
|
FULL_DISCOUNT: null,
|
||||||
COUPON: [],
|
COUPON: [],
|
||||||
DISCOUNT: null,
|
|
||||||
NTH: null,
|
|
||||||
PINTUAN: null,
|
PINTUAN: null,
|
||||||
KANJIA: null,
|
KANJIA: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
>¥{{ item.price }}</span>
|
>¥{{ item.price }}</span>
|
||||||
<span v-if="item.couponType === 'DISCOUNT'"
|
<span v-if="item.couponType === 'DISCOUNT'"
|
||||||
>{{ item.couponDiscount }}折</span>
|
>{{ item.couponDiscount }}折</span>
|
||||||
<span>满{{ item.consumeThreshold }}元可用</span>
|
<span>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
class="coupon-btn"
|
class="coupon-btn"
|
||||||
size="small"
|
size="small"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="coupon-center">
|
<div class="coupon-center">
|
||||||
<BaseHeader></BaseHeader>
|
<BaseHeader></BaseHeader>
|
||||||
|
<SearchHeader></SearchHeader>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2 class="coupon-page-title">领券中心</h2>
|
<h2 class="coupon-page-title">领券中心</h2>
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ import { onMounted, reactive, ref } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Modal } from "@/utils/message";
|
import { Modal } from "@/utils/message";
|
||||||
import storage from "@/plugins/storage";
|
import storage from "@/plugins/storage";
|
||||||
|
import SearchHeader from "@/components/indexDecorate/components/SearchHeader.vue";
|
||||||
import { couponList, receiveCoupon } from "@/api/member.js";
|
import { couponList, receiveCoupon } from "@/api/member.js";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<el-card shadow="never" v-for="(item, index) in autoCoupList" :key="index" style="margin: 10px 15px">
|
<el-card shadow="never" v-for="(item, index) in autoCoupList" :key="index" style="margin: 10px 15px">
|
||||||
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color" style="font-size: 15px;">¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
|
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color" style="font-size: 15px;">¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
|
||||||
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color" style="font-size: 15px;"><span class="price">{{item.discount}}</span>折</span>
|
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color" style="font-size: 15px;"><span class="price">{{item.discount}}</span>折</span>
|
||||||
<span class="describe" style="font-size: 15px;margin-left: 5px;">满{{item.consumeThreshold}}元可用</span>
|
<span class="describe" style="font-size: 15px;margin-left: 5px;">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
<p style="font-size: 14px;">使用范围:{{useScope(item.scopeType, item.storeName)}}</p>
|
<p style="font-size: 14px;">使用范围:{{useScope(item.scopeType, item.storeName)}}</p>
|
||||||
<p style="font-size: 14px;">有效期:{{item.endTime}}</p>
|
<p style="font-size: 14px;">有效期:{{item.endTime}}</p>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
<div class="layoutAll">
|
<div class="layoutAll">
|
||||||
<el-aside class="side-bar" width="220px">
|
<el-aside class="side-bar" width="220px">
|
||||||
<div class="article-cate">文章分类</div>
|
<div class="article-cate">文章分类</div>
|
||||||
|
<p v-if="categoryError" role="alert" class="load-message">
|
||||||
|
分类加载失败,可先浏览全部文章。
|
||||||
|
<el-button link type="primary" :loading="categoryLoading" @click="getCateList">重试分类</el-button>
|
||||||
|
</p>
|
||||||
|
<el-button link type="primary" @click="onSelect('')">全部文章</el-button>
|
||||||
<el-menu
|
<el-menu
|
||||||
class="side-menu"
|
class="side-menu"
|
||||||
:key="menuKey"
|
:key="menuKey"
|
||||||
@@ -31,14 +36,22 @@
|
|||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main class="content">
|
<el-main class="content">
|
||||||
<ul class="article-list" v-show="showList">
|
<div v-if="contentError" role="alert" class="load-message">
|
||||||
|
{{ contentError }}
|
||||||
|
<el-button link type="primary" @click="retryContent">重新加载</el-button>
|
||||||
|
</div>
|
||||||
|
<el-skeleton v-if="contentLoading" :rows="5" animated />
|
||||||
|
<ul class="article-list" v-show="showList && !contentLoading && !contentError">
|
||||||
<li v-if="!articleList.length" class="article-empty">暂无文章</li>
|
<li v-if="!articleList.length" class="article-empty">暂无文章</li>
|
||||||
<li v-for="(article, index) in articleList" :key="article.id || index" @click="getDetail(article.id)">
|
<li v-for="(article, index) in articleList" :key="article.id || index" @click="getDetail(article.id)">
|
||||||
{{ article.title }}
|
{{ article.title }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-show="!showList" class="article-detail">
|
<el-pagination v-if="showList && !contentLoading && !contentError && total > params.pageSize"
|
||||||
<button type="button" class="back-btn" @click="showList = true">返回列表</button>
|
:current-page="params.pageNumber" :page-size="params.pageSize" :total="total"
|
||||||
|
layout="prev, pager, next" @current-change="changePage" />
|
||||||
|
<div v-show="!showList && !contentLoading && !contentError" class="article-detail">
|
||||||
|
<button type="button" class="back-btn" @click="getList()">返回列表</button>
|
||||||
<h2>{{ detail.title }}</h2>
|
<h2>{{ detail.title }}</h2>
|
||||||
<div class="article-html" v-html="detail.content"></div>
|
<div class="article-html" v-html="detail.content"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,82 +73,111 @@ defineOptions({ name: 'ArticleIndex' })
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const detail = ref('')
|
const detail = ref({})
|
||||||
const articleList = ref([])
|
const articleList = ref([])
|
||||||
const activeName = ref('1347456734864367616')
|
const activeName = ref('')
|
||||||
const openName = ref([])
|
const openName = ref([])
|
||||||
const menuKey = ref(0)
|
const menuKey = ref(0)
|
||||||
const params = reactive({
|
const params = reactive({ pageNumber: 1, pageSize: 20, categoryId: '', sort: 'sort' })
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 100,
|
|
||||||
categoryId: '',
|
|
||||||
sort: 'sort'
|
|
||||||
})
|
|
||||||
const showList = ref(true)
|
const showList = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const categoryError = ref(false)
|
||||||
|
const categoryLoading = ref(false)
|
||||||
|
const contentError = ref('')
|
||||||
|
const contentLoading = ref(false)
|
||||||
|
const detailId = ref('')
|
||||||
|
let contentRequestId = 0
|
||||||
|
|
||||||
function onSelect (id) {
|
/** 切换分类时重置分页,并使之前的列表或正文请求失效。 */
|
||||||
getList(id)
|
function onSelect(id) {
|
||||||
detail.value = ''
|
activeName.value = id
|
||||||
showList.value = true
|
params.pageNumber = 1
|
||||||
|
return getList(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCateList () {
|
/** 独立加载分类;分类服务不可用时保留全部文章入口,不阻断正文和列表。 */
|
||||||
articleCateList().then(res => {
|
async function getCateList() {
|
||||||
if (res.success) {
|
if (categoryLoading.value) return
|
||||||
list.value = res.result || []
|
categoryLoading.value = true
|
||||||
openName.value = []
|
categoryError.value = false
|
||||||
if (route.query.id && detail.value && detail.value.categoryId) {
|
try {
|
||||||
activeName.value = detail.value.categoryId
|
const res = await articleCateList({ skipMessage: true })
|
||||||
list.value.forEach(e => {
|
if (!res.success || !Array.isArray(res.result)) throw new Error('分类响应无效')
|
||||||
const children = e.children || []
|
list.value = res.result
|
||||||
const hitParent = String(e.id) === String(activeName.value)
|
openName.value = list.value.filter(item => Array.isArray(item.children) && item.children.length)
|
||||||
const hitChild = children.some(i => i && String(i.id) === String(activeName.value))
|
.map(item => String(item.id))
|
||||||
if ((hitParent || hitChild) && children.length) {
|
menuKey.value++
|
||||||
openName.value.push(String(e.id))
|
} catch {
|
||||||
}
|
categoryError.value = true
|
||||||
})
|
} finally {
|
||||||
} else if (list.value[0] && list.value[0].id) {
|
categoryLoading.value = false
|
||||||
activeName.value = list.value[0].id
|
|
||||||
if (list.value[0].children && list.value[0].children.length) {
|
|
||||||
openName.value.push(String(list.value[0].id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuKey.value++
|
|
||||||
if (activeName.value) {
|
|
||||||
getList(activeName.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getList (id) {
|
|
||||||
params.categoryId = id
|
|
||||||
fetchArticleList(params).then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
articleList.value = res.result.records
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getDetail (id) {
|
|
||||||
await articleDetail(id).then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
detail.value = res.result
|
|
||||||
showList.value = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const articleId = route.query.id
|
|
||||||
if (articleId) {
|
|
||||||
await getDetail(articleId)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按分类分页查询公开文章;只接收最新一次请求结果,失败不冒充空列表。 */
|
||||||
|
async function getList(id = activeName.value) {
|
||||||
|
const requestId = ++contentRequestId
|
||||||
|
showList.value = true
|
||||||
|
contentLoading.value = true
|
||||||
|
contentError.value = ''
|
||||||
|
params.categoryId = id || ''
|
||||||
|
try {
|
||||||
|
const res = await fetchArticleList({ ...params }, { skipMessage: true })
|
||||||
|
if (!res.success || !Array.isArray(res.result?.records)) throw new Error('列表响应无效')
|
||||||
|
if (requestId !== contentRequestId) return
|
||||||
|
articleList.value = res.result.records
|
||||||
|
total.value = Number(res.result.total) || 0
|
||||||
|
} catch {
|
||||||
|
if (requestId === contentRequestId) contentError.value = '文章加载失败,请稍后重试。'
|
||||||
|
} finally {
|
||||||
|
if (requestId === contentRequestId) contentLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载指定文章;失败可原地重试,过时的正文响应不得覆盖用户新选择。 */
|
||||||
|
async function getDetail(id) {
|
||||||
|
const requestId = ++contentRequestId
|
||||||
|
detailId.value = id
|
||||||
|
showList.value = false
|
||||||
|
contentLoading.value = true
|
||||||
|
contentError.value = ''
|
||||||
|
try {
|
||||||
|
const res = await articleDetail(id, { skipMessage: true })
|
||||||
|
if (!res.success || !res.result) throw new Error('文章不存在')
|
||||||
|
if (requestId !== contentRequestId) return
|
||||||
|
detail.value = res.result
|
||||||
|
} catch {
|
||||||
|
if (requestId === contentRequestId) contentError.value = '文章详情加载失败或已不存在,请稍后重试。'
|
||||||
|
} finally {
|
||||||
|
if (requestId === contentRequestId) contentLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重试当前列表或正文,保留分类和分页条件。 */
|
||||||
|
function retryContent() {
|
||||||
|
return showList.value ? getList() : getDetail(detailId.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换文章列表页码,沿用当前分类。 */
|
||||||
|
function changePage(page) {
|
||||||
|
params.pageNumber = page
|
||||||
|
return getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
getCateList()
|
getCateList()
|
||||||
|
if (route.query.id) getDetail(route.query.id)
|
||||||
|
else getList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.load-message {
|
||||||
|
padding: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px 0 40px;
|
padding: 20px 0 40px;
|
||||||
|
|||||||
51
buyer/src/pages/home/memberCenter/MemberBenefitPanel.vue
Normal file
51
buyer/src/pages/home/memberCenter/MemberBenefitPanel.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<section class="benefits-panel">
|
||||||
|
<h3>会员权益</h3>
|
||||||
|
<div class="grade-options">
|
||||||
|
<button v-for="item in enabledGrades" :key="item.grade.id" :class="{active: selectedId === item.grade.id}" @click="selectedId = item.grade.id">
|
||||||
|
{{ item.grade.gradeName }}<small v-if="item.grade.id === currentGradeId">当前等级</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="benefit-subtitle">{{ selected?.grade?.gradeName || '暂无等级' }} · 点击查看权益说明</p>
|
||||||
|
<el-empty v-if="!selected?.benefits?.length" description="暂无权益" />
|
||||||
|
<div v-else class="benefits-grid">
|
||||||
|
<button v-for="benefit in selected.benefits" :key="benefit.id" class="benefit-card" :class="{locked: !owned(benefit)}" @click="detail = benefit">
|
||||||
|
<img v-if="benefit.benefitLogo" :src="benefit.benefitLogo" alt="" />
|
||||||
|
<strong>{{ benefit.benefitName }}</strong>
|
||||||
|
<span>{{ owned(benefit) ? '已享有' : '该等级专享' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<el-dialog :model-value="!!detail" :title="detail?.benefitName || '权益说明'" width="520px" @close="detail = null">
|
||||||
|
<template v-if="detail">
|
||||||
|
<p>{{ owned(detail) ? '当前等级已享有该权益' : '此权益属于所选等级,当前尚未享有' }}</p>
|
||||||
|
<p v-if="detail.benefitType === 'POINT_MULTIPLIER'">购物积分倍率:{{ config(detail).multiplier || 1 }} 倍,同类权益取最高值。</p>
|
||||||
|
<p v-if="detail.benefitType === 'GIFT_POINT'">首次达到等级赠送 {{ config(detail).giftPoint || 0 }} 积分。</p>
|
||||||
|
<div class="benefit-description" v-html="detail.benefitDesc || '暂无说明'" />
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
const props = defineProps({gradeDetails:{type:Array,default:()=>[]},currentGradeId:{type:String,default:''}})
|
||||||
|
const selectedId = ref(''), detail = ref(null)
|
||||||
|
const enabledGrades = computed(()=>props.gradeDetails.filter(item=>item.grade?.gradeState === 'OPEN'))
|
||||||
|
const selected = computed(()=>enabledGrades.value.find(item=>item.grade.id === selectedId.value))
|
||||||
|
const current = computed(()=>enabledGrades.value.find(item=>item.grade.id === props.currentGradeId))
|
||||||
|
const owned = benefit => (current.value?.benefits || []).some(item=>item.id === benefit.id)
|
||||||
|
function config(benefit) { try { return JSON.parse(benefit.benefitConfig || '{}') } catch { return {} } }
|
||||||
|
watch([()=>props.currentGradeId,enabledGrades],()=>{
|
||||||
|
if(!selectedId.value || !enabledGrades.value.some(item=>item.grade.id===selectedId.value)) selectedId.value=props.currentGradeId || enabledGrades.value[0]?.grade.id || ''
|
||||||
|
},{immediate:true})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.benefits-panel { margin:20px 0; padding:24px; background:#fff; border:1px solid #eee; border-radius:8px; }
|
||||||
|
h3 { margin:0 0 20px; font-size:18px; } .grade-options { display:flex; flex-wrap:wrap; gap:10px; }
|
||||||
|
.grade-options button { padding:10px 18px; border:1px solid #ddd; background:#fff; border-radius:6px; cursor:pointer; }
|
||||||
|
.grade-options button.active { background:#fff8eb; border-color:#c99a47; color:#866021; } small { margin-left:8px; }
|
||||||
|
.benefit-subtitle { color:#777; margin:20px 0; }.benefits-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:16px; }
|
||||||
|
.benefit-card { padding:20px 10px; display:flex; align-items:center; flex-direction:column; gap:12px; border:1px solid #f0e3cb; background:#fffcf6; border-radius:8px; cursor:pointer; }
|
||||||
|
.benefit-card img { width:48px; height:48px; object-fit:contain; }.benefit-card span { font-size:12px; color:#986d2a; }
|
||||||
|
.benefit-card.locked { background:#fafafa; border-color:#eee; color:#888; }.locked img { filter:grayscale(1); opacity:.5; }
|
||||||
|
.benefit-description { line-height:1.8; overflow-wrap:anywhere; }.benefit-description :deep(img) { max-width:100%; }
|
||||||
|
</style>
|
||||||
@@ -25,6 +25,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<MemberBenefitPanel :grade-details="gradeDetails" :current-grade-id="String(currentGradeId)" />
|
||||||
|
|
||||||
<div class="grade-panel">
|
<div class="grade-panel">
|
||||||
<div class="grade-tabs">
|
<div class="grade-tabs">
|
||||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
||||||
@@ -87,6 +89,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import MemberBenefitPanel from './MemberBenefitPanel.vue'
|
||||||
import { memberGradeCurrent, memberGradeList, memberGradeRules } from '@/api/member.js'
|
import { memberGradeCurrent, memberGradeList, memberGradeRules } from '@/api/member.js'
|
||||||
|
|
||||||
defineOptions({ name: 'MemberGrade' })
|
defineOptions({ name: 'MemberGrade' })
|
||||||
|
|||||||
@@ -176,6 +176,7 @@
|
|||||||
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="cancelAvail" title="请选择取消订单原因">
|
<el-dialog v-model="cancelAvail" title="请选择取消订单原因">
|
||||||
|
<el-alert v-if="cancelIsPickup" title="此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。" type="warning" :closable="false" style="margin-bottom:16px" />
|
||||||
<el-radio-group v-model="cancelParams.reason">
|
<el-radio-group v-model="cancelParams.reason">
|
||||||
<el-radio :label="item.reason" v-for="item in cancelReason" :key="item.id">
|
<el-radio :label="item.reason" v-for="item in cancelReason" :key="item.id">
|
||||||
{{ item.reason }}
|
{{ item.reason }}
|
||||||
@@ -227,6 +228,7 @@ const changeWay = ref(['全部', '待付款', '待发货', '待收货/使用']);
|
|||||||
const activeOrderTab = ref(0);
|
const activeOrderTab = ref(0);
|
||||||
const total = ref(0); // 数据总数
|
const total = ref(0); // 数据总数
|
||||||
const spinShow = ref(false); // 加载状态
|
const spinShow = ref(false); // 加载状态
|
||||||
|
const cancelIsPickup = ref(false);
|
||||||
const cancelAvail = ref(false); // 取消订单modal控制
|
const cancelAvail = ref(false); // 取消订单modal控制
|
||||||
const cancelReason = ref([]); // 取消订单原因
|
const cancelReason = ref([]); // 取消订单原因
|
||||||
|
|
||||||
@@ -365,6 +367,7 @@ function changePageSize (val) { // 修改页数
|
|||||||
}
|
}
|
||||||
function handleCancelOrder (sn) {
|
function handleCancelOrder (sn) {
|
||||||
// 取消订单
|
// 取消订单
|
||||||
|
cancelIsPickup.value = !!orderList.value.find(order => order.sn === sn)?.pickupCardId;
|
||||||
cancelParams.orderSn = sn;
|
cancelParams.orderSn = sn;
|
||||||
afterSaleReason('CANCEL').then(res => {
|
afterSaleReason('CANCEL').then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
|||||||
@@ -341,6 +341,7 @@
|
|||||||
v-model="cancelAvail"
|
v-model="cancelAvail"
|
||||||
title="请选择取消订单原因"
|
title="请选择取消订单原因"
|
||||||
>
|
>
|
||||||
|
<el-alert v-if="order.order?.pickupCardId" title="此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。" type="warning" :closable="false" style="margin-bottom:16px" />
|
||||||
<el-radio-group
|
<el-radio-group
|
||||||
v-model="cancelParams.reason"
|
v-model="cancelParams.reason"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
v-if="item.couponType === 'DISCOUNT'"
|
v-if="item.couponType === 'DISCOUNT'"
|
||||||
class="global_color coupon-amount"
|
class="global_color coupon-amount"
|
||||||
><span class="price">{{ item.discount }}</span>折</span>
|
><span class="price">{{ item.discount }}</span>折</span>
|
||||||
<span class="describe">满{{ item.consumeThreshold }}元可用</span>
|
<span class="describe">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
||||||
<p class="coupon-expire">有效期:{{ formatValidity(item) }}</p>
|
<p class="coupon-expire">有效期:{{ formatValidity(item) }}</p>
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper my-gift-cards">
|
<div class="wrapper my-gift-cards">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
|
<div class="gift-page-heading">
|
||||||
|
<div><h2>我的礼品卡</h2><p>查看卡片权益,兑换你的心意好礼</p></div>
|
||||||
|
</div>
|
||||||
<div class="bind-section mb_20">
|
<div class="bind-section mb_20">
|
||||||
<div class="section-title">兑换礼品卡</div>
|
<div class="section-title">兑换礼品卡</div>
|
||||||
<el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" inline class="bind-form">
|
<el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" class="bind-form">
|
||||||
<el-form-item label="卡号" prop="cardNo" label-width="60px">
|
<el-form-item label="卡号" prop="cardNo" label-width="60px">
|
||||||
<el-input v-model="bindForm.cardNo" placeholder="请输入卡号" maxlength="64" style="width: 220px" />
|
<el-input v-model="bindForm.cardNo" placeholder="请输入卡号" maxlength="64" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="兑换码" prop="cardSecret" label-width="70px">
|
<el-form-item label="兑换码" prop="cardSecret" label-width="70px">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="bindForm.cardSecret"
|
v-model="bindForm.cardSecret"
|
||||||
placeholder="请输入兑换码(卡密)"
|
placeholder="请输入兑换码(卡密)"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
style="width: 220px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label-width="0px">
|
<el-form-item label-width="0px">
|
||||||
@@ -25,15 +26,20 @@
|
|||||||
<el-tabs v-model="statusTab" class="status-tabs mb_16" @tab-click="(tab) => onStatusTab(tab.paneName)">
|
<el-tabs v-model="statusTab" class="status-tabs mb_16" @tab-click="(tab) => onStatusTab(tab.paneName)">
|
||||||
<el-tab-pane label="可用" name="AVAILABLE" />
|
<el-tab-pane label="可用" name="AVAILABLE" />
|
||||||
<el-tab-pane label="不可用" name="UNAVAILABLE" />
|
<el-tab-pane label="不可用" name="UNAVAILABLE" />
|
||||||
<el-tab-pane label="待激活" name="PENDING_ACTIVATION" />
|
<el-tab-pane name="PENDING_ACTIVATION">
|
||||||
|
<template #label>
|
||||||
|
<span class="pending-tab-label">待激活<span v-if="pendingCount !== null" class="pending-count">{{ pendingCount }}</span></span>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<div class="card-list-wrap">
|
<div class="card-list-wrap">
|
||||||
<el-skeleton v-if="loading" fix />
|
<el-skeleton v-if="loading" :rows="5" animated />
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<empty v-if="list.length === 0" />
|
<div v-if="loadFailed" class="gift-load-error"><p>卡片加载失败,请稍后重试</p><el-button @click="getList">重新加载</el-button></div>
|
||||||
|
<el-empty v-else-if="list.length === 0" :description="emptyDescription" :image-size="100" />
|
||||||
<div v-else class="gift-card-grid">
|
<div v-else class="gift-card-grid">
|
||||||
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item" :class="{ 'is-unavailable': statusTab === 'UNAVAILABLE' }">
|
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item" :class="{ 'is-unavailable': statusTab === 'UNAVAILABLE', 'is-pickup': item.cardType === 'PICKUP' }">
|
||||||
<div class="gcc-card-header">
|
<div class="gcc-card-header">
|
||||||
<div class="gcc-header-pattern" aria-hidden="true" />
|
<div class="gcc-header-pattern" aria-hidden="true" />
|
||||||
<div class="gcc-header-inner">
|
<div class="gcc-header-inner">
|
||||||
@@ -42,17 +48,19 @@
|
|||||||
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
||||||
<span class="gcc-status-chip">{{ statusChipLabel }}</span>
|
<span class="gcc-status-chip">{{ statusChipLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gcc-face"><span class="gcc-face-symbol">¥</span>{{ formatYuan(item.faceValue) }}</div>
|
<div v-if="item.cardType === 'PICKUP'" class="gcc-face">兑换 <strong>{{ item.pickupCount }}</strong> 种商品</div>
|
||||||
<div class="gcc-cardno">{{ item.cardNo }}</div>
|
<div v-else class="gcc-face"><span class="gcc-face-symbol">¥</span>{{ formatYuan(item.faceValue) }}</div>
|
||||||
|
<div class="gcc-cardno">卡号 {{ item.cardNo }}</div>
|
||||||
|
<div class="gcc-expire">{{ formatValidity(item) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gcc-header-right">
|
<div class="gcc-header-right">
|
||||||
<div class="gcc-type">现金卡</div>
|
<div class="gcc-type">{{ item.cardType === 'PICKUP' ? '提货卡' : '现金卡' }}</div>
|
||||||
<div class="gcc-expire">{{ formatValidity(item) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gcc-card-body">
|
<div class="gcc-card-body">
|
||||||
<div class="gcc-row">
|
<div v-if="item.cardType === 'PICKUP'" class="gcc-row">每种一件 · 一次兑完 · 免邮</div>
|
||||||
|
<div v-else class="gcc-row">
|
||||||
当前余额:<span class="gcc-strong" v-if="item.balance != null">¥{{ $filters.unitPrice(item.balance) }}</span><span class="gcc-strong" v-else>—</span>
|
当前余额:<span class="gcc-strong" v-if="item.balance != null">¥{{ $filters.unitPrice(item.balance) }}</span><span class="gcc-strong" v-else>—</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -112,6 +120,10 @@ const router = useRouter();
|
|||||||
const statusTab = ref("AVAILABLE");
|
const statusTab = ref("AVAILABLE");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
const pendingCount = ref(null);
|
||||||
|
const loadFailed = ref(false);
|
||||||
|
let listRequestId = 0;
|
||||||
|
let pendingCountRequestId = 0;
|
||||||
const list = ref([]);
|
const list = ref([]);
|
||||||
const params = reactive({
|
const params = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
@@ -130,7 +142,6 @@ const bindRules = {
|
|||||||
cardSecret: [{ required: true, message: "请输入兑换码", trigger: "blur" }],
|
cardSecret: [{ required: true, message: "请输入兑换码", trigger: "blur" }],
|
||||||
};
|
};
|
||||||
const noticeModal = ref(false);
|
const noticeModal = ref(false);
|
||||||
const serviceHotline = ref("12221111");
|
|
||||||
const activatingId = ref("");
|
const activatingId = ref("");
|
||||||
const bindFormRef = ref(null);
|
const bindFormRef = ref(null);
|
||||||
|
|
||||||
@@ -153,11 +164,30 @@ const statusChipLabel = computed(() => {
|
|||||||
const map = { AVAILABLE: "可用", UNAVAILABLE: "不可用", PENDING_ACTIVATION: "待激活" };
|
const map = { AVAILABLE: "可用", UNAVAILABLE: "不可用", PENDING_ACTIVATION: "待激活" };
|
||||||
return map[statusTab.value] || "";
|
return map[statusTab.value] || "";
|
||||||
});
|
});
|
||||||
|
const emptyDescription = computed(() => ({
|
||||||
|
AVAILABLE: "暂无可用礼品卡,可输入卡号和兑换码添加",
|
||||||
|
UNAVAILABLE: "暂无不可用礼品卡",
|
||||||
|
PENDING_ACTIVATION: "暂无待激活礼品卡",
|
||||||
|
}[statusTab.value]));
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 复用会员分页接口的 total 获取全部待激活数量,独立于当前标签和每页条数。 */
|
||||||
|
async function refreshPendingCount() {
|
||||||
|
const requestId = ++pendingCountRequestId;
|
||||||
|
try {
|
||||||
|
const res = await getGiftCardCashMemberCardPage({ memberCardStatus: "PENDING_ACTIVATION", pageNumber: 1, pageSize: 1 });
|
||||||
|
if (requestId === pendingCountRequestId && res.success && res.result) {
|
||||||
|
pendingCount.value = Number(res.result.total) || 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 统计失败时保留已知数量,不将未知数量显示成零。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatYuan(val) {
|
function formatYuan(val) {
|
||||||
if (val == null || val === "") {
|
if (val == null || val === "") {
|
||||||
return "—";
|
return "—";
|
||||||
@@ -195,21 +225,32 @@ function onStatusTab(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getList() {
|
function getList() {
|
||||||
|
const requestId = ++listRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
loadFailed.value = false;
|
||||||
const p = { ...params };
|
const p = { ...params };
|
||||||
if (p.memberCardStatus) {
|
if (p.memberCardStatus) {
|
||||||
p.memberCardStatus = String(p.memberCardStatus);
|
p.memberCardStatus = String(p.memberCardStatus);
|
||||||
}
|
}
|
||||||
getGiftCardCashMemberCardPage(p)
|
getGiftCardCashMemberCardPage(p)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (requestId !== listRequestId) return;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
list.value = res.result.records || [];
|
list.value = res.result.records || [];
|
||||||
total.value = res.result.total || 0;
|
total.value = res.result.total || 0;
|
||||||
|
if (p.memberCardStatus === "PENDING_ACTIVATION") {
|
||||||
|
++pendingCountRequestId;
|
||||||
|
pendingCount.value = Number(total.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadFailed.value = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (requestId !== listRequestId) return;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
loadFailed.value = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +291,7 @@ function submitBind() {
|
|||||||
resetBindForm();
|
resetBindForm();
|
||||||
params.pageNumber = 1;
|
params.pageNumber = 1;
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -274,6 +316,7 @@ function onCardPrimaryAction(item) {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
Message.success(res.message || "激活成功");
|
Message.success(res.message || "激活成功");
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -285,7 +328,7 @@ function onCardPrimaryAction(item) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statusTab.value === "AVAILABLE") {
|
if (statusTab.value === "AVAILABLE") {
|
||||||
router.push({ path: "/goodsList" });
|
router.push(item.cardType === "PICKUP" ? { name: "PickupCard", query: { id: resolveMemberCardId(item) } } : { path: "/goodsList" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,195 +351,125 @@ function isActivating(item) {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.my-gift-cards {
|
.my-gift-cards {
|
||||||
|
.gift-page-heading {
|
||||||
|
padding: 10px 4px 22px;
|
||||||
|
h2 { margin: 0 0 8px; font-size: 22px; font-weight: 600; color: #252833; }
|
||||||
|
p { margin: 0; font-size: 13px; color: #909399; }
|
||||||
|
}
|
||||||
.bind-section {
|
.bind-section {
|
||||||
padding: 16px 18px;
|
padding: 20px 24px 24px;
|
||||||
border-radius: 8px;
|
border-radius: 10px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #f3e7e8;
|
||||||
@include white_background_color();
|
background: #fff9f9;
|
||||||
}
|
|
||||||
.section-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
@include title_color($light_title_color);
|
|
||||||
}
|
}
|
||||||
|
.section-title { margin-bottom: 18px; font-size: 15px; font-weight: 600; color: #303133; }
|
||||||
.bind-form {
|
.bind-form {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
||||||
align-items: flex-end;
|
align-items: start;
|
||||||
gap: 4px 8px;
|
gap: 16px;
|
||||||
|
:deep(.el-form-item) { margin: 0; min-width: 0; }
|
||||||
|
:deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
:deep(.el-input__wrapper) { min-height: 36px; box-sizing: border-box; background: #fff; }
|
||||||
|
:deep(.el-button) { height: 36px; min-width: 88px; border-radius: 6px; }
|
||||||
}
|
}
|
||||||
.status-tabs {
|
.status-tabs {
|
||||||
:deep(.el-tabs__header) {
|
margin-top: 28px;
|
||||||
margin-bottom: 0;
|
:deep(.el-tabs__header) { margin-bottom: 0; }
|
||||||
}
|
:deep(.el-tabs__item) { height: 48px; font-size: 15px; }
|
||||||
|
:deep(.el-tabs__nav-wrap::after) { height: 1px; background: #eceef2; }
|
||||||
}
|
}
|
||||||
.card-list-wrap {
|
.pending-tab-label { display: inline-flex; align-items: center; gap: 8px; }
|
||||||
position: relative;
|
.pending-count {
|
||||||
min-height: 200px;
|
display: inline-flex;
|
||||||
margin-top: 24px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: $theme_color;
|
||||||
|
background: #fff0f1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.card-list-wrap { position: relative; min-height: 290px; margin-top: 24px; }
|
||||||
|
.gift-load-error { padding: 64px 0; text-align: center; color: #909399; }
|
||||||
.gift-card-grid {
|
.gift-card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 16px;
|
gap: 24px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
.gcc-card-item {
|
.gcc-card-item {
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
border: 1px solid #f0e3e4;
|
||||||
@include white_background_color();
|
border-radius: 12px;
|
||||||
}
|
background: #fff;
|
||||||
.gcc-card-header {
|
transition: box-shadow .2s, border-color .2s;
|
||||||
position: relative;
|
&:hover { border-color: #edc9cd; box-shadow: 0 6px 20px rgba(100, 33, 40, .06); }
|
||||||
min-height: 128px;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
|
|
||||||
}
|
}
|
||||||
|
.gcc-card-header { position: relative; overflow: hidden; background: linear-gradient(120deg, #fff3f3, #fff9f7); }
|
||||||
.gcc-header-pattern {
|
.gcc-header-pattern {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
width: 180px;
|
||||||
opacity: 0.22;
|
height: 180px;
|
||||||
|
right: -70px;
|
||||||
|
bottom: -110px;
|
||||||
|
border: 24px solid rgba(211, 134, 141, .07);
|
||||||
|
border-radius: 50%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
-36deg,
|
|
||||||
transparent,
|
|
||||||
transparent 5px,
|
|
||||||
rgba(255, 255, 255, 0.45) 5px,
|
|
||||||
rgba(255, 255, 255, 0.45) 6px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.gcc-header-inner {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 16px 14px 14px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.gcc-header-left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
.gcc-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
.gcc-name-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.gcc-status-chip {
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
.gcc-face {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 26px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.gcc-face-symbol {
|
|
||||||
margin-right: 2px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.gcc-cardno {
|
|
||||||
margin-top: 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
opacity: 0.75;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.gcc-header-right {
|
|
||||||
text-align: right;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.gcc-type {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.gcc-expire {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.gcc-card-body {
|
|
||||||
padding: 14px 14px 16px;
|
|
||||||
}
|
|
||||||
.gcc-row {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.85;
|
|
||||||
@include title_color($light_title_color);
|
|
||||||
}
|
|
||||||
.gcc-muted {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.gcc-strong {
|
|
||||||
color: #ff6b22;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.gcc-link {
|
|
||||||
color: $theme_color;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.gcc-service {
|
|
||||||
color: #515a6e;
|
|
||||||
}
|
}
|
||||||
|
.gcc-header-inner { position: relative; display: flex; gap: 12px; padding: 22px 20px 18px; }
|
||||||
|
.gcc-header-left { flex: 1; min-width: 0; }
|
||||||
|
.gcc-name-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
|
.gcc-title { max-width: 100%; overflow-wrap: anywhere; font-size: 15px; font-weight: 600; line-height: 1.5; color: #523338; }
|
||||||
|
.gcc-status-chip { padding: 1px 7px; border-radius: 4px; background: rgba(255,255,255,.8); color: #a9757b; font-size: 11px; line-height: 18px; }
|
||||||
|
.gcc-face { margin-top: 18px; color: #a63e4a; font-size: 30px; font-weight: 600; line-height: 1.3; }
|
||||||
|
.gcc-face strong { font-size: 32px; font-weight: 600; }
|
||||||
|
.gcc-face-symbol { margin-right: 3px; font-size: 18px; }
|
||||||
|
.gcc-cardno { margin-top: 18px; color: #96777b; font-size: 12px; overflow-wrap: anywhere; letter-spacing: .03em; }
|
||||||
|
.gcc-header-right { flex-shrink: 0; text-align: right; }
|
||||||
|
.gcc-type { padding: 3px 7px; border: 1px solid #e9cdd0; border-radius: 4px; font-size: 12px; color: #a46f75; white-space: nowrap; }
|
||||||
|
.gcc-expire { margin-top: 8px; color: #a08a8d; font-size: 12px; }
|
||||||
|
.gcc-card-body { padding: 16px 20px 20px; }
|
||||||
|
.gcc-row { min-height: 22px; color: #777e89; font-size: 13px; line-height: 22px; }
|
||||||
|
.gcc-strong { color: #a63e4a; font-weight: 600; }
|
||||||
.gcc-action {
|
.gcc-action {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 16px;
|
||||||
padding: 9px 16px;
|
padding: 10px 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 6px;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
transition: opacity .2s;
|
||||||
background: $theme_color;
|
&:hover:not(.is-disabled) { opacity: .85; }
|
||||||
transition: opacity 0.2s, background 0.2s;
|
&:focus-visible { outline: 2px solid $theme_color; outline-offset: 3px; }
|
||||||
}
|
&.is-disabled { color: #aaa; background: #f3f4f6; cursor: not-allowed; }
|
||||||
.gcc-action:hover:not(.is-disabled) {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
.gcc-action.is-disabled {
|
|
||||||
color: #c5c8ce;
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
}
|
||||||
|
.is-pickup .gcc-face { font-size: 22px; }
|
||||||
.gcc-card-item.is-unavailable {
|
.gcc-card-item.is-unavailable {
|
||||||
.gcc-card-header {
|
border-color: #e8eaed;
|
||||||
background: linear-gradient(125deg, #c9c9c9 0%, #a8a8a8 100%);
|
.gcc-card-header { background: #f5f6f8; }
|
||||||
}
|
.gcc-title, .gcc-face, .gcc-strong { color: #92969e; }
|
||||||
.gcc-strong {
|
.gcc-cardno, .gcc-expire, .gcc-status-chip, .gcc-type { color: #a1a5ad; }
|
||||||
color: #999;
|
.gcc-type { border-color: #dddfe4; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.page-size {
|
.page-size { display: flex; justify-content: flex-end; margin-top: 28px; padding-top: 20px; border-top: 1px solid #f0f1f4; }
|
||||||
display: flex;
|
@media (max-width: 900px) {
|
||||||
justify-content: flex-end;
|
.bind-form { grid-template-columns: minmax(0, 1fr); gap: 22px; }
|
||||||
margin-top: 20px;
|
.bind-form :deep(.el-form-item__label) { width: 70px !important; }
|
||||||
|
.bind-form :deep(.el-form-item:last-child .el-form-item__content) { padding-left: 70px; }
|
||||||
|
.gift-card-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.gcc-notice-text {
|
.gcc-notice-text { line-height: 1.7; color: #515a6e; margin-bottom: 16px; }
|
||||||
line-height: 1.7;
|
.gcc-notice-footer { text-align: right; }
|
||||||
color: #515a6e;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.gcc-notice-footer {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
194
buyer/src/pages/home/userCenter/PickupCard.vue
Normal file
194
buyer/src/pages/home/userCenter/PickupCard.vue
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<!--
|
||||||
|
提货卡选品与收货地址确认,兑换成功后进入普通订单履约。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-05
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<UserCenterLayout>
|
||||||
|
<div v-loading="loading" class="pickup-page">
|
||||||
|
<header class="pickup-heading">
|
||||||
|
<div><div class="pickup-eyebrow">提货卡兑换</div><h2>{{ detail.giftCardName || '选择你的好礼' }}</h2></div>
|
||||||
|
<span v-if="detail.credentialId" class="pickup-validity">提货权益</span>
|
||||||
|
</header>
|
||||||
|
<el-result v-if="error" icon="warning" :title="error"><template #extra><el-button @click="load">重新加载</el-button></template></el-result>
|
||||||
|
<template v-else-if="detail.credentialId">
|
||||||
|
<el-result v-if="completed || detail.tradeSn" icon="success" title="该提货卡已兑换"><template #extra><el-button type="primary" @click="router.push({name:'MyOrder'})">查看订单</el-button></template></el-result>
|
||||||
|
<template v-else>
|
||||||
|
<div class="pickup-rules"><strong>{{ ruleText }}</strong><span>每种一件</span><span>一次兑完</span><span>免邮</span></div>
|
||||||
|
<section class="pickup-products">
|
||||||
|
<div class="pickup-section-title"><h3>选择兑换商品</h3><span>共 {{ detail.goods?.length || 0 }} 种商品 · 已选 <b>{{ selected.length }}</b> / {{ detail.pickupCount }} 种</span></div>
|
||||||
|
<el-checkbox-group v-model="selected" :max="detail.pickupCount" :disabled="busy || detail.pickupMode === 'ALL'" aria-label="可兑换商品">
|
||||||
|
<div class="pickup-grid">
|
||||||
|
<el-checkbox v-for="goods in detail.goods" :key="goods.id" :value="goods.id" :disabled="!goods.available" class="pickup-item" :class="{'is-selected': selected.includes(goods.id), 'is-unavailable': !goods.available}">
|
||||||
|
<div class="pickup-image"><el-image :src="goods.thumbnail" fit="contain" :alt="goods.goodsName"><template #error><span class="pickup-image-fallback">暂无图片</span></template></el-image></div>
|
||||||
|
<div class="pickup-goods-info">
|
||||||
|
<strong class="pickup-goods-name" :title="goods.goodsName">{{ goods.goodsName }}</strong>
|
||||||
|
<div class="pickup-specs" :title="specsText(goods.specs)">{{ specsText(goods.specs) }}</div>
|
||||||
|
<div class="pickup-store">{{ goods.storeName }}</div>
|
||||||
|
<div v-if="!goods.available" class="pickup-goods-unavailable">暂不可兑换</div>
|
||||||
|
</div>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</section>
|
||||||
|
<section class="pickup-address">
|
||||||
|
<div class="pickup-section-title"><h3>收货地址</h3><div><el-button link :disabled="busy" @click="router.push({name:'MyAddress'})">管理地址</el-button><el-button link :disabled="busy" @click="loadAddresses">刷新</el-button></div></div>
|
||||||
|
<el-select v-model="addressId" placeholder="请选择收货地址" :disabled="busy" aria-label="收货地址" class="pickup-address-select">
|
||||||
|
<el-option v-for="address in addresses" :key="address.id" :value="address.id" :label="addressText(address)" />
|
||||||
|
</el-select>
|
||||||
|
<p v-if="!addresses.length" class="pickup-address-empty">还没有收货地址,请先添加地址后刷新。</p>
|
||||||
|
</section>
|
||||||
|
<footer class="pickup-checkout">
|
||||||
|
<div class="pickup-selection"><strong>已选 <b>{{ selected.length }}</b> / {{ detail.pickupCount }} 种商品</strong><span>{{ selectionHint }}</span></div>
|
||||||
|
<div class="pickup-submit"><div class="pickup-payable">实付 <strong>¥0<span>.00</span></strong><small>免运费</small></div><el-button type="primary" :loading="busy" :disabled="!canSubmit" @click="submit">确认兑换</el-button></div>
|
||||||
|
</footer>
|
||||||
|
<p class="pickup-terms">兑换成功后取消不退权益,不支持退款或退货。</p>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</UserCenterLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { memberAddress } from '@/api/address';
|
||||||
|
import storage from '@/plugins/storage';
|
||||||
|
import { getPickupCard, previewPickupCard, redeemPickupCard } from '@/api/pickupCard';
|
||||||
|
|
||||||
|
const route = useRoute(), router = useRouter();
|
||||||
|
const detail = ref({}), selected = ref([]), addresses = ref([]), addressId = ref('');
|
||||||
|
const loading = ref(false), busy = ref(false), error = ref(''), completed = ref(false);
|
||||||
|
let active = true, requestVersion = 0;
|
||||||
|
const owner = storage.getItem('userInfo') || '';
|
||||||
|
/** 账号切换后丢弃旧请求结果,避免展示上一个账号的卡和地址。 */
|
||||||
|
function sameOwner() { return active && owner === (storage.getItem('userInfo') || ''); }
|
||||||
|
const canSubmit = computed(() => selected.value.length === detail.value.pickupCount && !!addressId.value && !loading.value && !detail.value.goods?.some(g => selected.value.includes(g.id) && !g.available));
|
||||||
|
const ruleText = computed(() => detail.value.pickupMode === 'ALL' ? '兑换以下全部商品' : `从以下商品任选 ${detail.value.pickupCount || 1} 种`);
|
||||||
|
const selectionHint = computed(() => {
|
||||||
|
const remaining = (detail.value.pickupCount || 0) - selected.value.length;
|
||||||
|
if (remaining > 0) return `还需选择 ${remaining} 种商品`;
|
||||||
|
return addressId.value ? '商品已选齐,请核对收货地址' : '请选择收货地址';
|
||||||
|
});
|
||||||
|
/** 兼容键值及旧数组规格,过滤图片元数据,避免展示原始 JSON。 */
|
||||||
|
function specsText(value) {
|
||||||
|
try {
|
||||||
|
const specs = typeof value === 'string' ? JSON.parse(value || '{}') : value;
|
||||||
|
const values = Array.isArray(specs) ? specs.filter(s => s && s.specName !== 'images').map(s => s.specValue) : Object.entries(specs || {}).filter(([key]) => key !== 'images').map(([, val]) => val);
|
||||||
|
return values.filter(v => typeof v === 'string' || typeof v === 'number').join(' / ') || '默认规格';
|
||||||
|
} catch { return typeof value === 'string' && !/^[\s]*[\[{]/.test(value) ? value : '默认规格'; }
|
||||||
|
}
|
||||||
|
/** 地址仅用于展示,提交后仍由服务端校验当前会员归属。 */
|
||||||
|
function addressText(address) { return `${address.name} ${address.mobile} ${address.consigneeAddressPath || ''} ${address.detail || ''}`; }
|
||||||
|
/** 刷新地址,保留仍存在的选择。 */
|
||||||
|
async function loadAddresses() {
|
||||||
|
const response = await memberAddress();
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '地址加载失败');
|
||||||
|
addresses.value = response.result?.records || response.result || [];
|
||||||
|
if (!addresses.value.some(a => a.id === addressId.value)) addressId.value = (addresses.value.find(a => a.isDefault) || addresses.value[0])?.id || '';
|
||||||
|
}
|
||||||
|
/** 加载卡详情;离页或新请求发起后丢弃旧响应。 */
|
||||||
|
async function load() {
|
||||||
|
const version = ++requestVersion;
|
||||||
|
loading.value = true; error.value = '';
|
||||||
|
try {
|
||||||
|
const response = await getPickupCard(route.query.id);
|
||||||
|
if (!sameOwner() || version !== requestVersion) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '提货卡加载失败');
|
||||||
|
detail.value = response.result;
|
||||||
|
selected.value = detail.value.pickupMode === 'ALL' ? detail.value.goods.map(g => g.id) : [];
|
||||||
|
await loadAddresses();
|
||||||
|
} catch (e) { if (sameOwner() && version === requestVersion) error.value = e.message || '加载失败,请重试'; }
|
||||||
|
finally { if (sameOwner() && version === requestVersion) loading.value = false; }
|
||||||
|
}
|
||||||
|
/** 先服务端试算再确认核销,网络异常保留选项,允许幂等重试。 */
|
||||||
|
async function submit() {
|
||||||
|
if (busy.value || !canSubmit.value || !sameOwner()) return;
|
||||||
|
busy.value = true;
|
||||||
|
const payload = { credentialId: detail.value.credentialId, skuIds: [...selected.value], addressId: addressId.value };
|
||||||
|
try {
|
||||||
|
const preview = await previewPickupCard(payload);
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!preview.success) throw new Error(preview.message || '兑换校验失败');
|
||||||
|
await ElMessageBox.confirm(`本次兑换 ${selected.value.length} 种商品,每种一件,免邮且实付为零。兑换后取消不退权益,不支持退款或退货。`, '确认兑换', { confirmButtonText: '确认兑换', cancelButtonText: '返回检查' });
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
const response = await redeemPickupCard(payload);
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '兑换失败');
|
||||||
|
completed.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
if (sameOwner() && e !== 'cancel' && e !== 'close') {
|
||||||
|
// 网络超时可能发生在成功提交后,查询原卡确认结果,不重复创建订单。
|
||||||
|
try { const status = await getPickupCard(payload.credentialId); if (sameOwner() && status.success && status.result?.tradeSn) { detail.value = status.result; completed.value = true; return; } } catch { /* 保留原错误供重试 */ }
|
||||||
|
ElMessage.error(e.message || '兑换结果待确认,请重试或查看订单');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { if (sameOwner()) busy.value = false; }
|
||||||
|
}
|
||||||
|
onMounted(load);
|
||||||
|
onBeforeUnmount(() => { active = false; requestVersion++; });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pickup-page { padding: 10px 4px 0; color: #303133; }
|
||||||
|
.pickup-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 20px; }
|
||||||
|
.pickup-eyebrow { margin-bottom: 8px; color: #909399; font-size: 12px; }
|
||||||
|
.pickup-heading h2 { margin: 0; color: #252833; font-size: 22px; font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.pickup-validity { padding: 5px 10px; border: 1px solid #eee; border-radius: 4px; font-size: 12px; color: #777e89; white-space: nowrap; }
|
||||||
|
.pickup-rules { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding: 16px 18px; border-radius: 6px; background: #f7f8fa; font-size: 13px; color: #858b95; }
|
||||||
|
.pickup-rules strong { color: #434953; font-weight: 500; }
|
||||||
|
.pickup-products { margin-top: 28px; }
|
||||||
|
.pickup-section-title { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 16px; }
|
||||||
|
.pickup-section-title h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||||
|
.pickup-section-title > span { color: #909399; font-size: 13px; }
|
||||||
|
.pickup-section-title b { color: $theme_color; font-weight: 500; }
|
||||||
|
.pickup-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; max-height: 570px; overflow-y: auto; padding: 2px 4px 2px 2px; }
|
||||||
|
.pickup-item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #e9ebef;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color .2s, box-shadow .2s;
|
||||||
|
&:hover:not(.is-unavailable) { border-color: #e7acb1; }
|
||||||
|
&.is-selected { border-color: $theme_color; box-shadow: 0 0 0 1px $theme_color; }
|
||||||
|
&.is-unavailable { background: #f8f9fa; }
|
||||||
|
:deep(.el-checkbox__input) { position: absolute; z-index: 1; top: 12px; right: 12px; }
|
||||||
|
:deep(.el-checkbox__label) { display: block; padding: 0; width: 100%; white-space: normal; color: inherit; }
|
||||||
|
}
|
||||||
|
.pickup-image { display: flex; justify-content: center; align-items: center; height: 140px; margin: 14px 20px 0; background: #fff; }
|
||||||
|
.pickup-image :deep(.el-image) { width: 100%; height: 100%; }
|
||||||
|
.pickup-image-fallback { display: flex; height: 100%; align-items: center; justify-content: center; color: #a8abb2; background: #f7f8fa; font-size: 12px; }
|
||||||
|
.pickup-goods-info { padding: 14px; }
|
||||||
|
.pickup-goods-name { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; min-height: 40px; line-height: 20px; font-size: 14px; font-weight: 500; color: #303133; }
|
||||||
|
.pickup-specs { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 8px; color: #6b7280; font-size: 12px; line-height: 18px; }
|
||||||
|
.pickup-store { margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #a0a5ae; font-size: 12px; line-height: 18px; }
|
||||||
|
.pickup-goods-unavailable { margin-top: 8px; color: #909399; font-size: 12px; }
|
||||||
|
.is-unavailable .pickup-image { opacity: .5; }
|
||||||
|
.pickup-address { margin-top: 28px; padding-top: 24px; border-top: 1px solid #eee; }
|
||||||
|
.pickup-address-select { width: 100%; }
|
||||||
|
.pickup-address-select :deep(.el-select__wrapper) { min-height: 42px; }
|
||||||
|
.pickup-address-empty { color: #909399; font-size: 13px; }
|
||||||
|
.pickup-checkout { position: sticky; bottom: 0; z-index: 2; display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-top: 24px; padding: 20px 0; border-top: 1px solid #eee; background: #fff; }
|
||||||
|
.pickup-selection { display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
|
||||||
|
.pickup-selection strong { font-weight: 500; }
|
||||||
|
.pickup-selection b { color: $theme_color; font-weight: 600; }
|
||||||
|
.pickup-selection > span { color: #909399; font-size: 12px; }
|
||||||
|
.pickup-submit { display: flex; align-items: center; gap: 24px; }
|
||||||
|
.pickup-payable { font-size: 13px; color: #606266; }
|
||||||
|
.pickup-payable strong { margin-left: 6px; color: $theme_color; font-size: 26px; font-weight: 600; }
|
||||||
|
.pickup-payable strong span { font-size: 16px; }
|
||||||
|
.pickup-payable small { display: block; margin-top: 4px; text-align: right; font-size: 12px; color: #909399; }
|
||||||
|
.pickup-submit :deep(.el-button) { min-width: 136px; height: 42px; border-radius: 6px; font-size: 15px; }
|
||||||
|
.pickup-terms { margin: 0; padding-bottom: 6px; color: #a0a5ae; font-size: 12px; line-height: 1.6; }
|
||||||
|
@media (max-width: 900px) { .pickup-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
|
@media (max-width: 600px) { .pickup-grid { grid-template-columns: minmax(0, 1fr); } .pickup-checkout { flex-wrap: wrap; } .pickup-submit { width: 100%; justify-content: space-between; } .pickup-section-title { flex-wrap: wrap; } }
|
||||||
|
</style>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
<template v-else>{{ item.discount }}折</template>
|
<template v-else>{{ item.discount }}折</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="pay-coupon-info">
|
<div class="pay-coupon-info">
|
||||||
<p>满{{ item.consumeThreshold }}元可用</p>
|
<p>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</p>
|
||||||
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
||||||
<p>有效期:{{ item.endTime }}</p>
|
<p>有效期:{{ item.endTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ watch(currIndex, (val) => {
|
|||||||
interval = null;
|
interval = null;
|
||||||
nowHour.value = new Date().getHours();
|
nowHour.value = new Date().getHours();
|
||||||
countDown(val);
|
countDown(val);
|
||||||
goodsList.value = list.value[val].seckillGoodsList;
|
goodsList.value = list.value[val]?.seckillGoodsList || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(diffSeconds, (val) => {
|
watch(diffSeconds, (val) => {
|
||||||
@@ -108,11 +108,11 @@ watch(diffSeconds, (val) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getListByDay () {
|
function getListByDay () {
|
||||||
// 当天秒杀活动
|
// 当天秒杀活动;result 可能为空数组(当天无活动),需判空避免取下标 0 报错
|
||||||
seckillByDay().then((res) => {
|
seckillByDay().then((res) => {
|
||||||
if (res.success) {
|
if (res.success && res.result && res.result.length) {
|
||||||
list.value = res.result;
|
list.value = res.result;
|
||||||
goodsList.value = list.value[0].seckillGoodsList;
|
goodsList.value = list.value[0].seckillGoodsList || [];
|
||||||
countDown(currIndex.value);
|
countDown(currIndex.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -200,6 +200,12 @@ export const router = createRouter({
|
|||||||
component: MoneyManagement,
|
component: MoneyManagement,
|
||||||
meta: { title: "我的余额" },
|
meta: { title: "我的余额" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "PickupCard",
|
||||||
|
name: "PickupCard",
|
||||||
|
component: () => import("@/pages/home/userCenter/PickupCard.vue"),
|
||||||
|
meta: { title: "提货卡兑换" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "MyGiftCards",
|
path: "MyGiftCards",
|
||||||
name: "MyGiftCards",
|
name: "MyGiftCards",
|
||||||
@@ -273,6 +279,10 @@ export const router = createRouter({
|
|||||||
component: MemberGrade,
|
component: MemberGrade,
|
||||||
meta: { title: "我的等级" },
|
meta: { title: "我的等级" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "UserInfo",
|
||||||
|
redirect: { name: "Profile" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "Profile",
|
path: "Profile",
|
||||||
name: "Profile",
|
name: "Profile",
|
||||||
@@ -301,6 +311,11 @@ export const router = createRouter({
|
|||||||
name: "AddAddress",
|
name: "AddAddress",
|
||||||
component: AddAddress,
|
component: AddAddress,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 兼容已保存的装修顶部菜单中的旧消息地址,统一进入现有消息列表。
|
||||||
|
path: "Msg",
|
||||||
|
redirect: { name: "MsgList" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "MsgList",
|
path: "MsgList",
|
||||||
name: "MsgList",
|
name: "MsgList",
|
||||||
|
|||||||
@@ -239,3 +239,6 @@ export const deleteMemberBenefit = (id) => {
|
|||||||
export const getMemberBenefitTypes = () => {
|
export const getMemberBenefitTypes = () => {
|
||||||
return getRequest("/member/benefit/types");
|
return getRequest("/member/benefit/types");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMemberBenefitDeliveries = params => getRequest('/member/benefit/deliveries', params);
|
||||||
|
export const retryMemberBenefitDelivery = id => postRequest('/member/benefit/deliveries/' + id + '/retry');
|
||||||
|
|||||||
6
manager/src/api/paid-member.js
Normal file
6
manager/src/api/paid-member.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { getRequest, putRequestWithNoForm } from '@/libs/axios';
|
||||||
|
export const getPaidConfig = () => getRequest('/member/paid/config');
|
||||||
|
export const savePaidConfig = data => putRequestWithNoForm('/member/paid/config', data);
|
||||||
|
export const getPaidOrders = params => getRequest('/member/paid/orders', params);
|
||||||
|
export const getPaidResources = (type, ids) => getRequest('/member/paid/resources', { type, ids: ids.join(',') });
|
||||||
|
export const getPaidDiscountSkus = params => getRequest('/member/paid/discount-skus', params);
|
||||||
@@ -319,6 +319,11 @@ export const giftCardCashBatchCreate = (activityId, params) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 导出指定制卡批次的全部卡号、卡密,不受页面筛选和分页限制。 */
|
||||||
|
export const exportGiftCardCashCreateBatch = (createBatchId) => {
|
||||||
|
return getRequest(`/promotion/giftCardCash/createBatch/${encodeURIComponent(createBatchId)}/export`, {}, "blob");
|
||||||
|
};
|
||||||
|
|
||||||
// 下载批量发卡 Excel 模板
|
// 下载批量发卡 Excel 模板
|
||||||
export const downloadGiftCardCashIssueTemplate = () => {
|
export const downloadGiftCardCashIssueTemplate = () => {
|
||||||
return getRequest("/promotion/giftCardCash/issue/template", {}, "blob");
|
return getRequest("/promotion/giftCardCash/issue/template", {}, "blob");
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ import {
|
|||||||
postRequest
|
postRequest
|
||||||
} from '@/libs/axios';
|
} from '@/libs/axios';
|
||||||
|
|
||||||
// 查询短信站内信信息
|
// 查询站内信信息
|
||||||
export const getMessageData = params => {
|
export const getMessageData = params => {
|
||||||
return getRequest("/other/message", params);
|
return getRequest("/other/message", params);
|
||||||
};
|
};
|
||||||
//修改短信模板
|
|
||||||
export const editSmsMessageTemplate = (id, params) => {
|
|
||||||
return putRequest(`/sms/sms/${id}`, params)
|
|
||||||
}
|
|
||||||
//修改站内信模板
|
//修改站内信模板
|
||||||
export const editNoticeMessage = (id, params) => {
|
export const editNoticeMessage = (id, params) => {
|
||||||
return putRequest(`/setting/noticeMessage/${id}`, params)
|
return putRequest(`/setting/noticeMessage/${id}`, params)
|
||||||
@@ -45,14 +41,6 @@ export const openNoticeMessage = (id) => {
|
|||||||
export const updateMessageStatus = (id,status) => {
|
export const updateMessageStatus = (id,status) => {
|
||||||
return putRequest(`/setting/noticeMessage/${id}/${status}`);
|
return putRequest(`/setting/noticeMessage/${id}/${status}`);
|
||||||
};
|
};
|
||||||
//获取短信模板
|
|
||||||
export const getSmsTemplatePage = (params) => {
|
|
||||||
return getRequest("/sms/sms/template", params);
|
|
||||||
};
|
|
||||||
//获取短信签名
|
|
||||||
export const getSmsSignPage = (params) => {
|
|
||||||
return getRequest("/sms/sms/sign", params);
|
|
||||||
};
|
|
||||||
//获取设置
|
//获取设置
|
||||||
export const getSetting = (key) => {
|
export const getSetting = (key) => {
|
||||||
return getRequest(`/setting/setting/get/${key}`);
|
return getRequest(`/setting/setting/get/${key}`);
|
||||||
@@ -66,42 +54,6 @@ export const setSetting = (key, params) => {
|
|||||||
return postRequest(`/setting/setting/put/${key}`, params);
|
return postRequest(`/setting/setting/put/${key}`, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
//查看短信签名
|
|
||||||
export const getSmsSign = () => {
|
|
||||||
return getRequest(`/sms/sms/sign`);
|
|
||||||
};
|
|
||||||
//添加短信签名
|
|
||||||
export const addSmsSign = (params) => {
|
|
||||||
return postRequest(`/sms/sms/sign`, params);
|
|
||||||
};
|
|
||||||
//修改短信签名
|
|
||||||
export const editSmsSign = (params) => {
|
|
||||||
return putRequest(`/sms/sms/sign`, params);
|
|
||||||
};
|
|
||||||
//删除短信签名
|
|
||||||
export const delSmsSign = (id) => {
|
|
||||||
return deleteRequest(`/sms/sms/sign/${id}`);
|
|
||||||
};
|
|
||||||
//添加短信模板
|
|
||||||
export const addSmsTemplate = (params) => {
|
|
||||||
return postRequest(`/sms/sms/template`, params);
|
|
||||||
};
|
|
||||||
//修改短信模板
|
|
||||||
export const editSmsTemplate = (params) => {
|
|
||||||
return putRequest(`/sms/sms/template`, params);
|
|
||||||
};
|
|
||||||
//删除短信模板
|
|
||||||
export const delSmsTemplate = (id) => {
|
|
||||||
return deleteRequest(`/sms/sms/template/${id}`);
|
|
||||||
};
|
|
||||||
//查询短信发送记录
|
|
||||||
export const getSmsReachPage = (params) => {
|
|
||||||
return getRequest(`/sms/sms`, params);
|
|
||||||
};
|
|
||||||
//重新发送
|
|
||||||
export const repeatSendSms = (id) => {
|
|
||||||
return postRequest(`/sms/sms/repeat/${id}`);
|
|
||||||
};
|
|
||||||
//获取设置
|
//获取设置
|
||||||
export const getExperienceData = (key) => {
|
export const getExperienceData = (key) => {
|
||||||
return getRequest(`/setting/experience/get/${key}`);
|
return getRequest(`/setting/experience/get/${key}`);
|
||||||
@@ -218,41 +170,11 @@ export const updateWechatMpMessageTemplate = params => {
|
|||||||
|
|
||||||
|
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
// 补充缺失的系统配置、短信、版本及验证码 API
|
// 补充缺失的系统配置、版本及验证码 API
|
||||||
// -------------------------------------------------------------
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
// 短信任务发送与记录 (sms.vue)
|
|
||||||
export const getSmsPage = (params) => {
|
|
||||||
return getRequest(`/sms/sms`, params);
|
|
||||||
};
|
|
||||||
export const sendSms = (params) => {
|
|
||||||
return postRequest(`/sms/sms`, params);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 短信签名同步与详细 (sms.vue, smsSign.vue)
|
|
||||||
export const syncSign = () => {
|
|
||||||
return putRequest(`/sms/sign/querySmsSign`);
|
|
||||||
};
|
|
||||||
export const deleteSign = (id) => {
|
|
||||||
return deleteRequest(`/sms/sign/${id}`);
|
|
||||||
};
|
|
||||||
export const smsSignDetail = (id) => {
|
|
||||||
return getRequest(`/sms/sign/${id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 短信模板管理与同步 (sms.vue)
|
|
||||||
export const syncTemplate = () => {
|
|
||||||
return putRequest(`/sms/template/querySmsSign`);
|
|
||||||
};
|
|
||||||
export const addSmsTemplatePage = (params) => {
|
|
||||||
return postRequest(`/sms/template`, params);
|
|
||||||
};
|
|
||||||
export const editSmsTemplatePage = (params) => {
|
|
||||||
return putRequest(`/sms/template/modifySmsTemplate`, params);
|
|
||||||
};
|
|
||||||
export const deleteSmsTemplatePage = (params) => {
|
|
||||||
return deleteRequest(`/sms/template`, params);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 支付支持方式表单 (PAYMENT_SUPPORT.vue)
|
// 支付支持方式表单 (PAYMENT_SUPPORT.vue)
|
||||||
export const getPaymentSupportForm = () => {
|
export const getPaymentSupportForm = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 使用 fullscreen 类来控制是否全屏显示 -->
|
<!-- 使用 fullscreen 类来控制是否全屏显示 -->
|
||||||
<div :class="{ fullscreen: fullscreen }" class="tinymce-container">
|
<div :class="{ fullscreen: fullscreen }" class="tinymce-container">
|
||||||
<!-- 使用 tinymce-textarea 类作为编辑器的文本区域 -->
|
<!-- 使用 tinymce-textarea 类作为编辑器的文本区域 -->
|
||||||
<uploadImage @callback="insertImage" />
|
<uploadImage :use-image-picker="imagePicker" @callback="insertImage" />
|
||||||
<textarea :id="tinymceId" class="tinymce-textarea" />
|
<textarea :id="tinymceId" class="tinymce-textarea" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,10 +21,15 @@ import {
|
|||||||
} from "vue";
|
} from "vue";
|
||||||
import { initEditor } from "@/components/editor/config";
|
import { initEditor } from "@/components/editor/config";
|
||||||
import uploadImage from "@/components/editor/upload-image.vue";
|
import uploadImage from "@/components/editor/upload-image.vue";
|
||||||
|
import { useFormItem } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "Tinymce" });
|
defineOptions({ name: "Tinymce" });
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
imagePicker: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
@@ -39,6 +44,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["update:modelValue", "input"]);
|
const emit = defineEmits(["update:modelValue", "input"]);
|
||||||
|
const { formItem } = useFormItem();
|
||||||
|
|
||||||
// 引入编辑器的配置(data 中的 initEditor 与 import 同名,直接使用 import)
|
// 引入编辑器的配置(data 中的 initEditor 与 import 同名,直接使用 import)
|
||||||
const hasChange = ref(false); // 标记内容是否有更改
|
const hasChange = ref(false); // 标记内容是否有更改
|
||||||
@@ -66,6 +72,9 @@ watch(editorContent, (val) => {
|
|||||||
function emitContent(content) {
|
function emitContent(content) {
|
||||||
emit("update:modelValue", content);
|
emit("update:modelValue", content);
|
||||||
emit("input", content);
|
emit("input", content);
|
||||||
|
if (props.imagePicker && content) {
|
||||||
|
nextTick(() => formItem?.validate("change").catch(() => {}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 数据返回并给富文本框插入图片
|
// 数据返回并给富文本框插入图片
|
||||||
function insertImage(arr) {
|
function insertImage(arr) {
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="showOssManager"
|
v-model="showOssManager"
|
||||||
|
title="选择图片"
|
||||||
width="1000px"
|
width="1000px"
|
||||||
append-to-body
|
append-to-body
|
||||||
:z-index="3600"
|
:z-index="3600"
|
||||||
@@ -94,6 +95,7 @@ import { Message } from "@/utils/message";
|
|||||||
import OssManage from "@/views/sys/oss-manage/ossManage.vue";
|
import OssManage from "@/views/sys/oss-manage/ossManage.vue";
|
||||||
|
|
||||||
defineOptions({ name: "upload-image" });
|
defineOptions({ name: "upload-image" });
|
||||||
|
const props = defineProps({ useImagePicker: { type: Boolean, default: false } });
|
||||||
|
|
||||||
const emit = defineEmits(["callback"]);
|
const emit = defineEmits(["callback"]);
|
||||||
|
|
||||||
@@ -120,6 +122,10 @@ function draggableItemKey(item) {
|
|||||||
return item?.url || item;
|
return item?.url || item;
|
||||||
}
|
}
|
||||||
function handleClickUploadImage() {
|
function handleClickUploadImage() {
|
||||||
|
if (props.useImagePicker) {
|
||||||
|
importOSS();
|
||||||
|
return;
|
||||||
|
}
|
||||||
show.value = true;
|
show.value = true;
|
||||||
}
|
}
|
||||||
function handleView(url) {
|
function handleView(url) {
|
||||||
@@ -158,7 +164,16 @@ function handleUploadError(err) {
|
|||||||
Message.error(err?.message || String(err));
|
Message.error(err?.message || String(err));
|
||||||
}
|
}
|
||||||
function confirmUrls() {
|
function confirmUrls() {
|
||||||
applySelectedImages();
|
if (props.useImagePicker) {
|
||||||
|
const urls = [...new Set(selectedImage.value.map(parseOssSelectionUrl).filter(Boolean))];
|
||||||
|
if (!urls.length) {
|
||||||
|
Message.warning("请选择图片");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
emit("callback", urls);
|
||||||
|
} else {
|
||||||
|
applySelectedImages();
|
||||||
|
}
|
||||||
showOssManager.value = false;
|
showOssManager.value = false;
|
||||||
}
|
}
|
||||||
function cancelOssImport() {
|
function cancelOssImport() {
|
||||||
|
|||||||
@@ -92,6 +92,9 @@
|
|||||||
<span>{{ row.simpleSpecs || "默认规格" }}</span>
|
<span>{{ row.simpleSpecs || "默认规格" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="店铺名称" prop="storeName" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ row.storeName || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="销售价" width="120" align="right">
|
<el-table-column label="销售价" width="120" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="goods-price font-semibold">
|
<span class="goods-price font-semibold">
|
||||||
@@ -111,7 +114,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品类型" width="100" align="center">
|
<el-table-column label="商品类型" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag size="small" type="info">{{ goodsTypeLabel(row.goodsType) }}</el-tag>
|
<span>{{ goodsTypeLabel(row.goodsType) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -136,11 +139,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, reactive, ref, watch } from "vue";
|
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
||||||
import * as API_Goods from "@/api/goods";
|
import * as API_Goods from "@/api/goods";
|
||||||
import { goodsTypeLabel } from "@/constants/goodsType";
|
import { goodsTypeLabel } from "@/constants/goodsType";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
goodsLoader: { type: Function, default: null },
|
||||||
|
queryParams: { type: Object, default: () => ({}) },
|
||||||
selectedWay: {
|
selectedWay: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
@@ -167,6 +172,8 @@ const category = ref([]);
|
|||||||
const goodsData = ref([]);
|
const goodsData = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
let syncingSelection = false;
|
||||||
|
let querySequence = 0;
|
||||||
|
|
||||||
watch(category, (val) => {
|
watch(category, (val) => {
|
||||||
goodsParams.categoryPath = val && val.length ? val[2] : "";
|
goodsParams.categoryPath = val && val.length ? val[2] : "";
|
||||||
@@ -194,6 +201,8 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
watch(() => props.queryParams, onSearchGoods, { deep: true });
|
||||||
|
onUnmounted(() => { querySequence++; });
|
||||||
|
|
||||||
function onSearchGoods() {
|
function onSearchGoods() {
|
||||||
goodsData.value = [];
|
goodsData.value = [];
|
||||||
@@ -222,13 +231,14 @@ function onPageSizeChange(val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getQueryGoodsList() {
|
function getQueryGoodsList() {
|
||||||
|
const sequence = ++querySequence;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
API_Goods.getGoodsSkuData(goodsParams)
|
(props.goodsLoader || API_Goods.getGoodsSkuData)({ ...goodsParams, ...props.queryParams })
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
initGoods(res);
|
if (sequence === querySequence) initGoods(res);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
if (sequence === querySequence) loading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,16 +255,18 @@ function initGoods(res) {
|
|||||||
function syncTableSelection() {
|
function syncTableSelection() {
|
||||||
const table = tableRef.value;
|
const table = tableRef.value;
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
if (!selectedList.value.length) {
|
// 将不在当前页的已选 SKU 也加入保留选择,避免翻页回显时丢失。
|
||||||
|
syncingSelection = true;
|
||||||
|
try {
|
||||||
table.clearSelection();
|
table.clearSelection();
|
||||||
return;
|
selectedList.value.forEach((item) => {
|
||||||
|
const id = String(item.id || item.skuId);
|
||||||
|
const current = goodsData.value.find(row => String(row.id || row.skuId) === id);
|
||||||
|
table.toggleRowSelection(current || item, true);
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
syncingSelection = false;
|
||||||
}
|
}
|
||||||
goodsData.value.forEach((item) => {
|
|
||||||
const selected = selectedList.value.some(
|
|
||||||
(e) => (e.id || e.skuId) && String(e.id || e.skuId) === String(item.id || item.skuId)
|
|
||||||
);
|
|
||||||
table.toggleRowSelection(item, selected);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitSelected(list) {
|
function emitSelected(list) {
|
||||||
@@ -263,6 +275,7 @@ function emitSelected(list) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onSelectionChange(rows) {
|
function onSelectionChange(rows) {
|
||||||
|
if (syncingSelection) return;
|
||||||
if (type.value !== "multiple") {
|
if (type.value !== "multiple") {
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
emitSelected([]);
|
emitSelected([]);
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
v-if="flag"
|
v-if="flag"
|
||||||
ref="goodsDialogRef"
|
ref="goodsDialogRef"
|
||||||
:selectedWay="goodsData"
|
:selectedWay="goodsData"
|
||||||
|
:goods-loader="goodsLoader"
|
||||||
|
:query-params="queryParams"
|
||||||
@selected="
|
@selected="
|
||||||
(val) => {
|
(val) => {
|
||||||
goodsData = val;
|
goodsData = val;
|
||||||
@@ -69,6 +71,7 @@ import goodsDialog from "./goods-dialog";
|
|||||||
import linkDialog from "./link-dialog";
|
import linkDialog from "./link-dialog";
|
||||||
|
|
||||||
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
|
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
|
||||||
|
defineProps({ goodsLoader: { type: Function, default: null }, queryParams: { type: Object, default: () => ({}) } });
|
||||||
|
|
||||||
const goodsFlag = ref(false); // 是否商品选择器
|
const goodsFlag = ref(false); // 是否商品选择器
|
||||||
const goodsData = ref([]); // 选择的商品
|
const goodsData = ref([]); // 选择的商品
|
||||||
|
|||||||
@@ -1,174 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shop">
|
<StoreSelector embedded :multiple="false" :query-params="shopQuery" @selected="selectShop" />
|
||||||
<div class="wap-content">
|
|
||||||
<div class="query-wrapper">
|
|
||||||
<div class="query-item">
|
|
||||||
<div>店铺名称</div>
|
|
||||||
<el-input
|
|
||||||
v-model="params.storeName"
|
|
||||||
placeholder="请输入店铺名称"
|
|
||||||
clearable
|
|
||||||
style="width: 150px"
|
|
||||||
@clear="resetSearch"
|
|
||||||
@keyup.enter="resetSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="query-item">
|
|
||||||
<el-button type="primary" @click="resetSearch">搜索</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-table
|
|
||||||
ref="tableRef"
|
|
||||||
v-loading="loading"
|
|
||||||
class="shops-select-table"
|
|
||||||
:data="shopsData"
|
|
||||||
row-key="id"
|
|
||||||
height="340"
|
|
||||||
empty-text="暂无店铺信息"
|
|
||||||
@selection-change="onSelectionChange"
|
|
||||||
>
|
|
||||||
<el-table-column
|
|
||||||
type="selection"
|
|
||||||
label="选择"
|
|
||||||
width="60"
|
|
||||||
align="center"
|
|
||||||
reserve-selection
|
|
||||||
/>
|
|
||||||
<el-table-column label="店铺图片" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<img class="shop-logo" :src="row.storeLogo" alt="" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="storeName"
|
|
||||||
label="店铺名称"
|
|
||||||
min-width="240"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column
|
|
||||||
prop="createTime"
|
|
||||||
label="创建时间"
|
|
||||||
width="180"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column label="店铺类型" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span :class="{ theme_color: row.selfOperated }">
|
|
||||||
{{ row.selfOperated ? "自营" : "非自营" }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="店铺状态" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span :class="{ theme_color: row.storeDisable === 'OPEN' }">
|
|
||||||
{{ row.storeDisable === "OPEN" ? "开启中" : "未开启" }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-pagination
|
|
||||||
class="pageration"
|
|
||||||
size="small"
|
|
||||||
layout="total, prev, pager, next, jumper"
|
|
||||||
:total="total"
|
|
||||||
:page-size="params.pageSize"
|
|
||||||
:current-page="params.pageNumber"
|
|
||||||
@current-change="changePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, reactive, ref } from "vue";
|
import StoreSelector from '@/components/store-selector/index.vue';
|
||||||
import { getShopListData } from "@/api/shops.js";
|
// Author: Bulbasaur; 2026-11-05. 装修链接复用统一店铺选择组件,保留单选及启用店铺范围。
|
||||||
|
const emit = defineEmits(['selected']);
|
||||||
const emit = defineEmits(["selected"]);
|
const shopQuery = { storeDisable: 'OPEN' };
|
||||||
|
function selectShop(rows) { emit('selected', rows.map(row => ({ ...row, ___type: 'shops' }))); }
|
||||||
const loading = ref(false);
|
|
||||||
const total = ref(0);
|
|
||||||
const params = reactive({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 12,
|
|
||||||
storeDisable: "OPEN",
|
|
||||||
storeName: "",
|
|
||||||
});
|
|
||||||
const shopsData = ref([]);
|
|
||||||
const tableRef = ref(null);
|
|
||||||
const selectedShop = ref(null);
|
|
||||||
|
|
||||||
// 无 defineExpose:grep manager/src 全量确认无外部 ref/$refs/$parent 访问本组件成员
|
|
||||||
// (link-dialog.vue 的 $refs["lili-component"][0].type 写入指向首个 tab 的 goods-dialog,与本组件无关)
|
|
||||||
|
|
||||||
// created
|
|
||||||
init();
|
|
||||||
|
|
||||||
function resetSearch() {
|
|
||||||
shopsData.value = [];
|
|
||||||
params.pageNumber = 1;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
function changePageSize(v) {
|
|
||||||
params.pageNumber = v;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
loading.value = true;
|
|
||||||
getShopListData(params)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
total.value = res.result.total;
|
|
||||||
shopsData.value = res.result.records;
|
|
||||||
nextTick(syncTableSelection);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncTableSelection() {
|
|
||||||
const table = tableRef.value;
|
|
||||||
if (!table || !selectedShop.value) return;
|
|
||||||
shopsData.value.forEach((row) => {
|
|
||||||
table.toggleRowSelection(row, row.id === selectedShop.value.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSelectionChange(rows) {
|
|
||||||
// 选择链接只能保留一个店铺,勾选新店铺时自动取消上一个
|
|
||||||
if (!rows.length) {
|
|
||||||
selectedShop.value = null;
|
|
||||||
emit("selected", []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const row = rows[rows.length - 1];
|
|
||||||
if (rows.length > 1) {
|
|
||||||
tableRef.value?.clearSelection();
|
|
||||||
tableRef.value?.toggleRowSelection(row, true);
|
|
||||||
}
|
|
||||||
selectedShop.value = row;
|
|
||||||
emit("selected", [{ ...row, ___type: "shops" }]);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
@import "../style.scss";
|
|
||||||
.shop {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.shop-logo {
|
|
||||||
display: block;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
margin: 0 auto;
|
|
||||||
border-radius: 4px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
.pageration {
|
|
||||||
margin-top: 12px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const props = defineProps({
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
maxlength: Number,
|
maxlength: Number,
|
||||||
|
beforeSelect: Function,
|
||||||
});
|
});
|
||||||
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
|
const emit = defineEmits(["update:modelValue", "input", "on-change"]);
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
@@ -131,6 +132,7 @@ function handleCLickImg(val, index) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function handleOssCallback(item) {
|
function handleOssCallback(item) {
|
||||||
|
if (item?.url && props.beforeSelect && props.beforeSelect(item) === false) return;
|
||||||
picModalFlag.value = false;
|
picModalFlag.value = false;
|
||||||
if (!item?.url) return;
|
if (!item?.url) return;
|
||||||
currentValue.value = item.url;
|
currentValue.value = item.url;
|
||||||
|
|||||||
25
manager/src/components/store-selector/README.md
Normal file
25
manager/src/components/store-selector/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# 统一店铺选择器
|
||||||
|
|
||||||
|
作者:Bulbasaur;2026-11-05。
|
||||||
|
|
||||||
|
默认从右侧打开抽屉,支持搜索、分页、多选和已选回显。取消不提交,确认通过 `selected` 返回完整已选店铺数组。店铺类型、状态均为普通文字,无右侧预览区。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<StoreSelector ref="selector" @selected="onSelected" />
|
||||||
|
```
|
||||||
|
|
||||||
|
导入 `@/components/store-selector/index.vue`,通过 `selector.value.open(selectedRows)` 打开。`selectedRows` 为包含 `id` 的店铺对象数组;每次打开建立独立选择草稿。
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| multiple | true | false 时单选,返回数组最多一项 |
|
||||||
|
| embedded | false | true 时在父容器展示列表,并在选择时立即通知;适用于装修链接面板 |
|
||||||
|
| selectedWay | [] | 初始店铺数组;内嵌模式监听外部选择变化 |
|
||||||
|
| queryParams | {} | 店铺查询限制,例如 `{ storeDisable: 'OPEN' }` |
|
||||||
|
| title | 选择店铺 | 抽屉标题 |
|
||||||
|
| size | 1080px | 抽屉宽度,最大为视口宽度 |
|
||||||
|
| tableHeight | 自动 | 可选表格高度 |
|
||||||
|
|
||||||
|
已接入付费会员、装修链接单选、移动端装修店铺列表配置。查询复用 `getShopListData`(`/store/store`),不新增后端接口。
|
||||||
|
|
||||||
|
验证:在工作区根目录运行 `node scripts/test-store-selector.cjs`。
|
||||||
115
manager/src/components/store-selector/index.vue
Normal file
115
manager/src/components/store-selector/index.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<component :is="embedded ? 'div' : ElDrawer" v-bind="embedded ? {} : { modelValue: visible, title, size, direction: 'rtl', appendToBody: true, destroyOnClose: true }" :class="embedded ? 'store-selector-inline' : 'store-picker-drawer'" @update:model-value="visible = $event" @close="request++">
|
||||||
|
<el-form class="picker-search" inline @submit.prevent="search">
|
||||||
|
<el-form-item label="店铺名称"><el-input v-model="keyword" clearable placeholder="请输入店铺名称" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="picker-content">
|
||||||
|
<div class="picker-results">
|
||||||
|
<el-alert v-if="error" title="加载失败,请重试" type="error" :closable="false"><el-button link type="primary" @click="load">重新加载</el-button></el-alert>
|
||||||
|
<el-table ref="table" v-loading="loading" :data="rows" row-key="id" border :height="tableHeight || (embedded ? '340px' : 'max(300px, calc(100vh - 270px))')" :empty-text="error ? '加载失败' : '暂无匹配结果'" @select="selectStore" @select-all="selectPage" @row-click="toggleStore">
|
||||||
|
<el-table-column v-if="multiple" type="selection" :reserve-selection="false" width="48" align="center" />
|
||||||
|
<el-table-column v-else label="选择" width="60" align="center"><template #default="{row}"><el-radio :model-value="selectedStores[0]?.id" :value="row.id" :aria-label="'选择' + row.storeName" @click.stop @change="chooseSingle(row)" /></template></el-table-column>
|
||||||
|
<el-table-column label="店铺信息" min-width="265"><template #default="{row}"><div class="store-info"><el-avatar :size="40" shape="square" :src="row.storeLogo || ''">店</el-avatar><div class="store-copy"><div class="store-name" :title="row.storeName">{{ row.storeName || '未命名店铺' }}</div><div class="store-id">ID:{{ row.id }}</div></div></div></template></el-table-column>
|
||||||
|
<el-table-column label="店铺类型" width="96" align="center"><template #default="{row}">{{ selfOperated(row) ? '自营' : '非自营' }}</template></el-table-column>
|
||||||
|
<el-table-column label="状态" width="100" align="center"><template #default="{row}">{{ storeStatus(row.storeDisable) }}</template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination v-model:current-page="page" :total="total" :page-size="10" layout="total, prev, pager, next" @current-change="load" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-if="!embedded" #footer><div class="picker-footer"><span class="selected-count">已选择 <b>{{ selectedStores.length }}</b> 家店铺</span><div><el-button @click="visible = false">取消</el-button><el-button type="primary" :disabled="loading" @click="confirm">确定</el-button></div></div></template>
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||||
|
import { ElDrawer } from 'element-plus';
|
||||||
|
import { getShopListData } from '@/api/shops';
|
||||||
|
// Author: Bulbasaur; 2026-11-05.
|
||||||
|
const props = defineProps({
|
||||||
|
multiple: { type: Boolean, default: true },
|
||||||
|
embedded: { type: Boolean, default: false },
|
||||||
|
selectedWay: { type: Array, default: () => [] },
|
||||||
|
queryParams: { type: Object, default: () => ({}) },
|
||||||
|
title: { type: String, default: '选择店铺' },
|
||||||
|
size: { type: [String, Number], default: '1080px' },
|
||||||
|
tableHeight: { type: String, default: '' },
|
||||||
|
});
|
||||||
|
const emit = defineEmits(['selected']);
|
||||||
|
const visible = ref(false), keyword = ref(''), rows = ref([]), total = ref(0), page = ref(1), loading = ref(false), error = ref(false), table = ref();
|
||||||
|
const storeSelection = ref({});
|
||||||
|
const selectedStores = computed(() => Object.values(storeSelection.value));
|
||||||
|
let request = 0;
|
||||||
|
const selfOperated = row => [true, 1, 'true', '1'].includes(row.selfOperated);
|
||||||
|
const storeStatus = value => ({OPEN:'营业中',CLOSED:'已关闭',APPLY:'待审核',APPLYING:'待审核',REFUSED:'审核拒绝',EXPIRED:'已过期'}[value] || value || '—');
|
||||||
|
async function syncStores() {
|
||||||
|
await nextTick();
|
||||||
|
if (!table.value) return;
|
||||||
|
table.value.clearSelection();
|
||||||
|
rows.value.forEach(row => { if (storeSelection.value[String(row.id)]) table.value.toggleRowSelection(row, true); });
|
||||||
|
}
|
||||||
|
function notifyEmbedded() { if (props.embedded) emit('selected', selectedStores.value.map(row => ({...row}))); }
|
||||||
|
function chooseSingle(row) { storeSelection.value = { [String(row.id)]: {...row} }; notifyEmbedded(); }
|
||||||
|
function selectStore(selected, row) {
|
||||||
|
const key = String(row.id);
|
||||||
|
if (selected.some(item => String(item.id) === key)) storeSelection.value[key] = row;
|
||||||
|
else delete storeSelection.value[key];
|
||||||
|
notifyEmbedded();
|
||||||
|
}
|
||||||
|
function selectPage(selected) {
|
||||||
|
const ids = new Set(selected.map(row => String(row.id)));
|
||||||
|
rows.value.forEach(row => { if (ids.has(String(row.id))) storeSelection.value[String(row.id)] = row; else delete storeSelection.value[String(row.id)]; });
|
||||||
|
notifyEmbedded();
|
||||||
|
}
|
||||||
|
function toggleStore(row, column) {
|
||||||
|
if (column?.type === 'selection') return;
|
||||||
|
if (!props.multiple) { chooseSingle(row); return; }
|
||||||
|
const key = String(row.id);
|
||||||
|
if (storeSelection.value[key]) delete storeSelection.value[key]; else storeSelection.value[key] = row;
|
||||||
|
syncStores();
|
||||||
|
notifyEmbedded();
|
||||||
|
}
|
||||||
|
async function load() {
|
||||||
|
const id = ++request; loading.value = true; error.value = false;
|
||||||
|
try {
|
||||||
|
const res = await getShopListData({ ...props.queryParams, pageNumber: page.value, pageSize: 10, storeName: keyword.value.trim() });
|
||||||
|
if (id !== request || !visible.value) return;
|
||||||
|
if (!res.success) throw new Error('load');
|
||||||
|
rows.value = res.result.records || []; total.value = res.result.total || 0;
|
||||||
|
rows.value.forEach(row => { if (storeSelection.value[String(row.id)]) storeSelection.value[String(row.id)] = row; });
|
||||||
|
await syncStores();
|
||||||
|
} catch { if (id === request) { error.value = true; rows.value = []; total.value = 0; } }
|
||||||
|
finally { if (id === request) loading.value = false; }
|
||||||
|
}
|
||||||
|
function search() { page.value = 1; load(); }
|
||||||
|
function reset() { keyword.value = ''; search(); }
|
||||||
|
async function open(selected = props.selectedWay) {
|
||||||
|
request++; keyword.value = ''; rows.value = []; total.value = 0; page.value = 1; error.value = false;
|
||||||
|
setSelection(selected);
|
||||||
|
visible.value = true;
|
||||||
|
await nextTick(); table.value?.clearSelection();
|
||||||
|
await load();
|
||||||
|
}
|
||||||
|
function setSelection(selected) {
|
||||||
|
const valid = (Array.isArray(selected) ? selected : []).filter(row => row && row.id != null);
|
||||||
|
storeSelection.value = Object.fromEntries((props.multiple ? valid : valid.slice(0,1)).map(row => [String(row.id), {...row}]));
|
||||||
|
}
|
||||||
|
function confirm() { emit('selected', selectedStores.value.map(row => ({...row}))); visible.value = false; }
|
||||||
|
watch(() => props.selectedWay, selected => { if (props.embedded) { setSelection(selected); syncStores(); } }, { deep: true });
|
||||||
|
watch(() => props.queryParams, () => { if (visible.value) search(); }, { deep: true });
|
||||||
|
onMounted(() => { if (props.embedded) open(); });
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
:global(.store-picker-drawer) { max-width: 100vw; }
|
||||||
|
:global(.store-picker-drawer .el-drawer__header) { margin-bottom: 0; padding: 20px; border-bottom: 1px solid #ebeef5; }
|
||||||
|
:global(.store-picker-drawer .el-drawer__body) { padding: 20px; }
|
||||||
|
:global(.store-picker-drawer .el-drawer__footer) { padding: 0 20px 20px; }
|
||||||
|
.picker-search { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 16px 18px 0; margin-bottom: 18px; background: #f7f8fa; border-radius: 6px; }
|
||||||
|
.picker-search .el-form-item { margin-right: 0; margin-bottom: 16px; }.picker-search .el-input { width: 240px; }
|
||||||
|
.picker-content { min-width: 0; }.picker-results { min-width: 0; }
|
||||||
|
.el-pagination { margin-top: 18px; }.store-info { display: flex; align-items: center; gap: 12px; min-height: 50px; }.el-avatar { flex-shrink: 0; background: #edf2f8; color: #7b8da6; }
|
||||||
|
.store-copy { min-width: 0; flex: 1; }.store-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #303133; }.store-id { margin-top: 3px; font-size: 12px; color: #909399; overflow-wrap: anywhere; }
|
||||||
|
.selected-count b { color: var(--el-color-primary); }
|
||||||
|
.picker-footer { display: flex; justify-content: space-between; align-items: center; border-top: 1px solid #ebeef5; padding-top: 16px; }.selected-count { color: #909399; }
|
||||||
|
@media(max-width: 800px) { .picker-search .el-input { width: 180px; } }
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,8 @@ let util = {};
|
|||||||
|
|
||||||
/** 已弃用、前端未实现或已移除的菜单 frontRoute,注册路由时跳过 */
|
/** 已弃用、前端未实现或已移除的菜单 frontRoute,注册路由时跳过 */
|
||||||
const DEPRECATED_FRONT_ROUTES = new Set([
|
const DEPRECATED_FRONT_ROUTES = new Set([
|
||||||
|
"sys/message/sms",
|
||||||
|
"sys/message/smsSign",
|
||||||
"goods/goods-manage/spec",
|
"goods/goods-manage/spec",
|
||||||
"member/list/memberRecycle",
|
"member/list/memberRecycle",
|
||||||
"order/order/fictitiousOrderList",
|
"order/order/fictitiousOrderList",
|
||||||
|
|||||||
@@ -169,6 +169,12 @@ export const otherRouter = {
|
|||||||
name: "order-detail",
|
name: "order-detail",
|
||||||
component: () => import("@/views/order/order/orderDetail.vue")
|
component: () => import("@/views/order/order/orderDetail.vue")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "wechat-order-list",
|
||||||
|
title: "小程序订单",
|
||||||
|
name: "wechat-order-list",
|
||||||
|
component: () => import("@/views/order/wechat/orderList.vue")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "appointment/work-orders",
|
path: "appointment/work-orders",
|
||||||
title: "预约工单",
|
title: "预约工单",
|
||||||
@@ -218,6 +224,20 @@ export const otherRouter = {
|
|||||||
name: "member-benefit",
|
name: "member-benefit",
|
||||||
component: () => import("@/views/member/benefit/index.vue")
|
component: () => import("@/views/member/benefit/index.vue")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "paid-member-settings", title: "付费会员", name: "paid-member-settings",
|
||||||
|
component: () => import("@/views/member/paid/settings.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "paid-member-records", title: "付费记录", name: "paid-member-records",
|
||||||
|
component: () => import("@/views/member/paid/records.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "member-benefit-deliveries",
|
||||||
|
title: "权益发放明细",
|
||||||
|
name: "member-benefit-deliveries",
|
||||||
|
component: () => import("@/views/member/benefit/BenefitDeliveryRecords.vue")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "member-grade-experience",
|
path: "member-grade-experience",
|
||||||
title: "等级权益设置",
|
title: "等级权益设置",
|
||||||
@@ -379,12 +399,6 @@ export const otherRouter = {
|
|||||||
component: () => import("@/views/promotions/seckill/seckill-goods.vue")
|
component: () => import("@/views/promotions/seckill/seckill-goods.vue")
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
path: "add-sms-sign",
|
|
||||||
title: "短信签名",
|
|
||||||
name: "add-sms-sign",
|
|
||||||
component: () => import("@/views/sys/message/smsSign.vue")
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "live-add",
|
path: "live-add",
|
||||||
title: "创建直播",
|
title: "创建直播",
|
||||||
|
|||||||
44
manager/src/utils/specs.js
Normal file
44
manager/src/utils/specs.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 商品规格展示:兼容简要规格、键值 JSON 和旧数组结构,过滤图片数据,不展示原始 JSON。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-06
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 将规格中的简单值转为文字,图片对象等复杂数据不参与规格展示。 */
|
||||||
|
function specValueText(value) {
|
||||||
|
if (typeof value === "string") return value.trim();
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||||
|
if (typeof value === "boolean") return String(value);
|
||||||
|
if (Array.isArray(value)) return value.filter(item => !item || typeof item !== "object").map(specValueText).filter(Boolean).join("、");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成可读的 SKU 规格,优先复用接口已有的 simpleSpecs;无有效规格时显示默认规格。
|
||||||
|
* @param {string|object|Array|null} specs SKU 原始规格
|
||||||
|
* @param {string|null} simpleSpecs 接口提供的简要规格
|
||||||
|
* @returns {string} 排除 images 后的规格值,以斜线分隔
|
||||||
|
*/
|
||||||
|
export function formatSkuSpecs(specs, simpleSpecs) {
|
||||||
|
if (typeof simpleSpecs === "string" && simpleSpecs.trim()) return simpleSpecs.trim();
|
||||||
|
let parsed = specs;
|
||||||
|
if (typeof parsed === "string") {
|
||||||
|
const text = parsed.trim();
|
||||||
|
if (!text) return "默认规格";
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return /^[\[{]/.test(text) ? "默认规格" : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let values;
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
values = parsed.filter(item => item && typeof item === "object" && item.specName !== "images")
|
||||||
|
.map(item => specValueText(item.specValue));
|
||||||
|
} else if (parsed && typeof parsed === "object") {
|
||||||
|
values = Object.entries(parsed).filter(([name]) => name !== "images").map(([, value]) => specValueText(value));
|
||||||
|
} else {
|
||||||
|
values = [specValueText(parsed)];
|
||||||
|
}
|
||||||
|
return values.filter(Boolean).join(" / ") || "默认规格";
|
||||||
|
}
|
||||||
40
manager/src/views/member/benefit/BenefitConfigFields.vue
Normal file
40
manager/src/views/member/benefit/BenefitConfigFields.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<el-form-item v-if="form.benefitType === 'POINT_MULTIPLIER'" label="积分倍率" required>
|
||||||
|
<div class="number-field"><el-input-number v-model="form.multiplier" :min="1" :max="100" :precision="2" controls-position="right" /><span>倍</span></div>
|
||||||
|
<div class="hint">购物积分基数 × 倍率,同类权益取最高倍率。</div>
|
||||||
|
</el-form-item>
|
||||||
|
<div v-if="['CUSTOM', 'GRADE_BADGE', 'EXCLUSIVE_CUSTOMER_SERVICE'].includes(form.benefitType)" class="rule-description">{{ typeDescription }}</div>
|
||||||
|
<template v-if="form.benefitType === 'COUPON_PACKAGE'">
|
||||||
|
<el-form-item label="发券规则" required>
|
||||||
|
<el-radio-group v-model="form.grantRule">
|
||||||
|
<el-radio value="ONCE">发放一次</el-radio>
|
||||||
|
<el-radio value="MONTHLY">每月发放</el-radio>
|
||||||
|
<el-radio value="PERIOD">按周期发放</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.grantRule === 'MONTHLY'" label="发放日期" required>
|
||||||
|
<div class="number-field"><el-input-number v-model="form.grantDay" :min="1" :max="31" :precision="0" controls-position="right" /><span>号</span></div>
|
||||||
|
<div class="hint">当月不存在指定日期时,取当月最后一天。</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="form.grantRule === 'PERIOD'" label="发放周期" required>
|
||||||
|
<div class="number-field"><el-input-number v-model="form.periodMonths" :min="1" :max="12" :precision="0" controls-position="right" /><span>个月</span></div>
|
||||||
|
<div class="hint">以首次达到等级的日期为起点,首期立即发放。</div>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="rule-description coupon-description">最多选择 10 张平台优惠券,每券赠送 1~10 张。仅发放当前周期,暂停期间未生成的历史周期不追发。</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
const props = defineProps({ form: { type: Object, required: true } });
|
||||||
|
const typeDescription = computed(() => ({
|
||||||
|
GRADE_BADGE: '展示会员的等级徽章。请在下方配置权益图标和介绍,并在客户等级中关联使用。',
|
||||||
|
EXCLUSIVE_CUSTOMER_SERVICE: '展示会员专属客服权益。会员可通过商城现有客服入口联系,具体服务内容请在权益介绍中说明。',
|
||||||
|
CUSTOM: '展示自定义会员权益。请在权益介绍中说明适用条件、参与方式及服务内容。',
|
||||||
|
}[props.form.benefitType] || ''));
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.number-field { display: flex; align-items: center; gap: 10px; }
|
||||||
|
.hint { width: 100%; margin-top: 6px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px; }
|
||||||
|
.rule-description { padding: 12px 16px; color: var(--el-text-color-regular); background: #f6f8fb; border-radius: 6px; font-size: 13px; line-height: 22px; }
|
||||||
|
.coupon-description { margin-bottom: 20px; }
|
||||||
|
</style>
|
||||||
60
manager/src/views/member/benefit/BenefitDeliveryRecords.vue
Normal file
60
manager/src/views/member/benefit/BenefitDeliveryRecords.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search benefit-delivery-page">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<el-form :model="query" inline class="search-form" label-width="70px" @submit.prevent @keyup.enter="search">
|
||||||
|
<el-form-item label="会员ID"><el-input v-model="query.memberId" placeholder="请输入会员ID" clearable style="width:240px" /></el-form-item>
|
||||||
|
<el-form-item label="状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:240px">
|
||||||
|
<el-option label="成功" value="SUCCESS" /><el-option label="失败" value="FAILED" /><el-option label="待发放" value="PENDING" />
|
||||||
|
</el-select></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" class="search-btn" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" class="mt_10">
|
||||||
|
<template #header><div class="records-header"><span>权益发放明细</span><el-button @click="router.push({ name: 'member-benefit' })">返回客户权益</el-button></div></template>
|
||||||
|
<el-table :data="rows" v-loading="loading" border>
|
||||||
|
<el-table-column prop="memberId" label="会员ID" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="benefitName" label="权益" min-width="130" />
|
||||||
|
<el-table-column prop="sourceRef" label="来源等级ID" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cycleKey" label="发放周期" min-width="130" />
|
||||||
|
<el-table-column label="状态" width="100"><template #default="{row}">{{ statuses[row.status] || row.status }}</template></el-table-column>
|
||||||
|
<el-table-column prop="attempts" label="尝试次数" width="90" />
|
||||||
|
<el-table-column prop="errorMessage" label="失败原因" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="170" />
|
||||||
|
<el-table-column label="操作" width="90"><template #default="{row}"><el-button v-if="row.status === 'FAILED'" link type="primary" :loading="retrying === row.id" @click="retry(row)">重试</el-button></template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :total="total" :page-size="query.pageSize" layout="total, prev, pager, next" @current-change="load" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { getMemberBenefitDeliveries, retryMemberBenefitDelivery } from '@/api/member';
|
||||||
|
defineOptions({ name: 'memberBenefitDeliveries' });
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false), rows = ref([]), total = ref(0), retrying = ref('');
|
||||||
|
const query = reactive({memberId:'',status:'',pageNumber:1,pageSize:20});
|
||||||
|
const statuses = {SUCCESS:'成功',FAILED:'失败',PENDING:'待发放'};
|
||||||
|
async function load() {
|
||||||
|
loading.value = true;
|
||||||
|
try { const res = await getMemberBenefitDeliveries({ ...query }); if(res.success) { rows.value = res.result.records || []; total.value = res.result.total || 0; } }
|
||||||
|
catch { ElMessage.error('发放明细加载失败,请重新搜索'); }
|
||||||
|
finally { loading.value = false; }
|
||||||
|
}
|
||||||
|
function search() { query.pageNumber = 1; load(); }
|
||||||
|
function reset() { query.memberId = ''; query.status = ''; search(); }
|
||||||
|
async function retry(row) {
|
||||||
|
try { await ElMessageBox.confirm('按原发放配置补发此权益,系统会检查会员当前资格。', '重试权益发放'); }
|
||||||
|
catch { return; }
|
||||||
|
retrying.value = row.id;
|
||||||
|
try { const res = await retryMemberBenefitDelivery(row.id); if(res.success) ElMessage.success('补发成功'); await load(); }
|
||||||
|
catch { ElMessage.error('补发失败,请稍后重试'); }
|
||||||
|
finally { retrying.value = ''; }
|
||||||
|
}
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.records-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||||
|
.el-pagination { margin-top: 20px; justify-content: flex-end; }
|
||||||
|
</style>
|
||||||
129
manager/src/views/member/benefit/BenefitFormFields.vue
Normal file
129
manager/src/views/member/benefit/BenefitFormFields.vue
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<template>
|
||||||
|
<section class="benefit-section">
|
||||||
|
<div class="benefit-section-heading"><h3>基本信息</h3></div>
|
||||||
|
<div class="benefit-form-grid">
|
||||||
|
<el-form-item label="权益名称" prop="benefitName">
|
||||||
|
<el-input v-model="form.benefitName" maxlength="50" show-word-limit placeholder="请输入权益名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权益类型" prop="benefitType">
|
||||||
|
<el-select v-model="form.benefitType" :disabled="editing" :clearable="!editing" placeholder="请选择权益类型" @change="$emit('type-change', $event)">
|
||||||
|
<el-option v-for="item in typeOptions" :key="item.value" :value="item.value" :label="item.description" />
|
||||||
|
</el-select>
|
||||||
|
<div v-if="editing" class="benefit-field-hint">权益创建后不支持修改类型</div>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="form.benefitType" class="benefit-section">
|
||||||
|
<div class="benefit-section-heading"><h3>权益规则</h3><span>{{ typeLabel }}</span></div>
|
||||||
|
<el-form-item v-if="form.benefitType === 'GIFT_POINT'" label="赠送积分" prop="giftPoint" required>
|
||||||
|
<div class="benefit-number-field">
|
||||||
|
<el-input-number v-model="form.giftPoint" :min="0" :max="99999" :precision="0" controls-position="right" placeholder="请输入积分数量" />
|
||||||
|
<span class="benefit-input-unit">积分</span>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-field-hint">首次达到关联等级时赠送,范围为 0~99999 积分。</div>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<BenefitConfigFields :form="form" />
|
||||||
|
|
||||||
|
<el-form-item v-if="form.benefitType === 'COUPON_PACKAGE'" label="赠送优惠券" prop="couponPackageRows" required class="benefit-coupon-form-item">
|
||||||
|
<div class="benefit-coupon-field">
|
||||||
|
<div class="benefit-coupon-toolbar">
|
||||||
|
<span>已选择 <strong>{{ form.couponPackageRows.length }}</strong> / 10 张优惠券</span>
|
||||||
|
<el-button type="primary" plain @click="$emit('select-coupons', which)">添加优惠券</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table border size="small" class="benefit-coupon-table" :data="form.couponPackageRows" empty-text="暂未选择优惠券,请点击右上方添加">
|
||||||
|
<el-table-column prop="couponName" label="优惠券名称" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="有效期" min-width="200"><template #default="{ row }"><span v-if="row" v-html="row.validRange || '-'" /></template></el-table-column>
|
||||||
|
<el-table-column prop="faceValueLabel" label="面额" min-width="95" />
|
||||||
|
<el-table-column label="赠送张数" width="135"><template #default="{ row, $index }">
|
||||||
|
<el-input-number v-if="row" :model-value="row.quantity" :min="1" :max="10" :precision="0" controls-position="right" style="width: 104px" @change="value => $emit('quantity-change', which, $index, value)" />
|
||||||
|
</template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="70" align="center"><template #default="{ row, $index }">
|
||||||
|
<el-button v-if="row" link type="danger" @click="$emit('remove-coupon', which, $index)">删除</el-button>
|
||||||
|
</template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="benefit-section">
|
||||||
|
<div class="benefit-section-heading"><h3>展示内容</h3><span>用于会员端权益展示</span></div>
|
||||||
|
<el-form-item label="权益图标" prop="benefitLogo">
|
||||||
|
<div class="benefit-logo-field">
|
||||||
|
<div class="benefit-logo-preview">
|
||||||
|
<el-image v-if="form.benefitLogo" :src="form.benefitLogo" fit="contain" :preview-src-list="[form.benefitLogo]" preview-teleported>
|
||||||
|
<template #error><span class="benefit-logo-placeholder">图片不可用</span></template>
|
||||||
|
</el-image>
|
||||||
|
<span v-else class="benefit-logo-placeholder">图标预览</span>
|
||||||
|
</div>
|
||||||
|
<BenefitLogoInput v-model="form.benefitLogo" class="benefit-logo-upload" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="权益介绍" prop="benefitDesc">
|
||||||
|
<RichEditor v-model="form.benefitDesc" image-picker height="300px" class="benefit-rich-editor" />
|
||||||
|
</el-form-item>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="benefit-section">
|
||||||
|
<div class="benefit-section-heading"><h3>发布设置</h3></div>
|
||||||
|
<div class="benefit-form-grid">
|
||||||
|
<el-form-item label="启用状态" prop="benefitState">
|
||||||
|
<el-radio-group v-model="form.benefitState"><el-radio value="OPEN">开启</el-radio><el-radio value="CLOSE">关闭</el-radio></el-radio-group>
|
||||||
|
<div class="benefit-field-hint">关闭后,会员端隐藏该权益并停止后续发放。</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="显示排序" prop="benefitSort">
|
||||||
|
<el-input-number v-model="form.benefitSort" :min="1" :max="9999" :precision="0" controls-position="right" />
|
||||||
|
<div class="benefit-field-hint">数字越小越靠前,范围为 1~9999。</div>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import BenefitConfigFields from './BenefitConfigFields.vue';
|
||||||
|
import BenefitLogoInput from './BenefitLogoInput.vue';
|
||||||
|
import RichEditor from '@/components/editor/index.vue';
|
||||||
|
const props = defineProps({
|
||||||
|
form: { type: Object, required: true },
|
||||||
|
typeOptions: { type: Array, default: () => [] },
|
||||||
|
editing: { type: Boolean, default: false },
|
||||||
|
which: { type: String, required: true },
|
||||||
|
});
|
||||||
|
defineEmits(['type-change', 'select-coupons', 'quantity-change', 'remove-coupon']);
|
||||||
|
const typeLabel = computed(() => props.typeOptions.find(item => item.value === props.form.benefitType)?.description || '');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.benefit-section { padding: 24px; border: 1px solid var(--el-border-color-lighter); border-radius: 8px; background: #fff; }
|
||||||
|
.benefit-section + .benefit-section { margin-top: 16px; }
|
||||||
|
.benefit-section-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 22px; }
|
||||||
|
.benefit-section-heading h3 { margin: 0; color: var(--el-text-color-primary); font-size: 15px; font-weight: 600; line-height: 22px; }
|
||||||
|
.benefit-section-heading span { color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px; }
|
||||||
|
.benefit-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
|
||||||
|
.benefit-field-hint { width: 100%; margin-top: 6px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px; }
|
||||||
|
.benefit-section :deep(.el-form-item) { margin-bottom: 22px; }
|
||||||
|
.benefit-section > :deep(.el-form-item:last-child), .benefit-form-grid > :deep(.el-form-item) { margin-bottom: 0; }
|
||||||
|
.benefit-section :deep(.el-form-item__label) { padding: 0; margin-bottom: 8px; font-weight: 500; line-height: 22px; }
|
||||||
|
.benefit-section :deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
.benefit-section :deep(.el-select) { width: 100%; }
|
||||||
|
.benefit-section :deep(.el-input-number) { width: 180px; max-width: 100%; }
|
||||||
|
.benefit-number-field { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.benefit-input-unit { color: var(--el-text-color-regular); }
|
||||||
|
.benefit-coupon-field, .benefit-rich-editor { width: 100%; min-width: 0; }
|
||||||
|
.benefit-coupon-toolbar { display: flex; gap: 12px; flex-wrap: wrap; justify-content: space-between; align-items: center; margin-bottom: 12px; color: var(--el-text-color-secondary); font-size: 12px; }
|
||||||
|
.benefit-coupon-toolbar strong { color: var(--el-color-primary); font-weight: 500; }
|
||||||
|
.benefit-coupon-table { border-radius: 6px; overflow: hidden; }
|
||||||
|
.benefit-logo-field { display: flex; align-items: center; gap: 20px; width: 100%; }
|
||||||
|
.benefit-logo-preview { width: 88px; height: 88px; flex: 0 0 88px; display: flex; align-items: center; justify-content: center; border: 1px dashed var(--el-border-color); border-radius: 8px; background-color: #fafbfc; overflow: hidden; }
|
||||||
|
.benefit-logo-preview :deep(.el-image) { width: 64px; height: 64px; }
|
||||||
|
.benefit-logo-placeholder { color: var(--el-text-color-placeholder); font-size: 12px; }
|
||||||
|
.benefit-logo-upload { flex: 1; min-width: 0; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.benefit-section { padding: 18px 16px; }
|
||||||
|
.benefit-form-grid { grid-template-columns: minmax(0, 1fr); gap: 22px; }
|
||||||
|
.benefit-logo-field { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||||||
|
.benefit-logo-upload { width: 100%; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
manager/src/views/member/benefit/BenefitLogoInput.vue
Normal file
30
manager/src/views/member/benefit/BenefitLogoInput.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="logo-input">
|
||||||
|
<UploadPicInput :model-value="modelValue" :before-select="validateSelection" placeholder="请选择图标或粘贴图片地址" @update:model-value="updateLogo" />
|
||||||
|
<span class="hint">透明PNG,建议100×100像素,上传大小不超过10KB。</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import UploadPicInput from '@/components/lili/upload-pic-input.vue';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
|
import { ElMessage, useFormItem } from 'element-plus';
|
||||||
|
defineProps({modelValue:{type:String,default:''}});
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
const { formItem } = useFormItem();
|
||||||
|
function validateSelection({ file }) {
|
||||||
|
if (!file || file.fileType !== 'image/png' || Number(file.fileSize) > 10240) {
|
||||||
|
ElMessage.error('请选择不超过10KB的PNG图标');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
async function updateLogo(value) {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
await nextTick();
|
||||||
|
formItem?.validate('change').catch(() => {});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.logo-input { width: 100%; min-width: 0; }
|
||||||
|
.hint { display: block; margin-top: 8px; color: var(--el-text-color-secondary); font-size: 12px; line-height: 20px; }
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -76,9 +76,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="等级图标" prop="gradeImage">
|
<el-form-item label="等级图标" prop="gradeImage">
|
||||||
<upload-pic-input v-model="formAdd.gradeImage" />
|
<upload-pic-input v-model="formAdd.gradeImage" />
|
||||||
|
<div class="image-size-hint">推荐 100 × 100 像素,使用透明背景 PNG。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="等级背景图" prop="gradeBackground">
|
<el-form-item label="等级背景图" prop="gradeBackground">
|
||||||
<upload-pic-input v-model="formAdd.gradeBackground" />
|
<upload-pic-input v-model="formAdd.gradeBackground" />
|
||||||
|
<div class="image-size-hint">推荐 750 × 460 像素</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="字体颜色" prop="gradeFontColor">
|
<el-form-item label="字体颜色" prop="gradeFontColor">
|
||||||
<el-input v-model="formAdd.gradeFontColor" maxlength="20" placeholder="如:#333333" />
|
<el-input v-model="formAdd.gradeFontColor" maxlength="20" placeholder="如:#333333" />
|
||||||
@@ -133,9 +135,11 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="等级图标" prop="gradeImage">
|
<el-form-item label="等级图标" prop="gradeImage">
|
||||||
<upload-pic-input v-model="formEdit.gradeImage" />
|
<upload-pic-input v-model="formEdit.gradeImage" />
|
||||||
|
<div class="image-size-hint">推荐 100 × 100 像素,使用透明背景 PNG。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="等级背景图" prop="gradeBackground">
|
<el-form-item label="等级背景图" prop="gradeBackground">
|
||||||
<upload-pic-input v-model="formEdit.gradeBackground" />
|
<upload-pic-input v-model="formEdit.gradeBackground" />
|
||||||
|
<div class="image-size-hint">推荐 750 × 460 像素</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="字体颜色" prop="gradeFontColor">
|
<el-form-item label="字体颜色" prop="gradeFontColor">
|
||||||
<el-input v-model="formEdit.gradeFontColor" maxlength="20" placeholder="如:#333333" />
|
<el-input v-model="formEdit.gradeFontColor" maxlength="20" placeholder="如:#333333" />
|
||||||
@@ -456,6 +460,13 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.image-size-hint {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
.ops a {
|
.ops a {
|
||||||
color: #2d8cf0;
|
color: #2d8cf0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
47
manager/src/views/member/paid/records.vue
Normal file
47
manager/src/views/member/paid/records.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card shadow="never"><el-form inline :model="query" class="search-form" @submit.prevent @keyup.enter="search">
|
||||||
|
<el-form-item label="客户名称"><el-input v-model="query.keyword" placeholder="请输入客户昵称" clearable /></el-form-item><el-form-item label="会员ID"><el-input v-model="query.memberId" placeholder="请输入会员ID" clearable /></el-form-item>
|
||||||
|
<el-form-item label="支付状态"><el-select v-model="query.status" clearable placeholder="全部状态" style="width:160px"><el-option label="未支付" value="UNPAID" /><el-option label="已支付" value="PAID" /><el-option label="已取消" value="CANCELLED" /></el-select></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" @click="search">搜索</el-button><el-button @click="reset">重置</el-button></el-form-item>
|
||||||
|
</el-form></el-card>
|
||||||
|
<el-card shadow="never" class="mt_10"><div class="actions"><el-button :loading="exporting" @click="exportRecords">导出记录</el-button><el-button @click="router.push({name:'paid-member-settings'})">付费会员</el-button></div>
|
||||||
|
<el-table v-loading="loading" :data="rows" border>
|
||||||
|
<el-table-column prop="sn" label="会员费订单号" min-width="200" /><el-table-column prop="memberName" label="客户" width="130" /><el-table-column prop="memberId" label="会员ID" min-width="180" />
|
||||||
|
<el-table-column label="会员名称" min-width="140"><template #default="{row}">{{ name(row) }}</template></el-table-column>
|
||||||
|
<el-table-column prop="amount" label="付费金额(元)" width="115" /><el-table-column prop="months" label="有效月数" width="90" /><el-table-column label="支付状态" width="110"><template #default="{row}">{{ status(row) }}</template></el-table-column>
|
||||||
|
<el-table-column label="会员状态" width="110"><template #default="{row}">{{ membership(row) }}</template></el-table-column><el-table-column prop="payTime" label="支付时间" width="175" /><el-table-column prop="startTime" label="生效时间" width="175" /><el-table-column prop="endTime" label="到期时间" width="175" />
|
||||||
|
<el-table-column prop="paymentMethod" label="支付方式" width="115" /><el-table-column prop="createTime" label="创建时间" width="175" />
|
||||||
|
</el-table>
|
||||||
|
<el-pagination v-model:current-page="query.pageNumber" :page-size="20" :total="total" layout="total, prev, pager, next" @current-change="load" />
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { getPaidOrders } from '@/api/paid-member';
|
||||||
|
defineOptions({name:'paidMemberRecords'});
|
||||||
|
const router=useRouter(), query=reactive({keyword:'',memberId:'',status:'',pageNumber:1,pageSize:20});
|
||||||
|
const rows=ref([]), total=ref(0), loading=ref(false), exporting=ref(false);
|
||||||
|
let request=0;
|
||||||
|
const time=value => value ? new Date(String(value).replace(/-/g,'/')).getTime() : 0;
|
||||||
|
function status(row) { return row.status === 'UNPAID' && time(row.payDeadline)<Date.now() ? '已超时' : ({UNPAID:'未支付',PAID:'已支付',CANCELLED:'已取消'}[row.status] || row.status); }
|
||||||
|
function membership(row) { return row.status !== 'PAID' ? '未开通' : time(row.startTime)>Date.now() ? '待生效' : time(row.endTime)<=Date.now() ? '已到期' : '有效'; }
|
||||||
|
function name(row) { try { return JSON.parse(row.configSnapshot).name; } catch { return '付费会员'; } }
|
||||||
|
async function load() { const id=++request;loading.value=true;try {const res=await getPaidOrders({...query});if(id===request && res.success){rows.value=res.result.records||[];total.value=res.result.total||0;}} finally {if(id===request)loading.value=false;} }
|
||||||
|
function search(){query.pageNumber=1;load();} function reset(){query.keyword='';query.memberId='';query.status='';search();}
|
||||||
|
async function exportRecords() {
|
||||||
|
exporting.value=true;
|
||||||
|
try {
|
||||||
|
const list=[], filters={...query}; let page=1;
|
||||||
|
while(true){const res=await getPaidOrders({...filters,pageNumber:page,pageSize:100});if(!res.success)return;const records=res.result.records||[];if(!records.length)break;list.push(...records);if(list.length>=res.result.total)break;if(list.length>=10000){ElMessage.warning('一次最多导出10000条,请缩小筛选范围');return;}page++;}
|
||||||
|
const quote=value=>{let text=String(value??'');if(/^[=+\-@]/.test(text))text="'"+text;return '"'+text.replace(/"/g,'""')+'"';};
|
||||||
|
const csv=[['会员费订单号','客户','会员ID','会员名称','付费金额','有效月数','支付状态','会员状态','支付时间','生效时间','到期时间','支付方式'],...list.map(r=>[r.sn,r.memberName,r.memberId,name(r),r.amount,r.months,status(r),membership(r),r.payTime,r.startTime,r.endTime,r.paymentMethod])].map(row=>row.map(quote).join(',')).join('\r\n');
|
||||||
|
const url=URL.createObjectURL(new Blob(['\uFEFF'+csv],{type:'text/csv;charset=utf-8;'}));const a=document.createElement('a');a.href=url;a.download='付费记录.csv';a.click();URL.revokeObjectURL(url);
|
||||||
|
} finally {exporting.value=false;}
|
||||||
|
}
|
||||||
|
onMounted(load);
|
||||||
|
</script>
|
||||||
|
<style scoped>.actions { display:flex; gap:12px; margin-bottom:18px; }.el-pagination { margin-top:20px;justify-content:flex-end; }</style>
|
||||||
159
manager/src/views/member/paid/settings.vue
Normal file
159
manager/src/views/member/paid/settings.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div class="paid-settings" v-loading="loading">
|
||||||
|
<el-alert v-if="loadError" title="设置加载失败,请点击重试" type="error" :closable="false"><el-button link @click="loadSettings">重新加载</el-button></el-alert>
|
||||||
|
<el-form :model="form" label-width="150px" @submit.prevent>
|
||||||
|
<el-card shadow="never"><template #header>基本信息</template>
|
||||||
|
<el-form-item label="开放购买/续费"><el-switch v-model="form.enabled" /><span class="hint inline">关闭后,已支付会员仍有效至到期</span></el-form-item>
|
||||||
|
<el-form-item label="会员名称" required><el-input v-model="form.name" maxlength="50" style="width:360px" /></el-form-item>
|
||||||
|
<el-form-item label="会员图标"><UploadPicInput v-model="form.icon" /><div class="hint">推荐100 × 100像素,透明PNG</div></el-form-item>
|
||||||
|
<el-form-item label="会员背景"><el-radio-group v-model="form.backgroundType"><el-radio value="COLOR">背景色</el-radio><el-radio value="IMAGE">背景图</el-radio></el-radio-group><el-color-picker v-if="form.backgroundType === 'COLOR'" v-model="form.backgroundColor" class="inline" /></el-form-item>
|
||||||
|
<el-form-item v-if="form.backgroundType === 'IMAGE'" label="背景图"><UploadPicInput v-model="form.backgroundImage" /></el-form-item>
|
||||||
|
<el-form-item label="字体颜色"><el-color-picker v-model="form.fontColor" /></el-form-item>
|
||||||
|
<el-form-item label="开通价格" required><el-input-number v-model="form.price" :min="0.01" :max="1000000" :precision="2" :controls="false" /><span class="inline">元</span></el-form-item>
|
||||||
|
<el-form-item label="会员有效期" required><el-input-number v-model="form.months" :min="1" :max="120" :precision="0" :controls="false" /><span class="inline">个月</span><div class="hint">按自然月计算;未到期续费从原到期时间顺延。</div></el-form-item>
|
||||||
|
<el-form-item label="关联客户权益"><el-select v-model="form.benefitIds" multiple filterable style="width:480px;max-width:100%" placeholder="选择权益"><el-option v-for="b in benefitOptions" :key="b.id" :label="b.benefitName" :value="String(b.id)" /></el-select></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never"><template #header>商家设置</template>
|
||||||
|
<el-form-item label="商家范围"><el-radio-group v-model="form.storeScope"><el-radio value="SELF">自营商家</el-radio><el-radio value="CUSTOM">自定义选择</el-radio></el-radio-group></el-form-item>
|
||||||
|
<el-form-item v-if="form.storeScope === 'CUSTOM'" label="选择商家"><el-button @click="openPicker('stores')">添加商家</el-button><div class="picked-tags"><el-tag v-for="id in form.storeIds" :key="id" closable @close="remove('storeIds', id)">{{ stores[id]?.storeName || id }}</el-tag></div></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never"><template #header>商品折扣</template>
|
||||||
|
<el-form-item label="商品会员折扣"><el-radio-group v-model="form.discountMode"><el-radio value="ALL">所有适用商品统一折扣</el-radio><el-radio value="CUSTOM">自定义商品折扣</el-radio></el-radio-group></el-form-item>
|
||||||
|
<el-form-item v-if="form.discountMode === 'ALL'" label="统一折扣"><el-input-number v-model="form.discount" :min="0.01" :max="10" :precision="2" :controls="false" /><span class="inline">折</span></el-form-item>
|
||||||
|
<el-form-item v-else label="折扣商品"><div class="full"><el-button @click="openPicker('discount')">添加商品</el-button><el-table :data="filteredDiscountRules" border class="mt_10"><el-table-column label="商品" min-width="230"><template #default="{row}">{{ goods[row.skuId]?.goodsName || row.skuId }}</template></el-table-column><el-table-column label="SKU编码" width="180"><template #default="{row}">{{ goods[row.skuId]?.sn || row.skuId }}</template></el-table-column><el-table-column label="规格" min-width="160" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.simpleSpecs?.trim() || '—' }}</template></el-table-column><el-table-column label="分类" min-width="180" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.categoryName || '—' }}</template></el-table-column><el-table-column label="品牌" min-width="120" show-overflow-tooltip><template #default="{row}">{{ goods[row.skuId]?.brandName || '—' }}</template></el-table-column><el-table-column label="售价" width="120" align="right"><template #default="{row}">{{ goods[row.skuId]?.price == null ? '—' : '¥' + Number(goods[row.skuId].price).toFixed(2) }}</template></el-table-column><el-table-column label="折扣" width="190"><template #default="{row}"><el-input-number v-model="row.discount" :min="0.01" :max="10" :precision="2" style="width:140px" :controls="false" /> 折</template></el-table-column><el-table-column label="操作" width="80"><template #default="{row}"><el-button link type="danger" @click="form.goodsRules = form.goodsRules.filter(r => r.skuId !== row.skuId)">移除</el-button></template></el-table-column></el-table></div></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never"><template #header>会员中心推荐商品</template>
|
||||||
|
<el-button @click="openPicker('recommended')">添加商品</el-button>
|
||||||
|
<el-table :data="form.recommendedSkuIds.map(id => goods[id] || {id})" border class="mt_10"><el-table-column prop="sn" label="SKU编码" width="180" /><el-table-column label="商品名称" min-width="240"><template #default="{row}">{{ row.goodsName || row.id }}</template></el-table-column><el-table-column label="规格" min-width="160" show-overflow-tooltip><template #default="{row}">{{ row.simpleSpecs?.trim() || '—' }}</template></el-table-column><el-table-column label="分类" min-width="180" show-overflow-tooltip><template #default="{row}">{{ row.categoryName || '—' }}</template></el-table-column><el-table-column label="品牌" min-width="120" show-overflow-tooltip><template #default="{row}">{{ row.brandName || '—' }}</template></el-table-column><el-table-column label="售价" width="120" align="right"><template #default="{row}">{{ row.price == null ? '—' : '¥' + Number(row.price).toFixed(2) }}</template></el-table-column><el-table-column label="操作" width="90"><template #default="{row}"><el-button link type="danger" @click="remove('recommendedSkuIds', String(row.id))">移除</el-button></template></el-table-column></el-table>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header>展示设置</template>
|
||||||
|
<div class="entry-settings-grid">
|
||||||
|
<section class="entry-settings-block">
|
||||||
|
<h4>未开通会员入口</h4>
|
||||||
|
<el-form-item label="入口文案" required><el-input v-model="form.unopenedText" maxlength="50" show-word-limit class="entry-text-input" /></el-form-item>
|
||||||
|
<el-form-item label="字体颜色" required><el-color-picker v-model="form.unopenedFontColor" /><span class="entry-color-value">{{ form.unopenedFontColor }}</span></el-form-item>
|
||||||
|
<el-form-item label="背景图"><UploadPicInput v-model="form.unopenedBackground" /><div class="hint">用于个人中心未开通及已到期入口,推荐 750 × 160 像素,支持 PNG/JPG;不设置时使用默认渐变背景。</div></el-form-item>
|
||||||
|
<el-form-item label="效果预览"><div class="entry-preview" :style="{color: form.unopenedFontColor}"><img v-if="form.unopenedBackground" :src="form.unopenedBackground" alt="未开通入口背景" /><div class="entry-preview-content"><div><strong>{{ form.unopenedText || '开通付费会员,享专属权益' }}</strong><small>开通享商品优惠与专属权益</small></div><span>›</span></div></div></el-form-item>
|
||||||
|
</section>
|
||||||
|
<section class="entry-settings-block">
|
||||||
|
<h4>已开通会员入口</h4>
|
||||||
|
<el-form-item label="入口文案" required><el-input v-model="form.openedText" maxlength="50" show-word-limit class="entry-text-input" /></el-form-item>
|
||||||
|
<el-form-item label="字体颜色" required><el-color-picker v-model="form.openedFontColor" /><span class="entry-color-value">{{ form.openedFontColor }}</span></el-form-item>
|
||||||
|
<el-form-item label="背景图"><UploadPicInput v-model="form.openedBackground" /><div class="hint">用于个人中心有效会员入口,推荐 750 × 160 像素,支持 PNG/JPG;不设置时使用默认渐变背景。</div></el-form-item>
|
||||||
|
<el-form-item label="效果预览"><div class="entry-preview" :style="{color: form.openedFontColor}"><img v-if="form.openedBackground" :src="form.openedBackground" alt="已开通入口背景" /><div class="entry-preview-content"><div><strong>{{ form.openedText || '我的付费会员' }}</strong><small>有效期至 2027-11-05(示例)</small></div><span>›</span></div></div></el-form-item>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="会员价格标签"><UploadPicInput v-model="form.priceLabel" /><div class="hint">展示在商品列表、商品详情的会员价前,推荐 48 × 48 像素、透明 PNG;不设置时只显示会员价文字。</div></el-form-item>
|
||||||
|
<el-form-item label="标签预览"><div class="price-label-preview"><img v-if="form.priceLabel" :src="form.priceLabel" alt="会员价格标签" /><span>会员价 ¥88.00</span></div></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" class="document-card">
|
||||||
|
<template #header><div class="document-heading"><div><span>购买说明</span><div class="document-hint">展示在移动端付费会员页面,可填写权益介绍和使用说明。</div></div><el-button @click="fullscreenEditor(descriptionEditor)">全屏编辑</el-button></div></template>
|
||||||
|
<el-form-item label-width="0" class="document-field"><RichEditor ref="descriptionEditor" v-model="form.description" image-picker height="520px" class="full" /></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<el-card shadow="never" class="document-card">
|
||||||
|
<template #header><div class="document-heading"><div><span>会员协议</span><div class="document-hint">用户购买前阅读并同意的协议,开启购买前请完善内容。</div></div><el-button @click="fullscreenEditor(agreementEditor)">全屏编辑</el-button></div></template>
|
||||||
|
<el-form-item label-width="0" class="document-field"><RichEditor ref="agreementEditor" v-model="form.agreement" image-picker height="600px" class="full" /></el-form-item>
|
||||||
|
</el-card>
|
||||||
|
<div class="save-bar"><el-button type="primary" :loading="saving" :disabled="loading || !loaded" @click="save">保存设置</el-button></div>
|
||||||
|
</el-form>
|
||||||
|
<StoreSelector ref="picker" @selected="selected" />
|
||||||
|
<GoodsSelector ref="goodsPicker" :goods-loader="discountPicking ? getPaidDiscountSkus : undefined" :query-params="discountQuery" @selectedGoodsData="selected" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onMounted, watch, computed } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import UploadPicInput from '@/components/lili/upload-pic-input.vue';
|
||||||
|
import RichEditor from '@/components/editor/index.vue';
|
||||||
|
import StoreSelector from '@/components/store-selector/index.vue';
|
||||||
|
import GoodsSelector from '@/components/lili-dialog/index.vue';
|
||||||
|
import { getPaidConfig, savePaidConfig, getPaidResources, getPaidDiscountSkus } from '@/api/paid-member';
|
||||||
|
import { getMemberBenefitByPage } from '@/api/member';
|
||||||
|
defineOptions({name:'paidMemberSettings'});
|
||||||
|
const form = reactive({ goodsRules: [], storeIds: [], recommendedSkuIds: [], benefitIds: [] });
|
||||||
|
const loading = ref(false), loaded = ref(false), loadError = ref(false), saving = ref(false), picker = ref(), benefitOptions = ref([]), stores = reactive({}), goods = reactive({});
|
||||||
|
const descriptionEditor = ref(), agreementEditor = ref(), goodsPicker = ref(), discountPicking = ref(false);
|
||||||
|
const discountQuery = computed(() => discountPicking.value ? { storeScope: form.storeScope, storeIds: form.storeIds.join(',') } : {});
|
||||||
|
const filteredDiscountRules = computed(() => form.goodsRules.filter(rule => storeAllowed(goods[rule.skuId])));
|
||||||
|
function fullscreenEditor(editor) { editor?.getEditor()?.execCommand('mceFullScreen'); }
|
||||||
|
let target = '';
|
||||||
|
function remember(rows, type) { rows.forEach(row => { const cache = type === 'STORE' ? stores : goods; cache[String(row.id)] = { ...cache[String(row.id)], ...row }; }); }
|
||||||
|
function remove(field, id) { form[field] = form[field].filter(value => value !== id); }
|
||||||
|
async function openPicker(value) {
|
||||||
|
target = value;
|
||||||
|
if (value === 'stores') {
|
||||||
|
picker.value.open(form.storeIds.map(id => ({ ...stores[id], id: String(id) })));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
discountPicking.value = value === 'discount';
|
||||||
|
if (discountPicking.value) {
|
||||||
|
if (form.storeScope === 'CUSTOM' && !form.storeIds.length) { ElMessage.warning('请先选择参与商家'); return; }
|
||||||
|
try { await reconcileDiscounts(); } catch { ElMessage.error('商品商家信息加载失败,请重试'); return; }
|
||||||
|
}
|
||||||
|
const ids = value === 'discount' ? form.goodsRules.map(rule => rule.skuId) : form.recommendedSkuIds;
|
||||||
|
goodsPicker.value.goodsData = ids.map(id => ({ ...goods[id], id: String(id) }));
|
||||||
|
goodsPicker.value.open('goods');
|
||||||
|
}
|
||||||
|
async function selected(rows) {
|
||||||
|
remember(rows, target === 'stores' ? 'STORE' : 'SKU');
|
||||||
|
if (target === 'stores') { form.storeIds = rows.map(row => String(row.id)); return; }
|
||||||
|
if (target === 'discount') {
|
||||||
|
try { await hydrate('SKU', rows.map(row => String(row.id))); } catch { ElMessage.error('商品商家信息加载失败,请重试'); return; }
|
||||||
|
const validRows = rows.filter(row => storeAllowed(goods[String(row.id)]));
|
||||||
|
if (validRows.length !== rows.length) ElMessage.warning('已排除不属于参与商家的商品');
|
||||||
|
const discounts = new Map(form.goodsRules.map(rule => [String(rule.skuId), rule.discount]));
|
||||||
|
form.goodsRules = validRows.map(row => ({ skuId: String(row.id), discount: discounts.get(String(row.id)) ?? form.discount ?? 9.5 }));
|
||||||
|
} else { form.recommendedSkuIds = rows.map(row => String(row.id)); }
|
||||||
|
const missing = rows.filter(row => !Object.hasOwn(row, 'categoryName') || !Object.hasOwn(row, 'brandName'));
|
||||||
|
if (missing.length) { try { await hydrate('SKU', missing.map(row => String(row.id))); } catch { ElMessage.warning('商品分类和品牌加载失败,可刷新页面重试'); } }
|
||||||
|
}
|
||||||
|
async function save() { saving.value = true; try { await reconcileDiscounts(); const res = await savePaidConfig({...form}); if (res.success) ElMessage.success('付费会员设置已保存'); } finally { saving.value = false; } }
|
||||||
|
function storeAllowed(sku) {
|
||||||
|
return !!sku && (form.storeScope === 'SELF' ? sku.storeSelfOperated === true : form.storeIds.map(String).includes(String(sku.storeId)));
|
||||||
|
}
|
||||||
|
async function reconcileDiscounts() {
|
||||||
|
const missing = form.goodsRules.filter(rule => !Object.hasOwn(goods[rule.skuId] || {}, 'storeSelfOperated'));
|
||||||
|
if (missing.length) await hydrate('SKU', missing.map(rule => rule.skuId));
|
||||||
|
const remaining = form.goodsRules.filter(rule => storeAllowed(goods[rule.skuId]));
|
||||||
|
if (remaining.length !== form.goodsRules.length) { form.goodsRules = remaining; ElMessage.warning('已移除不属于当前参与商家的折扣商品'); }
|
||||||
|
}
|
||||||
|
watch(() => [form.storeScope, ...form.storeIds], () => {
|
||||||
|
if (loading.value || !loaded.value) return;
|
||||||
|
reconcileDiscounts().catch(() => ElMessage.error('商品商家信息加载失败,请重试'));
|
||||||
|
}, { flush: 'sync' });
|
||||||
|
async function hydrate(type, ids) {
|
||||||
|
for (let i=0; i<ids.length; i+=100) { const res = await getPaidResources(type, ids.slice(i,i+100)); if (!res.success) throw new Error('resources'); remember(res.result, type); }
|
||||||
|
}
|
||||||
|
async function loadSettings() {
|
||||||
|
loading.value = true; loaded.value = false; loadError.value = false;
|
||||||
|
try {
|
||||||
|
const [config, rights] = await Promise.all([getPaidConfig(), getMemberBenefitByPage({pageNumber:1,pageSize:200})]);
|
||||||
|
if (!config.success) { loadError.value = true; return; }
|
||||||
|
Object.assign(form, config.result); loaded.value = true; benefitOptions.value = rights.result?.records || [];
|
||||||
|
const ids = [...new Set([...form.recommendedSkuIds, ...form.goodsRules.map(r => r.skuId)])];
|
||||||
|
await Promise.all([hydrate('SKU',ids), hydrate('STORE',form.storeIds)]);
|
||||||
|
} catch { loadError.value = true; } finally { loading.value = false; }
|
||||||
|
}
|
||||||
|
onMounted(loadSettings);
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.entry-settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; margin-bottom: 24px; }
|
||||||
|
.entry-settings-block { min-width: 0; border: 1px solid #ebeef5; border-radius: 6px; padding: 20px 16px 0; }
|
||||||
|
.entry-settings-block h4 { margin: 0 0 24px; color: #303133; font-size: 14px; }
|
||||||
|
.entry-text-input { width: 480px; max-width: 100%; }
|
||||||
|
.entry-color-value { margin-left: 12px; color: #909399; font-size: 12px; }
|
||||||
|
.entry-preview { position: relative; overflow: hidden; width: 375px; max-width: 100%; min-height: 80px; border-radius: 10px; background: linear-gradient(110deg, #423c36, #756149); }
|
||||||
|
.entry-preview > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
||||||
|
.entry-preview-content { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px; min-height: 80px; box-sizing: border-box; line-height: 1.5; }
|
||||||
|
.entry-preview-content strong { font-size: 14px; }.entry-preview-content small { display: block; margin-top: 4px; opacity: .75; font-size: 11px; }.entry-preview-content > span { font-size: 24px; }
|
||||||
|
.price-label-preview { display: flex; align-items: center; gap: 6px; color: #a77c3e; font-size: 13px; }.price-label-preview img { width: 24px; height: 24px; object-fit: contain; }
|
||||||
|
@media (max-width: 1400px) { .entry-settings-grid { grid-template-columns: minmax(0, 1fr); } }
|
||||||
|
.document-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||||
|
.document-heading > div { min-width: 0; }
|
||||||
|
.document-hint { margin-top: 8px; color: #909399; font-size: 12px; font-weight: normal; line-height: 20px; }
|
||||||
|
.document-field { margin-bottom: 0; }
|
||||||
|
.document-field :deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
.document-card :deep(.tox-tinymce) { border-color: #dcdfe6; }
|
||||||
|
.paid-settings { padding-bottom: 70px; }.el-card { margin-top: 16px; }.hint { width: 100%; font-size: 12px; line-height: 20px; margin-top: 6px; color: #909399; }.inline { margin-left: 12px; }.hint.inline { width: auto; margin-top: 0; }.full { width: 100%; min-width: 0; }.picked-tags { width: 100%; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }.save-bar { position: sticky; bottom: 0; padding: 16px; background: #fff; border-top: 1px solid #ebeef5; display: flex; justify-content: flex-end; z-index: 10; }
|
||||||
|
</style>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="支付方式" prop="paymentMethod">
|
<el-form-item label="支付方式" prop="paymentMethod">
|
||||||
<el-select v-model="searchForm.paymentMethod" placeholder="请选择支付方式" clearable>
|
<el-select v-model="searchForm.paymentMethod" placeholder="请选择支付方式" :disabled="!!fixedPaymentMethod" :clearable="!fixedPaymentMethod">
|
||||||
<el-option v-for="item in paymentOptions" :key="item.value" :label="item.label" :value="item.value" />
|
<el-option v-for="item in paymentOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div v-if="allowExport">
|
||||||
<el-button type="warning" class="export" :loading="exporting" @click="exportOrder">导出订单</el-button>
|
<el-button type="warning" class="export" :loading="exporting" @click="exportOrder">导出订单</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,6 +157,13 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||||
const PAGE_SIZES = [20, 50, 100];
|
const PAGE_SIZES = [20, 50, 100];
|
||||||
|
|
||||||
|
// 独立渠道页面复用列表,来源限制不从地址栏或可重置表单中读取。
|
||||||
|
const props = defineProps({
|
||||||
|
fixedClientType: { type: String, default: "" },
|
||||||
|
fixedPaymentMethod: { type: String, default: "" },
|
||||||
|
allowExport: { type: Boolean, default: true },
|
||||||
|
});
|
||||||
|
|
||||||
const PROMOTION_OPTIONS = [
|
const PROMOTION_OPTIONS = [
|
||||||
{ label: "普通订单", value: "NORMAL" },
|
{ label: "普通订单", value: "NORMAL" },
|
||||||
{ label: "拼团订单", value: "PINTUAN" },
|
{ label: "拼团订单", value: "PINTUAN" },
|
||||||
@@ -195,7 +202,7 @@ function emptySearchForm(pageSize = 20) {
|
|||||||
consigneeMobile: "",
|
consigneeMobile: "",
|
||||||
storeName: "",
|
storeName: "",
|
||||||
orderStatus: "",
|
orderStatus: "",
|
||||||
paymentMethod: "",
|
paymentMethod: props.fixedPaymentMethod,
|
||||||
orderPromotionType: "",
|
orderPromotionType: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -367,7 +374,7 @@ function selectDateRange(v) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function countParams() {
|
function countParams() {
|
||||||
const params = { ...searchForm.value };
|
const params = listParams();
|
||||||
delete params.orderStatus;
|
delete params.orderStatus;
|
||||||
delete params.pageNumber;
|
delete params.pageNumber;
|
||||||
delete params.pageSize;
|
delete params.pageSize;
|
||||||
@@ -375,9 +382,15 @@ function countParams() {
|
|||||||
delete params.order;
|
delete params.order;
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
function listParams() {
|
||||||
|
const params = { ...searchForm.value };
|
||||||
|
if (props.fixedClientType) params.clientType = props.fixedClientType;
|
||||||
|
if (props.fixedPaymentMethod) params.paymentMethod = props.fixedPaymentMethod;
|
||||||
|
return params;
|
||||||
|
}
|
||||||
function getDataList() {
|
function getDataList() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
API_Order.getOrderList(searchForm.value)
|
API_Order.getOrderList(listParams())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
data.value = res.result.records;
|
data.value = res.result.records;
|
||||||
@@ -395,6 +408,7 @@ function detail(v) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function exportOrder() {
|
async function exportOrder() {
|
||||||
|
if (!props.allowExport) return;
|
||||||
if (!searchForm.value.startDate || !searchForm.value.endDate) {
|
if (!searchForm.value.startDate || !searchForm.value.endDate) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
@@ -407,7 +421,7 @@ async function exportOrder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exporting.value = true;
|
exporting.value = true;
|
||||||
API_Order.exportOrder(searchForm.value)
|
API_Order.exportOrder(listParams())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
const blob = new Blob([res], {
|
const blob = new Blob([res], {
|
||||||
type: "application/vnd.ms-excel;charset=utf-8",
|
type: "application/vnd.ms-excel;charset=utf-8",
|
||||||
|
|||||||
25
manager/src/views/order/wechat/orderList.vue
Normal file
25
manager/src/views/order/wechat/orderList.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-alert
|
||||||
|
title="小程序订单"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
class="wechat-order-notice"
|
||||||
|
description="仅展示微信小程序下单且使用微信支付的订单。自动上报开关开启且微信侧已开通发货管理时,按现有履约流程上报。"
|
||||||
|
/>
|
||||||
|
<OrderList fixed-client-type="WECHAT_MP" fixed-payment-method="WECHAT" :allow-export="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import OrderList from "../order/orderList.vue";
|
||||||
|
|
||||||
|
defineOptions({ name: "wechatOrderList" });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.wechat-order-notice {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -320,10 +320,10 @@ if (props.options.couponUrl === undefined) props.options.couponUrl = '/member/co
|
|||||||
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ensureServices = computed(() => {
|
const ensureServices = computed(() => {
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
defaultComponentStyle: {
|
defaultComponentStyle: {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const brandList = computed(() => {
|
|||||||
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<span class="val">{{ cp.price }}</span>
|
<span class="val">{{ cp.price }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-info">
|
<div class="right-info">
|
||||||
<span class="title">满{{ cp.consumeThreshold }}可用</span>
|
<span class="title">{{ cp.consumeThreshold > 0 ? '满' + cp.consumeThreshold + '可用' : '无门槛使用' }}</span>
|
||||||
<span class="scope" v-if="cp.title">{{ cp.title }}</span>
|
<span class="scope" v-if="cp.title">{{ cp.title }}</span>
|
||||||
<span class="btn">立即领取</span>
|
<span class="btn">立即领取</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<div class="ctrl-row">
|
<div class="ctrl-row">
|
||||||
<div class="ctrl-label">选择店铺</div>
|
<div class="ctrl-label">选择店铺</div>
|
||||||
<div class="ctrl-body">
|
<div class="ctrl-body">
|
||||||
<el-button plain type="primary" @click="shopDialogVisible = true">选择店铺</el-button>
|
<el-button plain type="primary" @click="openShops">选择店铺</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="shop-chips" v-if="firstItem.shopList && firstItem.shopList.length">
|
<div class="shop-chips" v-if="firstItem.shopList && firstItem.shopList.length">
|
||||||
@@ -176,38 +176,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<StoreSelector ref="storeSelector" :query-params="{ storeDisable: 'OPEN' }" @selected="confirmShops" />
|
||||||
v-model="shopDialogVisible"
|
|
||||||
title="选择店铺"
|
|
||||||
width="720px"
|
|
||||||
append-to-body
|
|
||||||
destroy-on-close
|
|
||||||
@open="loadShops"
|
|
||||||
>
|
|
||||||
<el-table
|
|
||||||
:data="shopOptions"
|
|
||||||
v-loading="shopLoading"
|
|
||||||
max-height="400"
|
|
||||||
@selection-change="onShopSelection"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="50" />
|
|
||||||
<el-table-column label="店铺" min-width="220">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="shop-cell">
|
|
||||||
<img :src="row.storeLogo" class="shop-cell-logo" alt="" />
|
|
||||||
<span>{{ row.storeName }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="类型" width="100">
|
|
||||||
<template #default="{ row }">{{ row.selfOperated ? "自营" : "旗舰店" }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="shopDialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="confirmShops">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -217,7 +186,7 @@ import ColorField from "../controls/ColorField.vue";
|
|||||||
import SliderField from "../controls/SliderField.vue";
|
import SliderField from "../controls/SliderField.vue";
|
||||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||||
import ways from "@/components/lili-dialog/wap.js";
|
import ways from "@/components/lili-dialog/wap.js";
|
||||||
import { getShopListData } from "@/api/shops";
|
import StoreSelector from "@/components/store-selector/index.vue";
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
bgColor1: "#FFFFFF",
|
bgColor1: "#FFFFFF",
|
||||||
@@ -247,7 +216,7 @@ const DEFAULT_ITEM = {
|
|||||||
export default {
|
export default {
|
||||||
name: "PanelShop",
|
name: "PanelShop",
|
||||||
hideDecorateTitle: true,
|
hideDecorateTitle: true,
|
||||||
components: { GradientField, ColorField, SliderField, ossManage },
|
components: { GradientField, ColorField, SliderField, ossManage, StoreSelector },
|
||||||
props: {
|
props: {
|
||||||
res: { type: Object, required: true },
|
res: { type: Object, required: true },
|
||||||
},
|
},
|
||||||
@@ -260,10 +229,6 @@ export default {
|
|||||||
{ key: "style", title: "样式" },
|
{ key: "style", title: "样式" },
|
||||||
],
|
],
|
||||||
picModelFlag: false,
|
picModelFlag: false,
|
||||||
shopDialogVisible: false,
|
|
||||||
shopLoading: false,
|
|
||||||
shopOptions: [],
|
|
||||||
selectedShops: [],
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -284,30 +249,15 @@ export default {
|
|||||||
if (!Array.isArray(item.shopList)) item.shopList = [];
|
if (!Array.isArray(item.shopList)) item.shopList = [];
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// 加载店铺列表供选择
|
openShops() { this.$refs.storeSelector.open(this.firstItem.shopList); },
|
||||||
loadShops() {
|
confirmShops(rows) {
|
||||||
this.shopLoading = true;
|
this.firstItem.shopList = rows.map((s) => ({
|
||||||
this.selectedShops = [];
|
|
||||||
getShopListData({ pageNumber: 1, pageSize: 100, status: "OPEN" })
|
|
||||||
.then((res) => {
|
|
||||||
this.shopOptions = (res && res.result && res.result.records) || [];
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.shopLoading = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onShopSelection(rows) {
|
|
||||||
this.selectedShops = rows;
|
|
||||||
},
|
|
||||||
confirmShops() {
|
|
||||||
this.firstItem.shopList = this.selectedShops.map((s) => ({
|
|
||||||
id: s.id,
|
id: s.id,
|
||||||
storeName: s.storeName,
|
storeName: s.storeName,
|
||||||
storeLogo: s.storeLogo,
|
storeLogo: s.storeLogo,
|
||||||
selfOperated: s.selfOperated,
|
selfOperated: s.selfOperated,
|
||||||
storeBanner: s.storeBanner || "",
|
storeBanner: s.storeBanner || "",
|
||||||
}));
|
}));
|
||||||
this.shopDialogVisible = false;
|
|
||||||
},
|
},
|
||||||
removeShop(index) {
|
removeShop(index) {
|
||||||
this.firstItem.shopList.splice(index, 1);
|
this.firstItem.shopList.splice(index, 1);
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
<el-table-column label="优惠券类型" width="130" align="center">
|
<el-table-column label="优惠券类型" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" size="small" effect="light" :type="row.couponType === 'DISCOUNT' ? 'warning' : 'danger'">
|
<el-tag v-if="row" size="small" effect="light" :type="row.couponType === 'DISCOUNT' ? 'warning' : 'danger'">
|
||||||
{{ couponTypeText(row.couponType) }}
|
{{ couponTypeText(row) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -181,9 +181,9 @@ onMounted(() => {
|
|||||||
getCouponActivity();
|
getCouponActivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
function couponTypeText(v) {
|
function couponTypeText(row) {
|
||||||
if (v === "DISCOUNT") return "打折券";
|
if (row.couponType === "DISCOUNT") return "满折优惠券";
|
||||||
if (v === "PRICE") return "减免现金券";
|
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
|
||||||
return "未知类型";
|
return "未知类型";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,13 +25,6 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="优惠券活动类型" prop="couponActivityType">
|
|
||||||
<el-radio-group v-model="form.couponActivityType">
|
|
||||||
<el-radio-button value="REGISTERED">新人发券</el-radio-button>
|
|
||||||
<el-radio-button value="SPECIFY">精确发券</el-radio-button>
|
|
||||||
<el-radio-button value="AUTO_COUPON">自动赠券</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item
|
<el-form-item
|
||||||
label="领取频率"
|
label="领取频率"
|
||||||
v-if="form.couponActivityType === 'AUTO_COUPON'"
|
v-if="form.couponActivityType === 'AUTO_COUPON'"
|
||||||
@@ -170,8 +163,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import couponTemplate from "@/views/promotions/coupon/coupon";
|
import couponTemplate from "@/views/promotions/coupon/coupon";
|
||||||
import userList from "@/views/member/list/index";
|
import userList from "@/views/member/list/index";
|
||||||
import { saveActivityCoupon } from "@/api/promotion";
|
import { saveActivityCoupon } from "@/api/promotion";
|
||||||
@@ -182,6 +175,7 @@ import { unixToDate } from "@/utils/filters";
|
|||||||
defineOptions({ name: "add-coupon-activity" });
|
defineOptions({ name: "add-coupon-activity" });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const options = reactive({
|
const options = reactive({
|
||||||
@@ -205,6 +199,15 @@ const form = reactive({
|
|||||||
couponActivityItems: [],
|
couponActivityItems: [],
|
||||||
couponFrequencyEnum: "",
|
couponFrequencyEnum: "",
|
||||||
});
|
});
|
||||||
|
watch(
|
||||||
|
() => route.query.couponActivityType,
|
||||||
|
(type) => {
|
||||||
|
if (["REGISTERED", "SPECIFY", "AUTO_COUPON"].includes(type)) {
|
||||||
|
form.couponActivityType = type;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
const selectCouponList = ref([]);
|
const selectCouponList = ref([]);
|
||||||
const formRule = reactive({
|
const formRule = reactive({
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation padding-row">
|
<div class="operation padding-row">
|
||||||
<el-button type="primary" @click="add">添加活动</el-button>
|
<el-button type="primary" plain @click="addRegistered">注册赠券</el-button>
|
||||||
|
<el-button type="primary" plain @click="addSpecify">精准发券</el-button>
|
||||||
|
<el-button type="primary" plain @click="addAutoCoupon">自动赠券</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@@ -105,7 +107,7 @@ function activityTypeText(type) {
|
|||||||
REGISTERED: "注册赠券",
|
REGISTERED: "注册赠券",
|
||||||
AUTO_COUPON: "自动发券",
|
AUTO_COUPON: "自动发券",
|
||||||
};
|
};
|
||||||
return map[type] || "精确发券";
|
return map[type] || "精准发券";
|
||||||
}
|
}
|
||||||
function activityScopeText(scope) {
|
function activityScopeText(scope) {
|
||||||
return scope === "DESIGNATED" ? "指定会员" : "全部会员";
|
return scope === "DESIGNATED" ? "指定会员" : "全部会员";
|
||||||
@@ -145,8 +147,23 @@ function promotionStatusTagType(status) {
|
|||||||
function init() {
|
function init() {
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function add() {
|
function addRegistered() {
|
||||||
router.push({ name: "add-coupon-activity" });
|
router.push({
|
||||||
|
name: "add-coupon-activity",
|
||||||
|
query: { couponActivityType: "REGISTERED" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function addSpecify() {
|
||||||
|
router.push({
|
||||||
|
name: "add-coupon-activity",
|
||||||
|
query: { couponActivityType: "SPECIFY" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function addAutoCoupon() {
|
||||||
|
router.push({
|
||||||
|
name: "add-coupon-activity",
|
||||||
|
query: { couponActivityType: "AUTO_COUPON" },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function info(v) {
|
function info(v) {
|
||||||
router.push({ name: "coupon-activity-info", query: { id: v.id } });
|
router.push({ name: "coupon-activity-info", query: { id: v.id } });
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -72,7 +72,7 @@
|
|||||||
<el-table-column label="面额/折扣" width="100">
|
<el-table-column label="面额/折扣" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme
|
<priceColorScheme
|
||||||
v-if="row && row.price"
|
v-if="row && row.couponType === 'PRICE'"
|
||||||
:value="row.price"
|
:value="row.price"
|
||||||
:color="$mainColor"
|
:color="$mainColor"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
<el-table-column label="面额/折扣" width="150">
|
<el-table-column label="面额/折扣" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">
|
<span v-if="row">
|
||||||
<span v-if="row.price" :style="{ color: $mainColor }">
|
<span v-if="row.couponType === 'PRICE'" :style="{ color: $mainColor }">
|
||||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||||
<span v-else>{{ row.couponDiscount }}折</span>
|
<span v-else>{{ row.couponDiscount }}折</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
<el-table-column label="优惠券类型" width="120">
|
<el-table-column label="优惠券类型" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="couponTypeTagType(row.couponType)">
|
<el-tag v-if="row" :type="couponTypeTagType(row.couponType)">
|
||||||
{{ couponTypeText(row.couponType) }}
|
{{ couponTypeText(row) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -362,12 +362,10 @@ function getTypeTagType(type) {
|
|||||||
};
|
};
|
||||||
return map[type] || "";
|
return map[type] || "";
|
||||||
}
|
}
|
||||||
function couponTypeText(type) {
|
function couponTypeText(row) {
|
||||||
const map = {
|
if (row.couponType === "DISCOUNT") return "满折优惠券";
|
||||||
DISCOUNT: "打折",
|
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
|
||||||
PRICE: "减免现金",
|
return "未知";
|
||||||
};
|
|
||||||
return map[type] || "未知";
|
|
||||||
}
|
}
|
||||||
function couponTypeTagType(type) {
|
function couponTypeTagType(type) {
|
||||||
const map = {
|
const map = {
|
||||||
|
|||||||
@@ -1,38 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="gift-card-activity-page">
|
<div class="gift-card-activity-page" :class="{ 'pickup-page': form.cardType === 'PICKUP', 'view-page': onlyView }">
|
||||||
<!-- 顶部标题与返回栏 -->
|
<!-- 礼品卡基础信息卡片 -->
|
||||||
<el-card shadow="never" class="header-card">
|
<el-card shadow="never" v-loading="detailLoading" class="info-card">
|
||||||
<div class="page-header-row">
|
<template #header>
|
||||||
<div class="header-left">
|
<div class="page-header-row">
|
||||||
<div class="header-title">
|
<div class="header-left">
|
||||||
<span>{{ pageTitle }}</span>
|
<div class="header-title">
|
||||||
<el-tag
|
<span>{{ pageTitle }}</span>
|
||||||
v-if="onlyView || isEditMode"
|
<el-tag
|
||||||
size="small"
|
v-if="onlyView || isEditMode"
|
||||||
effect="plain"
|
size="small"
|
||||||
:type="onlyView ? 'info' : (isEditMode ? 'warning' : 'primary')"
|
effect="plain"
|
||||||
class="mode-tag"
|
:type="onlyView ? 'info' : (isEditMode ? 'warning' : 'primary')"
|
||||||
>
|
class="mode-tag"
|
||||||
{{ onlyView ? "查看模式" : "编辑模式" }}
|
>
|
||||||
</el-tag>
|
{{ onlyView ? "查看模式" : "编辑模式" }}
|
||||||
<span v-if="route.query.id" class="activity-id font-mono">
|
</el-tag>
|
||||||
ID: {{ route.query.id }}
|
<span v-if="route.query.id" class="activity-id font-mono">
|
||||||
</span>
|
ID: {{ route.query.id }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
</template>
|
||||||
<el-button @click="closeCurrentPage">返回礼品卡列表</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 礼品卡基础信息卡片 -->
|
|
||||||
<el-card shadow="never" v-loading="detailLoading" class="info-card mt_10">
|
|
||||||
<div class="section-header">
|
|
||||||
<span class="section-indicator"></span>
|
|
||||||
<span class="section-title">礼品卡配置信息</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 查看模式:以结构化描述列表呈现 -->
|
<!-- 查看模式:以结构化描述列表呈现 -->
|
||||||
<template v-if="onlyView">
|
<template v-if="onlyView">
|
||||||
<el-descriptions :column="2" border class="custom-descriptions">
|
<el-descriptions :column="2" border class="custom-descriptions">
|
||||||
@@ -46,7 +36,7 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="礼品卡面值">
|
<el-descriptions-item v-if="form.cardType === 'CASH'" label="礼品卡面值">
|
||||||
<span class="price-highlight font-mono font-semibold">
|
<span class="price-highlight font-mono font-semibold">
|
||||||
{{ form.faceValue ? $filters.unitPrice(form.faceValue, "¥") : "-" }}
|
{{ form.faceValue ? $filters.unitPrice(form.faceValue, "¥") : "-" }}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,7 +46,6 @@
|
|||||||
<span class="font-mono text-dark font-semibold">
|
<span class="font-mono text-dark font-semibold">
|
||||||
{{ form.stockTotal != null ? `${form.stockTotal} 张` : "-" }}
|
{{ form.stockTotal != null ? `${form.stockTotal} 张` : "-" }}
|
||||||
</span>
|
</span>
|
||||||
<span class="hint-inline">(最多可制发凭证张数)</span>
|
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|
||||||
<el-descriptions-item label="有效期限" :span="2">
|
<el-descriptions-item label="有效期限" :span="2">
|
||||||
@@ -71,6 +60,17 @@
|
|||||||
<span class="font-mono text-secondary">{{ route.query.id }}</span>
|
<span class="font-mono text-secondary">{{ route.query.id }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
<template v-if="form.cardType === 'PICKUP'">
|
||||||
|
<p>兑换规则:{{ {ONE:'任选一种',ALL:'全部兑换',ANY:'任选 ' + form.pickupCount + ' 种'}[form.pickupMode] }},每种一件</p>
|
||||||
|
<el-table :data="pickupGoods">
|
||||||
|
<el-table-column prop="id" label="SKU ID" />
|
||||||
|
<el-table-column prop="goodsName" label="商品" />
|
||||||
|
<el-table-column label="规格" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ formatSkuSpecs(row.specs, row.simpleSpecs) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺" />
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 编辑 / 新增模式:表单输入 -->
|
<!-- 编辑 / 新增模式:表单输入 -->
|
||||||
@@ -78,11 +78,12 @@
|
|||||||
<el-form
|
<el-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="form"
|
:model="form"
|
||||||
label-width="120px"
|
label-position="top"
|
||||||
|
size="large"
|
||||||
:rules="formRule"
|
:rules="formRule"
|
||||||
class="edit-form"
|
class="edit-form"
|
||||||
>
|
>
|
||||||
<el-form-item label="礼品卡名称" prop="giftCardName">
|
<el-form-item label="礼品卡名称" prop="giftCardName" class="full-row">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.giftCardName"
|
v-model="form.giftCardName"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -90,100 +91,112 @@
|
|||||||
clearable
|
clearable
|
||||||
maxlength="64"
|
maxlength="64"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
style="width: 380px"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="礼品卡类型" prop="cardType">
|
<el-form-item v-if="form.cardType === 'CASH'" label="面值" prop="faceValue">
|
||||||
<el-radio-group v-model="form.cardType">
|
|
||||||
<el-radio value="CASH">现金卡</el-radio>
|
|
||||||
<el-radio value="PICKUP" disabled>提货卡(暂未开放)</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item label="面值" prop="faceValue">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.faceValue"
|
v-model="form.faceValue"
|
||||||
|
:disabled="coreLocked"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="请填写面值金额"
|
placeholder="请填写面值金额"
|
||||||
clearable
|
clearable
|
||||||
style="width: 260px"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<template #append>元</template>
|
<template #append>元</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="总库存" prop="stockTotal">
|
<template v-if="form.cardType === 'PICKUP'">
|
||||||
<el-input-number
|
<el-form-item label="兑换方式" required class="full-row">
|
||||||
v-model="form.stockTotal"
|
<el-radio-group v-model="form.pickupMode" :disabled="coreLocked">
|
||||||
:min="1"
|
<el-radio value="ONE">任选一种</el-radio>
|
||||||
:precision="0"
|
<el-radio value="ALL">全部兑换</el-radio>
|
||||||
placeholder="最多可制发凭证张数"
|
<el-radio value="ANY">任选 N 种</el-radio>
|
||||||
style="width: 220px"
|
</el-radio-group>
|
||||||
/>
|
<el-input-number v-if="form.pickupMode === 'ANY'" v-model="form.pickupCount" :min="1" :max="50" :precision="0" :controls="false" :disabled="coreLocked" style="width:100px;margin-left:12px" />
|
||||||
<span class="input-tip-text">张(最多可制发凭证张数)</span>
|
</el-form-item>
|
||||||
|
<el-form-item label="兑换商品" required class="full-row">
|
||||||
|
<el-button :disabled="coreLocked" @click="openPickupGoods">选择商品</el-button>
|
||||||
|
<span class="input-tip-text">最多 50 个 SKU,每种一件,每卡一次兑完</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-table :data="pickupGoods" border class="full-row" style="margin-bottom:4px">
|
||||||
|
<el-table-column prop="id" label="SKU ID" min-width="160" />
|
||||||
|
<el-table-column prop="goodsName" label="商品名称" min-width="220" />
|
||||||
|
<el-table-column label="规格" min-width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">{{ formatSkuSpecs(row.specs, row.simpleSpecs) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺" min-width="140" />
|
||||||
|
<el-table-column label="销售价(元)" width="130"><template #default="{row}">{{ row.price }}</template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="90"><template #default="{row}"><el-button link :disabled="coreLocked" @click="pickupGoods = pickupGoods.filter(g => g.id !== row.id)">移除</el-button></template></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</template>
|
||||||
|
<el-form-item label="总库存" prop="stockTotal" class="stock-form-item">
|
||||||
|
<div class="form-control unit-control">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.stockTotal"
|
||||||
|
:min="1"
|
||||||
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="请输入总库存"
|
||||||
|
/>
|
||||||
|
<span class="unit-addon">张</span>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="有效期限" required class="validity-form-item">
|
<el-form-item label="有效期限" required class="validity-form-item full-row">
|
||||||
<el-radio-group v-model="form.validityType" class="validity-radio-group">
|
<div class="validity-control">
|
||||||
<div class="validity-row">
|
<el-radio-group v-model="form.validityType" :disabled="coreLocked" class="validity-radio-group">
|
||||||
<el-radio value="LONG_TERM">长期有效</el-radio>
|
<el-radio value="LONG_TERM">长期有效</el-radio>
|
||||||
</div>
|
<el-radio value="AFTER_ACTIVATE_MONTHS">激活后有效</el-radio>
|
||||||
|
<el-radio value="FIXED_UNTIL">指定到期</el-radio>
|
||||||
<div class="validity-row">
|
</el-radio-group>
|
||||||
<el-radio value="AFTER_ACTIVATE_MONTHS">
|
<div v-if="form.validityType === 'AFTER_ACTIVATE_MONTHS'" class="validity-detail">
|
||||||
<span class="radio-inline-text">自激活后</span>
|
<div class="unit-control month-control">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="form.validMonthsAfterActivate"
|
v-model="form.validMonthsAfterActivate"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="99"
|
:max="99"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
|
:controls="false"
|
||||||
|
:disabled="coreLocked"
|
||||||
placeholder="1-99"
|
placeholder="1-99"
|
||||||
:disabled="form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
|
|
||||||
class="validity-input-num"
|
|
||||||
/>
|
/>
|
||||||
<span class="radio-inline-text">个月内有效</span>
|
<span class="unit-addon">个月</span>
|
||||||
</el-radio>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="form.validityType === 'FIXED_UNTIL'" class="validity-detail">
|
||||||
<div class="validity-row">
|
<el-date-picker
|
||||||
<el-radio value="FIXED_UNTIL">
|
v-model="form.validEndTime"
|
||||||
<span class="radio-inline-text">指定有效期至</span>
|
type="datetime"
|
||||||
<el-date-picker
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
v-model="form.validEndTime"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
type="datetime"
|
placeholder="选择有效截止时间"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
clearable
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
:disabled="coreLocked"
|
||||||
placeholder="选择有效截止时间"
|
class="validity-date-control"
|
||||||
clearable
|
/>
|
||||||
:disabled="form.validityType !== 'FIXED_UNTIL'"
|
|
||||||
class="validity-date-picker"
|
|
||||||
style="width: 220px"
|
|
||||||
/>
|
|
||||||
</el-radio>
|
|
||||||
</div>
|
</div>
|
||||||
</el-radio-group>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
|
||||||
<el-card shadow="never" class="footer-card mt_10">
|
|
||||||
<div class="footer-actions">
|
<div class="footer-actions">
|
||||||
<el-button @click="closeCurrentPage">
|
|
||||||
{{ onlyView ? "返回" : "取消" }}
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!onlyView"
|
v-if="!onlyView"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="submitLoading"
|
:loading="submitLoading"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
{{ isEditMode ? "保存修改" : "提交创建" }}
|
{{ isEditMode ? "保存修改" : (form.cardType === "PICKUP" ? "创建提货卡" : "创建现金卡") }}
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button @click="closeCurrentPage">{{ onlyView ? "返回" : "取消" }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<lili-dialog ref="pickupSelector" :query-params="{ goodsType: 'PHYSICAL_GOODS', marketEnable: 'UPPER', authFlag: 'PASS' }" @selectedGoodsData="confirmPickupGoods" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -197,8 +210,10 @@ import {
|
|||||||
} from "@/api/promotion";
|
} from "@/api/promotion";
|
||||||
import { regular } from "@/utils";
|
import { regular } from "@/utils";
|
||||||
import { unixToDate } from "@/utils/filters";
|
import { unixToDate } from "@/utils/filters";
|
||||||
|
import { formatSkuSpecs } from "@/utils/specs";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
import LiliDialog from "@/components/lili-dialog/index.vue";
|
||||||
|
|
||||||
defineOptions({ name: "addGiftCardCashActivity" });
|
defineOptions({ name: "addGiftCardCashActivity" });
|
||||||
|
|
||||||
@@ -212,7 +227,9 @@ const activityDetail = ref({});
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
giftCardName: "",
|
giftCardName: "",
|
||||||
cardType: "CASH",
|
cardType: route.query.cardType === "PICKUP" ? "PICKUP" : "CASH",
|
||||||
|
pickupMode: "ONE",
|
||||||
|
pickupCount: 1,
|
||||||
faceValue: "",
|
faceValue: "",
|
||||||
stockTotal: null,
|
stockTotal: null,
|
||||||
validityType: "LONG_TERM",
|
validityType: "LONG_TERM",
|
||||||
@@ -221,6 +238,19 @@ const form = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
|
const pickupGoods = ref([]);
|
||||||
|
const pickupSelector = ref(null);
|
||||||
|
const coreLocked = computed(() => Number(activityDetail.value.stockTotal || 0) - Number(activityDetail.value.remainingStock ?? activityDetail.value.stockTotal ?? 0) > 0);
|
||||||
|
/** 复用公共 SKU 选择器,取消不改变已确认商品。 */
|
||||||
|
function openPickupGoods() {
|
||||||
|
pickupSelector.value.goodsData = pickupGoods.value.map(g => ({ ...g }));
|
||||||
|
pickupSelector.value.open('goods');
|
||||||
|
}
|
||||||
|
/** 选择确认时校验数量并按 SKU 去重,服务端仍执行完整校验。 */
|
||||||
|
function confirmPickupGoods(rows) {
|
||||||
|
if (rows.length > 50) return Message.warning('最多选择 50 个 SKU');
|
||||||
|
pickupGoods.value = Array.from(new Map(rows.map(row => [String(row.id || row.skuId), { ...row, id: String(row.id || row.skuId) }])).values());
|
||||||
|
}
|
||||||
|
|
||||||
const onlyView = computed(() => {
|
const onlyView = computed(() => {
|
||||||
const q = route.query.onlyView;
|
const q = route.query.onlyView;
|
||||||
@@ -232,13 +262,14 @@ const isEditMode = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pageTitle = computed(() => {
|
const pageTitle = computed(() => {
|
||||||
|
const name = form.cardType === "PICKUP" ? "提货卡" : "现金卡";
|
||||||
if (onlyView.value) {
|
if (onlyView.value) {
|
||||||
return "查看礼品卡";
|
return `查看${name}`;
|
||||||
}
|
}
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
return "编辑礼品卡";
|
return `编辑${name}`;
|
||||||
}
|
}
|
||||||
return "添加礼品卡";
|
return `添加${name}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const validityDisplayText = computed(() => {
|
const validityDisplayText = computed(() => {
|
||||||
@@ -288,7 +319,7 @@ const formRule = {
|
|||||||
{ required: true, message: "活动名称不能为空", trigger: "blur" },
|
{ required: true, message: "活动名称不能为空", trigger: "blur" },
|
||||||
{ type: "string", max: 64, message: "活动名称过长", trigger: "blur" },
|
{ type: "string", max: 64, message: "活动名称过长", trigger: "blur" },
|
||||||
],
|
],
|
||||||
faceValue: [{ required: true, message: "请输入面额" }, { validator: checkFaceValue }],
|
faceValue: [{ validator: (rule, value, cb) => form.cardType === "PICKUP" ? cb() : checkFaceValue(rule, value, cb) }],
|
||||||
stockTotal: [{ required: true, validator: validateStock, trigger: "blur" }],
|
stockTotal: [{ required: true, validator: validateStock, trigger: "blur" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -314,6 +345,8 @@ const loadDetail = (id) => {
|
|||||||
const d = res.result;
|
const d = res.result;
|
||||||
form.giftCardName = d.giftCardName || "";
|
form.giftCardName = d.giftCardName || "";
|
||||||
form.cardType = d.cardType || "CASH";
|
form.cardType = d.cardType || "CASH";
|
||||||
|
form.pickupMode = d.pickupMode || "ONE"; form.pickupCount = d.pickupCount || 1;
|
||||||
|
pickupGoods.value = d.pickupGoods || [];
|
||||||
form.faceValue =
|
form.faceValue =
|
||||||
d.faceValue !== null && d.faceValue !== undefined ? String(d.faceValue) : "";
|
d.faceValue !== null && d.faceValue !== undefined ? String(d.faceValue) : "";
|
||||||
form.stockTotal =
|
form.stockTotal =
|
||||||
@@ -370,7 +403,10 @@ const buildPayload = () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
giftCardName: (form.giftCardName || "").trim(),
|
giftCardName: (form.giftCardName || "").trim(),
|
||||||
cardType: form.cardType || "CASH",
|
cardType: form.cardType || "CASH",
|
||||||
faceValue: parseFloat(form.faceValue),
|
faceValue: form.cardType === "PICKUP" ? 0 : parseFloat(form.faceValue),
|
||||||
|
pickupMode: form.cardType === "PICKUP" ? form.pickupMode : null,
|
||||||
|
pickupCount: form.pickupMode === "ALL" ? pickupGoods.value.length : form.pickupMode === "ONE" ? 1 : form.pickupCount,
|
||||||
|
pickupSkuIds: form.cardType === "PICKUP" ? pickupGoods.value.map(g => g.id) : null,
|
||||||
validityType: form.validityType || "LONG_TERM",
|
validityType: form.validityType || "LONG_TERM",
|
||||||
stockTotal: form.stockTotal,
|
stockTotal: form.stockTotal,
|
||||||
};
|
};
|
||||||
@@ -397,6 +433,9 @@ const handleSubmit = () => {
|
|||||||
if (!validateByValidityType()) {
|
if (!validateByValidityType()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (form.cardType === 'PICKUP' && (!pickupGoods.value.length || (form.pickupMode === 'ANY' && (!Number.isInteger(form.pickupCount) || form.pickupCount < 1 || form.pickupCount > pickupGoods.value.length)))) {
|
||||||
|
Message.warning('请选择兑换商品,并填写不超过商品种数的兑换数量'); return;
|
||||||
|
}
|
||||||
submitLoading.value = true;
|
submitLoading.value = true;
|
||||||
const req = isEditMode.value
|
const req = isEditMode.value
|
||||||
? updateGiftCardCashActivity(route.query.id, buildPayload())
|
? updateGiftCardCashActivity(route.query.id, buildPayload())
|
||||||
@@ -426,6 +465,13 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.cardType,
|
||||||
|
(type) => {
|
||||||
|
if (!route.query.id) form.cardType = type === "PICKUP" ? "PICKUP" : "CASH";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.id,
|
() => route.query.id,
|
||||||
(id) => {
|
(id) => {
|
||||||
@@ -444,180 +490,59 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.gift-card-activity-page {
|
.gift-card-activity-page {
|
||||||
padding: 16px 20px;
|
padding: 28px 24px;
|
||||||
background-color: #f0f2f5;
|
|
||||||
min-height: 100vh;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
.info-card { width: 100%; max-width: 1040px; margin: 0 auto; border: 1px solid var(--el-border-color-extra-light); border-radius: 8px; }
|
||||||
.mt_10 {
|
.info-card :deep(.el-card__header) { padding: 30px 40px 0; border-bottom: 0; }
|
||||||
margin-top: 12px;
|
.info-card :deep(.el-card__body) { padding: 28px 40px 32px; }
|
||||||
}
|
.page-header-row { max-width: 640px; margin: 0 auto; }
|
||||||
|
.header-title { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; color: var(--el-text-color-primary); font-size: 20px; font-weight: 600; line-height: 28px; }
|
||||||
.font-mono {
|
.mode-tag { font-weight: normal; }
|
||||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
.activity-id { color: var(--el-text-color-secondary); font-size: 12px; font-weight: normal; }
|
||||||
}
|
.edit-form { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 24px 20px; max-width: 640px; margin: 0 auto; }
|
||||||
|
.full-row { grid-column: 1 / -1; }
|
||||||
.font-semibold {
|
.edit-form :deep(.el-form-item) { min-width: 0; margin: 0; }
|
||||||
font-weight: 600;
|
.edit-form :deep(.el-form-item__label) { margin-bottom: 10px; padding: 0; font-size: 14px; line-height: 20px; color: var(--el-text-color-regular); }
|
||||||
}
|
.edit-form :deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
.edit-form :deep(.el-input__wrapper) { border-radius: 5px; }
|
||||||
.text-dark {
|
.form-control { width: 100%; max-width: 100%; }
|
||||||
color: #303133;
|
.input-tip-text { margin-left: 12px; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||||
}
|
.unit-control { display: flex; align-items: stretch; }
|
||||||
|
.unit-control :deep(.el-input-number) { flex: 1; width: 0; min-width: 0; }
|
||||||
.text-secondary {
|
.unit-control :deep(.el-input__wrapper) { border-radius: 5px 0 0 5px; }
|
||||||
color: #606266;
|
.unit-control :deep(.el-input__inner) { text-align: left; }
|
||||||
}
|
.unit-addon { display: flex; align-items: center; flex-shrink: 0; padding: 0 14px; border: 1px solid var(--el-border-color); border-left: 0; border-radius: 0 5px 5px 0; color: var(--el-text-color-regular); background: var(--el-fill-color-light); box-sizing: border-box; font-size: 14px; }
|
||||||
|
.validity-control { width: 100%; min-width: 0; }
|
||||||
.header-card {
|
.validity-radio-group { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; width: 100%; }
|
||||||
border: none;
|
.validity-radio-group :deep(.el-radio) { display: flex; justify-content: center; height: 48px; margin: 0; padding: 0 12px; border: 1px solid var(--el-border-color); border-radius: 6px; box-sizing: border-box; transition: border-color .15s, background-color .15s; }
|
||||||
border-radius: 4px;
|
.validity-radio-group :deep(.el-radio:hover:not(.is-disabled)) { border-color: var(--el-color-primary-light-5); }
|
||||||
|
.validity-radio-group :deep(.el-radio.is-checked) { border-color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
||||||
:deep(.el-card__body) {
|
.validity-radio-group :deep(.el-radio__label) { font-size: 14px; }
|
||||||
padding: 16px 20px;
|
.validity-detail { display: flex; margin-top: 12px; padding: 16px; background: #f7f9fc; border: 1px solid var(--el-border-color-extra-light); border-radius: 6px; }
|
||||||
}
|
.month-control { width: 200px; max-width: 100%; }
|
||||||
|
.validity-date-control { width: 100%; max-width: 360px; }
|
||||||
.page-header-row {
|
.footer-actions { display: flex; flex-wrap: wrap; gap: 12px; max-width: 640px; margin: 32px auto 0; padding-top: 24px; border-top: 1px solid var(--el-border-color-lighter); }
|
||||||
display: flex;
|
.footer-actions :deep(.el-button) { min-width: 88px; height: 36px; border-radius: 5px; }
|
||||||
align-items: center;
|
.footer-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||||
justify-content: space-between;
|
.font-mono { font-variant-numeric: tabular-nums; }
|
||||||
|
.font-semibold { font-weight: 600; }
|
||||||
.header-left {
|
.text-dark { color: var(--el-text-color-primary); }
|
||||||
.header-title {
|
.text-secondary { color: var(--el-text-color-regular); }
|
||||||
display: flex;
|
.custom-descriptions :deep(.el-descriptions__label) { width: 130px; font-weight: normal; }
|
||||||
align-items: center;
|
.price-highlight { color: var(--el-text-color-primary); }
|
||||||
gap: 12px;
|
&.pickup-page .info-card { max-width: 1200px; }
|
||||||
font-size: 18px;
|
&.pickup-page .page-header-row, &.pickup-page .edit-form, &.pickup-page .footer-actions { max-width: none; }
|
||||||
font-weight: 600;
|
&.pickup-page .stock-form-item { max-width: 420px; }
|
||||||
color: #1f2d3d;
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
.mode-tag {
|
.gift-card-activity-page {
|
||||||
font-weight: normal;
|
padding: 16px 12px;
|
||||||
}
|
.info-card :deep(.el-card__header) { padding: 24px 20px 0; }
|
||||||
|
.info-card :deep(.el-card__body) { padding: 24px 20px; }
|
||||||
.activity-id {
|
.edit-form { grid-template-columns: minmax(0, 1fr); }
|
||||||
font-size: 13px;
|
.validity-radio-group { grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
||||||
color: #909399;
|
.validity-radio-group :deep(.el-radio) { justify-content: flex-start; height: 40px; }
|
||||||
font-weight: normal;
|
.validity-detail { padding: 12px; }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
|
|
||||||
.section-indicator {
|
|
||||||
width: 4px;
|
|
||||||
height: 16px;
|
|
||||||
background-color: #409eff;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-descriptions {
|
|
||||||
:deep(.el-descriptions__label) {
|
|
||||||
width: 130px;
|
|
||||||
background-color: #f8f9fb;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-descriptions__content) {
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-highlight {
|
|
||||||
color: #f56c6c;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hint-inline {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-form {
|
|
||||||
padding: 8px 12px;
|
|
||||||
|
|
||||||
.input-tip-text {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #909399;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validity-radio-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.validity-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
:deep(.el-radio) {
|
|
||||||
margin-right: 0;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-radio__label) {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-inline-text {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validity-input-num {
|
|
||||||
width: 130px;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validity-date-picker {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 16px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
|
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation padding-row">
|
<div class="operation padding-row">
|
||||||
<el-button type="primary" @click="addActivity">添加礼品卡</el-button>
|
<el-button type="primary" @click="addActivity('PICKUP')">添加提货卡</el-button>
|
||||||
|
<el-button type="primary" @click="addActivity('CASH')">添加现金卡</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
@@ -45,10 +46,11 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">
|
<span v-if="row">
|
||||||
<span
|
<span
|
||||||
v-if="row.faceValue != null && row.faceValue !== ''"
|
v-if="row.cardType !== 'PICKUP' && row.faceValue != null && row.faceValue !== ''"
|
||||||
:style="{ color: $mainColor }"
|
:style="{ color: $mainColor }"
|
||||||
>
|
>
|
||||||
{{ $filters.unitPrice(row.faceValue, "¥") }}</span>
|
{{ $filters.unitPrice(row.faceValue, "¥") }}</span>
|
||||||
|
<span v-else-if="row.cardType === 'PICKUP'">兑换 {{ row.pickupCount }} 种</span>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -387,8 +389,9 @@ function formatValidity(row) {
|
|||||||
}
|
}
|
||||||
return parts.join(" ");
|
return parts.join(" ");
|
||||||
}
|
}
|
||||||
function addActivity() {
|
/** 从独立入口创建指定类型的卡,类型在创建页固定展示。 */
|
||||||
router.push({ name: "add-gift-card-cash-activity" });
|
function addActivity(cardType) {
|
||||||
|
router.push({ name: "add-gift-card-cash-activity", query: { cardType } });
|
||||||
}
|
}
|
||||||
function goEdit(row) {
|
function goEdit(row) {
|
||||||
if (!canEdit(row)) {
|
if (!canEdit(row)) {
|
||||||
|
|||||||
@@ -2,52 +2,56 @@
|
|||||||
<div class="search gcc-batch-credentials">
|
<div class="search gcc-batch-credentials">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="gcc-records-toolbar">
|
<div class="gcc-records-toolbar">
|
||||||
<el-button type="primary" class="gcc-records-back" @click="closePage">返回</el-button>
|
<div class="gcc-batch-heading">
|
||||||
</div>
|
<h3>{{ giftCardName || "批次凭证" }}</h3>
|
||||||
<div v-if="giftCardName" class="gcc-records-meta mb_10">
|
<div class="gcc-records-meta">
|
||||||
<strong>礼品卡:</strong>{{ giftCardName }}
|
<span>{{ batchTypeLabel }}:{{ batchIdDisplay || "—" }}</span>
|
||||||
</div>
|
<span v-if="batchRemark">本批备注:{{ batchRemark }}</span>
|
||||||
<div class="gcc-records-meta mb_10">
|
</div>
|
||||||
<strong>{{ batchTypeLabel }}:</strong>{{ batchIdDisplay || "—" }}
|
</div>
|
||||||
<span v-if="batchRemark" style="margin-left: 12px">
|
<div class="gcc-batch-actions">
|
||||||
<strong>本批备注:</strong>{{ batchRemark }}</span>
|
<el-button
|
||||||
|
v-if="activityId && createBatchId && !isIssueBatchMode"
|
||||||
|
type="primary"
|
||||||
|
:loading="exporting"
|
||||||
|
title="导出当前制卡批次全部卡号、卡密,不受筛选和分页限制"
|
||||||
|
@click="handleExport"
|
||||||
|
>导出卡密</el-button>
|
||||||
|
<el-button @click="closePage">返回</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-alert v-if="!activityId || !hasBatchId" type="warning" show-icon :closable="false">
|
<el-alert v-if="!activityId || !hasBatchId" type="warning" show-icon :closable="false">
|
||||||
缺少活动或批次参数,请从{{ batchSourceLabel }}进入。
|
缺少活动或批次参数,请从{{ batchSourceLabel }}进入。
|
||||||
</el-alert>
|
</el-alert>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="gcc-status-tabs">
|
<el-tabs :model-value="lifecycleTab" class="gcc-status-tabs" @tab-change="onLifecycleTab">
|
||||||
<span
|
<el-tab-pane
|
||||||
v-for="t in statusTabList"
|
v-for="t in statusTabList"
|
||||||
:key="t.key"
|
:key="t.key"
|
||||||
:class="{ active: lifecycleTab === t.key }"
|
:name="t.key"
|
||||||
@click="onLifecycleTab(t.key)"
|
:label="t.label"
|
||||||
>
|
/>
|
||||||
{{ t.label }}</span>
|
</el-tabs>
|
||||||
</div>
|
<el-form ref="searchFormRef" :model="searchForm" label-width="72px" class="gcc-filter-form">
|
||||||
<el-form ref="searchFormRef" :model="searchForm" inline class="search-form mb_10 gcc-filter-form">
|
<el-form-item label="卡号" prop="cardNo">
|
||||||
<el-form-item label="卡号" prop="cardNo" label-width="60px">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.cardNo"
|
v-model="searchForm.cardNo"
|
||||||
placeholder="请输入卡号"
|
placeholder="请输入卡号"
|
||||||
clearable
|
clearable
|
||||||
style="width: 160px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label-width="0" class="gcc-combo-item">
|
<el-form-item label="兑换会员" class="gcc-combo-item">
|
||||||
<span class="gcc-combo-label">兑换会员</span>
|
<el-select v-model="searchForm.redeemMemberField" aria-label="兑换会员查询方式">
|
||||||
<el-select v-model="searchForm.redeemMemberField" style="width: 130px">
|
<el-option label="昵称" value="nickName" />
|
||||||
<el-option label="兑换会员昵称" value="nickName" />
|
<el-option label="手机号" value="mobile" />
|
||||||
<el-option label="兑换会员手机" value="mobile" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.redeemMemberKeyword"
|
v-model="searchForm.redeemMemberKeyword"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
clearable
|
clearable
|
||||||
style="width: 140px; margin-left: 4px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="兑换时间" prop="redeemDateRange" label-width="72px">
|
<el-form-item label="兑换时间" prop="redeemDateRange">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="redeemDateRange"
|
v-model="redeemDateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
@@ -55,23 +59,20 @@
|
|||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
style="width: 240px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label-width="0" class="gcc-combo-item">
|
<el-form-item label="激活会员" class="gcc-combo-item">
|
||||||
<span class="gcc-combo-label">激活会员</span>
|
<el-select v-model="searchForm.activateMemberField" aria-label="激活会员查询方式">
|
||||||
<el-select v-model="searchForm.activateMemberField" style="width: 130px">
|
<el-option label="昵称" value="nickName" />
|
||||||
<el-option label="激活会员昵称" value="nickName" />
|
<el-option label="手机号" value="mobile" />
|
||||||
<el-option label="激活会员手机" value="mobile" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.activateMemberKeyword"
|
v-model="searchForm.activateMemberKeyword"
|
||||||
placeholder="请输入"
|
placeholder="请输入"
|
||||||
clearable
|
clearable
|
||||||
style="width: 140px; margin-left: 4px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="激活时间" prop="activateDateRange" label-width="72px">
|
<el-form-item label="激活时间" prop="activateDateRange">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="activateDateRange"
|
v-model="activateDateRange"
|
||||||
type="daterange"
|
type="daterange"
|
||||||
@@ -79,10 +80,11 @@
|
|||||||
value-format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
||||||
start-placeholder="开始日期"
|
start-placeholder="开始日期"
|
||||||
end-placeholder="结束日期"
|
end-placeholder="结束日期"
|
||||||
style="width: 240px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
<el-form-item label-width="0" class="gcc-filter-actions">
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-table v-if="!isIssueBatchMode" v-loading="loading" :data="data" style="width: 100%">
|
<el-table v-if="!isIssueBatchMode" v-loading="loading" :data="data" style="width: 100%">
|
||||||
@@ -90,12 +92,19 @@
|
|||||||
<el-table-column prop="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="cardSecret" label="卡密" min-width="120" show-overflow-tooltip />
|
||||||
<el-table-column label="面额" prop="faceValue" width="90" align="right">
|
<el-table-column label="面额" prop="faceValue" width="90" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ formatPrice(row.faceValue) }}</span>
|
<template v-if="row">
|
||||||
|
<span v-if="row.cardType === 'PICKUP'">提货权益</span>
|
||||||
|
<priceColorScheme v-else-if="hasValue(row.faceValue)" :value="row.faceValue" :color="$mainColor" />
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="余额" prop="balance" width="90" align="right">
|
<el-table-column label="余额" prop="balance" width="90" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ formatPrice(row.balance) }}</span>
|
<template v-if="row">
|
||||||
|
<priceColorScheme v-if="row.cardType !== 'PICKUP' && hasValue(row.balance)" :value="row.balance" :color="$mainColor" />
|
||||||
|
<span v-else>—</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" prop="status" width="90" align="center">
|
<el-table-column label="状态" prop="status" width="90" align="center">
|
||||||
@@ -143,14 +152,13 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
<div class="gcc-pagination">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="searchForm.pageNumber"
|
v-model:current-page="searchForm.pageNumber"
|
||||||
v-model:page-size="searchForm.pageSize"
|
v-model:page-size="searchForm.pageSize"
|
||||||
:page-sizes="[20, 50, 100]"
|
:page-sizes="[20, 50, 100]"
|
||||||
:total="total"
|
:total="total"
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
size="small"
|
|
||||||
@current-change="changePage"
|
@current-change="changePage"
|
||||||
@size-change="changePageSize"
|
@size-change="changePageSize"
|
||||||
/>
|
/>
|
||||||
@@ -163,8 +171,9 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { getGiftCardCashCredentialPage } from "@/api/promotion";
|
import { exportGiftCardCashCreateBatch, getGiftCardCashCredentialPage } from "@/api/promotion";
|
||||||
import { unitPrice } from "@/utils/filters";
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -177,6 +186,7 @@ const STATUS_LABEL = {
|
|||||||
UNACTIVATED: "未激活",
|
UNACTIVATED: "未激活",
|
||||||
BOUND: "已激活",
|
BOUND: "已激活",
|
||||||
ACTIVATED: "已激活",
|
ACTIVATED: "已激活",
|
||||||
|
REDEEMED: "已提货",
|
||||||
VOIDED: "已销卡",
|
VOIDED: "已销卡",
|
||||||
EXPIRED: "已过期",
|
EXPIRED: "已过期",
|
||||||
};
|
};
|
||||||
@@ -206,10 +216,12 @@ const statusTabList = ref([
|
|||||||
{ key: "activated", label: "已激活", status: "BOUND" },
|
{ key: "activated", label: "已激活", status: "BOUND" },
|
||||||
{ key: "cancelled", label: "已销卡", status: "VOIDED" },
|
{ key: "cancelled", label: "已销卡", status: "VOIDED" },
|
||||||
{ key: "expired", label: "已过期", status: "EXPIRED" },
|
{ key: "expired", label: "已过期", status: "EXPIRED" },
|
||||||
|
{ key: "redeemed", label: "已提货", status: "REDEEMED" },
|
||||||
]);
|
]);
|
||||||
const redeemDateRange = ref([]);
|
const redeemDateRange = ref([]);
|
||||||
const activateDateRange = ref([]);
|
const activateDateRange = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const exporting = ref(false);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
@@ -267,11 +279,9 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function formatPrice(value) {
|
/** 判断金额是否存在,保留零金额的有效展示。 */
|
||||||
if (value === null || value === undefined || value === "") {
|
function hasValue(value) {
|
||||||
return "—";
|
return value !== null && value !== undefined && value !== "";
|
||||||
}
|
|
||||||
return unitPrice(value, "¥");
|
|
||||||
}
|
}
|
||||||
function formatStatus(status) {
|
function formatStatus(status) {
|
||||||
return STATUS_LABEL[status] || status || "—";
|
return STATUS_LABEL[status] || status || "—";
|
||||||
@@ -307,6 +317,30 @@ function closePage() {
|
|||||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||||
router.go(-1);
|
router.go(-1);
|
||||||
}
|
}
|
||||||
|
/** 导出当前制卡批次全部凭证,复用后端 Excel 接口;错误响应不保存为文件。 */
|
||||||
|
async function handleExport() {
|
||||||
|
if (exporting.value || !activityId.value || !createBatchId.value || isIssueBatchMode.value) return;
|
||||||
|
const batchId = createBatchId.value;
|
||||||
|
exporting.value = true;
|
||||||
|
try {
|
||||||
|
const blob = await exportGiftCardCashCreateBatch(batchId);
|
||||||
|
if (!(blob instanceof Blob) || !blob.size) {
|
||||||
|
Message.error(blob?.message || "导出失败,请重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (blob.type.includes("json")) {
|
||||||
|
const result = JSON.parse(await blob.text());
|
||||||
|
Message.error(result.message || "导出失败,请重试");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadBlob(blob, `制卡导出_${batchId}.xlsx`);
|
||||||
|
Message.success("已开始下载");
|
||||||
|
} catch {
|
||||||
|
Message.error("导出失败,请重试");
|
||||||
|
} finally {
|
||||||
|
exporting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
function formatMemberText(name, mobile) {
|
function formatMemberText(name, mobile) {
|
||||||
const n = name != null && name !== "" ? String(name) : "";
|
const n = name != null && name !== "" ? String(name) : "";
|
||||||
const m = mobile != null && mobile !== "" ? String(mobile) : "";
|
const m = mobile != null && mobile !== "" ? String(mobile) : "";
|
||||||
@@ -474,64 +508,83 @@ function loadList() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.gcc-batch-credentials .gcc-records-toolbar {
|
.gcc-records-toolbar {
|
||||||
margin-bottom: 22px;
|
|
||||||
padding-bottom: 16px;
|
|
||||||
border-bottom: 1px solid #e8eaec;
|
|
||||||
}
|
|
||||||
.gcc-batch-credentials .gcc-records-back {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.gcc-batch-credentials .gcc-records-meta {
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #515a6e;
|
|
||||||
}
|
|
||||||
.gcc-batch-credentials .gcc-status-tabs {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
align-items: flex-start;
|
||||||
align-items: center;
|
justify-content: space-between;
|
||||||
border-bottom: 1px solid #e8eaec;
|
gap: 20px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
gap: 4px 8px;
|
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-status-tabs span {
|
.gcc-batch-heading {
|
||||||
padding: 10px 14px 12px;
|
min-width: 0;
|
||||||
cursor: pointer;
|
|
||||||
color: #808695;
|
|
||||||
font-size: 14px;
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-status-tabs span:hover {
|
.gcc-batch-actions {
|
||||||
color: #515a6e;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-status-tabs span.active {
|
.gcc-batch-heading h3 {
|
||||||
color: #ff6600;
|
margin: 0 0 10px;
|
||||||
font-weight: 500;
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-status-tabs span.active::after {
|
.gcc-records-meta {
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
right: 10px;
|
|
||||||
bottom: 0;
|
|
||||||
height: 3px;
|
|
||||||
background: #ff6600;
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
}
|
|
||||||
.gcc-batch-credentials .gcc-filter-form {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: flex-end;
|
gap: 8px 24px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-combo-item {
|
.gcc-status-tabs {
|
||||||
margin-right: 12px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.gcc-batch-credentials .gcc-combo-label {
|
.gcc-status-tabs :deep(.el-tabs__header) {
|
||||||
display: inline-block;
|
margin-bottom: 0;
|
||||||
margin-right: 6px;
|
}
|
||||||
color: #515a6e;
|
.gcc-filter-form {
|
||||||
font-size: 14px;
|
display: grid;
|
||||||
vertical-align: middle;
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
align-items: start;
|
||||||
|
gap: 16px 24px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.gcc-filter-form :deep(.el-form-item) {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.gcc-filter-form :deep(.el-form-item__content) {
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.gcc-filter-form :deep(.el-input),
|
||||||
|
.gcc-filter-form :deep(.el-date-editor) {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.gcc-combo-item :deep(.el-select) {
|
||||||
|
flex: 0 0 88px;
|
||||||
|
width: 88px;
|
||||||
|
}
|
||||||
|
.gcc-combo-item :deep(.el-input) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.gcc-filter-actions :deep(.el-form-item__content) {
|
||||||
|
padding-left: 72px;
|
||||||
|
}
|
||||||
|
.gcc-pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.gcc-filter-form {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -261,6 +261,7 @@
|
|||||||
<el-form-item label="交易类型" prop="transactionType">
|
<el-form-item label="交易类型" prop="transactionType">
|
||||||
<el-select v-model="usageSearchForm.transactionType" clearable style="width: 150px">
|
<el-select v-model="usageSearchForm.transactionType" clearable style="width: 150px">
|
||||||
<el-option label="订单抵扣" value="ORDER_DEDUCT" />
|
<el-option label="订单抵扣" value="ORDER_DEDUCT" />
|
||||||
|
<el-option label="提货兑换" value="PICKUP_REDEEM" />
|
||||||
<el-option label="订单退款" value="ORDER_REFUND" />
|
<el-option label="订单退款" value="ORDER_REFUND" />
|
||||||
<el-option label="订单取消" value="ORDER_CANCEL" />
|
<el-option label="订单取消" value="ORDER_CANCEL" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -277,7 +278,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="cardNo" label="卡号" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="cardNo" label="卡号" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column label="现金卡信息" min-width="180">
|
<el-table-column label="礼品卡信息" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<template v-if="row">
|
||||||
<div>{{ formatUsageGiftCardName(row) }}</div>
|
<div>{{ formatUsageGiftCardName(row) }}</div>
|
||||||
@@ -403,6 +404,7 @@ const issueSearchForm = reactive({
|
|||||||
const usageLoading = ref(false);
|
const usageLoading = ref(false);
|
||||||
const usageTotal = ref(0);
|
const usageTotal = ref(0);
|
||||||
const usageData = ref([]);
|
const usageData = ref([]);
|
||||||
|
let usageRequestId = 0;
|
||||||
const usageSearchForm = reactive({
|
const usageSearchForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -446,7 +448,7 @@ function formatExpireTime(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatUsageGiftCardName(row) {
|
function formatUsageGiftCardName(row) {
|
||||||
return row.giftCardName || giftCardName.value || "-";
|
return row.giftCardName || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUsageFaceValue(row) {
|
function formatUsageFaceValue(row) {
|
||||||
@@ -703,12 +705,13 @@ function formatUsageBizType(type) {
|
|||||||
ORDER_REFUND: "订单退款",
|
ORDER_REFUND: "订单退款",
|
||||||
ORDER_CANCEL: "订单取消",
|
ORDER_CANCEL: "订单取消",
|
||||||
CARD_ACTIVATE: "卡激活",
|
CARD_ACTIVATE: "卡激活",
|
||||||
|
PICKUP_REDEEM: "提货兑换",
|
||||||
};
|
};
|
||||||
return map[type] || type || "-";
|
return map[type] || type || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUsageAmountDisplay(row) {
|
function getUsageAmountDisplay(row) {
|
||||||
const raw =
|
const raw = row.bizType === "PICKUP_REDEEM" || row.transactionType === "PICKUP_REDEEM" ? row.deductAmount :
|
||||||
row.changeAmount != null && row.changeAmount !== "" ? row.changeAmount : row.deductAmount;
|
row.changeAmount != null && row.changeAmount !== "" ? row.changeAmount : row.deductAmount;
|
||||||
if (raw === null || raw === undefined || raw === "") {
|
if (raw === null || raw === undefined || raw === "") {
|
||||||
return null;
|
return null;
|
||||||
@@ -728,11 +731,16 @@ function getUsageAmountDisplay(row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadUsageRecordList() {
|
function loadUsageRecordList() {
|
||||||
|
const requestId = ++usageRequestId;
|
||||||
|
usageData.value = [];
|
||||||
|
usageTotal.value = 0;
|
||||||
if (!activityId.value) {
|
if (!activityId.value) {
|
||||||
|
usageLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
usageLoading.value = true;
|
usageLoading.value = true;
|
||||||
const params = {
|
const params = {
|
||||||
|
activityId: activityId.value,
|
||||||
pageNumber: usageSearchForm.pageNumber,
|
pageNumber: usageSearchForm.pageNumber,
|
||||||
pageSize: usageSearchForm.pageSize,
|
pageSize: usageSearchForm.pageSize,
|
||||||
sort: usageSearchForm.sort,
|
sort: usageSearchForm.sort,
|
||||||
@@ -749,6 +757,7 @@ function loadUsageRecordList() {
|
|||||||
);
|
);
|
||||||
getGiftCardCashUsageRecordPage(params)
|
getGiftCardCashUsageRecordPage(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (requestId !== usageRequestId || params.activityId !== activityId.value) return;
|
||||||
usageLoading.value = false;
|
usageLoading.value = false;
|
||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
const r = res.result;
|
const r = res.result;
|
||||||
@@ -757,6 +766,7 @@ function loadUsageRecordList() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (requestId !== usageRequestId || params.activityId !== activityId.value) return;
|
||||||
usageLoading.value = false;
|
usageLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,701 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search">
|
|
||||||
<el-card>
|
|
||||||
<el-tabs v-model="activeTab" class="sms-tabs" @tab-change="paneChange">
|
|
||||||
<el-tab-pane label="发送任务列表" name="LIST">
|
|
||||||
<div class="operation" style="margin-bottom: 10px">
|
|
||||||
<el-button type="primary" @click="sendBatchSmsModal">发送短信</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" border :data="smsData" style="width: 100%">
|
|
||||||
<el-table-column prop="smsName" label="模板名称" width="250" />
|
|
||||||
<el-table-column prop="signName" label="签名" width="150" />
|
|
||||||
<el-table-column prop="context" label="短信内容" min-width="300" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="num" label="预计发送条数" width="140" />
|
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<a v-if="row" class="link-text" @click="detail(row)">详细</a>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="smsSearchForm.pageNumber"
|
|
||||||
v-model:page-size="smsSearchForm.pageSize"
|
|
||||||
:total="smsTotal"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
@current-change="smsChangePage"
|
|
||||||
@size-change="smsChangePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="短信模板" name="TEMPLATE">
|
|
||||||
<div class="operation" style="margin-bottom: 10px">
|
|
||||||
<el-button type="primary" @click="addTemplate">添加短信模板</el-button>
|
|
||||||
<el-button type="info" @click="syncTemplate">同步</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" border :data="templateData" style="width: 100%">
|
|
||||||
<el-table-column prop="templateCode" label="模板code" min-width="120" />
|
|
||||||
<el-table-column prop="templateName" label="模板名称" min-width="140" />
|
|
||||||
<el-table-column prop="templateContent" label="模板内容" min-width="200" show-overflow-tooltip />
|
|
||||||
<el-table-column label="状态" width="120" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">{{ templateStatusText(row.templateStatus) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<a
|
|
||||||
class="link-text"
|
|
||||||
:class="{ disabled: row.templateStatus != 2 }"
|
|
||||||
@click="row.templateStatus == 2 && editTemplate(row)"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a
|
|
||||||
class="link-text"
|
|
||||||
:class="{ disabled: row.templateStatus == 0 }"
|
|
||||||
@click="row.templateStatus != 0 && deleteSmsTemplate(row)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="templateSearchForm.pageNumber"
|
|
||||||
v-model:page-size="templateSearchForm.pageSize"
|
|
||||||
:total="templateTotal"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
@current-change="templateChangePage"
|
|
||||||
@size-change="templateChangePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="短信签名" name="SIGN">
|
|
||||||
<div class="operation" style="margin-bottom: 10px">
|
|
||||||
<el-button type="primary" @click="addSign">添加短信签名</el-button>
|
|
||||||
<el-button type="info" @click="syncSign">同步</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" border :data="signData" style="width: 100%">
|
|
||||||
<el-table-column prop="signName" label="签名名称" min-width="140" />
|
|
||||||
<el-table-column prop="remark" label="申请说明" min-width="160" show-overflow-tooltip />
|
|
||||||
<el-table-column label="状态" width="160" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<template v-if="row.signStatus == 2">
|
|
||||||
审核拒绝
|
|
||||||
<el-popover trigger="hover" :content="row.reason" placement="top-start" width="200">
|
|
||||||
<template #reference>
|
|
||||||
<span style="color: #ed3f14; cursor: pointer">【原因】</span>
|
|
||||||
</template>
|
|
||||||
</el-popover>
|
|
||||||
</template>
|
|
||||||
<span v-else-if="row.signStatus == 0">审核中</span>
|
|
||||||
<span v-else-if="row.signStatus == 1">审核通过</span>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<a
|
|
||||||
class="link-text"
|
|
||||||
:class="{ disabled: row.signStatus != 2 }"
|
|
||||||
@click="row.signStatus == 2 && editSign(row)"
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a
|
|
||||||
class="link-text"
|
|
||||||
:class="{ disabled: row.signStatus == 0 }"
|
|
||||||
@click="row.signStatus != 0 && deleteSmsSign(row)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="signSearchForm.pageNumber"
|
|
||||||
v-model:page-size="signSearchForm.pageSize"
|
|
||||||
:total="signTotal"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
@current-change="signChangePage"
|
|
||||||
@size-change="signChangePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="templateModalVisible"
|
|
||||||
:title="templateModalTitle"
|
|
||||||
width="520px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<el-form ref="templateFormRef" :model="templateForm" label-width="100px" :rules="templateFormValidate">
|
|
||||||
<el-form-item label="模板名称" prop="templateName">
|
|
||||||
<el-input v-model="templateForm.templateName" maxlength="30" clearable placeholder="请输入模板名称,不超过30字符" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="模板内容" prop="templateContent">
|
|
||||||
<el-input
|
|
||||||
v-model="templateForm.templateContent"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="200"
|
|
||||||
:rows="5"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="请输入短信内容"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="申请说明" prop="remark">
|
|
||||||
<el-input
|
|
||||||
v-model="templateForm.remark"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="150"
|
|
||||||
:rows="4"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="请描述您的业务使用场景"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="templateModalVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitLoading" @click="templateSubmit">提交</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog
|
|
||||||
v-model="sendSmsModal"
|
|
||||||
:title="sendSmsModalTitle"
|
|
||||||
width="1100px"
|
|
||||||
:close-on-click-modal="false"
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<div class="send-setting">
|
|
||||||
<div class="left-show" v-html="smsContent"></div>
|
|
||||||
<div class="sms">效果预览</div>
|
|
||||||
<div class="send-form">
|
|
||||||
<el-form ref="smsFormRef" :model="smsForm" label-width="100px" :rules="smsFormValidate">
|
|
||||||
<el-form-item label="短信签名" prop="signName">
|
|
||||||
<el-select v-model="smsForm.signName" style="width: 35%" @change="selectSmsSign">
|
|
||||||
<el-option
|
|
||||||
v-for="item in smsSigns"
|
|
||||||
:key="item.signName"
|
|
||||||
:label="item.signName"
|
|
||||||
:value="item.signName"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="短信模板" prop="messageCode">
|
|
||||||
<el-select v-model="smsForm.messageCode" style="width: 35%" @change="selectSmsTemplate">
|
|
||||||
<el-option
|
|
||||||
v-for="(item, index) in smsTemplates"
|
|
||||||
:key="index"
|
|
||||||
:label="item.templateName"
|
|
||||||
:value="item.templateCode"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="短信内容" prop="context">
|
|
||||||
<el-input
|
|
||||||
v-model="smsForm.context"
|
|
||||||
type="textarea"
|
|
||||||
maxlength="50"
|
|
||||||
:rows="4"
|
|
||||||
show-word-limit
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="接收人" prop="smsRange">
|
|
||||||
<p>
|
|
||||||
已选<span style="color: #f56c1d"> {{ memberNum }}</span>人,预计耗费条数<span style="color: #f56c1d">
|
|
||||||
{{ smsForm.num }}条</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<el-radio-group v-model="smsForm.smsRange" @change="smsRangeChange">
|
|
||||||
<el-radio value="1">全部会员</el-radio>
|
|
||||||
<el-radio value="2">自定义选择</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="customSms" label-width="0" class="choose-member-item">
|
|
||||||
<div class="choose-member">
|
|
||||||
<div class="source-member">
|
|
||||||
<el-input
|
|
||||||
v-model="memberSearchParam.mobile"
|
|
||||||
placeholder="请输入手机号码"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="memberSearch"
|
|
||||||
@blur="memberSearch"
|
|
||||||
>
|
|
||||||
<template #append>
|
|
||||||
<el-button @click="memberSearch">搜索</el-button>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
<el-scrollbar height="280px" class="member-scroll" @scroll="onMemberScroll">
|
|
||||||
<div v-for="(item, index) in members" :key="index" class="scroll-card">
|
|
||||||
<el-button
|
|
||||||
class="btns"
|
|
||||||
:class="{ active: item.____selected }"
|
|
||||||
style="width: 100%; text-align: left"
|
|
||||||
@click="moveMember(index, item)"
|
|
||||||
>
|
|
||||||
<span v-if="item.mobile" class="mobile">{{ item.mobile }}</span>
|
|
||||||
<span class="nickname">{{ item.nickName }}</span>
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-scrollbar>
|
|
||||||
</div>
|
|
||||||
<div class="traget-member">
|
|
||||||
<el-tag
|
|
||||||
v-for="(item, index) in alreadyCheckShow"
|
|
||||||
:key="index"
|
|
||||||
closable
|
|
||||||
class="checkbox-tag"
|
|
||||||
@close="alreadyCheckClose(item, index)"
|
|
||||||
>
|
|
||||||
{{ item.mobile || item.nickName }}
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="sendSmsModal = false">取消</el-button>
|
|
||||||
<el-button type="primary" :loading="submitLoading" @click="sendSms">发送</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import * as API_Setting from "@/api/setting.js";
|
|
||||||
import * as API_Member from "@/api/member.js";
|
|
||||||
|
|
||||||
defineOptions({ name: "sms" });
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const activeTab = ref("LIST");
|
|
||||||
const loading = ref(false);
|
|
||||||
const customSms = ref(false);
|
|
||||||
const alreadyCheck = ref([]);
|
|
||||||
const alreadyCheckShow = ref([]);
|
|
||||||
const memberPage = ref(0);
|
|
||||||
const members = ref([]);
|
|
||||||
const smsTemplateContent = ref("");
|
|
||||||
const memberNum = ref(0);
|
|
||||||
const smsContent = ref("<div class='sms'>效果预览</div>");
|
|
||||||
const smsTemplates = ref([]);
|
|
||||||
const smsSigns = ref([]);
|
|
||||||
const sendSmsModal = ref(false);
|
|
||||||
const sendSmsModalTitle = ref("短信发送");
|
|
||||||
const modalType = ref(0);
|
|
||||||
const templateModalVisible = ref(false);
|
|
||||||
const templateModalTitle = ref("");
|
|
||||||
const templateForm = ref({});
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
const signSearchForm = reactive({ pageNumber: 1, pageSize: 20 });
|
|
||||||
const templateSearchForm = reactive({ pageNumber: 1, pageSize: 20 });
|
|
||||||
const memberSearchFrom = reactive({ disabled: "OPEN" });
|
|
||||||
const memberSearchParam = reactive({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 8,
|
|
||||||
disabled: "OPEN",
|
|
||||||
});
|
|
||||||
const smsSearchForm = reactive({
|
|
||||||
sort: "createTime",
|
|
||||||
order: "desc",
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
});
|
|
||||||
const smsForm = ref({
|
|
||||||
smsName: "",
|
|
||||||
signName: "",
|
|
||||||
context: "",
|
|
||||||
smsRange: "1",
|
|
||||||
num: 0,
|
|
||||||
messageCode: "",
|
|
||||||
});
|
|
||||||
const smsFormValidate = reactive({
|
|
||||||
signName: [{ required: true, message: "请选择短信签名", trigger: "change" }],
|
|
||||||
messageCode: [{ required: true, message: "请选择短信模板", trigger: "change" }],
|
|
||||||
});
|
|
||||||
const templateFormValidate = reactive({
|
|
||||||
templateName: [{ required: true, message: "请输入短信模板名称", trigger: "blur" }],
|
|
||||||
templateContent: [{ required: true, message: "请输入短信模板内容", trigger: "blur" }],
|
|
||||||
remark: [{ required: true, message: "请输入短信模板申请说明", trigger: "blur" }],
|
|
||||||
});
|
|
||||||
const smsData = ref([]);
|
|
||||||
const smsTotal = ref(0);
|
|
||||||
const templateData = ref([]);
|
|
||||||
const templateTotal = ref(0);
|
|
||||||
const signData = ref([]);
|
|
||||||
const signTotal = ref(0);
|
|
||||||
const memberLoadingMore = ref(false);
|
|
||||||
|
|
||||||
const smsFormRef = ref(null);
|
|
||||||
const templateFormRef = ref(null);
|
|
||||||
|
|
||||||
function templateStatusText(status) {
|
|
||||||
const map = { 0: "审核中", 1: "审核通过", 2: "审核失败" };
|
|
||||||
return map[status] ?? map[String(status)] ?? "-";
|
|
||||||
}
|
|
||||||
function init() {
|
|
||||||
getSms();
|
|
||||||
getMemberNum();
|
|
||||||
}
|
|
||||||
function getMemberNum() {
|
|
||||||
API_Member.getMemberNum(memberSearchFrom).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
memberNum.value = res.result;
|
|
||||||
smsForm.value.num = memberNum.value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function alreadyCheckClose(val, index) {
|
|
||||||
alreadyCheck.value.splice(index, 1);
|
|
||||||
alreadyCheckShow.value.splice(index, 1);
|
|
||||||
members.value.forEach((item) => {
|
|
||||||
if (item.____selected && item.mobile == val.mobile) {
|
|
||||||
item.____selected = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
smsForm.value.num--;
|
|
||||||
memberNum.value--;
|
|
||||||
}
|
|
||||||
function detail() {}
|
|
||||||
function smsRangeChange(v) {
|
|
||||||
memberNum.value = 0;
|
|
||||||
smsForm.value.num = 0;
|
|
||||||
if (v == 1) {
|
|
||||||
alreadyCheck.value = [];
|
|
||||||
alreadyCheckShow.value = [];
|
|
||||||
customSms.value = false;
|
|
||||||
getMemberNum();
|
|
||||||
}
|
|
||||||
if (v == 2) {
|
|
||||||
customSms.value = true;
|
|
||||||
members.value = [];
|
|
||||||
memberSearchParam.pageNumber = 1;
|
|
||||||
getMembers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function memberSearch() {
|
|
||||||
memberSearchParam.pageNumber = 1;
|
|
||||||
members.value = [];
|
|
||||||
getMembers();
|
|
||||||
}
|
|
||||||
function moveMember(index, item) {
|
|
||||||
if (!item.mobile) {
|
|
||||||
ElMessage.error("当前用户暂无手机号绑定");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
item.____selected = true;
|
|
||||||
if (alreadyCheck.value.length == 0) {
|
|
||||||
alreadyCheck.value.push(item.mobile);
|
|
||||||
alreadyCheckShow.value.push(item);
|
|
||||||
smsForm.value.num++;
|
|
||||||
memberNum.value++;
|
|
||||||
} else {
|
|
||||||
const result = alreadyCheck.value.indexOf(item.mobile);
|
|
||||||
if (result < 0) {
|
|
||||||
smsForm.value.num++;
|
|
||||||
memberNum.value++;
|
|
||||||
alreadyCheck.value.push(item.mobile);
|
|
||||||
alreadyCheckShow.value.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onMemberScroll({ scrollTop }) {
|
|
||||||
const wrap = document.querySelector(".member-scroll .el-scrollbar__wrap");
|
|
||||||
if (!wrap || memberLoadingMore.value) return;
|
|
||||||
const nearBottom = scrollTop + wrap.clientHeight >= wrap.scrollHeight - 20;
|
|
||||||
if (nearBottom && memberPage.value != memberSearchParam.pageNumber) {
|
|
||||||
memberLoadingMore.value = true;
|
|
||||||
memberSearchParam.pageNumber++;
|
|
||||||
getMembers(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getMembers(append = false) {
|
|
||||||
API_Member.getMemberListData(memberSearchParam).then((res) => {
|
|
||||||
memberLoadingMore.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
res.result.records.forEach((item) => {
|
|
||||||
item.____selected = false;
|
|
||||||
members.value.push(item);
|
|
||||||
});
|
|
||||||
memberPage.value = res.result.pages;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function sendBatchSmsModal() {
|
|
||||||
templateSearchForm.templateStatus = 1;
|
|
||||||
API_Setting.getSmsTemplatePage(templateSearchForm).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
smsTemplates.value = res.result.records;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
signSearchForm.signStatus = 1;
|
|
||||||
API_Setting.getSmsSignPage(signSearchForm).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
smsSigns.value = res.result.records;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
smsContent.value = "<div class='sms'>效果预览</div>";
|
|
||||||
alreadyCheck.value = [];
|
|
||||||
alreadyCheckShow.value = [];
|
|
||||||
smsTemplateContent.value = "效果预览";
|
|
||||||
smsForm.value = {
|
|
||||||
smsName: "",
|
|
||||||
signName: "",
|
|
||||||
context: "",
|
|
||||||
smsRange: "1",
|
|
||||||
num: 0,
|
|
||||||
messageCode: "",
|
|
||||||
};
|
|
||||||
getMemberNum();
|
|
||||||
sendSmsModal.value = true;
|
|
||||||
}
|
|
||||||
function paneChange(name) {
|
|
||||||
if (name == "TEMPLATE") {
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
if (name == "SIGN") {
|
|
||||||
getSmsSign();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function selectSmsSign(v) {
|
|
||||||
if (v != undefined) {
|
|
||||||
smsContent.value = "<div class='sms'>【" + v + "】" + " " + smsTemplateContent.value + "</div>";
|
|
||||||
} else {
|
|
||||||
smsContent.value = "<div class='sms'>效果预览" + smsTemplateContent.value + "</div>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function selectSmsTemplate() {
|
|
||||||
smsTemplates.value.forEach((e) => {
|
|
||||||
if (smsForm.value.messageCode == e.templateCode) {
|
|
||||||
smsTemplateContent.value = e.templateContent;
|
|
||||||
smsForm.value.smsName = e.templateName;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (smsForm.value.signName) {
|
|
||||||
smsContent.value =
|
|
||||||
"<div class='sms'>【" + smsForm.value.signName + "】" + smsTemplateContent.value + "</div>";
|
|
||||||
} else {
|
|
||||||
smsContent.value = "<div class='sms'>" + smsTemplateContent.value + "</div>";
|
|
||||||
}
|
|
||||||
smsForm.value.context = smsTemplateContent.value;
|
|
||||||
}
|
|
||||||
function deleteSmsTemplate(v) {
|
|
||||||
ElMessageBox.confirm("您确认要删除此短信模板?", "确认删除", { type: "warning" }).then(() => {
|
|
||||||
API_Setting.deleteSmsTemplatePage({ templateCode: v.templateCode }).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function sendSms() {
|
|
||||||
smsFormRef.value.validate((valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
smsForm.value.mobile = alreadyCheck.value;
|
|
||||||
API_Setting.sendSms(smsForm.value).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("发送成功");
|
|
||||||
getSms();
|
|
||||||
sendSmsModal.value = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function addSign() {
|
|
||||||
router.push({ name: "add-sms-sign" });
|
|
||||||
}
|
|
||||||
function addTemplate() {
|
|
||||||
templateModalVisible.value = true;
|
|
||||||
templateModalTitle.value = "添加短信模板";
|
|
||||||
templateForm.value = {};
|
|
||||||
modalType.value = 0;
|
|
||||||
}
|
|
||||||
function editTemplate(v) {
|
|
||||||
templateModalVisible.value = true;
|
|
||||||
templateModalTitle.value = "修改短信模板";
|
|
||||||
templateForm.value = { ...v };
|
|
||||||
modalType.value = 1;
|
|
||||||
}
|
|
||||||
function syncSign() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Setting.syncSign().then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("同步成功");
|
|
||||||
getSmsSign();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function templateSubmit() {
|
|
||||||
templateFormRef.value.validate((valid) => {
|
|
||||||
if (!valid) return;
|
|
||||||
submitLoading.value = true;
|
|
||||||
const req =
|
|
||||||
modalType.value == 0
|
|
||||||
? API_Setting.addSmsTemplatePage(templateForm.value)
|
|
||||||
: API_Setting.editSmsTemplatePage(templateForm.value);
|
|
||||||
req
|
|
||||||
.then((res) => {
|
|
||||||
submitLoading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success(modalType.value == 0 ? "添加成功" : "修改成功");
|
|
||||||
templateModalVisible.value = false;
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
submitLoading.value = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function deleteSmsSign(v) {
|
|
||||||
ElMessageBox.confirm("您确认要删除此短信签名?", "确认删除", { type: "warning" }).then(() => {
|
|
||||||
API_Setting.deleteSign(v.id).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("删除成功");
|
|
||||||
getSmsSign();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function syncTemplate() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Setting.syncTemplate().then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("同步成功");
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function smsChangePage(v) {
|
|
||||||
smsSearchForm.pageNumber = v;
|
|
||||||
getSms();
|
|
||||||
}
|
|
||||||
function smsChangePageSize(v) {
|
|
||||||
smsSearchForm.pageNumber = 1;
|
|
||||||
smsSearchForm.pageSize = v;
|
|
||||||
getSms();
|
|
||||||
}
|
|
||||||
function templateChangePage(v) {
|
|
||||||
templateSearchForm.pageNumber = v;
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
function templateChangePageSize(v) {
|
|
||||||
templateSearchForm.pageNumber = 1;
|
|
||||||
templateSearchForm.pageSize = v;
|
|
||||||
getSmsTemplate();
|
|
||||||
}
|
|
||||||
function getSmsTemplate() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Setting.getSmsTemplatePage(templateSearchForm)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
templateData.value = res.result.records;
|
|
||||||
templateTotal.value = res.result.total;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getSms() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Setting.getSmsPage(smsSearchForm)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
smsData.value = res.result.records;
|
|
||||||
smsTotal.value = res.result.total;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function signChangePage(v) {
|
|
||||||
signSearchForm.pageNumber = v;
|
|
||||||
getSmsSign();
|
|
||||||
}
|
|
||||||
function signChangePageSize(v) {
|
|
||||||
signSearchForm.pageNumber = 1;
|
|
||||||
signSearchForm.pageSize = v;
|
|
||||||
getSmsSign();
|
|
||||||
}
|
|
||||||
function editSign(v) {
|
|
||||||
router.push({ name: "add-sms-sign", query: { id: v.id } });
|
|
||||||
}
|
|
||||||
function getSmsSign() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Setting.getSmsSignPage(signSearchForm)
|
|
||||||
.then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
signData.value = res.result.records;
|
|
||||||
signTotal.value = res.result.total;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import "sms.scss";
|
|
||||||
|
|
||||||
.link-text {
|
|
||||||
color: #409eff;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
&.disabled {
|
|
||||||
color: #c5c8ce;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.op-split {
|
|
||||||
margin: 0 8px;
|
|
||||||
color: #dcdee2;
|
|
||||||
}
|
|
||||||
.mt_10 {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.sms-tabs {
|
|
||||||
min-height: 500px;
|
|
||||||
}
|
|
||||||
.checkbox-tag {
|
|
||||||
margin: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-card>
|
|
||||||
<div style="margin-top: 0">
|
|
||||||
<div class="sign-name">{{ id ? "修改签名" : "新增签名" }}</div>
|
|
||||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
|
||||||
<el-form-item label="签名" prop="signName">
|
|
||||||
<el-input
|
|
||||||
v-model="form.signName"
|
|
||||||
maxlength="12"
|
|
||||||
clearable
|
|
||||||
style="width: 28%"
|
|
||||||
placeholder="仅限2-12个字符,建议使用App应用名称或是网站名/公司名"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="签名来源" prop="signSource">
|
|
||||||
<el-select v-model="form.signSource" placeholder="请选择签名来源" style="width: 28%">
|
|
||||||
<el-option label="企事业单位的全称或简称" :value="0" />
|
|
||||||
<el-option label="工信部备案网站的全称或简称" :value="1" />
|
|
||||||
<el-option label="App应用的全称或简称" :value="2" />
|
|
||||||
<el-option label="公众号或小程序的全称或简称" :value="3" />
|
|
||||||
<el-option label="电商平台店铺名的全称或简称" :value="4" />
|
|
||||||
<el-option label="商标名的全称或简称" :value="5" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<div class="div-remark div-remark-first">
|
|
||||||
签名来源选择工信部备案网站的全称或简称时,请在说明中添加网站域名,加快审核速度;
|
|
||||||
</div>
|
|
||||||
<div class="div-remark div-remark-bottom">
|
|
||||||
如果选择APP应用的全称或简称或公众号或小程序的全称或简称,则网站、APP、小程序或公众号必须已上线;
|
|
||||||
</div>
|
|
||||||
<el-form-item label="证明文件">
|
|
||||||
<div style="float: left">
|
|
||||||
<upload-pic-thumb v-model="form.businessLicense" :multiple="false" :max-size="2048" />
|
|
||||||
</div>
|
|
||||||
<div style="float: left; margin-left: 20px">
|
|
||||||
<upload-pic-thumb v-model="form.license" :max-size="2048" :multiple="false" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<div class="div-remark div-remark-first">
|
|
||||||
第一张为营业执照,第二张为授权委托书,请上传签名归属方的企事业单位的企业营业执照、组织机构代码证、税务登记证三证合一的证件及授权委托书
|
|
||||||
</div>
|
|
||||||
<div class="div-remark div-remark-bottom">支持jpg、png、gif、jpeg格式的图片,每张图片不大于2MB</div>
|
|
||||||
<el-form-item label="申请说明" prop="remark">
|
|
||||||
<el-input
|
|
||||||
v-model="form.remark"
|
|
||||||
clearable
|
|
||||||
type="textarea"
|
|
||||||
style="width: 50%"
|
|
||||||
maxlength="100"
|
|
||||||
:autosize="{ maxRows: 4, minRows: 4 }"
|
|
||||||
show-word-limit
|
|
||||||
placeholder="请描述您的业务使用场景,不超过100字符;如:验证码、双十一大促营销"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<div class="footer">
|
|
||||||
<el-button type="primary" :loading="submitLoading" @click="addSignSubmit">提交</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from "vue";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import * as API_Setting from "@/api/setting.js";
|
|
||||||
import uploadPicThumb from "@/components/lili/upload-pic-thumb";
|
|
||||||
|
|
||||||
defineOptions({ name: "smsSign" });
|
|
||||||
|
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const id = ref(0);
|
|
||||||
const form = ref({
|
|
||||||
signName: "",
|
|
||||||
businessLicense: "",
|
|
||||||
license: "",
|
|
||||||
signSource: "",
|
|
||||||
remark: "",
|
|
||||||
});
|
|
||||||
const loading = ref(false);
|
|
||||||
const formValidate = reactive({
|
|
||||||
signName: [{ required: true, message: "签名名称不能为空", trigger: "blur" }],
|
|
||||||
remark: [{ required: true, message: "申请说明不能为空", trigger: "blur" }],
|
|
||||||
businessLicense: [{ required: true, message: " ", trigger: "blur" }],
|
|
||||||
});
|
|
||||||
const submitLoading = ref(false);
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
id.value = route.query.id;
|
|
||||||
if (id.value != undefined) {
|
|
||||||
getSmsSignDetail();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addSignSubmit() {
|
|
||||||
if (form.value.businessLicense == "" || form.value.license == "") {
|
|
||||||
ElMessage.error("请完善证件信息");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (form.value.signSource === "" || form.value.signSource === null || form.value.signSource === undefined) {
|
|
||||||
ElMessage.error("请选择签名来源");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
formRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
loading.value = true;
|
|
||||||
if (id.value == undefined) {
|
|
||||||
API_Setting.addSmsSign(form.value)
|
|
||||||
.then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("添加成功");
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
API_Setting.editSmsSign(form.value)
|
|
||||||
.then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("修改成功");
|
|
||||||
router.back();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
loading.value = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSmsSignDetail() {
|
|
||||||
API_Setting.smsSignDetail(id.value).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
form.value = res.result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.sign-name {
|
|
||||||
margin-top: 5px;
|
|
||||||
margin-left: 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.div-remark {
|
|
||||||
margin-left: 100px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.div-remark-first {
|
|
||||||
margin-top: -17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.div-remark-bottom {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-left: 100px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -986,7 +986,9 @@ function handleSelectConfirm() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const last = selectedOss.value[selectedOss.value.length - 1];
|
const last = selectedOss.value[selectedOss.value.length - 1];
|
||||||
emit("callback", { url: last.split(",")[1] });
|
const separator = last.indexOf(",");
|
||||||
|
const id = last.slice(0, separator);
|
||||||
|
emit("callback", { url: last.slice(separator + 1), file: data.value.find(item => String(item.id) === id) });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSelectCancel() {
|
function handleSelectCancel() {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<el-form ref="formValidateRef" label-width="150px" label-position="right" :model="formValidate" :rules="ruleValidate">
|
<el-form ref="formValidateRef" label-width="150px" label-position="right" :model="formValidate" :rules="ruleValidate">
|
||||||
|
<el-form-item label="微信订单自动上报" prop="wechatOrderAutoUploadEnabled">
|
||||||
|
<el-switch v-model="formValidate.wechatOrderAutoUploadEnabled" inline-prompt active-text="开启" inactive-text="关闭" />
|
||||||
|
<span class="desc wechat-order-desc">{{ formValidate.wechatOrderAutoUploadEnabled ? "已开启:符合条件的小程序订单自动上报发货信息并发送收货通知。" : "已关闭:停止微信发货上报和收货通知,商城正常发货不受影响。" }}</span>
|
||||||
|
<span class="desc wechat-order-desc">关闭此开关不会关闭微信平台侧的订单管理要求;如需上报,请通过微信后台处理。修改后点击保存生效。</span>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="订单自动取消" prop="autoCancel">
|
<el-form-item label="订单自动取消" prop="autoCancel">
|
||||||
<el-input type="number" v-model="formValidate.autoCancel">
|
<el-input type="number" v-model="formValidate.autoCancel">
|
||||||
<template #append>分</template>
|
<template #append>分</template>
|
||||||
@@ -58,6 +63,7 @@ const formValidate = ref({
|
|||||||
autoReceive: "",
|
autoReceive: "",
|
||||||
closeAfterSale: "",
|
closeAfterSale: "",
|
||||||
closeComplaint: "",
|
closeComplaint: "",
|
||||||
|
wechatOrderAutoUploadEnabled: true,
|
||||||
});
|
});
|
||||||
const result = ref("");
|
const result = ref("");
|
||||||
|
|
||||||
@@ -83,10 +89,16 @@ function setupSetting() {
|
|||||||
function init() {
|
function init() {
|
||||||
result.value = JSON.parse(props.res);
|
result.value = JSON.parse(props.res);
|
||||||
Object.keys(result.value).map((item) => {
|
Object.keys(result.value).map((item) => {
|
||||||
|
if (item === "wechatOrderAutoUploadEnabled") return;
|
||||||
result.value[item] += "";
|
result.value[item] += "";
|
||||||
});
|
});
|
||||||
formValidate.value = { ...result.value };
|
formValidate.value = {
|
||||||
|
...formValidate.value,
|
||||||
|
...result.value,
|
||||||
|
wechatOrderAutoUploadEnabled: result.value.wechatOrderAutoUploadEnabled !== false && result.value.wechatOrderAutoUploadEnabled !== "false",
|
||||||
|
};
|
||||||
Object.keys(formValidate.value).forEach((item) => {
|
Object.keys(formValidate.value).forEach((item) => {
|
||||||
|
if (item === "wechatOrderAutoUploadEnabled") return;
|
||||||
ruleValidate[item] = [
|
ruleValidate[item] = [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
@@ -130,4 +142,10 @@ init();
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wechat-order-desc {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="最低提现金额" prop="minPrice">
|
<el-form-item label="最低提现金额" prop="minPrice">
|
||||||
¥<el-input class="label-appkey" v-model="formValidate.minPrice" />
|
¥<el-input-number class="label-appkey" v-model="formValidate.minPrice" :min="0" :precision="2" :controls="false" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="提现方式" prop="type">
|
<el-form-item label="提现方式" prop="type">
|
||||||
@@ -57,12 +57,21 @@ const props = defineProps(["res", "type"]);
|
|||||||
// handleSubmit(that, name) 通过 that.$refs[name] 取表单实例
|
// handleSubmit(that, name) 通过 that.$refs[name] 取表单实例
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
const result = ref("");
|
const ruleValidate = reactive({
|
||||||
// 只做属性合并的成组对象
|
minPrice: [
|
||||||
const ruleValidate = reactive({});
|
{ required: true, message: "请填写最低提现金额", trigger: "blur" },
|
||||||
|
{ type: "number", min: 0, message: "最低提现金额必须是非负数字", trigger: "change" },
|
||||||
|
],
|
||||||
|
type: [{ required: true, message: "请选择提现方式", trigger: "change" }],
|
||||||
|
wechatAppId: [{
|
||||||
|
validator: (rule, value, callback) => {
|
||||||
|
if (formValidate.value.type === "WECHAT" && !value?.trim()) callback(new Error("请填写微信提现应用ID"));
|
||||||
|
else callback();
|
||||||
|
},
|
||||||
|
trigger: "blur",
|
||||||
|
}],
|
||||||
|
});
|
||||||
const formValidate = ref({});
|
const formValidate = ref({});
|
||||||
// 历史遗留:声明后未在模板使用,原样保留
|
|
||||||
const switchTitle = ref("提现审核是否开启");
|
|
||||||
|
|
||||||
// 模板 ref 与 data 同名会互相覆盖,改名 formValidateRef 并同步改 script
|
// 模板 ref 与 data 同名会互相覆盖,改名 formValidateRef 并同步改 script
|
||||||
const formValidateRef = ref(null);
|
const formValidateRef = ref(null);
|
||||||
@@ -83,51 +92,23 @@ function setupSetting() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** 保留字段真实类型,并兼容旧配置中的字符串数字、布尔值和字符串 null。 */
|
||||||
function init() {
|
function init() {
|
||||||
result.value = JSON.parse(props.res || "{}");
|
const data = JSON.parse(props.res || "{}");
|
||||||
Object.keys(result.value).map((item) => {
|
const values = Object.fromEntries(Object.entries(data).map(([key, value]) => {
|
||||||
result.value[item] += "";
|
const text = typeof value === "string" ? value.trim() : value;
|
||||||
});
|
return [key, text === "" || text === "null" ? null : text];
|
||||||
formValidate.value = { ...result.value };
|
}));
|
||||||
if (!formValidate.value.withdrawRule) {
|
formValidate.value = {
|
||||||
formValidate.value.withdrawRule = DEFAULT_WITHDRAW_RULE;
|
...values,
|
||||||
}
|
minPrice: values.minPrice == null ? undefined : Number(values.minPrice),
|
||||||
Object.keys(formValidate.value).forEach((item) => {
|
fee: values.fee == null ? null : Number(values.fee),
|
||||||
if (item === "minPrice") {
|
apply: values.apply === true || values.apply === "true",
|
||||||
ruleValidate[item] = [
|
type: values.type || "WECHAT",
|
||||||
{
|
wechatAppId: values.wechatAppId || "",
|
||||||
required: true,
|
wechatAppIdSource: values.wechatAppIdSource || null,
|
||||||
message: "请填写必填项",
|
withdrawRule: values.withdrawRule || DEFAULT_WITHDRAW_RULE,
|
||||||
trigger: "blur",
|
};
|
||||||
},
|
|
||||||
{
|
|
||||||
validator: (rule, value, callback) => {
|
|
||||||
if (Number(value) < 0) {
|
|
||||||
callback(new Error("不能输入负数!"));
|
|
||||||
} else {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trigger: "change",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else if (item === "withdrawRule") {
|
|
||||||
ruleValidate[item] = [
|
|
||||||
{
|
|
||||||
required: false,
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
ruleValidate[item] = [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: "请填写必填项",
|
|
||||||
trigger: "blur",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
47
seller/scripts/test-seller-navigation.mjs
Normal file
47
seller/scripts/test-seller-navigation.mjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 商家导航回归:覆盖权限裁剪、详情归属和跨分组检索,使用 Node 内置测试运行器。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-08
|
||||||
|
*/
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { flattenNavigation, findNavigationItem, searchNavigation } from '../src/libs/seller-navigation.js';
|
||||||
|
|
||||||
|
const component = () => {};
|
||||||
|
const menus = [
|
||||||
|
{ name: 'goods', title: '商品', children: [{ title: '商品管理', children: [
|
||||||
|
{ name: 'goods', title: '商品列表', component },
|
||||||
|
{ name: 'disabled', title: '不可用页面', component: null },
|
||||||
|
] }] },
|
||||||
|
{ name: 'order', title: '订单', children: [{ title: '订单管理', children: [
|
||||||
|
{ name: 'orderList', title: '商品订单', component },
|
||||||
|
] }] },
|
||||||
|
];
|
||||||
|
const items = flattenNavigation(menus);
|
||||||
|
|
||||||
|
test('搜索不补出未授权入口,也不展示无法加载的菜单', () => {
|
||||||
|
assert.equal(searchNavigation(items, '优惠券').length, 0);
|
||||||
|
assert.equal(searchNavigation(items, '不可用页面').length, 0);
|
||||||
|
});
|
||||||
|
test('详情页刷新可定位已授权业务入口', () => {
|
||||||
|
assert.equal(findNavigationItem(items, 'goods-operation-edit')?.topName, 'goods');
|
||||||
|
assert.equal(findNavigationItem(items, 'order-detail')?.name, 'orderList');
|
||||||
|
});
|
||||||
|
test('详情页对应入口被撤权后不能沿用旧账号菜单', () => {
|
||||||
|
const restricted = flattenNavigation(menus.slice(0, 1));
|
||||||
|
assert.equal(findNavigationItem(restricted, 'order-detail'), null);
|
||||||
|
assert.equal(findNavigationItem([], 'goods-operation-edit'), null);
|
||||||
|
});
|
||||||
|
test('详情本身是授权菜单时优先使用精确入口', () => {
|
||||||
|
const exact = { name: 'order-detail', topName: 'custom' };
|
||||||
|
assert.equal(findNavigationItem([...items, exact], 'order-detail'), exact);
|
||||||
|
});
|
||||||
|
test('关键词同时匹配栏目及名称,避免同名商品功能混淆', () => {
|
||||||
|
assert.deepEqual(searchNavigation(items, ' 订单 商品 ').map(item => item.name), ['orderList']);
|
||||||
|
assert.deepEqual(searchNavigation(items, '商品 列表').map(item => item.name), ['goods']);
|
||||||
|
});
|
||||||
|
test('搜索清空与空权限账号正确处理', () => {
|
||||||
|
assert.equal(searchNavigation(items, ' ').length, 2);
|
||||||
|
assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []);
|
||||||
|
assert.equal(findNavigationItem(items, 'unknown-detail'), null);
|
||||||
|
});
|
||||||
@@ -87,6 +87,11 @@ export const getShopInfo = () => {
|
|||||||
return getRequest(`/settings/storeSettings`)
|
return getRequest(`/settings/storeSettings`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取商家入驻详细信息
|
||||||
|
export const getShopEntryDetail = () => {
|
||||||
|
return getRequest(`/settings/storeSettings/storeDetail`)
|
||||||
|
}
|
||||||
|
|
||||||
// 保存商家详细信息
|
// 保存商家详细信息
|
||||||
export const saveShopInfo = (params) => {
|
export const saveShopInfo = (params) => {
|
||||||
return putRequest(`/settings/storeSettings`, params)
|
return putRequest(`/settings/storeSettings`, params)
|
||||||
@@ -127,5 +132,3 @@ export const editShipTemplate = (id, params, headers) => {
|
|||||||
export const editChecked = (logisticsId,params) => {
|
export const editChecked = (logisticsId,params) => {
|
||||||
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
|
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
mainColor: "#ff5c58", //主题色
|
mainColor: "#ff5c58", //主题色
|
||||||
setting: {
|
setting: {
|
||||||
isUseTabsRouter: true, //多标签Tab页模式
|
isUseTabsRouter: false, // 商家后台固定关闭多标签页内嵌模式,详情使用新页面展示。
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"setting": {
|
"setting": {
|
||||||
"isUseTabsRouter": true,
|
"isUseTabsRouter": false,
|
||||||
"showFooter": false
|
"showFooter": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
export default (url) =>()=>import(`@/views/${url}.vue`)
|
const explicitViews = {
|
||||||
|
"shop/shopInfo": () => import("@/views/shop/shopInfo.vue"),
|
||||||
|
"shop/addressManage": () => import("@/views/shop/addressManage.vue"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (url) => explicitViews[url] || (() => import(`@/views/${url}.vue`));
|
||||||
|
|||||||
@@ -489,6 +489,24 @@ export const result = [{
|
|||||||
component: "shop/shopSetting",
|
component: "shop/shopSetting",
|
||||||
children: null,
|
children: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "shopInfo",
|
||||||
|
level: 2,
|
||||||
|
type: 0,
|
||||||
|
title: "店铺信息",
|
||||||
|
path: "shopInfo",
|
||||||
|
component: "shop/shopInfo",
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "addressManage",
|
||||||
|
level: 2,
|
||||||
|
type: 0,
|
||||||
|
title: "地址管理",
|
||||||
|
path: "addressManage",
|
||||||
|
component: "shop/addressManage",
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "wapList",
|
name: "wapList",
|
||||||
level: 2,
|
level: 2,
|
||||||
|
|||||||
55
seller/src/libs/seller-navigation.js
Normal file
55
seller/src/libs/seller-navigation.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 商家导航的权限菜单检索及详情页归属;仅在已授权菜单中定位,不增加访问权限。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-08
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 非菜单页面回到对应业务入口;多个候选项按实际已授权入口匹配。
|
||||||
|
const DETAIL_MENUS = {
|
||||||
|
"goods-operation-edit": ["goods"],
|
||||||
|
"goods-template-operation-edit": ["template-goods"],
|
||||||
|
"goods-draft-operation-edit": ["draftGoods", "draft-goods", "goods"],
|
||||||
|
"card-key-pool": ["goods"],
|
||||||
|
"add-coupon": ["coupon"],
|
||||||
|
"coupon-receive": ["coupon"],
|
||||||
|
"add-live": ["live"],
|
||||||
|
"bill-detail": ["storeBill", "accountStatementBill"],
|
||||||
|
"order-detail": ["orderList", "virtualOrderList"],
|
||||||
|
"export-order-deliver": ["orderList"],
|
||||||
|
"order-complaint-detail": ["orderComplaint"],
|
||||||
|
"return-goods-order-detail": ["returnGoodsOrder", "returnMoneyOrder"],
|
||||||
|
"full-discount-detail": ["full-discount", "full-cut"],
|
||||||
|
"seckill-goods": ["seckill"],
|
||||||
|
"pintuan-goods": ["pintuan"],
|
||||||
|
"pintuan-edit": ["pintuan"],
|
||||||
|
main: ["wapList"],
|
||||||
|
renovation: ["pcList"],
|
||||||
|
"appointment-system-form-designer": ["appointment-system-forms"],
|
||||||
|
"procurement-purchase-order-operation": ["procurementPurchaseOrderList"],
|
||||||
|
"procurement-inbound-operation": ["procurementInboundList"],
|
||||||
|
"procurement-inventory-count-detail": ["procurementInventoryCountList"],
|
||||||
|
"procurement-damage-report-operation": ["procurementDamageReportList"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 将权限树展开为带栏目与分组信息的可跳转菜单,用于搜索和路由定位。 */
|
||||||
|
export function flattenNavigation(menus) {
|
||||||
|
return menus.flatMap((top) => (top.children || []).flatMap((group) =>
|
||||||
|
(group.children || []).filter((leaf) => leaf.name && typeof leaf.component === "function")
|
||||||
|
.map((leaf) => ({ ...leaf, topName: top.name, topTitle: top.title, groupTitle: group.title }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 优先精确匹配菜单,再匹配详情页业务入口;无授权入口时返回空,不沿用旧账号栏目。 */
|
||||||
|
export function findNavigationItem(items, routeName) {
|
||||||
|
return items.find((item) => item.name === routeName) ||
|
||||||
|
(DETAIL_MENUS[routeName] || []).map((name) => items.find((item) => item.name === name)).find(Boolean) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按菜单、分组和栏目检索,空格分隔的关键词须全部匹配。 */
|
||||||
|
export function searchNavigation(items, keyword) {
|
||||||
|
const terms = keyword.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
return items.filter((item) => {
|
||||||
|
const text = `${item.topTitle} ${item.groupTitle} ${item.title}`.toLowerCase();
|
||||||
|
return terms.every((term) => text.includes(term));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -450,47 +450,29 @@ util.initAllMenuData = function (constRoutes, data) {
|
|||||||
|
|
||||||
util.initMenuData = function (data) {
|
util.initMenuData = function (data) {
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const menuRoutes = [];
|
// 全量导航与搜索只使用当前账号权限接口返回的菜单,沿用已下线菜单过滤规则。
|
||||||
let menuData = data;
|
appStore.navigationMenus = data.map((top) => {
|
||||||
|
const children = [];
|
||||||
|
util.initRouterNode(children, top.children || []);
|
||||||
|
return { ...top, children: children.filter((group) => group.children?.length) };
|
||||||
|
}).filter((top) => top.children.length);
|
||||||
let navList = [HOME_NAV_ITEM];
|
let navList = [HOME_NAV_ITEM];
|
||||||
menuData.forEach((e) => {
|
appStore.navigationMenus.forEach((e) => {
|
||||||
navList.push({ name: e.name, title: e.title });
|
navList.push({ name: e.name, title: e.title });
|
||||||
});
|
});
|
||||||
if (navList.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appStore.setNavList(navList);
|
appStore.setNavList(navList);
|
||||||
let currNav = window.localStorage.getItem("currNav");
|
let currNav = window.localStorage.getItem("currNav");
|
||||||
if (!currNav) {
|
if (!navList.some((item) => item.name === currNav)) {
|
||||||
currNav = HOME_NAV_NAME;
|
currNav = HOME_NAV_NAME;
|
||||||
}
|
}
|
||||||
appStore.setCurrNav(currNav);
|
appStore.setCurrNav(currNav);
|
||||||
|
|
||||||
if (currNav === HOME_NAV_NAME) {
|
// 首页也必须准备全部标签入口;切换栏目只更新展示,不能依赖再次初始化路由补齐标签。
|
||||||
appStore.updateMenulist([]);
|
appStore.tagsList = appStore.routers.flatMap((item) => item.children || []);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let item of menuData) {
|
|
||||||
if (item.name == currNav) {
|
|
||||||
menuData = item.children;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
util.initRouterNode(menuRoutes, menuData);
|
|
||||||
appStore.updateMenulist(
|
appStore.updateMenulist(
|
||||||
menuRoutes.filter((item) => item.children.length > 0)
|
appStore.navigationMenus.find((item) => item.name === currNav)?.children || []
|
||||||
);
|
);
|
||||||
|
|
||||||
let tagsList = [];
|
|
||||||
appStore.routers.map((item) => {
|
|
||||||
if (item.children.length <= 1) {
|
|
||||||
tagsList.push(item.children[0]);
|
|
||||||
} else {
|
|
||||||
tagsList.push(...item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
appStore.setTagsList(tagsList);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
util.initRouterNode = function (routers, data) {
|
util.initRouterNode = function (routers, data) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const useAppStore = defineStore("app", {
|
|||||||
loading: false,
|
loading: false,
|
||||||
added: false,
|
added: false,
|
||||||
navList: [],
|
navList: [],
|
||||||
|
navigationMenus: [],
|
||||||
currNav: "",
|
currNav: "",
|
||||||
lang: "",
|
lang: "",
|
||||||
openedSubmenuArr: [],
|
openedSubmenuArr: [],
|
||||||
@@ -26,8 +27,18 @@ export const useAppStore = defineStore("app", {
|
|||||||
routers: [otherRouter],
|
routers: [otherRouter],
|
||||||
tagsList: [...otherRouter.children],
|
tagsList: [...otherRouter.children],
|
||||||
refMenu: "",
|
refMenu: "",
|
||||||
|
unreadMessageCount: 0,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 更新顶部未读消息数量。 */
|
||||||
|
setUnreadMessageCount(count) {
|
||||||
|
this.unreadMessageCount = Math.max(0, Number(count) || 0);
|
||||||
|
},
|
||||||
|
/** 清理账号绑定的路由与标签目录,保留静态页面,防止切换账号后复用旧菜单。 */
|
||||||
|
resetNavigationRoutes() {
|
||||||
|
this.routers = [otherRouter];
|
||||||
|
this.tagsList = [...otherRouter.children];
|
||||||
|
},
|
||||||
childrenMenu(v) {
|
childrenMenu(v) {
|
||||||
this.refMenu = v;
|
this.refMenu = v;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ const localSetting = window.localStorage.getItem("setting");
|
|||||||
|
|
||||||
export const useSettingStore = defineStore("setting", {
|
export const useSettingStore = defineStore("setting", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
setting: localSetting ? JSON.parse(localSetting) : setting,
|
// 兼容已保存的其他偏好,旧浏览器中的多标签开启状态统一归为关闭。
|
||||||
|
setting: { ...setting, ...(localSetting ? JSON.parse(localSetting) : {}), isUseTabsRouter: false },
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 更新其他页面偏好时,仍保持商家多标签模式关闭。 */
|
||||||
updateSetting(data) {
|
updateSetting(data) {
|
||||||
this.setting = data;
|
this.setting = { ...data, isUseTabsRouter: false };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { removeStore } from "@/libs/storage";
|
import { removeStore } from "@/libs/storage";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
// 退出登录时需要清除的会话/店铺数据
|
// 退出登录时需要清除的会话/店铺数据
|
||||||
// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键
|
// 保留 uuid(设备标识)、setting(主题)、category(缓存)、saveLogin(记住登录状态)等无关键
|
||||||
const LOGOUT_KEYS = [
|
const LOGOUT_KEYS = [
|
||||||
"accessToken",
|
"accessToken",
|
||||||
"refreshToken",
|
"refreshToken",
|
||||||
"menuData",
|
"menuData",
|
||||||
|
"currNav",
|
||||||
|
"storeOpenedList",
|
||||||
"seller_expiration_time",
|
"seller_expiration_time",
|
||||||
"sellersiteName",
|
"sellersiteName",
|
||||||
"sellerlogoImg",
|
"sellerlogoImg",
|
||||||
@@ -20,6 +23,16 @@ export const useUserStore = defineStore("user", {
|
|||||||
Cookies.remove("userInfoSeller");
|
Cookies.remove("userInfoSeller");
|
||||||
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
||||||
LOGOUT_KEYS.forEach((key) => removeStore(key));
|
LOGOUT_KEYS.forEach((key) => removeStore(key));
|
||||||
|
// 导航和已打开标签属于当前账号,退出时同步清理内存,避免下一账号复用旧权限菜单。
|
||||||
|
const appStore = useAppStore();
|
||||||
|
appStore.navigationMenus = [];
|
||||||
|
appStore.setNavList([]);
|
||||||
|
appStore.updateMenulist([]);
|
||||||
|
appStore.setCurrNav("");
|
||||||
|
appStore.setAdded(false);
|
||||||
|
appStore.setUnreadMessageCount(0);
|
||||||
|
appStore.resetNavigationRoutes();
|
||||||
|
appStore.storeOpenedList = [{ title: "首页", path: "", name: "home_index" }];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,21 +51,12 @@ export function enCode(v1) {
|
|||||||
|
|
||||||
import {router} from "@/router/index";
|
import {router} from "@/router/index";
|
||||||
/**
|
/**
|
||||||
* 自定义跳转
|
* 打开详情页面;商家后台固定关闭多标签页内嵌模式,不再读取旧浏览器偏好。
|
||||||
|
* @param push Vue Router 路由位置
|
||||||
*/
|
*/
|
||||||
export function customRouterPush(push){
|
export function customRouterPush(push){
|
||||||
const setting = window.localStorage.getItem('setting') ? JSON.parse(window.localStorage.getItem('setting')) : {};
|
const url = router.resolve(push);
|
||||||
|
window.open(url.href, '_blank');
|
||||||
if(setting.isUseTabsRouter){
|
|
||||||
router.push(push)
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
if(Object.keys(setting).length == 0){router.push(push)}
|
|
||||||
else{
|
|
||||||
let url = router.resolve(push);
|
|
||||||
window.open(url.href, '_blank');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main" :class="{ 'menu-collapsed': !showSubMenu }">
|
<div class="main">
|
||||||
<div class="sidebar-menu-con menu-bar">
|
<div class="sidebar-menu-con menu-bar">
|
||||||
<div class="logo-con">
|
|
||||||
<img alt="店铺 Logo" :src="storeSideLogo" key="max-logo" />
|
|
||||||
</div>
|
|
||||||
<shrinkable-menu />
|
<shrinkable-menu />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-header-con" :style="{ height: setting.isUseTabsRouter ? '106px' : '60px' }">
|
<div class="main-header-con">
|
||||||
|
<div class="logo-con">
|
||||||
|
<img alt="平台 Logo" :src="storeSideLogo" key="max-logo" @error="onLogoError" />
|
||||||
|
</div>
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
<div class="header-avator-con">
|
<div class="header-avator-con">
|
||||||
<div></div>
|
|
||||||
<div class="user-dropdown-menu-con">
|
<div class="user-dropdown-menu-con">
|
||||||
<el-row
|
<el-row
|
||||||
justify="end"
|
justify="end"
|
||||||
@@ -22,19 +21,36 @@
|
|||||||
>
|
>
|
||||||
<store-qrcode />
|
<store-qrcode />
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item" @click="im">
|
<li class="nav-item">
|
||||||
|
<el-tooltip
|
||||||
|
:content="unreadMessageCount > 0 ? `有 ${unreadMessageCount} 条未读消息` : '暂无未读消息'"
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-icon-button header-message-btn"
|
||||||
|
:aria-label="unreadMessageCount > 0 ? `有 ${unreadMessageCount} 条未读消息` : '消息中心'"
|
||||||
|
@click="openMessagePage"
|
||||||
|
>
|
||||||
|
<el-badge
|
||||||
|
:value="unreadMessageCount"
|
||||||
|
:max="99"
|
||||||
|
:hidden="unreadMessageCount <= 0"
|
||||||
|
class="header-message-badge"
|
||||||
|
>
|
||||||
|
<el-icon :size="18"><Bell /></el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
<el-tooltip content="联系客服" placement="bottom">
|
<el-tooltip content="联系客服" placement="bottom">
|
||||||
<el-button type="warning" size="small" :loading="load">
|
<el-button type="warning" size="small" :loading="load" @click="im">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
客服
|
客服
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" @click="handleClickSetting">
|
|
||||||
<el-tooltip content="设置" placement="bottom">
|
|
||||||
<el-icon :size="16"><Setting /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<el-dropdown trigger="hover" @command="handleClickUserDropdown">
|
<el-dropdown trigger="hover" @command="handleClickUserDropdown">
|
||||||
<div class="dropList">
|
<div class="dropList">
|
||||||
@@ -45,6 +61,7 @@
|
|||||||
>
|
>
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
|
<span class="header-store-name">{{ userInfo.storeName || '我的店铺' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -57,17 +74,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tags-con" v-if="setting.isUseTabsRouter">
|
|
||||||
<tags-page-opened :pageTagsList="pageTagsList" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="single-page-con">
|
||||||
class="single-page-con"
|
<div class="page-path-bar">
|
||||||
:style="{
|
<div class="header-page-info">
|
||||||
top: setting.isUseTabsRouter ? '106px' : '60px',
|
<template v-for="(part, index) in currentPathParts" :key="index">
|
||||||
height: setting.isUseTabsRouter ? 'calc(100% - 116px)' : 'calc(100% - 70px)',
|
<span v-if="index > 0" class="header-divider">/</span>
|
||||||
}"
|
<strong v-if="index === currentPathParts.length - 1">{{ part }}</strong>
|
||||||
>
|
<span v-else class="workspace-label">{{ part }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="single-page">
|
<div class="single-page">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<component :is="Component" v-if="Component" :key="$route.fullPath" />
|
<component :is="Component" v-if="Component" :key="$route.fullPath" />
|
||||||
@@ -75,51 +92,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<circleLoading class="loading-position" v-show="loading" />
|
<circleLoading class="loading-position" v-show="loading" />
|
||||||
<configDrawer ref="configRef" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { ChatDotRound, Setting, UserFilled } from "@element-plus/icons-vue";
|
import { Bell, ChatDotRound, UserFilled } from "@element-plus/icons-vue";
|
||||||
import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vue";
|
import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vue";
|
||||||
import tagsPageOpened from "./main-components/tags-page-opened.vue";
|
|
||||||
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
||||||
import configDrawer from "@/views/main-components/config-drawer.vue";
|
|
||||||
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import util from "@/libs/util.js";
|
import util from "@/libs/util.js";
|
||||||
import { logout } from "@/api/index";
|
import { flattenNavigation, findNavigationItem } from "@/libs/seller-navigation";
|
||||||
|
import defaultLogo from "@/assets/logo.png";
|
||||||
|
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||||
|
import { getMessageSendData, logout, userMsg } from "@/api/index";
|
||||||
import { getIMDetail } from "@/api/common";
|
import { getIMDetail } from "@/api/common";
|
||||||
import { userMsg } from "@/api/index";
|
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { getStore, setStore } from "@/libs/storage";
|
import { getStore, setStore } from "@/libs/storage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { useSettingStore } from "@/stores/setting";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const configRef = ref(null);
|
|
||||||
const sliceNum = ref(5);
|
const sliceNum = ref(5);
|
||||||
const userInfo = ref({});
|
const userInfo = ref({});
|
||||||
const storeSideLogo = ref("");
|
const storeSideLogo = ref(defaultLogo);
|
||||||
const IMLink = ref("");
|
const IMLink = ref("");
|
||||||
const load = ref(false);
|
const load = ref(false);
|
||||||
|
|
||||||
const setting = computed(() => settingStore.setting);
|
|
||||||
const loading = computed(() => appStore.loading);
|
const loading = computed(() => appStore.loading);
|
||||||
|
const unreadMessageCount = computed(() => appStore.unreadMessageCount);
|
||||||
const pageTagsList = computed(() => appStore.storeOpenedList);
|
const pageTagsList = computed(() => appStore.storeOpenedList);
|
||||||
const showSubMenu = computed(() => appStore.menuList.length > 0);
|
const currentPageTitle = computed(() => route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台');
|
||||||
|
const navigationItems = computed(() => flattenNavigation(appStore.navigationMenus || []));
|
||||||
|
const currentPathParts = computed(() => {
|
||||||
|
const matched = findNavigationItem(navigationItems.value, route.name);
|
||||||
|
const rawParts = matched
|
||||||
|
? [
|
||||||
|
matched.topTitle,
|
||||||
|
matched.groupTitle,
|
||||||
|
matched.title,
|
||||||
|
route.name !== matched.name ? currentPageTitle.value : "",
|
||||||
|
]
|
||||||
|
: ["商家后台", currentPageTitle.value];
|
||||||
|
return rawParts.filter((part, idx, arr) => part && part !== arr[idx - 1]);
|
||||||
|
});
|
||||||
const lang = computed(() => appStore.lang);
|
const lang = computed(() => appStore.lang);
|
||||||
|
|
||||||
function handleClickSetting() {
|
/**
|
||||||
configRef.value.open();
|
* 跳转至系统消息页面。
|
||||||
|
*/
|
||||||
|
function openMessagePage() {
|
||||||
|
util.openNewPage("message_index");
|
||||||
|
router.push({ name: "message_index" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取当前店铺未读消息总数,更新右上角消息角标。
|
||||||
|
*/
|
||||||
|
function loadUnreadMessageCount() {
|
||||||
|
getMessageSendData({ status: "UN_READY", pageNumber: 1, pageSize: 1 })
|
||||||
|
.then((res) => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
appStore.setUnreadMessageCount(res.result.total);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function im() {
|
async function im() {
|
||||||
@@ -142,23 +185,67 @@ async function getIMDetailMethods() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新页面站点图标。
|
||||||
|
* @param {string} href 图标地址
|
||||||
|
*/
|
||||||
|
function applyFavicon(href) {
|
||||||
|
if (!href || href === "undefined" || href === "null") return;
|
||||||
|
const link =
|
||||||
|
document.querySelector("link[rel*='icon']") ||
|
||||||
|
document.createElement("link");
|
||||||
|
link.type = "image/x-icon";
|
||||||
|
link.href = href;
|
||||||
|
link.rel = "shortcut icon";
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台 Logo 加载失败时回退为默认 Logo。
|
||||||
|
* @param {Event} e 图片加载错误事件
|
||||||
|
*/
|
||||||
|
function onLogoError(e) {
|
||||||
|
if (e?.target && e.target.src !== defaultLogo) {
|
||||||
|
e.target.src = defaultLogo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const pathArr = util.setCurrentPath(route.name);
|
const pathArr = util.setCurrentPath(route.name);
|
||||||
if (pathArr.length >= 2) {
|
if (pathArr.length >= 2) {
|
||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
|
const cachedLogo = localStorage.getItem("sellerlogoImg");
|
||||||
window.document.title = localStorage.getItem("sellersiteName");
|
storeSideLogo.value =
|
||||||
const link =
|
cachedLogo && cachedLogo !== "undefined" && cachedLogo !== "null"
|
||||||
document.querySelector("link[rel*='icon']") ||
|
? cachedLogo
|
||||||
document.createElement("link");
|
: defaultLogo;
|
||||||
link.type = "image/x-icon";
|
util.title(currentPageTitle.value);
|
||||||
link.href = localStorage.getItem("sellerIconImg");
|
applyFavicon(localStorage.getItem("sellerIconImg"));
|
||||||
link.rel = "shortcut icon";
|
|
||||||
document.getElementsByTagName("head")[0].appendChild(link);
|
loadSiteSettings()
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
const logo = data.storeSideLogo || data.domainLogo || "";
|
||||||
|
const icon = data.storeSideIcon || data.domainIcon || "";
|
||||||
|
if (logo) {
|
||||||
|
localStorage.setItem("sellerlogoImg", logo);
|
||||||
|
storeSideLogo.value = logo;
|
||||||
|
}
|
||||||
|
if (icon) {
|
||||||
|
localStorage.setItem("sellerIconImg", icon);
|
||||||
|
applyFavicon(icon);
|
||||||
|
}
|
||||||
|
if (data.siteName) {
|
||||||
|
localStorage.setItem("sellersiteName", data.siteName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
userInfo.value = JSON.parse(Cookies.get("userInfoSeller"));
|
userInfo.value = JSON.parse(Cookies.get("userInfoSeller"));
|
||||||
checkTag(route.name);
|
checkTag(route.name);
|
||||||
|
loadUnreadMessageCount();
|
||||||
|
|
||||||
const currWidth = document.body.clientWidth;
|
const currWidth = document.body.clientWidth;
|
||||||
if (currWidth <= 1200) {
|
if (currWidth <= 1200) {
|
||||||
@@ -203,6 +290,8 @@ watch(
|
|||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
checkTag(name);
|
checkTag(name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
|
loadUnreadMessageCount();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,6 +299,12 @@ watch(lang, () => {
|
|||||||
util.setCurrentPath(route.name);
|
util.setCurrentPath(route.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 刷新时权限菜单晚于布局挂载;目录到达后补齐当前标签和标题,无需再切换一次栏目。
|
||||||
|
watch(() => appStore.tagsList, () => {
|
||||||
|
checkTag(route.name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
|
});
|
||||||
|
|
||||||
appStore.setOpenedList();
|
appStore.setOpenedList();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -150,9 +150,9 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="!scheduledOnly" type="selection" width="100" align="center" />
|
<el-table-column v-if="!scheduledOnly" type="selection" width="48" align="center" />
|
||||||
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
<el-table-column prop="id" label="SPU编码" width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="商品图片" width="100" align="center">
|
<el-table-column label="商品图片" width="80" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img alt="商品图片"
|
<img alt="商品图片"
|
||||||
v-if="row && row.original"
|
v-if="row && row.original"
|
||||||
@@ -172,60 +172,57 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
||||||
<el-table-column label="价格" width="120">
|
<el-table-column label="价格" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="buyCount" label="销量" width="90" />
|
<el-table-column prop="buyCount" label="销量" width="70" />
|
||||||
<el-table-column prop="quantity" label="库存" width="90" />
|
<el-table-column prop="quantity" label="库存" width="70" />
|
||||||
<el-table-column label="商品类型" width="150">
|
<el-table-column label="商品类型" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品分类" min-width="200" show-overflow-tooltip>
|
<el-table-column label="商品分类" min-width="240" class-name="goods-category-column">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ platformCategoryText(row?.categoryPath) }}
|
{{ platformCategoryText(row?.categoryPath) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
|
<el-table-column v-if="!scheduledOnly" label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="审核状态" width="150">
|
<el-table-column label="审核状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" :width="scheduledOnly ? 150 : 320" align="right" fixed="right">
|
<el-table-column label="操作" :width="scheduledOnly ? 100 : 190" align="right" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="scheduledOnly">
|
<div class="goods-actions">
|
||||||
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
<template v-if="scheduledOnly">
|
||||||
<span class="op-split">|</span>
|
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
||||||
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="row.marketEnable === 'DOWN'">
|
<template v-if="row.marketEnable === 'DOWN'">
|
||||||
<a class="link-text" @click="upper(row)">上架</a>
|
<a class="link-text" @click="upper(row)">上架</a>
|
||||||
<span class="op-split">|</span>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
</template>
|
||||||
</template>
|
<template v-else>
|
||||||
<template v-else>
|
<a class="link-text" @click="edit(row)">下架</a>
|
||||||
<a class="link-text" @click="edit(row)">下架</a>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
</template>
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
||||||
</template>
|
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
||||||
<span class="op-split">|</span>
|
<template v-if="row.goodsType === 'E_COUPON'">
|
||||||
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
||||||
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
</template>
|
||||||
<template v-if="row.goodsType === 'E_COUPON'">
|
</template>
|
||||||
<span class="op-split">|</span>
|
</div>
|
||||||
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -753,6 +750,19 @@ onMounted(() => {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
// 分类完整换行,不使用省略号;操作按钮保持可读,额外入口允许换行。
|
||||||
|
:deep(.goods-category-column .cell) {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.goods-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 12px;
|
||||||
|
.link-text { white-space: nowrap; }
|
||||||
|
}
|
||||||
.link-text {
|
.link-text {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -2539,7 +2539,6 @@ function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
baseInfoForm.value.salesModel = "RETAIL";
|
baseInfoForm.value.salesModel = "RETAIL";
|
||||||
baseInfoForm.value.wholesaleList = [];
|
|
||||||
|
|
||||||
// 判断是否是复制商品(复制商品时不复用原有 goodsId,重新生成)
|
// 判断是否是复制商品(复制商品时不复用原有 goodsId,重新生成)
|
||||||
if (!route.query.copyId) {
|
if (!route.query.copyId) {
|
||||||
@@ -2550,7 +2549,6 @@ function save() {
|
|||||||
syncIntroFromEditors();
|
syncIntroFromEditors();
|
||||||
let submit = JSON.parse(JSON.stringify(baseInfoForm.value));
|
let submit = JSON.parse(JSON.stringify(baseInfoForm.value));
|
||||||
submit.salesModel = "RETAIL";
|
submit.salesModel = "RETAIL";
|
||||||
submit.wholesaleList = [];
|
|
||||||
|
|
||||||
// 5. 商品主图相册校验
|
// 5. 商品主图相册校验
|
||||||
if (
|
if (
|
||||||
@@ -3357,51 +3355,6 @@ onBeforeUnmount(() => {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wholesale-rule-form-item {
|
|
||||||
width: 100% !important;
|
|
||||||
min-width: 900px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-preview-table {
|
|
||||||
width: 480px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-preview-table :deep(.el-table__cell .cell) {
|
|
||||||
overflow: visible;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-rule-form-item :deep(.el-form-item__content) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-num-input {
|
|
||||||
width: 190px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-num-input :deep(.el-input__wrapper) {
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-price-cell {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-price-input {
|
|
||||||
width: 190px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wholesale-price-del {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: 100px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 单/多规格切换与单规格表格样式 */
|
/* 单/多规格切换与单规格表格样式 */
|
||||||
.spec-mode-switch-bar {
|
.spec-mode-switch-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-drawer v-model="drawer" title="页面配置" size="300px">
|
|
||||||
<h3>内容设置</h3>
|
|
||||||
<div class="config-item flex flex-a-c flex-j-sb">
|
|
||||||
<div>
|
|
||||||
<el-tooltip
|
|
||||||
placement="bottom-end"
|
|
||||||
content="关闭之后部分页面点击'查看''详情'等按钮将跳到新页面展示"
|
|
||||||
>
|
|
||||||
<div>多标签Tab页内嵌模式</div>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<el-switch v-model="setting.isUseTabsRouter" />
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from "vue";
|
|
||||||
import { useSettingStore } from "@/stores/setting";
|
|
||||||
import { setStore } from "@/libs/storage";
|
|
||||||
|
|
||||||
defineOptions({ name: "configDrawer" });
|
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const drawer = ref(false);
|
|
||||||
|
|
||||||
const setting = computed(() => settingStore.setting);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
setting,
|
|
||||||
(val) => {
|
|
||||||
setStore("setting", val);
|
|
||||||
settingStore.updateSetting(val);
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
drawer.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
drawer.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open, close });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
* {
|
|
||||||
color: #333 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 10px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-item {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-row class="header">
|
<el-row class="header">
|
||||||
<img alt="店铺 Logo" class="logo" :src="storeSideLogo" />
|
<img alt="平台 Logo" class="logo" :src="storeSideLogo" />
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,7 +16,7 @@ function init() {
|
|||||||
getSite();
|
getSite();
|
||||||
}
|
}
|
||||||
function applyFavicon(href) {
|
function applyFavicon(href) {
|
||||||
if (!href) return;
|
if (!href || href === "undefined" || href === "null") return;
|
||||||
const link =
|
const link =
|
||||||
document.querySelector("link[rel*='icon']") ||
|
document.querySelector("link[rel*='icon']") ||
|
||||||
document.createElement("link");
|
document.createElement("link");
|
||||||
@@ -29,16 +29,18 @@ function getSite() {
|
|||||||
loadSiteSettings()
|
loadSiteSettings()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const logo = data.storeSideLogo || data.domainLogo || "";
|
||||||
|
const icon = data.storeSideIcon || data.domainIcon || "";
|
||||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||||
localStorage.setItem("seller_expiration_time", expirationTime);
|
localStorage.setItem("seller_expiration_time", expirationTime);
|
||||||
localStorage.setItem("sellersiteName", data.siteName);
|
localStorage.setItem("sellersiteName", data.siteName || "");
|
||||||
localStorage.setItem("sellerlogoImg", data.storeSideLogo);
|
if (logo) localStorage.setItem("sellerlogoImg", logo);
|
||||||
localStorage.setItem("sellerIconImg", data.storeSideIcon);
|
if (icon) localStorage.setItem("sellerIconImg", icon);
|
||||||
localStorage.setItem("sellerCompanyName", data.companyName || "");
|
localStorage.setItem("sellerCompanyName", data.companyName || "");
|
||||||
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
|
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
|
||||||
storeSideLogo.value = data.storeSideLogo || storeSideLogo.value;
|
storeSideLogo.value = logo || storeSideLogo.value;
|
||||||
if (data.siteName) window.document.title = data.siteName;
|
if (data.siteName) window.document.title = data.siteName;
|
||||||
applyFavicon(data.storeSideIcon);
|
applyFavicon(icon);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -1,166 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
商家单侧栏导航:栏目纵向展开,功能入口按两列排列,沿用当前账号权限与路由定位。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-08
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="shrinkable-menu" :class="{ 'no-sub-menu': !menuList.length }">
|
<nav class="seller-navigation" aria-label="商家功能菜单">
|
||||||
<el-menu
|
<button type="button" class="nav-heading home-link" :class="{ 'is-active': route.name === 'home_index' }"
|
||||||
ref="sideMenu"
|
:aria-current="route.name === 'home_index' ? 'page' : undefined" @click="router.push({ name: 'home_index' })">
|
||||||
class="nav-menu-dark"
|
<el-icon :size="20"><House /></el-icon><span>首页</span>
|
||||||
:default-active="currNav"
|
</button>
|
||||||
@select="selectNav"
|
<section v-for="section in sections" :key="section.name" class="nav-section">
|
||||||
>
|
<button type="button" class="nav-heading" :class="{ 'is-current': activeItem?.topName === section.name }"
|
||||||
<el-menu-item v-for="(item, i) in navList" :key="i" :index="item.name">
|
:aria-label="section.title" :aria-expanded="!closedSections.includes(section.name)"
|
||||||
{{ item.title }}
|
:aria-controls="`seller-nav-${section.name}`" @click="toggleSection(section.name)">
|
||||||
</el-menu-item>
|
<el-icon :size="19"><component :is="navIcon(section)" /></el-icon>
|
||||||
</el-menu>
|
<span>{{ section.title }}</span>
|
||||||
<el-menu
|
<el-icon class="section-arrow" :class="{ 'is-open': !closedSections.includes(section.name) }" :size="14"><ArrowDown /></el-icon>
|
||||||
v-if="menuList.length"
|
</button>
|
||||||
ref="childrenMenu"
|
<div v-show="!closedSections.includes(section.name)" :id="`seller-nav-${section.name}`" class="nav-grid">
|
||||||
:key="currNav"
|
<button v-for="menu in section.items" :key="menu.name" type="button" class="nav-link"
|
||||||
class="sub-menu"
|
:class="{ 'is-active': activeItem?.name === menu.name }"
|
||||||
:default-active="$route.name"
|
:aria-current="activeItem?.name === menu.name ? 'page' : undefined"
|
||||||
@select="changeMenu"
|
:title="`${menu.groupTitle} / ${menu.title}`" @click="router.push({ name: menu.name })">
|
||||||
>
|
{{ menu.title }}
|
||||||
<template v-for="item in menuList" :key="item.id">
|
</button>
|
||||||
<el-menu-item-group :title="item.title">
|
</div>
|
||||||
<el-menu-item
|
</section>
|
||||||
v-for="menu in item.children"
|
</nav>
|
||||||
:key="menu.name"
|
|
||||||
:index="menu.name"
|
|
||||||
>
|
|
||||||
{{ menu.title }}
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu-item-group>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch, onMounted } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import util, { HOME_NAV_NAME } from "@/libs/util.js";
|
import { House, Goods, Tickets, Wallet, Present, TrendCharts, Shop, Bell, Grid, ArrowDown, User, Box } from "@element-plus/icons-vue";
|
||||||
|
import { HOME_NAV_NAME } from "@/libs/util.js";
|
||||||
|
import { flattenNavigation, findNavigationItem } from "@/libs/seller-navigation";
|
||||||
import { setStore } from "@/libs/storage";
|
import { setStore } from "@/libs/storage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
defineOptions({ name: "shrinkableMenu" });
|
defineOptions({ name: "shrinkableMenu" });
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const closedSections = ref([]);
|
||||||
|
const allItems = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
||||||
|
const activeItem = computed(() => findNavigationItem(allItems.value, route.name));
|
||||||
|
const sections = computed(() => appStore.navigationMenus.map((top) => ({
|
||||||
|
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
||||||
|
})).filter((top) => top.items.length));
|
||||||
|
|
||||||
const childrenMenu = ref(null);
|
/** 使用业务栏目图标,新增的未知栏目显示通用图标。 */
|
||||||
|
function navIcon(item) {
|
||||||
const menuList = computed(() => appStore.menuList);
|
return ({ 商品: Goods, 订单: Tickets, 财务: Wallet, 营销: Present, 统计: TrendCharts,
|
||||||
const navList = computed(() => appStore.navList);
|
数据: TrendCharts, 设置: Shop, 店铺: Shop, 消息: Bell, 会员: User, 采购: Box })[item.title] || Grid;
|
||||||
const currNav = computed(() => appStore.currNav);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
syncRouteMenu(route);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function syncRouteMenu(route) {
|
|
||||||
if (!route?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (route.name === "home_index") {
|
|
||||||
activateHomeNav();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstRouterName =
|
|
||||||
route.meta?.firstRouterName || util.getFirstRouterName(route.name);
|
|
||||||
if (firstRouterName && firstRouterName !== currNav.value) {
|
|
||||||
selectNav(firstRouterName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateHomeNav() {
|
/** 各栏目独立展开收起,可同时查看多个栏目的功能,不改变当前业务页面。 */
|
||||||
appStore.setCurrNav(HOME_NAV_NAME);
|
function toggleSection(name) {
|
||||||
setStore("currNav", HOME_NAV_NAME);
|
closedSections.value = closedSections.value.includes(name)
|
||||||
appStore.updateMenulist([]);
|
? closedSections.value.filter((item) => item !== name) : [...closedSections.value, name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMenu(name) {
|
/** 刷新、搜索和详情跳转后展开所属栏目;仅定位授权入口,不改变菜单顺序和权限。 */
|
||||||
if (!name) return;
|
async function syncRouteMenu() {
|
||||||
router.push({ name });
|
const name = activeItem.value?.topName || HOME_NAV_NAME;
|
||||||
}
|
|
||||||
|
|
||||||
function selectNav(name) {
|
|
||||||
appStore.childrenMenu(childrenMenu.value);
|
|
||||||
appStore.setCurrNav(name);
|
appStore.setCurrNav(name);
|
||||||
|
appStore.updateMenulist(appStore.navigationMenus.find((top) => top.name === name)?.children || []);
|
||||||
setStore("currNav", name);
|
setStore("currNav", name);
|
||||||
if (name === HOME_NAV_NAME) {
|
closedSections.value = closedSections.value.filter((item) => item !== name);
|
||||||
appStore.updateMenulist([]);
|
await nextTick();
|
||||||
if (route.name !== "home_index") {
|
document.querySelector('.seller-navigation .nav-link.is-active')?.scrollIntoView({ block: "nearest" });
|
||||||
router.push({ name: "home_index" });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
util.initRouter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
watch(() => appStore.navigationMenus, () => { closedSections.value = []; });
|
||||||
syncRouteMenu(route);
|
watch([() => route.fullPath, () => appStore.navigationMenus], syncRouteMenu, { immediate: true });
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.shrinkable-menu {
|
// 隐藏滚动条但保留滚轮、触控和键盘滚动,确保下方栏目仍可访问。
|
||||||
height: calc(100% - 60px);
|
.seller-navigation { height: 100%; box-sizing: border-box; padding: 18px 14px 32px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; background: #fff;
|
||||||
width: 180px;
|
&::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||||
display: flex;
|
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
&.no-sub-menu {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.nav-heading { width: 100%; display: flex; align-items: center; gap: 13px; min-height: 46px; padding: 11px 14px; border: 0; border-radius: 6px; background: transparent; color: #242832; text-align: left; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
|
||||||
.nav-menu-dark {
|
&:hover { background: #f7f8fc; }
|
||||||
width: 80px;
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
background-color: #191a23;
|
&.is-active { color: #315efb; background: #f0f4ff; }
|
||||||
border-right: none;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
:deep(.el-menu-item) {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 8px !important;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.3;
|
|
||||||
height: auto;
|
|
||||||
min-height: 56px;
|
|
||||||
white-space: normal;
|
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item:not(.is-active):hover),
|
|
||||||
:deep(.el-menu-item:not(.is-active):focus) {
|
|
||||||
background-color: #43444d !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active:hover),
|
|
||||||
:deep(.el-menu-item.is-active:focus) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.home-link { margin-bottom: 16px; }
|
||||||
.sub-menu {
|
.nav-section { margin-bottom: 22px; }
|
||||||
width: 100px;
|
.section-arrow { margin-left: auto; color: #b8bdc7; transition: transform .15s; &.is-open { transform: rotate(180deg); } }
|
||||||
overflow-y: auto;
|
.nav-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 8px; padding: 2px 5px 0 43px; }
|
||||||
border-right: none;
|
.nav-link { min-width: 0; min-height: 36px; padding: 7px 5px; border: 0; border-radius: 5px; background: transparent; color: #62666f; text-align: left; font: inherit; font-size: 13px; line-height: 22px; overflow-wrap: anywhere; cursor: pointer;
|
||||||
|
&:hover { background: #f5f7fc; color: #315efb; }
|
||||||
:deep(.el-menu-item-group__title) {
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
height: 40px;
|
&.is-active { color: #315efb; background: #eef3ff; font-weight: 600; }
|
||||||
line-height: 40px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .section-arrow { transition: none; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,15 +24,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu-con {
|
.sidebar-menu-con {
|
||||||
height: 100%;
|
height: calc(100% - 60px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 60px;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
width: 180px;
|
width: 300px;
|
||||||
transition: width 0.3s; // background: rgb(73, 80, 96)
|
transition: width 0.3s; // background: rgb(73, 80, 96)
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: rgba(0, 21, 41, 0.35) 2px 0px 6px;
|
border-right: 1px solid #e4e7ed;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-text {
|
.layout-text {
|
||||||
@@ -52,12 +53,16 @@
|
|||||||
&-header-con {
|
&-header-con {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: flex;
|
||||||
padding-left: 180px;
|
align-items: stretch;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #fff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 106px;
|
height: 60px;
|
||||||
z-index: 20;
|
z-index: 22;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
box-shadow: none;
|
||||||
transition: padding 0.3s;
|
transition: padding 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
.tags-con {
|
.tags-con {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background: #f0f0f0;
|
background: #f7f8fa;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@@ -123,10 +128,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
min-width: 740px;
|
flex: 1;
|
||||||
height: 60px;
|
min-width: 0;
|
||||||
background: #fff;
|
height: 100%;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
|
|
||||||
@@ -153,8 +159,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: 0 30px;
|
padding: 0 24px;
|
||||||
justify-content: space-between;
|
gap: 20px;
|
||||||
|
justify-content: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
|
||||||
@@ -227,15 +234,15 @@
|
|||||||
|
|
||||||
.single-page-con {
|
.single-page-con {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 180px;
|
left: 300px;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 106px;
|
top: 60px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f0f0f0;
|
background-color: #f7f8fa;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: left 0.3s;
|
transition: left 0.3s;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -247,22 +254,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.menu-collapsed {
|
|
||||||
.sidebar-menu-con {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header-con {
|
|
||||||
padding-left: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-page-con {
|
|
||||||
left: 80px;
|
|
||||||
right: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-copy {
|
&-copy {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 0 20px;
|
padding: 10px 0 20px;
|
||||||
@@ -274,21 +265,27 @@
|
|||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-con {
|
.main .logo-con {
|
||||||
width: 100%;
|
flex: 0 0 300px;
|
||||||
height: 60px;
|
height: 100%;
|
||||||
padding: 8px;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
padding: 8px 16px;
|
||||||
border-bottom: 1px solid #eee;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
img {
|
img {
|
||||||
height: 44px;
|
display: block;
|
||||||
|
max-height: 44px;
|
||||||
|
max-width: 100%;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar {
|
.menu-bar {
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar::-webkit-scrollbar {
|
.menu-bar::-webkit-scrollbar {
|
||||||
@@ -304,6 +301,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -339,3 +337,50 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导航与顶部栏的样式局限于商家外层布局,不覆盖业务页面的组件主题。
|
||||||
|
.main {
|
||||||
|
.page-path-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
.header-page-info { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 13px;
|
||||||
|
strong { color: #303749; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
}
|
||||||
|
.workspace-label { color: #969cab; white-space: nowrap; }
|
||||||
|
.header-divider { color: #d9dde6; }
|
||||||
|
.user-dropdown-innercon { flex-wrap: nowrap; gap: 16px; }
|
||||||
|
.header-store-name { margin-left: 9px; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #525a6b; }
|
||||||
|
.header-avator-con .nav-item { margin: 0; width: auto; height: auto; border: 0; border-radius: 0; }
|
||||||
|
.header-icon-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px solid #eceef3; border-radius: 6px; background: #fff; color: #626879; cursor: pointer;
|
||||||
|
&:hover { background: #f5f7fb; color: #315efb; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; }
|
||||||
|
}
|
||||||
|
.header-message-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.single-page-con .single-page { margin: 16px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main {
|
||||||
|
.header-avator-con { padding: 0 16px; gap: 12px; }
|
||||||
|
.page-path-bar { padding: 8px 14px; }
|
||||||
|
.user-dropdown-innercon { gap: 10px; }
|
||||||
|
.header-store-name { display: none; }
|
||||||
|
.single-page-con .single-page { margin: 12px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.main .sidebar-menu-con, .main-header-con, .main .single-page-con { transition: none; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,10 +84,12 @@ import { computed, onMounted, reactive, ref, watch } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { Bell, ChatDotRound, Clock, Delete } from "@element-plus/icons-vue";
|
import { Bell, ChatDotRound, Clock, Delete } from "@element-plus/icons-vue";
|
||||||
import * as API_Index from "@/api/index";
|
import * as API_Index from "@/api/index";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
defineOptions({ name: "message_index" });
|
defineOptions({ name: "message_index" });
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const mesTypeList = ref([
|
const mesTypeList = ref([
|
||||||
@@ -153,7 +155,10 @@ function refreshMessage() {
|
|||||||
}
|
}
|
||||||
function refreshUnreadCount() {
|
function refreshUnreadCount() {
|
||||||
API_Index.getAllMessage(params).then((res) => {
|
API_Index.getAllMessage(params).then((res) => {
|
||||||
if (res.success) unReadCount.value = res.result.UN_READY.total;
|
if (res.success) {
|
||||||
|
unReadCount.value = res.result.UN_READY.total;
|
||||||
|
appStore.setUnreadMessageCount(unReadCount.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getAll() {
|
function getAll() {
|
||||||
@@ -161,6 +166,7 @@ function getAll() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
unReadCount.value = res.result.UN_READY.total;
|
unReadCount.value = res.result.UN_READY.total;
|
||||||
|
appStore.setUnreadMessageCount(unReadCount.value);
|
||||||
currentMesList.value = res.result.UN_READY.records;
|
currentMesList.value = res.result.UN_READY.records;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,7 @@
|
|||||||
<el-table-column label="面额/折扣" width="100">
|
<el-table-column label="面额/折扣" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme
|
<priceColorScheme
|
||||||
v-if="row && row.price"
|
v-if="row && row.couponType === 'PRICE'"
|
||||||
:value="row.price"
|
:value="row.price"
|
||||||
:color="$mainColor"
|
:color="$mainColor"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
style="width: 240px"
|
style="width: 240px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="获取方式" prop="getType">
|
<el-form-item label="优惠券类型" prop="couponType">
|
||||||
<el-select v-model="searchForm.getType" placeholder="请选择" clearable style="width: 240px">
|
<el-select v-model="searchForm.couponType" placeholder="请选择" clearable style="width: 240px">
|
||||||
<el-option label="免费获取" value="FREE" />
|
<el-option label="满减优惠券" value="PRICE" />
|
||||||
<el-option label="活动获取" value="ACTIVITY" />
|
<el-option label="满折优惠券" value="DISCOUNT" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="活动状态" prop="promotionStatus">
|
<el-form-item label="活动状态" prop="promotionStatus">
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
<el-table-column label="面额/折扣" width="150">
|
<el-table-column label="面额/折扣" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">
|
<span v-if="row">
|
||||||
<span v-if="row.price" :style="{ color: $mainColor }">
|
<span v-if="row.couponType === 'PRICE'" :style="{ color: $mainColor }">
|
||||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||||
<span v-else>{{ row.couponDiscount }}折</span>
|
<span v-else>{{ row.couponDiscount }}折</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -94,21 +94,17 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="获取方式" width="120">
|
<el-table-column label="获取方式" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="getTypeTagType(row.getType)">{{ getTypeText(row.getType) }}</el-tag>
|
<span v-if="row">{{ getTypeText(row.getType) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="优惠券类型" width="120">
|
<el-table-column label="优惠券类型" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="couponTypeTagType(row.couponType)">
|
<span v-if="row">{{ couponTypeText(row) }}</span>
|
||||||
{{ couponTypeText(row.couponType) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="品类描述" width="120">
|
<el-table-column label="品类描述" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="scopeTypeTagType(row.scopeType)">
|
<span v-if="row">{{ scopeTypeText(row.scopeType) }}</span>
|
||||||
{{ scopeTypeText(row.scopeType) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="活动时间" width="200">
|
<el-table-column label="活动时间" width="200">
|
||||||
@@ -124,9 +120,7 @@
|
|||||||
fixed="right"
|
fixed="right"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
<span v-if="row">{{ promotionStatusText(row.promotionStatus) }}</span>
|
||||||
{{ promotionStatusText(row.promotionStatus) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@@ -269,15 +263,6 @@ function promotionStatusText(status) {
|
|||||||
return map[status] || "未知";
|
return map[status] || "未知";
|
||||||
}
|
}
|
||||||
|
|
||||||
function promotionStatusTagType(status) {
|
|
||||||
const map = {
|
|
||||||
NEW: "info",
|
|
||||||
START: "success",
|
|
||||||
END: "danger",
|
|
||||||
CLOSE: "danger",
|
|
||||||
};
|
|
||||||
return map[status] || "danger";
|
|
||||||
}
|
|
||||||
|
|
||||||
function scopeTypeText(type) {
|
function scopeTypeText(type) {
|
||||||
const map = {
|
const map = {
|
||||||
@@ -289,15 +274,6 @@ function scopeTypeText(type) {
|
|||||||
return map[type] || "未知";
|
return map[type] || "未知";
|
||||||
}
|
}
|
||||||
|
|
||||||
function scopeTypeTagType(type) {
|
|
||||||
const map = {
|
|
||||||
ALL: "info",
|
|
||||||
PORTION_GOODS_CATEGORY: "warning",
|
|
||||||
PORTION_SHOP_CATEGORY: "warning",
|
|
||||||
PORTION_GOODS: "primary",
|
|
||||||
};
|
|
||||||
return map[type] || "danger";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeText(type) {
|
function getTypeText(type) {
|
||||||
const map = {
|
const map = {
|
||||||
@@ -309,31 +285,13 @@ function getTypeText(type) {
|
|||||||
return map[type] || "未知";
|
return map[type] || "未知";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypeTagType(type) {
|
|
||||||
const map = {
|
function couponTypeText(row) {
|
||||||
FREE: "danger",
|
if (row.couponType === "DISCOUNT") return "满折优惠券";
|
||||||
ACTIVITY: "warning",
|
if (row.couponType === "PRICE") return Number(row.consumeThreshold) === 0 ? "立减优惠券" : "满减优惠券";
|
||||||
INSIDE: "success",
|
return "未知";
|
||||||
IGAME: "success",
|
|
||||||
};
|
|
||||||
return map[type] || "info";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function couponTypeText(type) {
|
|
||||||
const map = {
|
|
||||||
DISCOUNT: "打折",
|
|
||||||
PRICE: "减免现金",
|
|
||||||
};
|
|
||||||
return map[type] || "未知";
|
|
||||||
}
|
|
||||||
|
|
||||||
function couponTypeTagType(type) {
|
|
||||||
const map = {
|
|
||||||
DISCOUNT: "primary",
|
|
||||||
PRICE: "info",
|
|
||||||
};
|
|
||||||
return map[type] || "info";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSameSelection(next, current) {
|
function isSameSelection(next, current) {
|
||||||
const a = next || [];
|
const a = next || [];
|
||||||
|
|||||||
312
seller/src/views/shop/addressManage.vue
Normal file
312
seller/src/views/shop/addressManage.vue
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<!--
|
||||||
|
商家端地址管理页面:集中维护店铺发货地址与退货(售后)地址。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<div class="setting-page">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
label-width="150px"
|
||||||
|
label-position="right"
|
||||||
|
:model="form"
|
||||||
|
:rules="formRules"
|
||||||
|
>
|
||||||
|
<div class="setting-section">
|
||||||
|
<div class="section-title">发货地址</div>
|
||||||
|
<el-form-item label="发货人姓名" prop="salesConsignorName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorName"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入发货人姓名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发货人手机号" prop="salesConsignorMobile">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorMobile"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入发货人手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="salesConsignorAddressId">
|
||||||
|
<span v-if="form.salesConsignorAddressPath" class="address-text">
|
||||||
|
{{ form.salesConsignorAddressPath }}
|
||||||
|
</span>
|
||||||
|
<el-button @click="handleChangeAddress('addressGoods')">选择</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="salesConsignorDetail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorDetail"
|
||||||
|
clearable
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-section">
|
||||||
|
<div class="section-title">退货地址</div>
|
||||||
|
<el-form-item label="收货人" prop="salesConsigneeName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeName"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入收货人姓名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="收货人电话" prop="salesConsigneeMobile">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeMobile"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入收货人电话"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="售后地址" prop="salesConsigneeAddressId">
|
||||||
|
<span v-if="form.salesConsigneeAddressPath" class="address-text">
|
||||||
|
{{ form.salesConsigneeAddressPath }}
|
||||||
|
</span>
|
||||||
|
<el-button @click="handleChangeAddress('salesConsigneeAddressPath')">选择</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="salesConsigneeDetail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeDetail"
|
||||||
|
clearable
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="submit-bar">
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<multipleMap ref="mapRef" @callback="callbackAddress" />
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import * as API_Shop from "@/api/shops";
|
||||||
|
import multipleMap from "@/views/my-components/map/multiple-map";
|
||||||
|
import * as RegExp from "@/libs/RegExp.js";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
|
defineOptions({ name: "addressManage" });
|
||||||
|
|
||||||
|
const currentKey = ref("");
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
salesConsignorName: "",
|
||||||
|
salesConsignorMobile: "",
|
||||||
|
salesConsignorAddressId: "",
|
||||||
|
salesConsignorAddressPath: "",
|
||||||
|
salesConsignorDetail: "",
|
||||||
|
salesConsigneeName: "",
|
||||||
|
salesConsigneeMobile: "",
|
||||||
|
salesConsigneeAddressId: "",
|
||||||
|
salesConsigneeAddressPath: "",
|
||||||
|
salesConsigneeDetail: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
salesConsignorName: [
|
||||||
|
{ required: true, message: "请输入发货人姓名", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsignorMobile: [
|
||||||
|
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: RegExp.mobile,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "请输入正确的手机号",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
salesConsignorDetail: [
|
||||||
|
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsigneeName: [
|
||||||
|
{ required: true, message: "请输入收货人", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsigneeMobile: [
|
||||||
|
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: RegExp.mobile,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "请输入正确的手机号",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
salesConsigneeDetail: [
|
||||||
|
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const mapRef = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收地图/行政区选择器回传的地址数据。
|
||||||
|
* @param {object} val 地址选择结果
|
||||||
|
*/
|
||||||
|
function callbackAddress(val) {
|
||||||
|
if (val.type === "select") {
|
||||||
|
const paths = val.data.map((item) => item.name).join(",");
|
||||||
|
const ids = val.data.map((item) => item.id).join(",");
|
||||||
|
|
||||||
|
if (currentKey.value === "addressGoods") {
|
||||||
|
form.value.salesConsignorAddressPath = paths;
|
||||||
|
form.value.salesConsignorAddressId = ids;
|
||||||
|
}
|
||||||
|
if (currentKey.value === "salesConsigneeAddressPath") {
|
||||||
|
form.value.salesConsigneeAddressPath = paths;
|
||||||
|
form.value.salesConsigneeAddressId = ids;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentKey.value === "addressGoods") {
|
||||||
|
form.value.salesConsignorAddressPath = val.data.addr;
|
||||||
|
form.value.salesConsignorAddressId = val.data.addrId;
|
||||||
|
}
|
||||||
|
if (currentKey.value === "salesConsigneeAddressPath") {
|
||||||
|
form.value.salesConsigneeAddressPath = val.data.addr;
|
||||||
|
form.value.salesConsigneeAddressId = val.data.addrId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开地址选择弹窗。
|
||||||
|
* @param {string} key 当前编辑的地址类型标识
|
||||||
|
*/
|
||||||
|
function handleChangeAddress(key) {
|
||||||
|
currentKey.value = key;
|
||||||
|
mapRef.value.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取店铺发货地址配置。
|
||||||
|
*/
|
||||||
|
function getDeliverAddress() {
|
||||||
|
API_Shop.getDeliverAddress().then((res) => {
|
||||||
|
if (res.success && res.result) {
|
||||||
|
form.value.salesConsignorName = res.result.salesConsignorName || "";
|
||||||
|
form.value.salesConsignorMobile = res.result.salesConsignorMobile || "";
|
||||||
|
form.value.salesConsignorAddressId = res.result.salesConsignorAddressId || "";
|
||||||
|
form.value.salesConsignorAddressPath = res.result.salesConsignorAddressPath || "";
|
||||||
|
form.value.salesConsignorDetail = res.result.salesConsignorDetail || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取商家退货地址配置。
|
||||||
|
*/
|
||||||
|
function getRefundGoodsAddress() {
|
||||||
|
API_Shop.getRefundGoodsAddress().then((res) => {
|
||||||
|
if (res.success && res.result) {
|
||||||
|
form.value.salesConsigneeName = res.result.salesConsigneeName || "";
|
||||||
|
form.value.salesConsigneeMobile = res.result.salesConsigneeMobile || "";
|
||||||
|
form.value.salesConsigneeAddressId = res.result.salesConsigneeAddressId || "";
|
||||||
|
form.value.salesConsigneeAddressPath = res.result.salesConsigneeAddressPath || "";
|
||||||
|
form.value.salesConsigneeDetail = res.result.salesConsigneeDetail || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一校验并保存发货地址与退货地址配置。
|
||||||
|
*/
|
||||||
|
function handleSubmit() {
|
||||||
|
formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
submitLoading.value = true;
|
||||||
|
try {
|
||||||
|
const deliverParams = {
|
||||||
|
salesConsignorName: form.value.salesConsignorName,
|
||||||
|
salesConsignorMobile: form.value.salesConsignorMobile,
|
||||||
|
salesConsignorAddressId: form.value.salesConsignorAddressId,
|
||||||
|
salesConsignorAddressPath: form.value.salesConsignorAddressPath,
|
||||||
|
salesConsignorDetail: form.value.salesConsignorDetail,
|
||||||
|
};
|
||||||
|
const refundParams = {
|
||||||
|
salesConsigneeName: form.value.salesConsigneeName,
|
||||||
|
salesConsigneeMobile: form.value.salesConsigneeMobile,
|
||||||
|
salesConsigneeAddressId: form.value.salesConsigneeAddressId,
|
||||||
|
salesConsigneeAddressPath: form.value.salesConsigneeAddressPath,
|
||||||
|
salesConsigneeDetail: form.value.salesConsigneeDetail,
|
||||||
|
};
|
||||||
|
const [deliverRes, refundRes] = await Promise.all([
|
||||||
|
API_Shop.editDeliverAddress(deliverParams),
|
||||||
|
API_Shop.saveRefundGoodsAddress(refundParams),
|
||||||
|
]);
|
||||||
|
if (deliverRes?.success && refundRes?.success) {
|
||||||
|
Message.success("保存成功");
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
getDeliverAddress();
|
||||||
|
getRefundGoodsAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.setting-page {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px 24px 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -283,7 +283,7 @@ const ALL_COMPONENT_DEFINITIONS = [
|
|||||||
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
defaultComponentStyle: {
|
defaultComponentStyle: {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const brandList = computed(() => {
|
|||||||
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
{ title: '品质生活 · 限时第2件半价', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
294
seller/src/views/shop/shopInfo.vue
Normal file
294
seller/src/views/shop/shopInfo.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<!--
|
||||||
|
店铺信息页面:展示商家入驻时填写的企业及联系人信息、营业执照与法人信息、财务结算信息、店铺及经营信息。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="shop-info-page">
|
||||||
|
<el-card v-loading="loading" shadow="never" class="info-card">
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">店铺及经营信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="店铺名称">
|
||||||
|
{{ blankFilter(storeInfo.storeName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商家账号">
|
||||||
|
{{ blankFilter(storeInfo.memberName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺类型">
|
||||||
|
{{ storeInfo.selfOperated == 1 || storeInfo.selfOperated === true ? "自营" : "非自营" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺状态">
|
||||||
|
{{ storeStatusText(storeInfo.storeDisable) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开店时间">
|
||||||
|
{{ blankFilter(storeInfo.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺定位">
|
||||||
|
{{ blankFilter(storeInfo.storeCenter) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺所在地">
|
||||||
|
{{ blankFilter(storeInfo.storeAddressPath) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺详细地址">
|
||||||
|
{{ blankFilter(storeInfo.storeAddressDetail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺LOGO">
|
||||||
|
<el-image
|
||||||
|
v-if="isValid(storeInfo.storeLogo)"
|
||||||
|
:src="storeInfo.storeLogo"
|
||||||
|
:preview-src-list="[storeInfo.storeLogo]"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺经营类目">
|
||||||
|
{{ categories.length ? categories.map((item) => item.name).join("、") : "暂未配置" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺简介" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.storeDesc) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">企业及联系人信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="公司名称">
|
||||||
|
{{ blankFilter(storeInfo.companyName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司电话">
|
||||||
|
{{ blankFilter(storeInfo.companyPhone) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司所在地">
|
||||||
|
{{ blankFilter(storeInfo.companyAddressPath) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司详细地址">
|
||||||
|
{{ blankFilter(storeInfo.companyAddress) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="员工总数">
|
||||||
|
{{ isValid(storeInfo.employeeNum) ? `${storeInfo.employeeNum} 人` : "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="注册资金">
|
||||||
|
{{ isValid(storeInfo.registeredCapital) ? `${storeInfo.registeredCapital} 万元` : "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人姓名">
|
||||||
|
{{ blankFilter(storeInfo.linkName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人电话">
|
||||||
|
{{ blankFilter(storeInfo.linkPhone) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="电子邮箱" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.companyEmail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">营业执照与法人信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="营业执照号">
|
||||||
|
{{ blankFilter(storeInfo.licenseNum) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人姓名">
|
||||||
|
{{ blankFilter(storeInfo.legalName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人证件号" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.legalId) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法定经营范围" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.scope) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照电子版">
|
||||||
|
<el-image
|
||||||
|
v-if="isValid(storeInfo.licencePhoto)"
|
||||||
|
:src="storeInfo.licencePhoto"
|
||||||
|
:preview-src-list="[storeInfo.licencePhoto]"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人证件电子版">
|
||||||
|
<template v-if="legalPhotoList.length">
|
||||||
|
<el-image
|
||||||
|
v-for="(img, idx) in legalPhotoList"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
:preview-src-list="legalPhotoList"
|
||||||
|
:initial-index="idx"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">财务结算信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="银行开户名">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankAccountName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="银行账号">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankAccountNum) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开户银行支行名称">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankBranchName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支行联行号">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankJointName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算日" :span="2">
|
||||||
|
<span v-if="settlementCycles.length">每月 {{ settlementCycles.join("、") }} 日</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import * as API_Shop from "@/api/shops";
|
||||||
|
import { getGoodsCategoryAll } from "@/api/goods";
|
||||||
|
|
||||||
|
defineOptions({ name: "shopInfo" });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const storeInfo = ref({});
|
||||||
|
const categories = ref([]);
|
||||||
|
|
||||||
|
const isValid = (v) => v !== null && v !== undefined && v !== "" && v !== "null";
|
||||||
|
|
||||||
|
function blankFilter(v) {
|
||||||
|
return isValid(v) ? v : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeStatusText(status) {
|
||||||
|
const map = {
|
||||||
|
OPEN: "营业中",
|
||||||
|
CLOSED: "已关闭",
|
||||||
|
APPLY: "申请开店中",
|
||||||
|
APPLYING: "待平台审核",
|
||||||
|
REFUSED: "审核未通过",
|
||||||
|
};
|
||||||
|
return map[status] || status || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const legalPhotoList = computed(() => {
|
||||||
|
if (!isValid(storeInfo.value.legalPhoto)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(storeInfo.value.legalPhoto)) {
|
||||||
|
return storeInfo.value.legalPhoto.filter(isValid);
|
||||||
|
}
|
||||||
|
return String(storeInfo.value.legalPhoto)
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(isValid);
|
||||||
|
});
|
||||||
|
|
||||||
|
const settlementCycles = computed(() => {
|
||||||
|
if (!isValid(storeInfo.value.settlementCycle)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return String(storeInfo.value.settlementCycle)
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const [baseRes, detailRes, catRes] = await Promise.allSettled([
|
||||||
|
API_Shop.getShopInfo(),
|
||||||
|
API_Shop.getShopEntryDetail(),
|
||||||
|
getGoodsCategoryAll(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const merged = {};
|
||||||
|
if (baseRes.status === "fulfilled" && baseRes.value?.success && baseRes.value.result) {
|
||||||
|
Object.assign(merged, baseRes.value.result);
|
||||||
|
}
|
||||||
|
if (detailRes.status === "fulfilled" && detailRes.value?.success && detailRes.value.result) {
|
||||||
|
Object.assign(merged, detailRes.value.result);
|
||||||
|
}
|
||||||
|
storeInfo.value = merged;
|
||||||
|
|
||||||
|
if (catRes.status === "fulfilled" && catRes.value?.success && Array.isArray(catRes.value.result)) {
|
||||||
|
categories.value = catRes.value.result;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shop-info-page {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-block + .section-block {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-desc {
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 160px;
|
||||||
|
color: #606266;
|
||||||
|
background: #fafbfc;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__content) {
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__cell) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-img {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
margin: 0 10px 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card style="margin-left: 10px">
|
<el-card style="margin-left: 10px">
|
||||||
<el-tabs v-model="type" @tab-click="handleClickType">
|
<el-tabs v-model="type">
|
||||||
<el-tab-pane label="基本信息" name="INFO">
|
<el-tab-pane label="基本信息" name="INFO">
|
||||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||||
<el-form-item label="店铺名称">
|
<el-form-item label="店铺名称">
|
||||||
@@ -34,85 +34,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="发货地址">
|
|
||||||
<el-form ref="addressGoodsRef" :model="addressGoods" label-width="120px" :rules="addressGoodsValidate" class="shop-setting-form">
|
|
||||||
<el-form-item label="发货人姓名" prop="salesConsignorName">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorName" maxlength="11" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="发货人手机号" prop="salesConsignorMobile">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorMobile" maxlength="11" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="地址" prop="salesConsignorAddressId">
|
|
||||||
<span>{{ addressGoods.salesConsignorAddressPath }}</span>
|
|
||||||
<el-button style="margin-left: 10px;" @click="handleChangeAddress('addressGoods')">选择</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<!-- <el-form-item label="地址名称" prop="salesConsignorAddressPath">
|
|
||||||
<el-input
|
|
||||||
v-model="addressGoods.salesConsignorAddressPath"
|
|
||||||
clearable
|
|
||||||
style="width: 20%"
|
|
||||||
>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item label="详细地址" prop="salesConsignorDetail">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorDetail" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-button @click="setAddressGoods" type="primary" style="margin-left: 8px">确认
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="退货地址" name="REFUND_GOODS_ADDRESS">
|
|
||||||
<el-form ref="addressFormRef" :model="addressForm" label-width="100px" :rules="afterFormValidate">
|
|
||||||
<el-form-item label="收货人" prop="salesConsigneeName">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeName" maxlength="11" clearable style="width: 20%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="收货人电话" prop="salesConsigneeMobile">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeMobile" maxlength="11" style="width: 20%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="售后地址">
|
|
||||||
<span>{{ addressForm.salesConsigneeAddressPath }}</span>
|
|
||||||
<el-button style="margin-left: 10px;" @click="handleChangeAddress('salesConsigneeAddressPath')">选择</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="详细地址" prop="salesConsigneeDetail">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeDetail" clearable style="width: 20%" maxlength="50" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="afterHandleSubmit" :loading="submitLoading" type="primary" style="margin-right: 5px">修改
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
<!-- Udesk 坐席功能 后续维护 -->
|
|
||||||
<!-- <el-tab-pane label="客服设置" name="UDESK">
|
|
||||||
<el-form
|
|
||||||
ref="udeskForm"
|
|
||||||
:model="udeskForm"
|
|
||||||
label-width="100px"
|
|
||||||
:rules="udeskFormValidate"
|
|
||||||
>
|
|
||||||
<el-form-item label="坐席id" prop="merchantEuid">
|
|
||||||
<el-input
|
|
||||||
v-model="udeskForm.merchantEuid"
|
|
||||||
maxlength="30"
|
|
||||||
clearable
|
|
||||||
style="width: 20%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button
|
|
||||||
@click="merchantSubmit"
|
|
||||||
:loading="submitLoading"
|
|
||||||
type="primary"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
>修改
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane> -->
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -126,53 +47,23 @@ import * as API_Shop from "@/api/shops";
|
|||||||
import { validateMobile } from "@/libs/validate";
|
import { validateMobile } from "@/libs/validate";
|
||||||
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
|
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
|
||||||
import multipleMap from "@/views/my-components/map/multiple-map";
|
import multipleMap from "@/views/my-components/map/multiple-map";
|
||||||
import * as RegExp from "@/libs/RegExp.js";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
defineOptions({ name: "shopSetting" });
|
defineOptions({ name: "shopSetting" });
|
||||||
|
|
||||||
const currentKey = ref(""); //存储当前点击地址的key
|
const currentKey = ref(""); // 存储当前点击地址的key
|
||||||
const type = ref("INFO");
|
const type = ref("INFO");
|
||||||
|
|
||||||
const storeName = ref(""); //店铺名称
|
const storeName = ref(""); // 店铺名称
|
||||||
|
|
||||||
const addressForm = ref({
|
|
||||||
// 退货地址
|
|
||||||
salesConsigneeName: "", // 收货人姓名
|
|
||||||
salesConsigneeMobile: "", // 收货人电话
|
|
||||||
salesConsigneeAddressId: "", // 售后地址id,逗号分割
|
|
||||||
salesConsigneeAddressPath: "", // 售后地址,逗号分割
|
|
||||||
salesConsigneeDetail: "", // 详细地址
|
|
||||||
});
|
|
||||||
//im form
|
|
||||||
const udeskForm = ref({
|
|
||||||
merchantEuid: "",
|
|
||||||
});
|
|
||||||
const afterFormValidate = {
|
|
||||||
salesConsigneeMobile: [
|
|
||||||
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
|
||||||
{
|
|
||||||
pattern: RegExp.mobile,
|
|
||||||
trigger: "blur",
|
|
||||||
message: "请输入正确的手机号",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesConsigneeName: [
|
|
||||||
{ required: true, message: "请输入收货人", trigger: "blur" },
|
|
||||||
],
|
|
||||||
salesConsigneeDetail: [
|
|
||||||
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
// 添加或编辑表单对象初始化数据
|
|
||||||
storeAddressPath: "", // 店铺地址中文
|
storeAddressPath: "", // 店铺地址中文
|
||||||
storeCenter: "", // 经度 + 纬度
|
storeCenter: "", // 经度 + 纬度
|
||||||
longitude: "", //经度
|
longitude: "", // 经度
|
||||||
latitude: "", //纬度
|
latitude: "", // 纬度
|
||||||
storeAddressDetail: "", //详细地址
|
storeAddressDetail: "", // 详细地址
|
||||||
storeAddressIdPath: "", //地址
|
storeAddressIdPath: "", // 地址
|
||||||
storeDesc: "", // 店铺描述
|
storeDesc: "", // 店铺描述
|
||||||
pageShow: false,
|
pageShow: false,
|
||||||
selfPickFlag: false,
|
selfPickFlag: false,
|
||||||
@@ -220,134 +111,69 @@ const formValidate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const submitLoading = ref(false); // 添加或编辑提交状态
|
const submitLoading = ref(false);
|
||||||
//发货地址
|
|
||||||
const addressGoods = ref({
|
|
||||||
salesConsignorName: " ",
|
|
||||||
salesConsignorMobile: " ",
|
|
||||||
salesConsignorAddressId: " ",
|
|
||||||
salesConsignorAddressPath: " ",
|
|
||||||
salesConsignorDetail: " ",
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressGoodsValidate = {
|
|
||||||
salesConsignorName: [
|
|
||||||
{ required: true, message: "请输入发货人姓名", trigger: "blur" },
|
|
||||||
],
|
|
||||||
salesConsignorMobile: [
|
|
||||||
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
|
||||||
{
|
|
||||||
pattern: RegExp.mobile,
|
|
||||||
trigger: "blur",
|
|
||||||
message: "请输入正确的手机号",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesConsignorDetail: [
|
|
||||||
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const addressGoodsRef = ref(null);
|
|
||||||
const addressFormRef = ref(null);
|
|
||||||
const mapRef = ref(null);
|
const mapRef = ref(null);
|
||||||
|
|
||||||
// 回调地址
|
// 回调地址
|
||||||
function callbackAddress(val) {
|
function callbackAddress(val) {
|
||||||
if (val.type === 'select') {
|
if (val.type === "select") {
|
||||||
const paths = val.data.map(item => item.name).join(',')
|
const paths = val.data.map((item) => item.name).join(",");
|
||||||
|
const ids = val.data.map((item) => item.id).join(",");
|
||||||
|
|
||||||
const ids = val.data.map(item => item.id).join(',')
|
if (currentKey.value === "storeAddress") {
|
||||||
|
form.value.storeAddressPath = paths;
|
||||||
if (currentKey.value === 'storeAddress') {
|
form.value.storeAddressIdPath = ids;
|
||||||
form.value.storeAddressPath = paths
|
form.value.storeCenter = val.data[val.data.length - 1].center;
|
||||||
form.value.storeAddressIdPath = ids
|
|
||||||
form.value.storeCenter = val.data[val.data.length - 1].center
|
|
||||||
}
|
}
|
||||||
if (currentKey.value === 'addressGoods') {
|
} else {
|
||||||
addressGoods.value.salesConsignorAddressPath = paths
|
if (
|
||||||
addressGoods.value.salesConsignorAddressId = ids
|
currentKey.value === "storeAddress" ||
|
||||||
}
|
currentKey.value === "storeAddressstoreAddress"
|
||||||
if (currentKey.value === 'salesConsigneeAddressPath') {
|
) {
|
||||||
addressForm.value.salesConsigneeAddressPath = paths
|
|
||||||
addressForm.value.salesConsigneeAddressId = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (currentKey.value === 'storeAddressstoreAddress') {
|
|
||||||
form.value.storeAddressPath = val.data.addr;
|
form.value.storeAddressPath = val.data.addr;
|
||||||
form.value.storeAddressIdPath = val.data.addrId;
|
form.value.storeAddressIdPath = val.data.addrId;
|
||||||
form.value.storeCenter = val.data.position.lng + "," + val.data.position.lat;
|
form.value.storeCenter =
|
||||||
}
|
val.data.position.lng + "," + val.data.position.lat;
|
||||||
if (currentKey.value === 'addressGoods') {
|
|
||||||
addressGoods.value.salesConsignorAddressPath = val.data.addr;
|
|
||||||
addressGoods.value.salesConsignorAddressId = val.data.addrId;
|
|
||||||
}
|
|
||||||
if (currentKey.value === 'salesConsigneeAddressPath') {
|
|
||||||
addressForm.value.salesConsigneeAddressPath = val.data.addr;
|
|
||||||
addressForm.value.salesConsigneeAddressId = val.data.addrId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改地址
|
// 修改地址
|
||||||
function handleChangeAddress(key) {
|
function handleChangeAddress(key) {
|
||||||
currentKey.value = key
|
currentKey.value = key;
|
||||||
mapRef.value.open();
|
mapRef.value.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
function init() {
|
function init() {
|
||||||
getShopInfo();
|
getShopInfo();
|
||||||
getDeliverAddress()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取店铺信息
|
||||||
function setAddressGoods() {
|
|
||||||
addressGoodsRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
API_Shop.editDeliverAddress(addressGoods.value).then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
Message.success("修改成功")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//获取店铺信息
|
|
||||||
function getShopInfo() {
|
function getShopInfo() {
|
||||||
API_Shop.getShopInfo().then((res) => {
|
API_Shop.getShopInfo().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
form.value = res.result;
|
form.value = res.result;
|
||||||
|
|
||||||
storeName.value = res.result.storeName;
|
storeName.value = res.result.storeName;
|
||||||
form.value.storeCenter = res.result.storeCenter;
|
form.value.storeCenter = res.result.storeCenter;
|
||||||
Cookies.set("userInfoSeller", JSON.stringify(res.result));
|
Cookies.set("userInfoSeller", JSON.stringify(res.result));
|
||||||
if (res.result.merchantEuid) {
|
|
||||||
//赋予坐席id
|
|
||||||
udeskForm.value.merchantEuid = res.result.merchantEuid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function pageShow(type) {
|
|
||||||
form.value.pageShow = type
|
function pageShow(val) {
|
||||||
}
|
form.value.pageShow = val;
|
||||||
function changeSelfPickFlag(item) {
|
|
||||||
if (item) {
|
|
||||||
form.value.selfPickFlag = item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getDeliverAddress() {
|
|
||||||
API_Shop.getDeliverAddress().then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
if (res.result != '' && res.result != null) {
|
|
||||||
addressGoods.value = res.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//提交保存
|
function changeSelfPickFlag(item) {
|
||||||
|
if (item) {
|
||||||
|
form.value.selfPickFlag = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交保存
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
formRef.value.validate((valid) => {
|
formRef.value.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
@@ -363,42 +189,6 @@ function handleSubmit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//tab切换
|
|
||||||
function handleClickType(pane) {
|
|
||||||
const name = pane.paneName;
|
|
||||||
//退款
|
|
||||||
if (name === "INFO") {
|
|
||||||
getShopInfo();
|
|
||||||
}
|
|
||||||
//退货
|
|
||||||
if (name === "REFUND_GOODS_ADDRESS") {
|
|
||||||
getRefundGoodsAddress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//获取商家退货地址
|
|
||||||
function getRefundGoodsAddress() {
|
|
||||||
API_Shop.getRefundGoodsAddress().then((res) => {
|
|
||||||
if (res.result != null) {
|
|
||||||
addressForm.value = res.result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//提交保存
|
|
||||||
function afterHandleSubmit() {
|
|
||||||
addressFormRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
submitLoading.value = true;
|
|
||||||
API_Shop.saveRefundGoodsAddress(addressForm.value).then((res) => {
|
|
||||||
submitLoading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
Message.success("修改成功");
|
|
||||||
getRefundGoodsAddress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
@@ -408,15 +198,4 @@ onMounted(() => {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shop-setting-form {
|
|
||||||
:deep(.el-form-item) {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user