mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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. (北京宏业汇成科技有限公司)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
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:
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
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>
|
||||
<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="./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://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>
|
||||
@@ -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.
|
||||
|
||||
### ✨ 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.
|
||||
- 🧩 **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
|
||||
|
||||
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.**
|
||||
|
||||
17
README.md
17
README.md
@@ -5,7 +5,7 @@
|
||||
<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://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://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>
|
||||
@@ -28,7 +28,7 @@
|
||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||
|
||||
### ✨ 为什么选择 Lilishop
|
||||
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||
- 🟢 **真 Apache-2.0 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
||||
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
||||
|
||||
@@ -146,7 +146,7 @@ lilishop-ui
|
||||
|
||||
## 6. 开源与授权
|
||||
|
||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||
本项目遵循 [Apache-2.0](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||
|
||||
---
|
||||
|
||||
@@ -162,6 +162,8 @@ lilishop-ui
|
||||
|
||||
## 8. 升级计划
|
||||
|
||||
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
|
||||
|
||||
### ✅ 已完成特性
|
||||
| 功能模块 | 功能模块 |
|
||||
| :--- | :--- |
|
||||
@@ -170,12 +172,11 @@ lilishop-ui
|
||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
||||
| ✅ 定时任务管理 | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | ✅ 商家发货改造(整单/拆单/无需配送) |
|
||||
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
|
||||
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | |
|
||||
|
||||
### 🔜 近期规划中
|
||||
- 🔜 店铺结算汇总 & 结算台账
|
||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
|
||||
- 🔜 OpenAPI 开放平台与开发者中心
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||
.el-textarea__inner {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--el-color-primary: var(--theme-color, #ff3c2a);
|
||||
--el-color-danger: var(--theme-color, #ff3c2a);
|
||||
|
||||
@@ -43,25 +43,20 @@
|
||||
</div>
|
||||
<div class="information footer-bottmom">
|
||||
<div class="icp">
|
||||
<li v-if="config.icpCard">
|
||||
<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">
|
||||
<li v-if="icp" class="footer-bottmom">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||
{{config.icpMessage}}
|
||||
{{icp}}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<a class="flex " :href="config.company.href">
|
||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{config.company.name}}
|
||||
<a v-if="companyName" class="flex " :href="companyUrl || undefined">
|
||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{companyName}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="copyright">
|
||||
<p>Copyright © {{year}} {{config.title}}</p>
|
||||
<p>Copyright © {{year}} {{copyrightOwner}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -71,11 +66,12 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import config from '@/config'
|
||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||
|
||||
defineOptions({ name: 'Footer' })
|
||||
|
||||
const router = useRouter()
|
||||
const { companyName, companyUrl, icp, copyrightOwner } = useSiteSettings()
|
||||
|
||||
const guideArr = ref([
|
||||
// 导航链接
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
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 s = props.row.rowStyle || {};
|
||||
const style = {};
|
||||
@@ -96,8 +100,12 @@ const rowWrapperStyle = computed(() => {
|
||||
const h = String(s.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
style.height = hVal;
|
||||
style.minHeight = hVal;
|
||||
if (hasAutoComp.value) {
|
||||
style.minHeight = hVal;
|
||||
} else {
|
||||
style.height = hVal;
|
||||
style.minHeight = hVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +172,12 @@ const rowInnerFlexStyle = computed(() => {
|
||||
const h = String(s.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
styleObj.height = hVal;
|
||||
styleObj.minHeight = hVal;
|
||||
if (hasAutoComp.value) {
|
||||
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%' };
|
||||
|
||||
@@ -185,8 +198,12 @@ function getComponentCellStyle(style, rowStyle = {}, totalComps = 1) {
|
||||
const rh = String(rowStyle.height).trim();
|
||||
if (rh && Number(rh) > 0) {
|
||||
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
||||
s.height = rhVal;
|
||||
s.minHeight = rhVal;
|
||||
if (isAutoHeightComp) {
|
||||
s.minHeight = rhVal;
|
||||
} else {
|
||||
s.height = rhVal;
|
||||
s.minHeight = rhVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
const gap = Number(rowStyle?.gap) || 0;
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
:key="idx"
|
||||
class="zone-link"
|
||||
:style="{
|
||||
top: `${zone.top || 0}%`,
|
||||
left: `${zone.left || 0}%`,
|
||||
width: `${zone.width || 10}%`,
|
||||
height: `${zone.height || 10}%`
|
||||
top: `${zone.top !== undefined ? zone.top : ((zone.topPer || 0) * 100)}%`,
|
||||
left: `${zone.left !== undefined ? zone.left : ((zone.leftPer || 0) * 100)}%`,
|
||||
width: `${zone.width !== undefined ? zone.width : ((zone.widthPer || 0.1) * 100)}%`,
|
||||
height: `${zone.height !== undefined ? zone.height : ((zone.heightPer || 0.1) * 100)}%`
|
||||
}"
|
||||
:title="zone.title"
|
||||
@click="handleClickZone(zone)"
|
||||
@@ -68,7 +68,7 @@ const customStyle = computed(() => {
|
||||
});
|
||||
|
||||
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 (/^https?:\/\//i.test(url)) {
|
||||
window.open(url, '_blank');
|
||||
|
||||
@@ -724,7 +724,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
0,
|
||||
0
|
||||
],
|
||||
"height": 180
|
||||
"height": 220
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -901,6 +901,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
"backgroundColor": "transparent"
|
||||
},
|
||||
"options": {
|
||||
"height": 450,
|
||||
"elements": [
|
||||
{
|
||||
"type": "badge",
|
||||
@@ -979,7 +980,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 560
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1061,7 +1062,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 560
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1140,7 +1141,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 620
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1249,7 +1250,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 420
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1404,7 +1405,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 360
|
||||
"height": 450
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1417,6 +1418,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
},
|
||||
"options": {
|
||||
"videoUrl": "https://vjs.zencdn.net/v/oceans.mp4",
|
||||
"height": 450,
|
||||
"coverUrl": "https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png",
|
||||
"url": "/goodsList?brand=1",
|
||||
"title": "双11全球狂欢发布盛典 · 年度旗舰好物真实开箱测评",
|
||||
@@ -1481,7 +1483,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 220
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1656,7 +1658,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 980
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1810,7 +1812,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 1270
|
||||
"height": ""
|
||||
},
|
||||
"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 };
|
||||
}
|
||||
@@ -206,7 +206,11 @@ function getIndexData() {
|
||||
}
|
||||
|
||||
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++) {
|
||||
const type = dataJson.list[i].type;
|
||||
if (type === "carousel2") {
|
||||
|
||||
216
im/LICENSE
216
im/LICENSE
@@ -1,21 +1,205 @@
|
||||
MIT License
|
||||
Apache License, Version 2.0
|
||||
|
||||
Copyright (c) 2020 YuanDong
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
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:
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
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
|
||||
SOFTWARE.
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
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 {
|
||||
resize: none;
|
||||
resize: none !important;
|
||||
outline: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
@@ -59,4 +59,4 @@ textarea {
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
// 统一请求路径前缀在libs/axios.js中修改
|
||||
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) => {
|
||||
return getRequest('/goods/brand/getByPage', params)
|
||||
|
||||
@@ -170,6 +170,10 @@ export const deleteMemberGroup = (id) => {
|
||||
return deleteRequest(`/member/memberGroup/delete/${id}`);
|
||||
};
|
||||
|
||||
export const getMemberGroupUsers = (groupId, params) => {
|
||||
return getRequest(`/member/memberGroup/${groupId}/users`, params);
|
||||
};
|
||||
|
||||
export const addMemberGroupUsers = (groupId, memberIds) => {
|
||||
return postRequest(`/member/memberGroup/${groupId}/users`, {
|
||||
memberIds: Array.isArray(memberIds) ? memberIds.join(",") : memberIds,
|
||||
|
||||
@@ -283,11 +283,6 @@ export const getCouponReceiveList = (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) => {
|
||||
|
||||
@@ -22,37 +22,11 @@ export const editNoticeMessage = (id, params) => {
|
||||
export const wechatMessageSync = (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) => {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 查询站内信模板信息
|
||||
@@ -73,120 +47,135 @@ export const updateMessageStatus = (id,status) => {
|
||||
};
|
||||
//获取短信模板
|
||||
export const getSmsTemplatePage = (params) => {
|
||||
return getRequest(`sms/template/querySmsTemplatePage`, params);
|
||||
return getRequest("/sms/sms/template", params);
|
||||
};
|
||||
//添加短信模板
|
||||
export const addSmsTemplatePage = (params) => {
|
||||
return postRequest(`sms/template`, params);
|
||||
};
|
||||
//删除短信模板
|
||||
export const deleteSmsTemplatePage = (params) => {
|
||||
return deleteRequest(`sms/template`, params);
|
||||
};
|
||||
//删除短信模板
|
||||
export const editSmsTemplatePage = (params) => {
|
||||
return putRequest(`sms/template/modifySmsTemplate`, params);
|
||||
};
|
||||
//分页查询短信签名
|
||||
//获取短信签名
|
||||
export const getSmsSignPage = (params) => {
|
||||
return getRequest(`sms/sign/querySmsSignPage`, params);
|
||||
return getRequest("/sms/sms/sign", params);
|
||||
};
|
||||
//更新短信模板
|
||||
export const syncTemplate = () => {
|
||||
return putRequest(`sms/template/querySmsSign`);
|
||||
//获取设置
|
||||
export const getSetting = (key) => {
|
||||
return getRequest(`/setting/setting/get/${key}`);
|
||||
};
|
||||
//更新短信模板
|
||||
//根据类型查看配置信息
|
||||
export const getSettingList = (params) => {
|
||||
return getRequest(`/setting/setting/all`, params);
|
||||
};
|
||||
//设置配置信息
|
||||
export const setSetting = (key, params) => {
|
||||
return postRequest(`/setting/setting/put/${key}`, params);
|
||||
};
|
||||
|
||||
//查看短信签名
|
||||
export const getSmsSign = () => {
|
||||
return getRequest(`/sms/sms/sign`);
|
||||
};
|
||||
//添加短信签名
|
||||
export const addSmsSign = (params) => {
|
||||
return postRequest(`sms/sign`, params);
|
||||
return postRequest(`/sms/sms/sign`, params);
|
||||
};
|
||||
//同步短信签名
|
||||
export const syncSign = () => {
|
||||
return putRequest(`sms/sign/querySmsSign`);
|
||||
//修改短信签名
|
||||
export const editSmsSign = (params) => {
|
||||
return putRequest(`/sms/sms/sign`, params);
|
||||
};
|
||||
//删除短信签名
|
||||
export const deleteSign = (id) => {
|
||||
return deleteRequest(`sms/sign/${id}`);
|
||||
export const delSmsSign = (id) => {
|
||||
return deleteRequest(`/sms/sms/sign/${id}`);
|
||||
};
|
||||
//更新短信签名
|
||||
export const editSmsSign = (params) => {
|
||||
return putRequest(`sms/sign/modifySmsSign`, params);
|
||||
//添加短信模板
|
||||
export const addSmsTemplate = (params) => {
|
||||
return postRequest(`/sms/sms/template`, params);
|
||||
};
|
||||
//查询签名详情
|
||||
export const smsSignDetail = (id) => {
|
||||
return getRequest(`sms/sign/${id}`);
|
||||
//修改短信模板
|
||||
export const editSmsTemplate = (params) => {
|
||||
return putRequest(`/sms/sms/template`, params);
|
||||
};
|
||||
|
||||
//分页查询app版本信息
|
||||
export const appVersionPage = (params) => {
|
||||
return getRequest(`/other/appVersion`, params);
|
||||
//删除短信模板
|
||||
export const delSmsTemplate = (id) => {
|
||||
return deleteRequest(`/sms/sms/template/${id}`);
|
||||
};
|
||||
//添加app版本信息
|
||||
export const addVersion = (params) => {
|
||||
return postRequest(`/other/appVersion`, 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) => {
|
||||
return putRequest(`instant-delivery/${bean}/config`, params, headers);
|
||||
};
|
||||
//启用配送方案
|
||||
export const openInstantDelivery = (bean) => {
|
||||
return putRequest(`instant-delivery/${bean}/open`);
|
||||
};
|
||||
|
||||
//修改封面图片
|
||||
export const editInstantDeliveryImage = (bean, params) => {
|
||||
return putRequest(`instant-delivery/${bean}/image`, params);
|
||||
};
|
||||
|
||||
//查询短信发送记录
|
||||
export const getSmsPage = (params) => {
|
||||
return getRequest(`sms/sms`, params);
|
||||
export const getSmsReachPage = (params) => {
|
||||
return getRequest(`/sms/sms`, params);
|
||||
};
|
||||
//重新发送
|
||||
export const repeatSendSms = (id) => {
|
||||
return postRequest(`/sms/sms/repeat/${id}`);
|
||||
};
|
||||
//获取设置
|
||||
export const getExperienceData = (key) => {
|
||||
return getRequest(`/setting/experience/get/${key}`);
|
||||
};
|
||||
//保存
|
||||
export const setExperienceData = (key, params) => {
|
||||
return postRequest(`/setting/experience/put/${key}`, params);
|
||||
};
|
||||
|
||||
//发送短信
|
||||
export const sendSms = (params) => {
|
||||
return postRequest(`sms/sms`, params);
|
||||
//根据类型获取积分设置
|
||||
export const getPointData = (key) => {
|
||||
return getRequest(`/setting/point/get/${key}`);
|
||||
};
|
||||
//保存
|
||||
export const setPointData = (key, params) => {
|
||||
return postRequest(`/setting/point/put/${key}`, params);
|
||||
};
|
||||
/*
|
||||
* 获取 支付支持设置开关
|
||||
*/
|
||||
export const getPaymentSupportForm = () => {
|
||||
return getRequest(`setting/settingx/paymentSupport`);
|
||||
}
|
||||
|
||||
// 验证码源文件删除
|
||||
export const delVerification = (id) => {
|
||||
return deleteRequest(`/other/verificationSource/${id}`)
|
||||
}
|
||||
//根据类型获取积分设置
|
||||
export const getKuaidiSetting = (key) => {
|
||||
return getRequest(`/setting/kuaidi/get/${key}`);
|
||||
};
|
||||
//保存
|
||||
export const setKuaidiSetting = (key, params) => {
|
||||
return postRequest(`/setting/kuaidi/put/${key}`, params);
|
||||
};
|
||||
|
||||
// 添加验证码源文件
|
||||
export const addVerification = (params) => {
|
||||
//获取所有的物流公司
|
||||
export const getLogisticsList = () => {
|
||||
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 openLogistics = (id) => {
|
||||
return putRequest(`/setting/logistics/open/${id}`);
|
||||
};
|
||||
|
||||
//获取所有的物流公司
|
||||
export const getLogisticsAll = () => {
|
||||
return getRequest(`/setting/logistics/all`);
|
||||
};
|
||||
|
||||
// 验证码源文件添加
|
||||
export const createVerification = (params) => {
|
||||
return postRequest(`/other/verificationSource`, params)
|
||||
}
|
||||
|
||||
// 修改验证码源文件
|
||||
export const editVerification = (id, params) => {
|
||||
// 验证码源文件修改
|
||||
export const updateVerification = (id, params) => {
|
||||
return putRequest(`/other/verificationSource/${id}`, params)
|
||||
}
|
||||
|
||||
// 验证码源文件详情
|
||||
export const verificationDetail = (id) => {
|
||||
return getRequest(`/other/verificationSource/${id}`)
|
||||
// 验证码源文件删除
|
||||
export const deleteVerification = (id) => {
|
||||
return deleteRequest(`/other/verificationSource/${id}`)
|
||||
}
|
||||
|
||||
// 验证码源文件列表
|
||||
export const verificationPage = (params) => {
|
||||
return getRequest(`/other/verificationSource`, params)
|
||||
@@ -197,12 +186,94 @@ export const getNoticeMessageDetail = (id, params) => {
|
||||
return getRequest(`/setting/noticeMessage/${id}`, params)
|
||||
}
|
||||
|
||||
// 消息模板聚合分页(站内信 + 微信服务号 + 小程序)
|
||||
// 统一消息模板聚合分页(站内信 + 短信 + 微信服务号 + 小程序订阅)
|
||||
export const getMessageTemplateAggregatePage = params => {
|
||||
return getRequest("/setting/noticeMessage/aggregate", params);
|
||||
return getRequest("/setting/messageTemplate/page", params);
|
||||
};
|
||||
|
||||
// 修改通知类站内信邮箱渠道开关
|
||||
export const updateNoticeMessageEmailStatus = (id, status) => {
|
||||
return putRequest(`/setting/noticeMessage/${id}/email/${status}`);
|
||||
// 快速切换渠道开关 (channel: NOTICE / SMS / WECHAT_OA / WECHAT_MP, status: OPEN / CLOSE)
|
||||
export const updateMessageChannelSwitch = params => {
|
||||
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
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// 短信任务发送与记录 (sms.vue)
|
||||
export const getSmsPage = (params) => {
|
||||
return getRequest(`/sms/sms`, params);
|
||||
};
|
||||
export const sendSms = (params) => {
|
||||
return postRequest(`/sms/sms`, params);
|
||||
};
|
||||
|
||||
// 短信签名同步与详细 (sms.vue, smsSign.vue)
|
||||
export const syncSign = () => {
|
||||
return putRequest(`/sms/sign/querySmsSign`);
|
||||
};
|
||||
export const deleteSign = (id) => {
|
||||
return deleteRequest(`/sms/sign/${id}`);
|
||||
};
|
||||
export const smsSignDetail = (id) => {
|
||||
return getRequest(`/sms/sign/${id}`);
|
||||
};
|
||||
|
||||
// 短信模板管理与同步 (sms.vue)
|
||||
export const syncTemplate = () => {
|
||||
return putRequest(`/sms/template/querySmsSign`);
|
||||
};
|
||||
export const addSmsTemplatePage = (params) => {
|
||||
return postRequest(`/sms/template`, params);
|
||||
};
|
||||
export const editSmsTemplatePage = (params) => {
|
||||
return putRequest(`/sms/template/modifySmsTemplate`, params);
|
||||
};
|
||||
export const deleteSmsTemplatePage = (params) => {
|
||||
return deleteRequest(`/sms/template`, params);
|
||||
};
|
||||
|
||||
// 支付支持方式表单 (PAYMENT_SUPPORT.vue)
|
||||
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}`);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="flag"
|
||||
width="800px"
|
||||
:width="width"
|
||||
:close-on-click-modal="false"
|
||||
title="绘制热区"
|
||||
top="120px"
|
||||
@@ -26,6 +26,13 @@
|
||||
import { ref } from "vue";
|
||||
import hotzone from "./components/Hotzone.vue";
|
||||
|
||||
const props = defineProps({
|
||||
width: {
|
||||
type: String,
|
||||
default: "800px",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["changeZone", "closeFlag"]);
|
||||
|
||||
const flag = ref(false);
|
||||
|
||||
@@ -1,100 +1,140 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<div class="wap-content">
|
||||
<div class="query-wrapper">
|
||||
<div class="query-item">
|
||||
<div>搜索范围</div>
|
||||
<el-input
|
||||
v-model="goodsParams.goodsName"
|
||||
placeholder="商品名称"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
@clear="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 class="goods-dialog-wrapper">
|
||||
<!-- 顶部搜索筛选栏 -->
|
||||
<div class="goods-query-bar">
|
||||
<div class="query-item">
|
||||
<span class="query-label">商品名称:</span>
|
||||
<el-input
|
||||
v-model="goodsParams.goodsName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@clear="onSearchGoods"
|
||||
@keyup.enter="onSearchGoods"
|
||||
/>
|
||||
</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="销售价" 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 }">
|
||||
<el-tag size="small" type="info">{{ goodsTypeLabel(row.goodsType) }}</el-tag>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref, watch } from "vue";
|
||||
import * as API_Goods from "@/api/goods";
|
||||
@@ -131,6 +171,7 @@ const tableRef = ref(null);
|
||||
watch(category, (val) => {
|
||||
goodsParams.categoryPath = val && val.length ? val[2] : "";
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.selectedWay,
|
||||
(val) => {
|
||||
@@ -139,6 +180,7 @@ watch(
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => goodsParams.categoryPath,
|
||||
() => {
|
||||
@@ -158,10 +200,27 @@ function onSearchGoods() {
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
function changePageSize(v) {
|
||||
|
||||
function onResetGoods() {
|
||||
goodsParams.goodsName = "";
|
||||
goodsParams.sn = "";
|
||||
category.value = [];
|
||||
goodsParams.categoryPath = "";
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
goodsParams.pageNumber = v;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function onPageSizeChange(val) {
|
||||
goodsParams.pageSize = val;
|
||||
goodsParams.pageNumber = 1;
|
||||
getQueryGoodsList();
|
||||
}
|
||||
|
||||
function getQueryGoodsList() {
|
||||
loading.value = true;
|
||||
API_Goods.getGoodsSkuData(goodsParams)
|
||||
@@ -172,6 +231,7 @@ function getQueryGoodsList() {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function initGoods(res) {
|
||||
const records = res?.result?.records || [];
|
||||
records.forEach((item) => {
|
||||
@@ -181,6 +241,7 @@ function initGoods(res) {
|
||||
goodsData.value = records;
|
||||
nextTick(syncTableSelection);
|
||||
}
|
||||
|
||||
function syncTableSelection() {
|
||||
const table = tableRef.value;
|
||||
if (!table) return;
|
||||
@@ -189,16 +250,19 @@ function syncTableSelection() {
|
||||
return;
|
||||
}
|
||||
goodsData.value.forEach((item) => {
|
||||
const selected = selectedList.value.some((e) => (e.id || e.skuId) && String(e.id || e.skuId) === String(item.id || item.skuId));
|
||||
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) {
|
||||
selectedList.value = list;
|
||||
emit("selected", selectedList.value);
|
||||
}
|
||||
|
||||
function onSelectionChange(rows) {
|
||||
// 选择链接时为单选:保留最后勾选的一项,多选框交互不变
|
||||
if (type.value !== "multiple") {
|
||||
if (!rows.length) {
|
||||
emitSelected([]);
|
||||
@@ -214,17 +278,18 @@ function onSelectionChange(rows) {
|
||||
}
|
||||
emitSelected(rows);
|
||||
}
|
||||
|
||||
function init() {
|
||||
API_Goods.getGoodsSkuData(goodsParams).then((res) => {
|
||||
initGoods(res);
|
||||
});
|
||||
getQueryGoodsList();
|
||||
API_Goods.getCategoryTree({ deleteFlag: false }).then((res) => {
|
||||
if (res.success) {
|
||||
if (res.success && res.result) {
|
||||
deepGroup(res.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deepGroup(val) {
|
||||
skuList.value = [];
|
||||
val.forEach((item) => {
|
||||
let childWay = [];
|
||||
if (item.children) {
|
||||
@@ -252,34 +317,102 @@ function deepGroup(val) {
|
||||
});
|
||||
}
|
||||
|
||||
// 父组件 lili-dialog/index.vue 通过 $refs.goodsDialog.type = "single" 切换单选;
|
||||
// expose 的 ref 外部读写等价于原 data 字段
|
||||
defineExpose({ type });
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
.wap-content {
|
||||
.goods-dialog-wrapper {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
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 {
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
color: $theme_color;
|
||||
color: #f56c6c;
|
||||
}
|
||||
.pageration {
|
||||
|
||||
/* 底部翻页栏 */
|
||||
.goods-pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
padding-right: 8px;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
<el-dialog
|
||||
v-if="goodsFlag"
|
||||
v-model="flag"
|
||||
width="1160px"
|
||||
top="120px"
|
||||
title="选择商品"
|
||||
width="85%"
|
||||
top="5vh"
|
||||
:z-index="10000"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="goods-selector-dialog"
|
||||
@close="clickClose"
|
||||
>
|
||||
<goodsDialog
|
||||
@@ -21,8 +23,15 @@
|
||||
"
|
||||
/>
|
||||
<template #footer>
|
||||
<el-button @click="clickClose">取消</el-button>
|
||||
<el-button type="primary" @click="clickOK">确定</el-button>
|
||||
<div class="goods-dialog-footer">
|
||||
<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>
|
||||
</el-dialog>
|
||||
|
||||
@@ -53,6 +62,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import goodsDialog from "./goods-dialog";
|
||||
@@ -61,8 +71,8 @@ import linkDialog from "./link-dialog";
|
||||
const emit = defineEmits(["closeFlag", "selectedGoodsData", "selectedLink"]);
|
||||
|
||||
const goodsFlag = ref(false); // 是否商品选择器
|
||||
const goodsData = ref([]); //选择的商品
|
||||
const linkData = ref(""); //选择的链接
|
||||
const goodsData = ref([]); // 选择的商品
|
||||
const linkData = ref(""); // 选择的链接
|
||||
const flag = ref(false); // modal显隐
|
||||
|
||||
// 模板 ref:原 ref="goodsDialog" 与 import 的组件同名,改名避免互相覆盖
|
||||
@@ -112,14 +122,7 @@ function close() {
|
||||
flag.value = false;
|
||||
}
|
||||
|
||||
// 外部通过 ref 访问的成员(grep manager/src 全量确认):
|
||||
// - 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 实例天然暴露,保守保留
|
||||
// 外部通过 ref 访问的成员:
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
@@ -130,14 +133,73 @@ defineExpose({
|
||||
flag,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-dialog__body) {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
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 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 样式
|
||||
.link-selector-drawer {
|
||||
max-width: 96vw;
|
||||
@@ -152,7 +214,8 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
// 商品分类下拉挂载到 body,需要高于抽屉的 10000 层级
|
||||
.goods-dialog-cascader-popper {
|
||||
.goods-dialog-cascader-popper,
|
||||
.goods-dialog-pagination-popper {
|
||||
z-index: 10001 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
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 });
|
||||
</script>
|
||||
|
||||
|
||||
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 = {
|
||||
"customWords/index": "custom-words/index",
|
||||
"sensitiveWords/index": "sensitive-words/index",
|
||||
"sys/message/noticeMessageTemplate": "sys/message/messageTemplate",
|
||||
};
|
||||
|
||||
function resolveViewPath(url) {
|
||||
|
||||
@@ -8,6 +8,10 @@ let util = {};
|
||||
/** 已弃用、前端未实现或已移除的菜单 frontRoute,注册路由时跳过 */
|
||||
const DEPRECATED_FRONT_ROUTES = new Set([
|
||||
"goods/goods-manage/spec",
|
||||
"member/list/memberRecycle",
|
||||
"order/order/fictitiousOrderList",
|
||||
"promotions/flash-discount",
|
||||
"promotions/nth-item-discount",
|
||||
]);
|
||||
|
||||
function isDeprecatedMenu(menu) {
|
||||
|
||||
@@ -38,6 +38,20 @@ export const otherRouter = {
|
||||
redirect: "/home",
|
||||
component: Main,
|
||||
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",
|
||||
title: '首页',
|
||||
@@ -112,6 +126,12 @@ export const otherRouter = {
|
||||
name: "goods-category",
|
||||
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",
|
||||
title: "商品分组",
|
||||
@@ -346,18 +366,6 @@ export const otherRouter = {
|
||||
name: "full-discount-detail",
|
||||
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",
|
||||
title: "编辑秒杀活动",
|
||||
@@ -471,6 +479,13 @@ export const otherRouter = {
|
||||
meta: { title: "商品销售汇总报表" },
|
||||
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",
|
||||
title: "分销员导入记录",
|
||||
|
||||
@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
|
||||
import { otherRouter } from "@/router/router";
|
||||
import i18n from "@/locale";
|
||||
|
||||
const removedPageNames = new Set(["memberRecycle", "fictitiousOrderList"]);
|
||||
|
||||
// 原 vuex app 模块 + 根 state(openStyleStore/openStoreId/notices)合并
|
||||
// mutation 全部转为同名 action;两个例外:
|
||||
// - pageOpenedList mutation 与 state 同名,改名 updateOpenedList
|
||||
@@ -73,7 +75,8 @@ export const useAppStore = defineStore("app", {
|
||||
},
|
||||
initCachePage() {
|
||||
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) {
|
||||
@@ -120,8 +123,9 @@ export const useAppStore = defineStore("app", {
|
||||
},
|
||||
setOpenedList() {
|
||||
this.pageOpenedList = localStorage.pageOpenedList
|
||||
? JSON.parse(localStorage.pageOpenedList)
|
||||
? JSON.parse(localStorage.pageOpenedList).filter((page) => !removedPageNames.has(page.name))
|
||||
: [otherRouter.children[0]];
|
||||
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
|
||||
},
|
||||
setAvatarPath(path) {
|
||||
localStorage.avatorImgPath = path;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||
.el-textarea__inner {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
/* Element Plus 主题覆盖(对齐原 iView 主色) */
|
||||
:root {
|
||||
// --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);
|
||||
if (!el) {
|
||||
return;
|
||||
@@ -13,22 +13,196 @@ export function printElement(elementId, title = "打印") {
|
||||
doc.open();
|
||||
doc.write(
|
||||
`<!DOCTYPE html><html><head><title>${title}</title><style>
|
||||
body{font-family:Arial,sans-serif;padding:12px;color:#333}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
td,th{border:1px solid #ddd;padding:6px}
|
||||
.lineH30{line-height:30px}
|
||||
.f14{font-size:14px;color:#333}
|
||||
.printgoodtitle{font-size:14px;line-height:1.5;margin-top:15px;color:#333}
|
||||
.printgoodinfo{padding:10px}
|
||||
.printgooditem{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee}
|
||||
.printgoodname{flex:1}
|
||||
.printgoodguid{margin-top:4px;color:#666;font-size:12px}
|
||||
.printgoodguiditem{margin-right:8px}
|
||||
.printgoodnumber{white-space:nowrap;margin-left:12px}
|
||||
@page {
|
||||
size: portrait;
|
||||
margin: 8mm 10mm;
|
||||
}
|
||||
* {
|
||||
-webkit-print-color-adjust: exact !important;
|
||||
print-color-adjust: exact !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
html, body {
|
||||
margin: 0 !important;
|
||||
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>`
|
||||
);
|
||||
doc.close();
|
||||
iframe.contentWindow.focus();
|
||||
iframe.contentWindow.print();
|
||||
setTimeout(() => document.body.removeChild(iframe), 1000);
|
||||
setTimeout(() => {
|
||||
iframe.contentWindow.print();
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(iframe)) {
|
||||
document.body.removeChild(iframe);
|
||||
}
|
||||
}, 1500);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -45,12 +45,13 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form-item label="自定义分词" prop="name">
|
||||
@@ -61,7 +62,7 @@
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -127,9 +127,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 v-if="row">
|
||||
<div v-if="row" class="ops-row">
|
||||
<a class="link-text" @click="openEdit(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<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="resume(row)">恢复</a>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -260,10 +260,10 @@
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="editVisible"
|
||||
title="编辑分销员"
|
||||
width="520px"
|
||||
size="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -304,12 +304,12 @@
|
||||
<el-button @click="editVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editLoading" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="createVisible"
|
||||
title="新增分销员"
|
||||
width="520px"
|
||||
size="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -345,12 +345,12 @@
|
||||
<el-button @click="createVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="createLoading" @click="submitCreate">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="batchGradeVisible"
|
||||
title="批量设置等级"
|
||||
width="480px"
|
||||
size="480px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -380,12 +380,12 @@
|
||||
<el-button @click="batchGradeVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="batchGradeLoading" @click="submitBatchGrade">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="importVisible"
|
||||
title="批量导入分销员"
|
||||
width="520px"
|
||||
size="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
@@ -416,7 +416,7 @@
|
||||
开始导入
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -923,6 +923,9 @@ onMounted(() => {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
}
|
||||
.ops-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status-tabs {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -243,12 +243,13 @@
|
||||
</div>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="rebindVisible"
|
||||
title="换绑客户关系"
|
||||
width="520px"
|
||||
size="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="rebindFormRef" :model="rebindForm" :rules="rebindRules" label-width="110px">
|
||||
<el-form-item label="客户">
|
||||
@@ -273,7 +274,7 @@
|
||||
<el-button @click="rebindVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="rebindLoading" @click="submitRebind">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<distribution-selector
|
||||
v-model="distributorSelectorVisible"
|
||||
@@ -281,12 +282,13 @@
|
||||
@confirm="onDistributorSelected"
|
||||
/>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="unbindVisible"
|
||||
title="解绑客户关系"
|
||||
width="520px"
|
||||
size="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="unbindFormRef" :model="unbindForm" label-width="110px">
|
||||
<el-form-item label="客户">
|
||||
@@ -300,7 +302,7 @@
|
||||
<el-button @click="unbindVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="unbindLoading" @click="submitUnbind">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" border :data="data">
|
||||
<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="payAmount" label="支付金额" width="120" />
|
||||
<el-table-column prop="refundCount" label="退款笔数" width="100" />
|
||||
@@ -35,6 +39,17 @@ const loading = ref(false);
|
||||
const dateRange = ref([]);
|
||||
const data = ref([]);
|
||||
|
||||
const paymentNameMap = {
|
||||
WECHAT: "微信支付",
|
||||
ALIPAY: "支付宝",
|
||||
WALLET: "余额支付",
|
||||
BANK_TRANSFER: "线下转账",
|
||||
};
|
||||
|
||||
function formatPaymentName(name) {
|
||||
return paymentNameMap[name] || name || "-";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData();
|
||||
});
|
||||
|
||||
@@ -1,28 +1,56 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-form inline>
|
||||
<el-form-item label="会员">
|
||||
<el-input v-model="searchForm.memberName" clearable style="width: 160px" />
|
||||
<div class="finance-wallet-log-page search">
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form inline class="search-form" @keyup.enter="handleSearch">
|
||||
<el-form-item label="客户ID">
|
||||
<el-input v-model="searchForm.memberId" placeholder="请输入客户ID" clearable style="width: 180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="日期">
|
||||
<el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" />
|
||||
<el-form-item label="会员名称">
|
||||
<el-input v-model="searchForm.memberName" placeholder="请输入会员名称" clearable style="width: 160px" />
|
||||
</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 @click="handleReset">重置</el-button>
|
||||
<el-button @click="handleExport">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" border :data="data">
|
||||
<el-table-column prop="memberName" label="会员" width="140" />
|
||||
<el-table-column prop="money" label="金额" width="110" />
|
||||
<el-table-column prop="serviceType" label="业务类型" width="140" />
|
||||
<el-table-column prop="detail" label="明细" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="时间" width="170" />
|
||||
|
||||
<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 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>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -42,15 +70,16 @@ import { useTable } from "@/utils/useTable";
|
||||
import * as API_Finance from "@/api/finance";
|
||||
import { getRequest } from "@/libs/axios";
|
||||
import { downloadBlob } from "@/utils/downloadBlob";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "finance-wallet-log" });
|
||||
|
||||
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),
|
||||
{
|
||||
pageSize: 20,
|
||||
form: { memberName: "" },
|
||||
form: { memberId: "", memberName: "" },
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
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() {
|
||||
const params = { memberName: searchForm.memberName };
|
||||
const params = {
|
||||
memberId: searchForm.memberId || undefined,
|
||||
memberName: searchForm.memberName || undefined,
|
||||
};
|
||||
if (dateRange.value?.length === 2) {
|
||||
params.startDate = dateRange.value[0];
|
||||
params.endDate = dateRange.value[1];
|
||||
@@ -73,3 +112,78 @@ function handleExport() {
|
||||
});
|
||||
}
|
||||
</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>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="operation">
|
||||
<list-action-bar>
|
||||
<el-button type="primary" @click="add">添加</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
</div>
|
||||
</list-action-bar>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
@@ -66,6 +66,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import {
|
||||
addGoodsUnit,
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
</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-tab-pane
|
||||
v-for="item in goodsStatusWithCount"
|
||||
@@ -81,7 +82,7 @@
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="batch-operations" style="margin: 10px 0">
|
||||
<div v-if="!readOnly" class="batch-operations" style="margin: 10px 0">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@@ -125,7 +126,7 @@
|
||||
row-key="id"
|
||||
@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 label="商品图片" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
@@ -141,6 +142,8 @@
|
||||
<a class="link-text" @click="linkTo(row.id, row.skuId)">{{ row.goodsName }}</a>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: $mainColor }">{{ unitPrice(row.price, '¥') }}</span>
|
||||
@@ -153,7 +156,7 @@
|
||||
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="150">
|
||||
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||
</template>
|
||||
@@ -164,7 +167,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<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 v-if="row.authFlag === 'TOBEAUDITED'">
|
||||
<a class="link-text" @click="openAuditModal(row)">审核</a>
|
||||
@@ -298,6 +306,12 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||
|
||||
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)
|
||||
const searchFormRef = ref(null);
|
||||
@@ -315,7 +329,7 @@ const auditModalVisible = ref(false);
|
||||
const auditSubmitLoading = ref(false);
|
||||
const currentAuditGoods = ref(null);
|
||||
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(
|
||||
getGoodsListData,
|
||||
@@ -333,8 +347,13 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
|
||||
},
|
||||
beforeRequest: (p) => {
|
||||
const params = { ...p };
|
||||
if (currentStatus.value && currentStatus.value !== "ALL") {
|
||||
params.goodsStatus = currentStatus.value;
|
||||
const status = props.scheduledOnly ? props.scheduledStatus : 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 {
|
||||
delete params.goodsStatus;
|
||||
}
|
||||
@@ -347,7 +366,7 @@ const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch
|
||||
);
|
||||
|
||||
function handleReset() {
|
||||
currentStatus.value = "ALL";
|
||||
currentStatus.value = props.scheduledOnly ? props.scheduledStatus : "ALL";
|
||||
resetSearch();
|
||||
}
|
||||
const goodsNumerData = ref({});
|
||||
@@ -415,6 +434,7 @@ function init() {
|
||||
// 搜索
|
||||
// 获取数据
|
||||
function getNumberData() {
|
||||
if (props.scheduledOnly) return;
|
||||
// 创建一个不包含goodsStatus字段的搜索参数
|
||||
const { goodsStatus, ...searchParams } = searchForm;
|
||||
getGoodsNumerData(searchParams).then((res) => {
|
||||
@@ -488,6 +508,7 @@ function goodsStatusClick(name) {
|
||||
}
|
||||
selectedRows.value = [];
|
||||
clearTableSelection();
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
function examine(v, authFlag) {
|
||||
|
||||
@@ -1,47 +1,77 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="brand-manage-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="品牌名称">
|
||||
<el-form-item label="品牌名称" prop="name">
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入品牌名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 220px"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="mt_10">
|
||||
<div class="operation padding-row" style="margin-bottom: 10px;">
|
||||
<el-button type="primary" @click="add">添加</el-button>
|
||||
</div>
|
||||
<!-- 品牌数据表格卡片 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<list-action-bar>
|
||||
<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-column prop="name" label="品牌名称" width="200" />
|
||||
<el-table-column label="品牌图标" align="left">
|
||||
<el-table
|
||||
ref="table"
|
||||
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 }">
|
||||
<img
|
||||
v-if="row"
|
||||
:src="row.logo || ''"
|
||||
alt="加载图片失败"
|
||||
style="cursor: pointer; width: 80px; height: 60px; margin: 10px 0; object-fit: contain"
|
||||
/>
|
||||
<div v-if="row && row.logo" class="logo-box">
|
||||
<el-image
|
||||
:src="row.logo"
|
||||
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>
|
||||
</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 }">
|
||||
<el-switch
|
||||
v-if="row"
|
||||
@@ -53,8 +83,12 @@
|
||||
/>
|
||||
</template>
|
||||
</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 v-if="row">
|
||||
<a class="link-text" @click="edit(row)">编辑</a>
|
||||
@@ -67,7 +101,8 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -81,24 +116,31 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加 / 编辑品牌弹窗 -->
|
||||
<el-dialog
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
width="520px"
|
||||
: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-input v-model="form.name" style="width: 100%" />
|
||||
<el-input v-model="form.name" placeholder="请输入品牌名称" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌图标" prop="logo">
|
||||
<div style="display: flex; align-items: center; gap: 8px">
|
||||
<img
|
||||
:src="form.logo || defaultPic"
|
||||
alt=""
|
||||
style="width: 60px; height: 60px; object-fit: contain; border: 1px solid #dcdfe6; border-radius: 4px"
|
||||
/>
|
||||
<el-button type="primary" link @click="openLogoPicker">修改</el-button>
|
||||
<div class="dialog-logo-picker">
|
||||
<div class="logo-preview-box">
|
||||
<img
|
||||
:src="form.logo || defaultPic"
|
||||
alt="品牌图标"
|
||||
class="dialog-logo-img"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" link @click="openLogoPicker">上传/选择图标</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -108,7 +150,8 @@
|
||||
</template>
|
||||
</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
|
||||
ref="ossManageRef"
|
||||
:is-component="true"
|
||||
@@ -117,13 +160,15 @@
|
||||
/>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 关联分类弹窗 -->
|
||||
<el-dialog
|
||||
v-model="categoryModalVisible"
|
||||
:title="categoryModalTitle"
|
||||
width="700px"
|
||||
width="680px"
|
||||
: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
|
||||
ref="categoryTree"
|
||||
:key="categoryTreeKey"
|
||||
@@ -139,7 +184,7 @@
|
||||
<template #footer>
|
||||
<el-button @click="categoryModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="categorySubmitLoading" @click="submitBrandCategory">
|
||||
提交
|
||||
提交关联
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
@@ -147,6 +192,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref, reactive, nextTick, getCurrentInstance } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import {
|
||||
@@ -299,7 +345,7 @@ function handleStatusSwitchChange(row, checked) {
|
||||
}
|
||||
|
||||
function delBrand(row) {
|
||||
ElMessageBox.confirm("您确认要删除 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
||||
ElMessageBox.confirm("您确认要删除品牌 " + row.name + " ?", "确认删除", { type: "warning" }).then(() => {
|
||||
return delBrandApi(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("品牌删除成功!");
|
||||
@@ -339,7 +385,7 @@ function handleSubmit() {
|
||||
|
||||
function add() {
|
||||
modalType.value = 0;
|
||||
modalTitle.value = "添加";
|
||||
modalTitle.value = "添加品牌";
|
||||
formRef.value?.resetFields();
|
||||
delete form.value.id;
|
||||
modalVisible.value = true;
|
||||
@@ -347,7 +393,7 @@ function add() {
|
||||
|
||||
function edit(v) {
|
||||
modalType.value = 1;
|
||||
modalTitle.value = "编辑";
|
||||
modalTitle.value = "编辑品牌";
|
||||
formRef.value?.resetFields();
|
||||
for (const attr in v) {
|
||||
if (v[attr] === null) {
|
||||
@@ -360,13 +406,108 @@ function edit(v) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.brand-manage-page {
|
||||
.filter-card {
|
||||
.search-form {
|
||||
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;
|
||||
color: #dcdee2;
|
||||
|
||||
.dialog-logo-picker {
|
||||
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>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<div class="mb_10">
|
||||
<list-action-bar>
|
||||
<el-button type="primary" @click="addParent">添加一级分类</el-button>
|
||||
</div>
|
||||
</list-action-bar>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
@@ -121,6 +121,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import {
|
||||
delCategory,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-form ref="searchFormRef" :model="searchForm" inline>
|
||||
<el-card class="parameter-filter-card">
|
||||
<el-form ref="searchFormRef" :model="searchForm" inline class="search-form" @keyup.enter="handleSearch">
|
||||
<el-form-item label="参数名称" prop="paramName">
|
||||
<el-input
|
||||
v-model="searchForm.paramName"
|
||||
@@ -16,9 +16,12 @@
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</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-row>
|
||||
</list-action-bar>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
@@ -72,6 +75,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,77 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
<div class="goods-group-page search">
|
||||
<!-- 顶部筛选与操作栏 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<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>
|
||||
</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="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 label="操作" width="200" align="center" fixed="right">
|
||||
|
||||
<!-- 分组描述 -->
|
||||
<el-table-column prop="description" label="分组描述" min-width="240" show-overflow-tooltip>
|
||||
<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>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="remove(row)">删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -35,13 +85,32 @@
|
||||
</div>
|
||||
</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-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formAdd.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formAdd.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -50,14 +119,35 @@
|
||||
</template>
|
||||
</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-input v-model="formEdit.id" style="display: none" />
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组ID">
|
||||
<span>{{ formEdit.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<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>
|
||||
<template #footer>
|
||||
@@ -69,6 +159,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref, reactive, nextTick, onMounted } from "vue";
|
||||
import * as API_Goods from "@/api/goods.js";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
@@ -79,6 +170,7 @@ const loading = ref(true);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
groupName: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
@@ -86,18 +178,22 @@ const addFlag = ref(false);
|
||||
const editFlag = ref(false);
|
||||
const submitAddLoading = ref(false);
|
||||
const submitEditLoading = ref(false);
|
||||
|
||||
const formAdd = ref({
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const formEdit = ref({
|
||||
id: "",
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const rulesAdd = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const rulesEdit = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
@@ -105,7 +201,14 @@ const rulesEdit = reactive({
|
||||
const addFormRef = ref(null);
|
||||
const editFormRef = ref(null);
|
||||
|
||||
function init() {
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.groupName = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
@@ -216,36 +319,59 @@ function submitEdit() {
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
||||
return API_Goods.deleteGoodsGroup(row.id)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("删除失败,请检查权限或后端接口");
|
||||
});
|
||||
}).catch(() => {});
|
||||
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
return API_Goods.deleteGoodsGroup(row.id)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.error("删除失败,请检查权限或后端接口");
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ops a {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ops span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
<style scoped lang="scss">
|
||||
.goods-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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</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 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
|
||||
type="info"
|
||||
show-icon
|
||||
:closable="false"
|
||||
title="支持按规格逐条设置虚拟销量,列表总销量 = 真实销量 + 虚拟销量。"
|
||||
style="margin-bottom: 10px"
|
||||
@@ -71,55 +75,65 @@
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="data"
|
||||
border
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
@sort-change="changeSort"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<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 prop="goodsId" label="SPU编码" width="180" show-overflow-tooltip />
|
||||
<el-table-column label="商品信息" min-width="360">
|
||||
<el-table-column label="商品信息" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row" class="goods-info">
|
||||
<img
|
||||
v-if="row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
class="goods-thumbnail"
|
||||
alt=""
|
||||
/>
|
||||
<div class="goods-thumb-box">
|
||||
<el-image
|
||||
v-if="row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
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="div-zoom">{{ row.goodsName }}</div>
|
||||
<div v-if="row.simpleSpecs" class="sub-title">
|
||||
<div class="goods-title" :title="row.goodsName">{{ row.goodsName || "-" }}</div>
|
||||
<div v-if="row.simpleSpecs" class="goods-specs">
|
||||
规格:{{ row.simpleSpecs.trim() || "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</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 }">
|
||||
<span v-if="row">{{ row.buyCount || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="虚拟销量" width="110">
|
||||
<el-table-column label="虚拟销量" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.virtualSalesInput || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="总销量" width="110">
|
||||
<el-table-column label="总销量" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ getTotalSales(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="row.marketEnable === 'UPPER' ? 'success' : 'warning'">
|
||||
<span v-if="row">
|
||||
{{ row.marketEnable === "UPPER" ? "上架" : "下架" }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</template>
|
||||
</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 }">
|
||||
<a
|
||||
v-if="row"
|
||||
@@ -147,14 +161,19 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="editModalVisible"
|
||||
: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-press-escape="!modalSubmitting"
|
||||
:show-close="!modalSubmitting"
|
||||
destroy-on-close
|
||||
@closed="resetVirtualSalesModal"
|
||||
>
|
||||
<div v-if="modalMode === 'single' && currentSku" class="virtual-sales-modal">
|
||||
<div class="sku-info-item">
|
||||
@@ -199,12 +218,14 @@
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="modalSubmitting" @click="handleCancelVirtualSales">取消</el-button>
|
||||
<el-button type="primary" :loading="modalSubmitting" @click="handleSubmitVirtualSales">
|
||||
提交
|
||||
</el-button>
|
||||
<div class="drawer-actions">
|
||||
<el-button :disabled="modalSubmitting" @click="handleCancelVirtualSales">取消</el-button>
|
||||
<el-button type="primary" :loading="modalSubmitting" @click="handleSubmitVirtualSales">
|
||||
提交
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -239,6 +260,7 @@ const modalMode = ref("single");
|
||||
const currentSku = ref(null);
|
||||
const currentIndex = ref(-1);
|
||||
const selectedRows = ref([]);
|
||||
const salesTab = ref("all");
|
||||
const editForm = reactive({
|
||||
virtualSales: 0,
|
||||
});
|
||||
@@ -254,6 +276,7 @@ const searchForm = ref({
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
let dataRequestId = 0;
|
||||
|
||||
function init() {
|
||||
getDataList();
|
||||
@@ -271,6 +294,12 @@ function handleSearch() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
function handleSalesTabChange() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
selectedRows.value = [];
|
||||
tableRef.value?.clearSelection();
|
||||
getDataList();
|
||||
}
|
||||
function handleReset() {
|
||||
searchForm.value = {
|
||||
pageNumber: 1,
|
||||
@@ -298,9 +327,15 @@ function handleSelectionChange(selection) {
|
||||
selectedRows.value = selection;
|
||||
}
|
||||
function getDataList() {
|
||||
const requestId = ++dataRequestId;
|
||||
loading.value = true;
|
||||
getGoodsSkuData(searchForm.value)
|
||||
const params = { ...searchForm.value };
|
||||
if (salesTab.value !== "all") {
|
||||
params.virtualSalesSet = salesTab.value === "configured";
|
||||
}
|
||||
getGoodsSkuData(params)
|
||||
.then((res) => {
|
||||
if (requestId !== dataRequestId) return;
|
||||
if (res && res.success && res.result) {
|
||||
data.value = (res.result.records || []).map((item) => ({
|
||||
...item,
|
||||
@@ -308,6 +343,10 @@ function getDataList() {
|
||||
saving: false,
|
||||
}));
|
||||
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 = [];
|
||||
} else {
|
||||
data.value = [];
|
||||
@@ -317,13 +356,14 @@ function getDataList() {
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId !== dataRequestId) return;
|
||||
data.value = [];
|
||||
total.value = 0;
|
||||
selectedRows.value = [];
|
||||
ElMessage.error("加载规格列表失败");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
if (requestId === dataRequestId) loading.value = false;
|
||||
});
|
||||
}
|
||||
function getVirtualSalesValue(row) {
|
||||
@@ -354,6 +394,10 @@ function handleCancelVirtualSales() {
|
||||
if (modalSubmitting.value) return;
|
||||
resetVirtualSalesModal();
|
||||
}
|
||||
function handleBeforeCloseVirtualSales(done) {
|
||||
if (modalSubmitting.value) return;
|
||||
done();
|
||||
}
|
||||
function resetVirtualSalesModal() {
|
||||
editModalVisible.value = false;
|
||||
modalMode.value = "single";
|
||||
@@ -388,6 +432,7 @@ function handleSubmitVirtualSales() {
|
||||
}
|
||||
ElMessage.success("虚拟销量设置成功");
|
||||
resetVirtualSalesModal();
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error((res && res.message) || "规格虚拟销量设置失败");
|
||||
}
|
||||
@@ -424,6 +469,7 @@ function handleBatchSubmitVirtualSales(virtualSales) {
|
||||
tableRef.value?.clearSelection();
|
||||
ElMessage.success("虚拟销量设置成功");
|
||||
resetVirtualSalesModal();
|
||||
getDataList();
|
||||
} else {
|
||||
ElMessage.error((res && res.message) || "虚拟销量设置失败");
|
||||
}
|
||||
@@ -445,32 +491,85 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-title {
|
||||
margin-top: 6px;
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
.virtual-sales-drawer {
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0;
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
padding: 10px 0;
|
||||
padding: 4px 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 {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
margin-right: 12px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.goods-text {
|
||||
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;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -493,11 +592,28 @@ onMounted(() => {
|
||||
|
||||
.sku-info-label {
|
||||
width: 90px;
|
||||
color: #808695;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mt_10 {
|
||||
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>
|
||||
|
||||
@@ -77,9 +77,9 @@
|
||||
<div class="counts">{{ notices.appointmentWorkOrder || 0 }}</div>
|
||||
<div>未完成预约工单</div>
|
||||
</div>
|
||||
<div class="todo-item" @click="goProcurementAudit">
|
||||
<div class="counts">{{ notices.procurementAudit || 0 }}</div>
|
||||
<div>待审核采购单</div>
|
||||
<div class="todo-item" @click="goMemberWithdrawApply">
|
||||
<div class="counts">{{ notices.balanceCash || 0 }}</div>
|
||||
<div>待审核会员提现</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,10 +321,10 @@ function goAppointmentWorkOrders() {
|
||||
});
|
||||
}
|
||||
|
||||
function goProcurementAudit() {
|
||||
function goMemberWithdrawApply() {
|
||||
router.push({
|
||||
name: "manager-procurement-order-list",
|
||||
query: { status: "SUBMITTED" },
|
||||
name: "withdrawApply",
|
||||
query: { applyStatus: "APPLY" },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<li>多店铺管理与经营数据分析</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="brand-copyright">Copyright © {{ year }} Lilishop</p>
|
||||
<p class="brand-copyright">Copyright © {{ year }} {{ copyrightOwner }}</p>
|
||||
</div>
|
||||
|
||||
<div class="login-panel" @keyup.enter="submitLogin">
|
||||
@@ -94,11 +94,13 @@ import verify from "@/components/verify";
|
||||
import { setStore } from "@/libs/storage";
|
||||
import { md5 } from "@/utils/md5.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const year = ref(new Date().getFullYear());
|
||||
const { copyrightOwner } = useSiteSettings();
|
||||
const loading = ref(false);
|
||||
const form = reactive({
|
||||
username: "",
|
||||
|
||||
@@ -52,10 +52,11 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
@@ -93,7 +94,7 @@
|
||||
提交
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
<template>
|
||||
<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>
|
||||
</el-row>
|
||||
<el-row justify="center" class="copyright">
|
||||
Copyright © {{ year }} - Present
|
||||
<a :href="website" class="href" target="_blank" style="margin: 0 5px">{{ config.title }}</a>
|
||||
Copyright © {{ year }}
|
||||
<a :href="website || undefined" class="href" target="_blank" style="margin: 0 5px">{{ copyrightOwner }}</a>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import config from "@/config/index";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const year = ref(new Date().getFullYear());
|
||||
// 官网地址优先取接口下发缓存,兜底用本地配置
|
||||
const website = ref(localStorage.getItem("website") || config.website);
|
||||
const { website, copyrightOwner } = useSiteSettings();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const domainLogo = ref("");
|
||||
|
||||
@@ -24,11 +24,9 @@ function applyFavicon(href) {
|
||||
}
|
||||
|
||||
function getSite() {
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result && res.result.settingValue) {
|
||||
const { domainLogo: logo, domainIcon, siteName, website } = JSON.parse(
|
||||
res.result.settingValue
|
||||
);
|
||||
loadSiteSettings().then((data) => {
|
||||
if (data) {
|
||||
const { domainLogo: logo, domainIcon, siteName, website } = data;
|
||||
domainLogo.value = logo || "";
|
||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
localStorage.setItem("icontitle_expiration_time", expirationTime);
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</a>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu v-if="value != 0">
|
||||
<el-dropdown-item v-if="res.balanceCash" command="deposit">
|
||||
<el-badge :value="res.balanceCash">待处理预存款提现申请</el-badge>
|
||||
<el-dropdown-item v-if="res.balanceCash" command="withdrawApply">
|
||||
<el-badge :value="res.balanceCash">待处理会员提现申请</el-badge>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item v-if="res.complain" command="orderComplaint">
|
||||
<el-badge :value="res.complain">待处理投诉审核</el-badge>
|
||||
@@ -28,9 +28,6 @@
|
||||
<el-dropdown-item v-if="res.appointmentWorkOrder" command="manager-appointment-work-orders">
|
||||
<el-badge :value="res.appointmentWorkOrder">未完成预约工单</el-badge>
|
||||
</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>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -63,12 +60,12 @@ const value = computed(() => {
|
||||
});
|
||||
|
||||
function navigateTo(name) {
|
||||
if (name === "manager-appointment-work-orders") {
|
||||
router.push({ name, query: { serviceStatus: "OPEN" } });
|
||||
if (name === "withdrawApply") {
|
||||
router.push({ name: "withdrawApply", query: { applyStatus: "APPLY" } });
|
||||
return;
|
||||
}
|
||||
if (name === "manager-procurement-order-list") {
|
||||
router.push({ name, query: { status: "SUBMITTED" } });
|
||||
if (name === "manager-appointment-work-orders") {
|
||||
router.push({ name, query: { serviceStatus: "OPEN" } });
|
||||
return;
|
||||
}
|
||||
router.push({ name });
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-recharge-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@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-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="充值单号" prop="rechargeSn">
|
||||
@@ -22,59 +31,106 @@
|
||||
v-model="searchForm.rechargeSn"
|
||||
placeholder="请输入充值单号"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 190px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付时间">
|
||||
<el-form-item label="充值时间">
|
||||
<el-date-picker
|
||||
v-model="selectDate"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="rechargeSn" label="订单号" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="充值金额" width="160" sortable>
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<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 }">
|
||||
<priceColorScheme v-if="row" :value="row.rechargeMoney" :color="$mainColor" unit="+" />
|
||||
<span v-if="row" class="recharge-money-text">
|
||||
+¥ {{ formatMoney(row.rechargeMoney) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="充值方式" width="120">
|
||||
|
||||
<!-- 充值方式 -->
|
||||
<el-table-column label="充值方式" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ rechargeWayText(row.rechargeWay) }}</span>
|
||||
{{ row ? rechargeWayText(row.rechargeWay) : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="支付状态" width="120">
|
||||
|
||||
<!-- 支付状态 -->
|
||||
<el-table-column label="支付状态" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="row.payStatus === 'PAID' ? 'success' : 'danger'">
|
||||
{{ row.payStatus === "PAID" ? "已付款" : "未付款" }}
|
||||
</el-tag>
|
||||
{{ row ? (row.payStatus === "PAID" ? "已付款" : "未付款") : "-" }}
|
||||
</template>
|
||||
</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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="充值时间" width="190" />
|
||||
<el-table-column prop="payTime" label="支付时间" width="190" />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -91,49 +147,85 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getUserRecharge } from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "recharge" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
|
||||
const selectDate = ref(null);
|
||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
||||
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
getUserRecharge,
|
||||
{
|
||||
pageSize: 20,
|
||||
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) {
|
||||
if (way === "ALIPAY") return "支付宝";
|
||||
if (way === "WECHAT") return "微信";
|
||||
if (way === "WECHAT") 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) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function selectDateRange(v) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -145,6 +237,84 @@ function selectDateRange(v) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getDataList();
|
||||
});
|
||||
</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>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-wallet-log-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@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-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="支付时间">
|
||||
<el-form-item label="变动时间">
|
||||
<el-date-picker
|
||||
v-model="selectDate"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="100" />
|
||||
<el-table-column label="变动金额" width="150">
|
||||
<!-- 客户ID -->
|
||||
<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 v-if="row">
|
||||
<priceColorScheme
|
||||
v-if="row.money > 0"
|
||||
:value="row.money"
|
||||
color="green"
|
||||
/>
|
||||
<priceColorScheme
|
||||
v-else-if="row.money < 0"
|
||||
:value="row.money"
|
||||
:color="$mainColor"
|
||||
/>
|
||||
<span v-if="row.money > 0" class="money-increase">
|
||||
+¥ {{ formatMoney(row.money) }}
|
||||
</span>
|
||||
<span v-else-if="row.money < 0" class="money-decrease">
|
||||
-¥ {{ formatMoney(Math.abs(row.money)) }}
|
||||
</span>
|
||||
<span v-else class="money-zero">
|
||||
¥ 0.00
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</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 }">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="detail" label="详细" min-width="300" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -86,23 +134,42 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getUserWallet } from "@/api/member";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "walletLog" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
|
||||
const selectDate = ref(null);
|
||||
const { loading, data, total, searchForm, getDataList } = useTable(
|
||||
|
||||
const { loading, data, total, searchForm, getDataList, handleSearch, resetSearch } = useTable(
|
||||
getUserWallet,
|
||||
{
|
||||
pageSize: 20,
|
||||
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 "余额充值";
|
||||
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) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function selectDateRange(v) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -141,6 +231,93 @@ function selectDateRange(v) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getDataList();
|
||||
});
|
||||
</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>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-withdraw-apply-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@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-input
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入客户名称"
|
||||
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 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="VIA_AUDITING" />
|
||||
<el-option label="提现成功" value="SUCCESS" />
|
||||
<el-option label="用户确认" value="WAIT_USER_CONFIRM" />
|
||||
<el-option label="审核拒绝" value="FAIL_AUDITING" />
|
||||
<el-option label="提现成功" value="SUCCESS" />
|
||||
<el-option label="提现失败" value="ERROR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -33,55 +56,90 @@
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
clearable
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 360px"
|
||||
style="width: 320px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
@sort-change="changeSort"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column prop="sn" label="申请编号" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="memberName" label="客户名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="申请金额" width="120">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="memberId" label="客户ID" min-width="170" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<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 }">
|
||||
<priceColorScheme v-if="row" :value="row.applyMoney" :color="$mainColor" />
|
||||
<span v-if="row">¥ {{ formatMoney(row.applyMoney) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提现状态" width="120">
|
||||
|
||||
<!-- 提现状态 -->
|
||||
<el-table-column label="提现状态" width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="applyStatusTagType(row.applyStatus)">
|
||||
{{ applyStatusText(row.applyStatus) }}
|
||||
</el-tag>
|
||||
{{ row ? applyStatusText(row.applyStatus) : "-" }}
|
||||
</template>
|
||||
</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 }">
|
||||
<div v-if="row" class="ops">
|
||||
<a class="link-text" v-if="row.applyStatus === 'APPLY'" @click="openAudit(row)">审核</a>
|
||||
<a class="link-text" v-else @click="openQuery(row)">查看</a>
|
||||
</div>
|
||||
<template v-if="row">
|
||||
<a v-if="row.applyStatus === 'APPLY'" class="link-text" @click="openAudit(row)">审核</a>
|
||||
<a v-else class="link-text" @click="openQuery(row)">查看</a>
|
||||
<template v-if="row.memberId">
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="viewMemberDetail(row)">客户详情</a>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -95,114 +153,170 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog
|
||||
v-model="roleModalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
title="提现申请审核"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="申请编号">
|
||||
<span>{{ showList.sn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称">
|
||||
<span>{{ showList.memberName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额">
|
||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提现状态">
|
||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注">
|
||||
<el-input v-model="audit" type="textarea" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dialog-body">
|
||||
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||
<el-descriptions-item label="申请编号">
|
||||
<span class="font-mono">{{ showList.sn }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">
|
||||
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">
|
||||
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<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 v-if="showList.applyStatus === 'APPLY'">
|
||||
<el-button @click="submitRole(false)">拒绝</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitRole(true)">通过</el-button>
|
||||
</template>
|
||||
<el-button @click="roleModalVisible = false">取消</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
:loading="submitLoading"
|
||||
@click="submitRole(false)"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="submitRole(true)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 查看详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="queryModalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
title="提现申请详情"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="申请编号:">
|
||||
<span>{{ showList.sn }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称:">
|
||||
<span>{{ showList.memberName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额:">
|
||||
<priceColorScheme :value="showList.applyMoney" :color="$mainColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="提现状态:">
|
||||
<span>{{ paramTypeFilter(showList.applyStatus) }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间:">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核时间:">
|
||||
<span>{{ showList.inspectTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核备注:">
|
||||
<span>{{ showList.inspectRemark || "暂无备注" }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dialog-body">
|
||||
<el-descriptions :column="1" border size="small" class="custom-dialog-descriptions">
|
||||
<el-descriptions-item label="申请编号">
|
||||
<span class="font-mono">{{ showList.sn }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户ID">
|
||||
<span class="font-mono">{{ showList.memberId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户名称">
|
||||
<span class="font-semibold">{{ showList.memberName }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span class="money-text font-semibold">¥ {{ formatMoney(showList.applyMoney) }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="提现状态">
|
||||
<el-tag :type="applyStatusTagType(showList.applyStatus)" size="small">
|
||||
{{ paramTypeFilter(showList.applyStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">
|
||||
<span>{{ showList.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
<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 v-if="showList.applyStatus === 'APPLY'">
|
||||
<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>
|
||||
<el-button @click="queryModalVisible = false">关 闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "withdrawApply" });
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const route = useRoute();
|
||||
const searchFormRef = ref(null);
|
||||
// 模板 el-table 的 ref
|
||||
const table = ref(null);
|
||||
const selectDate = ref(null);
|
||||
|
||||
const modalTitle = ref("");
|
||||
const loading = ref(true);
|
||||
const audit = ref("");
|
||||
const roleModalVisible = 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 selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
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) {
|
||||
if (val === "APPLY") return "申请中";
|
||||
@@ -211,41 +325,47 @@ function paramTypeFilter(val) {
|
||||
if (val === "FAIL_AUDITING") return "审核拒绝";
|
||||
if (val === "SUCCESS") return "提现成功";
|
||||
if (val === "ERROR") return "提现失败";
|
||||
return "未知状态";
|
||||
return val || "未知状态";
|
||||
}
|
||||
|
||||
function applyStatusText(status) {
|
||||
if (status === "APPLY") return "申请中";
|
||||
if (status === "VIA_AUDITING") return "审核通过";
|
||||
if (status === "WAIT_USER_CONFIRM") return "用户确认";
|
||||
if (status === "SUCCESS") return "提现成功";
|
||||
if (status === "ERROR") return "提现失败";
|
||||
return "审核拒绝";
|
||||
if (status === "FAIL_AUDITING") return "审核拒绝";
|
||||
return status || "-";
|
||||
}
|
||||
|
||||
function applyStatusTagType(status) {
|
||||
if (status === "APPLY") return "warning";
|
||||
if (status === "VIA_AUDITING") return "success";
|
||||
if (status === "WAIT_USER_CONFIRM") return "warning";
|
||||
if (status === "SUCCESS") return "primary";
|
||||
if (status === "ERROR") return "primary";
|
||||
if (status === "ERROR") return "danger";
|
||||
return "danger";
|
||||
}
|
||||
|
||||
function openAudit(row) {
|
||||
showList.value = { ...row };
|
||||
audit.value = "";
|
||||
roleModalVisible.value = true;
|
||||
}
|
||||
|
||||
function openQuery(row) {
|
||||
showList.value = { ...row };
|
||||
modalTitle.value = "查看";
|
||||
queryModalVisible.value = true;
|
||||
}
|
||||
|
||||
function submitRole(res) {
|
||||
const params = {};
|
||||
params.applyId = showList.value.id;
|
||||
params.result = res;
|
||||
params.remark = audit.value;
|
||||
if (res === false && params.remark === "") {
|
||||
ElMessage.error("审核备注不能为空");
|
||||
const params = {
|
||||
applyId: showList.value.id,
|
||||
result: res,
|
||||
remark: audit.value ? audit.value.trim() : "",
|
||||
};
|
||||
if (res === false && !params.remark) {
|
||||
ElMessage.error("审核拒绝时备注说明不能为空");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
@@ -265,50 +385,36 @@ function submitRole(res) {
|
||||
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();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getDataList();
|
||||
clearSelectAll();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
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) {
|
||||
if (v && v.length === 2) {
|
||||
searchForm.startDate = v[0];
|
||||
@@ -318,30 +424,97 @@ function selectDateRange(v) {
|
||||
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(() => {
|
||||
init();
|
||||
if (route.query && route.query.applyStatus) {
|
||||
searchForm.applyStatus = route.query.applyStatus;
|
||||
}
|
||||
getDataList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ops a {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.member-withdraw-apply-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f6f8fa;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
.ops span {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,24 +1,32 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="member-experience-log-page search">
|
||||
<!-- 顶部筛选表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="90px"
|
||||
label-width="84px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户手机号" prop="mobile">
|
||||
<el-form-item label="客户手机号" prop="memberMobile">
|
||||
<el-input
|
||||
v-model="searchForm.memberMobile"
|
||||
clearable
|
||||
placeholder="请输入客户手机号"
|
||||
style="width: 220px"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则" prop="ruleKey">
|
||||
<el-select v-model="searchForm.ruleKey" clearable filterable style="width: 220px">
|
||||
<el-form-item label="规则类型" prop="ruleKey">
|
||||
<el-select
|
||||
v-model="searchForm.ruleKey"
|
||||
clearable
|
||||
filterable
|
||||
placeholder="全部规则"
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option label="全部规则" value="" />
|
||||
<el-option
|
||||
v-for="item in ruleOptions"
|
||||
:key="item.value"
|
||||
@@ -27,42 +35,103 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-table v-loading="loading" border :data="data" class="mt_10" style="width: 100%">
|
||||
<el-table-column label="客户手机号" width="140" show-overflow-tooltip>
|
||||
<!-- 经验值记录表格 -->
|
||||
<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
|
||||
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 }">
|
||||
<span v-if="row">{{ row.memberMobile || row.mobile || "-" }}</span>
|
||||
{{ row ? (row.memberMobile || row.mobile || "-") : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规则名称" width="150" show-overflow-tooltip>
|
||||
|
||||
<!-- 规则名称 -->
|
||||
<el-table-column label="规则名称" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.ruleName || findRuleName(row.ruleKey) || "-" }}</span>
|
||||
{{ row ? (row.ruleName || findRuleName(row.ruleKey) || "-") : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变化经验值" width="120">
|
||||
|
||||
<!-- 变动前经验值 -->
|
||||
<el-table-column label="变动前经验值" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ formatExperienceValue(row) }}</span>
|
||||
<span v-if="row">{{ row.beforeExperience != null ? formatNumber(row.beforeExperience) : "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="经验值限额" width="120">
|
||||
|
||||
<!-- 变动经验值 -->
|
||||
<el-table-column label="变动经验值" width="120" align="right">
|
||||
<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>
|
||||
</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>
|
||||
<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>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -82,8 +151,10 @@
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useTable } from "@/utils/useTable";
|
||||
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: "REGISTER", label: "注册" },
|
||||
{ value: "SIGN_IN", label: "签到" },
|
||||
@@ -101,7 +172,7 @@ defineOptions({ name: "memberGradeExperienceLog" });
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
|
||||
const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
||||
const { loading, data, total, searchForm, getDataList: getData, resetSearch } = useTable(
|
||||
API_Member.getMemberExperienceByPage,
|
||||
{
|
||||
pageSize: 20,
|
||||
@@ -111,45 +182,138 @@ const { loading, data, total, searchForm, getDataList: getData } = useTable(
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
function formatNumber(val) {
|
||||
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) {
|
||||
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) {
|
||||
const params = {
|
||||
pageNumber: base.pageNumber,
|
||||
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;
|
||||
return params;
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.memberMobile = "";
|
||||
searchForm.ruleKey = "";
|
||||
searchForm.pageNumber = 1;
|
||||
resetSearch();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getData();
|
||||
});
|
||||
</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>
|
||||
|
||||
@@ -1,29 +1,102 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<div class="operation padding-row">
|
||||
<el-button type="primary" @click="openAdd">添加分组</el-button>
|
||||
<div class="member-group-page search">
|
||||
<!-- 顶部筛选与操作栏 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<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>
|
||||
</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="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 label="操作" width="200" align="center" fixed="right">
|
||||
|
||||
<!-- 分组人数 -->
|
||||
<el-table-column prop="memberCount" label="分组人数" width="110" align="center">
|
||||
<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>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="remove(row)">删除</a>
|
||||
</div>
|
||||
<a class="link-text error-text" @click="remove(row)">删除</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -37,34 +110,78 @@
|
||||
</div>
|
||||
</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-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formAdd.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<el-input
|
||||
v-model="formAdd.groupName"
|
||||
maxlength="30"
|
||||
show-word-limit
|
||||
placeholder="请输入分组名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formAdd.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组描述" prop="description">
|
||||
<el-input
|
||||
v-model="formAdd.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
placeholder="请输入分组描述信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<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>
|
||||
</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-input v-model="formEdit.id" style="display: none" />
|
||||
<el-form-item label="分组名称" prop="groupName" style="width: 90%">
|
||||
<el-input v-model="formEdit.groupName" maxlength="30" placeholder="请输入分组名称" />
|
||||
<el-form-item label="分组ID">
|
||||
<span>{{ formEdit.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="分组描述" prop="description" style="width: 90%">
|
||||
<el-input v-model="formEdit.description" maxlength="200" placeholder="请输入分组描述" />
|
||||
<el-form-item label="分组名称" prop="groupName">
|
||||
<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>
|
||||
<template #footer>
|
||||
<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>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -72,11 +189,21 @@
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
|
||||
defineOptions({ name: "memberGroup" });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function viewGroupMembers(row) {
|
||||
router.push({
|
||||
name: "memberList",
|
||||
query: { groupId: row.id },
|
||||
});
|
||||
}
|
||||
|
||||
const addForm = ref(null);
|
||||
const editForm = ref(null);
|
||||
|
||||
@@ -84,6 +211,7 @@ const loading = ref(true);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
groupName: "",
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
@@ -91,44 +219,81 @@ const addFlag = ref(false);
|
||||
const editFlag = ref(false);
|
||||
const submitAddLoading = ref(false);
|
||||
const submitEditLoading = ref(false);
|
||||
|
||||
const formAdd = ref({
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const formEdit = ref({
|
||||
id: "",
|
||||
groupName: "",
|
||||
description: "",
|
||||
});
|
||||
|
||||
const rulesAdd = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
const rulesEdit = reactive({
|
||||
groupName: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
|
||||
});
|
||||
|
||||
function init() {
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.groupName = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getMemberGroupByPage(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
});
|
||||
API_Member.getMemberGroupByPage(searchForm)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
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() {
|
||||
addFlag.value = true;
|
||||
nextTick(() => {
|
||||
@@ -136,20 +301,26 @@ function openAdd() {
|
||||
formAdd.value = { groupName: "", description: "" };
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
addForm.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
submitAddLoading.value = true;
|
||||
API_Member.addMemberGroup(formAdd.value).then((res) => {
|
||||
submitAddLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
addFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
});
|
||||
API_Member.addMemberGroup(formAdd.value)
|
||||
.then((res) => {
|
||||
submitAddLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
addFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitAddLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editFlag.value = true;
|
||||
submitEditLoading.value = false;
|
||||
@@ -169,48 +340,120 @@ function openEdit(row) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
editForm.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
submitEditLoading.value = true;
|
||||
const { id, groupName, description } = formEdit.value;
|
||||
API_Member.updateMemberGroup(id, { groupName, description }).then((res) => {
|
||||
submitEditLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("修改成功");
|
||||
editFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
});
|
||||
API_Member.updateMemberGroup(id, { groupName, description })
|
||||
.then((res) => {
|
||||
submitEditLoading.value = false;
|
||||
if (res && res.success) {
|
||||
ElMessage.success("修改成功");
|
||||
editFlag.value = false;
|
||||
getData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitEditLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
ElMessageBox.confirm("确定删除该分组?", "提示", { type: "warning" }).then(() => {
|
||||
API_Member.deleteMemberGroup(row.id).then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
ElMessageBox.confirm(`确定删除分组 "${row.groupName || "该分组"}" 吗?`, "删除确认", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
API_Member.deleteMemberGroup(row.id).then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getData();
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
getData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ops a {
|
||||
<style scoped lang="scss">
|
||||
.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;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ops span {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
</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">
|
||||
<el-button type="primary" @click="addMember">添加会员</el-button>
|
||||
<el-button
|
||||
@@ -88,6 +93,11 @@
|
||||
</el-table-column>
|
||||
<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 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">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.mobile || "" }}</span>
|
||||
@@ -142,7 +152,7 @@
|
||||
<el-dropdown-item command="edit">编辑会员</el-dropdown-item>
|
||||
<el-dropdown-item command="increaseWallet">增加余额</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>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -308,6 +318,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { ArrowDown } from "@element-plus/icons-vue";
|
||||
import multipleMap from "@/components/map/multiple-map";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
@@ -320,6 +331,8 @@ import defaultPic from "@/assets/default.png";
|
||||
|
||||
defineOptions({ name: "member" });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps({
|
||||
selectedMember: {
|
||||
type: Boolean,
|
||||
@@ -356,6 +369,7 @@ const addMemberForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const memberStatus = ref("ALL");
|
||||
const searchForm = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
@@ -363,7 +377,7 @@ const searchForm = ref({
|
||||
id: "",
|
||||
username: "",
|
||||
mobile: "",
|
||||
disabled: "OPEN",
|
||||
disabled: props.selectedMember ? "OPEN" : "",
|
||||
groupId: "",
|
||||
});
|
||||
const picModelFlag = ref(false);
|
||||
@@ -511,7 +525,15 @@ function submitSetMemberGroup() {
|
||||
const groupId = memberGroupForm.value.groupId;
|
||||
memberGroupLoading.value = true;
|
||||
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) => {
|
||||
memberGroupLoading.value = false;
|
||||
@@ -575,6 +597,11 @@ function handleSearch() {
|
||||
searchForm.value.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
function handleStatusChange(status) {
|
||||
searchForm.value.disabled = status === "ALL" ? "" : status;
|
||||
searchForm.value.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
function editPerm(val) {
|
||||
descTitle.value = `查看用户 ${val.username}`;
|
||||
descFlag.value = true;
|
||||
@@ -598,20 +625,27 @@ function getMemberInfo(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
let memberListRequestId = 0;
|
||||
function getData() {
|
||||
const requestId = ++memberListRequestId;
|
||||
loading.value = true;
|
||||
if (!props.selectedMember) {
|
||||
selectedRows.value = [];
|
||||
clearSelection();
|
||||
}
|
||||
API_Member.getMemberListData(searchForm.value).then((res) => {
|
||||
if (res.result.records) {
|
||||
loading.value = false;
|
||||
init(res.result.records);
|
||||
total.value = res.result.total;
|
||||
syncTableSelection();
|
||||
}
|
||||
});
|
||||
API_Member.getMemberListData({ ...searchForm.value })
|
||||
.then((res) => {
|
||||
if (requestId !== memberListRequestId) return;
|
||||
if (res.success && res.result) {
|
||||
init(res.result.records || []);
|
||||
total.value = res.result.total || 0;
|
||||
syncTableSelection();
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (requestId === memberListRequestId) loading.value = false;
|
||||
});
|
||||
}
|
||||
function openPicSelector() {
|
||||
picModelFlag.value = true;
|
||||
@@ -655,14 +689,15 @@ function detail(row) {
|
||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||
}
|
||||
function disabled(v) {
|
||||
const action = v.disabled ? "禁用" : "启用";
|
||||
let params = {
|
||||
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) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("禁用成功");
|
||||
ElMessage.success(`${action}成功`);
|
||||
getData();
|
||||
}
|
||||
});
|
||||
@@ -758,11 +793,25 @@ function handleSubmitModal() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query && route.query.groupId) {
|
||||
searchForm.value.groupId = String(route.query.groupId);
|
||||
}
|
||||
getData();
|
||||
loadMemberGroupList();
|
||||
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())
|
||||
defineExpose({ getSelection });
|
||||
</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>
|
||||
@@ -1,37 +1,54 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card class="points-statistics-card">
|
||||
<el-row type="flex" justify="space-around" align="middle" class="points-statistics">
|
||||
<div class="member-point-page search">
|
||||
<!-- 顶部数据统计概览看板 -->
|
||||
<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">
|
||||
<div class="points-statistics-title">已发放积分数</div>
|
||||
<div class="points-statistics-subtitle">历史累计发放积分数</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
||||
<div class="stat-inner-box">
|
||||
<div class="points-statistics-title">已发放积分数</div>
|
||||
<div class="points-statistics-subtitle">平台历史累计发放积分总量</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.totalPoint) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" class="points-statistics-item">
|
||||
<div class="points-statistics-title">未使用积分数</div>
|
||||
<div class="points-statistics-subtitle">会员账户未使用积分数</div>
|
||||
<div class="points-statistics-value">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
||||
<div class="stat-inner-box">
|
||||
<div class="points-statistics-title">未使用积分数</div>
|
||||
<div class="points-statistics-subtitle">会员账户当前剩余未使用积分总量</div>
|
||||
<div class="points-statistics-value text-warning">{{ formatNumber(pointsStatistics.unUsedPoint) }}</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 选项卡与数据主区域 -->
|
||||
<div class="point-tabs-wrap">
|
||||
<el-tabs v-model="activeTab" class="point-tabs">
|
||||
<el-tab-pane label="积分列表" name="pointList">
|
||||
<el-card class="point-content-card">
|
||||
<!-- 选项卡 1:会员积分列表 -->
|
||||
<el-tab-pane label="会员积分列表" name="pointList">
|
||||
<!-- 筛选条件 -->
|
||||
<el-card shadow="never" class="point-filter-card">
|
||||
<el-form
|
||||
ref="memberSearchFormRef"
|
||||
:model="memberSearchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@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-input
|
||||
v-model="memberSearchForm.nickName"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户账号" prop="username">
|
||||
@@ -39,82 +56,128 @@
|
||||
v-model="memberSearchForm.username"
|
||||
placeholder="请输入客户账号"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
style="width: 160px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="账号状态" prop="disabled">
|
||||
<el-select
|
||||
v-model="memberSearchForm.disabled"
|
||||
clearable
|
||||
placeholder="请选择账号状态"
|
||||
style="width: 160px"
|
||||
placeholder="全部状态"
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" value="OPEN" />
|
||||
<el-option label="禁用" value="CLOSE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="积分值">
|
||||
<el-form-item label="积分区间">
|
||||
<el-input-number
|
||||
v-model="memberSearchForm.minPoint"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="最小积分值"
|
||||
style="width: 140px"
|
||||
placeholder="最小"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
<span class="point-range-separator">-</span>
|
||||
<el-input-number
|
||||
v-model="memberSearchForm.maxPoint"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
placeholder="最大积分值"
|
||||
style="width: 140px"
|
||||
placeholder="最大"
|
||||
controls-position="right"
|
||||
style="width: 120px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="search-btn" type="primary" @click="handleMemberSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button class="search-btn" type="primary" @click="handleMemberSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleMemberReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<el-table
|
||||
v-loading="memberLoading"
|
||||
border
|
||||
stripe
|
||||
:data="memberData"
|
||||
class="member-table"
|
||||
class="custom-point-table"
|
||||
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>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.nickName || "-" }}</span>
|
||||
<span v-if="row" class="font-semibold text-dark">{{ row.nickName || "-" }}</span>
|
||||
</template>
|
||||
</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 }">
|
||||
<span v-if="row">{{ row.username || "-" }}</span>
|
||||
<span v-if="row" class="font-mono">{{ row.username || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="账号状态" min-width="120" align="center">
|
||||
|
||||
<!-- 账号状态 -->
|
||||
<el-table-column label="账号状态" width="110" align="center">
|
||||
<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 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="积分余额" min-width="120" align="center">
|
||||
|
||||
<!-- 积分余额 -->
|
||||
<el-table-column label="积分余额" width="130" align="right">
|
||||
<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>
|
||||
</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 }">
|
||||
<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>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="memberSearchForm.pageNumber"
|
||||
v-model:page-size="memberSearchForm.pageSize"
|
||||
@@ -128,63 +191,140 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 选项卡 2:积分增减记录 -->
|
||||
<el-tab-pane label="积分增减记录" name="pointChangeRecord">
|
||||
<el-card class="point-content-card">
|
||||
<!-- 筛选条件 -->
|
||||
<el-card shadow="never" class="point-filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@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
|
||||
v-model="searchForm.memberName"
|
||||
placeholder="请输入会员名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button class="search-btn" type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button class="search-btn" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleHistoryReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</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
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
ref="table"
|
||||
class="mt_10 point-table"
|
||||
class="custom-point-table"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="memberName" label="会员名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="content" label="操作内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="beforePoint" label="之前积分" width="200" />
|
||||
<el-table-column label="变动积分" width="200">
|
||||
<!-- 客户ID -->
|
||||
<el-table-column label="客户ID" min-width="170">
|
||||
<template #default="{ row }">
|
||||
<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 v-if="row">
|
||||
<priceColorScheme
|
||||
<el-tag
|
||||
v-if="row.pointType === 'INCREASE'"
|
||||
:value="row.variablePoint"
|
||||
color="green"
|
||||
unit="+"
|
||||
/>
|
||||
<priceColorScheme
|
||||
type="success"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="point-change-tag"
|
||||
>
|
||||
+{{ formatNumber(row.variablePoint) }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-else
|
||||
:value="row.variablePoint"
|
||||
:color="$mainColor"
|
||||
unit="-"
|
||||
/>
|
||||
type="danger"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="point-change-tag"
|
||||
>
|
||||
-{{ formatNumber(row.variablePoint) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</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>
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -207,12 +347,12 @@
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "point" });
|
||||
|
||||
const emit = defineEmits(["callback"]);
|
||||
|
||||
// 模板 ref,script 内未使用
|
||||
const memberSearchFormRef = ref(null);
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
@@ -224,35 +364,60 @@ const pointsStatistics = ref({
|
||||
totalPoint: 0,
|
||||
unUsedPoint: 0,
|
||||
});
|
||||
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
memberId: "",
|
||||
memberName: "",
|
||||
});
|
||||
|
||||
const memberSearchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
id: "",
|
||||
nickName: "",
|
||||
username: "",
|
||||
disabled: "",
|
||||
minPoint: null,
|
||||
maxPoint: null,
|
||||
});
|
||||
|
||||
const data = ref([]);
|
||||
const memberData = ref([]);
|
||||
const memberTotal = ref(0);
|
||||
const total = ref(0);
|
||||
|
||||
function callback(val) {
|
||||
emit("callback", val);
|
||||
function copyText(text, label = "客户ID") {
|
||||
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) {
|
||||
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() {
|
||||
getStatistics();
|
||||
getMemberList();
|
||||
getData();
|
||||
}
|
||||
|
||||
function getStatistics() {
|
||||
API_Member.queryMemberPointsStatistics().then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
@@ -263,45 +428,71 @@ function getStatistics() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
const numericValue = Number(value || 0);
|
||||
if (!Number.isFinite(numericValue)) return "0";
|
||||
return numericValue.toLocaleString();
|
||||
}
|
||||
|
||||
function changePage(v) {
|
||||
searchForm.pageNumber = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize(v) {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = v;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleHistoryReset() {
|
||||
searchForm.memberId = "";
|
||||
searchForm.memberName = "";
|
||||
searchForm.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleMemberSearch() {
|
||||
memberSearchForm.pageNumber = 1;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function handleMemberReset() {
|
||||
memberSearchForm.id = "";
|
||||
memberSearchForm.nickName = "";
|
||||
memberSearchForm.username = "";
|
||||
memberSearchForm.disabled = "";
|
||||
memberSearchForm.minPoint = null;
|
||||
memberSearchForm.maxPoint = null;
|
||||
memberSearchForm.pageNumber = 1;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function changeMemberPage(v) {
|
||||
memberSearchForm.pageNumber = v;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function changeMemberPageSize(v) {
|
||||
memberSearchForm.pageNumber = 1;
|
||||
memberSearchForm.pageSize = v;
|
||||
getMemberList();
|
||||
}
|
||||
|
||||
function getMemberList() {
|
||||
memberLoading.value = true;
|
||||
const params = {
|
||||
pageNumber: memberSearchForm.pageNumber,
|
||||
pageSize: memberSearchForm.pageSize,
|
||||
nickName: memberSearchForm.nickName || undefined,
|
||||
username: memberSearchForm.username || undefined,
|
||||
id: memberSearchForm.id ? memberSearchForm.id.trim() : undefined,
|
||||
nickName: memberSearchForm.nickName ? memberSearchForm.nickName.trim() : undefined,
|
||||
username: memberSearchForm.username ? memberSearchForm.username.trim() : undefined,
|
||||
disabled: memberSearchForm.disabled || undefined,
|
||||
minPoint: memberSearchForm.minPoint,
|
||||
maxPoint: memberSearchForm.maxPoint,
|
||||
@@ -318,31 +509,111 @@ function getMemberList() {
|
||||
memberLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getHistoryPointData(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
const params = {
|
||||
pageNumber: searchForm.pageNumber,
|
||||
pageSize: searchForm.pageSize,
|
||||
memberId: searchForm.memberId ? searchForm.memberId.trim() : undefined,
|
||||
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(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.points-statistics-card {
|
||||
margin-bottom: 10px;
|
||||
.member-point-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-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 {
|
||||
padding: 12px 16px 16px;
|
||||
padding: 14px 18px 18px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(23, 35, 61, 0.04);
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.point-tabs {
|
||||
@@ -352,107 +623,108 @@ onMounted(() => {
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 8px 18px;
|
||||
color: #515a6e;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__active-bar) {
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.points-statistics {
|
||||
width: 100%;
|
||||
}
|
||||
/* 筛选表单 */
|
||||
.point-filter-card {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #ebeef5;
|
||||
background-color: #fafbfe;
|
||||
|
||||
.points-statistics-item {
|
||||
padding: 10px 0;
|
||||
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-card__body) {
|
||||
padding: 16px 16px 2px;
|
||||
}
|
||||
|
||||
:deep(.el-table__header .cell) {
|
||||
padding: 0 16px;
|
||||
line-height: 22px;
|
||||
}
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-table__body td.el-table__cell) {
|
||||
padding: 12px 0;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 14px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-table__body .cell) {
|
||||
padding: 0 16px;
|
||||
line-height: 22px;
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.point-range-separator {
|
||||
display: inline-block;
|
||||
margin: 0 8px;
|
||||
color: #808695;
|
||||
margin: 0 6px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
/* 表格区域卡片 */
|
||||
.point-content-card {
|
||||
border-radius: 6px;
|
||||
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;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
/* 表格单元格 */
|
||||
.custom-point-table {
|
||||
.id-cell {
|
||||
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>
|
||||
|
||||
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
@@ -150,12 +150,11 @@
|
||||
</template>
|
||||
|
||||
<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 { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { unitPrice, customRouterPush } from "@/utils/filters";
|
||||
|
||||
const QUERY_KEY = "manager-order-list-query";
|
||||
const PAGE_SIZES = [20, 50, 100];
|
||||
|
||||
const PROMOTION_OPTIONS = [
|
||||
@@ -201,37 +200,11 @@ function emptySearchForm(pageSize = 20) {
|
||||
};
|
||||
}
|
||||
|
||||
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" });
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
@@ -248,6 +221,25 @@ const total = ref(0);
|
||||
const orderNumData = ref({});
|
||||
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 count = (value) => (value ? `(${value})` : "");
|
||||
return [
|
||||
@@ -340,23 +332,6 @@ function orderStatusTagType(v) {
|
||||
};
|
||||
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() {
|
||||
getDataList();
|
||||
getOrderNumData();
|
||||
@@ -380,7 +355,6 @@ function handleReset() {
|
||||
searchForm.value = emptySearchForm(pageSize);
|
||||
selectDate.value = null;
|
||||
currentStatus.value = "ALL";
|
||||
sessionStorage.removeItem(QUERY_KEY);
|
||||
init();
|
||||
}
|
||||
function selectDateRange(v) {
|
||||
@@ -403,7 +377,6 @@ function countParams() {
|
||||
}
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
rememberQuery();
|
||||
API_Order.getOrderList(searchForm.value)
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
@@ -478,10 +451,24 @@ function getOrderNumData() {
|
||||
});
|
||||
}
|
||||
|
||||
// created
|
||||
restoreQuery();
|
||||
watch(
|
||||
() => route.query,
|
||||
(newQuery) => {
|
||||
if (newQuery && Object.keys(newQuery).length > 0) {
|
||||
applyRouteQuery();
|
||||
searchForm.value.pageNumber = 1;
|
||||
init();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
sessionStorage.removeItem("manager-order-list-query");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
applyRouteQuery();
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -29,6 +29,10 @@
|
||||
<div class="hotzone-preview-box" v-if="options.imageUrl">
|
||||
<img :src="options.imageUrl" alt="hotzone" class="img-preview" />
|
||||
<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-icon class="mr-4"><Picture /></el-icon>
|
||||
更换底图
|
||||
@@ -46,6 +50,9 @@
|
||||
<div class="section-title flex-between">
|
||||
<span>热区列表 ({{ options.zones?.length || 0 }})</span>
|
||||
<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-icon><Plus /></el-icon> 添加热区
|
||||
</el-button>
|
||||
@@ -204,16 +211,25 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 绘制热区弹窗(复用移动端热区绘制器) -->
|
||||
<hotzone ref="hotzoneDialogRef" width="960px" @changeZone="handleChangeZone" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { ref, inject } from 'vue';
|
||||
import { Plus, Delete, Link, Picture, Close, Top, Bottom, Aim } from '@element-plus/icons-vue';
|
||||
import { ElMessageBox, ElMessage } from 'element-plus';
|
||||
import hotzone from '@/components/hotzone';
|
||||
|
||||
const props = defineProps({
|
||||
component: {
|
||||
@@ -298,6 +314,68 @@ function handleAddZone() {
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -299,7 +299,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'channelGrid',
|
||||
defaultRowHeight: 120,
|
||||
defaultRowHeight: '',
|
||||
name: '频道宫格',
|
||||
group: 'navigation',
|
||||
icon: 'Grid',
|
||||
@@ -431,7 +431,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'brandWall',
|
||||
defaultRowHeight: 220,
|
||||
defaultRowHeight: '',
|
||||
name: '品牌图',
|
||||
group: 'media',
|
||||
icon: 'Medal',
|
||||
@@ -513,7 +513,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
defaultOptions: {
|
||||
|
||||
height: 320,
|
||||
height: 450,
|
||||
|
||||
elements: []
|
||||
|
||||
@@ -532,7 +532,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'promoGroup',
|
||||
defaultRowHeight: 180,
|
||||
defaultRowHeight: 220,
|
||||
|
||||
name: '活动卡组',
|
||||
|
||||
@@ -567,13 +567,14 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'video',
|
||||
defaultRowHeight: 360,
|
||||
defaultRowHeight: 450,
|
||||
name: '视频',
|
||||
group: 'media',
|
||||
icon: 'VideoCamera',
|
||||
description: '品牌宣传与商品视频播放',
|
||||
defaultOptions: {
|
||||
videoUrl: '',
|
||||
height: 450,
|
||||
coverUrl: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png',
|
||||
url: '',
|
||||
autoPlay: false,
|
||||
@@ -597,7 +598,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsList',
|
||||
defaultRowHeight: 980,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '商品列表',
|
||||
|
||||
@@ -727,7 +728,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'gridFloor',
|
||||
defaultRowHeight: 560,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '宫格楼层',
|
||||
|
||||
@@ -843,7 +844,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsGrid',
|
||||
defaultRowHeight: 420,
|
||||
defaultRowHeight: '',
|
||||
name: '商品宫格',
|
||||
group: 'commerce',
|
||||
icon: 'Menu',
|
||||
@@ -922,7 +923,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'bannerFloor',
|
||||
defaultRowHeight: 560,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '横幅楼层',
|
||||
|
||||
@@ -964,7 +965,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
{
|
||||
|
||||
type: 'goodsBar',
|
||||
defaultRowHeight: 280,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '商品条',
|
||||
|
||||
@@ -1001,7 +1002,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
|
||||
{type: 'channelFloor',
|
||||
defaultRowHeight: 620,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '频道楼层',
|
||||
|
||||
@@ -1045,7 +1046,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
|
||||
{
|
||||
type: 'recommendFeed',
|
||||
defaultRowHeight: 1270,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '推荐商品',
|
||||
|
||||
@@ -1120,7 +1121,7 @@ export const COMPONENT_DEFINITIONS = [
|
||||
},
|
||||
{
|
||||
type: 'couponList',
|
||||
defaultRowHeight: 160,
|
||||
defaultRowHeight: '',
|
||||
|
||||
name: '优惠券',
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
|
||||
:class="{ 'is-row-selected': isCurrentRowSelected(row.id) }"
|
||||
|
||||
:style="getRowWrapperStyle(row.rowStyle)"
|
||||
:style="getRowWrapperStyle(row.rowStyle, row.components)"
|
||||
|
||||
@click.stop="handleSelectRow(row, rIndex)"
|
||||
|
||||
@@ -289,14 +289,14 @@
|
||||
group="pc-row-components"
|
||||
item-key="id"
|
||||
class="row-components-flex"
|
||||
:style="getRowFlexStyle(row.rowStyle)"
|
||||
:style="getRowFlexStyle(row.rowStyle, row.components)"
|
||||
@change="markDirty"
|
||||
>
|
||||
<template #item="{ element: comp, index: cIndex }">
|
||||
<div
|
||||
class="canvas-comp-wrap"
|
||||
: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)"
|
||||
>
|
||||
<!-- 选中的组件左上角名称标识徽标(万米魔方设计) -->
|
||||
@@ -800,11 +800,19 @@ function clearSelection() {
|
||||
|
||||
function normalizeRows(list) {
|
||||
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 = [];
|
||||
list.forEach(r => {
|
||||
if (!r) return;
|
||||
if (r.rowStyle && (r.rowStyle.height === 40 || r.rowStyle.height === "40")) {
|
||||
r.rowStyle.height = "";
|
||||
if (r.rowStyle) {
|
||||
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);
|
||||
});
|
||||
@@ -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%' };
|
||||
|
||||
const s = {};
|
||||
@@ -1280,8 +1289,12 @@ function getRowWrapperStyle(style) {
|
||||
const h = String(style.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
if (hasAutoComp) {
|
||||
s.minHeight = hVal;
|
||||
} else {
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,7 +1331,8 @@ function getRowWrapperStyle(style) {
|
||||
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%' };
|
||||
const s = {
|
||||
display: 'flex',
|
||||
@@ -1333,8 +1347,12 @@ function getRowFlexStyle(style) {
|
||||
const h = String(style.height).trim();
|
||||
if (h && Number(h) > 0) {
|
||||
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
if (hasAutoComp) {
|
||||
s.minHeight = hVal;
|
||||
} else {
|
||||
s.height = hVal;
|
||||
s.minHeight = hVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
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 {};
|
||||
|
||||
@@ -1358,8 +1377,12 @@ function getCompWrapperStyle(style, rowStyle = {}, totalComps = 1) {
|
||||
const rh = String(rowStyle.height).trim();
|
||||
if (rh && Number(rh) > 0) {
|
||||
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
||||
s.height = rhVal;
|
||||
s.minHeight = rhVal;
|
||||
if (isAutoHeightComp) {
|
||||
s.minHeight = rhVal;
|
||||
} else {
|
||||
s.height = rhVal;
|
||||
s.minHeight = rhVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
const gap = Number(rowStyle?.gap) || 0;
|
||||
@@ -1612,7 +1635,11 @@ function goBack() {
|
||||
|
||||
} 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(() => {
|
||||
|
||||
const id = route.query.id;
|
||||
@@ -1671,7 +1700,10 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
if (autoSaveTimer) clearInterval(autoSaveTimer);
|
||||
if (autoSaveTimer) {
|
||||
clearInterval(autoSaveTimer);
|
||||
autoSaveTimer = null;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -724,7 +724,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
0,
|
||||
0
|
||||
],
|
||||
"height": 180
|
||||
"height": 220
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -901,6 +901,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
"backgroundColor": "transparent"
|
||||
},
|
||||
"options": {
|
||||
"height": 450,
|
||||
"elements": [
|
||||
{
|
||||
"type": "badge",
|
||||
@@ -979,7 +980,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 560
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1061,7 +1062,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 560
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1140,7 +1141,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 620
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1249,7 +1250,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 420
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1404,7 +1405,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 360
|
||||
"height": 450
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1417,6 +1418,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
},
|
||||
"options": {
|
||||
"videoUrl": "https://vjs.zencdn.net/v/oceans.mp4",
|
||||
"height": 450,
|
||||
"coverUrl": "https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png",
|
||||
"url": "/goodsList?brand=1",
|
||||
"title": "双11全球狂欢发布盛典 · 年度旗舰好物真实开箱测评",
|
||||
@@ -1481,7 +1483,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 220
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1656,7 +1658,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 980
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
@@ -1810,7 +1812,7 @@ export const DOUBLE11_PAGE_DOCUMENT = {
|
||||
16
|
||||
],
|
||||
"borderRadius": "8px",
|
||||
"height": 1270
|
||||
"height": ""
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
|
||||
@@ -23,7 +23,7 @@ const props = defineProps({
|
||||
const customStyle = computed(() => {
|
||||
const s = props.componentStyle || {};
|
||||
return {
|
||||
height: `${props.options?.height || 260}px`,
|
||||
height: `${props.options?.height || 450}px`,
|
||||
backgroundColor: s.backgroundColor || '#f8fafc',
|
||||
borderRadius: Array.isArray(s.radius)
|
||||
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
:key="idx"
|
||||
class="zone-mask"
|
||||
:style="{
|
||||
top: `${zone.top || 0}%`,
|
||||
left: `${zone.left || 0}%`,
|
||||
width: `${zone.width || 10}%`,
|
||||
height: `${zone.height || 10}%`
|
||||
top: `${zone.top !== undefined ? zone.top : ((zone.topPer || 0) * 100)}%`,
|
||||
left: `${zone.left !== undefined ? zone.left : ((zone.leftPer || 0) * 100)}%`,
|
||||
width: `${zone.width !== undefined ? zone.width : ((zone.widthPer || 0.1) * 100)}%`,
|
||||
height: `${zone.height !== undefined ? zone.height : ((zone.heightPer || 0.1) * 100)}%`
|
||||
}"
|
||||
>
|
||||
<span class="zone-label">{{ zone.title || `热区 ${idx + 1}` }}</span>
|
||||
|
||||
@@ -32,12 +32,13 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
>
|
||||
<el-form ref="formRef" :model="formAdd" label-width="100px" :rules="formValidate">
|
||||
<el-form-item v-if="showParent" label="上级分类" prop="parentId">
|
||||
@@ -55,7 +56,7 @@
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="Submit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
170
manager/src/views/page/article-manage/articleEdit.vue
Normal file
170
manager/src/views/page/article-manage/articleEdit.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>{{ isEdit ? "编辑文章" : "添加文章" }}</span>
|
||||
</template>
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<el-input v-model="form.title" clearable style="width: 40%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文章分类" prop="categoryId">
|
||||
<el-tree-select
|
||||
v-model="form.categoryId"
|
||||
:data="treeData"
|
||||
:props="{ label: 'title', value: 'value', children: 'children' }"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章排序" prop="sort">
|
||||
<el-input v-model="form.sort" type="number" clearable style="width: 10%" />
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item-view-el" label="文章内容" prop="content">
|
||||
<tinymec v-if="!loading" ref="editor" open-xss v-model="form.content" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否展示" prop="openStatus">
|
||||
<el-switch
|
||||
v-model="form.openStatus"
|
||||
inline-prompt
|
||||
active-text="展示"
|
||||
inactive-text="隐藏"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
<el-button @click="goBack">返回</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
getArticleCategory,
|
||||
saveArticle,
|
||||
updateArticle,
|
||||
seeArticle,
|
||||
} from "@/api/pages";
|
||||
import tinymec from "@/components/editor/index.vue";
|
||||
import { regular } from "@/utils";
|
||||
import { Message } from "@/utils/message";
|
||||
|
||||
const validateArticleContent = (rule, value, callback) => {
|
||||
const html = String(value || "").trim();
|
||||
if (!html) {
|
||||
callback(new Error("请填写文章内容"));
|
||||
return;
|
||||
}
|
||||
const text = html
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /gi, " ")
|
||||
.trim();
|
||||
const hasImage = /<img[\s>]/i.test(html);
|
||||
if (!text && !hasImage) {
|
||||
callback(new Error("请填写文章内容"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
defineOptions({ name: "articleEdit" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const formRef = ref(null);
|
||||
const editor = ref(null);
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const treeData = ref([]);
|
||||
const form = ref({
|
||||
openStatus: false,
|
||||
title: "",
|
||||
categoryId: "",
|
||||
sort: 1,
|
||||
content: "",
|
||||
id: "",
|
||||
});
|
||||
const formValidate = ref({
|
||||
title: [{ required: true, message: "请输入文章标题", trigger: "blur" }],
|
||||
categoryId: [{ required: true, message: "请选择文章分类", trigger: "change" }],
|
||||
sort: [regular.REQUIRED, regular.INTEGER],
|
||||
content: [{ required: true, validator: validateArticleContent, trigger: "change" }],
|
||||
});
|
||||
|
||||
const isEdit = ref(!!route.query.id);
|
||||
|
||||
function getTree(tree = []) {
|
||||
const arr = [];
|
||||
if (!!tree && tree.length !== 0) {
|
||||
tree.forEach((item) => {
|
||||
const obj = {};
|
||||
obj.title = item.articleCategoryName;
|
||||
obj.value = item.id;
|
||||
obj.children = getTree(item.children);
|
||||
arr.push(obj);
|
||||
});
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
function getCategoryTree() {
|
||||
getArticleCategory(0).then((res) => {
|
||||
if (res.success) {
|
||||
treeData.value = getTree(res.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
function getDetail(id) {
|
||||
loading.value = true;
|
||||
seeArticle(id)
|
||||
.then((res) => {
|
||||
if (res.result) {
|
||||
form.value.categoryId = res.result.categoryId;
|
||||
form.value.id = id;
|
||||
form.value.content = res.result.content;
|
||||
form.value.title = res.result.title;
|
||||
form.value.sort = res.result.sort;
|
||||
form.value.openStatus = res.result.openStatus;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (editor.value?.getContent) {
|
||||
form.value.content = editor.value.getContent();
|
||||
}
|
||||
formRef.value.validate((valid) => {
|
||||
if (!valid) return;
|
||||
submitLoading.value = true;
|
||||
const request = isEdit.value ? updateArticle(form.value) : saveArticle(form.value);
|
||||
request
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
Message.success("操作成功");
|
||||
goBack();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
function goBack() {
|
||||
router.back();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getCategoryTree();
|
||||
if (isEdit.value) {
|
||||
getDetail(route.query.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -2,6 +2,9 @@
|
||||
<div class="article-page">
|
||||
<div class="article-page__aside">
|
||||
<el-card class="article-category">
|
||||
<template #header>
|
||||
<span>文章分类</span>
|
||||
</template>
|
||||
<el-tree
|
||||
:data="treeData"
|
||||
:props="{ label: 'title', children: 'children' }"
|
||||
@@ -13,38 +16,43 @@
|
||||
</el-card>
|
||||
</div>
|
||||
<div class="article-page__main">
|
||||
<el-card class="article-detail">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
style="width: 100%"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入文章标题"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-card class="article-search">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入文章标题"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="operation padding-row">
|
||||
<el-card class="article-detail mt_10">
|
||||
<div class="toolbar">
|
||||
<el-button v-if="!selected" type="primary" @click="add">添加</el-button>
|
||||
</div>
|
||||
|
||||
<div class="article-table-wrap">
|
||||
<el-table ref="table" v-loading="loading" border :data="data" style="width: 100%">
|
||||
<el-table-column prop="articleCategoryName" label="分类名称" width="150" />
|
||||
<el-table-column prop="title" label="文章标题" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="是否显示" width="100">
|
||||
<el-table-column label="文章标题" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ decodeEntities(row.title) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否显示" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-if="row"
|
||||
@@ -56,10 +64,10 @@
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="100" />
|
||||
<el-table-column label="操作" width="230" align="center">
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="160" align="right">
|
||||
<template #default="{ row, $index }">
|
||||
<template v-if="row">
|
||||
<div v-if="row" class="ops-row">
|
||||
<a
|
||||
v-if="selected"
|
||||
class="link-text"
|
||||
@@ -71,7 +79,7 @@
|
||||
<a class="link-text" @click="edit(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="remove(row)">删除</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -91,92 +99,20 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-if="!selected"
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="1100px"
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form-item label="文章标题" prop="title">
|
||||
<el-input v-model="form.title" clearable style="width: 40%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文章分类" prop="categoryId">
|
||||
<el-tree-select
|
||||
v-model="form.categoryId"
|
||||
:data="treeDataDefault"
|
||||
:props="{ label: 'title', value: 'value', children: 'children' }"
|
||||
placeholder="请选择"
|
||||
clearable
|
||||
check-strictly
|
||||
style="width: 180px"
|
||||
@change="handleCategoryChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="文章排序" prop="sort">
|
||||
<el-input v-model="form.sort" type="number" clearable style="width: 10%" />
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item-view-el" label="文章内容" prop="content">
|
||||
<tinymec
|
||||
v-if="modalVisible"
|
||||
ref="editor"
|
||||
open-xss
|
||||
v-model="form.content"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否展示" prop="openStatus">
|
||||
<el-switch
|
||||
v-model="form.openStatus"
|
||||
inline-prompt
|
||||
active-text="展示"
|
||||
inactive-text="隐藏"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick, onMounted } from "vue";
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import {
|
||||
getArticleCategory,
|
||||
saveArticle,
|
||||
getArticle,
|
||||
delArticle,
|
||||
updateArticle,
|
||||
seeArticle,
|
||||
updateArticleStatus,
|
||||
} from "@/api/pages";
|
||||
import tinymec from "@/components/editor/index.vue";
|
||||
import { regular } from "@/utils";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
|
||||
const validateArticleContent = (rule, value, callback) => {
|
||||
const html = String(value || "").trim();
|
||||
if (!html) {
|
||||
callback(new Error("请填写文章内容"));
|
||||
return;
|
||||
}
|
||||
const text = html
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ /gi, " ")
|
||||
.trim();
|
||||
const hasImage = /<img[\s>]/i.test(html);
|
||||
if (!text && !hasImage) {
|
||||
callback(new Error("请填写文章内容"));
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
defineProps({
|
||||
selected: {
|
||||
type: Boolean,
|
||||
@@ -185,19 +121,14 @@ defineProps({
|
||||
});
|
||||
const emit = defineEmits(["callbacked"]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 模板 el-form 的 ref(原 ref="searchForm",与 data 同名会互相覆盖,改名为 searchFormRef)
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
// 模板 el-form 的 ref(原 ref="form",与 data 同名会互相覆盖,改名为 formRef)
|
||||
const formRef = ref(null);
|
||||
const editor = ref(null);
|
||||
|
||||
const selectedIndex = ref(99999);
|
||||
const loading = ref(true);
|
||||
const modalType = ref(0);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const treeDataDefault = ref([]);
|
||||
const searchForm = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
@@ -206,24 +137,7 @@ const searchForm = ref({
|
||||
categoryId: "",
|
||||
});
|
||||
const searchTreeValue = ref("");
|
||||
const form = ref({
|
||||
openStatus: false,
|
||||
title: "",
|
||||
categoryId: "",
|
||||
sort: 1,
|
||||
content: "",
|
||||
id: "",
|
||||
});
|
||||
const formValidate = ref({
|
||||
title: [{ required: true, message: "请输入文章标题", trigger: "blur" }],
|
||||
categoryId: [{ required: true, message: "请选择文章分类", trigger: "change" }],
|
||||
sort: [regular.REQUIRED, regular.INTEGER],
|
||||
content: [{ required: true, validator: validateArticleContent, trigger: "change" }],
|
||||
});
|
||||
const list = ref([]);
|
||||
const treeValue = ref("");
|
||||
const treeData = ref([]);
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
@@ -242,15 +156,6 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function getDefaultForm() {
|
||||
return {
|
||||
openStatus: false,
|
||||
title: "",
|
||||
categoryId: "",
|
||||
sort: 1,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
getAllList(0);
|
||||
@@ -268,37 +173,13 @@ function handleCateChange(data) {
|
||||
}
|
||||
function changeSwitch(v) {
|
||||
const params = { status: v.openStatus };
|
||||
updateArticleStatus(v.id, params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
}
|
||||
});
|
||||
updateArticleStatus(v.id, params).then(() => {});
|
||||
}
|
||||
function handleCategoryChange(value) {
|
||||
form.value.categoryId = value;
|
||||
const findTitle = (nodes, id) => {
|
||||
for (const node of nodes || []) {
|
||||
if (node.value === id) return node.title;
|
||||
const childTitle = findTitle(node.children, id);
|
||||
if (childTitle) return childTitle;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
treeValue.value = findTitle(treeDataDefault.value, value);
|
||||
}
|
||||
function handleCheckChange(data) {
|
||||
let value = "";
|
||||
let title = "";
|
||||
list.value = [];
|
||||
data.forEach((item) => {
|
||||
value += `${item.value},`;
|
||||
title += `${item.title},`;
|
||||
});
|
||||
value = value.substring(0, value.length - 1);
|
||||
title = title.substring(0, title.length - 1);
|
||||
list.value.push({ value, title });
|
||||
form.value.categoryId = value;
|
||||
treeValue.value = title;
|
||||
function decodeEntities(str) {
|
||||
if (!str) return "";
|
||||
const txt = document.createElement("textarea");
|
||||
txt.innerHTML = str;
|
||||
return txt.value;
|
||||
}
|
||||
function changePage(v) {
|
||||
searchForm.value.pageNumber = v;
|
||||
@@ -320,7 +201,6 @@ function getAllList(parent_id) {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
treeData.value = getTree(res.result);
|
||||
treeDataDefault.value = getTree(res.result);
|
||||
treeData.value.unshift({
|
||||
title: "全部",
|
||||
level: 0,
|
||||
@@ -360,8 +240,7 @@ function buildArticleSearchParams() {
|
||||
}
|
||||
return params;
|
||||
}
|
||||
function getDataList(val) {
|
||||
if (val) form.value = {};
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
getArticle(buildArticleSearchParams())
|
||||
.then((res) => {
|
||||
@@ -374,63 +253,11 @@ function getDataList(val) {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
function handleSubmit() {
|
||||
if (editor.value?.getContent) {
|
||||
form.value.content = editor.value.getContent();
|
||||
}
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
if (modalType.value === 0) {
|
||||
delete form.value.id;
|
||||
saveArticle(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success("操作成功");
|
||||
getDataList();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
updateArticle(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success("操作成功");
|
||||
getDataList();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function add() {
|
||||
modalType.value = 0;
|
||||
modalTitle.value = "添加文章";
|
||||
treeValue.value = "";
|
||||
form.value = getDefaultForm();
|
||||
modalVisible.value = true;
|
||||
nextTick(() => {
|
||||
formRef.value?.clearValidate?.();
|
||||
});
|
||||
router.push({ name: "article-edit" });
|
||||
}
|
||||
function edit(data) {
|
||||
modalType.value = 1;
|
||||
modalTitle.value = "编辑文章";
|
||||
treeValue.value = "";
|
||||
form.value = getDefaultForm();
|
||||
seeArticle(data.id).then((res) => {
|
||||
if (res.result) {
|
||||
modalVisible.value = true;
|
||||
form.value.categoryId = res.result.categoryId;
|
||||
treeValue.value = data.articleCategoryName;
|
||||
form.value.id = data.id;
|
||||
form.value.content = res.result.content;
|
||||
form.value.title = res.result.title;
|
||||
form.value.sort = res.result.sort;
|
||||
form.value.openStatus = res.result.openStatus;
|
||||
}
|
||||
});
|
||||
function edit(row) {
|
||||
router.push({ name: "article-edit", query: { id: row.id } });
|
||||
}
|
||||
function remove(v) {
|
||||
Modal.confirm({
|
||||
@@ -458,7 +285,7 @@ onMounted(() => {
|
||||
.article-page {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -468,6 +295,8 @@ onMounted(() => {
|
||||
width: 220px;
|
||||
min-width: 180px;
|
||||
max-width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.article-page__main {
|
||||
@@ -476,10 +305,25 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.article-category,
|
||||
.article-search,
|
||||
.article-detail {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.article-category {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ops-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.article-table-wrap {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
<el-empty v-if="!data.length" description="暂无热词,点击上方按钮设置" :image-size="80" />
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@@ -45,7 +45,7 @@
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="详细信息" width="500px" destroy-on-close>
|
||||
<el-drawer v-model="detailVisible" title="详细信息" size="500px" destroy-on-close append-to-body>
|
||||
<el-form ref="formValidate" :model="form" label-width="80px">
|
||||
<el-form-item label="用户名" prop="userName">
|
||||
<span>{{ form.userName }}</span>
|
||||
@@ -84,7 +84,7 @@
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">取消</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,76 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="content-goods-publish">
|
||||
<el-form ref="form" label-width="70px">
|
||||
<div class="base-info-item">
|
||||
<h4>优惠券活动详情</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="活动名称">
|
||||
<span>{{ couponActivity.promotionName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动类型">
|
||||
<span v-if="couponActivity.couponActivityType === 'REGISTERED'">新人发券</span>
|
||||
<span v-else-if="couponActivity.couponActivityType === 'AUTO_COUPON'">自动发券</span>
|
||||
<span v-else>精确发券</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="couponActivity.couponFrequencyEnum != '' && couponActivity.couponFrequencyEnum != null && couponActivity.couponFrequencyEnum != undefined"
|
||||
label="领取频率"
|
||||
>
|
||||
<span v-if="couponActivity.couponFrequencyEnum === 'DAY'">每日</span>
|
||||
<span v-else-if="couponActivity.couponFrequencyEnum === 'WEEK'">每周一次</span>
|
||||
<span v-else>每月一次</span>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="couponActivity.couponActivityType === 'SPECIFY' || couponActivity.couponActivityType === 'AUTO_COUPON'"
|
||||
label="活动范围"
|
||||
>
|
||||
<span v-if="couponActivity.activityScope === 'ALL'">全部会员</span>
|
||||
<span v-else>指定会员</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间">
|
||||
<span>{{ couponActivity.startTime }}~{{ couponActivity.endTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动状态">
|
||||
<span v-if="couponActivity.promotionStatus === 'NEW'">未开始</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'START'">已开始</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'END'">已结束</span>
|
||||
<span v-if="couponActivity.promotionStatus === 'CLOSE'">已关闭</span>
|
||||
</el-form-item>
|
||||
<div class="coupon-activity-info-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>优惠券活动详情</span>
|
||||
<el-tag :type="statusTagType(couponActivity.promotionStatus)" size="small" effect="plain" class="status-tag">
|
||||
{{ statusText(couponActivity.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
<h4>优惠券列表</h4>
|
||||
<el-table :data="couponData" style="width: 100%">
|
||||
<el-table-column prop="couponName" label="优惠券名称" />
|
||||
<el-table-column label="优惠券金额">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span v-if="row.couponType === 'DISCOUNT'">{{ row.couponDiscount }}折</span>
|
||||
<priceColorScheme v-else-if="row.couponType === 'PRICE'" :value="row.price" :color="$mainColor" />
|
||||
<span v-else>未知</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="优惠券类型">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ couponTypeText(row.couponType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="num" label="赠送数量" />
|
||||
</el-table>
|
||||
<template v-if="couponActivity.activityScopeInfo && memberData.length > 0">
|
||||
<h4 class="mt_10">会员列表列表</h4>
|
||||
<el-table :data="memberData" style="width: 100%">
|
||||
<el-table-column prop="id" label="会员id" />
|
||||
<el-table-column prop="nickName" label="昵称" />
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="footer">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
<!-- 活动基本信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">基本信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="活动名称">
|
||||
<span class="font-semibold text-dark">{{ couponActivity.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动类型">
|
||||
<el-tag size="small" effect="light" type="primary">
|
||||
{{ activityTypeText(couponActivity.couponActivityType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="couponActivity.couponFrequencyEnum"
|
||||
label="领取频率"
|
||||
>
|
||||
<span>{{ frequencyText(couponActivity.couponFrequencyEnum) }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="couponActivity.couponActivityType === 'SPECIFY' || couponActivity.couponActivityType === 'AUTO_COUPON'"
|
||||
label="活动范围"
|
||||
>
|
||||
<el-tag size="small" effect="light" :type="couponActivity.activityScope === 'ALL' ? 'success' : 'warning'">
|
||||
{{ couponActivity.activityScope === "ALL" ? "全部会员" : "指定会员" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ couponActivity.startTime || "-" }} 至 {{ couponActivity.endTime || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态" :span="2">
|
||||
<el-tag :type="statusTagType(couponActivity.promotionStatus)" size="small" effect="dark">
|
||||
{{ statusText(couponActivity.promotionStatus) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 关联优惠券列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">赠送优惠券列表</span>
|
||||
<span class="section-badge">共 {{ couponData.length }} 张优惠券</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="couponData"
|
||||
style="width: 100%"
|
||||
empty-text="暂无关联优惠券"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="couponName" label="优惠券名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-dark">{{ row.couponName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="优惠券类型" width="130" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" size="small" effect="light" :type="row.couponType === 'DISCOUNT' ? 'warning' : 'danger'">
|
||||
{{ couponTypeText(row.couponType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="面额 / 折扣" width="140" align="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span v-if="row.couponType === 'DISCOUNT'" class="price-val font-mono font-semibold">
|
||||
{{ row.couponDiscount }} 折
|
||||
</span>
|
||||
<span v-else-if="row.couponType === 'PRICE'" class="price-val font-mono font-semibold">
|
||||
¥ {{ Number(row.price || 0).toFixed(2) }}
|
||||
</span>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="num" label="赠送数量" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-dark">{{ row.num ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 指定会员列表(若存在) -->
|
||||
<el-card
|
||||
v-if="memberData && memberData.length > 0"
|
||||
shadow="never"
|
||||
class="info-card mt_10"
|
||||
>
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">指定参与会员列表</span>
|
||||
<span class="section-badge">共 {{ memberData.length }} 位会员</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="memberData"
|
||||
style="width: 100%"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="id" label="会员ID" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<span class="font-mono text-dark">{{ row.id || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="nickName" label="会员昵称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="font-semibold text-dark">{{ row.nickName || row.username || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -96,65 +182,175 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
function couponTypeText(v) {
|
||||
if (v === "DISCOUNT") return "打折";
|
||||
if (v === "PRICE") return "减免现金";
|
||||
return "未知";
|
||||
if (v === "DISCOUNT") return "打折券";
|
||||
if (v === "PRICE") return "减免现金券";
|
||||
return "未知类型";
|
||||
}
|
||||
|
||||
function activityTypeText(t) {
|
||||
if (t === "REGISTERED") return "新人发券";
|
||||
if (t === "AUTO_COUPON") return "自动发券";
|
||||
if (t === "SPECIFY") return "精确发券";
|
||||
return t || "-";
|
||||
}
|
||||
|
||||
function frequencyText(f) {
|
||||
if (f === "DAY") return "每日一次";
|
||||
if (f === "WEEK") return "每周一次";
|
||||
if (f === "MONTH") return "每月一次";
|
||||
return f || "-";
|
||||
}
|
||||
|
||||
function statusText(s) {
|
||||
if (s === "NEW") return "未开始";
|
||||
if (s === "START") return "已开始";
|
||||
if (s === "END") return "已结束";
|
||||
if (s === "CLOSE") return "已关闭";
|
||||
return s || "-";
|
||||
}
|
||||
|
||||
function statusTagType(s) {
|
||||
if (s === "START") return "success";
|
||||
if (s === "NEW") return "info";
|
||||
if (s === "END") return "danger";
|
||||
if (s === "CLOSE") return "warning";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function getCouponActivity() {
|
||||
if (!id) return;
|
||||
apiGetCouponActivity(id).then((res) => {
|
||||
couponActivity.value = res.result;
|
||||
couponData.value = couponActivity.value.couponActivityItems;
|
||||
memberData.value = JSON.parse(couponActivity.value.activityScopeInfo);
|
||||
if (res && res.success && res.result) {
|
||||
couponActivity.value = res.result;
|
||||
couponData.value = Array.isArray(res.result.couponActivityItems)
|
||||
? res.result.couponActivityItems
|
||||
: [];
|
||||
if (res.result.activityScopeInfo) {
|
||||
try {
|
||||
const parsed = JSON.parse(res.result.activityScopeInfo);
|
||||
memberData.value = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
memberData.value = [];
|
||||
}
|
||||
} else {
|
||||
memberData.value = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("coupon-activity-info");
|
||||
appStore.removeTag("coupon-activity");
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-goods-publish {
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
background: none repeat 0 0 #fff;
|
||||
height: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.coupon-activity-info-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
div.base-info-item {
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-left: 40px;
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.price-val {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.footer {
|
||||
width: 88.7%;
|
||||
padding: 10px;
|
||||
background-color: #ffc;
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
left: 10%;
|
||||
text-align: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,72 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<h4>基本信息</h4>
|
||||
<div class="form-item-view">
|
||||
<div class="coupon-publish-page">
|
||||
<!-- 顶部页面标题与操作栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ isOnlyView ? "平台优惠券详情" : (id ? "编辑平台优惠券" : "添加平台优惠券") }}</span>
|
||||
<el-tag v-if="isOnlyView" type="info" size="small" effect="plain" class="mode-tag">
|
||||
只读模式
|
||||
</el-tag>
|
||||
<el-tag v-else-if="id" type="warning" size="small" effect="plain" class="mode-tag">
|
||||
编辑模式
|
||||
</el-tag>
|
||||
<span v-if="id" class="coupon-id-tip font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
<p class="page-description">配置优惠内容、发放规则及适用商品范围。</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button :icon="ArrowLeft" @click="closeCurrentPage">返回优惠券列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 表单卡片 -->
|
||||
<el-card shadow="never" class="form-card mt_10">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="130px"
|
||||
:rules="formRule"
|
||||
class="custom-coupon-form"
|
||||
>
|
||||
<!-- 模块一:基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<span class="section-title">基本信息</span>
|
||||
<span class="section-subtitle">配置活动名称、券面额及发放规则</span>
|
||||
</div>
|
||||
|
||||
<div class="section-body">
|
||||
<el-form-item label="活动名称" prop="promotionName">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.promotionName"
|
||||
placeholder="活动名称"
|
||||
placeholder="请输入活动名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优惠券名称" prop="couponName">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.couponName"
|
||||
placeholder="优惠券名称"
|
||||
placeholder="请输入优惠券名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="优惠券类型" prop="couponType">
|
||||
<el-select :disabled="disabled" v-model="form.couponType" style="width: 260px">
|
||||
<el-select :disabled="disabled" v-model="form.couponType" style="width: 240px">
|
||||
<el-option label="打折" value="DISCOUNT" />
|
||||
<el-option label="减免现金" value="PRICE" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="折扣"
|
||||
prop="couponDiscount"
|
||||
@@ -42,24 +80,21 @@
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
v-model="form.couponDiscount"
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<span class="describe">请输入0-10的数字,可有一位小数</span>
|
||||
<span class="form-describe">请输入 0.1 ~ 9.9 之间的数值(1位小数)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="面额" prop="price" v-if="form.couponType == 'PRICE'">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.price"
|
||||
placeholder="面额"
|
||||
placeholder="请输入减免面额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动类型" prop="getType">
|
||||
<el-select :disabled="disabled" v-model="form.getType" style="width: 260px">
|
||||
<el-option label="免费领取" value="FREE" />
|
||||
<el-option label="活动赠送" value="ACTIVITY" />
|
||||
</el-select>
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="店铺承担比例" prop="storeCommission">
|
||||
@@ -67,21 +102,23 @@
|
||||
:disabled="disabled"
|
||||
v-model="form.storeCommission"
|
||||
placeholder="店铺承担比例"
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #append>%</template>
|
||||
</el-input>
|
||||
<span class="describe">店铺承担比例,输入0-100之间数值</span>
|
||||
<span class="form-describe">店铺承担的资金比例,范围 0-100</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="发放数量" prop="publishNum" v-if="form.getType === 'FREE'">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.publishNum"
|
||||
placeholder="发放数量"
|
||||
style="width: 260px"
|
||||
placeholder="请输入发放数量"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<div class="tips">如果发放数量为0时,则代表不限制发放数量</div>
|
||||
<span class="form-describe">若设置为 0,则代表不限制发放数量</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="领取数量限制"
|
||||
prop="couponLimitNum"
|
||||
@@ -90,148 +127,243 @@
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.couponLimitNum"
|
||||
placeholder="领取限制"
|
||||
placeholder="每人限领张数"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<div class="tips">如果领取数量为0时,则代表不限制领取数量</div>
|
||||
<span class="form-describe">若设置为 0,则代表不限制每人领取数量</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="范围描述" prop="description">
|
||||
|
||||
<el-form-item label="范围描述" prop="description" class="form-item-wide">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
:rows="3"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
clearable
|
||||
style="width: 260px"
|
||||
placeholder="请输入优惠券适用范围或简要说明"
|
||||
style="width: 480px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<h4>使用限制</h4>
|
||||
<div class="form-item-view">
|
||||
</div>
|
||||
|
||||
<!-- 模块二:使用限制 -->
|
||||
<div class="form-section mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-number">02</span>
|
||||
<span class="section-title">使用限制</span>
|
||||
<span class="section-subtitle">配置消费门槛、有效时间以及指定适用商品范围</span>
|
||||
</div>
|
||||
|
||||
<div class="section-body usage-fields">
|
||||
<el-form-item label="消费门槛" prop="consumeThreshold">
|
||||
<el-input
|
||||
:disabled="disabled"
|
||||
v-model="form.consumeThreshold"
|
||||
placeholder="消费门槛"
|
||||
placeholder="请输入消费门槛金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
style="width: 240px"
|
||||
>
|
||||
<template #prefix>¥</template>
|
||||
</el-input>
|
||||
<span class="form-describe">订单满此金额方可使用该优惠券</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期" prop="rangeTime">
|
||||
<div v-if="form.getType == 'ACTIVITY'">
|
||||
<el-radio-group v-model="rangeTimeType">
|
||||
<el-radio :disabled="disabled" :value="1" v-if="form.getType !== 'ACTIVITY'">起止时间</el-radio>
|
||||
<el-radio :disabled="disabled" :value="0">固定时间</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="rangeTimeType == 1">
|
||||
<el-date-picker
|
||||
:disabled="disabled"
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:disabled-date="options.disabledDate"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</div>
|
||||
<div class="effectiveDays" v-if="rangeTimeType == 0">
|
||||
领取当天开始
|
||||
<el-input-number
|
||||
:disabled="disabled"
|
||||
v-model="form.effectiveDays"
|
||||
:min="1"
|
||||
style="width: 100px"
|
||||
:max="365"
|
||||
/>
|
||||
天内有效(1-365间的整数)
|
||||
<div class="validity-field">
|
||||
<div v-if="form.getType == 'ACTIVITY'" class="validity-type">
|
||||
<el-radio-group v-model="rangeTimeType">
|
||||
<el-radio :disabled="disabled" :value="1" v-if="form.getType !== 'ACTIVITY'">起止时间</el-radio>
|
||||
<el-radio :disabled="disabled" :value="0">固定有效天数</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div v-if="rangeTimeType == 1">
|
||||
<el-date-picker
|
||||
:disabled="disabled"
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
:disabled-date="options.disabledDate"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="effectiveDays-row" v-if="rangeTimeType == 0">
|
||||
<span class="days-label">领取当天开始</span>
|
||||
<el-input-number
|
||||
:disabled="disabled"
|
||||
v-model="form.effectiveDays"
|
||||
:min="1"
|
||||
:max="365"
|
||||
style="width: 130px"
|
||||
/>
|
||||
<span class="days-unit">天内有效</span>
|
||||
</div>
|
||||
<div v-if="rangeTimeType == 0" class="validity-help">有效天数为 1–365 天的整数</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="使用范围" prop="scopeType">
|
||||
<el-radio-group v-model="form.scopeType">
|
||||
<el-radio-button :disabled="disabled" value="ALL">全品类</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="ALL">全品类通用</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="PORTION_GOODS">指定商品</el-radio-button>
|
||||
<el-radio-button :disabled="disabled" value="PORTION_GOODS_CATEGORY">部分商品分类</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item style="width: 100%" v-if="form.scopeType == 'PORTION_GOODS'">
|
||||
<div style="display: flex; margin-bottom: 10px">
|
||||
<el-button :disabled="disabled" type="primary" @click="openSkuList">选择商品</el-button>
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
type="danger"
|
||||
plain
|
||||
style="margin-left: 10px"
|
||||
@click="delSelectGoods"
|
||||
>批量删除</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
border
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ $index }">
|
||||
<!-- 指定适用商品表格 -->
|
||||
<el-form-item
|
||||
v-if="form.scopeType == 'PORTION_GOODS'"
|
||||
:label-width="0"
|
||||
class="portion-goods-form-item"
|
||||
>
|
||||
<div class="portion-goods-wrapper">
|
||||
<div class="portion-goods-toolbar">
|
||||
<div class="portion-goods-heading">
|
||||
<h3>指定商品</h3>
|
||||
<span>已选 {{ form.promotionGoodsList.length }} 件商品</span>
|
||||
</div>
|
||||
<div v-if="!isOnlyView && form.promotionGoodsList.length" class="goods-toolbar-actions">
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
v-if="selectedGoods.length"
|
||||
type="danger"
|
||||
size="small"
|
||||
plain
|
||||
@click="delGoods($index)"
|
||||
>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@click="delSelectGoods"
|
||||
>
|
||||
批量删除 ({{ selectedGoods.length }})
|
||||
</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openSkuList">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-if="form.promotionGoodsList.length"
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂无指定商品数据"
|
||||
class="portion-goods-table"
|
||||
row-key="skuId"
|
||||
scrollbar-always-on
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column
|
||||
v-if="!isOnlyView"
|
||||
type="selection"
|
||||
width="50"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column label="图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row && row.thumbnail"
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
class="table-goods-thumb"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail]"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="table-goods-thumb thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="goodsName"
|
||||
label="商品名称"
|
||||
min-width="280"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="selected-goods-name" :title="row.goodsName">{{ row.goodsName || '-' }}</div>
|
||||
<div class="selected-goods-sku">SKU:{{ row.skuId }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品单价" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="price-text font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="!isOnlyView"
|
||||
label="操作"
|
||||
width="90"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
@click="delGoods($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="portion-goods-empty">
|
||||
<div class="empty-icon"><el-icon :size="28"><ShoppingBag /></el-icon></div>
|
||||
<h3>选择优惠券适用的商品</h3>
|
||||
<p>{{ isOnlyView ? '暂无指定商品' : '仅选中的商品可使用该优惠券,可一次添加多个商品' }}</p>
|
||||
<el-button v-if="!isOnlyView" type="primary" :icon="Plus" @click="openSkuList">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item v-if="form.scopeType == 'PORTION_GOODS_CATEGORY'">
|
||||
<!-- 部分商品分类 -->
|
||||
<el-form-item v-if="form.scopeType == 'PORTION_GOODS_CATEGORY'" label="指定分类">
|
||||
<el-cascader
|
||||
:disabled="disabled"
|
||||
:options="goodsCategoryList"
|
||||
style="width: 260px"
|
||||
placeholder="请选择适用的商品分类"
|
||||
style="width: 360px"
|
||||
v-model="form.scopeIdGoods"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button :disabled="disabled" link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button
|
||||
:disabled="disabled"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>提交</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部固定操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">返回列表</el-button>
|
||||
<el-button
|
||||
v-if="!isOnlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存提交
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { ArrowLeft, Plus, ShoppingBag } from "@element-plus/icons-vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
savePlatformCoupon,
|
||||
@@ -277,7 +409,9 @@ const checkWeight = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const disabled = ref(route.query.onlyView);
|
||||
const isOnlyView = computed(() => route.query.onlyView === "true" || route.query.onlyView === true);
|
||||
const disabled = computed(() => isOnlyView.value);
|
||||
|
||||
const rangeTimeType = ref(1);
|
||||
const modalType = ref(0);
|
||||
const form = ref({
|
||||
@@ -288,7 +422,7 @@ const form = ref({
|
||||
couponType: "PRICE",
|
||||
couponName: "",
|
||||
promotionName: "",
|
||||
getType: "FREE",
|
||||
getType: route.query.getType === "ACTIVITY" ? "ACTIVITY" : "FREE",
|
||||
promotionGoodsList: [],
|
||||
scopeIdGoods: [],
|
||||
rangeDayType: "",
|
||||
@@ -298,6 +432,7 @@ const id = ref(route.query.id);
|
||||
const submitLoading = ref(false);
|
||||
const selectedGoods = ref([]);
|
||||
const goodsCategoryList = ref([]);
|
||||
|
||||
const formRule = reactive({
|
||||
promotionName: [{ required: true, message: "活动名称不能为空" }],
|
||||
couponName: [{ required: true, message: "优惠券名称不能为空" }],
|
||||
@@ -328,6 +463,7 @@ const formRule = reactive({
|
||||
],
|
||||
description: [{ required: true, message: "请输入范围描述" }],
|
||||
});
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -339,6 +475,7 @@ watch(
|
||||
(val) => {
|
||||
if (val == "FREE") {
|
||||
rangeTimeType.value = 1;
|
||||
formRule.rangeTime = [{ required: true, message: "请选择优惠券有效期" }];
|
||||
} else {
|
||||
rangeTimeType.value = 0;
|
||||
}
|
||||
@@ -346,8 +483,9 @@ watch(
|
||||
delete formRule.rangeTime;
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query,
|
||||
(e) => {
|
||||
@@ -355,7 +493,9 @@ watch(
|
||||
if (id.value) {
|
||||
getCoupon();
|
||||
} else {
|
||||
formRef.value.resetFields();
|
||||
formRef.value?.resetFields();
|
||||
modalType.value = 0;
|
||||
form.value.getType = e.getType === "ACTIVITY" ? "ACTIVITY" : "FREE";
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -374,7 +514,7 @@ function getCoupon() {
|
||||
if (!data.promotionGoodsList) data.promotionGoodsList = [];
|
||||
rangeTimeType.value = data.rangeDayType === "DYNAMICTIME" ? 0 : 1;
|
||||
if (data.scopeType == "PORTION_GOODS_CATEGORY") {
|
||||
let prevCascader = data.scopeId.split(",");
|
||||
let prevCascader = data.scopeId ? data.scopeId.split(",") : [];
|
||||
function next(params, prev) {
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const item = params[i];
|
||||
@@ -407,6 +547,7 @@ function getCoupon() {
|
||||
form.value = data;
|
||||
});
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
@@ -465,7 +606,7 @@ function handleSubmit() {
|
||||
savePlatformCoupon(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success("优惠券发送成功");
|
||||
Message.success("优惠券创建成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
@@ -484,11 +625,14 @@ function handleSubmit() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("edit-platform-coupon");
|
||||
appStore.removeTag("add-platform-coupon");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
function openSkuList() {
|
||||
skuSelectRef.value.open("goods");
|
||||
let data = JSON.parse(JSON.stringify(form.value.promotionGoodsList));
|
||||
@@ -497,9 +641,11 @@ function openSkuList() {
|
||||
});
|
||||
skuSelectRef.value.goodsData = data;
|
||||
}
|
||||
|
||||
function changeSelect(e) {
|
||||
selectedGoods.value = e;
|
||||
}
|
||||
|
||||
function delSelectGoods() {
|
||||
if (selectedGoods.value.length <= 0) {
|
||||
Message.warning("您还未选择要删除的数据");
|
||||
@@ -520,9 +666,11 @@ function delSelectGoods() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function delGoods(index) {
|
||||
form.value.promotionGoodsList.splice(index, 1);
|
||||
}
|
||||
|
||||
function selectedGoodsData(item) {
|
||||
let list = [];
|
||||
item.forEach((e) => {
|
||||
@@ -535,7 +683,7 @@ function selectedGoodsData(item) {
|
||||
storeName: e.storeName,
|
||||
skuId: e.id,
|
||||
categoryPath: e.categoryPath,
|
||||
thumbnail: e.small,
|
||||
thumbnail: e.thumbnail || e.small,
|
||||
goodsType: e.goodsType,
|
||||
goodsId: e.goodsId,
|
||||
originPrice: e.price,
|
||||
@@ -543,6 +691,7 @@ function selectedGoodsData(item) {
|
||||
});
|
||||
form.value.promotionGoodsList = list;
|
||||
}
|
||||
|
||||
async function getCagetoryList() {
|
||||
let data = await getCategoryTree();
|
||||
goodsCategoryList.value = data.result;
|
||||
@@ -572,6 +721,7 @@ async function getCagetoryList() {
|
||||
return { value: item.id, label: item.name, children: item.children };
|
||||
});
|
||||
}
|
||||
|
||||
function filterCategoryId(list, idArr) {
|
||||
list.forEach((e) => {
|
||||
if (e instanceof Array) {
|
||||
@@ -584,36 +734,230 @@ function filterCategoryId(list, idArr) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scpoed>
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
.coupon-publish-page {
|
||||
max-width: 1640px;
|
||||
padding: 12px 20px 24px;
|
||||
margin: 0 auto;
|
||||
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.header-card {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
:deep(.el-card__body) { padding: 8px 0 4px; }
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.coupon-id-tip {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
:deep(.el-card__body) { padding: 0; }
|
||||
.custom-coupon-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
background-color: var(--el-bg-color);
|
||||
margin-top: 16px;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-bottom: 14px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.section-body {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
column-gap: 24px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.section-body.usage-fields { display: block; }
|
||||
}
|
||||
}
|
||||
|
||||
.form-describe {
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.validity-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.validity-help {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.effectiveDays-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.el-input-number) {
|
||||
flex: 0 0 130px;
|
||||
width: 130px !important;
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
.days-label,
|
||||
.days-unit {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.portion-goods-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.portion-goods-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.portion-goods-table {
|
||||
.table-goods-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.price-text {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
margin-top: 16px;
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
.page-description { margin: 8px 0 0; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.section-number { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; height: 28px; border-radius: 7px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 12px; font-weight: 500; }
|
||||
.section-header { flex-wrap: wrap; }
|
||||
.section-body > .el-form-item { min-width: 0; margin-bottom: 22px; align-self: start; }
|
||||
.form-item-wide { grid-column: 1 / -1; }
|
||||
:deep(.el-form-item__content) { min-width: 0; }
|
||||
.section-body :deep(.el-input), .section-body :deep(.el-select), .section-body :deep(.el-input-number), .section-body :deep(.el-cascader) { width: 100% !important; max-width: 360px; }
|
||||
.section-body :deep(.el-textarea) { width: 100% !important; max-width: 720px; }
|
||||
.usage-fields :deep(.el-date-editor.el-date-editor--datetimerange) { width: 520px !important; max-width: 100%; }
|
||||
|
||||
.portion-goods-form-item :deep(.el-form-item__content) { margin-left: 0 !important; }
|
||||
.portion-goods-heading, .goods-toolbar-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }
|
||||
.portion-goods-heading h3 { margin: 0; font-size: 15px; font-weight: 500; }
|
||||
.portion-goods-heading span { color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.goods-toolbar-actions :deep(.el-button + .el-button), .footer-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.selected-goods-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.6; }
|
||||
.selected-goods-sku { margin-top: 4px; color: var(--el-text-color-secondary); font: 12px/1.5 Consolas, monospace; overflow-wrap: anywhere; }
|
||||
.portion-goods-table :deep(.el-table__cell) { padding: 12px 0; }
|
||||
.portion-goods-table :deep(th.el-table__cell) { background: var(--el-fill-color-light); font-weight: 400; }
|
||||
.portion-goods-empty { display: flex; align-items: center; justify-content: center; flex-direction: column; min-height: 230px; padding: 24px 20px; box-sizing: border-box; border: 1px dashed var(--el-border-color); border-radius: 8px; background: var(--el-fill-color-lighter); text-align: center; }
|
||||
.portion-goods-empty h3 { margin: 0; font-size: 16px; font-weight: 500; }
|
||||
.portion-goods-empty p { margin: 8px 0 20px; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.empty-icon { display: grid; place-items: center; width: 60px; height: 60px; margin-bottom: 16px; border-radius: 16px; color: var(--el-color-primary); background: var(--el-color-primary-light-9); }
|
||||
@media (max-width: 1100px) {
|
||||
.form-card .form-section .section-body { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
padding: 8px 8px 20px;
|
||||
.page-header-row { flex-wrap: wrap; gap: 16px; }
|
||||
.form-card .form-section { padding: 16px; }
|
||||
.section-subtitle { width: 100%; }
|
||||
.effectiveDays-row { flex-wrap: wrap; }
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -72,18 +72,18 @@
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<div class="operation padding-row" v-if="getType !== 'ACTIVITY'">
|
||||
<list-action-bar v-if="getType !== 'ACTIVITY'">
|
||||
<el-button type="primary" @click="add">添加优惠券</el-button>
|
||||
<el-button class="activity-coupon-button" @click="addActivityCoupon">添加活动优惠券</el-button>
|
||||
<el-button @click="delAll">批量关闭</el-button>
|
||||
<el-button type="warning" @click="receivePage()">优惠券领取记录</el-button>
|
||||
</div>
|
||||
</list-action-bar>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
:data="data"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
row-key="id"
|
||||
@selection-change="changeSelect"
|
||||
@@ -201,6 +201,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ListActionBar from "@/components/list-action-bar.vue";
|
||||
import { ref, computed, watch, nextTick, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
@@ -423,6 +424,10 @@ function init() {
|
||||
function add() {
|
||||
router.push({ name: "add-platform-coupon" });
|
||||
}
|
||||
|
||||
function addActivityCoupon() {
|
||||
router.push({ name: "add-platform-coupon", query: { getType: "ACTIVITY" } });
|
||||
}
|
||||
function changePage(v) {
|
||||
searchForm.value.pageNumber = v;
|
||||
getDataList();
|
||||
@@ -561,6 +566,19 @@ defineExpose({ check, getSelection, clearSelectAll });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.activity-coupon-button {
|
||||
--el-button-text-color: #b35c12;
|
||||
--el-button-bg-color: #fff7ed;
|
||||
--el-button-border-color: #fed7aa;
|
||||
--el-button-hover-text-color: #9a4b0b;
|
||||
--el-button-hover-bg-color: #ffedd5;
|
||||
--el-button-hover-border-color: #fdba74;
|
||||
--el-button-active-text-color: #9a4b0b;
|
||||
--el-button-active-bg-color: #fed7aa;
|
||||
--el-button-active-border-color: #fb923c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
@@ -580,9 +598,6 @@ defineExpose({ check, getSelection, clearSelectAll });
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.padding-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form inline @keyup.enter="load">
|
||||
<el-form-item label="活动名称"><el-input v-model="query.promotionName" clearable /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="load">搜索</el-button></el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" :data="rows" border>
|
||||
<el-table-column prop="promotionName" label="活动名称" />
|
||||
<el-table-column prop="storeName" label="店铺" />
|
||||
<el-table-column prop="startTime" label="开始" width="170" />
|
||||
<el-table-column prop="endTime" label="结束" width="170" />
|
||||
<el-table-column prop="promotionStatus" label="状态" width="100" />
|
||||
</el-table>
|
||||
<el-pagination class="mt_10" v-model:current-page="query.pageNumber" :total="total" @current-change="load" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getFlashDiscountList } from "@/api/promotion.js";
|
||||
|
||||
const { loading, data: rows, total, searchForm: query, getDataList: load } = useTable(
|
||||
getFlashDiscountList,
|
||||
{ pageSize: 20, form: { promotionName: "" } }
|
||||
);
|
||||
|
||||
</script>
|
||||
@@ -1,237 +1,223 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px">
|
||||
<div class="base-info-item">
|
||||
<h4>基本信息</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="活动名称" prop="promotionName">
|
||||
<el-input
|
||||
v-model="form.promotionName"
|
||||
disabled
|
||||
placeholder="活动名称"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
disabled
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
style="width: 320px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动描述" prop="description">
|
||||
<el-input
|
||||
v-model="form.description"
|
||||
disabled
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<h4>优惠设置</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="优惠门槛" prop="fullMoney">
|
||||
<el-input
|
||||
v-model="form.fullMoney"
|
||||
disabled
|
||||
placeholder="优惠门槛"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
<span class="describe">消费达到当前金额可以参与优惠</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="赠送优惠券">
|
||||
<el-radio-group v-model="form.discountType">
|
||||
<el-radio-button value="fullMinusFlag" disabled>减现金</el-radio-button>
|
||||
<el-radio-button value="fullRateFlag" disabled>打折</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.discountType == 'fullMinusFlag'"
|
||||
label="优惠金额"
|
||||
prop="fullMinus"
|
||||
>
|
||||
<el-input
|
||||
disabled
|
||||
v-model="form.fullMinus"
|
||||
placeholder="优惠金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.discountType == 'fullRateFlag'"
|
||||
label="优惠折扣"
|
||||
prop="fullRate"
|
||||
>
|
||||
<el-input-number
|
||||
placeholder="优惠折扣"
|
||||
:max="9.9"
|
||||
:min="0.1"
|
||||
:step="0.1"
|
||||
:precision="1"
|
||||
v-model="form.fullRate"
|
||||
style="width: 260px"
|
||||
/>
|
||||
<span class="describe">优惠折扣为0-10之间数字,可有一位小数</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="额外赠送">
|
||||
<el-checkbox v-model="form.freeFreightFlag" disabled>免邮费</el-checkbox>
|
||||
<el-checkbox v-model="form.couponFlag" disabled>送优惠券</el-checkbox>
|
||||
<el-checkbox v-model="form.giftFlag" disabled>送赠品</el-checkbox>
|
||||
<el-checkbox v-model="form.pointFlag" disabled>送积分</el-checkbox>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.couponFlag" label="赠送优惠券" prop="couponId">
|
||||
<el-select
|
||||
v-model="form.couponId"
|
||||
:disabled="form.promotionStatus != 'NEW'"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="getCouponList"
|
||||
placeholder="输入优惠券名称搜索"
|
||||
:loading="couponLoading"
|
||||
style="width: 280px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in couponList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.couponName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.giftFlag" label="赠品" prop="giftId">
|
||||
<el-select
|
||||
v-model="form.giftId"
|
||||
filterable
|
||||
remote
|
||||
:remote-method="getGiftList"
|
||||
placeholder="输入赠品名称搜索"
|
||||
disabled
|
||||
:loading="giftLoading"
|
||||
style="width: 260px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in giftList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.goodsName"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.pointFlag" label="赠积分" prop="point">
|
||||
<el-input
|
||||
v-model="form.point"
|
||||
type="number"
|
||||
disabled
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="使用范围" prop="scopeType">
|
||||
<el-radio-group v-model="form.scopeType">
|
||||
<el-radio-button value="ALL" disabled>全品类</el-radio-button>
|
||||
<el-radio-button value="PORTION_GOODS" disabled>指定商品</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
style="width: 100%"
|
||||
v-if="form.scopeType == 'PORTION_GOODS'"
|
||||
>
|
||||
<el-table border :data="form.promotionGoodsList" style="width: 100%">
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column label="商品名称" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text mr_10" @click="linkTo(row.goodsId, row.skuId)">{{
|
||||
row.goodsName
|
||||
}}</a>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
|
||||
<template #reference>
|
||||
<img
|
||||
src="../../../assets/qrcode.svg"
|
||||
style="vertical-align: middle"
|
||||
class="hover-pointer"
|
||||
width="20"
|
||||
height="20"
|
||||
alt=""
|
||||
/>
|
||||
</template>
|
||||
<vue-qr
|
||||
:text="wapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="0"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="150"
|
||||
/>
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
|
||||
<div>
|
||||
<el-button @click="$router.push({ name: 'promotions/full-discount' })">返回</el-button>
|
||||
</div>
|
||||
<div class="full-discount-detail-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>满优惠活动详情</span>
|
||||
<el-tag type="info" size="small" effect="plain" class="mode-tag">
|
||||
只读模式
|
||||
</el-tag>
|
||||
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 活动基本信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">基本信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="活动名称">
|
||||
<span class="font-semibold text-dark">{{ form.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动范围">
|
||||
<el-tag size="small" effect="light" :type="form.scopeType === 'ALL' ? 'success' : 'primary'">
|
||||
{{ form.scopeType === "ALL" ? "全品类通用" : "指定商品适用" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ form.startTime || "-" }} 至 {{ form.endTime || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动描述" :span="2">
|
||||
<span class="text-secondary">{{ form.description || "暂无活动描述" }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 优惠规则与赠品权益卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">优惠规则与权益</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="优惠门槛">
|
||||
<span class="money-highlight font-mono font-semibold">
|
||||
满 ¥ {{ Number(form.fullMoney || 0).toFixed(2) }}
|
||||
</span>
|
||||
<span class="text-secondary ml_10" style="font-size: 12px">(达到当前金额可享优惠)</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="优惠形式">
|
||||
<template v-if="form.discountType === 'fullMinusFlag'">
|
||||
<el-tag size="small" effect="light" type="danger">减现金</el-tag>
|
||||
<span class="money-highlight font-mono font-semibold ml_10">
|
||||
减 ¥ {{ Number(form.fullMinus || 0).toFixed(2) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="form.discountType === 'fullRateFlag'">
|
||||
<el-tag size="small" effect="light" type="warning">打折</el-tag>
|
||||
<span class="money-highlight font-mono font-semibold ml_10">
|
||||
打 {{ form.fullRate }} 折
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="text-secondary">无直接立减/打折</span>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="附加赠送权益" :span="2">
|
||||
<div class="extra-rights-row">
|
||||
<el-tag :type="form.freeFreightFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.freeFreightFlag ? "✓ 包邮(免运费)" : "不包邮" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.couponFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.couponFlag ? "✓ 赠送优惠券" : "不送券" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.giftFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.giftFlag ? "✓ 赠送赠品" : "不送赠品" }}
|
||||
</el-tag>
|
||||
<el-tag :type="form.pointFlag ? 'success' : 'info'" effect="plain" size="small">
|
||||
{{ form.pointFlag ? "✓ 赠送积分" : "不送积分" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.couponFlag" label="赠送优惠券" :span="2">
|
||||
<span class="font-semibold text-dark">{{ couponNameText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.giftFlag" label="赠品信息" :span="2">
|
||||
<span class="font-semibold text-dark">{{ giftNameText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="form.pointFlag" label="赠送积分数" :span="2">
|
||||
<span class="font-mono font-semibold text-dark">{{ form.point ?? 0 }} 积分</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 指定参与商品列表(当指定商品时展示) -->
|
||||
<el-card
|
||||
v-if="form.scopeType === 'PORTION_GOODS'"
|
||||
shadow="never"
|
||||
class="info-card mt_10"
|
||||
>
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">指定适用商品列表</span>
|
||||
<span class="section-badge">共 {{ (form.promotionGoodsList || []).length }} 件商品</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
border
|
||||
stripe
|
||||
:data="form.promotionGoodsList || []"
|
||||
style="width: 100%"
|
||||
empty-text="暂无指定商品数据"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row && (row.thumbnail || row.small)"
|
||||
:src="row.thumbnail || row.small"
|
||||
fit="cover"
|
||||
class="table-goods-thumb"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small]"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="table-goods-thumb thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="商品名称" min-width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<span class="goods-title font-semibold text-dark">{{ row.goodsName || "-" }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="商品单价" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="price-val font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回活动列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getPlatformCouponList, getFullDiscountById } from "@/api/promotion";
|
||||
import { getGoodsSkuData } from "@/api/goods";
|
||||
import vueQr from "vue-qr";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "add-full-discount" });
|
||||
defineOptions({ name: "full-discount-detail" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const formRef = ref(null);
|
||||
const id = ref(route.query.id);
|
||||
const form = ref({
|
||||
discountType: "fullMinusFlag",
|
||||
scopeType: "ALL",
|
||||
promotionGoodsList: [],
|
||||
});
|
||||
const id = ref(route.query.id);
|
||||
const couponList = ref([]);
|
||||
const giftList = ref([]);
|
||||
const giftLoading = ref(false);
|
||||
const couponLoading = ref(false);
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
},
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const couponNameText = computed(() => {
|
||||
if (!form.value.couponId) return "未指定优惠券";
|
||||
const found = couponList.value.find((c) => String(c.id) === String(form.value.couponId));
|
||||
return found ? found.couponName : `优惠券 ID: ${form.value.couponId}`;
|
||||
});
|
||||
|
||||
const giftNameText = computed(() => {
|
||||
if (!form.value.giftId) return "未指定赠品";
|
||||
const found = giftList.value.find((g) => String(g.id) === String(form.value.giftId));
|
||||
return found ? found.goodsName : `赠品 ID: ${form.value.giftId}`;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (id.value) {
|
||||
getDetail();
|
||||
}
|
||||
@@ -241,79 +227,194 @@ onMounted(async () => {
|
||||
|
||||
function getDetail() {
|
||||
getFullDiscountById(id.value).then((res) => {
|
||||
let data = res.result;
|
||||
if (!data.scopeType === "ALL") {
|
||||
data.promotionGoodsList = [];
|
||||
if (res && res.success && res.result) {
|
||||
let data = res.result;
|
||||
if (data.scopeType !== "PORTION_GOODS") {
|
||||
data.promotionGoodsList = [];
|
||||
}
|
||||
if (data.fullRateFlag) {
|
||||
data.discountType = "fullRateFlag";
|
||||
} else {
|
||||
data.discountType = "fullMinusFlag";
|
||||
}
|
||||
form.value = data;
|
||||
}
|
||||
if (data.fullMinusFlag) {
|
||||
data.discountType = "fullMinusFlag";
|
||||
delete data.fullMinusFlag;
|
||||
} else {
|
||||
data.discountType = "fullMinusFlag";
|
||||
delete data.fullRateFlag;
|
||||
}
|
||||
data.rangeTime = [];
|
||||
data.rangeTime.push(new Date(data.startTime), new Date(data.endTime));
|
||||
|
||||
form.value = data;
|
||||
});
|
||||
}
|
||||
function getCouponList(query) {
|
||||
let params = {
|
||||
pageSize: 20,
|
||||
|
||||
function getCouponList() {
|
||||
const params = {
|
||||
pageSize: 50,
|
||||
pageNumber: 1,
|
||||
getType: "ACTIVITY",
|
||||
storeId: "",
|
||||
couponName: query,
|
||||
promotionStatus: "START",
|
||||
};
|
||||
couponLoading.value = true;
|
||||
getPlatformCouponList(params).then((res) => {
|
||||
couponLoading.value = false;
|
||||
if (res.success) {
|
||||
couponList.value = res.result.records;
|
||||
if (res && res.success && res.result) {
|
||||
couponList.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
function getGiftList(query) {
|
||||
let params = {
|
||||
pageSize: 20,
|
||||
|
||||
function getGiftList() {
|
||||
const params = {
|
||||
pageSize: 50,
|
||||
pageNumber: 1,
|
||||
id: query === form.value.giftId ? form.value.giftId : null,
|
||||
goodsName: query === form.value.giftId ? null : query,
|
||||
marketEnable: "UPPER",
|
||||
authFlag: "PASS"
|
||||
authFlag: "PASS",
|
||||
};
|
||||
giftLoading.value = true;
|
||||
getGoodsSkuData(params).then((res) => {
|
||||
giftLoading.value = false;
|
||||
if (res.success) {
|
||||
giftList.value = res.result.records;
|
||||
if (res && res.success && res.result) {
|
||||
giftList.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("full-discount-detail");
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
.full-discount-detail-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.section-badge {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.money-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.extra-rights-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.table-goods-thumb {
|
||||
display: block;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.price-val {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.ml_10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,103 +1,188 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card v-loading="detailLoading">
|
||||
<el-form ref="formRef" :model="form" label-width="130px" :rules="onlyView ? {} : formRule">
|
||||
<div class="base-info-item">
|
||||
<h4>{{ pageTitle }}</h4>
|
||||
<div class="form-item-view gift-card-activity-form">
|
||||
<el-form-item label="礼品卡名称" prop="giftCardName">
|
||||
<el-input
|
||||
v-model="form.giftCardName"
|
||||
type="text"
|
||||
placeholder="请输入礼品卡名称"
|
||||
clearable
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
style="width: 360px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="礼品卡类型" prop="cardType" class="gift-card-type-form-item">
|
||||
<el-radio-group v-model="form.cardType" class="gift-card-type-group">
|
||||
<el-radio value="CASH" :disabled="onlyView">现金卡</el-radio>
|
||||
<el-radio value="PICKUP" disabled>提货卡</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="面值" prop="faceValue">
|
||||
<el-input
|
||||
v-model="form.faceValue"
|
||||
type="text"
|
||||
placeholder="面值"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="总库存" prop="stockTotal">
|
||||
<el-input-number
|
||||
v-model="form.stockTotal"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
placeholder="最多可制发凭证张数"
|
||||
style="width: 200px"
|
||||
:disabled="onlyView"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="validityType" :label-width="0" class="gift-card-validity-form-item">
|
||||
<div class="gift-card-validity-outer">
|
||||
<div class="gift-card-validity-label-col">
|
||||
<span v-if="!onlyView" class="gift-card-validity-required">*</span>
|
||||
<span>有效期</span>
|
||||
</div>
|
||||
<div class="gift-card-validity-radios">
|
||||
<el-radio-group v-model="form.validityType" class="gift-card-validity-group">
|
||||
<el-radio value="LONG_TERM" class="gift-card-validity-row" :disabled="onlyView"
|
||||
>长期有效</el-radio
|
||||
>
|
||||
<el-radio value="AFTER_ACTIVATE_MONTHS" class="gift-card-validity-row" :disabled="onlyView">
|
||||
<span>自激活后</span>
|
||||
<el-input-number
|
||||
v-model="form.validMonthsAfterActivate"
|
||||
:min="1"
|
||||
:max="99"
|
||||
:precision="0"
|
||||
placeholder="请输入1-99的整数"
|
||||
:disabled="onlyView || form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
|
||||
class="gift-card-validity-input-gap"
|
||||
/>
|
||||
<span>月内有效</span>
|
||||
</el-radio>
|
||||
<el-radio value="FIXED_UNTIL" class="gift-card-validity-row" :disabled="onlyView">
|
||||
<span>指定有效期至</span>
|
||||
<el-date-picker
|
||||
v-model="form.validEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择结束时间"
|
||||
clearable
|
||||
:disabled="onlyView || form.validityType !== 'FIXED_UNTIL'"
|
||||
class="gift-card-validity-date-gap"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="foot-btn">
|
||||
<el-button style="margin-right: 8px" @click="closeCurrentPage">返回</el-button>
|
||||
<el-button
|
||||
v-if="!onlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>提交</el-button
|
||||
<div class="gift-card-activity-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ pageTitle }}</span>
|
||||
<el-tag
|
||||
v-if="onlyView || isEditMode"
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="onlyView ? 'info' : (isEditMode ? 'warning' : 'primary')"
|
||||
class="mode-tag"
|
||||
>
|
||||
{{ onlyView ? "查看模式" : "编辑模式" }}
|
||||
</el-tag>
|
||||
<span v-if="route.query.id" class="activity-id font-mono">
|
||||
ID: {{ route.query.id }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回礼品卡列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 礼品卡基础信息卡片 -->
|
||||
<el-card shadow="never" v-loading="detailLoading" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">礼品卡配置信息</span>
|
||||
</div>
|
||||
|
||||
<!-- 查看模式:以结构化描述列表呈现 -->
|
||||
<template v-if="onlyView">
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="礼品卡名称">
|
||||
<span class="font-semibold text-dark">{{ form.giftCardName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="礼品卡类型">
|
||||
<el-tag size="small" effect="plain" type="primary">
|
||||
{{ form.cardType === "CASH" ? "现金卡" : "提货卡" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="礼品卡面值">
|
||||
<span class="price-highlight font-mono font-semibold">
|
||||
{{ form.faceValue ? $filters.unitPrice(form.faceValue, "¥") : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="总库存">
|
||||
<span class="font-mono text-dark font-semibold">
|
||||
{{ form.stockTotal != null ? `${form.stockTotal} 张` : "-" }}
|
||||
</span>
|
||||
<span class="hint-inline">(最多可制发凭证张数)</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="有效期限" :span="2">
|
||||
<span class="font-mono text-dark">{{ validityDisplayText }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="activityDetail.createTime" label="创建时间">
|
||||
<span class="font-mono text-secondary">{{ activityDetail.createTime }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="route.query.id" label="活动ID">
|
||||
<span class="font-mono text-secondary">{{ route.query.id }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- 编辑 / 新增模式:表单输入 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="120px"
|
||||
:rules="formRule"
|
||||
class="edit-form"
|
||||
>
|
||||
<el-form-item label="礼品卡名称" prop="giftCardName">
|
||||
<el-input
|
||||
v-model="form.giftCardName"
|
||||
type="text"
|
||||
placeholder="请输入礼品卡名称"
|
||||
clearable
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
style="width: 380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="礼品卡类型" prop="cardType">
|
||||
<el-radio-group v-model="form.cardType">
|
||||
<el-radio value="CASH">现金卡</el-radio>
|
||||
<el-radio value="PICKUP" disabled>提货卡(暂未开放)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="面值" prop="faceValue">
|
||||
<el-input
|
||||
v-model="form.faceValue"
|
||||
type="number"
|
||||
placeholder="请填写面值金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="总库存" prop="stockTotal">
|
||||
<el-input-number
|
||||
v-model="form.stockTotal"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
placeholder="最多可制发凭证张数"
|
||||
style="width: 220px"
|
||||
/>
|
||||
<span class="input-tip-text">张(最多可制发凭证张数)</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期限" required class="validity-form-item">
|
||||
<el-radio-group v-model="form.validityType" class="validity-radio-group">
|
||||
<div class="validity-row">
|
||||
<el-radio value="LONG_TERM">长期有效</el-radio>
|
||||
</div>
|
||||
|
||||
<div class="validity-row">
|
||||
<el-radio value="AFTER_ACTIVATE_MONTHS">
|
||||
<span class="radio-inline-text">自激活后</span>
|
||||
<el-input-number
|
||||
v-model="form.validMonthsAfterActivate"
|
||||
:min="1"
|
||||
:max="99"
|
||||
:precision="0"
|
||||
placeholder="1-99"
|
||||
:disabled="form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
|
||||
class="validity-input-num"
|
||||
/>
|
||||
<span class="radio-inline-text">个月内有效</span>
|
||||
</el-radio>
|
||||
</div>
|
||||
|
||||
<div class="validity-row">
|
||||
<el-radio value="FIXED_UNTIL">
|
||||
<span class="radio-inline-text">指定有效期至</span>
|
||||
<el-date-picker
|
||||
v-model="form.validEndTime"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="选择有效截止时间"
|
||||
clearable
|
||||
:disabled="form.validityType !== 'FIXED_UNTIL'"
|
||||
class="validity-date-picker"
|
||||
style="width: 220px"
|
||||
/>
|
||||
</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">
|
||||
{{ onlyView ? "返回" : "取消" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!onlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ isEditMode ? "保存修改" : "提交创建" }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -121,6 +206,56 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const activityDetail = ref({});
|
||||
|
||||
const form = reactive({
|
||||
giftCardName: "",
|
||||
cardType: "CASH",
|
||||
faceValue: "",
|
||||
stockTotal: null,
|
||||
validityType: "LONG_TERM",
|
||||
validEndTime: null,
|
||||
validMonthsAfterActivate: null,
|
||||
});
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const onlyView = computed(() => {
|
||||
const q = route.query.onlyView;
|
||||
return q === "true" || q === true;
|
||||
});
|
||||
|
||||
const isEditMode = computed(() => {
|
||||
return !!route.query.id && !onlyView.value;
|
||||
});
|
||||
|
||||
const pageTitle = computed(() => {
|
||||
if (onlyView.value) {
|
||||
return "查看礼品卡";
|
||||
}
|
||||
if (isEditMode.value) {
|
||||
return "编辑礼品卡";
|
||||
}
|
||||
return "添加礼品卡";
|
||||
});
|
||||
|
||||
const validityDisplayText = computed(() => {
|
||||
const type = String(form.validityType || "").toUpperCase();
|
||||
if (type === "LONG_TERM") {
|
||||
return "长期有效";
|
||||
}
|
||||
if (type === "AFTER_ACTIVATE_MONTHS") {
|
||||
const m = form.validMonthsAfterActivate;
|
||||
return m != null && m !== "" ? `自激活后 ${m} 个月内有效` : "自激活后指定月数有效";
|
||||
}
|
||||
if (type === "FIXED_UNTIL") {
|
||||
return form.validEndTime ? `指定有效期至 ${form.validEndTime}` : "指定时间有效";
|
||||
}
|
||||
return "-";
|
||||
});
|
||||
|
||||
const checkFaceValue = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("面额不能为空"));
|
||||
@@ -135,6 +270,7 @@ const checkFaceValue = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const validateStock = (rule, value, callback) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
callback(new Error("请填写总库存"));
|
||||
@@ -147,17 +283,6 @@ const validateStock = (rule, value, callback) => {
|
||||
callback();
|
||||
};
|
||||
|
||||
const detailLoading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const form = reactive({
|
||||
giftCardName: "",
|
||||
cardType: "CASH",
|
||||
faceValue: "",
|
||||
stockTotal: null,
|
||||
validityType: "LONG_TERM",
|
||||
validEndTime: null,
|
||||
validMonthsAfterActivate: null,
|
||||
});
|
||||
const formRule = {
|
||||
giftCardName: [
|
||||
{ required: true, message: "活动名称不能为空", trigger: "blur" },
|
||||
@@ -167,51 +292,6 @@ const formRule = {
|
||||
stockTotal: [{ required: true, validator: validateStock, trigger: "blur" }],
|
||||
};
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
const onlyView = computed(() => {
|
||||
const q = route.query.onlyView;
|
||||
return q === "true" || q === true;
|
||||
});
|
||||
const isEditMode = computed(() => {
|
||||
return !!route.query.id && !onlyView.value;
|
||||
});
|
||||
const pageTitle = computed(() => {
|
||||
if (onlyView.value) {
|
||||
return "查看礼品卡";
|
||||
}
|
||||
if (isEditMode.value) {
|
||||
return "编辑礼品卡";
|
||||
}
|
||||
return "添加礼品卡";
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.id) {
|
||||
loadDetail(route.query.id);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => form.validityType,
|
||||
(v) => {
|
||||
if (v !== "FIXED_UNTIL") {
|
||||
form.validEndTime = null;
|
||||
}
|
||||
if (v !== "AFTER_ACTIVATE_MONTHS") {
|
||||
form.validMonthsAfterActivate = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => route.query.id,
|
||||
(id) => {
|
||||
if (id) {
|
||||
loadDetail(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const parseDetailDate = (val) => {
|
||||
if (!val) {
|
||||
return null;
|
||||
@@ -222,14 +302,15 @@ const parseDetailDate = (val) => {
|
||||
const d = new Date(val);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
|
||||
const loadDetail = (id) => {
|
||||
detailLoading.value = true;
|
||||
getGiftCardCashActivity(id)
|
||||
.then((res) => {
|
||||
detailLoading.value = false;
|
||||
if (!res.success || !res.result) {
|
||||
return;
|
||||
}
|
||||
activityDetail.value = res.result;
|
||||
const d = res.result;
|
||||
form.giftCardName = d.giftCardName || "";
|
||||
form.cardType = d.cardType || "CASH";
|
||||
@@ -247,18 +328,26 @@ const loadDetail = (id) => {
|
||||
? d.validMonthsAfterActivate
|
||||
: null;
|
||||
})
|
||||
.catch(() => {
|
||||
.finally(() => {
|
||||
detailLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
const closeCurrentPage = () => {
|
||||
const name = route.name;
|
||||
if (name) {
|
||||
if (name && appStore?.removeTag) {
|
||||
appStore.removeTag(name);
|
||||
}
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
router.go(-1);
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push({ name: "manager-gift-card-cash" });
|
||||
}
|
||||
};
|
||||
|
||||
const validateByValidityType = () => {
|
||||
const t = form.validityType;
|
||||
if (t === "FIXED_UNTIL") {
|
||||
@@ -276,6 +365,7 @@ const validateByValidityType = () => {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildPayload = () => {
|
||||
const payload = {
|
||||
giftCardName: (form.giftCardName || "").trim(),
|
||||
@@ -295,6 +385,7 @@ const buildPayload = () => {
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (onlyView.value) {
|
||||
return;
|
||||
@@ -312,97 +403,221 @@ const handleSubmit = () => {
|
||||
: saveGiftCardCashActivity(buildPayload());
|
||||
req
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success(isEditMode.value ? "礼品卡活动已更新" : "礼品卡活动创建成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => form.validityType,
|
||||
(v) => {
|
||||
if (v !== "FIXED_UNTIL") {
|
||||
form.validEndTime = null;
|
||||
}
|
||||
if (v !== "AFTER_ACTIVATE_MONTHS") {
|
||||
form.validMonthsAfterActivate = null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => route.query.id,
|
||||
(id) => {
|
||||
if (id) {
|
||||
loadDetail(id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.id) {
|
||||
loadDetail(route.query.id);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gift-card-activity-form :deep(.el-form-item) {
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
.gift-card-type-form-item :deep(.el-form-item__label) {
|
||||
line-height: 32px;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-type-form-item :deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 32px;
|
||||
}
|
||||
.gift-card-type-group {
|
||||
gap: 20px;
|
||||
}
|
||||
.gift-card-validity-form-item :deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.gift-card-validity-outer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
.gift-card-validity-label-col {
|
||||
flex: 0 0 130px;
|
||||
width: 130px;
|
||||
padding-right: 12px;
|
||||
<style lang="scss" scoped>
|
||||
.gift-card-activity-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
text-align: right;
|
||||
line-height: 32px;
|
||||
min-height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-validity-required {
|
||||
color: #ed4014;
|
||||
margin-right: 4px;
|
||||
font-family: SimSun, serif;
|
||||
}
|
||||
.gift-card-validity-radios {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-top: 1px;
|
||||
}
|
||||
.gift-card-validity-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
line-height: 32px;
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio:last-child) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.gift-card-validity-group :deep(.el-radio__label) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
line-height: 32px;
|
||||
}
|
||||
.gift-card-validity-input-gap {
|
||||
width: 168px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
.gift-card-validity-date-gap {
|
||||
margin-left: 10px;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 130px;
|
||||
background-color: #f8f9fb;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.price-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.hint-inline {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding: 8px 12px;
|
||||
|
||||
.input-tip-text {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.validity-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
|
||||
.validity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
:deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.el-radio__label) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.radio-inline-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.validity-input-num {
|
||||
width: 130px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.validity-date-picker {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,109 +1,260 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<div class="form-item-view">
|
||||
<h4>商品信息</h4>
|
||||
<div class="kanjia-add-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>添加砍价活动</span>
|
||||
</div>
|
||||
<p class="page-description">设置活动时间,选择参与商品并配置砍价规则。</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button :icon="ArrowLeft" @click="closeCurrentPage">返回砍价列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-form-item label="选择商品" prop="promotionGoodsList">
|
||||
<el-button type="primary" @click="openSkuList">选择商品</el-button>
|
||||
</el-form-item>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="110px"
|
||||
:rules="formRule"
|
||||
class="main-form"
|
||||
>
|
||||
<!-- 活动时间设置卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<span class="section-title">活动时间设置</span>
|
||||
</div>
|
||||
|
||||
<el-form-item style="width: 100%">
|
||||
<el-table
|
||||
border
|
||||
class="kanjia-goods-table"
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="150">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算价格" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].settlementPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最低砍" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].lowestPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="最高砍" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].highestPrice"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动库存" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="form.promotionGoodsList[$index].stock"
|
||||
controls-position="right"
|
||||
class="kanjia-goods-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="small" plain @click="delGoods($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="kanjia-activity-time-form-item">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
<el-form-item
|
||||
label="活动时间"
|
||||
prop="rangeTime"
|
||||
class="kanjia-activity-time-form-item"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择砍价活动有效时间范围"
|
||||
:disabled-date="options.disabledDate"
|
||||
class="activity-date-picker"
|
||||
range-separator="至"
|
||||
/>
|
||||
<span class="time-tip-text">所有参与商品将使用同一活动时间</span>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<!-- 砍价商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-number">02</span>
|
||||
<span class="section-title">砍价活动商品</span>
|
||||
<span class="total-badge">已选 {{ form.promotionGoodsList.length }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button
|
||||
v-if="selectedGoods.length"
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="delBatch"
|
||||
>
|
||||
批量删除 ({{ selectedGoods.length }})
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="form.promotionGoodsList.length"
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="clearAllGoods"
|
||||
>
|
||||
清空商品
|
||||
</el-button>
|
||||
<el-button v-if="form.promotionGoodsList.length" type="primary" :icon="Plus" @click="openSkuList">
|
||||
选择商品
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-form-item prop="promotionGoodsList" :label-width="0" class="table-form-item">
|
||||
<el-table
|
||||
v-if="form.promotionGoodsList.length"
|
||||
scrollbar-always-on
|
||||
class="kanjia-goods-table"
|
||||
:data="form.promotionGoodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂未添加商品,请点击右上角【选择商品】按钮添加"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品信息" min-width="290">
|
||||
<template #default="{ row }">
|
||||
<div class="product-info">
|
||||
<div v-if="row && row.thumbnail" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
<div v-if="row" class="product-details">
|
||||
<div class="product-name text-dark" :title="row.goodsName">{{ row.goodsName || "-" }}</div>
|
||||
<div v-if="row.skuId" class="sku-sub-text font-mono">
|
||||
SKU: {{ row.skuId }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 所属商家 -->
|
||||
<el-table-column
|
||||
prop="storeName"
|
||||
label="所属商家"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ (row && row.storeName) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品原价 -->
|
||||
<el-table-column label="商品原价" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(row.price || row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 当前库存 -->
|
||||
<el-table-column label="可用库存" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 结算价格 -->
|
||||
<el-table-column label="结算价格(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].settlementPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="结算价"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 最低砍 -->
|
||||
<el-table-column label="最低可砍(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].lowestPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="最低可砍"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 最高砍 -->
|
||||
<el-table-column label="最高可砍(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].highestPrice"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="最高可砍"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 活动库存 -->
|
||||
<el-table-column label="活动库存(件)" width="130" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
v-model="form.promotionGoodsList[$index].stock"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:controls="false"
|
||||
class="kanjia-goods-input"
|
||||
placeholder="活动库存"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button
|
||||
type="danger"
|
||||
link
|
||||
@click="delGoods($index)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="goods-empty">
|
||||
<div class="empty-icon"><el-icon :size="28"><ShoppingBag /></el-icon></div>
|
||||
<h3>选择参与砍价的商品</h3>
|
||||
<p>添加商品后,可逐项设置结算价格、可砍金额和活动库存</p>
|
||||
<el-button type="primary" :icon="Plus" @click="openSkuList">选择活动商品</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<span class="footer-summary">已选 <strong>{{ form.promotionGoodsList.length }}</strong> 件商品</span>
|
||||
<div class="footer-buttons">
|
||||
<el-button @click="closeCurrentPage">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
提交创建
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
|
||||
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { nextTick, onMounted, reactive, ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { nextTick, reactive, ref } from "vue";
|
||||
import { ArrowLeft, Plus, ShoppingBag } from "@element-plus/icons-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { saveKanJiaActivityGoods } from "@/api/promotion";
|
||||
import { regular } from "@/utils";
|
||||
import skuSelect from "@/components/lili-dialog";
|
||||
@@ -114,19 +265,24 @@ import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "add-kanjia-activity-goods" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const modalType = ref(0);
|
||||
function getDefaultActivityTime() {
|
||||
const start = new Date();
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 7);
|
||||
return [unixToDate(start.getTime() / 1000), unixToDate(end.getTime() / 1000)];
|
||||
}
|
||||
|
||||
const form = reactive({
|
||||
promotionGoodsList: [],
|
||||
rangeTime: [],
|
||||
rangeTime: getDefaultActivityTime(),
|
||||
});
|
||||
const id = ref(route.query.id);
|
||||
|
||||
const submitLoading = ref(false);
|
||||
const selectedGoods = ref([]);
|
||||
const promotionGoodsList = ref([]);
|
||||
|
||||
const formRule = {
|
||||
promotionGoodsList: [
|
||||
{
|
||||
@@ -146,6 +302,7 @@ const formRule = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -155,14 +312,6 @@ const options = {
|
||||
const formRef = ref(null);
|
||||
const skuSelectRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (id.value) {
|
||||
// 历史遗留:原代码调用了未定义的 getCoupon 方法,此处原样保留
|
||||
getCoupon();
|
||||
modalType.value = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSubmit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (!valid) {
|
||||
@@ -182,18 +331,19 @@ function handleSubmit() {
|
||||
delete params.rangeTime;
|
||||
|
||||
let checkResult = true;
|
||||
form.promotionGoodsList.forEach((res) => {
|
||||
for (let i = 0; i < form.promotionGoodsList.length; i++) {
|
||||
const res = form.promotionGoodsList[i];
|
||||
const eCoupon = isECoupon(res.goodsType);
|
||||
if (res.stock <= 0 || res.stock > res.quantity) {
|
||||
checkResult = false;
|
||||
ElMessage.error("活动库存不能为0且不能超过商品库存");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】活动库存不能为0且不能超过可用库存`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!regular.money.test(res.settlementPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("结算价格金额格式不正确");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】结算价格金额格式不正确`);
|
||||
break;
|
||||
}
|
||||
if (
|
||||
(eCoupon ? res.settlementPrice < 0 : res.settlementPrice <= 0) ||
|
||||
@@ -202,63 +352,78 @@ function handleSubmit() {
|
||||
checkResult = false;
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
? "结算价格不能小于0且不能超过商品价格"
|
||||
: "结算价格金额不能为0且不能超过商品价格"
|
||||
? `商品【${res.goodsName}】结算价格不能小于0且不能超过商品原价`
|
||||
: `商品【${res.goodsName}】结算价格不能为0且不能超过商品原价`
|
||||
);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
if (!regular.money.test(res.highestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最高可砍金额格式错误");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最高可砍金额格式错误`);
|
||||
break;
|
||||
}
|
||||
if (res.highestPrice <= 0 || res.highestPrice > res.price) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最高可砍金额不能为0且不能超过商品价格");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最高可砍金额不能为0且不能超过商品原价`);
|
||||
break;
|
||||
}
|
||||
if (!regular.money.test(res.lowestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最低可砍金额格式错误");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最低可砍金额格式错误`);
|
||||
break;
|
||||
}
|
||||
if (res.lowestPrice < 0 || (!eCoupon && res.lowestPrice <= 0) || res.lowestPrice > res.price) {
|
||||
if (
|
||||
res.lowestPrice < 0 ||
|
||||
(!eCoupon && res.lowestPrice <= 0) ||
|
||||
res.lowestPrice > res.price
|
||||
) {
|
||||
checkResult = false;
|
||||
ElMessage.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
|
||||
return;
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
? `商品【${res.goodsName}】最低可砍金额不能小于0`
|
||||
: `商品【${res.goodsName}】最低可砍金额不能为0`
|
||||
);
|
||||
break;
|
||||
}
|
||||
if (parseInt(res.lowestPrice) > parseInt(res.highestPrice)) {
|
||||
if (parseFloat(res.lowestPrice) > parseFloat(res.highestPrice)) {
|
||||
checkResult = false;
|
||||
ElMessage.error("最低砍价金额不能大于最高砍价金额");
|
||||
return;
|
||||
ElMessage.error(`商品【${res.goodsName}】最低砍价金额不能大于最高砍价金额`);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!checkResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
saveKanJiaActivityGoods(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(
|
||||
modalType.value === 1 ? "砍价活动修改成功" : "砍价活动添加成功"
|
||||
);
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
saveKanJiaActivityGoods(params)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("砍价活动添加成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
if (appStore?.removeTag) {
|
||||
appStore.removeTag("add-kanjia-activity-goods");
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
}
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
function openSkuList() {
|
||||
skuSelectRef.value.open("goods");
|
||||
let data = JSON.parse(JSON.stringify(form.promotionGoodsList));
|
||||
const data = JSON.parse(JSON.stringify(form.promotionGoodsList));
|
||||
data.forEach((e) => {
|
||||
e.id = e.skuId;
|
||||
});
|
||||
@@ -266,7 +431,7 @@ function openSkuList() {
|
||||
}
|
||||
|
||||
function changeSelect(e) {
|
||||
selectedGoods.value = e;
|
||||
selectedGoods.value = e || [];
|
||||
}
|
||||
|
||||
function delGoods(index) {
|
||||
@@ -274,17 +439,33 @@ function delGoods(index) {
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function delBatch() {
|
||||
if (!selectedGoods.value.length) return;
|
||||
const skuIds = new Set(selectedGoods.value.map((g) => g.skuId || g.id));
|
||||
form.promotionGoodsList = form.promotionGoodsList.filter(
|
||||
(g) => !skuIds.has(g.skuId || g.id)
|
||||
);
|
||||
selectedGoods.value = [];
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function clearAllGoods() {
|
||||
form.promotionGoodsList = [];
|
||||
selectedGoods.value = [];
|
||||
touchPromotionGoodsValidate();
|
||||
}
|
||||
|
||||
function touchPromotionGoodsValidate() {
|
||||
nextTick(() => {
|
||||
const form = formRef.value;
|
||||
if (form && typeof form.validateField === "function") {
|
||||
form.validateField("promotionGoodsList", () => {});
|
||||
const f = formRef.value;
|
||||
if (f && typeof f.validateField === "function") {
|
||||
f.validateField("promotionGoodsList", () => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function selectedGoodsData(item) {
|
||||
let list = [];
|
||||
const list = [];
|
||||
item.forEach((e) => {
|
||||
list.push({
|
||||
settlementPrice: e.settlementPrice || 0,
|
||||
@@ -310,55 +491,236 @@ function selectedGoodsData(item) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.kanjia-goods-table {
|
||||
:deep(.el-table__body .el-table__cell .cell) {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
white-space: normal;
|
||||
.kanjia-add-page {
|
||||
padding: 12px 20px 24px;
|
||||
max-width: 1640px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.kanjia-goods-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kanjia-activity-time-form-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 360px !important;
|
||||
max-width: 360px;
|
||||
flex-grow: 0;
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.effectiveDays {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
|
||||
> * {
|
||||
margin: 0 4px;
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #1f2d3d;
|
||||
|
||||
.mode-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.main-form {
|
||||
.table-form-item {
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.time-tip-text {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.kanjia-goods-table {
|
||||
:deep(th.el-table__cell) {
|
||||
background-color: var(--el-fill-color-light);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-weight: 400;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 48px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
margin: 0;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.sku-sub-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.kanjia-goods-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 18px rgb(31 45 61 / 5%);
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-description { margin: 8px 0 0; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.section-number { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; height: 28px; border-radius: 7px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 12px; font-weight: 500; }
|
||||
.kanjia-activity-time-form-item { margin: 16px 0 0 36px; }
|
||||
.kanjia-activity-time-form-item :deep(.el-form-item__content) { min-width: 0; }
|
||||
:deep(.activity-date-picker.el-date-editor) { flex: 0 1 520px; width: 520px; max-width: 100%; }
|
||||
.goods-empty { width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 230px; border: 1px dashed var(--el-border-color); border-radius: 8px; background: var(--el-fill-color-lighter); padding: 24px 20px; text-align: center; }
|
||||
.empty-icon { display: grid; place-items: center; width: 60px; height: 60px; border-radius: 16px; color: var(--el-color-primary); background: var(--el-color-primary-light-9); margin-bottom: 16px; }
|
||||
.goods-empty h3 { margin: 0; font-size: 16px; font-weight: 500; color: var(--el-text-color-primary); }
|
||||
.goods-empty p { margin: 8px 0 20px; font-size: 13px; color: var(--el-text-color-secondary); line-height: 1.6; }
|
||||
.product-info { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
|
||||
.product-details { min-width: 0; }
|
||||
.product-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.6; }
|
||||
.sku-sub-text { overflow-wrap: anywhere; }
|
||||
.footer-summary { font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.footer-summary strong { color: var(--el-color-primary); font-weight: 500; padding: 0 4px; }
|
||||
.footer-buttons { display: flex; gap: 12px; }
|
||||
.footer-buttons :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
@media (max-width: 768px) {
|
||||
padding: 8px 8px 20px;
|
||||
.page-header-row, .card-header-bar { flex-wrap: wrap; gap: 16px; }
|
||||
.info-card :deep(.el-card__body) { padding: 16px; }
|
||||
.kanjia-activity-time-form-item { margin-left: 0; }
|
||||
.kanjia-activity-time-form-item :deep(.el-form-item__label) { width: 80px !important; }
|
||||
.header-title-group { flex-wrap: wrap; }
|
||||
.footer-card :deep(.el-card__body) { padding: 14px 16px; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,94 +1,238 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<div class="form-item-view">
|
||||
<h4>商品信息</h4>
|
||||
<el-form-item label="商品名称">
|
||||
<div>{{ form.goodsName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU编码">
|
||||
<div>{{ form.skuId }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺名称">
|
||||
<div>{{ form.goodsSku.storeName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品价格">
|
||||
<div>
|
||||
<priceColorScheme :value="form.goodsSku.price" :color="$mainColor" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品库存">
|
||||
<div>{{ form.goodsSku.quantity }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算价格" prop="settlementPrice">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.settlementPrice"
|
||||
placeholder="请填写结算价格"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最低可砍" prop="lowestPrice">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.lowestPrice"
|
||||
placeholder="请填写最低可砍金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最高可砍" prop="highestPrice">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.highestPrice"
|
||||
placeholder="请填写最高可砍金额"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动库存" prop="stock">
|
||||
<el-input
|
||||
:disabled="onlyView"
|
||||
type="number"
|
||||
v-model="form.stock"
|
||||
placeholder="请填写活动库存"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="kanjia-activity-time-form-item">
|
||||
<el-date-picker
|
||||
:disabled="onlyView"
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button v-if="!onlyView" type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||
提交
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="kanjia-edit-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ onlyView ? "砍价活动商品详情" : "编辑砍价活动商品" }}</span>
|
||||
<el-tag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="onlyView ? 'info' : 'warning'"
|
||||
class="mode-tag"
|
||||
>
|
||||
{{ onlyView ? "查看模式" : "编辑模式" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="form.promotionStatus"
|
||||
size="small"
|
||||
effect="dark"
|
||||
:type="promotionStatusTagType(form.promotionStatus)"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ promotionStatusText(form.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回砍价列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品基本信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">商品基本信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="商品图片" width="120px">
|
||||
<div class="goods-thumb-box">
|
||||
<el-image
|
||||
v-if="goodsThumbnail"
|
||||
:src="goodsThumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[goodsThumbnail]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="thumb-slot">无图</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品名称">
|
||||
<div class="font-semibold text-dark">{{ form.goodsName || "-" }}</div>
|
||||
<div v-if="form.goodsId" class="sub-id-text font-mono">
|
||||
商品ID: {{ form.goodsId }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="SKU编码">
|
||||
<span class="font-mono text-dark">{{ form.skuId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属店铺">
|
||||
<span class="text-secondary">{{ goodsStoreName }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品原价">
|
||||
<span class="original-price-val font-mono">
|
||||
{{ $filters.unitPrice(goodsPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品库存">
|
||||
<span class="font-mono text-dark">{{ goodsStock }} 件</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 砍价活动配置区域 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">砍价规则与参数</span>
|
||||
</div>
|
||||
|
||||
<!-- 查看模式:以结构化描述列表呈现 -->
|
||||
<template v-if="onlyView">
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="结算价格">
|
||||
<span class="price-highlight font-mono font-semibold">
|
||||
{{ $filters.unitPrice(form.settlementPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动库存">
|
||||
<span class="font-mono text-dark font-semibold">
|
||||
{{ form.stock != null ? `${form.stock} 件` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="最低可砍金额">
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.lowestPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="最高可砍金额">
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.highestPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="form.purchasePrice != null"
|
||||
label="最低购买金额"
|
||||
>
|
||||
<span class="font-mono text-dark">
|
||||
{{ $filters.unitPrice(form.purchasePrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="form.purchasePrice != null ? 1 : 2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ activityTimeRange }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- 编辑模式:表单输入 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="120px"
|
||||
:rules="formRule"
|
||||
class="edit-form"
|
||||
>
|
||||
<el-form-item label="结算价格" prop="settlementPrice">
|
||||
<el-input
|
||||
v-model="form.settlementPrice"
|
||||
type="number"
|
||||
placeholder="请填写结算价格"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最低可砍" prop="lowestPrice">
|
||||
<el-input
|
||||
v-model="form.lowestPrice"
|
||||
type="number"
|
||||
placeholder="请填写最低可砍金额"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最高可砍" prop="highestPrice">
|
||||
<el-input
|
||||
v-model="form.highestPrice"
|
||||
type="number"
|
||||
placeholder="请填写最高可砍金额"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动库存" prop="stock">
|
||||
<el-input
|
||||
v-model="form.stock"
|
||||
type="number"
|
||||
placeholder="请填写活动库存"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>件</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="活动时间"
|
||||
prop="rangeTime"
|
||||
class="kanjia-activity-time-form-item"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择活动时间"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">
|
||||
{{ onlyView ? "返回" : "取消" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!onlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存修改
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getKanJiaActivityGoodsById, editKanJiaActivityGoods } from "@/api/promotion";
|
||||
import { regular } from "@/utils";
|
||||
@@ -103,6 +247,66 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const id = computed(() => route.query.id || "");
|
||||
const onlyView = computed(() => {
|
||||
return route.query.onlyView === "true" || route.query.onlyView === true;
|
||||
});
|
||||
|
||||
const submitLoading = ref(false);
|
||||
const form = ref({
|
||||
purchasePrice: 0,
|
||||
goodsSku: {},
|
||||
rangeTime: [],
|
||||
});
|
||||
|
||||
const goodsThumbnail = computed(() => {
|
||||
return form.value.thumbnail || form.value.goodsSku?.thumbnail || "";
|
||||
});
|
||||
|
||||
const goodsStoreName = computed(() => {
|
||||
return form.value.goodsSku?.storeName || form.value.storeName || "-";
|
||||
});
|
||||
|
||||
const goodsPrice = computed(() => {
|
||||
if (form.value.goodsSku?.price != null) return form.value.goodsSku.price;
|
||||
if (form.value.originalPrice != null) return form.value.originalPrice;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const goodsStock = computed(() => {
|
||||
if (form.value.goodsSku?.quantity != null) return form.value.goodsSku.quantity;
|
||||
return "-";
|
||||
});
|
||||
|
||||
const activityTimeRange = computed(() => {
|
||||
const start = form.value.startTime || (form.value.rangeTime && form.value.rangeTime[0]);
|
||||
const end = form.value.endTime || (form.value.rangeTime && form.value.rangeTime[1]);
|
||||
if (start && end) {
|
||||
return `${start} 至 ${end}`;
|
||||
}
|
||||
return start || end || "-";
|
||||
});
|
||||
|
||||
function promotionStatusText(status) {
|
||||
const map = {
|
||||
NEW: "未开始",
|
||||
START: "已开始",
|
||||
END: "已结束",
|
||||
CLOSE: "已关闭",
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
function promotionStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "info",
|
||||
START: "success",
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
};
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
const checkSettlementPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("结算金额不能为空"));
|
||||
@@ -115,6 +319,7 @@ const checkSettlementPrice = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const checkLowestPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("最低可砍金额不能为空"));
|
||||
@@ -127,6 +332,7 @@ const checkLowestPrice = (rule, value, callback) => {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const checkHighestPrice = (rule, value, callback) => {
|
||||
if (!value && value !== 0) {
|
||||
return callback(new Error("最高可砍金额不能为空"));
|
||||
@@ -140,15 +346,6 @@ const checkHighestPrice = (rule, value, callback) => {
|
||||
}
|
||||
};
|
||||
|
||||
const modalType = ref(0);
|
||||
const form = ref({
|
||||
purchasePrice: 0,
|
||||
goodsSku: {},
|
||||
rangeTime: [],
|
||||
});
|
||||
const id = route.query.id;
|
||||
const onlyView = route.query.onlyView;
|
||||
const submitLoading = ref(false);
|
||||
const formRule = {
|
||||
settlementPrice: [
|
||||
{ required: true, message: "请输入结算金额" },
|
||||
@@ -172,6 +369,7 @@ const formRule = {
|
||||
],
|
||||
stock: [{ required: true, message: "请输入活动库存" }],
|
||||
};
|
||||
|
||||
const options = {
|
||||
disabledDate(date) {
|
||||
return date && date.valueOf() < Date.now() - 86400000;
|
||||
@@ -180,17 +378,21 @@ const options = {
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (id) {
|
||||
onMounted(() => {
|
||||
if (id.value) {
|
||||
getKanJiaActivityGoods();
|
||||
modalType.value = 1;
|
||||
}
|
||||
});
|
||||
|
||||
function getKanJiaActivityGoods() {
|
||||
getKanJiaActivityGoodsById(id).then((res) => {
|
||||
form.value = res.result;
|
||||
form.value.rangeTime = [res.result.startTime, res.result.endTime];
|
||||
getKanJiaActivityGoodsById(id.value).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
form.value = {
|
||||
...res.result,
|
||||
goodsSku: res.result.goodsSku || {},
|
||||
rangeTime: [res.result.startTime, res.result.endTime],
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,7 +415,7 @@ function handleSubmit() {
|
||||
|
||||
const eCoupon = isECoupon(params.goodsType || params.goodsSku?.goodsType);
|
||||
|
||||
if (params.stock <= 0 || params.stock > params.goodsSku.quantity) {
|
||||
if (params.stock <= 0 || (params.goodsSku?.quantity != null && params.stock > params.goodsSku.quantity)) {
|
||||
ElMessage.error("活动库存不能为0且不能超过商品库存");
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +425,7 @@ function handleSubmit() {
|
||||
}
|
||||
if (
|
||||
(eCoupon ? params.settlementPrice < 0 : params.settlementPrice <= 0) ||
|
||||
params.settlementPrice > params.price
|
||||
params.settlementPrice > (params.price || goodsPrice.value)
|
||||
) {
|
||||
ElMessage.error(
|
||||
eCoupon
|
||||
@@ -236,7 +438,7 @@ function handleSubmit() {
|
||||
ElMessage.error("最高可砍金额格式错误");
|
||||
return;
|
||||
}
|
||||
if (params.highestPrice <= 0 || params.highestPrice > params.price) {
|
||||
if (params.highestPrice <= 0 || params.highestPrice > (params.price || goodsPrice.value)) {
|
||||
ElMessage.error("最高可砍金额不能为0且不能超过商品价格");
|
||||
return;
|
||||
}
|
||||
@@ -247,52 +449,212 @@ function handleSubmit() {
|
||||
if (
|
||||
params.lowestPrice < 0 ||
|
||||
(!eCoupon && params.lowestPrice <= 0) ||
|
||||
params.lowestPrice > params.price
|
||||
params.lowestPrice > (params.price || goodsPrice.value)
|
||||
) {
|
||||
ElMessage.error(eCoupon ? "最低可砍金额不能小于0" : "最低可砍金额不能为0");
|
||||
return;
|
||||
}
|
||||
if (params.lowestPrice > params.highestPrice) {
|
||||
if (parseFloat(params.lowestPrice) > parseFloat(params.highestPrice)) {
|
||||
ElMessage.error("最低砍价金额不能大于最高砍价金额");
|
||||
return;
|
||||
}
|
||||
|
||||
submitLoading.value = true;
|
||||
editKanJiaActivityGoods(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("砍价活动修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
editKanJiaActivityGoods(params)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
ElMessage.success("砍价活动修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
if (appStore?.removeTag) {
|
||||
appStore.removeTag("add-kan-jia-goods");
|
||||
appStore.removeTag("edit-kanJia-activity-goods");
|
||||
}
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
router.go(-1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
}
|
||||
.kanjia-edit-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.kanjia-activity-time-form-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 360px !important;
|
||||
max-width: 360px;
|
||||
flex-grow: 0;
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
|
||||
.mode-tag,
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 130px;
|
||||
background-color: #f8f9fb;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-id-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.original-price-val {
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.price-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding: 8px 12px;
|
||||
|
||||
.kanjia-activity-time-form-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 380px !important;
|
||||
max-width: 380px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form inline @keyup.enter="load">
|
||||
<el-form-item label="活动名称"><el-input v-model="query.promotionName" clearable /></el-form-item>
|
||||
<el-form-item><el-button type="primary" @click="load">搜索</el-button></el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-card class="mt_10">
|
||||
<el-table v-loading="loading" :data="rows" border>
|
||||
<el-table-column prop="promotionName" label="活动名称" />
|
||||
<el-table-column prop="storeName" label="店铺" />
|
||||
<el-table-column prop="nthNum" label="第N件" width="80" />
|
||||
<el-table-column prop="discountType" label="优惠方式" width="100" />
|
||||
<el-table-column prop="startTime" label="开始" width="170" />
|
||||
<el-table-column prop="endTime" label="结束" width="170" />
|
||||
<el-table-column prop="promotionStatus" label="状态" width="100" />
|
||||
</el-table>
|
||||
<el-pagination class="mt_10" v-model:current-page="query.pageNumber" :total="total" @current-change="load" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useTable } from "@/utils/useTable";
|
||||
import { getNthItemDiscountList } from "@/api/promotion.js";
|
||||
|
||||
const { loading, data: rows, total, searchForm: query, getDataList: load } = useTable(
|
||||
getNthItemDiscountList,
|
||||
{ pageSize: 20, form: { promotionName: "" } }
|
||||
);
|
||||
</script>
|
||||
@@ -1,98 +1,290 @@
|
||||
<template>
|
||||
<div class="pintuan-goods">
|
||||
<el-card>
|
||||
<h4>活动详情</h4>
|
||||
<el-table border :data="data" style="width: 100%; margin: 10px 0">
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" />
|
||||
<el-table-column prop="startTime" label="活动开始时间" min-width="120" />
|
||||
<el-table-column prop="endTime" label="活动结束时间" min-width="120" />
|
||||
<el-table-column prop="requiredNum" label="成团人数" min-width="90" />
|
||||
<el-table-column prop="limitNum" label="限购数量" min-width="90" />
|
||||
<el-table-column label="状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="promotionStatusTagType(row.promotionStatus)">
|
||||
{{ promotionStatusText(row.promotionStatus) }}
|
||||
<div class="pintuan-goods-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>拼团活动商品管理</span>
|
||||
<el-tag
|
||||
v-if="pintuanInfo.promotionStatus"
|
||||
:type="promotionStatusTagType(pintuanInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ promotionStatusText(pintuanInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span v-if="pintuanId" class="activity-id font-mono">ID: {{ pintuanId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回拼团列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 活动信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">活动信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="3" border class="custom-descriptions">
|
||||
<el-descriptions-item label="活动名称">
|
||||
<span class="font-semibold text-dark">{{ pintuanInfo.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态">
|
||||
<el-tag
|
||||
v-if="pintuanInfo.promotionStatus"
|
||||
:type="promotionStatusTagType(pintuanInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ promotionStatusText(pintuanInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属店铺">
|
||||
<span class="text-secondary">{{ pintuanInfo.storeName || "全平台 / 自营" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动开始时间">
|
||||
<span class="font-mono text-dark">{{ pintuanInfo.startTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动结束时间">
|
||||
<span class="font-mono text-dark">{{ pintuanInfo.endTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="成团人数">
|
||||
<span class="font-mono text-dark">
|
||||
{{ pintuanInfo.requiredNum ? `${pintuanInfo.requiredNum} 人` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="限购数量">
|
||||
<span class="font-mono text-dark">
|
||||
{{ pintuanInfo.limitNum > 0 ? `${pintuanInfo.limitNum} 件` : "不限购" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="虚拟成团">
|
||||
<el-tag
|
||||
v-if="pintuanInfo.fictitious != null"
|
||||
:type="pintuanInfo.fictitious ? 'success' : 'info'"
|
||||
size="small"
|
||||
effect="plain"
|
||||
>
|
||||
{{ pintuanInfo.fictitious ? "已开启" : "未开启" }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动ID">
|
||||
<span class="font-mono text-secondary">{{ pintuanId || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item
|
||||
v-if="pintuanInfo.pintuanRule"
|
||||
label="规则说明"
|
||||
:span="3"
|
||||
>
|
||||
<span class="text-secondary rule-desc">{{ pintuanInfo.pintuanRule }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 拼团商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">已报名拼团商品</span>
|
||||
<span class="total-badge">共 {{ total }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>商品信息</h4>
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
class="operation"
|
||||
stripe
|
||||
:data="goodsData"
|
||||
style="width: 100%"
|
||||
empty-text="暂无报名的拼团商品"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column label="商品名称" min-width="120">
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row && (row.thumbnail || row.small || row.goodsImage)" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail || row.small || row.goodsImage"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small || row.goodsImage]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品名称 -->
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<div v-if="row">
|
||||
<a
|
||||
class="goods-link"
|
||||
@click="handleLinkTo(row.goodsId, row.skuId)"
|
||||
>
|
||||
{{ row.goodsName || "-" }}
|
||||
</a>
|
||||
<div v-if="row.skuId" class="sku-sub-text font-mono">
|
||||
SKU: {{ row.skuId }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 所属商家 -->
|
||||
<el-table-column prop="storeName" label="所属商家" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span class="text-secondary">{{ (row && row.storeName) || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 商品原价 -->
|
||||
<el-table-column label="商品原价" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="original-price font-mono">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 拼团价格 -->
|
||||
<el-table-column label="拼团价格" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="pintuan-price font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 活动库存 -->
|
||||
<el-table-column label="活动库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 限购数量 -->
|
||||
<el-table-column label="限购数量" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">
|
||||
{{ row.limitNum > 0 ? row.limitNum : "不限" }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="查看" width="140" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a class="link-text mr_10" @click="linkTo(row.goodsId, row.skuId)">{{ row.goodsName }}</a>
|
||||
<el-popover trigger="hover" title="扫码在手机中查看" placement="top" width="180">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleLinkTo(row.goodsId, row.skuId)"
|
||||
>
|
||||
PC查看
|
||||
</el-button>
|
||||
|
||||
<el-popover
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
width="180"
|
||||
>
|
||||
<template #reference>
|
||||
<img
|
||||
src="../../../assets/qrcode.svg"
|
||||
style="vertical-align: middle"
|
||||
class="hover-pointer"
|
||||
width="20"
|
||||
height="20"
|
||||
alt="qrcode"
|
||||
/>
|
||||
<el-button link type="primary" class="ml_8">
|
||||
手机扫码
|
||||
</el-button>
|
||||
</template>
|
||||
<vue-qr
|
||||
:text="wapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="0"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="150"
|
||||
/>
|
||||
<div class="qr-popover-content">
|
||||
<div class="qr-popover-title">扫码在手机中查看</div>
|
||||
<vue-qr
|
||||
:text="handleWapLinkTo(row.goodsId, row.skuId)"
|
||||
:margin="4"
|
||||
color-dark="#000"
|
||||
color-light="#fff"
|
||||
:size="140"
|
||||
/>
|
||||
</div>
|
||||
</el-popover>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quantity" label="库存" min-width="80" />
|
||||
<el-table-column label="拼团价格" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="page operation mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 分页区域 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
size="default"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部返回卡片 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="back">返回拼团列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { onMounted, reactive, ref, computed, getCurrentInstance } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getPintuanGoodsList, getPintuanDetail } from "@/api/promotion.js";
|
||||
import vueQr from "vue-qr";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance() || {};
|
||||
|
||||
const loading = ref(false);
|
||||
const pintuanInfo = ref({});
|
||||
const goodsData = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
const goodsData = ref([]);
|
||||
|
||||
const pintuanId = computed(() => {
|
||||
return route.query.id || pintuanInfo.value.id || "";
|
||||
});
|
||||
|
||||
function promotionStatusText(status) {
|
||||
const map = {
|
||||
@@ -103,6 +295,7 @@ function promotionStatusText(status) {
|
||||
};
|
||||
return map[status] || "未知";
|
||||
}
|
||||
|
||||
function promotionStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "info",
|
||||
@@ -110,34 +303,77 @@ function promotionStatusTagType(status) {
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
};
|
||||
return map[status] || "danger";
|
||||
return map[status] || "info";
|
||||
}
|
||||
function init() {
|
||||
getDataList();
|
||||
getPintuanMsg();
|
||||
|
||||
function handleLinkTo(goodsId, skuId) {
|
||||
if (proxy && typeof proxy.linkTo === "function") {
|
||||
proxy.linkTo(goodsId, skuId);
|
||||
} else {
|
||||
const pcUrl = window.BASE?.PC_URL || "";
|
||||
const url = skuId
|
||||
? `${pcUrl}/goodsDetail?skuId=${skuId}&goodsId=${goodsId}`
|
||||
: `${pcUrl}/goodsDetail?goodsId=${goodsId}`;
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
}
|
||||
|
||||
function handleWapLinkTo(goodsId, skuId) {
|
||||
if (proxy && typeof proxy.wapLinkTo === "function") {
|
||||
return proxy.wapLinkTo(goodsId, skuId);
|
||||
}
|
||||
const wapUrl = window.BASE?.WAP_URL || "";
|
||||
if (skuId) {
|
||||
return `${wapUrl}/pages/product/goods?id=${skuId}&goodsId=${goodsId}`;
|
||||
}
|
||||
return `${wapUrl}/pages/product/goods?goodsId=${goodsId}`;
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push({ name: "manager-pintuan" });
|
||||
}
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function getDataList() {
|
||||
if (!pintuanId.value) return;
|
||||
loading.value = true;
|
||||
searchForm.pintuanId = route.query.id;
|
||||
getPintuanGoodsList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
goodsData.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
searchForm.pintuanId = pintuanId.value;
|
||||
getPintuanGoodsList(searchForm)
|
||||
.then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
goodsData.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getPintuanMsg() {
|
||||
if (!pintuanId.value) return;
|
||||
getPintuanDetail(pintuanId.value).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
pintuanInfo.value = res.result;
|
||||
}
|
||||
});
|
||||
}
|
||||
function getPintuanMsg() {
|
||||
getPintuanDetail(route.query.id).then((res) => {
|
||||
if (res.success) data.value.push(res.result);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getPintuanMsg();
|
||||
getDataList();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -146,31 +382,246 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
border-left: 3px solid red;
|
||||
.pintuan-goods-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.ml_8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
background-color: #f8f9fb;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.rule-desc {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
:deep(th.el-table__cell) {
|
||||
background-color: #fafafa;
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
margin: 0 auto;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.goods-link {
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.sku-sub-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pintuan-price {
|
||||
color: #f56c6c;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.qr-popover-content {
|
||||
text-align: center;
|
||||
padding: 6px 0;
|
||||
|
||||
.mr_10 {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.hover-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
.qr-popover-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,126 +1,163 @@
|
||||
<template>
|
||||
<div class="wrapper">
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" :rules="formRule" label-width="120px">
|
||||
<div class="base-info-item">
|
||||
<div class="form-item-view">
|
||||
<el-form-item style="width: 100%">
|
||||
<div style="display: flex; margin-bottom: 10px">
|
||||
<el-button type="primary" @click="openSkuList">选择商品</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
style="margin-left: 10px"
|
||||
@click="delSelectGoods"
|
||||
>批量删除</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
border
|
||||
v-if="showTable"
|
||||
:data="promotionGoodsList"
|
||||
style="width: 100%"
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="60" align="center" />
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ goodsTypeLabel(row.goodsType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="SKU编码" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.skuId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storeName" label="店铺名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算价格" min-width="130">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="promotionGoodsList[$index].settlementPrice"
|
||||
style="width: 110px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分类" min-width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-select
|
||||
v-model="promotionGoodsList[$index].pointsGoodsCategoryId"
|
||||
@change="(val) => changeCategory(val, $index)"
|
||||
style="width: 140px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动库存" min-width="130">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="promotionGoodsList[$index].activeStock"
|
||||
style="width: 110px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="兑换积分" min-width="130">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
v-model="promotionGoodsList[$index].points"
|
||||
style="width: 110px"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="small" plain @click="delGoods($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
<div class="points-goods-add-page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>添加积分商品</h1>
|
||||
<p>设置兑换时间,选择商品并配置兑换积分、价格与库存。</p>
|
||||
</div>
|
||||
<el-button :icon="ArrowLeft" @click="closeCurrentPage">返回积分商品列表</el-button>
|
||||
</div>
|
||||
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="points-goods-time-form-item">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-button link @click="closeCurrentPage">返回</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
<el-form ref="formRef" :model="form" :rules="formRule" label-width="90px">
|
||||
<el-card shadow="never" class="section-card">
|
||||
<div class="section-heading">
|
||||
<span class="section-number">01</span>
|
||||
<h2>活动时间设置</h2>
|
||||
</div>
|
||||
<el-form-item label="活动时间" prop="rangeTime" class="points-goods-time-form-item">
|
||||
<el-date-picker
|
||||
type="datetimerange"
|
||||
v-model="form.rangeTime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
range-separator="至"
|
||||
:disabled-date="options.disabledDate"
|
||||
/>
|
||||
<span class="field-hint">所有参与商品将使用同一兑换时间</span>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="section-card">
|
||||
<div class="goods-section-heading">
|
||||
<div class="section-heading">
|
||||
<span class="section-number">02</span>
|
||||
<h2>积分商品</h2>
|
||||
<span class="selected-count">已选 {{ promotionGoodsList.length }} 件商品</span>
|
||||
</div>
|
||||
<div v-if="promotionGoodsList.length" class="goods-actions">
|
||||
<el-button v-if="selectedGoods.length" type="danger" plain @click="delSelectGoods">批量删除({{ selectedGoods.length }})</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openSkuList">选择商品</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<el-table
|
||||
v-if="promotionGoodsList.length"
|
||||
:data="promotionGoodsList"
|
||||
class="points-goods-table"
|
||||
style="width: 100%"
|
||||
row-key="skuId"
|
||||
scrollbar-always-on
|
||||
@selection-change="changeSelect"
|
||||
>
|
||||
<el-table-column type="selection" width="48" align="center" />
|
||||
<el-table-column label="商品信息" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div class="product-info">
|
||||
<el-image :src="row.thumbnail" fit="cover" class="product-image" preview-teleported :preview-src-list="row.thumbnail ? [row.thumbnail] : []">
|
||||
<template #error><span class="image-placeholder">无图</span></template>
|
||||
</el-image>
|
||||
<div class="product-details">
|
||||
<div class="product-name" :title="row.goodsName">{{ row.goodsName || '-' }}</div>
|
||||
<div class="product-code">SKU:{{ row.skuId }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ goodsTypeLabel(row.goodsType) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storeName" label="所属店铺" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="商品原价" width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可用库存" width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算价格(元)" width="140" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
:controls="false"
|
||||
v-model="promotionGoodsList[$index].settlementPrice"
|
||||
class="goods-number-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="积分分类" width="160">
|
||||
<template #default="{ $index }">
|
||||
<el-select
|
||||
v-model="promotionGoodsList[$index].pointsGoodsCategoryId"
|
||||
@change="(val) => changeCategory(val, $index)"
|
||||
style="width: 100%"
|
||||
placeholder="请选择分类"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:value="item.id"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
/>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动库存(件)" width="130" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
:controls="false"
|
||||
v-model="promotionGoodsList[$index].activeStock"
|
||||
class="goods-number-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="兑换积分" width="130" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-input-number
|
||||
:min="0"
|
||||
:controls="false"
|
||||
v-model="promotionGoodsList[$index].points"
|
||||
class="goods-number-input"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" link @click="delGoods($index)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-else class="goods-empty">
|
||||
<div class="empty-icon"><el-icon :size="28"><ShoppingBag /></el-icon></div>
|
||||
<h3>选择参与积分兑换的商品</h3>
|
||||
<p>添加商品后,可逐项设置积分分类、兑换积分和活动库存</p>
|
||||
<el-button type="primary" :icon="Plus" @click="openSkuList">选择活动商品</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" class="footer-card">
|
||||
<div class="footer-content">
|
||||
<span class="selected-count">已选 <strong>{{ promotionGoodsList.length }}</strong> 件商品</span>
|
||||
<div class="footer-buttons">
|
||||
<el-button @click="closeCurrentPage">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交创建</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<sku-select ref="skuSelectRef" @selectedGoodsData="selectedGoodsData"></sku-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from "vue";
|
||||
import { ArrowLeft, Plus, ShoppingBag } from "@element-plus/icons-vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { addPointsGoods, getPointsGoodsCategoryList } from "@/api/promotion";
|
||||
import skuSelect from "@/components/lili-dialog";
|
||||
@@ -141,7 +178,6 @@ const form = reactive({
|
||||
rangeTime: null,
|
||||
promotionGoodsList: [],
|
||||
});
|
||||
const showTable = ref(true);
|
||||
const promotionGoodsList = ref([]);
|
||||
const categoryList = ref([]);
|
||||
const submitLoading = ref(false);
|
||||
@@ -185,6 +221,10 @@ function handleSubmit() {
|
||||
return;
|
||||
}
|
||||
for (const row of params) {
|
||||
if (!row.pointsGoodsCategoryId) {
|
||||
Message.error(`请为【${row.goodsName}】选择积分分类`);
|
||||
return;
|
||||
}
|
||||
const stock = Number(row.quantity) || 0;
|
||||
if (isECoupon(row.goodsType) && Number(row.activeStock) > stock) {
|
||||
Message.error(`【${row.goodsName}】活动库存不能超过卡池库存(${stock})`);
|
||||
@@ -265,7 +305,7 @@ function selectedGoodsData(item) {
|
||||
item.forEach((e) => {
|
||||
const obj = {
|
||||
settlementPrice: e.settlementPrice || e.price,
|
||||
pointsGoodsCategoryId: e.pointsGoodsCategoryId || 0,
|
||||
pointsGoodsCategoryId: e.pointsGoodsCategoryId || "",
|
||||
pointsGoodsCategoryName: e.pointsGoodsCategoryName || "",
|
||||
activeStock: e.activeStock || 1,
|
||||
points: e.points || 1,
|
||||
@@ -286,34 +326,59 @@ function selectedGoodsData(item) {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
.points-goods-add-page {
|
||||
max-width: 1640px;
|
||||
margin: 0 auto;
|
||||
padding: 12px 20px 24px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.describe {
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
color: #999;
|
||||
.page-header, .goods-section-heading, .footer-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
min-height: 800px;
|
||||
.page-header { padding: 8px 0 4px; margin-bottom: 16px; }
|
||||
.page-header h1 { margin: 0; font-size: 22px; font-weight: 500; }
|
||||
.page-header p { margin: 8px 0 0; font-size: 13px; color: var(--el-text-color-secondary); }
|
||||
.section-card, .footer-card {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.points-goods-time-form-item {
|
||||
:deep(.el-date-editor.el-date-editor--datetimerange) {
|
||||
width: 420px !important;
|
||||
min-width: 420px;
|
||||
max-width: 420px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section-card :deep(.el-card__body) { padding: 20px 24px; }
|
||||
.section-heading { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.section-heading h2 { margin: 0; font-size: 15px; font-weight: 500; }
|
||||
.section-number { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 28px; height: 28px; border-radius: 7px; background: var(--el-color-primary-light-9); color: var(--el-color-primary); font-size: 12px; font-weight: 500; }
|
||||
.points-goods-time-form-item { margin: 16px 0 0 36px; }
|
||||
.points-goods-time-form-item :deep(.el-form-item__content) { min-width: 0; }
|
||||
.points-goods-time-form-item :deep(.el-date-editor) { flex: 0 1 520px; width: 520px; max-width: 100%; }
|
||||
.field-hint { width: 100%; margin-top: 6px; color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.goods-section-heading { margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.selected-count { color: var(--el-text-color-secondary); font-size: 13px; }
|
||||
.selected-count strong { padding: 0 4px; color: var(--el-color-primary); font-weight: 500; }
|
||||
.goods-actions, .footer-buttons { display: flex; align-items: center; flex-wrap: wrap; gap: 12px; }
|
||||
.goods-actions :deep(.el-button + .el-button), .footer-buttons :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.goods-empty { width: 100%; min-height: 230px; padding: 24px 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; background: var(--el-fill-color-lighter); border: 1px dashed var(--el-border-color); border-radius: 8px; box-sizing: border-box; }
|
||||
.empty-icon { display: grid; place-items: center; width: 60px; height: 60px; border-radius: 16px; color: var(--el-color-primary); background: var(--el-color-primary-light-9); margin-bottom: 16px; }
|
||||
.goods-empty h3 { margin: 0; font-size: 16px; font-weight: 500; }
|
||||
.goods-empty p { margin: 8px 0 20px; font-size: 13px; line-height: 1.6; color: var(--el-text-color-secondary); }
|
||||
.product-info { display: flex; align-items: center; gap: 12px; padding: 8px 0; }
|
||||
.product-image { width: 48px; height: 48px; flex: 0 0 48px; border: 1px solid var(--el-border-color-lighter); border-radius: 6px; }
|
||||
.image-placeholder { display: grid; place-items: center; width: 100%; height: 100%; font-size: 12px; color: var(--el-text-color-placeholder); background: var(--el-fill-color-light); }
|
||||
.product-details { min-width: 0; }
|
||||
.product-name { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.6; }
|
||||
.product-code { margin-top: 4px; font-size: 12px; color: var(--el-text-color-secondary); font-family: Consolas, monospace; overflow-wrap: anywhere; }
|
||||
.goods-number-input { width: 100%; }
|
||||
.points-goods-table :deep(th.el-table__cell) { background: var(--el-fill-color-light); font-weight: 400; padding: 12px 0; }
|
||||
.footer-card { box-shadow: 0 4px 18px rgb(31 45 61 / 5%); }
|
||||
.footer-card :deep(.el-card__body) { padding: 16px 24px; }
|
||||
@media (max-width: 768px) {
|
||||
.points-goods-add-page { padding: 8px 8px 20px; }
|
||||
.page-header { flex-wrap: wrap; }
|
||||
.section-card :deep(.el-card__body) { padding: 16px; }
|
||||
.points-goods-time-form-item { margin-left: 0; }
|
||||
.points-goods-time-form-item :deep(.el-form-item__label) { width: 80px !important; }
|
||||
.footer-card :deep(.el-card__body) { padding: 14px 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,89 +1,239 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card>
|
||||
<el-form ref="formRef" :model="form" label-width="120px" :rules="formRule">
|
||||
<div class="base-info-item">
|
||||
<h4>积分商品信息</h4>
|
||||
<div class="form-item-view">
|
||||
<el-form-item label="商品名称">
|
||||
<div>{{ form.goodsSku.goodsName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="SKU编码">
|
||||
<div>{{ form.goodsSku.id }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="店铺名称">
|
||||
<div>{{ form.goodsSku.storeName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品价格">
|
||||
<div>
|
||||
<priceColorScheme :value="form.goodsSku.price" :color="$mainColor" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="库存">
|
||||
<div>{{ form.goodsSku.quantity }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="结算价格" prop="settlementPrice">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model="form.settlementPrice"
|
||||
placeholder="请填写结算价格"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="pointsGoodsCategoryId">
|
||||
<el-select
|
||||
v-model="form.pointsGoodsCategoryId"
|
||||
placeholder="请选择分类"
|
||||
style="width: 260px"
|
||||
@change="changeCategory"
|
||||
>
|
||||
<el-option v-for="item in categoryList" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动库存" prop="activeStock">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model="form.activeStock"
|
||||
placeholder="请填写活动库存"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="兑换积分" prop="points">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model="form.points"
|
||||
placeholder="请填写兑换积分"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动开始时间">
|
||||
<el-date-picker
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:disabled-date="disabledDate"
|
||||
style="width: 360px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<el-button @click="closeCurrentPage" style="margin-right: 5px">返回</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
|
||||
<div class="points-goods-edit-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>{{ onlyView ? "积分商品详情" : "编辑积分商品" }}</span>
|
||||
<el-tag
|
||||
size="small"
|
||||
effect="plain"
|
||||
:type="onlyView ? 'info' : 'warning'"
|
||||
class="mode-tag"
|
||||
>
|
||||
{{ onlyView ? "查看模式" : "编辑模式" }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="form.promotionStatus"
|
||||
size="small"
|
||||
effect="dark"
|
||||
:type="promotionStatusTagType(form.promotionStatus)"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ promotionStatusText(form.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-if="id" class="activity-id font-mono">ID: {{ id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div class="header-right">
|
||||
<el-button @click="closeCurrentPage">返回积分商品列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 商品基本信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">商品基本信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="商品图片" width="120px">
|
||||
<div class="goods-thumb-box">
|
||||
<el-image
|
||||
v-if="goodsThumbnail"
|
||||
:src="goodsThumbnail"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[goodsThumbnail]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="thumb-slot">无图</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品名称">
|
||||
<div class="font-semibold text-dark">
|
||||
{{ form.goodsName || form.goodsSku?.goodsName || "-" }}
|
||||
</div>
|
||||
<div v-if="form.goodsId" class="sub-id-text font-mono">
|
||||
商品ID: {{ form.goodsId }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="SKU编码">
|
||||
<span class="font-mono text-dark">
|
||||
{{ form.skuId || form.goodsSku?.id || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="所属店铺">
|
||||
<span class="text-secondary">
|
||||
{{ form.goodsSku?.storeName || form.storeName || "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品原价">
|
||||
<span class="original-price-val font-mono">
|
||||
{{ $filters.unitPrice(goodsPrice, "¥") }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="商品库存">
|
||||
<span class="font-mono text-dark">
|
||||
{{ form.goodsSku?.quantity != null ? `${form.goodsSku.quantity} 件` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 积分兑换配置区域 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">积分兑换规则与参数</span>
|
||||
</div>
|
||||
|
||||
<!-- 查看模式:以结构化描述列表呈现 -->
|
||||
<template v-if="onlyView">
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="兑换所需积分">
|
||||
<span class="points-highlight font-mono font-semibold">
|
||||
{{ form.points != null ? `${form.points} 积分` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="结算价格">
|
||||
<span class="font-mono font-semibold text-dark">
|
||||
{{ form.settlementPrice != null ? $filters.unitPrice(form.settlementPrice, "¥") : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="积分商品分类">
|
||||
<el-tag size="small" effect="plain" type="primary">
|
||||
{{ currentCategoryName || "未设置分类" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动库存">
|
||||
<span class="font-mono text-dark font-semibold">
|
||||
{{ form.activeStock != null ? `${form.activeStock} 件` : "-" }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动时间" :span="2">
|
||||
<span class="font-mono text-dark">
|
||||
{{ activityTimeRange }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
|
||||
<!-- 编辑模式:表单输入 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="120px"
|
||||
:rules="formRule"
|
||||
class="edit-form"
|
||||
>
|
||||
<el-form-item label="兑换积分" prop="points">
|
||||
<el-input
|
||||
v-model="form.points"
|
||||
type="number"
|
||||
placeholder="请填写所需兑换积分"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>积分</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="结算价格" prop="settlementPrice">
|
||||
<el-input
|
||||
v-model="form.settlementPrice"
|
||||
type="number"
|
||||
placeholder="请填写结算价格"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>元</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="积分分类" prop="pointsGoodsCategoryId">
|
||||
<el-select
|
||||
v-model="form.pointsGoodsCategoryId"
|
||||
placeholder="请选择积分商品分类"
|
||||
style="width: 280px"
|
||||
@change="changeCategory"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in categoryList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动库存" prop="activeStock">
|
||||
<el-input
|
||||
v-model="form.activeStock"
|
||||
type="number"
|
||||
placeholder="请填写活动库存数量"
|
||||
clearable
|
||||
style="width: 280px"
|
||||
>
|
||||
<template #append>件</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="活动时间" required class="points-activity-time-item">
|
||||
<el-date-picker
|
||||
v-model="form.rangeTime"
|
||||
type="datetimerange"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="请选择活动时间"
|
||||
:disabled-date="disabledDate"
|
||||
style="width: 380px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button @click="closeCurrentPage">
|
||||
{{ onlyView ? "返回" : "取消" }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!onlyView"
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存修改
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
updatePointsGoods,
|
||||
@@ -99,6 +249,11 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const id = computed(() => route.query.id || "");
|
||||
const onlyView = computed(() => {
|
||||
return route.query.onlyView === "true" || route.query.onlyView === true;
|
||||
});
|
||||
|
||||
const pointsGoodsCategoryId = ref("");
|
||||
const pointsGoodsCategoryName = ref("");
|
||||
const form = ref({
|
||||
@@ -111,49 +266,106 @@ const form = ref({
|
||||
promotionStatus: "NEW",
|
||||
rangeTime: [],
|
||||
});
|
||||
|
||||
const categoryList = ref([]);
|
||||
const id = route.query.id;
|
||||
const submitLoading = ref(false);
|
||||
|
||||
const formRule = {
|
||||
settlementPrice: [{ required: true, message: "请填写结算价格" }],
|
||||
pointsGoodsCategoryId: [{ required: true, message: "请选择积分商品分类" }],
|
||||
points: [{ required: true, message: "请填写兑换积分" }],
|
||||
activeStock: [{ required: true, message: "请填写库存" }],
|
||||
settlementPrice: [{ required: true, message: "请填写结算价格", trigger: "blur" }],
|
||||
pointsGoodsCategoryId: [
|
||||
{ required: true, message: "请选择积分商品分类", trigger: "change" },
|
||||
],
|
||||
points: [{ required: true, message: "请填写兑换积分", trigger: "blur" }],
|
||||
activeStock: [{ required: true, message: "请填写库存", trigger: "blur" }],
|
||||
};
|
||||
|
||||
const formRef = ref(null);
|
||||
|
||||
onMounted(async () => {
|
||||
await getCategory();
|
||||
if (id) {
|
||||
getData();
|
||||
}
|
||||
const goodsThumbnail = computed(() => {
|
||||
return form.value.thumbnail || form.value.goodsSku?.thumbnail || "";
|
||||
});
|
||||
|
||||
const goodsPrice = computed(() => {
|
||||
if (form.value.goodsSku?.price != null) return form.value.goodsSku.price;
|
||||
if (form.value.originalPrice != null) return form.value.originalPrice;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const currentCategoryName = computed(() => {
|
||||
if (pointsGoodsCategoryName.value) return pointsGoodsCategoryName.value;
|
||||
if (form.value.pointsGoodsCategoryName) return form.value.pointsGoodsCategoryName;
|
||||
const match = categoryList.value.find(
|
||||
(c) => c.id === (pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId)
|
||||
);
|
||||
return match ? match.name : "";
|
||||
});
|
||||
|
||||
const activityTimeRange = computed(() => {
|
||||
const start = form.value.startTime || (form.value.rangeTime && form.value.rangeTime[0]);
|
||||
const end = form.value.endTime || (form.value.rangeTime && form.value.rangeTime[1]);
|
||||
if (start && end) {
|
||||
return `${start} 至 ${end}`;
|
||||
}
|
||||
return start || end || "-";
|
||||
});
|
||||
|
||||
function promotionStatusText(status) {
|
||||
const map = {
|
||||
NEW: "未开始",
|
||||
START: "已开始",
|
||||
END: "已结束",
|
||||
CLOSE: "已关闭",
|
||||
};
|
||||
return map[status] || status;
|
||||
}
|
||||
|
||||
function promotionStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "info",
|
||||
START: "success",
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
};
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
function disabledDate(date) {
|
||||
return date && date.getTime() < Date.now() - 86400000;
|
||||
}
|
||||
|
||||
function changeCategory(id) {
|
||||
const item = categoryList.value.find((c) => c.id === id);
|
||||
pointsGoodsCategoryId.value = id;
|
||||
function changeCategory(catId) {
|
||||
const item = categoryList.value.find((c) => c.id === catId);
|
||||
pointsGoodsCategoryId.value = catId;
|
||||
pointsGoodsCategoryName.value = item ? item.name : "";
|
||||
}
|
||||
|
||||
function closeCurrentPage() {
|
||||
appStore.removeTag("edit-points-goods");
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
router.go(-1);
|
||||
if (appStore?.removeTag) {
|
||||
appStore.removeTag("edit-points-goods");
|
||||
}
|
||||
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
|
||||
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
|
||||
}
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push({ name: "points-goods" });
|
||||
}
|
||||
}
|
||||
|
||||
function getData() {
|
||||
getPointsGoodsById(id).then((res) => {
|
||||
if (res.success) {
|
||||
if (!id.value) return;
|
||||
getPointsGoodsById(id.value).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
const data = res.result;
|
||||
form.value = data;
|
||||
data.rangeTime = [];
|
||||
form.value = {
|
||||
...data,
|
||||
goodsSku: data.goodsSku || {},
|
||||
};
|
||||
if (data.startTime && data.endTime) {
|
||||
data.rangeTime = [data.startTime, data.endTime];
|
||||
form.value.rangeTime = [data.startTime, data.endTime];
|
||||
} else {
|
||||
form.value.rangeTime = [];
|
||||
}
|
||||
if (data.pointsGoodsCategoryId) {
|
||||
changeCategory(data.pointsGoodsCategoryId);
|
||||
@@ -163,8 +375,14 @@ function getData() {
|
||||
}
|
||||
|
||||
async function getCategory() {
|
||||
const res = await getPointsGoodsCategoryList();
|
||||
categoryList.value = res.result.records;
|
||||
try {
|
||||
const res = await getPointsGoodsCategoryList();
|
||||
if (res && res.success && res.result) {
|
||||
categoryList.value = res.result.records || [];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("加载积分商品分类失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
@@ -179,69 +397,203 @@ function handleSubmit() {
|
||||
submitLoading.value = true;
|
||||
const params = {
|
||||
...form.value,
|
||||
pointsGoodsCategoryId: pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId,
|
||||
pointsGoodsCategoryId:
|
||||
pointsGoodsCategoryId.value || form.value.pointsGoodsCategoryId,
|
||||
pointsGoodsCategoryName:
|
||||
pointsGoodsCategoryName.value || form.value.pointsGoodsCategoryName,
|
||||
};
|
||||
updatePointsGoods(params).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
Message.success("积分商品修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
});
|
||||
updatePointsGoods(params)
|
||||
.then((res) => {
|
||||
if (res && res.success) {
|
||||
Message.success("积分商品修改成功");
|
||||
closeCurrentPage();
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getCategory();
|
||||
if (id.value) {
|
||||
getData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-form {
|
||||
padding-bottom: 80px;
|
||||
.points-goods-edit-page {
|
||||
padding: 16px 20px;
|
||||
background-color: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
|
||||
.el-form-item {
|
||||
width: 100%;
|
||||
color: gray;
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
div.base-info-item > div {
|
||||
margin-left: 5%;
|
||||
}
|
||||
|
||||
div.base-info-item {
|
||||
margin-bottom: 10px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f8f8f8;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 40px;
|
||||
text-align: left;
|
||||
.mt_10 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.form-item-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding-left: 80px;
|
||||
.font-mono {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background-color: #ffc;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
z-index: 9999;
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.header-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1f2d3d;
|
||||
|
||||
.mode-tag,
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background-color: #409eff;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 130px;
|
||||
background-color: #f8f9fb;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.goods-thumb-box {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #f5f7fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #ebeef5;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 12px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-id-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.original-price-val {
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.points-highlight {
|
||||
color: #f56c6c;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
padding: 8px 12px;
|
||||
|
||||
.points-activity-time-item {
|
||||
:deep(.el-date-editor) {
|
||||
width: 380px !important;
|
||||
max-width: 380px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,70 +1,192 @@
|
||||
<template>
|
||||
<div class="seckill-goods">
|
||||
<el-card>
|
||||
<el-table border :data="data" style="width: 100%">
|
||||
<el-table-column prop="promotionName" label="活动名称" min-width="120" />
|
||||
<el-table-column prop="startTime" label="活动开始时间" min-width="160" />
|
||||
<el-table-column label="报名截止时间" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ unixDate(row.applyEndTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时间场次" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<el-tag v-for="item in unixHours(row.hours)" :key="item" class="hour-tag">{{ item }}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动状态" min-width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row" :type="seckillStatusTagType(row.promotionStatus)">
|
||||
{{ seckillStatusText(row.promotionStatus) }}
|
||||
<div class="seckill-goods-page">
|
||||
<!-- 顶部标题与返回栏 -->
|
||||
<el-card shadow="never" class="header-card">
|
||||
<div class="page-header-row">
|
||||
<div class="header-left">
|
||||
<div class="header-title">
|
||||
<span>秒杀活动商品管理</span>
|
||||
<el-tag
|
||||
v-if="seckillInfo.promotionStatus"
|
||||
:type="seckillStatusTagType(seckillInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="plain"
|
||||
class="status-tag"
|
||||
>
|
||||
{{ seckillStatusText(seckillInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<span v-if="seckillId" class="activity-id font-mono">ID: {{ seckillId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button @click="back">返回秒杀列表</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 活动信息卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="section-header">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">活动信息</span>
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="2" border class="custom-descriptions">
|
||||
<el-descriptions-item label="活动名称">
|
||||
<span class="font-semibold text-dark">{{ seckillInfo.promotionName || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动状态">
|
||||
<el-tag
|
||||
v-if="seckillInfo.promotionStatus"
|
||||
:type="seckillStatusTagType(seckillInfo.promotionStatus)"
|
||||
size="small"
|
||||
effect="dark"
|
||||
>
|
||||
{{ seckillStatusText(seckillInfo.promotionStatus) }}
|
||||
</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="活动开始时间">
|
||||
<span class="font-mono text-dark">{{ seckillInfo.startTime || "-" }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="报名截止时间">
|
||||
<span class="font-mono text-dark">{{ unixDate(seckillInfo.applyEndTime) }}</span>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item label="时间场次" :span="2">
|
||||
<div class="hours-tags-row">
|
||||
<template v-if="seckillInfo.hours">
|
||||
<el-tag
|
||||
v-for="item in unixHours(seckillInfo.hours)"
|
||||
:key="item"
|
||||
size="small"
|
||||
effect="light"
|
||||
class="hour-tag"
|
||||
>
|
||||
{{ item }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else class="text-placeholder">未设置场次</span>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item v-if="seckillInfo.seckillRule" label="规则说明" :span="2">
|
||||
<span class="text-secondary">{{ seckillInfo.seckillRule }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<!-- 秒杀商品列表卡片 -->
|
||||
<el-card shadow="never" class="info-card mt_10">
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title-group">
|
||||
<span class="section-indicator"></span>
|
||||
<span class="section-title">已报名秒杀商品</span>
|
||||
<span class="total-badge">共 {{ total }} 件商品</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getDataList">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
class="operation"
|
||||
stripe
|
||||
:data="goodsList"
|
||||
style="width: 100%"
|
||||
empty-text="暂无报名的秒杀商品"
|
||||
class="custom-table"
|
||||
>
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="商品价格" width="110">
|
||||
<!-- 商品图片 -->
|
||||
<el-table-column label="商品图片" width="76" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}</span>
|
||||
<div v-if="row && (row.thumbnail || row.small || row.goodsImage)" class="goods-thumb-box">
|
||||
<el-image
|
||||
:src="row.thumbnail || row.small || row.goodsImage"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
:preview-src-list="[row.thumbnail || row.small || row.goodsImage]"
|
||||
class="goods-thumb-img"
|
||||
>
|
||||
<template #error>
|
||||
<div class="thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div v-else class="goods-thumb-box thumb-slot">无图</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="库存" width="90">
|
||||
|
||||
<!-- 商品名称 -->
|
||||
<el-table-column prop="goodsName" label="商品名称" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.quantity }}</span>
|
||||
<span class="font-semibold text-dark">{{ row.goodsName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="活动价格" width="100">
|
||||
|
||||
<!-- 商家名称 -->
|
||||
<el-table-column prop="storeName" label="所属商家" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" :style="{ color: $mainColor }">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}</span>
|
||||
<span class="text-secondary">{{ row.storeName || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="storeName" label="商家名称" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="活动场次" width="100">
|
||||
|
||||
<!-- 活动场次 -->
|
||||
<el-table-column label="活动场次" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row">{{ row.timeLine + ":00" }}</el-tag>
|
||||
<el-tag v-if="row && row.timeLine != null" size="small" effect="plain" type="warning">
|
||||
{{ row.timeLine }}:00 场
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center">
|
||||
|
||||
<!-- 原价 -->
|
||||
<el-table-column label="商品原价" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="original-price font-mono">
|
||||
{{ $filters.unitPrice(row.originalPrice, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 秒杀价格 -->
|
||||
<el-table-column label="秒杀价格" width="130" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="seckill-price font-mono font-semibold">
|
||||
{{ $filters.unitPrice(row.price, "¥") }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 库存 -->
|
||||
<el-table-column label="活动库存" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row" class="font-mono text-dark">{{ row.quantity ?? "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作 -->
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<template #default="{ row, $index }">
|
||||
<a v-if="row" class="link-text" @click="delGoods($index, row)">删除</a>
|
||||
<el-button
|
||||
v-if="row"
|
||||
link
|
||||
type="danger"
|
||||
@click="delGoods($index, row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
@@ -77,108 +199,142 @@
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<el-card shadow="never" class="footer-card mt_10">
|
||||
<div class="footer-actions">
|
||||
<el-button type="primary" @click="back">返回秒杀列表</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
seckillGoodsList,
|
||||
seckillDetail,
|
||||
delSeckillGoods,
|
||||
} from "@/api/promotion.js";
|
||||
import { Message, Modal } from "@/utils/message";
|
||||
import { unixToDate } from "@/utils/filters";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "seckill-goods" });
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const promotionStatus = ref("");
|
||||
const seckillId = route.query.id;
|
||||
const seckillInfo = ref({});
|
||||
const loading = ref(false);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
});
|
||||
const total = ref(0);
|
||||
const data = ref([]);
|
||||
const goodsList = ref([]);
|
||||
|
||||
function seckillStatusText(status) {
|
||||
const map = {
|
||||
NEW: "新建",
|
||||
START: "开始",
|
||||
END: "结束",
|
||||
CLOSE: "废弃",
|
||||
NEW: "未开始",
|
||||
START: "进行中",
|
||||
END: "已结束",
|
||||
CLOSE: "已关闭",
|
||||
};
|
||||
return map[status] || status || "-";
|
||||
}
|
||||
|
||||
function seckillStatusTagType(status) {
|
||||
const map = {
|
||||
NEW: "danger",
|
||||
NEW: "info",
|
||||
START: "success",
|
||||
END: "danger",
|
||||
CLOSE: "danger",
|
||||
CLOSE: "warning",
|
||||
};
|
||||
return map[status] || "danger";
|
||||
return map[status] || "info";
|
||||
}
|
||||
|
||||
function init() {
|
||||
getSeckillMsg();
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
|
||||
function getDataList() {
|
||||
if (!seckillId) return;
|
||||
loading.value = true;
|
||||
searchForm.seckillId = route.query.id;
|
||||
seckillGoodsList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success && res.result) {
|
||||
goodsList.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
searchForm.seckillId = seckillId;
|
||||
seckillGoodsList(searchForm)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res && res.success && res.result) {
|
||||
goodsList.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function getSeckillMsg() {
|
||||
seckillDetail(route.query.id).then((res) => {
|
||||
if (res.success && res.result) {
|
||||
data.value = [];
|
||||
data.value.push(res.result);
|
||||
promotionStatus.value = res.result.promotionStatus;
|
||||
if (!seckillId) return;
|
||||
seckillDetail(seckillId).then((res) => {
|
||||
if (res && res.success && res.result) {
|
||||
seckillInfo.value = res.result;
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function delGoods(index, row) {
|
||||
Modal.confirm({
|
||||
title: "确认删除",
|
||||
content: "您确认要删除该商品吗?删除后不可恢复",
|
||||
onOk: () => {
|
||||
ElMessageBox.confirm(`确认要删除商品 "${row.goodsName || "该商品"}" 吗?删除后不可恢复。`, "确认删除", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
const params = {
|
||||
seckillId: row.seckillId,
|
||||
id: row.id,
|
||||
};
|
||||
delSeckillGoods(params).then((res) => {
|
||||
if (res.success) {
|
||||
if (res && res.success) {
|
||||
goodsList.value.splice(index, 1);
|
||||
Message.success("删除成功!");
|
||||
total.value = Math.max(0, total.value - 1);
|
||||
ElMessage.success("删除成功!");
|
||||
} else if (res && res.message) {
|
||||
ElMessage.error(res.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function unixDate(time) {
|
||||
if (!time) return "-";
|
||||
return unixToDate(new Date(time) / 1000);
|
||||
}
|
||||
|
||||
function unixHours(item) {
|
||||
const hourArr = item.split(",");
|
||||
for (let i = 0; i < hourArr.length; i++) {
|
||||
hourArr[i] += ":00";
|
||||
}
|
||||
return hourArr;
|
||||
if (!item) return [];
|
||||
const hourArr = String(item).split(",");
|
||||
return hourArr.map((h) => `${h.trim()}:00`);
|
||||
}
|
||||
|
||||
function back() {
|
||||
appStore.removeTag("seckill-goods");
|
||||
router.go(-1);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -187,22 +343,193 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.operation {
|
||||
margin: 10px 0;
|
||||
}
|
||||
.seckill-goods-page {
|
||||
padding-bottom: 20px;
|
||||
|
||||
.hour-tag {
|
||||
margin-right: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.header-card {
|
||||
.page-header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.link-text {
|
||||
color: #409eff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.header-left {
|
||||
.header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
|
||||
.mt_10 {
|
||||
margin-top: 10px;
|
||||
.status-tag {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.activity-id {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-card {
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
|
||||
.header-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.section-indicator {
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 2px;
|
||||
background-color: #409eff;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.total-badge {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #909399;
|
||||
background-color: #f4f4f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-descriptions {
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 140px;
|
||||
background-color: #fafafa;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__content) {
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.hours-tags-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
.hour-tag {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table {
|
||||
.goods-thumb-box {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background-color: #fafafa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
.goods-thumb-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb-slot {
|
||||
font-size: 11px;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.original-price {
|
||||
color: #909399;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.seckill-price {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-card {
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-placeholder {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -102,10 +102,11 @@
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
@@ -118,7 +119,7 @@
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-card class="card">
|
||||
<div>
|
||||
<h4>交易概况</h4>
|
||||
<div class="flex">
|
||||
<div class="flex" style="align-items: flex-start;">
|
||||
<div class="transactionList">
|
||||
<div
|
||||
class="transaction-item"
|
||||
@@ -98,45 +98,159 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="shap">
|
||||
<div id="overViewChart">
|
||||
<!-- 上 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>访客数</span>
|
||||
<span>{{ overViewList.uvNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 中 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>下单笔数</span>
|
||||
<span>{{ overViewList.orderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 下 -->
|
||||
<div class="block">
|
||||
<div class="box">
|
||||
<span>付款笔数</span>
|
||||
<span>{{ overViewList.paymentOrderNum || 0 }}</span>
|
||||
</div>
|
||||
<div class="funnel-card">
|
||||
<div class="funnel-header">
|
||||
<h4>交易转化漏斗</h4>
|
||||
<span class="funnel-sub">全链路转化分析</span>
|
||||
</div>
|
||||
<div class="funnel-body">
|
||||
<svg
|
||||
class="funnel-svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 460 236"
|
||||
width="460"
|
||||
height="236"
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id="gradTier1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF5C58" />
|
||||
<stop offset="100%" stop-color="#E83D39" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradTier2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FF8259" />
|
||||
<stop offset="100%" stop-color="#F25B30" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gradTier3" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFA454" />
|
||||
<stop offset="100%" stop-color="#F28528" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 线 -->
|
||||
<div class="rightBorder"></div>
|
||||
<div class="leftTopBorder"></div>
|
||||
<div class="leftBottomBorder"></div>
|
||||
<!--数据 -->
|
||||
<div class="leftTopTips">
|
||||
<div>下单转化率</div>
|
||||
<div>{{ overViewList.orderConversionRate || "0%" }}</div>
|
||||
<filter id="funnelShadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="2" stdDeviation="2.5" flood-opacity="0.12" />
|
||||
</filter>
|
||||
<filter id="badgeShadow" x="-10%" y="-10%" width="120%" height="130%">
|
||||
<feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.06" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 连线:左侧 访客 -> 下单 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 138 42 L 98 42 Q 90 42 90 50 L 90 102 Q 90 110 98 110 L 161 110"
|
||||
/>
|
||||
|
||||
<!-- 连线:左侧 下单 -> 付款 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 161 110 L 98 110 Q 90 110 90 118 L 90 170 Q 90 178 98 178 L 182 178"
|
||||
/>
|
||||
|
||||
<!-- 连线:右侧 全店转化率 -->
|
||||
<path
|
||||
class="bracket-line"
|
||||
d="M 322 42 L 362 42 Q 370 42 370 50 L 370 170 Q 370 178 362 178 L 278 178"
|
||||
/>
|
||||
|
||||
<!-- 漏斗层级 1:访客数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="128,14 332,14 312,70 148,70"
|
||||
fill="url(#gradTier1)"
|
||||
stroke="url(#gradTier1)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="37" text-anchor="middle" class="funnel-label">访客数 UV</text>
|
||||
<text x="230" y="59" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.uvNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 漏斗层级 2:下单笔数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="152,82 308,82 290,138 170,138"
|
||||
fill="url(#gradTier2)"
|
||||
stroke="url(#gradTier2)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="105" text-anchor="middle" class="funnel-label">下单笔数</text>
|
||||
<text x="230" y="127" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.orderNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 漏斗层级 3:付款笔数 -->
|
||||
<g class="funnel-tier">
|
||||
<polygon
|
||||
points="174,150 286,150 270,206 190,206"
|
||||
fill="url(#gradTier3)"
|
||||
stroke="url(#gradTier3)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
filter="url(#funnelShadow)"
|
||||
/>
|
||||
<text x="230" y="173" text-anchor="middle" class="funnel-label">付款笔数</text>
|
||||
<text x="230" y="195" text-anchor="middle" class="funnel-value">
|
||||
{{ overViewList.paymentOrderNum || 0 }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 左侧卡片 1:下单转化率 -->
|
||||
<g transform="translate(6, 57)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">下单转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value">
|
||||
{{ overViewList.orderConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 左侧卡片 2:付款转化率 -->
|
||||
<g transform="translate(6, 125)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">付款转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value">
|
||||
{{ overViewList.paymentsConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
<!-- 右侧卡片:全店转化率 -->
|
||||
<g transform="translate(374, 91)" filter="url(#badgeShadow)">
|
||||
<rect width="80" height="38" class="badge-bg-highlight" />
|
||||
<text x="40" y="16" text-anchor="middle" class="badge-title">全店转化率</text>
|
||||
<text x="40" y="31" text-anchor="middle" class="badge-value-highlight">
|
||||
{{ overViewList.overallConversionRate || "0%" }}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="leftBottomTips">
|
||||
<div>付款转化率</div>
|
||||
<div>{{ overViewList.paymentsConversionRate || "0%" }}</div>
|
||||
</div>
|
||||
<div class="rightTips">
|
||||
<div>整体转换率</div>
|
||||
<div>{{ overViewList.overallConversionRate || "0%" }}</div>
|
||||
<div class="funnel-summary">
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier1"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">访客UV</span>
|
||||
<span class="val">{{ overViewList.uvNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-arrow">➔</div>
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier2"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">下单笔数</span>
|
||||
<span class="val">{{ overViewList.orderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-arrow">➔</div>
|
||||
<div class="summary-col">
|
||||
<div class="dot dot-tier3"></div>
|
||||
<div class="summary-text">
|
||||
<span class="label">付款笔数</span>
|
||||
<span class="val">{{ overViewList.paymentOrderNum || 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -553,128 +667,168 @@ onMounted(() => {
|
||||
text-align: right;
|
||||
margin: 20px 0;
|
||||
}
|
||||
#overViewChart {
|
||||
.shap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.funnel-card {
|
||||
background: #fbfcfe;
|
||||
border: 1px solid #ebedf0;
|
||||
border-radius: 8px;
|
||||
padding: 16px 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.funnel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
padding: 0 6px;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.funnel-sub {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-body {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.funnel-svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
|
||||
.funnel-label {
|
||||
font-size: 13px;
|
||||
fill: rgba(255, 255, 255, 0.94);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.funnel-value {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
fill: #ffffff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.bracket-line {
|
||||
fill: none;
|
||||
stroke: #cbd5e1;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 4, 3;
|
||||
}
|
||||
|
||||
.badge-bg {
|
||||
fill: #ffffff;
|
||||
stroke: #e2e8f0;
|
||||
stroke-width: 1;
|
||||
rx: 6px;
|
||||
}
|
||||
|
||||
.badge-bg-highlight {
|
||||
fill: #fff7f6;
|
||||
stroke: #ffccc7;
|
||||
stroke-width: 1;
|
||||
rx: 6px;
|
||||
}
|
||||
|
||||
.badge-title {
|
||||
font-size: 12px;
|
||||
fill: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.badge-value {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
fill: #1e293b;
|
||||
}
|
||||
|
||||
.badge-value-highlight {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
fill: #e83d39;
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-tier {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, filter 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
.funnel-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-left: 20px;
|
||||
> .leftTopTips {
|
||||
position: absolute;
|
||||
top: 68px;
|
||||
left: -2px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .leftBottomTips {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
left: -2px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .rightTips {
|
||||
position: absolute;
|
||||
bottom: 240px;
|
||||
right: 0px;
|
||||
width: 85px;
|
||||
text-align: center;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
> .rightBorder {
|
||||
width: 110px;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 40px;
|
||||
border: 2px solid #d9d9d9;
|
||||
border-left: 0;
|
||||
height: 280px;
|
||||
}
|
||||
> .leftTopBorder {
|
||||
border: 2px solid #d9d9d9;
|
||||
height: 118px;
|
||||
width: 56px;
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
top: 30px;
|
||||
border-right: 0;
|
||||
}
|
||||
> .leftBottomBorder {
|
||||
width: 108px;
|
||||
border: 2px solid #d9d9d9;
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
bottom: 45px;
|
||||
left: 40px;
|
||||
border-right: 0;
|
||||
}
|
||||
> .block {
|
||||
height: 0px;
|
||||
border-left: 30px solid transparent;
|
||||
border-right: 30px solid transparent;
|
||||
position: relative;
|
||||
background: rgb(255, 255, 255);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
> .block:nth-of-type(1) {
|
||||
width: 240px;
|
||||
border-top: 90px solid #ff4646;
|
||||
> .box {
|
||||
left: -30px;
|
||||
top: -90px;
|
||||
width: 240px;
|
||||
height: 90px;
|
||||
}
|
||||
}
|
||||
> .block:nth-of-type(2) {
|
||||
width: 172px;
|
||||
border-top: 100px solid #ff8585;
|
||||
margin-top: 10px;
|
||||
> .box {
|
||||
left: -29px;
|
||||
top: -100px;
|
||||
width: 172px;
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
> .block:nth-of-type(3) {
|
||||
width: 100px;
|
||||
margin-top: 10px;
|
||||
border-top: 150px solid #ffb396;
|
||||
border-left: 50px solid transparent;
|
||||
border-right: 50px solid transparent;
|
||||
justify-content: space-around;
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
background: #ffffff;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #f0f2f5;
|
||||
|
||||
> .box {
|
||||
left: -50px;
|
||||
top: -150px;
|
||||
width: 100px;
|
||||
height: 120px;
|
||||
z-index: 2;
|
||||
.summary-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.dot-tier1 {
|
||||
background: #ff5c58;
|
||||
}
|
||||
&.dot-tier2 {
|
||||
background: #ff8259;
|
||||
}
|
||||
&.dot-tier3 {
|
||||
background: #ffa454;
|
||||
}
|
||||
}
|
||||
:deep(.box){
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
|
||||
z-index: 2;
|
||||
.summary-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
> span {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
|
||||
.label {
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.val {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-arrow {
|
||||
color: #dcdfe6;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,13 +839,9 @@ h4 {
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
.transactionList {
|
||||
flex: 7;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.shap {
|
||||
width: 400px;
|
||||
margin-left: 20px;
|
||||
margin-top: 50px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.transaction-card {
|
||||
@@ -702,7 +852,7 @@ h4 {
|
||||
}
|
||||
.card-item-label {
|
||||
font-weight: bold;
|
||||
font-size: #666;
|
||||
color: #666;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -65,10 +65,11 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="1000px"
|
||||
size="1000px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
@@ -130,7 +131,7 @@
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="saveAppVersion">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog v-model="queryModalVisible" :title="queryModalTitle" width="700px">
|
||||
<el-form label-width="100px">
|
||||
|
||||
@@ -1,113 +1,143 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRoot">添加部门</el-button>
|
||||
<el-button type="primary" @click="add">添加子部门</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-button @click="getParentList">刷新</el-button>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a v-if="form.id" class="select-clear" @click="cancelSelect">取消选择</a>
|
||||
</el-alert>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
default-expand-all
|
||||
:check-strictly="!strict"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择角色">
|
||||
<el-select
|
||||
v-model="selectedRole"
|
||||
multiple
|
||||
:loading="userLoading"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
修改并保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="department-manage">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRoot">添加一级部门</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getParentList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="按部门名称筛选..."
|
||||
clearable
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯表格展示:树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="dept-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="部门名称" min-width="260" />
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="createTime" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.createTime || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="210" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="ops-row">
|
||||
<template v-if="row.parentId === 0 || row.parentId === '0'">
|
||||
<a class="link-text" @click="addChild(row)">添加子部门</a>
|
||||
<span class="op-split">|</span>
|
||||
</template>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<!-- 部门新建 / 编辑抽屉 -->
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="540px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="85px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级部门:">{{ form.title }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="formAdd.title" />
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="95px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="form.parentTitle" label="上级部门">
|
||||
<el-input :model-value="form.parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="选择角色">
|
||||
<el-select
|
||||
v-model="selectedRole"
|
||||
multiple
|
||||
:loading="userLoading"
|
||||
placeholder="请选择部门关联角色"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in users"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="cancelAdd">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
initDepartment,
|
||||
addDepartment,
|
||||
@@ -121,35 +151,34 @@ import {
|
||||
defineOptions({ name: "department-manage" });
|
||||
|
||||
const loading = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const strict = ref(true);
|
||||
const submitLoading = ref(false);
|
||||
const userLoading = ref(false);
|
||||
const loadingEdit = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
|
||||
const data = ref([]);
|
||||
const users = ref([]);
|
||||
const selectedRole = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const selectedRole = ref([]);
|
||||
const searchKey = ref("");
|
||||
const currentNodeKey = ref(null);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
isLeaf: "isLeaf",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
parentTitle: "",
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "部门名称不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -159,26 +188,58 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
const dataEdit = ref([]);
|
||||
const users = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches = node.title && node.title.toLowerCase().includes(query);
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getParentList();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
function getParentList() {
|
||||
loading.value = true;
|
||||
initDepartment().then((res) => {
|
||||
loading.value = false;
|
||||
initDepartment()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const list = Array.isArray(res.result) ? res.result : [];
|
||||
data.value = bubbleSort(normalizeDepartmentTree(list));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function loadRoles() {
|
||||
getRoleList({
|
||||
pageNumber: 1,
|
||||
pageSize: 10000,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
const list = Array.isArray(res.result) ? res.result : [];
|
||||
data.value = bubbleSort(normalizeDepartmentTree(list));
|
||||
users.value = res.result.records || [];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -195,47 +256,6 @@ function normalizeDepartmentTree(list) {
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeClick(data) {
|
||||
if (!data) {
|
||||
cancelSelect();
|
||||
return;
|
||||
}
|
||||
const row = JSON.parse(JSON.stringify(data));
|
||||
for (const attr in row) {
|
||||
if (row[attr] == null) {
|
||||
row[attr] = "";
|
||||
}
|
||||
}
|
||||
editTitle.value = row.title;
|
||||
currentNodeKey.value = row.id;
|
||||
userLoading.value = true;
|
||||
|
||||
getUserByDepartmentId(row.id).then((res) => {
|
||||
const way = [];
|
||||
res.result &&
|
||||
res.result.forEach((item) => {
|
||||
way.push(item.roleId);
|
||||
});
|
||||
selectedRole.value = way;
|
||||
});
|
||||
|
||||
getRoleList({
|
||||
pageNumber: 1,
|
||||
pageSize: 10000,
|
||||
}).then((res) => {
|
||||
userLoading.value = false;
|
||||
if (res.success) {
|
||||
users.value = res.result.records;
|
||||
form.value = row;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
|
||||
function bubbleSort(array) {
|
||||
if (!Array.isArray(array)) return [];
|
||||
const len = array.length;
|
||||
@@ -252,139 +272,267 @@ function bubbleSort(array) {
|
||||
return array;
|
||||
}
|
||||
|
||||
function cancelSelect() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
delete form.value.id;
|
||||
editTitle.value = "";
|
||||
selectedRole.value = [];
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function selectTreeEdit(v) {
|
||||
if (v.length > 0) {
|
||||
for (const attr in v[0]) {
|
||||
if (v[0][attr] == null) {
|
||||
v[0][attr] = "";
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
}
|
||||
const data = JSON.parse(JSON.stringify(v[0]));
|
||||
form.value.parentId = data.id;
|
||||
form.value.parentTitle = data.title;
|
||||
}
|
||||
});
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
|
||||
function cancelAdd() {
|
||||
modalVisible.value = false;
|
||||
// 添加一级部门
|
||||
function addRoot() {
|
||||
isEditMode.value = false;
|
||||
modalTitle.value = "添加一级部门";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: 0,
|
||||
parentTitle: "",
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.status = 0;
|
||||
// 添加子部门
|
||||
function addChild(row) {
|
||||
isEditMode.value = false;
|
||||
modalTitle.value = `添加子部门 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
parentId: row.id,
|
||||
parentTitle: row.title,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
// 编辑当前部门
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
modalTitle.value = `编辑部门 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
parentId: row.parentId,
|
||||
parentTitle: "",
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
status: row.status ?? 0,
|
||||
};
|
||||
selectedRole.value = [];
|
||||
userLoading.value = true;
|
||||
modalVisible.value = true;
|
||||
|
||||
getUserByDepartmentId(row.id)
|
||||
.then((res) => {
|
||||
userLoading.value = false;
|
||||
if (res && res.result) {
|
||||
selectedRole.value = res.result.map((item) => item.roleId);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
userLoading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 提交表单(统一处理新增与修改)
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的部门");
|
||||
return;
|
||||
}
|
||||
const roleWay = [];
|
||||
selectedRole.value.forEach((item) => {
|
||||
roleWay.push({
|
||||
departmentId: form.value.id,
|
||||
roleId: item,
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
editDepartment(form.value.id, form.value),
|
||||
updateDepartmentRole(form.value.id, roleWay),
|
||||
]).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res[0].success) {
|
||||
ElMessage.success("编辑成功");
|
||||
init();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
addDepartment(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
init();
|
||||
modalVisible.value = false;
|
||||
}
|
||||
});
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式
|
||||
const roleWay = selectedRole.value.map((roleId) => ({
|
||||
departmentId: form.value.id,
|
||||
roleId,
|
||||
}));
|
||||
Promise.all([
|
||||
editDepartment(form.value.id, form.value),
|
||||
updateDepartmentRole(form.value.id, roleWay),
|
||||
])
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res[0].success) {
|
||||
ElMessage.success("编辑部门成功");
|
||||
modalVisible.value = false;
|
||||
getParentList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
} else {
|
||||
// 新增模式
|
||||
addDepartment(form.value)
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加部门成功");
|
||||
modalVisible.value = false;
|
||||
getParentList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function add() {
|
||||
if (form.value.id == "" || form.value.id == null) {
|
||||
ElMessage.warning("请先点击选择一个部门");
|
||||
return;
|
||||
}
|
||||
modalTitle.value = "添加子部门";
|
||||
showParent.value = true;
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
function addRoot() {
|
||||
modalTitle.value = "添加一级部门";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
parentId: 0,
|
||||
sortOrder: 0,
|
||||
status: 0,
|
||||
};
|
||||
modalVisible.value = true;
|
||||
// 单个删除
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除部门 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteDepartment(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
getParentList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据及其下级所有数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条部门数据及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deleteDepartment(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelSelect();
|
||||
init();
|
||||
getParentList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.department-manage {
|
||||
width: 100%;
|
||||
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dept-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ops-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,245 +1,218 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
class="selectModel"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button type="primary" @click="addMenu">添加子菜单</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-dropdown @command="handleDropdown">
|
||||
<el-button>
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<div class="menu-manage-container">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="refresh">刷新</el-dropdown-item>
|
||||
<el-dropdown-item command="expandOne">收合所有</el-dropdown-item>
|
||||
<el-dropdown-item command="expandTwo">展开一级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandThree">展开两级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandAll">展开所有</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a class="select-clear" v-if="form.id" @click="cancelEdit">取消选择</a>
|
||||
</el-alert>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getAllList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
v-model="searchQuery"
|
||||
placeholder="按菜单名称筛选..."
|
||||
clearable
|
||||
placeholder="输入菜单名搜索"
|
||||
@input="search"
|
||||
@clear="search"
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
:check-strictly="!strict"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
>
|
||||
<template #default="{ data: nodeData }">
|
||||
<span class="tree-node-label">
|
||||
<el-icon v-if="nodeData.level == 0" class="tree-node-icon"><Menu /></el-icon>
|
||||
<el-icon v-else class="tree-node-icon"><List /></el-icon>
|
||||
<span>{{ nodeData.title }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯列表树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="menu-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="菜单名称" min-width="220" />
|
||||
|
||||
<el-table-column prop="level" label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.level === 0 ? "顶级菜单" : row.level === 1 ? "一级页面" : "二级页面" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.path || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="路由名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="frontRoute" label="前端组件" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.frontRoute || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="210" fixed="right" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="ops-row">
|
||||
<template v-if="row.level < 2">
|
||||
<a class="link-text" @click="addChildMenu(row)">添加子菜单</a>
|
||||
<span class="op-split">|</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="110px" :rules="formValidate">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="form.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="form.level == 1 || form.level == 2">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.level != 0"
|
||||
label="路由地址"
|
||||
prop="path"
|
||||
class="block-tool"
|
||||
>
|
||||
<el-tooltip placement="right" content="路由地址,英文唯一,跳转页面,路径展示用 ">
|
||||
<el-input class="menu-input" v-model="form.path" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input class="menu-input" v-model="form.name" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input class="menu-input" v-model="form.frontRoute" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="权限url" class="block-tool">
|
||||
<el-tooltip placement="right" content="*号模糊匹配,逗号分割">
|
||||
<el-input
|
||||
class="menu-input"
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
maxlength="1000"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="menuModalVisible"
|
||||
<!-- 菜单新建 / 编辑抽屉 -->
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
align-center
|
||||
size="560px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
class="menu-manage-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="100px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级节点:">{{ parentTitle }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="formAdd.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="formAdd.level != 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="100px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="parentTitle" label="上级菜单">
|
||||
<el-input :model-value="parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型">
|
||||
<el-tag v-if="form.level === 0" type="primary">顶级菜单</el-tag>
|
||||
<el-tag v-else type="success">页面菜单 (Level {{ form.level }})</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="formAdd.title" />
|
||||
<el-input v-model="form.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="formAdd.path" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="form.path" placeholder="英文唯一,如 /goods" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input v-model="formAdd.name" />
|
||||
|
||||
<el-form-item label="路由名称" prop="name">
|
||||
<el-tooltip placement="right" content="英文唯一,用于路由跳转">
|
||||
<el-input v-model="form.name" placeholder="请输入路由名称" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input v-model="formAdd.frontRoute" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="前端组件" prop="frontRoute">
|
||||
<el-input v-model="form.frontRoute" placeholder="例如 Main 或 views/goods/index" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="权限url">
|
||||
<el-input v-model="formAdd.permission" type="textarea" />
|
||||
<div class="desc">*号模糊匹配,逗号分割</div>
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="权限URL">
|
||||
<el-input
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="支持 * 号模糊匹配,多个用英文逗号分割"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="menuModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Menu, List, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllPermissionList,
|
||||
addPermission,
|
||||
editPermission,
|
||||
deletePermission,
|
||||
searchPermission,
|
||||
} from "@/api/index";
|
||||
import util from "@/libs/util.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
defineOptions({ name: "menu-manage" });
|
||||
defineOptions({ name: "manager-menu-manage" });
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const strict = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const expandLevel = ref(1);
|
||||
const menuModalVisible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
const parentTitle = ref("");
|
||||
|
||||
const data = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const searchKey = ref("");
|
||||
const parentTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const modalTitle = ref("");
|
||||
const expandedKeys = ref([]);
|
||||
const currentNodeKey = ref(null);
|
||||
const treeKey = ref(0);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
sortOrder: 0,
|
||||
level: 0,
|
||||
permission: "",
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "菜单名称名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
|
||||
name: [{ required: true, message: "路由名称不能为空", trigger: "blur" }],
|
||||
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "文件地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "前端组件路径不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -249,280 +222,217 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches =
|
||||
(node.title && node.title.toLowerCase().includes(query)) ||
|
||||
(node.name && node.name.toLowerCase().includes(query)) ||
|
||||
(node.path && node.path.toLowerCase().includes(query));
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getAllList();
|
||||
}
|
||||
|
||||
function collectExpandedKeys(nodes, keys = []) {
|
||||
if (!nodes) return keys;
|
||||
nodes.forEach((node) => {
|
||||
if (node.expand === true) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
collectExpandedKeys(node.children, keys);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
|
||||
function handleDropdown(name) {
|
||||
if (name == "expandOne") {
|
||||
expandLevel.value = 1;
|
||||
getAllList();
|
||||
} else if (name == "expandTwo") {
|
||||
expandLevel.value = 2;
|
||||
getAllList();
|
||||
} else if (name == "expandThree") {
|
||||
expandLevel.value = 3;
|
||||
getAllList();
|
||||
}
|
||||
if (name == "expandAll") {
|
||||
expandLevel.value = 4;
|
||||
getAllList();
|
||||
} else if (name == "refresh") {
|
||||
getAllList();
|
||||
}
|
||||
}
|
||||
|
||||
function getAllList() {
|
||||
loading.value = true;
|
||||
getAllPermissionList().then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const level = expandLevel.value;
|
||||
res.result.forEach(function (e) {
|
||||
if (level == 1) {
|
||||
if (e.level == 0) {
|
||||
e.expand = false;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 2) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 3) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 4) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
data.value = res.result;
|
||||
expandedKeys.value = collectExpandedKeys(res.result);
|
||||
treeKey.value += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onNodeClick(nodeData) {
|
||||
if (nodeData) {
|
||||
const menu = JSON.parse(JSON.stringify(nodeData));
|
||||
form.value = menu;
|
||||
editTitle.value = menu.title;
|
||||
currentNodeKey.value = menu.id;
|
||||
} else {
|
||||
cancelEdit();
|
||||
}
|
||||
}
|
||||
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
|
||||
function search() {
|
||||
if (searchKey.value) {
|
||||
loading.value = true;
|
||||
searchPermission({ title: searchKey.value }).then((res) => {
|
||||
getAllPermissionList()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result;
|
||||
expandedKeys.value = [];
|
||||
treeKey.value += 1;
|
||||
data.value = Array.isArray(res.result) ? res.result : [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getAllList();
|
||||
}
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
form.value.id = "";
|
||||
editTitle.value = "";
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.frontRoute = "";
|
||||
}
|
||||
|
||||
function submitEdit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的菜单节点");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
if (form.value.sortOrder == null) {
|
||||
form.value.sortOrder = 0;
|
||||
}
|
||||
delete form.value.icon;
|
||||
delete form.value.frontComponent;
|
||||
delete form.value.buttonType;
|
||||
delete form.value.updateTime;
|
||||
delete form.value.selected;
|
||||
delete form.value.description;
|
||||
delete form.value.children;
|
||||
|
||||
editPermission(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("编辑成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
|
||||
addPermission(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addMenu() {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择一个菜单权限节点");
|
||||
return;
|
||||
}
|
||||
parentTitle.value = form.value.title;
|
||||
modalTitle.value = "添加子节点";
|
||||
showParent.value = true;
|
||||
if (form.value.level == 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录", "抱歉,不能添加啦", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
level: Number(form.value.level) + 1,
|
||||
// 添加顶级菜单
|
||||
function addRootMenu() {
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "/",
|
||||
frontRoute: "Main",
|
||||
parentId: 0,
|
||||
level: 0,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
if (form.value.level == 0) {
|
||||
formAdd.value.path = "/";
|
||||
formAdd.value.frontRoute = "Main";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
function addChildMenu(row) {
|
||||
if (row.level >= 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录,不能再添加子菜单", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
menuModalVisible.value = true;
|
||||
}
|
||||
|
||||
function addRootMenu() {
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
level: 0,
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = row.title;
|
||||
modalTitle.value = `添加子菜单 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: row.id,
|
||||
level: Number(row.level) + 1,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
menuModalVisible.value = true;
|
||||
if (row.level === 0) {
|
||||
form.value.path = "/";
|
||||
form.value.frontRoute = "Main";
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑当前菜单
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = `编辑菜单 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
name: row.name,
|
||||
path: row.path,
|
||||
frontRoute: row.frontRoute,
|
||||
parentId: row.parentId,
|
||||
level: row.level,
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
permission: row.permission || "",
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 提交保存
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const payload = { ...form.value };
|
||||
if (payload.sortOrder == null) {
|
||||
payload.sortOrder = 0;
|
||||
}
|
||||
delete payload.children;
|
||||
delete payload.icon;
|
||||
delete payload.frontComponent;
|
||||
delete payload.buttonType;
|
||||
delete payload.updateTime;
|
||||
delete payload.selected;
|
||||
delete payload.description;
|
||||
|
||||
const request = isEditMode.value
|
||||
? editPermission(payload)
|
||||
: addPermission(payload);
|
||||
|
||||
request
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(isEditMode.value ? "编辑成功" : "添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
modalVisible.value = false;
|
||||
getAllList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除单个
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除菜单 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deletePermission(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deletePermission(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
@@ -530,42 +440,100 @@ function delAll() {
|
||||
util.initRouter();
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelEdit();
|
||||
init();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.menu-input {
|
||||
width: 362px;
|
||||
}
|
||||
.tree-node-icon {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tree-node-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
.menu-manage-container {
|
||||
width: 100%;
|
||||
|
||||
<style lang="scss">
|
||||
.menu-manage-dialog .el-dialog__body {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ops-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,201 +1,177 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-row class="operation">
|
||||
<el-switch
|
||||
v-model="strict"
|
||||
class="selectModel"
|
||||
active-text="级联"
|
||||
inactive-text="单选"
|
||||
style="margin-right: 5px"
|
||||
/>
|
||||
<el-button @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button type="primary" @click="addMenu">添加子菜单</el-button>
|
||||
<el-button @click="delAll">批量删除</el-button>
|
||||
<el-dropdown @command="handleDropdown">
|
||||
<el-button>
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<div class="menu-manage-container">
|
||||
<el-card shadow="never" class="manage-card">
|
||||
<!-- 顶部操作工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-button type="primary" @click="addRootMenu">添加顶级菜单</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectCount === 0"
|
||||
@click="delAll"
|
||||
>
|
||||
批量删除 {{ selectCount > 0 ? `(${selectCount})` : "" }}
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="refresh">刷新</el-dropdown-item>
|
||||
<el-dropdown-item command="expandOne">收合所有</el-dropdown-item>
|
||||
<el-dropdown-item command="expandTwo">展开一级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandThree">展开两级</el-dropdown-item>
|
||||
<el-dropdown-item command="expandAll">展开所有</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-row>
|
||||
<el-row type="flex" justify="start">
|
||||
<el-col :md="8" :lg="8" :xl="6">
|
||||
<el-alert type="info" show-icon :closable="false">
|
||||
当前选择编辑:
|
||||
<span class="select-title">{{ editTitle }}</span>
|
||||
<a class="select-clear" v-if="form.id" @click="cancelEdit">取消选择</a>
|
||||
</el-alert>
|
||||
<el-button @click="toggleExpandAll">
|
||||
{{ isAllExpanded ? "折叠全部" : "展开全部" }}
|
||||
</el-button>
|
||||
<el-button @click="getAllList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<el-input
|
||||
v-model="searchKey"
|
||||
v-model="searchQuery"
|
||||
placeholder="按菜单名称筛选..."
|
||||
clearable
|
||||
placeholder="输入菜单名搜索"
|
||||
@input="search"
|
||||
@clear="search"
|
||||
:prefix-icon="Search"
|
||||
class="search-input"
|
||||
/>
|
||||
<div v-loading="loading" class="tree-bar" :style="{ maxHeight: maxHeight }">
|
||||
<el-tree
|
||||
:key="treeKey"
|
||||
ref="treeRef"
|
||||
:data="data"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
show-checkbox
|
||||
highlight-current
|
||||
:check-strictly="!strict"
|
||||
:default-expanded-keys="expandedKeys"
|
||||
:current-node-key="currentNodeKey"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheckChange"
|
||||
>
|
||||
<template #default="{ data: nodeData }">
|
||||
<span class="tree-node-label">
|
||||
<el-icon v-if="nodeData.level == 0" class="tree-node-icon"><Menu /></el-icon>
|
||||
<el-icon v-else class="tree-node-icon"><List /></el-icon>
|
||||
<span>{{ nodeData.title }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 纯列表树形表格 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
:data="filteredData"
|
||||
row-key="id"
|
||||
default-expand-all
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
border
|
||||
stripe
|
||||
class="menu-table"
|
||||
@selection-change="onSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
|
||||
<el-table-column prop="title" label="菜单名称" min-width="220" />
|
||||
|
||||
<el-table-column prop="level" label="类型" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.level === 0 ? "顶级菜单" : row.level === 1 ? "一级页面" : "二级页面" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="path" label="路由地址" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.path || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="name" label="路由名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.name || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="frontRoute" label="前端组件" min-width="180" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.frontRoute || "-" }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="sortOrder" label="排序值" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.sortOrder ?? 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="210" fixed="right" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="ops-row">
|
||||
<template v-if="row.level < 2">
|
||||
<a class="link-text" @click="addChildMenu(row)">添加子菜单</a>
|
||||
<span class="op-split">|</span>
|
||||
</template>
|
||||
</el-tree>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :md="15" :lg="13" :xl="9" style="margin-left: 10px">
|
||||
<el-form ref="formRef" :model="form" label-width="110px" :rules="formValidate">
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="form.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="form.level == 1 || form.level == 2">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="form.title" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.level != 0"
|
||||
label="路由地址"
|
||||
prop="path"
|
||||
class="block-tool"
|
||||
>
|
||||
<el-tooltip placement="right" content="路由地址,英文唯一,跳转页面,路径展示用 ">
|
||||
<el-input class="menu-input" v-model="form.path" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input class="menu-input" v-model="form.name" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input class="menu-input" v-model="form.frontRoute" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.level != 0" label="权限url" class="block-tool">
|
||||
<el-tooltip placement="right" content="*号模糊匹配,逗号分割">
|
||||
<el-input
|
||||
class="menu-input"
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
maxlength="1000"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="form.sortOrder" :max="1000" :min="0" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
style="margin-right: 5px"
|
||||
:loading="submitLoading"
|
||||
@click="submitEdit"
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<a class="link-text" @click="editRow(row)">编辑</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text error-text" @click="deleteRow(row)">删除</a>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
v-model="menuModalVisible"
|
||||
<!-- 菜单新建 / 编辑抽屉 -->
|
||||
<el-drawer
|
||||
v-model="modalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
align-center
|
||||
size="560px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
class="menu-manage-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form ref="formAddRef" :model="formAdd" label-width="100px" :rules="formValidate">
|
||||
<div v-if="showParent">
|
||||
<el-form-item label="上级节点:">{{ parentTitle }}</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<div v-show="formAdd.level == 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><Menu /></el-icon>
|
||||
<span>顶级菜单</span>
|
||||
</div>
|
||||
<div v-show="formAdd.level != 0">
|
||||
<el-icon :size="16" style="margin-right: 5px"><List /></el-icon>
|
||||
<span>页面菜单</span>
|
||||
</div>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
label-width="100px"
|
||||
:rules="formValidate"
|
||||
>
|
||||
<el-form-item v-if="parentTitle" label="上级菜单">
|
||||
<el-input :model-value="parentTitle" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型">
|
||||
<el-tag v-if="form.level === 0" type="primary">顶级菜单</el-tag>
|
||||
<el-tag v-else type="success">页面菜单 (Level {{ form.level }})</el-tag>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="title">
|
||||
<el-input class="menu-input" v-model="formAdd.title" />
|
||||
<el-input v-model="form.title" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="formAdd.path" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="路由地址" prop="path">
|
||||
<el-input v-model="form.path" placeholder="英文唯一,如 /goods" />
|
||||
</el-form-item>
|
||||
<el-form-item label="路由名称" prop="name" class="block-tool">
|
||||
<el-tooltip placement="right" content="路由name,需英文唯一,跳转页面用">
|
||||
<el-input v-model="formAdd.name" />
|
||||
|
||||
<el-form-item label="路由名称" prop="name">
|
||||
<el-tooltip placement="right" content="英文唯一,用于路由跳转">
|
||||
<el-input v-model="form.name" placeholder="请输入路由名称" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="文件路径" prop="frontRoute">
|
||||
<el-input v-model="formAdd.frontRoute" />
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="前端组件" prop="frontRoute">
|
||||
<el-input v-model="form.frontRoute" placeholder="例如 Main 或 views/goods/index" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formAdd.level != 0" label="权限url">
|
||||
<el-input v-model="formAdd.permission" type="textarea" />
|
||||
<div class="desc">*号模糊匹配,逗号分割</div>
|
||||
|
||||
<el-form-item v-if="form.level != 0" label="权限URL">
|
||||
<el-input
|
||||
v-model="form.permission"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="支持 * 号模糊匹配,多个用英文逗号分割"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序值" prop="sortOrder">
|
||||
<el-tooltip placement="right" content="值越小越靠前,支持小数">
|
||||
<el-input-number v-model="formAdd.sortOrder" :max="1000" :min="0" />
|
||||
<el-tooltip placement="right" content="值越小越靠前">
|
||||
<el-input-number v-model="form.sortOrder" :min="0" :max="1000" />
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="menuModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitAdd">提交</el-button>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitForm">
|
||||
确定
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Menu, List, ArrowDown } from "@element-plus/icons-vue";
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getStoreAllPermissionList,
|
||||
addStorePermission,
|
||||
editStorePermission,
|
||||
deleteStorePermission,
|
||||
searchStorePermission,
|
||||
} from "@/api/index";
|
||||
import util from "@/libs/util.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
@@ -205,41 +181,38 @@ defineOptions({ name: "store-menu-manage" });
|
||||
const appStore = useAppStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const strict = ref(true);
|
||||
const maxHeight = ref("500px");
|
||||
const expandLevel = ref(1);
|
||||
const menuModalVisible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const modalVisible = ref(false);
|
||||
const modalTitle = ref("");
|
||||
const isEditMode = ref(false);
|
||||
const parentTitle = ref("");
|
||||
|
||||
const data = ref([]);
|
||||
const selectList = ref([]);
|
||||
const selectCount = ref(0);
|
||||
const showParent = ref(false);
|
||||
const searchKey = ref("");
|
||||
const parentTitle = ref("");
|
||||
const editTitle = ref("");
|
||||
const modalTitle = ref("");
|
||||
const expandedKeys = ref([]);
|
||||
const currentNodeKey = ref(null);
|
||||
const treeKey = ref(0);
|
||||
const treeProps = reactive({
|
||||
label: "title",
|
||||
children: "children",
|
||||
});
|
||||
const searchQuery = ref("");
|
||||
const isAllExpanded = ref(true);
|
||||
|
||||
const tableRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
|
||||
const form = ref({
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: "",
|
||||
parentId: "0",
|
||||
sortOrder: 0,
|
||||
level: 0,
|
||||
permission: "",
|
||||
});
|
||||
const formAdd = ref({});
|
||||
|
||||
const formValidate = reactive({
|
||||
title: [{ required: true, message: "菜单名称名称不能为空", trigger: "blur" }],
|
||||
title: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
|
||||
name: [{ required: true, message: "路由名称不能为空", trigger: "blur" }],
|
||||
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "文件地址不能为空", trigger: "blur" }],
|
||||
frontRoute: [{ required: true, message: "前端组件路径不能为空", trigger: "blur" }],
|
||||
sortOrder: [
|
||||
{
|
||||
required: true,
|
||||
@@ -249,267 +222,217 @@ const formValidate = reactive({
|
||||
},
|
||||
],
|
||||
});
|
||||
const submitLoading = ref(false);
|
||||
const data = ref([]);
|
||||
|
||||
const treeRef = ref(null);
|
||||
const formRef = ref(null);
|
||||
const formAddRef = ref(null);
|
||||
// 前端树形过滤
|
||||
const filteredData = computed(() => {
|
||||
const query = searchQuery.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
return data.value;
|
||||
}
|
||||
const filterTree = (nodes) => {
|
||||
return nodes
|
||||
.map((node) => {
|
||||
const matches =
|
||||
(node.title && node.title.toLowerCase().includes(query)) ||
|
||||
(node.name && node.name.toLowerCase().includes(query)) ||
|
||||
(node.path && node.path.toLowerCase().includes(query));
|
||||
const filteredChildren = node.children ? filterTree(node.children) : [];
|
||||
if (matches || filteredChildren.length > 0) {
|
||||
return {
|
||||
...node,
|
||||
children: filteredChildren.length > 0 ? filteredChildren : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
return filterTree(data.value);
|
||||
});
|
||||
|
||||
function init() {
|
||||
getAllList();
|
||||
}
|
||||
function collectExpandedKeys(nodes, keys = []) {
|
||||
if (!nodes) return keys;
|
||||
nodes.forEach((node) => {
|
||||
if (node.expand === true) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children && node.children.length > 0) {
|
||||
collectExpandedKeys(node.children, keys);
|
||||
}
|
||||
});
|
||||
return keys;
|
||||
}
|
||||
function handleDropdown(name) {
|
||||
if (name == "expandOne") {
|
||||
expandLevel.value = 1;
|
||||
getAllList();
|
||||
} else if (name == "expandTwo") {
|
||||
expandLevel.value = 2;
|
||||
getAllList();
|
||||
} else if (name == "expandThree") {
|
||||
expandLevel.value = 3;
|
||||
getAllList();
|
||||
}
|
||||
if (name == "expandAll") {
|
||||
expandLevel.value = 4;
|
||||
getAllList();
|
||||
} else if (name == "refresh") {
|
||||
getAllList();
|
||||
}
|
||||
}
|
||||
|
||||
function getAllList() {
|
||||
loading.value = true;
|
||||
getStoreAllPermissionList().then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
const level = expandLevel.value;
|
||||
res.result.forEach(function (e) {
|
||||
if (level == 1) {
|
||||
if (e.level == 0) {
|
||||
e.expand = false;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 2) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = false;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 3) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (level == 4) {
|
||||
if (e.level == 0) {
|
||||
e.expand = true;
|
||||
}
|
||||
if (e.children && e.children.length > 0) {
|
||||
e.children.forEach(function (c) {
|
||||
if (c.level == 1) {
|
||||
c.expand = true;
|
||||
}
|
||||
if (c.children && c.children.length > 0) {
|
||||
c.children.forEach(function (b) {
|
||||
if (b.level == 2) {
|
||||
b.expand = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
data.value = res.result;
|
||||
expandedKeys.value = collectExpandedKeys(res.result);
|
||||
treeKey.value += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
function onNodeClick(nodeData) {
|
||||
if (nodeData) {
|
||||
const menu = JSON.parse(JSON.stringify(nodeData));
|
||||
form.value = menu;
|
||||
editTitle.value = menu.title;
|
||||
currentNodeKey.value = menu.id;
|
||||
} else {
|
||||
cancelEdit();
|
||||
}
|
||||
}
|
||||
function onCheckChange(_data, { checkedNodes }) {
|
||||
selectList.value = checkedNodes;
|
||||
selectCount.value = checkedNodes.length;
|
||||
}
|
||||
function search() {
|
||||
if (searchKey.value) {
|
||||
loading.value = true;
|
||||
searchStorePermission({ title: searchKey.value }).then((res) => {
|
||||
getStoreAllPermissionList()
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result;
|
||||
expandedKeys.value = [];
|
||||
treeKey.value += 1;
|
||||
data.value = Array.isArray(res.result) ? res.result : [];
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function onSelectionChange(selection) {
|
||||
selectList.value = selection;
|
||||
selectCount.value = selection.length;
|
||||
}
|
||||
|
||||
function toggleExpandAll() {
|
||||
isAllExpanded.value = !isAllExpanded.value;
|
||||
const toggleRows = (rows) => {
|
||||
rows.forEach((row) => {
|
||||
tableRef.value?.toggleRowExpansion(row, isAllExpanded.value);
|
||||
if (row.children && row.children.length) {
|
||||
toggleRows(row.children);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
getAllList();
|
||||
}
|
||||
};
|
||||
toggleRows(data.value);
|
||||
}
|
||||
function cancelEdit() {
|
||||
treeRef.value?.setCurrentKey(null);
|
||||
currentNodeKey.value = null;
|
||||
formRef.value.resetFields();
|
||||
form.value.id = "";
|
||||
editTitle.value = "";
|
||||
}
|
||||
function handleReset() {
|
||||
formRef.value.resetFields();
|
||||
form.value.frontRoute = "";
|
||||
}
|
||||
function submitEdit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择要修改的菜单节点");
|
||||
return;
|
||||
}
|
||||
submitLoading.value = true;
|
||||
if (form.value.sortOrder == null) {
|
||||
form.value.sortOrder = 0;
|
||||
}
|
||||
delete form.value.icon;
|
||||
delete form.value.frontComponent;
|
||||
delete form.value.buttonType;
|
||||
delete form.value.updateTime;
|
||||
delete form.value.selected;
|
||||
delete form.value.description;
|
||||
delete form.value.children;
|
||||
|
||||
editStorePermission(form.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("编辑成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function submitAdd() {
|
||||
formAddRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
|
||||
addStorePermission(formAdd.value).then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success("添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
init();
|
||||
menuModalVisible.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function addMenu() {
|
||||
if (!form.value.id) {
|
||||
ElMessage.warning("请先点击选择一个菜单权限节点");
|
||||
return;
|
||||
}
|
||||
parentTitle.value = form.value.title;
|
||||
modalTitle.value = "添加子节点";
|
||||
showParent.value = true;
|
||||
if (form.value.level == 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录", "抱歉,不能添加啦", { type: "warning" });
|
||||
return;
|
||||
}
|
||||
formAdd.value = {
|
||||
parentId: form.value.id,
|
||||
level: Number(form.value.level) + 1,
|
||||
// 添加顶级菜单
|
||||
function addRootMenu() {
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "/",
|
||||
frontRoute: "Main",
|
||||
parentId: 0,
|
||||
level: 0,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
if (form.value.level == 0) {
|
||||
formAdd.value.path = "/";
|
||||
formAdd.value.frontRoute = "Main";
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
function addChildMenu(row) {
|
||||
if (row.level >= 2) {
|
||||
ElMessageBox.alert("仅支持2级菜单目录,不能再添加子菜单", "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
return;
|
||||
}
|
||||
menuModalVisible.value = true;
|
||||
}
|
||||
function addRootMenu() {
|
||||
modalTitle.value = "添加顶级菜单";
|
||||
showParent.value = false;
|
||||
formAdd.value = {
|
||||
level: 0,
|
||||
isEditMode.value = false;
|
||||
parentTitle.value = row.title;
|
||||
modalTitle.value = `添加子菜单 (上级: ${row.title})`;
|
||||
form.value = {
|
||||
id: "",
|
||||
title: "",
|
||||
name: "",
|
||||
path: "",
|
||||
frontRoute: "",
|
||||
parentId: row.id,
|
||||
level: Number(row.level) + 1,
|
||||
sortOrder: 0,
|
||||
permission: "",
|
||||
};
|
||||
menuModalVisible.value = true;
|
||||
if (row.level === 0) {
|
||||
form.value.path = "/";
|
||||
form.value.frontRoute = "Main";
|
||||
}
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑当前菜单
|
||||
function editRow(row) {
|
||||
isEditMode.value = true;
|
||||
parentTitle.value = "";
|
||||
modalTitle.value = `编辑菜单 - ${row.title}`;
|
||||
form.value = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
name: row.name,
|
||||
path: row.path,
|
||||
frontRoute: row.frontRoute,
|
||||
parentId: row.parentId,
|
||||
level: row.level,
|
||||
sortOrder: row.sortOrder ?? 0,
|
||||
permission: row.permission || "",
|
||||
};
|
||||
modalVisible.value = true;
|
||||
}
|
||||
|
||||
// 提交保存
|
||||
function submitForm() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const payload = { ...form.value };
|
||||
if (payload.sortOrder == null) {
|
||||
payload.sortOrder = 0;
|
||||
}
|
||||
delete payload.children;
|
||||
delete payload.icon;
|
||||
delete payload.frontComponent;
|
||||
delete payload.buttonType;
|
||||
delete payload.updateTime;
|
||||
delete payload.selected;
|
||||
delete payload.description;
|
||||
|
||||
const request = isEditMode.value
|
||||
? editStorePermission(payload)
|
||||
: addStorePermission(payload);
|
||||
|
||||
request
|
||||
.then((res) => {
|
||||
submitLoading.value = false;
|
||||
if (res.success) {
|
||||
ElMessage.success(isEditMode.value ? "编辑成功" : "添加成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
modalVisible.value = false;
|
||||
getAllList();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除单个
|
||||
function deleteRow(row) {
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除菜单 "${row.title}" 及其下级所有数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
deleteStorePermission(row.id).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
appStore.setAdded(false);
|
||||
util.initRouter();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function delAll() {
|
||||
if (selectCount.value <= 0) {
|
||||
ElMessage.warning("您还未勾选要删除的数据");
|
||||
return;
|
||||
}
|
||||
ElMessageBox.confirm("您确认要删除所选的 " + selectCount.value + " 条数据?", "确认删除", { type: "warning" }).then(() => {
|
||||
let ids = "";
|
||||
selectList.value.forEach(function (e) {
|
||||
ids += e.id + ",";
|
||||
});
|
||||
ids = ids.substring(0, ids.length - 1);
|
||||
ElMessageBox.confirm(
|
||||
`您确认要删除所选的 ${selectCount.value} 条数据?`,
|
||||
"确认删除",
|
||||
{
|
||||
type: "warning",
|
||||
confirmButtonText: "确定删除",
|
||||
cancelButtonText: "取消",
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const ids = selectList.value.map((e) => e.id).join(",");
|
||||
deleteStorePermission(ids).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
@@ -517,42 +440,100 @@ function delAll() {
|
||||
util.initRouter();
|
||||
selectList.value = [];
|
||||
selectCount.value = 0;
|
||||
cancelEdit();
|
||||
init();
|
||||
getAllList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const height = document.documentElement.clientHeight;
|
||||
maxHeight.value = Number(height - 287) + "px";
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/styles/tree-common.scss";
|
||||
.desc {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.menu-input {
|
||||
width: 362px;
|
||||
}
|
||||
.tree-node-icon {
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tree-node-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
.menu-manage-container {
|
||||
width: 100%;
|
||||
|
||||
<style lang="scss">
|
||||
.menu-manage-dialog .el-dialog__body {
|
||||
max-height: calc(100vh - 220px);
|
||||
overflow-y: auto;
|
||||
.manage-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 18px 20px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-table {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-table__row) {
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
color: #57a3f3;
|
||||
}
|
||||
|
||||
&.error-text {
|
||||
color: #ed4014;
|
||||
|
||||
&:hover {
|
||||
color: #f16643;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ops-row {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.op-split {
|
||||
margin: 0 8px;
|
||||
color: #dcdee2;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,45 +1,149 @@
|
||||
.search {
|
||||
.oss-operation {
|
||||
margin-bottom: 2vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin-right: 5px;
|
||||
.oss-header-card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 14px 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.none {
|
||||
display: none;
|
||||
.oss-main-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ebeef5;
|
||||
background: #fff;
|
||||
min-height: 600px;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-manage-box {
|
||||
display: flex;
|
||||
|
||||
.file-list {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #e5e6eb;
|
||||
padding: 20px 12px 28px;
|
||||
border-right: 1px solid #f0f2f5;
|
||||
padding: 0 16px 0 0;
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px 12px;
|
||||
margin-bottom: 4px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.add-group-btn {
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pic-list {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0 24px 0;
|
||||
padding: 0 0 0 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
.oss-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 2px 0 14px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
|
||||
.select-all-checkbox {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selection-badge {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
|
||||
b {
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img-box {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
gap: 16px 14px;
|
||||
margin-top: 16px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.img-item {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -47,10 +151,11 @@
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
aspect-ratio: 1 / 1;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #eef2f6;
|
||||
border: 1.5px solid #edf2f7;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
@@ -58,130 +163,343 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
transition: all 0.2s ease;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
|
||||
img,
|
||||
.card-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
margin: auto;
|
||||
&:hover {
|
||||
border-color: #ffaa66;
|
||||
box-shadow: 0 6px 16px -3px rgba(255, 106, 0, 0.16);
|
||||
|
||||
.card-actions {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-file {
|
||||
&.custom-checkbox-card-checked {
|
||||
border-color: #ff6a00 !important;
|
||||
background: #fffaf5 !important;
|
||||
box-shadow: 0 0 0 1px #ff6a00, 0 4px 12px -2px rgba(255, 106, 0, 0.25);
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-preview-media {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
color: #909399;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
|
||||
img,
|
||||
.card-media {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-play-overlay {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: all 0.2s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover .video-play-overlay {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
background: rgba(255, 106, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
.card-file-generic {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #edf2f7 100%);
|
||||
gap: 6px;
|
||||
|
||||
.file-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&.pdf .file-badge { background: #fee2e2; color: #dc2626; }
|
||||
&.doc .file-badge { background: #dbeafe; color: #2563eb; }
|
||||
&.xls .file-badge { background: #dcfce7; color: #16a34a; }
|
||||
&.zip .file-badge { background: #ffedd5; color: #ea580c; }
|
||||
&.code .file-badge { background: #f3e8ff; color: #9333ea; }
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 1000;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
border-radius: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1.5px solid #cbd5e1;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&::after {
|
||||
top: 1.5px;
|
||||
left: 4.5px;
|
||||
width: 3.5px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.is-checked .el-checkbox__inner) {
|
||||
background-color: #ff6a00;
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: #ff6a00;
|
||||
}
|
||||
.card-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 15;
|
||||
padding: 6px 8px;
|
||||
background: linear-gradient(to top, rgba(15, 23, 42, 0.88) 0%, rgba(15, 23, 42, 0.65) 85%, transparent 100%);
|
||||
backdrop-filter: blur(3px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
.custom-checkbox-card-checked {
|
||||
border: 2px solid #ff6a00 !important;
|
||||
background: #fffaf5 !important;
|
||||
}
|
||||
.action-btn {
|
||||
color: #f1f5f9;
|
||||
padding: 3px 5px;
|
||||
border-radius: 3px;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.oss-card-selectable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&.btn-delete:hover {
|
||||
color: #f87171;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
color: #252931;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-width: 220px;
|
||||
height: 18px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
color: #1e293b;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
margin-top: 6px;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
|
||||
.sub-info {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
height: 16px;
|
||||
color: #94a3b8;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
padding: 60px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.pagination-box {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.demo-tree-render .el-tree-node__label {
|
||||
width: 94%;
|
||||
margin-top: 24px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-group-tree {
|
||||
padding: 4px 0 0;
|
||||
|
||||
.pc-image-picker-dialog {
|
||||
margin-top: 16px !important;
|
||||
margin-bottom: 16px !important;
|
||||
max-height: calc(100vh - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-dialog__header {
|
||||
padding: 12px 20px 8px;
|
||||
flex-shrink: 0;
|
||||
:deep(.el-tree-node__expand-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 6px 16px 12px;
|
||||
overflow: hidden !important;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
flex: 1;
|
||||
:deep(.el-tree-node) {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none !important; /* Chrome, Safari, Edge */
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
:deep(.el-tree-node__content) {
|
||||
padding: 0 10px !important;
|
||||
height: 38px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 38px;
|
||||
color: #334155;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node__content:hover) {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
:deep(.el-tree-node.is-current > .el-tree-node__content) {
|
||||
background: #fff7ed;
|
||||
color: #ff6a00;
|
||||
font-weight: 600;
|
||||
|
||||
.oss-tree-icon {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.oss-tree-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding-right: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
.node-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
.oss-tree-icon {
|
||||
font-size: 16px;
|
||||
color: #94a3b8;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.oss-tree-more {
|
||||
cursor: pointer;
|
||||
color: #94a3b8;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e2e8f0;
|
||||
color: #1e293b;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .oss-tree-more {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.file-preview-fallback {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
color: #606266;
|
||||
|
||||
.file-preview-name {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.file-preview-tip {
|
||||
margin: 0 0 20px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
// 弹窗组件调用时的兼容样式
|
||||
.is-component-search {
|
||||
.is-component-card {
|
||||
border: none !important;
|
||||
@@ -196,22 +514,32 @@
|
||||
|
||||
.oss-manage-box {
|
||||
.file-list {
|
||||
padding: 0 10px 10px;
|
||||
padding: 0 10px 10px 0;
|
||||
}
|
||||
.pic-list {
|
||||
padding: 0;
|
||||
padding: 0 0 0 10px;
|
||||
}
|
||||
.oss-operation {
|
||||
margin-bottom: 6px;
|
||||
.oss-toolbar {
|
||||
padding: 0 0 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.img-box {
|
||||
margin-top: 6px;
|
||||
margin-top: 8px;
|
||||
gap: 10px 12px;
|
||||
|
||||
.img-item {
|
||||
.card {
|
||||
max-width: 128px;
|
||||
max-width: 130px;
|
||||
margin: 0 auto;
|
||||
|
||||
// 在选图组件模式下,不需要hover遮罩把选择搞复杂
|
||||
.card-actions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card-checkbox {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
height: 18px;
|
||||
@@ -228,12 +556,17 @@
|
||||
}
|
||||
}
|
||||
.page-box {
|
||||
margin-top: 8px;
|
||||
margin-top: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.oss-select-footer {
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
margin-top: 12px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid #ededed;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,13 +574,13 @@
|
||||
padding: 0;
|
||||
|
||||
.el-tree-node {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.el-tree-node__content {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-size: 14px;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,10 +49,11 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog
|
||||
<el-drawer
|
||||
v-model="roleModalVisible"
|
||||
:title="modalTitle"
|
||||
width="500px"
|
||||
size="500px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
>
|
||||
@@ -68,7 +69,7 @@
|
||||
<el-button @click="roleModalVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="submitRole">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-drawer>
|
||||
|
||||
<el-dialog
|
||||
v-model="permModalVisible"
|
||||
|
||||
@@ -1,48 +1,32 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="row" v-for="(item, index) in formValidate" :key="index">
|
||||
<div class="col">
|
||||
<el-card shadow="never" :body-style="{ padding: '0 16px' }">
|
||||
<div class="icon-item" v-if="item.client == 'APP'">
|
||||
<img class="icon" src="../../../../assets/setting/app.svg" alt="" />
|
||||
</div>
|
||||
<div class="icon-item" v-if="item.client == 'PC'">
|
||||
<img class="icon" src="../../../../assets/setting/pc.svg" alt="" />
|
||||
</div>
|
||||
<div class="icon-item" v-if="item.client == 'WECHAT_MP'">
|
||||
<img class="icon" src="../../../../assets/setting/wechat_mp.svg" alt="" />
|
||||
</div>
|
||||
<div class="icon-item" v-if="item.client == 'H5'">
|
||||
<img class="icon" src="../../../../assets/setting/h5.svg" alt="" />
|
||||
</div>
|
||||
<div class="pay-title">{{ way[item.client] }}</div>
|
||||
|
||||
<div>
|
||||
<el-divider content-position="left">支付设置</el-divider>
|
||||
|
||||
<div class="pay-list">
|
||||
<el-checkbox-group v-model="item.supports" @change="handleChangePayType">
|
||||
<el-checkbox
|
||||
v-for="(support, i) in supportForm.payments"
|
||||
:key="i"
|
||||
:value="support"
|
||||
>
|
||||
{{ payWay[support] || support }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<div class="setting-page">
|
||||
<div class="setting-section" v-for="(item, index) in formValidate" :key="index">
|
||||
<div class="section-title">
|
||||
<img class="client-icon" :src="clientIcons[item.client]" alt="" />
|
||||
<span>{{ way[item.client] || item.client }}</span>
|
||||
</div>
|
||||
<div class="pay-options">
|
||||
<el-checkbox
|
||||
v-for="payment in paymentOptions"
|
||||
:key="payment"
|
||||
:model-value="item.supports.includes(payment)"
|
||||
:label="payWay[payment] || payment"
|
||||
@change="(val) => togglePay(item, payment, val)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { getCurrentInstance, ref } from "vue";
|
||||
import { computed, getCurrentInstance, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { setSetting } from "@/api/index";
|
||||
import { handleSubmit } from "../setting/validate";
|
||||
import { getPaymentSupportForm } from "@/api/setting";
|
||||
import iconApp from "../../../../assets/setting/app.svg";
|
||||
import iconPc from "../../../../assets/setting/pc.svg";
|
||||
import iconWechatMp from "../../../../assets/setting/wechat_mp.svg";
|
||||
import iconH5 from "../../../../assets/setting/h5.svg";
|
||||
|
||||
const DEFAULT_PAYMENT_SUPPORT_ITEMS = [
|
||||
{ client: "PC", supports: [] },
|
||||
@@ -58,10 +42,16 @@ const { proxy } = getCurrentInstance();
|
||||
|
||||
const ruleValidate = ref({});
|
||||
const way = {
|
||||
APP: "移动应用端",
|
||||
H5: "移动端",
|
||||
WECHAT_MP: "小程序端",
|
||||
PC: "PC端",
|
||||
APP: "APP",
|
||||
H5: "手机网页",
|
||||
WECHAT_MP: "微信小程序",
|
||||
PC: "电脑网页",
|
||||
};
|
||||
const clientIcons = {
|
||||
APP: iconApp,
|
||||
PC: iconPc,
|
||||
WECHAT_MP: iconWechatMp,
|
||||
H5: iconH5,
|
||||
};
|
||||
const formValidate = ref([]);
|
||||
const payWay = {
|
||||
@@ -70,7 +60,8 @@ const payWay = {
|
||||
WALLET: "余额支付",
|
||||
};
|
||||
const supportForm = ref("");
|
||||
const checkSupport = ref({});
|
||||
|
||||
const paymentOptions = computed(() => supportForm.value.payments || []);
|
||||
|
||||
function submit(name) {
|
||||
handleSubmit(proxy, name)
|
||||
@@ -79,17 +70,21 @@ function submit(name) {
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
function handleChangePayType(val) {
|
||||
ElMessageBox.confirm("您是否修改此项?", "修改支付设置", { type: "warning" }).then(() => {
|
||||
setupSetting();
|
||||
}).catch(() => {
|
||||
formValidate.value = JSON.parse(JSON.stringify(checkSupport.value));
|
||||
});
|
||||
function togglePay(row, payment, checked) {
|
||||
ElMessageBox.confirm("您是否修改此项?", "修改支付设置", { type: "warning" })
|
||||
.then(() => {
|
||||
if (checked) {
|
||||
row.supports.push(payment);
|
||||
} else {
|
||||
row.supports = row.supports.filter((p) => p !== payment);
|
||||
}
|
||||
setupSetting();
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
function setupSetting() {
|
||||
setSetting(props.type, { paymentSupportItems: formValidate.value }).then((res) => {
|
||||
if (res.success) {
|
||||
checkSupport.value = JSON.parse(JSON.stringify(formValidate.value));
|
||||
ElMessage.success("保存成功!");
|
||||
} else {
|
||||
ElMessage.error("保存失败!");
|
||||
@@ -101,7 +96,10 @@ function init() {
|
||||
const items = setting.paymentSupportItems;
|
||||
formValidate.value =
|
||||
Array.isArray(items) && items.length ? items : DEFAULT_PAYMENT_SUPPORT_ITEMS;
|
||||
checkSupport.value = JSON.parse(JSON.stringify(formValidate.value));
|
||||
// 清理历史已保存的失效支付方式,下次保存时一并去除
|
||||
formValidate.value.forEach((item) => {
|
||||
item.supports = (item.supports || []).filter((p) => p in payWay);
|
||||
});
|
||||
|
||||
getPaymentSupportForm().then((res) => {
|
||||
if (res.success && res.result) {
|
||||
@@ -115,48 +113,37 @@ init();
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../setting/style.scss";
|
||||
.pay-title {
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.layout {
|
||||
padding: 20px;
|
||||
|
||||
.setting-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.client-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.pay-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.row {
|
||||
width: 300px;
|
||||
margin-right: 20px;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.label-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.pay-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.icon-item {
|
||||
width: 100%;
|
||||
padding: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
.el-form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
gap: 8px 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -98,23 +98,11 @@
|
||||
<el-radio-button value="CLOSE">关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品页面小图宽度" prop="smallPictureWidth">
|
||||
<el-input v-model="goods.smallPictureWidth" placeholder="商品页面小图宽度" style="width: 350px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="商品页面小图高度" prop="smallPictureHeight">
|
||||
<el-input v-model="goods.smallPictureHeight" placeholder="商品页面小图高度" style="width: 350px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缩略图宽度" prop="abbreviationPictureWidth">
|
||||
<el-input v-model="goods.abbreviationPictureWidth" placeholder="缩略图宽度" style="width: 350px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="缩略图高度" prop="abbreviationPictureHeight">
|
||||
<el-input v-model="goods.abbreviationPictureHeight" placeholder="缩略图高度" style="width: 350px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原图宽" prop="originalPictureWidth">
|
||||
<el-input v-model="goods.originalPictureWidth" placeholder="原图宽" style="width: 350px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原图高" prop="originalPictureHeight">
|
||||
<el-input v-model="goods.originalPictureHeight" placeholder="原图高" style="width: 350px" />
|
||||
<el-form-item label="自营店商品审核" prop="selfOperatedGoodsCheck">
|
||||
<el-radio-group v-model="goods.selfOperatedGoodsCheck">
|
||||
<el-radio-button value="OPEN">需要审核</el-radio-button>
|
||||
<el-radio-button value="CLOSE">无需审核</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" style="width: 100px;margin-right:5px" :loading="saveLoading" @click="saveGoods">
|
||||
@@ -162,12 +150,7 @@ const order = ref({
|
||||
});
|
||||
const goods = ref({
|
||||
goodsCheck: "OPEN",
|
||||
smallPictureWidth: "",
|
||||
smallPictureHeight: "",
|
||||
abbreviationPictureWidth: "",
|
||||
abbreviationPictureHeight: "",
|
||||
originalPictureWidth: "",
|
||||
originalPictureHeight: "",
|
||||
selfOperatedGoodsCheck: "OPEN",
|
||||
});
|
||||
// 历史遗留:wechat/qq/weibo/alipay 未在 data 中声明,直接挂 this,且模板中无对应表单项
|
||||
let wechat;
|
||||
@@ -262,86 +245,7 @@ const orderValidate = {
|
||||
},
|
||||
],
|
||||
};
|
||||
const goodsValidate = {
|
||||
smallPictureWidth: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
smallPictureHeight: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
abbreviationPictureWidth: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
abbreviationPictureHeight: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
originalPictureWidth: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
originalPictureHeight: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule, value, callback) => {
|
||||
if (!regular.integer.test(value)) {
|
||||
callback(new Error("请输入正整数,且不为零!"));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
};
|
||||
const goodsValidate = {};
|
||||
|
||||
const baseForm = ref(null);
|
||||
const pointForm = ref(null);
|
||||
@@ -401,7 +305,10 @@ function initGoods() {
|
||||
if (res.success) {
|
||||
loading.value = false;
|
||||
if (res.result) {
|
||||
goods.value = res.result;
|
||||
goods.value = {
|
||||
goodsCheck: res.result.goodsCheck,
|
||||
selfOperatedGoodsCheck: res.result.selfOperatedGoodsCheck ?? res.result.goodsCheck,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,81 +1,61 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="setting-page">
|
||||
<el-form ref="formValidateRef" label-width="150px" label-position="right" :model="formValidate" :rules="ruleValidate">
|
||||
<el-form-item label="站点名称" prop="siteName">
|
||||
<el-input style="width:200px;" v-model="formValidate.siteName" />
|
||||
<span class="desc">配置买家端站点名称</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="icp" prop="icp">
|
||||
<el-input v-model="formValidate.icp" />
|
||||
</el-form-item>
|
||||
<div class="setting-section">
|
||||
<div class="section-title">商城信息</div>
|
||||
<el-form-item label="商城名称" prop="siteName">
|
||||
<el-input v-model="formValidate.siteName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点地址" prop="staticPageAddress">
|
||||
<el-input v-model="formValidate.staticPageAddress" />
|
||||
</el-form-item>
|
||||
<el-form-item label="wap站点地址" prop="staticPageWapAddress">
|
||||
<el-input v-model="formValidate.staticPageWapAddress" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="后台Logo" prop="domainLogo">
|
||||
<div class="label-item-upload">
|
||||
<img alt="平台 Logo" v-if="formValidate.domainLogo" class="img" :src="formValidate.domainLogo" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('domainLogo')">选择图片</el-button>
|
||||
<span class="desc">后台管理左上角logo展示 17∶6</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="后台Icon" prop="domainIcon">
|
||||
<div class="label-item-upload">
|
||||
<img alt="平台图标" v-if="formValidate.domainIcon" class="img" :src="formValidate.domainIcon" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('domainIcon')">选择图片</el-button>
|
||||
<span class="desc">后台管理网站icon展示 1∶1</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="买家端Logo" prop="buyerSideLogo">
|
||||
<div class="label-item-upload">
|
||||
<img alt="买家端 Logo" v-if="formValidate.buyerSideLogo" class="img" :src="formValidate.buyerSideLogo" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('buyerSideLogo')">选择图片</el-button>
|
||||
<span class="desc">买家端logo展示 17∶6</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="买家端Icon" prop="buyerSideIcon">
|
||||
<div class="label-item-upload">
|
||||
<img alt="买家端图标" v-if="formValidate.buyerSideIcon" class="img" :src="formValidate.buyerSideIcon" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('buyerSideIcon')">选择图片</el-button>
|
||||
<span class="desc">买家端网站icon展示 1∶1</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商家端Logo" prop="storeSideLogo">
|
||||
<div class="label-item-upload">
|
||||
<img alt="商家端 Logo" v-if="formValidate.storeSideLogo" class="img" :src="formValidate.storeSideLogo" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('storeSideLogo')">选择图片</el-button>
|
||||
<span class="desc">商家端logo展示 17∶6</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商家端Icon" prop="storeSideIcon">
|
||||
<div class="label-item-upload">
|
||||
<img alt="商家端图标" v-if="formValidate.storeSideIcon" class="img" :src="formValidate.storeSideIcon" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('storeSideIcon')">选择图片</el-button>
|
||||
<span class="desc">商家端icon展示 1∶1</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商城Logo" prop="domainLogo">
|
||||
<div class="upload-item">
|
||||
<div class="upload-row">
|
||||
<img alt="商城 Logo" v-if="formValidate.domainLogo" class="img img-logo" :src="formValidate.domainLogo" />
|
||||
<img v-else class="img img-logo" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('domainLogo')">选择图片</el-button>
|
||||
</div>
|
||||
<span class="field-desc">各端logo统一展示,建议比例 17:6</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="商城Icon" prop="domainIcon">
|
||||
<div class="upload-item">
|
||||
<div class="upload-row">
|
||||
<img alt="商城图标" v-if="formValidate.domainIcon" class="img" :src="formValidate.domainIcon" />
|
||||
<img v-else class="img" src="../../../../assets/emptyImg.png" alt="" />
|
||||
<el-button @click="onClickImg('domainIcon')">选择图片</el-button>
|
||||
</div>
|
||||
<span class="field-desc">各端网站icon统一展示,建议比例 1:1</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item label="站点地址" prop="staticPageAddress">
|
||||
<el-input style="width:200px;" v-model="formValidate.staticPageAddress" />
|
||||
</el-form-item>
|
||||
<el-form-item label="wap站点地址" prop="staticPageWapAddress">
|
||||
<el-input v-model="formValidate.staticPageWapAddress" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司名称" prop="companyName">
|
||||
<el-input style="width:350px;" v-model="formValidate.companyName" />
|
||||
<span class="desc">页脚展示的公司名称</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="公司链接" prop="companyUrl">
|
||||
<el-input style="width:350px;" v-model="formValidate.companyUrl" />
|
||||
<span class="desc">页脚公司名称的跳转链接</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="官网地址" prop="website">
|
||||
<el-input style="width:350px;" v-model="formValidate.website" />
|
||||
</el-form-item>
|
||||
<div class="label-btns">
|
||||
<div class="setting-section">
|
||||
<div class="section-title">公司信息</div>
|
||||
<el-form-item label="公司名称" prop="companyName">
|
||||
<el-input v-model="formValidate.companyName" />
|
||||
</el-form-item>
|
||||
<el-form-item label="公司链接" prop="companyUrl">
|
||||
<el-input v-model="formValidate.companyUrl" />
|
||||
</el-form-item>
|
||||
<el-form-item label="官网地址" prop="website">
|
||||
<el-input v-model="formValidate.website" />
|
||||
</el-form-item>
|
||||
<el-form-item label="ICP备案号" prop="icp">
|
||||
<el-input v-model="formValidate.icp" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版权归属" prop="copyrightOwner">
|
||||
<el-input v-model="formValidate.copyrightOwner" placeholder="留空时使用商城名称" maxlength="100" />
|
||||
<span class="field-desc">仅填写版权归属名称,Copyright © 和当前年份由系统自动显示</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="submit-bar">
|
||||
<el-button type="primary" @click="submit('formValidateRef')">保存</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -115,6 +95,7 @@ const formValidate = ref({
|
||||
buyerSideLogo: "",
|
||||
companyName: "",
|
||||
companyUrl: "",
|
||||
copyrightOwner: "",
|
||||
domainLogo: "",
|
||||
icp: "",
|
||||
storeSideLogo: "",
|
||||
@@ -152,6 +133,10 @@ function callbackSelected(val) {
|
||||
formValidate.value[selected.value] = val.url;
|
||||
}
|
||||
function setupSetting() {
|
||||
formValidate.value.buyerSideLogo = formValidate.value.domainLogo;
|
||||
formValidate.value.storeSideLogo = formValidate.value.domainLogo;
|
||||
formValidate.value.buyerSideIcon = formValidate.value.domainIcon;
|
||||
formValidate.value.storeSideIcon = formValidate.value.domainIcon;
|
||||
setSetting(props.type, formValidate.value).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("保存成功!");
|
||||
@@ -165,8 +150,9 @@ function setupSetting() {
|
||||
}
|
||||
function init() {
|
||||
result.value = JSON.parse(props.res);
|
||||
formValidate.value = { ...result.value };
|
||||
formValidate.value = { copyrightOwner: "", ...result.value };
|
||||
Object.keys(result.value).forEach((item) => {
|
||||
if (item === "copyrightOwner") return;
|
||||
ruleValidate[item] = [
|
||||
{
|
||||
required: true,
|
||||
@@ -181,20 +167,65 @@ init();
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import "./style.scss";
|
||||
.label-item {
|
||||
display: flex;
|
||||
> .el-input {
|
||||
width: 200px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.setting-page {
|
||||
padding: 16px 0;
|
||||
}
|
||||
.label-item-upload {
|
||||
|
||||
.setting-section {
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
padding: 20px 24px 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.field-desc {
|
||||
margin-top: 8px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
img {
|
||||
margin-right: 10px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: contain;
|
||||
}
|
||||
// Logo 按建议比例 17:6 展示
|
||||
img.img-logo {
|
||||
width: 170px;
|
||||
height: 60px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
}
|
||||
}
|
||||
|
||||
// 覆盖共享样式里 :deep(.el-input) 的 200px !important,统一输入框宽度
|
||||
:deep(.el-input) {
|
||||
width: 400px !important;
|
||||
}
|
||||
|
||||
.submit-bar {
|
||||
text-align: center;
|
||||
margin: 24px 0 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user