mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-07 01:18:54 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2497f33cb4 | ||
|
|
0f08807f00 | ||
|
|
29a6b00a21 | ||
|
|
6be934d00b | ||
|
|
560286b7f6 | ||
|
|
961565149c | ||
|
|
1a457a60f8 | ||
|
|
d521587ad8 | ||
|
|
cbaf89c835 | ||
|
|
f6f106e12b | ||
|
|
484b6a6dad | ||
|
|
d64dd08b42 | ||
|
|
5300988643 | ||
|
|
fb99824a3a | ||
|
|
22f3e9b5e9 | ||
|
|
7db1ee4fb7 | ||
|
|
4e33ab19b6 | ||
|
|
ce11f50942 | ||
|
|
ccf243f31a | ||
|
|
c0cc8d8598 | ||
|
|
2563f122fc | ||
|
|
8739811b04 | ||
|
|
5973d43b9d | ||
|
|
d113f5c553 | ||
|
|
5c7b109be6 | ||
|
|
97e611c072 | ||
|
|
68dfae9a62 | ||
|
|
bb8e9a0d3d | ||
|
|
f4fd1c1acd | ||
|
|
21abd26b1f | ||
|
|
a55c8356c8 | ||
|
|
b025ab804e | ||
|
|
700c709afd | ||
|
|
8d0788e09b | ||
|
|
55a431ddd5 | ||
|
|
b72c792ade | ||
|
|
1f872254cc | ||
|
|
99e7f6ed47 | ||
|
|
06e02998aa | ||
|
|
1395f2ad24 | ||
|
|
67e8c86b5b | ||
|
|
9fcd325df9 |
216
LICENSE
216
LICENSE
@@ -1,21 +1,205 @@
|
|||||||
MIT License
|
Apache License, Version 2.0
|
||||||
|
|
||||||
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Apache License
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
Version 2.0, January 2004
|
||||||
in the Software without restriction, including without limitation the rights
|
http://www.apache.org/licenses/
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1. Definitions.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
SOFTWARE.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
|
||||||
|
|
||||||
### ✨ Why Choose Lilishop
|
### ✨ Why Choose Lilishop
|
||||||
- 🟢 **True MIT License**: Free for commercial use and private secondary development without licensing concerns.
|
- 🟢 **True Apache-2.0 License**: Free for commercial use and private secondary development without licensing concerns.
|
||||||
- 🚀 **Modern Vue 3 Stack**: Built on `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
|
- 🚀 **Modern Vue 3 Stack**: Built on `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
|
||||||
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
|
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
|
||||||
|
|
||||||
@@ -141,4 +141,4 @@ lilishop-ui
|
|||||||
|
|
||||||
## 6. License
|
## 6. License
|
||||||
|
|
||||||
Lilishop is licensed under the [MIT](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
Lilishop is licensed under the [Apache-2.0](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -5,7 +5,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
|
||||||
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
|
||||||
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License" /></a>
|
||||||
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
|
||||||
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
|
||||||
|
|
||||||
### ✨ 为什么选择 Lilishop
|
### ✨ 为什么选择 Lilishop
|
||||||
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
- 🟢 **真 Apache-2.0 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
|
||||||
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
|
||||||
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
|
||||||
|
|
||||||
@@ -146,7 +146,7 @@ lilishop-ui
|
|||||||
|
|
||||||
## 6. 开源与授权
|
## 6. 开源与授权
|
||||||
|
|
||||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
本项目遵循 [Apache-2.0](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -163,19 +163,44 @@ lilishop-ui
|
|||||||
## 8. 升级计划
|
## 8. 升级计划
|
||||||
|
|
||||||
### ✅ 已完成特性
|
### ✅ 已完成特性
|
||||||
| 功能模块 | 功能模块 |
|
|
||||||
| :--- | :--- |
|
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
|
||||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
|
||||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
| 时间 | 功能模块 | 完成内容 |
|
||||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
| :--- | :--- | :--- |
|
||||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
| 2026-11-05 | ✅ 付费会员 | 单一付费会员、开通与续费、有效期、关联权益、商家与商品折扣、平台补贴及付费记录。 |
|
||||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
| 2026-11-04 | ✅ 客户等级体系 / 客户权益 | 等级配置、权益介绍、经验值任务与明细、权益发放及重试,提供默认等级与权益数据。 |
|
||||||
| ✅ 定时任务管理 | ✅ 预约服务商品 |
|
| 2026-11-04 | ✅ 小程序订单管理 | 增加小程序订单查询入口及微信订单自动上报开关。 |
|
||||||
| ✅ 主题色管理 | ✅ 商家发货改造(整单/拆单/无需配送) |
|
| 2026-11-04 | ✅ 立减优惠券 | 新增立减券,完善优惠券校验、领取与订单优惠计算。 |
|
||||||
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
|
| 2026-11-04 | ✅ 商品定时上下架 | 完善定时上下架计划管理、独立列表及执行校验。 |
|
||||||
|
| 2026-11-04 | ✅ 微信小程序原生能力 | 接入微信地址导入、头像选择和昵称填写。 |
|
||||||
|
| 2026-11-02 | ✅ 消息模板 | 支持多渠道模板矩阵维护,统一站内信、短信、微信公众号及小程序订阅消息的场景配置和异步发送。 |
|
||||||
|
| 2026-10-30 | ✅ PC 页面装修 | 重构楼层组件,支持自适应高度、可视化热区绘制及配置交互优化。 |
|
||||||
|
| 2026-10-29 | ✅ 定时任务管理 | 重构为数据库配置驱动,支持运营后台维护任务。 |
|
||||||
|
| 2026-10-29 | ✅ 敏感词管理 | 支持按业务场景配置过滤开关,并优化词库加载。 |
|
||||||
|
| 2026-10-29 | ✅ PC 店铺搜索 | 新增店铺列表,支持商品与店铺双模式搜索。 |
|
||||||
|
| 2026-10-22 | ✅ 分类设置 | 支持四种分类页模板选择、商品列表展示字段开关及模板图片预览。 |
|
||||||
|
| 2026-10-20 | ✅ 移动端页面装修 | 完善可视化编辑器,新增首页组合组件及优惠券、店铺、文章等楼层。 |
|
||||||
|
| 2026-10-09 | ✅ 主题色管理 | 完善主题设置,支持自定义模式。 |
|
||||||
|
| 2026-10-02 | ✅ 预约服务商品 | 完善预约商品、预约交易及订单详情功能。 |
|
||||||
|
| 2026-08-05 | ✅ 二级分销 | 由一级分销扩展为二级分销,增加节点配置及分销员中心。 |
|
||||||
|
| 2026-08-03 | ✅ 卡密商品 | 完善电子卡券履约、订单状态及卡池管理。 |
|
||||||
|
| 2026-07-23 | ✅ 素材库管理 | 增加文件分类、目录管理及目录归属校验。 |
|
||||||
|
| 2026-07-20 | ✅ 经营统计报表 | 新增店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表。 |
|
||||||
|
| 2026-07-13 | ✅ 腾讯云直播 | 接入并完善直播、聊天与直播商品相关功能。 |
|
||||||
|
| 2026-06-30 | ✅ 移动端技术升级 | 升级 Vue 3 与 uView Plus,适配组件和页面。 |
|
||||||
|
| 2026-06-29 | ✅ 电子面单发货 | 完善快递鸟电子面单,支持自动携带运单号发货。 |
|
||||||
|
| 2026-06-05 | ✅ 后端与管理端框架升级 | 5 月 25 日至 6 月 5 日完成 Spring Boot 4、管理端 Vue 3 与 Element Plus 升级。 |
|
||||||
|
| 2026-05-22 | ✅ 购物卡 | 新增礼品卡接口,以及选择、绑定、激活和列表管理。 |
|
||||||
|
| 2026-05-20 | ✅ 微信提现 | 完善转账状态查询、确认回调和重试处理,后续补充提现规则说明配置。 |
|
||||||
|
| 2026-04-16 | ✅ 置顶评论 | 支持评论置顶及商品评价管理。 |
|
||||||
|
| 2026-04-13 | ✅ 专用发票(发票上传) | 完善发票信息字段、发票详情及管理展示。 |
|
||||||
|
| 2026-04-01 | ✅ 商品虚拟销量 | 支持按商品规格设置虚拟销量,提供单个及批量设置入口。 |
|
||||||
|
| 2026-03-30 | ✅ 商品分组 | 新增商品分组管理及商品关联功能。 |
|
||||||
|
| 2026-03-24 | ✅ 会员分组 | 新增会员分组管理。 |
|
||||||
|
|
||||||
### 🔜 近期规划中
|
### 🔜 近期规划中
|
||||||
- 🔜 店铺结算汇总 & 结算台账
|
|
||||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
|
||||||
- 🔜 微信小店对接
|
- 🔜 微信小店对接
|
||||||
- 🔜 抖店对接
|
- 🔜 抖店对接
|
||||||
|
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
|
||||||
|
- 🔜 OpenAPI 开放平台与开发者中心
|
||||||
|
|||||||
40
buyer/pnpm-lock.yaml
generated
40
buyer/pnpm-lock.yaml
generated
@@ -36,6 +36,9 @@ importers:
|
|||||||
nprogress:
|
nprogress:
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
|
pinia:
|
||||||
|
specifier: ^4.0.3
|
||||||
|
version: 4.1.0(@vue/devtools-api@6.6.4)(vue@3.5.43)
|
||||||
psl:
|
psl:
|
||||||
specifier: ^1.8.0
|
specifier: ^1.8.0
|
||||||
version: 1.15.0
|
version: 1.15.0
|
||||||
@@ -60,9 +63,6 @@ importers:
|
|||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.6.4
|
specifier: ^4.6.4
|
||||||
version: 4.6.4(vue@3.5.43)
|
version: 4.6.4(vue@3.5.43)
|
||||||
vuex:
|
|
||||||
specifier: ^4.1.0
|
|
||||||
version: 4.1.0(vue@3.5.43)
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@vue/cli-plugin-babel':
|
'@vue/cli-plugin-babel':
|
||||||
specifier: ^5.0.9
|
specifier: ^5.0.9
|
||||||
@@ -91,6 +91,9 @@ importers:
|
|||||||
less-loader:
|
less-loader:
|
||||||
specifier: ^11.1.4
|
specifier: ^11.1.4
|
||||||
version: 11.1.4(less@4.9.1)(webpack@5.111.0(postcss@8.5.28))
|
version: 11.1.4(less@4.9.1)(webpack@5.111.0(postcss@8.5.28))
|
||||||
|
vue-eslint-parser:
|
||||||
|
specifier: ^10.4.1
|
||||||
|
version: 10.4.1(eslint@9.39.4)
|
||||||
webpack:
|
webpack:
|
||||||
specifier: ^5.95.0
|
specifier: ^5.95.0
|
||||||
version: 5.111.0(cssnano@5.1.15(postcss@8.5.28))(postcss@8.5.28)
|
version: 5.111.0(cssnano@5.1.15(postcss@8.5.28))(postcss@8.5.28)
|
||||||
@@ -2968,6 +2971,9 @@ packages:
|
|||||||
normalize-wheel-es@1.2.0:
|
normalize-wheel-es@1.2.0:
|
||||||
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
resolution: {integrity: sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==}
|
||||||
|
|
||||||
|
nostics@1.3.0:
|
||||||
|
resolution: {integrity: sha512-wzRsrJwNlFbqIRQt8aCeL2WE8USip/uuvGSOZV1D02CxX8ABTMbw8IywKjH7yWHETMXbWJI4OBpihg9rmz2Vyg==}
|
||||||
|
|
||||||
npm-run-path@2.0.2:
|
npm-run-path@2.0.2:
|
||||||
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3135,6 +3141,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
pinia@4.1.0:
|
||||||
|
resolution: {integrity: sha512-+5/HMuVjYQfKlCD5xQ7zfXn1xUzYFEa7pd0DQK5SaMfZCaFRTHlaH/c02N1FoGOdlzSiwf5EOPgJx//1QDw6gw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/devtools-api': ^8.2.1
|
||||||
|
typescript: '>=5.6.0'
|
||||||
|
vue: ^3.5.11
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pkg-dir@4.2.0:
|
pkg-dir@4.2.0:
|
||||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -4006,11 +4022,6 @@ packages:
|
|||||||
typescript:
|
typescript:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
vuex@4.1.0:
|
|
||||||
resolution: {integrity: sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==}
|
|
||||||
peerDependencies:
|
|
||||||
vue: ^3.2.0
|
|
||||||
|
|
||||||
watchpack@2.5.2:
|
watchpack@2.5.2:
|
||||||
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
|
resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -7374,6 +7385,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-wheel-es@1.2.0: {}
|
normalize-wheel-es@1.2.0: {}
|
||||||
|
|
||||||
|
nostics@1.3.0: {}
|
||||||
|
|
||||||
npm-run-path@2.0.2:
|
npm-run-path@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 2.0.1
|
path-key: 2.0.1
|
||||||
@@ -7533,6 +7546,12 @@ snapshots:
|
|||||||
picomatch@4.0.7:
|
picomatch@4.0.7:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
pinia@4.1.0(@vue/devtools-api@6.6.4)(vue@3.5.43):
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 6.6.4
|
||||||
|
nostics: 1.3.0
|
||||||
|
vue: 3.5.43
|
||||||
|
|
||||||
pkg-dir@4.2.0:
|
pkg-dir@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up: 4.1.0
|
find-up: 4.1.0
|
||||||
@@ -8449,11 +8468,6 @@ snapshots:
|
|||||||
'@vue/server-renderer': 3.5.43
|
'@vue/server-renderer': 3.5.43
|
||||||
'@vue/shared': 3.5.43
|
'@vue/shared': 3.5.43
|
||||||
|
|
||||||
vuex@4.1.0(vue@3.5.43):
|
|
||||||
dependencies:
|
|
||||||
'@vue/devtools-api': 6.6.4
|
|
||||||
vue: 3.5.43
|
|
||||||
|
|
||||||
watchpack@2.5.2:
|
watchpack@2.5.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
|
|||||||
@@ -95,21 +95,25 @@ export function cancelStoreCollect (type, id) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看是否收藏商品
|
// 查看是否收藏商品(后台查询,token 失效时静默失败,不弹全局登录框)
|
||||||
export function isCollection (type, goodsId) {
|
export function isCollection (type, goodsId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/member/collection/isCollection/${type}/${goodsId}`,
|
url: `/buyer/member/collection/isCollection/${type}/${goodsId}`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
needToken: true
|
needToken: true,
|
||||||
|
skipAuthHandler: true,
|
||||||
|
skipMessage: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查看是否收藏店铺
|
// 查看是否收藏店铺(后台查询,token 失效时静默失败,不弹全局登录框)
|
||||||
export function isStoreCollection (type, goodsId) {
|
export function isStoreCollection (type, goodsId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/member/storeCollection/isCollection/${type}/${goodsId}`,
|
url: `/buyer/member/storeCollection/isCollection/${type}/${goodsId}`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
needToken: true
|
needToken: true,
|
||||||
|
skipAuthHandler: true,
|
||||||
|
skipMessage: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,22 +137,26 @@ export function storeCollectList (params) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单个商品评价
|
// 单个商品评价(需登录;token 失效时静默失败,不弹全局登录框)
|
||||||
export function goodsComment (params) {
|
export function goodsComment (params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/member/evaluation/${params.goodsId}/goodsEvaluation`,
|
url: `/buyer/member/evaluation/${params.goodsId}/goodsEvaluation`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
needToken: false,
|
needToken: true,
|
||||||
|
skipAuthHandler: true,
|
||||||
|
skipMessage: true,
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 商品各评价类别数量
|
// 商品各评价类别数量(需登录;token 失效时静默失败,不弹全局登录框)
|
||||||
export function goodsCommentNum (goodsId) {
|
export function goodsCommentNum (goodsId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/member/evaluation/${goodsId}/evaluationNumber`,
|
url: `/buyer/member/evaluation/${goodsId}/evaluationNumber`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
needToken: false
|
needToken: true,
|
||||||
|
skipAuthHandler: true,
|
||||||
|
skipMessage: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
buyer/src/api/pickupCard.js
Normal file
13
buyer/src/api/pickupCard.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 提货卡选品、试算及一次兑换接口。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-05
|
||||||
|
*/
|
||||||
|
import request, { Method } from '@/plugins/request.js';
|
||||||
|
|
||||||
|
/** 查询当前会员的卡规则及候选 SKU。 */
|
||||||
|
export const getPickupCard = id => request({ url: `/buyer/promotion/pickupCard/${encodeURIComponent(id)}`, method: Method.GET, needToken: true });
|
||||||
|
/** 服务端校验商品与地址,返回零现金试算。 */
|
||||||
|
export const previewPickupCard = data => request({ url: '/buyer/promotion/pickupCard/preview', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data });
|
||||||
|
/** 一次兑换;重试返回同一卡已创建的交易。 */
|
||||||
|
export const redeemPickupCard = data => request({ url: '/buyer/promotion/pickupCard/redeem', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data });
|
||||||
@@ -6,7 +6,8 @@ import request, {
|
|||||||
export function shopList (params) {
|
export function shopList (params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/buyer/store/store',
|
url: '/buyer/store/store',
|
||||||
needToken: true,
|
needToken: false,
|
||||||
|
optionalToken: true,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
@@ -52,6 +53,16 @@ export function getStoreById (id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 店铺详情
|
// 店铺详情
|
||||||
|
// 获取店铺工商/营业执照信息
|
||||||
|
export function getLicencePhoto (id) {
|
||||||
|
return request({
|
||||||
|
url: `/buyer/store/store/get/licencePhoto/${id}`,
|
||||||
|
needToken: false,
|
||||||
|
optionalToken: true,
|
||||||
|
method: Method.GET
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function getDetailById (id) {
|
export function getDetailById (id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/store/store/get/detail/${id}`,
|
url: `/buyer/store/store/get/detail/${id}`,
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||||
|
.el-textarea__inner {
|
||||||
|
resize: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--el-color-primary: var(--theme-color, #ff3c2a);
|
--el-color-primary: var(--theme-color, #ff3c2a);
|
||||||
--el-color-danger: var(--theme-color, #ff3c2a);
|
--el-color-danger: var(--theme-color, #ff3c2a);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
v-model="searchData"
|
v-model="searchData"
|
||||||
size="large"
|
size="large"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
placeholder="输入你想查找的商品"
|
:placeholder="placeholderText"
|
||||||
@focus="openPanel"
|
@focus="openPanel"
|
||||||
@input="queueSuggest"
|
@input="queueSuggest"
|
||||||
@keyup.enter="search"
|
@keyup.enter="search"
|
||||||
@@ -37,12 +37,22 @@
|
|||||||
@click="$router.push('/')"
|
@click="$router.push('/')"
|
||||||
/>
|
/>
|
||||||
<div class="search-box">
|
<div class="search-box">
|
||||||
|
<!-- 搜索类型切换:商品 / 店铺(店铺内搜索时不展示) -->
|
||||||
|
<div v-if="showTypeSwitch" class="search-type">
|
||||||
|
<span
|
||||||
|
v-for="item in searchTypes"
|
||||||
|
:key="item.value"
|
||||||
|
class="search-type-item"
|
||||||
|
:class="{ active: searchType === item.value }"
|
||||||
|
@click="switchType(item.value)"
|
||||||
|
>{{ item.label }}</span>
|
||||||
|
</div>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchData"
|
v-model="searchData"
|
||||||
size="large"
|
size="large"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
placeholder="输入你想查找的商品"
|
:placeholder="placeholderText"
|
||||||
@focus="openPanel"
|
@focus="openPanel"
|
||||||
@input="queueSuggest"
|
@input="queueSuggest"
|
||||||
@keyup.enter="search"
|
@keyup.enter="search"
|
||||||
@@ -165,7 +175,27 @@ const promotionTags = computed(() => {
|
|||||||
return parseHotWords(appStore.hotWordsList)
|
return parseHotWords(appStore.hotWordsList)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 搜索类型:goods 商品 / store 店铺
|
||||||
|
const searchTypes = [
|
||||||
|
{ label: '商品', value: 'goods' },
|
||||||
|
{ label: '店铺', value: 'store' },
|
||||||
|
]
|
||||||
|
const searchType = ref(route.path === '/storeList' ? 'store' : 'goods')
|
||||||
|
// 店铺内搜索(传了 storeId)只能搜本店商品,不展示切换
|
||||||
|
const showTypeSwitch = computed(() => !props.storeId && props.useClass !== 'fixed-show')
|
||||||
|
const isStoreMode = computed(() => searchType.value === 'store' && !props.storeId)
|
||||||
|
const placeholderText = computed(() => (isStoreMode.value ? '输入你想查找的店铺' : '输入你想查找的商品'))
|
||||||
|
|
||||||
|
function switchType(type) {
|
||||||
|
if (searchType.value === type) return
|
||||||
|
searchType.value = type
|
||||||
|
suggestions.value = []
|
||||||
|
suggesting.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const showPanel = computed(() => {
|
const showPanel = computed(() => {
|
||||||
|
// 店铺搜索暂无联想/历史,不展示下拉面板
|
||||||
|
if (isStoreMode.value) return false
|
||||||
if (!panelVisible.value) return false
|
if (!panelVisible.value) return false
|
||||||
if (trimmedKeyword.value) return true
|
if (trimmedKeyword.value) return true
|
||||||
return history.value.length > 0 || promotionTags.value.length > 0
|
return history.value.length > 0 || promotionTags.value.length > 0
|
||||||
@@ -179,6 +209,8 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function selectTags(item) {
|
function selectTags(item) {
|
||||||
|
// 热词/历史均为商品关键词,点击时按商品搜索
|
||||||
|
searchType.value = 'goods'
|
||||||
searchData.value = item
|
searchData.value = item
|
||||||
search()
|
search()
|
||||||
}
|
}
|
||||||
@@ -208,6 +240,7 @@ function onDocumentClick(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function queueSuggest() {
|
function queueSuggest() {
|
||||||
|
if (isStoreMode.value) return
|
||||||
panelVisible.value = true
|
panelVisible.value = true
|
||||||
clearTimeout(suggestTimer)
|
clearTimeout(suggestTimer)
|
||||||
const keyword = trimmedKeyword.value
|
const keyword = trimmedKeyword.value
|
||||||
@@ -254,6 +287,14 @@ function search() {
|
|||||||
const keyword = trimmedKeyword.value
|
const keyword = trimmedKeyword.value
|
||||||
searchData.value = keyword
|
searchData.value = keyword
|
||||||
closePanel()
|
closePanel()
|
||||||
|
if (isStoreMode.value) {
|
||||||
|
if (route.path === '/storeList') {
|
||||||
|
emit('search', keyword)
|
||||||
|
} else {
|
||||||
|
router.push({ path: '/storeList', query: keyword ? { keyword } : {} })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (keyword) remember(keyword)
|
if (keyword) remember(keyword)
|
||||||
const url = route.path
|
const url = route.path
|
||||||
if (url == '/goodsList') {
|
if (url == '/goodsList') {
|
||||||
@@ -261,7 +302,7 @@ function search() {
|
|||||||
} else {
|
} else {
|
||||||
const pushData = {
|
const pushData = {
|
||||||
path: '/goodsList',
|
path: '/goodsList',
|
||||||
query: { keyword },
|
query: keyword ? { keyword } : {},
|
||||||
}
|
}
|
||||||
if (props.storeId) pushData.query.storeId = props.storeId
|
if (props.storeId) pushData.query.storeId = props.storeId
|
||||||
|
|
||||||
@@ -346,9 +387,8 @@ onUnmounted(() => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
:deep(.el-input__wrapper) {
|
:deep(.el-input__wrapper) {
|
||||||
border: 1.4px solid $theme_color;
|
border: 2px solid #409EFF;
|
||||||
box-sizing: border-box;
|
border-radius: 4px;
|
||||||
border-radius: 19.6px;
|
|
||||||
padding-left: 26px;
|
padding-left: 26px;
|
||||||
padding-right: calc(#{$btn-gap * 2} + #{$btn-width});
|
padding-right: calc(#{$btn-gap * 2} + #{$btn-width});
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -365,15 +405,20 @@ onUnmounted(() => {
|
|||||||
top: $btn-gap;
|
top: $btn-gap;
|
||||||
bottom: $btn-gap;
|
bottom: $btn-gap;
|
||||||
width: $btn-width;
|
width: $btn-width;
|
||||||
border-radius: calc((#{$input-height} - #{$btn-gap * 2}) / 2);
|
border-radius: 0 3px 3px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
background-color: $theme_color;
|
background-color: #409EFF;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +429,38 @@ onUnmounted(() => {
|
|||||||
.search-box{
|
.search-box{
|
||||||
margin-left: 28px;
|
margin-left: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-type {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
height: 22px;
|
||||||
|
padding-left: 18px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-type-item {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-type + .search {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
.logo-img {
|
.logo-img {
|
||||||
max-width: 150px;
|
max-width: 150px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
|
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
|
||||||
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
|
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
|
||||||
<span class="describe">满{{coupon.consumeThreshold}}元可用</span>
|
<span class="describe">{{ coupon.consumeThreshold > 0 ? '满' + coupon.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>使用范围:{{useScope(coupon.scopeType, coupon.storeName)}}</p>
|
<p>使用范围:{{useScope(coupon.scopeType, coupon.storeName)}}</p>
|
||||||
<p>有效期:{{coupon.endTime}}</p>
|
<p>有效期:{{coupon.endTime}}</p>
|
||||||
|
|||||||
@@ -43,25 +43,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="information footer-bottmom">
|
<div class="information footer-bottmom">
|
||||||
<div class="icp">
|
<div class="icp">
|
||||||
<li v-if="config.icpCard">
|
<li v-if="icp" class="footer-bottmom">
|
||||||
<a :href='"https://tsm.miit.gov.cn/dxxzsp/xkz/xkzgl/resource/qiyesearch.jsp?num="+config.icpCard+"&type=xuke"' target="_blank">
|
|
||||||
{{config.icpCard}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li v-if="config.icpMessage" class="footer-bottmom">
|
|
||||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||||
{{config.icpMessage}}
|
{{icp}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
<a class="flex " :href="config.company.href">
|
<a v-if="companyName" class="flex " :href="companyUrl || undefined">
|
||||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{config.company.name}}
|
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{companyName}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="copyright">
|
<div class="copyright">
|
||||||
<p>Copyright © {{year}} {{config.title}}</p>
|
<p>Copyright © {{year}} {{copyrightOwner}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -71,11 +66,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import config from '@/config'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
|
|
||||||
defineOptions({ name: 'Footer' })
|
defineOptions({ name: 'Footer' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { companyName, companyUrl, icp, copyrightOwner } = useSiteSettings()
|
||||||
|
|
||||||
const guideArr = ref([
|
const guideArr = ref([
|
||||||
// 导航链接
|
// 导航链接
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="goods-thumb" ref="rootEl" :class="{ 'is-loaded': loaded, 'is-error': error }">
|
<div class="goods-thumb" ref="rootEl" :class="{ 'is-loaded': loaded, 'is-error': error }">
|
||||||
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
|
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
|
||||||
|
<img v-else-if="error" :src="defaultImg" :alt="alt || '默认商品图片'" class="default-img" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
|
import defaultImg from "@/assets/images/default.png";
|
||||||
|
|
||||||
defineOptions({ name: "GoodsThumb" });
|
defineOptions({ name: "GoodsThumb" });
|
||||||
|
|
||||||
@@ -26,6 +28,21 @@ const loaded = ref(false);
|
|||||||
const error = ref(false);
|
const error = ref(false);
|
||||||
let observer = null;
|
let observer = null;
|
||||||
|
|
||||||
|
function isValidUrl(url) {
|
||||||
|
if (!url || typeof url !== "string") return false;
|
||||||
|
const s = url.trim().toLowerCase();
|
||||||
|
if (
|
||||||
|
!s ||
|
||||||
|
s === "null" ||
|
||||||
|
s === "undefined" ||
|
||||||
|
s.startsWith("null?") ||
|
||||||
|
s.startsWith("undefined?")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.src,
|
() => props.src,
|
||||||
() => {
|
() => {
|
||||||
@@ -45,7 +62,7 @@ function observe() {
|
|||||||
currentSrc.value = "";
|
currentSrc.value = "";
|
||||||
loaded.value = false;
|
loaded.value = false;
|
||||||
error.value = false;
|
error.value = false;
|
||||||
if (!props.src) {
|
if (!isValidUrl(props.src)) {
|
||||||
error.value = true;
|
error.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -109,6 +126,13 @@ onBeforeUnmount(() => {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
&.default-img {
|
||||||
|
object-fit: contain;
|
||||||
|
padding: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes goods-thumb-loading {
|
@keyframes goods-thumb-loading {
|
||||||
|
|||||||
@@ -3,27 +3,44 @@
|
|||||||
<div class="item-detail-show">
|
<div class="item-detail-show">
|
||||||
<!-- 详情左侧展示数据、图片,收藏、举报 -->
|
<!-- 详情左侧展示数据、图片,收藏、举报 -->
|
||||||
<div class="item-detail-left">
|
<div class="item-detail-left">
|
||||||
<!-- 大图、放大镜 -->
|
<!-- 大图、放大镜(视频商品首项为视频) -->
|
||||||
<!-- <div id="dplayer"></div> -->
|
<div class="item-detail-big-img" v-if="mediaList[mediaIndex]">
|
||||||
<div class="item-detail-big-img" v-if="imgList[imgIndex]">
|
|
||||||
<el-image
|
<el-image
|
||||||
|
v-show="!isCurrentVideo"
|
||||||
:src="currentImg"
|
:src="currentImg"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
style="width: 100%; height: 100%"
|
style="width: 100%; height: 100%"
|
||||||
/>
|
/>
|
||||||
|
<div v-show="isCurrentVideo" class="goods-video-wrap">
|
||||||
|
<div class="goods-video-bg" :style="videoBgStyle"></div>
|
||||||
|
<div
|
||||||
|
v-show="hasVideo"
|
||||||
|
id="goodsVideoPlayer"
|
||||||
|
class="goods-video-player"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div id="dplayer"></div> -->
|
|
||||||
<div class="item-detail-img-row">
|
<div class="item-detail-img-row">
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="item-detail-img-small"
|
class="item-detail-img-small"
|
||||||
:class="{ 'is-active': imgIndex === index }"
|
:class="{ 'is-active': mediaIndex === index }"
|
||||||
@mouseover="imgIndex = index"
|
@mouseover="switchMedia(index)"
|
||||||
v-for="(item, index) in imgList"
|
v-for="(item, index) in mediaList"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<img :src="item.url || item"/>
|
<video
|
||||||
|
v-if="item.type === 'video'"
|
||||||
|
class="video-thumb"
|
||||||
|
:src="item.url"
|
||||||
|
muted
|
||||||
|
preload="metadata"
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
<img v-else :src="mediaThumb(item)"/>
|
||||||
|
<span v-if="item.type === 'video'" class="video-badge">
|
||||||
|
<img :src="playIcon" alt="" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,35 +87,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 商品原价 -->
|
<!-- 商品原价 -->
|
||||||
<div class="item-price-row" v-else>
|
<div class="item-price-row" v-else>
|
||||||
<!-- 批发价格 -->
|
<span class="item-price">{{ $filters.unitPrice(skuDetail.price, "¥") }}</span>
|
||||||
<div v-if="!isECouponGoods && wholesaleNum && wholesaleNum.length">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="item-price-title"></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in wholesalePrice"
|
|
||||||
:key="index"
|
|
||||||
class="item-price item-num"
|
|
||||||
>
|
|
||||||
{{ $filters.unitPrice(item, "¥") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
<div class="item-price-title">起批量</div>
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in wholesaleNum"
|
|
||||||
:key="index"
|
|
||||||
class="item-num item-price-num"
|
|
||||||
>
|
|
||||||
{{ item }}{{ skuDetail.goodsUnit }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 普通价格 -->
|
|
||||||
<div v-else>
|
|
||||||
<span class="item-price">{{ $filters.unitPrice(skuDetail.price, "¥") }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 优惠券展示 -->
|
<!-- 优惠券展示 -->
|
||||||
<div class="item-price-coupon-row" v-if="promotionMap['COUPON'].length">
|
<div class="item-price-coupon-row" v-if="promotionMap['COUPON'].length">
|
||||||
@@ -111,7 +100,7 @@
|
|||||||
@click="handleReceiveCoupon(item.id)"
|
@click="handleReceiveCoupon(item.id)"
|
||||||
>
|
>
|
||||||
<span v-if="item.couponType == 'PRICE'"
|
<span v-if="item.couponType == 'PRICE'"
|
||||||
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
|
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
|
||||||
<span v-if="item.couponType == 'DISCOUNT'"
|
<span v-if="item.couponType == 'DISCOUNT'"
|
||||||
>满{{ item.consumeThreshold }}打{{
|
>满{{ item.consumeThreshold }}打{{
|
||||||
item.couponDiscount
|
item.couponDiscount
|
||||||
@@ -128,7 +117,7 @@
|
|||||||
@click="handleReceiveCoupon(item.id)"
|
@click="handleReceiveCoupon(item.id)"
|
||||||
>
|
>
|
||||||
<span v-if="item.couponType == 'PRICE'"
|
<span v-if="item.couponType == 'PRICE'"
|
||||||
>满{{ item.consumeThreshold }}减{{ item.price }}</span>
|
>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '减' + item.price : '无门槛,立减' + item.price + '元' }}</span>
|
||||||
<span v-if="item.couponType == 'DISCOUNT'"
|
<span v-if="item.couponType == 'DISCOUNT'"
|
||||||
>满{{ item.consumeThreshold }}打{{
|
>满{{ item.consumeThreshold }}打{{
|
||||||
item.couponDiscount
|
item.couponDiscount
|
||||||
@@ -138,26 +127,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 限时折扣 -->
|
|
||||||
<div class="item-price-row" v-if="promotionMap['DISCOUNT']">
|
|
||||||
<p>
|
|
||||||
<span class="item-price-title">促销</span>
|
|
||||||
<span class="item-promotion">限时折扣</span>
|
|
||||||
<span class="item-desc-pintuan" v-if="promotionMap['DISCOUNT'].discount">
|
|
||||||
享 {{ promotionMap["DISCOUNT"].discount }} 折
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- 第N件优惠 -->
|
|
||||||
<div class="item-price-row" v-if="promotionMap['NTH']">
|
|
||||||
<p>
|
|
||||||
<span class="item-price-title">促销</span>
|
|
||||||
<span class="item-promotion">第N件</span>
|
|
||||||
<span class="item-desc-pintuan">
|
|
||||||
第 {{ promotionMap["NTH"].nthNum || promotionMap["NTH"].num }} 件优惠
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<!-- 拼团 -->
|
<!-- 拼团 -->
|
||||||
<div class="item-price-row" v-if="promotionMap['PINTUAN']">
|
<div class="item-price-row" v-if="promotionMap['PINTUAN']">
|
||||||
<p>
|
<p>
|
||||||
@@ -303,23 +272,17 @@
|
|||||||
show-icon
|
show-icon
|
||||||
:title="eCouponAfterSaleHint()"
|
:title="eCouponAfterSaleHint()"
|
||||||
/>
|
/>
|
||||||
<el-tooltip content="观看视频" v-if="skuDetail.goodsVideo">
|
|
||||||
<img class="view-video" @click="showGoodsVideo = true" :src="playIcon" alt="">
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog title="浏览视频" v-model="showGoodsVideo">
|
|
||||||
<div id="dplayer"></div>
|
|
||||||
</el-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { StarFilled } from '@element-plus/icons-vue';
|
import { StarFilled } from '@element-plus/icons-vue';
|
||||||
@@ -362,13 +325,11 @@ const emit = defineEmits(["handleClickSku"]);
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const showGoodsVideo = ref(false);
|
|
||||||
const goodsVideo = ref("");
|
|
||||||
const wholesaleList = ref([]);
|
|
||||||
const count = ref(1); // 商品数量
|
const count = ref(1); // 商品数量
|
||||||
const imgIndex = ref(0); // 展示图片下标
|
const mediaIndex = ref(0); // 展示媒体下标(相册图片/视频)
|
||||||
const currentSelceted = ref([]); // 当前商品sku
|
const currentSelceted = ref([]); // 当前商品sku
|
||||||
const imgList = ref([]); // 商品图片列表
|
const mediaList = ref([]); // 媒体列表:视频项在前,其后为图片字符串
|
||||||
|
let dp = null; // DPlayer 实例,有视频时进页即创建(静音自动播放)
|
||||||
const skuDetail = ref({
|
const skuDetail = ref({
|
||||||
specList: [],
|
specList: [],
|
||||||
}); // sku详情
|
}); // sku详情
|
||||||
@@ -384,26 +345,25 @@ const skuList = ref([]); // formatSku 中赋值(原 data 未声明)
|
|||||||
// startCheckout 通过字符串 key 操作对应 loading
|
// startCheckout 通过字符串 key 操作对应 loading
|
||||||
const loadings = { loading, loading1, loadingPintuan };
|
const loadings = { loading, loading1, loadingPintuan };
|
||||||
|
|
||||||
|
const currentMedia = computed(() => mediaList.value[mediaIndex.value] || null);
|
||||||
|
const isCurrentVideo = computed(() => currentMedia.value?.type === "video");
|
||||||
|
const hasVideo = computed(() =>
|
||||||
|
mediaList.value.some((item) => item.type === "video")
|
||||||
|
);
|
||||||
|
// 视频两侧用封面的高斯模糊填充,避免竖视频出现黑边
|
||||||
|
const videoBgStyle = computed(() => {
|
||||||
|
const video = mediaList.value.find((item) => item.type === "video");
|
||||||
|
return video && video.poster
|
||||||
|
? { backgroundImage: `url("${video.poster}")` }
|
||||||
|
: {};
|
||||||
|
});
|
||||||
|
|
||||||
const currentImg = computed(() => {
|
const currentImg = computed(() => {
|
||||||
const item = imgList.value[imgIndex.value];
|
const item = currentMedia.value;
|
||||||
return item ? item.url || item : "";
|
if (!item) return "";
|
||||||
|
return item.type === "video" ? item.poster : item.url || item;
|
||||||
});
|
});
|
||||||
|
|
||||||
const wholesalePrice = computed(() => {
|
|
||||||
return wholesaleList.value.length
|
|
||||||
? wholesaleList.value.map((item) => {
|
|
||||||
return item.price;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const wholesaleNum = computed(() => {
|
|
||||||
return wholesaleList.value.length
|
|
||||||
? wholesaleList.value.map((item) => {
|
|
||||||
return item.num;
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
});
|
|
||||||
|
|
||||||
/** 卡密商品:禁加购,仅立即购买;库存读 quantity(M-02 起支持促销) */
|
/** 卡密商品:禁加购,仅立即购买;库存读 quantity(M-02 起支持促销) */
|
||||||
const isECouponGoods = computed(() => {
|
const isECouponGoods = computed(() => {
|
||||||
@@ -436,7 +396,6 @@ watch(
|
|||||||
() => props.detail,
|
() => props.detail,
|
||||||
(val) => {
|
(val) => {
|
||||||
skuDetail.value = val.data;
|
skuDetail.value = val.data;
|
||||||
wholesaleList.value = val.wholesaleList;
|
|
||||||
syncCountWithQuantity();
|
syncCountWithQuantity();
|
||||||
promotion();
|
promotion();
|
||||||
swiperGoodsImg();
|
swiperGoodsImg();
|
||||||
@@ -444,23 +403,46 @@ watch(
|
|||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch(showGoodsVideo, (val) => {
|
// 天猫/淘宝风格:进页即在相册首位静音循环播放,用户可通过控制条开声音
|
||||||
if (val) {
|
function initVideoPlayer() {
|
||||||
initVideo();
|
const video = mediaList.value[0];
|
||||||
}
|
if (!video || video.type !== "video") return;
|
||||||
});
|
nextTick(() => {
|
||||||
|
if (dp) return;
|
||||||
|
dp = new DPlayer({
|
||||||
|
container: document.getElementById("goodsVideoPlayer"),
|
||||||
|
autoplay: true,
|
||||||
|
loop: true,
|
||||||
|
muted: true,
|
||||||
|
video: {
|
||||||
|
url: video.url,
|
||||||
|
pic: video.poster,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化video
|
// 悬停切换相册项:离开视频时暂停,回到视频时续播
|
||||||
function initVideo() {
|
function switchMedia(index) {
|
||||||
if (!goodsVideo.value) {
|
if (index === mediaIndex.value) return;
|
||||||
setTimeout(() => {
|
const leavingVideo = isCurrentVideo.value;
|
||||||
goodsVideo.value = new DPlayer({
|
mediaIndex.value = index;
|
||||||
container: document.getElementById('dplayer'),
|
if (leavingVideo && dp) {
|
||||||
video: {
|
dp.pause();
|
||||||
url: skuDetail.value.goodsVideo,
|
} else if (isCurrentVideo.value && dp) {
|
||||||
},
|
dp.play();
|
||||||
});
|
}
|
||||||
}, 100);
|
}
|
||||||
|
|
||||||
|
// 缩略图:视频用封面,图片兼容字符串与 { url } 两种形态
|
||||||
|
function mediaThumb(item) {
|
||||||
|
return item.type === "video" ? item.poster : item.url || item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyVideo() {
|
||||||
|
if (dp) {
|
||||||
|
dp.destroy();
|
||||||
|
dp = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,10 +454,6 @@ function syncCountWithQuantity() {
|
|||||||
count.value = 1;
|
count.value = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isECouponGoods.value && wholesaleList.value && wholesaleList.value.length > 0) {
|
|
||||||
count.value = Math.min(Math.max(wholesaleList.value[0].num, 1), qty);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const max = isECouponGoods.value ? getECouponMaxBuyNum(skuDetail.value) : qty;
|
const max = isECouponGoods.value ? getECouponMaxBuyNum(skuDetail.value) : qty;
|
||||||
if (count.value > max) {
|
if (count.value > max) {
|
||||||
count.value = max;
|
count.value = max;
|
||||||
@@ -484,14 +462,7 @@ function syncCountWithQuantity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeCount(val) {
|
function changeCount(val) {}
|
||||||
if (wholesaleList.value && wholesaleList.value.length > 0) {
|
|
||||||
if (count.value <= wholesaleList.value[0].num) {
|
|
||||||
Message.warning("批发商品购买数量不能小于起批数量");
|
|
||||||
count.value = wholesaleList.value[0].num;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function select(index, value) {
|
function select(index, value) {
|
||||||
// 选择规格
|
// 选择规格
|
||||||
@@ -702,18 +673,29 @@ function promotion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图
|
// 相册里会混进规格 JSON,只留图片地址;没有相册时用主图。有视频时视频排在相册第一项
|
||||||
function swiperGoodsImg() {
|
function swiperGoodsImg() {
|
||||||
|
destroyVideo();
|
||||||
|
mediaIndex.value = 0;
|
||||||
const source = skuDetail.value.goodsGalleryList || [];
|
const source = skuDetail.value.goodsGalleryList || [];
|
||||||
const gallery = Array.from(source).filter((item) => {
|
const gallery = Array.from(source).filter((item) => {
|
||||||
if (typeof item !== "string") return false;
|
if (typeof item !== "string") return false;
|
||||||
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
|
return item.indexOf('"url":') === -1 && item.indexOf('"status":') === -1;
|
||||||
});
|
});
|
||||||
if (gallery.length) {
|
const images = gallery.length
|
||||||
imgList.value = gallery;
|
? gallery
|
||||||
return;
|
: skuDetail.value.original
|
||||||
|
? [skuDetail.value.original]
|
||||||
|
: [];
|
||||||
|
mediaList.value = [...images];
|
||||||
|
if (skuDetail.value.goodsVideo) {
|
||||||
|
mediaList.value.unshift({
|
||||||
|
type: "video",
|
||||||
|
url: skuDetail.value.goodsVideo,
|
||||||
|
poster: images[0] || "",
|
||||||
|
});
|
||||||
|
initVideoPlayer();
|
||||||
}
|
}
|
||||||
imgList.value = skuDetail.value.original ? [skuDetail.value.original] : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -723,6 +705,8 @@ onMounted(() => {
|
|||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
isCollected.value = true;
|
isCollected.value = true;
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 收藏状态为可选数据,失败时静默
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -730,6 +714,10 @@ onMounted(() => {
|
|||||||
// promotion();
|
// promotion();
|
||||||
document.title = skuDetail.value.goodsName;
|
document.title = skuDetail.value.goodsName;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
destroyVideo();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -793,10 +781,58 @@ onMounted(() => {
|
|||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#dplayer{
|
.goods-video-wrap {
|
||||||
width: 100%;
|
position: relative;
|
||||||
height: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-video-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
filter: blur(28px) brightness(0.85);
|
||||||
|
transform: scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-video-player {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
/* 播放器默认黑底改为透明,露出毛玻璃背景 */
|
||||||
|
:deep(.dplayer),
|
||||||
|
:deep(.dplayer-video-wrap) {
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-thumb {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.item-detail-img-row {
|
.item-detail-img-row {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -806,6 +842,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.item-detail-img-small {
|
.item-detail-img-small {
|
||||||
|
position: relative;
|
||||||
width: 86px;
|
width: 86px;
|
||||||
height: 86px;
|
height: 86px;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
@@ -1236,8 +1273,5 @@ onMounted(() => {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
.view-video{
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
/******************商品图片及购买详情结束******************/
|
/******************商品图片及购买详情结束******************/
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
v-model:current-page="commentParams.pageNumber"
|
v-model:current-page="commentParams.pageNumber"
|
||||||
v-model:page-size="commentParams.pageSize"
|
v-model:page-size="commentParams.pageSize"
|
||||||
:total="commentTotal"
|
:total="commentTotal"
|
||||||
small
|
size="small"
|
||||||
@current-change="changePageNum"
|
@current-change="changePageNum"
|
||||||
@size-change="changePageSize"
|
@size-change="changePageSize"
|
||||||
layout="prev, pager, next"
|
layout="prev, pager, next"
|
||||||
@@ -136,6 +136,7 @@
|
|||||||
import { computed, onMounted, onBeforeUnmount, reactive, ref } from 'vue';
|
import { computed, onMounted, onBeforeUnmount, reactive, ref } from 'vue';
|
||||||
import { Refresh } from '@element-plus/icons-vue';
|
import { Refresh } from '@element-plus/icons-vue';
|
||||||
import { goodsComment, goodsCommentNum } from '@/api/member.js';
|
import { goodsComment, goodsCommentNum } from '@/api/member.js';
|
||||||
|
import storage from '@/plugins/storage.js';
|
||||||
import StoreSide from './StoreSide.vue';
|
import StoreSide from './StoreSide.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'ShowGoodsDetail' });
|
defineOptions({ name: 'ShowGoodsDetail' });
|
||||||
@@ -246,17 +247,23 @@ function changePageSize (val) {
|
|||||||
getList();
|
getList();
|
||||||
}
|
}
|
||||||
function getList () {
|
function getList () {
|
||||||
|
// 评价接口需要登录,未登录时不发起请求
|
||||||
|
if (!storage.getItem('accessToken') || !storage.getItem('userInfo')) return;
|
||||||
commentParams.goodsId = skuDetail.value.goodsId;
|
commentParams.goodsId = skuDetail.value.goodsId;
|
||||||
goodsComment(commentParams).then(res => {
|
goodsComment(commentParams).then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
commentList.value = res.result.records;
|
commentList.value = res.result.records;
|
||||||
commentTotal.value = res.result.total;
|
commentTotal.value = res.result.total;
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 评价为可选数据,加载失败时静默
|
||||||
});
|
});
|
||||||
goodsCommentNum(skuDetail.value.goodsId).then(res => {
|
goodsCommentNum(skuDetail.value.goodsId).then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
commentTypeNum.value = res.result;
|
commentTypeNum.value = res.result;
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 评价统计为可选数据,加载失败时静默
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function viewByGrade (grade) {
|
function viewByGrade (grade) {
|
||||||
|
|||||||
@@ -245,10 +245,11 @@ function getCartList() {
|
|||||||
|
|
||||||
.box {
|
.box {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background: #333;
|
background: #f8fafc;
|
||||||
color: #fff;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
color: #64748b;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@@ -281,14 +282,19 @@ function getCartList() {
|
|||||||
|
|
||||||
.nav a,
|
.nav a,
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #fff;
|
color: #64748b;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
|
|||||||
@@ -1,131 +1,251 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="scroll-show">
|
<div class="scroll-show">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<cateNav
|
<!-- 1. 左侧区域:商城 Logo -->
|
||||||
class="cate"
|
<div class="left-section">
|
||||||
:hover="true"
|
<router-link to="/" class="logo-link">
|
||||||
:showNavBar="false"
|
<img v-if="logoSrc" :src="logoSrc" alt="Logo" class="site-logo" />
|
||||||
useClass="fixed-show"
|
<span v-else class="text-logo">LiliShop</span>
|
||||||
/>
|
</router-link>
|
||||||
<Search
|
</div>
|
||||||
class="search-con"
|
|
||||||
:hover="true"
|
<!-- 2. 中间搜索区域:与 SearchHeader 样式完全一致(蓝底色、圆角4px、搜索按钮) -->
|
||||||
ref="search"
|
<div class="search-main">
|
||||||
:showLogo="false"
|
<div class="search-box">
|
||||||
:showTag="false"
|
<input
|
||||||
useClass="fixed-show"
|
v-model="keyword"
|
||||||
/>
|
type="text"
|
||||||
<div class="cart">
|
class="search-input"
|
||||||
<span @mouseenter="getCartList">
|
:placeholder="placeholderText"
|
||||||
<el-icon class="cart-icon" :size="20" @click="goCartList"><ShoppingCart /></el-icon>
|
@keyup.enter="handleSearch"
|
||||||
</span>
|
/>
|
||||||
<i class="cart-badge">{{ Number(cartNum) > 99 ? "99+" : cartNum }}</i>
|
<button type="button" class="search-btn" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
<span>搜索</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 右侧“我的购物车”胶囊入口:与 SearchHeader 样式完全一致 -->
|
||||||
|
<div class="cart-entry">
|
||||||
|
<router-link to="/cart" class="cart-btn" @mouseenter="getCartList">
|
||||||
|
<el-icon class="cart-icon"><ShoppingCart /></el-icon>
|
||||||
|
<span class="cart-text">{{ options.cartText || '我的购物车' }}</span>
|
||||||
|
<span class="cart-count">{{ Number(cartNum) > 99 ? '99+' : cartNum }}</span>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import Search from '@/components/Search.vue'
|
import { cartCount } from '@/api/cart.js';
|
||||||
import cateNav from '@/components/nav/CateNav.vue'
|
import storage from '@/plugins/storage.js';
|
||||||
import { cartCount } from '@/api/cart.js'
|
import { ShoppingCart, Search } from '@element-plus/icons-vue';
|
||||||
import storage from '@/plugins/storage.js'
|
import { useAppStore } from '@/stores/app';
|
||||||
import { ShoppingCart } from '@element-plus/icons-vue'
|
|
||||||
import { useAppStore } from '@/stores/app'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const props = defineProps({
|
||||||
const appStore = useAppStore()
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const userInfo = ref({})
|
const router = useRouter();
|
||||||
const cartNum = computed(() => appStore.cartNum)
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
function goCartList() {
|
const keyword = ref(route.query.keyword || '');
|
||||||
router.push('/cart')
|
const cartNum = computed(() => appStore.cartNum || 0);
|
||||||
|
|
||||||
|
watch(() => route.query.keyword, (v) => {
|
||||||
|
keyword.value = v || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoSrc = computed(() => {
|
||||||
|
return props.options?.logoUrl || appStore.logoImg || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderText = computed(() => {
|
||||||
|
const ph = props.options?.placeholder;
|
||||||
|
if (!ph || ph === '搜索品牌或商品' || ph === '请输入商品名称、品牌或型号') {
|
||||||
|
return '请输入商品名称';
|
||||||
|
}
|
||||||
|
return ph;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
const kw = (keyword.value || '').trim();
|
||||||
|
if (route.path === '/goodsList') {
|
||||||
|
const nextQuery = { ...route.query };
|
||||||
|
if (kw) {
|
||||||
|
nextQuery.keyword = kw;
|
||||||
|
} else {
|
||||||
|
delete nextQuery.keyword;
|
||||||
|
}
|
||||||
|
router.push({ path: '/goodsList', query: nextQuery });
|
||||||
|
} else {
|
||||||
|
router.push({ path: '/goodsList', query: kw ? { keyword: kw } : {} });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCartList() {
|
function getCartList() {
|
||||||
if (storage.getItem('userInfo')) {
|
if (storage.getItem('userInfo')) {
|
||||||
cartCount().then((res) => {
|
cartCount().then((res) => {
|
||||||
appStore.setCartNum(res.result)
|
appStore.setCartNum(res.result);
|
||||||
storage.setItem('cartNum', res.result)
|
storage.setItem('cartNum', res.result);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (storage.getItem('userInfo')) {
|
getCartList();
|
||||||
userInfo.value = JSON.parse(storage.getItem('userInfo'))
|
});
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
|
||||||
$side-col-width: 263.2px;
|
|
||||||
$col-gap: 10px;
|
|
||||||
$cart-col-width: 40px;
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
.scroll-show {
|
.scroll-show {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
overflow: visible;
|
width: 100%;
|
||||||
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
width: 1200px;
|
width: 1200px;
|
||||||
|
max-width: 100%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: $side-col-width 1fr $cart-col-width;
|
|
||||||
column-gap: $col-gap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cate {
|
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
:deep(.cate-nav) {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.nav-con) {
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-con {
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
height: 60px;
|
box-sizing: border-box;
|
||||||
position: relative;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-icon {
|
.left-section {
|
||||||
font-size: 25px;
|
display: flex;
|
||||||
color: $theme_color;
|
align-items: center;
|
||||||
cursor: pointer;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.logo-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
.site-logo {
|
||||||
|
max-height: 36px;
|
||||||
|
max-width: 160px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-logo {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #409EFF;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-badge {
|
.search-main {
|
||||||
position: absolute;
|
width: 560px;
|
||||||
font-style: normal;
|
flex-shrink: 0;
|
||||||
top: 12px;
|
|
||||||
right: -2px;
|
.search-box {
|
||||||
display: block;
|
display: flex;
|
||||||
background-color: $theme_color;
|
border: 2px solid #409EFF;
|
||||||
color: #fff;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
overflow: hidden;
|
||||||
min-width: 17px;
|
height: 38px;
|
||||||
height: 17px;
|
box-sizing: border-box;
|
||||||
padding: 0 4px;
|
background: #ffffff;
|
||||||
border-radius: 10px;
|
|
||||||
line-height: 17px;
|
.search-input {
|
||||||
text-align: center;
|
flex: 1;
|
||||||
z-index: 5;
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
outline: none;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: #409EFF;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 0 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-entry {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.cart-btn {
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
background: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.cart-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-text {
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-count {
|
||||||
|
background: #409EFF;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #409EFF;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="model-form">
|
<div class="model-form" :style="pageContainerStyle">
|
||||||
<div class="model-content">
|
<!-- Version 2: 新万米行模型渲染 -->
|
||||||
|
<div v-if="isVersion2" class="v2-rows-container">
|
||||||
|
<row-wrapper
|
||||||
|
v-for="row in v2Rows"
|
||||||
|
:key="row.id"
|
||||||
|
:row="row"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 旧版降级渲染 -->
|
||||||
|
<div v-else class="model-content">
|
||||||
<model-form-item
|
<model-form-item
|
||||||
v-for="(element, index) in renderList"
|
v-for="(element, index) in renderList"
|
||||||
:key="element.key"
|
:key="element.key"
|
||||||
@@ -11,14 +21,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import ModelFormItem from './ModelFormItem.vue';
|
import ModelFormItem from './ModelFormItem.vue';
|
||||||
|
import RowWrapper from './RowWrapper.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'modelForm' });
|
defineOptions({ name: 'modelForm' });
|
||||||
|
|
||||||
const props = defineProps(['data']);
|
const props = defineProps(['data']);
|
||||||
|
|
||||||
|
// 是否为 v2 行模型数据
|
||||||
|
const isVersion2 = computed(() => {
|
||||||
|
return (props.data?.version === 2 || props.data?.version === '2.0') && Array.isArray(props.data?.list);
|
||||||
|
});
|
||||||
|
|
||||||
|
const v2Rows = computed(() => {
|
||||||
|
return props.data?.list || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageContainerStyle = computed(() => {
|
||||||
|
if (isVersion2.value && props.data?.pageStyle?.backgroundColor) {
|
||||||
|
return {
|
||||||
|
backgroundColor: props.data.pageStyle.backgroundColor
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
const renderList = computed(() => {
|
const renderList = computed(() => {
|
||||||
const list = props.data?.list || [];
|
const list = props.data?.list || [];
|
||||||
return list.filter(
|
return list.filter(
|
||||||
@@ -27,21 +57,26 @@ const renderList = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function isBuiltinModule(type) {
|
function isBuiltinModule(type) {
|
||||||
// 顶部广告、快捷导航由 Index 页单独渲染
|
|
||||||
return type === 'topAdvert' || type === 'navBar';
|
return type === 'topAdvert' || type === 'navBar';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.model-form {
|
.model-form {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v2-rows-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
.model-content {
|
.model-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 1200px;
|
min-height: 1200px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
/* 勿设 overflow-x: hidden,会裁切 discountAdvert 的 margin-left: -100px 左溢背景 */
|
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
385
buyer/src/components/indexDecorate/RowWrapper.vue
Normal file
385
buyer/src/components/indexDecorate/RowWrapper.vue
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="row-wrapper" :style="rowWrapperStyle">
|
||||||
|
|
||||||
|
<div class="row-inner-flex" :style="rowInnerFlexStyle">
|
||||||
|
|
||||||
|
<div
|
||||||
|
|
||||||
|
v-for="comp in row.components"
|
||||||
|
|
||||||
|
:key="comp.id"
|
||||||
|
|
||||||
|
class="component-cell"
|
||||||
|
|
||||||
|
:style="getComponentCellStyle(comp.componentStyle, row.rowStyle, row.components ? row.components.length : 1, comp.type)"
|
||||||
|
|
||||||
|
>
|
||||||
|
|
||||||
|
<component
|
||||||
|
|
||||||
|
:is="resolveComponent(comp.type)"
|
||||||
|
|
||||||
|
v-if="resolveComponent(comp.type)"
|
||||||
|
:component="comp"
|
||||||
|
|
||||||
|
:options="comp.options || {}"
|
||||||
|
|
||||||
|
:component-style="comp.componentStyle || {}"
|
||||||
|
|
||||||
|
:row-style="row.rowStyle || {}"
|
||||||
|
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-else class="unknown-component-placeholder">
|
||||||
|
|
||||||
|
未知组件: {{ comp.type }}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
import { resolveComponent } from './registry.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
|
||||||
|
row: {
|
||||||
|
|
||||||
|
type: Object,
|
||||||
|
|
||||||
|
required: true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 = {};
|
||||||
|
|
||||||
|
// 宽度 (width)
|
||||||
|
if (s.width !== undefined && s.width !== '') {
|
||||||
|
const w = String(s.width).trim();
|
||||||
|
if (w && w !== '100%') {
|
||||||
|
const num = parseInt(w, 10);
|
||||||
|
if (!isNaN(num) && num >= 1920 && !w.endsWith('%')) {
|
||||||
|
style.width = '100%';
|
||||||
|
} else {
|
||||||
|
style.width = /^\d+$/.test(w) ? `${w}px` : w;
|
||||||
|
style.maxWidth = '100%';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style.width = '100%';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style.width = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 高度 (height)
|
||||||
|
if (s.height !== undefined && s.height !== '') {
|
||||||
|
const h = String(s.height).trim();
|
||||||
|
if (h && Number(h) > 0) {
|
||||||
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
|
if (hasAutoComp.value) {
|
||||||
|
style.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
style.height = hVal;
|
||||||
|
style.minHeight = hVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 外边距 (margin)
|
||||||
|
const isNarrow = style.width !== '100%';
|
||||||
|
if (Array.isArray(s.margin)) {
|
||||||
|
style.marginTop = `${s.margin[0] || 0}px`;
|
||||||
|
style.marginRight = s.margin[1] ? `${s.margin[1]}px` : (isNarrow ? 'auto' : '0px');
|
||||||
|
style.marginBottom = `${s.margin[2] || 0}px`;
|
||||||
|
style.marginLeft = s.margin[3] ? `${s.margin[3]}px` : (isNarrow ? 'auto' : '0px');
|
||||||
|
} else if (isNarrow) {
|
||||||
|
style.marginLeft = 'auto';
|
||||||
|
style.marginRight = 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内边距 (padding)
|
||||||
|
if (Array.isArray(s.padding)) {
|
||||||
|
style.paddingTop = `${s.padding[0] || 0}px`;
|
||||||
|
style.paddingRight = `${s.padding[1] || 0}px`;
|
||||||
|
style.paddingBottom = `${s.padding[2] || 0}px`;
|
||||||
|
style.paddingLeft = `${s.padding[3] || 0}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 圆角 (radius)
|
||||||
|
if (Array.isArray(s.radius)) {
|
||||||
|
style.borderTopLeftRadius = `${s.radius[0] || 0}px`;
|
||||||
|
style.borderTopRightRadius = `${s.radius[1] || 0}px`;
|
||||||
|
style.borderBottomRightRadius = `${s.radius[2] || 0}px`;
|
||||||
|
style.borderBottomLeftRadius = `${s.radius[3] || 0}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 行背景由各组件配置统一管理
|
||||||
|
|
||||||
|
return style;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const rowInnerFlexStyle = computed(() => {
|
||||||
|
|
||||||
|
const s = props.row.rowStyle || {};
|
||||||
|
|
||||||
|
const styleObj = {
|
||||||
|
|
||||||
|
display: 'flex',
|
||||||
|
|
||||||
|
flexDirection: 'row',
|
||||||
|
|
||||||
|
flexWrap: 'nowrap',
|
||||||
|
|
||||||
|
alignItems: s.alignItems || 'stretch',
|
||||||
|
|
||||||
|
justifyContent: s.justifyContent || 'flex-start',
|
||||||
|
|
||||||
|
width: '100%',
|
||||||
|
|
||||||
|
height: '100%',
|
||||||
|
|
||||||
|
gap: `${s.gap || 0}px`
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
if (s.height !== undefined && s.height !== '') {
|
||||||
|
const h = String(s.height).trim();
|
||||||
|
if (h && Number(h) > 0) {
|
||||||
|
const hVal = /^\d+$/.test(h) ? `${h}px` : h;
|
||||||
|
if (hasAutoComp.value) {
|
||||||
|
styleObj.minHeight = hVal;
|
||||||
|
} else {
|
||||||
|
styleObj.height = hVal;
|
||||||
|
styleObj.minHeight = hVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return styleObj;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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%' };
|
||||||
|
|
||||||
|
const s = {};
|
||||||
|
|
||||||
|
if (rowStyle?.height !== undefined && rowStyle?.height !== '') {
|
||||||
|
const rh = String(rowStyle.height).trim();
|
||||||
|
if (rh && Number(rh) > 0) {
|
||||||
|
const rhVal = /^\d+$/.test(rh) ? `${rh}px` : rh;
|
||||||
|
if (isAutoHeightComp) {
|
||||||
|
s.minHeight = rhVal;
|
||||||
|
} else {
|
||||||
|
s.height = rhVal;
|
||||||
|
s.minHeight = rhVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const gap = Number(rowStyle?.gap) || 0;
|
||||||
|
const totalGap = (totalComps > 1 && gap > 0) ? gap * (totalComps - 1) : 0;
|
||||||
|
|
||||||
|
if (style.widthRatio && style.widthRatio !== 'auto') {
|
||||||
|
let w = style.widthRatio;
|
||||||
|
if (w === '16.67%' || w === '16.6%' || w === '16.7%') {
|
||||||
|
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 6)` : 'calc(100% / 6)';
|
||||||
|
} else if (w === '66.67%' || w === '66.6%' || w === '66.7%') {
|
||||||
|
w = totalGap > 0 ? `calc((100% - ${totalGap}px) * 2 / 3)` : 'calc(100% * 2 / 3)';
|
||||||
|
} else if (w === '33.33%' || w === '33.3%') {
|
||||||
|
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 3)` : 'calc(100% / 3)';
|
||||||
|
} else if (w === '50%') {
|
||||||
|
w = totalGap > 0 ? `calc((100% - ${totalGap}px) / 2)` : '50%';
|
||||||
|
}
|
||||||
|
s.flex = `0 0 ${w}`;
|
||||||
|
|
||||||
|
s.width = w;
|
||||||
|
|
||||||
|
s.maxWidth = w;
|
||||||
|
|
||||||
|
s.minWidth = '0';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
s.flex = '1 1 0%';
|
||||||
|
|
||||||
|
s.minWidth = '0';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (Array.isArray(style.padding)) {
|
||||||
|
|
||||||
|
s.paddingTop = `${style.padding[0] || 0}px`;
|
||||||
|
|
||||||
|
s.paddingRight = `${style.padding[1] || 0}px`;
|
||||||
|
|
||||||
|
s.paddingBottom = `${style.padding[2] || 0}px`;
|
||||||
|
|
||||||
|
s.paddingLeft = `${style.padding[3] || 0}px`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (Array.isArray(style.radius)) {
|
||||||
|
|
||||||
|
s.borderTopLeftRadius = `${style.radius[0] || 0}px`;
|
||||||
|
|
||||||
|
s.borderTopRightRadius = `${style.radius[1] || 0}px`;
|
||||||
|
|
||||||
|
s.borderBottomRightRadius = `${style.radius[2] || 0}px`;
|
||||||
|
|
||||||
|
s.borderBottomLeftRadius = `${style.radius[3] || 0}px`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (Array.isArray(style.margin)) {
|
||||||
|
|
||||||
|
s.marginTop = `${style.margin[0] || 0}px`;
|
||||||
|
|
||||||
|
s.marginRight = `${style.margin[1] || 0}px`;
|
||||||
|
|
||||||
|
s.marginBottom = `${style.margin[2] || 0}px`;
|
||||||
|
|
||||||
|
s.marginLeft = `${style.margin[3] || 0}px`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (style.backgroundColor && style.backgroundColor !== 'transparent') {
|
||||||
|
let bg = style.backgroundColor;
|
||||||
|
if (/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*0\s*\)$/.test(bg.trim())) {
|
||||||
|
bg = bg.trim().replace(/,\s*0\s*\)$/, ', 1)');
|
||||||
|
}
|
||||||
|
s.backgroundColor = bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (style.backgroundImage) {
|
||||||
|
|
||||||
|
s.backgroundImage = `url(${style.backgroundImage})`;
|
||||||
|
|
||||||
|
s.backgroundSize = 'cover';
|
||||||
|
|
||||||
|
s.backgroundRepeat = 'no-repeat';
|
||||||
|
|
||||||
|
s.backgroundPosition = 'center';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return s;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
|
.row-wrapper {
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.row-inner-flex {
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.component-cell {
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.unknown-component-placeholder {
|
||||||
|
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
background: #fef2f2;
|
||||||
|
|
||||||
|
border: 1px dashed #f87171;
|
||||||
|
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
202
buyer/src/components/indexDecorate/components/ArticleContent.vue
Normal file
202
buyer/src/components/indexDecorate/components/ArticleContent.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-article-content" :style="customStyle">
|
||||||
|
<!-- 文章顶部标题区 -->
|
||||||
|
<div class="article-header" v-if="options.showTitle !== false && options.title">
|
||||||
|
<h2 class="article-main-title">{{ options.title }}</h2>
|
||||||
|
<p class="article-sub-title" v-if="options.subtitle">{{ options.subtitle }}</p>
|
||||||
|
|
||||||
|
<div class="article-meta-bar" v-if="options.showMeta !== false">
|
||||||
|
<span class="meta-tag">{{ options.tag || '品牌专栏' }}</span>
|
||||||
|
<span class="meta-item author" v-if="options.author">作者:{{ options.author }}</span>
|
||||||
|
<span class="meta-item date" v-if="options.date">发布于 {{ options.date }}</span>
|
||||||
|
<span class="meta-item views">阅读 2.4k+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文章正文排版区 (Rich Text Article Body) -->
|
||||||
|
<div
|
||||||
|
class="article-body-typography"
|
||||||
|
:class="[`align-${options.textAlign || 'left'}`]"
|
||||||
|
:style="{ fontSize: `${options.fontSize || 15}px` }"
|
||||||
|
v-html="renderedContent"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultArticleHtml = `
|
||||||
|
<h3>关于我们 · 甄选美好生活</h3>
|
||||||
|
<p>我们始终坚信,一件好的产品不仅能改善日常生活的品质,更传递着对美好生活的执着追求。自创立以来,我们严选全球顶尖供应链,深耕每一道生产工艺与质检环节,力求将兼具美学价值与极致实用性的商品送达每一位用户手中。</p>
|
||||||
|
<blockquote>品质是我们的信仰,体验是我们的底线。从选材到交付,每一步都经得起时光检验。</blockquote>
|
||||||
|
<h4>四大核心服务保障</h4>
|
||||||
|
<ul>
|
||||||
|
<li><strong>100% 官方正品</strong>:源头品牌直采,全链路防伪溯源,假一赔十保障。</li>
|
||||||
|
<li><strong>极速顺丰冷链</strong>:全国多仓联动发货,核心城市支持当日达/次日达。</li>
|
||||||
|
<li><strong>7天无理由退换</strong>:专业客服团队 7×24 小时贴心守护,售后无忧。</li>
|
||||||
|
<li><strong>会员专享权益</strong>:尊享积分抵现、生日特权礼及专属品鉴沙龙。</li>
|
||||||
|
</ul>
|
||||||
|
<p>感谢一路相伴的每一位朋友,未来我们将持续探索更多优质生活场景,与您共同见证品质生活的美好绽放。</p>
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const renderedContent = computed(() => {
|
||||||
|
return props.options?.content || defaultArticleHtml;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '24px 28px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '12px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-article-content {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.article-header {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
.article-main-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-sub-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-meta-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-body-typography {
|
||||||
|
color: #334155;
|
||||||
|
line-height: 1.8;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
|
||||||
|
&.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&.align-justify {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h2), :deep(h3), :deep(h4) {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h2) {
|
||||||
|
font-size: 20px;
|
||||||
|
border-left: 4px solid #2563eb;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h3) {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(h4) {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(p) {
|
||||||
|
margin: 0 0 14px 0;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(blockquote) {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 12px 18px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-left: 4px solid #3b82f6;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
color: #475569;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(ul), :deep(ol) {
|
||||||
|
margin: 12px 0 16px 20px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: block;
|
||||||
|
margin: 16px auto;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(strong) {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
buyer/src/components/indexDecorate/components/BrandWall.vue
Normal file
142
buyer/src/components/indexDecorate/components/BrandWall.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-brand-wall" :style="customStyle">
|
||||||
|
<div
|
||||||
|
class="brand-grid"
|
||||||
|
:style="{
|
||||||
|
gridTemplateColumns: `repeat(${actualColumns}, 1fr)`,
|
||||||
|
gap: `${options.gap ?? 12}px`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-for="(item, idx) in brandList"
|
||||||
|
:key="idx"
|
||||||
|
:to="item.url || '/goods'"
|
||||||
|
class="brand-card"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="item.img"
|
||||||
|
:src="item.img"
|
||||||
|
:alt="item.title || 'brand'"
|
||||||
|
class="brand-img"
|
||||||
|
/>
|
||||||
|
<div v-else class="brand-placeholder">
|
||||||
|
<span>{{ item.title || item.name || '品牌' }}</span>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const brandList = computed(() => {
|
||||||
|
if (Array.isArray(props.options.items) && props.options.items.length > 0) {
|
||||||
|
return props.options.items;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ title: '数码潮玩 · 爆款立减500', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_1.png', url: '/goodsList?category=1' },
|
||||||
|
{ title: '美妆奢宠 · 每满300减50', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_2.png', url: '/goodsList?category=2' },
|
||||||
|
{ title: '运动潮品 · 爆款低至5折', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_3.png', url: '/goodsList?category=3' },
|
||||||
|
{ title: '品质生活 · 精选好物', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/brand-wall/brand_4.png', url: '/goodsList?category=4' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualColumns = computed(() => {
|
||||||
|
const baseCols = Number(props.options?.columns) || 4;
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '50%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||||
|
return Math.min(baseCols, 3);
|
||||||
|
}
|
||||||
|
return baseCols;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '10px 0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-brand-wall {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.brand-grid {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.brand-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 2 / 1;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
min-height: 80px;
|
||||||
|
max-height: 180px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
|
||||||
|
.brand-img {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
170
buyer/src/components/indexDecorate/components/Carousel.vue
Normal file
170
buyer/src/components/indexDecorate/components/Carousel.vue
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="decor-carousel"
|
||||||
|
:class="[
|
||||||
|
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||||
|
]"
|
||||||
|
:style="customStyle"
|
||||||
|
>
|
||||||
|
<el-carousel
|
||||||
|
height="100%"
|
||||||
|
:autoplay="options.autoPlay !== false"
|
||||||
|
:interval="Number(options.interval) || 4000"
|
||||||
|
:arrow="options.showArrow !== false ? 'hover' : 'never'"
|
||||||
|
:indicator-position="options.showIndicator !== false ? '' : 'none'"
|
||||||
|
class="custom-carousel"
|
||||||
|
>
|
||||||
|
<el-carousel-item v-for="(slide, idx) in slidesList" :key="idx">
|
||||||
|
<router-link :to="slide.url || '/'" class="slide-link">
|
||||||
|
<img
|
||||||
|
:src="slide.img"
|
||||||
|
:alt="slide.title || 'banner'"
|
||||||
|
class="slide-img"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</el-carousel-item>
|
||||||
|
</el-carousel>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
rowStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const isMini = computed(() => props.componentStyle?.widthRatio === '16.67%');
|
||||||
|
const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||||
|
|
||||||
|
const slidesList = computed(() => {
|
||||||
|
if (Array.isArray(props.options.list) && props.options.list.length > 0) {
|
||||||
|
return props.options.list;
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.options.banners) && props.options.banners.length > 0) {
|
||||||
|
return props.options.banners.map(b => ({
|
||||||
|
title: b.title,
|
||||||
|
img: b.image || b.img || b.url,
|
||||||
|
url: b.url || b.link
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ title: '潮流数码', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_1.png', url: '/goodsList?category=1' },
|
||||||
|
{ title: '奢宠美妆', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_2.png', url: '/goodsList?category=2' },
|
||||||
|
{ title: '运动潮流', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_3.png', url: '/goodsList?category=3' },
|
||||||
|
{ title: '居家生活', img: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/carousel/banner_4.png', url: '/goodsList?category=4' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
const fit = props.options.fitMode || 'cover';
|
||||||
|
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||||
|
|
||||||
|
const styleObj = {
|
||||||
|
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px',
|
||||||
|
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
|
||||||
|
'--carousel-fit': fit,
|
||||||
|
height: '100%',
|
||||||
|
minHeight: hasRowHeight ? '0' : '420px'
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.options.aspectRatio === 'custom' && props.options.height) {
|
||||||
|
styleObj.height = `${props.options.height}px`;
|
||||||
|
styleObj.minHeight = `${props.options.height}px`;
|
||||||
|
}
|
||||||
|
return styleObj;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-carousel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.custom-carousel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
:deep(.el-carousel__container) {
|
||||||
|
height: 100% !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-carousel__indicators--horizontal) {
|
||||||
|
bottom: 12px;
|
||||||
|
z-index: 10;
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
|
||||||
|
.el-carousel__indicator {
|
||||||
|
padding: 3px;
|
||||||
|
|
||||||
|
.el-carousel__button {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.6);
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active .el-carousel__button {
|
||||||
|
width: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
.slide-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: var(--carousel-fit, cover);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-mini {
|
||||||
|
:deep(.el-carousel__indicators--horizontal) {
|
||||||
|
bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="decor-category-mega-menu"
|
||||||
|
:class="{ 'is-hovering': panelVisible && activeChildren.length }"
|
||||||
|
:style="customStyle"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<!-- 顶部标题:分类 / 商品分类 -->
|
||||||
|
<div class="menu-header">
|
||||||
|
<span class="header-title">{{ options.title || '商品分类' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 垂直一级类目列表 -->
|
||||||
|
<div class="category-list">
|
||||||
|
<div
|
||||||
|
v-for="(cat, idx) in displayCategories"
|
||||||
|
:key="cat.id || idx"
|
||||||
|
class="cat-item"
|
||||||
|
:class="{ 'is-active': activeIndex === idx && panelVisible }"
|
||||||
|
:style="{
|
||||||
|
'--text-color': options.textColor || '#334155',
|
||||||
|
'--active-color': options.activeColor || '#409EFF',
|
||||||
|
'--font-size': `${options.fontSize || 13}px`
|
||||||
|
}"
|
||||||
|
@mouseenter="handleMouseEnter(idx, cat)"
|
||||||
|
@click="goGoodsList(cat.id)"
|
||||||
|
>
|
||||||
|
<span class="cat-name">{{ cat.name }}</span>
|
||||||
|
<el-icon class="arrow-icon"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧悬浮超级大面板 (Mega Menu) -->
|
||||||
|
<transition name="fade-flyout">
|
||||||
|
<div
|
||||||
|
v-if="panelVisible && activeChildren.length"
|
||||||
|
class="mega-flyout-panel"
|
||||||
|
@mouseenter="handlePanelEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<!-- 顶部快捷推荐标签(展示前8个二级分类) -->
|
||||||
|
<div class="flyout-top-tags" v-if="activeChildren && activeChildren.length">
|
||||||
|
<span
|
||||||
|
v-for="(sub, sIdx) in activeChildren.slice(0, 8)"
|
||||||
|
:key="sub.id || sIdx"
|
||||||
|
class="top-tag-pill"
|
||||||
|
@click.stop="goGoodsList(sub.id, sub.parentId)"
|
||||||
|
>
|
||||||
|
{{ sub.name }}
|
||||||
|
<el-icon class="pill-arrow"><ArrowRight /></el-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 详细分类二级与三级平铺网格 -->
|
||||||
|
<div class="flyout-content-scroll">
|
||||||
|
<div
|
||||||
|
v-for="(sub, sIdx) in activeChildren"
|
||||||
|
:key="sub.id || sIdx"
|
||||||
|
class="cat-row"
|
||||||
|
>
|
||||||
|
<!-- 二级分类标题 -->
|
||||||
|
<div
|
||||||
|
class="lvl2-title"
|
||||||
|
@click.stop="goGoodsList(sub.id, sub.parentId)"
|
||||||
|
>
|
||||||
|
<span class="lvl2-name">{{ sub.name }}</span>
|
||||||
|
<el-icon class="lvl2-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 三级分类细项 -->
|
||||||
|
<div class="lvl3-tags-wrap">
|
||||||
|
<span
|
||||||
|
v-for="(leaf, lIdx) in (sub.children || [])"
|
||||||
|
:key="leaf.id || lIdx"
|
||||||
|
class="lvl3-tag"
|
||||||
|
@click.stop="goGoodsList(leaf.id, sub.id, sub.parentId)"
|
||||||
|
>
|
||||||
|
{{ leaf.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ArrowRight } from '@element-plus/icons-vue';
|
||||||
|
import { getCategory } from '@/api/goods';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
rowStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 默认真实类目备选数据(防止弱网下未加载出接口时空白)
|
||||||
|
const fallbackCategories = [
|
||||||
|
{
|
||||||
|
id: '1348576427264204941',
|
||||||
|
name: '电脑办公',
|
||||||
|
children: [
|
||||||
|
{ id: '1-1', name: '电脑整机', parentId: '1348576427264204941', children: [{ id: '1-1-1', name: '笔记本' }, { id: '1-1-2', name: '游戏本' }, { id: '1-1-3', name: '台式机' }, { id: '1-1-4', name: '平板电脑' }] },
|
||||||
|
{ id: '1-2', name: '电脑配件', parentId: '1348576427264204941', children: [{ id: '1-2-1', name: '显示器' }, { id: '1-2-2', name: '显卡' }, { id: '1-2-3', name: '机械键盘' }, { id: '1-2-4', name: '鼠标' }] },
|
||||||
|
{ id: '1-3', name: '手机通讯', parentId: '1348576427264204941', children: [{ id: '1-3-1', name: '5G智能手机' }, { id: '1-3-2', name: '拍照手机' }, { id: '1-3-3', name: '蓝牙耳机' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399553',
|
||||||
|
name: '家用电器',
|
||||||
|
children: [
|
||||||
|
{ id: '2-1', name: '大家电', parentId: '1348576427268399553', children: [{ id: '2-1-1', name: '平板电视' }, { id: '2-1-2', name: '风冷冰箱' }, { id: '2-1-3', name: '滚筒洗衣机' }, { id: '2-1-4', name: '变频空调' }] },
|
||||||
|
{ id: '2-2', name: '生活电器', parentId: '1348576427268399553', children: [{ id: '2-2-1', name: '空气净化器' }, { id: '2-2-2', name: '扫地机器人' }, { id: '2-2-3', name: '吸尘器' }] },
|
||||||
|
{ id: '2-3', name: '厨房小电', parentId: '1348576427268399553', children: [{ id: '2-3-1', name: '电饭煲' }, { id: '2-3-2', name: '复古咖啡机' }, { id: '2-3-3', name: '破壁料理机' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427260010496',
|
||||||
|
name: '服装鞋包',
|
||||||
|
children: [
|
||||||
|
{ id: '3-1', name: '女装流行', parentId: '1348576427260010496', children: [{ id: '3-1-1', name: '连衣裙' }, { id: '3-1-2', name: '羊绒大衣' }, { id: '3-1-3', name: '早秋衬衫' }, { id: '3-1-4', name: '休闲裤' }] },
|
||||||
|
{ id: '3-2', name: '男装精选', parentId: '1348576427260010496', children: [{ id: '3-2-1', name: '商务夹克' }, { id: '3-2-2', name: '休闲T恤' }, { id: '3-2-3', name: '免烫衬衫' }, { id: '3-2-4', name: '牛仔裤' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399363',
|
||||||
|
name: '食品生鲜',
|
||||||
|
children: [
|
||||||
|
{ id: '4-1', name: '生鲜水果', parentId: '1348576427268399363', children: [{ id: '4-1-1', name: '进口车厘子' }, { id: '4-1-2', name: '红富士苹果' }, { id: '4-1-3', name: '佳沛奇异果' }] },
|
||||||
|
{ id: '4-2', name: '休闲零食', parentId: '1348576427268399363', children: [{ id: '4-2-1', name: '每日坚果' }, { id: '4-2-2', name: '烘焙饼干' }, { id: '4-2-3', name: '巧克力糖果' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399172',
|
||||||
|
name: '精品箱包',
|
||||||
|
children: [
|
||||||
|
{ id: '5-1', name: '潮流女包', parentId: '1348576427268399172', children: [{ id: '5-1-1', name: '真皮单肩包' }, { id: '5-1-2', name: '托特大包' }, { id: '5-1-3', name: '斜挎小方包' }] },
|
||||||
|
{ id: '5-2', name: '商务男包', parentId: '1348576427268399172', children: [{ id: '5-2-1', name: '双肩电脑包' }, { id: '5-2-2', name: '公文手提包' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399318',
|
||||||
|
name: '美妆个护',
|
||||||
|
children: [
|
||||||
|
{ id: '6-1', name: '面部护肤', parentId: '1348576427268399318', children: [{ id: '6-1-1', name: '修护精华' }, { id: '6-1-2', name: '补水面膜' }, { id: '6-1-3', name: '爽肤水' }, { id: '6-1-4', name: '眼霜' }] },
|
||||||
|
{ id: '6-2', name: '彩妆香水', parentId: '1348576427268399318', children: [{ id: '6-2-1', name: '大牌口红' }, { id: '6-2-2', name: '持久气垫' }, { id: '6-2-3', name: '沙龙香氛' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399274',
|
||||||
|
name: '运动户外',
|
||||||
|
children: [
|
||||||
|
{ id: '7-1', name: '运动鞋服', parentId: '1348576427268399274', children: [{ id: '7-1-1', name: '缓震跑鞋' }, { id: '7-1-2', name: '专业篮球鞋' }, { id: '7-1-3', name: '运动速干衣' }] },
|
||||||
|
{ id: '7-2', name: '户外装备', parentId: '1348576427268399274', children: [{ id: '7-2-1', name: '露营帐篷' }, { id: '7-2-2', name: '登山包' }, { id: '7-2-3', name: '运动手表' }] }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1348576427268399217',
|
||||||
|
name: '家居家装',
|
||||||
|
children: [
|
||||||
|
{ id: '8-1', name: '家纺布艺', parentId: '1348576427268399217', children: [{ id: '8-1-1', name: '四件套' }, { id: '8-1-2', name: '乳胶枕' }, { id: '8-1-3', name: '羽绒被' }] },
|
||||||
|
{ id: '8-2', name: '现代家具', parentId: '1348576427268399217', children: [{ id: '8-2-1', name: '人体工学椅' }, { id: '8-2-2', name: '升降电脑桌' }, { id: '8-2-3', name: '真皮沙发' }] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const systemCategories = ref([]);
|
||||||
|
const activeIndex = ref(-1);
|
||||||
|
const activeCategory = ref(null);
|
||||||
|
const panelVisible = ref(false);
|
||||||
|
|
||||||
|
const isCustomSource = computed(() => {
|
||||||
|
// 1. 如果明确指定为系统分类,始终按系统商品分类处理
|
||||||
|
if (props.options?.source === 'system') return false;
|
||||||
|
// 2. 如果配置为 custom,但分类条目依然是初始未编辑的 4 个占位条目,依然展示真实系统分类
|
||||||
|
if (props.options?.source === 'custom') {
|
||||||
|
if (Array.isArray(props.options?.categories) && props.options.categories.length === 4) {
|
||||||
|
const names = props.options.categories.map(c => c.name || c.title || '');
|
||||||
|
const isPlaceholder = names.some(n => n.includes('精选')) &&
|
||||||
|
names.some(n => n.includes('新品')) &&
|
||||||
|
names.some(n => n.includes('企业')) &&
|
||||||
|
names.some(n => n.includes('领券'));
|
||||||
|
if (isPlaceholder) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// 3. 其它默认情况均默认展示系统商品分类
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayCategories = computed(() => {
|
||||||
|
if (isCustomSource.value && Array.isArray(props.options?.categories) && props.options.categories.length) {
|
||||||
|
return props.options.categories.map(c => ({
|
||||||
|
...c,
|
||||||
|
name: c.name || c.title || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (systemCategories.value && systemCategories.value.length) {
|
||||||
|
return systemCategories.value;
|
||||||
|
}
|
||||||
|
return fallbackCategories;
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeChildren = computed(() => {
|
||||||
|
if (!activeCategory.value) return [];
|
||||||
|
if (Array.isArray(activeCategory.value.children) && activeCategory.value.children.length) {
|
||||||
|
return activeCategory.value.children;
|
||||||
|
}
|
||||||
|
if (Array.isArray(activeCategory.value.subCategories) && activeCategory.value.subCategories.length) {
|
||||||
|
return activeCategory.value.subCategories.map((s, idx) => ({
|
||||||
|
id: s.id || `sub_${idx}`,
|
||||||
|
name: s.name,
|
||||||
|
parentId: activeCategory.value.id,
|
||||||
|
children: (s.items || []).map((it, iIdx) => ({
|
||||||
|
id: `leaf_${idx}_${iIdx}`,
|
||||||
|
name: typeof it === 'string' ? it : it.name,
|
||||||
|
parentId: s.id
|
||||||
|
}))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleMouseEnter(idx, cat) {
|
||||||
|
activeIndex.value = idx;
|
||||||
|
activeCategory.value = cat;
|
||||||
|
if (cat.children && cat.children.length) {
|
||||||
|
panelVisible.value = true;
|
||||||
|
} else if (cat.subCategories && cat.subCategories.length) {
|
||||||
|
panelVisible.value = true;
|
||||||
|
} else {
|
||||||
|
panelVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePanelEnter() {
|
||||||
|
panelVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
panelVisible.value = false;
|
||||||
|
activeIndex.value = -1;
|
||||||
|
activeCategory.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function goGoodsList(id, secondId, firstId) {
|
||||||
|
const arr = [firstId, secondId, id];
|
||||||
|
if (!arr[1]) arr.splice(0, 2);
|
||||||
|
if (!arr[0]) arr.shift();
|
||||||
|
router.push({
|
||||||
|
path: '/goodsList',
|
||||||
|
query: { categoryId: arr.filter(Boolean).join(',') }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCategories() {
|
||||||
|
// 1. 优先读取 localStorage 缓存快速渲染
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem('category');
|
||||||
|
const exp = localStorage.getItem('category_expiration_time');
|
||||||
|
if (cached && exp && new Date() <= Number(exp)) {
|
||||||
|
const parsed = JSON.parse(cached);
|
||||||
|
if (Array.isArray(parsed) && parsed.length) {
|
||||||
|
systemCategories.value = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 2. 调接口拉取最新分类
|
||||||
|
getCategory(0).then(res => {
|
||||||
|
if (res.success && Array.isArray(res.result) && res.result.length) {
|
||||||
|
systemCategories.value = res.result;
|
||||||
|
const expTime = new Date().setHours(new Date().getHours() + 1);
|
||||||
|
localStorage.setItem('category_expiration_time', expTime);
|
||||||
|
localStorage.setItem('category', JSON.stringify(res.result));
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn('获取分类失败', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||||
|
return {
|
||||||
|
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
|
||||||
|
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: hasRowHeight ? '0' : '420px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-category-mega-menu {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
&.is-hovering {
|
||||||
|
z-index: 1050;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-header {
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
padding: 4px 0 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.cat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
.cat-name {
|
||||||
|
font-size: var(--font-size, 13px);
|
||||||
|
color: var(--text-color, #334155);
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
transition: transform 0.15s ease, color 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.is-active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
|
||||||
|
.cat-name {
|
||||||
|
color: var(--active-color, #409EFF);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-icon {
|
||||||
|
color: var(--active-color, #409EFF);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mega-flyout-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 100%;
|
||||||
|
width: 780px;
|
||||||
|
min-height: 100%;
|
||||||
|
max-height: 520px;
|
||||||
|
background: #ffffff;
|
||||||
|
z-index: 1100;
|
||||||
|
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12), 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-left: none;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.flyout-top-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.top-tag-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #334155;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
|
||||||
|
.pill-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409EFF;
|
||||||
|
|
||||||
|
.pill-arrow {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-content-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 6px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cat-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px dashed #f1f5f9;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lvl2-title {
|
||||||
|
width: 96px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #1e293b;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-top: 1px;
|
||||||
|
|
||||||
|
.lvl2-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lvl2-arrow {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
.lvl2-arrow {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lvl3-tags-wrap {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 14px;
|
||||||
|
|
||||||
|
.lvl3-tag {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
|
line-height: 1.6;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-flyout-enter-active,
|
||||||
|
.fade-flyout-leave-active {
|
||||||
|
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-flyout-enter-from,
|
||||||
|
.fade-flyout-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-4px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
199
buyer/src/components/indexDecorate/components/CategoryNav.vue
Normal file
199
buyer/src/components/indexDecorate/components/CategoryNav.vue
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-category-nav" :style="customStyle">
|
||||||
|
<div class="nav-inner">
|
||||||
|
<!-- 1. 全部分类入口 (当开启显示全部分类时) -->
|
||||||
|
<div
|
||||||
|
v-if="options.showAllCategory"
|
||||||
|
class="all-category-btn"
|
||||||
|
:style="{ backgroundColor: options.activeColor || '#e11d48' }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="options.allCategoryIcon"
|
||||||
|
:src="options.allCategoryIcon"
|
||||||
|
class="all-icon-img"
|
||||||
|
alt="icon"
|
||||||
|
/>
|
||||||
|
<el-icon v-else class="all-icon-svg"><Menu /></el-icon>
|
||||||
|
<span class="all-text">{{ options.allCategoryText || '全部分类' }}</span>
|
||||||
|
<el-icon class="all-arrow"><ArrowDown /></el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 分类导航项水平列表 -->
|
||||||
|
<ul class="nav-items-ul">
|
||||||
|
<li
|
||||||
|
v-for="(item, idx) in options.navItems"
|
||||||
|
:key="idx"
|
||||||
|
class="nav-li"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="item.url || '/'"
|
||||||
|
class="nav-link"
|
||||||
|
:style="{
|
||||||
|
'--text-color': options.textColor || '#334155',
|
||||||
|
'--active-color': options.activeColor || '#e11d48'
|
||||||
|
}"
|
||||||
|
active-class="is-active"
|
||||||
|
>
|
||||||
|
<img v-if="item.img" :src="item.img" class="cat-icon" alt="cat-icon" />
|
||||||
|
<span class="cat-title">{{ item.title }}</span>
|
||||||
|
<span
|
||||||
|
class="badge"
|
||||||
|
v-if="item.badge"
|
||||||
|
:style="{ backgroundColor: options.activeColor || '#e11d48' }"
|
||||||
|
>
|
||||||
|
{{ item.badge }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { Menu, ArrowDown } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
height: `${s.height || 42}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-category-nav {
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.nav-inner {
|
||||||
|
max-width: 1600px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
// 全部分类按钮
|
||||||
|
.all-category-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 18px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-icon-img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-icon-svg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-text {
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-arrow {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-items-ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-li {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color, #334155);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
height: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
.cat-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &.is-active {
|
||||||
|
color: var(--active-color, #e11d48) !important;
|
||||||
|
|
||||||
|
.cat-icon {
|
||||||
|
transform: scale(1.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: -12px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 9px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
572
buyer/src/components/indexDecorate/components/ChannelFloor.vue
Normal file
572
buyer/src/components/indexDecorate/components/ChannelFloor.vue
Normal file
@@ -0,0 +1,572 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-channel-floor" :style="customStyle">
|
||||||
|
<!-- 1. 标题区 -->
|
||||||
|
<div class="channel-floor-header" v-if="options.showTitle !== false && options.title">
|
||||||
|
<div class="header-main-title">
|
||||||
|
<span class="title-pill"></span>
|
||||||
|
<h3 class="title-text">{{ options.title }}</h3>
|
||||||
|
<span class="subtitle-text" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
<router-link :to="options.heroHref || '/goods'" class="header-more">
|
||||||
|
探索更多场景 >
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 场景频道组合布局:左侧主视觉+活动入口 + 右侧商品网格 -->
|
||||||
|
<div class="channel-floor-body" :class="{ 'no-hero': !showHero }">
|
||||||
|
<!-- 左侧主视觉卡片 (Hero Card) -->
|
||||||
|
<div
|
||||||
|
class="channel-hero-card"
|
||||||
|
:class="[`style-${options.heroStyle || 'gradient'}`]"
|
||||||
|
v-if="showHero"
|
||||||
|
>
|
||||||
|
<div class="hero-top-info">
|
||||||
|
<div class="hero-badge">SCENE CHANNEL</div>
|
||||||
|
<h3 class="hero-title">{{ options.heroTitle || '品质生活馆' }}</h3>
|
||||||
|
<p class="hero-sub">{{ options.heroSubtitle || '精选场景好物,一站购齐' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-image-wrap">
|
||||||
|
<img :src="heroImg" alt="channel hero" class="hero-img" />
|
||||||
|
<router-link :to="options.heroHref || '/goods'" class="hero-cta-btn">
|
||||||
|
<span>{{ options.heroButtonText || '进入频道' }}</span>
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||||
|
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
|
||||||
|
</svg>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 专属活动入口 (Promos) -->
|
||||||
|
<div class="hero-promos-wrap" v-if="promosList.length">
|
||||||
|
<router-link
|
||||||
|
v-for="(p, pIdx) in promosList"
|
||||||
|
:key="pIdx"
|
||||||
|
:to="p.url || '/goods'"
|
||||||
|
class="hero-promo-item"
|
||||||
|
>
|
||||||
|
<span class="promo-tag" v-if="p.tag">{{ p.tag }}</span>
|
||||||
|
<div class="promo-text-box">
|
||||||
|
<span class="promo-title">{{ p.title }}</span>
|
||||||
|
<span class="promo-desc">{{ p.desc }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="promo-arrow">></span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧商品网格 (Scene Goods Grid) -->
|
||||||
|
<div
|
||||||
|
class="channel-goods-grid"
|
||||||
|
:class="`cols-${rightColumns}`"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${rightColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(g, idx) in displayGoods"
|
||||||
|
:key="g.id || idx"
|
||||||
|
class="scene-goods-card"
|
||||||
|
>
|
||||||
|
<router-link :to="`/goods/${g.id || 1}`" class="card-thumb-wrap">
|
||||||
|
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
|
||||||
|
<span class="scene-tag">场景精选</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<div class="card-info">
|
||||||
|
<h4 class="goods-name">
|
||||||
|
<router-link :to="`/goods/${g.id || 1}`">{{ g.goodsName }}</router-link>
|
||||||
|
</h4>
|
||||||
|
<div class="price-action-row">
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="sym">¥</span>
|
||||||
|
<span class="val">{{ g.price }}</span>
|
||||||
|
<span class="orig" v-if="g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="cart-btn"
|
||||||
|
title="立即加购"
|
||||||
|
@click="addToCart(g)"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleNavigate(url) {
|
||||||
|
const target = url ? String(url).trim() : '/goods';
|
||||||
|
if (!target) return;
|
||||||
|
if (/^https?:\/\//i.test(target)) {
|
||||||
|
window.open(target, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||||
|
|
||||||
|
const heroImg = computed(() => {
|
||||||
|
return props.options?.heroImage || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||||
|
});
|
||||||
|
|
||||||
|
const showHero = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rightColumns = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||||
|
if (ratio === '50%') return showHero.value ? 2 : 3;
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%') return showHero.value ? 3 : 4;
|
||||||
|
return 3;
|
||||||
|
});
|
||||||
|
|
||||||
|
const promosList = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.promos) && props.options.promos.length > 0) {
|
||||||
|
return props.options.promos;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ title: '大牌补贴', desc: '领券最高减100', tag: '限时', url: '/goods' },
|
||||||
|
{ title: '新品驾到', desc: '官方正品首发', tag: '新品', url: '/goods' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultGoods = [
|
||||||
|
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||||
|
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '5', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||||
|
{ id: '6', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayGoods = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||||
|
return props.options.goods;
|
||||||
|
}
|
||||||
|
const count = props.options?.goodsCount || 6;
|
||||||
|
return defaultGoods.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '16px 20px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '12px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToCart(item) {
|
||||||
|
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-channel-floor {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.channel-floor-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 2px solid #f1f5f9;
|
||||||
|
|
||||||
|
.header-main-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.title-pill {
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
background: linear-gradient(180deg, #6366f1, #4f46e5);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle-text {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-more {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-floor-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
&.no-hero {
|
||||||
|
.channel-goods-grid {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-hero-card {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px 18px 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.style-gradient {
|
||||||
|
background: linear-gradient(145deg, #1e1b4b, #312e81 60%, #4338ca);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.style-plain {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #1e293b;
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #4338ca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta-btn {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top-info {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.hero-badge {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 220px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.hero-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .hero-img {
|
||||||
|
transform: scale(1.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-cta-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #312e81;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.25);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-promos-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
|
||||||
|
.hero-promo-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.22);
|
||||||
|
transform: translateX(3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-text-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.promo-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.promo-arrow {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-goods-grid {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.scene-goods-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumb-wrap {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.goods-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .goods-img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1e293b;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-action-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
.sym {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orig {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-btn {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
277
buyer/src/components/indexDecorate/components/ChannelGrid.vue
Normal file
277
buyer/src/components/indexDecorate/components/ChannelGrid.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-channel-grid" :style="customStyle">
|
||||||
|
<div
|
||||||
|
class="grid-container"
|
||||||
|
:style="{
|
||||||
|
gridTemplateColumns: `repeat(${gridColumns}, 1fr)`,
|
||||||
|
gap: `${options.gap ?? 12}px`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
v-for="(item, idx) in options.items"
|
||||||
|
:key="idx"
|
||||||
|
:to="item.url || '/goods'"
|
||||||
|
class="channel-card"
|
||||||
|
:class="[
|
||||||
|
`is-${layoutMode}`,
|
||||||
|
{ 'is-compact': isCompact }
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<!-- 图标/图片 -->
|
||||||
|
<div
|
||||||
|
class="card-thumb"
|
||||||
|
:style="{ borderRadius: `${options.imageRadius ?? 10}px` }"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="item.img"
|
||||||
|
:src="item.img"
|
||||||
|
:alt="item.title"
|
||||||
|
class="thumb-img"
|
||||||
|
/>
|
||||||
|
<div v-else class="thumb-placeholder">
|
||||||
|
<span>{{ (item.title || '频').charAt(0) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 主标题与副标题 -->
|
||||||
|
<div class="card-info">
|
||||||
|
<div class="card-title">{{ item.title || '频道名称' }}</div>
|
||||||
|
<div class="card-sub">{{ item.subtitle || '副标题文案' }}</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 排版宽度比例 (整行: 100%, 三分之二: 66.67%, 一半: 50%, 三分之一: 33.33%, 六分之一: 16.67%)
|
||||||
|
const widthRatio = computed(() => {
|
||||||
|
return props.componentStyle?.widthRatio || '100%';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 响应式列数:50%、33.33%、16.67% 为 2 列;100%、66.67% 为 4 列
|
||||||
|
const gridColumns = computed(() => {
|
||||||
|
const w = widthRatio.value;
|
||||||
|
if (w === '50%' || w === '33.33%' || w === '33.3%' || w === '16.67%' || w === '16.6%') {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 卡片内部排布方式:
|
||||||
|
// 100% (整行)、50% (一半) -> 水平横向 (左图右文)
|
||||||
|
// 66.67% (三分之二)、33.33% (三分之一)、16.67% (六分之一) -> 垂直纵向 (上图下文居中)
|
||||||
|
const layoutMode = computed(() => {
|
||||||
|
const w = widthRatio.value;
|
||||||
|
if (w === '66.67%' || w === '66.6%' || w === '33.33%' || w === '33.3%' || w === '16.67%' || w === '16.6%') {
|
||||||
|
return 'vertical';
|
||||||
|
}
|
||||||
|
return 'horizontal';
|
||||||
|
});
|
||||||
|
|
||||||
|
// 六分之一紧凑模式
|
||||||
|
const isCompact = computed(() => {
|
||||||
|
const w = widthRatio.value;
|
||||||
|
return w === '16.67%' || w === '16.6%';
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '10px 0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-channel-grid {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.grid-container {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.channel-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
|
||||||
|
.card-thumb .thumb-img {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info .card-title {
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumb {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f8fafc;
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, #f97316, #ea580c);
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
color: #94a3b8;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 水平横向排布 (整行 100%、一半 50%)
|
||||||
|
&.is-horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
|
||||||
|
.card-thumb {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
gap: 3px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 垂直纵向排布 (三分之二 66.67%、三分之一 33.33%、六分之一 16.67%)
|
||||||
|
&.is-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px 10px 12px 10px;
|
||||||
|
|
||||||
|
.card-thumb {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
width: 100%;
|
||||||
|
gap: 2px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 六分之一紧凑模式
|
||||||
|
&.is-compact {
|
||||||
|
padding: 10px 6px 8px 6px;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.card-thumb {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
184
buyer/src/components/indexDecorate/components/CouponList.vue
Normal file
184
buyer/src/components/indexDecorate/components/CouponList.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-coupon-list" :class="`skin-${options.skin || 'red'}`" :style="customStyle">
|
||||||
|
<div
|
||||||
|
class="coupon-grid"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(cp, idx) in coupons"
|
||||||
|
:key="idx"
|
||||||
|
class="coupon-card"
|
||||||
|
>
|
||||||
|
<div class="card-left">
|
||||||
|
<div class="amount-wrap">
|
||||||
|
<span class="symbol">¥</span>
|
||||||
|
<span class="amount">{{ cp.price }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="threshold">{{ cp.consumeThreshold ? `满 ${cp.consumeThreshold} 可用` : '无门槛' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-right">
|
||||||
|
<span class="scope">{{ cp.title }}</span>
|
||||||
|
<button type="button" class="claim-btn" @click="handleClaim(cp)">
|
||||||
|
立即领取
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultCoupons = [
|
||||||
|
{ id: '1', price: 20, consumeThreshold: 199, title: '全品类通用券', limitText: '全场通用 无门槛限制' },
|
||||||
|
{ id: '2', price: 50, consumeThreshold: 399, title: '数码家电专享券', limitText: '数码/手机/家电可用' },
|
||||||
|
{ id: '3', price: 100, consumeThreshold: 699, title: '大牌美妆特惠券', limitText: '美妆奢护专区可用' },
|
||||||
|
{ id: '4', price: 10, consumeThreshold: 99, title: '新客首单立减券', limitText: '首次下单专享福利' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const actualColumns = computed(() => {
|
||||||
|
const baseCols = Number(props.options?.columns) || 4;
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '50%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||||
|
return Math.min(baseCols, 3);
|
||||||
|
}
|
||||||
|
return baseCols;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '10px 0',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const coupons = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.coupons) && props.options.coupons.length > 0) {
|
||||||
|
return props.options.coupons;
|
||||||
|
}
|
||||||
|
const count = props.options?.columns || 4;
|
||||||
|
return defaultCoupons.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClaim(cp) {
|
||||||
|
ElMessage.success(`领取成功!¥${cp.price} 优惠券已放入您的卡券包`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-coupon-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.coupon-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.coupon-card {
|
||||||
|
display: flex;
|
||||||
|
height: 90px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-left {
|
||||||
|
width: 120px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.amount-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.symbol { font-size: 14px; font-weight: 700; }
|
||||||
|
.amount { font-size: 28px; font-weight: 800; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.threshold {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-right {
|
||||||
|
flex: 1;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 12px 14px;
|
||||||
|
|
||||||
|
.scope {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: none;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover { opacity: 0.85; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skin-red {
|
||||||
|
.coupon-card .card-left { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
||||||
|
.claim-btn { background: #ffe4e6; color: #e11d48; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skin-dark {
|
||||||
|
.coupon-card .card-left { background: linear-gradient(135deg, #334155, #0f172a); }
|
||||||
|
.claim-btn { background: #f1f5f9; color: #0f172a; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.skin-gradient {
|
||||||
|
.coupon-card .card-left { background: linear-gradient(135deg, #8b5cf6, #ec4899); }
|
||||||
|
.claim-btn { background: #fdf2f8; color: #db2777; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
buyer/src/components/indexDecorate/components/FooterNav.vue
Normal file
165
buyer/src/components/indexDecorate/components/FooterNav.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-footer-nav" :style="customStyle">
|
||||||
|
<div class="footer-inner">
|
||||||
|
<!-- 1. 服务保障条 -->
|
||||||
|
<div class="assurance-row" v-if="options.showAssurance && options.assurances?.length">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in options.assurances"
|
||||||
|
:key="idx"
|
||||||
|
class="assurance-col"
|
||||||
|
>
|
||||||
|
<div class="code-badge">{{ item.code }}</div>
|
||||||
|
<div class="text-info">
|
||||||
|
<span class="main-t">{{ item.title }}</span>
|
||||||
|
<span class="sub-t">{{ item.desc }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 服务指南与链接列 -->
|
||||||
|
<div class="guides-row" v-if="options.guideGroups?.length">
|
||||||
|
<div
|
||||||
|
v-for="(group, gIdx) in options.guideGroups"
|
||||||
|
:key="gIdx"
|
||||||
|
class="guide-col"
|
||||||
|
>
|
||||||
|
<h4 class="col-title">{{ group.title }}</h4>
|
||||||
|
<ul class="col-links">
|
||||||
|
<li v-for="(link, lIdx) in group.links" :key="lIdx">
|
||||||
|
<router-link :to="link.url || '/article'">{{ link.text }}</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 版权与备案 -->
|
||||||
|
<div class="copyright-row">
|
||||||
|
<p class="icp-text" v-if="options.icpText">{{ options.icpText }}</p>
|
||||||
|
<p class="copy-text">{{ options.copyright || 'Copyright © 2026 Lilishop. All Rights Reserved.' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#f1f5f9'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-footer-nav {
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px 0 30px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
.footer-inner {
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.assurance-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
.assurance-col {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.code-badge {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #e11d48;
|
||||||
|
color: #e11d48;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.main-t {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-t {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guides-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 30px 0;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
.guide-col {
|
||||||
|
.col-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-links {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
li a {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright-row {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 24px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
60
buyer/src/components/indexDecorate/components/FreeLayout.vue
Normal file
60
buyer/src/components/indexDecorate/components/FreeLayout.vue
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-free-layout" :style="customStyle">
|
||||||
|
<div
|
||||||
|
v-for="(el, idx) in options.elements"
|
||||||
|
:key="idx"
|
||||||
|
class="free-element"
|
||||||
|
:style="{
|
||||||
|
top: `${el.top || 0}px`,
|
||||||
|
left: `${el.left || 0}px`,
|
||||||
|
width: el.width ? `${el.width}px` : 'auto',
|
||||||
|
height: el.height ? `${el.height}px` : 'auto'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<img v-if="el.type === 'image'" :src="el.src" alt="" style="width:100%;height:100%;object-fit:cover;" />
|
||||||
|
<span v-else-if="el.type === 'text'">{{ el.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
height: `${props.options?.height || 320}px`,
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '0',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-free-layout {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.free-element {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
343
buyer/src/components/indexDecorate/components/GoodsGrid.vue
Normal file
343
buyer/src/components/indexDecorate/components/GoodsGrid.vue
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-goods-grid" :style="customStyle">
|
||||||
|
<!-- 宫格顶部标题区 -->
|
||||||
|
<div class="grid-header" v-if="options.title">
|
||||||
|
<div class="grid-title-wrap">
|
||||||
|
<span class="grid-title-decor"></span>
|
||||||
|
<h3 class="grid-main-title">{{ options.title }}</h3>
|
||||||
|
<span class="grid-sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
<a href="javascript:;" class="grid-more" v-if="options.showMoreLink !== false" @click="handleNavigate(options.moreUrl || '/goods')">
|
||||||
|
{{ options.moreText || '查看全部' }} >
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 紧凑严谨的商品九宫格/矩阵网格 (Bordered Matrix Grid) -->
|
||||||
|
<div
|
||||||
|
class="matrix-grid-container"
|
||||||
|
:class="[`cols-${actualColumns}`, { 'with-grid-border': options.showBorder !== false }]"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in displayGoods"
|
||||||
|
:key="item.id || idx"
|
||||||
|
class="matrix-grid-cell"
|
||||||
|
>
|
||||||
|
<div class="cell-rank-tag" v-if="idx < 5">
|
||||||
|
<span class="rank-num">TOP {{ idx + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="javascript:;" class="cell-thumb-box" @click="handleNavigate(item.url || `/goods/${item.id || 1}`)">
|
||||||
|
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" class="cell-thumb" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="cell-body">
|
||||||
|
<h4 class="cell-name">
|
||||||
|
<a href="javascript:;" @click="handleNavigate(item.url || `/goods/${item.id || 1}`)">{{ item.goodsName }}</a>
|
||||||
|
</h4>
|
||||||
|
<div class="cell-meta">
|
||||||
|
<span class="badge-label">热卖好物</span>
|
||||||
|
</div>
|
||||||
|
<div class="cell-bottom-row">
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="sym">¥</span>
|
||||||
|
<span class="val">{{ item.price }}</span>
|
||||||
|
<span class="del" v-if="item.originalPrice">¥{{ item.originalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<span class="action-add-pill" title="立即选购" @click="addToCart(item)">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||||
|
|
||||||
|
const actualColumns = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
const base = Number(props.options?.columns) || 5;
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||||
|
if (ratio === '50%') return 3;
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%') return 4;
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultGridGoods = [
|
||||||
|
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||||
|
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||||
|
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||||
|
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||||
|
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||||
|
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '10', goodsName: '15.6英寸 4K超高清便携式显示器 Type-C一线直连高色域', price: '1299.00', originalPrice: '1699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayGoods = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||||
|
return props.options.goods;
|
||||||
|
}
|
||||||
|
const count = (actualColumns.value * 2) || 10;
|
||||||
|
return defaultGridGoods.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '18px 20px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '12px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToCart(item) {
|
||||||
|
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-goods-grid {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.grid-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
.grid-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.grid-title-decor {
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
background: #f97316;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-main-title {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-more {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-grid-container {
|
||||||
|
display: grid;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.with-grid-border {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.matrix-grid-cell {
|
||||||
|
border-right: 1px solid #f1f5f9;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.matrix-grid-cell {
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 14px 14px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #fafbfc;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.cell-thumb {
|
||||||
|
transform: scale(1.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-add-pill {
|
||||||
|
background: #f97316;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-rank-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.rank-num {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #f97316;
|
||||||
|
background: #fff7ed;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-thumb-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.cell-thumb {
|
||||||
|
width: 85%;
|
||||||
|
height: 85%;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.cell-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #334155;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-meta {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.badge-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #d97706;
|
||||||
|
background: #fef3c7;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-bottom-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1px;
|
||||||
|
|
||||||
|
.sym {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-add-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
transition: all 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
435
buyer/src/components/indexDecorate/components/GoodsList.vue
Normal file
435
buyer/src/components/indexDecorate/components/GoodsList.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-goods-list" :style="customStyle">
|
||||||
|
<!-- 标题栏 -->
|
||||||
|
<div class="floor-title-bar" v-if="options.showTitle !== false && options.title">
|
||||||
|
<div class="title-left">
|
||||||
|
<h3 class="main-title">{{ options.title }}</h3>
|
||||||
|
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
:to="options.moreUrl || (options.categoryId ? `/goodsList?categoryId=${options.categoryId}` : '/goodsList')"
|
||||||
|
class="more-link"
|
||||||
|
v-if="options.showMoreLink !== false"
|
||||||
|
>
|
||||||
|
{{ options.moreText || '查看更多' }} >
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 6 列商品列表网格 (整行展示 6 个商品) -->
|
||||||
|
<div
|
||||||
|
class="goods-flow-grid"
|
||||||
|
:class="`cols-${actualColumns}`"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(g, idx) in sampleGoods"
|
||||||
|
:key="g.id || idx"
|
||||||
|
class="goods-flow-card"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ path: '/goodsDetail', query: { skuId: g.id, goodsId: g.goodsId || g.id } }"
|
||||||
|
class="card-thumb-wrap"
|
||||||
|
>
|
||||||
|
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="thumb-img" />
|
||||||
|
<span class="self-tag" v-if="idx % 3 === 0">自营</span>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="goods-name" v-if="options.showTitle !== false">
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: g.id, goodsId: g.goodsId || g.id } }">
|
||||||
|
{{ g.goodsName }}
|
||||||
|
</router-link>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div class="price-row">
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="symbol">¥</span>
|
||||||
|
<span class="price-val" v-if="options.showPrice !== false">{{ g.price }}</span>
|
||||||
|
<span class="orig-price" v-if="options.showOriginalPrice && g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="add-cart-circle"
|
||||||
|
v-if="options.showCartBtn !== false"
|
||||||
|
title="加入购物车"
|
||||||
|
@click="addToCart(g)"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { goodsList } from '@/api/goods';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleNavigateGoods(g) {
|
||||||
|
if (g.url) {
|
||||||
|
const url = String(g.url).trim();
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push({
|
||||||
|
path: '/goodsDetail',
|
||||||
|
query: { skuId: g.id, goodsId: g.goodsId || g.id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigateMore() {
|
||||||
|
const url = props.options?.moreUrl || (props.options?.categoryId ? `/goodsList?categoryId=${props.options.categoryId}` : '/goodsList');
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||||
|
|
||||||
|
const defaultMockGoods = [
|
||||||
|
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||||
|
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||||
|
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||||
|
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||||
|
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||||
|
{ id: '9', goodsName: '折叠屏旗舰手机 典藏陶瓷白 悬停双旋水滴铰链', price: '6999.00', originalPrice: '7999.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '10', goodsName: '真无线触控降噪蓝牙耳机 超低延迟游戏降噪', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '11', goodsName: '国AA级减蓝光润眼长臂台灯 智能感光多档色温无极调光', price: '249.00', originalPrice: '359.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '12', goodsName: '航空级铝镁合金静音万向轮拉杆登机箱 20英寸防刮耐磨', price: '499.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const categoryFetchedGoods = ref([]);
|
||||||
|
const isFetching = ref(false);
|
||||||
|
|
||||||
|
async function fetchGoodsByCategory() {
|
||||||
|
if (props.options.dataSource !== 'category') return;
|
||||||
|
|
||||||
|
const count = Number(props.options.goodsCount) || (props.component?.type === 'goodsCard' ? 1 : 6);
|
||||||
|
const params = {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: count
|
||||||
|
};
|
||||||
|
|
||||||
|
if (props.options.categoryId) {
|
||||||
|
const rawCat = String(props.options.categoryId);
|
||||||
|
const catParts = rawCat.split(',');
|
||||||
|
params.categoryId = catParts[catParts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 排序参数映射
|
||||||
|
if (props.options.sortType === 'sales') {
|
||||||
|
params.sort = 'buyCount';
|
||||||
|
params.order = 'desc';
|
||||||
|
} else if (props.options.sortType === 'price_asc') {
|
||||||
|
params.sort = 'price';
|
||||||
|
params.order = 'asc';
|
||||||
|
} else if (props.options.sortType === 'price_desc') {
|
||||||
|
params.sort = 'price';
|
||||||
|
params.order = 'desc';
|
||||||
|
} else if (props.options.sortType === 'new') {
|
||||||
|
params.sort = 'releaseTime';
|
||||||
|
params.order = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
isFetching.value = true;
|
||||||
|
const res = await goodsList(params);
|
||||||
|
const records = res?.data?.result?.records || res?.result?.records || res?.data?.records || [];
|
||||||
|
if (Array.isArray(records) && records.length > 0) {
|
||||||
|
categoryFetchedGoods.value = records.map(item => ({
|
||||||
|
id: item.id || item.goodsId || item.sn,
|
||||||
|
goodsId: item.goodsId || item.id,
|
||||||
|
goodsName: item.goodsName || item.name,
|
||||||
|
price: item.price,
|
||||||
|
originalPrice: item.originalPrice || item.price,
|
||||||
|
thumbnail: item.thumbnail || item.pic || defaultThumb
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Fetch goods by category failed:', err);
|
||||||
|
} finally {
|
||||||
|
isFetching.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchGoodsByCategory();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.options.dataSource, props.options.categoryId, props.options.goodsCount, props.options.sortType],
|
||||||
|
() => {
|
||||||
|
fetchGoodsByCategory();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const isGoodsCard = computed(() => {
|
||||||
|
return props.component?.type === 'goodsCard' || props.options?.type === 'goodsCard';
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualColumns = computed(() => {
|
||||||
|
let baseCols = Number(props.options?.columns) || 6;
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '50%') {
|
||||||
|
return Math.min(baseCols, 3);
|
||||||
|
}
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||||
|
return Math.min(baseCols, 4);
|
||||||
|
}
|
||||||
|
return baseCols; // 100% 默认 6 列
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '14px 0',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampleGoods = computed(() => {
|
||||||
|
if (props.options.dataSource === 'category' && categoryFetchedGoods.value.length > 0) {
|
||||||
|
const targetCount = Number(props.options.goodsCount) || (props.component?.type === 'goodsCard' ? 1 : 6);
|
||||||
|
return categoryFetchedGoods.value.slice(0, targetCount);
|
||||||
|
}
|
||||||
|
if (Array.isArray(props.options.goods) && props.options.goods.length > 0) {
|
||||||
|
return props.options.goods;
|
||||||
|
}
|
||||||
|
const count = props.options.goodsCount || (actualColumns.value * 2) || 12;
|
||||||
|
return defaultMockGoods.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToCart(item) {
|
||||||
|
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-goods-list {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.floor-title-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
padding: 0 4px;
|
||||||
|
|
||||||
|
.title-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-flow-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.goods-flow-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-cart-circle {
|
||||||
|
background: #e11d48;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumb-wrap {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.thumb-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.self-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
background: #e11d48;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1e293b;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-val {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orig-price {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-cart-circle {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
576
buyer/src/components/indexDecorate/components/GridFloor.vue
Normal file
576
buyer/src/components/indexDecorate/components/GridFloor.vue
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-grid-floor" :style="customStyle">
|
||||||
|
<!-- 楼层大标题栏 (Floor Title Bar) -->
|
||||||
|
<div class="floor-title-header" v-if="options.showFloorTitle !== false">
|
||||||
|
<div class="floor-main-heading">
|
||||||
|
<span class="floor-badge">{{ options.floorNum || '1F' }}</span>
|
||||||
|
<h3 class="floor-title-text">{{ options.title || '潮流数码' }}</h3>
|
||||||
|
<span class="floor-subtitle-text" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="floor-header-right">
|
||||||
|
<!-- 子分类快捷标签 -->
|
||||||
|
<div class="sub-category-tags" v-if="subCategories.length">
|
||||||
|
<router-link
|
||||||
|
v-for="(tag, idx) in subCategories"
|
||||||
|
:key="idx"
|
||||||
|
to="/goods"
|
||||||
|
class="tag-item"
|
||||||
|
:class="{ active: activeTagIdx === idx }"
|
||||||
|
@click.prevent="activeTagIdx = idx"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<a href="javascript:;" class="more-link" @click="handleNavigate(options.moreUrl || '/goods')">
|
||||||
|
查看更多 >
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 楼层主体:左侧竖向营销海报 + 右侧商品网格 -->
|
||||||
|
<div class="floor-main-body" :class="{ 'no-poster': !showPoster }">
|
||||||
|
<!-- 左侧营销大促海报 -->
|
||||||
|
<div class="floor-left-poster" v-if="showPoster">
|
||||||
|
<div class="poster-link" @click="handleNavigate(options.bannerUrl || options.posterUrl || '/goods')">
|
||||||
|
<img :src="posterImg" alt="floor poster" class="poster-img" />
|
||||||
|
<div class="poster-overlay">
|
||||||
|
<div class="poster-top-badge">{{ options.floorNum || '1F' }} 甄选</div>
|
||||||
|
<h4 class="poster-headline">{{ options.posterTitle || '数码潮电 酷玩季' }}</h4>
|
||||||
|
<p class="poster-subline">{{ options.posterSub || '领券最高立减 600 元' }}</p>
|
||||||
|
<div class="poster-pill-tags" v-if="posterTags.length">
|
||||||
|
<span v-for="(pt, pIdx) in posterTags" :key="pIdx" class="pill-tag">{{ pt }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="poster-action-btn">
|
||||||
|
<span>立即抢购 ></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧商品网格 (2行 x N列) -->
|
||||||
|
<div
|
||||||
|
class="floor-right-goods"
|
||||||
|
:class="`cols-${rightColumns}`"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${rightColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(g, idx) in displayGoods"
|
||||||
|
:key="g.id || idx"
|
||||||
|
class="floor-goods-card"
|
||||||
|
>
|
||||||
|
<a href="javascript:;" class="card-thumb-wrap" @click="handleNavigate(g.url || `/goods/${g.id || 1}`)">
|
||||||
|
<img :src="g.thumbnail || defaultThumb" :alt="g.goodsName" class="goods-img" />
|
||||||
|
<span class="corner-badge" v-if="idx < 3">HOT</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="card-detail">
|
||||||
|
<h4 class="goods-name-line">
|
||||||
|
<a href="javascript:;" @click="handleNavigate(g.url || `/goods/${g.id || 1}`)">{{ g.goodsName }}</a>
|
||||||
|
</h4>
|
||||||
|
<div class="goods-tag-row">
|
||||||
|
<span class="mini-tag">官方直营</span>
|
||||||
|
<span class="mini-tag light">免息</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-action-row">
|
||||||
|
<div class="price-group">
|
||||||
|
<span class="currency">¥</span>
|
||||||
|
<span class="amount">{{ g.price }}</span>
|
||||||
|
<span class="orig-amount" v-if="g.originalPrice">¥{{ g.originalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="buy-action-btn"
|
||||||
|
title="加入购物车"
|
||||||
|
@click="addToCart(g)"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||||
|
<path d="M11 9h2V6h3V4h-3V1h-2v3H8v2h3v3zm-4 9c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2zm-9.83-3.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.86-7.01L19.42 4h-.01l-1.1 2-2.76 5H8.53l-.13-.27L6.16 6l-.95-2-.94-2H1v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.13 0-.25-.11-.25-.25z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function handleNavigate(url) {
|
||||||
|
const target = url ? String(url).trim() : '/goods';
|
||||||
|
if (!target) return;
|
||||||
|
if (/^https?:\/\//i.test(target)) {
|
||||||
|
window.open(target, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNavigateGoods(g) {
|
||||||
|
if (g.url) {
|
||||||
|
handleNavigate(g.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/goods/${g.id || 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||||
|
const activeTagIdx = ref(0);
|
||||||
|
|
||||||
|
const subCategories = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.subCategories) && props.options.subCategories.length > 0) {
|
||||||
|
return props.options.subCategories;
|
||||||
|
}
|
||||||
|
return ['旗舰手机', '轻薄笔电', '降噪耳机', '智能穿戴', '数码配件'];
|
||||||
|
});
|
||||||
|
|
||||||
|
const posterTags = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.posterTags) && props.options.posterTags.length > 0) {
|
||||||
|
return props.options.posterTags;
|
||||||
|
}
|
||||||
|
return ['爆款低至5折', '新品首发', '分期免息'];
|
||||||
|
});
|
||||||
|
|
||||||
|
const posterImg = computed(() => {
|
||||||
|
return props.options?.posterImg || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/banner-floor/floor_banner_1.png';
|
||||||
|
});
|
||||||
|
|
||||||
|
const showPoster = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return props.options?.showPoster !== false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rightColumns = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
let baseCols = Number(props.options?.columns) || 4;
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||||
|
if (ratio === '50%') return showPoster.value ? 2 : 3;
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%') return showPoster.value ? 3 : 4;
|
||||||
|
return baseCols;
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultFloorGoods = [
|
||||||
|
{ id: '101', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '102', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '103', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||||
|
{ id: '104', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '105', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||||
|
{ id: '106', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||||
|
{ id: '107', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||||
|
{ id: '108', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayGoods = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||||
|
return props.options.goods;
|
||||||
|
}
|
||||||
|
const count = (rightColumns.value * 2) || 8;
|
||||||
|
return defaultFloorGoods.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '16px 20px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '12px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToCart(item) {
|
||||||
|
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-grid-floor {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.floor-title-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 2px solid #f1f5f9;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.floor-main-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.floor-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #2563eb, #1d4ed8);
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 2px 6px rgba(37, 99, 235, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-title-text {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-subtitle-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-header-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.sub-category-tags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.tag-item {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover, &.active {
|
||||||
|
color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-main-body {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
&.no-poster {
|
||||||
|
.floor-right-goods {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-left-poster {
|
||||||
|
width: 240px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background: #1e293b;
|
||||||
|
|
||||||
|
.poster-link {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 520px;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
.poster-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
filter: brightness(0.85);
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .poster-img {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
padding: 24px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: linear-gradient(180deg, rgba(15, 23, 42, 0.75) 0%, rgba(15, 23, 42, 0.2) 40%, rgba(15, 23, 42, 0.85) 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
.poster-top-badge {
|
||||||
|
align-self: flex-start;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-headline {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-subline {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-pill-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
.pill-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: rgba(255, 255, 255, 0.18);
|
||||||
|
border-radius: 10px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.poster-action-btn {
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-top: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.floor-right-goods {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.floor-goods-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumb-wrap {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.goods-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .goods-img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-detail {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.goods-name-line {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1e293b;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-tag-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.mini-tag {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #dc2626;
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-action-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.price-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orig-amount {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy-action-btn {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
buyer/src/components/indexDecorate/components/HotZone.vue
Normal file
116
buyer/src/components/indexDecorate/components/HotZone.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-hot-zone" :style="customStyle">
|
||||||
|
<div class="hot-zone-wrap" v-if="displayImageUrl">
|
||||||
|
<img :src="displayImageUrl" alt="hotzone" class="base-image" />
|
||||||
|
<div
|
||||||
|
v-for="(zone, idx) in displayZones"
|
||||||
|
:key="idx"
|
||||||
|
class="zone-link"
|
||||||
|
:style="{
|
||||||
|
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)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultZones = [
|
||||||
|
{ title: '大促主会场 · 领券中心', url: '/couponCenter', left: 3.5, top: 12, width: 28.5, height: 76 },
|
||||||
|
{ title: '数码潮电馆 · 爆款直降', url: '/goodsList?category=1', left: 35.5, top: 12, width: 28.5, height: 76 },
|
||||||
|
{ title: '美妆奢品馆 · 5折特惠', url: '/goodsList?category=2', left: 67.5, top: 12, width: 29, height: 76 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayImageUrl = computed(() => {
|
||||||
|
return props.options?.imageUrl || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/hot-zone/hotzone_default.png';
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayZones = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.zones) && props.options.zones.length > 0) {
|
||||||
|
return props.options.zones;
|
||||||
|
}
|
||||||
|
return defaultZones;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '10px 0',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px',
|
||||||
|
overflow: 'hidden'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClickZone(zone) {
|
||||||
|
const url = (zone?.url || zone?.link) ? String(zone.url || zone.link).trim() : '';
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-hot-zone {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.hot-zone-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.base-image {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-link {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2;
|
||||||
|
transition: background 0.2s, box-shadow 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
336
buyer/src/components/indexDecorate/components/NoticeBar.vue
Normal file
336
buyer/src/components/indexDecorate/components/NoticeBar.vue
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="decor-notice-bar"
|
||||||
|
:class="[
|
||||||
|
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||||
|
]"
|
||||||
|
:style="customStyle"
|
||||||
|
@mouseenter="stopTimer"
|
||||||
|
@mouseleave="startTimer"
|
||||||
|
>
|
||||||
|
<!-- 左侧快讯徽章 -->
|
||||||
|
<div class="notice-badge-box">
|
||||||
|
<img
|
||||||
|
v-if="options.badgeImg || options.icon"
|
||||||
|
:src="options.badgeImg || options.icon"
|
||||||
|
class="badge-custom-img"
|
||||||
|
alt="icon"
|
||||||
|
/>
|
||||||
|
<div v-else class="badge-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
|
||||||
|
<path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 12h-2v-2h2v2zm0-4h-2V6h2v4z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<span class="badge-title">{{ options.title || '公告快讯' }}</span>
|
||||||
|
<span class="badge-divider"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间动态滚动快讯区 (Vertical Carousel) -->
|
||||||
|
<div class="notice-content-viewport">
|
||||||
|
<div
|
||||||
|
class="notice-items-strip"
|
||||||
|
:style="{ transform: `translateY(-${currentIndex * 40}px)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in noticeList"
|
||||||
|
:key="idx"
|
||||||
|
class="notice-strip-item"
|
||||||
|
>
|
||||||
|
<span class="notice-item-tag" :class="getTagClass(item.tag)">
|
||||||
|
{{ item.tag || '最新' }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="javascript:;"
|
||||||
|
class="notice-item-link"
|
||||||
|
@click="handleNavigate(item.url || item.href || '/article')"
|
||||||
|
>
|
||||||
|
{{ item.title || item.text }}
|
||||||
|
</a>
|
||||||
|
<span class="notice-item-time" v-if="item.time && !isMini && !isCompact">
|
||||||
|
{{ item.time }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧更多链接 -->
|
||||||
|
<div class="notice-more-action" v-if="options.showMore !== false && !isMini">
|
||||||
|
<a
|
||||||
|
href="javascript:;"
|
||||||
|
class="more-link"
|
||||||
|
@click="handleNavigate(options.moreUrl || '/article')"
|
||||||
|
>
|
||||||
|
{{ options.moreText || '更多' }} >
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNavigate(url) {
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMini = computed(() => {
|
||||||
|
const r = props.componentStyle?.widthRatio;
|
||||||
|
return r === '16.67%' || r === '16.6%' || r === '16.7%';
|
||||||
|
});
|
||||||
|
|
||||||
|
const isCompact = computed(() => {
|
||||||
|
const r = props.componentStyle?.widthRatio;
|
||||||
|
return r === '33.33%' || r === '33.3%';
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultItems = [
|
||||||
|
{ tag: '最新', title: '商城秋季大促重磅开启,全场满300立减50元!', time: '10-28', url: '/article' },
|
||||||
|
{ tag: '活动', title: '会员专享积分商城全新上线,精选好礼0元兑换', time: '10-27', url: '/article' },
|
||||||
|
{ tag: '通知', title: '关于商城系统例行优化维护及物流发货保障通知', time: '10-26', url: '/article' },
|
||||||
|
{ tag: '热议', title: '品质生活馆场景专区火热来袭,领券最高减100', time: '10-25', url: '/article' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const noticeList = computed(() => {
|
||||||
|
const list = props.options?.items || props.options?.notices;
|
||||||
|
if (Array.isArray(list) && list.length > 0) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
return defaultItems;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentIndex = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
if (noticeList.value.length <= 1) return;
|
||||||
|
timer = setInterval(() => {
|
||||||
|
currentIndex.value = (currentIndex.value + 1) % noticeList.value.length;
|
||||||
|
}, 3500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopTimer() {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
startTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopTimer();
|
||||||
|
});
|
||||||
|
|
||||||
|
function getTagClass(tag) {
|
||||||
|
if (tag === '活动') return 'tag-promo';
|
||||||
|
if (tag === '通知') return 'tag-notice';
|
||||||
|
if (tag === '热议') return 'tag-hot';
|
||||||
|
return 'tag-new';
|
||||||
|
}
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '0 16px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-notice-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.02);
|
||||||
|
gap: 12px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.is-mini {
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px !important;
|
||||||
|
|
||||||
|
.notice-badge-box {
|
||||||
|
.badge-title, .badge-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-compact {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 10px !important;
|
||||||
|
|
||||||
|
.notice-badge-box {
|
||||||
|
.badge-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-badge-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
color: #f97316;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-custom-img {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 14px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-content-viewport {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.notice-items-strip {
|
||||||
|
transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.notice-strip-item {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.notice-item-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.tag-new {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-promo {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-notice {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-hot {
|
||||||
|
background: #fdf2f8;
|
||||||
|
color: #db2777;
|
||||||
|
border: 1px solid #fbcfe8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item-link {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-item-time {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-more-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
154
buyer/src/components/indexDecorate/components/PromoGroup.vue
Normal file
154
buyer/src/components/indexDecorate/components/PromoGroup.vue
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-promo-group" :style="customStyle">
|
||||||
|
<div
|
||||||
|
class="promo-grid"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${actualColumns}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(card, idx) in cardsList"
|
||||||
|
:key="idx"
|
||||||
|
class="promo-card"
|
||||||
|
:style="card.bgImg ? { backgroundImage: `url(${card.bgImg})` } : {}"
|
||||||
|
@click="handleClickCard(card)"
|
||||||
|
>
|
||||||
|
<div class="card-inner">
|
||||||
|
<h3 class="card-title">{{ card.title }}</h3>
|
||||||
|
<p class="card-sub">{{ card.subtitle }}</p>
|
||||||
|
<span class="action-btn">立即查看 ></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultCards = [
|
||||||
|
{ title: '限时秒杀', subtitle: '抢券立减50元 · 爆款直降', bgImg: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/promo-group/promo_1.png', url: '/goodsList?seckill=1' },
|
||||||
|
{ title: '品牌特惠', subtitle: '大牌买一送一 · 领百元神券', bgImg: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/promo-group/promo_2.png', url: '/goodsList?brand=1' },
|
||||||
|
{ title: '新品首发', subtitle: '首单专享特惠 · 1元起尝鲜', bgImg: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/promo-group/promo_3.png', url: '/goodsList?new=1' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const cardsList = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.cards) && props.options.cards.length > 0) {
|
||||||
|
return props.options.cards;
|
||||||
|
}
|
||||||
|
return defaultCards;
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualColumns = computed(() => {
|
||||||
|
const baseCols = Number(props.options?.columns) || 3;
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%' || ratio === '16.7%') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ratio === '50%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%' || ratio === '66.7%') {
|
||||||
|
return Math.min(baseCols, 2);
|
||||||
|
}
|
||||||
|
return baseCols;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || 'transparent',
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '10px 0'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleClickCard(card) {
|
||||||
|
const url = card?.url ? String(card.url).trim() : '';
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-promo-group {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.promo-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.promo-card {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 24px;
|
||||||
|
background-color: #1e293b;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-inner {
|
||||||
|
.card-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffffff;
|
||||||
|
margin: 0;
|
||||||
|
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-sub {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin: 6px 0 14px 0;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
688
buyer/src/components/indexDecorate/components/RecommendFeed.vue
Normal file
688
buyer/src/components/indexDecorate/components/RecommendFeed.vue
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-recommend-feed" :style="customStyle">
|
||||||
|
<!-- 1. 推荐流居中标题栏 -->
|
||||||
|
<div class="recommend-feed-header">
|
||||||
|
<div class="header-decor-line left"></div>
|
||||||
|
<div class="header-center-title">
|
||||||
|
<svg class="heart-icon" viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
||||||
|
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
|
||||||
|
</svg>
|
||||||
|
<h3 class="main-title">{{ options.title || '猜你喜欢' }}</h3>
|
||||||
|
<span class="sub-title" v-if="options.subtitle">{{ options.subtitle }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-decor-line right"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 推荐流布局:精选大卡+网格 (featured) 或 标准网格 (grid) -->
|
||||||
|
<!-- A. 精选大卡+网格 (featured 布局) -->
|
||||||
|
<div
|
||||||
|
v-if="(options.layoutStyle || 'featured') === 'featured'"
|
||||||
|
class="recommend-featured-layout"
|
||||||
|
>
|
||||||
|
<!-- 左侧精选特大卡片 (Featured Hero Card) -->
|
||||||
|
<div class="featured-big-card" v-if="featuredGoods">
|
||||||
|
<div class="big-card-badge">为你特别甄选</div>
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: featuredGoods.id, goodsId: featuredGoods.goodsId || featuredGoods.id } }" class="big-card-thumb">
|
||||||
|
<img :src="featuredGoods.thumbnail || defaultThumb" :alt="featuredGoods.goodsName" />
|
||||||
|
</router-link>
|
||||||
|
<div class="big-card-info">
|
||||||
|
<h4 class="big-title">
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: featuredGoods.id, goodsId: featuredGoods.goodsId || featuredGoods.id } }">
|
||||||
|
{{ featuredGoods.goodsName }}
|
||||||
|
</router-link>
|
||||||
|
</h4>
|
||||||
|
<div class="big-reason">官方甄选 · 猜你对这款高人气商品感兴趣</div>
|
||||||
|
|
||||||
|
<!-- 核心特色标签 -->
|
||||||
|
<div class="big-tags-row">
|
||||||
|
<span class="feat-tag highlight">🔥 镇店爆品</span>
|
||||||
|
<span class="feat-tag">🛡️ 官方正品</span>
|
||||||
|
<span class="feat-tag">⚡ 极速发货</span>
|
||||||
|
<span class="feat-tag">🏷️ 售后无忧</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 权威热度指标 -->
|
||||||
|
<div class="big-stats-bar">
|
||||||
|
<div class="stat-cell">
|
||||||
|
<span class="stat-val">100%</span>
|
||||||
|
<span class="stat-lbl">品质正品</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-line"></div>
|
||||||
|
<div class="stat-cell">
|
||||||
|
<span class="stat-val">极速达</span>
|
||||||
|
<span class="stat-lbl">顺丰速运</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-line"></div>
|
||||||
|
<div class="stat-cell">
|
||||||
|
<span class="stat-val">TOP 1</span>
|
||||||
|
<span class="stat-lbl">品类热榜</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务保障权益 -->
|
||||||
|
<div class="big-guarantees">
|
||||||
|
<div class="guar-item"><span class="guar-icon">✓</span> 7天无理由退换货</div>
|
||||||
|
<div class="guar-item"><span class="guar-icon">✓</span> 赠运费险 · 破损包赔</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="big-price-row">
|
||||||
|
<div class="big-price">
|
||||||
|
<span class="sym">¥</span>
|
||||||
|
<span class="val">{{ featuredGoods.price }}</span>
|
||||||
|
<span class="del" v-if="featuredGoods.originalPrice">¥{{ featuredGoods.originalPrice }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="big-buy-btn" @click="addToCart(featuredGoods)">立即入手 ></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧与后续标准卡片网格 -->
|
||||||
|
<div class="featured-sub-grid">
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in regularGoods"
|
||||||
|
:key="item.id || idx"
|
||||||
|
class="recommend-card"
|
||||||
|
>
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }" class="card-thumb">
|
||||||
|
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
|
||||||
|
<span class="rec-tag">推荐</span>
|
||||||
|
</router-link>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="goods-title">
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }">{{ item.goodsName }}</router-link>
|
||||||
|
</h4>
|
||||||
|
<div class="recommend-meta">
|
||||||
|
<span class="reason-tag">同品类热度榜</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-row">
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="sym">¥</span>
|
||||||
|
<span class="val">{{ item.price }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="cart-pill-btn" @click="addToCart(item)">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- B. 标准网格流 (grid 布局) -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="recommend-standard-grid"
|
||||||
|
:class="`cols-${actualCols}`"
|
||||||
|
:style="{ gridTemplateColumns: `repeat(${actualCols}, 1fr)` }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, idx) in displayGoods"
|
||||||
|
:key="item.id || idx"
|
||||||
|
class="recommend-card"
|
||||||
|
>
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }" class="card-thumb">
|
||||||
|
<img :src="item.thumbnail || defaultThumb" :alt="item.goodsName" />
|
||||||
|
<span class="rec-tag">为你推荐</span>
|
||||||
|
</router-link>
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="goods-title">
|
||||||
|
<router-link :to="{ path: '/goodsDetail', query: { skuId: item.id, goodsId: item.goodsId || item.id } }">{{ item.goodsName }}</router-link>
|
||||||
|
</h4>
|
||||||
|
<div class="recommend-meta">
|
||||||
|
<span class="reason-tag">甄选推荐</span>
|
||||||
|
</div>
|
||||||
|
<div class="price-row">
|
||||||
|
<div class="price-box">
|
||||||
|
<span class="sym">¥</span>
|
||||||
|
<span class="val">{{ item.price }}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="cart-pill-btn" @click="addToCart(item)">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部换一批 / 探索更多推荐 -->
|
||||||
|
<div class="recommend-footer-action">
|
||||||
|
<button type="button" class="refresh-rec-btn" @click="handleRefresh">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
|
||||||
|
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||||
|
</svg>
|
||||||
|
<span>换一批推荐好物</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
component: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultThumb = 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png';
|
||||||
|
|
||||||
|
const defaultPool = [
|
||||||
|
{ id: '1', goodsName: '旗舰 5G 智能手机 12GB+256GB 超光影影像系统', price: '3999.00', originalPrice: '4599.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' },
|
||||||
|
{ id: '2', goodsName: '无线头戴式深度主动降噪蓝牙耳机 空间音频Hi-Res', price: '1099.00', originalPrice: '1499.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_2.png' },
|
||||||
|
{ id: '3', goodsName: '14英寸 全面屏轻薄商务笔记本电脑 酷睿i7 高刷防眩光', price: '5699.00', originalPrice: '6299.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_3.png' },
|
||||||
|
{ id: '4', goodsName: '运动健康专业监测智能手表 心率血氧长续航50米防水', price: '899.00', originalPrice: '1199.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_4.png' },
|
||||||
|
{ id: '5', goodsName: '奢宠酵母焕活修护精华露 50ml 紧致淡纹抗皱滋养', price: '680.00', originalPrice: '890.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_5.png' },
|
||||||
|
{ id: '6', goodsName: '意式半自动家用复古咖啡机 20Bar高压恒温萃取蒸汽奶泡', price: '899.00', originalPrice: '1280.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_6.png' },
|
||||||
|
{ id: '7', goodsName: '碳板专业竞速轻量缓震跑步鞋 透气防滑男女同款潮鞋', price: '459.00', originalPrice: '699.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_7.png' },
|
||||||
|
{ id: '8', goodsName: '极简静音无雾负离子香薰加湿器 大容量客厅卧室通用', price: '299.00', originalPrice: '399.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_8.png' },
|
||||||
|
{ id: '9', goodsName: '智能降噪AI翻译蓝牙墨镜 超轻防紫外线偏光太阳镜', price: '599.00', originalPrice: '799.00', thumbnail: 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/goods/goods_1.png' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayGoods = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.goods) && props.options.goods.length > 0) {
|
||||||
|
return props.options.goods;
|
||||||
|
}
|
||||||
|
const count = props.options?.pageSize || 8;
|
||||||
|
return defaultPool.slice(0, count);
|
||||||
|
});
|
||||||
|
|
||||||
|
const featuredGoods = computed(() => {
|
||||||
|
return displayGoods.value[0] || defaultPool[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const regularGoods = computed(() => {
|
||||||
|
return displayGoods.value.slice(1, 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
const actualCols = computed(() => {
|
||||||
|
const ratio = props.componentStyle?.widthRatio || '100%';
|
||||||
|
if (ratio === '16.67%' || ratio === '16.6%') return 1;
|
||||||
|
if (ratio === '33.33%' || ratio === '33.3%') return 2;
|
||||||
|
if (ratio === '50%') return 3;
|
||||||
|
if (ratio === '66.67%' || ratio === '66.6%') return 4;
|
||||||
|
return 5;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
const bg = props.options?.backgroundColor || s.backgroundColor || '#f8fafc';
|
||||||
|
return {
|
||||||
|
backgroundColor: bg,
|
||||||
|
padding: Array.isArray(s.padding)
|
||||||
|
? `${s.padding[0]}px ${s.padding[1]}px ${s.padding[2]}px ${s.padding[3]}px`
|
||||||
|
: '24px 20px',
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '12px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function addToCart(item) {
|
||||||
|
ElMessage.success(`已将 "${item.goodsName?.slice(0, 10)}..." 加入购物车`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRefresh() {
|
||||||
|
ElMessage.info('已刷新为您推荐的专属商品');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-recommend-feed {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.recommend-feed-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.header-decor-line {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 120px;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, #cbd5e1);
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
background: linear-gradient(90deg, #cbd5e1, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-center-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.heart-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-featured-layout {
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.featured-big-card {
|
||||||
|
width: 320px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-card-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
background: linear-gradient(135deg, #ef4444, #f97316);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-card-thumb {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 85%;
|
||||||
|
height: 85%;
|
||||||
|
object-fit: contain;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-card-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.big-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0f172a;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-reason {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #f97316;
|
||||||
|
background: #fff7ed;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-tags-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.feat-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
background: #f1f5f9;
|
||||||
|
padding: 2px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
color: #dc2626;
|
||||||
|
background: #fee2e2;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-stats-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.stat-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
.stat-val {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-lbl {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-line {
|
||||||
|
width: 1px;
|
||||||
|
height: 20px;
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-guarantees {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #fbfbfb;
|
||||||
|
border-left: 3px solid #ef4444;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.guar-item {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.guar-icon {
|
||||||
|
color: #16a34a;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-price-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.big-price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.sym {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.del {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-buy-btn {
|
||||||
|
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-sub-grid {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-standard-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #f1f5f9;
|
||||||
|
transition: all 0.25s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-thumb img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-pill-btn {
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumb {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rec-tag {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
background: #ef4444;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.goods-title {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #1e293b;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-meta {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #f97316;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
|
||||||
|
.reason-tag {
|
||||||
|
background: #fff7ed;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sales-num {
|
||||||
|
color: #94a3b8;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-num {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.sym {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-pill-btn {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recommend-footer-action {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.refresh-rec-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 24px;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #ef4444;
|
||||||
|
color: #ef4444;
|
||||||
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
421
buyer/src/components/indexDecorate/components/SearchHeader.vue
Normal file
421
buyer/src/components/indexDecorate/components/SearchHeader.vue
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-search-header" :style="customStyle">
|
||||||
|
<div class="header-inner">
|
||||||
|
<!-- 1. 商城 Logo 区域 -->
|
||||||
|
<div class="logo-area">
|
||||||
|
<router-link to="/">
|
||||||
|
<img
|
||||||
|
v-if="logoSrc"
|
||||||
|
:src="logoSrc"
|
||||||
|
alt="Logo"
|
||||||
|
class="site-logo"
|
||||||
|
/>
|
||||||
|
<span v-else class="text-logo">LiliShop</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 搜索核心区(紧凑居中,含商品/店铺 Tab、搜索框和热门推荐词) -->
|
||||||
|
<div class="search-main">
|
||||||
|
<!-- 搜索类型切换:商品 / 店铺 -->
|
||||||
|
<div class="search-tabs" v-if="options.showSearchTypes !== false">
|
||||||
|
<span
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentSearchType === 'goods' }"
|
||||||
|
@click="switchSearchType('goods')"
|
||||||
|
>
|
||||||
|
商品
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: currentSearchType === 'store' }"
|
||||||
|
@click="switchSearchType('store')"
|
||||||
|
>
|
||||||
|
店铺
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索输入框与按钮 -->
|
||||||
|
<div class="search-box" :class="{ 'tab-store-active': currentSearchType === 'store' }">
|
||||||
|
<input
|
||||||
|
v-model="keyword"
|
||||||
|
type="text"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="placeholderText"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<button type="button" class="search-btn" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
<span>搜索</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 热门推荐词 -->
|
||||||
|
<div class="hot-words" v-if="options.showHotWords !== false && currentSearchType === 'goods' && hotWordsList.length">
|
||||||
|
<span
|
||||||
|
v-for="(word, idx) in hotWordsList"
|
||||||
|
:key="idx"
|
||||||
|
class="hot-w"
|
||||||
|
@click="quickSearch(word)"
|
||||||
|
>
|
||||||
|
{{ word }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 购物车入口(紧凑跟随在搜索框右侧,带数量徽标) -->
|
||||||
|
<div class="cart-entry" v-if="options.showCart !== false">
|
||||||
|
<router-link to="/cart" class="cart-btn" :style="cartBtnStyle">
|
||||||
|
<el-icon class="cart-icon"><ShoppingCart /></el-icon>
|
||||||
|
<span class="cart-text">{{ options.cartText || '购物车' }}</span>
|
||||||
|
<span class="cart-count">{{ cartCount }}</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import { Search, ShoppingCart } from '@element-plus/icons-vue';
|
||||||
|
import { useAppStore } from '@/stores/app.js';
|
||||||
|
import { cartCount as fetchCartCount } from '@/api/cart.js';
|
||||||
|
import storage from '@/plugins/storage.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['search']);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
|
const currentSearchType = ref(
|
||||||
|
route.path === '/storeList'
|
||||||
|
? 'store'
|
||||||
|
: (props.options?.defaultSearchType || 'goods')
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(p) => {
|
||||||
|
if (p === '/storeList') {
|
||||||
|
currentSearchType.value = 'store';
|
||||||
|
} else if (p === '/goodsList') {
|
||||||
|
currentSearchType.value = 'goods';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function switchSearchType(type) {
|
||||||
|
currentSearchType.value = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyword = ref(route.query?.keyword || '');
|
||||||
|
|
||||||
|
watch(() => route.query?.keyword, (v) => {
|
||||||
|
keyword.value = v || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const logoSrc = computed(() => {
|
||||||
|
return props.options?.logoUrl || appStore.logoImg || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const placeholderText = computed(() => {
|
||||||
|
if (currentSearchType.value === 'store') {
|
||||||
|
const sph = props.options?.storePlaceholder;
|
||||||
|
if (!sph || sph === '请输入店铺名称、品牌或经营范围') return '请输入店铺名称';
|
||||||
|
return sph;
|
||||||
|
}
|
||||||
|
const ph = props.options?.placeholder;
|
||||||
|
if (!ph || ph === '搜索品牌或商品' || ph === '请输入商品名称、品牌或型号') {
|
||||||
|
return '请输入商品名称';
|
||||||
|
}
|
||||||
|
return ph;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hotWordsList = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.hotWords) && props.options.hotWords.length) {
|
||||||
|
return props.options.hotWords;
|
||||||
|
}
|
||||||
|
return ['iPhone 16 Pro', '小棕瓶', '戴森吹风机', '华为Mate 70', '五粮液', '跑鞋'];
|
||||||
|
});
|
||||||
|
|
||||||
|
const cartCount = computed(() => {
|
||||||
|
return appStore.cartNum || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
const kw = (keyword.value || '').trim();
|
||||||
|
emit('search', kw);
|
||||||
|
if (currentSearchType.value === 'store') {
|
||||||
|
if (route.path === '/storeList') {
|
||||||
|
const nextQuery = { ...route.query };
|
||||||
|
if (kw) {
|
||||||
|
nextQuery.keyword = kw;
|
||||||
|
} else {
|
||||||
|
delete nextQuery.keyword;
|
||||||
|
}
|
||||||
|
router.push({ path: '/storeList', query: nextQuery });
|
||||||
|
} else {
|
||||||
|
router.push({ path: '/storeList', query: kw ? { keyword: kw } : {} });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (route.path === '/goodsList') {
|
||||||
|
const nextQuery = { ...route.query };
|
||||||
|
if (kw) {
|
||||||
|
nextQuery.keyword = kw;
|
||||||
|
} else {
|
||||||
|
delete nextQuery.keyword;
|
||||||
|
}
|
||||||
|
router.push({ path: '/goodsList', query: nextQuery });
|
||||||
|
} else {
|
||||||
|
router.push({ path: '/goodsList', query: kw ? { keyword: kw } : {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickSearch(word) {
|
||||||
|
keyword.value = word;
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCartList() {
|
||||||
|
if (storage.getItem('userInfo')) {
|
||||||
|
fetchCartCount().then((res) => {
|
||||||
|
appStore.setCartNum(res.result);
|
||||||
|
storage.setItem('cartNum', res.result);
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getCartList();
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#ffffff',
|
||||||
|
minHeight: `${s.height || 120}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const cartBtnStyle = computed(() => {
|
||||||
|
const o = props.options || {};
|
||||||
|
return {
|
||||||
|
backgroundColor: o.cartBgColor || '#ffffff',
|
||||||
|
borderColor: o.cartBorderColor || '#e2e8f0',
|
||||||
|
color: o.cartTextColor || '#334155'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-search-header {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #ffffff;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
|
||||||
|
.header-inner {
|
||||||
|
width: 1200px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 16px 0 14px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.logo-area {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-logo {
|
||||||
|
max-height: 40px;
|
||||||
|
max-width: 180px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-logo {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #409EFF;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-main {
|
||||||
|
width: 580px;
|
||||||
|
max-width: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.search-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
padding-left: 2px;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
padding: 4px 16px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #ffffff;
|
||||||
|
background: #409EFF;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
border: 2px solid #409EFF;
|
||||||
|
border-radius: 0 4px 4px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.tab-store-active {
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
padding: 0 14px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
color: #334155;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-btn {
|
||||||
|
background: #409EFF;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
padding: 0 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hot-words {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
.hot-w {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-entry {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.cart-btn {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 18px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #334155;
|
||||||
|
background: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.cart-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-text {
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart-count {
|
||||||
|
background: #409EFF;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #409EFF;
|
||||||
|
color: #409EFF;
|
||||||
|
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
204
buyer/src/components/indexDecorate/components/TitleBar.vue
Normal file
204
buyer/src/components/indexDecorate/components/TitleBar.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="decor-title-bar"
|
||||||
|
:class="[
|
||||||
|
`align-${options.align || 'left'}`,
|
||||||
|
isMini ? 'is-mini' : (isCompact ? 'is-compact' : '')
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="title-wrap" :class="{ 'with-icon': options.titleImg || options.icon }">
|
||||||
|
<img
|
||||||
|
v-if="options.titleImg || options.icon"
|
||||||
|
:src="options.titleImg || options.icon"
|
||||||
|
class="title-custom-icon"
|
||||||
|
alt="icon"
|
||||||
|
/>
|
||||||
|
<div class="title-text-group">
|
||||||
|
<h2
|
||||||
|
class="main-title"
|
||||||
|
:class="{ 'clickable': Boolean(options.titleUrl) }"
|
||||||
|
@click="options.titleUrl && handleNavigate(options.titleUrl)"
|
||||||
|
>
|
||||||
|
{{ options.mainTitle || '楼层标题' }}
|
||||||
|
</h2>
|
||||||
|
<p class="subtitle" v-if="options.subtitle">{{ options.subtitle }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="javascript:;"
|
||||||
|
class="more-link"
|
||||||
|
v-if="options.showMore"
|
||||||
|
@click="handleNavigate(options.moreUrl || '/goods')"
|
||||||
|
>
|
||||||
|
{{ options.moreText || '查看更多 >' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNavigate(url) {
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMini = computed(() => props.componentStyle?.widthRatio === '16.67%');
|
||||||
|
const isCompact = computed(() => props.componentStyle?.widthRatio === '33.33%');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-title-bar {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
box-sizing: border-box;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
&.align-center {
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.title-custom-icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
object-fit: contain;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text-group {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
word-break: break-word;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
&:hover {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #94a3b8;
|
||||||
|
margin: 6px 0 0 0;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #e11d48;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-compact {
|
||||||
|
.title-wrap {
|
||||||
|
.title-custom-icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text-group {
|
||||||
|
.main-title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.more-link {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-mini {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
&.align-center {
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
position: static;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
.title-custom-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-text-group {
|
||||||
|
.main-title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-link {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
181
buyer/src/components/indexDecorate/components/TopBar.vue
Normal file
181
buyer/src/components/indexDecorate/components/TopBar.vue
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-top-bar" :style="customStyle">
|
||||||
|
<div class="top-bar-inner">
|
||||||
|
<div class="bar-left">
|
||||||
|
<span class="welcome-txt">{{ options.welcomeText || '欢迎来到商城' }}</span>
|
||||||
|
<span class="auth-links" v-if="!username">
|
||||||
|
<router-link :to="options.loginUrl || '/login'" class="auth-item">
|
||||||
|
[{{ options.loginText || '登录' }}]
|
||||||
|
</router-link>
|
||||||
|
<router-link :to="options.registerUrl || '/signUp'" class="auth-item">
|
||||||
|
[{{ options.registerText || '免费注册' }}]
|
||||||
|
</router-link>
|
||||||
|
</span>
|
||||||
|
<span class="auth-links user-logged" v-else>
|
||||||
|
<router-link :to="options.userCenterUrl || '/home/MyOrder'" class="user-name">
|
||||||
|
欢迎,{{ username }}
|
||||||
|
</router-link>
|
||||||
|
<span class="sep">|</span>
|
||||||
|
<a class="auth-item" @click="handleLogout">{{ options.logoutText || '退出' }}</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bar-right">
|
||||||
|
<router-link
|
||||||
|
v-for="(menu, idx) in (options.rightMenus || defaultMenus)"
|
||||||
|
:key="idx"
|
||||||
|
:to="menu.url || '/'"
|
||||||
|
class="bar-link"
|
||||||
|
>
|
||||||
|
{{ menu.title }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultMenus = [
|
||||||
|
{ title: '基本信息', url: '/home/UserInfo' },
|
||||||
|
{ title: '我的订单', url: '/home/MyOrder' },
|
||||||
|
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||||
|
{ title: '消息', url: '/home/Msg' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const username = computed(() => {
|
||||||
|
const userInfo = Cookies.get('userInfo');
|
||||||
|
if (userInfo) {
|
||||||
|
try {
|
||||||
|
const u = JSON.parse(userInfo);
|
||||||
|
return u.nickName || u.username || '';
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
Cookies.remove('userInfo');
|
||||||
|
Cookies.remove('accessToken');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
const opt = props.options || {};
|
||||||
|
const textColor = opt.textColor || s.textColor || '#4b5563';
|
||||||
|
const fontSize = opt.fontSize ? `${opt.fontSize}px` : '12px';
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: s.backgroundColor || '#f8fafc',
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize,
|
||||||
|
height: `${s.height || 40}px`,
|
||||||
|
lineHeight: `${s.height || 40}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-top-bar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-inner {
|
||||||
|
width: 1200px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.bar-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.welcome-txt {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-links {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.auth-item {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sep {
|
||||||
|
margin: 0 4px;
|
||||||
|
color: #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
color: #333333;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.bar-link {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
330
buyer/src/components/indexDecorate/components/UserBenefits.vue
Normal file
330
buyer/src/components/indexDecorate/components/UserBenefits.vue
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
<template>
|
||||||
|
<div class="decor-user-benefits" :style="customStyle">
|
||||||
|
<!-- 1. 用户头像与信息区 -->
|
||||||
|
<div class="user-main-info">
|
||||||
|
<div class="avatar-box">
|
||||||
|
<img v-if="options.avatar" :src="options.avatar" alt="avatar" />
|
||||||
|
<div v-else class="avatar-placeholder">
|
||||||
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
||||||
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-greeting">
|
||||||
|
<div class="badge-tag">{{ options.badgeText || '请登录' }}</div>
|
||||||
|
<div class="welcome-text" :title="options.welcomeText || '欢迎来到商城'">
|
||||||
|
{{ options.welcomeText || '欢迎来到商城' }}
|
||||||
|
</div>
|
||||||
|
<div class="sub-text" :title="options.subtitle || '登录后查看专属权益'">
|
||||||
|
{{ options.subtitle || '登录后查看专属权益' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 2. 登录 / 注册按钮组 -->
|
||||||
|
<div class="action-btn-row">
|
||||||
|
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
|
||||||
|
{{ options.loginText || '登录' }}
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="action-btn register-btn" @click="handleNavigate(options.registerUrl || '/signUp')">
|
||||||
|
{{ options.registerText || '注册' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||||
|
<div class="metrics-cards-row">
|
||||||
|
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
|
||||||
|
<div class="metric-val">0</div>
|
||||||
|
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(options.couponUrl || '/member/coupon')">
|
||||||
|
<div class="metric-val">0</div>
|
||||||
|
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
||||||
|
</a>
|
||||||
|
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(options.orderUrl || '/member/order')">
|
||||||
|
<div class="metric-val">0</div>
|
||||||
|
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
||||||
|
<div class="quick-service-row">
|
||||||
|
<a
|
||||||
|
v-for="(srv, sIdx) in displayServices"
|
||||||
|
:key="sIdx"
|
||||||
|
href="javascript:;"
|
||||||
|
class="service-item"
|
||||||
|
@click="handleNavigate(srv.url)"
|
||||||
|
>
|
||||||
|
<el-icon class="service-icon"><component :is="getServiceIcon(srv.icon)" /></el-icon>
|
||||||
|
<span class="service-name">{{ srv.name }}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
rowStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNavigate(url) {
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultServices = [
|
||||||
|
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
||||||
|
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
||||||
|
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
||||||
|
{ name: '客服', url: '/article', icon: 'Service' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
Star,
|
||||||
|
Clock,
|
||||||
|
Present,
|
||||||
|
Service
|
||||||
|
};
|
||||||
|
|
||||||
|
function getServiceIcon(iconName) {
|
||||||
|
return iconMap[iconName] || Service;
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayServices = computed(() => {
|
||||||
|
if (Array.isArray(props.options?.services) && props.options.services.length > 0) {
|
||||||
|
return props.options.services;
|
||||||
|
}
|
||||||
|
return defaultServices;
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||||
|
return {
|
||||||
|
backgroundColor: (s.backgroundColor && s.backgroundColor !== 'transparent') ? s.backgroundColor : '#ffffff',
|
||||||
|
borderRadius: Array.isArray(s.radius) && s.radius.some(r => r > 0)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px',
|
||||||
|
height: '100%',
|
||||||
|
minHeight: hasRowHeight ? '0' : '420px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-user-benefits {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.03);
|
||||||
|
padding: 24px 16px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.user-main-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.avatar-box {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #cbd5e1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-greeting {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.badge-tag {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: #ecf5ff;
|
||||||
|
color: #409EFF;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 8px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #94a3b8;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
|
||||||
|
&.login-btn {
|
||||||
|
background: #409EFF;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.register-btn {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #409EFF;
|
||||||
|
border: 1px solid #409EFF;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-cards-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding: 12px 0;
|
||||||
|
margin-top: 6px;
|
||||||
|
background: #f8fafc;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.metric-val {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #409EFF;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-service-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px dashed #f1f5f9;
|
||||||
|
margin-top: 6px;
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.15s ease;
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
213
buyer/src/components/indexDecorate/components/VideoPlayer.vue
Normal file
213
buyer/src/components/indexDecorate/components/VideoPlayer.vue
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="decor-video-player"
|
||||||
|
:class="{ 'has-link': !!options.url }"
|
||||||
|
:style="customStyle"
|
||||||
|
@click="handleClickPlaceholder"
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
v-if="options.videoUrl"
|
||||||
|
:src="options.videoUrl"
|
||||||
|
:poster="options.coverUrl"
|
||||||
|
:autoplay="options.autoPlay"
|
||||||
|
:loop="options.loop"
|
||||||
|
:controls="options.controls !== false"
|
||||||
|
:muted="options.autoPlay"
|
||||||
|
:style="{ height: `${options.height || 450}px` }"
|
||||||
|
class="video-element"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="video-placeholder"
|
||||||
|
:style="{
|
||||||
|
height: `${options.height || 450}px`,
|
||||||
|
backgroundImage: `url(${options.coverUrl || 'https://lilishop-pc.oss-cn-beijing.aliyuncs.com/static/video/video_cover.png'})`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="video-play-hint">
|
||||||
|
<span class="play-icon">▶</span>
|
||||||
|
<span class="hint-text">{{ options.title || '官方品牌宣传片' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 跳转行动点徽标(如果配置了链接) -->
|
||||||
|
<div
|
||||||
|
v-if="options.url"
|
||||||
|
class="video-cta-badge"
|
||||||
|
@click.stop="handleNavigate"
|
||||||
|
title="点击查看详情"
|
||||||
|
>
|
||||||
|
<span class="cta-dot"></span>
|
||||||
|
<span class="cta-text">查看活动详情</span>
|
||||||
|
<span class="cta-arrow">></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
options: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
componentStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const customStyle = computed(() => {
|
||||||
|
const s = props.componentStyle || {};
|
||||||
|
return {
|
||||||
|
borderRadius: Array.isArray(s.radius)
|
||||||
|
? `${s.radius[0]}px ${s.radius[1]}px ${s.radius[2]}px ${s.radius[3]}px`
|
||||||
|
: '8px'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNavigate() {
|
||||||
|
const url = props.options?.url ? String(props.options.url).trim() : '';
|
||||||
|
if (!url) return;
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
} else {
|
||||||
|
router.push(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickPlaceholder() {
|
||||||
|
if (!props.options?.videoUrl && props.options?.url) {
|
||||||
|
handleNavigate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.decor-video-player {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&.has-link {
|
||||||
|
.video-placeholder {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-element {
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
background: #0f172a;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.video-play-hint {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
transition: transform 0.2s ease, background 0.2s ease;
|
||||||
|
|
||||||
|
.play-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background: #e11d48;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 20px;
|
||||||
|
padding-left: 3px;
|
||||||
|
box-shadow: 0 4px 14px rgba(225, 29, 72, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .video-play-hint {
|
||||||
|
transform: scale(1.04);
|
||||||
|
background: rgba(15, 23, 42, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-cta-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 14px;
|
||||||
|
background: rgba(15, 23, 42, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 20px;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.cta-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e11d48;
|
||||||
|
box-shadow: 0 0 8px #e11d48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-text {
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-arrow {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(225, 29, 72, 0.9);
|
||||||
|
border-color: rgba(255, 255, 255, 0.4);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 18px rgba(225, 29, 72, 0.35);
|
||||||
|
|
||||||
|
.cta-arrow {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1967
buyer/src/components/indexDecorate/presets/double11Preset.js
Normal file
1967
buyer/src/components/indexDecorate/presets/double11Preset.js
Normal file
File diff suppressed because it is too large
Load Diff
73
buyer/src/components/indexDecorate/registry.js
Normal file
73
buyer/src/components/indexDecorate/registry.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* PC 前台装修楼层组件注册表
|
||||||
|
* 映射 6 大分类、24 个组件
|
||||||
|
*/
|
||||||
|
import TopBar from './components/TopBar.vue';
|
||||||
|
import SearchHeader from './components/SearchHeader.vue';
|
||||||
|
import FooterNav from './components/FooterNav.vue';
|
||||||
|
import CategoryNav from './components/CategoryNav.vue';
|
||||||
|
import ChannelGrid from './components/ChannelGrid.vue';
|
||||||
|
import CategoryMegaMenu from './components/CategoryMegaMenu.vue';
|
||||||
|
import Carousel from './components/Carousel.vue';
|
||||||
|
import BrandWall from './components/BrandWall.vue';
|
||||||
|
import HotZone from './components/HotZone.vue';
|
||||||
|
import FreeLayout from './components/FreeLayout.vue';
|
||||||
|
import PromoGroup from './components/PromoGroup.vue';
|
||||||
|
import VideoPlayer from './components/VideoPlayer.vue';
|
||||||
|
import GoodsList from './components/GoodsList.vue';
|
||||||
|
import GridFloor from './components/GridFloor.vue';
|
||||||
|
import GoodsGrid from './components/GoodsGrid.vue';
|
||||||
|
import ChannelFloor from './components/ChannelFloor.vue';
|
||||||
|
import RecommendFeed from './components/RecommendFeed.vue';
|
||||||
|
import CouponList from './components/CouponList.vue';
|
||||||
|
import UserBenefits from './components/UserBenefits.vue';
|
||||||
|
import TitleBar from './components/TitleBar.vue';
|
||||||
|
import ArticleContent from './components/ArticleContent.vue';
|
||||||
|
import NoticeBar from './components/NoticeBar.vue';
|
||||||
|
|
||||||
|
export const COMPONENT_MAP = {
|
||||||
|
// 1. 结构 (3)
|
||||||
|
topBar: TopBar,
|
||||||
|
searchHeader: SearchHeader,
|
||||||
|
footerNav: FooterNav,
|
||||||
|
|
||||||
|
// 2. 导航 (3)
|
||||||
|
categoryNav: CategoryNav,
|
||||||
|
channelGrid: ChannelGrid,
|
||||||
|
categoryMegaMenu: CategoryMegaMenu,
|
||||||
|
|
||||||
|
// 3. 广告 (6)
|
||||||
|
carousel: Carousel,
|
||||||
|
brandWall: BrandWall,
|
||||||
|
hotZone: HotZone,
|
||||||
|
freeLayout: FreeLayout,
|
||||||
|
promoGroup: PromoGroup,
|
||||||
|
video: VideoPlayer,
|
||||||
|
|
||||||
|
// 4. 商品 (8)
|
||||||
|
goodsList: GoodsList,
|
||||||
|
gridFloor: GridFloor,
|
||||||
|
goodsGrid: GoodsGrid,
|
||||||
|
goodsCard: GoodsList,
|
||||||
|
bannerFloor: GridFloor,
|
||||||
|
goodsBar: GoodsList,
|
||||||
|
channelFloor: ChannelFloor,
|
||||||
|
recommendFeed: RecommendFeed,
|
||||||
|
|
||||||
|
// 5. 营销 (1)
|
||||||
|
couponList: CouponList,
|
||||||
|
userBenefits: UserBenefits,
|
||||||
|
|
||||||
|
// 6. 内容 (3)
|
||||||
|
titleBar: TitleBar,
|
||||||
|
articleContent: ArticleContent,
|
||||||
|
noticeBar: NoticeBar
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据组件 type 解析 Vue 组件
|
||||||
|
*/
|
||||||
|
export function resolveComponent(type) {
|
||||||
|
if (!type) return null;
|
||||||
|
return COMPONENT_MAP[type] || null;
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="selector">
|
<div class="selector">
|
||||||
<div class="selector-item" v-for="(plant, plantIndex) in Object.keys(data)" :key="plantIndex">
|
<div class="selector-item" v-for="(plant, plantIndex) in Object.keys(data)" :key="plantIndex">
|
||||||
<div :class="{ 'active': chiosend[plantIndex].id == item.id }" v-for="(item, index) in data[plant]"
|
<div :class="{ 'active': chiosend[plantIndex].id == item.id }" v-for="(item, index) in data[plant]"
|
||||||
:key="index"
|
:key="index" :title="item.name"
|
||||||
@click="init(item, plantIndex != Object.keys(data).length - 1 ? Object.keys(data)[plantIndex + 1] : 0, plantIndex)"
|
@click="init(item, plantIndex != Object.keys(data).length - 1 ? Object.keys(data)[plantIndex + 1] : 0, plantIndex)"
|
||||||
class="map-item">
|
class="map-item">
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
@@ -125,7 +125,8 @@ function finished() {
|
|||||||
.selector-item {
|
.selector-item {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-item {
|
.map-item {
|
||||||
@@ -133,6 +134,10 @@ function finished() {
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #eee;
|
background: #eee;
|
||||||
|
|||||||
33
buyer/src/composables/useSiteSettings.js
Normal file
33
buyer/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import { getBaseSite } from "@/api/common.js";
|
||||||
|
import config from "@/config";
|
||||||
|
|
||||||
|
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||||
|
const settings = ref(null);
|
||||||
|
let pendingRequest;
|
||||||
|
|
||||||
|
export function loadSiteSettings() {
|
||||||
|
if (!pendingRequest) {
|
||||||
|
pendingRequest = getBaseSite()
|
||||||
|
.then((res) => {
|
||||||
|
if (res.success && res.result?.settingValue) {
|
||||||
|
settings.value = JSON.parse(res.result.settingValue);
|
||||||
|
}
|
||||||
|
return settings.value;
|
||||||
|
})
|
||||||
|
.catch(() => settings.value)
|
||||||
|
.finally(() => { pendingRequest = null; });
|
||||||
|
}
|
||||||
|
return pendingRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSiteSettings() {
|
||||||
|
onMounted(loadSiteSettings);
|
||||||
|
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||||
|
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||||
|
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||||
|
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||||
|
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||||
|
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||||
|
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ export const E_COUPON_ORDER_TYPE = "E_COUPON";
|
|||||||
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
|
||||||
export const E_COUPON_MAX_BUY_NUM = 999;
|
export const E_COUPON_MAX_BUY_NUM = 999;
|
||||||
|
|
||||||
/** 常规立即购买(秒杀/满减/券/限时/第N件等) */
|
/** 常规立即购买(秒杀/满减/券等) */
|
||||||
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
|
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
|
||||||
export const E_COUPON_CART_PINTUAN = "PINTUAN";
|
export const E_COUPON_CART_PINTUAN = "PINTUAN";
|
||||||
export const E_COUPON_CART_POINTS = "POINTS";
|
export const E_COUPON_CART_POINTS = "POINTS";
|
||||||
@@ -72,8 +72,6 @@ export function createEmptyPromotionMap() {
|
|||||||
SECKILL: null,
|
SECKILL: null,
|
||||||
FULL_DISCOUNT: null,
|
FULL_DISCOUNT: null,
|
||||||
COUPON: [],
|
COUPON: [],
|
||||||
DISCOUNT: null,
|
|
||||||
NTH: null,
|
|
||||||
PINTUAN: null,
|
PINTUAN: null,
|
||||||
KANJIA: null,
|
KANJIA: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
>¥{{ item.price }}</span>
|
>¥{{ item.price }}</span>
|
||||||
<span v-if="item.couponType === 'DISCOUNT'"
|
<span v-if="item.couponType === 'DISCOUNT'"
|
||||||
>{{ item.couponDiscount }}折</span>
|
>{{ item.couponDiscount }}折</span>
|
||||||
<span>满{{ item.consumeThreshold }}元可用</span>
|
<span>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
<el-button
|
<el-button
|
||||||
class="coupon-btn"
|
class="coupon-btn"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -118,13 +118,6 @@
|
|||||||
<p>
|
<p>
|
||||||
{{ goods.goodsSku.goodsName }}
|
{{ goods.goodsSku.goodsName }}
|
||||||
</p>
|
</p>
|
||||||
<p><el-tag
|
|
||||||
v-if="goods.goodsSku.salesModel === 'WHOLESALE'"
|
|
||||||
class="goods-show-tag"
|
|
||||||
color="purple"
|
|
||||||
>
|
|
||||||
批发商品
|
|
||||||
</el-tag></p>
|
|
||||||
<template
|
<template
|
||||||
v-for="(promotion, promotionIndex) in goods.promotions"
|
v-for="(promotion, promotionIndex) in goods.promotions"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="coupon-center">
|
<div class="coupon-center">
|
||||||
<BaseHeader></BaseHeader>
|
<BaseHeader></BaseHeader>
|
||||||
|
<SearchHeader></SearchHeader>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h2 class="coupon-page-title">领券中心</h2>
|
<h2 class="coupon-page-title">领券中心</h2>
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ import { onMounted, reactive, ref } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Modal } from "@/utils/message";
|
import { Modal } from "@/utils/message";
|
||||||
import storage from "@/plugins/storage";
|
import storage from "@/plugins/storage";
|
||||||
|
import SearchHeader from "@/components/indexDecorate/components/SearchHeader.vue";
|
||||||
import { couponList, receiveCoupon } from "@/api/member.js";
|
import { couponList, receiveCoupon } from "@/api/member.js";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="goods-detail-page">
|
<div class="goods-detail-page">
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search></Search>
|
<SearchHeader></SearchHeader>
|
||||||
<div class="shop-item-path">
|
<div class="shop-item-path">
|
||||||
<div class="shop-nav-container">
|
<div class="shop-nav-container">
|
||||||
<el-breadcrumb>
|
<el-breadcrumb>
|
||||||
@@ -44,7 +44,8 @@ import { ref } from "vue";
|
|||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import storage from "@/plugins/storage.js";
|
import storage from "@/plugins/storage.js";
|
||||||
import Search from "@/components/Search";
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import ShowGoods from "@/components/goodsDetail/ShowGoods";
|
import ShowGoods from "@/components/goodsDetail/ShowGoods";
|
||||||
import empty from "@/components/empty/Main";
|
import empty from "@/components/empty/Main";
|
||||||
import ShowGoodsDetail from "@/components/goodsDetail/ShowGoodsDetail";
|
import ShowGoodsDetail from "@/components/goodsDetail/ShowGoodsDetail";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search @search="handleSearch"></Search>
|
<SearchHeader @search="handleSearch"></SearchHeader>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<!-- 商品筛选栏 -->
|
<!-- 商品筛选栏 -->
|
||||||
@@ -56,12 +56,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="goods-show-detail">
|
<div class="goods-show-detail">
|
||||||
<el-tag
|
|
||||||
v-if="item.salesModel === 'WHOLESALE'"
|
|
||||||
class="goods-show-tag goods-show-tag-wholesale"
|
|
||||||
>
|
|
||||||
批发
|
|
||||||
</el-tag>
|
|
||||||
<span>{{ item.goodsName }}</span>
|
<span>{{ item.goodsName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="goods-show-num">
|
<div class="goods-show-num">
|
||||||
@@ -121,6 +115,8 @@ import { ArrowDown, ArrowUp } from '@element-plus/icons-vue';
|
|||||||
import GoodsClassNav from "@/components/nav/GoodsClassNav";
|
import GoodsClassNav from "@/components/nav/GoodsClassNav";
|
||||||
import GoodsThumb from "@/components/goods/GoodsThumb.vue";
|
import GoodsThumb from "@/components/goods/GoodsThumb.vue";
|
||||||
import * as apiGoods from "@/api/goods";
|
import * as apiGoods from "@/api/goods";
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -351,16 +347,6 @@ getGoodsList();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-show-tag-wholesale {
|
|
||||||
margin-right: 5px;
|
|
||||||
background-color: #f9f0ff;
|
|
||||||
border-color: #d3adf7;
|
|
||||||
color: #9254de;
|
|
||||||
|
|
||||||
:deep(.el-tag__content) {
|
|
||||||
color: #9254de;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-show-seller {
|
.goods-show-seller {
|
||||||
// padding:3px 0;
|
// padding:3px 0;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<el-card shadow="never" v-for="(item, index) in autoCoupList" :key="index" style="margin: 10px 15px">
|
<el-card shadow="never" v-for="(item, index) in autoCoupList" :key="index" style="margin: 10px 15px">
|
||||||
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color" style="font-size: 15px;">¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
|
<span v-if="item.couponType === 'PRICE'" class="fontsize_12 global_color" style="font-size: 15px;">¥<span class="price">{{ $filters.unitPrice(item.price) }}</span></span>
|
||||||
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color" style="font-size: 15px;"><span class="price">{{item.discount}}</span>折</span>
|
<span v-if="item.couponType === 'DISCOUNT'" class="fontsize_12 global_color" style="font-size: 15px;"><span class="price">{{item.discount}}</span>折</span>
|
||||||
<span class="describe" style="font-size: 15px;margin-left: 5px;">满{{item.consumeThreshold}}元可用</span>
|
<span class="describe" style="font-size: 15px;margin-left: 5px;">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
<p style="font-size: 14px;">使用范围:{{useScope(item.scopeType, item.storeName)}}</p>
|
<p style="font-size: 14px;">使用范围:{{useScope(item.scopeType, item.storeName)}}</p>
|
||||||
<p style="font-size: 14px;">有效期:{{item.endTime}}</p>
|
<p style="font-size: 14px;">有效期:{{item.endTime}}</p>
|
||||||
</el-card>
|
</el-card>
|
||||||
@@ -22,33 +22,39 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 固定头部 -->
|
<!-- 固定头部 -->
|
||||||
<hover-search class="hover-search" :class="{show: topSearchShow}"></hover-search>
|
<hover-search class="hover-search" :class="{show: topSearchShow}" :options="searchHeaderOptions"></hover-search>
|
||||||
<!-- 顶部广告 -->
|
<!-- 顶部广告 -->
|
||||||
<FixedTopPage :data="topAdvert"></FixedTopPage>
|
<FixedTopPage :data="topAdvert" v-if="!isVersion2"></FixedTopPage>
|
||||||
<!-- 头部 包括登录,我的订单等 -->
|
<!-- 头部 包括登录,我的订单等 -->
|
||||||
<BaseHeader></BaseHeader>
|
<BaseHeader v-if="!hasComponentInV2('topBar')"></BaseHeader>
|
||||||
<!-- 搜索框、logo -->
|
<!-- 搜索框、logo -->
|
||||||
<Search></Search>
|
<Search v-if="!hasComponentInV2('searchHeader')"></Search>
|
||||||
<div class="index-main">
|
<!-- Version 2 页面装修:全宽自由响应容器,由各行独立配置 rowStyle.width(如 100%、1920、1400、1200)实现全宽或精准居中 -->
|
||||||
<!-- 商品分类 + 楼层装修(同一宽度容器,保证与分类栏左缘对齐) -->
|
<div v-if="isVersion2" class="v2-index-main">
|
||||||
|
<model-form :data="modelFormData"></model-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Version 1 历史旧版:保持 1200px 传统固定容器与分类栏 -->
|
||||||
|
<div v-else class="index-main">
|
||||||
<cateNav :showAlways="true" v-if="showNav" :large="carouselLarge" :opacity="carouselOpacity"></cateNav>
|
<cateNav :showAlways="true" v-if="showNav" :large="carouselLarge" :opacity="carouselOpacity"></cateNav>
|
||||||
<model-form :data="modelFormData"></model-form>
|
<model-form :data="modelFormData"></model-form>
|
||||||
</div>
|
</div>
|
||||||
<!-- 底部栏 -->
|
<!-- 底部栏 -->
|
||||||
<BaseFooter></BaseFooter>
|
<BaseFooter v-if="!hasComponentInV2('footerNav')"></BaseFooter>
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<fixedBar class="fixed-bar" :class="{'show-fixed': topSearchShow}"></fixedBar>
|
<fixedBar class="fixed-bar" :class="{'show-fixed': topSearchShow}"></fixedBar>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted, ref } from "vue";
|
import { onMounted, onUnmounted, ref, computed } from "vue";
|
||||||
import Search from '@/components/Search';
|
import Search from '@/components/Search';
|
||||||
import ModelForm from '@/components/indexDecorate/ModelForm';
|
import ModelForm from '@/components/indexDecorate/ModelForm';
|
||||||
import HoverSearch from '@/components/header/hoverSearch';
|
import HoverSearch from '@/components/header/hoverSearch';
|
||||||
import fixedBar from '@/components/fixed/index';
|
import fixedBar from '@/components/fixed/index';
|
||||||
import storage from "@/plugins/storage";
|
import storage from "@/plugins/storage";
|
||||||
import { indexData, getAutoCoup } from '@/api/index.js';
|
import { indexData, getAutoCoup } from '@/api/index.js';
|
||||||
|
import { DOUBLE11_PAGE_DOCUMENT } from '@/components/indexDecorate/presets/double11Preset.js';
|
||||||
import { seckillByDay } from '@/api/promotion';
|
import { seckillByDay } from '@/api/promotion';
|
||||||
|
|
||||||
defineOptions({ name: "Index" });
|
defineOptions({ name: "Index" });
|
||||||
@@ -69,6 +75,29 @@ const topSearchShow = ref(false); // 滚动后顶部搜索栏展示
|
|||||||
const carouselLarge = ref(false); // 不同轮播分类尺寸
|
const carouselLarge = ref(false); // 不同轮播分类尺寸
|
||||||
const carouselOpacity = ref(false); // 不同轮播分类样式
|
const carouselOpacity = ref(false); // 不同轮播分类样式
|
||||||
|
|
||||||
|
const isVersion2 = computed(() => {
|
||||||
|
return modelFormData.value?.version === 2 || modelFormData.value?.version === '2.0';
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchHeaderOptions = computed(() => {
|
||||||
|
if (!Array.isArray(modelFormData.value?.list)) return {};
|
||||||
|
for (const row of modelFormData.value.list) {
|
||||||
|
if (Array.isArray(row.components)) {
|
||||||
|
const target = row.components.find(c => c && c.type === 'searchHeader');
|
||||||
|
if (target?.options) {
|
||||||
|
return target.options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasComponentInV2 = (type) => {
|
||||||
|
if (!isVersion2.value || !Array.isArray(modelFormData.value?.list)) return false;
|
||||||
|
return modelFormData.value.list.some(r => r.components && r.components.some(c => c.type === type));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
// 优惠券可用范围
|
// 优惠券可用范围
|
||||||
function useScope(type, storeName) {
|
function useScope(type, storeName) {
|
||||||
let shop = '平台';
|
let shop = '平台';
|
||||||
@@ -137,7 +166,7 @@ function handleReachBottom() {
|
|||||||
function getIndexData() {
|
function getIndexData() {
|
||||||
if (props.pageData) {
|
if (props.pageData) {
|
||||||
parsePageData(JSON.stringify(props.pageData)).catch(() => {
|
parsePageData(JSON.stringify(props.pageData)).catch(() => {
|
||||||
// 装修数据解析失败时保持默认空楼层
|
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 获取首页装修数据
|
// 获取首页装修数据
|
||||||
@@ -146,11 +175,15 @@ function getIndexData() {
|
|||||||
try {
|
try {
|
||||||
await parsePageData(res.result.pageData);
|
await parsePageData(res.result.pageData);
|
||||||
} catch {
|
} catch {
|
||||||
// 装修数据解析失败时保持默认空楼层
|
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// 无服务端数据时默认展示双11平台首页
|
||||||
|
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn('获取首页装修数据失败', err);
|
console.warn('获取首页装修数据失败,启用双11平台首页预设', err);
|
||||||
|
modelFormData.value = DOUBLE11_PAGE_DOCUMENT;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,6 +193,10 @@ async function parsePageData(pageData) {
|
|||||||
if (!dataJson?.list?.length) {
|
if (!dataJson?.list?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (dataJson.version === 2 || dataJson.version === '2.0') {
|
||||||
|
modelFormData.value = dataJson;
|
||||||
|
return;
|
||||||
|
}
|
||||||
// 秒杀活动不是装修的数据,需要调用接口判断是否有秒杀商品
|
// 秒杀活动不是装修的数据,需要调用接口判断是否有秒杀商品
|
||||||
// 轮播图根据不同轮播,样式不同
|
// 轮播图根据不同轮播,样式不同
|
||||||
for (let i = 0; i < dataJson.list.length; i++) {
|
for (let i = 0; i < dataJson.list.length; i++) {
|
||||||
@@ -221,6 +258,11 @@ onUnmounted(() => {
|
|||||||
.container {
|
.container {
|
||||||
@include sub_background_color($light_background_color);
|
@include sub_background_color($light_background_color);
|
||||||
}
|
}
|
||||||
|
.v2-index-main {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<style>
|
<style>
|
||||||
.hover-search {
|
.hover-search {
|
||||||
@@ -231,7 +273,8 @@ onUnmounted(() => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
box-shadow:0 0 10px 2px rgb(90 90 90 / 60%);
|
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.08);
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
transition: 0.35s;
|
transition: 0.35s;
|
||||||
}
|
}
|
||||||
.show {
|
.show {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="merchant">
|
<div class="merchant">
|
||||||
<BaseHeader/>
|
<TopBar></TopBar>
|
||||||
<!-- 搜索栏 -->
|
<!-- 搜索栏 -->
|
||||||
<Search :storeId="storeMsg.storeId || $route.query.id" @search="search"></Search>
|
<SearchHeader @search="search"></SearchHeader>
|
||||||
<div class="store-head">
|
<div class="store-head">
|
||||||
<div class="store-head-main">
|
<div class="store-head-main">
|
||||||
<div class="shop-info">
|
<div class="shop-info">
|
||||||
@@ -87,10 +87,6 @@
|
|||||||
<span class="seckill-price text-danger">{{ $filters.unitPrice(item.price, "¥") }}</span>
|
<span class="seckill-price text-danger">{{ $filters.unitPrice(item.price, "¥") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="goods-show-detail">
|
<div class="goods-show-detail">
|
||||||
<el-tag
|
|
||||||
v-if="item.salesModel === 'WHOLESALE'"
|
|
||||||
class="goods-show-tag goods-show-tag-wholesale"
|
|
||||||
>批发</el-tag>
|
|
||||||
<span>{{ item.goodsName }}</span>
|
<span>{{ item.goodsName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="goods-show-num">
|
<div class="goods-show-num">
|
||||||
@@ -133,7 +129,8 @@ import {
|
|||||||
isStoreCollection,
|
isStoreCollection,
|
||||||
} from "@/api/member";
|
} from "@/api/member";
|
||||||
import {goodsList as goodsListApi} from "@/api/goods";
|
import {goodsList as goodsListApi} from "@/api/goods";
|
||||||
import Search from "@/components/Search";
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import ModelForm from "@/components/indexDecorate/ModelForm";
|
import ModelForm from "@/components/indexDecorate/ModelForm";
|
||||||
import storage from "@/plugins/storage";
|
import storage from "@/plugins/storage";
|
||||||
import {getFloorStoreData} from "@/api/index.js";
|
import {getFloorStoreData} from "@/api/index.js";
|
||||||
@@ -209,7 +206,11 @@ function getIndexData() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function parsePageData(pageData) {
|
async function parsePageData(pageData) {
|
||||||
const dataJson = JSON.parse(pageData);
|
const dataJson = typeof pageData === 'string' ? JSON.parse(pageData) : pageData;
|
||||||
|
if (dataJson.version === 2 || dataJson.version === '2.0') {
|
||||||
|
modelForm.value = dataJson;
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (let i = 0; i < dataJson.list.length; i++) {
|
for (let i = 0; i < dataJson.list.length; i++) {
|
||||||
const type = dataJson.list[i].type;
|
const type = dataJson.list[i].type;
|
||||||
if (type === "carousel2") {
|
if (type === "carousel2") {
|
||||||
@@ -719,15 +720,6 @@ window.onscroll = () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-show-tag-wholesale {
|
|
||||||
--el-tag-bg-color: #f9f0ff;
|
|
||||||
--el-tag-border-color: #d3adf7;
|
|
||||||
--el-tag-text-color: #9254de;
|
|
||||||
background-color: #f9f0ff;
|
|
||||||
border-color: #d3adf7;
|
|
||||||
color: #9254de;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-page {
|
.goods-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="not-found-page">
|
<div class="not-found-page">
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search :show-tag="false"></Search>
|
<SearchHeader :options="{ showHotWords: false }"></SearchHeader>
|
||||||
<div class="not-found">
|
<div class="not-found">
|
||||||
<h1>页面不存在</h1>
|
<h1>页面不存在</h1>
|
||||||
<p>你访问的地址没有对应页面</p>
|
<p>你访问的地址没有对应页面</p>
|
||||||
@@ -12,6 +12,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
|
|
||||||
defineOptions({ name: "NotFound" });
|
defineOptions({ name: "NotFound" });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search></Search>
|
<SearchHeader></SearchHeader>
|
||||||
|
|
||||||
<!-- 添加成功提示 -->
|
<!-- 添加成功提示 -->
|
||||||
<div class="add-info-box-container">
|
<div class="add-info-box-container">
|
||||||
@@ -50,6 +50,8 @@ export default {
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
878
buyer/src/pages/StoreList.vue
Normal file
878
buyer/src/pages/StoreList.vue
Normal file
@@ -0,0 +1,878 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<TopBar></TopBar>
|
||||||
|
<SearchHeader @search="handleSearch"></SearchHeader>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<!-- 工具栏:排序 + 筛选 -->
|
||||||
|
<div class="store-tool">
|
||||||
|
<div class="store-sort">
|
||||||
|
<span
|
||||||
|
v-for="(item, index) in sortTools"
|
||||||
|
:key="item.value"
|
||||||
|
class="store-sort-item"
|
||||||
|
:class="{ active: index === sortIndex }"
|
||||||
|
@click="changeSort(index)"
|
||||||
|
>
|
||||||
|
{{ item.title }}
|
||||||
|
<el-icon v-if="index === sortIndex && item.value"><ArrowDown /></el-icon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-tool-right">
|
||||||
|
<span class="store-total-text">
|
||||||
|
<template v-if="params.storeName">
|
||||||
|
“<span class="keyword">{{ params.storeName }}</span>”
|
||||||
|
</template>
|
||||||
|
共找到 <span class="num">{{ total }}</span> 家店铺
|
||||||
|
</span>
|
||||||
|
<el-checkbox v-model="selfOnly" @change="changeSelfOnly">只看自营</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 店铺列表 -->
|
||||||
|
<div class="store-list" v-loading="loading">
|
||||||
|
<div v-if="searched && !loading && storeList.length === 0" class="store-empty">
|
||||||
|
<empty :_Title="emptyTitle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="store in storeList" :key="store.id" class="store-item">
|
||||||
|
<!-- 店铺信息 -->
|
||||||
|
<div class="store-info">
|
||||||
|
<div class="store-logo" @click="goShopPage(store.id)">
|
||||||
|
<img
|
||||||
|
v-if="store.storeLogo"
|
||||||
|
:src="store.storeLogo"
|
||||||
|
:alt="store.storeName"
|
||||||
|
loading="lazy"
|
||||||
|
@error="$event.target.style.visibility = 'hidden'"
|
||||||
|
/>
|
||||||
|
<span v-else class="store-logo-text">{{ (store.storeName || '店').slice(0, 1) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-meta">
|
||||||
|
<div class="store-name" @click="goShopPage(store.id)">
|
||||||
|
<span class="store-name-text" :title="store.storeName" v-html="highlight(store.storeName)"></span>
|
||||||
|
<span v-if="store.selfOperated" class="store-tag-self">自营</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 结构化字段:商家名称 / 所在地区 / 工商信息 -->
|
||||||
|
<div class="store-fields">
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">商家名称</span>
|
||||||
|
<span class="field-value" :title="store.storeName">{{ store.storeName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">所在地区</span>
|
||||||
|
<span class="field-value" :title="formatRegion(store.storeAddressPath)">
|
||||||
|
{{ formatRegion(store.storeAddressPath) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<span class="field-label">工商信息</span>
|
||||||
|
<span class="field-value">
|
||||||
|
<span class="licence-btn" @click.stop="openLicence(store)">[查看详情]</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="store-ops">
|
||||||
|
<button class="btn-enter" @click="goShopPage(store.id)">
|
||||||
|
进入店铺<el-icon><ArrowRight /></el-icon>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn-collect"
|
||||||
|
:class="{ 'is-collected': collectedMap[store.id] }"
|
||||||
|
@click="toggleCollect(store)"
|
||||||
|
>
|
||||||
|
{{ collectedMap[store.id] ? '已收藏' : '+ 收藏' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 店铺热销商品 -->
|
||||||
|
<div class="store-goods">
|
||||||
|
<template v-if="goodsMap[store.id] && goodsMap[store.id].length">
|
||||||
|
<div
|
||||||
|
v-for="goods in goodsMap[store.id]"
|
||||||
|
:key="goods.id"
|
||||||
|
class="store-goods-item"
|
||||||
|
@click="goGoodsDetail(goods.id, goods.goodsId)"
|
||||||
|
>
|
||||||
|
<div class="store-goods-img">
|
||||||
|
<GoodsThumb :src="goods.thumbnail || goods.small" :alt="goods.goodsName" />
|
||||||
|
</div>
|
||||||
|
<div class="store-goods-name" :title="goods.goodsName">{{ goods.goodsName }}</div>
|
||||||
|
<div class="store-goods-price">{{ $filters.unitPrice(goods.price, '¥') }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="goodsMap[store.id]" class="store-goods-empty">
|
||||||
|
店铺暂无在售商品,<span @click="goShopPage(store.id)">去店铺逛逛 ›</span>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div v-for="n in PREVIEW_GOODS_SIZE" :key="n" class="store-goods-item is-placeholder">
|
||||||
|
<div class="store-goods-img"><GoodsThumb src="" /></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="store-page" v-if="total > 0">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
:page-sizes="[10, 20, 30]"
|
||||||
|
:total="total"
|
||||||
|
layout="total, prev, pager, next, sizes"
|
||||||
|
@current-change="getStoreList"
|
||||||
|
@size-change="changePageSize"
|
||||||
|
></el-pagination>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 工商资质信息弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="licenceDialogVisible"
|
||||||
|
title="工商资质信息公示"
|
||||||
|
width="520px"
|
||||||
|
align-center
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<div v-loading="licenceLoading" class="licence-modal">
|
||||||
|
<template v-if="licenceData">
|
||||||
|
<div class="licence-row">
|
||||||
|
<span class="l-label">店铺名称</span>
|
||||||
|
<span class="l-val">{{ currentStore?.storeName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="licence-row" v-if="licenceData.companyName">
|
||||||
|
<span class="l-label">企业名称</span>
|
||||||
|
<span class="l-val">{{ licenceData.companyName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="licence-row" v-if="licenceData.companyAddressPath || licenceData.companyAddress">
|
||||||
|
<span class="l-label">企业住所</span>
|
||||||
|
<span class="l-val">
|
||||||
|
{{ formatRegion(licenceData.companyAddressPath) }} {{ licenceData.companyAddress || '' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="licence-row" v-if="licenceData.scope">
|
||||||
|
<span class="l-label">经营范围</span>
|
||||||
|
<span class="l-val">{{ licenceData.scope }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="licence-photo-box">
|
||||||
|
<div class="photo-title">营业执照电子版</div>
|
||||||
|
<div v-if="licenceData.licencePhoto" class="photo-wrap">
|
||||||
|
<el-image
|
||||||
|
:src="licenceData.licencePhoto"
|
||||||
|
:preview-src-list="[licenceData.licencePhoto]"
|
||||||
|
fit="contain"
|
||||||
|
class="licence-img"
|
||||||
|
/>
|
||||||
|
<div class="photo-hint">点击图片可放大查看</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="photo-empty">暂未上传营业执照电子版</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else-if="!licenceLoading" class="licence-none">
|
||||||
|
暂未查询到该店铺的工商资质信息
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<BaseFooter></BaseFooter>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "StoreList",
|
||||||
|
beforeRouteEnter(to, from, next) {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref, watch } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { ArrowDown, ArrowRight } from "@element-plus/icons-vue";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
import storage from "@/plugins/storage";
|
||||||
|
import GoodsThumb from "@/components/goods/GoodsThumb.vue";
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
|
import { shopList, getLicencePhoto } from "@/api/shopentry";
|
||||||
|
import { goodsList as goodsListApi } from "@/api/goods";
|
||||||
|
import { cancelStoreCollect, collectStore, isStoreCollection } from "@/api/member";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 每个店铺预览的商品数量
|
||||||
|
const PREVIEW_GOODS_SIZE = 5;
|
||||||
|
|
||||||
|
// 排序项(字段由后端 camel2Underline 转为列名)
|
||||||
|
const sortTools = [
|
||||||
|
{ title: "综合", value: "" },
|
||||||
|
{ title: "商品数", value: "goodsNum" },
|
||||||
|
{ title: "收藏数", value: "collectionNum" },
|
||||||
|
{ title: "服务评分", value: "serviceScore" },
|
||||||
|
];
|
||||||
|
const sortIndex = ref(0);
|
||||||
|
const selfOnly = ref(false);
|
||||||
|
|
||||||
|
const params = reactive({
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
storeName: route.query.keyword || "",
|
||||||
|
// 买家端只展示营业中的店铺
|
||||||
|
storeDisable: "OPEN",
|
||||||
|
sort: "",
|
||||||
|
order: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const storeList = ref([]);
|
||||||
|
const total = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const searched = ref(false);
|
||||||
|
// 店铺预览商品:storeId -> goods[]
|
||||||
|
const goodsMap = reactive({});
|
||||||
|
// 收藏状态:storeId -> boolean
|
||||||
|
const collectedMap = reactive({});
|
||||||
|
let requestSeq = 0;
|
||||||
|
|
||||||
|
// 工商资质弹窗状态
|
||||||
|
const licenceDialogVisible = ref(false);
|
||||||
|
const licenceLoading = ref(false);
|
||||||
|
const currentStore = ref(null);
|
||||||
|
const licenceData = ref(null);
|
||||||
|
|
||||||
|
const emptyTitle = computed(() =>
|
||||||
|
params.storeName ? `没有找到与“${params.storeName}”相关的店铺` : "暂无店铺"
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.keyword,
|
||||||
|
(keyword) => {
|
||||||
|
if (route.path !== "/storeList") return;
|
||||||
|
params.storeName = keyword || "";
|
||||||
|
params.pageNumber = 1;
|
||||||
|
getStoreList();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 格式化地区展示(如 "北京市,东城区" -> "北京市东城区")
|
||||||
|
function formatRegion(path) {
|
||||||
|
if (!path) return "暂未填写";
|
||||||
|
const parts = String(path).split(/[,,\s]+/).filter(Boolean);
|
||||||
|
if (!parts.length) return "暂未填写";
|
||||||
|
const deduped = [];
|
||||||
|
parts.forEach((p) => {
|
||||||
|
if (!deduped.length || deduped[deduped.length - 1] !== p) {
|
||||||
|
deduped.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return deduped.join("") || "暂未填写";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开工商资质信息弹窗
|
||||||
|
function openLicence(store) {
|
||||||
|
currentStore.value = store;
|
||||||
|
licenceData.value = null;
|
||||||
|
licenceDialogVisible.value = true;
|
||||||
|
licenceLoading.value = true;
|
||||||
|
getLicencePhoto(store.id)
|
||||||
|
.then((res) => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
licenceData.value = res.result;
|
||||||
|
} else {
|
||||||
|
licenceData.value = null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
licenceData.value = null;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
licenceLoading.value = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索框(店铺模式)回调:写入 URL,便于刷新 / 回退
|
||||||
|
function handleSearch(keyword) {
|
||||||
|
const value = keyword || "";
|
||||||
|
if (value === (route.query.keyword || "")) {
|
||||||
|
params.storeName = value;
|
||||||
|
params.pageNumber = 1;
|
||||||
|
getStoreList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push({ path: "/storeList", query: value ? { keyword: value } : {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSort(index) {
|
||||||
|
sortIndex.value = index;
|
||||||
|
const { value } = sortTools[index];
|
||||||
|
params.sort = value;
|
||||||
|
params.order = value ? "desc" : "";
|
||||||
|
params.pageNumber = 1;
|
||||||
|
getStoreList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeSelfOnly() {
|
||||||
|
params.pageNumber = 1;
|
||||||
|
getStoreList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changePageSize(size) {
|
||||||
|
params.pageSize = size;
|
||||||
|
params.pageNumber = 1;
|
||||||
|
getStoreList();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoreList() {
|
||||||
|
const seq = ++requestSeq;
|
||||||
|
loading.value = true;
|
||||||
|
const query = { ...params };
|
||||||
|
if (!query.sort) {
|
||||||
|
delete query.sort;
|
||||||
|
delete query.order;
|
||||||
|
}
|
||||||
|
if (!query.storeName) delete query.storeName;
|
||||||
|
if (selfOnly.value) query.selfOperated = true;
|
||||||
|
shopList(query)
|
||||||
|
.then((res) => {
|
||||||
|
if (seq !== requestSeq) return;
|
||||||
|
if (res.success) {
|
||||||
|
storeList.value = res.result.records || [];
|
||||||
|
total.value = res.result.total || 0;
|
||||||
|
loadStoreExtras(storeList.value);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (seq !== requestSeq) return;
|
||||||
|
storeList.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (seq !== requestSeq) return;
|
||||||
|
loading.value = false;
|
||||||
|
searched.value = true;
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载每个店铺的预览商品与收藏状态(失败不影响列表展示)
|
||||||
|
function loadStoreExtras(list) {
|
||||||
|
const logged = !!storage.getItem("userInfo");
|
||||||
|
list.forEach((store) => {
|
||||||
|
const storeId = store.id;
|
||||||
|
if (!storeId) return;
|
||||||
|
if (!goodsMap[storeId]) {
|
||||||
|
goodsListApi({
|
||||||
|
storeId,
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: PREVIEW_GOODS_SIZE,
|
||||||
|
sort: "buyCount",
|
||||||
|
order: "desc",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
goodsMap[storeId] = res && res.success ? res.result.records || [] : [];
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
goodsMap[storeId] = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (logged && collectedMap[storeId] === undefined) {
|
||||||
|
isStoreCollection("STORE", storeId)
|
||||||
|
.then((res) => {
|
||||||
|
collectedMap[storeId] = !!(res && res.success && res.result);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 收藏状态查询失败静默处理
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleCollect(store) {
|
||||||
|
const storeId = store.id;
|
||||||
|
if (!storage.getItem("userInfo")) {
|
||||||
|
Message.warning("请先登录");
|
||||||
|
router.push("/login");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (collectedMap[storeId]) {
|
||||||
|
const res = await cancelStoreCollect("STORE", storeId);
|
||||||
|
if (res.success) {
|
||||||
|
collectedMap[storeId] = false;
|
||||||
|
store.collectionNum = Math.max((store.collectionNum || 1) - 1, 0);
|
||||||
|
Message.success("已取消收藏");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await collectStore("STORE", storeId);
|
||||||
|
if (res.success || res.code === 200) {
|
||||||
|
collectedMap[storeId] = true;
|
||||||
|
store.collectionNum = (store.collectionNum || 0) + 1;
|
||||||
|
Message.success("收藏店铺成功,可前往我的收藏查看");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err?.message || err?.data?.message || "";
|
||||||
|
if (msg.includes("重复收藏")) {
|
||||||
|
collectedMap[storeId] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 店铺名关键词高亮(先转义,避免 XSS)
|
||||||
|
function highlight(name) {
|
||||||
|
const text = escapeHtml(name || "");
|
||||||
|
const keyword = (params.storeName || "").trim();
|
||||||
|
if (!keyword) return text;
|
||||||
|
const escapedKeyword = escapeHtml(keyword).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return text.replace(new RegExp(escapedKeyword, "gi"), (m) => `<em class="hl">${m}</em>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function goShopPage(id) {
|
||||||
|
if (!id) {
|
||||||
|
Message.warning("店铺信息不存在");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push({ path: "/merchant", query: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goGoodsDetail(skuId, goodsId) {
|
||||||
|
const location = { path: "/goodsDetail", query: { skuId, goodsId } };
|
||||||
|
window.open(router.resolve(location).href, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
getStoreList();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
$theme_light: color-mix(in srgb, #{$theme_color} 8%, #fff);
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 20px auto 32px;
|
||||||
|
width: 1200px;
|
||||||
|
min-width: 1000px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------工具栏------------------- */
|
||||||
|
.store-tool {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 56px;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-tool-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-total-text {
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
.keyword,
|
||||||
|
.num {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-sort {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-sort-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------店铺列表------------------- */
|
||||||
|
.store-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
min-height: 240px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-empty {
|
||||||
|
padding: 40px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 28px;
|
||||||
|
padding: 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-info {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 16px;
|
||||||
|
width: 350px;
|
||||||
|
padding-right: 28px;
|
||||||
|
border-right: 1px dashed #e8e8e8;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 84px;
|
||||||
|
height: 84px;
|
||||||
|
border: 2px solid #f2f2f2;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-logo-text {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-meta {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.store-name-text {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #171a26;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .store-name-text {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hl) {
|
||||||
|
color: $theme_color;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-tag-self {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 结构化属性 */
|
||||||
|
.store-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.field-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 56px;
|
||||||
|
color: #999;
|
||||||
|
text-align: justify;
|
||||||
|
text-align-last: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.licence-btn {
|
||||||
|
color: #2d8cf0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $theme_color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-ops {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enter {
|
||||||
|
border: none;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-collect {
|
||||||
|
border: 1px solid $theme_color;
|
||||||
|
background: #fff;
|
||||||
|
color: $theme_color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $theme_light;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-collected {
|
||||||
|
border-color: #ddd;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------热销商品------------------- */
|
||||||
|
.store-goods {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-goods-item {
|
||||||
|
min-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.store-goods-img {
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .store-goods-img {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .store-goods-name {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-placeholder {
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.store-goods-img {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-goods-name {
|
||||||
|
margin-top: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #555;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-goods-price {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: $theme_color;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-goods-empty {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 120px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fafafa;
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: $theme_color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------工商资质弹窗------------------- */
|
||||||
|
.licence-modal {
|
||||||
|
min-height: 160px;
|
||||||
|
padding: 8px 12px 16px;
|
||||||
|
|
||||||
|
.licence-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
.l-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 70px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.l-val {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.licence-photo-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px dashed #eee;
|
||||||
|
|
||||||
|
.photo-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.licence-img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 260px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-hint {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-empty {
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #bbb;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.licence-none {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search></Search>
|
<SearchHeader></SearchHeader>
|
||||||
|
|
||||||
<div class="container width_1200">
|
<div class="container width_1200">
|
||||||
<h1 class="page-title">文章帮助中心</h1>
|
<h1 class="page-title">文章帮助中心</h1>
|
||||||
@@ -50,6 +50,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { articleCateList, articleDetail, articleList as fetchArticleList } from '@/api/common.js'
|
import { articleCateList, articleDetail, articleList as fetchArticleList } from '@/api/common.js'
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,105 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="asset-link">查看</span>
|
<span class="asset-link">查看</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="asset-card" @click="go('Point')">
|
||||||
|
<div>
|
||||||
|
<div class="asset-label">我的积分</div>
|
||||||
|
<div class="asset-value">{{ point }}<span>积分</span></div>
|
||||||
|
</div>
|
||||||
|
<span class="asset-link">查看</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="asset-card" @click="go('MemberGrade')">
|
||||||
|
<div>
|
||||||
|
<div class="asset-label">会员等级</div>
|
||||||
|
<div class="asset-value grade-value">{{ gradeName }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="asset-link">查看</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="panel-title">最近订单</span>
|
||||||
|
<button type="button" class="panel-more" @click="go('MyOrder')">全部订单 ›</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="recentOrders.length" class="order-list">
|
||||||
|
<button
|
||||||
|
v-for="order in recentOrders"
|
||||||
|
:key="order.sn"
|
||||||
|
type="button"
|
||||||
|
class="order-row"
|
||||||
|
@click="goOrderDetail(order.sn)"
|
||||||
|
>
|
||||||
|
<div class="order-thumb">
|
||||||
|
<img
|
||||||
|
:src="firstItem(order).image"
|
||||||
|
:alt="firstItem(order).name"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="order-info">
|
||||||
|
<div class="order-name">
|
||||||
|
{{ firstItem(order).name }}
|
||||||
|
<span v-if="order.orderItems.length > 1" class="order-more">
|
||||||
|
等{{ order.orderItems.length }}件
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="order-meta">{{ order.createTime }} · {{ order.storeName }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-side">
|
||||||
|
<div class="order-amount">{{ $filters.unitPrice(order.flowPrice, "¥") }}</div>
|
||||||
|
<div class="order-status">{{ orderStatusText(order.orderStatus) }}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="ordersLoaded" class="panel-empty">暂无订单</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="panel-title">我的足迹</span>
|
||||||
|
<button type="button" class="panel-more" @click="go('MyTracks')">查看更多 ›</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="tracks.length" class="goods-strip">
|
||||||
|
<button
|
||||||
|
v-for="item in tracks"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="goods-card"
|
||||||
|
@click="openGoods(item.id, item.goodsId)"
|
||||||
|
>
|
||||||
|
<div class="goods-thumb">
|
||||||
|
<img :src="item.thumbnail" :alt="item.goodsName" loading="lazy" decoding="async" />
|
||||||
|
</div>
|
||||||
|
<div class="goods-name">{{ item.goodsName }}</div>
|
||||||
|
<div class="goods-price">{{ $filters.unitPrice(item.price, "¥") }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="tracksLoaded" class="panel-empty">暂无足迹</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<span class="panel-title">我的收藏</span>
|
||||||
|
<button type="button" class="panel-more" @click="go('Favorites')">查看更多 ›</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="favorites.length" class="goods-strip">
|
||||||
|
<button
|
||||||
|
v-for="item in favorites"
|
||||||
|
:key="item.skuId"
|
||||||
|
type="button"
|
||||||
|
class="goods-card"
|
||||||
|
@click="openGoods(item.skuId, item.goodsId)"
|
||||||
|
>
|
||||||
|
<div class="goods-thumb">
|
||||||
|
<img :src="item.image" :alt="item.goodsName" loading="lazy" decoding="async" />
|
||||||
|
</div>
|
||||||
|
<div class="goods-name">{{ item.goodsName }}</div>
|
||||||
|
<div class="goods-price">{{ $filters.unitPrice(item.price, "¥") }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="favoritesLoaded" class="panel-empty">暂无收藏</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -50,9 +149,18 @@ import { useRouter } from "vue-router";
|
|||||||
import { Bell } from "@element-plus/icons-vue";
|
import { Bell } from "@element-plus/icons-vue";
|
||||||
import storage from "@/plugins/storage";
|
import storage from "@/plugins/storage";
|
||||||
import { getOrderList } from "@/api/order";
|
import { getOrderList } from "@/api/order";
|
||||||
import { afterSaleList, getMembersWallet, memberCouponList } from "@/api/member";
|
import {
|
||||||
|
afterSaleList,
|
||||||
|
collectList,
|
||||||
|
getMembersWallet,
|
||||||
|
memberCouponList,
|
||||||
|
memberGradeCurrent,
|
||||||
|
memberPoint,
|
||||||
|
tracksList,
|
||||||
|
} from "@/api/member";
|
||||||
import { refreshUnreadMessage } from "@/utils/unreadMessage";
|
import { refreshUnreadMessage } from "@/utils/unreadMessage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
import { orderStatusList } from "./enumeration.js";
|
||||||
|
|
||||||
defineOptions({ name: "UserCenterHome" });
|
defineOptions({ name: "UserCenterHome" });
|
||||||
|
|
||||||
@@ -63,6 +171,12 @@ function totalOf(res) {
|
|||||||
return res && res.success ? Number(res.result && res.result.total) || 0 : 0;
|
return res && res.success ? Number(res.result && res.result.total) || 0 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function recordsOf(res) {
|
||||||
|
return res && res.success && res.result && Array.isArray(res.result.records)
|
||||||
|
? res.result.records.filter((item) => item != null)
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
const displayName = ref("会员");
|
const displayName = ref("会员");
|
||||||
const waitPay = ref(0);
|
const waitPay = ref(0);
|
||||||
const waitShip = ref(0);
|
const waitShip = ref(0);
|
||||||
@@ -70,6 +184,14 @@ const waitReceive = ref(0);
|
|||||||
const afterSale = ref(0);
|
const afterSale = ref(0);
|
||||||
const couponCount = ref(0);
|
const couponCount = ref(0);
|
||||||
const balance = ref(0);
|
const balance = ref(0);
|
||||||
|
const point = ref(0);
|
||||||
|
const gradeName = ref("-");
|
||||||
|
const recentOrders = ref([]);
|
||||||
|
const tracks = ref([]);
|
||||||
|
const favorites = ref([]);
|
||||||
|
const ordersLoaded = ref(false);
|
||||||
|
const tracksLoaded = ref(false);
|
||||||
|
const favoritesLoaded = ref(false);
|
||||||
|
|
||||||
const unreadMsg = computed(() => appStore.unreadMsg);
|
const unreadMsg = computed(() => appStore.unreadMsg);
|
||||||
const unreadLabel = computed(() => (unreadMsg.value > 99 ? "99+" : String(unreadMsg.value)));
|
const unreadLabel = computed(() => (unreadMsg.value > 99 ? "99+" : String(unreadMsg.value)));
|
||||||
@@ -90,6 +212,15 @@ function readUser() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstItem(order) {
|
||||||
|
return (order.orderItems && order.orderItems[0]) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderStatusText(status) {
|
||||||
|
const hit = orderStatusList.find((e) => e.status === status);
|
||||||
|
return hit ? hit.name : status;
|
||||||
|
}
|
||||||
|
|
||||||
function loadSummary() {
|
function loadSummary() {
|
||||||
const page = { pageNumber: 1, pageSize: 1 };
|
const page = { pageNumber: 1, pageSize: 1 };
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -100,14 +231,45 @@ function loadSummary() {
|
|||||||
memberCouponList({ ...page, memberCouponStatus: "NEW" }).catch(() => null),
|
memberCouponList({ ...page, memberCouponStatus: "NEW" }).catch(() => null),
|
||||||
getMembersWallet().catch(() => null),
|
getMembersWallet().catch(() => null),
|
||||||
refreshUnreadMessage(),
|
refreshUnreadMessage(),
|
||||||
]).then(([pay, ship, receive, afterSaleRes, coupon, wallet]) => {
|
getOrderList({ pageNumber: 1, pageSize: 3 }).catch(() => null),
|
||||||
waitPay.value = totalOf(pay);
|
tracksList({ pageNumber: 1, pageSize: 10, order: "desc", sort: "updateTime" }).catch(() => null),
|
||||||
waitShip.value = totalOf(ship);
|
collectList({ pageNumber: 1, pageSize: 10, type: "GOODS" }).catch(() => null),
|
||||||
waitReceive.value = totalOf(receive);
|
memberPoint().catch(() => null),
|
||||||
afterSale.value = totalOf(afterSaleRes);
|
memberGradeCurrent({ pageNumber: 1, pageSize: 10 }).catch(() => null),
|
||||||
couponCount.value = totalOf(coupon);
|
]).then(
|
||||||
balance.value = wallet && wallet.success ? wallet.result.memberWallet || 0 : 0;
|
([
|
||||||
});
|
pay,
|
||||||
|
ship,
|
||||||
|
receive,
|
||||||
|
afterSaleRes,
|
||||||
|
coupon,
|
||||||
|
wallet,
|
||||||
|
,
|
||||||
|
recent,
|
||||||
|
tracksRes,
|
||||||
|
favoritesRes,
|
||||||
|
pointRes,
|
||||||
|
gradeRes,
|
||||||
|
]) => {
|
||||||
|
waitPay.value = totalOf(pay);
|
||||||
|
waitShip.value = totalOf(ship);
|
||||||
|
waitReceive.value = totalOf(receive);
|
||||||
|
afterSale.value = totalOf(afterSaleRes);
|
||||||
|
couponCount.value = totalOf(coupon);
|
||||||
|
balance.value = wallet && wallet.success ? wallet.result.memberWallet || 0 : 0;
|
||||||
|
|
||||||
|
recentOrders.value = recordsOf(recent).filter((order) => order && order.sn);
|
||||||
|
ordersLoaded.value = true;
|
||||||
|
tracks.value = recordsOf(tracksRes);
|
||||||
|
tracksLoaded.value = true;
|
||||||
|
favorites.value = recordsOf(favoritesRes);
|
||||||
|
favoritesLoaded.value = true;
|
||||||
|
|
||||||
|
point.value = pointRes && pointRes.success && pointRes.result ? pointRes.result.point || 0 : 0;
|
||||||
|
const grade = gradeRes && gradeRes.success && gradeRes.result ? gradeRes.result.grade : null;
|
||||||
|
gradeName.value = grade && grade.gradeName ? grade.gradeName : "暂无等级";
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function go(name) {
|
function go(name) {
|
||||||
@@ -118,6 +280,15 @@ function goOrder(tag) {
|
|||||||
router.push({ name: "MyOrder", query: { tag } });
|
router.push({ name: "MyOrder", query: { tag } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goOrderDetail(sn) {
|
||||||
|
router.push({ name: "OrderDetail", query: { sn } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGoods(skuId, goodsId) {
|
||||||
|
const routeUrl = router.resolve({ path: "/goodsDetail", query: { skuId, goodsId } });
|
||||||
|
window.open(routeUrl.href, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
readUser();
|
readUser();
|
||||||
loadSummary();
|
loadSummary();
|
||||||
@@ -196,7 +367,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.asset-grid {
|
.asset-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-card,
|
.stat-card,
|
||||||
@@ -236,7 +407,12 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
padding: 18px 20px;
|
padding: 18px 20px;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-label {
|
.asset-label {
|
||||||
@@ -251,6 +427,9 @@ onMounted(() => {
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
@@ -260,8 +439,201 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grade-value {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-link {
|
.asset-link {
|
||||||
|
flex: 0 0 auto;
|
||||||
color: $theme_color;
|
color: $theme_color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-title {
|
||||||
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-more {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-empty {
|
||||||
|
padding: 32px 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-thumb {
|
||||||
|
flex: 0 0 56px;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-name {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-more {
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-meta {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-side {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-amount {
|
||||||
|
color: #333;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-status {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: $theme_color;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-strip {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-card {
|
||||||
|
flex: 0 0 140px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-thumb {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
background: #f5f5f5;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
margin: 8px 10px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
height: 36px;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
margin: 6px 10px 10px;
|
||||||
|
color: $theme_color;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -27,9 +27,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="connect-row">
|
<div class="connect-row">
|
||||||
<span class="connect-name">{{ wechatConnect.label }}</span>
|
<span class="connect-name">{{ wechatConnect.label }}</span>
|
||||||
<span class="connect-status" :class="{ 'is-bound': isBound(wechatConnect.type) }">
|
<el-tag :type="isBound(wechatConnect.type) ? 'success' : 'info'" size="small">
|
||||||
{{ isBound(wechatConnect.type) ? "已绑定" : "未绑定" }}
|
{{ isBound(wechatConnect.type) ? "已绑定" : "未绑定" }}
|
||||||
</span>
|
</el-tag>
|
||||||
<el-skeleton v-if="connectLoading" animated class="connect-skeleton" />
|
<el-skeleton v-if="connectLoading" animated class="connect-skeleton" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,8 +277,9 @@ onMounted(() => {
|
|||||||
|
|
||||||
.safe-card {
|
.safe-card {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #eee;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.safe-card-connect {
|
.safe-card-connect {
|
||||||
@@ -295,7 +296,7 @@ onMounted(() => {
|
|||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #fff5f4;
|
background: color-mix(in srgb, #{$theme_color} 8%, #fff);
|
||||||
color: $theme_color;
|
color: $theme_color;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -309,7 +310,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.safe-title {
|
.safe-title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #171A26;
|
color: #333;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
@@ -333,26 +334,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.connect-name {
|
.connect-name {
|
||||||
color: #171A26;
|
color: #333;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.connect-status {
|
|
||||||
height: 22px;
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
color: #999;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-status.is-bound {
|
|
||||||
background: #f0faf3;
|
|
||||||
color: #1f9d55;
|
|
||||||
}
|
|
||||||
|
|
||||||
.connect-skeleton {
|
.connect-skeleton {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div class="comment-cell">{{ item.grade === 'GOOD' ? '好评' : item.grade === 'WORSE' ? '差评' : '中评' }}</div>
|
<div class="comment-cell">{{ item.grade === 'GOOD' ? '好评' : item.grade === 'WORSE' ? '差评' : '中评' }}</div>
|
||||||
<div class="comment-content">{{ item.content }}</div>
|
<div class="comment-content">{{ item.content }}</div>
|
||||||
<div class="comment-ops">
|
<div class="comment-ops">
|
||||||
<button type="button" class="text-link" @click="evaluateDetail(item.id)">评价详情</button>
|
<el-button link type="primary" @click="evaluateDetail(item.id)">评价详情</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,19 +230,4 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-link {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,185 +1,224 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="add-eval">
|
<div class="complain-detail">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
<div class="title">
|
<div class="detail-head">
|
||||||
<p>
|
<el-tag :type="statusTagType" effect="dark">{{ statusText }}</el-tag>
|
||||||
<span class="fontsize_16 global_color">{{ statusLabel[detail.complainStatus] }}</span>
|
<span class="create-time">创建时间:{{ detail.createTime || "-" }}</span>
|
||||||
<span class="color999 ml_20">创建时间:</span><span>{{ detail.createTime }}</span>
|
|
||||||
<span class="color999 ml_20">{{ detail.createTime }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert class="l_title" show-icon type="warning">我的申诉信息</el-alert>
|
<section class="panel">
|
||||||
<table cellspacing="0" cellpadding='0' border="1">
|
<div class="panel-title">我的申诉信息</div>
|
||||||
<tbody>
|
<div class="info-row">
|
||||||
<tr>
|
<span class="info-label">投诉商品</span>
|
||||||
<td>投诉商品</td>
|
<div class="info-value">
|
||||||
<td class="hover-color" @click="linkTo(`/goodsDetail?goodsId=${detail.goodsId}&skuId=${detail.skuId}`)"><img :src="detail.goodsImage" loading="lazy" decoding="async" width="60" alt=""> {{ detail.goodsName }}</td>
|
<button
|
||||||
</tr>
|
v-if="detail.goodsId"
|
||||||
<tr>
|
type="button"
|
||||||
<td>投诉主题</td>
|
class="goods-info"
|
||||||
<td>{{ detail.complainTopic }}</td>
|
@click="goGoodsDetail(detail.skuId, detail.goodsId)"
|
||||||
</tr>
|
>
|
||||||
<tr>
|
<img :src="detail.goodsImage" loading="lazy" decoding="async" alt="" />
|
||||||
<td>投诉内容</td>
|
<span class="goods-name">{{ detail.goodsName }}</span>
|
||||||
<td>{{ detail.content }}</td>
|
</button>
|
||||||
</tr>
|
<span v-else class="empty-text">暂无</span>
|
||||||
<tr>
|
</div>
|
||||||
<td>补充内容</td>
|
</div>
|
||||||
<td>
|
<div class="info-row">
|
||||||
<div style="display:flex;align-items:center;">
|
<span class="info-label">投诉主题</span>
|
||||||
<template v-if="detail.images">
|
<span class="info-value">{{ detail.complainTopic || "暂无" }}</span>
|
||||||
<div class="demo-upload-list" v-for="(img, index) in detail.images.split(',')" :key="index">
|
</div>
|
||||||
<img :src="img" loading="lazy" decoding="async">
|
<div class="info-row">
|
||||||
|
<span class="info-label">投诉内容</span>
|
||||||
|
<span class="info-value">{{ detail.content || "暂无" }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">补充内容</span>
|
||||||
|
<div class="info-value">
|
||||||
|
<div v-if="images.length" class="img-list">
|
||||||
|
<div
|
||||||
|
v-for="(img, index) in images"
|
||||||
|
:key="index"
|
||||||
|
class="demo-upload-list"
|
||||||
|
@click="handleView(img)"
|
||||||
|
>
|
||||||
|
<img :src="img" loading="lazy" decoding="async" />
|
||||||
<div class="demo-upload-list-cover">
|
<div class="demo-upload-list-cover">
|
||||||
<el-icon @click="handleView(img)"><View /></el-icon>
|
<el-icon><View /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<div v-else>暂无</div>
|
<span v-else class="empty-text">暂无</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</section>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<el-alert class="l_title" show-icon type="warning">商家申诉信息</el-alert>
|
<section class="panel">
|
||||||
<table cellspacing="0" cellpadding='0' border="1">
|
<div class="panel-title">商家申诉信息</div>
|
||||||
<tbody>
|
<div class="info-row">
|
||||||
<tr>
|
<span class="info-label">申诉时间</span>
|
||||||
<td>申诉时间</td>
|
<span class="info-value">{{ detail.appealTime || "暂无" }}</span>
|
||||||
<td>{{ detail.appealTime || '暂无' }}</td>
|
</div>
|
||||||
</tr>
|
<div class="info-row">
|
||||||
<tr>
|
<span class="info-label">申诉内容</span>
|
||||||
<td>申诉内容</td>
|
<span class="info-value">{{ detail.appealContent || "暂无" }}</span>
|
||||||
<td>{{ detail.appealContent || '暂无' }}</td>
|
</div>
|
||||||
</tr>
|
<div class="info-row">
|
||||||
<tr>
|
<span class="info-label">申诉凭证</span>
|
||||||
<td>申诉凭证</td>
|
<div class="info-value">
|
||||||
<td>
|
<div v-if="appealImages.length" class="img-list">
|
||||||
|
<div
|
||||||
<div style="display:flex;align-items:center;">
|
v-for="(img, index) in appealImages"
|
||||||
<template v-if="detail.appealImages">
|
:key="index"
|
||||||
<div class="demo-upload-list" v-for="(img, index) in detail.appealImages.split(',')" :key="index">
|
class="demo-upload-list"
|
||||||
<img :src="img" loading="lazy" decoding="async">
|
@click="handleView(img)"
|
||||||
|
>
|
||||||
|
<img :src="img" loading="lazy" decoding="async" />
|
||||||
<div class="demo-upload-list-cover">
|
<div class="demo-upload-list-cover">
|
||||||
<el-icon @click="handleView(img)"><View /></el-icon>
|
<el-icon><View /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</div>
|
||||||
<div v-else>暂无</div>
|
<span v-else class="empty-text">暂无</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</section>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<el-alert class="l_title" show-icon type="warning">平台仲裁</el-alert>
|
<section class="panel">
|
||||||
<table cellspacing="0" cellpadding='0' border="1">
|
<div class="panel-title">平台仲裁</div>
|
||||||
<tbody>
|
<div class="info-row">
|
||||||
<tr>
|
<span class="info-label">仲裁意见</span>
|
||||||
<td>仲裁意见</td>
|
<span class="info-value">{{ detail.arbitrationResult || "暂无" }}</span>
|
||||||
<td>{{ detail.arbitrationResult || '暂无' }}</td>
|
</div>
|
||||||
</tr>
|
</section>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<el-alert class="l_title" show-icon type="warning">对话详情</el-alert>
|
<section class="panel">
|
||||||
<div class="speak-way" v-if="detail.orderComplaintCommunications && detail.orderComplaintCommunications.length">
|
<div class="panel-title">对话详情</div>
|
||||||
<div
|
<div v-if="communications.length" class="chat-list">
|
||||||
class="speak-msg seller"
|
<div
|
||||||
:class="{'speak-buyer': item.owner == 'BUYER', 'speak-platform': item.owner == 'PLATFORM', 'speak-seller': item.owner == 'STORE',}"
|
v-for="(item, i) in communications"
|
||||||
:key="i"
|
:key="i"
|
||||||
v-for="(item, i) in detail.orderComplaintCommunications"
|
class="chat-item"
|
||||||
>
|
:class="{
|
||||||
{{
|
'is-buyer': item.owner === 'BUYER',
|
||||||
item.owner == "PLATFORM"
|
'is-platform': item.owner === 'PLATFORM',
|
||||||
? "平台"
|
}"
|
||||||
: item.owner == "BUYER"
|
>
|
||||||
? "买家"
|
<div class="chat-meta">{{ ownerName(item.owner) }} · {{ item.createTime }}</div>
|
||||||
: "卖家"
|
<div class="chat-bubble">{{ item.content }}</div>
|
||||||
}}[{{ item.createTime }}]:
|
</div>
|
||||||
<span>{{ item.content }}</span>
|
</div>
|
||||||
</div>
|
<div v-else class="empty-text">暂无对话</div>
|
||||||
</div>
|
|
||||||
<div v-else>暂无对话</div>
|
<div v-if="detail.complainStatus !== 'COMPLETE'" class="reply-box">
|
||||||
<table cellspacing="0" cellpadding='0' border="1" v-if="detail.complainStatus!='COMPLETE'">
|
<el-input
|
||||||
<tbody>
|
v-model="params.content"
|
||||||
<tr>
|
type="textarea"
|
||||||
<td>回复:</td>
|
:rows="3"
|
||||||
<td><label>
|
maxlength="200"
|
||||||
<input type="textarea" maxlength="200" :rows="4" clearable
|
show-word-limit
|
||||||
style="width:260px" v-model="params.content" />
|
placeholder="请输入回复内容"
|
||||||
</label></td>
|
/>
|
||||||
</tr>
|
<el-button
|
||||||
<tr>
|
type="primary"
|
||||||
<td></td>
|
:loading="submitLoading"
|
||||||
<td>
|
class="reply-btn"
|
||||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit" style="margin-left: 5px">
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
回复
|
回复
|
||||||
</el-button>
|
</el-button>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</section>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<el-dialog title="图片预览" v-model="visible">
|
<el-dialog title="图片预览" v-model="visible">
|
||||||
<img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%">
|
<img :src="previewImage" loading="lazy" decoding="async" v-if="visible" style="width: 100%" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</UserCenterLayout>
|
</UserCenterLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { View } from '@element-plus/icons-vue';
|
import { View } from "@element-plus/icons-vue";
|
||||||
import {getComplainDetail} from '@/api/member.js';
|
import { getComplainDetail } from "@/api/member.js";
|
||||||
import {communication} from '@/api/order';
|
import { communication } from "@/api/order";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const detail = ref({}); // 评价详情
|
const detail = ref({}); // 投诉详情
|
||||||
const visible = ref(false); // 图片预览
|
const visible = ref(false); // 图片预览
|
||||||
const previewImage = ref(''); // 预览图片地址
|
const previewImage = ref(""); // 预览图片地址
|
||||||
const loading = ref(false); // 加载状态
|
|
||||||
const submitLoading = ref(false); // 回复消息loading
|
const submitLoading = ref(false); // 回复消息loading
|
||||||
// 状态
|
// 状态
|
||||||
const statusLabel = {
|
const statusLabel = {
|
||||||
NO_APPLY: '未申请',
|
NO_APPLY: "未申请",
|
||||||
APPLYING: '申请中',
|
APPLYING: "申请中",
|
||||||
COMPLETE: '已完成',
|
COMPLETE: "已完成",
|
||||||
EXPIRED: '已失效',
|
EXPIRED: "已失效",
|
||||||
CANCEL: '已取消',
|
CANCEL: "已取消",
|
||||||
NEW: '待审核'
|
NEW: "待审核",
|
||||||
};
|
};
|
||||||
// 商家回复内容
|
// 商家回复内容
|
||||||
const params = reactive({
|
const params = reactive({
|
||||||
content: '',
|
content: "",
|
||||||
complainId: ''
|
complainId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
function getDetail () { // 获取投诉详情
|
const statusText = computed(
|
||||||
getComplainDetail(route.query.id).then(res => {
|
() => statusLabel[detail.value.complainStatus] || detail.value.complainStatus || ""
|
||||||
if (res.success) detail.value = res.result
|
);
|
||||||
})
|
const statusTagType = computed(() => {
|
||||||
|
const map = {
|
||||||
|
COMPLETE: "success",
|
||||||
|
APPLYING: "warning",
|
||||||
|
NEW: "warning",
|
||||||
|
CANCEL: "info",
|
||||||
|
EXPIRED: "info",
|
||||||
|
NO_APPLY: "info",
|
||||||
|
};
|
||||||
|
return map[detail.value.complainStatus] || "info";
|
||||||
|
});
|
||||||
|
const images = computed(() =>
|
||||||
|
detail.value.images ? detail.value.images.split(",").filter(Boolean) : []
|
||||||
|
);
|
||||||
|
const appealImages = computed(() =>
|
||||||
|
detail.value.appealImages ? detail.value.appealImages.split(",").filter(Boolean) : []
|
||||||
|
);
|
||||||
|
const communications = computed(() =>
|
||||||
|
Array.isArray(detail.value.orderComplaintCommunications)
|
||||||
|
? detail.value.orderComplaintCommunications
|
||||||
|
: []
|
||||||
|
);
|
||||||
|
|
||||||
|
function ownerName(owner) {
|
||||||
|
return owner === "PLATFORM" ? "平台" : owner === "BUYER" ? "买家" : "卖家";
|
||||||
}
|
}
|
||||||
function goGoodsDetail (skuId, goodsId) { // 跳转商品详情
|
|
||||||
|
function getDetail() {
|
||||||
|
// 获取投诉详情
|
||||||
|
getComplainDetail(route.query.id).then((res) => {
|
||||||
|
if (res.success) detail.value = res.result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function goGoodsDetail(skuId, goodsId) {
|
||||||
|
// 跳转商品详情
|
||||||
let routerUrl = router.resolve({
|
let routerUrl = router.resolve({
|
||||||
path: '/goodsDetail',
|
path: "/goodsDetail",
|
||||||
query: {skuId, goodsId}
|
query: { skuId, goodsId },
|
||||||
})
|
});
|
||||||
window.open(routerUrl.href, '_blank')
|
window.open(routerUrl.href, "_blank");
|
||||||
}
|
}
|
||||||
function handleView (name) { // 预览图片
|
function handleView(name) {
|
||||||
|
// 预览图片
|
||||||
previewImage.value = name;
|
previewImage.value = name;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
}
|
}
|
||||||
// 回复消息
|
// 回复消息
|
||||||
function handleSubmit () {
|
function handleSubmit() {
|
||||||
if (params.content === '') {
|
if (params.content === "") {
|
||||||
Message.error('请填写对话内容');
|
Message.error("请填写对话内容");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
submitLoading.value = true;
|
submitLoading.value = true;
|
||||||
@@ -187,64 +226,129 @@ function handleSubmit () {
|
|||||||
communication(params).then((res) => {
|
communication(params).then((res) => {
|
||||||
submitLoading.value = false;
|
submitLoading.value = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
Message.success('对话成功');
|
Message.success("对话成功");
|
||||||
params.content = '';
|
params.content = "";
|
||||||
getDetail();
|
getDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getDetail()
|
getDetail();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.speak-way {
|
.detail-head {
|
||||||
background-color: #fff;
|
display: flex;
|
||||||
width: 100%;
|
align-items: center;
|
||||||
border: 1px solid #999;
|
gap: 12px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.speak-seller {
|
.create-time {
|
||||||
background-color: #fff2c6;
|
color: #999;
|
||||||
}
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.speak-platform {
|
.panel {
|
||||||
background-color: #ffe2d1;
|
margin-top: 16px;
|
||||||
}
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.speak-buyer {
|
.panel-title {
|
||||||
background-color: #c1d6d5;
|
color: #333;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.speak-msg {
|
.info-label {
|
||||||
height: 40px;
|
flex: 0 0 80px;
|
||||||
line-height: 40px;
|
color: #999;
|
||||||
padding: 0 5px;
|
font-size: 14px;
|
||||||
border-radius: 5px;
|
line-height: 22px;
|
||||||
margin: 5px;
|
}
|
||||||
background-color: #eee;
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .goods-name {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-upload-list {
|
.demo-upload-list {
|
||||||
display: inline-block;
|
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
text-align: center;
|
border-radius: 6px;
|
||||||
line-height: 100px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 4px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #f5f5f5;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 1px 1px rgba(0, 0, 0, .2);
|
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||||
margin-right: 4px;
|
cursor: pointer;
|
||||||
margin-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-upload-list img {
|
.demo-upload-list img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-upload-list-cover {
|
.demo-upload-list-cover {
|
||||||
@@ -254,51 +358,70 @@ onMounted(() => {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: rgba(0, 0, 0, .6);
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-upload-list:hover .demo-upload-list-cover {
|
.demo-upload-list:hover .demo-upload-list-cover {
|
||||||
display: block;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.demo-upload-list-cover i {
|
.chat-list {
|
||||||
color: #fff;
|
display: flex;
|
||||||
font-size: 30px;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
gap: 12px;
|
||||||
margin: 0 2px;
|
padding: 16px 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-upload {
|
.chat-item {
|
||||||
width: 58px;
|
display: flex;
|
||||||
height: 58px;
|
flex-direction: column;
|
||||||
line-height: 58px;
|
align-items: flex-start;
|
||||||
text-align: center;
|
max-width: 80%;
|
||||||
display: inline-block;
|
|
||||||
border: 1px dashed #999;
|
|
||||||
border-radius: 4px;
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:hover {
|
&.is-buyer {
|
||||||
cursor: pointer;
|
align-self: flex-end;
|
||||||
border-color: $theme_color;
|
align-items: flex-end;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.l_title {
|
.chat-bubble {
|
||||||
margin: 10px 0;
|
background: $theme_color;
|
||||||
}
|
color: #fff;
|
||||||
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
border-color: #eee;
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
&:nth-child(1) {
|
|
||||||
width: 120px;
|
|
||||||
color: #999;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-platform .chat-bubble {
|
||||||
|
background: #fdf3e4;
|
||||||
|
color: #8a5a00;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-meta {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-bubble {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-box {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-btn {
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -32,17 +32,19 @@
|
|||||||
{{ item.goodsName }}
|
{{ item.goodsName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="complain-cell">{{ statusLabel[item.complainStatus] || item.complainStatus }}</div>
|
<div class="complain-cell">
|
||||||
|
<el-tag size="small" :type="statusTagType(item.complainStatus)">{{ statusLabel[item.complainStatus] || item.complainStatus }}</el-tag>
|
||||||
|
</div>
|
||||||
<div class="complain-topic">{{ item.complainTopic }}</div>
|
<div class="complain-topic">{{ item.complainTopic }}</div>
|
||||||
<div class="complain-content">{{ item.content }}</div>
|
<div class="complain-content">{{ item.content }}</div>
|
||||||
<div class="complain-ops">
|
<div class="complain-ops">
|
||||||
<button type="button" class="text-link" @click="goDetail(item.id)">投诉详情</button>
|
<el-button link type="primary" @click="goDetail(item.id)">投诉详情</el-button>
|
||||||
<button
|
<el-button
|
||||||
v-if="item.complainStatus === 'APPLYING' || item.complainStatus === 'NEW'"
|
v-if="item.complainStatus === 'APPLYING' || item.complainStatus === 'NEW'"
|
||||||
type="button"
|
link
|
||||||
class="text-link"
|
type="danger"
|
||||||
@click="cancel(item.id)"
|
@click="cancel(item.id)"
|
||||||
>取消投诉</button>
|
>取消投诉</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +86,18 @@ const params = reactive({ // 请求参数
|
|||||||
pageSize: 20
|
pageSize: 20
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function statusTagType(status) { // 投诉状态标签类型
|
||||||
|
const map = {
|
||||||
|
COMPLETE: 'success',
|
||||||
|
APPLYING: 'warning',
|
||||||
|
NEW: 'warning',
|
||||||
|
CANCEL: 'info',
|
||||||
|
EXPIRED: 'info',
|
||||||
|
NO_APPLY: 'info'
|
||||||
|
};
|
||||||
|
return map[status] || 'info';
|
||||||
|
}
|
||||||
|
|
||||||
function getList() { // 获取投诉列表
|
function getList() { // 获取投诉列表
|
||||||
complainList(params).then(res => {
|
complainList(params).then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -268,20 +282,9 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
.el-button + .el-button {
|
||||||
border: none;
|
margin-left: 0;
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
0
buyer/src/pages/home/memberCenter/EvalDetail.vue
Normal file
0
buyer/src/pages/home/memberCenter/EvalDetail.vue
Normal file
@@ -33,10 +33,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="params.type === 'GOODS'" class="fav-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
<div v-if="params.type === 'GOODS'" class="fav-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
||||||
<div class="fav-ops">
|
<div class="fav-ops">
|
||||||
<button v-if="params.type === 'GOODS'" type="button" class="op-btn" @click="buynow(item.skuId, item.goodsId)">立即购买</button>
|
<el-button v-if="params.type === 'GOODS'" type="primary" size="small" @click="buynow(item.skuId, item.goodsId)">立即购买</el-button>
|
||||||
<button v-else type="button" class="op-btn" @click="goShop(item.id)">查看店铺</button>
|
<el-button v-else type="primary" size="small" @click="goShop(item.id)">查看店铺</el-button>
|
||||||
<button v-if="params.type === 'GOODS'" type="button" class="text-link" @click="cancel(item.skuId)">取消收藏</button>
|
<el-button v-if="params.type === 'GOODS'" link type="danger" @click="cancel(item.skuId)">取消收藏</el-button>
|
||||||
<button v-else type="button" class="text-link" @click="cancelStore(item.id)">取消收藏</button>
|
<el-button v-else link type="danger" @click="cancelStore(item.id)">取消收藏</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
||||||
@@ -72,10 +72,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="params.type === 'GOODS'" class="fav-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
<div v-if="params.type === 'GOODS'" class="fav-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
|
||||||
<div class="fav-ops">
|
<div class="fav-ops">
|
||||||
<button v-if="params.type === 'GOODS'" type="button" class="op-btn" @click="buynow(item.skuId, item.goodsId)">立即购买</button>
|
<el-button v-if="params.type === 'GOODS'" type="primary" size="small" @click="buynow(item.skuId, item.goodsId)">立即购买</el-button>
|
||||||
<button v-else type="button" class="op-btn" @click="goShop(item.id)">查看店铺</button>
|
<el-button v-else type="primary" size="small" @click="goShop(item.id)">查看店铺</el-button>
|
||||||
<button v-if="params.type === 'GOODS'" type="button" class="text-link" @click="cancel(item.skuId)">取消收藏</button>
|
<el-button v-if="params.type === 'GOODS'" link type="danger" @click="cancel(item.skuId)">取消收藏</el-button>
|
||||||
<button v-else type="button" class="text-link" @click="cancelStore(item.id)">取消收藏</button>
|
<el-button v-else link type="danger" @click="cancelStore(item.id)">取消收藏</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
||||||
@@ -316,7 +316,7 @@ onMounted(() => {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fff1f0;
|
background: color-mix(in srgb, #{$theme_color} 8%, #fff);
|
||||||
color: $theme_color;
|
color: $theme_color;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
@@ -336,37 +336,4 @@ onMounted(() => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn {
|
|
||||||
min-width: 96px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: $theme_color;
|
|
||||||
color: #fff;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
51
buyer/src/pages/home/memberCenter/MemberBenefitPanel.vue
Normal file
51
buyer/src/pages/home/memberCenter/MemberBenefitPanel.vue
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<section class="benefits-panel">
|
||||||
|
<h3>会员权益</h3>
|
||||||
|
<div class="grade-options">
|
||||||
|
<button v-for="item in enabledGrades" :key="item.grade.id" :class="{active: selectedId === item.grade.id}" @click="selectedId = item.grade.id">
|
||||||
|
{{ item.grade.gradeName }}<small v-if="item.grade.id === currentGradeId">当前等级</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="benefit-subtitle">{{ selected?.grade?.gradeName || '暂无等级' }} · 点击查看权益说明</p>
|
||||||
|
<el-empty v-if="!selected?.benefits?.length" description="暂无权益" />
|
||||||
|
<div v-else class="benefits-grid">
|
||||||
|
<button v-for="benefit in selected.benefits" :key="benefit.id" class="benefit-card" :class="{locked: !owned(benefit)}" @click="detail = benefit">
|
||||||
|
<img v-if="benefit.benefitLogo" :src="benefit.benefitLogo" alt="" />
|
||||||
|
<strong>{{ benefit.benefitName }}</strong>
|
||||||
|
<span>{{ owned(benefit) ? '已享有' : '该等级专享' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<el-dialog :model-value="!!detail" :title="detail?.benefitName || '权益说明'" width="520px" @close="detail = null">
|
||||||
|
<template v-if="detail">
|
||||||
|
<p>{{ owned(detail) ? '当前等级已享有该权益' : '此权益属于所选等级,当前尚未享有' }}</p>
|
||||||
|
<p v-if="detail.benefitType === 'POINT_MULTIPLIER'">购物积分倍率:{{ config(detail).multiplier || 1 }} 倍,同类权益取最高值。</p>
|
||||||
|
<p v-if="detail.benefitType === 'GIFT_POINT'">首次达到等级赠送 {{ config(detail).giftPoint || 0 }} 积分。</p>
|
||||||
|
<div class="benefit-description" v-html="detail.benefitDesc || '暂无说明'" />
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
const props = defineProps({gradeDetails:{type:Array,default:()=>[]},currentGradeId:{type:String,default:''}})
|
||||||
|
const selectedId = ref(''), detail = ref(null)
|
||||||
|
const enabledGrades = computed(()=>props.gradeDetails.filter(item=>item.grade?.gradeState === 'OPEN'))
|
||||||
|
const selected = computed(()=>enabledGrades.value.find(item=>item.grade.id === selectedId.value))
|
||||||
|
const current = computed(()=>enabledGrades.value.find(item=>item.grade.id === props.currentGradeId))
|
||||||
|
const owned = benefit => (current.value?.benefits || []).some(item=>item.id === benefit.id)
|
||||||
|
function config(benefit) { try { return JSON.parse(benefit.benefitConfig || '{}') } catch { return {} } }
|
||||||
|
watch([()=>props.currentGradeId,enabledGrades],()=>{
|
||||||
|
if(!selectedId.value || !enabledGrades.value.some(item=>item.grade.id===selectedId.value)) selectedId.value=props.currentGradeId || enabledGrades.value[0]?.grade.id || ''
|
||||||
|
},{immediate:true})
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.benefits-panel { margin:20px 0; padding:24px; background:#fff; border:1px solid #eee; border-radius:8px; }
|
||||||
|
h3 { margin:0 0 20px; font-size:18px; } .grade-options { display:flex; flex-wrap:wrap; gap:10px; }
|
||||||
|
.grade-options button { padding:10px 18px; border:1px solid #ddd; background:#fff; border-radius:6px; cursor:pointer; }
|
||||||
|
.grade-options button.active { background:#fff8eb; border-color:#c99a47; color:#866021; } small { margin-left:8px; }
|
||||||
|
.benefit-subtitle { color:#777; margin:20px 0; }.benefits-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:16px; }
|
||||||
|
.benefit-card { padding:20px 10px; display:flex; align-items:center; flex-direction:column; gap:12px; border:1px solid #f0e3cb; background:#fffcf6; border-radius:8px; cursor:pointer; }
|
||||||
|
.benefit-card img { width:48px; height:48px; object-fit:contain; }.benefit-card span { font-size:12px; color:#986d2a; }
|
||||||
|
.benefit-card.locked { background:#fafafa; border-color:#eee; color:#888; }.locked img { filter:grayscale(1); opacity:.5; }
|
||||||
|
.benefit-description { line-height:1.8; overflow-wrap:anywhere; }.benefit-description :deep(img) { max-width:100%; }
|
||||||
|
</style>
|
||||||
@@ -9,63 +9,76 @@
|
|||||||
<span v-for="b in currentGradeBenefits" :key="b.id" class="benefit-tag">{{ b.benefitName || '-' }}</span>
|
<span v-for="b in currentGradeBenefits" :key="b.id" class="benefit-tag">{{ b.benefitName || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grade-exp">当前经验值:<span class="num">{{ currentExperience || 0 }}</span></div>
|
<div class="grade-exp">当前经验值:<span class="num">{{ currentExperience || 0 }}</span></div>
|
||||||
<div class="grade-next" v-if="nextGrade">
|
<template v-if="nextGrade">
|
||||||
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
|
<div class="grade-next">
|
||||||
</div>
|
距 {{ nextGrade.gradeName }} 还差 <span class="num">{{ diffExperience }}</span> 经验值
|
||||||
|
</div>
|
||||||
|
<el-progress
|
||||||
|
class="grade-progress"
|
||||||
|
:percentage="progressPercent"
|
||||||
|
:stroke-width="8"
|
||||||
|
:show-text="false"
|
||||||
|
color="#f0c46c"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
<div class="grade-next" v-else>已达最高等级</div>
|
<div class="grade-next" v-else>已达最高等级</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grade-tabs">
|
<MemberBenefitPanel :grade-details="gradeDetails" :current-grade-id="String(currentGradeId)" />
|
||||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
|
||||||
<div class="grade-tab" :class="{ active: activeGradeTab === 'logs' }" @click="activeGradeTab = 'logs'">经验值记录</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="activeGradeTab === 'rules'" v-loading="rulesLoading">
|
<div class="grade-panel">
|
||||||
<empty v-if="!rulesLoading && !ruleList.length" />
|
<div class="grade-tabs">
|
||||||
<div v-else class="rule-list">
|
<div class="grade-tab" :class="{ active: activeGradeTab === 'rules' }" @click="activeGradeTab = 'rules'">获取经验值方式</div>
|
||||||
<div class="rule-item" v-for="row in ruleList" :key="row.ruleKey || row.id">
|
<div class="grade-tab" :class="{ active: activeGradeTab === 'logs' }" @click="activeGradeTab = 'logs'">经验值记录</div>
|
||||||
<div class="rule-top">
|
|
||||||
<div class="rule-name">{{ row.ruleName || row.ruleKey || '-' }}</div>
|
|
||||||
<span class="rule-status" :class="{ done: row.completed === true }">{{ row.completed === true ? '已完成' : '未完成' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="rule-desc">{{ getRuleDesc(row.ruleKey) }}</div>
|
|
||||||
<div class="rule-meta">
|
|
||||||
<span>单次获得 <b>+{{ row.value == null ? 0 : row.value }}</b></span>
|
|
||||||
<span v-if="row.maxValue != null && Number(row.maxValue) > 0">限额 <b>{{ row.maxValue }}</b></span>
|
|
||||||
<span>已获得 <b>{{ row.gainedExperience == null ? 0 : row.gainedExperience }}</b></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="rules-desc" v-if="rulesDescription">{{ rulesDescription }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="activeGradeTab === 'logs'" v-loading="logLoading">
|
<div v-show="activeGradeTab === 'rules'" v-loading="rulesLoading">
|
||||||
<empty v-if="!(experienceLogs.records || []).length" />
|
<empty v-if="!rulesLoading && !ruleList.length" />
|
||||||
<div v-else class="log-list">
|
<div v-else class="rule-list">
|
||||||
<div class="log-head">
|
<div class="rule-item" v-for="row in ruleList" :key="row.ruleKey || row.id">
|
||||||
<span>时间</span>
|
<div class="rule-top">
|
||||||
<span>规则</span>
|
<div class="rule-name">{{ row.ruleName || row.ruleKey || '-' }}</div>
|
||||||
<span>变动值</span>
|
<span class="rule-status" :class="{ done: row.completed === true }">{{ row.completed === true ? '已完成' : '未完成' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-row" v-for="(row, index) in experienceLogs.records" :key="row.id || index">
|
<div class="rule-desc">{{ getRuleDesc(row.ruleKey) }}</div>
|
||||||
<div class="log-time">{{ row.createTime }}</div>
|
<div class="rule-meta">
|
||||||
<div class="log-rule">{{ getRuleName(row.ruleKey) }}</div>
|
<span>单次获得 <b>+{{ row.value == null ? 0 : row.value }}</b></span>
|
||||||
<div class="log-point" :class="getLogDeltaSign(row) === '+' ? 'is-plus' : 'is-minus'">
|
<span v-if="row.maxValue != null && Number(row.maxValue) > 0">限额 <b>{{ row.maxValue }}</b></span>
|
||||||
{{ getLogDeltaSign(row) }}{{ getLogDeltaAbsValue(row) }}
|
<span>已获得 <b>{{ row.gainedExperience == null ? 0 : row.gainedExperience }}</b></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="rules-desc" v-if="rulesDescription">{{ rulesDescription }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grade-pagination">
|
|
||||||
<el-pagination
|
<div v-show="activeGradeTab === 'logs'" v-loading="logLoading">
|
||||||
v-model:current-page="params.pageNumber"
|
<empty v-if="!(experienceLogs.records || []).length" />
|
||||||
v-model:page-size="params.pageSize"
|
<div v-else class="log-list">
|
||||||
:total="experienceLogs.total || 0"
|
<div class="log-head">
|
||||||
@current-change="changePage"
|
<span>时间</span>
|
||||||
@size-change="changePageSize"
|
<span>规则</span>
|
||||||
:page-sizes="[20, 50, 100]"
|
<span>变动值</span>
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
</div>
|
||||||
/>
|
<div class="log-row" v-for="(row, index) in experienceLogs.records" :key="row.id || index">
|
||||||
|
<div class="log-time">{{ row.createTime }}</div>
|
||||||
|
<div class="log-rule">{{ getRuleName(row.ruleKey) }}</div>
|
||||||
|
<div class="log-point" :class="getLogDeltaSign(row) === '+' ? 'is-plus' : 'is-minus'">
|
||||||
|
{{ getLogDeltaSign(row) }}{{ getLogDeltaAbsValue(row) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grade-pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
:total="experienceLogs.total || 0"
|
||||||
|
@current-change="changePage"
|
||||||
|
@size-change="changePageSize"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UserCenterLayout>
|
</UserCenterLayout>
|
||||||
@@ -76,6 +89,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import MemberBenefitPanel from './MemberBenefitPanel.vue'
|
||||||
import { memberGradeCurrent, memberGradeList, memberGradeRules } from '@/api/member.js'
|
import { memberGradeCurrent, memberGradeList, memberGradeRules } from '@/api/member.js'
|
||||||
|
|
||||||
defineOptions({ name: 'MemberGrade' })
|
defineOptions({ name: 'MemberGrade' })
|
||||||
@@ -92,7 +106,6 @@ const currentGradeId = ref('')
|
|||||||
const selectedRuleKey = ref('')
|
const selectedRuleKey = ref('')
|
||||||
const activeGradeTab = ref('rules')
|
const activeGradeTab = ref('rules')
|
||||||
const logLoading = ref(false)
|
const logLoading = ref(false)
|
||||||
const gradeLoading = ref(false)
|
|
||||||
const params = reactive({
|
const params = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 20
|
pageSize: 20
|
||||||
@@ -137,6 +150,15 @@ const diffExperience = computed(() => {
|
|||||||
return diff > 0 ? diff : 0
|
return diff > 0 ? diff : 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 当前经验占下一级所需经验的百分比(无下一级时视为 100%) */
|
||||||
|
const progressPercent = computed(() => {
|
||||||
|
if (!nextGrade.value || !nextGrade.value.requiredExperience) return 100
|
||||||
|
const required = Number(nextGrade.value.requiredExperience)
|
||||||
|
if (!required || required <= 0) return 100
|
||||||
|
const percent = Math.round((Number(currentExperience.value || 0) / required) * 100)
|
||||||
|
return Math.min(100, Math.max(0, percent))
|
||||||
|
})
|
||||||
|
|
||||||
/** 当前等级在列表详情中对应的启用权益(顺序与后台 benefits 一致) */
|
/** 当前等级在列表详情中对应的启用权益(顺序与后台 benefits 一致) */
|
||||||
const currentGradeBenefits = computed(() => {
|
const currentGradeBenefits = computed(() => {
|
||||||
const id = currentGradeId.value
|
const id = currentGradeId.value
|
||||||
@@ -174,7 +196,6 @@ function loadRules () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadGradeList () {
|
function loadGradeList () {
|
||||||
gradeLoading.value = true
|
|
||||||
memberGradeList()
|
memberGradeList()
|
||||||
.then(res => {
|
.then(res => {
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
@@ -197,9 +218,6 @@ function loadGradeList () {
|
|||||||
gradeList.value = []
|
gradeList.value = []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
|
||||||
gradeLoading.value = false
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadCurrent () {
|
function loadCurrent () {
|
||||||
@@ -223,12 +241,6 @@ function loadCurrent () {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectRule (ruleKey) {
|
|
||||||
selectedRuleKey.value = ruleKey || ''
|
|
||||||
params.pageNumber = 1
|
|
||||||
loadCurrent()
|
|
||||||
}
|
|
||||||
|
|
||||||
function changePage (val) {
|
function changePage (val) {
|
||||||
params.pageNumber = val
|
params.pageNumber = val
|
||||||
loadCurrent()
|
loadCurrent()
|
||||||
@@ -283,30 +295,31 @@ function getLogDeltaAbsValue (row) {
|
|||||||
const num = getLogDeltaRaw(row)
|
const num = getLogDeltaRaw(row)
|
||||||
return Math.abs(num)
|
return Math.abs(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLogDeltaColor (row) {
|
|
||||||
return getLogDeltaRaw(row) >= 0 ? 'green' : 'red'
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.grade-card {
|
.grade-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 20px;
|
||||||
margin-bottom: 8px;
|
padding: 24px 28px;
|
||||||
padding: 16px;
|
border-radius: 12px;
|
||||||
background: #fafafa;
|
background: linear-gradient(135deg, #3a4157 0%, #23262f 100%);
|
||||||
border-radius: 8px;
|
box-shadow: 0 6px 20px rgba(35, 38, 47, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-icon {
|
.grade-icon {
|
||||||
width: 56px;
|
width: 64px;
|
||||||
height: 56px;
|
height: 64px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grade-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grade-name-row {
|
.grade-name-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -315,39 +328,57 @@ function getLogDeltaColor (row) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.grade-name {
|
.grade-name {
|
||||||
color: #171A26;
|
color: #f0c46c;
|
||||||
font-size: 18px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-tag {
|
.benefit-tag {
|
||||||
padding: 0 8px;
|
padding: 0 10px;
|
||||||
border-radius: 4px;
|
border: 1px solid rgba(240, 196, 108, 0.4);
|
||||||
background: #fff;
|
border-radius: 10px;
|
||||||
color: $theme_color;
|
background: rgba(240, 196, 108, 0.12);
|
||||||
font-size: 14px;
|
color: #f0d9a8;
|
||||||
line-height: 22px;
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-exp,
|
.grade-exp,
|
||||||
.grade-next {
|
.grade-next {
|
||||||
margin-top: 6px;
|
margin-top: 8px;
|
||||||
color: #666;
|
color: rgba(255, 255, 255, 0.75);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.num {
|
.num {
|
||||||
color: $theme_color;
|
color: #f0c46c;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grade-progress {
|
||||||
|
margin-top: 10px;
|
||||||
|
|
||||||
|
:deep(.el-progress-bar__outer) {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-panel {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 4px 20px 20px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.grade-tabs {
|
.grade-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
margin: 16px 0;
|
margin-bottom: 16px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
<template #append><span>输入验证码</span></template>
|
<template #append><span>输入验证码</span></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="submitRegister">下一步</el-button>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="(status == 2 || status == 3) && !showPwd" style="text-align: center;width: 50%">
|
|
||||||
<el-button type="primary" class="ml_10" @click="submitRegister">下一步</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 修改 -->
|
<!-- 修改 -->
|
||||||
<el-form ref="formRegisterRef" :model="formRegister" :rules="ruleInline" label-width="80px" v-if="status == 1 && !showPwd">
|
<el-form ref="formRegisterRef" :model="formRegister" :rules="ruleInline" label-width="80px" v-if="status == 1 && !showPwd">
|
||||||
@@ -43,11 +43,10 @@
|
|||||||
<template #prepend><el-icon><Lock /></el-icon></template>
|
<template #prepend><el-icon><Lock /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleRegist">修改</el-button>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="status == 1 && !showPwd" style="width: 50%;text-align: center">
|
|
||||||
<el-button type="primary" class="ml_10" @click="handleRegist">修改</el-button>
|
|
||||||
</div>
|
|
||||||
<!-- 设置&修改的第二种情况 -->
|
<!-- 设置&修改的第二种情况 -->
|
||||||
<el-form ref="formDataUpdateRef" :model="formDataUpdate" label-position="left" label-width="100px" :rules="ruleIn"
|
<el-form ref="formDataUpdateRef" :model="formDataUpdate" label-position="left" label-width="100px" :rules="ruleIn"
|
||||||
v-if="showPwd">
|
v-if="showPwd">
|
||||||
@@ -65,10 +64,10 @@
|
|||||||
<template #prepend><el-icon><Lock /></el-icon></template>
|
<template #prepend><el-icon><Lock /></el-icon></template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="setPassword">设置</el-button>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="showPwd" style="text-align: center;width: 50%">
|
|
||||||
<el-button type="primary" class="ml_10" @click="setPassword">设置</el-button>
|
|
||||||
</div>
|
|
||||||
</UserCenterLayout>
|
</UserCenterLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -252,9 +251,6 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wrapper {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.phone {
|
.phone {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ onMounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h3 {
|
h3 {
|
||||||
margin: 8px 0 12px;
|
margin: 8px 0 12px;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-summary {
|
.point-summary {
|
||||||
@@ -102,7 +102,8 @@ h3 {
|
|||||||
.point-stat {
|
.point-stat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
background: #fafafa;
|
background: #fff;
|
||||||
|
border: 1px solid #eee;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:headers="accessToken"
|
:headers="accessToken"
|
||||||
>
|
>
|
||||||
<button type="button" class="ghost-btn">上传头像</button>
|
<el-button size="small">上传头像</el-button>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -29,13 +29,13 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="性别">
|
<el-form-item label="性别">
|
||||||
<div class="sex-group">
|
<el-radio-group v-model="formItem.sex">
|
||||||
<button type="button" class="sex-btn" :class="{ active: formItem.sex === 1 }" @click="formItem.sex = 1">男</button>
|
<el-radio-button :value="1">男</el-radio-button>
|
||||||
<button type="button" class="sex-btn" :class="{ active: formItem.sex === 0 }" @click="formItem.sex = 0">女</button>
|
<el-radio-button :value="0">女</el-radio-button>
|
||||||
</div>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<button type="button" class="op-btn" @click="save">确认修改</button>
|
<el-button type="primary" @click="save">确认修改</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</UserCenterLayout>
|
</UserCenterLayout>
|
||||||
@@ -167,7 +167,7 @@ function handleSuccess (res, file) { // 上传成功
|
|||||||
:deep(.el-form-item__label) {
|
:deep(.el-form-item__label) {
|
||||||
height: auto;
|
height: auto;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
color: #666;
|
color: #999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,51 +199,5 @@ function handleSuccess (res, file) { // 上传成功
|
|||||||
:deep(.profile-field.el-date-editor) {
|
:deep(.profile-field.el-date-editor) {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sex-group {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sex-btn,
|
|
||||||
.ghost-btn,
|
|
||||||
.op-btn {
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sex-btn,
|
|
||||||
.ghost-btn {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: #fff;
|
|
||||||
color: #171A26;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $theme_color;
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sex-btn.active {
|
|
||||||
border-color: $theme_color;
|
|
||||||
background: #fff5f4;
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.op-btn {
|
|
||||||
min-width: 96px;
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
background: $theme_color;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="order-card status-card" v-if="orderGoods.goodsName || orderGoods.content">
|
<div class="order-card status-card" v-if="orderGoods.goodsName || orderGoods.content">
|
||||||
<div class="status-head">
|
<div class="status-head">
|
||||||
<div class="status-main">
|
<div class="status-main">
|
||||||
<p class="status-text">{{ gradeText }}</p>
|
<el-tag :type="gradeTagType" effect="dark">{{ gradeText }}</el-tag>
|
||||||
<p class="status-sn">评价时间:{{ orderGoods.createTime }}</p>
|
<p class="status-sn">评价时间:{{ orderGoods.createTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,6 +99,10 @@ const gradeText = computed(() => {
|
|||||||
const map = { GOOD: '好评', MODERATE: '中评', WORSE: '差评' }
|
const map = { GOOD: '好评', MODERATE: '中评', WORSE: '差评' }
|
||||||
return map[orderGoods.value.grade] || '评价详情'
|
return map[orderGoods.value.grade] || '评价详情'
|
||||||
});
|
});
|
||||||
|
const gradeTagType = computed(() => {
|
||||||
|
const map = { GOOD: 'success', MODERATE: 'warning', WORSE: 'info' }
|
||||||
|
return map[orderGoods.value.grade] || 'info'
|
||||||
|
});
|
||||||
|
|
||||||
function getDetail() {
|
function getDetail() {
|
||||||
// 获取评价详情
|
// 获取评价详情
|
||||||
@@ -130,33 +134,23 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-card {
|
.order-card {
|
||||||
padding: 4px 0 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 8px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 15px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
}
|
|
||||||
|
|
||||||
&:last-of-type {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.status-card {
|
||||||
padding-top: 8px;
|
margin-top: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
margin: 0;
|
|
||||||
color: $theme_color;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-sn {
|
.status-sn {
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
<div class="order-sn">
|
<div class="order-sn">
|
||||||
{{ order.createTime }}<span>售后单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
{{ order.createTime }}<span>售后单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-status">{{ filterOrderStatus(order.serviceStatus) }}</div>
|
<div class="order-status">
|
||||||
|
<el-tag size="small" :type="statusTagType(order.serviceStatus)">{{ filterOrderStatus(order.serviceStatus) }}</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-body">
|
<div class="order-body">
|
||||||
<div class="col-detail">
|
<div class="col-detail">
|
||||||
@@ -59,19 +61,19 @@
|
|||||||
<div class="order-amount">{{ $filters.unitPrice(order.applyRefundPrice, "¥") }}</div>
|
<div class="order-amount">{{ $filters.unitPrice(order.applyRefundPrice, "¥") }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-ops">
|
<div class="col-ops">
|
||||||
<button
|
<el-button
|
||||||
v-if="order.serviceStatus == 'PASS' && order.serviceType != 'RETURN_MONEY'"
|
v-if="order.serviceStatus == 'PASS' && order.serviceType != 'RETURN_MONEY'"
|
||||||
type="button"
|
type="primary"
|
||||||
class="op-btn"
|
size="small"
|
||||||
@click="openModal(order)"
|
@click="openModal(order)"
|
||||||
>提交物流</button>
|
>提交物流</el-button>
|
||||||
<button type="button" class="text-link" @click="goDetail(order.sn)">售后详情</button>
|
<el-button link type="primary" @click="goDetail(order.sn)">售后详情</el-button>
|
||||||
<button
|
<el-button
|
||||||
v-if="order.afterSaleAllowOperationVO && order.afterSaleAllowOperationVO.cancel"
|
v-if="order.afterSaleAllowOperationVO && order.afterSaleAllowOperationVO.cancel"
|
||||||
type="button"
|
link
|
||||||
class="text-link"
|
type="danger"
|
||||||
@click="cancel(order.sn)"
|
@click="cancel(order.sn)"
|
||||||
>取消售后</button>
|
>取消售后</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +255,14 @@ function filterOrderStatus(status) { // 获取订单状态中文
|
|||||||
});
|
});
|
||||||
return ob && ob[0] ? ob[0].name : status
|
return ob && ob[0] ? ob[0].name : status
|
||||||
}
|
}
|
||||||
|
function statusTagType(status) { // 售后状态对应标签类型
|
||||||
|
const map = {
|
||||||
|
APPLY: 'warning', APPLYING: 'warning',
|
||||||
|
COMPLETE: 'success',
|
||||||
|
CANCEL: 'info', EXPIRED: 'info'
|
||||||
|
}
|
||||||
|
return map[status] || 'info'
|
||||||
|
}
|
||||||
// 获取物流公司列表
|
// 获取物流公司列表
|
||||||
function getCompany() {
|
function getCompany() {
|
||||||
getLogisticsCompany().then(res => {
|
getLogisticsCompany().then(res => {
|
||||||
@@ -372,7 +382,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #171A26;
|
color: #333;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@@ -476,38 +486,9 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.op-btn {
|
:deep(.el-button + .el-button) {
|
||||||
min-width: 96px;
|
margin-left: 0;
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: $theme_color;
|
|
||||||
color: #fff;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="order-detail">
|
<div class="order-detail">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
<div class="order-card status-card" v-if="afterSale.sn">
|
<div class="panel" v-if="afterSale.sn">
|
||||||
<div class="status-head">
|
<div class="status-head">
|
||||||
<div class="status-main">
|
<div class="status-main">
|
||||||
<p class="status-text">{{ afterSale.serviceName }}</p>
|
<el-tag :type="statusTagType" effect="dark">{{ afterSale.serviceName }}</el-tag>
|
||||||
<p class="status-sn">
|
<p class="status-sn">
|
||||||
售后单号:{{ afterSale.sn }}
|
售后单号:{{ afterSale.sn }}
|
||||||
<span>订单号:{{ afterSale.orderSn }}</span>
|
<span>订单号:{{ afterSale.orderSn }}</span>
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
<div class="status-side">
|
<div class="status-side">
|
||||||
<div class="operation-time">创建时间:{{ afterSale.createTime }}</div>
|
<div class="operation-time">创建时间:{{ afterSale.createTime }}</div>
|
||||||
<div class="status-actions" v-if="canSubmitLogistics || canCancel">
|
<div class="status-actions" v-if="canSubmitLogistics || canCancel">
|
||||||
<button v-if="canSubmitLogistics" type="button" class="op-btn" @click="openModal">提交物流</button>
|
<el-button v-if="canSubmitLogistics" type="primary" size="small" @click="openModal">提交物流</el-button>
|
||||||
<button v-if="canCancel" type="button" class="text-link" @click="cancel(afterSale.sn)">取消售后</button>
|
<el-button v-if="canCancel" link type="primary" @click="cancel(afterSale.sn)">取消售后</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card" v-if="logList.length">
|
<div class="panel" v-if="logList.length">
|
||||||
<h3>售后进程</h3>
|
<h3>售后进程</h3>
|
||||||
<el-steps
|
<el-steps
|
||||||
class="progress"
|
class="progress"
|
||||||
@@ -43,50 +43,48 @@
|
|||||||
></el-step>
|
></el-step>
|
||||||
</el-steps>
|
</el-steps>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card">
|
<div class="panel">
|
||||||
<h3>服务单信息</h3>
|
<h3>服务单信息</h3>
|
||||||
<div class="info-list">
|
<div class="info-row">
|
||||||
|
<span class="info-label">退款方式</span>
|
||||||
|
<span class="info-value">{{ afterSale.refundWay == 'ORIGINAL' ? '原路退回' : '账号退款' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">申请退款金额</span>
|
||||||
|
<span class="info-value">{{ $filters.unitPrice(afterSale.applyRefundPrice, '¥') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="afterSale.actualRefundPrice">
|
||||||
|
<span class="info-label">实际退款金额</span>
|
||||||
|
<span class="info-value">{{ $filters.unitPrice(afterSale.actualRefundPrice, '¥') }}</span>
|
||||||
|
</div>
|
||||||
|
<template v-if="afterSale.refundWay === 'OFFLINE'">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">退款方式</span>
|
<span class="info-label">退款开户行</span>
|
||||||
<span>{{ afterSale.refundWay == 'ORIGINAL' ? '原路退回' : '账号退款' }}</span>
|
<span class="info-value">{{ afterSale.bankDepositName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">申请退款金额</span>
|
<span class="info-label">退款开户名</span>
|
||||||
<span>{{ $filters.unitPrice(afterSale.applyRefundPrice, '¥') }}</span>
|
<span class="info-value">{{ afterSale.bankAccountName }}</span>
|
||||||
</div>
|
|
||||||
<div class="info-row" v-if="afterSale.actualRefundPrice">
|
|
||||||
<span class="label">实际退款金额</span>
|
|
||||||
<span>{{ $filters.unitPrice(afterSale.actualRefundPrice, '¥') }}</span>
|
|
||||||
</div>
|
|
||||||
<template v-if="afterSale.refundWay === 'OFFLINE'">
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">退款开户行</span>
|
|
||||||
<span>{{ afterSale.bankDepositName }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">退款开户名</span>
|
|
||||||
<span>{{ afterSale.bankAccountName }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">退款卡号</span>
|
|
||||||
<span>{{ afterSale.bankAccountNumber }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">商品处理方式</span>
|
|
||||||
<span>{{ afterSale.serviceType == 'RETURN_MONEY' ? '退款' : '退货' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="label">退款原因</span>
|
<span class="info-label">退款卡号</span>
|
||||||
<span>{{ afterSale.reasonName }}</span>
|
<span class="info-value">{{ afterSale.bankAccountNumber }}</span>
|
||||||
</div>
|
|
||||||
<div class="info-row">
|
|
||||||
<span class="label">问题描述</span>
|
|
||||||
<span>{{ afterSale.problemDesc }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">商品处理方式</span>
|
||||||
|
<span class="info-value">{{ afterSale.serviceType == 'RETURN_MONEY' ? '退款' : '退货' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">退款原因</span>
|
||||||
|
<span class="info-value">{{ afterSale.reasonName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">问题描述</span>
|
||||||
|
<span class="info-value">{{ afterSale.problemDesc }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card" v-if="afterSale.afterSaleImage">
|
<div class="panel" v-if="afterSale.afterSaleImage">
|
||||||
<h3>图片信息</h3>
|
<h3>图片信息</h3>
|
||||||
<div class="image-list">
|
<div class="image-list">
|
||||||
<img
|
<img
|
||||||
@@ -175,6 +173,22 @@ const canCancel = computed(() => {
|
|||||||
return afterSale.value.afterSaleAllowOperationVO && afterSale.value.afterSaleAllowOperationVO.cancel
|
return afterSale.value.afterSaleAllowOperationVO && afterSale.value.afterSaleAllowOperationVO.cancel
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const statusTagType = computed(() => {
|
||||||
|
const map = {
|
||||||
|
APPLY: 'warning',
|
||||||
|
PASS: 'success',
|
||||||
|
REFUSE: 'info',
|
||||||
|
BUYER_RETURN: 'warning',
|
||||||
|
SELLER_CONFIRM: 'warning',
|
||||||
|
SELLER_TERMINATION: 'info',
|
||||||
|
BUYER_CONFIRM: 'warning',
|
||||||
|
BUYER_CANCEL: 'info',
|
||||||
|
COMPLETE: 'success',
|
||||||
|
WAIT_REFUND: 'warning'
|
||||||
|
};
|
||||||
|
return map[afterSale.value.serviceStatus] || 'info';
|
||||||
|
});
|
||||||
|
|
||||||
function getDetail () { // 售后详情
|
function getDetail () { // 售后详情
|
||||||
afterSaleDetail(route.query.sn).then(res => {
|
afterSaleDetail(route.query.sn).then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -286,26 +300,22 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-card {
|
.panel {
|
||||||
padding: 4px 0 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 8px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 15px;
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-card {
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-head {
|
.status-head {
|
||||||
@@ -315,17 +325,16 @@ onMounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-main {
|
||||||
margin: 0;
|
display: flex;
|
||||||
color: $theme_color;
|
align-items: center;
|
||||||
font-size: 18px;
|
gap: 12px;
|
||||||
font-weight: 600;
|
flex-wrap: wrap;
|
||||||
line-height: 1.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-sn {
|
.status-sn {
|
||||||
margin: 6px 0 0;
|
margin: 0;
|
||||||
color: #171A26;
|
color: #999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|
||||||
@@ -340,7 +349,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.operation-time {
|
.operation-time {
|
||||||
color: #171A26;
|
color: #999;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
}
|
}
|
||||||
@@ -392,7 +401,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
span {
|
span {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
color: #438cde;
|
color: #333;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -435,35 +444,30 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-list {
|
|
||||||
border: 1px solid #f0f0f0;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 140px minmax(0, 1fr);
|
gap: 16px;
|
||||||
min-height: 44px;
|
padding: 12px 0;
|
||||||
border-top: 1px solid #f0f0f0;
|
border-bottom: 1px solid #f5f5f5;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 22px;
|
line-height: 22px;
|
||||||
|
|
||||||
&:first-child {
|
&:last-child {
|
||||||
border-top: none;
|
border-bottom: none;
|
||||||
|
padding-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
.info-label {
|
||||||
display: flex;
|
flex: 0 0 96px;
|
||||||
align-items: center;
|
color: #999;
|
||||||
padding: 10px 16px;
|
}
|
||||||
color: #171A26;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.info-value {
|
||||||
background: #fafafa;
|
flex: 1;
|
||||||
color: #666;
|
min-width: 0;
|
||||||
}
|
color: #333;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-list {
|
.image-list {
|
||||||
@@ -480,37 +484,4 @@ onMounted(() => {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn {
|
|
||||||
min-width: 96px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: $theme_color;
|
|
||||||
color: #fff;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="apply-after-sale">
|
<div class="apply-after-sale">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
<el-table :data="goodsData" border>
|
<div class="goods-row" v-for="(goods, index) in goodsData" :key="index">
|
||||||
<el-table-column label="商品名称">
|
<img class="goods-img" :src="goods.image" loading="lazy" decoding="async" alt="">
|
||||||
<template #default="{ row }">
|
<div class="goods-info">
|
||||||
<div style="padding:5px;display:flex;">
|
<div class="goods-name">{{ goods.goodsName }}</div>
|
||||||
<img :src="row.image" loading="lazy" decoding="async" style="vertical-align: top;" width="60" height="60" alt="">
|
<div class="goods-meta">
|
||||||
<span class="ml_10">{{ row.goodsName }}</span>
|
<span class="goods-price">{{ $filters.unitPrice(goods.applyRefundPrice, '¥') }}</span>
|
||||||
</div>
|
<span class="goods-num">×{{ goods.num }}</span>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
</div>
|
||||||
<el-table-column label="价格">
|
</div>
|
||||||
<template #default="{ row }">
|
|
||||||
<div>{{ $filters.unitPrice(row.applyRefundPrice, '¥') }}</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="num" label="购买数量" />
|
|
||||||
</el-table>
|
|
||||||
<div>
|
<div>
|
||||||
<el-form :model="form" ref="formRef" class="mt_10" :rules="rules" label-width="80px">
|
<el-form :model="form" ref="formRef" class="mt_10" :rules="rules" label-width="80px">
|
||||||
<el-form-item label="售后类别">
|
<el-form-item label="售后类别">
|
||||||
<el-radio-group v-model="form.serviceType" @change="changeReason" type="button" button-style="solid">
|
<el-radio-group v-model="form.serviceType" @change="changeReason">
|
||||||
<el-radio v-if="info.returnGoods" label="RETURN_GOODS">退货</el-radio>
|
<el-radio v-if="info.returnGoods" label="RETURN_GOODS">退货</el-radio>
|
||||||
<el-radio v-if="info.returnMoney" label="RETURN_MONEY">退款</el-radio>
|
<el-radio v-if="info.returnMoney" label="RETURN_MONEY">退款</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
@@ -224,6 +218,50 @@ onMounted(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
||||||
/** 商品信息 */
|
/** 商品信息 */
|
||||||
|
.goods-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-img {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
color: $theme_color;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-num {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.order-price {
|
.order-price {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
|
|||||||
@@ -164,17 +164,6 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.delivery-rate {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
height: 50px;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
>div:nth-child(1) {
|
|
||||||
width: 30%;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.goods-eval{
|
.goods-eval{
|
||||||
display: flex;
|
display: flex;
|
||||||
.goods-con {
|
.goods-con {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无收货地址" />
|
<empty v-else _Title="暂无收货地址" />
|
||||||
</UserCenterLayout>
|
</UserCenterLayout>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -130,7 +130,7 @@ onMounted(() => {
|
|||||||
.address-name {
|
.address-name {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: $light_title_color;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-mobile {
|
.address-mobile {
|
||||||
@@ -183,13 +183,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.del-btn:hover {
|
.del-btn:hover {
|
||||||
color: #f56c6c;
|
color: $theme_color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.default-address-tag {
|
.default-address-tag {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
background-color: #fff3f2 !important;
|
background-color: color-mix(in srgb, #{$theme_color} 8%, #fff) !important;
|
||||||
border: 1px solid $theme_color !important;
|
border: 1px solid $theme_color !important;
|
||||||
color: $theme_color !important;
|
color: $theme_color !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,11 @@
|
|||||||
<div class="order-sn" v-else>
|
<div class="order-sn" v-else>
|
||||||
{{ order.createTime }}<span>订单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
{{ order.createTime }}<span>订单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-status">{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}</div>
|
<div class="order-status">
|
||||||
|
<el-tag size="small" :type="statusTagType(order.isAfterSale ? order.serviceStatus : order.orderStatus)">
|
||||||
|
{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-body">
|
<div class="order-body">
|
||||||
<div class="col-detail">
|
<div class="col-detail">
|
||||||
@@ -64,12 +68,10 @@
|
|||||||
<div class="goods-name hover-color" @click="goodsDetail(goods.skuId, goods.goodsId)">{{ goods.name }}</div>
|
<div class="goods-name hover-color" @click="goodsDetail(goods.skuId, goods.goodsId)">{{ goods.name }}</div>
|
||||||
<div class="goods-num">共{{ goods.num }}件</div>
|
<div class="goods-num">共{{ goods.num }}件</div>
|
||||||
<div class="goods-extra">
|
<div class="goods-extra">
|
||||||
<span v-if="refundPriceList(goods.isRefund)" class="refund-tag">{{ refundPriceList(goods.isRefund) }}</span>
|
<el-tag v-if="refundPriceList(goods.isRefund) && !isUnpaid(order)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||||
<button v-if="goods.complainStatus == 'NO_APPLY'" type="button" class="sub-btn" @click="complain(order.sn, goodsIndex)">投诉</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="canAfterSale(order)" type="button" class="sub-btn" @click="applyAfterSale(order.orderItems)">申请售后</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-amount">
|
<div class="col-amount">
|
||||||
<div class="order-amount">{{ $filters.unitPrice(order.flowPrice, "¥") }}</div>
|
<div class="order-amount">{{ $filters.unitPrice(order.flowPrice, "¥") }}</div>
|
||||||
@@ -77,14 +79,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-ops">
|
<div class="col-ops">
|
||||||
<template v-if="order.isAfterSale">
|
<template v-if="order.isAfterSale">
|
||||||
<button type="button" class="text-link" @click="goAfterSaleDetail(order.serviceSn)">售后详情</button>
|
<el-button link type="primary" @click="goAfterSaleDetail(order.serviceSn)">售后详情</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.pay" type="button" class="op-btn" @click="goPay(order.sn)">去支付</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.pay" type="primary" size="small" @click="goPay(order.sn)">去支付</el-button>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.rog" type="button" class="op-btn" @click="received(order.sn)">确认收货</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.rog" type="primary" size="small" @click="received(order.sn)">确认收货</el-button>
|
||||||
<button type="button" class="text-link" @click="orderDetail(order.sn)">订单详情</button>
|
<el-button link type="primary" @click="orderDetail(order.sn)">订单详情</el-button>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.cancel" type="button" class="text-link" @click="handleCancelOrder(order.sn)">取消订单</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.cancel" link type="info" @click="handleCancelOrder(order.sn)">取消订单</el-button>
|
||||||
<button v-if="order.orderStatus === 'COMPLETED'" type="button" class="text-link" @click="delOrder(order.sn)">删除订单</button>
|
<el-button v-if="order.orderStatus === 'COMPLETED'" link type="info" @click="delOrder(order.sn)">删除订单</el-button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,7 +122,11 @@
|
|||||||
<div class="order-sn" v-else>
|
<div class="order-sn" v-else>
|
||||||
{{ order.createTime }}<span>订单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
{{ order.createTime }}<span>订单号 {{ order.sn }}</span><span class="order-store" @click="shopPage(order.storeId)">{{ order.storeName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-status">{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}</div>
|
<div class="order-status">
|
||||||
|
<el-tag size="small" :type="statusTagType(order.isAfterSale ? order.serviceStatus : order.orderStatus)">
|
||||||
|
{{ order.isAfterSale ? filterAfterSaleStatus(order.serviceStatus) : filterOrderStatus(order.orderStatus) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-body">
|
<div class="order-body">
|
||||||
<div class="col-detail">
|
<div class="col-detail">
|
||||||
@@ -144,12 +150,10 @@
|
|||||||
<div class="goods-name hover-color" @click="goodsDetail(goods.skuId, goods.goodsId)">{{ goods.name }}</div>
|
<div class="goods-name hover-color" @click="goodsDetail(goods.skuId, goods.goodsId)">{{ goods.name }}</div>
|
||||||
<div class="goods-num">共{{ goods.num }}件</div>
|
<div class="goods-num">共{{ goods.num }}件</div>
|
||||||
<div class="goods-extra">
|
<div class="goods-extra">
|
||||||
<span v-if="refundPriceList(goods.isRefund)" class="refund-tag">{{ refundPriceList(goods.isRefund) }}</span>
|
<el-tag v-if="refundPriceList(goods.isRefund) && !isUnpaid(order)" type="warning" size="small">{{ refundPriceList(goods.isRefund) }}</el-tag>
|
||||||
<button v-if="goods.complainStatus == 'NO_APPLY'" type="button" class="sub-btn" @click="complain(order.sn, goodsIndex)">投诉</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="canAfterSale(order)" type="button" class="sub-btn" @click="applyAfterSale(order.orderItems)">申请售后</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-amount">
|
<div class="col-amount">
|
||||||
<div class="order-amount">{{ $filters.unitPrice(order.flowPrice, "¥") }}</div>
|
<div class="order-amount">{{ $filters.unitPrice(order.flowPrice, "¥") }}</div>
|
||||||
@@ -157,31 +161,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-ops">
|
<div class="col-ops">
|
||||||
<template v-if="order.isAfterSale">
|
<template v-if="order.isAfterSale">
|
||||||
<button type="button" class="text-link" @click="goAfterSaleDetail(order.serviceSn)">售后详情</button>
|
<el-button link type="primary" @click="goAfterSaleDetail(order.serviceSn)">售后详情</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.pay" type="button" class="op-btn" @click="goPay(order.sn)">去支付</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.pay" type="primary" size="small" @click="goPay(order.sn)">去支付</el-button>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.rog" type="button" class="op-btn" @click="received(order.sn)">确认收货</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.rog" type="primary" size="small" @click="received(order.sn)">确认收货</el-button>
|
||||||
<button type="button" class="text-link" @click="orderDetail(order.sn)">订单详情</button>
|
<el-button link type="primary" @click="orderDetail(order.sn)">订单详情</el-button>
|
||||||
<button v-if="order.allowOperationVO && order.allowOperationVO.cancel" type="button" class="text-link" @click="handleCancelOrder(order.sn)">取消订单</button>
|
<el-button v-if="order.allowOperationVO && order.allowOperationVO.cancel" link type="info" @click="handleCancelOrder(order.sn)">取消订单</el-button>
|
||||||
<button v-if="order.orderStatus === 'COMPLETED'" type="button" class="text-link" @click="delOrder(order.sn)">删除订单</button>
|
<el-button v-if="order.orderStatus === 'COMPLETED'" link type="info" @click="delOrder(order.sn)">删除订单</el-button>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
<el-skeleton size="large" fix v-if="spinShow"></el-skeleton>
|
||||||
</div>
|
</div>
|
||||||
<!-- 选择售后商品 -->
|
|
||||||
<el-dialog v-model="afterSaleModal" title="请选择申请售后的商品">
|
|
||||||
<div>
|
|
||||||
<el-table :data="afterSaleArr" border @row-click="afterSaleSelect">
|
|
||||||
<el-table-column prop="name" label="商品名称" />
|
|
||||||
<el-table-column prop="goodsPrice" label="价格" />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
<template #footer><div></div></template>
|
|
||||||
</el-dialog>
|
|
||||||
<el-dialog v-model="cancelAvail" title="请选择取消订单原因">
|
<el-dialog v-model="cancelAvail" title="请选择取消订单原因">
|
||||||
|
<el-alert v-if="cancelIsPickup" title="此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。" type="warning" :closable="false" style="margin-bottom:16px" />
|
||||||
<el-radio-group v-model="cancelParams.reason">
|
<el-radio-group v-model="cancelParams.reason">
|
||||||
<el-radio :label="item.reason" v-for="item in cancelReason" :key="item.id">
|
<el-radio :label="item.reason" v-for="item in cancelReason" :key="item.id">
|
||||||
{{ item.reason }}
|
{{ item.reason }}
|
||||||
@@ -233,8 +228,7 @@ const changeWay = ref(['全部', '待付款', '待发货', '待收货/使用']);
|
|||||||
const activeOrderTab = ref(0);
|
const activeOrderTab = ref(0);
|
||||||
const total = ref(0); // 数据总数
|
const total = ref(0); // 数据总数
|
||||||
const spinShow = ref(false); // 加载状态
|
const spinShow = ref(false); // 加载状态
|
||||||
const afterSaleModal = ref(false); // 选择售后商品模态框
|
const cancelIsPickup = ref(false);
|
||||||
const afterSaleArr = ref([]); // 售后商品列表
|
|
||||||
const cancelAvail = ref(false); // 取消订单modal控制
|
const cancelAvail = ref(false); // 取消订单modal控制
|
||||||
const cancelReason = ref([]); // 取消订单原因
|
const cancelReason = ref([]); // 取消订单原因
|
||||||
|
|
||||||
@@ -312,27 +306,6 @@ function received (sn) { // 确认收货
|
|||||||
function goPay (sn) { // 去支付
|
function goPay (sn) { // 去支付
|
||||||
router.push({path: '/payment', query: {orderType: 'ORDER', sn}});
|
router.push({path: '/payment', query: {orderType: 'ORDER', sn}});
|
||||||
}
|
}
|
||||||
function applyAfterSale (goodsItem) { // 申请售后
|
|
||||||
let arr = []
|
|
||||||
goodsItem.forEach(e => {
|
|
||||||
if (e.afterSaleStatus === 'NOT_APPLIED'|| e.afterSaleStatus === 'PART_AFTER_SALE') {
|
|
||||||
arr.push(e)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (arr.length === 1) {
|
|
||||||
router.push({name: 'ApplyAfterSale', query: {sn: arr[0].sn}})
|
|
||||||
} else {
|
|
||||||
afterSaleArr.value = arr;
|
|
||||||
afterSaleModal.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 申请售后
|
|
||||||
function afterSaleSelect (item) {
|
|
||||||
router.push({name: 'ApplyAfterSale', query: {sn: item.sn}})
|
|
||||||
}
|
|
||||||
function complain (sn, goodsIndex) { // 投诉
|
|
||||||
router.push({name: 'Complain', query: {sn, index: goodsIndex}})
|
|
||||||
}
|
|
||||||
function delOrder (sn) { // 删除订单
|
function delOrder (sn) { // 删除订单
|
||||||
Modal.confirm({
|
Modal.confirm({
|
||||||
title: '删除订单',
|
title: '删除订单',
|
||||||
@@ -394,6 +367,7 @@ function changePageSize (val) { // 修改页数
|
|||||||
}
|
}
|
||||||
function handleCancelOrder (sn) {
|
function handleCancelOrder (sn) {
|
||||||
// 取消订单
|
// 取消订单
|
||||||
|
cancelIsPickup.value = !!orderList.value.find(order => order.sn === sn)?.pickupCardId;
|
||||||
cancelParams.orderSn = sn;
|
cancelParams.orderSn = sn;
|
||||||
afterSaleReason('CANCEL').then(res => {
|
afterSaleReason('CANCEL').then(res => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
@@ -420,11 +394,16 @@ function filterAfterSaleStatus (status) {
|
|||||||
const ob = afterSaleStatusList.filter(e => e.status === status);
|
const ob = afterSaleStatusList.filter(e => e.status === status);
|
||||||
return ob && ob[0] ? ob[0].name : status
|
return ob && ob[0] ? ob[0].name : status
|
||||||
}
|
}
|
||||||
function canAfterSale (order) {
|
function statusTagType (status) { // 订单/售后状态对应标签类型
|
||||||
if (order.isAfterSale || order.orderType === 'E_COUPON' || !order.groupAfterSaleStatus) {
|
const map = {
|
||||||
return false
|
UNPAID: 'warning', WAIT_PAY: 'warning', PAID: 'warning',
|
||||||
|
UNDELIVERED: 'warning', PARTS_DELIVERED: 'warning', DELIVERED: 'warning',
|
||||||
|
WAIT_SHIP: 'warning', WAIT_USE: 'warning', TAKE: 'warning', STAY_PICKED_UP: 'warning',
|
||||||
|
APPLY: 'warning', APPLYING: 'warning',
|
||||||
|
COMPLETED: 'success', COMPLETE: 'success',
|
||||||
|
CANCELLED: 'info', CANCEL: 'info'
|
||||||
}
|
}
|
||||||
return order.groupAfterSaleStatus.includes('NOT_APPLIED') || order.groupAfterSaleStatus.includes('PART_AFTER_SALE')
|
return map[status] || 'info'
|
||||||
}
|
}
|
||||||
function paymentLabel (order) {
|
function paymentLabel (order) {
|
||||||
const map = {
|
const map = {
|
||||||
@@ -436,11 +415,14 @@ function paymentLabel (order) {
|
|||||||
if (order.paymentMethod && map[order.paymentMethod]) {
|
if (order.paymentMethod && map[order.paymentMethod]) {
|
||||||
return map[order.paymentMethod]
|
return map[order.paymentMethod]
|
||||||
}
|
}
|
||||||
if (order.payStatus === 'UNPAID' || order.orderStatus === 'UNPAID') {
|
if (isUnpaid(order)) {
|
||||||
return '待支付'
|
return '待支付'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
function isUnpaid (order) {
|
||||||
|
return order.payStatus === 'UNPAID' || order.orderStatus === 'UNPAID'
|
||||||
|
}
|
||||||
|
|
||||||
watch(() => route.query.tag, () => {
|
watch(() => route.query.tag, () => {
|
||||||
if (props.homePage || route.name !== 'MyOrder') return;
|
if (props.homePage || route.name !== 'MyOrder') return;
|
||||||
@@ -529,7 +511,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #171A26;
|
color: #333;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
@@ -630,37 +612,6 @@ onMounted(() => {
|
|||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.refund-tag {
|
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff7e8;
|
|
||||||
color: #ff9900;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-btn {
|
|
||||||
height: 28px;
|
|
||||||
margin-top: 8px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 26px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $theme_color;
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-extra .sub-btn {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.col-amount,
|
.col-amount,
|
||||||
.col-ops {
|
.col-ops {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -685,38 +636,9 @@ onMounted(() => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
|
|
||||||
.op-btn {
|
:deep(.el-button + .el-button) {
|
||||||
min-width: 96px;
|
margin-left: 0;
|
||||||
height: 32px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: #fff;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #fff5f4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-link {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: #171A26;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="order-detail" v-if="order.order">
|
<div class="order-detail" v-if="order.order">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
<div class="order-card status-card">
|
<div class="panel">
|
||||||
<div class="status-head">
|
<div class="status-head">
|
||||||
<div class="status-main">
|
<div class="status-main">
|
||||||
<p class="status-text">{{ order.orderStatusValue }}</p>
|
<el-tag :type="statusTagType" effect="dark">{{ order.orderStatusValue }}</el-tag>
|
||||||
<p class="status-sn">订单号:{{ order.order.sn }}</p>
|
<p class="status-sn">订单号:{{ order.order.sn }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-side">
|
<div class="status-side">
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
type="success"
|
type="primary"
|
||||||
@click="goPay(order.order.sn)"
|
@click="goPay(order.order.sn)"
|
||||||
size="small"
|
size="small"
|
||||||
v-if="order.allowOperationVO.pay"
|
v-if="order.allowOperationVO.pay"
|
||||||
@@ -35,7 +35,6 @@
|
|||||||
v-if="order.allowOperationVO.rog"
|
v-if="order.allowOperationVO.rog"
|
||||||
>确认收货</el-button>
|
>确认收货</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="danger"
|
|
||||||
@click="handleCancelOrder(order.order.sn)"
|
@click="handleCancelOrder(order.order.sn)"
|
||||||
v-if="order.allowOperationVO.cancel"
|
v-if="order.allowOperationVO.cancel"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -61,95 +60,152 @@
|
|||||||
<p class="verificationCode" v-if="order.order.verificationCode && !isECouponOrder">
|
<p class="verificationCode" v-if="order.order.verificationCode && !isECouponOrder">
|
||||||
核验码:<span>{{ order.order.verificationCode }}</span>
|
核验码:<span>{{ order.order.verificationCode }}</span>
|
||||||
</p>
|
</p>
|
||||||
<div class="order-card" v-if="order.order.deliveryMethod === 'LOGISTICS' && !isNonPhysicalOrder">
|
<div class="panel" v-if="order.order.deliveryMethod === 'LOGISTICS' && !isNonPhysicalOrder">
|
||||||
<h3>收货人信息</h3>
|
<h3>收货人信息</h3>
|
||||||
<p>收货人:{{ order.order.consigneeName }}</p>
|
<div class="info-row">
|
||||||
<p>手机号码:{{ $filters.secrecyMobile( order.order.consigneeMobile ) }}</p>
|
<span class="info-label">收货人</span>
|
||||||
<p>
|
<span class="info-value">{{ order.order.consigneeName }}</span>
|
||||||
收货地址:{{ $filters.unitAddress(order.order.consigneeAddressPath) }}
|
</div>
|
||||||
{{ order.order.consigneeDetail }}
|
<div class="info-row">
|
||||||
</p>
|
<span class="info-label">手机号码</span>
|
||||||
|
<span class="info-value">{{ $filters.secrecyMobile( order.order.consigneeMobile ) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">收货地址</span>
|
||||||
|
<span class="info-value">
|
||||||
|
{{ $filters.unitAddress(order.order.consigneeAddressPath) }}
|
||||||
|
{{ order.order.consigneeDetail }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card" v-if="order.order.deliveryMethod === 'SELF_PICK_UP'">
|
<div class="panel" v-if="order.order.deliveryMethod === 'SELF_PICK_UP'">
|
||||||
<h3>自提点信息</h3>
|
<h3>自提点信息</h3>
|
||||||
<p>自提点名称:{{ order.order.storeAddressPath }}</p>
|
<div class="info-row">
|
||||||
<p>联系方式:{{ order.order.storeAddressMobile }}</p>
|
<span class="info-label">自提点名称</span>
|
||||||
|
<span class="info-value">{{ order.order.storeAddressPath }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">联系方式</span>
|
||||||
|
<span class="info-value">{{ order.order.storeAddressMobile }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card">
|
<div class="panel">
|
||||||
<h3>付款信息</h3>
|
<h3>付款信息</h3>
|
||||||
<p>支付方式:{{ order.paymentMethodValue }}</p>
|
<div class="info-row">
|
||||||
<p>付款状态:{{ order.payStatusValue }}</p>
|
<span class="info-label">支付方式</span>
|
||||||
|
<span class="info-value">{{ order.paymentMethodValue }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">付款状态</span>
|
||||||
|
<span class="info-value">{{ order.payStatusValue }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card" v-if="!order.order.verificationCode && !isNonPhysicalOrder">
|
<div class="panel" v-if="!order.order.verificationCode && !isNonPhysicalOrder">
|
||||||
<h3>配送信息</h3>
|
<h3>配送信息</h3>
|
||||||
<p>配送方式:{{ displayDeliveryMethodValue }}</p>
|
<div class="info-row">
|
||||||
<p v-if="order.order.deliveryMethod === 'LOGISTICS' && !isNoDeliveryOrder">配送状态:{{ order.deliverStatusValue }}</p>
|
<span class="info-label">配送方式</span>
|
||||||
|
<span class="info-value">{{ displayDeliveryMethodValue }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="order.order.deliveryMethod === 'LOGISTICS' && !isNoDeliveryOrder">
|
||||||
|
<span class="info-label">配送状态</span>
|
||||||
|
<span class="info-value">{{ order.deliverStatusValue }}</span>
|
||||||
|
</div>
|
||||||
<template v-if="isNoDeliveryOrder">
|
<template v-if="isNoDeliveryOrder">
|
||||||
<p v-if="noDeliveryShipTime">发货时间:{{ noDeliveryShipTime }}</p>
|
<div class="info-row" v-if="noDeliveryShipTime">
|
||||||
<p v-if="noDeliveryRemark">配送说明:{{ noDeliveryRemark }}</p>
|
<span class="info-label">发货时间</span>
|
||||||
|
<span class="info-value">{{ noDeliveryShipTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="noDeliveryRemark">
|
||||||
|
<span class="info-label">配送说明</span>
|
||||||
|
<span class="info-value">{{ noDeliveryRemark }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p v-if="logistics">
|
<div class="info-row" v-if="logistics">
|
||||||
物流信息:{{ logistics.shipper || "暂无物流信息" }}
|
<span class="info-label">物流信息</span>
|
||||||
</p>
|
<span class="info-value">{{ logistics.shipper || "暂无物流信息" }}</span>
|
||||||
<p v-if="logistics">
|
</div>
|
||||||
物流单号:{{ logistics.logisticCode || "暂无物流单号" }}
|
<div class="info-row" v-if="logistics">
|
||||||
</p>
|
<span class="info-label">物流单号</span>
|
||||||
<div class="div-express-log" v-if="logistics">
|
<span class="info-value">{{ logistics.logisticCode || "暂无物流单号" }}</span>
|
||||||
<div class="express-log">
|
</div>
|
||||||
<p>订单日志:</p>
|
<div class="info-row" v-if="logistics">
|
||||||
<div v-for="(item, index) in logistics.traces" :key="index">
|
<span class="info-label">订单日志</span>
|
||||||
<span class="time">{{ item.AcceptTime }}</span>
|
<div class="info-value">
|
||||||
<span class="detail">{{ item.AcceptStation }}</span>
|
<div class="div-express-log">
|
||||||
|
<div class="express-log">
|
||||||
|
<div v-for="(item, index) in logistics.traces" :key="index">
|
||||||
|
<span class="time">{{ item.AcceptTime }}</span>
|
||||||
|
<span class="detail">{{ item.AcceptStation }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="order-card" v-if="order.order.payStatus === 'PAID'">
|
<div class="panel" v-if="order.order.payStatus === 'PAID'">
|
||||||
<h3>发票信息</h3>
|
<h3>发票信息</h3>
|
||||||
<!-- 以 li_receipt 为准:needReceipt 可能为 null/false 导致有 receipt 仍显示「未开发票」 -->
|
<!-- 以 li_receipt 为准:needReceipt 可能为 null/false 导致有 receipt 仍显示「未开发票」 -->
|
||||||
<template v-if="hasValidReceipt()">
|
<template v-if="hasValidReceipt()">
|
||||||
<p>发票类型:{{ formatReceiptType(order.receipt) }}</p>
|
<div class="info-row">
|
||||||
<p>发票抬头:{{ formatReceiptHeaderType(order.receipt) }}</p>
|
<span class="info-label">发票类型</span>
|
||||||
<p v-if="isPersonalReceipt(order.receipt) && order.receipt.personalName">
|
<span class="info-value">{{ formatReceiptType(order.receipt) }}</span>
|
||||||
个人名称:{{ order.receipt.personalName }}
|
</div>
|
||||||
</p>
|
<div class="info-row">
|
||||||
<p v-if="order.receipt.companyName">
|
<span class="info-label">发票抬头</span>
|
||||||
单位名称:{{ order.receipt.companyName }}
|
<span class="info-value">{{ formatReceiptHeaderType(order.receipt) }}</span>
|
||||||
</p>
|
</div>
|
||||||
<p v-if="order.receipt.taxpayerId">
|
<div class="info-row" v-if="isPersonalReceipt(order.receipt) && order.receipt.personalName">
|
||||||
纳税人识别号:{{ order.receipt.taxpayerId }}
|
<span class="info-label">个人名称</span>
|
||||||
</p>
|
<span class="info-value">{{ order.receipt.personalName }}</span>
|
||||||
<p v-if="order.receipt.companyAddress">
|
</div>
|
||||||
单位地址:{{ order.receipt.companyAddress }}
|
<div class="info-row" v-if="order.receipt.companyName">
|
||||||
</p>
|
<span class="info-label">单位名称</span>
|
||||||
<p v-if="order.receipt.companyPhone">
|
<span class="info-value">{{ order.receipt.companyName }}</span>
|
||||||
单位电话:{{ order.receipt.companyPhone }}
|
</div>
|
||||||
</p>
|
<div class="info-row" v-if="order.receipt.taxpayerId">
|
||||||
<p v-if="order.receipt.bankName">
|
<span class="info-label">纳税人识别号</span>
|
||||||
开户银行:{{ order.receipt.bankName }}
|
<span class="info-value">{{ order.receipt.taxpayerId }}</span>
|
||||||
</p>
|
</div>
|
||||||
<p v-if="order.receipt.bankAccount">
|
<div class="info-row" v-if="order.receipt.companyAddress">
|
||||||
银行账号:{{ order.receipt.bankAccount }}
|
<span class="info-label">单位地址</span>
|
||||||
</p>
|
<span class="info-value">{{ order.receipt.companyAddress }}</span>
|
||||||
<p>发票内容:{{ order.receipt.receiptContent }}</p>
|
</div>
|
||||||
<p v-if="order.receipt.receiptPhone">
|
<div class="info-row" v-if="order.receipt.companyPhone">
|
||||||
收票人手机:{{ order.receipt.receiptPhone }}
|
<span class="info-label">单位电话</span>
|
||||||
</p>
|
<span class="info-value">{{ order.receipt.companyPhone }}</span>
|
||||||
<p v-if="order.receipt.receiptEmail">
|
</div>
|
||||||
收票人邮箱:{{ order.receipt.receiptEmail }}
|
<div class="info-row" v-if="order.receipt.bankName">
|
||||||
</p>
|
<span class="info-label">开户银行</span>
|
||||||
|
<span class="info-value">{{ order.receipt.bankName }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="order.receipt.bankAccount">
|
||||||
|
<span class="info-label">银行账号</span>
|
||||||
|
<span class="info-value">{{ order.receipt.bankAccount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">发票内容</span>
|
||||||
|
<span class="info-value">{{ order.receipt.receiptContent }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="order.receipt.receiptPhone">
|
||||||
|
<span class="info-label">收票人手机</span>
|
||||||
|
<span class="info-value">{{ order.receipt.receiptPhone }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="order.receipt.receiptEmail">
|
||||||
|
<span class="info-label">收票人邮箱</span>
|
||||||
|
<span class="info-value">{{ order.receipt.receiptEmail }}</span>
|
||||||
|
</div>
|
||||||
<div v-if="Number(order.receipt.receiptStatus) === 1" class="receipt-action">
|
<div v-if="Number(order.receipt.receiptStatus) === 1" class="receipt-action">
|
||||||
<el-button size="small" type="primary" ghost @click="viewReceiptInvoice(order.receipt)">
|
<el-button size="small" type="primary" @click="viewReceiptInvoice(order.receipt)">
|
||||||
查看发票
|
查看发票
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else style="color: #999; margin-left: 5px">未开发票</div>
|
<div v-else class="empty-text">未开发票</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 卡密信息(当前订单 orderItems;满赠赠品卡密在赠品子单详情查看) -->
|
<!-- 卡密信息(当前订单 orderItems;满赠赠品卡密在赠品子单详情查看) -->
|
||||||
<div class="order-card ecoupon-card-keys" v-if="hasCardKeySection">
|
<div class="panel ecoupon-card-keys" v-if="hasCardKeySection">
|
||||||
<h3>卡密信息</h3>
|
<h3>卡密信息</h3>
|
||||||
<div
|
<div
|
||||||
v-for="line in cardKeyFulfillLines"
|
v-for="line in cardKeyFulfillLines"
|
||||||
@@ -194,75 +250,62 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 订单商品 -->
|
<!-- 订单商品 -->
|
||||||
<div class="goods">
|
<div class="panel goods-panel">
|
||||||
<div class="shop-name">
|
<div class="shop-name">
|
||||||
<span @click="shopPage(order.order.storeId)">{{order.order.storeName}}</span>
|
<span @click="shopPage(order.order.storeId)">{{order.order.storeName}}</span>
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<div class="goods-list">
|
||||||
<thead>
|
<div class="goods-row" v-for="(goods, goodsIndex) in order.orderItems" :key="goodsIndex">
|
||||||
<tr>
|
<img
|
||||||
<th width="28%">商品</th>
|
class="goods-thumb"
|
||||||
<th width="16%">货号</th>
|
@click="goodsDetail(goods.skuId, goods.goodsId)"
|
||||||
<th width="10%">单价</th>
|
:src="goods.image"
|
||||||
<th width="8%">数量</th>
|
loading="lazy"
|
||||||
<th width="10%">小计</th>
|
decoding="async"
|
||||||
<th width="8%">操作</th>
|
alt=""
|
||||||
</tr>
|
/>
|
||||||
</thead>
|
<div class="goods-info">
|
||||||
<tbody>
|
<p
|
||||||
<tr v-for="(goods, goodsIndex) in order.orderItems" :key="goodsIndex">
|
@click="goodsDetail(goods.skuId, goods.goodsId)"
|
||||||
<td>
|
class="hover-color goods-name"
|
||||||
<div class="goods-cell">
|
>
|
||||||
<img
|
{{ goods.goodsName }}
|
||||||
@click="goodsDetail(goods.skuId, goods.goodsId)"
|
</p>
|
||||||
:src="goods.image"
|
<p class="goods-sn">货号:{{ goods.id }}</p>
|
||||||
loading="lazy"
|
</div>
|
||||||
decoding="async"
|
<div class="goods-price">
|
||||||
alt=""
|
<span class="unit-price">
|
||||||
/>
|
{{ $filters.unitPrice(goods.goodsPrice, "¥") }} × {{ goods.num }}
|
||||||
<p
|
</span>
|
||||||
@click="goodsDetail(goods.skuId, goods.goodsId)"
|
<span class="subtotal">{{ $filters.unitPrice((goods.goodsPrice * goods.num), "¥") }}</span>
|
||||||
class="hover-color goods-name"
|
</div>
|
||||||
>
|
<div class="goods-actions">
|
||||||
{{ goods.goodsName }}
|
<button
|
||||||
</p>
|
v-if="goods.commentStatus == 'UNFINISHED'"
|
||||||
</div>
|
type="button"
|
||||||
</td>
|
class="comment-text"
|
||||||
<td>{{ goods.id }}</td>
|
@click="comment(order.order.sn, goodsIndex)"
|
||||||
<td>{{ $filters.unitPrice(goods.goodsPrice, "¥") }}</td>
|
>评价</button>
|
||||||
<td>{{ goods.num }}</td>
|
<el-button
|
||||||
<td>{{ $filters.unitPrice((goods.goodsPrice * goods.num), "¥") }}</td>
|
v-if="goods.complainStatus == 'NO_APPLY'"
|
||||||
<td class="order-item-actions">
|
@click="complain(order.order.sn, goodsIndex)"
|
||||||
<button
|
type="danger"
|
||||||
v-if="goods.commentStatus == 'UNFINISHED'"
|
size="small"
|
||||||
type="button"
|
>投诉</el-button
|
||||||
class="comment-text"
|
>
|
||||||
@click="comment(order.order.sn, goodsIndex)"
|
<el-button
|
||||||
>评价</button>
|
v-if="
|
||||||
<el-button
|
!isECouponOrder &&
|
||||||
v-if="goods.complainStatus == 'NO_APPLY'"
|
(goods.afterSaleStatus.includes('NOT_APPLIED') ||
|
||||||
@click="complain(order.order.sn, goodsIndex)"
|
goods.afterSaleStatus.includes('PART_AFTER_SALE'))
|
||||||
type="danger"
|
"
|
||||||
class="order-item-action-btn mb_5"
|
@click="applyAfterSale(goods.sn)"
|
||||||
size="small"
|
size="small"
|
||||||
>投诉</el-button
|
>申请售后</el-button
|
||||||
>
|
>
|
||||||
<el-button
|
</div>
|
||||||
v-if="
|
</div>
|
||||||
!isECouponOrder &&
|
</div>
|
||||||
(goods.afterSaleStatus.includes('NOT_APPLIED') ||
|
|
||||||
goods.afterSaleStatus.includes('PART_AFTER_SALE'))
|
|
||||||
"
|
|
||||||
@click="applyAfterSale(goods.sn)"
|
|
||||||
type="default"
|
|
||||||
size="small"
|
|
||||||
class="order-item-action-btn mb_5"
|
|
||||||
>申请售后</el-button
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<!-- 订单价格 -->
|
<!-- 订单价格 -->
|
||||||
<div class="order-price">
|
<div class="order-price">
|
||||||
<div>
|
<div>
|
||||||
@@ -298,6 +341,7 @@
|
|||||||
v-model="cancelAvail"
|
v-model="cancelAvail"
|
||||||
title="请选择取消订单原因"
|
title="请选择取消订单原因"
|
||||||
>
|
>
|
||||||
|
<el-alert v-if="order.order?.pickupCardId" title="此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。" type="warning" :closable="false" style="margin-bottom:16px" />
|
||||||
<el-radio-group
|
<el-radio-group
|
||||||
v-model="cancelParams.reason"
|
v-model="cancelParams.reason"
|
||||||
>
|
>
|
||||||
@@ -488,6 +532,20 @@ const noDeliveryShipTime = computed(() => {
|
|||||||
const pkg = noDeliveryPackages.value[0];
|
const pkg = noDeliveryPackages.value[0];
|
||||||
return pkg?.logisticsTime || "";
|
return pkg?.logisticsTime || "";
|
||||||
});
|
});
|
||||||
|
const statusTagType = computed(() => {
|
||||||
|
const map = {
|
||||||
|
UNPAID: "warning",
|
||||||
|
PAID: "warning",
|
||||||
|
UNDELIVERED: "warning",
|
||||||
|
PARTS_DELIVERED: "warning",
|
||||||
|
DELIVERED: "warning",
|
||||||
|
TAKE: "warning",
|
||||||
|
STAY_PICKED_UP: "warning",
|
||||||
|
COMPLETED: "success",
|
||||||
|
CANCELLED: "info",
|
||||||
|
};
|
||||||
|
return map[order.value?.order?.orderStatus] || "info";
|
||||||
|
});
|
||||||
|
|
||||||
function lineCardKeyRows(line) {
|
function lineCardKeyRows(line) {
|
||||||
return line.cardKeys || [];
|
return line.cardKeys || [];
|
||||||
@@ -720,36 +778,59 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mb_5 {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
.order-detail {
|
.order-detail {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-card {
|
.panel {
|
||||||
padding: 4px 0 16px;
|
margin-top: 16px;
|
||||||
margin-bottom: 8px;
|
padding: 16px 20px;
|
||||||
border-bottom: 1px solid #f0f0f0;
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
font-size: 15px;
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
line-height: 1.4;
|
line-height: 24px;
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0;
|
|
||||||
padding: 4px 0;
|
|
||||||
color: #666;
|
|
||||||
line-height: 22px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-card {
|
.info-row {
|
||||||
padding-top: 8px;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
flex: 0 0 80px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-head {
|
.status-head {
|
||||||
@@ -759,16 +840,17 @@ onMounted(() => {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text {
|
.status-main {
|
||||||
color: $theme_color;
|
display: flex;
|
||||||
font-size: 18px;
|
align-items: center;
|
||||||
font-weight: 600;
|
gap: 12px;
|
||||||
line-height: 1.4;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-sn {
|
.status-sn {
|
||||||
margin-top: 6px;
|
margin: 0;
|
||||||
color: #666;
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-side {
|
.status-side {
|
||||||
@@ -828,78 +910,61 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods {
|
.goods-panel {
|
||||||
padding-top: 8px;
|
.shop-name {
|
||||||
}
|
margin: 0 0 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
.shop-name {
|
span {
|
||||||
margin: 8px 0 12px;
|
color: #333;
|
||||||
font-size: 14px;
|
cursor: pointer;
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
span {
|
&:hover {
|
||||||
color: #438cde;
|
color: $theme_color;
|
||||||
cursor: pointer;
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
// 覆盖全局 .goods-list 的 flex 布局,避免订单商品行被压缩
|
||||||
border: 1px solid #eee;
|
.goods-list {
|
||||||
color: #666;
|
display: block;
|
||||||
border-collapse: collapse;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
tr {
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead > tr {
|
|
||||||
height: 40px;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
th {
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding: 12px 8px;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-cell {
|
.goods-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
min-width: 0;
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
|
||||||
img {
|
&:last-child {
|
||||||
width: 64px;
|
border-bottom: none;
|
||||||
height: 64px;
|
|
||||||
border-radius: 6px;
|
|
||||||
object-fit: cover;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background: #f5f5f5;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.goods-thumb {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.goods-name {
|
.goods-name {
|
||||||
min-width: 0;
|
margin: 0;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@@ -909,12 +974,41 @@ table {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-item-actions {
|
.goods-sn {
|
||||||
vertical-align: middle;
|
margin: 4px 0 0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.unit-price {
|
||||||
|
display: block;
|
||||||
|
color: #999;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtotal {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
.comment-text {
|
.comment-text {
|
||||||
display: block;
|
|
||||||
margin: 0 auto 6px;
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -924,9 +1018,8 @@ table {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-item-action-btn {
|
.el-button {
|
||||||
display: block;
|
margin: 0;
|
||||||
margin: 0 auto 6px;
|
|
||||||
min-width: 88px;
|
min-width: 88px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -958,13 +1051,12 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.verificationCode {
|
.verificationCode {
|
||||||
font-size: 20px;
|
margin: 16px 0 0;
|
||||||
margin-bottom: 20px;
|
font-size: 14px;
|
||||||
color: rgb(65, 63, 63);
|
color: #333;
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
span {
|
span {
|
||||||
color: $theme_color;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1086,34 +1178,4 @@ table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.layui-layer-wrap {
|
|
||||||
dl {
|
|
||||||
border-top: solid 1px #f5f5f5;
|
|
||||||
margin-top: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
dt {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 28px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 1% 8px 0;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 28px;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 8px 0 8px 8px;
|
|
||||||
border-left: solid 1px #f5f5f5;
|
|
||||||
|
|
||||||
.text-box {
|
|
||||||
line-height: 40px;
|
|
||||||
color: #333;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,7 +7,12 @@
|
|||||||
>
|
>
|
||||||
<empty v-if="list.length == 0" />
|
<empty v-if="list.length == 0" />
|
||||||
<ul class="coupon-list" v-else>
|
<ul class="coupon-list" v-else>
|
||||||
<li v-for="(item, index) in list" class="coupon-item" :key="index">
|
<li
|
||||||
|
v-for="(item, index) in list"
|
||||||
|
class="coupon-item"
|
||||||
|
:class="{ 'is-disabled': params.memberCouponStatus !== 'NEW' }"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
<div class="c-left">
|
<div class="c-left">
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
@@ -18,7 +23,7 @@
|
|||||||
v-if="item.couponType === 'DISCOUNT'"
|
v-if="item.couponType === 'DISCOUNT'"
|
||||||
class="global_color coupon-amount"
|
class="global_color coupon-amount"
|
||||||
><span class="price">{{ item.discount }}</span>折</span>
|
><span class="price">{{ item.discount }}</span>折</span>
|
||||||
<span class="describe">满{{ item.consumeThreshold }}元可用</span>
|
<span class="describe">{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
||||||
<p class="coupon-expire">有效期:{{ formatValidity(item) }}</p>
|
<p class="coupon-expire">有效期:{{ formatValidity(item) }}</p>
|
||||||
@@ -176,34 +181,61 @@ onMounted(() => {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@import "../../../assets/styles/coupon.scss";
|
@import "../../../assets/styles/coupon.scss";
|
||||||
|
|
||||||
|
.coupon-list {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.coupon-item {
|
.coupon-item {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 168px;
|
min-height: 140px;
|
||||||
margin-left: 0;
|
margin: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||||
$claim-width: 55px;
|
$claim-width: 55px;
|
||||||
|
|
||||||
|
&:not(.is-disabled):hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.c-left {
|
.c-left {
|
||||||
width: calc(100% - #{$claim-width});
|
width: calc(100% - #{$claim-width});
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 16px 16px 14px;
|
padding: 16px 16px 14px;
|
||||||
|
background: #fff8f5;
|
||||||
|
|
||||||
.coupon-amount {
|
.coupon-amount {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.describe {
|
.describe {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-overflow: unset;
|
text-overflow: unset;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: break-all;
|
word-break: normal;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p:nth-of-type(1) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coupon-expire {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b {
|
b {
|
||||||
@@ -248,6 +280,7 @@ onMounted(() => {
|
|||||||
right: calc(#{$claim-width} - 9px);
|
right: calc(#{$claim-width} - 9px);
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
border-color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
i.circle-top {
|
i.circle-top {
|
||||||
@@ -257,6 +290,26 @@ onMounted(() => {
|
|||||||
i.circle-bottom {
|
i.circle-bottom {
|
||||||
bottom: -9px;
|
bottom: -9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
.c-left {
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
.coupon-amount,
|
||||||
|
.price {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.describe {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: #999 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageration {
|
.pageration {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout
|
||||||
|
:tabs="walletTabs"
|
||||||
|
:active-tab="activeWalletTabIndex"
|
||||||
|
@tab-change="handleTabChange"
|
||||||
|
>
|
||||||
|
|
||||||
<div class="wallet-summary">
|
<div class="wallet-summary">
|
||||||
<div class="wallet-stat">
|
<div class="wallet-stat">
|
||||||
@@ -13,8 +17,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="account-btns">
|
<div class="account-btns">
|
||||||
<button type="button" class="op-btn" @click="recharge">在线充值</button>
|
<el-button type="primary" @click="recharge">在线充值</el-button>
|
||||||
<button type="button" class="ghost-btn" @click="withdrawalApply">申请提现</button>
|
<el-button @click="withdrawalApply">申请提现</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-dialog v-model="modal" width="530">
|
<el-dialog v-model="modal" width="530">
|
||||||
<template #header><p>
|
<template #header><p>
|
||||||
@@ -36,7 +40,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer><div style="text-align: center">
|
<template #footer><div style="text-align: center">
|
||||||
<el-button size="large" type="success" @click="rechargePrice">充值</el-button>
|
<el-button size="large" type="primary" @click="rechargePrice">充值</el-button>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<!-- 提现申请 -->
|
<!-- 提现申请 -->
|
||||||
@@ -65,7 +69,7 @@
|
|||||||
number
|
number
|
||||||
size="large"
|
size="large"
|
||||||
><template #append><span>元</span></template></el-input>
|
><template #append><span>元</span></template></el-input>
|
||||||
<span style="color: red">最低提现金额 {{ withdrawApplyFormData.minPrice }}元</span>
|
<span class="form-tip">最低提现金额 {{ withdrawApplyFormData.minPrice }}元</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item v-if="withdrawApplyFormData.type === 'ALI'" label="真实姓名" prop="realName">
|
<el-form-item v-if="withdrawApplyFormData.type === 'ALI'" label="真实姓名" prop="realName">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -86,14 +90,9 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer><div style="text-align: center">
|
<template #footer><div style="text-align: center">
|
||||||
<el-button size="large" type="success" @click="withdrawal">提现</el-button>
|
<el-button size="large" type="primary" @click="withdrawal">提现</el-button>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
<div class="wallet-tabs">
|
|
||||||
<div class="wallet-tab" :class="{ active: activeWalletTab === 'log' }" @click="tabPaneChange('log')">余额日志</div>
|
|
||||||
<div class="wallet-tab" :class="{ active: activeWalletTab === 'recharge' }" @click="tabPaneChange('recharge')">充值记录</div>
|
|
||||||
<div class="wallet-tab" :class="{ active: activeWalletTab === 'withdrawApply' }" @click="tabPaneChange('withdrawApply')">提现记录</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="activeWalletTab === 'log'">
|
<div v-show="activeWalletTab === 'log'">
|
||||||
<empty v-if="!(logColumnsData.records || []).length" />
|
<empty v-if="!(logColumnsData.records || []).length" />
|
||||||
@@ -181,7 +180,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { computed, onMounted, reactive, ref } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import {getDepositLog, getMembersWallet, getRecharge, getWithdrawApply, recharge as apiRecharge, withdrawalApply as apiWithdrawalApply} from '@/api/member';
|
import {getDepositLog, getMembersWallet, getRecharge, getWithdrawApply, recharge as apiRecharge, withdrawalApply as apiWithdrawalApply} from '@/api/member';
|
||||||
@@ -196,6 +195,12 @@ const formDataRef = ref(null);
|
|||||||
const withdrawApplyFormDataRef = ref(null);
|
const withdrawApplyFormDataRef = ref(null);
|
||||||
|
|
||||||
const activeWalletTab = ref("log");
|
const activeWalletTab = ref("log");
|
||||||
|
const walletTabs = ["余额日志", "充值记录", "提现记录"]; // 钱包页签
|
||||||
|
const walletTabKeys = ["log", "recharge", "withdrawApply"]; // 页签对应标识
|
||||||
|
const activeWalletTabIndex = computed(() => {
|
||||||
|
const index = walletTabKeys.indexOf(activeWalletTab.value);
|
||||||
|
return index > -1 ? index : 0;
|
||||||
|
});
|
||||||
const frozenDeposit = ref(0); // 冻结余额
|
const frozenDeposit = ref(0); // 冻结余额
|
||||||
const memberDeposit = ref(0); // 余额
|
const memberDeposit = ref(0); // 余额
|
||||||
|
|
||||||
@@ -290,6 +295,10 @@ function tabPaneChange(v) {
|
|||||||
getWithdrawApplyData();
|
getWithdrawApplyData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 页签切换
|
||||||
|
function handleTabChange(index) {
|
||||||
|
tabPaneChange(walletTabKeys[index]);
|
||||||
|
}
|
||||||
// 充值记录
|
// 充值记录
|
||||||
function getRechargeData() {
|
function getRechargeData() {
|
||||||
getRecharge(rechargeForm).then((res) => {
|
getRecharge(rechargeForm).then((res) => {
|
||||||
@@ -416,7 +425,8 @@ function withdrawal() {
|
|||||||
.wallet-stat {
|
.wallet-stat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
background: #fafafa;
|
background: #fff;
|
||||||
|
border: 1px solid #eee;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
@@ -445,67 +455,9 @@ function withdrawal() {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.op-btn,
|
.form-tip {
|
||||||
.ghost-btn {
|
color: #999;
|
||||||
height: 32px;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 30px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.op-btn {
|
|
||||||
border: 1px solid $theme_color;
|
|
||||||
background: $theme_color;
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ghost-btn {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background: #fff;
|
|
||||||
color: #171A26;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: $theme_color;
|
|
||||||
color: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-tabs {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
margin: 16px 0;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wallet-tab {
|
|
||||||
position: relative;
|
|
||||||
padding: 10px 0;
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
color: $theme_color;
|
|
||||||
font-weight: 600;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 2px;
|
|
||||||
background: $theme_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wallet-list {
|
.wallet-list {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="wrapper my-gift-cards">
|
<div class="wrapper my-gift-cards">
|
||||||
<UserCenterLayout>
|
<UserCenterLayout>
|
||||||
|
<div class="gift-page-heading">
|
||||||
|
<div><h2>我的礼品卡</h2><p>查看卡片权益,兑换你的心意好礼</p></div>
|
||||||
|
</div>
|
||||||
<div class="bind-section mb_20">
|
<div class="bind-section mb_20">
|
||||||
<div class="section-title">兑换礼品卡</div>
|
<div class="section-title">兑换礼品卡</div>
|
||||||
<el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" inline class="bind-form">
|
<el-form ref="bindFormRef" :model="bindForm" :rules="bindRules" class="bind-form">
|
||||||
<el-form-item label="卡号" prop="cardNo" label-width="60px">
|
<el-form-item label="卡号" prop="cardNo" label-width="60px">
|
||||||
<el-input v-model="bindForm.cardNo" placeholder="请输入卡号" maxlength="64" style="width: 220px" />
|
<el-input v-model="bindForm.cardNo" placeholder="请输入卡号" maxlength="64" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="兑换码" prop="cardSecret" label-width="70px">
|
<el-form-item label="兑换码" prop="cardSecret" label-width="70px">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="bindForm.cardSecret"
|
v-model="bindForm.cardSecret"
|
||||||
placeholder="请输入兑换码(卡密)"
|
placeholder="请输入兑换码(卡密)"
|
||||||
maxlength="128"
|
maxlength="128"
|
||||||
style="width: 220px"
|
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label-width="0px">
|
<el-form-item label-width="0px">
|
||||||
@@ -25,31 +26,41 @@
|
|||||||
<el-tabs v-model="statusTab" class="status-tabs mb_16" @tab-click="(tab) => onStatusTab(tab.paneName)">
|
<el-tabs v-model="statusTab" class="status-tabs mb_16" @tab-click="(tab) => onStatusTab(tab.paneName)">
|
||||||
<el-tab-pane label="可用" name="AVAILABLE" />
|
<el-tab-pane label="可用" name="AVAILABLE" />
|
||||||
<el-tab-pane label="不可用" name="UNAVAILABLE" />
|
<el-tab-pane label="不可用" name="UNAVAILABLE" />
|
||||||
<el-tab-pane label="待激活" name="PENDING_ACTIVATION" />
|
<el-tab-pane name="PENDING_ACTIVATION">
|
||||||
|
<template #label>
|
||||||
|
<span class="pending-tab-label">待激活<span v-if="pendingCount !== null" class="pending-count">{{ pendingCount }}</span></span>
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
|
||||||
<div class="card-list-wrap">
|
<div class="card-list-wrap">
|
||||||
<el-skeleton v-if="loading" fix />
|
<el-skeleton v-if="loading" :rows="5" animated />
|
||||||
<template v-if="!loading">
|
<template v-if="!loading">
|
||||||
<empty v-if="list.length === 0" />
|
<div v-if="loadFailed" class="gift-load-error"><p>卡片加载失败,请稍后重试</p><el-button @click="getList">重新加载</el-button></div>
|
||||||
|
<el-empty v-else-if="list.length === 0" :description="emptyDescription" :image-size="100" />
|
||||||
<div v-else class="gift-card-grid">
|
<div v-else class="gift-card-grid">
|
||||||
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item">
|
<div v-for="item in list" :key="item.id || item.cardNo" class="gcc-card-item" :class="{ 'is-unavailable': statusTab === 'UNAVAILABLE', 'is-pickup': item.cardType === 'PICKUP' }">
|
||||||
<div class="gcc-card-header">
|
<div class="gcc-card-header">
|
||||||
<div class="gcc-header-pattern" aria-hidden="true" />
|
<div class="gcc-header-pattern" aria-hidden="true" />
|
||||||
<div class="gcc-header-inner">
|
<div class="gcc-header-inner">
|
||||||
<div class="gcc-header-left">
|
<div class="gcc-header-left">
|
||||||
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
<div class="gcc-name-row">
|
||||||
<div class="gcc-face">面值{{ formatYuan(item.faceValue) }}元</div>
|
<div class="gcc-title">{{ item.giftCardName || "礼品卡" }}</div>
|
||||||
<div class="gcc-cardno">{{ item.cardNo }}</div>
|
<span class="gcc-status-chip">{{ statusChipLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="item.cardType === 'PICKUP'" class="gcc-face">兑换 <strong>{{ item.pickupCount }}</strong> 种商品</div>
|
||||||
|
<div v-else class="gcc-face"><span class="gcc-face-symbol">¥</span>{{ formatYuan(item.faceValue) }}</div>
|
||||||
|
<div class="gcc-cardno">卡号 {{ item.cardNo }}</div>
|
||||||
|
<div class="gcc-expire">{{ formatValidity(item) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gcc-header-right">
|
<div class="gcc-header-right">
|
||||||
<div class="gcc-type">现金卡</div>
|
<div class="gcc-type">{{ item.cardType === 'PICKUP' ? '提货卡' : '现金卡' }}</div>
|
||||||
<div class="gcc-expire">{{ formatValidity(item) }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gcc-card-body">
|
<div class="gcc-card-body">
|
||||||
<div class="gcc-row">
|
<div v-if="item.cardType === 'PICKUP'" class="gcc-row">每种一件 · 一次兑完 · 免邮</div>
|
||||||
|
<div v-else class="gcc-row">
|
||||||
当前余额:<span class="gcc-strong" v-if="item.balance != null">¥{{ $filters.unitPrice(item.balance) }}</span><span class="gcc-strong" v-else>—</span>
|
当前余额:<span class="gcc-strong" v-if="item.balance != null">¥{{ $filters.unitPrice(item.balance) }}</span><span class="gcc-strong" v-else>—</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,6 +120,10 @@ const router = useRouter();
|
|||||||
const statusTab = ref("AVAILABLE");
|
const statusTab = ref("AVAILABLE");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
|
const pendingCount = ref(null);
|
||||||
|
const loadFailed = ref(false);
|
||||||
|
let listRequestId = 0;
|
||||||
|
let pendingCountRequestId = 0;
|
||||||
const list = ref([]);
|
const list = ref([]);
|
||||||
const params = reactive({
|
const params = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
@@ -127,7 +142,6 @@ const bindRules = {
|
|||||||
cardSecret: [{ required: true, message: "请输入兑换码", trigger: "blur" }],
|
cardSecret: [{ required: true, message: "请输入兑换码", trigger: "blur" }],
|
||||||
};
|
};
|
||||||
const noticeModal = ref(false);
|
const noticeModal = ref(false);
|
||||||
const serviceHotline = ref("12221111");
|
|
||||||
const activatingId = ref("");
|
const activatingId = ref("");
|
||||||
const bindFormRef = ref(null);
|
const bindFormRef = ref(null);
|
||||||
|
|
||||||
@@ -146,11 +160,34 @@ const primaryActionDisabled = computed(() => {
|
|||||||
const pendingActivateLocked = computed(() => {
|
const pendingActivateLocked = computed(() => {
|
||||||
return statusTab.value === "PENDING_ACTIVATION" && !!activatingId.value;
|
return statusTab.value === "PENDING_ACTIVATION" && !!activatingId.value;
|
||||||
});
|
});
|
||||||
|
const statusChipLabel = computed(() => {
|
||||||
|
const map = { AVAILABLE: "可用", UNAVAILABLE: "不可用", PENDING_ACTIVATION: "待激活" };
|
||||||
|
return map[statusTab.value] || "";
|
||||||
|
});
|
||||||
|
const emptyDescription = computed(() => ({
|
||||||
|
AVAILABLE: "暂无可用礼品卡,可输入卡号和兑换码添加",
|
||||||
|
UNAVAILABLE: "暂无不可用礼品卡",
|
||||||
|
PENDING_ACTIVATION: "暂无待激活礼品卡",
|
||||||
|
}[statusTab.value]));
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 复用会员分页接口的 total 获取全部待激活数量,独立于当前标签和每页条数。 */
|
||||||
|
async function refreshPendingCount() {
|
||||||
|
const requestId = ++pendingCountRequestId;
|
||||||
|
try {
|
||||||
|
const res = await getGiftCardCashMemberCardPage({ memberCardStatus: "PENDING_ACTIVATION", pageNumber: 1, pageSize: 1 });
|
||||||
|
if (requestId === pendingCountRequestId && res.success && res.result) {
|
||||||
|
pendingCount.value = Number(res.result.total) || 0;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 统计失败时保留已知数量,不将未知数量显示成零。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatYuan(val) {
|
function formatYuan(val) {
|
||||||
if (val == null || val === "") {
|
if (val == null || val === "") {
|
||||||
return "—";
|
return "—";
|
||||||
@@ -188,21 +225,32 @@ function onStatusTab(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getList() {
|
function getList() {
|
||||||
|
const requestId = ++listRequestId;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
loadFailed.value = false;
|
||||||
const p = { ...params };
|
const p = { ...params };
|
||||||
if (p.memberCardStatus) {
|
if (p.memberCardStatus) {
|
||||||
p.memberCardStatus = String(p.memberCardStatus);
|
p.memberCardStatus = String(p.memberCardStatus);
|
||||||
}
|
}
|
||||||
getGiftCardCashMemberCardPage(p)
|
getGiftCardCashMemberCardPage(p)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (requestId !== listRequestId) return;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success && res.result) {
|
if (res.success && res.result) {
|
||||||
list.value = res.result.records || [];
|
list.value = res.result.records || [];
|
||||||
total.value = res.result.total || 0;
|
total.value = res.result.total || 0;
|
||||||
|
if (p.memberCardStatus === "PENDING_ACTIVATION") {
|
||||||
|
++pendingCountRequestId;
|
||||||
|
pendingCount.value = Number(total.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
loadFailed.value = true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
if (requestId !== listRequestId) return;
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
loadFailed.value = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +291,7 @@ function submitBind() {
|
|||||||
resetBindForm();
|
resetBindForm();
|
||||||
params.pageNumber = 1;
|
params.pageNumber = 1;
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -267,6 +316,7 @@ function onCardPrimaryAction(item) {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
Message.success(res.message || "激活成功");
|
Message.success(res.message || "激活成功");
|
||||||
getList();
|
getList();
|
||||||
|
refreshPendingCount();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@@ -278,7 +328,7 @@ function onCardPrimaryAction(item) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (statusTab.value === "AVAILABLE") {
|
if (statusTab.value === "AVAILABLE") {
|
||||||
router.push({ path: "/goodsList" });
|
router.push(item.cardType === "PICKUP" ? { name: "PickupCard", query: { id: resolveMemberCardId(item) } } : { path: "/goodsList" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,168 +351,125 @@ function isActivating(item) {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.my-gift-cards {
|
.my-gift-cards {
|
||||||
|
.gift-page-heading {
|
||||||
|
padding: 10px 4px 22px;
|
||||||
|
h2 { margin: 0 0 8px; font-size: 22px; font-weight: 600; color: #252833; }
|
||||||
|
p { margin: 0; font-size: 13px; color: #909399; }
|
||||||
|
}
|
||||||
.bind-section {
|
.bind-section {
|
||||||
padding: 16px 18px;
|
padding: 20px 24px 24px;
|
||||||
border-radius: 4px;
|
border-radius: 10px;
|
||||||
border: 1px solid #e8eaec;
|
border: 1px solid #f3e7e8;
|
||||||
@include white_background_color();
|
background: #fff9f9;
|
||||||
}
|
|
||||||
.section-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
@include title_color($light_title_color);
|
|
||||||
}
|
}
|
||||||
|
.section-title { margin-bottom: 18px; font-size: 15px; font-weight: 600; color: #303133; }
|
||||||
.bind-form {
|
.bind-form {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-wrap: wrap;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
|
||||||
align-items: flex-end;
|
align-items: start;
|
||||||
gap: 4px 8px;
|
gap: 16px;
|
||||||
|
:deep(.el-form-item) { margin: 0; min-width: 0; }
|
||||||
|
:deep(.el-form-item__content) { min-width: 0; }
|
||||||
|
:deep(.el-input__wrapper) { min-height: 36px; box-sizing: border-box; background: #fff; }
|
||||||
|
:deep(.el-button) { height: 36px; min-width: 88px; border-radius: 6px; }
|
||||||
}
|
}
|
||||||
.status-tabs {
|
.status-tabs {
|
||||||
:deep(.el-tabs__header) {
|
margin-top: 28px;
|
||||||
margin-bottom: 0;
|
:deep(.el-tabs__header) { margin-bottom: 0; }
|
||||||
}
|
:deep(.el-tabs__item) { height: 48px; font-size: 15px; }
|
||||||
|
:deep(.el-tabs__nav-wrap::after) { height: 1px; background: #eceef2; }
|
||||||
}
|
}
|
||||||
.card-list-wrap {
|
.pending-tab-label { display: inline-flex; align-items: center; gap: 8px; }
|
||||||
position: relative;
|
.pending-count {
|
||||||
min-height: 200px;
|
display: inline-flex;
|
||||||
margin-top: 24px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 22px;
|
||||||
|
padding: 0 6px;
|
||||||
|
height: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: $theme_color;
|
||||||
|
background: #fff0f1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
.card-list-wrap { position: relative; min-height: 290px; margin-top: 24px; }
|
||||||
|
.gift-load-error { padding: 64px 0; text-align: center; color: #909399; }
|
||||||
.gift-card-grid {
|
.gift-card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 20px 18px;
|
gap: 24px;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
.gcc-card-item {
|
.gcc-card-item {
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.08);
|
border: 1px solid #f0e3e4;
|
||||||
@include white_background_color();
|
border-radius: 12px;
|
||||||
}
|
background: #fff;
|
||||||
.gcc-card-header {
|
transition: box-shadow .2s, border-color .2s;
|
||||||
position: relative;
|
&:hover { border-color: #edc9cd; box-shadow: 0 6px 20px rgba(100, 33, 40, .06); }
|
||||||
min-height: 128px;
|
|
||||||
padding: 0;
|
|
||||||
background: linear-gradient(125deg, #ff9a4a 0%, #ff7729 42%, #ff8f3d 100%);
|
|
||||||
}
|
}
|
||||||
|
.gcc-card-header { position: relative; overflow: hidden; background: linear-gradient(120deg, #fff3f3, #fff9f7); }
|
||||||
.gcc-header-pattern {
|
.gcc-header-pattern {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
width: 180px;
|
||||||
opacity: 0.22;
|
height: 180px;
|
||||||
|
right: -70px;
|
||||||
|
bottom: -110px;
|
||||||
|
border: 24px solid rgba(211, 134, 141, .07);
|
||||||
|
border-radius: 50%;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background-image: repeating-linear-gradient(
|
|
||||||
-36deg,
|
|
||||||
transparent,
|
|
||||||
transparent 5px,
|
|
||||||
rgba(255, 255, 255, 0.45) 5px,
|
|
||||||
rgba(255, 255, 255, 0.45) 6px
|
|
||||||
);
|
|
||||||
}
|
|
||||||
.gcc-header-inner {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 16px 14px 14px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.gcc-header-left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding-right: 8px;
|
|
||||||
}
|
|
||||||
.gcc-title {
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.3;
|
|
||||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
.gcc-face {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.95;
|
|
||||||
}
|
|
||||||
.gcc-cardno {
|
|
||||||
margin-top: 22px;
|
|
||||||
font-size: 14px;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
opacity: 0.75;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.gcc-header-right {
|
|
||||||
text-align: right;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.gcc-type {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
.gcc-expire {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
opacity: 0.9;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.gcc-card-body {
|
|
||||||
padding: 14px 14px 16px;
|
|
||||||
}
|
|
||||||
.gcc-row {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.85;
|
|
||||||
@include title_color($light_title_color);
|
|
||||||
}
|
|
||||||
.gcc-muted {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
.gcc-strong {
|
|
||||||
color: #ff6b22;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.gcc-link {
|
|
||||||
color: $theme_color;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.gcc-service {
|
|
||||||
color: #515a6e;
|
|
||||||
}
|
}
|
||||||
|
.gcc-header-inner { position: relative; display: flex; gap: 12px; padding: 22px 20px 18px; }
|
||||||
|
.gcc-header-left { flex: 1; min-width: 0; }
|
||||||
|
.gcc-name-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||||
|
.gcc-title { max-width: 100%; overflow-wrap: anywhere; font-size: 15px; font-weight: 600; line-height: 1.5; color: #523338; }
|
||||||
|
.gcc-status-chip { padding: 1px 7px; border-radius: 4px; background: rgba(255,255,255,.8); color: #a9757b; font-size: 11px; line-height: 18px; }
|
||||||
|
.gcc-face { margin-top: 18px; color: #a63e4a; font-size: 30px; font-weight: 600; line-height: 1.3; }
|
||||||
|
.gcc-face strong { font-size: 32px; font-weight: 600; }
|
||||||
|
.gcc-face-symbol { margin-right: 3px; font-size: 18px; }
|
||||||
|
.gcc-cardno { margin-top: 18px; color: #96777b; font-size: 12px; overflow-wrap: anywhere; letter-spacing: .03em; }
|
||||||
|
.gcc-header-right { flex-shrink: 0; text-align: right; }
|
||||||
|
.gcc-type { padding: 3px 7px; border: 1px solid #e9cdd0; border-radius: 4px; font-size: 12px; color: #a46f75; white-space: nowrap; }
|
||||||
|
.gcc-expire { margin-top: 8px; color: #a08a8d; font-size: 12px; }
|
||||||
|
.gcc-card-body { padding: 16px 20px 20px; }
|
||||||
|
.gcc-row { min-height: 22px; color: #777e89; font-size: 13px; line-height: 22px; }
|
||||||
|
.gcc-strong { color: #a63e4a; font-weight: 600; }
|
||||||
.gcc-action {
|
.gcc-action {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 14px;
|
margin-top: 16px;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 6px;
|
||||||
font-size: 15px;
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #e95b6c;
|
transition: opacity .2s;
|
||||||
background: #faf4f5;
|
&:hover:not(.is-disabled) { opacity: .85; }
|
||||||
transition: opacity 0.2s, background 0.2s;
|
&:focus-visible { outline: 2px solid $theme_color; outline-offset: 3px; }
|
||||||
|
&.is-disabled { color: #aaa; background: #f3f4f6; cursor: not-allowed; }
|
||||||
}
|
}
|
||||||
.gcc-action:hover:not(.is-disabled) {
|
.is-pickup .gcc-face { font-size: 22px; }
|
||||||
background: #f5e8ea;
|
.gcc-card-item.is-unavailable {
|
||||||
|
border-color: #e8eaed;
|
||||||
|
.gcc-card-header { background: #f5f6f8; }
|
||||||
|
.gcc-title, .gcc-face, .gcc-strong { color: #92969e; }
|
||||||
|
.gcc-cardno, .gcc-expire, .gcc-status-chip, .gcc-type { color: #a1a5ad; }
|
||||||
|
.gcc-type { border-color: #dddfe4; }
|
||||||
}
|
}
|
||||||
.gcc-action.is-disabled {
|
.page-size { display: flex; justify-content: flex-end; margin-top: 28px; padding-top: 20px; border-top: 1px solid #f0f1f4; }
|
||||||
color: #c5c8ce;
|
@media (max-width: 900px) {
|
||||||
cursor: not-allowed;
|
.bind-form { grid-template-columns: minmax(0, 1fr); gap: 22px; }
|
||||||
background: #f7f7f7;
|
.bind-form :deep(.el-form-item__label) { width: 70px !important; }
|
||||||
}
|
.bind-form :deep(.el-form-item:last-child .el-form-item__content) { padding-left: 70px; }
|
||||||
.page-size {
|
.gift-card-grid { grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); }
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.gcc-notice-text {
|
.gcc-notice-text { line-height: 1.7; color: #515a6e; margin-bottom: 16px; }
|
||||||
line-height: 1.7;
|
.gcc-notice-footer { text-align: right; }
|
||||||
color: #515a6e;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.gcc-notice-footer {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
194
buyer/src/pages/home/userCenter/PickupCard.vue
Normal file
194
buyer/src/pages/home/userCenter/PickupCard.vue
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<!--
|
||||||
|
提货卡选品与收货地址确认,兑换成功后进入普通订单履约。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-05
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<UserCenterLayout>
|
||||||
|
<div v-loading="loading" class="pickup-page">
|
||||||
|
<header class="pickup-heading">
|
||||||
|
<div><div class="pickup-eyebrow">提货卡兑换</div><h2>{{ detail.giftCardName || '选择你的好礼' }}</h2></div>
|
||||||
|
<span v-if="detail.credentialId" class="pickup-validity">提货权益</span>
|
||||||
|
</header>
|
||||||
|
<el-result v-if="error" icon="warning" :title="error"><template #extra><el-button @click="load">重新加载</el-button></template></el-result>
|
||||||
|
<template v-else-if="detail.credentialId">
|
||||||
|
<el-result v-if="completed || detail.tradeSn" icon="success" title="该提货卡已兑换"><template #extra><el-button type="primary" @click="router.push({name:'MyOrder'})">查看订单</el-button></template></el-result>
|
||||||
|
<template v-else>
|
||||||
|
<div class="pickup-rules"><strong>{{ ruleText }}</strong><span>每种一件</span><span>一次兑完</span><span>免邮</span></div>
|
||||||
|
<section class="pickup-products">
|
||||||
|
<div class="pickup-section-title"><h3>选择兑换商品</h3><span>共 {{ detail.goods?.length || 0 }} 种商品 · 已选 <b>{{ selected.length }}</b> / {{ detail.pickupCount }} 种</span></div>
|
||||||
|
<el-checkbox-group v-model="selected" :max="detail.pickupCount" :disabled="busy || detail.pickupMode === 'ALL'" aria-label="可兑换商品">
|
||||||
|
<div class="pickup-grid">
|
||||||
|
<el-checkbox v-for="goods in detail.goods" :key="goods.id" :value="goods.id" :disabled="!goods.available" class="pickup-item" :class="{'is-selected': selected.includes(goods.id), 'is-unavailable': !goods.available}">
|
||||||
|
<div class="pickup-image"><el-image :src="goods.thumbnail" fit="contain" :alt="goods.goodsName"><template #error><span class="pickup-image-fallback">暂无图片</span></template></el-image></div>
|
||||||
|
<div class="pickup-goods-info">
|
||||||
|
<strong class="pickup-goods-name" :title="goods.goodsName">{{ goods.goodsName }}</strong>
|
||||||
|
<div class="pickup-specs" :title="specsText(goods.specs)">{{ specsText(goods.specs) }}</div>
|
||||||
|
<div class="pickup-store">{{ goods.storeName }}</div>
|
||||||
|
<div v-if="!goods.available" class="pickup-goods-unavailable">暂不可兑换</div>
|
||||||
|
</div>
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
</el-checkbox-group>
|
||||||
|
</section>
|
||||||
|
<section class="pickup-address">
|
||||||
|
<div class="pickup-section-title"><h3>收货地址</h3><div><el-button link :disabled="busy" @click="router.push({name:'MyAddress'})">管理地址</el-button><el-button link :disabled="busy" @click="loadAddresses">刷新</el-button></div></div>
|
||||||
|
<el-select v-model="addressId" placeholder="请选择收货地址" :disabled="busy" aria-label="收货地址" class="pickup-address-select">
|
||||||
|
<el-option v-for="address in addresses" :key="address.id" :value="address.id" :label="addressText(address)" />
|
||||||
|
</el-select>
|
||||||
|
<p v-if="!addresses.length" class="pickup-address-empty">还没有收货地址,请先添加地址后刷新。</p>
|
||||||
|
</section>
|
||||||
|
<footer class="pickup-checkout">
|
||||||
|
<div class="pickup-selection"><strong>已选 <b>{{ selected.length }}</b> / {{ detail.pickupCount }} 种商品</strong><span>{{ selectionHint }}</span></div>
|
||||||
|
<div class="pickup-submit"><div class="pickup-payable">实付 <strong>¥0<span>.00</span></strong><small>免运费</small></div><el-button type="primary" :loading="busy" :disabled="!canSubmit" @click="submit">确认兑换</el-button></div>
|
||||||
|
</footer>
|
||||||
|
<p class="pickup-terms">兑换成功后取消不退权益,不支持退款或退货。</p>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</UserCenterLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { memberAddress } from '@/api/address';
|
||||||
|
import storage from '@/plugins/storage';
|
||||||
|
import { getPickupCard, previewPickupCard, redeemPickupCard } from '@/api/pickupCard';
|
||||||
|
|
||||||
|
const route = useRoute(), router = useRouter();
|
||||||
|
const detail = ref({}), selected = ref([]), addresses = ref([]), addressId = ref('');
|
||||||
|
const loading = ref(false), busy = ref(false), error = ref(''), completed = ref(false);
|
||||||
|
let active = true, requestVersion = 0;
|
||||||
|
const owner = storage.getItem('userInfo') || '';
|
||||||
|
/** 账号切换后丢弃旧请求结果,避免展示上一个账号的卡和地址。 */
|
||||||
|
function sameOwner() { return active && owner === (storage.getItem('userInfo') || ''); }
|
||||||
|
const canSubmit = computed(() => selected.value.length === detail.value.pickupCount && !!addressId.value && !loading.value && !detail.value.goods?.some(g => selected.value.includes(g.id) && !g.available));
|
||||||
|
const ruleText = computed(() => detail.value.pickupMode === 'ALL' ? '兑换以下全部商品' : `从以下商品任选 ${detail.value.pickupCount || 1} 种`);
|
||||||
|
const selectionHint = computed(() => {
|
||||||
|
const remaining = (detail.value.pickupCount || 0) - selected.value.length;
|
||||||
|
if (remaining > 0) return `还需选择 ${remaining} 种商品`;
|
||||||
|
return addressId.value ? '商品已选齐,请核对收货地址' : '请选择收货地址';
|
||||||
|
});
|
||||||
|
/** 兼容键值及旧数组规格,过滤图片元数据,避免展示原始 JSON。 */
|
||||||
|
function specsText(value) {
|
||||||
|
try {
|
||||||
|
const specs = typeof value === 'string' ? JSON.parse(value || '{}') : value;
|
||||||
|
const values = Array.isArray(specs) ? specs.filter(s => s && s.specName !== 'images').map(s => s.specValue) : Object.entries(specs || {}).filter(([key]) => key !== 'images').map(([, val]) => val);
|
||||||
|
return values.filter(v => typeof v === 'string' || typeof v === 'number').join(' / ') || '默认规格';
|
||||||
|
} catch { return typeof value === 'string' && !/^[\s]*[\[{]/.test(value) ? value : '默认规格'; }
|
||||||
|
}
|
||||||
|
/** 地址仅用于展示,提交后仍由服务端校验当前会员归属。 */
|
||||||
|
function addressText(address) { return `${address.name} ${address.mobile} ${address.consigneeAddressPath || ''} ${address.detail || ''}`; }
|
||||||
|
/** 刷新地址,保留仍存在的选择。 */
|
||||||
|
async function loadAddresses() {
|
||||||
|
const response = await memberAddress();
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '地址加载失败');
|
||||||
|
addresses.value = response.result?.records || response.result || [];
|
||||||
|
if (!addresses.value.some(a => a.id === addressId.value)) addressId.value = (addresses.value.find(a => a.isDefault) || addresses.value[0])?.id || '';
|
||||||
|
}
|
||||||
|
/** 加载卡详情;离页或新请求发起后丢弃旧响应。 */
|
||||||
|
async function load() {
|
||||||
|
const version = ++requestVersion;
|
||||||
|
loading.value = true; error.value = '';
|
||||||
|
try {
|
||||||
|
const response = await getPickupCard(route.query.id);
|
||||||
|
if (!sameOwner() || version !== requestVersion) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '提货卡加载失败');
|
||||||
|
detail.value = response.result;
|
||||||
|
selected.value = detail.value.pickupMode === 'ALL' ? detail.value.goods.map(g => g.id) : [];
|
||||||
|
await loadAddresses();
|
||||||
|
} catch (e) { if (sameOwner() && version === requestVersion) error.value = e.message || '加载失败,请重试'; }
|
||||||
|
finally { if (sameOwner() && version === requestVersion) loading.value = false; }
|
||||||
|
}
|
||||||
|
/** 先服务端试算再确认核销,网络异常保留选项,允许幂等重试。 */
|
||||||
|
async function submit() {
|
||||||
|
if (busy.value || !canSubmit.value || !sameOwner()) return;
|
||||||
|
busy.value = true;
|
||||||
|
const payload = { credentialId: detail.value.credentialId, skuIds: [...selected.value], addressId: addressId.value };
|
||||||
|
try {
|
||||||
|
const preview = await previewPickupCard(payload);
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!preview.success) throw new Error(preview.message || '兑换校验失败');
|
||||||
|
await ElMessageBox.confirm(`本次兑换 ${selected.value.length} 种商品,每种一件,免邮且实付为零。兑换后取消不退权益,不支持退款或退货。`, '确认兑换', { confirmButtonText: '确认兑换', cancelButtonText: '返回检查' });
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
const response = await redeemPickupCard(payload);
|
||||||
|
if (!sameOwner()) return;
|
||||||
|
if (!response.success) throw new Error(response.message || '兑换失败');
|
||||||
|
completed.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
if (sameOwner() && e !== 'cancel' && e !== 'close') {
|
||||||
|
// 网络超时可能发生在成功提交后,查询原卡确认结果,不重复创建订单。
|
||||||
|
try { const status = await getPickupCard(payload.credentialId); if (sameOwner() && status.success && status.result?.tradeSn) { detail.value = status.result; completed.value = true; return; } } catch { /* 保留原错误供重试 */ }
|
||||||
|
ElMessage.error(e.message || '兑换结果待确认,请重试或查看订单');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { if (sameOwner()) busy.value = false; }
|
||||||
|
}
|
||||||
|
onMounted(load);
|
||||||
|
onBeforeUnmount(() => { active = false; requestVersion++; });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.pickup-page { padding: 10px 4px 0; color: #303133; }
|
||||||
|
.pickup-heading { display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-bottom: 20px; }
|
||||||
|
.pickup-eyebrow { margin-bottom: 8px; color: #909399; font-size: 12px; }
|
||||||
|
.pickup-heading h2 { margin: 0; color: #252833; font-size: 22px; font-weight: 600; overflow-wrap: anywhere; }
|
||||||
|
.pickup-validity { padding: 5px 10px; border: 1px solid #eee; border-radius: 4px; font-size: 12px; color: #777e89; white-space: nowrap; }
|
||||||
|
.pickup-rules { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 20px; padding: 16px 18px; border-radius: 6px; background: #f7f8fa; font-size: 13px; color: #858b95; }
|
||||||
|
.pickup-rules strong { color: #434953; font-weight: 500; }
|
||||||
|
.pickup-products { margin-top: 28px; }
|
||||||
|
.pickup-section-title { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 16px; }
|
||||||
|
.pickup-section-title h3 { margin: 0; font-size: 16px; font-weight: 600; }
|
||||||
|
.pickup-section-title > span { color: #909399; font-size: 13px; }
|
||||||
|
.pickup-section-title b { color: $theme_color; font-weight: 500; }
|
||||||
|
.pickup-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; max-height: 570px; overflow-y: auto; padding: 2px 4px 2px 2px; }
|
||||||
|
.pickup-item {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #e9ebef;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
transition: border-color .2s, box-shadow .2s;
|
||||||
|
&:hover:not(.is-unavailable) { border-color: #e7acb1; }
|
||||||
|
&.is-selected { border-color: $theme_color; box-shadow: 0 0 0 1px $theme_color; }
|
||||||
|
&.is-unavailable { background: #f8f9fa; }
|
||||||
|
:deep(.el-checkbox__input) { position: absolute; z-index: 1; top: 12px; right: 12px; }
|
||||||
|
:deep(.el-checkbox__label) { display: block; padding: 0; width: 100%; white-space: normal; color: inherit; }
|
||||||
|
}
|
||||||
|
.pickup-image { display: flex; justify-content: center; align-items: center; height: 140px; margin: 14px 20px 0; background: #fff; }
|
||||||
|
.pickup-image :deep(.el-image) { width: 100%; height: 100%; }
|
||||||
|
.pickup-image-fallback { display: flex; height: 100%; align-items: center; justify-content: center; color: #a8abb2; background: #f7f8fa; font-size: 12px; }
|
||||||
|
.pickup-goods-info { padding: 14px; }
|
||||||
|
.pickup-goods-name { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; min-height: 40px; line-height: 20px; font-size: 14px; font-weight: 500; color: #303133; }
|
||||||
|
.pickup-specs { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 8px; color: #6b7280; font-size: 12px; line-height: 18px; }
|
||||||
|
.pickup-store { margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #a0a5ae; font-size: 12px; line-height: 18px; }
|
||||||
|
.pickup-goods-unavailable { margin-top: 8px; color: #909399; font-size: 12px; }
|
||||||
|
.is-unavailable .pickup-image { opacity: .5; }
|
||||||
|
.pickup-address { margin-top: 28px; padding-top: 24px; border-top: 1px solid #eee; }
|
||||||
|
.pickup-address-select { width: 100%; }
|
||||||
|
.pickup-address-select :deep(.el-select__wrapper) { min-height: 42px; }
|
||||||
|
.pickup-address-empty { color: #909399; font-size: 13px; }
|
||||||
|
.pickup-checkout { position: sticky; bottom: 0; z-index: 2; display: flex; justify-content: space-between; align-items: center; gap: 20px; margin-top: 24px; padding: 20px 0; border-top: 1px solid #eee; background: #fff; }
|
||||||
|
.pickup-selection { display: flex; flex-direction: column; gap: 8px; font-size: 14px; }
|
||||||
|
.pickup-selection strong { font-weight: 500; }
|
||||||
|
.pickup-selection b { color: $theme_color; font-weight: 600; }
|
||||||
|
.pickup-selection > span { color: #909399; font-size: 12px; }
|
||||||
|
.pickup-submit { display: flex; align-items: center; gap: 24px; }
|
||||||
|
.pickup-payable { font-size: 13px; color: #606266; }
|
||||||
|
.pickup-payable strong { margin-left: 6px; color: $theme_color; font-size: 26px; font-weight: 600; }
|
||||||
|
.pickup-payable strong span { font-size: 16px; }
|
||||||
|
.pickup-payable small { display: block; margin-top: 4px; text-align: right; font-size: 12px; color: #909399; }
|
||||||
|
.pickup-submit :deep(.el-button) { min-width: 136px; height: 42px; border-radius: 6px; font-size: 15px; }
|
||||||
|
.pickup-terms { margin: 0; padding-bottom: 6px; color: #a0a5ae; font-size: 12px; line-height: 1.6; }
|
||||||
|
@media (max-width: 900px) { .pickup-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||||
|
@media (max-width: 600px) { .pickup-grid { grid-template-columns: minmax(0, 1fr); } .pickup-checkout { flex-wrap: wrap; } .pickup-submit { width: 100%; justify-content: space-between; } .pickup-section-title { flex-wrap: wrap; } }
|
||||||
|
</style>
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
<template v-else>{{ item.discount }}折</template>
|
<template v-else>{{ item.discount }}折</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="pay-coupon-info">
|
<div class="pay-coupon-info">
|
||||||
<p>满{{ item.consumeThreshold }}元可用</p>
|
<p>{{ item.consumeThreshold > 0 ? '满' + item.consumeThreshold + '元可用' : '无门槛使用' }}</p>
|
||||||
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
|
||||||
<p>有效期:{{ item.endTime }}</p>
|
<p>有效期:{{ item.endTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="background:#fff;">
|
<div style="background:#fff;">
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search style="border-bottom:2px solid red;"></Search>
|
<SearchHeader></SearchHeader>
|
||||||
<div class="base-width cate-container">
|
<div class="base-width cate-container">
|
||||||
<el-breadcrumb>
|
<el-breadcrumb>
|
||||||
<el-breadcrumb-item to="/">首页</el-breadcrumb-item>
|
<el-breadcrumb-item to="/">首页</el-breadcrumb-item>
|
||||||
@@ -106,7 +106,8 @@ export default {
|
|||||||
import { ref, computed, onMounted, nextTick } from "vue";
|
import { ref, computed, onMounted, nextTick } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import Search from "@/components/Search";
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import { addCartGoods } from "@/api/cart.js";
|
import { addCartGoods } from "@/api/cart.js";
|
||||||
import { pointGoodsDetail } from "@/api/promotion";
|
import { pointGoodsDetail } from "@/api/promotion";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="point-mall">
|
<div class="point-mall">
|
||||||
<BaseHeader></BaseHeader>
|
<TopBar></TopBar>
|
||||||
<Search></Search>
|
<SearchHeader></SearchHeader>
|
||||||
<cateNav></cateNav>
|
<cateNav></cateNav>
|
||||||
<h3 class="promotion-decorate">积分商品</h3>
|
<h3 class="promotion-decorate">积分商品</h3>
|
||||||
<el-select @select="selectCate" size="small" class="cate-select-con" v-model="cateId">
|
<el-select @select="selectCate" size="small" class="cate-select-con" v-model="cateId">
|
||||||
@@ -46,6 +46,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import TopBar from '@/components/indexDecorate/components/TopBar.vue';
|
||||||
|
import SearchHeader from '@/components/indexDecorate/components/SearchHeader.vue';
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { pointGoods, pointGoodsCategory } from "@/api/promotion.js";
|
import { pointGoods, pointGoodsCategory } from "@/api/promotion.js";
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ watch(currIndex, (val) => {
|
|||||||
interval = null;
|
interval = null;
|
||||||
nowHour.value = new Date().getHours();
|
nowHour.value = new Date().getHours();
|
||||||
countDown(val);
|
countDown(val);
|
||||||
goodsList.value = list.value[val].seckillGoodsList;
|
goodsList.value = list.value[val]?.seckillGoodsList || [];
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(diffSeconds, (val) => {
|
watch(diffSeconds, (val) => {
|
||||||
@@ -108,11 +108,11 @@ watch(diffSeconds, (val) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getListByDay () {
|
function getListByDay () {
|
||||||
// 当天秒杀活动
|
// 当天秒杀活动;result 可能为空数组(当天无活动),需判空避免取下标 0 报错
|
||||||
seckillByDay().then((res) => {
|
seckillByDay().then((res) => {
|
||||||
if (res.success) {
|
if (res.success && res.result && res.result.length) {
|
||||||
list.value = res.result;
|
list.value = res.result;
|
||||||
goodsList.value = list.value[0].seckillGoodsList;
|
goodsList.value = list.value[0].seckillGoodsList || [];
|
||||||
countDown(currIndex.value);
|
countDown(currIndex.value);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,27 +5,33 @@
|
|||||||
:model="form"
|
:model="form"
|
||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-width="140px"
|
label-width="140px"
|
||||||
|
scroll-to-error
|
||||||
@submit.prevent
|
@submit.prevent
|
||||||
@keydown.enter.prevent
|
@keydown.enter.prevent
|
||||||
@focusout="saveDraft"
|
@focusout="saveDraft"
|
||||||
>
|
>
|
||||||
<h4>基础信息</h4>
|
<div class="section-title">基础信息</div>
|
||||||
<el-form-item prop="companyName" label="公司名称">
|
<el-form-item prop="companyName" label="公司名称">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="form.companyName"
|
v-model="form.companyName"
|
||||||
placeholder="请填写公司信息"
|
placeholder="请填写公司名称"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="companyAddressIdPath" label="公司所在地">
|
<el-form-item prop="companyAddressIdPath" label="公司所在地">
|
||||||
<span>{{ form.companyAddressPath || '暂无地址' }}</span>
|
<div class="address-select" @click="openMap">
|
||||||
<el-button type="default" style="margin-left: 10px;" @click="$refs.map.open()">选择</el-button>
|
<el-icon class="addr-icon"><Location /></el-icon>
|
||||||
|
<span class="addr-text" :class="{ placeholder: !form.companyAddressPath }">
|
||||||
|
{{ form.companyAddressPath || '请选择公司所在地' }}
|
||||||
|
</span>
|
||||||
|
<span class="addr-action">{{ form.companyAddressPath ? '重新选择' : '选择' }}</span>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="companyAddress" label="公司详细地址">
|
<el-form-item prop="companyAddress" label="公司详细地址">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
v-model="form.companyAddress"
|
v-model="form.companyAddress"
|
||||||
placeholder="请填写公司详细信息"
|
placeholder="请填写公司详细地址"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="employeeNum" label="员工总数">
|
<el-form-item prop="employeeNum" label="员工总数">
|
||||||
@@ -71,7 +77,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<h4>营业执照信息</h4>
|
<div class="section-title">营业执照信息</div>
|
||||||
<el-form-item prop="licenseNum" label="营业执照号">
|
<el-form-item prop="licenseNum" label="营业执照号">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -92,25 +98,23 @@
|
|||||||
<el-form-item prop="licencePhoto" label="营业执照电子版">
|
<el-form-item prop="licencePhoto" label="营业执照电子版">
|
||||||
<div class="upload-wrap">
|
<div class="upload-wrap">
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="uploadLicence"
|
v-if="form.licencePhoto.length < 1"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleSuccess"
|
:on-success="handleSuccess"
|
||||||
accept=".jpg,.jpeg,.png,.gif"
|
accept=".jpg,.jpeg,.png,.gif"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:on-error="uploadErr"
|
:on-error="uploadErr"
|
||||||
:disabled="form.licencePhoto.length >= 1"
|
|
||||||
:action="action"
|
:action="action"
|
||||||
:headers="accessToken"
|
:headers="accessToken"
|
||||||
>
|
>
|
||||||
<el-button
|
<div class="upload-box">
|
||||||
type="info"
|
<el-icon v-if="uploadLoading" class="is-loading"><Loading /></el-icon>
|
||||||
:loading="uploadLoading"
|
<el-icon v-else><Plus /></el-icon>
|
||||||
:disabled="form.licencePhoto.length >= 1"
|
<span>上传营业执照</span>
|
||||||
>证照上传</el-button
|
</div>
|
||||||
>
|
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="describe">
|
<div class="describe">
|
||||||
请压缩图片在2M以内,格式为gif,jpg,png,并确保文字清晰,以免上传或审核失败,仅可上传1张
|
请压缩图片在2M以内,格式为gif、jpg、png,并确保文字清晰,以免上传或审核失败,仅可上传1张
|
||||||
</div>
|
</div>
|
||||||
<div class="img-list-wrap">
|
<div class="img-list-wrap">
|
||||||
<div
|
<div
|
||||||
@@ -128,7 +132,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<h4>法人信息</h4>
|
<div class="section-title">法人信息</div>
|
||||||
<el-form-item prop="legalName" label="法人姓名">
|
<el-form-item prop="legalName" label="法人姓名">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -147,7 +151,7 @@
|
|||||||
<el-form-item prop="legalPhoto" label="法人证件电子版">
|
<el-form-item prop="legalPhoto" label="法人证件电子版">
|
||||||
<div class="upload-wrap">
|
<div class="upload-wrap">
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="uploadLegal"
|
v-if="form.legalPhoto.length < 2"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleSuccess1"
|
:on-success="handleSuccess1"
|
||||||
:before-upload="beforeUpload1"
|
:before-upload="beforeUpload1"
|
||||||
@@ -157,7 +161,11 @@
|
|||||||
:action="action"
|
:action="action"
|
||||||
:headers="accessToken"
|
:headers="accessToken"
|
||||||
>
|
>
|
||||||
<el-button type="info" :loading="uploadLoading1">证照上传</el-button>
|
<div class="upload-box">
|
||||||
|
<el-icon v-if="uploadLoading1" class="is-loading"><Loading /></el-icon>
|
||||||
|
<el-icon v-else><Plus /></el-icon>
|
||||||
|
<span>上传法人证件</span>
|
||||||
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="describe">
|
<div class="describe">
|
||||||
请压缩图片在2M以内,身份证正反面两张照片,确保图片清晰无缺角
|
请压缩图片在2M以内,身份证正反面两张照片,确保图片清晰无缺角
|
||||||
@@ -178,9 +186,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" :loading="loading" @click="next"
|
<div class="form-actions">
|
||||||
>填写财务资质信息</el-button
|
<el-button type="primary" size="large" :loading="loading" @click="next"
|
||||||
>
|
>下一步:填写财务资质信息</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-dialog title="图片预览" v-model="visible">
|
<el-dialog title="图片预览" v-model="visible">
|
||||||
@@ -192,8 +202,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
import { onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||||
import { Message, Notice } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { Delete, View } from '@element-plus/icons-vue';
|
import { Delete, Loading, Location, Plus, View } from '@element-plus/icons-vue';
|
||||||
import { applyFirst } from '@/api/shopentry';
|
import { applyFirst } from '@/api/shopentry';
|
||||||
import * as RegExp from '@/plugins/RegExp.js';
|
import * as RegExp from '@/plugins/RegExp.js';
|
||||||
import multipleMap from "@/components/map/multiple-map";
|
import multipleMap from "@/components/map/multiple-map";
|
||||||
@@ -237,7 +247,7 @@ const form = ref({ // 表单数据
|
|||||||
licencePhoto: []
|
licencePhoto: []
|
||||||
});
|
});
|
||||||
const rules = { // 验证规则
|
const rules = { // 验证规则
|
||||||
companyName: [{ required: true, message: '请填写公司信息' }],
|
companyName: [{ required: true, message: '请填写公司名称' }],
|
||||||
companyAddressIdPath: [{ required: true, message: '请选择公司所在地' }],
|
companyAddressIdPath: [{ required: true, message: '请选择公司所在地' }],
|
||||||
companyAddress: [{ required: true, message: '请填写公司详细地址' }],
|
companyAddress: [{ required: true, message: '请填写公司详细地址' }],
|
||||||
employeeNum: [
|
employeeNum: [
|
||||||
@@ -254,30 +264,31 @@ const rules = { // 验证规则
|
|||||||
{ pattern: RegExp.mobile, message: '请填写正确的号码' }
|
{ pattern: RegExp.mobile, message: '请填写正确的号码' }
|
||||||
],
|
],
|
||||||
companyPhone: [
|
companyPhone: [
|
||||||
{ required: true, message: '请填写公司电话' },
|
{ required: true, message: '请填写公司电话' }
|
||||||
{ pattern: RegExp.mobile, message: '请填写正确的号码' }
|
|
||||||
],
|
],
|
||||||
companyEmail: [
|
companyEmail: [
|
||||||
{ required: true, message: '请填写电子邮箱' },
|
{ required: true, message: '请填写电子邮箱' },
|
||||||
{ type: 'email', message: '请输入正确的邮箱' }
|
{ type: 'email', message: '请输入正确的邮箱' }
|
||||||
],
|
],
|
||||||
licenseNum: [
|
licenseNum: [
|
||||||
{ required: true, message: '请填写营业执照号' },
|
{ required: true, message: '请填写营业执照号' }
|
||||||
{ pattern: RegExp.licenseNum, message: '请输入正确的营业执照号' }
|
|
||||||
],
|
],
|
||||||
scope: [{ required: true, message: '请填写营业执照所示经营范围' }],
|
scope: [{ required: true, message: '请填写营业执照所示经营范围' }],
|
||||||
legalPhoto: [{ required: true, message: '请上传法人身份证照片' }],
|
legalPhoto: [{ required: true, message: '请上传法人身份证照片' }],
|
||||||
licencePhoto: [{ required: true, message: '请上传营业执照' }],
|
licencePhoto: [{ required: true, message: '请上传营业执照' }],
|
||||||
legalName: [{ required: true, message: '请输入法人姓名' }],
|
legalName: [{ required: true, message: '请输入法人姓名' }],
|
||||||
legalId: [
|
legalId: [
|
||||||
{ required: true, message: '请输入法人证件号' },
|
{ required: true, message: '请输入法人证件号' }
|
||||||
{ pattern: RegExp.IDCard, message: '请输入正确的证件号' }
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const uploadLoading1 = ref(false); // 上传loading
|
const uploadLoading1 = ref(false); // 上传loading
|
||||||
const uploadLoading = ref(false); // 上传loading
|
const uploadLoading = ref(false); // 上传loading
|
||||||
const contentInitialized = ref(false);
|
const contentInitialized = ref(false);
|
||||||
|
|
||||||
|
function openMap() {
|
||||||
|
map.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
// 获取店铺地址
|
// 获取店铺地址
|
||||||
function getAddress(val){
|
function getAddress(val){
|
||||||
if(val.type === 'select'){
|
if(val.type === 'select'){
|
||||||
@@ -352,38 +363,34 @@ function validateUploadFile (file) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 上传成功回调
|
// 上传成功回调(el-upload 只认 HTTP 状态码,业务失败 success:false 也会进这里,需要判断)
|
||||||
function handleSuccess (res, file) {
|
function handleSuccess (res, file) {
|
||||||
uploadLoading.value = false;
|
uploadLoading.value = false;
|
||||||
form.value.licencePhoto = [res.result];
|
if (res && res.success && res.result) {
|
||||||
|
form.value.licencePhoto = [res.result];
|
||||||
|
} else {
|
||||||
|
Message.error(res && res.message ? res.message : '上传失败,请重试');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 上传成功回调
|
// 上传成功回调
|
||||||
function handleSuccess1 (res, file) {
|
function handleSuccess1 (res, file) {
|
||||||
uploadLoading1.value = false;
|
uploadLoading1.value = false;
|
||||||
form.value.legalPhoto.push(res.result);
|
if (res && res.success && res.result) {
|
||||||
|
form.value.legalPhoto.push(res.result);
|
||||||
|
} else {
|
||||||
|
Message.error(res && res.message ? res.message : '上传失败,请重试');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 上传失败
|
// 上传失败(非 2xx:err.message 通常是响应体字符串,尝试解析出服务端提示)
|
||||||
function uploadErr () {
|
function uploadErr (err) {
|
||||||
uploadLoading.value = false;
|
uploadLoading.value = false;
|
||||||
uploadLoading1.value = false;
|
uploadLoading1.value = false;
|
||||||
}
|
let msg = '上传失败,请重试';
|
||||||
// 上传失败回调
|
try {
|
||||||
function handleFormatError (file) {
|
const body = JSON.parse(err && err.message);
|
||||||
uploadLoading.value = false;
|
if (body && body.message) msg = body.message;
|
||||||
uploadLoading1.value = false;
|
} catch (e) { /* 非 JSON 响应,用默认提示 */ }
|
||||||
Notice.warning({
|
Message.error(msg);
|
||||||
title: 'The file format is incorrect',
|
|
||||||
desc: '上传文件格式不正确'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 上传大小限制
|
|
||||||
function handleMaxSize (file) {
|
|
||||||
uploadLoading.value = false;
|
|
||||||
uploadLoading1.value = false;
|
|
||||||
Notice.warning({
|
|
||||||
title: 'Exceeding file size limit',
|
|
||||||
desc: '文件大小不能超过2M'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// 图片查看
|
// 图片查看
|
||||||
function handleView (item) {
|
function handleView (item) {
|
||||||
@@ -456,19 +463,76 @@ onBeforeUnmount(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h4 {
|
.section-title {
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
padding: 0 10px;
|
align-items: center;
|
||||||
border: 1px solid #ddd;
|
margin: 24px 0 18px;
|
||||||
background-color: #f8f8f8;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #303133;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 40px;
|
&::before {
|
||||||
text-align: left;
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: $theme_color;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.el-input {
|
.el-input {
|
||||||
width: 300px;
|
width: 360px;
|
||||||
|
}
|
||||||
|
.el-textarea {
|
||||||
|
width: 640px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.address-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 360px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-icon {
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-text {
|
||||||
|
flex: 1;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #a8abb2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-action {
|
||||||
|
color: $theme_color;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.img-list-wrap {
|
.img-list-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -486,6 +550,7 @@ h4 {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
@@ -531,6 +596,32 @@ h4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border: 1px dashed #c0c4cc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.describe {
|
.describe {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="photo-msg">
|
<div class="photo-msg">
|
||||||
<el-form ref="secondForm" :model="form" :rules="rules" label-width="140px">
|
<el-form ref="secondForm" :model="form" :rules="rules" label-width="140px" scroll-to-error>
|
||||||
<h4>基础信息</h4>
|
<div class="section-title">结算银行信息</div>
|
||||||
<el-form-item prop="settlementBankAccountName" label="银行开户名">
|
<el-form-item prop="settlementBankAccountName" label="银行开户名">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -32,18 +32,22 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button @click="$emit('change', 0)">返回</el-button>
|
<div class="form-actions">
|
||||||
<el-button type="primary" :loading="loading" @click="next"
|
<el-button size="large" @click="$emit('change', 0)">上一步</el-button>
|
||||||
>填写其他信息</el-button
|
<el-button type="primary" size="large" :loading="loading" @click="next"
|
||||||
>
|
>下一步:填写其他信息</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||||
import { applySecond } from '@/api/shopentry';
|
import { applySecond } from '@/api/shopentry';
|
||||||
|
|
||||||
|
const SECOND_APPLY_DRAFT_KEY = 'shopEntrySecondDraft';
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
content: {
|
content: {
|
||||||
@@ -78,7 +82,10 @@ function next() {
|
|||||||
applySecond(form)
|
applySecond(form)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) emit('change', 2);
|
if (res.success) {
|
||||||
|
sessionStorage.removeItem(SECOND_APPLY_DRAFT_KEY);
|
||||||
|
emit('change', 2);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -87,25 +94,62 @@ function next() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function restoreDraft() {
|
||||||
|
const draft = sessionStorage.getItem(SECOND_APPLY_DRAFT_KEY);
|
||||||
|
if (!draft) return;
|
||||||
|
try {
|
||||||
|
Object.assign(form, JSON.parse(draft));
|
||||||
|
} catch (e) {
|
||||||
|
sessionStorage.removeItem(SECOND_APPLY_DRAFT_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDraft() {
|
||||||
|
sessionStorage.setItem(SECOND_APPLY_DRAFT_KEY, JSON.stringify(form));
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.content && Object.keys(props.content).length) {
|
if (props.content && Object.keys(props.content).length) {
|
||||||
Object.assign(form, JSON.parse(JSON.stringify(props.content)));
|
Object.assign(form, JSON.parse(JSON.stringify(props.content)));
|
||||||
|
} else {
|
||||||
|
restoreDraft();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
saveDraft();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h4 {
|
.section-title {
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
padding: 0 10px;
|
align-items: center;
|
||||||
border: 1px solid #ddd;
|
margin: 24px 0 18px;
|
||||||
background-color: #f8f8f8;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #303133;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 40px;
|
&::before {
|
||||||
text-align: left;
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: $theme_color;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.el-input {
|
.el-input {
|
||||||
width: 300px;
|
width: 360px;
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,14 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="shop-entry">
|
<div class="shop-entry">
|
||||||
<div style="height: 20px"></div>
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<h1>店铺入驻</h1>
|
<div class="page-header">
|
||||||
<el-steps :active="currentIndex" class="margin" finish-status="success">
|
<div class="header-icon">
|
||||||
<el-step title="企业资质信息"></el-step>
|
<el-icon><Shop /></el-icon>
|
||||||
<el-step title="财务资质信息"></el-step>
|
</div>
|
||||||
<el-step title="其他信息"></el-step>
|
<div class="header-text">
|
||||||
<el-step title="提交审核"></el-step>
|
<h1>店铺入驻</h1>
|
||||||
</el-steps>
|
<p>完善企业资质信息,快速开启您的电商之旅</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="steps-bar">
|
||||||
|
<div
|
||||||
|
v-for="(step, i) in steps"
|
||||||
|
:key="i"
|
||||||
|
class="step-item"
|
||||||
|
:class="{
|
||||||
|
active: currentIndex === i,
|
||||||
|
done: currentIndex > i,
|
||||||
|
clickable: canClickStep(i),
|
||||||
|
}"
|
||||||
|
@click="onStepClick(i)"
|
||||||
|
>
|
||||||
|
<div class="step-circle">
|
||||||
|
<el-icon v-if="currentIndex > i"><Check /></el-icon>
|
||||||
|
<span v-else>{{ i + 1 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="step-title">{{ step.title }}</div>
|
||||||
|
<div class="step-desc">{{ step.desc }}</div>
|
||||||
|
<div class="step-line" v-if="i < steps.length - 1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="initLoading" class="step-loading" v-loading="true"></div>
|
<div v-if="initLoading" class="step-loading" v-loading="true"></div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<first-apply
|
<first-apply
|
||||||
@@ -30,34 +54,68 @@
|
|||||||
></third-apply>
|
></third-apply>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="success-page" v-if="currentIndex == 3">
|
<div class="status-page" v-if="currentIndex === 3">
|
||||||
<span v-if="storeDisable == '' || storeDisable == 'APPLYING'">入驻申请提交成功,等待平台审核</span>
|
<el-result
|
||||||
<span v-if="storeDisable == 'OPEN'">申请已通过,请联系管理员</span>
|
v-if="storeDisable === '' || storeDisable === 'APPLYING'"
|
||||||
<span v-if="storeDisable == 'CLOSED'">店铺已关闭,重申请联系管理员</span>
|
icon="info"
|
||||||
<span v-if="storeDisable == 'REFUSED'">审核未通过,请修改资质信息,如有疑问请联系管理员</span>
|
title="入驻申请提交成功"
|
||||||
|
sub-title="您的入驻申请已提交,请耐心等待平台审核"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
<el-result
|
||||||
|
v-else-if="storeDisable === 'OPEN'"
|
||||||
|
icon="success"
|
||||||
|
title="申请已通过"
|
||||||
|
sub-title="您的店铺已开通,如有问题请联系管理员"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
<el-result
|
||||||
|
v-else-if="storeDisable === 'CLOSED'"
|
||||||
|
icon="warning"
|
||||||
|
title="店铺已关闭"
|
||||||
|
sub-title="如需重新入驻,请联系管理员"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button type="primary" @click="$router.push('/')">返回首页</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
|
<el-result
|
||||||
|
v-else-if="storeDisable === 'REFUSED'"
|
||||||
|
icon="error"
|
||||||
|
title="审核未通过"
|
||||||
|
sub-title="请修改资质信息后重新提交,如有疑问请联系管理员"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<el-button @click="$router.push('/')">返回首页</el-button>
|
||||||
|
<el-button type="primary" @click="currentIndex = 0">重新申请</el-button>
|
||||||
|
</template>
|
||||||
|
</el-result>
|
||||||
</div>
|
</div>
|
||||||
<el-button v-if="currentIndex === 3" @click="$router.push('/')">返回</el-button>
|
|
||||||
|
|
||||||
<el-button type="primary" @click='currentIndex = 0'
|
|
||||||
v-if="storeDisable === 'REFUSED' && currentIndex === 3">重新申请</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-dialog title="店铺入驻协议" v-model="showAgreement" width="1200" :show-close="false" :close-on-click-modal="false">
|
<el-dialog title="店铺入驻协议" v-model="showAgreement" width="900px" :show-close="false" :close-on-click-modal="false">
|
||||||
<div class="agreement-scroll">
|
<div class="agreement-scroll">
|
||||||
<div class="agreeent-con" v-html="agreementCon"></div>
|
<div class="agreeent-con" v-html="agreementCon"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer><div style="text-align: center">
|
<template #footer><div style="text-align: center">
|
||||||
<p>
|
<p>
|
||||||
<el-checkbox v-model="checked">我已同意以上协议</el-checkbox>
|
<el-checkbox v-model="checked">我已阅读并同意以上协议</el-checkbox>
|
||||||
</p>
|
</p>
|
||||||
<el-button type="primary" :disabled="!checked" class="margin" @click="confirmAgreement">同意协议填写资质信息</el-button>
|
<el-button type="primary" :disabled="!checked" class="margin" @click="confirmAgreement">同意协议,填写资质信息</el-button>
|
||||||
</div></template>
|
</div></template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onMounted, reactive, ref } from "vue";
|
||||||
|
import { Check, Shop } from "@element-plus/icons-vue";
|
||||||
import { agreement, applyStatus } from "@/api/shopentry";
|
import { agreement, applyStatus } from "@/api/shopentry";
|
||||||
import firstApply from "./FirstApply";
|
import firstApply from "./FirstApply";
|
||||||
import secondApply from "./SecondApply";
|
import secondApply from "./SecondApply";
|
||||||
@@ -74,6 +132,24 @@ const storeDisable = ref(""); // APPLY OPEN 开店中 CLOSED 关闭 REFUSED 拒
|
|||||||
const initLoading = ref(true);
|
const initLoading = ref(true);
|
||||||
const agreementAccepted = ref(false); // 是否已同意入驻协议
|
const agreementAccepted = ref(false); // 是否已同意入驻协议
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ title: "企业资质信息", desc: "填写企业与法人资料" },
|
||||||
|
{ title: "财务资质信息", desc: "填写结算银行信息" },
|
||||||
|
{ title: "其他信息", desc: "完善店铺资料" },
|
||||||
|
{ title: "提交审核", desc: "等待平台审核" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 已完成步骤可点击回跳;审核状态页仅在 REFUSED 时允许回跳修改
|
||||||
|
function canClickStep(i) {
|
||||||
|
if (!agreementAccepted.value || i >= currentIndex.value) return false;
|
||||||
|
if (currentIndex.value === 3) return storeDisable.value === "REFUSED";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStepClick(i) {
|
||||||
|
if (canClickStep(i)) nextPage(i);
|
||||||
|
}
|
||||||
|
|
||||||
function confirmAgreement() {
|
function confirmAgreement() {
|
||||||
agreementAccepted.value = true;
|
agreementAccepted.value = true;
|
||||||
showAgreement.value = false;
|
showAgreement.value = false;
|
||||||
@@ -193,16 +269,126 @@ onMounted(() => {
|
|||||||
min-height: 500px;
|
min-height: 500px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 10px 20px;
|
padding: 28px 48px 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
margin-top: 20px;
|
p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.steps-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin: 28px 0 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover .step-title {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .step-circle {
|
||||||
|
border-color: $theme_color;
|
||||||
|
background: $theme_color;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active .step-title {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done .step-circle {
|
||||||
|
border-color: $theme_color;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.done .step-line {
|
||||||
|
background: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-circle {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #dcdfe6;
|
||||||
|
background: #fff;
|
||||||
|
color: #909399;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
transition: color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-desc {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-line {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: calc(50% + 28px);
|
||||||
|
width: calc(100% - 56px);
|
||||||
|
height: 2px;
|
||||||
|
background: #e4e7ed;
|
||||||
|
transition: background 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
.margin {
|
.margin {
|
||||||
margin: 30px 0;
|
margin: 30px 0 0;
|
||||||
}
|
}
|
||||||
.agreeent-con {
|
.agreeent-con {
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
@@ -218,12 +404,11 @@ onMounted(() => {
|
|||||||
.step-loading {
|
.step-loading {
|
||||||
min-height: 320px;
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
.success-page {
|
.status-page {
|
||||||
height: 500px;
|
min-height: 420px;
|
||||||
width: 100%;
|
display: flex;
|
||||||
line-height: 500px;
|
align-items: center;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
font-size: 20px;
|
|
||||||
}
|
}
|
||||||
.shop-entry {
|
.shop-entry {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="person-msg">
|
<div class="person-msg">
|
||||||
<el-form ref="thirdFormRef" :model="form" :rules="rules" label-width="140px">
|
<el-form ref="thirdFormRef" :model="form" :rules="rules" label-width="140px" scroll-to-error>
|
||||||
<h4>基础信息</h4>
|
<div class="section-title">店铺信息</div>
|
||||||
<el-form-item prop="storeName" label="店铺名称">
|
<el-form-item prop="storeName" label="店铺名称">
|
||||||
<el-input
|
<el-input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -13,19 +13,22 @@
|
|||||||
<el-form-item prop="storeLogo" label="店铺logo">
|
<el-form-item prop="storeLogo" label="店铺logo">
|
||||||
<div class="upload-wrap">
|
<div class="upload-wrap">
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="uploadLogo"
|
v-if="form.storeLogo.length < 1"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleSuccess"
|
:on-success="handleSuccess"
|
||||||
accept=".jpg,.jpeg,.png,.gif"
|
accept=".jpg,.jpeg,.png,.gif"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
:on-error="uploadErr"
|
:on-error="uploadErr"
|
||||||
:disabled="form.storeLogo.length >= 1"
|
|
||||||
:action="action"
|
:action="action"
|
||||||
:headers="accessToken"
|
:headers="accessToken"
|
||||||
>
|
>
|
||||||
<el-button type="info" :loading="uploadLoading" :disabled="form.storeLogo.length >= 1">上传logo</el-button>
|
<div class="upload-box">
|
||||||
|
<el-icon v-if="uploadLoading" class="is-loading"><Loading /></el-icon>
|
||||||
|
<el-icon v-else><Plus /></el-icon>
|
||||||
|
<span>上传logo</span>
|
||||||
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="describe">请压缩图片在2M以内,格式为gif,jpg,png,仅可上传1张</div>
|
<div class="describe">请压缩图片在2M以内,格式为gif、jpg、png,仅可上传1张</div>
|
||||||
<div class="img-list-wrap">
|
<div class="img-list-wrap">
|
||||||
<div
|
<div
|
||||||
class="img-list"
|
class="img-list"
|
||||||
@@ -45,8 +48,8 @@
|
|||||||
<el-select
|
<el-select
|
||||||
v-model="form.goodsManagementCategory"
|
v-model="form.goodsManagementCategory"
|
||||||
multiple
|
multiple
|
||||||
placeholder="请选择"
|
placeholder="请选择店铺经营类目"
|
||||||
style="width: 300px"
|
style="width: 360px"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in categoryList"
|
v-for="item in categoryList"
|
||||||
@@ -58,8 +61,13 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item prop="storeAddressIdPath" label="店铺所在地">
|
<el-form-item prop="storeAddressIdPath" label="店铺所在地">
|
||||||
<span>{{ form.storeAddressPath || '暂无地址' }}</span>
|
<div class="address-select" @click="openMap">
|
||||||
<el-button type="default" style="margin-left: 10px;" @click="$refs.map.open()">选择</el-button>
|
<el-icon class="addr-icon"><Location /></el-icon>
|
||||||
|
<span class="addr-text" :class="{ placeholder: !form.storeAddressPath }">
|
||||||
|
{{ form.storeAddressPath || '请选择店铺所在地' }}
|
||||||
|
</span>
|
||||||
|
<span class="addr-action">{{ form.storeAddressPath ? '重新选择' : '选择' }}</span>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="storeAddressDetail" label="店铺详细地址">
|
<el-form-item prop="storeAddressDetail" label="店铺详细地址">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -80,10 +88,12 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button @click="$emit('change', 1)">返回</el-button>
|
<div class="form-actions">
|
||||||
<el-button type="primary" :loading="loading" @click="next"
|
<el-button size="large" @click="$emit('change', 1)">上一步</el-button>
|
||||||
>提交平台审核</el-button
|
<el-button type="primary" size="large" :loading="loading" @click="next"
|
||||||
>
|
>提交平台审核</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-dialog title="图片预览" v-model="visible">
|
<el-dialog title="图片预览" v-model="visible">
|
||||||
@@ -93,9 +103,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive, ref } from "vue";
|
import { onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { Delete, View } from '@element-plus/icons-vue';
|
import { Delete, Loading, Location, Plus, View } from '@element-plus/icons-vue';
|
||||||
import { applyThird } from '@/api/shopentry';
|
import { applyThird } from '@/api/shopentry';
|
||||||
import { getCategory } from '@/api/goods';
|
import { getCategory } from '@/api/goods';
|
||||||
|
|
||||||
@@ -105,6 +115,7 @@ import { buyerUrl } from '@/plugins/request.js';
|
|||||||
|
|
||||||
import multipleMap from "@/components/map/multiple-map";
|
import multipleMap from "@/components/map/multiple-map";
|
||||||
|
|
||||||
|
const THIRD_APPLY_DRAFT_KEY = 'shopEntryThirdDraft';
|
||||||
|
|
||||||
const emit = defineEmits(['change']);
|
const emit = defineEmits(['change']);
|
||||||
|
|
||||||
@@ -116,6 +127,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const thirdFormRef = ref(null);
|
const thirdFormRef = ref(null);
|
||||||
|
const map = ref(null);
|
||||||
|
|
||||||
const loading = ref(false); // 加载状态
|
const loading = ref(false); // 加载状态
|
||||||
const uploadLoading = ref(false); // 上传加载状态
|
const uploadLoading = ref(false); // 上传加载状态
|
||||||
@@ -147,6 +159,10 @@ const rules = { // 验证规则
|
|||||||
};
|
};
|
||||||
const categoryList = ref([]); // 分类数据
|
const categoryList = ref([]); // 分类数据
|
||||||
|
|
||||||
|
function openMap() {
|
||||||
|
map.value?.open();
|
||||||
|
}
|
||||||
|
|
||||||
// 下一步
|
// 下一步
|
||||||
function next () {
|
function next () {
|
||||||
thirdFormRef.value.validate((valid) => {
|
thirdFormRef.value.validate((valid) => {
|
||||||
@@ -158,7 +174,10 @@ function next () {
|
|||||||
applyThird(params)
|
applyThird(params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) emit('change', 3);
|
if (res.success) {
|
||||||
|
sessionStorage.removeItem(THIRD_APPLY_DRAFT_KEY);
|
||||||
|
emit('change', 3);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -192,14 +211,24 @@ function validateUploadFile (file) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// 上传成功回调
|
// 上传成功回调(el-upload 只认 HTTP 状态码,业务失败 success:false 也会进这里,需要判断)
|
||||||
function handleSuccess (res, file) {
|
function handleSuccess (res, file) {
|
||||||
uploadLoading.value = false;
|
uploadLoading.value = false;
|
||||||
form.value.storeLogo = [res.result];
|
if (res && res.success && res.result) {
|
||||||
|
form.value.storeLogo = [res.result];
|
||||||
|
} else {
|
||||||
|
Message.error(res && res.message ? res.message : '上传失败,请重试');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 上传失败
|
// 上传失败(非 2xx:err.message 通常是响应体字符串,尝试解析出服务端提示)
|
||||||
function uploadErr () {
|
function uploadErr (err) {
|
||||||
uploadLoading.value = false;
|
uploadLoading.value = false;
|
||||||
|
let msg = '上传失败,请重试';
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(err && err.message);
|
||||||
|
if (body && body.message) msg = body.message;
|
||||||
|
} catch (e) { /* 非 JSON 响应,用默认提示 */ }
|
||||||
|
Message.error(msg);
|
||||||
}
|
}
|
||||||
// 查看图片
|
// 查看图片
|
||||||
function handleView (item) {
|
function handleView (item) {
|
||||||
@@ -277,28 +306,117 @@ function initFormFromContent (content) {
|
|||||||
if (form.value[key] == null) form.value[key] = "";
|
if (form.value[key] == null) form.value[key] = "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function restoreDraft () {
|
||||||
|
const draft = sessionStorage.getItem(THIRD_APPLY_DRAFT_KEY);
|
||||||
|
if (!draft) return;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(draft);
|
||||||
|
form.value = {
|
||||||
|
...createEmptyForm(),
|
||||||
|
...parsed,
|
||||||
|
storeLogo: Array.isArray(parsed.storeLogo) ? parsed.storeLogo : [],
|
||||||
|
goodsManagementCategory: Array.isArray(parsed.goodsManagementCategory)
|
||||||
|
? parsed.goodsManagementCategory
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
sessionStorage.removeItem(THIRD_APPLY_DRAFT_KEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function saveDraft () {
|
||||||
|
sessionStorage.setItem(THIRD_APPLY_DRAFT_KEY, JSON.stringify(form.value));
|
||||||
|
}
|
||||||
|
|
||||||
initFormFromContent(props.content);
|
if (Object.keys(props.content).length) {
|
||||||
|
initFormFromContent(props.content);
|
||||||
|
} else {
|
||||||
|
restoreDraft();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
accessToken.accessToken = storage.getItem('accessToken');
|
accessToken.accessToken = storage.getItem('accessToken');
|
||||||
getCategoryList();
|
getCategoryList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
saveDraft();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
h4 {
|
.section-title {
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
padding: 0 10px;
|
align-items: center;
|
||||||
border: 1px solid #ddd;
|
margin: 24px 0 18px;
|
||||||
background-color: #f8f8f8;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #303133;
|
||||||
font-size: 14px;
|
|
||||||
line-height: 40px;
|
&::before {
|
||||||
text-align: left;
|
content: '';
|
||||||
|
width: 4px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: $theme_color;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.el-input {
|
.el-input {
|
||||||
width: 300px;
|
width: 360px;
|
||||||
|
}
|
||||||
|
.el-textarea {
|
||||||
|
width: 640px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.address-select {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 360px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-icon {
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 6px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-text {
|
||||||
|
flex: 1;
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #a8abb2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.addr-action {
|
||||||
|
color: $theme_color;
|
||||||
|
margin-left: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.form-actions {
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.img-list-wrap {
|
.img-list-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -316,6 +434,7 @@ h4 {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
|
||||||
@@ -362,6 +481,32 @@ h4 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border: 1px dashed #c0c4cc;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: $theme_color;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.describe {
|
.describe {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -115,54 +115,6 @@ const member = [{
|
|||||||
// display: true
|
// display: true
|
||||||
// }]
|
// }]
|
||||||
|
|
||||||
// 批发中心
|
|
||||||
// const wholesale = [
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '批发中心',
|
|
||||||
// menus: [
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '我的采购单',
|
|
||||||
// path: 'MyOrder',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '采购退货单',
|
|
||||||
// path: 'MyOrder',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '我的求购单',
|
|
||||||
// path: 'MyOrder',
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// ],
|
|
||||||
// display: true
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
// 店铺
|
|
||||||
// const shop = [
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '店铺管理',
|
|
||||||
// menus: [
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '店铺后台',
|
|
||||||
// path: 'MyOrder',
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// icon: '',
|
|
||||||
// title: '商家等级',
|
|
||||||
// path: 'MyOrder',
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// display: true
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// wholesale[0], shop[0]
|
|
||||||
let menuList = []
|
let menuList = []
|
||||||
menuList.push(order[0], member[0])
|
menuList.push(order[0], member[0])
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import "element-plus/es/components/radio/style/css";
|
|||||||
import "element-plus/es/components/radio-button/style/css";
|
import "element-plus/es/components/radio-button/style/css";
|
||||||
import "element-plus/es/components/radio-group/style/css";
|
import "element-plus/es/components/radio-group/style/css";
|
||||||
import "element-plus/es/components/rate/style/css";
|
import "element-plus/es/components/rate/style/css";
|
||||||
|
import "element-plus/es/components/result/style/css";
|
||||||
import "element-plus/es/components/select/style/css";
|
import "element-plus/es/components/select/style/css";
|
||||||
import "element-plus/es/components/skeleton/style/css";
|
import "element-plus/es/components/skeleton/style/css";
|
||||||
import "element-plus/es/components/step/style/css";
|
import "element-plus/es/components/step/style/css";
|
||||||
@@ -94,6 +95,7 @@ import {
|
|||||||
ElRadioButton,
|
ElRadioButton,
|
||||||
ElRadioGroup,
|
ElRadioGroup,
|
||||||
ElRate,
|
ElRate,
|
||||||
|
ElResult,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSkeleton,
|
ElSkeleton,
|
||||||
ElStep,
|
ElStep,
|
||||||
@@ -149,6 +151,7 @@ const components = [
|
|||||||
ElRadioButton,
|
ElRadioButton,
|
||||||
ElRadioGroup,
|
ElRadioGroup,
|
||||||
ElRate,
|
ElRate,
|
||||||
|
ElResult,
|
||||||
ElSelect,
|
ElSelect,
|
||||||
ElSkeleton,
|
ElSkeleton,
|
||||||
ElStep,
|
ElStep,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const MyGiftCards = () => import("@/pages/home/userCenter/MyGiftCards");
|
|||||||
const Home = () => import("@/pages/user/Home");
|
const Home = () => import("@/pages/user/Home");
|
||||||
const UserCenter = () => import("@/pages/home/Main");
|
const UserCenter = () => import("@/pages/home/Main");
|
||||||
const Merchant = () => import("@/pages/Merchant");
|
const Merchant = () => import("@/pages/Merchant");
|
||||||
|
const StoreList = () => import("@/pages/StoreList");
|
||||||
const NotFound = () => import("@/pages/NotFound");
|
const NotFound = () => import("@/pages/NotFound");
|
||||||
const topic = () => import("@/pages/Topic");
|
const topic = () => import("@/pages/Topic");
|
||||||
const ShopEntry = () => import("@/pages/shopEntry/ShopEntry");
|
const ShopEntry = () => import("@/pages/shopEntry/ShopEntry");
|
||||||
@@ -199,6 +200,12 @@ export const router = createRouter({
|
|||||||
component: MoneyManagement,
|
component: MoneyManagement,
|
||||||
meta: { title: "我的余额" },
|
meta: { title: "我的余额" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "PickupCard",
|
||||||
|
name: "PickupCard",
|
||||||
|
component: () => import("@/pages/home/userCenter/PickupCard.vue"),
|
||||||
|
meta: { title: "提货卡兑换" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "MyGiftCards",
|
path: "MyGiftCards",
|
||||||
name: "MyGiftCards",
|
name: "MyGiftCards",
|
||||||
@@ -338,6 +345,12 @@ export const router = createRouter({
|
|||||||
component: Merchant,
|
component: Merchant,
|
||||||
meta: { title: "店铺" },
|
meta: { title: "店铺" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/storeList",
|
||||||
|
name: "StoreList",
|
||||||
|
component: StoreList,
|
||||||
|
meta: { title: "店铺列表" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:pathMatch(.*)*",
|
path: "/:pathMatch(.*)*",
|
||||||
name: "NotFound",
|
name: "NotFound",
|
||||||
|
|||||||
216
im/LICENSE
216
im/LICENSE
@@ -1,21 +1,205 @@
|
|||||||
MIT License
|
Apache License, Version 2.0
|
||||||
|
|
||||||
Copyright (c) 2020 YuanDong
|
Copyright (c) 2020 YuanDong
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Apache License
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
Version 2.0, January 2004
|
||||||
in the Software without restriction, including without limitation the rights
|
http://www.apache.org/licenses/
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
1. Definitions.
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
SOFTWARE.
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"babel-plugin-prismjs": "^2.0.1",
|
"babel-plugin-prismjs": "^2.0.1",
|
||||||
"core-js": "^3.49.0",
|
"core-js": "^3.49.0",
|
||||||
"element-plus": "^2.14.1",
|
"element-plus": "2.14.1",
|
||||||
"js-audio-recorder": "^1.0.6",
|
"js-audio-recorder": "^1.0.6",
|
||||||
"js-base64": "^2.5.1",
|
"js-base64": "^2.5.1",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
|
|||||||
@@ -34,11 +34,12 @@ function unloadHandler (e) {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setRefreshView(refreshView);
|
setRefreshView(refreshView);
|
||||||
window.addEventListener('beforeunload', e => beforeunloadHandler(e))
|
window.addEventListener('beforeunload', beforeunloadHandler)
|
||||||
window.addEventListener('unload', e => unloadHandler(e))
|
// unload 事件已被 Chrome 弃用(Permissions-Policy 禁止),改用 pagehide
|
||||||
|
window.addEventListener('pagehide', unloadHandler)
|
||||||
});
|
});
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('beforeunload', e => beforeunloadHandler(e))
|
window.removeEventListener('beforeunload', beforeunloadHandler)
|
||||||
window.removeEventListener('unload', e => unloadHandler(e))
|
window.removeEventListener('pagehide', unloadHandler)
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
resize: none;
|
resize: none !important;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-tabs v-model="activeName" @tab-click="handleClick" type="card" :stretch=true>
|
<el-tabs v-model="activeName" @tab-click="handleClick" :stretch=true>
|
||||||
<el-tab-pane :label="toUser.storeFlag ? '正在咨询' : '他的足迹'" name="history">
|
<el-tab-pane :label="toUser.storeFlag ? '正在咨询' : '他的足迹'" name="history">
|
||||||
<div>
|
<div>
|
||||||
<GoodsLink :goodsDetail="goodsDetail" v-if="toUser.userId === goodsDetail.storeId"
|
<GoodsLink :goodsDetail="goodsDetail" v-if="toUser.userId === goodsDetail.storeId"
|
||||||
@@ -306,4 +306,20 @@ function formatDateToString(date) {
|
|||||||
:deep(.el-tabs__header) {
|
:deep(.el-tabs__header) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 高度链:tabs 撑满面板,pane 高度传递给内部滚动容器 */
|
||||||
|
:deep(.el-tabs) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__content) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tab-pane) {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<aside class="avatar-column">
|
<aside class="avatar-column">
|
||||||
<div class="avatar-box" v-if="item.float == 'right'">
|
<div class="avatar-box" v-if="item.float == 'right'">
|
||||||
<face :text="face" v-if="face"></face>
|
<face :text="userFace" v-if="userFace"></face>
|
||||||
<face-null :text="name" v-else></face-null>
|
<face-null :text="name" v-else></face-null>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-box" v-if="item.float == 'left'">
|
<div class="avatar-box" v-if="item.float == 'left'">
|
||||||
@@ -53,14 +53,10 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<main class="main-column">
|
<main class="main-column">
|
||||||
<div class="talk-content">
|
<div class="talk-content">
|
||||||
<span class="nickname">
|
|
||||||
{{ item.float == "right" ? name : toUser.name }} |
|
|
||||||
{{ unixToDate(item.createTime, "MM月dd日 hh:mm") }}
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-a-c">
|
<div class="flex flex-a-c">
|
||||||
<legacy-el-icon name="el-icon-refresh-left" class="again main-color" @click="againSendMessage(item)" v-if="item.webSocketStatus" />
|
<legacy-el-icon name="el-icon-refresh-left" class="again main-color" @click="againSendMessage(item)" v-if="item.webSocketStatus" />
|
||||||
<!-- 文本消息 -->
|
<!-- 文本消息 -->
|
||||||
<div v-if="item.messageType == 'MESSAGE'" style="background-color: #d0e9ff;color: black;"
|
<div v-if="item.messageType == 'MESSAGE'"
|
||||||
class="text-message" :class="{
|
class="text-message" :class="{
|
||||||
left: item.float == 'left',
|
left: item.float == 'left',
|
||||||
right: item.float == 'right',
|
right: item.float == 'right',
|
||||||
@@ -79,7 +75,7 @@
|
|||||||
}">
|
}">
|
||||||
<div class="base" @click="linkToGoods(item.text.goodsId, item.text.id)">
|
<div class="base" @click="linkToGoods(item.text.goodsId, item.text.id)">
|
||||||
<div>
|
<div>
|
||||||
<img :src="item.text.thumbnail" class="image" />
|
<img :src="item.text.thumbnail" :onerror="defaultAvatar" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="goods_name">
|
<div class="goods_name">
|
||||||
@@ -106,7 +102,7 @@
|
|||||||
<div class="goods-shared-box">
|
<div class="goods-shared-box">
|
||||||
|
|
||||||
<div class="goods-item" v-for="(order,orderIndex) in item.text.orderItems" :key="orderIndex">
|
<div class="goods-item" v-for="(order,orderIndex) in item.text.orderItems" :key="orderIndex">
|
||||||
<img :src="order.image" style="height: 100px;width: 100px;" />
|
<img :src="order.image" :onerror="defaultAvatar" style="height: 100px;width: 100px;" />
|
||||||
<div>
|
<div>
|
||||||
<span class="orderGoodsName">{{ order.name }}</span>
|
<span class="orderGoodsName">{{ order.name }}</span>
|
||||||
<div class="goods-item-price">
|
<div class="goods-item-price">
|
||||||
@@ -212,6 +208,7 @@ import SocketInstance from "@/im-server/socket-instance";
|
|||||||
import { SvgMentionDown } from "@/core/icons";
|
import { SvgMentionDown } from "@/core/icons";
|
||||||
import { formatTime, parseTime, copyTextToClipboard } from "@/utils/functions";
|
import { formatTime, parseTime, copyTextToClipboard } from "@/utils/functions";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useTalkStore } from "@/stores/talk";
|
import { useTalkStore } from "@/stores/talk";
|
||||||
import { useNotifyStore } from "@/stores/notify";
|
import { useNotifyStore } from "@/stores/notify";
|
||||||
import { useSettingsStore } from "@/stores/settings";
|
import { useSettingsStore } from "@/stores/settings";
|
||||||
@@ -259,6 +256,7 @@ const emit = defineEmits(["close-talk"]);
|
|||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
const talkStore = useTalkStore();
|
const talkStore = useTalkStore();
|
||||||
const notifyStore = useNotifyStore();
|
const notifyStore = useNotifyStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
@@ -306,13 +304,16 @@ const panelHeader = ref(null);
|
|||||||
|
|
||||||
const talkItems = computed(() => talkStore.talkItems);
|
const talkItems = computed(() => talkStore.talkItems);
|
||||||
const name = computed(() => userStore.name);
|
const name = computed(() => userStore.name);
|
||||||
const face = computed(() => userStore.face);
|
// 不能命名为 face:会遮蔽 <face> 组件解析,头像 URL 会被当作标签名渲染
|
||||||
|
const userFace = computed(() => userStore.face);
|
||||||
const unreadMessage = computed(() => talkStore.unreadMessage);
|
const unreadMessage = computed(() => talkStore.unreadMessage);
|
||||||
const inputEvent = computed(() => notifyStore.inputEvent);
|
const inputEvent = computed(() => notifyStore.inputEvent);
|
||||||
const id = computed(() => userStore.id);
|
const id = computed(() => userStore.id);
|
||||||
const records = computed(() => dialogueStore.records);
|
const records = computed(() => dialogueStore.records);
|
||||||
const index_name = computed(() => dialogueStore.index_name);
|
const index_name = computed(() => dialogueStore.index_name);
|
||||||
const toUser = computed(() => userStore.toUser);
|
const toUser = computed(() => userStore.toUser);
|
||||||
|
// 商品/订单图片加载失败时的默认图(onerror 属性值)
|
||||||
|
const defaultAvatar = computed(() => appStore.defaultAvatar);
|
||||||
|
|
||||||
const sendTime = formatTime;
|
const sendTime = formatTime;
|
||||||
|
|
||||||
@@ -1147,7 +1148,7 @@ function talkPanelScrollBottom() {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
order: 1;
|
order: 1;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding-top: 25px;
|
padding-top: 8px;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
@@ -1168,7 +1169,7 @@ function talkPanelScrollBottom() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
order: 2;
|
order: 2;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
padding-top: 22px;
|
padding-top: 5px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1206,13 +1207,6 @@ function talkPanelScrollBottom() {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.nickname {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #a7a0a0;
|
|
||||||
margin: -4px 0 4px -8px;
|
|
||||||
transform: scale(0.9);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -1250,8 +1244,8 @@ function talkPanelScrollBottom() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bg-left-color: #f5f5f5;
|
@bg-left-color: #f2f2f2;
|
||||||
@bg-right-color: #ffffff;
|
@bg-right-color: #409eff;
|
||||||
|
|
||||||
.text-message {
|
.text-message {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="box" :style="{height:toUser.storeFlag ? '858px' : '815px'}" @scroll="scrollBottom">
|
<div class="box" @scroll="scrollBottom">
|
||||||
<div class="tab">
|
<div class="tab">
|
||||||
<el-tabs v-model="activeName" @tab-click="handleClick" :stretch=true>
|
<el-tabs v-model="activeName" @tab-click="handleClick" type="card" :stretch=true>
|
||||||
<el-tab-pane label="最近浏览" name="goods">
|
<el-tab-pane label="最近浏览" name="goods">
|
||||||
<dl v-if='list.length' class='base-list'>
|
<dl v-if='list.length' class='base-list'>
|
||||||
<dd :key="index" @click="linkToGoods(item.goodsId, item.id)" v-for="(item,index) in list">
|
<dd :key="index" @click="linkToGoods(item.goodsId, item.id)" v-for="(item,index) in list">
|
||||||
<div class="base">
|
<div class="base">
|
||||||
<div>
|
<div>
|
||||||
<img style="width: 60px; height: 60px;margin-left:20px;box-sizing: border-box;" :src="item.thumbnail"
|
<img style="width: 60px; height: 60px;margin-left:20px;box-sizing: border-box;" :src="item.thumbnail"
|
||||||
class="image" />
|
:onerror="defaultAvatar" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<div class="recent_views">
|
<div class="recent_views">
|
||||||
<a class="goods_name">{{ item.goodsName }}</a>
|
<a class="goods_name">{{ item.goodsName }}</a>
|
||||||
@@ -35,24 +35,8 @@
|
|||||||
<dl class='base-order-list' v-if='orderList.length'>
|
<dl class='base-order-list' v-if='orderList.length'>
|
||||||
<dd v-for="(item, index) in orderList" :key="index">
|
<dd v-for="(item, index) in orderList" :key="index">
|
||||||
<div class="order-list">
|
<div class="order-list">
|
||||||
<div class="order-top order-padding">
|
<div class="order-head">
|
||||||
<span class="order-sn" @click="linkToOrders(item.sn)">订单号:{{ item.sn }}</span>
|
<span class="order-sn" :title="item.sn" @click="linkToOrders(item.sn)">订单号:{{ item.sn }}</span>
|
||||||
</div>
|
|
||||||
<div class="order-section order-padding">
|
|
||||||
<div class="order-items" v-for="(order,orderIndex) in item.orderItems" :key="orderIndex">
|
|
||||||
<img :src="order.image" alt="">
|
|
||||||
<span class="order-goods-name" @click="linkToOrders(item.sn)"> {{ order.name }}</span>
|
|
||||||
<span class="price">{{ $filters.unitPrice(order.goodsPrice, "¥") }}</span>
|
|
||||||
</div>
|
|
||||||
<!-- <img :src="item.groupImages" alt=""> -->
|
|
||||||
|
|
||||||
<div class="order-btn ">
|
|
||||||
<el-button class="store-button" v-if="item.btnHide == 1 && toUser.storeFlag"
|
|
||||||
size="small" @click="submitSendOrderMessage(item, index)" plain>发送</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="order-footer order-padding">
|
|
||||||
<span></span>
|
|
||||||
<el-tag size="small" :type="col[item.orderStatus]">{{
|
<el-tag size="small" :type="col[item.orderStatus]">{{
|
||||||
item.orderStatus == 'STAY_PICKED_UP' ? '待自提'
|
item.orderStatus == 'STAY_PICKED_UP' ? '待自提'
|
||||||
: item.orderStatus == 'CANCELLED' ? '已取消' : item.orderStatus == 'UNPAID' ? '未付款' : item.orderStatus
|
: item.orderStatus == 'CANCELLED' ? '已取消' : item.orderStatus == 'UNPAID' ? '未付款' : item.orderStatus
|
||||||
@@ -62,7 +46,16 @@
|
|||||||
''
|
''
|
||||||
}}</el-tag>
|
}}</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="order-items" v-for="(order,orderIndex) in item.orderItems" :key="orderIndex">
|
||||||
|
<img :src="order.image" :onerror="defaultAvatar" alt="">
|
||||||
|
<div class="order-item-info">
|
||||||
|
<span class="order-goods-name" :title="order.name" @click="linkToOrders(item.sn)">{{ order.name }}</span>
|
||||||
|
<span class="price">{{ $filters.unitPrice(order.goodsPrice, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="order-foot" v-if="item.btnHide == 1 && toUser.storeFlag">
|
||||||
|
<el-button class="store-button" size="small" @click="submitSendOrderMessage(item, index)" plain>发送</el-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</dd>
|
</dd>
|
||||||
@@ -80,9 +73,11 @@
|
|||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useTalkStore } from "@/stores/talk";
|
import { useTalkStore } from "@/stores/talk";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
const talkStore = useTalkStore();
|
const talkStore = useTalkStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const emit = defineEmits(["loadMore", "sendMessage"]);
|
const emit = defineEmits(["loadMore", "sendMessage"]);
|
||||||
|
|
||||||
@@ -118,6 +113,8 @@ const col = ref({
|
|||||||
const talkItems = computed(() => talkStore.talkItems);
|
const talkItems = computed(() => talkStore.talkItems);
|
||||||
const id = computed(() => userStore.id);
|
const id = computed(() => userStore.id);
|
||||||
const toUser = computed(() => userStore.toUser);
|
const toUser = computed(() => userStore.toUser);
|
||||||
|
// 图片加载失败时的默认图(onerror 属性值)
|
||||||
|
const defaultAvatar = computed(() => appStore.defaultAvatar);
|
||||||
|
|
||||||
//跳转订单列表
|
//跳转订单列表
|
||||||
|
|
||||||
@@ -204,7 +201,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.box {
|
.box {
|
||||||
max-width: 362px;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -237,57 +236,51 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.order-list {
|
.order-list {
|
||||||
padding: 9px;
|
border: 1px solid #eee;
|
||||||
transition: 0.35s;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
padding: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #5a606b;
|
transition: border-color .2s;
|
||||||
box-sizing: border-box;
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #b3d3f9;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-sn {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.box::-webkit-scrollbar {
|
.box::-webkit-scrollbar {
|
||||||
width: 0px !important;
|
width: 0px !important;
|
||||||
}
|
}
|
||||||
.order-section {
|
|
||||||
margin: 10px 0;
|
|
||||||
font-size: 12px;
|
|
||||||
img {
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-footer {
|
.order-foot {
|
||||||
font-size: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-goods-name {
|
|
||||||
width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
position: absolute;
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orderSn {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-btn {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
position: relative;
|
margin-top: 8px;
|
||||||
bottom: 55px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods_name {
|
.goods_name {
|
||||||
@@ -300,7 +293,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
:deep(.el-tabs__header ) {
|
:deep(.el-tabs__header ) {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 362px;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -329,10 +322,6 @@ onMounted(() => {
|
|||||||
// width: 350px;
|
// width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-padding {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base {
|
.base {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -354,7 +343,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.base:hover,.order-list:hover{
|
.base:hover{
|
||||||
background: #ededed;
|
background: #ededed;
|
||||||
padding-left: 14px;
|
padding-left: 14px;
|
||||||
>.review{
|
>.review{
|
||||||
@@ -371,9 +360,6 @@ onMounted(() => {
|
|||||||
.separate {
|
.separate {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
.order-sn{
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
// .el-tabs--card {
|
// .el-tabs--card {
|
||||||
// height: calc(100vh - 110px);
|
// height: calc(100vh - 110px);
|
||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
@@ -383,17 +369,54 @@ onMounted(() => {
|
|||||||
// overflow-y: auto;
|
// overflow-y: auto;
|
||||||
// }
|
// }
|
||||||
.base-list{
|
.base-list{
|
||||||
padding-bottom: 120px;
|
padding: 10px 10px 12px;
|
||||||
}
|
}
|
||||||
.base-order-list{
|
.base-order-list{
|
||||||
padding-bottom: 100px;
|
padding: 10px 10px 12px;
|
||||||
}
|
|
||||||
.price{
|
|
||||||
color: red;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.order-items{
|
.order-items{
|
||||||
margin-bottom: 10px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-item-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-goods-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #333;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #f23030;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
当前浏览
|
当前浏览
|
||||||
<div class="base">
|
<div class="base">
|
||||||
<div>
|
<div>
|
||||||
<img :src="goodsDetail.thumbnail" class="image" />
|
<img :src="goodsDetail.thumbnail" :onerror="defaultAvatar" class="image" />
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-left: 13px">
|
<div style="margin-left: 13px">
|
||||||
<a @click="linkToGoods(goodsDetail.goodsId, goodsDetail.id)"> {{ goodsDetail.goodsName }} </a>
|
<a @click="linkToGoods(goodsDetail.goodsId, goodsDetail.id)"> {{ goodsDetail.goodsName }} </a>
|
||||||
@@ -23,10 +23,12 @@
|
|||||||
import { computed, onMounted, nextTick, ref } from "vue";
|
import { computed, onMounted, nextTick, ref } from "vue";
|
||||||
import SocketInstance from "@/im-server/socket-instance";
|
import SocketInstance from "@/im-server/socket-instance";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useDialogueStore } from "@/stores/dialogue";
|
import { useDialogueStore } from "@/stores/dialogue";
|
||||||
import { useTalkStore } from "@/stores/talk";
|
import { useTalkStore } from "@/stores/talk";
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const appStore = useAppStore();
|
||||||
const dialogueStore = useDialogueStore();
|
const dialogueStore = useDialogueStore();
|
||||||
const talkStore = useTalkStore();
|
const talkStore = useTalkStore();
|
||||||
|
|
||||||
@@ -44,6 +46,8 @@ const talkItems = computed(() => talkStore.talkItems);
|
|||||||
const id = computed(() => userStore.id);
|
const id = computed(() => userStore.id);
|
||||||
const index_name = computed(() => dialogueStore.index_name);
|
const index_name = computed(() => dialogueStore.index_name);
|
||||||
const toUser = computed(() => userStore.toUser);
|
const toUser = computed(() => userStore.toUser);
|
||||||
|
// 图片加载失败时的默认图(onerror 属性值)
|
||||||
|
const defaultAvatar = computed(() => appStore.defaultAvatar);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log(props.goodsDetail)
|
console.log(props.goodsDetail)
|
||||||
|
|||||||
@@ -1,100 +1,157 @@
|
|||||||
<template>
|
<template>
|
||||||
<div style="width: 362px;">
|
<div class="store-card">
|
||||||
<div class="base">
|
<div class="base">
|
||||||
<div>
|
<img :src="storeInfo.storeLogo" :onerror="onError" class="image" alt="" />
|
||||||
<img :src="storeInfo.storeLogo" class="image" />
|
|
||||||
</div>
|
|
||||||
<div class="store-detail">
|
<div class="store-detail">
|
||||||
<div>
|
<div class="store-name">
|
||||||
<div class="div-zoom">
|
<span class="name-text" :title="storeInfo.storeName">{{ storeInfo.storeName }}</span>
|
||||||
<span class="title_left_name"> 店铺名称:</span> <span class="title_right_name">{{ storeInfo.storeName }}</span>
|
<el-tag type="danger" v-if="storeInfo.selfOperated" size="small">自营</el-tag>
|
||||||
<el-tag style="margin-left: 10px;" type="danger" v-if="storeInfo.selfOperated" size="small">自营</el-tag>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="store-actions">
|
||||||
<span class="title_left_name"> 联系方式:</span> <span class="title_right_name">{{ storeInfo.memberName }}</span>
|
<el-button @click="linkToStore(storeInfo.id)" size="small" plain>进入店铺</el-button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-button class="store-button" @click="linkToStore(storeInfo.id)" size="small"
|
|
||||||
plain>进入店铺</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-row" v-for="row in infoRows" :key="row.label">
|
||||||
|
<span class="label">{{ row.label }}</span>
|
||||||
|
<span class="value" :title="row.value">{{ row.value || '—' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="separate">店铺评分: <el-rate v-model="storeInfo.serviceScore" disabled show-score text-color="#ff9900"
|
<div class="rate-row" v-for="row in rates" :key="row.label">
|
||||||
score-template="{value}">
|
<span class="rate-label">{{ row.label }}</span>
|
||||||
</el-rate></div>
|
<el-rate :model-value="row.score" disabled show-score text-color="#ff9900" score-template="{value}" />
|
||||||
<div class="separate">服务评分: <el-rate v-model="storeInfo.descriptionScore" disabled show-score text-color="#ff9900"
|
</div>
|
||||||
score-template="{value}">
|
|
||||||
</el-rate></div>
|
|
||||||
<div class="separate">物流评分: <el-rate v-model="storeInfo.deliveryScore" disabled show-score text-color="#ff9900"
|
|
||||||
score-template="{value}">
|
|
||||||
</el-rate></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue";
|
import { computed } from "vue";
|
||||||
|
import { defaultAvatar } from "@/assets/images";
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
storeInfo: {
|
storeInfo: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const value = ref(null);
|
const onError = "this.src='" + defaultAvatar + "'";
|
||||||
const colors = ref(['#99A9BF', '#F7BA2A', '#FF9900']);
|
|
||||||
|
const infoRows = computed(() => [
|
||||||
|
{ label: "店铺地址", value: props.storeInfo.storeAddressPath },
|
||||||
|
{ label: "详细地址", value: props.storeInfo.storeAddressDetail },
|
||||||
|
{ label: "店铺简介", value: props.storeInfo.storeDesc },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 与买家端字段口径一致:店铺评分=描述相符 descriptionScore,服务评分=serviceScore
|
||||||
|
const rates = computed(() => [
|
||||||
|
{ label: "店铺评分", score: Number(props.storeInfo.descriptionScore) || 0 },
|
||||||
|
{ label: "服务评分", score: Number(props.storeInfo.serviceScore) || 0 },
|
||||||
|
{ label: "物流评分", score: Number(props.storeInfo.deliveryScore) || 0 },
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.wrapper{
|
.store-card {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-top: 1px solid #f5eeee;
|
display: flex;
|
||||||
}
|
|
||||||
.title_left_name {
|
.image {
|
||||||
font-size: 12px;
|
height: 72px;
|
||||||
color: #999;
|
width: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title_right_name {
|
.store-detail {
|
||||||
font-size: 12px;
|
flex: 1;
|
||||||
color: #343036;
|
min-width: 0;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.store-button {
|
|
||||||
background-color: white;
|
|
||||||
|
|
||||||
}
|
|
||||||
.store-detail{
|
|
||||||
margin-left: 13px;
|
margin-left: 13px;
|
||||||
display:flex;
|
display: flex;
|
||||||
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
.base {
|
|
||||||
padding: 16px;
|
|
||||||
margin-top: 5px;
|
|
||||||
|
|
||||||
|
.store-name {
|
||||||
display: flex;
|
display: flex;
|
||||||
.image {
|
align-items: center;
|
||||||
height: 100px;
|
gap: 8px;
|
||||||
|
|
||||||
width: 100px
|
.name-text {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #343036;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
padding: 0 16px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #343036;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.separate {
|
.wrapper {
|
||||||
margin-top: 8px;
|
padding: 12px 16px 16px;
|
||||||
// display: inline;
|
border-top: 1px solid #f5f5f5;
|
||||||
display: flex;
|
}
|
||||||
font-size: 12px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
color: #a1a6af;
|
.rate-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-label {
|
||||||
|
width: 60px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #a1a6af;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,16 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<img :src="'https://avatars.dicebear.com/api/initials/'+text+'.svg?fontSize=38'" alt=""/>
|
<i class="face-initial" :style="{ backgroundColor: color }">{{ initial }}</i>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { computed } from "vue";
|
||||||
text:{
|
import { avatarColor } from "@/utils/functions";
|
||||||
type:null,
|
|
||||||
default:''
|
const props = defineProps({
|
||||||
}
|
text: {
|
||||||
})
|
type: null,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 无头像时用「底色 + 首字」本地生成,替代原来的 dicebear 外链
|
||||||
|
const initial = computed(() => String(props.text || "").slice(0, 1) || "?");
|
||||||
|
const color = computed(() => avatarColor(String(props.text || "")));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="less" scoped>
|
||||||
|
.face-initial {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: #fff;
|
||||||
|
font-style: normal;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user