mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 01:18:54 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2497f33cb4 | ||
|
|
0f08807f00 | ||
|
|
29a6b00a21 | ||
|
|
6be934d00b | ||
|
|
560286b7f6 | ||
|
|
961565149c | ||
|
|
1a457a60f8 | ||
|
|
d521587ad8 | ||
|
|
cbaf89c835 | ||
|
|
f6f106e12b | ||
|
|
484b6a6dad | ||
|
|
d64dd08b42 | ||
|
|
5300988643 | ||
|
|
fb99824a3a | ||
|
|
22f3e9b5e9 | ||
|
|
7db1ee4fb7 | ||
|
|
4e33ab19b6 | ||
|
|
ce11f50942 | ||
|
|
ccf243f31a | ||
|
|
c0cc8d8598 |
216
LICENSE
216
LICENSE
@@ -1,21 +1,205 @@
|
|||||||
MIT License
|
Apache License, Version 2.0
|
||||||
|
|
||||||
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Apache License
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
Version 2.0, January 2004
|
||||||
in the Software without restriction, including without limitation the rights
|
http://www.apache.org/licenses/
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1. Definitions.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
SOFTWARE.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
||||||
|
|
||||||
### ✨ Why Choose Lilishop
|
### ✨ Why Choose Lilishop
|
||||||
- 🟢 **True MIT License**: Free for commercial use and private secondary development without licensing concerns.
|
- 🟢 **True Apache-2.0 License**: Free for commercial use and private secondary development without licensing concerns.
|
||||||
- 🚀 **Modern Vue 3 Stack**: Built on `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
|
- 🚀 **Modern Vue 3 Stack**: Built on `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
|
||||||
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
|
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
|
||||||
|
|
||||||
@@ -141,4 +141,4 @@ lilishop-ui
|
|||||||
|
|
||||||
## 6. License
|
## 6. License
|
||||||
|
|
||||||
Lilishop is licensed under the [MIT](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
Lilishop is licensed under the [Apache-2.0](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -5,7 +5,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||||
|
|
||||||
### ✨ 为什么选择 Lilishop
|
### ✨ 为什么选择 Lilishop
|
||||||
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
- 🟢 **真 Apache-2.0 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||||
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
||||||
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ lilishop-ui
|
|||||||
|
|
||||||
## 6. 开源与授权
|
## 6. 开源与授权
|
||||||
|
|
||||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
本项目遵循 [Apache-2.0](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -163,19 +163,44 @@ lilishop-ui
|
|||||||
## 8. 升级计划
|
## 8. 升级计划
|
||||||
|
|
||||||
### ✅ 已完成特性
|
### ✅ 已完成特性
|
||||||
| 功能模块 | 功能模块 |
|
|
||||||
| :--- | :--- |
|
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
|
||||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
|
||||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
| 时间 | 功能模块 | 完成内容 |
|
||||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
| :--- | :--- | :--- |
|
||||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
| 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 | ✅ 会员分组 | 新增会员分组管理。 |
|
||||||
|
|
||||||
### 🔜 近期规划中
|
### 🔜 近期规划中
|
||||||
- 🔜 店铺结算汇总 & 结算台账
|
|
||||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
|
||||||
- 🔜 微信小店对接
|
- 🔜 微信小店对接
|
||||||
- 🔜 抖店对接
|
- 🔜 抖店对接
|
||||||
|
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
|
||||||
|
- 🔜 OpenAPI 开放平台与开发者中心
|
||||||
|
|||||||
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 });
|
||||||
|
/** 一次兑换;重试返回同一卡已创建的交易。 */
|
||||||
|
export const redeemPickupCard = data => request({ url: '/buyer/promotion/pickupCard/redeem', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data });
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||||
|
.el-textarea__inner {
|
||||||
|
resize: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--el-color-primary: var(--theme-color, #ff3c2a);
|
--el-color-primary: var(--theme-color, #ff3c2a);
|
||||||
--el-color-danger: var(--theme-color, #ff3c2a);
|
--el-color-danger: var(--theme-color, #ff3c2a);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -43,25 +43,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="information footer-bottmom">
|
<div class="information footer-bottmom">
|
||||||
<div class="icp">
|
<div class="icp">
|
||||||
<li v-if="config.icpCard">
|
<li v-if="icp" class="footer-bottmom">
|
||||||
<a :href='"https://tsm.miit.gov.cn/dxxzsp/xkz/xkzgl/resource/qiyesearch.jsp?num="+config.icpCard+"&type=xuke"' target="_blank">
|
|
||||||
{{config.icpCard}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li v-if="config.icpMessage" class="footer-bottmom">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||||
{{config.icpMessage}}
|
{{icp}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<a class="flex " :href="config.company.href">
|
<a v-if="companyName" class="flex " :href="companyUrl || undefined">
|
||||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{config.company.name}}
|
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{companyName}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<p>Copyright © {{year}} {{config.title}}</p>
|
<p>Copyright © {{year}} {{copyrightOwner}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -71,11 +66,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import config from '@/config'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
|
|
||||||
defineOptions({ name: 'Footer' })
|
defineOptions({ name: 'Footer' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { companyName, companyUrl, icp, copyrightOwner } = useSiteSettings()
|
||||||
|
|
||||||
const guideArr = ref([
|
const guideArr = ref([
|
||||||
// 导航链接
|
// 导航链接
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
class="component-cell"
|
class="component-cell"
|
||||||
|
|
||||||
:style="getComponentCellStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1)"
|
:style="getComponentCellStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1, comp.type)"
|
||||||
|
|
||||||
>
|
>
|
||||||
|
|
||||||
@@ -69,6 +69,10 @@ const props = defineProps({
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const hasAutoComp = computed(() => {
|
||||||
|
return Array.isArray(props.row.components) && props.row.components.some(c => ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'].includes(c?.type));
|
||||||
|
});
|
||||||
|
|
||||||
const rowWrapperStyle = computed(() => {
|
const rowWrapperStyle = computed(() => {
|
||||||
const s = props.row.rowStyle || {};
|
const s = props.row.rowStyle || {};
|
||||||
const style = {};
|
const style = {};
|
||||||
@@ -96,8 +100,12 @@ const rowWrapperStyle = computed(() => {
|
|||||||
const h = String(s.height).trim();
|
const h = String(s.height).trim();
|
||||||
if (h && Number(h) > 0) {
|
if (h && Number(h) > 0) {
|
||||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
style.height = hVal;
|
if (hasAutoComp.value) {
|
||||||
style.minHeight = hVal;
|
style.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
style.height = hVal;
|
||||||
|
style.minHeight = hVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,8 +172,12 @@ const rowInnerFlexStyle = computed(() => {
|
|||||||
const h = String(s.height).trim();
|
const h = String(s.height).trim();
|
||||||
if (h && Number(h) > 0) {
|
if (h && Number(h) > 0) {
|
||||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
styleObj.height = hVal;
|
if (hasAutoComp.value) {
|
||||||
styleObj.minHeight = hVal;
|
styleObj.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
styleObj.height = hVal;
|
||||||
|
styleObj.minHeight = hVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +187,8 @@ const rowInnerFlexStyle = computed(() => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
|
function getComponentCellStyle(style, rowStyle = {}, totalComps = 1, compType = '') {
|
||||||
|
const isAutoHeightComp = compType && ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'].includes(compType);
|
||||||
|
|
||||||
if (!style) return { flex: '1 1 0%' };
|
if (!style) return { flex: '1 1 0%' };
|
||||||
|
|
||||||
@@ -185,8 +198,12 @@ function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
|
|||||||
const rh = String(rowStyle.height).trim();
|
const rh = String(rowStyle.height).trim();
|
||||||
if (rh && Number(rh) > 0) {
|
if (rh && Number(rh) > 0) {
|
||||||
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
||||||
s.height = rhVal;
|
if (isAutoHeightComp) {
|
||||||
s.minHeight = rhVal;
|
s.minHeight = rhVal;
|
||||||
|
} else {
|
||||||
|
s.height = rhVal;
|
||||||
|
s.minHeight = rhVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const gap = Number(rowStyle?.gap) || 0;
|
const gap = Number(rowStyle?.gap) || 0;
|
||||||
|
|||||||
@@ -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' }
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
:key="idx"
|
:key="idx"
|
||||||
class="zone-link"
|
class="zone-link"
|
||||||
:style="{
|
:style="{
|
||||||
top: `${zone.top || 0}%`,
|
top: `${zone.top !== undefined ? zone.top : ((zone.topPer || 0) * 100)}%`,
|
||||||
left: `${zone.left || 0}%`,
|
left: `${zone.left !== undefined ? zone.left : ((zone.leftPer || 0) * 100)}%`,
|
||||||
width: `${zone.width || 10}%`,
|
width: `${zone.width !== undefined ? zone.width : ((zone.widthPer || 0.1) * 100)}%`,
|
||||||
height: `${zone.height || 10}%`
|
height: `${zone.height !== undefined ? zone.height : ((zone.heightPer || 0.1) * 100)}%`
|
||||||
}"
|
}"
|
||||||
:title="zone.title"
|
:title="zone.title"
|
||||||
@click="handleClickZone(zone)"
|
@click="handleClickZone(zone)"
|
||||||
@@ -68,7 +68,7 @@ const customStyle = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleClickZone(zone) {
|
function handleClickZone(zone) {
|
||||||
const url = zone?.url ? String(zone.url).trim() : '';
|
const url = (zone?.url || zone?.link) ? String(zone.url || zone.link).trim() : '';
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
if (/^https?:\/\//i.test(url)) {
|
if (/^https?:\/\//i.test(url)) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
|
|||||||
@@ -724,7 +724,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
0,
|
0,
|
||||||
0
|
0
|
||||||
],
|
],
|
||||||
"height": 180
|
"height": 220
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -901,6 +901,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
"backgroundColor": "transparent"
|
"backgroundColor": "transparent"
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
"height": 450,
|
||||||
"elements": [
|
"elements": [
|
||||||
{
|
{
|
||||||
"type": "badge",
|
"type": "badge",
|
||||||
@@ -979,7 +980,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 560
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1061,7 +1062,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 560
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1140,7 +1141,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 620
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1249,7 +1250,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 420
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1404,7 +1405,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 360
|
"height": 450
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1417,6 +1418,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"videoUrl": "https://vjs.zencdn.net/v/oceans.mp4",
|
"videoUrl": "https://vjs.zencdn.net/v/oceans.mp4",
|
||||||
|
"height": 450,
|
||||||
"coverUrl": "https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png",
|
"coverUrl": "https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png",
|
||||||
"url": "/goodsList?brand=1",
|
"url": "/goodsList?brand=1",
|
||||||
"title": "双11全球狂欢发布盛典 · 年度旗舰好物真实开箱测评",
|
"title": "双11全球狂欢发布盛典 · 年度旗舰好物真实开箱测评",
|
||||||
@@ -1481,7 +1483,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 220
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1656,7 +1658,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 980
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
@@ -1810,7 +1812,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
|||||||
16
|
16
|
||||||
],
|
],
|
||||||
"borderRadius": "8px",
|
"borderRadius": "8px",
|
||||||
"height": 1270
|
"height": ""
|
||||||
},
|
},
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
|
|||||||
33
buyer/src/composables/useSiteSettings.js
Normal file
33
buyer/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { getBaseSite } from "@/api/common.js";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||||
|
const settings = ref(null);
|
||||||
|
let pendingRequest;
|
||||||
|
|
||||||
|
export function loadSiteSettings() {
|
||||||
|
if (!pendingRequest) {
|
||||||
|
pendingRequest = getBaseSite()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.success && res.result?.settingValue) {
|
||||||
|
settings.value = JSON.parse(res.result.settingValue);
|
||||||
|
}
|
||||||
|
return settings.value;
|
||||||
|
})
|
||||||
|
.catch(() => settings.value)
|
||||||
|
.finally(() => { pendingRequest = null; });
|
||||||
|
}
|
||||||
|
return pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteSettings() {
|
||||||
|
onMounted(loadSiteSettings);
|
||||||
|
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||||
|
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||||
|
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||||
|
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||||
|
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||||
|
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||||
|
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ function getIndexData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function parsePageData(pageData) {
|
async function parsePageData(pageData) {
|
||||||
const dataJson = JSON.parse(pageData);
|
const dataJson = typeof pageData === 'string' ? JSON.parse(pageData) : pageData;
|
||||||
|
if (dataJson.version === 2 || dataJson.version === '2.0') {
|
||||||
|
modelForm.value = dataJson;
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (let i = 0; i < dataJson.list.length; i++) {
|
for (let i = 0; i < dataJson.list.length; i++) {
|
||||||
const type = dataJson.list[i].type;
|
const type = dataJson.list[i].type;
|
||||||
if (type === "carousel2") {
|
if (type === "carousel2") {
|
||||||
|
|||||||
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;
|
</style>
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.gcc-notice-footer {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</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",
|
||||||
|
|||||||
216
im/LICENSE
216
im/LICENSE
@@ -1,21 +1,205 @@
|
|||||||
MIT License
|
Apache License, Version 2.0
|
||||||
|
|
||||||
Copyright (c) 2020 YuanDong
|
Copyright (c) 2020 YuanDong
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Apache License
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
Version 2.0, January 2004
|
||||||
in the Software without restriction, including without limitation the rights
|
http://www.apache.org/licenses/
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1. Definitions.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
SOFTWARE.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: none;
|
resize: none !important;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
@@ -59,4 +59,4 @@ textarea {
|
|||||||
|
|
||||||
.clear {
|
.clear {
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// 统一请求路径前缀在libs/axios.js中修改
|
// 统一请求路径前缀在libs/axios.js中修改
|
||||||
import { getRequest, postRequest, putRequest, deleteRequest} from '@/libs/axios';
|
import { getRequest, postRequest, putRequest, deleteRequest} from '@/libs/axios';
|
||||||
|
|
||||||
|
export const cancelGoodsMarketSchedule = (goodsId, status) => {
|
||||||
|
return deleteRequest(`/goods/goods/schedule/market/${goodsId}`, { status });
|
||||||
|
};
|
||||||
|
|
||||||
// 获取商品品牌分页列表
|
// 获取商品品牌分页列表
|
||||||
export const getManagerBrandPage = (params) => {
|
export const getManagerBrandPage = (params) => {
|
||||||
return getRequest('/goods/brand/getByPage', params)
|
return getRequest('/goods/brand/getByPage', params)
|
||||||
|
|||||||
@@ -170,6 +170,10 @@ export const deleteMemberGroup = (id) => {
|
|||||||
return deleteRequest(`/member/memberGroup/delete/${id}`);
|
return deleteRequest(`/member/memberGroup/delete/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMemberGroupUsers = (groupId, params) => {
|
||||||
|
return getRequest(`/member/memberGroup/${groupId}/users`, params);
|
||||||
|
};
|
||||||
|
|
||||||
export const addMemberGroupUsers = (groupId, memberIds) => {
|
export const addMemberGroupUsers = (groupId, memberIds) => {
|
||||||
return postRequest(`/member/memberGroup/${groupId}/users`, {
|
return postRequest(`/member/memberGroup/${groupId}/users`, {
|
||||||
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
||||||
@@ -235,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);
|
||||||
@@ -283,11 +283,6 @@ export const getCouponReceiveList = (params) => {
|
|||||||
return getRequest("/promotion/coupon/received", params);
|
return getRequest("/promotion/coupon/received", params);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFlashDiscountList = (params) => getRequest("/promotion/flashDiscount", params);
|
|
||||||
export const getFlashDiscountDetail = (id) => getRequest(`/promotion/flashDiscount/${id}`);
|
|
||||||
export const updateFlashDiscountStatus = (id, params) => putRequest(`/promotion/flashDiscount/status/${id}`, params);
|
|
||||||
export const deleteFlashDiscount = (id) => deleteRequest(`/promotion/flashDiscount/${id}`);
|
|
||||||
export const getNthItemDiscountList = (params) => getRequest("/promotion/nthItemDiscount", params);
|
|
||||||
|
|
||||||
// 现金礼品卡活动分页
|
// 现金礼品卡活动分页
|
||||||
export const getGiftCardCashActivityPage = (params) => {
|
export const getGiftCardCashActivityPage = (params) => {
|
||||||
@@ -324,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");
|
||||||
@@ -355,4 +355,4 @@ export const getGiftCardCashUsageRecordPage = (params) => {
|
|||||||
// 卡密凭证分页(平台库存+会员资产)
|
// 卡密凭证分页(平台库存+会员资产)
|
||||||
export const getGiftCardCashCredentialPage = (params) => {
|
export const getGiftCardCashCredentialPage = (params) => {
|
||||||
return getRequest("/promotion/giftCardCash/credential", params);
|
return getRequest("/promotion/giftCardCash/credential", params);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -22,37 +18,11 @@ export const editNoticeMessage = (id, params) => {
|
|||||||
export const wechatMessageSync = (params) => {
|
export const wechatMessageSync = (params) => {
|
||||||
return getRequest("/wechat/wechatMessage/init", params)
|
return getRequest("/wechat/wechatMessage/init", params)
|
||||||
}
|
}
|
||||||
//分页获取微信消息
|
|
||||||
export const getWechatMessagePage = (params) => {
|
|
||||||
return getRequest("/wechat/wechatMessage", params)
|
|
||||||
}
|
|
||||||
//编辑微信消息模版
|
|
||||||
export const editWechatMessageTemplate = (id, params) => {
|
|
||||||
return putRequest(`/wechat/wechatMessage/${id}`, params)
|
|
||||||
}
|
|
||||||
//删除微信消息模版
|
|
||||||
export const delWechatMessageTemplate = (id, params) => {
|
|
||||||
return deleteRequest(`/wechat/wechatMessage/${id}`, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
//微信消息码同步
|
||||||
//微信消息订阅同步
|
|
||||||
export const wechatMPMessageSync = (params) => {
|
export const wechatMPMessageSync = (params) => {
|
||||||
return getRequest("/wechat/wechatMPMessage/init", params)
|
return getRequest("/wechat/wechatMPMessage/init", params)
|
||||||
}
|
}
|
||||||
//分页获取微信消息订阅
|
|
||||||
export const getWechatMPMessagePage = (params) => {
|
|
||||||
return getRequest("/wechat/wechatMPMessage", params)
|
|
||||||
}
|
|
||||||
//编辑微信消息订阅模版
|
|
||||||
export const editWechatMPMessageTemplate = (id, params) => {
|
|
||||||
return putRequest(`/wechat/wechatMPMessage/${id}`, params)
|
|
||||||
}
|
|
||||||
//删除微信消息订阅模版
|
|
||||||
export const delWechatMPMessageTemplate = (id, params) => {
|
|
||||||
return deleteRequest(`/wechat/wechatMPMessage/${id}`, params)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 查询站内信模板信息
|
// 查询站内信模板信息
|
||||||
@@ -71,122 +41,93 @@ 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) => {
|
export const getSetting = (key) => {
|
||||||
return getRequest(`sms/template/querySmsTemplatePage`, params);
|
return getRequest(`/setting/setting/get/${key}`);
|
||||||
};
|
};
|
||||||
//添加短信模板
|
//根据类型查看配置信息
|
||||||
export const addSmsTemplatePage = (params) => {
|
export const getSettingList = (params) => {
|
||||||
return postRequest(`sms/template`, params);
|
return getRequest(`/setting/setting/all`, params);
|
||||||
};
|
};
|
||||||
//删除短信模板
|
//设置配置信息
|
||||||
export const deleteSmsTemplatePage = (params) => {
|
export const setSetting = (key, params) => {
|
||||||
return deleteRequest(`sms/template`, params);
|
return postRequest(`/setting/setting/put/${key}`, params);
|
||||||
};
|
|
||||||
//删除短信模板
|
|
||||||
export const editSmsTemplatePage = (params) => {
|
|
||||||
return putRequest(`sms/template/modifySmsTemplate`, params);
|
|
||||||
};
|
|
||||||
//分页查询短信签名
|
|
||||||
export const getSmsSignPage = (params) => {
|
|
||||||
return getRequest(`sms/sign/querySmsSignPage`, params);
|
|
||||||
};
|
|
||||||
//更新短信模板
|
|
||||||
export const syncTemplate = () => {
|
|
||||||
return putRequest(`sms/template/querySmsSign`);
|
|
||||||
};
|
|
||||||
//更新短信模板
|
|
||||||
export const addSmsSign = (params) => {
|
|
||||||
return postRequest(`sms/sign`, params);
|
|
||||||
};
|
|
||||||
//同步短信签名
|
|
||||||
export const syncSign = () => {
|
|
||||||
return putRequest(`sms/sign/querySmsSign`);
|
|
||||||
};
|
|
||||||
//删除短信签名
|
|
||||||
export const deleteSign = (id) => {
|
|
||||||
return deleteRequest(`sms/sign/${id}`);
|
|
||||||
};
|
|
||||||
//更新短信签名
|
|
||||||
export const editSmsSign = (params) => {
|
|
||||||
return putRequest(`sms/sign/modifySmsSign`, params);
|
|
||||||
};
|
|
||||||
//查询签名详情
|
|
||||||
export const smsSignDetail = (id) => {
|
|
||||||
return getRequest(`sms/sign/${id}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//分页查询app版本信息
|
//获取设置
|
||||||
export const appVersionPage = (params) => {
|
export const getExperienceData = (key) => {
|
||||||
return getRequest(`/other/appVersion`, params);
|
return getRequest(`/setting/experience/get/${key}`);
|
||||||
};
|
};
|
||||||
//添加app版本信息
|
//保存
|
||||||
export const addVersion = (params) => {
|
export const setExperienceData = (key, params) => {
|
||||||
return postRequest(`/other/appVersion`, params);
|
return postRequest(`/setting/experience/put/${key}`, params);
|
||||||
};
|
|
||||||
//修改app版本信息
|
|
||||||
export const editVersion = (params, id) => {
|
|
||||||
return putRequest(`/other/appVersion/${id}`, params);
|
|
||||||
};
|
|
||||||
//删除app版本信息
|
|
||||||
export const deleteVersion = (id) => {
|
|
||||||
return deleteRequest(`/other/appVersion/${id}`);
|
|
||||||
};
|
|
||||||
//查询同城配送列表
|
|
||||||
export const getInstantDelivery = (params) => {
|
|
||||||
return getRequest(`instant-delivery/getByPage`, params);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
//添加同城配送信息
|
//根据类型获取积分设置
|
||||||
export const editInstantDelivery = (bean, params, headers) => {
|
export const getPointData = (key) => {
|
||||||
return putRequest(`instant-delivery/${bean}/config`, params, headers);
|
return getRequest(`/setting/point/get/${key}`);
|
||||||
};
|
};
|
||||||
//启用配送方案
|
//保存
|
||||||
export const openInstantDelivery = (bean) => {
|
export const setPointData = (key, params) => {
|
||||||
return putRequest(`instant-delivery/${bean}/open`);
|
return postRequest(`/setting/point/put/${key}`, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
//修改封面图片
|
//根据类型获取积分设置
|
||||||
export const editInstantDeliveryImage = (bean, params) => {
|
export const getKuaidiSetting = (key) => {
|
||||||
return putRequest(`instant-delivery/${bean}/image`, params);
|
return getRequest(`/setting/kuaidi/get/${key}`);
|
||||||
|
};
|
||||||
|
//保存
|
||||||
|
export const setKuaidiSetting = (key, params) => {
|
||||||
|
return postRequest(`/setting/kuaidi/put/${key}`, params);
|
||||||
};
|
};
|
||||||
|
|
||||||
//查询短信发送记录
|
//获取所有的物流公司
|
||||||
export const getSmsPage = (params) => {
|
export const getLogisticsList = () => {
|
||||||
return getRequest(`sms/sms`, params);
|
return getRequest(`/setting/logistics/all`);
|
||||||
|
};
|
||||||
|
//分页获取物流公司
|
||||||
|
export const getLogisticsPage = (params) => {
|
||||||
|
return getRequest(`/setting/logistics/getByPage`, params);
|
||||||
|
};
|
||||||
|
//添加物流公司
|
||||||
|
export const addLogistics = (params) => {
|
||||||
|
return postRequest(`/setting/logistics`, params);
|
||||||
|
};
|
||||||
|
//获取物流公司详情
|
||||||
|
export const getLogisticsDetail = (id) => {
|
||||||
|
return getRequest(`/setting/logistics/get/${id}`);
|
||||||
|
};
|
||||||
|
//编辑物流公司
|
||||||
|
export const editLogistics = (id, params) => {
|
||||||
|
return putRequest(`/setting/logistics/${id}`, params);
|
||||||
|
};
|
||||||
|
//删除物流公司
|
||||||
|
export const delLogistics = (id) => {
|
||||||
|
return deleteRequest(`/setting/logistics/delete/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
//发送短信
|
//禁用启用物流公司
|
||||||
export const sendSms = (params) => {
|
export const openLogistics = (id) => {
|
||||||
return postRequest(`sms/sms`, params);
|
return putRequest(`/setting/logistics/open/${id}`);
|
||||||
};
|
};
|
||||||
/*
|
|
||||||
* 获取 支付支持设置开关
|
|
||||||
*/
|
|
||||||
export const getPaymentSupportForm = () => {
|
|
||||||
return getRequest(`setting/settingx/paymentSupport`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证码源文件删除
|
//获取所有的物流公司
|
||||||
export const delVerification = (id) => {
|
export const getLogisticsAll = () => {
|
||||||
return deleteRequest(`/other/verificationSource/${id}`)
|
return getRequest(`/setting/logistics/all`);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 添加验证码源文件
|
// 验证码源文件添加
|
||||||
export const addVerification = (params) => {
|
export const createVerification = (params) => {
|
||||||
return postRequest(`/other/verificationSource`, params)
|
return postRequest(`/other/verificationSource`, params)
|
||||||
}
|
}
|
||||||
|
// 验证码源文件修改
|
||||||
// 修改验证码源文件
|
export const updateVerification = (id, params) => {
|
||||||
export const editVerification = (id, params) => {
|
|
||||||
return putRequest(`/other/verificationSource/${id}`, params)
|
return putRequest(`/other/verificationSource/${id}`, params)
|
||||||
}
|
}
|
||||||
|
// 验证码源文件删除
|
||||||
// 验证码源文件详情
|
export const deleteVerification = (id) => {
|
||||||
export const verificationDetail = (id) => {
|
return deleteRequest(`/other/verificationSource/${id}`)
|
||||||
return getRequest(`/other/verificationSource/${id}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证码源文件列表
|
// 验证码源文件列表
|
||||||
export const verificationPage = (params) => {
|
export const verificationPage = (params) => {
|
||||||
return getRequest(`/other/verificationSource`, params)
|
return getRequest(`/other/verificationSource`, params)
|
||||||
@@ -197,12 +138,64 @@ export const getNoticeMessageDetail = (id, params) => {
|
|||||||
return getRequest(`/setting/noticeMessage/${id}`, params)
|
return getRequest(`/setting/noticeMessage/${id}`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 消息模板聚合分页(站内信 + 微信服务号 + 小程序)
|
// 统一消息模板聚合分页(站内信 + 短信 + 微信服务号 + 小程序订阅)
|
||||||
export const getMessageTemplateAggregatePage = params => {
|
export const getMessageTemplateAggregatePage = params => {
|
||||||
return getRequest("/setting/noticeMessage/aggregate", params);
|
return getRequest("/setting/messageTemplate/page", params);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 修改通知类站内信邮箱渠道开关
|
// 快速切换渠道开关 (channel: NOTICE / SMS / WECHAT_OA / WECHAT_MP, status: OPEN / CLOSE)
|
||||||
export const updateNoticeMessageEmailStatus = (id, status) => {
|
export const updateMessageChannelSwitch = params => {
|
||||||
return putRequest(`/setting/noticeMessage/${id}/email/${status}`);
|
return putRequest("/setting/messageTemplate/switch", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存站内信配置 (title, content, status)
|
||||||
|
export const updateNoticeMessageTemplate = params => {
|
||||||
|
return putRequest("/setting/messageTemplate/notice", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存短信配置 (smsTemplateCode, status; 内容只读不可编辑)
|
||||||
|
export const updateSmsMessageTemplateConfig = params => {
|
||||||
|
return putRequest("/setting/messageTemplate/sms", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存微信公众号服务号配置 (code, enable; 内容只读)
|
||||||
|
export const updateWechatOaMessageTemplate = params => {
|
||||||
|
return putRequest("/setting/messageTemplate/wechat-oa", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存微信小程序订阅消息配置 (code, enable; 内容只读)
|
||||||
|
export const updateWechatMpMessageTemplate = params => {
|
||||||
|
return putRequest("/setting/messageTemplate/wechat-mp", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
// 补充缺失的系统配置、版本及验证码 API
|
||||||
|
// -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 支付支持方式表单 (PAYMENT_SUPPORT.vue)
|
||||||
|
export const getPaymentSupportForm = () => {
|
||||||
|
return getRequest(`/setting/settingx/paymentSupport`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 验证码源管理别名 (slider.vue)
|
||||||
|
export const addVerification = createVerification;
|
||||||
|
export const editVerification = updateVerification;
|
||||||
|
export const delVerification = deleteVerification;
|
||||||
|
|
||||||
|
// APP版本管理 (appVersion.vue)
|
||||||
|
export const appVersionPage = (params) => {
|
||||||
|
return getRequest(`/other/appVersion`, params);
|
||||||
|
};
|
||||||
|
export const addVersion = (params) => {
|
||||||
|
return postRequest(`/other/appVersion`, params);
|
||||||
|
};
|
||||||
|
export const editVersion = (params, id) => {
|
||||||
|
return putRequest(`/other/appVersion/${id}`, params);
|
||||||
|
};
|
||||||
|
export const deleteVersion = (id) => {
|
||||||
|
return deleteRequest(`/other/appVersion/${id}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="flag"
|
v-model="flag"
|
||||||
width="800px"
|
:width="width"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
title="绘制热区"
|
title="绘制热区"
|
||||||
top="120px"
|
top="120px"
|
||||||
@@ -26,6 +26,13 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import hotzone from "./components/Hotzone.vue";
|
import hotzone from "./components/Hotzone.vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
width: {
|
||||||
|
type: String,
|
||||||
|
default: "800px",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const emit = defineEmits(["changeZone", "closeFlag"]);
|
const emit = defineEmits(["changeZone", "closeFlag"]);
|
||||||
|
|
||||||
const flag = ref(false);
|
const flag = ref(false);
|
||||||
|
|||||||
@@ -1,106 +1,151 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="goods-dialog-wrapper">
|
||||||
<div class="wap-content">
|
<!-- 顶部搜索筛选栏 -->
|
||||||
<div class="query-wrapper">
|
<div class="goods-query-bar">
|
||||||
<div class="query-item">
|
<div class="query-item">
|
||||||
<div>搜索范围</div>
|
<span class="query-label">商品名称:</span>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="goodsParams.goodsName"
|
v-model="goodsParams.goodsName"
|
||||||
placeholder="商品名称"
|
placeholder="请输入商品名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 150px"
|
style="width: 220px"
|
||||||
@clear="onSearchGoods"
|
@clear="onSearchGoods"
|
||||||
@keyup.enter="onSearchGoods"
|
@keyup.enter="onSearchGoods"
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="query-item">
|
|
||||||
<el-cascader
|
|
||||||
v-model="category"
|
|
||||||
:options="skuList"
|
|
||||||
placeholder="请选择商品分类"
|
|
||||||
popper-class="goods-dialog-cascader-popper"
|
|
||||||
style="width: 250px"
|
|
||||||
clearable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="query-item">
|
|
||||||
<el-button type="primary" @click="onSearchGoods">搜索</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-table
|
|
||||||
ref="tableRef"
|
|
||||||
v-loading="loading"
|
|
||||||
class="goods-select-table"
|
|
||||||
:data="goodsData"
|
|
||||||
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="90" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<img class="goods-thumb" :src="row.thumbnail" alt="" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column
|
|
||||||
prop="goodsName"
|
|
||||||
label="商品名称"
|
|
||||||
min-width="200"
|
|
||||||
show-overflow-tooltip
|
|
||||||
/>
|
|
||||||
<el-table-column label="商品规格" min-width="140" show-overflow-tooltip>
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.simpleSpecs || "默认规格" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="价格" width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="goods-price">¥{{ $filters.unitPrice(row.price) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="库存" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.quantity != null ? row.quantity : "—" }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="销量" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.buyCount != null ? row.buyCount : 0 }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="商品类型" width="110">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ goodsTypeLabel(row.goodsType) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="goodsParams.pageNumber"
|
|
||||||
class="pageration"
|
|
||||||
:total="total"
|
|
||||||
:page-size="goodsParams.pageSize"
|
|
||||||
layout="total, prev, pager, next"
|
|
||||||
size="small"
|
|
||||||
@current-change="changePageSize"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="query-item">
|
||||||
|
<span class="query-label">商品编号:</span>
|
||||||
|
<el-input
|
||||||
|
v-model="goodsParams.sn"
|
||||||
|
placeholder="请输入商品编号/货号"
|
||||||
|
clearable
|
||||||
|
style="width: 190px"
|
||||||
|
@clear="onSearchGoods"
|
||||||
|
@keyup.enter="onSearchGoods"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="query-item">
|
||||||
|
<span class="query-label">商品分类:</span>
|
||||||
|
<el-cascader
|
||||||
|
v-model="category"
|
||||||
|
:options="skuList"
|
||||||
|
placeholder="请选择商品分类"
|
||||||
|
popper-class="goods-dialog-cascader-popper"
|
||||||
|
style="width: 240px"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="query-item query-actions">
|
||||||
|
<el-button type="primary" @click="onSearchGoods">搜索</el-button>
|
||||||
|
<el-button @click="onResetGoods">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格展示区域 (自适应填充剩余高度) -->
|
||||||
|
<div class="goods-table-wrap">
|
||||||
|
<el-table
|
||||||
|
ref="tableRef"
|
||||||
|
v-loading="loading"
|
||||||
|
class="goods-select-table"
|
||||||
|
:data="goodsData"
|
||||||
|
row-key="id"
|
||||||
|
height="100%"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
empty-text="暂无匹配商品信息"
|
||||||
|
@selection-change="onSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column
|
||||||
|
type="selection"
|
||||||
|
width="50"
|
||||||
|
align="center"
|
||||||
|
reserve-selection
|
||||||
|
/>
|
||||||
|
<el-table-column label="图片" width="76" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-image
|
||||||
|
class="goods-thumb"
|
||||||
|
:src="row.thumbnail"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
:preview-src-list="row.thumbnail ? [row.thumbnail] : []"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="thumb-slot">无图</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="goodsName"
|
||||||
|
label="商品名称"
|
||||||
|
min-width="260"
|
||||||
|
show-overflow-tooltip
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="goods-title-line">{{ row.goodsName || "-" }}</div>
|
||||||
|
<div v-if="row.sn" class="goods-sn-line font-mono">货号: {{ row.sn }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品规格" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.simpleSpecs || "默认规格" }}</span>
|
||||||
|
</template>
|
||||||
|
</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">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="goods-price font-semibold">
|
||||||
|
¥ {{ Number(row.price || 0).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="可用库存" width="100" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.quantity != null ? row.quantity : "—" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="累计销量" width="100" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.buyCount != null ? row.buyCount : 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品类型" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ goodsTypeLabel(row.goodsType) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部翻页栏 -->
|
||||||
|
<div class="goods-pagination-bar">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="goodsParams.pageNumber"
|
||||||
|
v-model:page-size="goodsParams.pageSize"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[15, 30, 50, 100]"
|
||||||
|
popper-class="goods-dialog-pagination-popper"
|
||||||
|
:teleported="true"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
size="small"
|
||||||
|
@current-change="changePage"
|
||||||
|
@size-change="onPageSizeChange"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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: () => [],
|
||||||
@@ -127,10 +172,13 @@ 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] : "";
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.selectedWay,
|
() => props.selectedWay,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -139,6 +187,7 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => goodsParams.categoryPath,
|
() => goodsParams.categoryPath,
|
||||||
() => {
|
() => {
|
||||||
@@ -152,26 +201,47 @@ watch(
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
watch(() => props.queryParams, onSearchGoods, { deep: true });
|
||||||
|
onUnmounted(() => { querySequence++; });
|
||||||
|
|
||||||
function onSearchGoods() {
|
function onSearchGoods() {
|
||||||
goodsData.value = [];
|
goodsData.value = [];
|
||||||
goodsParams.pageNumber = 1;
|
goodsParams.pageNumber = 1;
|
||||||
getQueryGoodsList();
|
getQueryGoodsList();
|
||||||
}
|
}
|
||||||
function changePageSize(v) {
|
|
||||||
|
function onResetGoods() {
|
||||||
|
goodsParams.goodsName = "";
|
||||||
|
goodsParams.sn = "";
|
||||||
|
category.value = [];
|
||||||
|
goodsParams.categoryPath = "";
|
||||||
|
goodsParams.pageNumber = 1;
|
||||||
|
getQueryGoodsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePage(v) {
|
||||||
goodsParams.pageNumber = v;
|
goodsParams.pageNumber = v;
|
||||||
getQueryGoodsList();
|
getQueryGoodsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPageSizeChange(val) {
|
||||||
|
goodsParams.pageSize = val;
|
||||||
|
goodsParams.pageNumber = 1;
|
||||||
|
getQueryGoodsList();
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initGoods(res) {
|
function initGoods(res) {
|
||||||
const records = res?.result?.records || [];
|
const records = res?.result?.records || [];
|
||||||
records.forEach((item) => {
|
records.forEach((item) => {
|
||||||
@@ -181,24 +251,31 @@ function initGoods(res) {
|
|||||||
goodsData.value = records;
|
goodsData.value = records;
|
||||||
nextTick(syncTableSelection);
|
nextTick(syncTableSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
selectedList.value = list;
|
selectedList.value = list;
|
||||||
emit("selected", selectedList.value);
|
emit("selected", selectedList.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
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([]);
|
||||||
@@ -214,17 +291,18 @@ function onSelectionChange(rows) {
|
|||||||
}
|
}
|
||||||
emitSelected(rows);
|
emitSelected(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
API_Goods.getGoodsSkuData(goodsParams).then((res) => {
|
getQueryGoodsList();
|
||||||
initGoods(res);
|
|
||||||
});
|
|
||||||
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
|
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
|
||||||
if (res.success) {
|
if (res.success && res.result) {
|
||||||
deepGroup(res.result);
|
deepGroup(res.result);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepGroup(val) {
|
function deepGroup(val) {
|
||||||
|
skuList.value = [];
|
||||||
val.forEach((item) => {
|
val.forEach((item) => {
|
||||||
let childWay = [];
|
let childWay = [];
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
@@ -252,34 +330,102 @@ function deepGroup(val) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 父组件 lili-dialog/index.vue 通过 $refs.goodsDialog.type = "single" 切换单选;
|
|
||||||
// expose 的 ref 外部读写等价于原 data 字段
|
|
||||||
defineExpose({ type });
|
defineExpose({ type });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "./style.scss";
|
.goods-dialog-wrapper {
|
||||||
.wap-content {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
height: 100%;
|
||||||
padding: 0;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部搜索栏 */
|
||||||
|
.goods-query-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 0 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.query-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.query-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.query-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格容器 */
|
||||||
|
.goods-table-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.goods-thumb {
|
.goods-thumb {
|
||||||
display: block;
|
display: block;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
object-fit: cover;
|
border: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.thumb-slot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-title-line {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-sn-line {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.goods-price {
|
.goods-price {
|
||||||
color: $theme_color;
|
color: #f56c6c;
|
||||||
}
|
}
|
||||||
.pageration {
|
|
||||||
|
/* 底部翻页栏 */
|
||||||
|
.goods-pagination-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
width: 100%;
|
align-items: center;
|
||||||
margin-top: 16px;
|
padding-top: 12px;
|
||||||
padding-right: 8px;
|
flex-shrink: 0;
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,18 +2,22 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
v-if="goodsFlag"
|
v-if="goodsFlag"
|
||||||
v-model="flag"
|
v-model="flag"
|
||||||
width="1160px"
|
title="选择商品"
|
||||||
top="120px"
|
width="85%"
|
||||||
|
top="5vh"
|
||||||
:z-index="10000"
|
:z-index="10000"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
append-to-body
|
append-to-body
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
class="goods-selector-dialog"
|
||||||
@close="clickClose"
|
@close="clickClose"
|
||||||
>
|
>
|
||||||
<goodsDialog
|
<goodsDialog
|
||||||
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;
|
||||||
@@ -21,8 +25,15 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="clickClose">取消</el-button>
|
<div class="goods-dialog-footer">
|
||||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
<span class="selected-count-tip">
|
||||||
|
已选择 <strong class="text-primary">{{ goodsData.length }}</strong> 件商品
|
||||||
|
</span>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<el-button @click="clickClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
@@ -53,16 +64,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import goodsDialog from "./goods-dialog";
|
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([]); // 选择的商品
|
||||||
const linkData = ref(""); //选择的链接
|
const linkData = ref(""); // 选择的链接
|
||||||
const flag = ref(false); // modal显隐
|
const flag = ref(false); // modal显隐
|
||||||
|
|
||||||
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
|
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
|
||||||
@@ -112,14 +125,7 @@ function close() {
|
|||||||
flag.value = false;
|
flag.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 外部通过 ref 访问的成员(grep manager/src 全量确认):
|
// 外部通过 ref 访问的成员:
|
||||||
// - open:page-decoration 各 modelList、modelForm/modelFormItem、hotzone/Zone、wap/advertising、alertAdvertising 等打开弹窗(goods/link)
|
|
||||||
// - singleGoods:modelList/forYour.vue、mixModel.vue 直接调用
|
|
||||||
// - clearGoodsSelected:wap/decorate.vue、modelList/goodsAndType.vue 调用
|
|
||||||
// - goodsData:modelList/newGoodsSort.vue、notEnough.vue 回写已选商品
|
|
||||||
// - goodsFlag:wap/decorate.vue 直接写入
|
|
||||||
// - flag:wap/decorate.vue 直接写入 true 打开弹窗
|
|
||||||
// - close:未被 grep 到外部调用,原 Options API 实例天然暴露,保守保留
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
@@ -130,14 +136,73 @@ defineExpose({
|
|||||||
flag,
|
flag,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
:deep(.el-dialog__body) {
|
:deep(.el-dialog__body) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 500px;
|
height: 500px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.selected-count-tip {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
margin: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
/* 商品选择器弹窗全局尺寸与布局增强 */
|
||||||
|
.goods-selector-dialog {
|
||||||
|
max-width: 1360px;
|
||||||
|
min-width: 900px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.el-dialog__header {
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-right: 0;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.el-dialog__title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__body {
|
||||||
|
width: 100% !important;
|
||||||
|
height: calc(84vh - 120px) !important;
|
||||||
|
min-height: 520px !important;
|
||||||
|
max-height: 760px !important;
|
||||||
|
padding: 16px 20px !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__footer {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 抽屉 append-to-body 后不在当前组件 DOM 内,不能使用 scoped 样式
|
// 抽屉 append-to-body 后不在当前组件 DOM 内,不能使用 scoped 样式
|
||||||
.link-selector-drawer {
|
.link-selector-drawer {
|
||||||
max-width: 96vw;
|
max-width: 96vw;
|
||||||
@@ -152,7 +217,8 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 商品分类下拉挂载到 body,需要高于抽屉的 10000 层级
|
// 商品分类下拉挂载到 body,需要高于抽屉的 10000 层级
|
||||||
.goods-dialog-cascader-popper {
|
.goods-dialog-cascader-popper,
|
||||||
|
.goods-dialog-pagination-popper {
|
||||||
z-index: 10001 !important;
|
z-index: 10001 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
30
manager/src/components/list-action-bar.vue
Normal file
30
manager/src/components/list-action-bar.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list-action-bar" role="group" aria-label="列表操作">
|
||||||
|
<div class="list-action-bar__primary"><slot /></div>
|
||||||
|
<div v-if="$slots.right" class="list-action-bar__secondary"><slot name="right" /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-action-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
&__primary,
|
||||||
|
&__secondary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -119,7 +119,7 @@ function finished() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 父组件(member/index、memberRecycle、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
|
// 父组件(member/index、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
|
||||||
defineExpose({ open });
|
defineExpose({ open });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
33
manager/src/composables/useSiteSettings.js
Normal file
33
manager/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { getBaseSite } from "@/api/common.js";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||||
|
const settings = ref(null);
|
||||||
|
let pendingRequest;
|
||||||
|
|
||||||
|
export function loadSiteSettings() {
|
||||||
|
if (!pendingRequest) {
|
||||||
|
pendingRequest = getBaseSite()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.success && res.result?.settingValue) {
|
||||||
|
settings.value = JSON.parse(res.result.settingValue);
|
||||||
|
}
|
||||||
|
return settings.value;
|
||||||
|
})
|
||||||
|
.catch(() => settings.value)
|
||||||
|
.finally(() => { pendingRequest = null; });
|
||||||
|
}
|
||||||
|
return pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteSettings() {
|
||||||
|
onMounted(loadSiteSettings);
|
||||||
|
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||||
|
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||||
|
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||||
|
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||||
|
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||||
|
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||||
|
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ const viewContext = require.context("@/views", true, /\.vue$/);
|
|||||||
const VIEW_ALIASES = {
|
const VIEW_ALIASES = {
|
||||||
"customWords/index": "custom-words/index",
|
"customWords/index": "custom-words/index",
|
||||||
"sensitiveWords/index": "sensitive-words/index",
|
"sensitiveWords/index": "sensitive-words/index",
|
||||||
|
"sys/message/noticeMessageTemplate": "sys/message/messageTemplate",
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveViewPath(url) {
|
function resolveViewPath(url) {
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ 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",
|
||||||
|
"order/order/fictitiousOrderList",
|
||||||
|
"promotions/flash-discount",
|
||||||
|
"promotions/nth-item-discount",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
function isDeprecatedMenu(menu) {
|
function isDeprecatedMenu(menu) {
|
||||||
|
|||||||
@@ -38,6 +38,20 @@ export const otherRouter = {
|
|||||||
redirect: "/home",
|
redirect: "/home",
|
||||||
component: Main,
|
component: Main,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "messageTemplate",
|
||||||
|
title: "消息模板",
|
||||||
|
name: "messageTemplate",
|
||||||
|
meta: { title: "消息模板" },
|
||||||
|
component: () => import("@/views/sys/message/messageTemplate.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "noticeMessageTemplate",
|
||||||
|
title: "消息模板",
|
||||||
|
name: "noticeMessageTemplate",
|
||||||
|
meta: { title: "消息模板" },
|
||||||
|
component: () => import("@/views/sys/message/messageTemplate.vue")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "home",
|
path: "home",
|
||||||
title: '首页',
|
title: '首页',
|
||||||
@@ -112,6 +126,12 @@ export const otherRouter = {
|
|||||||
name: "goods-category",
|
name: "goods-category",
|
||||||
component: () => import("@/views/goods/goods-manage/category.vue")
|
component: () => import("@/views/goods/goods-manage/category.vue")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "goods-scheduled-upper",
|
||||||
|
title: "定时上架",
|
||||||
|
name: "goods-scheduled-upper",
|
||||||
|
component: () => import("@/views/goods/scheduled-upper/index.vue")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "goods-group",
|
path: "goods-group",
|
||||||
title: "商品分组",
|
title: "商品分组",
|
||||||
@@ -149,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: "预约工单",
|
||||||
@@ -198,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: "等级权益设置",
|
||||||
@@ -346,18 +386,6 @@ export const otherRouter = {
|
|||||||
name: "full-discount-detail",
|
name: "full-discount-detail",
|
||||||
component: () => import("@/views/promotions/full-discount/full-discount-detail.vue")
|
component: () => import("@/views/promotions/full-discount/full-discount-detail.vue")
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "promotions/flash-discount",
|
|
||||||
title: "限时直降",
|
|
||||||
name: "flash-discount",
|
|
||||||
component: () => import("@/views/promotions/flash-discount/flash-discount.vue")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "promotions/nth-item-discount",
|
|
||||||
title: "第N件优惠",
|
|
||||||
name: "nth-item-discount",
|
|
||||||
component: () => import("@/views/promotions/nth-item-discount/nth-item-discount.vue")
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "promotions/seckill/manager-seckill-add",
|
path: "promotions/seckill/manager-seckill-add",
|
||||||
title: "编辑秒杀活动",
|
title: "编辑秒杀活动",
|
||||||
@@ -371,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: "创建直播",
|
||||||
@@ -471,6 +493,13 @@ export const otherRouter = {
|
|||||||
meta: { title: "商品销售汇总报表" },
|
meta: { title: "商品销售汇总报表" },
|
||||||
component: () => import("@/views/statistics/goods-sales-summary.vue")
|
component: () => import("@/views/statistics/goods-sales-summary.vue")
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "article-edit",
|
||||||
|
title: "文章编辑",
|
||||||
|
name: "article-edit",
|
||||||
|
meta: { title: "文章编辑" },
|
||||||
|
component: () => import("@/views/page/article-manage/articleEdit.vue")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "distributionImportRecord",
|
path: "distributionImportRecord",
|
||||||
title: "分销员导入记录",
|
title: "分销员导入记录",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
|
|||||||
import { otherRouter } from "@/router/router";
|
import { otherRouter } from "@/router/router";
|
||||||
import i18n from "@/locale";
|
import i18n from "@/locale";
|
||||||
|
|
||||||
|
const removedPageNames = new Set(["memberRecycle", "fictitiousOrderList"]);
|
||||||
|
|
||||||
// 原 vuex app 模块 + 根 state(openStyleStore/openStoreId/notices)合并
|
// 原 vuex app 模块 + 根 state(openStyleStore/openStoreId/notices)合并
|
||||||
// mutation 全部转为同名 action;两个例外:
|
// mutation 全部转为同名 action;两个例外:
|
||||||
// - pageOpenedList mutation 与 state 同名,改名 updateOpenedList
|
// - pageOpenedList mutation 与 state 同名,改名 updateOpenedList
|
||||||
@@ -73,7 +75,8 @@ export const useAppStore = defineStore("app", {
|
|||||||
},
|
},
|
||||||
initCachePage() {
|
initCachePage() {
|
||||||
if (localStorage.cachePage) {
|
if (localStorage.cachePage) {
|
||||||
this.cachePage = JSON.parse(localStorage.cachePage);
|
this.cachePage = JSON.parse(localStorage.cachePage).filter((name) => !removedPageNames.has(name));
|
||||||
|
localStorage.cachePage = JSON.stringify(this.cachePage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeTag(name) {
|
removeTag(name) {
|
||||||
@@ -120,8 +123,9 @@ export const useAppStore = defineStore("app", {
|
|||||||
},
|
},
|
||||||
setOpenedList() {
|
setOpenedList() {
|
||||||
this.pageOpenedList = localStorage.pageOpenedList
|
this.pageOpenedList = localStorage.pageOpenedList
|
||||||
? JSON.parse(localStorage.pageOpenedList)
|
? JSON.parse(localStorage.pageOpenedList).filter((page) => !removedPageNames.has(page.name))
|
||||||
: [otherRouter.children[0]];
|
: [otherRouter.children[0]];
|
||||||
|
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
|
||||||
},
|
},
|
||||||
setAvatarPath(path) {
|
setAvatarPath(path) {
|
||||||
localStorage.avatorImgPath = path;
|
localStorage.avatorImgPath = path;
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||||
|
.el-textarea__inner {
|
||||||
|
resize: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Element Plus 主题覆盖(对齐原 iView 主色) */
|
/* Element Plus 主题覆盖(对齐原 iView 主色) */
|
||||||
:root {
|
:root {
|
||||||
// --el-color-primary: #ff5c58;
|
// --el-color-primary: #ff5c58;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 打印指定 DOM 区域(替代 vue-print-nb)
|
* 打印指定 DOM 区域(支持标准 A4 纵向与完整打印样式隔离)
|
||||||
*/
|
*/
|
||||||
export function printElement(elementId, title = "打印") {
|
export function printElement(elementId, title = "打印发货单") {
|
||||||
const el = document.getElementById(elementId);
|
const el = document.getElementById(elementId);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
@@ -13,22 +13,196 @@ export function printElement(elementId, title = "打印") {
|
|||||||
doc.open();
|
doc.open();
|
||||||
doc.write(
|
doc.write(
|
||||||
`<!DOCTYPE html><html><head><title>${title}</title><style>
|
`<!DOCTYPE html><html><head><title>${title}</title><style>
|
||||||
body{font-family:Arial,sans-serif;padding:12px;color:#333}
|
@page {
|
||||||
table{width:100%;border-collapse:collapse}
|
size: portrait;
|
||||||
td,th{border:1px solid #ddd;padding:6px}
|
margin: 8mm 10mm;
|
||||||
.lineH30{line-height:30px}
|
}
|
||||||
.f14{font-size:14px;color:#333}
|
* {
|
||||||
.printgoodtitle{font-size:14px;line-height:1.5;margin-top:15px;color:#333}
|
-webkit-print-color-adjust: exact !important;
|
||||||
.printgoodinfo{padding:10px}
|
print-color-adjust: exact !important;
|
||||||
.printgooditem{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee}
|
box-sizing: border-box !important;
|
||||||
.printgoodname{flex:1}
|
}
|
||||||
.printgoodguid{margin-top:4px;color:#666;font-size:12px}
|
html, body {
|
||||||
.printgoodguiditem{margin-right:8px}
|
margin: 0 !important;
|
||||||
.printgoodnumber{white-space:nowrap;margin-left:12px}
|
padding: 0 !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
width: 100% !important;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.invoice-wrap {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
color: #000;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background: #fff;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inv-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.inv-title-box h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
.inv-sub-title {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #555;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.inv-meta-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.barcode-svg {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.barcode-text {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 2px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.inv-meta-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #000;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.inv-meta-table td {
|
||||||
|
border: 1px solid #000;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
.inv-meta-table .m-k {
|
||||||
|
width: 75px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.inv-meta-table .m-v {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.inv-table {
|
||||||
|
width: 100% !important;
|
||||||
|
table-layout: fixed !important;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #000;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.inv-table th {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 4px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
}
|
||||||
|
.inv-table td {
|
||||||
|
padding: 6px 6px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
color: #000;
|
||||||
|
vertical-align: middle;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
.inv-table .goods-title {
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.inv-table .goods-specs {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #444;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.check-box-square {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: 1.5px solid #000;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.inv-summary-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1px solid #000;
|
||||||
|
border-top: none;
|
||||||
|
margin-top: -1px !important;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.inv-summary-left {
|
||||||
|
color: #111;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.inv-summary-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
.inv-summary-right .sum-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.inv-summary-right .sum-total {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.inv-sign-area {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 4px 6px;
|
||||||
|
border-top: 1px dashed #666;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #222;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.sign-line {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
height: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.inv-footer-note {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid #e5e7eb;
|
||||||
|
}
|
||||||
</style></head><body>${el.innerHTML}</body></html>`
|
</style></head><body>${el.innerHTML}</body></html>`
|
||||||
);
|
);
|
||||||
doc.close();
|
doc.close();
|
||||||
iframe.contentWindow.focus();
|
iframe.contentWindow.focus();
|
||||||
iframe.contentWindow.print();
|
setTimeout(() => {
|
||||||
setTimeout(() => document.body.removeChild(iframe), 1000);
|
iframe.contentWindow.print();
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.body.contains(iframe)) {
|
||||||
|
document.body.removeChild(iframe);
|
||||||
|
}
|
||||||
|
}, 1500);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|||||||
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(" / ") || "默认规格";
|
||||||
|
}
|
||||||
@@ -45,12 +45,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="modalVisible"
|
v-model="modalVisible"
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
width="500px"
|
size="500px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<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="自定义分词" prop="name">
|
<el-form-item label="自定义分词" prop="name">
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
<el-button @click="modalVisible = false">取消</el-button>
|
<el-button @click="modalVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
|
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -127,9 +127,9 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="joinTime" label="加入时间" min-width="160" />
|
<el-table-column prop="joinTime" label="加入时间" min-width="160" />
|
||||||
<el-table-column label="操作" width="270" align="center" fixed="right">
|
<el-table-column label="操作" width="270" align="right" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<div v-if="row" class="ops-row">
|
||||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
<span class="op-split">|</span>
|
||||||
<a class="link-text" @click="openDetail(row)">详情</a>
|
<a class="link-text" @click="openDetail(row)">详情</a>
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
<a v-if="row.distributionStatus != 'RETREAT'" class="link-text" @click="retreat(row)">清退</a>
|
<a v-if="row.distributionStatus != 'RETREAT'" class="link-text" @click="retreat(row)">清退</a>
|
||||||
<a v-if="row.distributionStatus == 'RETREAT'" class="link-text" @click="resume(row)">恢复</a>
|
<a v-if="row.distributionStatus == 'RETREAT'" class="link-text" @click="resume(row)">恢复</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -260,10 +260,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="editVisible"
|
v-model="editVisible"
|
||||||
title="编辑分销员"
|
title="编辑分销员"
|
||||||
width="520px"
|
size="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -304,12 +304,12 @@
|
|||||||
<el-button @click="editVisible = false">取消</el-button>
|
<el-button @click="editVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
|
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="createVisible"
|
v-model="createVisible"
|
||||||
title="新增分销员"
|
title="新增分销员"
|
||||||
width="520px"
|
size="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -345,12 +345,12 @@
|
|||||||
<el-button @click="createVisible = false">取消</el-button>
|
<el-button @click="createVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">确定</el-button>
|
<el-button type="primary" :loading="createLoading" @click="submitCreate">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="batchGradeVisible"
|
v-model="batchGradeVisible"
|
||||||
title="批量设置等级"
|
title="批量设置等级"
|
||||||
width="480px"
|
size="480px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -380,12 +380,12 @@
|
|||||||
<el-button @click="batchGradeVisible = false">取消</el-button>
|
<el-button @click="batchGradeVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="batchGradeLoading" @click="submitBatchGrade">确定</el-button>
|
<el-button type="primary" :loading="batchGradeLoading" @click="submitBatchGrade">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="importVisible"
|
v-model="importVisible"
|
||||||
title="批量导入分销员"
|
title="批量导入分销员"
|
||||||
width="520px"
|
size="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
append-to-body
|
append-to-body
|
||||||
@@ -416,7 +416,7 @@
|
|||||||
开始导入
|
开始导入
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -923,6 +923,9 @@ onMounted(() => {
|
|||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
color: #dcdee2;
|
color: #dcdee2;
|
||||||
}
|
}
|
||||||
|
.ops-row {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.status-tabs {
|
.status-tabs {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,12 +243,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="rebindVisible"
|
v-model="rebindVisible"
|
||||||
title="换绑客户关系"
|
title="换绑客户关系"
|
||||||
width="520px"
|
size="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form ref="rebindFormRef" :model="rebindForm" :rules="rebindRules" label-width="110px">
|
<el-form ref="rebindFormRef" :model="rebindForm" :rules="rebindRules" label-width="110px">
|
||||||
<el-form-item label="客户">
|
<el-form-item label="客户">
|
||||||
@@ -273,7 +274,7 @@
|
|||||||
<el-button @click="rebindVisible = false">取消</el-button>
|
<el-button @click="rebindVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="rebindLoading" @click="submitRebind">确定</el-button>
|
<el-button type="primary" :loading="rebindLoading" @click="submitRebind">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
|
|
||||||
<distribution-selector
|
<distribution-selector
|
||||||
v-model="distributorSelectorVisible"
|
v-model="distributorSelectorVisible"
|
||||||
@@ -281,12 +282,13 @@
|
|||||||
@confirm="onDistributorSelected"
|
@confirm="onDistributorSelected"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="unbindVisible"
|
v-model="unbindVisible"
|
||||||
title="解绑客户关系"
|
title="解绑客户关系"
|
||||||
width="520px"
|
size="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
append-to-body
|
||||||
>
|
>
|
||||||
<el-form ref="unbindFormRef" :model="unbindForm" label-width="110px">
|
<el-form ref="unbindFormRef" :model="unbindForm" label-width="110px">
|
||||||
<el-form-item label="客户">
|
<el-form-item label="客户">
|
||||||
@@ -300,7 +302,7 @@
|
|||||||
<el-button @click="unbindVisible = false">取消</el-button>
|
<el-button @click="unbindVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="unbindLoading" @click="submitUnbind">确定</el-button>
|
<el-button type="primary" :loading="unbindLoading" @click="submitUnbind">确定</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
<el-card class="mt_10">
|
<el-card class="mt_10">
|
||||||
<el-table v-loading="loading" border :data="data">
|
<el-table v-loading="loading" border :data="data">
|
||||||
<el-table-column prop="period" label="日期" width="120" />
|
<el-table-column prop="period" label="日期" width="120" />
|
||||||
<el-table-column prop="paymentName" label="支付方式" width="140" />
|
<el-table-column prop="paymentName" label="支付方式" width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPaymentName(row.paymentName) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="payCount" label="支付笔数" width="100" />
|
<el-table-column prop="payCount" label="支付笔数" width="100" />
|
||||||
<el-table-column prop="payAmount" label="支付金额" width="120" />
|
<el-table-column prop="payAmount" label="支付金额" width="120" />
|
||||||
<el-table-column prop="refundCount" label="退款笔数" width="100" />
|
<el-table-column prop="refundCount" label="退款笔数" width="100" />
|
||||||
@@ -35,6 +39,17 @@ const loading = ref(false);
|
|||||||
const dateRange = ref([]);
|
const dateRange = ref([]);
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
|
|
||||||
|
const paymentNameMap = {
|
||||||
|
WECHAT: "微信支付",
|
||||||
|
ALIPAY: "支付宝",
|
||||||
|
WALLET: "余额支付",
|
||||||
|
BANK_TRANSFER: "线下转账",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatPaymentName(name) {
|
||||||
|
return paymentNameMap[name] || name || "-";
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +1,56 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="finance-wallet-log-page search">
|
||||||
<el-card>
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form inline>
|
<el-form inline class="search-form" @keyup.enter="handleSearch">
|
||||||
<el-form-item label="会员">
|
<el-form-item label="客户ID">
|
||||||
<el-input v-model="searchForm.memberName" clearable style="width: 160px" />
|
<el-input v-model="searchForm.memberId" placeholder="请输入客户ID" clearable style="width: 180px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="日期">
|
<el-form-item label="会员名称">
|
||||||
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" />
|
<el-input v-model="searchForm.memberName" placeholder="请输入会员名称" clearable style="width: 160px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" style="width: 240px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
<el-button @click="handleExport">导出</el-button>
|
<el-button @click="handleExport">导出</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="mt_10">
|
|
||||||
<el-table v-loading="loading" border :data="data">
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
<el-table-column prop="memberName" label="会员" width="140" />
|
<template #header>
|
||||||
<el-table-column prop="money" label="金额" width="110" />
|
<div class="card-header-bar">
|
||||||
<el-table-column prop="serviceType" label="业务类型" width="140" />
|
<div class="header-title">
|
||||||
<el-table-column prop="detail" label="明细" min-width="200" show-overflow-tooltip />
|
<span>钱包流水明细</span>
|
||||||
<el-table-column prop="createTime" label="时间" width="170" />
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" border stripe :data="data">
|
||||||
|
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="memberName" label="会员名称" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="money" label="金额" width="130" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" :class="row.money >= 0 ? 'money-increase' : 'money-decrease'">
|
||||||
|
{{ row.money >= 0 ? '+' : '' }}¥ {{ Number(row.money || 0).toFixed(2) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="serviceType" label="业务类型" width="130" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row ? row.serviceType || "-" : "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="detail" label="明细" min-width="240" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="createTime" label="时间" width="170" align="center" />
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -42,15 +70,16 @@ import { useTable } from "@/utils/useTable";
|
|||||||
import * as API_Finance from "@/api/finance";
|
import * as API_Finance from "@/api/finance";
|
||||||
import { getRequest } from "@/libs/axios";
|
import { getRequest } from "@/libs/axios";
|
||||||
import { downloadBlob } from "@/utils/downloadBlob";
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "finance-wallet-log" });
|
defineOptions({ name: "finance-wallet-log" });
|
||||||
|
|
||||||
const dateRange = ref([]);
|
const dateRange = ref([]);
|
||||||
const { loading, data, total, searchForm, getDataList, handleSearch } = useTable(
|
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||||
(p) => getRequest("/wallet/log", p),
|
(p) => getRequest("/wallet/log", p),
|
||||||
{
|
{
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
form: { memberName: "" },
|
form: { memberId: "", memberName: "" },
|
||||||
beforeRequest: (p) => {
|
beforeRequest: (p) => {
|
||||||
const params = { ...p };
|
const params = { ...p };
|
||||||
if (dateRange.value?.length === 2) {
|
if (dateRange.value?.length === 2) {
|
||||||
@@ -62,8 +91,18 @@ const { loading, data, total, searchForm, getDataList, handleSearch } = useTable
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
dateRange.value = [];
|
||||||
|
searchForm.memberId = "";
|
||||||
|
searchForm.memberName = "";
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
|
||||||
function handleExport() {
|
function handleExport() {
|
||||||
const params = { memberName: searchForm.memberName };
|
const params = {
|
||||||
|
memberId: searchForm.memberId || undefined,
|
||||||
|
memberName: searchForm.memberName || undefined,
|
||||||
|
};
|
||||||
if (dateRange.value?.length === 2) {
|
if (dateRange.value?.length === 2) {
|
||||||
params.startDate = dateRange.value[0];
|
params.startDate = dateRange.value[0];
|
||||||
params.endDate = dateRange.value[1];
|
params.endDate = dateRange.value[1];
|
||||||
@@ -73,3 +112,78 @@ function handleExport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.finance-wallet-log-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 16px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-increase {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-decrease {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="operation">
|
<list-action-bar>
|
||||||
<el-button type="primary" @click="add">添加</el-button>
|
<el-button type="primary" @click="add">添加</el-button>
|
||||||
<el-button @click="delAll">批量删除</el-button>
|
<el-button @click="delAll">批量删除</el-button>
|
||||||
</div>
|
</list-action-bar>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
ref="table"
|
ref="table"
|
||||||
@@ -66,6 +66,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ListActionBar from "@/components/list-action-bar.vue";
|
||||||
import { ref, reactive, onMounted } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import {
|
import {
|
||||||
addGoodsUnit,
|
addGoodsUnit,
|
||||||
|
|||||||
@@ -70,7 +70,8 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="goods-tab">
|
<slot v-if="scheduledOnly" name="scheduled-controls" />
|
||||||
|
<div v-if="!scheduledOnly" class="goods-tab">
|
||||||
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
|
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
|
||||||
<el-tab-pane
|
<el-tab-pane
|
||||||
v-for="item in goodsStatusWithCount"
|
v-for="item in goodsStatusWithCount"
|
||||||
@@ -81,7 +82,7 @@
|
|||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="batch-operations" style="margin: 10px 0">
|
<div v-if="!readOnly" class="batch-operations" style="margin: 10px 0">
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="selectedRows.length === 0"
|
:disabled="selectedRows.length === 0"
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="100" align="center" />
|
<el-table-column v-if="!readOnly" type="selection" width="100" align="center" />
|
||||||
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="商品图片" width="100" align="center">
|
<el-table-column label="商品图片" width="100" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -141,6 +142,8 @@
|
|||||||
<a class="link-text" @click="linkTo(row.id, row.skuId)">{{ row.goodsName }}</a>
|
<a class="link-text" @click="linkTo(row.id, row.skuId)">{{ row.goodsName }}</a>
|
||||||
</template>
|
</template>
|
||||||
</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_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
||||||
<el-table-column label="价格" width="200">
|
<el-table-column label="价格" width="200">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ color: $mainColor }">{{ unitPrice(row.price, '¥') }}</span>
|
<span :style="{ color: $mainColor }">{{ unitPrice(row.price, '¥') }}</span>
|
||||||
@@ -153,7 +156,7 @@
|
|||||||
<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="状态" width="150">
|
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -164,7 +167,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="storeName" label="店铺名称" width="200" show-overflow-tooltip />
|
<el-table-column prop="storeName" label="店铺名称" width="200" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
<el-table-column v-if="scheduledOnly" label="操作" width="100" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-else-if="!readOnly" label="操作" width="200" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row.authFlag === 'TOBEAUDITED'">
|
<template v-if="row.authFlag === 'TOBEAUDITED'">
|
||||||
<a class="link-text" @click="openAuditModal(row)">审核</a>
|
<a class="link-text" @click="openAuditModal(row)">审核</a>
|
||||||
@@ -298,6 +306,12 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||||
|
|
||||||
defineOptions({ name: "goods" });
|
defineOptions({ name: "goods" });
|
||||||
|
const emit = defineEmits(["delete-schedule"]);
|
||||||
|
const props = defineProps({
|
||||||
|
scheduledStatus: { type: String, default: "SCHEDULED_UPPER" },
|
||||||
|
scheduledOnly: { type: Boolean, default: false },
|
||||||
|
readOnly: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
// 模板 ref(searchFormRef/underFormRef/batchAuditFormRef/goodsGroupFormRef 因与 data 同名改为 xxxRef)
|
// 模板 ref(searchFormRef/underFormRef/batchAuditFormRef/goodsGroupFormRef 因与 data 同名改为 xxxRef)
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
@@ -315,7 +329,7 @@ const auditModalVisible = ref(false);
|
|||||||
const auditSubmitLoading = ref(false);
|
const auditSubmitLoading = ref(false);
|
||||||
const currentAuditGoods = ref(null);
|
const currentAuditGoods = ref(null);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
const currentStatus = ref("ALL");
|
const currentStatus = ref(props.scheduledOnly ? props.scheduledStatus : "ALL");
|
||||||
|
|
||||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||||
getGoodsListData,
|
getGoodsListData,
|
||||||
@@ -333,8 +347,13 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
|
|||||||
},
|
},
|
||||||
beforeRequest: (p) => {
|
beforeRequest: (p) => {
|
||||||
const params = { ...p };
|
const params = { ...p };
|
||||||
if (currentStatus.value && currentStatus.value !== "ALL") {
|
const status = props.scheduledOnly ? props.scheduledStatus : currentStatus.value;
|
||||||
params.goodsStatus = currentStatus.value;
|
if (status && status !== "ALL") {
|
||||||
|
params.goodsStatus = status;
|
||||||
|
if (["SCHEDULED_UPPER", "SCHEDULED_DOWN"].includes(status)) {
|
||||||
|
params.sort = status === "SCHEDULED_UPPER" ? "scheduled_upper_time" : "scheduled_down_time";
|
||||||
|
params.order = "asc";
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
delete params.goodsStatus;
|
delete params.goodsStatus;
|
||||||
}
|
}
|
||||||
@@ -347,7 +366,7 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
|
|||||||
);
|
);
|
||||||
|
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
currentStatus.value = "ALL";
|
currentStatus.value = props.scheduledOnly ? props.scheduledStatus : "ALL";
|
||||||
resetSearch();
|
resetSearch();
|
||||||
}
|
}
|
||||||
const goodsNumerData = ref({});
|
const goodsNumerData = ref({});
|
||||||
@@ -415,6 +434,7 @@ function init() {
|
|||||||
// 搜索
|
// 搜索
|
||||||
// 获取数据
|
// 获取数据
|
||||||
function getNumberData() {
|
function getNumberData() {
|
||||||
|
if (props.scheduledOnly) return;
|
||||||
// 创建一个不包含goodsStatus字段的搜索参数
|
// 创建一个不包含goodsStatus字段的搜索参数
|
||||||
const { goodsStatus, ...searchParams } = searchForm;
|
const { goodsStatus, ...searchParams } = searchForm;
|
||||||
getGoodsNumerData(searchParams).then((res) => {
|
getGoodsNumerData(searchParams).then((res) => {
|
||||||
@@ -488,6 +508,7 @@ function goodsStatusClick(name) {
|
|||||||
}
|
}
|
||||||
selectedRows.value = [];
|
selectedRows.value = [];
|
||||||
clearTableSelection();
|
clearTableSelection();
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function examine(v, authFlag) {
|
function examine(v, authFlag) {
|
||||||
|
|||||||
@@ -1,47 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="brand-manage-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选表单 -->
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<el-form-item label="品牌名称">
|
<el-form-item label="品牌名称" prop="name">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.name"
|
v-model="searchForm.name"
|
||||||
placeholder="请输入品牌名称"
|
placeholder="请输入品牌名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 220px"
|
||||||
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||||
<el-button @click="resetSearch">重置</el-button>
|
<el-button @click="resetSearch">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="mt_10">
|
<!-- 品牌数据表格卡片 -->
|
||||||
<div class="operation padding-row" style="margin-bottom: 10px;">
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
<el-button type="primary" @click="add">添加</el-button>
|
<list-action-bar>
|
||||||
</div>
|
<el-button type="primary" @click="add">添加品牌</el-button>
|
||||||
|
<template #right>
|
||||||
|
<el-button @click="getDataList">刷新</el-button>
|
||||||
|
</template>
|
||||||
|
</list-action-bar>
|
||||||
|
|
||||||
<el-table ref="table" v-loading="loading" border :data="data" style="width: 100%">
|
<el-table
|
||||||
<el-table-column prop="name" label="品牌名称" width="200" />
|
ref="table"
|
||||||
<el-table-column label="品牌图标" align="left">
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:data="data"
|
||||||
|
style="width: 100%"
|
||||||
|
class="custom-brand-table"
|
||||||
|
>
|
||||||
|
<!-- 品牌ID -->
|
||||||
|
<el-table-column prop="id" label="品牌ID" min-width="190" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 品牌图标 -->
|
||||||
|
<el-table-column label="品牌图标" width="96" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img
|
<div v-if="row && row.logo" class="logo-box">
|
||||||
v-if="row"
|
<el-image
|
||||||
:src="row.logo || ''"
|
:src="row.logo"
|
||||||
alt="加载图片失败"
|
fit="contain"
|
||||||
style="cursor: pointer; width: 80px; height: 60px; margin: 10px 0; object-fit: contain"
|
preview-teleported
|
||||||
/>
|
:preview-src-list="[row.logo]"
|
||||||
|
class="brand-logo-img"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="logo-slot">无图</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
</div>
|
||||||
|
<span v-else>-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" align="left" width="150">
|
|
||||||
|
<!-- 品牌名称 -->
|
||||||
|
<el-table-column prop="name" label="品牌名称" min-width="180" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 状态 -->
|
||||||
|
<el-table-column label="状态" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-switch
|
<el-switch
|
||||||
v-if="row"
|
v-if="row"
|
||||||
@@ -53,8 +83,12 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" min-width="170" show-overflow-tooltip />
|
|
||||||
<el-table-column label="操作" width="230" align="center" fixed="right">
|
<!-- 创建时间 -->
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<template v-if="row">
|
||||||
<a class="link-text" @click="edit(row)">编辑</a>
|
<a class="link-text" @click="edit(row)">编辑</a>
|
||||||
@@ -67,7 +101,8 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -81,24 +116,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 添加 / 编辑品牌弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="modalVisible"
|
v-model="modalVisible"
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
width="500px"
|
width="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
<el-form ref="formRef" :model="form" label-width="90px" :rules="formValidate">
|
||||||
|
<el-form-item v-if="modalType === 1 && form.id" label="品牌ID">
|
||||||
|
<span class="font-mono text-dark">{{ form.id }}</span>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="品牌名称" prop="name">
|
<el-form-item label="品牌名称" prop="name">
|
||||||
<el-input v-model="form.name" style="width: 100%" />
|
<el-input v-model="form.name" placeholder="请输入品牌名称" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="品牌图标" prop="logo">
|
<el-form-item label="品牌图标" prop="logo">
|
||||||
<div style="display: flex; align-items: center; gap: 8px">
|
<div class="dialog-logo-picker">
|
||||||
<img
|
<div class="logo-preview-box">
|
||||||
:src="form.logo || defaultPic"
|
<img
|
||||||
alt=""
|
:src="form.logo || defaultPic"
|
||||||
style="width: 60px; height: 60px; object-fit: contain; border: 1px solid #dcdfe6; border-radius: 4px"
|
alt="品牌图标"
|
||||||
/>
|
class="dialog-logo-img"
|
||||||
<el-button type="primary" link @click="openLogoPicker">修改</el-button>
|
/>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" link @click="openLogoPicker">上传/选择图标</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -108,7 +150,8 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="picModelFlag" width="1200px" :show-close="true">
|
<!-- OSS 图片选择组件弹窗 -->
|
||||||
|
<el-dialog v-model="picModelFlag" width="1200px" title="选择品牌图标" :show-close="true">
|
||||||
<ossManage
|
<ossManage
|
||||||
ref="ossManageRef"
|
ref="ossManageRef"
|
||||||
:is-component="true"
|
:is-component="true"
|
||||||
@@ -117,13 +160,15 @@
|
|||||||
/>
|
/>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 关联分类弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="categoryModalVisible"
|
v-model="categoryModalVisible"
|
||||||
:title="categoryModalTitle"
|
:title="categoryModalTitle"
|
||||||
width="700px"
|
width="680px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<div v-loading="categoryTreeLoading" style="position: relative; max-height: 520px; overflow: auto">
|
<div v-loading="categoryTreeLoading" class="category-tree-wrap">
|
||||||
<el-tree
|
<el-tree
|
||||||
ref="categoryTree"
|
ref="categoryTree"
|
||||||
:key="categoryTreeKey"
|
:key="categoryTreeKey"
|
||||||
@@ -139,7 +184,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="categoryModalVisible = false">取消</el-button>
|
<el-button @click="categoryModalVisible = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="categorySubmitLoading" @click="submitBrandCategory">
|
<el-button type="primary" :loading="categorySubmitLoading" @click="submitBrandCategory">
|
||||||
提交
|
提交关联
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -147,6 +192,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ListActionBar from "@/components/list-action-bar.vue";
|
||||||
import { ref, reactive, nextTick, getCurrentInstance } from "vue";
|
import { ref, reactive, nextTick, getCurrentInstance } from "vue";
|
||||||
import { useTable } from "@/utils/useTable";
|
import { useTable } from "@/utils/useTable";
|
||||||
import {
|
import {
|
||||||
@@ -299,7 +345,7 @@ function handleStatusSwitchChange(row, checked) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function delBrand(row) {
|
function delBrand(row) {
|
||||||
ElMessageBox.confirm("您确认要删除 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
ElMessageBox.confirm("您确认要删除品牌 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
||||||
return delBrandApi(row.id).then((res) => {
|
return delBrandApi(row.id).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
ElMessage.success("品牌删除成功!");
|
ElMessage.success("品牌删除成功!");
|
||||||
@@ -339,7 +385,7 @@ function handleSubmit() {
|
|||||||
|
|
||||||
function add() {
|
function add() {
|
||||||
modalType.value = 0;
|
modalType.value = 0;
|
||||||
modalTitle.value = "添加";
|
modalTitle.value = "添加品牌";
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
delete form.value.id;
|
delete form.value.id;
|
||||||
modalVisible.value = true;
|
modalVisible.value = true;
|
||||||
@@ -347,7 +393,7 @@ function add() {
|
|||||||
|
|
||||||
function edit(v) {
|
function edit(v) {
|
||||||
modalType.value = 1;
|
modalType.value = 1;
|
||||||
modalTitle.value = "编辑";
|
modalTitle.value = "编辑品牌";
|
||||||
formRef.value?.resetFields();
|
formRef.value?.resetFields();
|
||||||
for (const attr in v) {
|
for (const attr in v) {
|
||||||
if (v[attr] === null) {
|
if (v[attr] === null) {
|
||||||
@@ -360,13 +406,108 @@ function edit(v) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.link-text {
|
.brand-manage-page {
|
||||||
color: #409eff;
|
.filter-card {
|
||||||
cursor: pointer;
|
.search-form {
|
||||||
text-decoration: none;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-placeholder {
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-brand-table {
|
||||||
|
.logo-box {
|
||||||
|
width: 52px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.brand-logo-img {
|
||||||
|
width: 48px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-slot {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.op-split {
|
|
||||||
margin: 0 8px;
|
.dialog-logo-picker {
|
||||||
color: #dcdee2;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.logo-preview-box {
|
||||||
|
width: 64px;
|
||||||
|
height: 48px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.dialog-logo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-tree-wrap {
|
||||||
|
position: relative;
|
||||||
|
max-height: 520px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-card>
|
<el-card>
|
||||||
<div class="mb_10">
|
<list-action-bar>
|
||||||
<el-button type="primary" @click="addParent">添加一级分类</el-button>
|
<el-button type="primary" @click="addParent">添加一级分类</el-button>
|
||||||
</div>
|
</list-action-bar>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
@@ -121,6 +121,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ListActionBar from "@/components/list-action-bar.vue";
|
||||||
import { ref, reactive, onMounted } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import {
|
import {
|
||||||
delCategory,
|
delCategory,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card>
|
<el-card class="parameter-filter-card">
|
||||||
<el-form ref="searchFormRef" :model="searchForm" inline>
|
<el-form ref="searchFormRef" :model="searchForm" inline class="search-form" @keyup.enter="handleSearch">
|
||||||
<el-form-item label="参数名称" prop="paramName">
|
<el-form-item label="参数名称" prop="paramName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.paramName"
|
v-model="searchForm.paramName"
|
||||||
@@ -16,9 +16,12 @@
|
|||||||
<el-button @click="handleReset">重置</el-button>
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-row class="operation" style="margin-bottom: 10px;">
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="parameter-list-card" style="margin-top: 10px">
|
||||||
|
<list-action-bar>
|
||||||
<el-button type="primary" @click="goAdd">添加</el-button>
|
<el-button type="primary" @click="goAdd">添加</el-button>
|
||||||
</el-row>
|
</list-action-bar>
|
||||||
<el-table
|
<el-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
@@ -72,6 +75,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ListActionBar from "@/components/list-action-bar.vue";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { useTable } from "@/utils/useTable";
|
import { useTable } from "@/utils/useTable";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,77 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="goods-group-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选与操作栏 -->
|
||||||
<div class="operation padding-row">
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
<div class="card-toolbar">
|
||||||
|
<el-form inline :model="searchForm" class="filter-form" @keyup.enter="handleSearch">
|
||||||
|
<el-form-item label="分组名称">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.groupName"
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
clearable
|
||||||
|
style="width: 220px"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<el-table ref="table" v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
|
<!-- 分组数据表格卡片 -->
|
||||||
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
|
<list-action-bar>
|
||||||
|
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||||
|
<template #right>
|
||||||
|
<el-button @click="getData">刷新</el-button>
|
||||||
|
</template>
|
||||||
|
</list-action-bar>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
ref="table"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="data"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
class="custom-group-table"
|
||||||
|
>
|
||||||
|
<!-- 分组ID -->
|
||||||
|
<el-table-column prop="id" label="分组ID" min-width="190" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 分组名称 -->
|
||||||
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="createTime" label="创建时间" width="180" />
|
<!-- 分组描述 -->
|
||||||
<el-table-column prop="updateTime" label="更新时间" width="180" />
|
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip>
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
{{ row ? row.description || "-" : "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 更新时间 -->
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" min-width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row">
|
||||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
<span class="op-split">|</span>
|
||||||
<a class="link-text" @click="remove(row)">删除</a>
|
<a class="link-text" @click="remove(row)">删除</a>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -35,13 +85,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="addFlag" title="添加商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
<!-- 添加商品分组弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="addFlag"
|
||||||
|
title="添加商品分组"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<el-form ref="addFormRef" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
<el-form ref="addFormRef" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
||||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
<el-form-item label="分组名称" prop="groupName">
|
||||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
<el-input
|
||||||
|
v-model="formAdd.groupName"
|
||||||
|
maxlength="30"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
<el-form-item label="分组描述" prop="description">
|
||||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
<el-input
|
||||||
|
v-model="formAdd.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组描述信息"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -50,14 +119,35 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="editFlag" title="编辑商品分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
<!-- 编辑商品分组弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="editFlag"
|
||||||
|
title="编辑商品分组"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<el-form ref="editFormRef" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
<el-form ref="editFormRef" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
||||||
<el-input v-model="formEdit.id" style="display: none" />
|
<el-form-item label="分组ID">
|
||||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
<span>{{ formEdit.id }}</span>
|
||||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
<el-form-item label="分组名称" prop="groupName">
|
||||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
<el-input
|
||||||
|
v-model="formEdit.groupName"
|
||||||
|
maxlength="30"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分组描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="formEdit.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组描述信息"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -69,6 +159,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import ListActionBar from "@/components/list-action-bar.vue";
|
||||||
import { ref, reactive, nextTick, onMounted } from "vue";
|
import { ref, reactive, nextTick, onMounted } from "vue";
|
||||||
import * as API_Goods from "@/api/goods.js";
|
import * as API_Goods from "@/api/goods.js";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
@@ -79,6 +170,7 @@ const loading = ref(true);
|
|||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
groupName: "",
|
||||||
});
|
});
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -86,18 +178,22 @@ const addFlag = ref(false);
|
|||||||
const editFlag = ref(false);
|
const editFlag = ref(false);
|
||||||
const submitAddLoading = ref(false);
|
const submitAddLoading = ref(false);
|
||||||
const submitEditLoading = ref(false);
|
const submitEditLoading = ref(false);
|
||||||
|
|
||||||
const formAdd = ref({
|
const formAdd = ref({
|
||||||
groupName: "",
|
groupName: "",
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const formEdit = ref({
|
const formEdit = ref({
|
||||||
id: "",
|
id: "",
|
||||||
groupName: "",
|
groupName: "",
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const rulesAdd = reactive({
|
const rulesAdd = reactive({
|
||||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rulesEdit = reactive({
|
const rulesEdit = reactive({
|
||||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
@@ -105,7 +201,14 @@ const rulesEdit = reactive({
|
|||||||
const addFormRef = ref(null);
|
const addFormRef = ref(null);
|
||||||
const editFormRef = ref(null);
|
const editFormRef = ref(null);
|
||||||
|
|
||||||
function init() {
|
function handleSearch() {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
searchForm.groupName = "";
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,36 +319,59 @@ function submitEdit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function remove(row) {
|
function remove(row) {
|
||||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "提示", {
|
||||||
return API_Goods.deleteGoodsGroup(row.id)
|
type: "warning",
|
||||||
.then((res) => {
|
confirmButtonText: "确定删除",
|
||||||
if (res && res.success) {
|
cancelButtonText: "取消",
|
||||||
ElMessage.success("删除成功");
|
})
|
||||||
getData();
|
.then(() => {
|
||||||
} else if (res && res.message) {
|
return API_Goods.deleteGoodsGroup(row.id)
|
||||||
ElMessage.error(res.message);
|
.then((res) => {
|
||||||
}
|
if (res && res.success) {
|
||||||
})
|
ElMessage.success("删除成功");
|
||||||
.catch(() => {
|
getData();
|
||||||
ElMessage.error("删除失败,请检查权限或后端接口");
|
} else if (res && res.message) {
|
||||||
});
|
ElMessage.error(res.message);
|
||||||
}).catch(() => {});
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ElMessage.error("删除失败,请检查权限或后端接口");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
getData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.ops a {
|
.goods-group-page {
|
||||||
color: #2d8cf0;
|
.card-toolbar {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
text-decoration: none;
|
justify-content: space-between;
|
||||||
}
|
align-items: center;
|
||||||
.ops span {
|
flex-wrap: wrap;
|
||||||
display: inline-block;
|
gap: 12px;
|
||||||
margin: 0 8px;
|
|
||||||
color: #dcdee2;
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
45
manager/src/views/goods/scheduled-upper/index.vue
Normal file
45
manager/src/views/goods/scheduled-upper/index.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<goods-list :key="`${activeStatus}-${refreshKey}`" scheduled-only read-only :scheduled-status="activeStatus" @delete-schedule="deleteSchedule">
|
||||||
|
<template #scheduled-controls>
|
||||||
|
<el-tabs v-model="activeStatus">
|
||||||
|
<el-tab-pane label="定时上架" name="SCHEDULED_UPPER" />
|
||||||
|
<el-tab-pane label="定时下架" name="SCHEDULED_DOWN" />
|
||||||
|
</el-tabs>
|
||||||
|
</template>
|
||||||
|
</goods-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 平台商品定时上架列表。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-03
|
||||||
|
*/
|
||||||
|
import GoodsList from "@/views/goods/goods-info/goods.vue";
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { cancelGoodsMarketSchedule } from "@/api/goods";
|
||||||
|
|
||||||
|
defineOptions({ name: "goodsScheduledUpper" });
|
||||||
|
|
||||||
|
const activeStatus = ref("SCHEDULED_UPPER");
|
||||||
|
const refreshKey = ref(0);
|
||||||
|
|
||||||
|
async function deleteSchedule(row) {
|
||||||
|
const status = activeStatus.value === "SCHEDULED_UPPER" ? "UPPER" : "DOWN";
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认删除“${row.goodsName}”的定时${status === 'UPPER' ? '上架' : '下架'}计划?`, "删除定时计划", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" });
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await cancelGoodsMarketSchedule(row.id, status);
|
||||||
|
if (res.success) {
|
||||||
|
ElMessage.success("定时计划已删除");
|
||||||
|
refreshKey.value++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 接口错误由统一拦截器提示。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -49,9 +49,13 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="mt_10">
|
<el-card class="mt_10">
|
||||||
|
<el-tabs v-model="salesTab" @tab-change="handleSalesTabChange">
|
||||||
|
<el-tab-pane label="全部" name="all" />
|
||||||
|
<el-tab-pane label="已设置" name="configured" />
|
||||||
|
<el-tab-pane label="未设置" name="unconfigured" />
|
||||||
|
</el-tabs>
|
||||||
<el-alert
|
<el-alert
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
|
||||||
:closable="false"
|
:closable="false"
|
||||||
title="支持按规格逐条设置虚拟销量,列表总销量 = 真实销量 + 虚拟销量。"
|
title="支持按规格逐条设置虚拟销量,列表总销量 = 真实销量 + 虚拟销量。"
|
||||||
style="margin-bottom: 10px"
|
style="margin-bottom: 10px"
|
||||||
@@ -71,55 +75,65 @@
|
|||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="data"
|
:data="data"
|
||||||
|
border
|
||||||
class="mt_10"
|
class="mt_10"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@sort-change="changeSort"
|
@sort-change="changeSort"
|
||||||
@selection-change="handleSelectionChange"
|
@selection-change="handleSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
<el-table-column prop="id" label="SKU ID" width="180" show-overflow-tooltip />
|
<el-table-column label="商品信息" min-width="320">
|
||||||
<el-table-column prop="goodsId" label="SPU编码" width="180" show-overflow-tooltip />
|
|
||||||
<el-table-column label="商品信息" min-width="360">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row" class="goods-info">
|
<div v-if="row" class="goods-info">
|
||||||
<img
|
<div class="goods-thumb-box">
|
||||||
v-if="row.thumbnail"
|
<el-image
|
||||||
:src="row.thumbnail"
|
v-if="row.thumbnail"
|
||||||
class="goods-thumbnail"
|
:src="row.thumbnail"
|
||||||
alt=""
|
fit="cover"
|
||||||
/>
|
preview-teleported
|
||||||
|
:preview-src-list="[row.thumbnail]"
|
||||||
|
class="goods-thumbnail"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="thumb-slot">无图</div>
|
||||||
|
</template>
|
||||||
|
</el-image>
|
||||||
|
<div v-else class="thumb-slot">无图</div>
|
||||||
|
</div>
|
||||||
<div class="goods-text">
|
<div class="goods-text">
|
||||||
<div class="div-zoom">{{ row.goodsName }}</div>
|
<div class="goods-title" :title="row.goodsName">{{ row.goodsName || "-" }}</div>
|
||||||
<div v-if="row.simpleSpecs" class="sub-title">
|
<div v-if="row.simpleSpecs" class="goods-specs">
|
||||||
规格:{{ row.simpleSpecs.trim() || "-" }}
|
规格:{{ row.simpleSpecs.trim() || "-" }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="真实销量" width="110">
|
<el-table-column prop="goodsId" label="SPU编码" width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="id" label="SKU ID" width="220" show-overflow-tooltip />
|
||||||
|
<el-table-column label="真实销量" width="110" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.buyCount || 0 }}</span>
|
<span v-if="row">{{ row.buyCount || 0 }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="虚拟销量" width="110">
|
<el-table-column label="虚拟销量" width="110" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.virtualSalesInput || 0 }}</span>
|
<span v-if="row">{{ row.virtualSalesInput || 0 }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="总销量" width="110">
|
<el-table-column label="总销量" width="110" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ getTotalSales(row) }}</span>
|
<span v-if="row">{{ getTotalSales(row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="状态" width="100">
|
<el-table-column label="状态" width="90" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="row.marketEnable === 'UPPER' ? 'success' : 'warning'">
|
<span v-if="row">
|
||||||
{{ row.marketEnable === "UPPER" ? "上架" : "下架" }}
|
{{ row.marketEnable === "UPPER" ? "上架" : "下架" }}
|
||||||
</el-tag>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="100" align="center" fixed="right">
|
<el-table-column label="操作" width="90" align="center" fixed="right">
|
||||||
<template #default="{ row, $index }">
|
<template #default="{ row, $index }">
|
||||||
<a
|
<a
|
||||||
v-if="row"
|
v-if="row"
|
||||||
@@ -147,14 +161,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="editModalVisible"
|
v-model="editModalVisible"
|
||||||
:title="modalMode === 'batch' ? '批量设置虚拟销量' : '设置虚拟销量'"
|
:title="modalMode === 'batch' ? '批量设置虚拟销量' : '设置虚拟销量'"
|
||||||
width="480px"
|
class="virtual-sales-drawer"
|
||||||
|
direction="rtl"
|
||||||
|
size="min(480px, 100vw)"
|
||||||
|
append-to-body
|
||||||
|
:before-close="handleBeforeCloseVirtualSales"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
:close-on-press-escape="!modalSubmitting"
|
:close-on-press-escape="!modalSubmitting"
|
||||||
:show-close="!modalSubmitting"
|
:show-close="!modalSubmitting"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
|
@closed="resetVirtualSalesModal"
|
||||||
>
|
>
|
||||||
<div v-if="modalMode === 'single' && currentSku" class="virtual-sales-modal">
|
<div v-if="modalMode === 'single' && currentSku" class="virtual-sales-modal">
|
||||||
<div class="sku-info-item">
|
<div class="sku-info-item">
|
||||||
@@ -199,12 +218,14 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button :disabled="modalSubmitting" @click="handleCancelVirtualSales">取消</el-button>
|
<div class="drawer-actions">
|
||||||
<el-button type="primary" :loading="modalSubmitting" @click="handleSubmitVirtualSales">
|
<el-button :disabled="modalSubmitting" @click="handleCancelVirtualSales">取消</el-button>
|
||||||
提交
|
<el-button type="primary" :loading="modalSubmitting" @click="handleSubmitVirtualSales">
|
||||||
</el-button>
|
提交
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -239,6 +260,7 @@ const modalMode = ref("single");
|
|||||||
const currentSku = ref(null);
|
const currentSku = ref(null);
|
||||||
const currentIndex = ref(-1);
|
const currentIndex = ref(-1);
|
||||||
const selectedRows = ref([]);
|
const selectedRows = ref([]);
|
||||||
|
const salesTab = ref("all");
|
||||||
const editForm = reactive({
|
const editForm = reactive({
|
||||||
virtualSales: 0,
|
virtualSales: 0,
|
||||||
});
|
});
|
||||||
@@ -254,6 +276,7 @@ const searchForm = ref({
|
|||||||
});
|
});
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
let dataRequestId = 0;
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
getDataList();
|
getDataList();
|
||||||
@@ -271,6 +294,12 @@ function handleSearch() {
|
|||||||
searchForm.value.pageNumber = 1;
|
searchForm.value.pageNumber = 1;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
|
function handleSalesTabChange() {
|
||||||
|
searchForm.value.pageNumber = 1;
|
||||||
|
selectedRows.value = [];
|
||||||
|
tableRef.value?.clearSelection();
|
||||||
|
getDataList();
|
||||||
|
}
|
||||||
function handleReset() {
|
function handleReset() {
|
||||||
searchForm.value = {
|
searchForm.value = {
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
@@ -298,9 +327,15 @@ function handleSelectionChange(selection) {
|
|||||||
selectedRows.value = selection;
|
selectedRows.value = selection;
|
||||||
}
|
}
|
||||||
function getDataList() {
|
function getDataList() {
|
||||||
|
const requestId = ++dataRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
getGoodsSkuData(searchForm.value)
|
const params = { ...searchForm.value };
|
||||||
|
if (salesTab.value !== "all") {
|
||||||
|
params.virtualSalesSet = salesTab.value === "configured";
|
||||||
|
}
|
||||||
|
getGoodsSkuData(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (requestId !== dataRequestId) return;
|
||||||
if (res && res.success && res.result) {
|
if (res && res.success && res.result) {
|
||||||
data.value = (res.result.records || []).map((item) => ({
|
data.value = (res.result.records || []).map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
@@ -308,6 +343,10 @@ function getDataList() {
|
|||||||
saving: false,
|
saving: false,
|
||||||
}));
|
}));
|
||||||
total.value = res.result.total || 0;
|
total.value = res.result.total || 0;
|
||||||
|
if (data.value.length === 0 && total.value > 0 && searchForm.value.pageNumber > 1) {
|
||||||
|
searchForm.value.pageNumber = Math.max(1, Math.ceil(total.value / searchForm.value.pageSize));
|
||||||
|
getDataList();
|
||||||
|
}
|
||||||
selectedRows.value = [];
|
selectedRows.value = [];
|
||||||
} else {
|
} else {
|
||||||
data.value = [];
|
data.value = [];
|
||||||
@@ -317,13 +356,14 @@ function getDataList() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (requestId !== dataRequestId) return;
|
||||||
data.value = [];
|
data.value = [];
|
||||||
total.value = 0;
|
total.value = 0;
|
||||||
selectedRows.value = [];
|
selectedRows.value = [];
|
||||||
ElMessage.error("加载规格列表失败");
|
ElMessage.error("加载规格列表失败");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
loading.value = false;
|
if (requestId === dataRequestId) loading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getVirtualSalesValue(row) {
|
function getVirtualSalesValue(row) {
|
||||||
@@ -354,6 +394,10 @@ function handleCancelVirtualSales() {
|
|||||||
if (modalSubmitting.value) return;
|
if (modalSubmitting.value) return;
|
||||||
resetVirtualSalesModal();
|
resetVirtualSalesModal();
|
||||||
}
|
}
|
||||||
|
function handleBeforeCloseVirtualSales(done) {
|
||||||
|
if (modalSubmitting.value) return;
|
||||||
|
done();
|
||||||
|
}
|
||||||
function resetVirtualSalesModal() {
|
function resetVirtualSalesModal() {
|
||||||
editModalVisible.value = false;
|
editModalVisible.value = false;
|
||||||
modalMode.value = "single";
|
modalMode.value = "single";
|
||||||
@@ -388,6 +432,7 @@ function handleSubmitVirtualSales() {
|
|||||||
}
|
}
|
||||||
ElMessage.success("虚拟销量设置成功");
|
ElMessage.success("虚拟销量设置成功");
|
||||||
resetVirtualSalesModal();
|
resetVirtualSalesModal();
|
||||||
|
getDataList();
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error((res && res.message) || "规格虚拟销量设置失败");
|
ElMessage.error((res && res.message) || "规格虚拟销量设置失败");
|
||||||
}
|
}
|
||||||
@@ -424,6 +469,7 @@ function handleBatchSubmitVirtualSales(virtualSales) {
|
|||||||
tableRef.value?.clearSelection();
|
tableRef.value?.clearSelection();
|
||||||
ElMessage.success("虚拟销量设置成功");
|
ElMessage.success("虚拟销量设置成功");
|
||||||
resetVirtualSalesModal();
|
resetVirtualSalesModal();
|
||||||
|
getDataList();
|
||||||
} else {
|
} else {
|
||||||
ElMessage.error((res && res.message) || "虚拟销量设置失败");
|
ElMessage.error((res && res.message) || "虚拟销量设置失败");
|
||||||
}
|
}
|
||||||
@@ -445,32 +491,85 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.sub-title {
|
.virtual-sales-drawer {
|
||||||
margin-top: 6px;
|
:deep(.el-drawer__header) {
|
||||||
color: #808695;
|
margin-bottom: 0;
|
||||||
font-size: 12px;
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-drawer__footer) {
|
||||||
|
border-top: 1px solid var(--el-border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
:deep(.el-button + .el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-info {
|
.goods-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 5px 0;
|
padding: 4px 0;
|
||||||
padding: 10px 0;
|
}
|
||||||
|
|
||||||
|
.goods-thumb-box {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
margin-right: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-thumbnail {
|
.goods-thumbnail {
|
||||||
width: 50px;
|
width: 44px;
|
||||||
height: 50px;
|
height: 44px;
|
||||||
margin-right: 12px;
|
display: block;
|
||||||
object-fit: cover;
|
}
|
||||||
flex-shrink: 0;
|
|
||||||
|
.thumb-slot {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-text {
|
.goods-text {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.div-zoom {
|
.goods-title {
|
||||||
|
line-height: 1.4;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-all;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-specs {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -493,11 +592,28 @@ onMounted(() => {
|
|||||||
|
|
||||||
.sku-info-label {
|
.sku-info-label {
|
||||||
width: 90px;
|
width: 90px;
|
||||||
color: #808695;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mt_10 {
|
.mt_10 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
|
color: #2d8cf0;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #57a3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: #c5c8ce;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -77,9 +77,9 @@
|
|||||||
<div class="counts">{{ notices.appointmentWorkOrder || 0 }}</div>
|
<div class="counts">{{ notices.appointmentWorkOrder || 0 }}</div>
|
||||||
<div>未完成预约工单</div>
|
<div>未完成预约工单</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="todo-item" @click="goProcurementAudit">
|
<div class="todo-item" @click="goMemberWithdrawApply">
|
||||||
<div class="counts">{{ notices.procurementAudit || 0 }}</div>
|
<div class="counts">{{ notices.balanceCash || 0 }}</div>
|
||||||
<div>待审核采购单</div>
|
<div>待审核会员提现</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -321,10 +321,10 @@ function goAppointmentWorkOrders() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function goProcurementAudit() {
|
function goMemberWithdrawApply() {
|
||||||
router.push({
|
router.push({
|
||||||
name: "manager-procurement-order-list",
|
name: "withdrawApply",
|
||||||
query: { status: "SUBMITTED" },
|
query: { applyStatus: "APPLY" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<li>多店铺管理与经营数据分析</li>
|
<li>多店铺管理与经营数据分析</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<p class="brand-copyright">Copyright © {{ year }} Lilishop</p>
|
<p class="brand-copyright">Copyright © {{ year }} {{ copyrightOwner }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-panel" @keyup.enter="submitLogin">
|
<div class="login-panel" @keyup.enter="submitLogin">
|
||||||
@@ -94,11 +94,13 @@ import verify from "@/components/verify";
|
|||||||
import { setStore } from "@/libs/storage";
|
import { setStore } from "@/libs/storage";
|
||||||
import { md5 } from "@/utils/md5.js";
|
import { md5 } from "@/utils/md5.js";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const year = ref(new Date().getFullYear());
|
const year = ref(new Date().getFullYear());
|
||||||
|
const { copyrightOwner } = useSiteSettings();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
username: "",
|
username: "",
|
||||||
|
|||||||
@@ -52,10 +52,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog
|
<el-drawer
|
||||||
v-model="modalVisible"
|
v-model="modalVisible"
|
||||||
:title="modalTitle"
|
:title="modalTitle"
|
||||||
width="500px"
|
size="500px"
|
||||||
|
append-to-body
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
提交
|
提交
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-drawer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="foot">
|
<div class="foot">
|
||||||
<el-row justify="space-around" class="help">
|
<el-row v-if="website" justify="space-around" class="help">
|
||||||
<a class="item" :href="website" target="_blank">帮助</a>
|
<a class="item" :href="website" target="_blank">帮助</a>
|
||||||
<a class="item" :href="website" target="_blank">隐私</a>
|
<a class="item" :href="website" target="_blank">隐私</a>
|
||||||
<a class="item" :href="website" target="_blank">条款</a>
|
<a class="item" :href="website" target="_blank">条款</a>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row justify="center" class="copyright">
|
<el-row justify="center" class="copyright">
|
||||||
Copyright © {{ year }} - Present
|
Copyright © {{ year }}
|
||||||
<a :href="website" class="href" target="_blank" style="margin: 0 5px">{{ config.title }}</a>
|
<a :href="website || undefined" class="href" target="_blank" style="margin: 0 5px">{{ copyrightOwner }}</a>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import config from "@/config/index";
|
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||||
|
|
||||||
const year = ref(new Date().getFullYear());
|
const year = ref(new Date().getFullYear());
|
||||||
// 官网地址优先取接口下发缓存,兜底用本地配置
|
const { website, copyrightOwner } = useSiteSettings();
|
||||||
const website = ref(localStorage.getItem("website") || config.website);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { getBaseSite } from "@/api/common.js";
|
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||||
|
|
||||||
const domainLogo = ref("");
|
const domainLogo = ref("");
|
||||||
|
|
||||||
@@ -24,11 +24,9 @@ function applyFavicon(href) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSite() {
|
function getSite() {
|
||||||
getBaseSite().then((res) => {
|
loadSiteSettings().then((data) => {
|
||||||
if (res.success && res.result && res.result.settingValue) {
|
if (data) {
|
||||||
const { domainLogo: logo, domainIcon, siteName, website } = JSON.parse(
|
const { domainLogo: logo, domainIcon, siteName, website } = data;
|
||||||
res.result.settingValue
|
|
||||||
);
|
|
||||||
domainLogo.value = logo || "";
|
domainLogo.value = logo || "";
|
||||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||||
localStorage.setItem("icontitle_expiration_time", expirationTime);
|
localStorage.setItem("icontitle_expiration_time", expirationTime);
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
</a>
|
</a>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu v-if="value != 0">
|
<el-dropdown-menu v-if="value != 0">
|
||||||
<el-dropdown-item v-if="res.balanceCash" command="deposit">
|
<el-dropdown-item v-if="res.balanceCash" command="withdrawApply">
|
||||||
<el-badge :value="res.balanceCash">待处理预存款提现申请</el-badge>
|
<el-badge :value="res.balanceCash">待处理会员提现申请</el-badge>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item v-if="res.complain" command="orderComplaint">
|
<el-dropdown-item v-if="res.complain" command="orderComplaint">
|
||||||
<el-badge :value="res.complain">待处理投诉审核</el-badge>
|
<el-badge :value="res.complain">待处理投诉审核</el-badge>
|
||||||
@@ -28,9 +28,6 @@
|
|||||||
<el-dropdown-item v-if="res.appointmentWorkOrder" command="manager-appointment-work-orders">
|
<el-dropdown-item v-if="res.appointmentWorkOrder" command="manager-appointment-work-orders">
|
||||||
<el-badge :value="res.appointmentWorkOrder">未完成预约工单</el-badge>
|
<el-badge :value="res.appointmentWorkOrder">未完成预约工单</el-badge>
|
||||||
</el-dropdown-item>
|
</el-dropdown-item>
|
||||||
<el-dropdown-item v-if="res.procurementAudit" command="manager-procurement-order-list">
|
|
||||||
<el-badge :value="res.procurementAudit">待审核采购单</el-badge>
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -63,12 +60,12 @@ const value = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function navigateTo(name) {
|
function navigateTo(name) {
|
||||||
if (name === "manager-appointment-work-orders") {
|
if (name === "withdrawApply") {
|
||||||
router.push({ name, query: { serviceStatus: "OPEN" } });
|
router.push({ name: "withdrawApply", query: { applyStatus: "APPLY" } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "manager-procurement-order-list") {
|
if (name === "manager-appointment-work-orders") {
|
||||||
router.push({ name, query: { status: "SUBMITTED" } });
|
router.push({ name, query: { serviceStatus: "OPEN" } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push({ name });
|
router.push({ name });
|
||||||
|
|||||||
@@ -1,20 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-recharge-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选表单 -->
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="客户ID" prop="memberId">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.memberId"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="memberName">
|
<el-form-item label="客户名称" prop="memberName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.memberName"
|
v-model="searchForm.memberName"
|
||||||
placeholder="请输入客户名称"
|
placeholder="请输入客户名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="充值单号" prop="rechargeSn">
|
<el-form-item label="充值单号" prop="rechargeSn">
|
||||||
@@ -22,59 +31,106 @@
|
|||||||
v-model="searchForm.rechargeSn"
|
v-model="searchForm.rechargeSn"
|
||||||
placeholder="请输入充值单号"
|
placeholder="请输入充值单号"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 190px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="支付时间">
|
<el-form-item label="充值时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="selectDate"
|
v-model="selectDate"
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
clearable
|
clearable
|
||||||
|
range-separator="至"
|
||||||
start-placeholder="开始时间"
|
start-placeholder="开始时间"
|
||||||
end-placeholder="结束时间"
|
end-placeholder="结束时间"
|
||||||
placeholder="选择起始时间"
|
style="width: 320px"
|
||||||
style="width: 360px"
|
|
||||||
@change="selectDateRange"
|
@change="selectDateRange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card>
|
|
||||||
|
<!-- 充值数据表格 -->
|
||||||
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>会员充值记录</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
ref="table"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
|
stripe
|
||||||
:data="data"
|
:data="data"
|
||||||
ref="table"
|
|
||||||
class="mt_10"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
<!-- 客户ID -->
|
||||||
<el-table-column prop="rechargeSn" label="订单号" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="充值金额" width="160" sortable>
|
|
||||||
|
<!-- 客户名称 -->
|
||||||
|
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 充值单号 -->
|
||||||
|
<el-table-column prop="rechargeSn" label="充值单号" min-width="180" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 充值金额 -->
|
||||||
|
<el-table-column label="充值金额" width="140" align="right" sortable prop="rechargeMoney">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme v-if="row" :value="row.rechargeMoney" :color="$mainColor" unit="+" />
|
<span v-if="row" class="recharge-money-text">
|
||||||
|
+¥ {{ formatMoney(row.rechargeMoney) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="充值方式" width="120">
|
|
||||||
|
<!-- 充值方式 -->
|
||||||
|
<el-table-column label="充值方式" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ rechargeWayText(row.rechargeWay) }}</span>
|
{{ row ? rechargeWayText(row.rechargeWay) : "-" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="支付状态" width="120">
|
|
||||||
|
<!-- 支付状态 -->
|
||||||
|
<el-table-column label="支付状态" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="row.payStatus === 'PAID' ? 'success' : 'danger'">
|
{{ row ? (row.payStatus === "PAID" ? "已付款" : "未付款") : "-" }}
|
||||||
{{ row.payStatus === "PAID" ? "已付款" : "未付款" }}
|
</template>
|
||||||
</el-tag>
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 第三方流水号 -->
|
||||||
|
<el-table-column prop="receivableNo" label="第三方流水号" min-width="180" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 充值时间 -->
|
||||||
|
<el-table-column prop="createTime" label="充值时间" width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 支付时间 -->
|
||||||
|
<el-table-column prop="payTime" label="支付时间" width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row">
|
||||||
|
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="充值时间" width="190" />
|
|
||||||
<el-table-column prop="payTime" label="支付时间" width="190" />
|
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -91,49 +147,85 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useTable } from "@/utils/useTable";
|
import { useTable } from "@/utils/useTable";
|
||||||
import { getUserRecharge } from "@/api/member";
|
import { getUserRecharge } from "@/api/member";
|
||||||
|
import { customRouterPush } from "@/utils/filters";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "recharge" });
|
defineOptions({ name: "recharge" });
|
||||||
|
|
||||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
const table = ref(null);
|
const table = ref(null);
|
||||||
|
|
||||||
const selectDate = ref(null);
|
const selectDate = ref(null);
|
||||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
|
||||||
|
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||||
getUserRecharge,
|
getUserRecharge,
|
||||||
{
|
{
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
immediate: false,
|
immediate: false,
|
||||||
form: { sort: "createTime", order: "desc", startDate: "", endDate: "", memberName: "" },
|
form: {
|
||||||
|
sort: "createTime",
|
||||||
|
order: "desc",
|
||||||
|
memberId: "",
|
||||||
|
memberName: "",
|
||||||
|
rechargeSn: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
},
|
||||||
|
beforeRequest: (p) => {
|
||||||
|
const params = { ...p };
|
||||||
|
if (selectDate.value && selectDate.value.length === 2) {
|
||||||
|
params.startDate = selectDate.value[0];
|
||||||
|
params.endDate = selectDate.value[1];
|
||||||
|
} else {
|
||||||
|
params.startDate = "";
|
||||||
|
params.endDate = "";
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
function rechargeWayText(way) {
|
function rechargeWayText(way) {
|
||||||
if (way === "ALIPAY") return "支付宝";
|
if (way === "ALIPAY") return "支付宝";
|
||||||
if (way === "WECHAT") return "微信";
|
if (way === "WECHAT") return "微信支付";
|
||||||
if (way === "BANK_TRANSFER") return "线下转账";
|
if (way === "BANK_TRANSFER") return "线下转账";
|
||||||
return "";
|
return way || "-";
|
||||||
}
|
}
|
||||||
function init() {
|
|
||||||
getDataList();
|
function formatMoney(val) {
|
||||||
|
const num = Number(val || 0);
|
||||||
|
return num.toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function viewMemberDetail(row) {
|
||||||
|
if (!row || !row.memberId) return;
|
||||||
|
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
selectDate.value = null;
|
||||||
|
searchForm.memberId = "";
|
||||||
|
searchForm.memberName = "";
|
||||||
|
searchForm.rechargeSn = "";
|
||||||
|
searchForm.startDate = "";
|
||||||
|
searchForm.endDate = "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function handleSearch() {
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
searchForm.pageSize = 20;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function selectDateRange(v) {
|
function selectDateRange(v) {
|
||||||
if (v && v.length === 2) {
|
if (v && v.length === 2) {
|
||||||
searchForm.startDate = v[0];
|
searchForm.startDate = v[0];
|
||||||
@@ -145,6 +237,84 @@ function selectDateRange(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
getDataList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.member-recharge-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选卡片 */
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 16px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recharge-money-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,75 +1,123 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-wallet-log-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选表单 -->
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="客户ID" prop="memberId">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.memberId"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="memberName">
|
<el-form-item label="客户名称" prop="memberName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.memberName"
|
v-model="searchForm.memberName"
|
||||||
placeholder="请输入客户名称"
|
placeholder="请输入客户名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="支付时间">
|
<el-form-item label="变动时间">
|
||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="selectDate"
|
v-model="selectDate"
|
||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
clearable
|
clearable
|
||||||
|
range-separator="至"
|
||||||
start-placeholder="开始时间"
|
start-placeholder="开始时间"
|
||||||
end-placeholder="结束时间"
|
end-placeholder="结束时间"
|
||||||
placeholder="选择起始时间"
|
style="width: 320px"
|
||||||
style="width: 360px"
|
|
||||||
@change="selectDateRange"
|
@change="selectDateRange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card>
|
|
||||||
|
<!-- 变动记录表格 -->
|
||||||
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>预存款变动明细</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
ref="table"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
|
stripe
|
||||||
:data="data"
|
:data="data"
|
||||||
ref="table"
|
|
||||||
class="mt_10"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="memberName" label="客户名称" min-width="100" />
|
<!-- 客户ID -->
|
||||||
<el-table-column label="变动金额" width="150">
|
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 客户名称 -->
|
||||||
|
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 变动金额 -->
|
||||||
|
<el-table-column label="变动金额" width="140" align="right" sortable prop="money">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<template v-if="row">
|
||||||
<priceColorScheme
|
<span v-if="row.money > 0" class="money-increase">
|
||||||
v-if="row.money > 0"
|
+¥ {{ formatMoney(row.money) }}
|
||||||
:value="row.money"
|
</span>
|
||||||
color="green"
|
<span v-else-if="row.money < 0" class="money-decrease">
|
||||||
/>
|
-¥ {{ formatMoney(Math.abs(row.money)) }}
|
||||||
<priceColorScheme
|
</span>
|
||||||
v-else-if="row.money < 0"
|
<span v-else class="money-zero">
|
||||||
:value="row.money"
|
¥ 0.00
|
||||||
:color="$mainColor"
|
</span>
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="变更时间" width="200" />
|
|
||||||
<el-table-column label="业务类型" width="200">
|
<!-- 业务类型 -->
|
||||||
|
<el-table-column label="业务类型" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ serviceTypeText(row.serviceType) }}</span>
|
{{ row ? serviceTypeText(row.serviceType) : "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 日志详细说明 -->
|
||||||
|
<el-table-column prop="detail" label="变动说明" min-width="260" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 变动时间 -->
|
||||||
|
<el-table-column prop="createTime" label="变动时间" width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row">
|
||||||
|
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="detail" label="详细" min-width="300" show-overflow-tooltip />
|
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -86,23 +134,42 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useTable } from "@/utils/useTable";
|
import { useTable } from "@/utils/useTable";
|
||||||
import { getUserWallet } from "@/api/member";
|
import { getUserWallet } from "@/api/member";
|
||||||
|
import { customRouterPush } from "@/utils/filters";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "walletLog" });
|
defineOptions({ name: "walletLog" });
|
||||||
|
|
||||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
const table = ref(null);
|
const table = ref(null);
|
||||||
|
|
||||||
const selectDate = ref(null);
|
const selectDate = ref(null);
|
||||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
|
||||||
|
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||||
getUserWallet,
|
getUserWallet,
|
||||||
{
|
{
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
immediate: false,
|
immediate: false,
|
||||||
form: { sort: "createTime", order: "desc", startDate: "", endDate: "", memberName: "" },
|
form: {
|
||||||
|
sort: "createTime",
|
||||||
|
order: "desc",
|
||||||
|
memberId: "",
|
||||||
|
memberName: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
},
|
||||||
|
beforeRequest: (p) => {
|
||||||
|
const params = { ...p };
|
||||||
|
if (selectDate.value && selectDate.value.length === 2) {
|
||||||
|
params.startDate = selectDate.value[0];
|
||||||
|
params.endDate = selectDate.value[1];
|
||||||
|
} else {
|
||||||
|
params.startDate = "";
|
||||||
|
params.endDate = "";
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -113,23 +180,46 @@ function serviceTypeText(type) {
|
|||||||
if (type === "WALLET_RECHARGE") return "余额充值";
|
if (type === "WALLET_RECHARGE") return "余额充值";
|
||||||
return "佣金提成";
|
return "佣金提成";
|
||||||
}
|
}
|
||||||
function init() {
|
|
||||||
getDataList();
|
function getServiceTagType(type) {
|
||||||
|
if (type === "WALLET_RECHARGE") return "success";
|
||||||
|
if (type === "WALLET_REFUND") return "primary";
|
||||||
|
if (type === "WALLET_PAY") return "warning";
|
||||||
|
if (type === "WALLET_WITHDRAWAL") return "danger";
|
||||||
|
return "info";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMoney(val) {
|
||||||
|
const num = Number(val || 0);
|
||||||
|
return num.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewMemberDetail(row) {
|
||||||
|
if (!row || !row.memberId) return;
|
||||||
|
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
selectDate.value = null;
|
||||||
|
searchForm.memberId = "";
|
||||||
|
searchForm.memberName = "";
|
||||||
|
searchForm.startDate = "";
|
||||||
|
searchForm.endDate = "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function handleSearch() {
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
searchForm.pageSize = 20;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function selectDateRange(v) {
|
function selectDateRange(v) {
|
||||||
if (v && v.length === 2) {
|
if (v && v.length === 2) {
|
||||||
searchForm.startDate = v[0];
|
searchForm.startDate = v[0];
|
||||||
@@ -141,6 +231,93 @@ function selectDateRange(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
getDataList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.member-wallet-log-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选卡片 */
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 16px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-increase {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-decrease {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-zero {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,29 +1,52 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-withdraw-apply-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选表单 -->
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="客户ID" prop="memberId">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.memberId"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="memberName">
|
<el-form-item label="客户名称" prop="memberName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.memberName"
|
v-model="searchForm.memberName"
|
||||||
placeholder="请输入客户名称"
|
placeholder="请输入客户名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="申请编号" prop="sn">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.sn"
|
||||||
|
placeholder="请输入申请编号"
|
||||||
|
clearable
|
||||||
|
style="width: 180px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="提现状态" prop="applyStatus">
|
<el-form-item label="提现状态" prop="applyStatus">
|
||||||
<el-select v-model="searchForm.applyStatus" clearable style="width: 240px">
|
<el-select
|
||||||
|
v-model="searchForm.applyStatus"
|
||||||
|
placeholder="全部状态"
|
||||||
|
clearable
|
||||||
|
style="width: 130px"
|
||||||
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
<el-option label="申请中" value="APPLY" />
|
<el-option label="申请中" value="APPLY" />
|
||||||
<el-option label="审核通过" value="VIA_AUDITING" />
|
<el-option label="审核通过" value="VIA_AUDITING" />
|
||||||
|
<el-option label="提现成功" value="SUCCESS" />
|
||||||
<el-option label="用户确认" value="WAIT_USER_CONFIRM" />
|
<el-option label="用户确认" value="WAIT_USER_CONFIRM" />
|
||||||
<el-option label="审核拒绝" value="FAIL_AUDITING" />
|
<el-option label="审核拒绝" value="FAIL_AUDITING" />
|
||||||
<el-option label="提现成功" value="SUCCESS" />
|
|
||||||
<el-option label="提现失败" value="ERROR" />
|
<el-option label="提现失败" value="ERROR" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -33,55 +56,90 @@
|
|||||||
type="datetimerange"
|
type="datetimerange"
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
clearable
|
clearable
|
||||||
|
range-separator="至"
|
||||||
start-placeholder="开始时间"
|
start-placeholder="开始时间"
|
||||||
end-placeholder="结束时间"
|
end-placeholder="结束时间"
|
||||||
placeholder="选择起始时间"
|
style="width: 320px"
|
||||||
style="width: 360px"
|
|
||||||
@change="selectDateRange"
|
@change="selectDateRange"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card>
|
|
||||||
|
<!-- 提现申请数据表格 -->
|
||||||
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>会员提现申请</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
|
ref="table"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
|
stripe
|
||||||
:data="data"
|
:data="data"
|
||||||
ref="table"
|
|
||||||
class="mt_10"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@sort-change="changeSort"
|
@sort-change="changeSort"
|
||||||
@selection-change="changeSelect"
|
|
||||||
>
|
>
|
||||||
<el-table-column prop="sn" label="申请编号" min-width="160" show-overflow-tooltip />
|
<!-- 客户ID -->
|
||||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="申请金额" width="120">
|
|
||||||
|
<!-- 客户名称 -->
|
||||||
|
<el-table-column prop="memberName" label="客户名称" min-width="140" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 申请编号 -->
|
||||||
|
<el-table-column prop="sn" label="申请编号" min-width="180" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 申请提现金额 -->
|
||||||
|
<el-table-column label="申请金额" width="130" align="right" sortable="custom" prop="applyMoney">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme v-if="row" :value="row.applyMoney" :color="$mainColor" />
|
<span v-if="row">¥ {{ formatMoney(row.applyMoney) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="提现状态" width="120">
|
|
||||||
|
<!-- 提现状态 -->
|
||||||
|
<el-table-column label="提现状态" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="applyStatusTagType(row.applyStatus)">
|
{{ row ? applyStatusText(row.applyStatus) : "-" }}
|
||||||
{{ applyStatusText(row.applyStatus) }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="申请时间" width="170" sortable="custom" />
|
|
||||||
<el-table-column prop="inspectTime" label="审核时间" width="170" />
|
<!-- 申请时间 -->
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
<el-table-column prop="createTime" label="申请时间" width="170" align="center" sortable="custom" />
|
||||||
|
|
||||||
|
<!-- 审核时间 -->
|
||||||
|
<el-table-column prop="inspectTime" label="审核时间" width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row" class="ops">
|
<template v-if="row">
|
||||||
<a class="link-text" v-if="row.applyStatus === 'APPLY'" @click="openAudit(row)">审核</a>
|
<a v-if="row.applyStatus === 'APPLY'" class="link-text" @click="openAudit(row)">审核</a>
|
||||||
<a class="link-text" v-else @click="openQuery(row)">查看</a>
|
<a v-else class="link-text" @click="openQuery(row)">查看</a>
|
||||||
</div>
|
<template v-if="row.memberId">
|
||||||
|
<span class="op-split">|</span>
|
||||||
|
<a class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -95,114 +153,170 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 审核弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="roleModalVisible"
|
v-model="roleModalVisible"
|
||||||
:title="modalTitle"
|
title="提现申请审核"
|
||||||
width="500px"
|
width="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-form label-width="80px">
|
<div class="dialog-body">
|
||||||
<el-form-item label="申请编号">
|
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||||
<span>{{ showList.sn }}</span>
|
<el-descriptions-item label="申请编号">
|
||||||
</el-form-item>
|
<span class="font-mono">{{ showList.sn }}</span>
|
||||||
<el-form-item label="用户名称">
|
</el-descriptions-item>
|
||||||
<span>{{ showList.memberName }}</span>
|
<el-descriptions-item label="客户ID">
|
||||||
</el-form-item>
|
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||||
<el-form-item label="申请金额">
|
</el-descriptions-item>
|
||||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
<el-descriptions-item label="客户名称">
|
||||||
</el-form-item>
|
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||||
<el-form-item label="提现状态">
|
</el-descriptions-item>
|
||||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
<el-descriptions-item label="申请金额">
|
||||||
</el-form-item>
|
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||||
<el-form-item label="申请时间">
|
</el-descriptions-item>
|
||||||
<span>{{ showList.createTime }}</span>
|
<el-descriptions-item label="申请时间">
|
||||||
</el-form-item>
|
<span>{{ showList.createTime }}</span>
|
||||||
<el-form-item label="审核备注">
|
</el-descriptions-item>
|
||||||
<el-input v-model="audit" type="textarea" />
|
</el-descriptions>
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
<div class="remark-form-item" style="margin-top: 14px">
|
||||||
|
<div class="remark-label" style="font-size: 13px; color: #606266; margin-bottom: 6px">
|
||||||
|
审核备注说明(拒绝时必填):
|
||||||
|
</div>
|
||||||
|
<el-input
|
||||||
|
v-model="audit"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入审核备注说明..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<template v-if="showList.applyStatus === 'APPLY'">
|
<el-button @click="roleModalVisible = false">取消</el-button>
|
||||||
<el-button @click="submitRole(false)">拒绝</el-button>
|
<el-button
|
||||||
<el-button type="primary" :loading="submitLoading" @click="submitRole(true)">通过</el-button>
|
type="danger"
|
||||||
</template>
|
:loading="submitLoading"
|
||||||
|
@click="submitRole(false)"
|
||||||
|
>
|
||||||
|
拒绝
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="submitLoading"
|
||||||
|
@click="submitRole(true)"
|
||||||
|
>
|
||||||
|
通过
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 查看详情弹窗 -->
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="queryModalVisible"
|
v-model="queryModalVisible"
|
||||||
:title="modalTitle"
|
title="提现申请详情"
|
||||||
width="500px"
|
width="520px"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<el-form label-width="100px">
|
<div class="dialog-body">
|
||||||
<el-form-item label="申请编号:">
|
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||||
<span>{{ showList.sn }}</span>
|
<el-descriptions-item label="申请编号">
|
||||||
</el-form-item>
|
<span class="font-mono">{{ showList.sn }}</span>
|
||||||
<el-form-item label="用户名称:">
|
</el-descriptions-item>
|
||||||
<span>{{ showList.memberName }}</span>
|
<el-descriptions-item label="客户ID">
|
||||||
</el-form-item>
|
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||||
<el-form-item label="申请金额:">
|
</el-descriptions-item>
|
||||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
<el-descriptions-item label="客户名称">
|
||||||
</el-form-item>
|
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||||
<el-form-item label="提现状态:">
|
</el-descriptions-item>
|
||||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
<el-descriptions-item label="申请金额">
|
||||||
</el-form-item>
|
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||||
<el-form-item label="申请时间:">
|
</el-descriptions-item>
|
||||||
<span>{{ showList.createTime }}</span>
|
<el-descriptions-item label="提现状态">
|
||||||
</el-form-item>
|
<el-tag :type="applyStatusTagType(showList.applyStatus)" size="small">
|
||||||
<el-form-item label="审核时间:">
|
{{ paramTypeFilter(showList.applyStatus) }}
|
||||||
<span>{{ showList.inspectTime }}</span>
|
</el-tag>
|
||||||
</el-form-item>
|
</el-descriptions-item>
|
||||||
<el-form-item label="审核备注:">
|
<el-descriptions-item label="申请时间">
|
||||||
<span>{{ showList.inspectRemark || "暂无备注" }}</span>
|
<span>{{ showList.createTime }}</span>
|
||||||
</el-form-item>
|
</el-descriptions-item>
|
||||||
</el-form>
|
<el-descriptions-item label="审核时间">
|
||||||
|
<span>{{ showList.inspectTime || "-" }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="审核备注">
|
||||||
|
<span>{{ showList.inspectRemark || "暂无备注" }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<template v-if="showList.applyStatus === 'APPLY'">
|
<el-button @click="queryModalVisible = false">关 闭</el-button>
|
||||||
<el-button @click="submitRole(false)">拒绝</el-button>
|
|
||||||
<el-button type="primary" :loading="submitLoading" @click="submitRole(true)">通过</el-button>
|
|
||||||
</template>
|
|
||||||
<el-button v-else @click="queryModalVisible = false">取消</el-button>
|
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import { useTable } from "@/utils/useTable";
|
||||||
import { getUserWithdrawApply, withdrawApply } from "@/api/member";
|
import { getUserWithdrawApply, withdrawApply } from "@/api/member";
|
||||||
|
import { customRouterPush } from "@/utils/filters";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "withdrawApply" });
|
defineOptions({ name: "withdrawApply" });
|
||||||
|
|
||||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
const route = useRoute();
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
// 模板 el-table 的 ref
|
|
||||||
const table = ref(null);
|
const table = ref(null);
|
||||||
|
const selectDate = ref(null);
|
||||||
|
|
||||||
const modalTitle = ref("");
|
|
||||||
const loading = ref(true);
|
|
||||||
const audit = ref("");
|
const audit = ref("");
|
||||||
const roleModalVisible = ref(false);
|
const roleModalVisible = ref(false);
|
||||||
const queryModalVisible = ref(false);
|
const queryModalVisible = ref(false);
|
||||||
const searchForm = reactive({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
sort: "createTime",
|
|
||||||
order: "desc",
|
|
||||||
startDate: "",
|
|
||||||
endDate: "",
|
|
||||||
memberName: "",
|
|
||||||
applyStatus: "",
|
|
||||||
});
|
|
||||||
const selectDate = ref(null);
|
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
const selectList = ref([]);
|
|
||||||
const selectCount = ref(0);
|
|
||||||
const showList = ref({});
|
const showList = ref({});
|
||||||
const data = ref([]);
|
|
||||||
const total = ref(0);
|
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||||
|
getUserWithdrawApply,
|
||||||
|
{
|
||||||
|
pageSize: 20,
|
||||||
|
immediate: false,
|
||||||
|
form: {
|
||||||
|
sort: "createTime",
|
||||||
|
order: "desc",
|
||||||
|
memberId: "",
|
||||||
|
memberName: "",
|
||||||
|
sn: "",
|
||||||
|
applyStatus: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
},
|
||||||
|
beforeRequest: (p) => {
|
||||||
|
const params = { ...p };
|
||||||
|
if (selectDate.value && selectDate.value.length === 2) {
|
||||||
|
params.startDate = selectDate.value[0];
|
||||||
|
params.endDate = selectDate.value[1];
|
||||||
|
} else {
|
||||||
|
params.startDate = "";
|
||||||
|
params.endDate = "";
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function formatMoney(val) {
|
||||||
|
const num = Number(val || 0);
|
||||||
|
return num.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewMemberDetail(row) {
|
||||||
|
if (!row || !row.memberId) return;
|
||||||
|
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||||
|
}
|
||||||
|
|
||||||
function paramTypeFilter(val) {
|
function paramTypeFilter(val) {
|
||||||
if (val === "APPLY") return "申请中";
|
if (val === "APPLY") return "申请中";
|
||||||
@@ -211,41 +325,47 @@ function paramTypeFilter(val) {
|
|||||||
if (val === "FAIL_AUDITING") return "审核拒绝";
|
if (val === "FAIL_AUDITING") return "审核拒绝";
|
||||||
if (val === "SUCCESS") return "提现成功";
|
if (val === "SUCCESS") return "提现成功";
|
||||||
if (val === "ERROR") return "提现失败";
|
if (val === "ERROR") return "提现失败";
|
||||||
return "未知状态";
|
return val || "未知状态";
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyStatusText(status) {
|
function applyStatusText(status) {
|
||||||
if (status === "APPLY") return "申请中";
|
if (status === "APPLY") return "申请中";
|
||||||
if (status === "VIA_AUDITING") return "审核通过";
|
if (status === "VIA_AUDITING") return "审核通过";
|
||||||
if (status === "WAIT_USER_CONFIRM") return "用户确认";
|
if (status === "WAIT_USER_CONFIRM") return "用户确认";
|
||||||
if (status === "SUCCESS") return "提现成功";
|
if (status === "SUCCESS") return "提现成功";
|
||||||
if (status === "ERROR") return "提现失败";
|
if (status === "ERROR") return "提现失败";
|
||||||
return "审核拒绝";
|
if (status === "FAIL_AUDITING") return "审核拒绝";
|
||||||
|
return status || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyStatusTagType(status) {
|
function applyStatusTagType(status) {
|
||||||
if (status === "APPLY") return "warning";
|
if (status === "APPLY") return "warning";
|
||||||
if (status === "VIA_AUDITING") return "success";
|
if (status === "VIA_AUDITING") return "success";
|
||||||
if (status === "WAIT_USER_CONFIRM") return "warning";
|
if (status === "WAIT_USER_CONFIRM") return "warning";
|
||||||
if (status === "SUCCESS") return "primary";
|
if (status === "SUCCESS") return "primary";
|
||||||
if (status === "ERROR") return "primary";
|
if (status === "ERROR") return "danger";
|
||||||
return "danger";
|
return "danger";
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAudit(row) {
|
function openAudit(row) {
|
||||||
showList.value = { ...row };
|
showList.value = { ...row };
|
||||||
audit.value = "";
|
audit.value = "";
|
||||||
roleModalVisible.value = true;
|
roleModalVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openQuery(row) {
|
function openQuery(row) {
|
||||||
showList.value = { ...row };
|
showList.value = { ...row };
|
||||||
modalTitle.value = "查看";
|
|
||||||
queryModalVisible.value = true;
|
queryModalVisible.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitRole(res) {
|
function submitRole(res) {
|
||||||
const params = {};
|
const params = {
|
||||||
params.applyId = showList.value.id;
|
applyId: showList.value.id,
|
||||||
params.result = res;
|
result: res,
|
||||||
params.remark = audit.value;
|
remark: audit.value ? audit.value.trim() : "",
|
||||||
if (res === false && params.remark === "") {
|
};
|
||||||
ElMessage.error("审核备注不能为空");
|
if (res === false && !params.remark) {
|
||||||
|
ElMessage.error("审核拒绝时备注说明不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
submitLoading.value = true;
|
submitLoading.value = true;
|
||||||
@@ -265,50 +385,36 @@ function submitRole(res) {
|
|||||||
submitLoading.value = false;
|
submitLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function init() {
|
|
||||||
|
function handleReset() {
|
||||||
|
selectDate.value = null;
|
||||||
|
searchForm.memberId = "";
|
||||||
|
searchForm.memberName = "";
|
||||||
|
searchForm.sn = "";
|
||||||
|
searchForm.applyStatus = "";
|
||||||
|
searchForm.startDate = "";
|
||||||
|
searchForm.endDate = "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSort(e) {
|
||||||
|
searchForm.sort = e.prop || e.key || "createTime";
|
||||||
|
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
clearSelectAll();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getDataList();
|
getDataList();
|
||||||
}
|
}
|
||||||
function handleSearch() {
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
searchForm.pageSize = 20;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function handleReset() {
|
|
||||||
searchFormRef.value.resetFields();
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
searchForm.pageSize = 20;
|
|
||||||
selectDate.value = null;
|
|
||||||
searchForm.startDate = "";
|
|
||||||
searchForm.endDate = "";
|
|
||||||
searchForm.memberName = "";
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function changeSort(e) {
|
|
||||||
searchForm.sort = e.prop || e.key;
|
|
||||||
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
|
||||||
if (!e.order) {
|
|
||||||
searchForm.order = "";
|
|
||||||
}
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function clearSelectAll() {
|
|
||||||
if (table.value) {
|
|
||||||
table.value.clearSelection();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function changeSelect(e) {
|
|
||||||
selectList.value = e;
|
|
||||||
selectCount.value = e.length;
|
|
||||||
}
|
|
||||||
function selectDateRange(v) {
|
function selectDateRange(v) {
|
||||||
if (v && v.length === 2) {
|
if (v && v.length === 2) {
|
||||||
searchForm.startDate = v[0];
|
searchForm.startDate = v[0];
|
||||||
@@ -318,30 +424,97 @@ function selectDateRange(v) {
|
|||||||
searchForm.endDate = "";
|
searchForm.endDate = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getDataList() {
|
|
||||||
loading.value = true;
|
|
||||||
getUserWithdrawApply(searchForm).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
data.value = res.result.records;
|
|
||||||
total.value = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
if (route.query && route.query.applyStatus) {
|
||||||
|
searchForm.applyStatus = route.query.applyStatus;
|
||||||
|
}
|
||||||
|
getDataList();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.ops a {
|
.member-withdraw-apply-page {
|
||||||
color: #2d8cf0;
|
padding: 16px 20px;
|
||||||
cursor: pointer;
|
background-color: #f6f8fa;
|
||||||
text-decoration: none;
|
min-height: calc(100vh - 110px);
|
||||||
}
|
}
|
||||||
.ops span {
|
|
||||||
display: inline-block;
|
.font-mono {
|
||||||
margin: 0 8px;
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
color: #dcdee2;
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选卡片 */
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 16px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-dialog-descriptions {
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 100px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-text {
|
||||||
|
font-size: 15px;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
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
@@ -1,24 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-experience-log-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选表单 -->
|
||||||
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="90px"
|
label-width="84px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<el-form-item label="客户手机号" prop="mobile">
|
<el-form-item label="客户手机号" prop="memberMobile">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.memberMobile"
|
v-model="searchForm.memberMobile"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入客户手机号"
|
placeholder="请输入客户手机号"
|
||||||
style="width: 220px"
|
style="width: 200px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="规则" prop="ruleKey">
|
<el-form-item label="规则类型" prop="ruleKey">
|
||||||
<el-select v-model="searchForm.ruleKey" clearable filterable style="width: 220px">
|
<el-select
|
||||||
|
v-model="searchForm.ruleKey"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="全部规则"
|
||||||
|
style="width: 180px"
|
||||||
|
>
|
||||||
|
<el-option label="全部规则" value="" />
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in ruleOptions"
|
v-for="item in ruleOptions"
|
||||||
:key="item.value"
|
:key="item.value"
|
||||||
@@ -27,42 +35,103 @@
|
|||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<!-- 经验值记录表格 -->
|
||||||
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
<el-table-column label="客户手机号" width="140" show-overflow-tooltip>
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>客户经验值流水</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getData">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
:data="data"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<!-- 客户ID -->
|
||||||
|
<el-table-column prop="memberId" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 客户名称 -->
|
||||||
|
<el-table-column prop="memberName" label="客户名称" min-width="130" show-overflow-tooltip />
|
||||||
|
|
||||||
|
<!-- 客户手机号 -->
|
||||||
|
<el-table-column label="客户手机号" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.memberMobile || row.mobile || "-" }}</span>
|
{{ row ? (row.memberMobile || row.mobile || "-") : "-" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="规则名称" width="150" show-overflow-tooltip>
|
|
||||||
|
<!-- 规则名称 -->
|
||||||
|
<el-table-column label="规则名称" width="130" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.ruleName || findRuleName(row.ruleKey) || "-" }}</span>
|
{{ row ? (row.ruleName || findRuleName(row.ruleKey) || "-") : "-" }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="变化经验值" width="120">
|
|
||||||
|
<!-- 变动前经验值 -->
|
||||||
|
<el-table-column label="变动前经验值" width="120" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ formatExperienceValue(row) }}</span>
|
<span v-if="row">{{ row.beforeExperience != null ? formatNumber(row.beforeExperience) : "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="经验值限额" width="120">
|
|
||||||
|
<!-- 变动经验值 -->
|
||||||
|
<el-table-column label="变动经验值" width="120" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ formatMaxValue(row) }}</span>
|
<span v-if="row" class="experience-increase">
|
||||||
|
+{{ formatNumber(row.variableExperience ?? row.value ?? 0) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 当前经验值 -->
|
||||||
|
<el-table-column label="变动后经验值" width="120" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" class="experience-after">
|
||||||
|
{{ formatNumber(row.experience ?? 0) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作说明 / 备注 -->
|
||||||
<el-table-column label="操作说明" min-width="200" show-overflow-tooltip>
|
<el-table-column label="操作说明" min-width="200" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.content || row.remark || row.description || "-" }}</span>
|
<span>{{ row ? row.content || row.remark || row.description || "-" : "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row">
|
||||||
|
<a v-if="row.memberId" class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -82,8 +151,10 @@
|
|||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
import { useTable } from "@/utils/useTable";
|
import { useTable } from "@/utils/useTable";
|
||||||
import * as API_Member from "@/api/member";
|
import * as API_Member from "@/api/member";
|
||||||
|
import { customRouterPush } from "@/utils/filters";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
const RULE_OPTIONS = [
|
const ruleOptions = [
|
||||||
{ value: "CONSUME", label: "消费" },
|
{ value: "CONSUME", label: "消费" },
|
||||||
{ value: "REGISTER", label: "注册" },
|
{ value: "REGISTER", label: "注册" },
|
||||||
{ value: "SIGN_IN", label: "签到" },
|
{ value: "SIGN_IN", label: "签到" },
|
||||||
@@ -101,7 +172,7 @@ defineOptions({ name: "memberGradeExperienceLog" });
|
|||||||
|
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
|
|
||||||
const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
const { loading, data, total, searchForm, getDataList: getData, resetSearch } = useTable(
|
||||||
API_Member.getMemberExperienceByPage,
|
API_Member.getMemberExperienceByPage,
|
||||||
{
|
{
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -111,45 +182,138 @@ const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
function formatNumber(val) {
|
||||||
init();
|
const num = Number(val || 0);
|
||||||
});
|
return num.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
function formatExperienceValue(row) {
|
|
||||||
const v = row.value ?? row.variableExperience ?? row.experience ?? row.variableValue;
|
|
||||||
return v == null ? "-" : v;
|
|
||||||
}
|
|
||||||
function formatMaxValue(row) {
|
|
||||||
const v = row.maxValue ?? row.maxExperience ?? row.limitValue;
|
|
||||||
return v == null ? "-" : v;
|
|
||||||
}
|
|
||||||
function findRuleName(ruleKey) {
|
function findRuleName(ruleKey) {
|
||||||
const hit = ruleOptions.find((item) => item.value === ruleKey);
|
const hit = ruleOptions.find((item) => item.value === ruleKey);
|
||||||
return hit ? hit.label : "";
|
return hit ? hit.label : ruleKey || "";
|
||||||
}
|
}
|
||||||
function init() {
|
|
||||||
getData();
|
function viewMemberDetail(row) {
|
||||||
|
if (!row || !row.memberId) return;
|
||||||
|
customRouterPush({ name: "member-detail", query: { id: row.memberId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(base = searchForm) {
|
function buildParams(base = searchForm) {
|
||||||
const params = {
|
const params = {
|
||||||
pageNumber: base.pageNumber,
|
pageNumber: base.pageNumber,
|
||||||
pageSize: base.pageSize,
|
pageSize: base.pageSize,
|
||||||
};
|
};
|
||||||
if (base.memberMobile) params.mobile = base.memberMobile;
|
if (base.memberMobile) params.mobile = base.memberMobile.trim();
|
||||||
if (base.ruleKey) params.ruleKey = base.ruleKey;
|
if (base.ruleKey) params.ruleKey = base.ruleKey;
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.memberMobile = "";
|
||||||
|
searchForm.ruleKey = "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getData();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.member-experience-log-page {
|
||||||
|
padding: 16px 20px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 筛选卡片 */
|
||||||
|
.filter-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 16px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格卡片 */
|
||||||
|
.table-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-increase {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.experience-after {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,29 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-group-page search">
|
||||||
<el-card>
|
<!-- 顶部筛选与操作栏 -->
|
||||||
<div class="operation padding-row">
|
<el-card shadow="never" class="filter-card">
|
||||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
<div class="card-toolbar">
|
||||||
|
<el-form inline :model="searchForm" class="filter-form" @keyup.enter="handleSearch">
|
||||||
|
<el-form-item label="分组名称">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.groupName"
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
clearable
|
||||||
|
style="width: 220px"
|
||||||
|
@clear="handleSearch"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<!-- 分组数据表格 -->
|
||||||
<el-table ref="table" v-loading="loading" :data="data" border class="mt_10" style="width: 100%">
|
<el-card shadow="never" class="table-card mt_10">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>会员分组列表</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getData">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
ref="table"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="data"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
class="custom-group-table"
|
||||||
|
>
|
||||||
|
<!-- 分组ID -->
|
||||||
|
<el-table-column prop="id" label="分组ID" min-width="190" />
|
||||||
|
|
||||||
|
<!-- 分组名称 -->
|
||||||
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="groupName" label="分组名称" min-width="160" show-overflow-tooltip />
|
||||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="createTime" label="创建时间" min-width="180" />
|
<!-- 分组人数 -->
|
||||||
<el-table-column prop="updateTime" label="更新时间" min-width="180" />
|
<el-table-column prop="memberCount" label="分组人数" width="110" align="center">
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
<template v-if="row">
|
||||||
|
<a
|
||||||
|
v-if="row.memberCount > 0"
|
||||||
|
class="link-text"
|
||||||
|
@click="viewGroupMembers(row)"
|
||||||
|
>
|
||||||
|
{{ row.memberCount }}
|
||||||
|
</a>
|
||||||
|
<span v-else>{{ row.memberCount ?? 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 分组描述 -->
|
||||||
|
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row ? row.description || "-" : "-" }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 创建时间 -->
|
||||||
|
<el-table-column prop="createTime" label="创建时间" min-width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 更新时间 -->
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" min-width="170" align="center" />
|
||||||
|
|
||||||
|
<!-- 操作栏 -->
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<template v-if="row">
|
||||||
|
<a class="link-text" @click="viewGroupMembers(row)">查看会员</a>
|
||||||
|
<span class="op-split">|</span>
|
||||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
<span class="op-split">|</span>
|
||||||
<a class="link-text" @click="remove(row)">删除</a>
|
<a class="link-text error-text" @click="remove(row)">删除</a>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
<!-- 底部分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -37,34 +110,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-dialog v-model="addFlag" title="添加分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
<!-- 添加分组弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="addFlag"
|
||||||
|
title="添加分组"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<el-form ref="addForm" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
<el-form ref="addForm" :model="formAdd" :rules="rulesAdd" label-width="90px">
|
||||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
<el-form-item label="分组名称" prop="groupName">
|
||||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
<el-input
|
||||||
|
v-model="formAdd.groupName"
|
||||||
|
maxlength="30"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
<el-form-item label="分组描述" prop="description">
|
||||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
<el-input
|
||||||
|
v-model="formAdd.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组描述信息"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="addFlag = false">取消</el-button>
|
<el-button @click="addFlag = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitAddLoading" @click="submitAdd">确定</el-button>
|
<el-button type="primary" :loading="submitAddLoading" @click="submitAdd">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="editFlag" title="编辑分组" width="500px" :close-on-click-modal="false" destroy-on-close>
|
<!-- 编辑分组弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="editFlag"
|
||||||
|
title="编辑分组"
|
||||||
|
width="520px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
<el-form ref="editForm" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
<el-form ref="editForm" :model="formEdit" :rules="rulesEdit" label-width="90px">
|
||||||
<el-input v-model="formEdit.id" style="display: none" />
|
<el-form-item label="分组ID">
|
||||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
<span>{{ formEdit.id }}</span>
|
||||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
<el-form-item label="分组名称" prop="groupName">
|
||||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
<el-input
|
||||||
|
v-model="formEdit.groupName"
|
||||||
|
maxlength="30"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分组描述" prop="description">
|
||||||
|
<el-input
|
||||||
|
v-model="formEdit.description"
|
||||||
|
type="textarea"
|
||||||
|
:rows="3"
|
||||||
|
maxlength="200"
|
||||||
|
show-word-limit
|
||||||
|
placeholder="请输入分组描述信息"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="editFlag = false">取消</el-button>
|
<el-button @click="editFlag = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitEditLoading" @click="submitEdit">确定</el-button>
|
<el-button type="primary" :loading="submitEditLoading" @click="submitEdit">
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,11 +189,21 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
import * as API_Member from "@/api/member.js";
|
import * as API_Member from "@/api/member.js";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "memberGroup" });
|
defineOptions({ name: "memberGroup" });
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function viewGroupMembers(row) {
|
||||||
|
router.push({
|
||||||
|
name: "memberList",
|
||||||
|
query: { groupId: row.id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const addForm = ref(null);
|
const addForm = ref(null);
|
||||||
const editForm = ref(null);
|
const editForm = ref(null);
|
||||||
|
|
||||||
@@ -84,6 +211,7 @@ const loading = ref(true);
|
|||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
groupName: "",
|
||||||
});
|
});
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
@@ -91,44 +219,81 @@ const addFlag = ref(false);
|
|||||||
const editFlag = ref(false);
|
const editFlag = ref(false);
|
||||||
const submitAddLoading = ref(false);
|
const submitAddLoading = ref(false);
|
||||||
const submitEditLoading = ref(false);
|
const submitEditLoading = ref(false);
|
||||||
|
|
||||||
const formAdd = ref({
|
const formAdd = ref({
|
||||||
groupName: "",
|
groupName: "",
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const formEdit = ref({
|
const formEdit = ref({
|
||||||
id: "",
|
id: "",
|
||||||
groupName: "",
|
groupName: "",
|
||||||
description: "",
|
description: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const rulesAdd = reactive({
|
const rulesAdd = reactive({
|
||||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const rulesEdit = reactive({
|
const rulesEdit = reactive({
|
||||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
function init() {
|
function handleSearch() {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
searchForm.groupName = "";
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getData() {
|
function getData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
API_Member.getMemberGroupByPage(searchForm).then((res) => {
|
API_Member.getMemberGroupByPage(searchForm)
|
||||||
loading.value = false;
|
.then((res) => {
|
||||||
if (res && res.success && res.result) {
|
loading.value = false;
|
||||||
data.value = res.result.records || [];
|
if (res && res.success && res.result) {
|
||||||
total.value = res.result.total || 0;
|
const records = res.result.records || [];
|
||||||
}
|
data.value = records;
|
||||||
});
|
total.value = res.result.total || 0;
|
||||||
|
|
||||||
|
// 异步获取各分组的会员总数
|
||||||
|
records.forEach((row) => {
|
||||||
|
if (row.memberCount == null) {
|
||||||
|
API_Member.getMemberGroupUsers(row.id, { pageNumber: 1, pageSize: 1 })
|
||||||
|
.then((userRes) => {
|
||||||
|
if (userRes && userRes.success && userRes.result) {
|
||||||
|
row.memberCount = userRes.result.total || 0;
|
||||||
|
} else {
|
||||||
|
row.memberCount = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
row.memberCount = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAdd() {
|
function openAdd() {
|
||||||
addFlag.value = true;
|
addFlag.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
@@ -136,20 +301,26 @@ function openAdd() {
|
|||||||
formAdd.value = { groupName: "", description: "" };
|
formAdd.value = { groupName: "", description: "" };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitAdd() {
|
function submitAdd() {
|
||||||
addForm.value.validate((valid) => {
|
addForm.value.validate((valid) => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
submitAddLoading.value = true;
|
submitAddLoading.value = true;
|
||||||
API_Member.addMemberGroup(formAdd.value).then((res) => {
|
API_Member.addMemberGroup(formAdd.value)
|
||||||
submitAddLoading.value = false;
|
.then((res) => {
|
||||||
if (res && res.success) {
|
submitAddLoading.value = false;
|
||||||
ElMessage.success("添加成功");
|
if (res && res.success) {
|
||||||
addFlag.value = false;
|
ElMessage.success("添加成功");
|
||||||
getData();
|
addFlag.value = false;
|
||||||
}
|
getData();
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
submitAddLoading.value = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(row) {
|
function openEdit(row) {
|
||||||
editFlag.value = true;
|
editFlag.value = true;
|
||||||
submitEditLoading.value = false;
|
submitEditLoading.value = false;
|
||||||
@@ -169,48 +340,120 @@ function openEdit(row) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitEdit() {
|
function submitEdit() {
|
||||||
editForm.value.validate((valid) => {
|
editForm.value.validate((valid) => {
|
||||||
if (!valid) return;
|
if (!valid) return;
|
||||||
submitEditLoading.value = true;
|
submitEditLoading.value = true;
|
||||||
const { id, groupName, description } = formEdit.value;
|
const { id, groupName, description } = formEdit.value;
|
||||||
API_Member.updateMemberGroup(id, { groupName, description }).then((res) => {
|
API_Member.updateMemberGroup(id, { groupName, description })
|
||||||
submitEditLoading.value = false;
|
.then((res) => {
|
||||||
if (res && res.success) {
|
submitEditLoading.value = false;
|
||||||
ElMessage.success("修改成功");
|
if (res && res.success) {
|
||||||
editFlag.value = false;
|
ElMessage.success("修改成功");
|
||||||
getData();
|
editFlag.value = false;
|
||||||
}
|
getData();
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
submitEditLoading.value = false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove(row) {
|
function remove(row) {
|
||||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "删除确认", {
|
||||||
API_Member.deleteMemberGroup(row.id).then((res) => {
|
type: "warning",
|
||||||
if (res && res.success) {
|
confirmButtonText: "确定删除",
|
||||||
ElMessage.success("删除成功");
|
cancelButtonText: "取消",
|
||||||
getData();
|
})
|
||||||
} else if (res && res.message) {
|
.then(() => {
|
||||||
ElMessage.error(res.message);
|
API_Member.deleteMemberGroup(row.id).then((res) => {
|
||||||
}
|
if (res && res.success) {
|
||||||
});
|
ElMessage.success("删除成功");
|
||||||
}).catch(() => {});
|
getData();
|
||||||
|
} else if (res && res.message) {
|
||||||
|
ElMessage.error(res.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
getData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
.ops a {
|
.member-group-page {
|
||||||
|
.card-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-text {
|
||||||
color: #2d8cf0;
|
color: #2d8cf0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
font-size: 14px;
|
||||||
.ops span {
|
line-height: 1;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #57a3f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error-text {
|
||||||
|
color: #ed4014;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #f16643;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-split {
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
color: #dcdee2;
|
color: #dcdee2;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -48,6 +48,11 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card>
|
<el-card>
|
||||||
|
<el-tabs v-if="!selectedMember" v-model="memberStatus" @tab-change="handleStatusChange">
|
||||||
|
<el-tab-pane label="全部" name="ALL" />
|
||||||
|
<el-tab-pane label="启用" name="OPEN" />
|
||||||
|
<el-tab-pane label="禁用" name="CLOSE" />
|
||||||
|
</el-tabs>
|
||||||
<div v-if="!selectedMember" class="operation padding-row">
|
<div v-if="!selectedMember" class="operation padding-row">
|
||||||
<el-button type="primary" @click="addMember">添加会员</el-button>
|
<el-button type="primary" @click="addMember">添加会员</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -88,6 +93,11 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="nickName" label="会员昵称" min-width="120" show-overflow-tooltip />
|
<el-table-column prop="nickName" label="会员昵称" min-width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column v-if="!selectedMember" label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.disabled ? 'success' : 'info'">{{ row.disabled ? '启用' : '禁用' }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="联系方式" min-width="130">
|
<el-table-column label="联系方式" min-width="130">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.mobile || "" }}</span>
|
<span v-if="row">{{ row.mobile || "" }}</span>
|
||||||
@@ -142,7 +152,7 @@
|
|||||||
<el-dropdown-item command="edit">编辑会员</el-dropdown-item>
|
<el-dropdown-item command="edit">编辑会员</el-dropdown-item>
|
||||||
<el-dropdown-item command="increaseWallet">增加余额</el-dropdown-item>
|
<el-dropdown-item command="increaseWallet">增加余额</el-dropdown-item>
|
||||||
<el-dropdown-item command="updatePoint">修改积分</el-dropdown-item>
|
<el-dropdown-item command="updatePoint">修改积分</el-dropdown-item>
|
||||||
<el-dropdown-item command="disabled">禁用会员</el-dropdown-item>
|
<el-dropdown-item command="disabled">{{ row.disabled ? '禁用会员' : '启用会员' }}</el-dropdown-item>
|
||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
@@ -308,6 +318,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, nextTick, onMounted } from "vue";
|
import { ref, watch, nextTick, onMounted } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import { ArrowDown } from "@element-plus/icons-vue";
|
import { ArrowDown } from "@element-plus/icons-vue";
|
||||||
import multipleMap from "@/components/map/multiple-map";
|
import multipleMap from "@/components/map/multiple-map";
|
||||||
import * as API_Member from "@/api/member.js";
|
import * as API_Member from "@/api/member.js";
|
||||||
@@ -320,6 +331,8 @@ import defaultPic from "@/assets/default.png";
|
|||||||
|
|
||||||
defineOptions({ name: "member" });
|
defineOptions({ name: "member" });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
selectedMember: {
|
selectedMember: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -356,6 +369,7 @@ const addMemberForm = ref({
|
|||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
});
|
});
|
||||||
|
const memberStatus = ref("ALL");
|
||||||
const searchForm = ref({
|
const searchForm = ref({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
@@ -363,7 +377,7 @@ const searchForm = ref({
|
|||||||
id: "",
|
id: "",
|
||||||
username: "",
|
username: "",
|
||||||
mobile: "",
|
mobile: "",
|
||||||
disabled: "OPEN",
|
disabled: props.selectedMember ? "OPEN" : "",
|
||||||
groupId: "",
|
groupId: "",
|
||||||
});
|
});
|
||||||
const picModelFlag = ref(false);
|
const picModelFlag = ref(false);
|
||||||
@@ -511,7 +525,15 @@ function submitSetMemberGroup() {
|
|||||||
const groupId = memberGroupForm.value.groupId;
|
const groupId = memberGroupForm.value.groupId;
|
||||||
memberGroupLoading.value = true;
|
memberGroupLoading.value = true;
|
||||||
Promise.all(
|
Promise.all(
|
||||||
memberIds.map((memberId) => API_Member.setMemberUserGroups(memberId, [groupId]))
|
memberIds.map((memberId) =>
|
||||||
|
API_Member.setMemberUserGroups(memberId, [groupId]).then((res) => {
|
||||||
|
if (!res || !res.success) {
|
||||||
|
// 后端 updateUserGroups 若因原无分组返回失败,自动降级尝试 addMemberGroupUsers
|
||||||
|
return API_Member.addMemberGroupUsers(groupId, [memberId]);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
memberGroupLoading.value = false;
|
memberGroupLoading.value = false;
|
||||||
@@ -575,6 +597,11 @@ function handleSearch() {
|
|||||||
searchForm.value.pageSize = 20;
|
searchForm.value.pageSize = 20;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
function handleStatusChange(status) {
|
||||||
|
searchForm.value.disabled = status === "ALL" ? "" : status;
|
||||||
|
searchForm.value.pageNumber = 1;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
function editPerm(val) {
|
function editPerm(val) {
|
||||||
descTitle.value = `查看用户 ${val.username}`;
|
descTitle.value = `查看用户 ${val.username}`;
|
||||||
descFlag.value = true;
|
descFlag.value = true;
|
||||||
@@ -598,20 +625,27 @@ function getMemberInfo(id) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
let memberListRequestId = 0;
|
||||||
function getData() {
|
function getData() {
|
||||||
|
const requestId = ++memberListRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
if (!props.selectedMember) {
|
if (!props.selectedMember) {
|
||||||
selectedRows.value = [];
|
selectedRows.value = [];
|
||||||
clearSelection();
|
clearSelection();
|
||||||
}
|
}
|
||||||
API_Member.getMemberListData(searchForm.value).then((res) => {
|
API_Member.getMemberListData({ ...searchForm.value })
|
||||||
if (res.result.records) {
|
.then((res) => {
|
||||||
loading.value = false;
|
if (requestId !== memberListRequestId) return;
|
||||||
init(res.result.records);
|
if (res.success && res.result) {
|
||||||
total.value = res.result.total;
|
init(res.result.records || []);
|
||||||
syncTableSelection();
|
total.value = res.result.total || 0;
|
||||||
}
|
syncTableSelection();
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
if (requestId === memberListRequestId) loading.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
function openPicSelector() {
|
function openPicSelector() {
|
||||||
picModelFlag.value = true;
|
picModelFlag.value = true;
|
||||||
@@ -655,14 +689,15 @@ function detail(row) {
|
|||||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||||
}
|
}
|
||||||
function disabled(v) {
|
function disabled(v) {
|
||||||
|
const action = v.disabled ? "禁用" : "启用";
|
||||||
let params = {
|
let params = {
|
||||||
memberIds: [v.id],
|
memberIds: [v.id],
|
||||||
disabled: false,
|
disabled: !v.disabled,
|
||||||
};
|
};
|
||||||
ElMessageBox.confirm("确认禁用此会员?", "提示", { type: "warning" }).then(() => {
|
ElMessageBox.confirm(`确认${action}此会员?`, "提示", { type: "warning" }).then(() => {
|
||||||
API_Member.updateMemberStatus(params).then((res) => {
|
API_Member.updateMemberStatus(params).then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
ElMessage.success("禁用成功");
|
ElMessage.success(`${action}成功`);
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -758,11 +793,25 @@ function handleSubmitModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (route.query && route.query.groupId) {
|
||||||
|
searchForm.value.groupId = String(route.query.groupId);
|
||||||
|
}
|
||||||
getData();
|
getData();
|
||||||
loadMemberGroupList();
|
loadMemberGroupList();
|
||||||
loadMemberGradeList();
|
loadMemberGradeList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.groupId,
|
||||||
|
(val) => {
|
||||||
|
if (val !== undefined) {
|
||||||
|
searchForm.value.groupId = val ? String(val) : "";
|
||||||
|
searchForm.value.pageNumber = 1;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 外部通过 ref 调用(promotions/coupon-activity/coupon-publish.vue 的 memberLayoutRef.getSelection())
|
// 外部通过 ref 调用(promotions/coupon-activity/coupon-publish.vue 的 memberLayoutRef.getSelection())
|
||||||
defineExpose({ getSelection });
|
defineExpose({ getSelection });
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,323 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search">
|
|
||||||
<el-card>
|
|
||||||
<el-form
|
|
||||||
ref="searchFormRef"
|
|
||||||
:model="searchForm"
|
|
||||||
inline
|
|
||||||
label-width="70px"
|
|
||||||
class="search-form"
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
>
|
|
||||||
<el-form-item label="客户ID" prop="id">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.id"
|
|
||||||
placeholder="请输入客户ID"
|
|
||||||
clearable
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="客户名称" prop="username">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.username"
|
|
||||||
placeholder="请输入客户名称"
|
|
||||||
clearable
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="联系方式" prop="mobile">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.mobile"
|
|
||||||
placeholder="请输入会员联系方式"
|
|
||||||
clearable
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card>
|
|
||||||
<el-table
|
|
||||||
ref="table"
|
|
||||||
v-loading="loading"
|
|
||||||
border
|
|
||||||
:data="data"
|
|
||||||
class="mt_10"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-table-column prop="id" label="客户ID" min-width="120" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="username" label="客户名称" min-width="150" align="left" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="nickName" label="昵称" min-width="120" align="left" show-overflow-tooltip />
|
|
||||||
<el-table-column label="联系方式" min-width="130">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">{{ row.mobile || "" }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="createTime" label="注册时间" min-width="180" />
|
|
||||||
<el-table-column label="积分数量" min-width="120" align="left">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">{{ row.point == void 0 ? "0" : row.point }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div v-if="row" class="ops" style="display: flex; justify-content: center">
|
|
||||||
<template v-if="selectedMember">
|
|
||||||
<a class="link-text" @click="callback(row)">选择</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
</template>
|
|
||||||
<a class="link-text" @click="detail(row)">查看</a>
|
|
||||||
<template v-if="!selectedMember">
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="enable(row)">启用</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="editPerm(row)">编辑</a>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="searchForm.pageNumber"
|
|
||||||
v-model:page-size="searchForm.pageSize"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
:total="total"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
@current-change="changePage"
|
|
||||||
@size-change="changePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-dialog v-model="descFlag" :title="descTitle" width="500px" destroy-on-close>
|
|
||||||
<el-form ref="formValidateRef" :model="formValidate" :rules="ruleValidate" label-width="80px">
|
|
||||||
<el-form-item label="头像">
|
|
||||||
<img alt="会员头像" :src="formValidate.face" class="face" />
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
link
|
|
||||||
class="upload"
|
|
||||||
@click="openPicSelector"
|
|
||||||
>修改</el-button>
|
|
||||||
<input type="file" style="display: none" id="file" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="客户名称" prop="name">
|
|
||||||
<el-input v-model="formValidate.username" style="width: 200px" disabled />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="用户昵称" prop="name">
|
|
||||||
<el-input v-model="formValidate.nickName" style="width: 200px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="性别" prop="sex">
|
|
||||||
<el-radio-group v-model="formValidate.sex">
|
|
||||||
<el-radio-button :value="1">男</el-radio-button>
|
|
||||||
<el-radio-button :value="0">女</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="修改密码" prop="password">
|
|
||||||
<el-input v-model="formValidate.newPassword" type="password" show-password style="width: 220px" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="生日" prop="birthday">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="formValidate.birthday"
|
|
||||||
type="date"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
style="width: 220px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="所在地" prop="mail">
|
|
||||||
{{ formValidate.region || "暂无地址" }}
|
|
||||||
<el-button style="margin-left: 10px" @click="$refs.map.open()">选择</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="descFlag = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="handleSubmitModal">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-dialog v-model="picModelFlag" width="1200px">
|
|
||||||
<ossManage
|
|
||||||
ref="ossManageRef"
|
|
||||||
@callback="callbackSelected"
|
|
||||||
:isComponent="true"
|
|
||||||
:initialize="picModelFlag"
|
|
||||||
/>
|
|
||||||
</el-dialog>
|
|
||||||
<multipleMap ref="map" @callback="selectedRegion" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, nextTick, onMounted } from "vue";
|
|
||||||
import * as API_Member from "@/api/member.js";
|
|
||||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
|
||||||
import multipleMap from "@/components/map/multiple-map";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import { customRouterPush } from "@/utils/filters";
|
|
||||||
import { md5 } from "@/utils/md5.js";
|
|
||||||
|
|
||||||
defineOptions({ name: "memberRecycle" });
|
|
||||||
|
|
||||||
const emit = defineEmits(["callback"]);
|
|
||||||
|
|
||||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
|
||||||
const searchFormRef = ref(null);
|
|
||||||
const table = ref(null);
|
|
||||||
// 模板编辑弹窗 el-form 的 ref(原 ref="formValidate",与 data 同名会互相覆盖,改名为 formValidateRef)
|
|
||||||
const formValidateRef = ref(null);
|
|
||||||
// 模板 ossManage 组件的 ref(原 ref="ossManage",与组件 import 同名会冲突,改名为 ossManageRef)
|
|
||||||
const ossManageRef = ref(null);
|
|
||||||
|
|
||||||
const selectedMember = ref(false);
|
|
||||||
const descTitle = ref("");
|
|
||||||
const descFlag = ref(false);
|
|
||||||
const openSearch = ref(true);
|
|
||||||
const loading = ref(true);
|
|
||||||
const searchForm = ref({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
order: "desc",
|
|
||||||
id: "",
|
|
||||||
username: "",
|
|
||||||
mobile: "",
|
|
||||||
disabled: "CLOSE",
|
|
||||||
});
|
|
||||||
const picModelFlag = ref(false);
|
|
||||||
const formValidate = ref({});
|
|
||||||
const ruleValidate = ref({});
|
|
||||||
const data = ref([]);
|
|
||||||
const total = ref(0);
|
|
||||||
|
|
||||||
function callback(val) {
|
|
||||||
emit("callback", val);
|
|
||||||
}
|
|
||||||
function init() {
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
function changePage() {
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
function changePageSize() {
|
|
||||||
searchForm.value.pageNumber = 1;
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
function handleSearch() {
|
|
||||||
searchForm.value.pageNumber = 1;
|
|
||||||
searchForm.value.pageSize = 20;
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
function editPerm(val) {
|
|
||||||
descTitle.value = `查看用户 ${val.username}`;
|
|
||||||
descFlag.value = true;
|
|
||||||
getMemberInfo(val.id);
|
|
||||||
}
|
|
||||||
function getMemberInfo(id) {
|
|
||||||
API_Member.getMemberInfoData(id).then((res) => {
|
|
||||||
if (res.result) {
|
|
||||||
formValidate.value = res.result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getData() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Member.getMemberListData(searchForm.value).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
data.value = res.result.records;
|
|
||||||
total.value = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function openPicSelector() {
|
|
||||||
picModelFlag.value = true;
|
|
||||||
nextTick(() => {
|
|
||||||
if (ossManageRef.value) {
|
|
||||||
ossManageRef.value.selectImage = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function callbackSelected(val) {
|
|
||||||
picModelFlag.value = false;
|
|
||||||
formValidate.value.face = val.url;
|
|
||||||
}
|
|
||||||
function selectedRegion(val) {
|
|
||||||
if (val.type === "select") {
|
|
||||||
const paths = val.data.map((item) => item.name).join(",");
|
|
||||||
const ids = val.data.map((item) => item.id).join(",");
|
|
||||||
formValidate.value.region = paths;
|
|
||||||
formValidate.value.regionId = ids;
|
|
||||||
} else {
|
|
||||||
formValidate.value.region = val.data.addr;
|
|
||||||
formValidate.value.regionId = val.data.addrId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function detail(row) {
|
|
||||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
|
||||||
}
|
|
||||||
function enable(v) {
|
|
||||||
let params = {
|
|
||||||
memberIds: [v.id],
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
ElMessageBox.confirm("确定启用此会员?", "提示", { type: "warning" }).then(() => {
|
|
||||||
API_Member.updateMemberStatus(params).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("启用成功");
|
|
||||||
getData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function handleSubmitModal() {
|
|
||||||
const { nickName, sex, username, face, newPassword, id, regionId, region } = formValidate.value;
|
|
||||||
let time = new Date(formValidate.value.birthday);
|
|
||||||
let birthday = time.getFullYear() + "-" + (time.getMonth() + 1) + "-" + time.getDate();
|
|
||||||
let submit = {
|
|
||||||
regionId: regionId,
|
|
||||||
region: region,
|
|
||||||
nickName,
|
|
||||||
sex,
|
|
||||||
birthday,
|
|
||||||
face: face || "",
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
if (newPassword) {
|
|
||||||
// 原 this.md5 为 main.js 注册的全局属性,改为具名导入(同 login.vue / userManage.vue)
|
|
||||||
submit.password = md5(newPassword);
|
|
||||||
}
|
|
||||||
API_Member.updateMember(submit).then((res) => {
|
|
||||||
if (res.result) {
|
|
||||||
ElMessage.success("修改成功!");
|
|
||||||
descFlag.value = false;
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.face {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
.ops a {
|
|
||||||
color: #2d8cf0;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.ops span {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 8px;
|
|
||||||
color: #dcdee2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-dialog v-model="wechatModal" title="微信设置" width="530px">
|
|
||||||
<el-form ref="wechatFormRef" :model="wechatFormData" label-width="100px">
|
|
||||||
<el-form-item v-if="tab === 'WECHAT'" label="模板名称">
|
|
||||||
<el-input v-model="wechatFormData.name" maxlength="9" disabled />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="tab === 'WECHAT'" label="头部信息" prop="first">
|
|
||||||
<el-input v-model="wechatFormData.first" maxlength="50" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="tab === 'WECHAT'" label="备注" prop="remark">
|
|
||||||
<el-input v-model="wechatFormData.remark" type="textarea" :rows="5" maxlength="150" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="是否开启" prop="enable">
|
|
||||||
<el-switch v-model="wechatFormData.enable" active-text="开启" inactive-text="关闭" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button v-if="tab === 'WECHAT'" type="primary" @click="wechatFormDataEdit">保存</el-button>
|
|
||||||
<el-button v-else type="primary" @click="wechatMPFormDataEdit">保存</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<el-card>
|
|
||||||
<el-tabs v-model="tab" @tab-click="tabPaneChange">
|
|
||||||
<el-tab-pane label="微信消息" name="WECHAT">
|
|
||||||
<div class="search">
|
|
||||||
<div class="operation mt_10">
|
|
||||||
<el-button type="primary" @click="weChatSync">初始化微信消息</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" border :data="weChatData" ref="weChatTable" style="width: 100%">
|
|
||||||
<el-table-column prop="code" label="模板编号" width="500" sortable />
|
|
||||||
<el-table-column label="是否开启" width="150" sortable>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">{{ row.enable ? "开启" : "关闭" }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="name" label="模板名称" width="200" sortable />
|
|
||||||
<el-table-column prop="createTime" label="创建时间" sortable />
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<a class="link-text" @click="wechatSettingAlert(row)">编辑</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="delWeChat(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="weChatSearchForm.pageNumber"
|
|
||||||
v-model:page-size="weChatSearchForm.pageSize"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
:total="weChatTotal"
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
size="small"
|
|
||||||
@current-change="changePage"
|
|
||||||
@size-change="changePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
|
|
||||||
<el-tab-pane label="微信小程序订阅消息" name="WECHATMP">
|
|
||||||
<div class="search">
|
|
||||||
<div class="operation mt_10">
|
|
||||||
<el-button type="primary" @click="weChatSync('mp')">初始化微信小程序订阅消息</el-button>
|
|
||||||
</div>
|
|
||||||
<el-table v-loading="loading" border :data="weChatMPData" ref="weChatMPTable" style="width: 100%">
|
|
||||||
<el-table-column prop="code" label="模板编号" width="500" sortable />
|
|
||||||
<el-table-column label="是否开启" width="150" sortable>
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">{{ row.enable ? "开启" : "关闭" }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="name" label="模板名称" width="200" sortable />
|
|
||||||
<el-table-column prop="createTime" label="创建时间" sortable />
|
|
||||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<a class="link-text" @click="wechatSettingAlert(row)">编辑</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="delWeChat(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="weChatMPSearchForm.pageNumber"
|
|
||||||
v-model:page-size="weChatMPSearchForm.pageSize"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
:total="weChatMPTotal"
|
|
||||||
layout="total, sizes, prev, pager, next"
|
|
||||||
size="small"
|
|
||||||
@current-change="changePage"
|
|
||||||
@size-change="changePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, reactive, ref } from "vue";
|
|
||||||
import {
|
|
||||||
wechatMessageSync,
|
|
||||||
getWechatMessagePage as fetchWechatMessagePage,
|
|
||||||
editWechatMessageTemplate,
|
|
||||||
delWechatMessageTemplate,
|
|
||||||
wechatMPMessageSync,
|
|
||||||
getWechatMPMessagePage as fetchWechatMPMessagePage,
|
|
||||||
editWechatMPMessageTemplate,
|
|
||||||
delWechatMPMessageTemplate,
|
|
||||||
} from "@/api/setting";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
|
|
||||||
defineOptions({ title: "wechat-message-manage" });
|
|
||||||
|
|
||||||
const wechatFormRef = ref(null);
|
|
||||||
|
|
||||||
const wechatModal = ref(false);
|
|
||||||
const wechatFormData = ref({});
|
|
||||||
const wechatMPFormData = ref({});
|
|
||||||
const tab = ref("WECHAT");
|
|
||||||
const searchForm = reactive({ type: "WECHAT" });
|
|
||||||
const loading = ref(true);
|
|
||||||
const id = ref("");
|
|
||||||
const weChatSearchForm = reactive({ pageNumber: 1, pageSize: 20 });
|
|
||||||
const weChatMPSearchForm = reactive({ pageNumber: 1, pageSize: 20 });
|
|
||||||
const weChatData = ref([]);
|
|
||||||
const weChatMPData = ref([]);
|
|
||||||
const weChatTotal = ref(0);
|
|
||||||
const weChatMPTotal = ref(0);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function changePage(v) {
|
|
||||||
searchForm.type = tab.value;
|
|
||||||
if (tab.value === "WECHAT") {
|
|
||||||
weChatSearchForm.pageNumber = v;
|
|
||||||
getWechatMessagePage();
|
|
||||||
} else {
|
|
||||||
weChatMPSearchForm.pageNumber = v;
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function changePageSize(v) {
|
|
||||||
searchForm.type = tab.value;
|
|
||||||
if (tab.value === "WECHAT") {
|
|
||||||
weChatSearchForm.pageSize = v;
|
|
||||||
getWechatMessagePage();
|
|
||||||
} else {
|
|
||||||
weChatMPSearchForm.pageSize = v;
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function wechatSettingAlert(v) {
|
|
||||||
wechatFormData.value = v;
|
|
||||||
id.value = v.id;
|
|
||||||
wechatModal.value = true;
|
|
||||||
}
|
|
||||||
function weChatSync(mp) {
|
|
||||||
ElMessageBox.confirm("确认要初始化微信小程序消息订阅?", "提示", { type: "warning" }).then(() => {
|
|
||||||
if (mp === "mp") {
|
|
||||||
wechatMPMessageSync().then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("微信小程序消息订阅初始化");
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
wechatMessageSync().then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("微信消息模板初始化成功");
|
|
||||||
getWechatMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function wechatFormDataEdit() {
|
|
||||||
wechatFormRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
if (!wechatFormData.value.updateTime) {
|
|
||||||
wechatFormData.value.updateTime = "";
|
|
||||||
}
|
|
||||||
editWechatMessageTemplate(id.value, wechatFormData.value).then((res) => {
|
|
||||||
if (res.message === "success") {
|
|
||||||
ElMessage.success("微信模板修改成功");
|
|
||||||
wechatModal.value = false;
|
|
||||||
getWechatMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function wechatMPFormDataEdit() {
|
|
||||||
wechatFormRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
editWechatMPMessageTemplate(id.value, wechatMPFormData.value).then((res) => {
|
|
||||||
if (res.message === "success") {
|
|
||||||
ElMessage.success("微信消息订阅模板修改成功");
|
|
||||||
wechatModal.value = false;
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function delWeChat(v) {
|
|
||||||
ElMessageBox.confirm("确定删除此模板?", "提示", { type: "warning" }).then(() => {
|
|
||||||
if (tab.value === "WECHAT") {
|
|
||||||
delWechatMessageTemplate(v.id).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("微信模板删除成功");
|
|
||||||
getWechatMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
delWechatMPMessageTemplate(v.id).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("微信消息订阅删除成功");
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function getDataList() {
|
|
||||||
getWechatMessagePage();
|
|
||||||
}
|
|
||||||
function getWechatMessagePage() {
|
|
||||||
loading.value = true;
|
|
||||||
fetchWechatMessagePage(weChatSearchForm).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
weChatData.value = res.result.records;
|
|
||||||
weChatTotal.value = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function getWechatMPMessagePage() {
|
|
||||||
loading.value = true;
|
|
||||||
fetchWechatMPMessagePage(weChatMPSearchForm).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
weChatMPData.value = res.result.records;
|
|
||||||
weChatMPTotal.value = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function tabPaneChange(tabPane) {
|
|
||||||
const v = tabPane.paneName || tab.value;
|
|
||||||
searchForm.type = v;
|
|
||||||
if (v === "WECHAT") {
|
|
||||||
getWechatMessagePage();
|
|
||||||
} else if (v === "WECHATMP") {
|
|
||||||
getWechatMPMessagePage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.link-text {
|
|
||||||
color: #2d8cf0;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.op-split {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 8px;
|
|
||||||
color: #dcdee2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
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>
|
||||||
@@ -1,37 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="member-point-page search">
|
||||||
<el-card class="points-statistics-card">
|
<!-- 顶部数据统计概览看板 -->
|
||||||
<el-row type="flex" justify="space-around" align="middle" class="points-statistics">
|
<el-card shadow="never" class="points-statistics-card">
|
||||||
|
<el-row :gutter="24" class="points-statistics">
|
||||||
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
||||||
<div class="points-statistics-title">已发放积分数</div>
|
<div class="stat-inner-box">
|
||||||
<div class="points-statistics-subtitle">历史累计发放积分数</div>
|
<div class="points-statistics-title">已发放积分数</div>
|
||||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
<div class="points-statistics-subtitle">平台历史累计发放积分总量</div>
|
||||||
|
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
||||||
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
||||||
<div class="points-statistics-title">未使用积分数</div>
|
<div class="stat-inner-box">
|
||||||
<div class="points-statistics-subtitle">会员账户未使用积分数</div>
|
<div class="points-statistics-title">未使用积分数</div>
|
||||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
<div class="points-statistics-subtitle">会员账户当前剩余未使用积分总量</div>
|
||||||
|
<div class="points-statistics-value text-warning">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
||||||
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 选项卡与数据主区域 -->
|
||||||
<div class="point-tabs-wrap">
|
<div class="point-tabs-wrap">
|
||||||
<el-tabs v-model="activeTab" class="point-tabs">
|
<el-tabs v-model="activeTab" class="point-tabs">
|
||||||
<el-tab-pane label="积分列表" name="pointList">
|
<!-- 选项卡 1:会员积分列表 -->
|
||||||
<el-card class="point-content-card">
|
<el-tab-pane label="会员积分列表" name="pointList">
|
||||||
|
<!-- 筛选条件 -->
|
||||||
|
<el-card shadow="never" class="point-filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="memberSearchFormRef"
|
ref="memberSearchFormRef"
|
||||||
:model="memberSearchForm"
|
:model="memberSearchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleMemberSearch"
|
@keyup.enter="handleMemberSearch"
|
||||||
>
|
>
|
||||||
|
<el-form-item label="客户ID" prop="id">
|
||||||
|
<el-input
|
||||||
|
v-model="memberSearchForm.id"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 190px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="客户名称" prop="nickName">
|
<el-form-item label="客户名称" prop="nickName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="memberSearchForm.nickName"
|
v-model="memberSearchForm.nickName"
|
||||||
placeholder="请输入客户名称"
|
placeholder="请输入客户名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 180px"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="客户账号" prop="username">
|
<el-form-item label="客户账号" prop="username">
|
||||||
@@ -39,82 +56,128 @@
|
|||||||
v-model="memberSearchForm.username"
|
v-model="memberSearchForm.username"
|
||||||
placeholder="请输入客户账号"
|
placeholder="请输入客户账号"
|
||||||
clearable
|
clearable
|
||||||
style="width: 180px"
|
style="width: 160px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="账号状态" prop="disabled">
|
<el-form-item label="账号状态" prop="disabled">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="memberSearchForm.disabled"
|
v-model="memberSearchForm.disabled"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请选择账号状态"
|
placeholder="全部状态"
|
||||||
style="width: 160px"
|
style="width: 130px"
|
||||||
>
|
>
|
||||||
|
<el-option label="全部" value="" />
|
||||||
<el-option label="启用" value="OPEN" />
|
<el-option label="启用" value="OPEN" />
|
||||||
<el-option label="禁用" value="CLOSE" />
|
<el-option label="禁用" value="CLOSE" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="积分值">
|
<el-form-item label="积分区间">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="memberSearchForm.minPoint"
|
v-model="memberSearchForm.minPoint"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
placeholder="最小积分值"
|
placeholder="最小"
|
||||||
style="width: 140px"
|
controls-position="right"
|
||||||
|
style="width: 120px"
|
||||||
/>
|
/>
|
||||||
<span class="point-range-separator">-</span>
|
<span class="point-range-separator">-</span>
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="memberSearchForm.maxPoint"
|
v-model="memberSearchForm.maxPoint"
|
||||||
:min="0"
|
:min="0"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
placeholder="最大积分值"
|
placeholder="最大"
|
||||||
style="width: 140px"
|
controls-position="right"
|
||||||
|
style="width: 120px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button class="search-btn" type="primary" @click="handleMemberSearch">搜索</el-button>
|
<el-button class="search-btn" type="primary" @click="handleMemberSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleMemberReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="point-content-card member-list-card">
|
|
||||||
|
<!-- 会员积分表格 -->
|
||||||
|
<el-card shadow="never" class="point-content-card member-list-card mt_10">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-title">用户列表</div>
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>会员积分信息</span>
|
||||||
|
<span class="total-badge">共 {{ memberTotal }} 位会员</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getMemberList">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="memberLoading"
|
v-loading="memberLoading"
|
||||||
border
|
border
|
||||||
|
stripe
|
||||||
:data="memberData"
|
:data="memberData"
|
||||||
class="member-table"
|
class="custom-point-table"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
|
<!-- 客户ID -->
|
||||||
|
<el-table-column label="客户ID" min-width="170">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" class="font-mono text-dark">{{ row.id || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 客户名称 -->
|
||||||
<el-table-column label="客户名称" min-width="160" show-overflow-tooltip>
|
<el-table-column label="客户名称" min-width="160" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.nickName || "-" }}</span>
|
<span v-if="row" class="font-semibold text-dark">{{ row.nickName || "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="客户账号" min-width="260" show-overflow-tooltip>
|
|
||||||
|
<!-- 客户账号 -->
|
||||||
|
<el-table-column label="客户账号" min-width="180" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.username || "-" }}</span>
|
<span v-if="row" class="font-mono">{{ row.username || "-" }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="账号状态" min-width="120" align="center">
|
|
||||||
|
<!-- 账号状态 -->
|
||||||
|
<el-table-column label="账号状态" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row" :type="row.disabled === true ? 'success' : 'info'">
|
<el-tag
|
||||||
|
v-if="row"
|
||||||
|
:type="row.disabled === true ? 'success' : 'info'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
{{ row.disabled === true ? "启用" : "禁用" }}
|
{{ row.disabled === true ? "启用" : "禁用" }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="积分余额" min-width="120" align="center">
|
|
||||||
|
<!-- 积分余额 -->
|
||||||
|
<el-table-column label="积分余额" width="130" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ row.point == null ? 0 : row.point }}</span>
|
<span v-if="row" class="point-num-text">
|
||||||
|
{{ formatNumber(row.point == null ? 0 : row.point) }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
|
||||||
|
<!-- 操作 -->
|
||||||
|
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<a v-if="row" class="link-text" @click="detail(row)">详情</a>
|
<template v-if="row">
|
||||||
|
<a class="link-text" @click="detail(row)">客户详情</a>
|
||||||
|
<span class="op-split">|</span>
|
||||||
|
<a class="link-text" @click="viewMemberHistory(row)">变动明细</a>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<el-pagination
|
<el-pagination
|
||||||
v-model:current-page="memberSearchForm.pageNumber"
|
v-model:current-page="memberSearchForm.pageNumber"
|
||||||
v-model:page-size="memberSearchForm.pageSize"
|
v-model:page-size="memberSearchForm.pageSize"
|
||||||
@@ -128,63 +191,140 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 选项卡 2:积分增减记录 -->
|
||||||
<el-tab-pane label="积分增减记录" name="pointChangeRecord">
|
<el-tab-pane label="积分增减记录" name="pointChangeRecord">
|
||||||
<el-card class="point-content-card">
|
<!-- 筛选条件 -->
|
||||||
|
<el-card shadow="never" class="point-filter-card">
|
||||||
<el-form
|
<el-form
|
||||||
ref="searchFormRef"
|
ref="searchFormRef"
|
||||||
:model="searchForm"
|
:model="searchForm"
|
||||||
inline
|
inline
|
||||||
label-width="70px"
|
label-width="76px"
|
||||||
class="search-form"
|
class="search-form"
|
||||||
@keyup.enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<el-form-item label="会员名称" prop="username">
|
<el-form-item label="客户ID" prop="memberId">
|
||||||
|
<el-input
|
||||||
|
v-model="searchForm.memberId"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="会员名称" prop="memberName">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchForm.memberName"
|
v-model="searchForm.memberName"
|
||||||
placeholder="请输入会员名称"
|
placeholder="请输入会员名称"
|
||||||
clearable
|
clearable
|
||||||
style="width: 240px"
|
style="width: 180px"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item class="form-actions">
|
||||||
<el-button class="search-btn" type="primary" @click="handleSearch">搜索</el-button>
|
<el-button class="search-btn" type="primary" @click="handleSearch">
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleHistoryReset">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-card>
|
</el-card>
|
||||||
<el-card class="point-content-card">
|
|
||||||
|
<!-- 积分记录表格 -->
|
||||||
|
<el-card shadow="never" class="point-content-card mt_10">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header-bar">
|
||||||
|
<div class="header-title">
|
||||||
|
<span>积分流水记录</span>
|
||||||
|
<span class="total-badge">共 {{ total }} 条明细</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button size="small" @click="getData">刷新</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
|
stripe
|
||||||
:data="data"
|
:data="data"
|
||||||
ref="table"
|
ref="table"
|
||||||
class="mt_10 point-table"
|
class="custom-point-table"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="memberName" label="会员名称" min-width="120" show-overflow-tooltip />
|
<!-- 客户ID -->
|
||||||
<el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip />
|
<el-table-column label="客户ID" min-width="170">
|
||||||
<el-table-column prop="beforePoint" label="之前积分" width="200" />
|
<template #default="{ row }">
|
||||||
<el-table-column label="变动积分" width="200">
|
<span v-if="row" class="font-mono text-dark">{{ row.memberId || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 会员名称 -->
|
||||||
|
<el-table-column prop="memberName" label="会员名称" min-width="140" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" class="font-semibold text-dark">{{ row.memberName || "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作内容/原因 -->
|
||||||
|
<el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row ? row.content || "-" : "-" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 变动前积分 -->
|
||||||
|
<el-table-column label="变动前积分" width="130" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row">{{ formatNumber(row.beforePoint) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 变动积分 -->
|
||||||
|
<el-table-column label="变动积分" width="130" align="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<template v-if="row">
|
<template v-if="row">
|
||||||
<priceColorScheme
|
<el-tag
|
||||||
v-if="row.pointType === 'INCREASE'"
|
v-if="row.pointType === 'INCREASE'"
|
||||||
:value="row.variablePoint"
|
type="success"
|
||||||
color="green"
|
size="small"
|
||||||
unit="+"
|
effect="light"
|
||||||
/>
|
class="point-change-tag"
|
||||||
<priceColorScheme
|
>
|
||||||
|
+{{ formatNumber(row.variablePoint) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
v-else
|
v-else
|
||||||
:value="row.variablePoint"
|
type="danger"
|
||||||
:color="$mainColor"
|
size="small"
|
||||||
unit="-"
|
effect="light"
|
||||||
/>
|
class="point-change-tag"
|
||||||
|
>
|
||||||
|
-{{ formatNumber(row.variablePoint) }}
|
||||||
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="point" label="当前积分" width="200" />
|
|
||||||
<el-table-column prop="createTime" label="操作时间" width="200" />
|
<!-- 变动后积分 -->
|
||||||
|
<el-table-column label="变动后积分" width="130" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" class="point-num-text">
|
||||||
|
{{ formatNumber(row.point) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<!-- 操作时间 -->
|
||||||
|
<el-table-column prop="createTime" label="操作时间" width="170" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row" class="time-text">{{ row.createTime }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrap mt_10">
|
||||||
<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"
|
||||||
@@ -207,12 +347,12 @@
|
|||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
import * as API_Member from "@/api/member.js";
|
import * as API_Member from "@/api/member.js";
|
||||||
import { customRouterPush } from "@/utils/filters";
|
import { customRouterPush } from "@/utils/filters";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
|
||||||
defineOptions({ name: "point" });
|
defineOptions({ name: "point" });
|
||||||
|
|
||||||
const emit = defineEmits(["callback"]);
|
const emit = defineEmits(["callback"]);
|
||||||
|
|
||||||
// 模板 ref,script 内未使用
|
|
||||||
const memberSearchFormRef = ref(null);
|
const memberSearchFormRef = ref(null);
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
const table = ref(null);
|
const table = ref(null);
|
||||||
@@ -224,35 +364,60 @@ const pointsStatistics = ref({
|
|||||||
totalPoint: 0,
|
totalPoint: 0,
|
||||||
unUsedPoint: 0,
|
unUsedPoint: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
memberId: "",
|
||||||
|
memberName: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const memberSearchForm = reactive({
|
const memberSearchForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
id: "",
|
||||||
nickName: "",
|
nickName: "",
|
||||||
username: "",
|
username: "",
|
||||||
disabled: "",
|
disabled: "",
|
||||||
minPoint: null,
|
minPoint: null,
|
||||||
maxPoint: null,
|
maxPoint: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = ref([]);
|
const data = ref([]);
|
||||||
const memberData = ref([]);
|
const memberData = ref([]);
|
||||||
const memberTotal = ref(0);
|
const memberTotal = ref(0);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
|
||||||
function callback(val) {
|
function copyText(text, label = "客户ID") {
|
||||||
emit("callback", val);
|
if (!text) return;
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(String(text))
|
||||||
|
.then(() => ElMessage.success(`已复制${label}到剪贴板`))
|
||||||
|
.catch(() => ElMessage.info(String(text)));
|
||||||
|
} else {
|
||||||
|
ElMessage.info(String(text));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function detail(row) {
|
function detail(row) {
|
||||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function viewMemberHistory(row) {
|
||||||
|
activeTab.value = "pointChangeRecord";
|
||||||
|
searchForm.memberId = row.id;
|
||||||
|
searchForm.memberName = row.nickName || row.username || "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
getStatistics();
|
getStatistics();
|
||||||
getMemberList();
|
getMemberList();
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatistics() {
|
function getStatistics() {
|
||||||
API_Member.queryMemberPointsStatistics().then((res) => {
|
API_Member.queryMemberPointsStatistics().then((res) => {
|
||||||
if (res && res.success && res.result) {
|
if (res && res.success && res.result) {
|
||||||
@@ -263,45 +428,71 @@ function getStatistics() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(value) {
|
function formatNumber(value) {
|
||||||
const numericValue = Number(value || 0);
|
const numericValue = Number(value || 0);
|
||||||
if (!Number.isFinite(numericValue)) return "0";
|
if (!Number.isFinite(numericValue)) return "0";
|
||||||
return numericValue.toLocaleString();
|
return numericValue.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePage(v) {
|
function changePage(v) {
|
||||||
searchForm.pageNumber = v;
|
searchForm.pageNumber = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePageSize(v) {
|
function changePageSize(v) {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = v;
|
searchForm.pageSize = v;
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
searchForm.pageNumber = 1;
|
searchForm.pageNumber = 1;
|
||||||
searchForm.pageSize = 20;
|
|
||||||
getData();
|
getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleHistoryReset() {
|
||||||
|
searchForm.memberId = "";
|
||||||
|
searchForm.memberName = "";
|
||||||
|
searchForm.pageNumber = 1;
|
||||||
|
getData();
|
||||||
|
}
|
||||||
|
|
||||||
function handleMemberSearch() {
|
function handleMemberSearch() {
|
||||||
memberSearchForm.pageNumber = 1;
|
memberSearchForm.pageNumber = 1;
|
||||||
getMemberList();
|
getMemberList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleMemberReset() {
|
||||||
|
memberSearchForm.id = "";
|
||||||
|
memberSearchForm.nickName = "";
|
||||||
|
memberSearchForm.username = "";
|
||||||
|
memberSearchForm.disabled = "";
|
||||||
|
memberSearchForm.minPoint = null;
|
||||||
|
memberSearchForm.maxPoint = null;
|
||||||
|
memberSearchForm.pageNumber = 1;
|
||||||
|
getMemberList();
|
||||||
|
}
|
||||||
|
|
||||||
function changeMemberPage(v) {
|
function changeMemberPage(v) {
|
||||||
memberSearchForm.pageNumber = v;
|
memberSearchForm.pageNumber = v;
|
||||||
getMemberList();
|
getMemberList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMemberPageSize(v) {
|
function changeMemberPageSize(v) {
|
||||||
memberSearchForm.pageNumber = 1;
|
memberSearchForm.pageNumber = 1;
|
||||||
memberSearchForm.pageSize = v;
|
memberSearchForm.pageSize = v;
|
||||||
getMemberList();
|
getMemberList();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMemberList() {
|
function getMemberList() {
|
||||||
memberLoading.value = true;
|
memberLoading.value = true;
|
||||||
const params = {
|
const params = {
|
||||||
pageNumber: memberSearchForm.pageNumber,
|
pageNumber: memberSearchForm.pageNumber,
|
||||||
pageSize: memberSearchForm.pageSize,
|
pageSize: memberSearchForm.pageSize,
|
||||||
nickName: memberSearchForm.nickName || undefined,
|
id: memberSearchForm.id ? memberSearchForm.id.trim() : undefined,
|
||||||
username: memberSearchForm.username || undefined,
|
nickName: memberSearchForm.nickName ? memberSearchForm.nickName.trim() : undefined,
|
||||||
|
username: memberSearchForm.username ? memberSearchForm.username.trim() : undefined,
|
||||||
disabled: memberSearchForm.disabled || undefined,
|
disabled: memberSearchForm.disabled || undefined,
|
||||||
minPoint: memberSearchForm.minPoint,
|
minPoint: memberSearchForm.minPoint,
|
||||||
maxPoint: memberSearchForm.maxPoint,
|
maxPoint: memberSearchForm.maxPoint,
|
||||||
@@ -318,31 +509,111 @@ function getMemberList() {
|
|||||||
memberLoading.value = false;
|
memberLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getData() {
|
function getData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
API_Member.getHistoryPointData(searchForm).then((res) => {
|
const params = {
|
||||||
loading.value = false;
|
pageNumber: searchForm.pageNumber,
|
||||||
if (res.success) {
|
pageSize: searchForm.pageSize,
|
||||||
data.value = res.result.records;
|
memberId: searchForm.memberId ? searchForm.memberId.trim() : undefined,
|
||||||
total.value = res.result.total;
|
memberName: searchForm.memberName ? searchForm.memberName.trim() : undefined,
|
||||||
}
|
};
|
||||||
});
|
API_Member.getHistoryPointData(params)
|
||||||
|
.then((res) => {
|
||||||
|
loading.value = false;
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
data.value = res.result.records || [];
|
||||||
|
total.value = res.result.total || 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.points-statistics-card {
|
.member-point-page {
|
||||||
margin-bottom: 10px;
|
padding: 16px 20px;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-mono {
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-dark {
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-warning {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 顶部统计卡片 */
|
||||||
|
.points-statistics-card {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-statistics {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.points-statistics-item {
|
||||||
|
padding: 6px 0;
|
||||||
|
|
||||||
|
.stat-inner-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fbfbfc;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #f2f3f5;
|
||||||
|
|
||||||
|
.points-statistics-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-statistics-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
line-height: 18px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-statistics-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f56c6c;
|
||||||
|
line-height: 30px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选项卡容器 */
|
||||||
.point-tabs-wrap {
|
.point-tabs-wrap {
|
||||||
padding: 12px 16px 16px;
|
padding: 14px 18px 18px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(23, 35, 61, 0.04);
|
border: 1px solid #ebeef5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-tabs {
|
.point-tabs {
|
||||||
@@ -352,107 +623,108 @@ onMounted(() => {
|
|||||||
|
|
||||||
:deep(.el-tabs__item) {
|
:deep(.el-tabs__item) {
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
color: #515a6e;
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-tabs__item.is-active) {
|
:deep(.el-tabs__item.is-active) {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-tabs__active-bar) {
|
|
||||||
height: 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.points-statistics {
|
/* 筛选表单 */
|
||||||
width: 100%;
|
.point-filter-card {
|
||||||
}
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
background-color: #fafbfe;
|
||||||
|
|
||||||
.points-statistics-item {
|
:deep(.el-card__body) {
|
||||||
padding: 10px 0;
|
padding: 16px 16px 2px;
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.points-statistics-title {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #17233d;
|
|
||||||
line-height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.points-statistics-subtitle {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #808695;
|
|
||||||
line-height: 18px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.points-statistics-value {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #fa6419;
|
|
||||||
line-height: 26px;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.point-content-card {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-list-card {
|
|
||||||
:deep(.el-card__header) {
|
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #17233d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-table {
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
:deep(.el-table__header th.el-table__cell) {
|
|
||||||
background: #fafafa;
|
|
||||||
padding: 14px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #17233d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__header .cell) {
|
.search-form {
|
||||||
padding: 0 16px;
|
display: flex;
|
||||||
line-height: 22px;
|
flex-wrap: wrap;
|
||||||
}
|
align-items: center;
|
||||||
|
|
||||||
:deep(.el-table__body td.el-table__cell) {
|
.el-form-item {
|
||||||
padding: 12px 0;
|
margin-bottom: 14px;
|
||||||
}
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-table__body .cell) {
|
.form-actions {
|
||||||
padding: 0 16px;
|
margin-left: auto;
|
||||||
line-height: 22px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-range-separator {
|
.point-range-separator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin: 0 8px;
|
margin: 0 6px;
|
||||||
color: #808695;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-text {
|
/* 表格区域卡片 */
|
||||||
color: #409eff;
|
.point-content-card {
|
||||||
cursor: pointer;
|
border-radius: 6px;
|
||||||
text-decoration: none;
|
border: 1px solid #ebeef5;
|
||||||
|
|
||||||
|
.card-header-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
.total-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.face {
|
/* 表格单元格 */
|
||||||
width: 60px;
|
.custom-point-table {
|
||||||
height: 60px;
|
.id-cell {
|
||||||
border-radius: 50%;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-num-text {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-change-tag {
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,235 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="search">
|
|
||||||
<el-card>
|
|
||||||
<el-form
|
|
||||||
ref="searchFormRef"
|
|
||||||
:model="searchForm"
|
|
||||||
inline
|
|
||||||
label-width="70px"
|
|
||||||
class="search-form"
|
|
||||||
@keyup.enter="handleSearch"
|
|
||||||
>
|
|
||||||
<el-form-item label="订单号" prop="orderSn">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.orderSn"
|
|
||||||
placeholder="请输入订单号"
|
|
||||||
clearable
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="会员名称" prop="buyerName">
|
|
||||||
<el-input
|
|
||||||
v-model="searchForm.buyerName"
|
|
||||||
placeholder="请输入会员名称"
|
|
||||||
clearable
|
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="下单时间">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="selectDate"
|
|
||||||
type="datetimerange"
|
|
||||||
value-format="YYYY-MM-DD"
|
|
||||||
clearable
|
|
||||||
start-placeholder="开始时间"
|
|
||||||
end-placeholder="结束时间"
|
|
||||||
placeholder="选择起始时间"
|
|
||||||
style="width: 240px"
|
|
||||||
@change="selectDateRange"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card>
|
|
||||||
<div class="order-tab">
|
|
||||||
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
|
|
||||||
<el-tab-pane
|
|
||||||
v-for="(item, index) in orderStatus"
|
|
||||||
:key="index"
|
|
||||||
:label="item.title"
|
|
||||||
:name="item.value"
|
|
||||||
/>
|
|
||||||
</el-tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
ref="tableRef"
|
|
||||||
v-loading="loading"
|
|
||||||
border
|
|
||||||
:data="data"
|
|
||||||
class="mt_10"
|
|
||||||
style="width: 100%"
|
|
||||||
@sort-change="changeSort"
|
|
||||||
>
|
|
||||||
<el-table-column prop="sn" label="订单号" min-width="230" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="createTime" label="下单时间" width="200" />
|
|
||||||
<el-table-column label="订单来源" width="95">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row">
|
|
||||||
<span v-if="row.clientType == 'H5'">移动端</span>
|
|
||||||
<span v-else-if="row.clientType == 'PC'">PC端</span>
|
|
||||||
<span v-else-if="row.clientType == 'WECHAT_MP'">小程序端</span>
|
|
||||||
<span v-else-if="row.clientType == 'APP'">移动应用端</span>
|
|
||||||
<span v-else>{{ row.clientType }}</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="memberName" label="买家名称" width="130" />
|
|
||||||
<el-table-column label="订单金额" prop="flowPrice" min-width="120" sortable="custom">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<priceColorScheme v-if="row" :value="row.flowPrice" :color="$mainColor" />
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="订单状态" width="95">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<el-tag v-if="row.orderStatus == 'UNPAID'" type="danger">未付款</el-tag>
|
|
||||||
<el-tag v-else-if="row.orderStatus == 'PAID'" type="primary">已付款</el-tag>
|
|
||||||
<el-tag v-else-if="row.orderStatus == 'COMPLETED'" type="success">已完成</el-tag>
|
|
||||||
<el-tag v-else-if="row.orderStatus == 'TAKE'" type="warning">待核验</el-tag>
|
|
||||||
<el-tag v-else-if="row.orderStatus == 'CANCELLED'" type="info">已关闭</el-tag>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<template v-if="row">
|
|
||||||
<a
|
|
||||||
class="link-text"
|
|
||||||
:class="{ disabled: row.orderStatus != 'UNPAID' }"
|
|
||||||
@click="row.orderStatus == 'UNPAID' && confirmPrice(row)"
|
|
||||||
>收款</a>
|
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="detail(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="searchForm.pageNumber"
|
|
||||||
v-model:page-size="searchForm.pageSize"
|
|
||||||
:page-sizes="[20, 50, 100]"
|
|
||||||
:total="total"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
size="small"
|
|
||||||
@current-change="changePage"
|
|
||||||
@size-change="changePageSize"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, reactive, ref } from "vue";
|
|
||||||
import * as API_Order from "@/api/order";
|
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
|
||||||
import { customRouterPush } from "@/utils/filters";
|
|
||||||
|
|
||||||
defineOptions({ name: "fictitiousOrderList" });
|
|
||||||
|
|
||||||
const searchFormRef = ref(null);
|
|
||||||
const tableRef = ref(null);
|
|
||||||
|
|
||||||
const loading = ref(true);
|
|
||||||
const searchForm = reactive({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
sort: "",
|
|
||||||
order: "",
|
|
||||||
startDate: "",
|
|
||||||
endDate: "",
|
|
||||||
orderType: "VIRTUAL",
|
|
||||||
orderSn: "",
|
|
||||||
buyerName: "",
|
|
||||||
orderStatus: "",
|
|
||||||
});
|
|
||||||
const selectDate = ref(null);
|
|
||||||
const data = ref([]);
|
|
||||||
const total = ref(0);
|
|
||||||
const orderStatus = ref([
|
|
||||||
{ title: "全部", value: "" },
|
|
||||||
{ title: "未付款", value: "UNPAID" },
|
|
||||||
{ title: "已付款", value: "PAID" },
|
|
||||||
{ title: "待核验", value: "TAKE" },
|
|
||||||
{ title: "已完成", value: "COMPLETED" },
|
|
||||||
{ title: "已关闭", value: "CANCELLED" },
|
|
||||||
]);
|
|
||||||
const currentStatus = ref("");
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function changePage() {
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function changePageSize() {
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function handleSearch() {
|
|
||||||
searchForm.pageNumber = 1;
|
|
||||||
searchForm.pageSize = 20;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function changeSort(e) {
|
|
||||||
searchForm.sort = e.prop;
|
|
||||||
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
function selectDateRange(v) {
|
|
||||||
if (v) {
|
|
||||||
searchForm.startDate = v[0];
|
|
||||||
searchForm.endDate = v[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getDataList() {
|
|
||||||
loading.value = true;
|
|
||||||
API_Order.getOrderList(searchForm).then((res) => {
|
|
||||||
loading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
data.value = res.result.records;
|
|
||||||
total.value = res.result.total;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function confirmPrice(v) {
|
|
||||||
ElMessageBox.confirm("您确定要收款吗?", "确认收款", { type: "warning" }).then(() => {
|
|
||||||
API_Order.orderPay(v.sn).then((res) => {
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success("收款成功");
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
function detail(v) {
|
|
||||||
let sn = v.sn;
|
|
||||||
customRouterPush({
|
|
||||||
name: "order-detail",
|
|
||||||
query: { sn: sn, orderType: v.orderType },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function onStatusTabClick(tab) {
|
|
||||||
const item = tab.paneName;
|
|
||||||
currentStatus.value = item;
|
|
||||||
searchForm.orderStatus = item;
|
|
||||||
getDataList();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.order-tab {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|
||||||
@@ -150,14 +150,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref, watch } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
import * as API_Order from "@/api/order";
|
import * as API_Order from "@/api/order";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||||
|
|
||||||
const QUERY_KEY = "manager-order-list-query";
|
|
||||||
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" },
|
||||||
@@ -196,42 +202,16 @@ function emptySearchForm(pageSize = 20) {
|
|||||||
consigneeMobile: "",
|
consigneeMobile: "",
|
||||||
storeName: "",
|
storeName: "",
|
||||||
orderStatus: "",
|
orderStatus: "",
|
||||||
paymentMethod: "",
|
paymentMethod: props.fixedPaymentMethod,
|
||||||
orderPromotionType: "",
|
orderPromotionType: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSavedQuery() {
|
|
||||||
const raw = sessionStorage.getItem(QUERY_KEY);
|
|
||||||
if (!raw) return null;
|
|
||||||
try {
|
|
||||||
const saved = JSON.parse(raw);
|
|
||||||
const form = emptySearchForm();
|
|
||||||
const src = saved && saved.searchForm ? saved.searchForm : {};
|
|
||||||
Object.keys(form).forEach((key) => {
|
|
||||||
if (src[key] !== undefined && src[key] !== null) {
|
|
||||||
form[key] = src[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
form.pageNumber = Number(form.pageNumber) || 1;
|
|
||||||
form.pageSize = PAGE_SIZES.includes(Number(form.pageSize)) ? Number(form.pageSize) : 20;
|
|
||||||
const currentStatus = saved.currentStatus || form.orderStatus || "ALL";
|
|
||||||
if (!currentStatus || currentStatus === "ALL") {
|
|
||||||
form.orderStatus = "";
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
searchForm: form,
|
|
||||||
selectDate: Array.isArray(saved.selectDate) ? saved.selectDate : null,
|
|
||||||
currentStatus: currentStatus || "ALL",
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
sessionStorage.removeItem(QUERY_KEY);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
defineOptions({ name: "orderList" });
|
defineOptions({ name: "orderList" });
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
const searchFormRef = ref(null);
|
const searchFormRef = ref(null);
|
||||||
const tableRef = ref(null);
|
const tableRef = ref(null);
|
||||||
|
|
||||||
@@ -248,6 +228,25 @@ const total = ref(0);
|
|||||||
const orderNumData = ref({});
|
const orderNumData = ref({});
|
||||||
const currentStatus = ref("ALL");
|
const currentStatus = ref("ALL");
|
||||||
|
|
||||||
|
function applyRouteQuery() {
|
||||||
|
const q = route.query;
|
||||||
|
if (!q) return;
|
||||||
|
if (q.orderSn) searchForm.value.orderSn = String(q.orderSn);
|
||||||
|
if (q.buyerName) searchForm.value.buyerName = String(q.buyerName);
|
||||||
|
if (q.goodsName) searchForm.value.goodsName = String(q.goodsName);
|
||||||
|
if (q.storeName) searchForm.value.storeName = String(q.storeName);
|
||||||
|
if (q.orderType) searchForm.value.orderType = String(q.orderType);
|
||||||
|
if (q.orderStatus) {
|
||||||
|
searchForm.value.orderStatus = String(q.orderStatus);
|
||||||
|
currentStatus.value = String(q.orderStatus);
|
||||||
|
}
|
||||||
|
if (q.startDate && q.endDate) {
|
||||||
|
searchForm.value.startDate = String(q.startDate);
|
||||||
|
searchForm.value.endDate = String(q.endDate);
|
||||||
|
selectDate.value = [String(q.startDate), String(q.endDate)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const orderStatusWithCount = computed(() => {
|
const orderStatusWithCount = computed(() => {
|
||||||
const count = (value) => (value ? `(${value})` : "");
|
const count = (value) => (value ? `(${value})` : "");
|
||||||
return [
|
return [
|
||||||
@@ -340,23 +339,6 @@ function orderStatusTagType(v) {
|
|||||||
};
|
};
|
||||||
return map[v] || "info";
|
return map[v] || "info";
|
||||||
}
|
}
|
||||||
function rememberQuery() {
|
|
||||||
sessionStorage.setItem(
|
|
||||||
QUERY_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
searchForm: searchForm.value,
|
|
||||||
selectDate: selectDate.value,
|
|
||||||
currentStatus: currentStatus.value,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function restoreQuery() {
|
|
||||||
const saved = readSavedQuery();
|
|
||||||
if (!saved) return;
|
|
||||||
searchForm.value = saved.searchForm;
|
|
||||||
selectDate.value = saved.selectDate;
|
|
||||||
currentStatus.value = saved.currentStatus;
|
|
||||||
}
|
|
||||||
function init() {
|
function init() {
|
||||||
getDataList();
|
getDataList();
|
||||||
getOrderNumData();
|
getOrderNumData();
|
||||||
@@ -380,7 +362,6 @@ function handleReset() {
|
|||||||
searchForm.value = emptySearchForm(pageSize);
|
searchForm.value = emptySearchForm(pageSize);
|
||||||
selectDate.value = null;
|
selectDate.value = null;
|
||||||
currentStatus.value = "ALL";
|
currentStatus.value = "ALL";
|
||||||
sessionStorage.removeItem(QUERY_KEY);
|
|
||||||
init();
|
init();
|
||||||
}
|
}
|
||||||
function selectDateRange(v) {
|
function selectDateRange(v) {
|
||||||
@@ -393,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;
|
||||||
@@ -401,10 +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;
|
||||||
rememberQuery();
|
API_Order.getOrderList(listParams())
|
||||||
API_Order.getOrderList(searchForm.value)
|
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
data.value = res.result.records;
|
data.value = res.result.records;
|
||||||
@@ -422,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(
|
||||||
@@ -434,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",
|
||||||
@@ -478,10 +465,24 @@ function getOrderNumData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// created
|
watch(
|
||||||
restoreQuery();
|
() => route.query,
|
||||||
|
(newQuery) => {
|
||||||
|
if (newQuery && Object.keys(newQuery).length > 0) {
|
||||||
|
applyRouteQuery();
|
||||||
|
searchForm.value.pageNumber = 1;
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
try {
|
||||||
|
sessionStorage.removeItem("manager-order-list-query");
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
applyRouteQuery();
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
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>
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<div class="hotzone-preview-box" v-if="options.imageUrl">
|
<div class="hotzone-preview-box" v-if="options.imageUrl">
|
||||||
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
||||||
<div class="preview-actions-bar">
|
<div class="preview-actions-bar">
|
||||||
|
<el-button size="small" type="primary" @click="handleOpenHotzone">
|
||||||
|
<el-icon class="mr-4"><Aim /></el-icon>
|
||||||
|
绘制热区
|
||||||
|
</el-button>
|
||||||
<el-button size="small" type="primary" plain @click="handleSelectImage">
|
<el-button size="small" type="primary" plain @click="handleSelectImage">
|
||||||
<el-icon class="mr-4"><Picture /></el-icon>
|
<el-icon class="mr-4"><Picture /></el-icon>
|
||||||
更换底图
|
更换底图
|
||||||
@@ -46,6 +50,9 @@
|
|||||||
<div class="section-title flex-between">
|
<div class="section-title flex-between">
|
||||||
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
||||||
<div class="header-action-group">
|
<div class="header-action-group">
|
||||||
|
<el-button type="primary" plain size="small" @click="handleOpenHotzone">
|
||||||
|
<el-icon class="mr-4"><Aim /></el-icon> 绘制热区
|
||||||
|
</el-button>
|
||||||
<el-button link type="primary" size="small" @click="handleAddZone">
|
<el-button link type="primary" size="small" @click="handleAddZone">
|
||||||
<el-icon><Plus /></el-icon> 添加热区
|
<el-icon><Plus /></el-icon> 添加热区
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -204,16 +211,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="empty-zones-box" v-else>
|
<div class="empty-zones-box" v-else>
|
||||||
<span>暂无热区,请点击右上角“添加热区”</span>
|
<span>暂无热区,可点击“绘制热区”在底图上拖拽画热区</span>
|
||||||
|
<div class="mt-8">
|
||||||
|
<el-button type="primary" size="small" @click="handleOpenHotzone">
|
||||||
|
<el-icon class="mr-4"><Aim /></el-icon> 立即绘制热区
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 绘制热区弹窗(复用移动端热区绘制器) -->
|
||||||
|
<hotzone ref="hotzoneDialogRef" width="960px" @changeZone="handleChangeZone" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { inject } from 'vue';
|
import { ref, inject } from 'vue';
|
||||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
import { Plus, Delete, Link, Picture, Close, Top, Bottom, Aim } from '@element-plus/icons-vue';
|
||||||
import { ElMessageBox } from 'element-plus';
|
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||||
|
import hotzone from '@/components/hotzone';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
component: {
|
component: {
|
||||||
@@ -298,6 +314,68 @@ function handleAddZone() {
|
|||||||
height: 35
|
height: 35
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hotzoneDialogRef = ref(null);
|
||||||
|
|
||||||
|
// 绘制热区弹窗打开
|
||||||
|
function handleOpenHotzone() {
|
||||||
|
const imgUrl = props.options.imageUrl || '';
|
||||||
|
if (!imgUrl) {
|
||||||
|
ElMessage.warning('请先输入或选择热区底图');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const zoneInfo = (props.options.zones || []).map((z, idx) => {
|
||||||
|
const leftPer = z.leftPer !== undefined ? z.leftPer : (Number(z.left) || 0) / 100;
|
||||||
|
const topPer = z.topPer !== undefined ? z.topPer : (Number(z.top) || 0) / 100;
|
||||||
|
const widthPer = z.widthPer !== undefined ? z.widthPer : (Number(z.width) || 20) / 100;
|
||||||
|
const heightPer = z.heightPer !== undefined ? z.heightPer : (Number(z.height) || 20) / 100;
|
||||||
|
const link = z.url || z.link || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...z,
|
||||||
|
leftPer,
|
||||||
|
topPer,
|
||||||
|
widthPer,
|
||||||
|
heightPer,
|
||||||
|
link,
|
||||||
|
title: z.title || `热区 ${idx + 1}`,
|
||||||
|
type: z.type || 'other'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hotzoneDialogRef.value) {
|
||||||
|
hotzoneDialogRef.value.open({
|
||||||
|
img: imgUrl,
|
||||||
|
zoneInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 热区绘制器变更回调
|
||||||
|
function handleChangeZone(zoneList) {
|
||||||
|
if (!Array.isArray(zoneList)) return;
|
||||||
|
props.options.zones = zoneList.map((z, idx) => {
|
||||||
|
const leftVal = z.leftPer !== undefined ? z.leftPer * 100 : (z.left !== undefined ? z.left : 0);
|
||||||
|
const topVal = z.topPer !== undefined ? z.topPer * 100 : (z.top !== undefined ? z.top : 0);
|
||||||
|
const widthVal = z.widthPer !== undefined ? z.widthPer * 100 : (z.width !== undefined ? z.width : 20);
|
||||||
|
const heightVal = z.heightPer !== undefined ? z.heightPer * 100 : (z.height !== undefined ? z.height : 20);
|
||||||
|
const linkVal = z.link || z.url || '';
|
||||||
|
const titleVal = z.title || `热区 ${idx + 1}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...z,
|
||||||
|
title: titleVal,
|
||||||
|
url: linkVal,
|
||||||
|
link: linkVal,
|
||||||
|
left: Math.round(leftVal * 10) / 10,
|
||||||
|
top: Math.round(topVal * 10) / 10,
|
||||||
|
width: Math.round(widthVal * 10) / 10,
|
||||||
|
height: Math.round(heightVal * 10) / 10,
|
||||||
|
type: z.type || 'other'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'channelGrid',
|
type: 'channelGrid',
|
||||||
defaultRowHeight: 120,
|
defaultRowHeight: '',
|
||||||
name: '频道宫格',
|
name: '频道宫格',
|
||||||
group: 'navigation',
|
group: 'navigation',
|
||||||
icon: 'Grid',
|
icon: 'Grid',
|
||||||
@@ -431,7 +431,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'brandWall',
|
type: 'brandWall',
|
||||||
defaultRowHeight: 220,
|
defaultRowHeight: '',
|
||||||
name: '品牌图',
|
name: '品牌图',
|
||||||
group: 'media',
|
group: 'media',
|
||||||
icon: 'Medal',
|
icon: 'Medal',
|
||||||
@@ -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: {
|
||||||
@@ -513,7 +513,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
|
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
|
||||||
height: 320,
|
height: 450,
|
||||||
|
|
||||||
elements: []
|
elements: []
|
||||||
|
|
||||||
@@ -532,7 +532,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'promoGroup',
|
type: 'promoGroup',
|
||||||
defaultRowHeight: 180,
|
defaultRowHeight: 220,
|
||||||
|
|
||||||
name: '活动卡组',
|
name: '活动卡组',
|
||||||
|
|
||||||
@@ -567,13 +567,14 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'video',
|
type: 'video',
|
||||||
defaultRowHeight: 360,
|
defaultRowHeight: 450,
|
||||||
name: '视频',
|
name: '视频',
|
||||||
group: 'media',
|
group: 'media',
|
||||||
icon: 'VideoCamera',
|
icon: 'VideoCamera',
|
||||||
description: '品牌宣传与商品视频播放',
|
description: '品牌宣传与商品视频播放',
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
videoUrl: '',
|
videoUrl: '',
|
||||||
|
height: 450,
|
||||||
coverUrl: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png',
|
coverUrl: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png',
|
||||||
url: '',
|
url: '',
|
||||||
autoPlay: false,
|
autoPlay: false,
|
||||||
@@ -597,7 +598,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'goodsList',
|
type: 'goodsList',
|
||||||
defaultRowHeight: 980,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '商品列表',
|
name: '商品列表',
|
||||||
|
|
||||||
@@ -727,7 +728,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'gridFloor',
|
type: 'gridFloor',
|
||||||
defaultRowHeight: 560,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '宫格楼层',
|
name: '宫格楼层',
|
||||||
|
|
||||||
@@ -843,7 +844,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'goodsGrid',
|
type: 'goodsGrid',
|
||||||
defaultRowHeight: 420,
|
defaultRowHeight: '',
|
||||||
name: '商品宫格',
|
name: '商品宫格',
|
||||||
group: 'commerce',
|
group: 'commerce',
|
||||||
icon: 'Menu',
|
icon: 'Menu',
|
||||||
@@ -922,7 +923,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'bannerFloor',
|
type: 'bannerFloor',
|
||||||
defaultRowHeight: 560,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '横幅楼层',
|
name: '横幅楼层',
|
||||||
|
|
||||||
@@ -964,7 +965,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
{
|
{
|
||||||
|
|
||||||
type: 'goodsBar',
|
type: 'goodsBar',
|
||||||
defaultRowHeight: 280,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '商品条',
|
name: '商品条',
|
||||||
|
|
||||||
@@ -1001,7 +1002,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
{type: 'channelFloor',
|
{type: 'channelFloor',
|
||||||
defaultRowHeight: 620,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '频道楼层',
|
name: '频道楼层',
|
||||||
|
|
||||||
@@ -1045,7 +1046,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
type: 'recommendFeed',
|
type: 'recommendFeed',
|
||||||
defaultRowHeight: 1270,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '推荐商品',
|
name: '推荐商品',
|
||||||
|
|
||||||
@@ -1120,7 +1121,7 @@ export const COMPONENT_DEFINITIONS = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'couponList',
|
type: 'couponList',
|
||||||
defaultRowHeight: 160,
|
defaultRowHeight: '',
|
||||||
|
|
||||||
name: '优惠券',
|
name: '优惠券',
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@
|
|||||||
|
|
||||||
:class="{ 'is-row-selected': isCurrentRowSelected(row.id) }"
|
:class="{ 'is-row-selected': isCurrentRowSelected(row.id) }"
|
||||||
|
|
||||||
:style="getRowWrapperStyle(row.rowStyle)"
|
:style="getRowWrapperStyle(row.rowStyle, row.components)"
|
||||||
|
|
||||||
@click.stop="handleSelectRow(row, rIndex)"
|
@click.stop="handleSelectRow(row, rIndex)"
|
||||||
|
|
||||||
@@ -289,14 +289,14 @@
|
|||||||
group="pc-row-components"
|
group="pc-row-components"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
class="row-components-flex"
|
class="row-components-flex"
|
||||||
:style="getRowFlexStyle(row.rowStyle)"
|
:style="getRowFlexStyle(row.rowStyle, row.components)"
|
||||||
@change="markDirty"
|
@change="markDirty"
|
||||||
>
|
>
|
||||||
<template #item="{ element: comp, index: cIndex }">
|
<template #item="{ element: comp, index: cIndex }">
|
||||||
<div
|
<div
|
||||||
class="canvas-comp-wrap"
|
class="canvas-comp-wrap"
|
||||||
:class="{ 'is-selected': selectedCompId === comp.id }"
|
:class="{ 'is-selected': selectedCompId === comp.id }"
|
||||||
:style="getCompWrapperStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1)"
|
:style="getCompWrapperStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1, comp.type)"
|
||||||
@click.stop="handleSelectComp(row, rIndex, comp)"
|
@click.stop="handleSelectComp(row, rIndex, comp)"
|
||||||
>
|
>
|
||||||
<!-- 选中的组件左上角名称标识徽标(万米魔方设计) -->
|
<!-- 选中的组件左上角名称标识徽标(万米魔方设计) -->
|
||||||
@@ -800,11 +800,19 @@ function clearSelection() {
|
|||||||
|
|
||||||
function normalizeRows(list) {
|
function normalizeRows(list) {
|
||||||
if (!Array.isArray(list)) return [];
|
if (!Array.isArray(list)) return [];
|
||||||
|
const autoCompTypes = ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'];
|
||||||
|
const legacyHeights = [40, '40', 280, '280', 420, '420', 560, '560', 620, '620', 980, '980', 1270, '1270'];
|
||||||
const normalized = [];
|
const normalized = [];
|
||||||
list.forEach(r => {
|
list.forEach(r => {
|
||||||
if (!r) return;
|
if (!r) return;
|
||||||
if (r.rowStyle && (r.rowStyle.height === 40 || r.rowStyle.height === "40")) {
|
if (r.rowStyle) {
|
||||||
r.rowStyle.height = "";
|
if (r.rowStyle.height === 40 || r.rowStyle.height === "40") {
|
||||||
|
r.rowStyle.height = "";
|
||||||
|
}
|
||||||
|
const hasAutoComp = Array.isArray(r.components) && r.components.some(c => autoCompTypes.includes(c?.type));
|
||||||
|
if (hasAutoComp && legacyHeights.includes(r.rowStyle.height)) {
|
||||||
|
r.rowStyle.height = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
normalized.push(r);
|
normalized.push(r);
|
||||||
});
|
});
|
||||||
@@ -1252,7 +1260,8 @@ function getCompIcon(type) {
|
|||||||
|
|
||||||
// 计算行样式
|
// 计算行样式
|
||||||
|
|
||||||
function getRowWrapperStyle(style) {
|
function getRowWrapperStyle(style, components = []) {
|
||||||
|
const hasAutoComp = Array.isArray(components) && components.some(c => ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'].includes(c?.type));
|
||||||
if (!style) return { width: '100%' };
|
if (!style) return { width: '100%' };
|
||||||
|
|
||||||
const s = {};
|
const s = {};
|
||||||
@@ -1280,8 +1289,12 @@ function getRowWrapperStyle(style) {
|
|||||||
const h = String(style.height).trim();
|
const h = String(style.height).trim();
|
||||||
if (h && Number(h) > 0) {
|
if (h && Number(h) > 0) {
|
||||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
s.height = hVal;
|
if (hasAutoComp) {
|
||||||
s.minHeight = hVal;
|
s.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
s.height = hVal;
|
||||||
|
s.minHeight = hVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1318,7 +1331,8 @@ function getRowWrapperStyle(style) {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRowFlexStyle(style) {
|
function getRowFlexStyle(style, components = []) {
|
||||||
|
const hasAutoComp = Array.isArray(components) && components.some(c => ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'].includes(c?.type));
|
||||||
if (!style) return { display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', width: '100%', height: '100%' };
|
if (!style) return { display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', width: '100%', height: '100%' };
|
||||||
const s = {
|
const s = {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -1333,8 +1347,12 @@ function getRowFlexStyle(style) {
|
|||||||
const h = String(style.height).trim();
|
const h = String(style.height).trim();
|
||||||
if (h && Number(h) > 0) {
|
if (h && Number(h) > 0) {
|
||||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
s.height = hVal;
|
if (hasAutoComp) {
|
||||||
s.minHeight = hVal;
|
s.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
s.height = hVal;
|
||||||
|
s.minHeight = hVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (style.gap !== undefined && style.gap !== '') {
|
if (style.gap !== undefined && style.gap !== '') {
|
||||||
@@ -1348,7 +1366,8 @@ function getRowFlexStyle(style) {
|
|||||||
|
|
||||||
// 计算组件外层样式
|
// 计算组件外层样式
|
||||||
|
|
||||||
function getCompWrapperStyle(style, rowStyle = {}, totalComps = 1) {
|
function getCompWrapperStyle(style, rowStyle = {}, totalComps = 1, compType = '') {
|
||||||
|
const isAutoHeightComp = compType && ['goodsGrid', 'gridFloor', 'bannerFloor', 'goodsBar', 'channelFloor', 'goodsList', 'recommendFeed', 'articleContent', 'brandWall', 'channelGrid', 'couponList'].includes(compType);
|
||||||
|
|
||||||
if (!style) return {};
|
if (!style) return {};
|
||||||
|
|
||||||
@@ -1358,8 +1377,12 @@ function getCompWrapperStyle(style, rowStyle = {}, totalComps = 1) {
|
|||||||
const rh = String(rowStyle.height).trim();
|
const rh = String(rowStyle.height).trim();
|
||||||
if (rh && Number(rh) > 0) {
|
if (rh && Number(rh) > 0) {
|
||||||
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
||||||
s.height = rhVal;
|
if (isAutoHeightComp) {
|
||||||
s.minHeight = rhVal;
|
s.minHeight = rhVal;
|
||||||
|
} else {
|
||||||
|
s.height = rhVal;
|
||||||
|
s.minHeight = rhVal;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const gap = Number(rowStyle?.gap) || 0;
|
const gap = Number(rowStyle?.gap) || 0;
|
||||||
@@ -1612,7 +1635,11 @@ function goBack() {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
router.push('/pcFloor');
|
if (window.history.state?.back) {
|
||||||
|
router.go(-1);
|
||||||
|
} else {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1643,6 +1670,8 @@ function goBack() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let autoSaveTimer = null;
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|
||||||
const id = route.query.id;
|
const id = route.query.id;
|
||||||
@@ -1671,7 +1700,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
||||||
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
if (autoSaveTimer) {
|
||||||
|
clearInterval(autoSaveTimer);
|
||||||
|
autoSaveTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user