mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp.git
synced 2026-11-07 01:18:53 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75b20f494d | ||
|
|
cc71d20aa6 | ||
|
|
06af86af3f | ||
|
|
8188a8af7b | ||
|
|
0637f1da1f | ||
|
|
4adcf0a13a | ||
|
|
d72f02f7cf | ||
|
|
7b2b045fd4 | ||
|
|
e1a48d6d11 |
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-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -27,7 +27,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 `uni-app + Vue 3 + uview-plus 3.x`, leaving legacy Vue 2 templates behind.
|
- 🚀 **Modern Vue 3 Stack**: Built on `uni-app + Vue 3 + uview-plus 3.x`, leaving legacy Vue 2 templates behind.
|
||||||
- 🧩 **Production-Ready Completeness**: Not a static UI template—fully integrated with B2B2C multi-vendor workflows, visual storefront decoration, IM customer service, flash sales, group buying, affiliate distribution, and live streaming.
|
- 🧩 **Production-Ready Completeness**: Not a static UI template—fully integrated with B2B2C multi-vendor workflows, visual storefront decoration, IM customer service, flash sales, group buying, affiliate distribution, and live streaming.
|
||||||
|
|
||||||
@@ -137,4 +137,4 @@ lilishop-uniapp
|
|||||||
|
|
||||||
## 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.**
|
||||||
|
|||||||
57
README.md
57
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-brightgreen.svg" alt="Vue 3" /></a>
|
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3-brightgreen.svg" alt="Vue 3" /></a>
|
||||||
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
<a href="https://uniapp.dcloud.net.cn/"><img src="https://img.shields.io/badge/uni--app-Vue3-blue.svg" alt="uni-app" /></a>
|
||||||
</p>
|
</p>
|
||||||
@@ -27,7 +27,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 全新栈**:基于 `uni-app + Vue 3 + uview-plus 3.x`,告别老旧 Vue 2 模板。
|
- 🚀 **Vue 3 全新栈**:基于 `uni-app + Vue 3 + uview-plus 3.x`,告别老旧 Vue 2 模板。
|
||||||
- 🧩 **生产级完整度**:非静态切图模板,完整覆盖 B2B2C 多商户、可视化装修、IM 客服、秒杀拼团、分销直播全闭环。
|
- 🧩 **生产级完整度**:非静态切图模板,完整覆盖 B2B2C 多商户、可视化装修、IM 客服、秒杀拼团、分销直播全闭环。
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ lilishop-uniapp
|
|||||||
|
|
||||||
## 6. 开源与授权
|
## 6. 开源与授权
|
||||||
|
|
||||||
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
本项目遵循 [Apache-2.0](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -160,20 +160,47 @@ lilishop-uniapp
|
|||||||
|
|
||||||
## 8. 升级计划
|
## 8. 升级计划
|
||||||
|
|
||||||
|
> 更新日期:2026-11-05,依据各端提交记录及当前代码实现同步。
|
||||||
|
|
||||||
### ✅ 已完成特性
|
### ✅ 已完成特性
|
||||||
| 功能模块 | 功能模块 |
|
|
||||||
| :--- | :--- |
|
按可核实的实现或完善时间倒序记录;日期表示代码完成或功能更新时间,不代表生产上线时间。
|
||||||
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
|
|
||||||
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
|
| 时间 | 功能模块 | 完成内容 |
|
||||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
| :--- | :--- | :--- |
|
||||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
| 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 开放平台与开发者中心
|
||||||
|
|||||||
@@ -477,3 +477,11 @@ export function withdrawalSettingVO() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getCurrentMemberBenefits() { return http.request({url:'/member/benefit/current',method:Method.GET,needToken:true}) }
|
||||||
|
export function getMemberGradeBenefits() { return http.request({url:'/member/memberGrade/list',method:Method.GET,needToken:true}) }
|
||||||
|
|
||||||
|
/** 仅返回后台已开启的经验值规则及当前会员累计获得情况。 */
|
||||||
|
export function getExperienceTasks() { return http.request({ url: '/member/memberGrade/rules', method: Method.GET, needToken: true }) }
|
||||||
|
/** 当前会员等级、经验值及分页流水;会员身份由服务端登录态确定。 */
|
||||||
|
export function getExperienceRecords(params) { return http.request({ url: '/member/memberGrade/current', method: Method.GET, params, needToken: true }) }
|
||||||
|
|||||||
@@ -110,3 +110,15 @@ export function getLogisticsMessages(params) {
|
|||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取微信公众号通知配置及绑定状态
|
||||||
|
*/
|
||||||
|
export function getWechatOfficialAccountSetting() {
|
||||||
|
return http.request({
|
||||||
|
url: '/message/wechatOfficialAccount',
|
||||||
|
method: Method.GET,
|
||||||
|
needToken: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
7
api/paid-member.js
Normal file
7
api/paid-member.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { http, Method } from '@/utils/request.js'
|
||||||
|
export const getPaidMember = () => http.request({ url: '/member/paid/current', method: Method.GET, needToken: true })
|
||||||
|
export const createPaidMemberOrder = data => http.request({ url: '/member/paid/orders', method: Method.POST, data, needToken: true, header: { 'Content-Type': 'application/json' } })
|
||||||
|
export const getPaidMemberOrders = params => http.request({ url: '/member/paid/orders', method: Method.GET, params, needToken: true })
|
||||||
|
export const getPaidMemberOrder = sn => http.request({ url: `/member/paid/orders/${encodeURIComponent(sn)}`, method: Method.GET, needToken: true })
|
||||||
|
export const cancelPaidMemberOrder = sn => http.request({ url: `/member/paid/orders/${encodeURIComponent(sn)}/cancel`, method: Method.POST, needToken: true })
|
||||||
|
export const getPaidMemberQuotes = skuIds => http.request({ url: '/member/paid/quotes', method: Method.GET, params: { skuIds: skuIds.join(',') }, needToken: true })
|
||||||
13
api/pickupCard.js
Normal file
13
api/pickupCard.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 提货卡选品、试算及一次兑换接口。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-05
|
||||||
|
*/
|
||||||
|
import { http, Method } from '@/utils/request.js'
|
||||||
|
|
||||||
|
/** 获取当前会员的卡规则及候选商品。 */
|
||||||
|
export const getPickupCard = id => http.request({ url: `/promotion/pickupCard/${encodeURIComponent(id)}`, method: Method.GET, needToken: true })
|
||||||
|
/** 服务端试算,校验商品与地址且不消耗权益。 */
|
||||||
|
export const previewPickupCard = data => http.request({ url: '/promotion/pickupCard/preview', method: Method.POST, needToken: true, header: { 'content-type': 'application/json' }, data })
|
||||||
|
/** 原子核销创单,网络重试返回原交易。 */
|
||||||
|
export const redeemPickupCard = data => http.request({ url: '/promotion/pickupCard/redeem', method: Method.POST, needToken: true, header: { 'content-type': 'application/json' }, data })
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</view>
|
</view>
|
||||||
|
<PaidMemberPrice :quote="paidQuotes[item.id]" />
|
||||||
<div class="promotion" @click="navigateToDetailPage(item)">
|
<div class="promotion" @click="navigateToDetailPage(item)">
|
||||||
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
<div v-for="(promotionItem,promotionIndex) in getPromotion(item)" :key="promotionIndex">
|
||||||
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
<span class="promotion-tag" v-if="promotionItem.indexOf('COUPON') != -1">劵</span>
|
||||||
@@ -80,6 +81,7 @@
|
|||||||
<u-icon name="shopping-cart" size="16" :color="mainColor"></u-icon>
|
<u-icon name="shopping-cart" size="16" :color="mainColor"></u-icon>
|
||||||
</div>
|
</div>
|
||||||
</view>
|
</view>
|
||||||
|
<PaidMemberPrice v-if="flag('price')" :quote="paidQuotes[item.id]" />
|
||||||
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
<div class="store-seller-name" v-if="storeName" @click="navigateToStoreDetailPage(item)">
|
||||||
<view class="store-name-row">
|
<view class="store-name-row">
|
||||||
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
|
<text class="self-tag" v-if="item.selfOperated && flag('selfTag')">自营</text>
|
||||||
@@ -96,7 +98,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch, ref } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import PaidMemberPrice from '@/components/paid-member-price/index.vue'
|
||||||
|
import { getPaidMemberQuotes } from '@/api/paid-member'
|
||||||
|
import { isLogin } from '@/utils/filters'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { useGoodsListCommon } from './common'
|
import { useGoodsListCommon } from './common'
|
||||||
import { goodsFormatPrice } from '@/utils/filters.js'
|
import { goodsFormatPrice } from '@/utils/filters.js'
|
||||||
@@ -123,6 +129,21 @@ const props = withDefaults(defineProps<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['addCart'])
|
const emit = defineEmits(['addCart'])
|
||||||
|
const paidQuotes = ref<Record<string, any>>({})
|
||||||
|
let paidRequest = 0
|
||||||
|
async function loadPaidQuotes() {
|
||||||
|
const request = ++paidRequest, memberId = String(isLogin()?.id || '')
|
||||||
|
paidQuotes.value = {}
|
||||||
|
if (!memberId) return
|
||||||
|
const ids = [...new Set((props.res || []).map(item => String(item.id || '')).filter(Boolean))]
|
||||||
|
try {
|
||||||
|
const results = await Promise.all(Array.from({length:Math.ceil(ids.length/100)},(_,i)=>getPaidMemberQuotes(ids.slice(i*100,i*100+100))))
|
||||||
|
if (request !== paidRequest || String(isLogin()?.id || '') !== memberId) return
|
||||||
|
paidQuotes.value = Object.assign({}, ...results.filter(res=>res.data?.success).map(res=>res.data.result || {}))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
watch(() => [(props.res || []).map(item=>item.id).join(','), store.state.userInfo?.id, store.state.hasLogin], loadPaidQuotes, {immediate:true})
|
||||||
|
onShow(loadPaidQuotes)
|
||||||
|
|
||||||
// 存量字段(促销/销量/自营/价格):showFields 为 null 时维持现状渲染
|
// 存量字段(促销/销量/自营/价格):showFields 为 null 时维持现状渲染
|
||||||
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false
|
const flag = (key: string) => props.showFields == null || props.showFields[key] !== false
|
||||||
|
|||||||
6
components/paid-member-price/index.vue
Normal file
6
components/paid-member-price/index.vue
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<template><view v-if="quote" class="paid-price" @click.stop="open"><image v-if="quote.label" :src="quote.label" mode="aspectFit" /><text>会员价 ¥{{ Number(quote.price).toFixed(2) }}</text><text v-if="!quote.eligible" class="open">开通享 ›</text></view></template>
|
||||||
|
<script setup>
|
||||||
|
defineProps({quote:{type:Object,default:null}})
|
||||||
|
function open(){uni.navigateTo({url:'/pages/mine/paidMember'})}
|
||||||
|
</script>
|
||||||
|
<style scoped>.paid-price{display:flex;align-items:center;flex-wrap:wrap;gap:6rpx;margin-top:8rpx;color:#a77c3e;font-size:22rpx;line-height:1.6}.paid-price image{width:28rpx;height:28rpx}.open{font-size:20rpx;opacity:.8}</style>
|
||||||
@@ -101,12 +101,18 @@ const props = withDefaults(defineProps<{
|
|||||||
left?: string
|
left?: string
|
||||||
top?: string
|
top?: string
|
||||||
business?: string
|
business?: string
|
||||||
|
apiType?: string
|
||||||
}>(), {
|
}>(), {
|
||||||
height: '80rpx',
|
height: '80rpx',
|
||||||
width: '350rpx',
|
width: '350rpx',
|
||||||
left: '180rpx',
|
left: '180rpx',
|
||||||
top: '30rpx',
|
top: '30rpx',
|
||||||
business: 'LOGIN'
|
business: 'LOGIN',
|
||||||
|
apiType: 'buyer'
|
||||||
|
})
|
||||||
|
|
||||||
|
const sliderBaseUrl = computed(() => {
|
||||||
|
return props.apiType === 'store' ? api.store : api.buyer
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
@@ -182,7 +188,7 @@ function getCode() {
|
|||||||
storage.setUuid(uuid.v1())
|
storage.setUuid(uuid.v1())
|
||||||
}
|
}
|
||||||
uni.request({
|
uni.request({
|
||||||
url: api.buyer + '/common/slider/' + props.business,
|
url: sliderBaseUrl.value + '/common/slider/' + props.business,
|
||||||
header: {
|
header: {
|
||||||
uuid: storage.getUuid()
|
uuid: storage.getUuid()
|
||||||
},
|
},
|
||||||
@@ -217,7 +223,7 @@ function end() {
|
|||||||
uni.request({
|
uni.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url:
|
url:
|
||||||
api.buyer +
|
sliderBaseUrl.value +
|
||||||
'/common/slider/' +
|
'/common/slider/' +
|
||||||
props.business +
|
props.business +
|
||||||
'?xPos=' +
|
'?xPos=' +
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* buyer : 买家 API(baseURL 为 buyer + /buyer)
|
* buyer : 买家 API(baseURL 为 buyer + /buyer)
|
||||||
|
* store : 商家 API(baseURL 为 store + /store)
|
||||||
*/
|
*/
|
||||||
// 开发环境
|
// 开发环境
|
||||||
const dev = {
|
const dev = {
|
||||||
buyer: "https://buyer-api.pickmall.cn",
|
buyer: "https://buyer-api.pickmall.cn",
|
||||||
|
store: "https://admin-api.pickmall.cn",
|
||||||
im: "https://im-api.pickmall.cn",
|
im: "https://im-api.pickmall.cn",
|
||||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||||
};
|
};
|
||||||
// 生产环境
|
// 生产环境
|
||||||
const prod = {
|
const prod = {
|
||||||
buyer: "https://buyer-api.pickmall.cn",
|
buyer: "https://buyer-api.pickmall.cn",
|
||||||
|
store: "https://admin-api.pickmall.cn",
|
||||||
im: "https://im-api.pickmall.cn",
|
im: "https://im-api.pickmall.cn",
|
||||||
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
mqtt: "wss://lilishop-mqtt-pull.dllll.xyz/mqtt",
|
||||||
};
|
};
|
||||||
@@ -26,10 +29,13 @@ api = prod;
|
|||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
const buyerHost = api.buyer;
|
const buyerHost = api.buyer;
|
||||||
|
const storeHost = api.store;
|
||||||
api.buyer = buyerHost + "/buyer";
|
api.buyer = buyerHost + "/buyer";
|
||||||
|
api.store = storeHost + "/store";
|
||||||
api.im = api.im + "/im";
|
api.im = api.im + "/im";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...api,
|
...api,
|
||||||
buyerHost,
|
buyerHost,
|
||||||
|
storeHost,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,18 +2,105 @@
|
|||||||
* 微信小程序支付
|
* 微信小程序支付
|
||||||
* 此处针对于微信小程序开发的支付插件
|
* 此处针对于微信小程序开发的支付插件
|
||||||
* 第一次支付成功后会跳出订阅的消息 如果用户拒绝或同意都会跳转到支付成功页面
|
* 第一次支付成功后会跳出订阅的消息 如果用户拒绝或同意都会跳转到支付成功页面
|
||||||
* 如果点击订阅 会将状态写进缓存 之后不再提醒。
|
|
||||||
*
|
*
|
||||||
* @param {sn,price}
|
* @param {sn,price,orderPromotionType}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getWeChatMpMessage } from "@/api/message.js";
|
import { getWeChatMpMessage } from "@/api/message.js";
|
||||||
import { initiatePay } from "@/api/trade";
|
import { initiatePay } from "@/api/trade";
|
||||||
|
|
||||||
|
const SCENE_NAME_MAP = {
|
||||||
|
ORDER_DELIVER: ["订单发货提醒", "订单发货通知"],
|
||||||
|
ORDER_COMPLETE: ["订单已完成提醒", "订单完成通知"],
|
||||||
|
PINTUAN_SUCCESS: ["拼团成功通知"],
|
||||||
|
AFTER_SALE_AUDIT: ["售后申请审核通知", "售后审核通知"],
|
||||||
|
POINT_CHANGE: ["积分变更提醒", "积分变更通知"],
|
||||||
|
WALLET_CHANGE: ["账户余额变动通知", "余额账户变更通知"],
|
||||||
|
WALLET_WITHDRAWAL_AUDIT_ERROR: ["提现结果通知", "提现申请审核通知"],
|
||||||
|
};
|
||||||
|
|
||||||
|
let cachedMpMessageList = null;
|
||||||
|
|
||||||
|
export function prefetchMpMessageTemplates() {
|
||||||
|
return getWeChatMpMessage()
|
||||||
|
.then((res) => {
|
||||||
|
if (res && res.data && Array.isArray(res.data.result)) {
|
||||||
|
cachedMpMessageList = res.data.result;
|
||||||
|
}
|
||||||
|
return cachedMpMessageList || [];
|
||||||
|
})
|
||||||
|
.catch(() => cachedMpMessageList || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTmplIdsByScenes(list, targetScenes) {
|
||||||
|
const tmplIds = [];
|
||||||
|
(targetScenes || []).forEach((scene) => {
|
||||||
|
const aliasNames = SCENE_NAME_MAP[scene] || [];
|
||||||
|
const matched = (list || []).find(
|
||||||
|
(item) =>
|
||||||
|
item &&
|
||||||
|
item.enable !== false &&
|
||||||
|
item.code &&
|
||||||
|
(item.sceneCode === scene || aliasNames.includes(item.name))
|
||||||
|
);
|
||||||
|
if (matched && matched.code && !tmplIds.includes(matched.code)) {
|
||||||
|
tmplIds.push(matched.code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tmplIds.slice(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeMpMessageByScenes(sceneCodes, onComplete) {
|
||||||
|
const done = typeof onComplete === "function" ? onComplete : () => {};
|
||||||
|
const targetScenes = Array.isArray(sceneCodes) ? sceneCodes : [];
|
||||||
|
if (!targetScenes.length) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Array.isArray(cachedMpMessageList) && cachedMpMessageList.length) {
|
||||||
|
const finalTmplIds = extractTmplIdsByScenes(cachedMpMessageList, targetScenes);
|
||||||
|
if (!finalTmplIds.length) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("[微信订阅消息] 请求订阅 tmplIds:", finalTmplIds);
|
||||||
|
uni.requestSubscribeMessage({
|
||||||
|
tmplIds: finalTmplIds,
|
||||||
|
complete: (res) => {
|
||||||
|
console.log("[微信订阅消息] 授权返回结果:", res);
|
||||||
|
done();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
.then((list) => {
|
||||||
|
const finalTmplIds = extractTmplIdsByScenes(list, targetScenes);
|
||||||
|
if (!finalTmplIds.length) {
|
||||||
|
done();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.requestSubscribeMessage({
|
||||||
|
tmplIds: finalTmplIds,
|
||||||
|
complete: () => done(),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => done());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribePayMessageThen(onComplete, options = {}) {
|
||||||
|
const isPintuan =
|
||||||
|
options &&
|
||||||
|
String(options.orderPromotionType || "").toUpperCase() === "PINTUAN";
|
||||||
|
const scenes = isPintuan
|
||||||
|
? ["ORDER_DELIVER", "ORDER_COMPLETE", "PINTUAN_SUCCESS"]
|
||||||
|
: ["ORDER_DELIVER", "ORDER_COMPLETE"];
|
||||||
|
subscribeMpMessageByScenes(scenes, onComplete);
|
||||||
|
}
|
||||||
|
|
||||||
class LiLiWXPay {
|
class LiLiWXPay {
|
||||||
constructor(...payList) {
|
constructor(...payList) {
|
||||||
this.data = payList[0];
|
this.data = payList[0];
|
||||||
console.log(payList);
|
|
||||||
// 调用支付
|
|
||||||
this.pay = () => {
|
this.pay = () => {
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: "加载中",
|
title: "加载中",
|
||||||
@@ -26,7 +113,6 @@ class LiLiWXPay {
|
|||||||
};
|
};
|
||||||
const paymentMethod = "WECHAT";
|
const paymentMethod = "WECHAT";
|
||||||
const paymentClient = "MP";
|
const paymentClient = "MP";
|
||||||
// 调用支付
|
|
||||||
initiatePay(paymentMethod, paymentClient, submitData).then((res) => {
|
initiatePay(paymentMethod, paymentClient, submitData).then((res) => {
|
||||||
let response = res.data.result;
|
let response = res.data.result;
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
@@ -38,16 +124,25 @@ class LiLiWXPay {
|
|||||||
package: response.package,
|
package: response.package,
|
||||||
signType: response.signType,
|
signType: response.signType,
|
||||||
paySign: response.paySign,
|
paySign: response.paySign,
|
||||||
success: (e) => {
|
success: () => {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
icon: "none",
|
icon: "none",
|
||||||
title: "支付成功!",
|
title: "支付成功!",
|
||||||
});
|
});
|
||||||
sendMessage(payList[0].price);
|
subscribePayMessageThen(
|
||||||
|
() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url:
|
||||||
|
"/pages/cart/payment/success?paymentMethod=WECHAT" +
|
||||||
|
"&payPrice=" +
|
||||||
|
payList[0].price,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ orderPromotionType: this.data.orderPromotionType }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
fail: (e) => {
|
fail: (e) => {
|
||||||
this.exception = e;
|
this.exception = e;
|
||||||
// 支付异常或支付失败之后跳转到订单页面
|
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
content: "支付失败,如果您已支付,请勿反复支付",
|
content: "支付失败,如果您已支付,请勿反复支付",
|
||||||
showCancel: false,
|
showCancel: false,
|
||||||
@@ -64,41 +159,4 @@ class LiLiWXPay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMessage(price) {
|
|
||||||
|
|
||||||
|
|
||||||
//订阅消息
|
|
||||||
getWeChatMpMessage().then((res) => {
|
|
||||||
var message = res.data.result;
|
|
||||||
var templateid = message.map((item) => item.code);
|
|
||||||
uni.requestSubscribeMessage({
|
|
||||||
tmplIds: templateid,
|
|
||||||
success: (res) => {
|
|
||||||
|
|
||||||
},
|
|
||||||
fail: (res) => {
|
|
||||||
console.log('fail', res)
|
|
||||||
uni.showToast({
|
|
||||||
icon: "none",
|
|
||||||
title: "订阅消息失败",
|
|
||||||
})
|
|
||||||
},
|
|
||||||
complete: (res) => {
|
|
||||||
console.log('complete', res)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 已经支付成功
|
|
||||||
*/
|
|
||||||
uni.redirectTo({
|
|
||||||
url:
|
|
||||||
"/pages/cart/payment/success?paymentMethod=WECHAT" +
|
|
||||||
"&payPrice=" +
|
|
||||||
price,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LiLiWXPay;
|
export default LiLiWXPay;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"name" : "lilishop",
|
"name" : "lilishop",
|
||||||
"appid" : "__UNI__ED807EA",
|
"appid" : "__UNI__ED807EA",
|
||||||
"description" : "",
|
"description" : "",
|
||||||
"versionName" : "4.5.3",
|
"versionName" : "5.0.4",
|
||||||
"versionCode" : 4000503,
|
"versionCode" : 503,
|
||||||
"transformPx" : false,
|
"transformPx" : false,
|
||||||
"app-plus" : {
|
"app-plus" : {
|
||||||
"compatible" : {
|
"compatible" : {
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
"plugins" : {},
|
"plugins" : {},
|
||||||
"libVersion" : "3.5.8",
|
"libVersion" : "3.5.8",
|
||||||
"__usePrivacyCheck__" : false,
|
"__usePrivacyCheck__" : false,
|
||||||
"requiredPrivateInfos" : [ "chooseLocation", "getLocation" ]
|
"requiredPrivateInfos" : [ "chooseLocation", "getLocation", "chooseAddress" ]
|
||||||
},
|
},
|
||||||
"h5" : {
|
"h5" : {
|
||||||
"devServer" : {
|
"devServer" : {
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -8,7 +8,7 @@
|
|||||||
"name": "lilishop-uniapp",
|
"name": "lilishop-uniapp",
|
||||||
"version": "4.5.3",
|
"version": "4.5.3",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "ISC",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tencentcloud/chat": "^3.5.5",
|
"@tencentcloud/chat": "^3.5.5",
|
||||||
"hls.js": "^1.6.16",
|
"hls.js": "^1.6.16",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp"
|
"url": "https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tencentcloud/chat": "^3.5.5",
|
"@tencentcloud/chat": "^3.5.5",
|
||||||
"hls.js": "^1.6.16",
|
"hls.js": "^1.6.16",
|
||||||
|
|||||||
26
pages-sub/merchant/api/evaluation.js
Normal file
26
pages-sub/merchant/api/evaluation.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { storeHttp } from "../utils/storeRequest.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页获取客户评价列表
|
||||||
|
* @param {Object} params { grade, goodsName, replyStatus, pageNumber, pageSize }
|
||||||
|
*/
|
||||||
|
export function getStoreEvaluationList(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/member/evaluation",
|
||||||
|
method: "GET",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复客户评价
|
||||||
|
* @param {String} id 评价ID
|
||||||
|
* @param {String} reply 回复文字
|
||||||
|
* @param {String} replyImage 可选回复图片
|
||||||
|
*/
|
||||||
|
export function replyStoreEvaluation(id, reply, replyImage = "") {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/member/evaluation/reply/${id}?reply=${encodeURIComponent(reply)}&replyImage=${encodeURIComponent(replyImage)}`,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
70
pages-sub/merchant/api/goods.js
Normal file
70
pages-sub/merchant/api/goods.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { storeHttp } from "../utils/storeRequest.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商家商品列表
|
||||||
|
* @param {Object} params { goodsName, marketEnable, authFlag, pageNumber, pageSize }
|
||||||
|
*/
|
||||||
|
export function getStoreGoodsList(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/goods/goods/list",
|
||||||
|
method: "GET",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商家商品详情
|
||||||
|
* @param {String} id 商品ID
|
||||||
|
*/
|
||||||
|
export function getStoreGoodsDetail(id) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/goods/goods/get/${id}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商品关联的 SKU 列表
|
||||||
|
* @param {String} goodsId 商品ID
|
||||||
|
*/
|
||||||
|
export function getStoreGoodsSkuList(goodsId) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/goods/goods/sku/${goodsId}/list`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上架商品
|
||||||
|
* @param {String|Array} goodsId
|
||||||
|
*/
|
||||||
|
export function upGoods(goodsId) {
|
||||||
|
const ids = Array.isArray(goodsId) ? goodsId.join(",") : goodsId;
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/goods/goods/up?goodsId=${ids}`,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下架商品
|
||||||
|
* @param {String|Array} goodsId
|
||||||
|
*/
|
||||||
|
export function downGoods(goodsId) {
|
||||||
|
const ids = Array.isArray(goodsId) ? goodsId.join(",") : goodsId;
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/goods/goods/under?goodsId=${ids}`,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取商品统计数量(出售中、已下架等)
|
||||||
|
*/
|
||||||
|
export function getStoreGoodsNumber(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/goods/goods/goodsNumber",
|
||||||
|
method: "GET",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
104
pages-sub/merchant/api/order.js
Normal file
104
pages-sub/merchant/api/order.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { storeHttp } from "../utils/storeRequest.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家订单列表
|
||||||
|
* @param {Object} params { orderStatus, orderType, keywords, pageNumber, pageSize }
|
||||||
|
*/
|
||||||
|
export function getStoreOrderList(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/order/order",
|
||||||
|
method: "GET",
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 商家订单详情
|
||||||
|
* @param {String} sn
|
||||||
|
*/
|
||||||
|
export function getStoreOrderDetail(sn) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/order/order/${sn}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自提 / 虚拟商品单码核销
|
||||||
|
* @param {String} sn
|
||||||
|
* @param {String} verificationCode
|
||||||
|
*/
|
||||||
|
export function storeOrderTake(sn, verificationCode) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/order/order/take/${sn}/${verificationCode}`,
|
||||||
|
method: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用核销预检(按核销码查询)
|
||||||
|
* @param {String} code
|
||||||
|
*/
|
||||||
|
export function previewStoreVerification(code) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/order/order/verification/preview",
|
||||||
|
method: "GET",
|
||||||
|
params: { code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用确认核销
|
||||||
|
* @param {String} code
|
||||||
|
*/
|
||||||
|
export function confirmStoreVerification(code) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/order/order/verification/confirm",
|
||||||
|
method: "POST",
|
||||||
|
data: { code },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约服务 — 根据核销码查询预约订单详情
|
||||||
|
* @param {String} code
|
||||||
|
*/
|
||||||
|
export function getAppointmentOrderByCode(code) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/appointment/verification/order/${code}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约服务 — 执行核销
|
||||||
|
* @param {Object} params { orderSn, code, serviceStaffId }
|
||||||
|
*/
|
||||||
|
export function verifyAppointmentCode(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/appointment/verification/verify",
|
||||||
|
method: "POST",
|
||||||
|
data: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约服务 — 获取已启用的服务人员列表(供核销时指派/确认)
|
||||||
|
*/
|
||||||
|
export function getEnabledServiceStaff() {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/appointment/service-staff/enabled",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预约服务 — 根据订单号获取预约单核销状态
|
||||||
|
* @param {String} orderSn
|
||||||
|
*/
|
||||||
|
export function getAppointmentVerificationByOrderSn(orderSn) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/appointment/verification/order-by-sn/${orderSn}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
94
pages-sub/merchant/api/passport.js
Normal file
94
pages-sub/merchant/api/passport.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { storeHttp } from "../utils/storeRequest.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账号密码登录(支持店主与店员账号)
|
||||||
|
* @param {Object} params { username, password }
|
||||||
|
*/
|
||||||
|
export function userLogin(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/passport/login/userLogin",
|
||||||
|
method: "POST",
|
||||||
|
params,
|
||||||
|
header: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信验证码登录(支持店主与店员手机号)
|
||||||
|
* @param {Object} params { mobile, code }
|
||||||
|
*/
|
||||||
|
export function smsLogin(params) {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/passport/login/smsLogin",
|
||||||
|
method: "POST",
|
||||||
|
params,
|
||||||
|
header: {
|
||||||
|
"content-type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送登录短信
|
||||||
|
* @param {String} mobile
|
||||||
|
* @param {String} verificationEnums (LOGIN)
|
||||||
|
*/
|
||||||
|
export function sendStoreSms(mobile, verificationEnums = "LOGIN") {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/common/sms/${verificationEnums}/${mobile}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取滑块验证码
|
||||||
|
* @param {String} verificationEnums (LOGIN)
|
||||||
|
*/
|
||||||
|
export function getStoreSlider(verificationEnums = "LOGIN") {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: `/common/slider/${verificationEnums}`,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
export function storeLogout() {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/passport/login/logout",
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取店铺基本配置与信息
|
||||||
|
*/
|
||||||
|
export function getStoreSettings() {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/settings/storeSettings",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录人员(店主 / 店员)个人信息
|
||||||
|
*/
|
||||||
|
export function getStoreUserInfo() {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/member/user/info",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录所属店铺详情
|
||||||
|
*/
|
||||||
|
export function getStoreDetail() {
|
||||||
|
return storeHttp.request({
|
||||||
|
url: "/member/user",
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
}
|
||||||
650
pages-sub/merchant/pages/evaluation/list.vue
Normal file
650
pages-sub/merchant/pages/evaluation/list.vue
Normal file
@@ -0,0 +1,650 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-evaluation-page">
|
||||||
|
<!-- 顶部 Tab 过滤 -->
|
||||||
|
<view class="sticky-header">
|
||||||
|
<view class="tab-scroll-wrap">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
v-for="item in tabList"
|
||||||
|
:key="item.key"
|
||||||
|
:class="{ active: currentTab === item.key }"
|
||||||
|
@click="switchTab(item.key)"
|
||||||
|
>
|
||||||
|
<text class="tab-label">{{ item.label }}</text>
|
||||||
|
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 评价列表主体 -->
|
||||||
|
<view class="eval-list-body">
|
||||||
|
<view
|
||||||
|
class="eval-card"
|
||||||
|
v-for="item in evalList"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<!-- 头部:买家信息与星级 -->
|
||||||
|
<view class="eval-header">
|
||||||
|
<view class="user-box">
|
||||||
|
<image
|
||||||
|
class="user-avatar"
|
||||||
|
:src="item.memberProfile || '/static/tabbar/home.png'"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<view class="user-meta">
|
||||||
|
<text class="user-name">{{ maskName(item.memberName) }}</text>
|
||||||
|
<text class="eval-time">{{ item.createTime || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 评分 -->
|
||||||
|
<view class="rating-stars">
|
||||||
|
<u-icon
|
||||||
|
v-for="star in 5"
|
||||||
|
:key="star"
|
||||||
|
name="star-fill"
|
||||||
|
size="14"
|
||||||
|
:color="star <= (item.descriptionScore || 5) ? '#f59e0b' : '#e5e7eb'"
|
||||||
|
></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 评价文字内容 -->
|
||||||
|
<view class="eval-content">
|
||||||
|
<text class="content-text">{{ item.content || '用户未填写文字评价' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 买家晒图 -->
|
||||||
|
<view class="eval-images" v-if="getImages(item.images).length > 0">
|
||||||
|
<image
|
||||||
|
class="eval-pic"
|
||||||
|
v-for="(img, idx) in getImages(item.images)"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
mode="aspectFill"
|
||||||
|
@click="previewImages(getImages(item.images), idx)"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 关联商品简要信息 -->
|
||||||
|
<view class="goods-snippet" v-if="item.goodsName">
|
||||||
|
<image
|
||||||
|
class="goods-snippet-img"
|
||||||
|
:src="item.goodsImage || '/static/tabbar/home.png'"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<text class="goods-snippet-title">{{ item.goodsName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商家已回复内容 -->
|
||||||
|
<view class="seller-reply-box" v-if="item.reply">
|
||||||
|
<view class="reply-title-row">
|
||||||
|
<text class="reply-tag">掌柜回复:</text>
|
||||||
|
<text class="reply-time" v-if="item.replyTime">{{ item.replyTime }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="reply-text">{{ item.reply }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 回复动作条 -->
|
||||||
|
<view class="eval-footer">
|
||||||
|
<view
|
||||||
|
class="reply-trigger-btn"
|
||||||
|
@click="openReplyModal(item)"
|
||||||
|
>
|
||||||
|
<u-icon name="chat" size="14" :color="item.reply ? '#6b7280' : '#ffffff'"></u-icon>
|
||||||
|
<text :class="{ 'is-replied': item.reply }">
|
||||||
|
{{ item.reply ? '修改回复' : '立即回复' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-state" v-if="!loading && evalList.length === 0">
|
||||||
|
<u-icon name="chat" size="56" color="#d1d5db"></u-icon>
|
||||||
|
<text class="empty-text">暂无相关评价数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载提示 -->
|
||||||
|
<view class="loading-more" v-if="loading">
|
||||||
|
<text>正在加载评价数据...</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-more" v-else-if="finished && evalList.length > 0">
|
||||||
|
<text>没有更多了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 回复弹窗 -->
|
||||||
|
<view class="reply-modal-mask" v-if="showReplyModal" @click.self="closeReplyModal">
|
||||||
|
<view class="reply-modal-card">
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">回复买家评价</text>
|
||||||
|
<view class="close-btn" @click="closeReplyModal">
|
||||||
|
<u-icon name="close" size="18" color="#9ca3af"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-body">
|
||||||
|
<textarea
|
||||||
|
class="reply-textarea"
|
||||||
|
v-model="replyText"
|
||||||
|
placeholder="请输入真诚的掌柜回复内容,让买家感受到热情与专业..."
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
maxlength="300"
|
||||||
|
></textarea>
|
||||||
|
<view class="char-count">
|
||||||
|
<text>{{ replyText.length }}/300</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="modal-btn cancel" @click="closeReplyModal">取消</view>
|
||||||
|
<view
|
||||||
|
class="modal-btn confirm"
|
||||||
|
:class="{ loading: submittingReply }"
|
||||||
|
@click="submitReply"
|
||||||
|
>
|
||||||
|
{{ submittingReply ? '提交中...' : '提交回复' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { getStoreEvaluationList, replyStoreEvaluation } from '../../api/evaluation.js'
|
||||||
|
|
||||||
|
const currentTab = ref('ALL')
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const evalList = ref([])
|
||||||
|
|
||||||
|
const showReplyModal = ref(false)
|
||||||
|
const currentEval = ref(null)
|
||||||
|
const replyText = ref('')
|
||||||
|
const submittingReply = ref(false)
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{ key: 'ALL', label: '全部' },
|
||||||
|
{ key: 'GOOD', label: '好评' },
|
||||||
|
{ key: 'POOR', label: '中差评' },
|
||||||
|
{ key: 'UNREPLIED', label: '待回复' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
fetchEvaluations(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(async () => {
|
||||||
|
await fetchEvaluations(true)
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!finished.value && !loading.value) {
|
||||||
|
pageNumber.value++
|
||||||
|
fetchEvaluations(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchEvaluations = async (reset = false) => {
|
||||||
|
if (reset) {
|
||||||
|
pageNumber.value = 1
|
||||||
|
evalList.value = []
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
pageNumber: pageNumber.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.value === 'GOOD') {
|
||||||
|
params.grade = 'GOOD'
|
||||||
|
} else if (currentTab.value === 'POOR') {
|
||||||
|
params.grade = 'POOR'
|
||||||
|
} else if (currentTab.value === 'UNREPLIED') {
|
||||||
|
params.replyStatus = false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getStoreEvaluationList(params)
|
||||||
|
loading.value = false
|
||||||
|
if (res.data && res.data.success && res.data.result) {
|
||||||
|
const records = res.data.result.records || []
|
||||||
|
if (reset) {
|
||||||
|
evalList.value = records
|
||||||
|
} else {
|
||||||
|
evalList.value = [...evalList.value, ...records]
|
||||||
|
}
|
||||||
|
if (records.length < pageSize.value) {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loading.value = false
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchTab = (tabKey) => {
|
||||||
|
if (currentTab.value === tabKey) return
|
||||||
|
currentTab.value = tabKey
|
||||||
|
fetchEvaluations(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskName = (name) => {
|
||||||
|
if (!name) return '匿名买家'
|
||||||
|
if (name.length <= 2) return name[0] + '*'
|
||||||
|
return name[0] + '***' + name[name.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const getImages = (imgs) => {
|
||||||
|
if (!imgs) return []
|
||||||
|
if (Array.isArray(imgs)) return imgs
|
||||||
|
if (typeof imgs === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(imgs)
|
||||||
|
if (Array.isArray(parsed)) return parsed
|
||||||
|
} catch (e) {
|
||||||
|
return imgs.split(',').filter(Boolean)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewImages = (urls, current) => {
|
||||||
|
uni.previewImage({
|
||||||
|
urls,
|
||||||
|
current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开回复弹窗
|
||||||
|
const openReplyModal = (item) => {
|
||||||
|
currentEval.value = item
|
||||||
|
replyText.value = item.reply || ''
|
||||||
|
showReplyModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeReplyModal = () => {
|
||||||
|
showReplyModal.value = false
|
||||||
|
currentEval.value = null
|
||||||
|
replyText.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交回复
|
||||||
|
const submitReply = async () => {
|
||||||
|
if (!replyText.value.trim()) {
|
||||||
|
uni.showToast({ title: '请输入回复内容', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (submittingReply.value) return
|
||||||
|
|
||||||
|
submittingReply.value = true
|
||||||
|
uni.showLoading({ title: '提交中...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await replyStoreEvaluation(currentEval.value.id, replyText.value.trim())
|
||||||
|
uni.hideLoading()
|
||||||
|
submittingReply.value = false
|
||||||
|
|
||||||
|
if (res.data && res.data.success) {
|
||||||
|
uni.showToast({ title: '回复成功', icon: 'success' })
|
||||||
|
closeReplyModal()
|
||||||
|
fetchEvaluations(true)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res.data?.message || '回复失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
submittingReply.value = false
|
||||||
|
uni.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-evaluation-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.tab-scroll-wrap {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.tab-label {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-list-body {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
|
||||||
|
.eval-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
.eval-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.user-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-time {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-stars {
|
||||||
|
display: flex;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-content {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #1f2937;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
|
||||||
|
.eval-pic {
|
||||||
|
width: 150rpx;
|
||||||
|
height: 150rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-snippet {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 14rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.goods-snippet-img {
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #e5e7eb;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-snippet-title {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-left: 14rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-reply-box {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-left: 6rpx solid #111827;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
|
||||||
|
.reply-title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
.reply-tag {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-time {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4b5563;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.eval-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
.reply-trigger-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 10rpx 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
.is-replied {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.is-replied) {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1rpx solid #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 回复弹窗 */
|
||||||
|
.reply-modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
|
||||||
|
.reply-modal-card {
|
||||||
|
background: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 640rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 36rpx;
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.reply-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 240rpx;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1rpx solid #e5e7eb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-count {
|
||||||
|
position: absolute;
|
||||||
|
right: 16rpx;
|
||||||
|
bottom: 16rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.confirm {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
737
pages-sub/merchant/pages/goods/detail.vue
Normal file
737
pages-sub/merchant/pages/goods/detail.vue
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-goods-detail" v-if="goodsDetail">
|
||||||
|
<!-- 顶部主图轮播 -->
|
||||||
|
<view class="gallery-wrapper">
|
||||||
|
<swiper
|
||||||
|
class="gallery-swiper"
|
||||||
|
:indicator-dots="false"
|
||||||
|
:autoplay="false"
|
||||||
|
@change="onSwiperChange"
|
||||||
|
>
|
||||||
|
<swiper-item
|
||||||
|
v-for="(img, idx) in galleryImages"
|
||||||
|
:key="idx"
|
||||||
|
@click="previewGallery(idx)"
|
||||||
|
>
|
||||||
|
<image class="swiper-img" :src="img" mode="aspectFill"></image>
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view class="swiper-indicator" v-if="galleryImages.length > 1">
|
||||||
|
<text>{{ currentSwiperIndex + 1 }} / {{ galleryImages.length }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心信息卡片 -->
|
||||||
|
<view class="info-card core-card">
|
||||||
|
<view class="price-row">
|
||||||
|
<view class="main-price">
|
||||||
|
<text class="symbol">¥</text>
|
||||||
|
<text class="num">{{ goodsDetail.price || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="sub-price" v-if="goodsDetail.cost">
|
||||||
|
<text>成本价 ¥{{ goodsDetail.cost }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-tags">
|
||||||
|
<view
|
||||||
|
class="badge market"
|
||||||
|
:class="goodsDetail.marketEnable === 'UPPER' ? 'on-sale' : 'off-sale'"
|
||||||
|
>
|
||||||
|
{{ goodsDetail.marketEnable === 'UPPER' ? '出售中' : '已下架' }}
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="badge auth"
|
||||||
|
:class="goodsDetail.authFlag === 'PASS' ? 'auth-pass' : 'auth-pending'"
|
||||||
|
>
|
||||||
|
{{ getAuthText(goodsDetail.authFlag) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="title-row">
|
||||||
|
<text class="goods-title">{{ goodsDetail.goodsName }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="meta-stats-grid">
|
||||||
|
<view class="stat-col">
|
||||||
|
<text class="stat-num">{{ goodsDetail.quantity ?? 0 }}</text>
|
||||||
|
<text class="stat-label">总库存</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-sep"></view>
|
||||||
|
<view class="stat-col">
|
||||||
|
<text class="stat-num">{{ goodsDetail.buyCount ?? 0 }}</text>
|
||||||
|
<text class="stat-label">累计销量</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-sep"></view>
|
||||||
|
<view class="stat-col">
|
||||||
|
<text class="stat-num">{{ goodsDetail.sn || '--' }}</text>
|
||||||
|
<text class="stat-label">商品货号</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 规格与多 SKU 列表 -->
|
||||||
|
<view class="info-card" v-if="skuList && skuList.length > 0">
|
||||||
|
<view class="card-head">
|
||||||
|
<view class="head-left">
|
||||||
|
<text class="card-title">规格与 SKU 明细</text>
|
||||||
|
<text class="sku-count-pill">共 {{ skuList.length }} 种规格</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="sku-list">
|
||||||
|
<view
|
||||||
|
class="sku-item"
|
||||||
|
v-for="(sku, index) in skuList"
|
||||||
|
:key="sku.id || sku.sn || index"
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
class="sku-thumb"
|
||||||
|
:src="getSkuThumb(sku)"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<view class="sku-info">
|
||||||
|
<text class="sku-spec">{{ formatSpecs(sku, index) }}</text>
|
||||||
|
<text class="sku-sn" v-if="sku.sn">SKU编码:{{ sku.sn }}</text>
|
||||||
|
<view class="sku-nums">
|
||||||
|
<text class="sku-price">售价:<text class="val">¥{{ sku.price || '0.00' }}</text></text>
|
||||||
|
<text class="sku-stock">库存:<text class="val">{{ sku.quantity ?? sku.stock ?? sku.enableQuantity ?? 0 }}</text></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 基本属性卡片(优化分类展示) -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">基本属性</text>
|
||||||
|
</view>
|
||||||
|
<view class="prop-row" v-if="goodsDetail.categoryName">
|
||||||
|
<text class="prop-label">所属分类</text>
|
||||||
|
<text class="prop-val cat-val">{{ formatCategoryName(goodsDetail.categoryName) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="prop-row" v-if="goodsDetail.brandName">
|
||||||
|
<text class="prop-label">商品品牌</text>
|
||||||
|
<text class="prop-val">{{ goodsDetail.brandName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="prop-row" v-if="goodsDetail.goodsUnit">
|
||||||
|
<text class="prop-label">计量单位</text>
|
||||||
|
<text class="prop-val">{{ goodsDetail.goodsUnit }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="prop-row" v-if="goodsDetail.releaseTime">
|
||||||
|
<text class="prop-label">发布时间</text>
|
||||||
|
<text class="prop-val">{{ goodsDetail.releaseTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 图文详情 -->
|
||||||
|
<view class="info-card intro-card" v-if="goodsDetail.intro || goodsDetail.mobileIntro">
|
||||||
|
<view class="card-head">
|
||||||
|
<text class="card-title">商品图文详情</text>
|
||||||
|
</view>
|
||||||
|
<view class="intro-html">
|
||||||
|
<rich-text :nodes="formatRichText(goodsDetail.mobileIntro || goodsDetail.intro)"></rich-text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部固定悬浮操作栏(带安全底间距,防止遮挡内容) -->
|
||||||
|
<view class="bottom-action-bar">
|
||||||
|
<view class="preview-btn" @click="previewInMall">
|
||||||
|
<u-icon name="eye" size="20" color="#4b5563"></u-icon>
|
||||||
|
<text>买家预览</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="market-action-btn off"
|
||||||
|
v-if="goodsDetail.marketEnable === 'UPPER'"
|
||||||
|
@click="handleToggleMarket('DOWN')"
|
||||||
|
>
|
||||||
|
<text>下架商品</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="market-action-btn on"
|
||||||
|
v-else
|
||||||
|
@click="handleToggleMarket('UPPER')"
|
||||||
|
>
|
||||||
|
<text>立即上架</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getStoreGoodsDetail, getStoreGoodsSkuList, upGoods, downGoods } from '../../api/goods.js'
|
||||||
|
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||||
|
|
||||||
|
const getSkuThumb = (sku: any) => {
|
||||||
|
const img = sku.thumbnail || sku.original
|
||||||
|
if (img) {
|
||||||
|
const parsed = parseGoodsImageUrl(img)
|
||||||
|
if (parsed) return parsed
|
||||||
|
}
|
||||||
|
const rawSpec = sku.simpleSpecs || sku.specs
|
||||||
|
if (rawSpec && typeof rawSpec === 'string') {
|
||||||
|
const match = rawSpec.match(/https?:\/\/[^\s"'\\}]+\.(jpeg|jpg|png|webp|gif)/i)
|
||||||
|
if (match) return match[0]
|
||||||
|
}
|
||||||
|
return galleryImages.value[0] || '/static/tabbar/home.png'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const goodsId = ref('')
|
||||||
|
const goodsDetail = ref<any>(null)
|
||||||
|
const skuList = ref<any[]>([])
|
||||||
|
const currentSwiperIndex = ref(0)
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
if (options?.id) {
|
||||||
|
goodsId.value = options.id
|
||||||
|
fetchGoodsDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const galleryImages = computed(() => {
|
||||||
|
if (!goodsDetail.value) return []
|
||||||
|
if (Array.isArray(goodsDetail.value.goodsGalleryList) && goodsDetail.value.goodsGalleryList.length > 0) {
|
||||||
|
return goodsDetail.value.goodsGalleryList
|
||||||
|
}
|
||||||
|
const defaultImg = goodsDetail.value.original || goodsDetail.value.thumbnail
|
||||||
|
return defaultImg ? [defaultImg] : ['/static/tabbar/home.png']
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchGoodsDetail = async () => {
|
||||||
|
uni.showLoading({ title: '加载中...' })
|
||||||
|
try {
|
||||||
|
const res: any = await getStoreGoodsDetail(goodsId.value)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (res?.data && res.data.success && res.data.result) {
|
||||||
|
goodsDetail.value = res.data.result
|
||||||
|
skuList.value = res.data.result.skuList || []
|
||||||
|
if (skuList.value.length === 0) {
|
||||||
|
fetchSkuList()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '获取商品详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchSkuList = async () => {
|
||||||
|
try {
|
||||||
|
const skuRes: any = await getStoreGoodsSkuList(goodsId.value)
|
||||||
|
if (skuRes?.data && skuRes.data.success && Array.isArray(skuRes.data.result)) {
|
||||||
|
skuList.value = skuRes.data.result
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSwiperChange = (e: any) => {
|
||||||
|
currentSwiperIndex.value = e.detail.current
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewGallery = (current: number) => {
|
||||||
|
uni.previewImage({
|
||||||
|
urls: galleryImages.value,
|
||||||
|
current,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能清洗并优雅格式化规格文本(过滤 images 等系统 JSON 冗余)
|
||||||
|
const formatSpecs = (sku: any, index: number) => {
|
||||||
|
// 1. 优先使用规范的 specList 数组
|
||||||
|
if (sku.specList && Array.isArray(sku.specList) && sku.specList.length > 0) {
|
||||||
|
const listStr = sku.specList
|
||||||
|
.map((s: any) => {
|
||||||
|
if (s.specName && s.specValue) return `${s.specName}: ${s.specValue}`
|
||||||
|
return s.specValue || s.specName
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ')
|
||||||
|
if (listStr) return listStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 深度清洗 simpleSpecs 或 specs 中的复杂复合 JSON
|
||||||
|
const rawSpec = sku.simpleSpecs || sku.specs
|
||||||
|
if (rawSpec && typeof rawSpec === 'string') {
|
||||||
|
const trimmed = rawSpec.trim()
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(trimmed)
|
||||||
|
const ignoreKeys = ['images', 'code', 'message', 'result', 'success', 'showProgress', 'url', 'status', 'specImage', 'specType']
|
||||||
|
const pairs: string[] = []
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (!ignoreKeys.includes(k) && typeof v === 'string' && !v.startsWith('http') && v.length < 50) {
|
||||||
|
pairs.push(`${k}: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pairs.length > 0) return pairs.join(' / ')
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// JSON 解析失败时正则提取真实的规格键值对
|
||||||
|
const matches = [...trimmed.matchAll(/"([^"]{1,20})"\s*:\s*"([^"]{1,30})"/g)]
|
||||||
|
const ignoreKeys = ['code', 'message', 'success', 'url', 'result', 'showProgress', 'status', 'name']
|
||||||
|
const pairs: string[] = []
|
||||||
|
for (const m of matches) {
|
||||||
|
const k = m[1]
|
||||||
|
const v = m[2]
|
||||||
|
if (!ignoreKeys.includes(k) && !v.startsWith('http')) {
|
||||||
|
pairs.push(`${k}: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pairs.length > 0) return pairs.join(' / ')
|
||||||
|
} else {
|
||||||
|
// 纯文本规格
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 提取不同于 SPU 主标题的商品名称差异
|
||||||
|
if (sku.goodsName && sku.goodsName !== goodsDetail.value?.goodsName) {
|
||||||
|
return sku.goodsName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 最终兜底
|
||||||
|
return skuList.value.length > 1 ? `规格 ${index + 1}` : '统一规格'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化分类展示:将原始 JSON 数组 [ "数码办公", "手机通讯", "手机" ] 转为层级面包屑 "数码办公 > 手机通讯 > 手机"
|
||||||
|
const formatCategoryName = (cat: any) => {
|
||||||
|
if (!cat) return '--'
|
||||||
|
if (Array.isArray(cat)) {
|
||||||
|
return cat.filter(Boolean).join(' > ')
|
||||||
|
}
|
||||||
|
if (typeof cat === 'string') {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(cat)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.filter(Boolean).join(' > ')
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
// 处理带引号或逗号的非标准字符串
|
||||||
|
return cat.replace(/[\[\]"']/g, '').split(',').map((s: string) => s.trim()).filter(Boolean).join(' > ')
|
||||||
|
}
|
||||||
|
return String(cat)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAuthText = (flag: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
PASS: '审核通过',
|
||||||
|
AUDIT: '待审核',
|
||||||
|
REFUSE: '审核驳回',
|
||||||
|
}
|
||||||
|
return map[flag] || flag || '已通过'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatRichText = (html: string) => {
|
||||||
|
if (!html) return ''
|
||||||
|
return html
|
||||||
|
.replace(/<img[^>]*>/gi, (match) => {
|
||||||
|
return match
|
||||||
|
.replace(/style="[^"]*"/gi, '')
|
||||||
|
.replace(/width="[^"]*"/gi, '')
|
||||||
|
.replace(/height="[^"]*"/gi, '')
|
||||||
|
})
|
||||||
|
.replace(/\<img/gi, '<img style="max-width:100%;height:auto;display:block;border-radius:12rpx;margin:16rpx 0;"')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上下架切换
|
||||||
|
const handleToggleMarket = (targetStatus: string) => {
|
||||||
|
const isUp = targetStatus === 'UPPER'
|
||||||
|
const actionText = isUp ? '上架' : '下架'
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: `${actionText}商品`,
|
||||||
|
content: `确定要${actionText}当前商品?`,
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showLoading({ title: `正在${actionText}...` })
|
||||||
|
try {
|
||||||
|
const apiCall = isUp ? upGoods(goodsId.value) : downGoods(goodsId.value)
|
||||||
|
const resp: any = await apiCall
|
||||||
|
uni.hideLoading()
|
||||||
|
if (resp?.data && resp.data.success) {
|
||||||
|
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
||||||
|
fetchGoodsDetail()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: resp?.data?.message || `${actionText}失败`,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前往买家端预览
|
||||||
|
const previewInMall = () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/product/goods?id=${goodsId.value}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-goods-detail {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f4f5f7;
|
||||||
|
/* 预留出底部固定操作栏的高以及全面屏安全区,彻底解决内容被底部遮挡 */
|
||||||
|
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 680rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
|
||||||
|
.gallery-swiper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.swiper-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.swiper-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: 28rpx;
|
||||||
|
bottom: 44rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin: 20rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 16rpx;
|
||||||
|
border-bottom: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
.head-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-count-pill {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.core-card {
|
||||||
|
margin-top: -30rpx;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.main-price {
|
||||||
|
color: #ef4444;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.num {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-price {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tags {
|
||||||
|
display: flex;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.on-sale {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off-sale {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.auth-pass {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.auth-pending {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.goods-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-stats-grid {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 22rpx 0;
|
||||||
|
margin-top: 26rpx;
|
||||||
|
|
||||||
|
.stat-col {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-sep {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 38rpx;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-list {
|
||||||
|
.sku-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f3f4f6;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-thumb {
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1rpx solid #f3f4f6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.sku-spec {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-sn {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-nums {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
|
||||||
|
.sku-price {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-stock {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
.val {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
width: 160rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-val {
|
||||||
|
flex: 1;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
&.cat-val {
|
||||||
|
color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.intro-card {
|
||||||
|
.intro-html {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #374151;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-action-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16rpx 28rpx calc(16rpx + env(safe-area-inset-bottom)) 28rpx;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
z-index: 100;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.preview-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #4b5563;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1rpx solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
472
pages-sub/merchant/pages/goods/list.vue
Normal file
472
pages-sub/merchant/pages/goods/list.vue
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-goods-page">
|
||||||
|
<!-- 顶部吸顶搜索栏与 Tab -->
|
||||||
|
<view class="sticky-header">
|
||||||
|
<view class="search-bar">
|
||||||
|
<u-icon name="search" size="18" color="#9ca3af"></u-icon>
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
type="text"
|
||||||
|
v-model="keywords"
|
||||||
|
placeholder="搜索商品名称"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
@confirm="onSearch"
|
||||||
|
/>
|
||||||
|
<view class="clear-btn" v-if="keywords" @click="clearSearch">
|
||||||
|
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="tab-scroll-wrap">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
v-for="item in tabList"
|
||||||
|
:key="item.key"
|
||||||
|
:class="{ active: currentTab === item.key }"
|
||||||
|
@click="switchTab(item.key)"
|
||||||
|
>
|
||||||
|
<text class="tab-label">{{ item.label }}</text>
|
||||||
|
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品列表主体 -->
|
||||||
|
<view class="goods-list-body">
|
||||||
|
<view
|
||||||
|
class="goods-card"
|
||||||
|
v-for="goods in goodsList"
|
||||||
|
:key="goods.id"
|
||||||
|
@click="goToDetail(goods.id)"
|
||||||
|
>
|
||||||
|
<view class="card-main">
|
||||||
|
<image
|
||||||
|
class="goods-img"
|
||||||
|
:src="goods.thumbnail || goods.original || '/static/tabbar/home.png'"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
<view class="goods-meta">
|
||||||
|
<view class="title-row">
|
||||||
|
<text class="goods-title">{{ goods.goodsName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="stock-sales-row">
|
||||||
|
<text class="meta-tag">库存: {{ goods.quantity ?? 0 }}</text>
|
||||||
|
<text class="meta-tag">销量: {{ goods.buyCount ?? 0 }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-status-row">
|
||||||
|
<view class="price-box">
|
||||||
|
<text class="symbol">¥</text>
|
||||||
|
<text class="price-num">{{ goods.price || goods.sellingPrice || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="status-badge" :class="goods.marketEnable === 'UPPER' ? 'on-sale' : 'off-sale'">
|
||||||
|
{{ goods.marketEnable === 'UPPER' ? '出售中' : '已下架' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部快捷操作条 -->
|
||||||
|
<view class="card-footer">
|
||||||
|
<text class="goods-sn" v-if="goods.sn">商品货号:{{ goods.sn }}</text>
|
||||||
|
<view class="actions-group">
|
||||||
|
<view
|
||||||
|
class="action-btn off"
|
||||||
|
v-if="goods.marketEnable === 'UPPER'"
|
||||||
|
@click.stop="toggleMarket(goods, 'DOWN')"
|
||||||
|
>
|
||||||
|
<text>下架商品</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="action-btn on"
|
||||||
|
v-else
|
||||||
|
@click.stop="toggleMarket(goods, 'UPPER')"
|
||||||
|
>
|
||||||
|
<text>立即上架</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-state" v-if="!loading && goodsList.length === 0">
|
||||||
|
<u-icon name="bag" size="56" color="#d1d5db"></u-icon>
|
||||||
|
<text class="empty-text">暂无相关商品</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载提示 -->
|
||||||
|
<view class="loading-more" v-if="loading">
|
||||||
|
<text>正在加载商品列表...</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-more" v-else-if="finished && goodsList.length > 0">
|
||||||
|
<text>没有更多了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { getStoreGoodsList, upGoods, downGoods } from '../../api/goods.js'
|
||||||
|
|
||||||
|
const keywords = ref('')
|
||||||
|
const currentTab = ref('ALL')
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const goodsList = ref([])
|
||||||
|
|
||||||
|
const tabList = [
|
||||||
|
{ key: 'ALL', label: '全部' },
|
||||||
|
{ key: 'UPPER', label: '出售中' },
|
||||||
|
{ key: 'DOWN', label: '已下架' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onLoad((options) => {
|
||||||
|
if (options?.tab) {
|
||||||
|
currentTab.value = options.tab
|
||||||
|
}
|
||||||
|
fetchGoods(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(async () => {
|
||||||
|
await fetchGoods(true)
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!finished.value && !loading.value) {
|
||||||
|
pageNumber.value++
|
||||||
|
fetchGoods(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchGoods = async (reset = false) => {
|
||||||
|
if (reset) {
|
||||||
|
pageNumber.value = 1
|
||||||
|
goodsList.value = []
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
pageNumber: pageNumber.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords.value.trim()) {
|
||||||
|
params.goodsName = keywords.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.value !== 'ALL') {
|
||||||
|
params.marketEnable = currentTab.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getStoreGoodsList(params)
|
||||||
|
loading.value = false
|
||||||
|
if (res.data && res.data.success && res.data.result) {
|
||||||
|
const records = res.data.result.records || []
|
||||||
|
if (reset) {
|
||||||
|
goodsList.value = records
|
||||||
|
} else {
|
||||||
|
goodsList.value = [...goodsList.value, ...records]
|
||||||
|
}
|
||||||
|
if (records.length < pageSize.value) {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loading.value = false
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchTab = (tabKey) => {
|
||||||
|
if (currentTab.value === tabKey) return
|
||||||
|
currentTab.value = tabKey
|
||||||
|
fetchGoods(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
fetchGoods(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
keywords.value = ''
|
||||||
|
fetchGoods(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDetail = (id) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-sub/merchant/pages/goods/detail?id=${id}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷上下架切换
|
||||||
|
const toggleMarket = (goods, targetStatus) => {
|
||||||
|
const isUp = targetStatus === 'UPPER'
|
||||||
|
const actionText = isUp ? '上架' : '下架'
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: `${actionText}提示`,
|
||||||
|
content: `确定要${actionText}商品「${goods.goodsName}」吗?`,
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showLoading({ title: `正在${actionText}...` })
|
||||||
|
try {
|
||||||
|
const apiCall = isUp ? upGoods(goods.id) : downGoods(goods.id)
|
||||||
|
const resp = await apiCall
|
||||||
|
uni.hideLoading()
|
||||||
|
if (resp.data && resp.data.success) {
|
||||||
|
uni.showToast({ title: `${actionText}成功`, icon: 'success' })
|
||||||
|
fetchGoods(true)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: resp.data?.message || `${actionText}失败`,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-goods-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f3f4f6;
|
||||||
|
margin: 20rpx 28rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-scroll-wrap {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.tab-label {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 40rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-list-body {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
|
||||||
|
.goods-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
.card-main {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.goods-img {
|
||||||
|
width: 170rpx;
|
||||||
|
height: 170rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-meta {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
.goods-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-sales-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
|
||||||
|
.meta-tag {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-status-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
|
||||||
|
.price-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
color: #ff3c2a;
|
||||||
|
|
||||||
|
.symbol {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-num {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.on-sale {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.off-sale {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
.goods-sn {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
padding: 10rpx 28rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.off {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1rpx solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.on {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
812
pages-sub/merchant/pages/index/index.vue
Normal file
812
pages-sub/merchant/pages/index/index.vue
Normal file
@@ -0,0 +1,812 @@
|
|||||||
|
<template>
|
||||||
|
<view class="workbench-page">
|
||||||
|
<!-- 顶部商家信息栏 -->
|
||||||
|
<view class="store-hero">
|
||||||
|
<view class="store-info-row">
|
||||||
|
<view class="store-avatar">
|
||||||
|
<image
|
||||||
|
class="avatar-img"
|
||||||
|
:src="storeInfo.settings?.storeLogo || '/static/tabbar/home.png'"
|
||||||
|
mode="aspectFill"
|
||||||
|
></image>
|
||||||
|
</view>
|
||||||
|
<view class="store-meta">
|
||||||
|
<view class="name-row">
|
||||||
|
<text class="store-name">{{ storeInfo.settings?.storeName || storeInfo.store?.storeName || '商家移动工作台' }}</text>
|
||||||
|
<view class="badge" :class="isClerk ? 'clerk-badge' : 'owner-badge'">
|
||||||
|
{{ isClerk ? '店员' : '店主' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="clerk-name">当前操作员:{{ operatorName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="logout-btn" @click="handleLogout">
|
||||||
|
<text>退出</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心操作区(快速核销) -->
|
||||||
|
<view class="core-actions-card">
|
||||||
|
<view class="card-header">
|
||||||
|
<text class="card-title">快速核销</text>
|
||||||
|
<text class="card-subtitle">支持预约单、自提单与虚拟卡密</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 扫码主按钮 -->
|
||||||
|
<view class="scan-hero-btn" @click="handleScanCode">
|
||||||
|
<view class="scan-icon-circle">
|
||||||
|
<u-icon name="scan" size="38" color="#ffffff"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="scan-text-box">
|
||||||
|
<text class="main-text">扫码快速核销</text>
|
||||||
|
<text class="sub-text">扫描顾客出示的核销码/订单码</text>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" size="18" color="rgba(255,255,255,0.6)"></u-icon>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 手动输码 -->
|
||||||
|
<view class="manual-input-box">
|
||||||
|
<view class="input-wrap">
|
||||||
|
<u-icon name="edit-pen" size="20" color="#9ca3af"></u-icon>
|
||||||
|
<input
|
||||||
|
class="code-input"
|
||||||
|
type="number"
|
||||||
|
maxlength="12"
|
||||||
|
v-model="manualCode"
|
||||||
|
placeholder="输入8位数字核销码"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
@confirm="handleManualVerify"
|
||||||
|
/>
|
||||||
|
<view class="clear-icon" v-if="manualCode" @click="clearManualCode">
|
||||||
|
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="verify-btn"
|
||||||
|
:class="{ active: manualCode.trim().length >= 6 }"
|
||||||
|
@click="handleManualVerify"
|
||||||
|
>
|
||||||
|
<text>核销</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核心业务四大板块 -->
|
||||||
|
<view class="business-grid-card">
|
||||||
|
<view class="grid-card-header">
|
||||||
|
<text class="grid-card-title">经营管理</text>
|
||||||
|
</view>
|
||||||
|
<view class="business-grid">
|
||||||
|
<!-- 订单管理 -->
|
||||||
|
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/order/list?tab=ALL')">
|
||||||
|
<view class="biz-icon-box bg-blue">
|
||||||
|
<u-icon name="order" size="26" color="#2563eb"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="biz-title">订单管理</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品管理 -->
|
||||||
|
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/goods/list')">
|
||||||
|
<view class="biz-icon-box bg-emerald">
|
||||||
|
<u-icon name="bag" size="26" color="#059669"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="biz-title">商品管理</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 评价管理 -->
|
||||||
|
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/evaluation/list')">
|
||||||
|
<view class="biz-icon-box bg-amber">
|
||||||
|
<u-icon name="chat" size="26" color="#d97706"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="biz-title">评价管理</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 待核销专区 -->
|
||||||
|
<view class="biz-item" @click="navigateTo('/pages-sub/merchant/pages/order/list?tab=TAKE')">
|
||||||
|
<view class="biz-icon-box bg-orange">
|
||||||
|
<u-icon name="checkmark-circle" size="26" color="#ea580c"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="biz-title">待核销单</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 核销确认弹窗 -->
|
||||||
|
<view class="modal-mask" v-if="showModal" @click.self="closeModal">
|
||||||
|
<view class="modal-card">
|
||||||
|
<view class="modal-header">
|
||||||
|
<text class="modal-title">核销信息确认</text>
|
||||||
|
<view class="close-btn" @click="closeModal">
|
||||||
|
<u-icon name="close" size="18" color="#9ca3af"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单概览 -->
|
||||||
|
<view class="order-summary" v-if="verifyingOrder">
|
||||||
|
<view class="summary-row">
|
||||||
|
<text class="label">核销码:</text>
|
||||||
|
<text class="value highlight-code">{{ currentCode }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-row">
|
||||||
|
<text class="label">订单编号:</text>
|
||||||
|
<text class="value">{{ verifyingOrder.sn || verifyingOrder.orderSn || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-row" v-if="verifyingOrder.consigneeMobile || verifyingOrder.memberPhone">
|
||||||
|
<text class="label">顾客电话:</text>
|
||||||
|
<text class="value">{{ maskPhone(verifyingOrder.consigneeMobile || verifyingOrder.memberPhone) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-row" v-if="verifyingOrder.goodsName || verifyingOrder.orderItems?.[0]?.goodsName">
|
||||||
|
<text class="label">商品名称:</text>
|
||||||
|
<text class="value goods-text">
|
||||||
|
{{ verifyingOrder.goodsName || verifyingOrder.orderItems?.[0]?.goodsName }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="summary-row" v-if="verifyingOrder.num">
|
||||||
|
<text class="label">购买数量:</text>
|
||||||
|
<text class="value">{{ verifyingOrder.num }} 件</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 预约服务专属选项:选择服务人员 -->
|
||||||
|
<view class="staff-picker-box" v-if="isAppointmentOrder">
|
||||||
|
<text class="staff-label">指派/核销服务人员:</text>
|
||||||
|
<picker
|
||||||
|
class="picker-ctrl"
|
||||||
|
mode="selector"
|
||||||
|
:range="staffList"
|
||||||
|
range-key="name"
|
||||||
|
:value="selectedStaffIndex"
|
||||||
|
@change="onStaffChange"
|
||||||
|
>
|
||||||
|
<view class="picker-display">
|
||||||
|
<text>{{ currentStaffName }}</text>
|
||||||
|
<u-icon name="arrow-down" size="14" color="#6b7280"></u-icon>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 弹窗操作 -->
|
||||||
|
<view class="modal-footer">
|
||||||
|
<view class="modal-btn cancel" @click="closeModal">取消</view>
|
||||||
|
<view
|
||||||
|
class="modal-btn confirm"
|
||||||
|
:class="{ loading: confirming }"
|
||||||
|
@click="submitConfirmVerification"
|
||||||
|
>
|
||||||
|
{{ confirming ? '核销中...' : '确认核销' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import storage from '@/utils/storage.js'
|
||||||
|
import {
|
||||||
|
previewStoreVerification,
|
||||||
|
storeOrderTake,
|
||||||
|
confirmStoreVerification,
|
||||||
|
getAppointmentOrderByCode,
|
||||||
|
verifyAppointmentCode,
|
||||||
|
getEnabledServiceStaff,
|
||||||
|
} from '../../api/order.js'
|
||||||
|
import { storeLogout } from '../../api/passport.js'
|
||||||
|
|
||||||
|
const storeInfo = ref({})
|
||||||
|
|
||||||
|
const operatorName = computed(() => {
|
||||||
|
const u = storeInfo.value?.user
|
||||||
|
return u?.nickName || u?.username || '当前操作员'
|
||||||
|
})
|
||||||
|
|
||||||
|
const isClerk = computed(() => {
|
||||||
|
const u = storeInfo.value?.user
|
||||||
|
if (u && (u.isSuper === false || u.shopkeeper === false)) return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
const manualCode = ref('')
|
||||||
|
const showModal = ref(false)
|
||||||
|
const confirming = ref(false)
|
||||||
|
const currentCode = ref('')
|
||||||
|
const verifyingOrder = ref(null)
|
||||||
|
const isAppointmentOrder = ref(false)
|
||||||
|
|
||||||
|
const staffList = ref([])
|
||||||
|
const selectedStaffIndex = ref(0)
|
||||||
|
|
||||||
|
const currentStaffName = computed(() => {
|
||||||
|
if (staffList.value.length > 0 && staffList.value[selectedStaffIndex.value]) {
|
||||||
|
return staffList.value[selectedStaffIndex.value].name
|
||||||
|
}
|
||||||
|
return '未指派 / 默认'
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const token = storage.getStoreAccessToken()
|
||||||
|
if (!token) {
|
||||||
|
uni.redirectTo({ url: '/pages-sub/merchant/pages/login/index' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
storeInfo.value = storage.getStoreInfo() || {}
|
||||||
|
loadStaffList()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadStaffList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getEnabledServiceStaff()
|
||||||
|
if (res.data && res.data.success && Array.isArray(res.data.result)) {
|
||||||
|
staffList.value = res.data.result
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略加载异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskPhone = (phone) => {
|
||||||
|
if (!phone || phone.length < 7) return phone || ''
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扫码快速核销
|
||||||
|
const handleScanCode = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
onlyFromCamera: true,
|
||||||
|
scanType: ['qrCode', 'barCode'],
|
||||||
|
success: (res) => {
|
||||||
|
let code = res.result || ''
|
||||||
|
if (code.includes('code=')) {
|
||||||
|
const match = code.match(/code=([a-zA-Z0-9]+)/)
|
||||||
|
if (match && match[1]) {
|
||||||
|
code = match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (code) {
|
||||||
|
queryAndShowVerify(code.trim())
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '未能识别核销码', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动输码核销
|
||||||
|
const clearManualCode = () => {
|
||||||
|
manualCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleManualVerify = () => {
|
||||||
|
const code = manualCode.value.trim()
|
||||||
|
if (!code || code.length < 6) {
|
||||||
|
uni.showToast({ title: '请输入有效的核销码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
queryAndShowVerify(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预检核销码并展示弹窗
|
||||||
|
const queryAndShowVerify = async (code) => {
|
||||||
|
currentCode.value = code
|
||||||
|
uni.showLoading({ title: '正在查询订单...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
let isAppoint = false
|
||||||
|
let orderData = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const appointRes = await getAppointmentOrderByCode(code)
|
||||||
|
if (appointRes.data && appointRes.data.success && appointRes.data.result) {
|
||||||
|
isAppoint = true
|
||||||
|
orderData = appointRes.data.result
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
if (!orderData) {
|
||||||
|
const previewRes = await previewStoreVerification(code)
|
||||||
|
if (previewRes.data && previewRes.data.success && previewRes.data.result) {
|
||||||
|
orderData = previewRes.data.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
|
||||||
|
if (orderData) {
|
||||||
|
verifyingOrder.value = orderData
|
||||||
|
isAppointmentOrder.value = isAppoint
|
||||||
|
showModal.value = true
|
||||||
|
} else {
|
||||||
|
verifyingOrder.value = { sn: '以提交核销为准' }
|
||||||
|
isAppointmentOrder.value = false
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
uni.hideLoading()
|
||||||
|
verifyingOrder.value = { sn: '以提交核销为准' }
|
||||||
|
isAppointmentOrder.value = false
|
||||||
|
showModal.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStaffChange = (e) => {
|
||||||
|
selectedStaffIndex.value = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
showModal.value = false
|
||||||
|
verifyingOrder.value = null
|
||||||
|
manualCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认核销提交
|
||||||
|
const submitConfirmVerification = async () => {
|
||||||
|
if (confirming.value) return
|
||||||
|
confirming.value = true
|
||||||
|
uni.showLoading({ title: '核销中...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
let success = false
|
||||||
|
let message = ''
|
||||||
|
|
||||||
|
if (isAppointmentOrder.value) {
|
||||||
|
const staff = staffList.value[selectedStaffIndex.value]
|
||||||
|
const params = {
|
||||||
|
orderSn: verifyingOrder.value?.orderSn || verifyingOrder.value?.sn,
|
||||||
|
code: currentCode.value,
|
||||||
|
serviceStaffId: staff ? staff.id : undefined,
|
||||||
|
}
|
||||||
|
const res = await verifyAppointmentCode(params)
|
||||||
|
success = res.data && res.data.success
|
||||||
|
message = res.data?.message
|
||||||
|
} else {
|
||||||
|
const sn = verifyingOrder.value?.orderSn || verifyingOrder.value?.sn
|
||||||
|
if (sn && sn !== '以提交核销为准') {
|
||||||
|
const res = await storeOrderTake(sn, currentCode.value)
|
||||||
|
success = res.data && res.data.success
|
||||||
|
message = res.data?.message
|
||||||
|
} else {
|
||||||
|
const res = await confirmStoreVerification(currentCode.value)
|
||||||
|
success = res.data && res.data.success
|
||||||
|
message = res.data?.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
confirming.value = false
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
try {
|
||||||
|
uni.vibrateShort()
|
||||||
|
} catch (e) {}
|
||||||
|
uni.showToast({ title: '核销成功!', icon: 'success' })
|
||||||
|
closeModal()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: message || '核销失败,请核对核销码',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
confirming.value = false
|
||||||
|
uni.showToast({ title: '请求失败,请稍后重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '退出商家工作台',
|
||||||
|
content: '退出后将清除本店登录凭证,确认退出?',
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
try {
|
||||||
|
await storeLogout()
|
||||||
|
} catch (e) {}
|
||||||
|
storage.clearStoreAuth()
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages-sub/merchant/pages/login/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigateTo = (url) => {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.workbench-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
padding-bottom: 60rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-hero {
|
||||||
|
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||||
|
padding: 60rpx 36rpx 100rpx 36rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
.store-info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.store-avatar {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.avatar-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-meta {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
|
||||||
|
.store-name {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.owner-badge {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clerk-badge {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clerk-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-actions-card {
|
||||||
|
margin: -60rpx 32rpx 28rpx 32rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 36rpx;
|
||||||
|
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-hero-btn {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
border-radius: 22rpx;
|
||||||
|
padding: 32rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 10rpx 24rpx rgba(16, 185, 129, 0.3);
|
||||||
|
|
||||||
|
.scan-icon-circle {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-text-box {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 24rpx;
|
||||||
|
|
||||||
|
.main-text {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
margin-top: 6rpx;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.manual-input-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.input-wrap {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1rpx solid #e5e7eb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 18rpx 24rpx;
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-btn {
|
||||||
|
padding: 20rpx 36rpx;
|
||||||
|
background: #e5e7eb;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-grid-card {
|
||||||
|
margin: 0 32rpx 24rpx 32rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 32rpx 24rpx;
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.grid-card-header {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-left: 8rpx;
|
||||||
|
|
||||||
|
.grid-card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.biz-item {
|
||||||
|
width: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.biz-icon-box {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&.bg-blue {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
&.bg-emerald {
|
||||||
|
background: #ecfdf5;
|
||||||
|
}
|
||||||
|
&.bg-amber {
|
||||||
|
background: #fffbeb;
|
||||||
|
}
|
||||||
|
&.bg-orange {
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-sub {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 弹窗遮罩 */
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
|
||||||
|
.modal-card {
|
||||||
|
background: #ffffff;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 620rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 36rpx;
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-summary {
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
.summary-row {
|
||||||
|
display: flex;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: #6b7280;
|
||||||
|
width: 150rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: #111827;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
&.highlight-code {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.goods-text {
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.staff-picker-box {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx dashed #e5e7eb;
|
||||||
|
|
||||||
|
.staff-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-display {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1rpx solid #d1d5db;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 16rpx 20rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 84rpx;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.cancel {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.confirm {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
550
pages-sub/merchant/pages/login/index.vue
Normal file
550
pages-sub/merchant/pages/login/index.vue
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-login-page">
|
||||||
|
<!-- 顶部状态栏沉浸背景 -->
|
||||||
|
<view class="header-bg">
|
||||||
|
<view class="brand-box">
|
||||||
|
<view class="badge-icon">
|
||||||
|
<u-icon name="bag" size="32" color="#ffffff"></u-icon>
|
||||||
|
</view>
|
||||||
|
<text class="brand-title">商家管理工作台</text>
|
||||||
|
<text class="brand-desc">随时随地 · 快捷扫码核销与订单管理</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录卡片 -->
|
||||||
|
<view class="login-card">
|
||||||
|
<!-- 模式切换 Tab -->
|
||||||
|
<view class="tab-header">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: loginType === 'account' }"
|
||||||
|
@click="switchTab('account')"
|
||||||
|
>
|
||||||
|
<text>账号密码登录</text>
|
||||||
|
<view class="active-indicator" v-if="loginType === 'account'"></view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: loginType === 'sms' }"
|
||||||
|
@click="switchTab('sms')"
|
||||||
|
>
|
||||||
|
<text>短信验证码登录</text>
|
||||||
|
<view class="active-indicator" v-if="loginType === 'sms'"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 账号密码表单 -->
|
||||||
|
<view class="form-container" v-if="loginType === 'account'">
|
||||||
|
<view class="input-item">
|
||||||
|
<u-input
|
||||||
|
v-model="accountForm.username"
|
||||||
|
placeholder="请输入商家账号/手机号"
|
||||||
|
border="surround"
|
||||||
|
prefixIcon="account"
|
||||||
|
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||||
|
:customStyle="customInputStyle"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="input-item">
|
||||||
|
<u-input
|
||||||
|
v-model="accountForm.password"
|
||||||
|
placeholder="请输入登录密码"
|
||||||
|
type="password"
|
||||||
|
border="surround"
|
||||||
|
prefixIcon="lock"
|
||||||
|
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||||
|
:customStyle="customInputStyle"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 短信验证码表单 -->
|
||||||
|
<view class="form-container" v-else>
|
||||||
|
<view class="input-item">
|
||||||
|
<u-input
|
||||||
|
v-model="smsForm.mobile"
|
||||||
|
placeholder="请输入店主手机号"
|
||||||
|
type="number"
|
||||||
|
maxlength="11"
|
||||||
|
border="surround"
|
||||||
|
prefixIcon="phone"
|
||||||
|
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||||
|
:customStyle="customInputStyle"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="input-item">
|
||||||
|
<u-input
|
||||||
|
v-model="smsForm.code"
|
||||||
|
placeholder="请输入 6 位短信验证码"
|
||||||
|
type="number"
|
||||||
|
maxlength="6"
|
||||||
|
border="surround"
|
||||||
|
prefixIcon="email"
|
||||||
|
:prefixIconStyle="{ color: '#8c8c8c', fontSize: '36rpx', marginRight: '16rpx' }"
|
||||||
|
:customStyle="customInputStyle"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<view
|
||||||
|
class="send-btn"
|
||||||
|
:class="{ disabled: smsCount > 0 || !smsForm.mobile }"
|
||||||
|
@click="handleSendSms"
|
||||||
|
>
|
||||||
|
<text>{{ smsCount > 0 ? `${smsCount}s后重试` : '获取验证码' }}</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</u-input>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 登录提交按钮 -->
|
||||||
|
<view class="action-btn" :class="{ loading: submitting }" @click="handleLogin">
|
||||||
|
<text class="btn-text">{{ submitting ? '正在登录...' : '立即进入工作台' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="notice-tip">
|
||||||
|
<u-icon name="info-circle" size="14" color="#8c8c8c"></u-icon>
|
||||||
|
<text class="notice-text">仅限已入驻商城的店主/店员账号登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 安全滑块拼图验证码弹窗 -->
|
||||||
|
<myVerification
|
||||||
|
v-if="codeFlag"
|
||||||
|
ref="verificationRef"
|
||||||
|
apiType="store"
|
||||||
|
business="LOGIN"
|
||||||
|
@send="handleVerificationSuccess"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import storage from '@/utils/storage.js'
|
||||||
|
import myVerification from '@/components/verification/verification.vue'
|
||||||
|
import {
|
||||||
|
userLogin,
|
||||||
|
smsLogin,
|
||||||
|
sendStoreSms,
|
||||||
|
getStoreSettings,
|
||||||
|
getStoreUserInfo,
|
||||||
|
getStoreDetail,
|
||||||
|
} from '../../api/passport.js'
|
||||||
|
|
||||||
|
const loginType = ref<'account' | 'sms'>('account')
|
||||||
|
const submitting = ref(false)
|
||||||
|
const smsCount = ref(0)
|
||||||
|
let timer: any = null
|
||||||
|
|
||||||
|
// 滑块验证码相关控制
|
||||||
|
const codeFlag = ref(false)
|
||||||
|
const verificationRef = ref<any>(null)
|
||||||
|
const verifyAction = ref<'login' | 'sms'>('login')
|
||||||
|
|
||||||
|
const customInputStyle = {
|
||||||
|
backgroundColor: '#f9fafb',
|
||||||
|
borderRadius: '16rpx',
|
||||||
|
height: '84rpx',
|
||||||
|
paddingLeft: '16rpx',
|
||||||
|
fontSize: '28rpx',
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const smsForm = reactive({
|
||||||
|
mobile: '',
|
||||||
|
code: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// 如果已存在商家 token,直接进入工作台
|
||||||
|
if (storage.getStoreAccessToken()) {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages-sub/merchant/pages/index/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function switchTab(type: 'account' | 'sms') {
|
||||||
|
loginType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹出滑块拼图验证码
|
||||||
|
function openVerification(action: 'login' | 'sms') {
|
||||||
|
verifyAction.value = action
|
||||||
|
codeFlag.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
verificationRef.value?.show()
|
||||||
|
verificationRef.value?.getCode()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击获取短信验证码
|
||||||
|
const handleSendSms = () => {
|
||||||
|
if (smsCount.value > 0) return
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(smsForm.mobile)) {
|
||||||
|
uni.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送短信验证码前先通过滑块拼图验证
|
||||||
|
openVerification('sms')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击“立即进入工作台”按钮
|
||||||
|
const handleLogin = () => {
|
||||||
|
if (submitting.value) return
|
||||||
|
|
||||||
|
if (loginType.value === 'account') {
|
||||||
|
if (!accountForm.username.trim()) {
|
||||||
|
uni.showToast({ title: '请输入商家账号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!accountForm.password) {
|
||||||
|
uni.showToast({ title: '请输入登录密码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 账号密码登录:必须先通过安全滑块拼图验证
|
||||||
|
openVerification('login')
|
||||||
|
} else {
|
||||||
|
// 短信验证码登录
|
||||||
|
if (!/^1[3-9]\d{9}$/.test(smsForm.mobile)) {
|
||||||
|
uni.showToast({ title: '请输入有效的手机号', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!smsForm.code.trim()) {
|
||||||
|
uni.showToast({ title: '请输入验证码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 短信验证码登录直接提交
|
||||||
|
doSmsLogin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滑块验证码成功回调
|
||||||
|
const handleVerificationSuccess = () => {
|
||||||
|
if (verifyAction.value === 'login') {
|
||||||
|
doAccountLogin()
|
||||||
|
} else if (verifyAction.value === 'sms') {
|
||||||
|
doSendSms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行账号密码登录
|
||||||
|
const doAccountLogin = async () => {
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '正在登录...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await userLogin({
|
||||||
|
username: accountForm.username.trim(),
|
||||||
|
password: accountForm.password,
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
submitting.value = false
|
||||||
|
|
||||||
|
if (res?.data && res.data.success && res.data.result) {
|
||||||
|
codeFlag.value = false
|
||||||
|
const { accessToken, refreshToken } = res.data.result
|
||||||
|
storage.setStoreAccessToken(accessToken)
|
||||||
|
if (refreshToken) {
|
||||||
|
storage.setStoreRefreshToken(refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取店铺与店员信息
|
||||||
|
try {
|
||||||
|
const [settingsRes, userRes, storeRes] = (await Promise.all([
|
||||||
|
getStoreSettings().catch(() => null),
|
||||||
|
getStoreUserInfo().catch(() => null),
|
||||||
|
getStoreDetail().catch(() => null),
|
||||||
|
])) || []
|
||||||
|
|
||||||
|
const storeInfo = {
|
||||||
|
settings: settingsRes?.data?.result || {},
|
||||||
|
user: userRes?.data?.result || {},
|
||||||
|
store: storeRes?.data?.result || {},
|
||||||
|
}
|
||||||
|
storage.setStoreInfo(storeInfo)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('获取店铺额外信息失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages-sub/merchant/pages/index/index',
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res?.data?.message || '登录失败,请检查账号密码',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
verificationRef.value?.getCode()
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
uni.hideLoading()
|
||||||
|
submitting.value = false
|
||||||
|
verificationRef.value?.getCode()
|
||||||
|
uni.showToast({
|
||||||
|
title: err?.data?.message || '网络连接异常或账号密码错误',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行发送短信验证码
|
||||||
|
const doSendSms = async () => {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '发送中...' })
|
||||||
|
const res = await sendStoreSms(smsForm.mobile, 'LOGIN')
|
||||||
|
uni.hideLoading()
|
||||||
|
if (res?.data && res.data.success) {
|
||||||
|
codeFlag.value = false
|
||||||
|
uni.showToast({ title: '验证码已发送', icon: 'success' })
|
||||||
|
smsCount.value = 60
|
||||||
|
timer = setInterval(() => {
|
||||||
|
smsCount.value--
|
||||||
|
if (smsCount.value <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
timer = null
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
verificationRef.value?.getCode()
|
||||||
|
uni.showToast({
|
||||||
|
title: res?.data?.message || '发送失败,请稍后重试',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
uni.hideLoading()
|
||||||
|
verificationRef.value?.getCode()
|
||||||
|
uni.showToast({ title: e?.data?.message || '网络异常,发送失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行短信登录
|
||||||
|
const doSmsLogin = async () => {
|
||||||
|
submitting.value = true
|
||||||
|
uni.showLoading({ title: '正在登录...' })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await smsLogin({
|
||||||
|
mobile: smsForm.mobile.trim(),
|
||||||
|
code: smsForm.code.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
|
uni.hideLoading()
|
||||||
|
submitting.value = false
|
||||||
|
|
||||||
|
if (res?.data && res.data.success && res.data.result) {
|
||||||
|
const { accessToken, refreshToken } = res.data.result
|
||||||
|
storage.setStoreAccessToken(accessToken)
|
||||||
|
if (refreshToken) {
|
||||||
|
storage.setStoreRefreshToken(refreshToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [settingsRes, userRes, storeRes] = (await Promise.all([
|
||||||
|
getStoreSettings().catch(() => null),
|
||||||
|
getStoreUserInfo().catch(() => null),
|
||||||
|
getStoreDetail().catch(() => null),
|
||||||
|
])) || []
|
||||||
|
|
||||||
|
const storeInfo = {
|
||||||
|
settings: settingsRes?.data?.result || {},
|
||||||
|
user: userRes?.data?.result || {},
|
||||||
|
store: storeRes?.data?.result || {},
|
||||||
|
}
|
||||||
|
storage.setStoreInfo(storeInfo)
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('获取店铺额外信息失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({ title: '登录成功', icon: 'success' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages-sub/merchant/pages/index/index',
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res?.data?.message || '登录失败,请检查验证码',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
uni.hideLoading()
|
||||||
|
submitting.value = false
|
||||||
|
uni.showToast({ title: err?.data?.message || '网络连接异常', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-login-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f7f8fa;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-bg {
|
||||||
|
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||||
|
padding: 100rpx 40rpx 140rpx 40rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.brand-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.badge-icon {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 38rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-card {
|
||||||
|
margin: -80rpx 32rpx 40rpx 32rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
padding: 40rpx 36rpx;
|
||||||
|
box-shadow: 0 12rpx 36rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.tab-header {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
margin-bottom: 36rpx;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
.input-item {
|
||||||
|
margin-bottom: 28rpx;
|
||||||
|
|
||||||
|
.send-btn {
|
||||||
|
padding: 10rpx 20rpx;
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
background: #d1d5db;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
height: 92rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 32rpx;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(17, 24, 39, 0.2);
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-tip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 28rpx;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.notice-text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
586
pages-sub/merchant/pages/order/detail.vue
Normal file
586
pages-sub/merchant/pages/order/detail.vue
Normal file
@@ -0,0 +1,586 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-order-detail">
|
||||||
|
<!-- 状态头部 -->
|
||||||
|
<view class="status-banner" :class="getStatusClass(orderDetail?.order?.orderStatus)">
|
||||||
|
<view class="status-icon">
|
||||||
|
<u-icon :name="getStatusIcon(orderDetail?.order?.orderStatus)" size="28" color="#ffffff"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="status-text-box">
|
||||||
|
<text class="status-title">{{ getStatusText(orderDetail?.order?.orderStatus) }}</text>
|
||||||
|
<view class="sn-row" @click="copySn(orderSn)">
|
||||||
|
<text class="status-sub">订单编号:{{ orderSn }}</text>
|
||||||
|
<u-icon name="file-text" size="13" color="rgba(255,255,255,0.8)"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核销码专属卡片 -->
|
||||||
|
<view class="verify-code-card" v-if="orderDetail?.order?.verificationCode || isAppointment">
|
||||||
|
<view class="card-title-row">
|
||||||
|
<text class="card-title">核销凭证</text>
|
||||||
|
<view class="code-badge">
|
||||||
|
{{ isAppointment ? '预约核销' : '自提核销' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="code-box">
|
||||||
|
<text class="code-number">{{ orderDetail?.order?.verificationCode || '--' }}</text>
|
||||||
|
<view
|
||||||
|
class="verify-now-btn"
|
||||||
|
v-if="orderDetail?.order?.orderStatus === 'TAKE'"
|
||||||
|
@click="handleDirectVerify"
|
||||||
|
>
|
||||||
|
立即核销
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 买家 / 收货信息 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-title-row">
|
||||||
|
<text class="card-title">买家信息</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">收货人/自提人:</text>
|
||||||
|
<text class="info-value">{{ orderDetail?.order?.consigneeName || '匿名买家' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">联系电话:</text>
|
||||||
|
<text class="info-value">{{ maskPhone(orderDetail?.order?.consigneeMobile) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="orderDetail?.order?.consigneeAddressPath">
|
||||||
|
<text class="info-label">地址:</text>
|
||||||
|
<text class="info-value">{{ orderDetail?.order?.consigneeAddressPath }} {{ orderDetail?.order?.consigneeDetail || '' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品清单(干净规范,拒绝裸露 JSON) -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-title-row">
|
||||||
|
<text class="card-title">商品清单</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="goods-row"
|
||||||
|
v-for="(item, idx) in (orderDetail?.orderItems || [])"
|
||||||
|
:key="item.id || item.goodsId || idx"
|
||||||
|
>
|
||||||
|
<image class="goods-img" :src="getGoodsImage(item)" mode="aspectFill"></image>
|
||||||
|
<view class="goods-detail">
|
||||||
|
<text class="goods-title">{{ getGoodsTitle(item) }}</text>
|
||||||
|
<view class="sku-tag" v-if="getCleanSpecs(item)">
|
||||||
|
<text>{{ getCleanSpecs(item) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="goods-bottom">
|
||||||
|
<text class="price">¥{{ item.unitPrice || item.goodsPrice || '0.00' }}</text>
|
||||||
|
<text class="qty">x{{ item.num || 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单财务明细 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="card-title-row">
|
||||||
|
<text class="card-title">费用明细</text>
|
||||||
|
</view>
|
||||||
|
<view class="fee-row">
|
||||||
|
<text class="fee-label">商品总价</text>
|
||||||
|
<text class="fee-val">¥{{ orderDetail?.order?.goodsPrice || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="fee-row" v-if="orderDetail?.order?.discountPrice">
|
||||||
|
<text class="fee-label">优惠金额</text>
|
||||||
|
<text class="fee-val discount">-¥{{ orderDetail?.order?.discountPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="fee-row" v-if="orderDetail?.order?.freightPrice">
|
||||||
|
<text class="fee-label">运费</text>
|
||||||
|
<text class="fee-val">+¥{{ orderDetail?.order?.freightPrice }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="fee-divider"></view>
|
||||||
|
<view class="fee-row total">
|
||||||
|
<text class="fee-label">实收金额</text>
|
||||||
|
<text class="fee-val total-price">¥{{ orderDetail?.order?.flowPrice || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单参数(支付方式中文人性化映射) -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="info-label">下单时间:</text>
|
||||||
|
<text class="info-value">{{ orderDetail?.order?.createTime || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="orderDetail?.order?.paymentTime">
|
||||||
|
<text class="info-label">支付时间:</text>
|
||||||
|
<text class="info-value">{{ orderDetail?.order?.paymentTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row" v-if="orderDetail?.order?.paymentMethod">
|
||||||
|
<text class="info-label">支付方式:</text>
|
||||||
|
<text class="info-value pay-method-val">{{ formatPaymentMethod(orderDetail?.order?.paymentMethod) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { getStoreOrderDetail, storeOrderTake } from '../../api/order.js'
|
||||||
|
import { parseGoodsImageUrl } from '@/utils/filters.js'
|
||||||
|
|
||||||
|
const orderSn = ref('')
|
||||||
|
const orderDetail = ref<any>(null)
|
||||||
|
|
||||||
|
const isAppointment = computed(() => {
|
||||||
|
return orderDetail.value?.order?.orderType === 'APPOINTMENT_GOODS'
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
if (options?.sn) {
|
||||||
|
orderSn.value = options.sn
|
||||||
|
fetchDetail()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchDetail = async () => {
|
||||||
|
uni.showLoading({ title: '加载中...' })
|
||||||
|
try {
|
||||||
|
const res: any = await getStoreOrderDetail(orderSn.value)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (res?.data && res.data.success && res.data.result) {
|
||||||
|
orderDetail.value = res.data.result
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '获取订单详情失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制单号
|
||||||
|
const copySn = (sn: string) => {
|
||||||
|
if (!sn) return
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: sn,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '单号已复制', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取商品标题
|
||||||
|
const getGoodsTitle = (item: any) => {
|
||||||
|
if (item.goodsName && item.goodsName.trim()) return item.goodsName
|
||||||
|
if (item.name && item.name.trim()) return item.name
|
||||||
|
if (orderDetail.value?.order?.orderName) return orderDetail.value.order.orderName
|
||||||
|
return '精选商品'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能提取商品图片,过滤并兼顾富文本/JSON 结构
|
||||||
|
const getGoodsImage = (item: any) => {
|
||||||
|
if (item.image) {
|
||||||
|
const parsed = parseGoodsImageUrl(item.image)
|
||||||
|
if (parsed) return parsed
|
||||||
|
}
|
||||||
|
// 若 image 为空,尝试从 specs JSON 里抓取合法的图片 url
|
||||||
|
if (item.specs && typeof item.specs === 'string') {
|
||||||
|
const match = item.specs.match(/https?:\/\/[^\s"'\\}]+\.(jpeg|jpg|png|webp|gif)/i)
|
||||||
|
if (match) return match[0]
|
||||||
|
}
|
||||||
|
return '/static/tabbar/home.png'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能提炼清洁规格,绝不裸露冗长的原始 JSON
|
||||||
|
const getCleanSpecs = (item: any) => {
|
||||||
|
const specs = item.specs
|
||||||
|
if (!specs || !String(specs).trim()) return ''
|
||||||
|
const trimmed = String(specs).trim()
|
||||||
|
|
||||||
|
// 若为 JSON 格式
|
||||||
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(trimmed)
|
||||||
|
const ignoreKeys = ['images', 'code', 'message', 'result', 'success', 'showProgress', 'url', 'status']
|
||||||
|
const pairs: string[] = []
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (!ignoreKeys.includes(k) && typeof v === 'string' && !v.startsWith('http') && v.length < 50) {
|
||||||
|
pairs.push(`${k}: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pairs.length > 0) return pairs.join(' / ')
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// 解析失败正则提取业务键值对
|
||||||
|
const matches = [...trimmed.matchAll(/"([^"]{1,20})"\s*:\s*"([^"]{1,30})"/g)]
|
||||||
|
const ignoreKeys = ['code', 'message', 'success', 'url', 'result', 'showProgress', 'status', 'name']
|
||||||
|
const pairs: string[] = []
|
||||||
|
for (const m of matches) {
|
||||||
|
const k = m[1]
|
||||||
|
const v = m[2]
|
||||||
|
if (!ignoreKeys.includes(k) && !v.startsWith('http')) {
|
||||||
|
pairs.push(`${k}: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pairs.length > 0) return pairs.join(' / ')
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 纯文本规格
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支付方式中文映射
|
||||||
|
const formatPaymentMethod = (method: string) => {
|
||||||
|
if (!method) return '--'
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
WALLET: '余额支付',
|
||||||
|
ALIPAY: '支付宝支付',
|
||||||
|
ALIPAY_PC: '支付宝支付',
|
||||||
|
ALIPAY_WAP: '支付宝支付',
|
||||||
|
ALIPAY_APP: '支付宝支付',
|
||||||
|
WECHAT: '微信支付',
|
||||||
|
WEIXIN: '微信支付',
|
||||||
|
WECHAT_MP: '微信支付',
|
||||||
|
WEIXIN_MP: '微信支付',
|
||||||
|
WX_JSAPI: '微信支付',
|
||||||
|
WX_APP: '微信支付',
|
||||||
|
WX_NATIVE: '微信支付',
|
||||||
|
WX_H5: '微信支付',
|
||||||
|
BANK_TRANSFER: '银行转账',
|
||||||
|
POINT: '积分兑换',
|
||||||
|
CASH: '现金支付',
|
||||||
|
OFFLINE: '线下支付',
|
||||||
|
FREE: '免费免付',
|
||||||
|
}
|
||||||
|
const key = String(method).toUpperCase().trim()
|
||||||
|
return map[key] || method
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskPhone = (phone: string) => {
|
||||||
|
if (!phone || phone.length < 7) return phone || ''
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
UNPAID: '待付款',
|
||||||
|
PAID: '已付款',
|
||||||
|
UNDELIVERED: '待发货',
|
||||||
|
DELIVERED: '待收货',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
CANCELLED: '已取消',
|
||||||
|
TAKE: '待核销',
|
||||||
|
}
|
||||||
|
return map[status] || status || '处理中'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
if (status === 'TAKE') return 'bg-take'
|
||||||
|
if (status === 'COMPLETED') return 'bg-done'
|
||||||
|
if (status === 'CANCELLED') return 'bg-cancel'
|
||||||
|
return 'bg-normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
if (status === 'TAKE') return 'checkmark-circle-fill'
|
||||||
|
if (status === 'COMPLETED') return 'checkmark'
|
||||||
|
if (status === 'CANCELLED') return 'close'
|
||||||
|
return 'clock'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接在详情页核销
|
||||||
|
const handleDirectVerify = async () => {
|
||||||
|
const code = orderDetail.value?.order?.verificationCode
|
||||||
|
if (!code) {
|
||||||
|
uni.showToast({ title: '该订单暂无可核销码', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认核销',
|
||||||
|
content: `确认核销此单?核销码:${code}`,
|
||||||
|
success: async (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.showLoading({ title: '正在核销...' })
|
||||||
|
try {
|
||||||
|
const takeRes: any = await storeOrderTake(orderSn.value, code)
|
||||||
|
uni.hideLoading()
|
||||||
|
if (takeRes?.data && takeRes.data.success) {
|
||||||
|
uni.showToast({ title: '核销成功', icon: 'success' })
|
||||||
|
fetchDetail()
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: takeRes?.data?.message || '核销失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
uni.hideLoading()
|
||||||
|
uni.showToast({ title: '核销异常,请稍后重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-order-detail {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f4f5f7;
|
||||||
|
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner {
|
||||||
|
padding: 44rpx 36rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.bg-take {
|
||||||
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||||
|
}
|
||||||
|
&.bg-normal {
|
||||||
|
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||||
|
}
|
||||||
|
&.bg-done {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
&.bg-cancel {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
width: 68rpx;
|
||||||
|
height: 68rpx;
|
||||||
|
border-radius: 34rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.status-title {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sn-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
|
||||||
|
.status-sub {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-code-card {
|
||||||
|
margin: -24rpx 28rpx 24rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #059669;
|
||||||
|
background: #ecfdf5;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-box {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f9fafb;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 20rpx 28rpx;
|
||||||
|
|
||||||
|
.code-number {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #10b981;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-now-btn {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 14rpx 32rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(17, 24, 39, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin: 0 28rpx 24rpx 28rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
|
.card-title-row {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
padding-bottom: 14rpx;
|
||||||
|
border-bottom: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #6b7280;
|
||||||
|
width: 180rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #111827;
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
|
||||||
|
&.pay-method-val {
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-row {
|
||||||
|
display: flex;
|
||||||
|
padding: 18rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-img {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1rpx solid #f3f4f6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-detail {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.goods-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sku-tag {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qty {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fee-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 26rpx;
|
||||||
|
margin-bottom: 14rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
|
||||||
|
.discount {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.total {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fee-divider {
|
||||||
|
height: 1rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
margin: 16rpx 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
653
pages-sub/merchant/pages/order/list.vue
Normal file
653
pages-sub/merchant/pages/order/list.vue
Normal file
@@ -0,0 +1,653 @@
|
|||||||
|
<template>
|
||||||
|
<view class="merchant-order-page">
|
||||||
|
<!-- 顶部吸顶搜索栏与 Tab -->
|
||||||
|
<view class="sticky-header">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<view class="search-box">
|
||||||
|
<view class="search-input-wrap">
|
||||||
|
<u-icon name="search" size="18" color="#9ca3af"></u-icon>
|
||||||
|
<input
|
||||||
|
class="search-input"
|
||||||
|
type="text"
|
||||||
|
v-model="keywords"
|
||||||
|
placeholder="搜索订单号 / 买家手机号"
|
||||||
|
placeholder-class="placeholder"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="onSearch"
|
||||||
|
/>
|
||||||
|
<view class="clear-btn" v-if="keywords" @click="clearSearch">
|
||||||
|
<u-icon name="close-circle-fill" size="16" color="#d1d5db"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="search-action-btn" v-if="keywords" @click="onSearch">
|
||||||
|
<text>搜索</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态横向滑动 Tab 栏(绝不折行) -->
|
||||||
|
<scroll-view class="tab-scroll" scroll-x :show-scrollbar="false" :scroll-with-animation="true">
|
||||||
|
<view class="tab-inner">
|
||||||
|
<view
|
||||||
|
class="tab-item"
|
||||||
|
v-for="item in tabList"
|
||||||
|
:key="item.key"
|
||||||
|
:class="{ active: currentTab === item.key }"
|
||||||
|
@click="switchTab(item.key)"
|
||||||
|
>
|
||||||
|
<text class="tab-label">{{ item.label }}</text>
|
||||||
|
<view class="indicator" v-if="currentTab === item.key"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单列表主体 -->
|
||||||
|
<view class="order-list-body">
|
||||||
|
<!-- 列表数据 -->
|
||||||
|
<view
|
||||||
|
class="order-card"
|
||||||
|
v-for="order in orderList"
|
||||||
|
:key="order.sn"
|
||||||
|
@click="goToDetail(order.sn)"
|
||||||
|
>
|
||||||
|
<!-- 头部:单号与状态 -->
|
||||||
|
<view class="card-head">
|
||||||
|
<view class="sn-box" @click.stop="copySn(order.sn)">
|
||||||
|
<text class="order-sn">单号:{{ order.sn }}</text>
|
||||||
|
<u-icon name="file-text" size="14" color="#9ca3af"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="status-badge" :class="getStatusClass(order.orderStatus)">
|
||||||
|
{{ getStatusText(order.orderStatus) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 商品信息 -->
|
||||||
|
<view class="goods-container">
|
||||||
|
<view
|
||||||
|
class="goods-item"
|
||||||
|
v-for="goods in (order.orderItems || [])"
|
||||||
|
:key="goods.id || goods.goodsId"
|
||||||
|
>
|
||||||
|
<image class="goods-thumb" :src="goods.image || '/static/tabbar/home.png'" mode="aspectFill"></image>
|
||||||
|
<view class="goods-info">
|
||||||
|
<text class="goods-name">{{ getGoodsName(goods, order) }}</text>
|
||||||
|
<view class="spec-pill" v-if="getSpecText(goods)">
|
||||||
|
<text>{{ getSpecText(goods) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="price-qty-row">
|
||||||
|
<text class="goods-price">¥{{ goods.unitPrice || goods.goodsPrice || '0.00' }}</text>
|
||||||
|
<text class="goods-qty">x{{ goods.num || 1 }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 订单金额与买家信息 -->
|
||||||
|
<view class="card-foot">
|
||||||
|
<view class="buyer-info" v-if="order.consigneeMobile || order.memberPhone">
|
||||||
|
<u-icon name="account" size="14" color="#9ca3af"></u-icon>
|
||||||
|
<text class="buyer-tel">{{ maskPhone(order.consigneeMobile || order.memberPhone) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="buyer-info" v-else>
|
||||||
|
<text class="order-type-tag" v-if="order.orderType === 'APPOINTMENT_GOODS'">预约商品单</text>
|
||||||
|
<text class="order-type-tag" v-else-if="order.verificationCode">自提单</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="total-bar">
|
||||||
|
<text class="total-label">实付:</text>
|
||||||
|
<text class="total-symbol">¥</text>
|
||||||
|
<text class="total-price">{{ order.flowPrice || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 核销专属快捷动作 -->
|
||||||
|
<view class="card-actions" v-if="order.verificationCode || order.orderType === 'APPOINTMENT_GOODS' || order.orderStatus === 'TAKE'">
|
||||||
|
<view class="action-btn verify" @click.stop="quickVerify(order)">
|
||||||
|
<u-icon name="scan" size="15" color="#ffffff"></u-icon>
|
||||||
|
<text class="btn-lbl">扫码/输码核销</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-state" v-if="!loading && orderList.length === 0">
|
||||||
|
<u-icon name="order" size="64" color="#d1d5db"></u-icon>
|
||||||
|
<text class="empty-text">暂无相关订单记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载提示 -->
|
||||||
|
<view class="loading-more" v-if="loading">
|
||||||
|
<text>正在加载订单数据...</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-more" v-else-if="finished && orderList.length > 0">
|
||||||
|
<text>— 没有更多了 —</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { getStoreOrderList } from '../../api/order.js'
|
||||||
|
|
||||||
|
const keywords = ref('')
|
||||||
|
const currentTab = ref('ALL')
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const pageNumber = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const orderList = ref<any[]>([])
|
||||||
|
|
||||||
|
// 优化 Tab 标题,保持精练统一、绝不换行
|
||||||
|
const tabList = [
|
||||||
|
{ key: 'ALL', label: '全部' },
|
||||||
|
{ key: 'TAKE', label: '待核销' },
|
||||||
|
{ key: 'UNDELIVERED', label: '待发货' },
|
||||||
|
{ key: 'DELIVERED', label: '待收货' },
|
||||||
|
{ key: 'COMPLETED', label: '已完成' },
|
||||||
|
]
|
||||||
|
|
||||||
|
onLoad((options: any) => {
|
||||||
|
if (options?.tab) {
|
||||||
|
currentTab.value = options.tab
|
||||||
|
}
|
||||||
|
fetchOrders(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(async () => {
|
||||||
|
await fetchOrders(true)
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!finished.value && !loading.value) {
|
||||||
|
pageNumber.value++
|
||||||
|
fetchOrders(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const fetchOrders = async (reset = false) => {
|
||||||
|
if (reset) {
|
||||||
|
pageNumber.value = 1
|
||||||
|
orderList.value = []
|
||||||
|
finished.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const params: any = {
|
||||||
|
pageNumber: pageNumber.value,
|
||||||
|
pageSize: pageSize.value,
|
||||||
|
keywords: keywords.value.trim() || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab.value !== 'ALL') {
|
||||||
|
params.orderStatus = currentTab.value
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await getStoreOrderList(params)
|
||||||
|
loading.value = false
|
||||||
|
|
||||||
|
if (res?.data && res.data.success) {
|
||||||
|
const records = res.data.result?.records || []
|
||||||
|
if (reset) {
|
||||||
|
orderList.value = records
|
||||||
|
} else {
|
||||||
|
orderList.value = [...orderList.value, ...records]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length < pageSize.value) {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res?.data?.message || '获取订单列表失败',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
loading.value = false
|
||||||
|
uni.showToast({ title: '网络连接异常', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchTab = (key: string) => {
|
||||||
|
if (currentTab.value === key) return
|
||||||
|
currentTab.value = key
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
keywords.value = ''
|
||||||
|
fetchOrders(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToDetail = (sn: string) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-sub/merchant/pages/order/detail?sn=${sn}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快捷核销
|
||||||
|
const quickVerify = (order: any) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages-sub/merchant/pages/order/detail?sn=${order.sn}&autoVerify=1`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制单号
|
||||||
|
const copySn = (sn: string) => {
|
||||||
|
if (!sn) return
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: sn,
|
||||||
|
success: () => {
|
||||||
|
uni.showToast({ title: '单号已复制', icon: 'none' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取商品名(容错兜底)
|
||||||
|
const getGoodsName = (goods: any, order: any) => {
|
||||||
|
if (goods.goodsName && goods.goodsName.trim()) return goods.goodsName
|
||||||
|
if (goods.name && goods.name.trim()) return goods.name
|
||||||
|
if (order.orderName && order.orderName.trim()) return order.orderName
|
||||||
|
if (goods.specs && goods.specs.trim()) return goods.specs
|
||||||
|
return '精选商品'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取规格文本(智能剔除多余字段)
|
||||||
|
const getSpecText = (goods: any) => {
|
||||||
|
const specs = goods.specs
|
||||||
|
if (!specs || !String(specs).trim()) return ''
|
||||||
|
if (specs === goods.goodsName) return ''
|
||||||
|
|
||||||
|
// 处理 JSON 字符串形式
|
||||||
|
if (typeof specs === 'string' && (specs.startsWith('{') || specs.startsWith('['))) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(specs)
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const values = Object.values(obj).filter(Boolean)
|
||||||
|
if (values.length) return values.join(' / ')
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return String(specs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号脱敏
|
||||||
|
const maskPhone = (phone: string) => {
|
||||||
|
if (!phone) return ''
|
||||||
|
const str = String(phone).trim()
|
||||||
|
if (str.length === 11) {
|
||||||
|
return str.substring(0, 3) + '****' + str.substring(7)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态文字映射
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
UNPAID: '待付款',
|
||||||
|
PAID: '已付款',
|
||||||
|
UNDELIVERED: '待发货',
|
||||||
|
DELIVERED: '待收货',
|
||||||
|
COMPLETED: '已完成',
|
||||||
|
CANCELLED: '已取消',
|
||||||
|
TAKE: '待核销',
|
||||||
|
}
|
||||||
|
return map[status] || status || '处理中'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态样式类
|
||||||
|
const getStatusClass = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'TAKE':
|
||||||
|
return 'status-take'
|
||||||
|
case 'COMPLETED':
|
||||||
|
return 'status-done'
|
||||||
|
case 'CANCELLED':
|
||||||
|
return 'status-cancel'
|
||||||
|
default:
|
||||||
|
return 'status-normal'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.merchant-order-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f4f5f7;
|
||||||
|
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
padding: 16rpx 28rpx 10rpx 28rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
|
||||||
|
.search-input-wrap {
|
||||||
|
flex: 1;
|
||||||
|
height: 72rpx;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
padding: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-action-btn {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-scroll {
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.tab-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
min-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 130rpx;
|
||||||
|
text-align: center;
|
||||||
|
padding: 22rpx 12rpx;
|
||||||
|
position: relative;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.tab-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.tab-label {
|
||||||
|
color: #111827;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4rpx;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 44rpx;
|
||||||
|
height: 6rpx;
|
||||||
|
background: #111827;
|
||||||
|
border-radius: 3rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-list-body {
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
|
||||||
|
.order-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
|
border-bottom: 1rpx solid #f3f4f6;
|
||||||
|
|
||||||
|
.sn-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.order-sn {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
|
||||||
|
&.status-take {
|
||||||
|
background: #ecfdf5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
&.status-done {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
&.status-cancel {
|
||||||
|
background: #f9fafb;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
&.status-normal {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #ea580c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-container {
|
||||||
|
padding: 10rpx 0;
|
||||||
|
|
||||||
|
.goods-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 16rpx 0;
|
||||||
|
border-bottom: 1rpx solid #fafafa;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-thumb {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 14rpx;
|
||||||
|
background: #f9fafb;
|
||||||
|
border: 1rpx solid #f3f4f6;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.goods-name {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spec-pill {
|
||||||
|
margin-top: 6rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-qty-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: baseline;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
|
||||||
|
.goods-price {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-qty {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-foot {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
border-top: 1rpx solid #f9fafb;
|
||||||
|
|
||||||
|
.buyer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
|
||||||
|
.buyer-tel {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-type-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.total-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-price {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ef4444;
|
||||||
|
margin-left: 2rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-top: 20rpx;
|
||||||
|
margin-top: 16rpx;
|
||||||
|
border-top: 1rpx dashed #e5e7eb;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
padding: 14rpx 28rpx;
|
||||||
|
border-radius: 36rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.verify {
|
||||||
|
background: #111827;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(17, 24, 39, 0.15);
|
||||||
|
|
||||||
|
.btn-lbl {
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 0;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more,
|
||||||
|
.no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
155
pages-sub/merchant/utils/storeRequest.js
Normal file
155
pages-sub/merchant/utils/storeRequest.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import api from "@/config/api.js";
|
||||||
|
import Request from "@/lib/request/index.js";
|
||||||
|
import Foundation from "@/utils/Foundation.js";
|
||||||
|
import { md5 } from "@/utils/md5.js";
|
||||||
|
import storage from "@/utils/storage.js";
|
||||||
|
import uuid from "@/utils/uuid.modified.js";
|
||||||
|
|
||||||
|
let isStoreNavigateTo = false;
|
||||||
|
|
||||||
|
function cleanStoreAuth() {
|
||||||
|
uni.showToast({
|
||||||
|
title: "商家登录状态已过期,请重新登录",
|
||||||
|
icon: "none",
|
||||||
|
duration: 1800,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.clearStoreAuth();
|
||||||
|
|
||||||
|
if (!isStoreNavigateTo) {
|
||||||
|
isStoreNavigateTo = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
isStoreNavigateTo = false;
|
||||||
|
uni.reLaunch({
|
||||||
|
url: "/pages-sub/merchant/pages/login/index",
|
||||||
|
});
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeHttp = new Request();
|
||||||
|
|
||||||
|
const ensureUuid = () => {
|
||||||
|
if (!storage.getUuid()) {
|
||||||
|
storage.setUuid(uuid.v1());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
storeHttp.setConfig((config) => {
|
||||||
|
ensureUuid();
|
||||||
|
config.baseURL = api.store;
|
||||||
|
config.header = {
|
||||||
|
...config.header,
|
||||||
|
};
|
||||||
|
config.validateStatus = () => true;
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
storeHttp.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
ensureUuid();
|
||||||
|
const token = storage.getStoreAccessToken();
|
||||||
|
if (token) {
|
||||||
|
const nonce = Foundation.randomString(6);
|
||||||
|
const timestamp = parseInt(new Date().getTime() / 1000);
|
||||||
|
const sign = md5(nonce + timestamp + token);
|
||||||
|
const _params = { nonce, timestamp, sign };
|
||||||
|
config.params = { ...(config.params || {}), ..._params };
|
||||||
|
config.header.accessToken = token;
|
||||||
|
}
|
||||||
|
config.header = {
|
||||||
|
...config.header,
|
||||||
|
uuid: storage.getUuid(),
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(config) => Promise.reject(config)
|
||||||
|
);
|
||||||
|
|
||||||
|
let isStoreRefreshing = false;
|
||||||
|
let storeRequests = [];
|
||||||
|
|
||||||
|
function refreshTokenApi(refreshToken) {
|
||||||
|
return uni.request({
|
||||||
|
url: `${api.store}/passport/login/refresh/${refreshToken}`,
|
||||||
|
method: "GET",
|
||||||
|
header: {
|
||||||
|
uuid: storage.getUuid(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
storeHttp.interceptors.response.use(
|
||||||
|
async (response) => {
|
||||||
|
isStoreNavigateTo = false;
|
||||||
|
try {
|
||||||
|
uni.hideLoading();
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
const token = storage.getStoreAccessToken();
|
||||||
|
const refreshToken = storage.getStoreRefreshToken();
|
||||||
|
|
||||||
|
// 403 / 401 并且有 refreshToken,尝试刷新
|
||||||
|
if (
|
||||||
|
token &&
|
||||||
|
refreshToken &&
|
||||||
|
(response.statusCode === 403 || response.statusCode === 401 || response.data?.code === 403)
|
||||||
|
) {
|
||||||
|
if (!isStoreRefreshing) {
|
||||||
|
isStoreRefreshing = true;
|
||||||
|
storage.removeStoreAccessToken();
|
||||||
|
try {
|
||||||
|
const [err, res] = await refreshTokenApi(refreshToken);
|
||||||
|
const data = res?.data || err?.data;
|
||||||
|
if (data && data.success && data.result) {
|
||||||
|
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = data.result;
|
||||||
|
storage.setStoreAccessToken(newAccessToken);
|
||||||
|
if (newRefreshToken) {
|
||||||
|
storage.setStoreRefreshToken(newRefreshToken);
|
||||||
|
}
|
||||||
|
response.header = response.header || {};
|
||||||
|
response.header.accessToken = `${newAccessToken}`;
|
||||||
|
storeRequests.forEach((cb) => cb(newAccessToken));
|
||||||
|
storeRequests = [];
|
||||||
|
return storeHttp.request(response.config);
|
||||||
|
} else {
|
||||||
|
cleanStoreAuth();
|
||||||
|
return Promise.reject(data);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
cleanStoreAuth();
|
||||||
|
return Promise.reject(e);
|
||||||
|
} finally {
|
||||||
|
isStoreRefreshing = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
storeRequests.push((newToken) => {
|
||||||
|
response.header = response.header || {};
|
||||||
|
response.header.accessToken = `${newToken}`;
|
||||||
|
resolve(storeHttp.request(response.config));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
(!token && (response.statusCode === 403 || response.statusCode === 401)) ||
|
||||||
|
response.data?.code === 403 ||
|
||||||
|
response.data?.code === 401
|
||||||
|
) {
|
||||||
|
cleanStoreAuth();
|
||||||
|
} else if ((response.statusCode === 200 && response.data && !response.data.success) || response.statusCode === 400) {
|
||||||
|
if (response.data?.message) {
|
||||||
|
uni.showToast({
|
||||||
|
title: response.data.message,
|
||||||
|
icon: "none",
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => error
|
||||||
|
);
|
||||||
|
|
||||||
|
export { storeHttp };
|
||||||
|
export default storeHttp;
|
||||||
92
pages.json
92
pages.json
@@ -106,9 +106,77 @@
|
|||||||
],
|
],
|
||||||
"subPackages": [
|
"subPackages": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"root": "pages-sub/merchant",
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商家工作台",
|
||||||
|
"navigationBarBackgroundColor": "#111827",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商家登录",
|
||||||
|
"navigationBarBackgroundColor": "#111827",
|
||||||
|
"navigationBarTextStyle": "white"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单核销管理",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/order/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "订单详情",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/goods/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商品管理",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/goods/detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "商品详情",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/evaluation/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "评价管理",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"root": "pages/mine",
|
"root": "pages/mine",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
{"path":"memberBenefits","style":{"navigationBarTitleText":"会员权益","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
{"path":"paidMember","style":{"navigationBarTitleText":"付费会员","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
{"path":"paidMemberRecords","style":{"navigationBarTitleText":"会员购买记录","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
{"path":"experience","style":{"navigationBarTitleText":"我的经验","enablePullDownRefresh":true,"navigationBarBackgroundColor":"#f5f6f9","navigationBarTextStyle":"black"}},
|
||||||
|
|
||||||
{
|
{
|
||||||
"path": "signIn",
|
"path": "signIn",
|
||||||
@@ -447,6 +515,13 @@
|
|||||||
"navigationBarTitleText": "设置"
|
"navigationBarTitleText": "设置"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "set/messageNotice",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "消息通知",
|
||||||
|
"navigationStyle": "custom"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "set/personMsg",
|
"path": "set/personMsg",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -471,7 +546,8 @@
|
|||||||
}, {
|
}, {
|
||||||
"path": "point/myPoint",
|
"path": "point/myPoint",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的积分"
|
"navigationBarTitleText": "我的积分",
|
||||||
|
"navigationStyle": "custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -772,7 +848,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "gift-card/myGiftCards",
|
"path": "gift-card/pickupCard",
|
||||||
|
"style": { "navigationBarTitleText": "提货卡兑换" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "gift-card/myGiftCards",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的礼品卡"
|
"navigationBarTitleText": "我的礼品卡"
|
||||||
}
|
}
|
||||||
@@ -926,6 +1006,7 @@
|
|||||||
{
|
{
|
||||||
"path": "afterSales/applySuccess",
|
"path": "afterSales/applySuccess",
|
||||||
"style": {
|
"style": {
|
||||||
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "提交成功",
|
"navigationBarTitleText": "提交成功",
|
||||||
"app-plus": {
|
"app-plus": {
|
||||||
"bounce": "none",
|
"bounce": "none",
|
||||||
@@ -957,13 +1038,6 @@
|
|||||||
"navigationBarTitleText": "申请售后"
|
"navigationBarTitleText": "申请售后"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "afterSales/afterSales",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "售后管理",
|
|
||||||
"enablePullDownRefresh": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "fillorder",
|
"path": "fillorder",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<view class="coupon-card-right">
|
<view class="coupon-card-right">
|
||||||
<view class="coupon-info">
|
<view class="coupon-info">
|
||||||
<text class="coupon-card-name">{{ item.storeName == 'platform' ? '全平台' : item.storeName + '店铺' }}使用</text>
|
<text class="coupon-card-name">{{ item.storeName == 'platform' ? '全平台' : item.storeName + '店铺' }}使用</text>
|
||||||
<text class="coupon-card-desc">满{{unitPrice(item.consumeThreshold) }}元可用</text>
|
<text class="coupon-card-desc">{{ item.consumeThreshold > 0 ? '满' + unitPrice(item.consumeThreshold) + '元可用' : '无门槛使用' }}</text>
|
||||||
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime.split(" ")[0] }}</text>
|
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime.split(" ")[0] }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="coupon-claim-btn" @click="receive(item)">
|
<view class="coupon-claim-btn" @click="receive(item)">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<view class="title">{{ coupon.title }}</view>
|
<view class="title">{{ coupon.title }}</view>
|
||||||
<view class="price" v-if="coupon.couponType == 'PRICE'"><text>¥</text>{{ unitPrice(coupon.price) }}</view>
|
<view class="price" v-if="coupon.couponType == 'PRICE'"><text>¥</text>{{ unitPrice(coupon.price) }}</view>
|
||||||
<view class="price" v-if="coupon.couponType == 'DISCOUNT'">{{ coupon.discount }}折</view>
|
<view class="price" v-if="coupon.couponType == 'DISCOUNT'">{{ coupon.discount }}折</view>
|
||||||
<view class="text">满{{ coupon.consumeThreshold }}元可用</view>
|
<view class="text">{{ coupon.consumeThreshold > 0 ? '满' + coupon.consumeThreshold + '元可用' : '无门槛使用' }}</view>
|
||||||
<view class="bg-quan">券</view>
|
<view class="bg-quan">券</view>
|
||||||
<view class="jiao-1" :class="{ 'used-color': coupon.used_status != 0 }">
|
<view class="jiao-1" :class="{ 'used-color': coupon.used_status != 0 }">
|
||||||
<text class="text-1">{{ coupon.used_status == 0 ? '新到' : coupon.used_status_text }}</text>
|
<text class="text-1">{{ coupon.used_status == 0 ? '新到' : coupon.used_status_text }}</text>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
</template>
|
</template>
|
||||||
使用
|
使用
|
||||||
</text>
|
</text>
|
||||||
<text class="coupon-card-desc">满{{ unitPrice(item.consumeThreshold) }}元可用</text>
|
<text class="coupon-card-desc">{{ item.consumeThreshold > 0 ? '满' + unitPrice(item.consumeThreshold) + '元可用' : '无门槛使用' }}</text>
|
||||||
<text class="coupon-card-time" v-if="item.reason">{{ item.reason }}</text>
|
<text class="coupon-card-time" v-if="item.reason">{{ item.reason }}</text>
|
||||||
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime }}</text>
|
<text class="coupon-card-time" v-if="item.endTime">有效期至:{{ item.endTime }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@
|
|||||||
<view class="coupon-card-right">
|
<view class="coupon-card-right">
|
||||||
<view class="coupon-info">
|
<view class="coupon-info">
|
||||||
<text class="coupon-card-name">{{ coupon.title }}</text>
|
<text class="coupon-card-name">{{ coupon.title }}</text>
|
||||||
|
<text class="coupon-card-desc">{{ coupon.consumeThreshold > 0 ? '满' + unitPrice(coupon.consumeThreshold) + '元可用' : '无门槛使用' }}</text>
|
||||||
<text class="coupon-card-desc">使用范围:{{
|
<text class="coupon-card-desc">使用范围:{{
|
||||||
coupon.scopeType == "ALL" && coupon.storeId == "0"
|
coupon.scopeType == "ALL" && coupon.storeId == "0"
|
||||||
? "全平台"
|
? "全平台"
|
||||||
|
|||||||
@@ -52,17 +52,18 @@
|
|||||||
<view class="gcc-header-inner">
|
<view class="gcc-header-inner">
|
||||||
<view class="gcc-header-left">
|
<view class="gcc-header-left">
|
||||||
<view class="gcc-title">{{ item.giftCardName || '礼品卡' }}</view>
|
<view class="gcc-title">{{ item.giftCardName || '礼品卡' }}</view>
|
||||||
<view class="gcc-face">面值{{ item.faceText }}元</view>
|
<view class="gcc-face">{{ item.cardType === 'PICKUP' ? '兑换 ' + item.pickupCount + ' 种商品' : '面值' + item.faceText + '元' }}</view>
|
||||||
<view class="gcc-cardno">{{ item.cardNo }}</view>
|
<view class="gcc-cardno">{{ item.cardNo }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="gcc-header-right">
|
<view class="gcc-header-right">
|
||||||
<view class="gcc-type">现金卡</view>
|
<view class="gcc-type">{{ item.cardType === 'PICKUP' ? '提货卡' : '现金卡' }}</view>
|
||||||
<view class="gcc-expire">{{ item.expireText }}</view>
|
<view class="gcc-expire">{{ item.expireText }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="gcc-body">
|
<view class="gcc-body">
|
||||||
<view class="gcc-row">
|
<view v-if="item.cardType === 'PICKUP'" class="gcc-row">每种一件 · 一次兑完 · 免邮</view>
|
||||||
|
<view v-else class="gcc-row">
|
||||||
当前余额:
|
当前余额:
|
||||||
<text class="gcc-strong" v-if="item.balance != null">¥{{ unitPrice(item.balance) }}</text>
|
<text class="gcc-strong" v-if="item.balance != null">¥{{ unitPrice(item.balance) }}</text>
|
||||||
<text class="gcc-strong" v-else>—</text>
|
<text class="gcc-strong" v-else>—</text>
|
||||||
@@ -236,7 +237,8 @@ function onCardPrimaryAction(item: any) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (statusTab.value === GIFT_CARD_STATUS.AVAILABLE) {
|
if (statusTab.value === GIFT_CARD_STATUS.AVAILABLE) {
|
||||||
uni.switchTab({ url: '/pages/tabbar/home/index' })
|
if (item.cardType === 'PICKUP') uni.navigateTo({ url: '/pages/cart/gift-card/pickupCard?id=' + encodeURIComponent(resolveMemberCardId(item)) })
|
||||||
|
else uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
118
pages/cart/gift-card/pickupCard.vue
Normal file
118
pages/cart/gift-card/pickupCard.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<!--
|
||||||
|
提货卡选品及确认兑换,兼容 H5、App 和微信小程序。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-05
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<view class="pickup-page">
|
||||||
|
<view class="notice">每张卡一次兑完,每种一件,免邮。兑换成功后取消不退权益,不支持退款或退货。</view>
|
||||||
|
<view v-if="loading" class="state">加载中...</view>
|
||||||
|
<view v-else-if="error" class="state" @click="load">{{ error }},点击重试</view>
|
||||||
|
<template v-else-if="detail.credentialId">
|
||||||
|
<view class="title">{{ detail.giftCardName }}</view>
|
||||||
|
<view v-if="completed || detail.tradeSn" class="state">该提货卡已兑换<button @click="viewOrders">查看订单</button></view>
|
||||||
|
<template v-else>
|
||||||
|
<view class="rule">{{ detail.pickupMode === 'ALL' ? '兑换以下全部商品' : '任选 ' + detail.pickupCount + ' 种商品' }} · 已选 {{ selected.length }} 种</view>
|
||||||
|
<view v-for="goods in detail.goods" :key="goods.id" class="goods" :class="{ selected: selected.includes(goods.id), disabled: !goods.available }" @click="toggle(goods)">
|
||||||
|
<image :src="goods.thumbnail" mode="aspectFill" />
|
||||||
|
<view class="goods-info"><view>{{ goods.goodsName }}</view><view class="muted">{{ specsText(goods.specs) }}</view><view class="muted">{{ goods.storeName }} · 1 件</view><text v-if="!goods.available">暂不可兑换</text></view>
|
||||||
|
<text>{{ selected.includes(goods.id) ? '●' : '○' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="address">
|
||||||
|
<view class="title">收货地址</view>
|
||||||
|
<picker :range="addresses" :range-key="'label'" :value="addressIndex" :disabled="busy" @change="chooseAddress"><view>{{ addresses[addressIndex]?.label || '请选择收货地址' }} ›</view></picker>
|
||||||
|
<view class="links"><text @click="manageAddress">管理收货地址</text><text @click="loadAddresses">刷新地址</text></view>
|
||||||
|
</view>
|
||||||
|
<button class="submit" :loading="busy" :disabled="!canSubmit || busy" @click="submit">确认兑换 · 实付 ¥0</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
|
||||||
|
import { getAddressList } from '@/api/address.js'
|
||||||
|
import { getPickupCard, previewPickupCard, redeemPickupCard } from '@/api/pickupCard.js'
|
||||||
|
import storage from '@/utils/storage.js'
|
||||||
|
|
||||||
|
const detail = ref({}), selected = ref([]), addresses = ref([]), addressIndex = ref(0)
|
||||||
|
const loading = ref(false), busy = ref(false), error = ref(''), completed = ref(false)
|
||||||
|
let cardId = '', active = true, version = 0, owner = ''
|
||||||
|
const canSubmit = computed(() => selected.value.length === detail.value.pickupCount && !!addresses.value[addressIndex.value] && !detail.value.goods?.some(g => selected.value.includes(g.id) && !g.available))
|
||||||
|
/** 隔离账号切换和离页后的异步响应。 */
|
||||||
|
function sameOwner() { return active && owner === String(storage.getUserInfo()?.id || '') }
|
||||||
|
/** 将既有 SKU 规格转成可读文字。 */
|
||||||
|
function specsText(value) { try { const specs = JSON.parse(value || '[]'); return Array.isArray(specs) ? specs.filter(s => s.specName !== 'images').map(s => s.specValue).join(' / ') : '' } catch { return value || '' } }
|
||||||
|
/** 非全选模式可增减选项,数量仍由后端再次校验。 */
|
||||||
|
function toggle(goods) {
|
||||||
|
if (busy.value || !goods.available || detail.value.pickupMode === 'ALL') return
|
||||||
|
if (selected.value.includes(goods.id)) selected.value = selected.value.filter(id => id !== goods.id)
|
||||||
|
else if (detail.value.pickupCount === 1) selected.value = [goods.id]
|
||||||
|
else if (selected.value.length < detail.value.pickupCount) selected.value = [...selected.value, goods.id]
|
||||||
|
else uni.showToast({ title: '已达到可兑换种数', icon: 'none' })
|
||||||
|
}
|
||||||
|
/** 读取当前会员地址并保留有效选项。 */
|
||||||
|
async function loadAddresses() {
|
||||||
|
const previous = addresses.value[addressIndex.value]?.id
|
||||||
|
const response = await getAddressList(1, 100)
|
||||||
|
if (!sameOwner()) return
|
||||||
|
if (!response.data.success) throw new Error(response.data.message || '地址加载失败')
|
||||||
|
addresses.value = (response.data.result?.records || []).map(a => ({ ...a, label: `${a.name} ${a.mobile} ${a.consigneeAddressPath || ''} ${a.detail || ''}` }))
|
||||||
|
const index = addresses.value.findIndex(a => a.id === previous)
|
||||||
|
addressIndex.value = index >= 0 ? index : Math.max(0, addresses.value.findIndex(a => a.isDefault))
|
||||||
|
}
|
||||||
|
/** 刷新卡可用性;返回页面重新校验已选商品。 */
|
||||||
|
async function load() {
|
||||||
|
const request = ++version
|
||||||
|
loading.value = true; error.value = ''
|
||||||
|
try {
|
||||||
|
const response = await getPickupCard(cardId)
|
||||||
|
if (!sameOwner() || request !== version) return
|
||||||
|
if (!response.data.success) throw new Error(response.data.message || '卡片加载失败')
|
||||||
|
detail.value = response.data.result
|
||||||
|
selected.value = detail.value.pickupMode === 'ALL' ? detail.value.goods.map(g => g.id) : selected.value.filter(id => detail.value.goods.some(g => g.id === id && g.available))
|
||||||
|
await loadAddresses()
|
||||||
|
} catch (e) { if (sameOwner() && request === version) error.value = e.message || '加载失败' }
|
||||||
|
finally { if (sameOwner() && request === version) loading.value = false }
|
||||||
|
}
|
||||||
|
/** 提交前服务端试算;确认后核销,网络错误允许幂等重试。 */
|
||||||
|
async function submit() {
|
||||||
|
if (busy.value || !canSubmit.value || !sameOwner()) return
|
||||||
|
busy.value = true
|
||||||
|
const payload = { credentialId: cardId, skuIds: [...selected.value], addressId: addresses.value[addressIndex.value].id }
|
||||||
|
try {
|
||||||
|
const preview = await previewPickupCard(payload)
|
||||||
|
if (!sameOwner()) return
|
||||||
|
if (!preview.data.success) throw new Error(preview.data.message || '兑换校验失败')
|
||||||
|
const confirmed = await new Promise(resolve => uni.showModal({ title: '确认兑换', content: '每种一件,免邮且实付为零。兑换后取消不退权益,不支持退款或退货。', confirmText: '确认兑换', success: result => resolve(result.confirm), fail: () => resolve(false) }))
|
||||||
|
if (!confirmed || !sameOwner()) return
|
||||||
|
const response = await redeemPickupCard(payload)
|
||||||
|
if (!sameOwner()) return
|
||||||
|
if (!response.data.success) throw new Error(response.data.message || '兑换失败')
|
||||||
|
completed.value = true
|
||||||
|
} catch (e) {
|
||||||
|
if (sameOwner()) {
|
||||||
|
try { const status = await getPickupCard(cardId); if (sameOwner() && status.data.success && status.data.result?.tradeSn) { detail.value = status.data.result; completed.value = true; return } } catch { /* 保留选项以便重试 */ }
|
||||||
|
uni.showToast({ title: e.message || '结果待确认,请重试或查看订单', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { if (sameOwner()) busy.value = false }
|
||||||
|
}
|
||||||
|
/** 地址选择仅改变本页状态,不修改地址本身。 */
|
||||||
|
function chooseAddress(event) { addressIndex.value = Number(event.detail.value) }
|
||||||
|
/** 复用现有地址管理,返回时刷新。 */
|
||||||
|
function manageAddress() { if (!busy.value) uni.navigateTo({ url: '/pages/mine/address/addressManage' }) }
|
||||||
|
/** 兑换订单沿用普通订单列表与物流详情。 */
|
||||||
|
function viewOrders() { uni.navigateTo({ url: '/pages/order/myOrder?status=0' }) }
|
||||||
|
onLoad(options => { cardId = options.id || ''; owner = String(storage.getUserInfo()?.id || '') })
|
||||||
|
onShow(() => {
|
||||||
|
if (owner !== String(storage.getUserInfo()?.id || '')) { detail.value = {}; selected.value = []; addresses.value = []; error.value = '账号已变更,请返回我的礼品卡'; return }
|
||||||
|
load()
|
||||||
|
})
|
||||||
|
onUnload(() => { active = false; version++ })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pickup-page{min-height:100vh;background:#f6f7f9;padding:24rpx;box-sizing:border-box;padding-bottom:calc(40rpx + env(safe-area-inset-bottom))}.notice{background:#fff5e8;color:#965b24;padding:24rpx;border-radius:16rpx;font-size:26rpx;line-height:1.7}.title{font-weight:600;font-size:32rpx;margin:28rpx 0 20rpx}.rule,.muted{color:#777;font-size:24rpx;margin:12rpx 0}.goods{display:flex;align-items:center;gap:20rpx;background:#fff;border:2rpx solid transparent;border-radius:16rpx;padding:24rpx;margin:16rpx 0}.goods.selected{border-color:#f56c36}.goods.disabled{opacity:.5}.goods image{width:130rpx;height:130rpx;border-radius:12rpx;flex-shrink:0}.goods-info{flex:1;font-size:28rpx}.address{background:#fff;padding:4rpx 24rpx 24rpx;border-radius:16rpx;font-size:26rpx}.links{display:flex;justify-content:space-between;color:#e75d31;margin-top:24rpx}.submit{background:#e75d31;color:#fff;margin-top:32rpx;border-radius:48rpx;font-size:30rpx}.state{text-align:center;padding:60rpx 20rpx}
|
||||||
|
</style>
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, getCurrentInstance, onMounted, computed } from 'vue'
|
import { ref, getCurrentInstance, onMounted, computed } from 'vue'
|
||||||
import { onLoad, onBackPress } from '@dcloudio/uni-app'
|
import { onLoad, onBackPress, onUnload } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import * as API_Trade from '@/api/trade'
|
import * as API_Trade from '@/api/trade'
|
||||||
import { payCallback } from '@/api/members'
|
import { payCallback } from '@/api/members'
|
||||||
@@ -88,6 +88,8 @@ const paymentType = ref('')
|
|||||||
const paymentClient = ref('')
|
const paymentClient = ref('')
|
||||||
const walletValue = ref(0)
|
const walletValue = ref(0)
|
||||||
const autoCancelTime = ref(0)
|
const autoCancelTime = ref(0)
|
||||||
|
let h5PollTimer: ReturnType<typeof setInterval> | undefined
|
||||||
|
onUnload(() => { if (h5PollTimer) clearInterval(h5PollTimer) })
|
||||||
|
|
||||||
onLoad((val) => {
|
onLoad((val) => {
|
||||||
routerVal.value = val || {}
|
routerVal.value = val || {}
|
||||||
@@ -107,7 +109,9 @@ onLoad((val) => {
|
|||||||
|
|
||||||
onBackPress((e) => {
|
onBackPress((e) => {
|
||||||
if (e.from == 'backbutton') {
|
if (e.from == 'backbutton') {
|
||||||
if (routerVal.value.recharge_sn) {
|
if (routerVal.value.paid_member_sn) {
|
||||||
|
uni.redirectTo({ url: '/pages/mine/paidMemberRecords' })
|
||||||
|
} else if (routerVal.value.recharge_sn) {
|
||||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||||
} else {
|
} else {
|
||||||
uni.navigateTo({ url: '/pages/order/myOrder?status=0' })
|
uni.navigateTo({ url: '/pages/order/myOrder?status=0' })
|
||||||
@@ -126,21 +130,33 @@ function hideLoadingIfNeeded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function callback(paymentMethod: string) {
|
function callback(paymentMethod: string) {
|
||||||
uni.redirectTo({
|
if (orderType.value === 'PAID_MEMBER') {
|
||||||
url:
|
uni.redirectTo({ url: '/pages/mine/paidMember?payment_sn=' + encodeURIComponent(sn.value) })
|
||||||
'/pages/cart/payment/success?paymentMethod=' +
|
return
|
||||||
paymentMethod +
|
}
|
||||||
'&payPrice=' +
|
const goSuccess = () => {
|
||||||
cashierParams.value.price +
|
uni.redirectTo({
|
||||||
'&orderType=' +
|
url:
|
||||||
orderType.value,
|
'/pages/cart/payment/success?paymentMethod=' +
|
||||||
})
|
paymentMethod +
|
||||||
|
'&payPrice=' +
|
||||||
|
cashierParams.value.price +
|
||||||
|
'&orderType=' +
|
||||||
|
orderType.value +
|
||||||
|
'&orderPromotionType=' +
|
||||||
|
(cashierParams.value.orderPromotionType || ''),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
goSuccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
function cashierData() {
|
function cashierData() {
|
||||||
const parms: Record<string, string> = {}
|
const parms: Record<string, string> = {}
|
||||||
|
|
||||||
if (routerVal.value.recharge_sn) {
|
if (routerVal.value.paid_member_sn) {
|
||||||
|
sn.value = routerVal.value.paid_member_sn
|
||||||
|
orderType.value = 'PAID_MEMBER'
|
||||||
|
} else if (routerVal.value.recharge_sn) {
|
||||||
sn.value = routerVal.value.recharge_sn
|
sn.value = routerVal.value.recharge_sn
|
||||||
orderType.value = 'RECHARGE'
|
orderType.value = 'RECHARGE'
|
||||||
} else if (routerVal.value.trade_sn) {
|
} else if (routerVal.value.trade_sn) {
|
||||||
@@ -207,6 +223,11 @@ function onPayTimeout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function goPayError(message = '支付失败,如果您已支付,请勿反复支付') {
|
function goPayError(message = '支付失败,如果您已支付,请勿反复支付') {
|
||||||
|
if (orderType.value === 'PAID_MEMBER') {
|
||||||
|
uni.showToast({ title: '支付未完成,请在购买记录中查看结果', icon: 'none' })
|
||||||
|
uni.redirectTo({ url: '/pages/mine/paidMemberRecords' })
|
||||||
|
return
|
||||||
|
}
|
||||||
const query = [`orderSn=${encodeURIComponent(sn.value)}`, `message=${encodeURIComponent(message)}`]
|
const query = [`orderSn=${encodeURIComponent(sn.value)}`, `message=${encodeURIComponent(message)}`]
|
||||||
uni.navigateTo({ url: `/pages/cart/payment/error?${query.join('&')}` })
|
uni.navigateTo({ url: `/pages/cart/payment/error?${query.join('&')}` })
|
||||||
}
|
}
|
||||||
@@ -271,15 +292,24 @@ async function pay(payment: string) {
|
|||||||
})
|
})
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
} else {
|
} else {
|
||||||
window.location.href = JSON.parse(response.result).h5_url
|
const paymentUrl = typeof response.result === 'string' && response.result.startsWith('http')
|
||||||
|
? response.result : JSON.parse(response.result).h5_url
|
||||||
|
const returnUrl = orderType.value === 'PAID_MEMBER'
|
||||||
|
? window.location.href.split('#')[0] + '#/pages/mine/paidMember?payment_sn=' + encodeURIComponent(sn.value)
|
||||||
|
: window.location.href
|
||||||
|
window.location.href = paymentUrl + (paymentUrl.includes('?') ? '&' : '?') + 'redirect_url=' + encodeURIComponent(returnUrl)
|
||||||
const searchParams = { ...params, price: cashierParams.value }
|
const searchParams = { ...params, price: cashierParams.value }
|
||||||
const timer = setInterval(() => {
|
let attempts = 0
|
||||||
|
if (h5PollTimer) clearInterval(h5PollTimer)
|
||||||
|
h5PollTimer = setInterval(() => {
|
||||||
|
if (++attempts > 100) { clearInterval(h5PollTimer); return }
|
||||||
payCallback(searchParams).then((cbRes) => {
|
payCallback(searchParams).then((cbRes) => {
|
||||||
if (cbRes.data.result) {
|
if (cbRes.data.result) {
|
||||||
clearInterval(timer)
|
clearInterval(h5PollTimer)
|
||||||
uni.navigateTo({ url: '/pages/order/myOrder' })
|
if (orderType.value === 'PAID_MEMBER') callback(paymentMethod)
|
||||||
|
else uni.navigateTo({ url: '/pages/order/myOrder' })
|
||||||
}
|
}
|
||||||
})
|
}).catch(() => {})
|
||||||
}, 3000)
|
}, 3000)
|
||||||
hideLoadingIfNeeded()
|
hideLoadingIfNeeded()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div :style="themeStyle" class="wrapper">
|
<div :style="themeStyle" class="wrapper">
|
||||||
<div class="pay-wrapper">
|
<div class="pay-wrapper">
|
||||||
<div class="pay-money">
|
<div class="pay-money">
|
||||||
¥{{unitPrice(Number(payPrice)) }}
|
¥{{ unitPrice(Number(payPrice)) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="pay-btns">
|
<div class="pay-btns">
|
||||||
<div v-show="!from" @click="checkOrder">查看{{ orderType == "RECHARGE" ? '余额' : '订单' }}</div>
|
<div v-show="!from" @click="checkOrder">查看{{ orderType == "RECHARGE" ? '余额' : '订单' }}</div>
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<goodsRecommend />
|
<goodsRecommend />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
@@ -31,6 +30,11 @@ import goodsRecommend from '@/components/m-goods-recommend'
|
|||||||
import { unitPrice } from '@/utils/filters.js'
|
import { unitPrice } from '@/utils/filters.js'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import {
|
||||||
|
prefetchMpMessageTemplates,
|
||||||
|
subscribePayMessageThen,
|
||||||
|
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
@@ -38,12 +42,17 @@ const paymentMethod = ref('')
|
|||||||
const from = ref('')
|
const from = ref('')
|
||||||
const payPrice = ref<number | string>(0)
|
const payPrice = ref<number | string>(0)
|
||||||
const orderType = ref('')
|
const orderType = ref('')
|
||||||
|
const orderPromotionType = ref('')
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
paymentMethod.value = options.paymentMethod || ''
|
paymentMethod.value = options.paymentMethod || ''
|
||||||
from.value = options.from || ''
|
from.value = options.from || ''
|
||||||
payPrice.value = options.payPrice || 0
|
payPrice.value = options.payPrice || 0
|
||||||
orderType.value = options.orderType || ''
|
orderType.value = options.orderType || ''
|
||||||
|
orderPromotionType.value = options.orderPromotionType || ''
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
// #endif
|
||||||
})
|
})
|
||||||
|
|
||||||
function paymentTypeFilter(val: string) {
|
function paymentTypeFilter(val: string) {
|
||||||
@@ -66,19 +75,44 @@ function checkOrder() {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uni.reLaunch({
|
const doGoOrder = () => {
|
||||||
url: '/pages/order/myOrder?status=0',
|
uni.reLaunch({
|
||||||
|
url: '/pages/order/myOrder?status=0',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribePayMessageThen(doGoOrder, {
|
||||||
|
orderPromotionType: orderPromotionType.value,
|
||||||
})
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doGoOrder()
|
||||||
|
// #endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(url: string, type?: string) {
|
function navigateTo(url: string, type?: string) {
|
||||||
if (type === 'switch') {
|
const doNavigate = () => {
|
||||||
uni.switchTab({ url })
|
if (type === 'switch') {
|
||||||
} else {
|
uni.switchTab({ url })
|
||||||
uni.redirectTo({ url })
|
} else {
|
||||||
|
uni.redirectTo({ url })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (orderType.value == 'RECHARGE') {
|
||||||
|
doNavigate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribePayMessageThen(doNavigate, {
|
||||||
|
orderPromotionType: orderPromotionType.value,
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doNavigate()
|
||||||
|
// #endif
|
||||||
}
|
}
|
||||||
</script><style scoped lang="scss">
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
.subscribe {
|
.subscribe {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle" class="add-address">
|
<view :style="themeStyle" class="address-page add-address">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<view class="wechat-import">
|
||||||
|
<button class="wechat-import-button" :disabled="importing" @click="importWechatAddress">
|
||||||
|
<view class="wechat-icon"><u-icon name="weixin-fill" color="#07b75b" size="26" /></view>
|
||||||
|
<view class="wechat-copy">
|
||||||
|
<text class="wechat-title">{{ importing ? '正在导入微信地址…' : '导入微信收货地址' }}</text>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" color="#77a38a" size="14" />
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
<view v-if="importNotice" class="import-notice">{{ importNotice }}</view>
|
||||||
|
<view class="section-heading">收货信息</view>
|
||||||
|
<view class="address-card form-card">
|
||||||
<up-form
|
<up-form
|
||||||
:model="form"
|
:model="form"
|
||||||
ref="uForm"
|
ref="uForm"
|
||||||
@@ -7,30 +21,31 @@
|
|||||||
:rules="rules"
|
:rules="rules"
|
||||||
label-position="left"
|
label-position="left"
|
||||||
label-width="180rpx"
|
label-width="180rpx"
|
||||||
|
:label-style="{ color: '#5b606b', fontSize: '28rpx' }"
|
||||||
>
|
>
|
||||||
<up-form-item label="收货人" label-width="180rpx" prop="name" :border-bottom="true">
|
<up-form-item label="收货人" label-width="160rpx" prop="name" :border-bottom="true" :custom-style="fieldStyle">
|
||||||
<u-input
|
<u-input
|
||||||
v-model="form.name"
|
v-model="form.name"
|
||||||
border="none"
|
border="none"
|
||||||
input-align="right"
|
input-align="left"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入收货人姓名"
|
placeholder="姓名"
|
||||||
/>
|
/>
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<up-form-item label="手机号码" label-width="180rpx" prop="mobile" :border-bottom="true">
|
<up-form-item label="手机号码" label-width="160rpx" prop="mobile" :border-bottom="true" :custom-style="fieldStyle">
|
||||||
<u-input
|
<u-input
|
||||||
v-model="form.mobile"
|
v-model="form.mobile"
|
||||||
type="number"
|
type="number"
|
||||||
maxlength="11"
|
maxlength="11"
|
||||||
border="none"
|
border="none"
|
||||||
input-align="right"
|
input-align="left"
|
||||||
placeholder="请输入收货人手机号码"
|
placeholder="收货人的手机号码"
|
||||||
/>
|
/>
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<up-form-item label="所在区域" label-width="180rpx" prop="___path" :border-bottom="true">
|
<up-form-item label="所在地区" label-width="160rpx" prop="___path" :border-bottom="true" :custom-style="fieldStyle" @click="showPicker">
|
||||||
<view class="form-value" @click="showPicker">
|
<view class="form-value" :class="{ 'is-placeholder': !form.___path }">
|
||||||
{{ form.___path || '请选择所在地区' }}
|
{{ form.___path || '请选择所在地区' }}
|
||||||
</view>
|
</view>
|
||||||
<template #right>
|
<template #right>
|
||||||
@@ -38,40 +53,38 @@
|
|||||||
</template>
|
</template>
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<up-form-item class="detailAddress" label="详细地址" label-width="180rpx" prop="detail" :border-bottom="true">
|
<up-form-item label="详细地址" label-width="160rpx" prop="detail" :border-bottom="true" :custom-style="{ ...fieldStyle, alignItems: 'flex-start' }">
|
||||||
<u-input
|
<textarea
|
||||||
type="textarea"
|
class="address-textarea"
|
||||||
v-model="form.detail"
|
v-model="form.detail"
|
||||||
maxlength="100"
|
maxlength="100"
|
||||||
height="150"
|
:auto-height="true"
|
||||||
border="none"
|
placeholder="街道、门牌号等详细信息"
|
||||||
input-align="right"
|
placeholder-style="color: #b1b5bd; font-size: 28rpx;"
|
||||||
placeholder="街道楼牌号等"
|
|
||||||
/>
|
/>
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<up-form-item label="地址别名" label-width="180rpx" :border-bottom="false">
|
<up-form-item label="地址标签" label-width="160rpx" :border-bottom="false" :custom-style="fieldStyle">
|
||||||
<u-input
|
<u-input
|
||||||
v-model="form.alias"
|
v-model="form.alias"
|
||||||
border="none"
|
border="none"
|
||||||
input-align="right"
|
input-align="left"
|
||||||
placeholder="请输入地址别名"
|
placeholder="如:家、公司(选填)"
|
||||||
/>
|
/>
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<view class="default-row">
|
|
||||||
<u-checkbox
|
|
||||||
usedAlone
|
|
||||||
shape="circle"
|
|
||||||
size="30"
|
|
||||||
:active-color="lightColor"
|
|
||||||
v-model:checked="form.isDefault"
|
|
||||||
label="设为默认地址"
|
|
||||||
></u-checkbox>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="saveBtn" @click="save">保存</view>
|
|
||||||
</up-form>
|
</up-form>
|
||||||
|
</view>
|
||||||
|
<view class="address-card default-row">
|
||||||
|
<view>
|
||||||
|
<text class="default-title">设为默认地址</text>
|
||||||
|
<text class="default-hint">下单时优先使用这个地址</text>
|
||||||
|
</view>
|
||||||
|
<switch :checked="!!form.isDefault" :color="mainColor" @change="form.isDefault = $event.detail.value" />
|
||||||
|
</view>
|
||||||
|
<view class="address-footer">
|
||||||
|
<button class="address-primary-button" :disabled="importing" @click="save">保存收货地址</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
<m-city
|
<m-city
|
||||||
:provinceData="list"
|
:provinceData="list"
|
||||||
@@ -93,16 +106,114 @@ import MCity from '@/components/m-city/m-city.vue'
|
|||||||
import uniMap from '@/components/uniMap'
|
import uniMap from '@/components/uniMap'
|
||||||
import permision from '@/js_sdk/wa-permission/permission.js'
|
import permision from '@/js_sdk/wa-permission/permission.js'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import { getRegionsById } from '@/api/common.js'
|
||||||
|
import { resolveWechatRegion, getWechatAddressErrorType } from '@/utils/wechatAddress.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
const { proxy } = getCurrentInstance()!
|
const { proxy } = getCurrentInstance()!
|
||||||
|
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
const mainColor = computed(() => store.getters.mainColor)
|
||||||
|
const fieldStyle = { padding: '28rpx 0', minHeight: '100rpx', boxSizing: 'border-box', alignItems: 'center' }
|
||||||
const mapFlag = ref(false)
|
const mapFlag = ref(false)
|
||||||
const routerVal = ref<Record<string, string>>({})
|
const routerVal = ref<Record<string, string>>({})
|
||||||
const uForm = ref<any>(null)
|
const uForm = ref<any>(null)
|
||||||
const cityPicker = ref<any>(null)
|
const cityPicker = ref<any>(null)
|
||||||
|
const importing = ref(false)
|
||||||
|
const importNotice = ref('')
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
function importWechatAddress() {
|
||||||
|
if (importing.value) return
|
||||||
|
importing.value = true
|
||||||
|
uni.chooseAddress({
|
||||||
|
success: async (address: any) => {
|
||||||
|
uni.showLoading({ title: '正在匹配地区', mask: true })
|
||||||
|
// Clear the old region and coordinates, including when editing an existing address.
|
||||||
|
Object.assign(form, {
|
||||||
|
name: address.userName || '',
|
||||||
|
mobile: address.telNumber || '',
|
||||||
|
detail: address.detailInfoNew || address.detailInfo || '',
|
||||||
|
consigneeAddressIdPath: [],
|
||||||
|
consigneeAddressPath: [],
|
||||||
|
___path: '',
|
||||||
|
lat: '',
|
||||||
|
lon: '',
|
||||||
|
})
|
||||||
|
importNotice.value = ''
|
||||||
|
try {
|
||||||
|
const regions = await resolveWechatRegion(address, getRegionsById)
|
||||||
|
if (regions) {
|
||||||
|
if (regions.length < 4 && address.streetName) {
|
||||||
|
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||||
|
}
|
||||||
|
const names = regions.map((item: any) => item.name || item.localName)
|
||||||
|
form.consigneeAddressIdPath = regions.map((item: any) => item.id)
|
||||||
|
form.consigneeAddressPath = names
|
||||||
|
form.___path = names.join('')
|
||||||
|
const center = String(regions[regions.length - 1].center || '').split(',')
|
||||||
|
if (center.length === 2) [form.lon, form.lat] = center
|
||||||
|
importNotice.value = '已导入微信地址,请核对后保存'
|
||||||
|
} else {
|
||||||
|
showImportedRegionHint(address)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showImportedRegionHint(address)
|
||||||
|
} finally {
|
||||||
|
importing.value = false
|
||||||
|
uni.hideLoading()
|
||||||
|
uForm.value?.clearValidate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error: any) => {
|
||||||
|
importing.value = false
|
||||||
|
const type = getWechatAddressErrorType(error)
|
||||||
|
if (type === 'cancel') return
|
||||||
|
let platform = ''
|
||||||
|
try { platform = (globalThis as any).wx?.getDeviceInfo?.()?.platform || '' } catch {}
|
||||||
|
// Log diagnostics only, never the selected address or other personal information.
|
||||||
|
console.warn('[微信地址导入失败]', {
|
||||||
|
errMsg: String(error?.errMsg || error?.message || ''),
|
||||||
|
errCode: error?.errCode ?? error?.errno ?? '',
|
||||||
|
type,
|
||||||
|
platform,
|
||||||
|
})
|
||||||
|
if (type === 'auth') {
|
||||||
|
uni.showModal({
|
||||||
|
title: '开启收货地址授权',
|
||||||
|
content: '需要允许使用微信收货地址才能导入,也可以继续手动填写。',
|
||||||
|
confirmText: '去设置',
|
||||||
|
success: (result) => { if (result.confirm) uni.openSetting({}) },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (type === 'privacy') {
|
||||||
|
console.warn('[微信地址导入] 请核对小程序后台的用户隐私保护指引是否声明收货地址,以及用户隐私授权状态。')
|
||||||
|
}
|
||||||
|
if (type === 'configuration') {
|
||||||
|
console.warn('[微信地址导入] manifest.json 的 mp-weixin.requiredPrivateInfos 必须包含 chooseAddress,修改后需重新编译小程序。')
|
||||||
|
}
|
||||||
|
const content = type === 'configuration'
|
||||||
|
? '收货地址导入暂未配置完成,请稍后再试,也可以手动填写。'
|
||||||
|
: type === 'privacy'
|
||||||
|
? '暂时无法获取微信收货地址,请稍后重试,也可以手动填写。'
|
||||||
|
: platform === 'devtools'
|
||||||
|
? '当前开发工具中未能打开微信地址簿,请使用手机微信扫码预览测试,也可以手动填写。'
|
||||||
|
: type === 'unsupported'
|
||||||
|
? '当前微信环境不支持导入收货地址,请更新微信后重试,也可以手动填写。'
|
||||||
|
: '暂时无法打开微信地址簿,请稍后重试,也可以手动填写。'
|
||||||
|
uni.showModal({ title: '微信地址未导入', content, showCancel: false })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImportedRegionHint(address: any) {
|
||||||
|
const region = [address.provinceName, address.cityName, address.countyName, address.streetName].filter(Boolean).join('')
|
||||||
|
// Preserve the street if the user has to select it manually and the new detail field omits it.
|
||||||
|
form.detail = address.detailInfo || [address.streetName, address.detailInfoNew].filter(Boolean).join('')
|
||||||
|
importNotice.value = `已导入姓名、电话和详细地址,请补选所在地区${region ? '(微信地址:' + region + ')' : ''}`
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
const form = reactive<Record<string, any>>({
|
const form = reactive<Record<string, any>>({
|
||||||
detail: '',
|
detail: '',
|
||||||
@@ -160,18 +271,24 @@ const rules = {
|
|||||||
|
|
||||||
onShow(() => {})
|
onShow(() => {})
|
||||||
|
|
||||||
onLoad((option) => {
|
onLoad(async (option) => {
|
||||||
uni.showLoading({ title: '加载中' })
|
|
||||||
routerVal.value = option || {}
|
routerVal.value = option || {}
|
||||||
if (option.id) {
|
uni.setNavigationBarTitle({ title: option?.id ? '编辑收货地址' : '新增收货地址' })
|
||||||
getAddressDetail(option.id).then((res) => {
|
if (!option?.id) return
|
||||||
const params = res.data.result
|
uni.showLoading({ title: '加载中' })
|
||||||
params.___path = params.consigneeAddressPath
|
try {
|
||||||
Object.assign(form, params)
|
const res = await getAddressDetail(option.id)
|
||||||
if (store.state.isShowToast) uni.hideLoading()
|
const params = res.data?.result
|
||||||
})
|
if (!res.data?.success || !params) {
|
||||||
|
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Object.assign(form, params, { ___path: params.consigneeAddressPath || '' })
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '地址加载失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
}
|
}
|
||||||
uni.hideLoading()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onReady(() => {
|
onReady(() => {
|
||||||
@@ -233,6 +350,7 @@ function callBackAddress(val: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
|
if (importing.value) return
|
||||||
uForm.value?.validate().then(() => {
|
uForm.value?.validate().then(() => {
|
||||||
const params = { ...form }
|
const params = { ...form }
|
||||||
delete params.___path
|
delete params.___path
|
||||||
@@ -278,11 +396,12 @@ function getpickerParentValue(e: any[]) {
|
|||||||
form.___path = name
|
form.___path = name
|
||||||
}
|
}
|
||||||
if (index == e.length - 1) {
|
if (index == e.length - 1) {
|
||||||
const _town = item.children.filter((_child: any) => _child.id == item.id)
|
const center = String(item.center || item.children?.find((_child: any) => _child.id == item.id)?.center || '').split(',')
|
||||||
form.lat = _town[0].center.split(',')[1]
|
form.lat = center.length === 2 ? center[1] : ''
|
||||||
form.lon = _town[0].center.split(',')[0]
|
form.lon = center.length === 2 ? center[0] : ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
importNotice.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function showPicker() {
|
function showPicker() {
|
||||||
@@ -290,99 +409,25 @@ function showPicker() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
page {
|
@import "./address-page.scss";
|
||||||
background: #f5f5f5;
|
.wechat-import { margin-bottom: 28rpx; }
|
||||||
}
|
.wechat-import-button {
|
||||||
|
display: flex; align-items: center; padding: 28rpx; margin: 0; width: 100%; text-align: left;
|
||||||
.add-address {
|
background: #eef9f2; border-radius: 24rpx; line-height: 1.5;
|
||||||
min-height: 100vh;
|
&::after { border: 1rpx solid #dcefe3; border-radius: 24rpx; }
|
||||||
background: #fff;
|
&[disabled] { background: #eef9f2; opacity: .65; }
|
||||||
padding-bottom: 40rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item) {
|
|
||||||
padding: 0 30rpx;
|
|
||||||
min-height: 100rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item__body) {
|
|
||||||
min-height: 100rpx;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item__body__left__content__label) {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item__body__right__content__slot) {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-input) {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-input__content__field-wrapper__field) {
|
|
||||||
font-size: 28rpx !important;
|
|
||||||
color: #666 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-value {
|
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detailAddress {
|
|
||||||
:deep(.u-form-item__body) {
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-top: 24rpx;
|
|
||||||
padding-bottom: 24rpx;
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item__body__left) {
|
|
||||||
padding-top: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-form-item__body__right__content__slot) {
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.u-textarea__field),
|
|
||||||
:deep(.u-input__content__field-wrapper__field) {
|
|
||||||
text-align: right !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.uni-textarea-placeholder),
|
|
||||||
:deep(.input-placeholder) {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-row {
|
|
||||||
padding: 24rpx 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.default-row .u-checkbox__label) {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: $font-color-light;
|
|
||||||
}
|
|
||||||
|
|
||||||
.saveBtn {
|
|
||||||
margin: 40rpx 30rpx 0;
|
|
||||||
height: 80rpx;
|
|
||||||
line-height: 80rpx;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 30rpx;
|
|
||||||
background: $light-color;
|
|
||||||
color: #fff;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
}
|
}
|
||||||
|
.wechat-icon { width: 76rpx; height: 76rpx; border-radius: 20rpx; background: #fff; display: flex; align-items: center; justify-content: center; flex-shrink: 0; margin-right: 22rpx; }
|
||||||
|
.wechat-copy { flex: 1; min-width: 0; }
|
||||||
|
.wechat-title { display: block; color: #253e30; font-size: 30rpx; font-weight: 600; }
|
||||||
|
.import-notice { margin-bottom: 24rpx; padding: 20rpx 24rpx; border-radius: 16rpx; background: #fff5e5; color: #a47a3b; font-size: 24rpx; line-height: 1.6; }
|
||||||
|
.section-heading { padding: 0 8rpx 20rpx; font-size: 26rpx; color: #858b96; }
|
||||||
|
.form-card { padding: 0 28rpx; }
|
||||||
|
.form-value { width: 100%; line-height: 1.6; font-size: 28rpx; color: #303744; overflow-wrap: anywhere; }
|
||||||
|
.is-placeholder { color: #b1b5bd; }
|
||||||
|
.address-textarea { width: 100%; min-height: 88rpx; font-size: 28rpx; color: #303744; line-height: 1.6; padding: 0; }
|
||||||
|
.default-row { display: flex; justify-content: space-between; align-items: center; margin-top: 24rpx; padding: 28rpx; }
|
||||||
|
.default-title { display: block; font-size: 28rpx; color: #353b47; }
|
||||||
|
.default-hint { display: block; font-size: 23rpx; color: #a0a5af; margin-top: 8rpx; }
|
||||||
|
.default-row switch { transform: scale(.8); transform-origin: right center; }
|
||||||
</style>
|
</style>
|
||||||
43
pages/mine/address/address-page.scss
Normal file
43
pages/mine/address/address-page.scss
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.address-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f6f7f9;
|
||||||
|
padding: 28rpx 24rpx calc(180rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 90;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #fff;
|
||||||
|
border-top: 1rpx solid #f0f1f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-primary-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
width: 100%;
|
||||||
|
height: 88rpx;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
background: var(--theme-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1;
|
||||||
|
&::after { border: none; }
|
||||||
|
&[disabled] { opacity: .5; background: var(--theme-primary); color: #fff; }
|
||||||
|
}
|
||||||
@@ -1,55 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="address">
|
<view class="address-page address-manage" :style="themeStyle">
|
||||||
<u-empty class="empty" v-if="addressList.length === 0" text="暂无收货地址" mode="address"></u-empty>
|
<view v-if="!loading && addressList.length === 0" class="empty-state">
|
||||||
<view class="list" >
|
<view class="empty-icon"><u-icon name="map" size="42" color="#c4c8d1" /></view>
|
||||||
<view class="item c-content" v-for="(item, index) in addressList" :key="index">
|
<text class="empty-title">还没有收货地址</text>
|
||||||
<view class="basic">
|
<text class="empty-description">添加常用地址,下次购物更方便</text>
|
||||||
<text>{{ item.name }}</text>
|
</view>
|
||||||
<text>{{ item.mobile }}</text>
|
<view v-if="addressList.length" class="list-heading">
|
||||||
<text class="default" v-show="item.isDefault">默认</text>
|
<text>我的收货地址</text>
|
||||||
<view>
|
<text class="address-count">共 {{ addressList.length }} 个</text>
|
||||||
<div class="region">
|
</view>
|
||||||
<span v-if="item.consigneeAddressPath[0]">{{item.consigneeAddressPath[0]}}</span>
|
<view class="address-list">
|
||||||
<span v-if="item.consigneeAddressPath[1]">{{item.consigneeAddressPath[1]}}</span>
|
<view class="address-card address-item" v-for="item in addressList" :key="item.id">
|
||||||
<span v-if="item.consigneeAddressPath[2]">{{item.consigneeAddressPath[2]}}</span>
|
<view class="recipient-line">
|
||||||
<span v-if="item.consigneeAddressPath[3]">{{item.consigneeAddressPath[3]}}</span>
|
<text class="recipient-name">{{ item.name }}</text>
|
||||||
<span>{{ item.detail }}</span>
|
<text class="recipient-mobile">{{ item.mobile }}</text>
|
||||||
</div>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="edit">
|
<view class="address-region">{{ item.consigneeAddressPath.join(' ') }}</view>
|
||||||
<view class="relative" @click="setDefault(item)">
|
<text class="address-detail">{{ item.detail }}</text>
|
||||||
<view v-if="item.isDefault" class="alifont icon-xuanzhong"></view>
|
<view class="address-tags" v-if="item.isDefault || item.alias">
|
||||||
<text v-else class="unchecked"></text>
|
<text v-if="item.isDefault" class="address-tag default-tag">默认地址</text>
|
||||||
<text>{{ item.isDefault ? "默认地址" : "设为默认" }}</text>
|
<text v-if="item.alias" class="address-tag">{{ item.alias }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-actions">
|
||||||
|
<view class="default-action" @click="setDefault(item)">
|
||||||
|
<view class="default-check" :class="{ 'is-checked': item.isDefault }">
|
||||||
|
<u-icon v-if="item.isDefault" name="checkmark" size="11" color="#fff" />
|
||||||
|
</view>
|
||||||
|
<text>{{ item.isDefault ? '已设为默认' : '设为默认' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="relative">
|
<view class="edit-actions">
|
||||||
<view class="alifont icon-bianji-copy"></view>
|
<view class="text-action" @click="addAddress(item.id)">
|
||||||
<text class="mr-40" @click="addAddress(item.id)">编辑</text>
|
<u-icon name="edit-pen" size="16" color="#6c7280" /><text>编辑</text>
|
||||||
<view class="alifont icon-lajitong" @click.stop="removeAddress(item.id)"></view>
|
</view>
|
||||||
<text @click.stop="removeAddress(item.id)">删除</text>
|
<view class="action-divider"></view>
|
||||||
|
<view class="text-action" @click="removeAddress(item.id)">
|
||||||
|
<u-icon name="trash" size="16" color="#9297a1" /><text>删除</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view class="address-footer">
|
||||||
<view class="btn-bar">
|
<button class="address-primary-button" @click="addAddress('')">
|
||||||
<view class="btn" @click="addAddress('')">
|
<u-icon name="plus" color="#fff" size="17" /><text>新增收货地址</text>
|
||||||
<u-icon name="plus-circle" color="#ffffff" size="18"></u-icon>
|
</button>
|
||||||
<text class="btn-text">添加新收货人</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-app'
|
import { onLoad, onShow, onPullDownRefresh, onBackPress } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { tipsToLogin } from '@/utils/filters.js'
|
import { tipsToLogin } from '@/utils/filters.js'
|
||||||
import * as API_Address from '@/api/address.js'
|
import * as API_Address from '@/api/address.js'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
const addressList = ref<any[]>([])
|
const addressList = ref<any[]>([])
|
||||||
const routerVal = ref<Record<string, string>>({})
|
const routerVal = ref<Record<string, string>>({})
|
||||||
@@ -65,7 +74,6 @@ onLoad((val) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
addressList.value = []
|
|
||||||
getAddressList()
|
getAddressList()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -75,19 +83,23 @@ onShow(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function hideLoadingIfNeeded() {
|
async function getAddressList() {
|
||||||
if (store.state.isShowToast) uni.hideLoading()
|
loading.value = true
|
||||||
}
|
try {
|
||||||
|
const res = await API_Address.getAddressList(params.pageNumber, params.pageSize)
|
||||||
function getAddressList() {
|
if (!res.data?.success || !Array.isArray(res.data.result?.records)) throw new Error('地址加载失败')
|
||||||
uni.showLoading()
|
addressList.value = res.data.result.records.map((item: any) => ({
|
||||||
API_Address.getAddressList(params.pageNumber, params.pageSize).then((res) => {
|
...item,
|
||||||
res.data.result.records.forEach((item: any) => {
|
consigneeAddressPath: Array.isArray(item.consigneeAddressPath)
|
||||||
item.consigneeAddressPath = item.consigneeAddressPath.split(',')
|
? item.consigneeAddressPath
|
||||||
})
|
: String(item.consigneeAddressPath || '').split(',').filter(Boolean),
|
||||||
addressList.value = res.data.result.records
|
}))
|
||||||
hideLoadingIfNeeded()
|
} catch {
|
||||||
})
|
uni.showToast({ title: '地址加载失败,请下拉重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAddress(id: string) {
|
function removeAddress(id: string) {
|
||||||
@@ -97,7 +109,7 @@ function removeAddress(id: string) {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.confirm) return
|
if (!res.confirm) return
|
||||||
API_Address.deleteAddress(id).then((result) => {
|
API_Address.deleteAddress(id).then((result) => {
|
||||||
if (result.statusCode == 200) {
|
if (result.data?.success) {
|
||||||
uni.showToast({ icon: 'none', title: '删除成功' })
|
uni.showToast({ icon: 'none', title: '删除成功' })
|
||||||
getAddressList()
|
getAddressList()
|
||||||
} else {
|
} else {
|
||||||
@@ -115,17 +127,45 @@ function addAddress(id?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setDefault(item: any) {
|
function setDefault(item: any) {
|
||||||
delete item.updateBy
|
if (item.isDefault) return
|
||||||
delete item.updateTime
|
const data = { ...item, isDefault: true, consigneeAddressPath: item.consigneeAddressPath.join(',') }
|
||||||
delete item.deleteFlag
|
delete data.updateBy
|
||||||
if (!item.isDefault) item.isDefault = true
|
delete data.updateTime
|
||||||
API_Address.editAddress(item).then(() => {
|
delete data.deleteFlag
|
||||||
uni.showToast({ title: '设置默认地址成功', icon: 'none' })
|
API_Address.editAddress(data).then((res) => {
|
||||||
getAddressList()
|
if (res.data?.success) {
|
||||||
})
|
uni.showToast({ title: '已设为默认地址', icon: 'none' })
|
||||||
|
getAddressList()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.data?.message || '设置失败,请重试', icon: 'none' })
|
||||||
|
}
|
||||||
|
}).catch(() => uni.showToast({ title: '设置失败,请重试', icon: 'none' }))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "./address.scss";
|
@import "./address-page.scss";
|
||||||
|
|
||||||
|
.list-heading { display: flex; justify-content: space-between; padding: 8rpx 8rpx 24rpx; font-size: 26rpx; color: #6c7280; }
|
||||||
|
.address-count { color: #a0a5af; font-size: 24rpx; }
|
||||||
|
.address-item { padding: 32rpx 32rpx 0; margin-bottom: 24rpx; }
|
||||||
|
.recipient-line { display: flex; align-items: baseline; flex-wrap: wrap; gap: 20rpx; margin-bottom: 18rpx; }
|
||||||
|
.recipient-name { font-size: 34rpx; font-weight: 600; color: #222732; overflow-wrap: anywhere; }
|
||||||
|
.recipient-mobile { color: #636a77; font-size: 28rpx; }
|
||||||
|
.address-region { color: #9399a4; font-size: 25rpx; line-height: 1.7; }
|
||||||
|
.address-detail { display: block; color: #343b48; font-size: 29rpx; line-height: 1.7; overflow-wrap: anywhere; }
|
||||||
|
.address-tags { display: flex; flex-wrap: wrap; gap: 12rpx; padding-top: 20rpx; }
|
||||||
|
.address-tag { max-width: 100%; overflow-wrap: anywhere; padding: 4rpx 14rpx; background: #f3f4f6; border-radius: 8rpx; color: #7f8591; font-size: 22rpx; }
|
||||||
|
.default-tag { color: var(--theme-primary); background: var(--theme-primary-10); }
|
||||||
|
.card-actions { margin-top: 26rpx; border-top: 1rpx solid #f0f1f4; min-height: 92rpx; display: flex; justify-content: space-between; align-items: center; }
|
||||||
|
.default-action, .text-action, .edit-actions { display: flex; align-items: center; }
|
||||||
|
.default-action { gap: 12rpx; min-height: 80rpx; color: #858b96; font-size: 24rpx; }
|
||||||
|
.default-check { width: 30rpx; height: 30rpx; box-sizing: border-box; border: 2rpx solid #dce0e6; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.is-checked { background: var(--theme-primary); border-color: var(--theme-primary); }
|
||||||
|
.text-action { gap: 8rpx; min-height: 80rpx; color: #6c7280; font-size: 24rpx; padding: 0 8rpx; }
|
||||||
|
.action-divider { width: 1rpx; height: 22rpx; background: #e9ebef; margin: 0 16rpx; }
|
||||||
|
.empty-state { display: flex; flex-direction: column; align-items: center; padding: 160rpx 0 80rpx; }
|
||||||
|
.empty-icon { width: 148rpx; height: 148rpx; display: flex; align-items: center; justify-content: center; background: #eaecef; border-radius: 50%; margin-bottom: 36rpx; }
|
||||||
|
.empty-title { color: #454c59; font-size: 32rpx; font-weight: 500; }
|
||||||
|
.empty-description { color: #a0a5af; font-size: 26rpx; margin-top: 16rpx; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
37
pages/mine/components/MemberGradeCard.vue
Normal file
37
pages/mine/components/MemberGradeCard.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<view class="member-card" :style="{ color: cardColor }">
|
||||||
|
<image v-if="grade.gradeBackground && !backgroundFailed" :key="grade.gradeBackground" class="card-background" :src="grade.gradeBackground"
|
||||||
|
mode="aspectFill" @load="backgroundLoaded = true" @error="backgroundFailed = true" />
|
||||||
|
<view class="member-card-content">
|
||||||
|
<view class="card-status">{{ isCurrent ? '当前等级' : '等级预览' }}</view>
|
||||||
|
<view class="grade-heading">
|
||||||
|
<image v-if="grade.gradeImage && !iconFailed" :key="grade.gradeImage" class="grade-icon" :src="grade.gradeImage" mode="aspectFit" @error="iconFailed = true" />
|
||||||
|
<u-icon v-else name="level" size="30" :color="cardColor" /><text class="grade-title">{{ grade.gradeName }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="card-subtitle">{{ benefitCount }} 项专属权益 · {{ thresholdText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
const props = defineProps({ grade: { type: Object, required: true }, benefitCount: { type: Number, default: 0 }, isCurrent: { type: Boolean, default: false } })
|
||||||
|
const backgroundLoaded = ref(false), backgroundFailed = ref(false), iconFailed = ref(false)
|
||||||
|
const cardColor = computed(() => {
|
||||||
|
if (!backgroundLoaded.value || backgroundFailed.value) return '#fff5df'
|
||||||
|
const color = props.grade.gradeFontColor
|
||||||
|
return /^#[0-9a-f]{6}$/i.test(color || '') ? color : '#333333'
|
||||||
|
})
|
||||||
|
const thresholdText = computed(() => props.grade.isDefault ? '默认等级' : `${Math.max(0, Number(props.grade.requiredExperience) || 0).toLocaleString('en-US')} 经验值可达`)
|
||||||
|
watch(() => props.grade.gradeBackground, () => { backgroundLoaded.value = false; backgroundFailed.value = false }, { flush: 'sync' })
|
||||||
|
watch(() => props.grade.gradeImage, () => { iconFailed.value = false }, { flush: 'sync' })
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.member-card { position: relative; overflow: hidden; width: 100%; height: 100%; border-radius: 24rpx; background: linear-gradient(115deg, #51443b, #8e7351); }
|
||||||
|
.card-background { position: absolute; width: 100%; height: 100%; top: 0; left: 0; }
|
||||||
|
.member-card-content { position: relative; height: 100%; padding: 30rpx 32rpx; box-sizing: border-box; display: flex; flex-direction: column; justify-content: center; align-items: flex-start; }
|
||||||
|
.card-status { border: 1rpx solid currentColor; opacity: .8; font-size: 20rpx; padding: 4rpx 14rpx; border-radius: 20rpx; }
|
||||||
|
.grade-heading { display: flex; align-items: center; gap: 14rpx; margin: 18rpx 0 12rpx; max-width: 100%; }
|
||||||
|
.grade-icon { width: 68rpx; height: 68rpx; flex-shrink: 0; }
|
||||||
|
.grade-title { font-size: 38rpx; line-height: 1.4; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.card-subtitle { font-size: 23rpx; opacity: .85; }
|
||||||
|
</style>
|
||||||
@@ -1,87 +1,136 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle" class="wap">
|
<view :style="themeStyle" class="deposit-log-page">
|
||||||
<u-navbar
|
<u-navbar
|
||||||
title="预存款列表"
|
title="预存款明细"
|
||||||
:fixed="true"
|
:fixed="true"
|
||||||
:placeholder="true"
|
:placeholder="true"
|
||||||
:border="false"
|
:border="false"
|
||||||
:auto-back="true"
|
:auto-back="true"
|
||||||
></u-navbar>
|
></u-navbar>
|
||||||
|
|
||||||
<view class="wrapper-show-money">
|
<!-- 顶部余额概览卡片 -->
|
||||||
<view class="money-view">
|
<view class="header-wrap">
|
||||||
<text class="money-label">预存款金额</text>
|
<view class="balance-hero">
|
||||||
<text class="money">¥{{ unitPrice(walletNum) }}</text>
|
<view class="hero-glow hero-glow-1"></view>
|
||||||
|
<view class="hero-glow hero-glow-2"></view>
|
||||||
|
<view class="hero-top">
|
||||||
|
<text class="money-label">当前预存款余额 (元)</text>
|
||||||
|
</view>
|
||||||
|
<view class="money-row">
|
||||||
|
<text class="money-symbol">¥</text>
|
||||||
|
<text class="money-value">{{ unitPrice(walletNum) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 收支筛选栏 -->
|
||||||
|
<view class="filter-bar">
|
||||||
|
<view
|
||||||
|
v-for="(tab, idx) in filterTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
class="filter-pill"
|
||||||
|
:class="{ active: activeFilter === idx }"
|
||||||
|
@click="activeFilter = idx"
|
||||||
|
>
|
||||||
|
{{ tab.name }}
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="wrapper-tabs">
|
<!-- 明细列表区 -->
|
||||||
<swiper class="swiper-box" :current="swiperCurrent">
|
<view class="list-container">
|
||||||
<swiper-item class="swiper-item" v-for="index in list.length" :key="index">
|
<view v-if="filteredData.length > 0" class="list-card">
|
||||||
<scroll-view
|
<view
|
||||||
class="scroll-v view-wrapper"
|
class="log-item"
|
||||||
enable-back-to-top
|
v-for="(item, logIndex) in filteredData"
|
||||||
scroll-with-animation
|
:key="logIndex"
|
||||||
scroll-y
|
>
|
||||||
@scrolltolower="loadMore"
|
<!-- 第一行:类型标签 + 标题 | 变动金额 -->
|
||||||
>
|
<view class="log-row-main">
|
||||||
<view v-if="depositData.length != 0" class="list-card">
|
<view class="log-title-wrap">
|
||||||
<view
|
<text class="type-tag" :class="item.parsed.tagClass">{{ item.parsed.tagText }}</text>
|
||||||
class="view-item"
|
<text class="log-title">{{ item.parsed.title }}</text>
|
||||||
v-for="(logItem, logIndex) in depositData"
|
|
||||||
:key="logIndex"
|
|
||||||
>
|
|
||||||
<view class="view-item-detail">
|
|
||||||
<text class="item-title">{{ logItem.detail }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="view-item-change">
|
|
||||||
<text
|
|
||||||
class="item-money"
|
|
||||||
:class="getMoneyClass(logItem.serviceType)"
|
|
||||||
>{{ formatLogMoney(logItem) }}</text>
|
|
||||||
<text class="item-time">{{ logItem.createTime }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="log-amount" :class="item.parsed.moneyClass">
|
||||||
|
{{ item.parsed.formattedMoney }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<u-empty v-if="depositData.length == 0" mode="history" text="暂无记录" />
|
<!-- 第二行:提取出的单号或备注说明 -->
|
||||||
</scroll-view>
|
<view v-if="item.parsed.metaList.length > 0" class="log-meta-box">
|
||||||
</swiper-item>
|
<view
|
||||||
</swiper>
|
class="log-meta-line"
|
||||||
|
v-for="(meta, mIdx) in item.parsed.metaList"
|
||||||
|
:key="mIdx"
|
||||||
|
>
|
||||||
|
<text class="meta-label">{{ meta.label }}:</text>
|
||||||
|
<text class="meta-val">{{ meta.value }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 第三行:变动时间 -->
|
||||||
|
<view class="log-row-foot">
|
||||||
|
<text class="log-time">{{ item.createTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="loaded" class="empty-box">
|
||||||
|
<u-empty mode="history" text="暂无预存款变动记录" />
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { onReachBottom } from '@dcloudio/uni-app'
|
||||||
import { getWalletLog, getUserWallet } from '@/api/members'
|
import { getWalletLog, getUserWallet } from '@/api/members'
|
||||||
import { unitPrice } from '@/utils/filters.js'
|
import { unitPrice } from '@/utils/filters.js'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
const walletNum = ref(0)
|
const walletNum = ref(0)
|
||||||
const swiperCurrent = ref(0)
|
const loaded = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const activeFilter = ref(0)
|
||||||
|
const filterTabs = [
|
||||||
|
{ key: 'ALL', name: '全部明细' },
|
||||||
|
{ key: 'IN', name: '收入 / 退款' },
|
||||||
|
{ key: 'OUT', name: '支出 / 提现' },
|
||||||
|
]
|
||||||
|
|
||||||
const params = ref({
|
const params = ref({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
order: 'desc',
|
order: 'desc',
|
||||||
})
|
})
|
||||||
const depositData = ref<any[]>([])
|
const depositData = ref<any[]>([])
|
||||||
const list = [{ name: '预存款变动明细' }]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
getWallet()
|
getWallet()
|
||||||
const result = await getUserWallet()
|
const result = await getUserWallet()
|
||||||
walletNum.value = result.data.result.memberWallet
|
walletNum.value = result.data?.result?.memberWallet ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
function isOutgoing(serviceType: string) {
|
onReachBottom(() => {
|
||||||
|
if (!hasMore.value) return
|
||||||
|
loadMore()
|
||||||
|
})
|
||||||
|
|
||||||
|
function isOutgoing(logItem: any) {
|
||||||
|
const money = Number(logItem?.money) || 0
|
||||||
|
if (money < 0) return true
|
||||||
|
const serviceType = logItem?.serviceType
|
||||||
return serviceType === 'WALLET_PAY' || serviceType === 'WALLET_WITHDRAWAL'
|
return serviceType === 'WALLET_PAY' || serviceType === 'WALLET_WITHDRAWAL'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIncoming(serviceType: string) {
|
function isIncoming(logItem: any) {
|
||||||
|
const money = Number(logItem?.money) || 0
|
||||||
|
if (money > 0) return true
|
||||||
|
const serviceType = logItem?.serviceType
|
||||||
return (
|
return (
|
||||||
serviceType === 'WALLET_REFUND' ||
|
serviceType === 'WALLET_REFUND' ||
|
||||||
serviceType === 'WALLET_RECHARGE' ||
|
serviceType === 'WALLET_RECHARGE' ||
|
||||||
@@ -89,23 +138,109 @@ function isIncoming(serviceType: string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMoneyClass(serviceType: string) {
|
function parseLogDetail(logItem: any) {
|
||||||
if (isOutgoing(serviceType)) return 'out'
|
const raw = String(logItem?.detail || '').trim()
|
||||||
if (isIncoming(serviceType)) return 'in'
|
const serviceType = logItem?.serviceType || ''
|
||||||
return ''
|
const out = isOutgoing(logItem)
|
||||||
|
const inc = isIncoming(logItem)
|
||||||
|
|
||||||
|
const absAmount = unitPrice(Math.abs(Number(logItem?.money) || 0))
|
||||||
|
const formattedMoney = out ? `-${absAmount}` : inc ? `+${absAmount}` : absAmount
|
||||||
|
const moneyClass = out ? 'out' : inc ? 'in' : ''
|
||||||
|
|
||||||
|
let title = '余额变动'
|
||||||
|
let tagText = '变动'
|
||||||
|
let tagClass = 'tag-default'
|
||||||
|
|
||||||
|
const orderMatch = raw.match(/订单\[([^\]]+)\]/)
|
||||||
|
const afterSaleMatch = raw.match(/售后单\[([^\]]+)\]/)
|
||||||
|
const rechargeMatch = raw.match(/充值单号为[::]\s*([A-Za-z0-9_-]+)/)
|
||||||
|
|
||||||
|
if (afterSaleMatch && afterSaleMatch[1] === 'ORDER_CANCEL') {
|
||||||
|
title = '订单取消退回'
|
||||||
|
tagText = '退款'
|
||||||
|
tagClass = 'tag-in'
|
||||||
|
} else if (serviceType === 'WALLET_REFUND' || raw.includes('退还') || raw.includes('返还')) {
|
||||||
|
title = '售后退款入账'
|
||||||
|
tagText = '退款'
|
||||||
|
tagClass = 'tag-in'
|
||||||
|
} else if (serviceType === 'WALLET_PAY' || raw.includes('支付金额')) {
|
||||||
|
title = '订单余额支付'
|
||||||
|
tagText = '消费'
|
||||||
|
tagClass = 'tag-out'
|
||||||
|
} else if (serviceType === 'WALLET_RECHARGE') {
|
||||||
|
title = raw.includes('后台') ? '平台余额充值' : '预存款充值'
|
||||||
|
tagText = '充值'
|
||||||
|
tagClass = 'tag-in'
|
||||||
|
} else if (serviceType === 'WALLET_WITHDRAWAL') {
|
||||||
|
title = out ? '余额提现申请' : '提现退回解冻'
|
||||||
|
tagText = '提现'
|
||||||
|
tagClass = out ? 'tag-out' : 'tag-in'
|
||||||
|
} else if (serviceType === 'WALLET_COMMISSION') {
|
||||||
|
title = '分销佣金入账'
|
||||||
|
tagText = '佣金'
|
||||||
|
tagClass = 'tag-in'
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaList: Array<{ label: string; value: string }> = []
|
||||||
|
if (orderMatch && orderMatch[1]) {
|
||||||
|
metaList.push({ label: '订单编号', value: orderMatch[1] })
|
||||||
|
}
|
||||||
|
if (afterSaleMatch && afterSaleMatch[1] && afterSaleMatch[1] !== 'ORDER_CANCEL') {
|
||||||
|
metaList.push({ label: '售后单号', value: afterSaleMatch[1] })
|
||||||
|
}
|
||||||
|
if (rechargeMatch && rechargeMatch[1]) {
|
||||||
|
metaList.push({ label: '充值单号', value: rechargeMatch[1] })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metaList.length === 0 && raw && !raw.includes('???')) {
|
||||||
|
const cleaned = raw
|
||||||
|
.replace(/[,,]?\s*(支付金额|退还金额|余额返还)\[[^\]]+\]/g, '')
|
||||||
|
.replace(/:\s*\d+(\.\d+)?$/, '')
|
||||||
|
.trim()
|
||||||
|
if (cleaned && cleaned !== title) {
|
||||||
|
metaList.push({ label: '变动说明', value: cleaned })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
tagText,
|
||||||
|
tagClass,
|
||||||
|
formattedMoney,
|
||||||
|
moneyClass,
|
||||||
|
isOut: out,
|
||||||
|
isIn: inc,
|
||||||
|
metaList,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLogMoney(logItem: any) {
|
const decoratedList = computed(() =>
|
||||||
const amount = unitPrice(logItem.money)
|
depositData.value.map((item) => ({
|
||||||
if (isOutgoing(logItem.serviceType)) return `-${amount}`
|
...item,
|
||||||
if (isIncoming(logItem.serviceType)) return `+${amount}`
|
parsed: parseLogDetail(item),
|
||||||
return amount
|
}))
|
||||||
}
|
)
|
||||||
|
|
||||||
|
const filteredData = computed(() => {
|
||||||
|
if (activeFilter.value === 1) {
|
||||||
|
return decoratedList.value.filter((item) => item.parsed.isIn)
|
||||||
|
}
|
||||||
|
if (activeFilter.value === 2) {
|
||||||
|
return decoratedList.value.filter((item) => item.parsed.isOut)
|
||||||
|
}
|
||||||
|
return decoratedList.value
|
||||||
|
})
|
||||||
|
|
||||||
function getWallet() {
|
function getWallet() {
|
||||||
getWalletLog(params.value).then((res) => {
|
getWalletLog(params.value).then((res) => {
|
||||||
if (res.data.success && res.data.result.records.length) {
|
loaded.value = true
|
||||||
depositData.value.push(...res.data.result.records)
|
const records = (res.data?.success && res.data?.result?.records) || []
|
||||||
|
if (records.length) {
|
||||||
|
depositData.value.push(...records)
|
||||||
|
}
|
||||||
|
if (records.length < params.value.pageSize) {
|
||||||
|
hasMore.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -117,123 +252,233 @@ function loadMore() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.wap {
|
.deposit-log-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
background: #f5f6fa;
|
||||||
flex-direction: column;
|
padding-bottom: 48rpx;
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper-show-money {
|
|
||||||
flex-shrink: 0;
|
|
||||||
height: 200rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.money-view {
|
|
||||||
height: 100%;
|
|
||||||
padding: 32rpx 40rpx;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-wrap {
|
||||||
|
padding: 24rpx 24rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-hero {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 36rpx 36rpx 34rpx;
|
||||||
|
color: #ffffff;
|
||||||
|
background: linear-gradient(135deg, $main-color 0%, $light-color 60%, $aider-light-color 100%);
|
||||||
|
box-shadow: 0 12rpx 28rpx rgba(229, 50, 56, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow-1 {
|
||||||
|
width: 220rpx;
|
||||||
|
height: 220rpx;
|
||||||
|
right: -60rpx;
|
||||||
|
top: -70rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow-2 {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
right: 90rpx;
|
||||||
|
bottom: -60rpx;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
|
||||||
color: #fff;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
25deg,
|
|
||||||
$main-color,
|
|
||||||
$light-color,
|
|
||||||
$aider-light-color
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.money-label {
|
.money-label {
|
||||||
font-size: 28rpx;
|
font-size: 25rpx;
|
||||||
opacity: 0.95;
|
color: rgba(255, 255, 255, 0.88);
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.money {
|
.money-row {
|
||||||
margin-top: 12rpx;
|
margin-top: 14rpx;
|
||||||
max-width: 100%;
|
display: flex;
|
||||||
font-size: 56rpx;
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.money-symbol {
|
||||||
|
font-size: 34rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
margin-right: 8rpx;
|
||||||
word-break: break-all;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper-tabs {
|
.money-value {
|
||||||
flex: 1;
|
font-size: 58rpx;
|
||||||
min-height: 0;
|
font-weight: 700;
|
||||||
padding: 24rpx;
|
line-height: 1.15;
|
||||||
box-sizing: border-box;
|
letter-spacing: 1rpx;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.swiper-box,
|
.filter-bar {
|
||||||
.swiper-item,
|
display: flex;
|
||||||
.scroll-v {
|
align-items: center;
|
||||||
height: 100%;
|
gap: 16rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-pill {
|
||||||
|
padding: 12rpx 28rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
color: #666666;
|
||||||
|
background: #ffffff;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $main-color;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
padding: 16rpx 24rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-card {
|
.list-card {
|
||||||
background: #fff;
|
display: flex;
|
||||||
border-radius: 16rpx;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
gap: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-item {
|
.log-item {
|
||||||
display: flex;
|
background: #ffffff;
|
||||||
flex-direction: row;
|
border-radius: 20rpx;
|
||||||
align-items: flex-start;
|
padding: 28rpx 30rpx;
|
||||||
justify-content: space-between;
|
box-shadow: 0 2rpx 12rpx rgba(17, 30, 79, 0.03);
|
||||||
padding: 28rpx 32rpx;
|
}
|
||||||
border-bottom: 1px solid #f5f5f5;
|
|
||||||
|
|
||||||
&:last-child {
|
.log-row-main {
|
||||||
border-bottom: none;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4rpx 12rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
|
&.tag-in {
|
||||||
|
color: #00a870;
|
||||||
|
background: rgba(0, 168, 112, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-out {
|
||||||
|
color: #ff4d4f;
|
||||||
|
background: rgba(255, 77, 79, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-default {
|
||||||
|
color: #666666;
|
||||||
|
background: #f0f2f5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-item-detail {
|
.log-title {
|
||||||
flex: 1;
|
font-size: 30rpx;
|
||||||
min-width: 0;
|
|
||||||
padding-right: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.5;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-item-change {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
align-self: center;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-money {
|
|
||||||
font-size: 34rpx;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
color: #1d2129;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-amount {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: #1d2129;
|
||||||
|
|
||||||
&.in {
|
&.in {
|
||||||
color: $aider-color-green;
|
color: #00a870;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.out {
|
&.out {
|
||||||
color: #ff5a5f;
|
color: #ff4d4f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-time {
|
.log-meta-box {
|
||||||
margin-top: 8rpx;
|
margin-top: 16rpx;
|
||||||
|
padding: 14rpx 18rpx;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-meta-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #4e5969;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-row-foot {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-time {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
color: #999;
|
color: #999999;
|
||||||
line-height: 1.3;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
|
||||||
|
.empty-box {
|
||||||
|
padding-top: 120rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ import { getUserWallet } from '@/api/members'
|
|||||||
import { isLogin, navigateToLogin, unitPrice } from '@/utils/filters.js'
|
import { isLogin, navigateToLogin, unitPrice } from '@/utils/filters.js'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import {
|
||||||
|
prefetchMpMessageTemplates,
|
||||||
|
subscribeMpMessageByScenes,
|
||||||
|
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
@@ -52,6 +57,9 @@ const walletNum = ref(0)
|
|||||||
|
|
||||||
onShow(async () => {
|
onShow(async () => {
|
||||||
if (isLogin('auth')) {
|
if (isLogin('auth')) {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
// #endif
|
||||||
const result = await getUserWallet()
|
const result = await getUserWallet()
|
||||||
walletNum.value = result.data.result.memberWallet
|
walletNum.value = result.data.result.memberWallet
|
||||||
} else {
|
} else {
|
||||||
@@ -60,7 +68,15 @@ onShow(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
const doBack = () => {
|
||||||
|
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['WALLET_CHANGE'], doBack)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doBack()
|
||||||
|
// #endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateTo(url: string) {
|
function navigateTo(url: string) {
|
||||||
|
|||||||
@@ -1,41 +1,114 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle" class="wap">
|
<view :style="themeStyle" class="withdraw-log-page">
|
||||||
<u-navbar
|
<u-navbar
|
||||||
title="提现记录"
|
title="提现记录"
|
||||||
:fixed="true"
|
:fixed="true"
|
||||||
:placeholder="true"
|
:placeholder="true"
|
||||||
:border="false"
|
:border="false"
|
||||||
:auto-back="true"
|
:auto-back="false"
|
||||||
|
@leftClick="back"
|
||||||
></u-navbar>
|
></u-navbar>
|
||||||
<scroll-view class="scroll-v view-wrapper" enableBackToTop="true" scroll-with-animation scroll-y @scrolltolower="loadMore">
|
|
||||||
<view v-if="records.length != 0" class="view-item" v-for="(item, index) in records" :key="index">
|
<!-- 状态筛选胶囊栏 -->
|
||||||
<view class="view-item-detail">
|
<view class="filter-bar">
|
||||||
<view class="-title">{{ withdrawStatusText(item.applyStatus) }}</view>
|
<view
|
||||||
<view class="-number" v-if="item.inspectRemark || item.errorMessage">{{ item.inspectRemark || item.errorMessage }}</view>
|
v-for="(tab, idx) in filterTabs"
|
||||||
</view>
|
:key="tab.key"
|
||||||
<view class="view-item-change">
|
class="filter-pill"
|
||||||
<view class="-money">-{{unitPrice(item.applyMoney) }}</view>
|
:class="{ active: activeFilter === idx }"
|
||||||
<view class="-time">{{ item.createTime || item.inspectTime || "" }}</view>
|
@click="activeFilter = idx"
|
||||||
<view v-if="item.applyStatus === 'WAIT_USER_CONFIRM'" class="confirm-btn" @click.stop="confirmWechatReceive(item)">确认收款</view>
|
>
|
||||||
|
{{ tab.name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 提现记录列表 -->
|
||||||
|
<view class="list-container">
|
||||||
|
<view v-if="filteredRecords.length > 0" class="list-card">
|
||||||
|
<view
|
||||||
|
class="record-card"
|
||||||
|
v-for="(item, index) in filteredRecords"
|
||||||
|
:key="item.id || index"
|
||||||
|
>
|
||||||
|
<!-- 第一行:状态标签 + 标题 | 提现金额 -->
|
||||||
|
<view class="card-row-main">
|
||||||
|
<view class="title-wrap">
|
||||||
|
<text class="status-tag" :class="item.parsed.statusClass">
|
||||||
|
{{ item.parsed.statusText }}
|
||||||
|
</text>
|
||||||
|
<text class="card-title">余额提现</text>
|
||||||
|
</view>
|
||||||
|
<view class="amount-wrap">
|
||||||
|
<text class="amount-symbol">¥</text>
|
||||||
|
<text class="amount-val">{{ unitPrice(item.applyMoney) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 第二行:单号、审核备注或失败原因 -->
|
||||||
|
<view
|
||||||
|
v-if="item.sn || item.inspectRemark || item.errorMessage || item.inspectTime"
|
||||||
|
class="meta-box"
|
||||||
|
>
|
||||||
|
<view v-if="item.sn" class="meta-line">
|
||||||
|
<text class="meta-label">提现单号:</text>
|
||||||
|
<text class="meta-val mono">{{ item.sn }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.inspectTime" class="meta-line">
|
||||||
|
<text class="meta-label">审核时间:</text>
|
||||||
|
<text class="meta-val">{{ item.inspectTime }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.inspectRemark || item.errorMessage" class="meta-line">
|
||||||
|
<text class="meta-label">备注说明:</text>
|
||||||
|
<text class="meta-val remark-text">{{ item.inspectRemark || item.errorMessage }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 第三行:申请时间 + 确认收款按钮 -->
|
||||||
|
<view class="card-row-foot">
|
||||||
|
<text class="apply-time">申请时间:{{ item.createTime || item.inspectTime || '-' }}</text>
|
||||||
|
<view
|
||||||
|
v-if="item.applyStatus === 'WAIT_USER_CONFIRM'"
|
||||||
|
class="confirm-btn"
|
||||||
|
@click.stop="confirmWechatReceive(item)"
|
||||||
|
>
|
||||||
|
确认收款
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<u-empty v-if="loaded && records.length == 0" mode="history" text="暂无记录" />
|
<view v-else-if="loaded" class="empty-box">
|
||||||
</scroll-view>
|
<u-empty mode="history" text="暂无提现记录" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow, onReachBottom } from '@dcloudio/uni-app'
|
||||||
import { getWithdrawApplyPage, getWithdrawApplyWechatTransferInfo } from '@/api/members'
|
import { getWithdrawApplyPage, getWithdrawApplyWechatTransferInfo } from '@/api/members'
|
||||||
import { unitPrice } from '@/utils/filters.js'
|
import { unitPrice } from '@/utils/filters.js'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import {
|
||||||
|
prefetchMpMessageTemplates,
|
||||||
|
subscribeMpMessageByScenes,
|
||||||
|
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
const loaded = ref(false)
|
const loaded = ref(false)
|
||||||
|
const hasMore = ref(true)
|
||||||
|
const activeFilter = ref(0)
|
||||||
|
const filterTabs = [
|
||||||
|
{ key: 'ALL', name: '全部' },
|
||||||
|
{ key: 'PENDING', name: '处理中' },
|
||||||
|
{ key: 'SUCCESS', name: '已到账' },
|
||||||
|
{ key: 'FAIL', name: '未通过/失败' },
|
||||||
|
]
|
||||||
|
|
||||||
const params = ref({
|
const params = ref({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@@ -44,40 +117,88 @@ const params = ref({
|
|||||||
const records = ref<any[]>([])
|
const records = ref<any[]>([])
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
// 预拉取订阅模板,保证点击左上角返回时可同步拉起微信订阅弹窗
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
// #endif
|
||||||
|
reloadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 左上角返回:提示订阅「提现结果通知」后再返回
|
||||||
|
function back() {
|
||||||
|
const doBack = () => {
|
||||||
|
if (getCurrentPages().length > 1) {
|
||||||
|
uni.navigateBack({ delta: 1 })
|
||||||
|
} else {
|
||||||
|
uni.redirectTo({ url: '/pages/mine/deposit/operation' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['WALLET_WITHDRAWAL_AUDIT_ERROR'], doBack)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doBack()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!hasMore.value) return
|
||||||
|
loadMore()
|
||||||
|
})
|
||||||
|
|
||||||
|
function reloadData() {
|
||||||
params.value.pageNumber = 1
|
params.value.pageNumber = 1
|
||||||
|
hasMore.value = true
|
||||||
records.value = []
|
records.value = []
|
||||||
loaded.value = false
|
loaded.value = false
|
||||||
getData()
|
getData()
|
||||||
})
|
}
|
||||||
|
|
||||||
function withdrawStatusText(applyStatus: string) {
|
function parseWithdrawStatus(applyStatus: string) {
|
||||||
switch (applyStatus) {
|
switch (applyStatus) {
|
||||||
case 'APPLY':
|
case 'APPLY':
|
||||||
return '申请中'
|
return { statusText: '申请中', statusClass: 'tag-pending', group: 'PENDING' }
|
||||||
case 'VIA_AUDITING':
|
case 'VIA_AUDITING':
|
||||||
return '审核通过'
|
return { statusText: '审核通过', statusClass: 'tag-pending', group: 'PENDING' }
|
||||||
case 'D_VIA_AUDITING':
|
case 'D_VIA_AUDITING':
|
||||||
return '分销提现审核通过'
|
return { statusText: '分销审核通过', statusClass: 'tag-pending', group: 'PENDING' }
|
||||||
case 'FAIL_AUDITING':
|
|
||||||
return '审核未通过'
|
|
||||||
case 'D_FAIL_AUDITING':
|
|
||||||
return '分销提现审核未通过'
|
|
||||||
case 'WAIT_USER_CONFIRM':
|
case 'WAIT_USER_CONFIRM':
|
||||||
return '等待用户确认'
|
return { statusText: '待确认收款', statusClass: 'tag-action', group: 'PENDING' }
|
||||||
case 'SUCCESS':
|
case 'SUCCESS':
|
||||||
return '提现成功'
|
return { statusText: '提现成功', statusClass: 'tag-success', group: 'SUCCESS' }
|
||||||
|
case 'FAIL_AUDITING':
|
||||||
|
return { statusText: '审核未通过', statusClass: 'tag-fail', group: 'FAIL' }
|
||||||
|
case 'D_FAIL_AUDITING':
|
||||||
|
return { statusText: '分销审核未通过', statusClass: 'tag-fail', group: 'FAIL' }
|
||||||
case 'ERROR':
|
case 'ERROR':
|
||||||
return '提现失败'
|
return { statusText: '提现失败', statusClass: 'tag-fail', group: 'FAIL' }
|
||||||
default:
|
default:
|
||||||
return applyStatus || ''
|
return { statusText: applyStatus || '处理中', statusClass: 'tag-pending', group: 'PENDING' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decoratedRecords = computed(() =>
|
||||||
|
records.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
parsed: parseWithdrawStatus(item.applyStatus),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredRecords = computed(() => {
|
||||||
|
const targetKey = filterTabs[activeFilter.value]?.key || 'ALL'
|
||||||
|
if (targetKey === 'ALL') return decoratedRecords.value
|
||||||
|
return decoratedRecords.value.filter((item) => item.parsed.group === targetKey)
|
||||||
|
})
|
||||||
|
|
||||||
function getData() {
|
function getData() {
|
||||||
getWithdrawApplyPage(params.value).then((res) => {
|
getWithdrawApplyPage(params.value).then((res) => {
|
||||||
loaded.value = true
|
loaded.value = true
|
||||||
if (res.data.success && res.data.result.records.length != 0) {
|
const list = (res.data?.success && res.data?.result?.records) || []
|
||||||
records.value.push(...res.data.result.records)
|
if (list.length > 0) {
|
||||||
|
records.value.push(...list)
|
||||||
|
}
|
||||||
|
if (list.length < params.value.pageSize) {
|
||||||
|
hasMore.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -119,7 +240,8 @@ function confirmWechatReceive(item: any) {
|
|||||||
|
|
||||||
const openResultToast = (errMsg?: string) => {
|
const openResultToast = (errMsg?: string) => {
|
||||||
if (errMsg === 'requestMerchantTransfer:ok') {
|
if (errMsg === 'requestMerchantTransfer:ok') {
|
||||||
uni.showToast({ title: '已唤起确认页面', duration: 2000, icon: 'none' })
|
uni.showToast({ title: '已确认收款', duration: 2000, icon: 'none' })
|
||||||
|
setTimeout(() => reloadData(), 800)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (errMsg === 'requestMerchantTransfer:cancel') {
|
if (errMsg === 'requestMerchantTransfer:cancel') {
|
||||||
@@ -175,55 +297,191 @@ function confirmWechatReceive(item: any) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.view-item {
|
.withdraw-log-page {
|
||||||
padding: 32rpx;
|
min-height: 100vh;
|
||||||
|
background: #f5f6fa;
|
||||||
|
padding-bottom: 48rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx 24rpx 8rpx;
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
.view-item-change {
|
|
||||||
text-align: right;
|
.filter-pill {
|
||||||
> .-money {
|
flex-shrink: 0;
|
||||||
font-size: 36rpx;
|
padding: 12rpx 28rpx;
|
||||||
color: $main-color;
|
border-radius: 999rpx;
|
||||||
font-weight: bold;
|
font-size: 25rpx;
|
||||||
}
|
color: #666666;
|
||||||
> .-time {
|
background: #ffffff;
|
||||||
font-size: 22rpx;
|
font-weight: 500;
|
||||||
color: #999;
|
transition: all 0.2s;
|
||||||
}
|
|
||||||
> .confirm-btn {
|
&.active {
|
||||||
display: inline-block;
|
|
||||||
margin-top: 12rpx;
|
|
||||||
padding: 12rpx 20rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
background: $main-color;
|
background: $main-color;
|
||||||
color: #fff;
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.view-item-detail {
|
|
||||||
line-height: 1.75;
|
.list-container {
|
||||||
|
padding: 16rpx 24rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 28rpx 30rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(17, 30, 79, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
}
|
||||||
> .-title {
|
|
||||||
font-size: 28rpx;
|
.status-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 21rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
line-height: 1.35;
|
||||||
|
|
||||||
|
&.tag-pending {
|
||||||
|
color: #ff7d00;
|
||||||
|
background: rgba(255, 125, 0, 0.1);
|
||||||
}
|
}
|
||||||
> .-number {
|
|
||||||
font-size: 22rpx;
|
&.tag-action {
|
||||||
color: #999;
|
color: $main-color;
|
||||||
overflow: hidden;
|
background: rgba(229, 50, 56, 0.1);
|
||||||
text-overflow: ellipsis;
|
}
|
||||||
white-space: nowrap;
|
|
||||||
|
&.tag-success {
|
||||||
|
color: #00a870;
|
||||||
|
background: rgba(0, 168, 112, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tag-fail {
|
||||||
|
color: #f53f3f;
|
||||||
|
background: rgba(245, 63, 63, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wap {
|
.card-title {
|
||||||
width: 100%;
|
font-size: 30rpx;
|
||||||
height: calc(100vh - 44px);
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-v {
|
.amount-wrap {
|
||||||
height: calc(100vh - 88rpx);
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-val {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-box {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 14rpx 18rpx;
|
||||||
|
background: #f8f9fb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 23rpx;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-val {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
color: #4e5969;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
|
&.mono {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.remark-text {
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row-foot {
|
||||||
|
margin-top: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apply-time {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999999;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 10rpx 28rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
background: linear-gradient(135deg, $main-color 0%, $light-color 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(229, 50, 56, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-box {
|
||||||
|
padding-top: 140rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -118,12 +118,23 @@ onMounted(async () => {
|
|||||||
withdrawRule.value = res.data.result.withdrawRule || ''
|
withdrawRule.value = res.data.result.withdrawRule || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let submitting = false
|
||||||
|
|
||||||
function cashd() {
|
function cashd() {
|
||||||
|
if (submitting) return
|
||||||
const amount = Number(price.value)
|
const amount = Number(price.value)
|
||||||
if (!proxy.$u.test.amount(parseInt(String(amount)))) {
|
if (!(amount > 0) || !proxy.$u.test.amount(parseInt(String(amount)))) {
|
||||||
uni.showToast({ title: '请输入正确金额', duration: 2000, icon: 'none' })
|
uni.showToast({ title: '请输入正确金额', duration: 2000, icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (minPrice.value && amount < Number(minPrice.value)) {
|
||||||
|
uni.showToast({ title: `最低提现金额为 ${minPrice.value} 元`, duration: 2000, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (amount > Number(walletNum.value || 0)) {
|
||||||
|
uni.showToast({ title: '提现金额不能大于可提现金额', duration: 2000, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const params: Record<string, any> = { price: amount }
|
const params: Record<string, any> = { price: amount }
|
||||||
if (type.value === 'ALI') {
|
if (type.value === 'ALI') {
|
||||||
@@ -139,14 +150,23 @@ function cashd() {
|
|||||||
params.realName = realName.value
|
params.realName = realName.value
|
||||||
}
|
}
|
||||||
|
|
||||||
withdrawalApply(params).then((res) => {
|
const doSubmit = () => {
|
||||||
if (res.data.success) {
|
submitting = true
|
||||||
uni.showToast({ title: '提现成功!', duration: 2000, icon: 'none' })
|
withdrawalApply(params)
|
||||||
setTimeout(() => {
|
.then((res) => {
|
||||||
uni.navigateBack({ delta: 1 })
|
if (res.data.success) {
|
||||||
}, 1000)
|
uni.showToast({ title: '提现申请已提交', duration: 2000, icon: 'none' })
|
||||||
}
|
setTimeout(() => {
|
||||||
})
|
uni.navigateBack({ delta: 1 })
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitting = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
doSubmit()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAll() {
|
function handleAll() {
|
||||||
|
|||||||
164
pages/mine/experience.vue
Normal file
164
pages/mine/experience.vue
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
<template>
|
||||||
|
<view class="experience-page">
|
||||||
|
<view class="summary-card">
|
||||||
|
<text class="summary-label">当前经验值</text><text class="summary-number">{{ summaryLoaded ? formatNumber(experience) : '—' }}</text>
|
||||||
|
<text class="summary-grade">{{ gradeName || '经验积累,解锁更多等级权益' }}</text>
|
||||||
|
<text v-if="logError && !summaryLoaded" class="summary-retry" @click="loadLogs(true)">经验值加载失败,点击重试</text>
|
||||||
|
</view>
|
||||||
|
<view class="tabs"><view class="tab" :class="{ active: activeTab === 'tasks' }" @click="activeTab = 'tasks'">经验值任务</view><view class="tab" :class="{ active: activeTab === 'logs' }" @click="activeTab = 'logs'">经验值明细</view></view>
|
||||||
|
<view v-if="activeTab === 'tasks'">
|
||||||
|
<view class="section-heading"><text>做任务,积累经验</text></view>
|
||||||
|
<view v-if="taskLoading && !tasksLoaded" class="state">正在加载任务…</view>
|
||||||
|
<view v-else-if="taskError" class="state"><text>任务加载失败</text><button class="retry" @click="loadTasks">点击重试</button></view>
|
||||||
|
<template v-else>
|
||||||
|
<view v-for="item in tasks" :key="item.ruleKey" class="task-card">
|
||||||
|
<view class="task-main"><view class="task-title-row"><text class="task-title">{{ taskName(item) }}</text><text class="reward">{{ rewardText(item) }}</text></view>
|
||||||
|
<text class="task-description">{{ taskDescription(item) }}</text>
|
||||||
|
<text class="task-progress">累计获得 {{ formatNumber(item.gainedExperience) }} 经验<text v-if="hasLimit(item)"> · 上限 {{ formatNumber(item.maxValue) }}</text></text>
|
||||||
|
</view>
|
||||||
|
<view class="task-footer"><text class="task-status">{{ statusText(item) }}</text><button class="task-button" :class="{ finished: isFinished(item) }" :disabled="isFinished(item)" @click="doTask(item)">{{ actionText(item) }}</button></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="tasksLoaded && !tasks.length" class="state"><u-icon name="list" size="36" color="#bec4ce" /><text>暂无已开启的经验值任务</text></view>
|
||||||
|
<view v-if="description" class="rules-note"><text class="note-title">经验值说明</text><text>{{ description }}</text></view>
|
||||||
|
<text class="page-note">完成相应行为并满足规则后,由系统自动发放经验值,无需手动领取。</text>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
<view v-else>
|
||||||
|
<view class="section-heading"><text>经验值变动记录</text><text class="section-caption">按时间倒序展示</text></view>
|
||||||
|
<view v-if="logsLoading && !records.length" class="state">正在加载明细…</view>
|
||||||
|
<view v-else-if="!records.length && logError" class="state"><text>明细加载失败</text><button class="retry" @click="retryLogs">点击重试</button></view>
|
||||||
|
<view v-else-if="!records.length" class="state"><u-icon name="list" size="36" color="#bec4ce" /><text>暂无经验值明细</text><text class="state-hint">完成已开启的任务后,可在这里查看</text></view>
|
||||||
|
<view v-else class="record-list">
|
||||||
|
<view v-for="item in records" :key="item.id" class="record-row"><view class="record-content"><text class="record-title">{{ item.content || ruleNames[item.ruleKey] || '经验值变动' }}</text><text class="record-time">{{ item.createTime || '—' }}</text><text class="record-balance">变动后经验:{{ formatNumber(item.experience) }}</text></view><text class="record-change" :class="{ negative: Number(item.variableExperience) < 0 }">{{ changeText(item.variableExperience) }}</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="records.length" class="load-more" @click="retryLogs">{{ logsLoading ? '加载中…' : logError ? '加载失败,点击重试' : hasMore ? '点击或上拉加载更多' : '已显示全部明细' }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad, onShow, onReachBottom, onPullDownRefresh, onUnload } from '@dcloudio/uni-app'
|
||||||
|
import { getExperienceTasks, getExperienceRecords } from '@/api/members'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
|
||||||
|
const activeTab = ref('tasks'), experience = ref(0), gradeName = ref(''), summaryLoaded = ref(false)
|
||||||
|
const tasks = ref([]), description = ref(''), taskLoading = ref(false), tasksLoaded = ref(false), taskError = ref(false)
|
||||||
|
const records = ref([]), logsLoading = ref(false), logError = ref(false), hasMore = ref(true)
|
||||||
|
let ownerId = '', taskRequest = 0, logRequest = 0, nextPage = 1, retryReset = true
|
||||||
|
const pageSize = 20
|
||||||
|
const ruleNames = { CONSUME: '购物消费', REGISTER: '注册会员', SIGN_IN: '每日签到', COMMENT: '商品评价', SHARE: '分享商城', PROFILE: '完善个人信息', FOLLOW_STORE: '关注店铺', BIND_WECHAT: '绑定微信', ADD_ADDRESS: '添加收货地址', SHARE_REGISTER: '邀请注册', SHARE_BUY: '邀请购买' }
|
||||||
|
const onceRules = new Set(['REGISTER', 'PROFILE', 'BIND_WECHAT', 'ADD_ADDRESS'])
|
||||||
|
const taskDescriptions = { CONSUME: '订单完成后,按实际消费金额向下取整计算经验。', REGISTER: '注册成功后获得一次经验奖励。', SIGN_IN: '每天签到可获得一次经验奖励。', COMMENT: '对已购买商品提交超过30字的评价,满足条件后获得经验。', SHARE: '通过商城的分享入口分享页面,满足条件后获得经验。', PROFILE: '完善个人信息,满足条件后获得一次经验奖励。', FOLLOW_STORE: '关注店铺可获得经验,同一店铺不重复发放。', BIND_WECHAT: '成功绑定微信,满足条件后获得一次经验奖励。', ADD_ADDRESS: '添加收货地址后获得一次经验奖励。', SHARE_REGISTER: '通过分享邀请新用户注册成功后获得经验。', SHARE_BUY: '通过分享邀请用户购买并满足奖励条件后获得经验。' }
|
||||||
|
function formatNumber(value) { return Math.max(0, Number(value) || 0).toLocaleString('en-US') }
|
||||||
|
function changeText(value) { const n = Number(value) || 0; return `${n > 0 ? '+' : n < 0 ? '-' : ''}${Math.abs(n).toLocaleString('en-US')}` }
|
||||||
|
function taskName(item) { return item.ruleName || ruleNames[item.ruleKey] || '经验值任务' }
|
||||||
|
function taskDescription(item) { return taskDescriptions[item.ruleKey] || '按商城配置的规则完成相应行为后获得经验。' }
|
||||||
|
function rewardText(item) { return item.ruleKey === 'CONSUME' ? `每1元 +${formatNumber(item.value)}` : `+${formatNumber(item.value)} 经验` }
|
||||||
|
function hasLimit(item) { return item.ruleKey !== 'CONSUME' && item.maxValue != null && Number(item.maxValue) > 0 }
|
||||||
|
function reachedLimit(item) { return hasLimit(item) && Number(item.gainedExperience) >= Number(item.maxValue) }
|
||||||
|
function isFinished(item) { return reachedLimit(item) || (onceRules.has(item.ruleKey) && Boolean(item.completed)) }
|
||||||
|
function statusText(item) { return reachedLimit(item) ? '已达经验上限' : isFinished(item) ? '已完成' : item.ruleKey === 'SIGN_IN' ? '每日可参与' : onceRules.has(item.ruleKey) ? '仅限一次' : '满足条件可获得' }
|
||||||
|
function actionText(item) { return isFinished(item) ? '已完成' : item.ruleKey === 'SIGN_IN' ? '去签到' : item.ruleKey === 'PROFILE' ? '去完善' : item.ruleKey === 'ADD_ADDRESS' ? '去添加' : item.ruleKey === 'COMMENT' ? '去评价' : item.ruleKey === 'CONSUME' || item.ruleKey === 'FOLLOW_STORE' || item.ruleKey.startsWith('SHARE') ? '去逛逛' : '查看规则' }
|
||||||
|
function doTask(item) {
|
||||||
|
if (isFinished(item)) return
|
||||||
|
const routes = { SIGN_IN: '/pages/mine/signIn', PROFILE: '/pages/mine/set/personMsg', ADD_ADDRESS: '/pages/mine/address/addressManage', COMMENT: '/pages/order/myOrder?status=4' }
|
||||||
|
if (routes[item.ruleKey]) uni.navigateTo({ url: routes[item.ruleKey] })
|
||||||
|
else if (item.ruleKey === 'CONSUME' || item.ruleKey === 'FOLLOW_STORE' || item.ruleKey.startsWith('SHARE')) uni.switchTab({ url: '/pages/tabbar/home/index' })
|
||||||
|
else uni.showModal({ title: taskName(item), content: taskDescription(item), showCancel: false })
|
||||||
|
}
|
||||||
|
function stillOwner(memberId) { return ownerId === memberId && String(isLogin()?.id || '') === memberId }
|
||||||
|
function reset() {
|
||||||
|
taskRequest++; logRequest++; ownerId = ''
|
||||||
|
tasks.value = []; records.value = []; description.value = ''; experience.value = 0; gradeName.value = ''
|
||||||
|
summaryLoaded.value = false; tasksLoaded.value = false; taskLoading.value = false; logsLoading.value = false
|
||||||
|
taskError.value = false; logError.value = false; hasMore.value = true; nextPage = 1; retryReset = true
|
||||||
|
}
|
||||||
|
async function loadTasks() {
|
||||||
|
if (!ownerId) return
|
||||||
|
const memberId = ownerId, id = ++taskRequest
|
||||||
|
taskLoading.value = true; taskError.value = false
|
||||||
|
try {
|
||||||
|
const response = await getExperienceTasks()
|
||||||
|
if (id !== taskRequest || !stillOwner(memberId)) return
|
||||||
|
if (!response.data?.success) throw new Error('tasks')
|
||||||
|
const data = response.data.result || {}
|
||||||
|
// 服务端 /rules 仅返回开启的任务;兼容接口未来增加 enabled 字段。
|
||||||
|
tasks.value = (data.items || []).filter(item => item && item.ruleKey && item.enabled !== false)
|
||||||
|
description.value = data.description || ''; tasksLoaded.value = true
|
||||||
|
} catch { if (id === taskRequest && stillOwner(memberId)) taskError.value = true }
|
||||||
|
finally { if (id === taskRequest) taskLoading.value = false }
|
||||||
|
}
|
||||||
|
async function loadLogs(refresh = false) {
|
||||||
|
if (!ownerId || (!refresh && (logsLoading.value || !hasMore.value))) return
|
||||||
|
const memberId = ownerId, id = ++logRequest, page = refresh ? 1 : nextPage
|
||||||
|
retryReset = refresh; logsLoading.value = true; logError.value = false
|
||||||
|
try {
|
||||||
|
const response = await getExperienceRecords({ pageNumber: page, pageSize })
|
||||||
|
if (id !== logRequest || !stillOwner(memberId)) return
|
||||||
|
if (!response.data?.success) throw new Error('logs')
|
||||||
|
const data = response.data.result || {}, pageData = data.experienceLogs || {}, rows = pageData.records || []
|
||||||
|
experience.value = data.experience || 0; gradeName.value = data.grade?.gradeName || ''; summaryLoaded.value = true
|
||||||
|
const previous = refresh ? [] : records.value
|
||||||
|
const ids = new Set(previous.map(item => String(item.id)))
|
||||||
|
records.value = previous.concat(rows.filter(item => !ids.has(String(item.id))))
|
||||||
|
hasMore.value = pageData.total != null ? page * pageSize < Number(pageData.total) : rows.length === pageSize
|
||||||
|
nextPage = page + 1; retryReset = false
|
||||||
|
} catch { if (id === logRequest && stillOwner(memberId)) logError.value = true }
|
||||||
|
finally { if (id === logRequest) logsLoading.value = false }
|
||||||
|
}
|
||||||
|
function retryLogs() { if (!logsLoading.value) loadLogs(retryReset) }
|
||||||
|
async function refresh() {
|
||||||
|
const memberId = String(isLogin()?.id || '')
|
||||||
|
if (!memberId) { reset(); uni.stopPullDownRefresh(); navigateToLogin(); return }
|
||||||
|
if (ownerId !== memberId) { reset(); ownerId = memberId }
|
||||||
|
await Promise.all([loadTasks(), loadLogs(true)])
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
}
|
||||||
|
onLoad(options => { activeTab.value = options?.tab === 'logs' ? 'logs' : 'tasks' })
|
||||||
|
onShow(refresh)
|
||||||
|
onPullDownRefresh(refresh)
|
||||||
|
onReachBottom(() => { if (activeTab.value === 'logs' && !logError.value) loadLogs(false) })
|
||||||
|
onUnload(() => { taskRequest++; logRequest++ })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.experience-page { min-height: 100vh; box-sizing: border-box; padding: 24rpx 28rpx calc(36rpx + env(safe-area-inset-bottom)); background: #f5f6f9; color: #292f39; }
|
||||||
|
.summary-card { background: linear-gradient(115deg, #51443b, #8e7351); border-radius: 24rpx; padding: 32rpx; color: #fff1d8; display: flex; flex-direction: column; align-items: flex-start; }
|
||||||
|
.summary-label { font-size: 24rpx; opacity: .8; }
|
||||||
|
.summary-number { font-size: 58rpx; font-weight: 600; line-height: 1.5; }
|
||||||
|
.summary-grade { margin-top: 4rpx; font-size: 23rpx; opacity: .8; }
|
||||||
|
.summary-retry { margin-top: 16rpx; font-size: 23rpx; text-decoration: underline; }
|
||||||
|
.tabs { display: flex; margin: 24rpx 0 32rpx; padding: 8rpx; background: #eceef2; border-radius: 18rpx; }
|
||||||
|
.tab { flex: 1; padding: 18rpx 0; text-align: center; font-size: 27rpx; color: #858c97; border-radius: 14rpx; }
|
||||||
|
.tab.active { color: #896337; background: #fff; font-weight: 600; }
|
||||||
|
.section-heading { display: flex; justify-content: space-between; align-items: center; font-size: 28rpx; font-weight: 600; margin-bottom: 20rpx; }
|
||||||
|
.section-caption { color: #9ba2ad; font-size: 21rpx; font-weight: 400; }
|
||||||
|
.task-card { padding: 26rpx; margin-bottom: 20rpx; background: #fff; border-radius: 20rpx; }
|
||||||
|
.task-title-row { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 10rpx; }
|
||||||
|
.task-title { font-size: 29rpx; font-weight: 600; }
|
||||||
|
.reward { font-size: 25rpx; color: #aa7a3c; }
|
||||||
|
.task-description { display: block; font-size: 24rpx; color: #89919d; margin-top: 14rpx; line-height: 1.7; }
|
||||||
|
.task-progress { display: block; font-size: 22rpx; color: #a1a8b2; margin-top: 12rpx; }
|
||||||
|
.task-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 20rpx; padding-top: 18rpx; border-top: 1rpx solid #f2f3f6; }
|
||||||
|
.task-status { color: #929aa5; font-size: 22rpx; }
|
||||||
|
.task-button { padding: 0 28rpx; margin: 0; line-height: 58rpx; font-size: 24rpx; border-radius: 32rpx; color: #886135; background: #f5ead8; }
|
||||||
|
.task-button.finished { background: #f2f3f5; color: #a2a8b0; }
|
||||||
|
button::after { border: none; }
|
||||||
|
.state { padding: 80rpx 20rpx; display: flex; flex-direction: column; align-items: center; gap: 20rpx; text-align: center; font-size: 26rpx; color: #949ca8; }
|
||||||
|
.state-hint { font-size: 23rpx; }
|
||||||
|
.retry { margin: 0; padding: 0 30rpx; line-height: 64rpx; background: #f4e8d5; color: #927043; font-size: 25rpx; border-radius: 32rpx; }
|
||||||
|
.rules-note { padding: 24rpx; background: #fff; border-radius: 18rpx; font-size: 24rpx; color: #8b939f; line-height: 1.8; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.rules-note text { display: block; }
|
||||||
|
.note-title { color: #737d8b; font-weight: 500; margin-bottom: 8rpx; }
|
||||||
|
.page-note { display: block; margin-top: 24rpx; color: #a0a7b1; font-size: 22rpx; line-height: 1.8; }
|
||||||
|
.record-list { background: #fff; padding: 0 26rpx; border-radius: 20rpx; }
|
||||||
|
.record-row { display: flex; align-items: center; gap: 24rpx; padding: 26rpx 0; border-bottom: 1rpx solid #f0f2f5; }
|
||||||
|
.record-row:last-child { border-bottom: none; }
|
||||||
|
.record-content { flex: 1; min-width: 0; }
|
||||||
|
.record-title { display: block; font-size: 27rpx; line-height: 1.6; overflow-wrap: anywhere; }
|
||||||
|
.record-time, .record-balance { display: block; margin-top: 8rpx; color: #a0a7b1; font-size: 22rpx; }
|
||||||
|
.record-change { flex-shrink: 0; color: #b28449; font-size: 34rpx; font-weight: 600; }
|
||||||
|
.record-change.negative { color: #798694; }
|
||||||
|
.load-more { padding: 30rpx 0; text-align: center; color: #a0a7b1; font-size: 23rpx; }
|
||||||
|
</style>
|
||||||
230
pages/mine/memberBenefits.vue
Normal file
230
pages/mine/memberBenefits.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<template>
|
||||||
|
<view class="benefits-page">
|
||||||
|
<view v-if="loading && !loaded" class="page-skeleton" aria-label="正在加载会员权益">
|
||||||
|
<view class="skeleton skeleton-card" /><view class="skeleton skeleton-growth" />
|
||||||
|
<view class="skeleton-grid"><view v-for="n in 4" :key="n" class="skeleton skeleton-benefit" /></view>
|
||||||
|
</view>
|
||||||
|
<view v-else-if="error && !loaded" class="page-state">
|
||||||
|
<u-icon name="info-circle" size="40" color="#b4bac4" /><text class="state-title">权益加载失败</text>
|
||||||
|
<text class="state-description">请检查网络后重试</text><button class="retry-button" @click="load">重新加载</button>
|
||||||
|
</view>
|
||||||
|
<template v-else-if="loaded">
|
||||||
|
<view v-if="error" class="refresh-error" @click="load">刷新失败,点击重试</view>
|
||||||
|
<view v-if="grades.length">
|
||||||
|
<swiper class="grade-swiper" :current="selectedIndex" :duration="250" :disable-touch="grades.length < 2" @change="onGradeSwipe">
|
||||||
|
<swiper-item v-for="item in grades" :key="item.grade.id">
|
||||||
|
<MemberGradeCard :grade="item.grade" :benefit-count="item.benefits?.length || 0" :is-current="sameId(current.gradeId, item.grade.id)" />
|
||||||
|
</swiper-item>
|
||||||
|
</swiper>
|
||||||
|
<view v-if="grades.length > 1" class="swipe-hint"><text>左右滑动查看等级</text><text>{{ selectedIndex + 1 }} / {{ grades.length }}</text></view>
|
||||||
|
<view class="growth-card">
|
||||||
|
<view class="section-heading"><text class="section-title">我的成长</text><text class="section-caption">{{ current.grade?.gradeName || '暂无等级' }}</text></view>
|
||||||
|
<view class="experience-row"><text class="experience-number">{{ number(current.experience) }}</text><text class="experience-label">当前经验值</text></view>
|
||||||
|
<template v-if="current.nextGrade">
|
||||||
|
<view class="growth-track"><view class="growth-fill" :style="{ width: progress + '%' }" /></view>
|
||||||
|
<view class="growth-labels"><text>{{ number(growthBaseline) }}</text><text>{{ number(current.nextGrade.requiredExperience) }}</text></view>
|
||||||
|
<text v-if="Number(current.remainingExperience) > 0" class="growth-description">再获得 <text class="accent">{{ number(current.remainingExperience) }}</text> 经验值,即可升级为{{ current.nextGrade.gradeName }}</text>
|
||||||
|
<text v-else class="growth-description">已达到{{ current.nextGrade.gradeName }}的经验门槛,等级以系统更新结果为准</text>
|
||||||
|
</template>
|
||||||
|
<view v-else-if="current.highestGrade" class="highest-grade"><u-icon name="checkmark-circle" size="16" color="#a47a3b" /><text>已达到最高等级,尽享专属权益</text></view>
|
||||||
|
<text v-else class="growth-description">暂无升级信息</text>
|
||||||
|
<view class="growth-actions">
|
||||||
|
<view class="growth-action" @click="openExperience('tasks')"><u-icon name="level" size="17" color="#9a7545" /><text>经验值任务</text><u-icon name="arrow-right" size="11" color="#b4bac4" /></view>
|
||||||
|
<view class="growth-action" @click="openExperience('logs')"><u-icon name="list" size="17" color="#9a7545" /><text>经验值明细</text><u-icon name="arrow-right" size="11" color="#b4bac4" /></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="benefits-section">
|
||||||
|
<view class="section-heading"><text class="section-title">专属权益 <text class="benefit-count">{{ selected.benefits?.length || 0 }}项</text></text><text class="section-caption">点击查看详情</text></view>
|
||||||
|
<view v-if="selected.benefits?.length" class="benefit-grid">
|
||||||
|
<view v-for="benefit in selected.benefits" :key="benefit.id" class="benefit-card" @click="detail = benefit">
|
||||||
|
<view class="benefit-topline"><view class="benefit-icon-wrap">
|
||||||
|
<image v-if="benefit.benefitLogo && !failedBenefitIcons[benefit.id]" class="benefit-icon" :src="benefit.benefitLogo" mode="aspectFit" @error="failedBenefitIcons[benefit.id] = true" />
|
||||||
|
<u-icon v-else name="gift" size="28" color="#ac844e" />
|
||||||
|
</view><u-icon name="arrow-right" size="12" color="#c4c8ce" /></view>
|
||||||
|
<text class="benefit-name">{{ benefit.benefitName }}</text><text class="benefit-summary">{{ summary(benefit) }}</text>
|
||||||
|
<text class="benefit-state" :class="{ owned: owned(benefit) }">{{ benefitState(benefit) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-benefits"><u-icon name="gift" size="36" color="#c1c6ce" /><text>该等级暂未配置权益</text><text class="state-description">可以切换其他等级查看</text></view>
|
||||||
|
</view>
|
||||||
|
<view class="page-notice"><text>权益说明</text><text>各等级权益独立配置,以当前等级实际享有的权益为准。具体适用条件与发放规则请查看权益详情。</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="page-state"><u-icon name="gift" size="40" color="#b4bac4" /><text class="state-title">暂无会员等级</text><text class="state-description">等级权益配置后将在这里展示</text></view>
|
||||||
|
</template>
|
||||||
|
<view v-if="detail" class="detail-overlay">
|
||||||
|
<view class="detail-backdrop" @click="detail = null" @touchmove.stop.prevent />
|
||||||
|
<view class="detail-sheet">
|
||||||
|
<view class="sheet-handle" /><view class="detail-header"><text>权益详情</text><view class="close-icon" aria-label="关闭权益详情" @click="detail = null"><u-icon name="close" size="20" color="#707783" /></view></view>
|
||||||
|
<scroll-view scroll-y class="detail-scroll">
|
||||||
|
<view class="detail-content">
|
||||||
|
<view class="detail-identity">
|
||||||
|
<image v-if="detail.benefitLogo && !failedBenefitIcons[detail.id]" class="detail-icon" :src="detail.benefitLogo" mode="aspectFit" @error="failedBenefitIcons[detail.id] = true" />
|
||||||
|
<u-icon v-else name="gift" size="36" color="#ac844e" /><text class="detail-title">{{ detail.benefitName }}</text>
|
||||||
|
<text class="benefit-state" :class="{ owned: owned(detail) }">{{ benefitState(detail) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="detail-rule"><text class="rule-label">权益规则</text><text>{{ rule(detail) }}</text></view>
|
||||||
|
<text class="description-title">使用说明</text><rich-text v-if="detail.benefitDesc" class="rich-description" :nodes="detailDescription" />
|
||||||
|
<text v-else class="state-description">暂无详细说明</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view class="sheet-footer"><button class="confirm-button" @click="detail = null">我知道了</button></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { onShow, onPullDownRefresh, onUnload } from '@dcloudio/uni-app'
|
||||||
|
import { getCurrentMemberBenefits, getMemberGradeBenefits } from '@/api/members'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
import MemberGradeCard from './components/MemberGradeCard.vue'
|
||||||
|
|
||||||
|
const loading = ref(false), loaded = ref(false), error = ref(false)
|
||||||
|
const current = ref({}), grades = ref([]), selectedId = ref(''), detail = ref(null)
|
||||||
|
const failedBenefitIcons = ref({})
|
||||||
|
let requestId = 0, ownerId = ''
|
||||||
|
const sameId = (a, b) => a != null && b != null && String(a) === String(b)
|
||||||
|
const selected = computed(() => grades.value.find(item => sameId(item.grade.id, selectedId.value)) || {})
|
||||||
|
const selectedIndex = computed(() => Math.max(0, grades.value.findIndex(item => sameId(item.grade.id, selectedId.value))))
|
||||||
|
const progress = computed(() => Math.max(0, Math.min(100, Number(current.value.upgradeProgress) || 0)))
|
||||||
|
const growthBaseline = computed(() => current.value.grade?.isDefault ? 0 : current.value.grade?.requiredExperience || 0)
|
||||||
|
const owned = benefit => (current.value.benefits || []).some(item => sameId(item.id, benefit.id))
|
||||||
|
function number(value) { return Math.max(0, Number(value) || 0).toLocaleString('en-US') }
|
||||||
|
function config(benefit) {
|
||||||
|
try { const value = typeof benefit.benefitConfig === 'string' ? JSON.parse(benefit.benefitConfig) : benefit.benefitConfig; return value && typeof value === 'object' ? value : {} } catch { return {} }
|
||||||
|
}
|
||||||
|
function summary(benefit) {
|
||||||
|
const value = config(benefit)
|
||||||
|
switch (benefit.benefitType) {
|
||||||
|
case 'POINT_MULTIPLIER': return `购物积分按 ${value.multiplier || 1} 倍计算`
|
||||||
|
case 'GIFT_POINT': return `首次达到等级赠送 ${number(value.giftPoint)} 积分`
|
||||||
|
case 'COUPON_PACKAGE': return value.grantRule === 'MONTHLY' ? `每月 ${value.grantDay || 1} 号发放优惠券` : value.grantRule === 'PERIOD' ? `每 ${value.periodMonths || 1} 个月发放优惠券` : '首次达到等级赠送优惠券'
|
||||||
|
case 'GRADE_BADGE': return '展示你的专属等级身份'
|
||||||
|
case 'EXCLUSIVE_CUSTOMER_SERVICE': return '享受会员专属客服服务'
|
||||||
|
default: return '查看权益介绍与适用说明'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function rule(benefit) {
|
||||||
|
const text = summary(benefit)
|
||||||
|
switch (benefit.benefitType) {
|
||||||
|
case 'POINT_MULTIPLIER': return `${text},同类权益取最高倍率,不叠加。`
|
||||||
|
case 'GIFT_POINT': return `${text},同一等级的该权益仅赠送一次。`
|
||||||
|
case 'COUPON_PACKAGE': return `${text}。每月不存在指定日期时取当月最后一天;按周期发放时首期立即发放,暂停期间未生成的历史周期不追发。具体券种和数量以实际发放为准。`
|
||||||
|
case 'CUSTOM': return '适用条件、参与方式及服务内容以以下权益介绍为准。'
|
||||||
|
default: return `${text},具体服务内容请查看以下说明。`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function benefitState(benefit) {
|
||||||
|
if (owned(benefit)) return '已享有'
|
||||||
|
return Number(selected.value.grade?.requiredExperience) > Number(current.value.grade?.requiredExperience || 0) ? '达到该等级可享' : '当前等级未包含'
|
||||||
|
}
|
||||||
|
const detailDescription = computed(() => String(detail.value?.benefitDesc || '').replace(/<img\b[^>]*>/gi, tag =>
|
||||||
|
tag.replace(/\sstyle\s*=\s*(?:"[^"]*"|'[^']*')/gi, '').replace(/<img\b/i, '<img style="max-width:100%;height:auto;display:block;"')))
|
||||||
|
function selectGrade(id) { selectedId.value = id; detail.value = null }
|
||||||
|
function openExperience(tab) { uni.navigateTo({ url: `/pages/mine/experience?tab=${tab}` }) }
|
||||||
|
function onGradeSwipe(event) {
|
||||||
|
// 初始化和刷新引发的程序切换无需回写,避免旧事件覆盖最新选择。
|
||||||
|
if (event.detail?.source !== 'touch') return
|
||||||
|
const item = grades.value[event.detail.current]
|
||||||
|
if (item) selectGrade(item.grade.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMembership() {
|
||||||
|
requestId++; ownerId = ''; loaded.value = false; loading.value = false; error.value = false
|
||||||
|
current.value = {}; grades.value = []; selectedId.value = ''; detail.value = null; failedBenefitIcons.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const memberId = String(isLogin()?.id || '')
|
||||||
|
if (!memberId) { resetMembership(); navigateToLogin(); uni.stopPullDownRefresh(); return }
|
||||||
|
if (ownerId !== memberId) {
|
||||||
|
ownerId = memberId; loaded.value = false
|
||||||
|
current.value = {}; grades.value = []; selectedId.value = ''; detail.value = null; failedBenefitIcons.value = {}
|
||||||
|
}
|
||||||
|
const id = ++requestId
|
||||||
|
const stillCurrent = () => id === requestId && String(isLogin()?.id || '') === memberId
|
||||||
|
loading.value = true; error.value = false
|
||||||
|
try {
|
||||||
|
const [a, b] = await Promise.all([getCurrentMemberBenefits(), getMemberGradeBenefits()])
|
||||||
|
if (!stillCurrent()) return
|
||||||
|
if (!a.data?.success || !b.data?.success) throw new Error('load')
|
||||||
|
current.value = a.data.result || {}
|
||||||
|
grades.value = (b.data.result || []).filter(item => item.grade?.gradeState === 'OPEN')
|
||||||
|
const requested = selectedId.value || current.value.gradeId
|
||||||
|
selectedId.value = grades.value.find(item => sameId(item.grade.id, requested))?.grade.id || grades.value[0]?.grade.id || ''
|
||||||
|
detail.value = null; failedBenefitIcons.value = {}; loaded.value = true
|
||||||
|
} catch { if (stillCurrent()) error.value = true }
|
||||||
|
finally { if (id === requestId) { loading.value = false; uni.stopPullDownRefresh() } }
|
||||||
|
}
|
||||||
|
onShow(load)
|
||||||
|
onPullDownRefresh(load)
|
||||||
|
onUnload(() => { requestId++ })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.benefits-page { min-height: 100vh; box-sizing: border-box; background: #f5f6f9; padding: 24rpx 28rpx calc(36rpx + env(safe-area-inset-bottom)); color: #252b35; }
|
||||||
|
.grade-swiper { width: 100%; height: 299rpx; border-radius: 24rpx; overflow: hidden; }
|
||||||
|
.swipe-hint { display: flex; justify-content: space-between; margin-top: 14rpx; color: #a0a5af; font-size: 21rpx; }
|
||||||
|
.growth-card { margin-top: 24rpx; padding: 28rpx; background: #fff; border-radius: 20rpx; }
|
||||||
|
.section-heading { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||||
|
.section-title { font-size: 30rpx; font-weight: 600; }
|
||||||
|
.section-caption { font-size: 22rpx; color: #959ba5; }
|
||||||
|
.experience-row { display: flex; align-items: baseline; gap: 12rpx; margin-top: 20rpx; }
|
||||||
|
.experience-number { font-size: 42rpx; font-weight: 600; }
|
||||||
|
.experience-label { font-size: 22rpx; color: #959ba5; }
|
||||||
|
.growth-track { margin-top: 20rpx; height: 10rpx; overflow: hidden; background: #f2eee8; border-radius: 8rpx; }
|
||||||
|
.growth-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, #dfc59b, #ae8245); transition: width .25s; }
|
||||||
|
.growth-labels { display: flex; justify-content: space-between; font-size: 20rpx; color: #a0a5af; margin-top: 8rpx; }
|
||||||
|
.growth-description { display: block; margin-top: 16rpx; color: #777e88; font-size: 24rpx; line-height: 1.7; }
|
||||||
|
.accent { color: #a47a3b; font-weight: 600; }
|
||||||
|
.highest-grade { display: flex; align-items: center; gap: 10rpx; margin-top: 18rpx; color: #987343; font-size: 24rpx; }
|
||||||
|
.growth-actions { display: flex; margin-top: 24rpx; padding-top: 22rpx; border-top: 1rpx solid #f0f1f4; }
|
||||||
|
.growth-action { flex: 1; display: flex; justify-content: center; align-items: center; gap: 10rpx; padding: 8rpx 0; font-size: 24rpx; color: #777e88; }
|
||||||
|
.growth-action + .growth-action { border-left: 1rpx solid #eceef2; }
|
||||||
|
.benefits-section { margin-top: 36rpx; }
|
||||||
|
.benefit-count { color: #9a7b50; font-size: 24rpx; font-weight: 400; margin-left: 10rpx; }
|
||||||
|
.benefit-grid { margin-top: 20rpx; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20rpx; }
|
||||||
|
.benefit-card { background: #fff; padding: 24rpx; border-radius: 20rpx; display: flex; flex-direction: column; align-items: flex-start; }
|
||||||
|
.benefit-card:active { background: #fcfaf6; }
|
||||||
|
.benefit-topline { display: flex; align-items: center; justify-content: space-between; width: 100%; }
|
||||||
|
.benefit-icon-wrap { width: 84rpx; height: 84rpx; background: #faf6ee; border-radius: 22rpx; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.benefit-icon { width: 64rpx; height: 64rpx; }
|
||||||
|
.benefit-name { font-size: 27rpx; font-weight: 600; line-height: 1.5; margin-top: 18rpx; overflow-wrap: anywhere; }
|
||||||
|
.benefit-summary { margin: 8rpx 0 20rpx; color: #9298a2; font-size: 22rpx; line-height: 1.6; flex: 1; }
|
||||||
|
.benefit-state { display: inline-block; border-radius: 8rpx; padding: 5rpx 10rpx; background: #f4f5f7; color: #9298a2; font-size: 20rpx; line-height: 1.5; }
|
||||||
|
.benefit-state.owned { background: #f6eedf; color: #97713c; }
|
||||||
|
.empty-benefits { margin-top: 20rpx; padding: 48rpx 20rpx; background: #fff; border-radius: 20rpx; display: flex; align-items: center; flex-direction: column; gap: 16rpx; font-size: 26rpx; color: #777e88; }
|
||||||
|
.page-notice { margin-top: 32rpx; color: #a0a5ae; font-size: 22rpx; line-height: 1.8; }
|
||||||
|
.page-notice text { display: block; }
|
||||||
|
.page-notice text:first-child { margin-bottom: 8rpx; color: #858c97; font-weight: 500; }
|
||||||
|
.page-state { min-height: 65vh; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 22rpx; }
|
||||||
|
.state-title { font-size: 30rpx; color: #727985; }
|
||||||
|
.state-description { font-size: 24rpx; color: #a0a6af; }
|
||||||
|
.retry-button { font-size: 26rpx; color: #8d6836; background: #f1e6d4; padding: 0 36rpx; margin-top: 10rpx; border-radius: 40rpx; }
|
||||||
|
button::after { border: none; }
|
||||||
|
.refresh-error { padding: 16rpx; margin-bottom: 20rpx; background: #fff3df; color: #a27a42; font-size: 24rpx; border-radius: 12rpx; text-align: center; }
|
||||||
|
.skeleton { background: #e8ebef; border-radius: 20rpx; }
|
||||||
|
.skeleton-card { height: 299rpx; }
|
||||||
|
.skeleton-growth { height: 160rpx; margin: 24rpx 0; }
|
||||||
|
.skeleton-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20rpx; }
|
||||||
|
.skeleton-benefit { height: 270rpx; }
|
||||||
|
.detail-overlay { position: fixed; inset: 0; z-index: 100; display: flex; align-items: flex-end; }
|
||||||
|
.detail-backdrop { position: absolute; inset: 0; background: rgba(20, 25, 34, .45); }
|
||||||
|
.detail-sheet { position: relative; width: 100%; background: #fff; border-radius: 32rpx 32rpx 0 0; overflow: hidden; }
|
||||||
|
.sheet-handle { width: 64rpx; height: 7rpx; margin: 16rpx auto 8rpx; background: #e1e4e9; border-radius: 8rpx; }
|
||||||
|
.detail-header { display: flex; justify-content: space-between; align-items: center; padding: 8rpx 20rpx 16rpx 32rpx; font-size: 28rpx; font-weight: 500; }
|
||||||
|
.close-icon { padding: 16rpx; }
|
||||||
|
.detail-scroll { height: 48vh; }
|
||||||
|
.detail-content { padding: 8rpx 32rpx 32rpx; }
|
||||||
|
.detail-identity { display: flex; flex-direction: column; align-items: center; gap: 16rpx; padding: 12rpx 0 28rpx; }
|
||||||
|
.detail-icon { width: 96rpx; height: 96rpx; }
|
||||||
|
.detail-title { font-size: 34rpx; font-weight: 600; text-align: center; }
|
||||||
|
.detail-rule { background: #faf6ef; border-radius: 16rpx; padding: 24rpx; font-size: 25rpx; color: #987445; line-height: 1.8; }
|
||||||
|
.detail-rule text { display: block; }
|
||||||
|
.rule-label { font-weight: 600; margin-bottom: 8rpx; }
|
||||||
|
.description-title { display: block; font-size: 28rpx; font-weight: 600; margin: 28rpx 0 20rpx; }
|
||||||
|
.rich-description { display: block; font-size: 26rpx; line-height: 1.8; color: #737a85; overflow-wrap: anywhere; }
|
||||||
|
.sheet-footer { padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom)); border-top: 1rpx solid #f2f3f5; }
|
||||||
|
.confirm-button { background: #f2e5cf; color: #805d2d; font-size: 28rpx; border-radius: 40rpx; line-height: 80rpx; }
|
||||||
|
</style>
|
||||||
79
pages/mine/paidMember.vue
Normal file
79
pages/mine/paidMember.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<view class="paid-page">
|
||||||
|
<view v-if="loading && !data.config" class="empty">加载中…</view>
|
||||||
|
<view v-else-if="error && !data.config" class="empty" @click="load">加载失败,点击重试</view>
|
||||||
|
<template v-else-if="data.config">
|
||||||
|
<view v-if="error" class="poll-message" @click="load">刷新失败,点击重试</view>
|
||||||
|
<view class="member-card" :style="cardStyle">
|
||||||
|
<image v-if="data.config.backgroundType === 'IMAGE' && data.config.backgroundImage" class="card-bg" :src="data.config.backgroundImage" mode="aspectFill" />
|
||||||
|
<view class="card-content"><view class="card-title"><image v-if="data.config.icon" class="member-icon" :src="data.config.icon" mode="aspectFit" /><text>{{ data.config.name }}</text></view><text class="member-status">{{ data.active ? '会员有效期至 ' + data.endTime : data.status === 'EXPIRED' ? '会员已到期' : '开通会员,享专属权益' }}</text><text v-if="data.status !== 'UNOPENED'" class="saved">累计已优惠 ¥{{ money(data.savedAmount) }}</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="pollMessage" class="poll-message" @click="checkPayment">{{ pollMessage }}</view>
|
||||||
|
<view class="purchase-card"><view><text class="price">¥{{ money(data.config.price) }}</text><text class="period"> / {{ data.config.months }}个月</text></view><text class="hint">{{ data.active ? '续费将从原到期时间顺延,续费权益在新周期开始后生效' : '支付成功后开通,按自然月计算有效期' }}</text></view>
|
||||||
|
<view class="section"><view class="section-heading"><text>专属权益</text><text class="record-link" @click="goRecords">购买记录 ›</text></view>
|
||||||
|
<view class="rights-grid"><view v-for="b in displayedBenefits" :key="b.id" class="right-item" @click="detail = b"><image v-if="b.benefitLogo" :src="b.benefitLogo" mode="aspectFit" /><u-icon v-else name="gift" size="28" color="#ae8245" /><text>{{ b.benefitName }}</text></view></view><text v-if="!displayedBenefits.length" class="hint">{{ data.active ? '当前暂无关联权益' : '开通后按会员配置享有权益' }}</text>
|
||||||
|
<view class="discount-note"><text>会员专享商品折扣</text><text>优惠由平台提供,限适用商家及商品。实际优惠以结算页为准。</text></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="data.recommendedGoods?.length" class="section"><view class="section-heading">为你推荐</view><view class="goods-grid"><view v-for="g in data.recommendedGoods" :key="g.id" class="goods-card" @click="goGoods(g)"><image class="goods-cover" :src="g.thumbnail" mode="aspectFit" /><text class="goods-name">{{ g.goodsName }}</text><text class="goods-price">¥{{ money(g.price) }}</text><view v-if="quotes[g.id]" class="member-price"><image v-if="quotes[g.id].label" class="member-price-label" :src="quotes[g.id].label" mode="aspectFit" /><text>会员价 ¥{{ money(quotes[g.id].price) }}</text></view></view></view></view>
|
||||||
|
<view v-if="data.config.description" class="section"><view class="section-heading">购买说明</view><rich-text :nodes="data.config.description" /></view>
|
||||||
|
<view class="purchase-footer"><view class="agreement-row" @click="agreed = !agreed"><u-icon :name="agreed ? 'checkmark-circle-fill' : 'checkmark-circle'" size="17" color="#a78149" /><text>我已阅读并同意</text><text class="agreement-link" @click.stop="showAgreement = true">《付费会员协议》</text></view><button :disabled="!data.config.enabled || buying || !agreed" :loading="buying" class="buy-button" @click="buy">{{ !data.config.enabled ? '暂未开放购买和续费' : data.active ? '立即续费' : '立即开通' }}</button></view>
|
||||||
|
</template>
|
||||||
|
<view v-if="detail || showAgreement" class="overlay"><view class="mask" @click="closeDetail" @touchmove.stop.prevent /><view class="sheet"><text class="sheet-title">{{ detail?.benefitName || '付费会员协议' }}</text><scroll-view scroll-y class="sheet-scroll"><rich-text :nodes="detail ? detail.benefitDesc || '暂无说明' : data.config?.agreement || '请联系平台查看会员协议'" /></scroll-view><button class="close-button" @click="closeDetail">关闭</button></view></view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onLoad, onShow, onHide, onUnload, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
|
import { getPaidMember, createPaidMemberOrder, getPaidMemberOrder, getPaidMemberQuotes } from '@/api/paid-member'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
const data=ref({}), quotes=ref({}), loading=ref(false), error=ref(false), agreed=ref(false), buying=ref(false), detail=ref(null), showAgreement=ref(false), pollMessage=ref('')
|
||||||
|
let request=0, owner='', purchaseRequest='', paymentSn='', timer=null, pollId=0, pollCount=0
|
||||||
|
const money=value=>(Number(value)||0).toFixed(2)
|
||||||
|
const cardStyle=computed(()=>({backgroundColor:data.value.config?.backgroundColor || '#51443b',color:data.value.config?.fontColor || '#fff1d8'}))
|
||||||
|
const displayedBenefits=computed(()=>data.value.active ? data.value.benefits || [] : data.value.offeredBenefits || [])
|
||||||
|
function closeDetail(){detail.value=null;showAgreement.value=false}
|
||||||
|
function goRecords(){uni.navigateTo({url:'/pages/mine/paidMemberRecords'})}
|
||||||
|
function goGoods(g){uni.navigateTo({url:`/pages/product/goods?id=${g.id}&goodsId=${g.goodsId}`})}
|
||||||
|
async function load(){
|
||||||
|
const memberId=String(isLogin()?.id || '')
|
||||||
|
if(!memberId){request++;stopPolling();owner='';data.value={};quotes.value={};loading.value=false;agreed.value=false;purchaseRequest='';paymentSn='';pollMessage.value='';navigateToLogin();uni.stopPullDownRefresh();return}
|
||||||
|
if(owner!==memberId){if(owner){stopPolling();paymentSn='';pollMessage.value=''}owner=memberId;data.value={};quotes.value={};agreed.value=false;purchaseRequest='';detail.value=null}
|
||||||
|
const id=++request;loading.value=true;error.value=false
|
||||||
|
try{
|
||||||
|
const res=await getPaidMember()
|
||||||
|
if(id!==request || String(isLogin()?.id||'')!==memberId)return
|
||||||
|
if(!res.data?.success)throw new Error('load')
|
||||||
|
if(data.value.configVersion !== res.data.result?.configVersion) agreed.value=false
|
||||||
|
data.value=res.data.result||{};quotes.value={}
|
||||||
|
const ids=(data.value.recommendedGoods||[]).map(g=>g.id)
|
||||||
|
const results=await Promise.all(Array.from({length:Math.ceil(ids.length/100)},(_,i)=>getPaidMemberQuotes(ids.slice(i*100,i*100+100))))
|
||||||
|
if(id===request && String(isLogin()?.id||'')===memberId) quotes.value=Object.assign({},...results.filter(q=>q.data?.success).map(q=>q.data.result||{}))
|
||||||
|
}catch{if(id===request)error.value=true}finally{if(id===request)loading.value=false;uni.stopPullDownRefresh()}
|
||||||
|
}
|
||||||
|
async function buy(){
|
||||||
|
if(buying.value || !agreed.value || !data.value.config?.enabled)return
|
||||||
|
buying.value=true
|
||||||
|
const memberId=owner
|
||||||
|
purchaseRequest=purchaseRequest || 'pm_'+Date.now().toString(36)+'_'+Math.random().toString(36).slice(2)
|
||||||
|
try{
|
||||||
|
const res=await createPaidMemberOrder({requestId:purchaseRequest,configVersion:data.value.configVersion,agreed:true})
|
||||||
|
if(String(isLogin()?.id||'')!==memberId)return
|
||||||
|
if(res.data?.success){const o=res.data.result;purchaseRequest='';if(o.status==='PAID')uni.redirectTo({url:'/pages/mine/paidMember?payment_sn='+encodeURIComponent(o.sn)});else uni.navigateTo({url:`/pages/cart/payment/payOrder?paid_member_sn=${o.sn}`})}
|
||||||
|
else{purchaseRequest='';agreed.value=false;await load()}
|
||||||
|
}catch{uni.showToast({title:'购买结果暂未确认,请重试或查看购买记录',icon:'none'})}finally{buying.value=false}
|
||||||
|
}
|
||||||
|
function stopPolling(){pollId++;if(timer)clearTimeout(timer);timer=null}
|
||||||
|
async function checkPayment(){
|
||||||
|
if(!paymentSn)return
|
||||||
|
const id=++pollId;pollMessage.value='正在确认支付结果…'
|
||||||
|
try{const res=await getPaidMemberOrder(paymentSn);if(id!==pollId)return;if(res.data?.success && res.data.result.status==='PAID'){paymentSn='';pollMessage.value='支付已确认,会员资格已更新';await load();return}pollCount++;if(pollCount<10){timer=setTimeout(checkPayment,1500)}else pollMessage.value='支付结果待确认,点击刷新或查看购买记录'}catch{if(id===pollId)pollMessage.value='支付结果待确认,点击重试'}
|
||||||
|
}
|
||||||
|
onLoad(options=>{paymentSn=options?.payment_sn||''})
|
||||||
|
onShow(async()=>{await load();if(paymentSn){pollCount=0;checkPayment()}})
|
||||||
|
onPullDownRefresh(load)
|
||||||
|
onHide(stopPolling)
|
||||||
|
onUnload(()=>{request++;stopPolling()})
|
||||||
|
</script>
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.paid-page{min-height:100vh;box-sizing:border-box;background:#f5f6f9;padding:28rpx 28rpx 240rpx;color:#292f39}.member-card{position:relative;overflow:hidden;border-radius:24rpx;min-height:240rpx}.card-bg{position:absolute;inset:0;width:100%;height:100%}.card-content{position:relative;padding:36rpx}.card-title{display:flex;align-items:center;gap:16rpx;font-size:38rpx;font-weight:600}.member-icon{width:70rpx;height:70rpx}.member-status,.saved{display:block;margin-top:18rpx;font-size:24rpx}.saved{opacity:.8}.purchase-card,.section{margin-top:24rpx;background:#fff;padding:28rpx;border-radius:20rpx}.price{font-size:48rpx;font-weight:600;color:#a67b3d}.period{font-size:25rpx;color:#929aa6}.hint{display:block;margin-top:14rpx;font-size:23rpx;line-height:1.7;color:#929aa6}.section-heading{display:flex;align-items:center;justify-content:space-between;font-size:30rpx;font-weight:600;margin-bottom:24rpx}.record-link{font-size:23rpx;font-weight:400;color:#a78149}.rights-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:28rpx 16rpx}.right-item{display:flex;flex-direction:column;align-items:center;gap:12rpx;text-align:center;font-size:24rpx}.right-item image{width:72rpx;height:72rpx}.discount-note{margin-top:24rpx;padding:20rpx;background:#fbf6ed;border-radius:14rpx;font-size:23rpx;color:#a78149;line-height:1.8}.discount-note text{display:block}.goods-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24rpx}.goods-cover{width:100%;height:230rpx}.goods-name{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:24rpx;line-height:1.6}.goods-price,.member-price{display:block;margin-top:8rpx;font-size:25rpx}.member-price{display:flex;align-items:center;gap:6rpx;color:#9c7139;font-size:22rpx}.member-price-label{width:28rpx;height:28rpx;flex-shrink:0}.purchase-footer{position:fixed;left:0;right:0;bottom:0;background:#fff;padding:22rpx 28rpx calc(22rpx + env(safe-area-inset-bottom));border-top:1rpx solid #eee;z-index:8}.agreement-row{display:flex;justify-content:center;align-items:center;gap:8rpx;font-size:22rpx;color:#929aa6;margin-bottom:18rpx}.agreement-link{color:#a78149}.buy-button,.close-button{background:#ead5b1;color:#705125;border-radius:44rpx;font-size:28rpx;line-height:84rpx}button::after{border:none}.empty{padding:160rpx 20rpx;text-align:center;color:#969da8;font-size:27rpx}.poll-message{padding:20rpx;background:#fff2d9;border-radius:12rpx;margin-top:20rpx;color:#9d753e;font-size:24rpx}.overlay{position:fixed;inset:0;z-index:20;display:flex;align-items:flex-end}.mask{position:absolute;inset:0;background:rgba(0,0,0,.4)}.sheet{position:relative;width:100%;background:#fff;border-radius:28rpx 28rpx 0 0;padding:30rpx;box-sizing:border-box;padding-bottom:calc(30rpx + env(safe-area-inset-bottom))}.sheet-title{display:block;text-align:center;font-size:32rpx;font-weight:600;margin-bottom:24rpx}.sheet-scroll{height:50vh;margin-bottom:24rpx;font-size:26rpx;line-height:1.8}
|
||||||
|
</style>
|
||||||
36
pages/mine/paidMemberRecords.vue
Normal file
36
pages/mine/paidMemberRecords.vue
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<view class="records-page">
|
||||||
|
<view v-for="order in rows" :key="order.id" class="record-card">
|
||||||
|
<view class="heading"><text>{{ memberName(order) }}</text><text class="status">{{ status(order) }}</text></view>
|
||||||
|
<text class="amount">¥{{ Number(order.amount).toFixed(2) }} <text class="months">/ {{ order.months }}个月</text></text>
|
||||||
|
<text class="line">订单号:{{ order.sn }}</text><text class="line">创建时间:{{ order.createTime }}</text>
|
||||||
|
<template v-if="order.status === 'PAID'"><text class="line">支付时间:{{ order.payTime }}</text><text class="line">有效期:{{ order.startTime }} 至 {{ order.endTime }}</text></template>
|
||||||
|
<view v-if="canPay(order)" class="actions"><button class="cancel" @click="cancel(order)">取消订单</button><button class="pay" @click="pay(order)">继续支付</button></view>
|
||||||
|
</view>
|
||||||
|
<view v-if="!rows.length" class="empty">{{ loading ? '加载中…' : error ? '加载失败,点击重试' : '暂无购买记录' }}</view>
|
||||||
|
<view v-if="error" class="load-more" @click="load(retryReset)">点击重新加载</view><view v-else-if="rows.length" class="load-more" @click="load(false)">{{ loading ? '加载中…' : more ? '上拉或点击加载更多' : '已显示全部记录' }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onShow, onReachBottom, onPullDownRefresh, onUnload } from '@dcloudio/uni-app'
|
||||||
|
import { getPaidMemberOrders, cancelPaidMemberOrder } from '@/api/paid-member'
|
||||||
|
import { isLogin, navigateToLogin } from '@/utils/filters'
|
||||||
|
const rows=ref([]),loading=ref(false),error=ref(false),more=ref(true)
|
||||||
|
let page=1,owner='',request=0,retryReset=true
|
||||||
|
const time=value=>new Date(String(value||'').replace(/-/g,'/')).getTime()
|
||||||
|
function memberName(order){try{return JSON.parse(order.configSnapshot).name}catch{return '付费会员'}}
|
||||||
|
function canPay(order){return order.status==='UNPAID' && time(order.payDeadline)>Date.now()}
|
||||||
|
function status(order){return order.status==='PAID' ? time(order.startTime)>Date.now() ? '待生效' : time(order.endTime)<=Date.now() ? '已到期' : '会员有效' : order.status==='CANCELLED' ? '已取消' : canPay(order) ? '待支付' : '已超时'}
|
||||||
|
function pay(order){uni.navigateTo({url:`/pages/cart/payment/payOrder?paid_member_sn=${order.sn}`})}
|
||||||
|
function cancel(order){uni.showModal({title:'取消会员订单',content:'确定取消这笔未支付订单?',success:async result=>{if(result.confirm){const res=await cancelPaidMemberOrder(order.sn);if(res.data?.success)load(true)}}})}
|
||||||
|
async function load(reset=false){
|
||||||
|
const memberId=String(isLogin()?.id||'');if(!memberId){request++;rows.value=[];owner='';navigateToLogin();uni.stopPullDownRefresh();return}
|
||||||
|
if(!reset && (loading.value || !more.value))return
|
||||||
|
if(owner!==memberId){owner=memberId;rows.value=[];reset=true}
|
||||||
|
const id=++request,next=reset?1:page;loading.value=true;error.value=false;retryReset=reset
|
||||||
|
try{const res=await getPaidMemberOrders({pageNumber:next,pageSize:20});if(id!==request||String(isLogin()?.id||'')!==memberId)return;if(!res.data?.success)throw new Error('load');const data=res.data.result||{};rows.value=reset ? data.records||[] : [...rows.value,...(data.records||[]).filter(row=>!rows.value.some(old=>old.id===row.id))];page=next+1;more.value=next*20<Number(data.total||0);retryReset=false}catch{if(id===request)error.value=true}finally{if(id===request)loading.value=false;uni.stopPullDownRefresh()}
|
||||||
|
}
|
||||||
|
onShow(()=>load(true));onPullDownRefresh(()=>load(true));onReachBottom(()=>{if(!error.value)load(false)});onUnload(()=>request++)
|
||||||
|
</script>
|
||||||
|
<style scoped>.records-page{min-height:100vh;box-sizing:border-box;padding:24rpx 28rpx calc(30rpx + env(safe-area-inset-bottom));background:#f5f6f9}.record-card{padding:28rpx;background:#fff;border-radius:20rpx;margin-bottom:20rpx}.heading{display:flex;align-items:center;justify-content:space-between;font-size:29rpx;font-weight:600}.status{font-size:23rpx;color:#a78149;font-weight:400}.amount{display:block;font-size:40rpx;margin:20rpx 0;color:#9d753e}.months{font-size:23rpx;color:#989faa}.line{display:block;font-size:22rpx;line-height:1.9;color:#929aa6;overflow-wrap:anywhere}.actions{display:flex;justify-content:flex-end;gap:16rpx;margin-top:22rpx}.actions button{margin:0;padding:0 26rpx;font-size:23rpx;line-height:58rpx;border-radius:30rpx}.actions button::after{border:none}.pay{background:#f0dfc2;color:#8d6836}.cancel{background:#f2f3f5;color:#929aa6}.empty,.load-more{padding:40rpx 0;text-align:center;font-size:25rpx;color:#a0a7b1}</style>
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle" class="content">
|
<view :style="themeStyle" class="content">
|
||||||
|
<u-navbar
|
||||||
|
title="我的积分"
|
||||||
|
:fixed="true"
|
||||||
|
:placeholder="true"
|
||||||
|
:border="false"
|
||||||
|
:auto-back="false"
|
||||||
|
@leftClick="handleBack"
|
||||||
|
></u-navbar>
|
||||||
<view class="portrait-box">
|
<view class="portrait-box">
|
||||||
<image src="/pages/mine/static/point_bg_1.png" mode=""></image>
|
<image src="/pages/mine/static/point_bg_1.png" mode=""></image>
|
||||||
<image class="point-img" src="/pages/mine/static/tradehall.png" />
|
<image class="point-img" src="/pages/mine/static/tradehall.png" />
|
||||||
@@ -41,6 +49,10 @@ import { onLoad, onReachBottom } from '@dcloudio/uni-app'
|
|||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getPointsData, getMemberPointSum } from '@/api/members.js'
|
import { getPointsData, getMemberPointSum } from '@/api/members.js'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import {
|
||||||
|
prefetchMpMessageTemplates,
|
||||||
|
subscribeMpMessageByScenes,
|
||||||
|
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
@@ -51,10 +63,29 @@ const params = ref({ pageNumber: 1, pageSize: 10 })
|
|||||||
const pointSummary = ref<Record<string, any>>({})
|
const pointSummary = ref<Record<string, any>>({})
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
// #endif
|
||||||
fetchPointSummary()
|
fetchPointSummary()
|
||||||
fetchPointLogList()
|
fetchPointLogList()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
const doBack = () => {
|
||||||
|
uni.navigateBack({
|
||||||
|
fail: () => {
|
||||||
|
uni.switchTab({ url: '/pages/tabbar/user/my' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['POINT_CHANGE'], doBack)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doBack()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
params.value.pageNumber++
|
params.value.pageNumber++
|
||||||
fetchPointLogList()
|
fetchPointLogList()
|
||||||
|
|||||||
320
pages/mine/set/messageNotice.vue
Normal file
320
pages/mine/set/messageNotice.vue
Normal file
@@ -0,0 +1,320 @@
|
|||||||
|
<template>
|
||||||
|
<view :style="themeStyle" class="notice-page">
|
||||||
|
<u-navbar title="消息通知" :auto-back="true" :placeholder="true" :border="false"></u-navbar>
|
||||||
|
|
||||||
|
<view class="content">
|
||||||
|
<view class="tip-card">
|
||||||
|
<u-icon name="volume" color="#ff7a00" size="18"></u-icon>
|
||||||
|
<text class="tip-text">开启通知后,可第一时间接收订单发货、售后审核及资金变动提醒。</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 微信公众号通知设置 -->
|
||||||
|
<view v-if="oaSetting.enabled" class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<view class="card-title-wrap">
|
||||||
|
<view class="icon-badge oa-badge">
|
||||||
|
<u-icon name="weixin-fill" color="#fff" size="20"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="title-box">
|
||||||
|
<view class="title-row">
|
||||||
|
<text class="card-title">{{ oaSetting.accountName || '微信公众号通知' }}</text>
|
||||||
|
<text class="status-tag" :class="oaSetting.bound ? 'status-open' : 'status-close'">
|
||||||
|
{{ oaSetting.bound ? '已开启' : '未开启' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text class="card-desc">关注官方公众号,长期接收服务与订单通知</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-body">
|
||||||
|
<view class="notice-points">
|
||||||
|
<view class="point-item">
|
||||||
|
<text class="dot">•</text>
|
||||||
|
<text class="point-text">订单发货、完成及拼团进度实时推送</text>
|
||||||
|
</view>
|
||||||
|
<view class="point-item">
|
||||||
|
<text class="dot">•</text>
|
||||||
|
<text class="point-text">售后申请审核结果、商家退款提醒</text>
|
||||||
|
</view>
|
||||||
|
<view class="point-item">
|
||||||
|
<text class="dot">•</text>
|
||||||
|
<text class="point-text">余额变动、提现审核与到账结果通知</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-foot">
|
||||||
|
<view
|
||||||
|
class="action-btn"
|
||||||
|
:class="oaSetting.bound ? 'btn-bound' : 'btn-primary'"
|
||||||
|
@click="handleOaAction"
|
||||||
|
>
|
||||||
|
{{ oaSetting.bound ? '查看公众号文章' : '关注公众号开启通知' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 微信小程序订阅消息说明 -->
|
||||||
|
<view class="card">
|
||||||
|
<view class="card-head">
|
||||||
|
<view class="card-title-wrap">
|
||||||
|
<view class="icon-badge mp-badge">
|
||||||
|
<u-icon name="chat-fill" color="#fff" size="20"></u-icon>
|
||||||
|
</view>
|
||||||
|
<view class="title-box">
|
||||||
|
<view class="title-row">
|
||||||
|
<text class="card-title">小程序订阅消息</text>
|
||||||
|
<text class="status-tag status-open">已支持</text>
|
||||||
|
</view>
|
||||||
|
<text class="card-desc">在支付成功、售后提交、积分与提现等页面主动提示</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-body">
|
||||||
|
<text class="mp-intro">
|
||||||
|
小程序订阅消息支持:订单发货提醒、订单已完成提醒、拼团成功通知、售后申请审核通知、积分变更提醒、账户余额变动通知、提现结果通知。您在相关操作时点击授权即可生效。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
|
import { getWechatOfficialAccountSetting } from '@/api/message.js'
|
||||||
|
import { useStore } from '@/store'
|
||||||
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
|
const oaSetting = ref({
|
||||||
|
enabled: false,
|
||||||
|
accountName: '',
|
||||||
|
articleUrl: '',
|
||||||
|
bound: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
fetchSetting()
|
||||||
|
})
|
||||||
|
|
||||||
|
function fetchSetting() {
|
||||||
|
getWechatOfficialAccountSetting()
|
||||||
|
.then((res: any) => {
|
||||||
|
if (res && res.data && res.data.success && res.data.result) {
|
||||||
|
oaSetting.value = res.data.result
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOaAction() {
|
||||||
|
const url = (oaSetting.value.articleUrl || '').trim()
|
||||||
|
if (!url) {
|
||||||
|
uni.showToast({ title: '暂未配置公众号文章', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
if (wx.openOfficialAccountArticle) {
|
||||||
|
wx.openOfficialAccountArticle({
|
||||||
|
url,
|
||||||
|
fail: () => {
|
||||||
|
fallbackOpen(url)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
fallbackOpen(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackOpen(url: string) {
|
||||||
|
uni.setClipboardData({
|
||||||
|
data: url,
|
||||||
|
success: () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '公众号引导文章链接已复制到剪贴板,请在微信中粘贴发送并打开关注公众号',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.notice-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f7f8fa;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff7eb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #ff7a00;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 32rpx;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 1px solid #f2f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-badge {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oa-badge {
|
||||||
|
background: linear-gradient(135deg, #07c160, #0ab155);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-badge {
|
||||||
|
background: linear-gradient(135deg, #2b85e4, #007aff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
font-size: 20rpx;
|
||||||
|
padding: 4rpx 14rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-left: 14rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-open {
|
||||||
|
background: #e8f8f0;
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-close {
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #86909c;
|
||||||
|
margin-top: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
padding: 24rpx 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notice-points {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4e5969;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
color: #07c160;
|
||||||
|
font-size: 32rpx;
|
||||||
|
line-height: 1;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.point-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-foot {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
height: 80rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #07c160, #05a04e);
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 6rpx 18rpx rgba(7, 193, 96, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-bound {
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mp-intro {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #4e5969;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle" class="person-msg">
|
<view :style="themeStyle" class="person-msg">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<button class="head avatar-button" open-type="chooseAvatar" :disabled="avatarUploading || saving" @chooseavatar="onChooseAvatar">
|
||||||
|
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
|
||||||
|
<view>{{ avatarUploading ? '头像上传中…' : '点击选择微信头像' }}</view>
|
||||||
|
</button>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
<view class="head" @click="changeFace">
|
<view class="head" @click="changeFace">
|
||||||
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
|
<image :src="form.face || '/static/missing-face.png'" mode="aspectFill"></image>
|
||||||
<view>点击修改头像</view>
|
<view>{{ avatarUploading ? '头像上传中…' : '点击修改头像' }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
|
|
||||||
<up-form
|
<up-form
|
||||||
:model="form"
|
:model="form"
|
||||||
@@ -13,12 +21,26 @@
|
|||||||
label-width="180rpx"
|
label-width="180rpx"
|
||||||
>
|
>
|
||||||
<up-form-item label="昵称" label-width="180rpx" :border-bottom="true">
|
<up-form-item label="昵称" label-width="180rpx" :border-bottom="true">
|
||||||
|
<!-- #ifdef MP-WEIXIN -->
|
||||||
|
<input
|
||||||
|
class="nickname-input"
|
||||||
|
type="nickname"
|
||||||
|
:value="form.nickName"
|
||||||
|
:disabled="saving"
|
||||||
|
placeholder="点击填写微信昵称"
|
||||||
|
@input="onNicknameChange"
|
||||||
|
@blur="onNicknameChange"
|
||||||
|
@nicknamereview="onNicknameReview"
|
||||||
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
|
<!-- #ifndef MP-WEIXIN -->
|
||||||
<u-input
|
<u-input
|
||||||
v-model="form.nickName"
|
v-model="form.nickName"
|
||||||
border="none"
|
border="none"
|
||||||
input-align="right"
|
input-align="right"
|
||||||
placeholder="请输入昵称"
|
placeholder="请输入昵称"
|
||||||
/>
|
/>
|
||||||
|
<!-- #endif -->
|
||||||
</up-form-item>
|
</up-form-item>
|
||||||
|
|
||||||
<up-form-item label="性别" label-width="180rpx" :border-bottom="true">
|
<up-form-item label="性别" label-width="180rpx" :border-bottom="true">
|
||||||
@@ -55,7 +77,9 @@
|
|||||||
</up-form>
|
</up-form>
|
||||||
|
|
||||||
<view class="bottom">
|
<view class="bottom">
|
||||||
<view class="submit" @click="submit">保存</view>
|
<button class="submit" :disabled="avatarUploading || saving" :loading="saving" @click="submit">
|
||||||
|
{{ avatarUploading ? '头像上传中…' : saving ? '正在保存…' : '保存' }}
|
||||||
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<u-datetime-picker
|
<u-datetime-picker
|
||||||
@@ -80,7 +104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, nextTick } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { saveUserInfo } from '@/api/members.js'
|
import { saveUserInfo } from '@/api/members.js'
|
||||||
@@ -126,6 +150,24 @@ const maxBirthdayDate = Date.now()
|
|||||||
const region = ref([{ id: '', localName: '请选择', children: [] }])
|
const region = ref([{ id: '', localName: '请选择', children: [] }])
|
||||||
const showBirthday = ref(false)
|
const showBirthday = ref(false)
|
||||||
const cityPicker = ref<any>(null)
|
const cityPicker = ref<any>(null)
|
||||||
|
const avatarUploading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
|
||||||
|
function onNicknameChange(e: any) {
|
||||||
|
if (typeof e?.detail?.value === 'string') form.nickName = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNicknameReview(e: any) {
|
||||||
|
if (e?.detail?.pass === false) {
|
||||||
|
form.nickName = ''
|
||||||
|
uni.showToast({ title: '昵称未通过审核,请重新填写', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChooseAvatar(e: any) {
|
||||||
|
const filePath = e?.detail?.avatarUrl
|
||||||
|
if (typeof filePath === 'string' && filePath) uploadAvatar(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
function getPickerParentValue(selected: any[]) {
|
function getPickerParentValue(selected: any[]) {
|
||||||
form.region = []
|
form.region = []
|
||||||
@@ -145,34 +187,69 @@ function clickRegion() {
|
|||||||
cityPicker.value?.show()
|
cityPicker.value?.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
function submit() {
|
async function submit() {
|
||||||
|
if (avatarUploading.value || saving.value) return
|
||||||
|
// Nickname autofill can update the value in the blur event before the save tap.
|
||||||
|
await nextTick()
|
||||||
|
if (avatarUploading.value || saving.value) return
|
||||||
const params = JSON.parse(JSON.stringify(form))
|
const params = JSON.parse(JSON.stringify(form))
|
||||||
|
params.nickName = String(params.nickName || '').trim()
|
||||||
|
if (!params.nickName) {
|
||||||
|
uni.showToast({ title: '请填写昵称', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
delete params.regionPath
|
delete params.regionPath
|
||||||
saveUserInfo(params).then((res) => {
|
saving.value = true
|
||||||
if (res.statusCode === 200) {
|
try {
|
||||||
|
const res = await saveUserInfo(params)
|
||||||
|
if (res.data?.success && res.data.result) {
|
||||||
storage.setUserInfo(res.data.result)
|
storage.setUserInfo(res.data.result)
|
||||||
uni.navigateBack()
|
uni.navigateBack()
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: res.data?.message || '保存失败,请重试', icon: 'none' })
|
||||||
}
|
}
|
||||||
})
|
} catch {
|
||||||
|
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeFace() {
|
function changeFace() {
|
||||||
|
if (avatarUploading.value || saving.value) return
|
||||||
uni.chooseImage({
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
success: (chooseImageRes) => {
|
success: (chooseImageRes) => {
|
||||||
uni.uploadFile({
|
const filePath = chooseImageRes.tempFilePaths?.[0]
|
||||||
url: upload,
|
if (filePath) uploadAvatar(filePath)
|
||||||
filePath: chooseImageRes.tempFilePaths[0],
|
|
||||||
name: 'file',
|
|
||||||
header: { accessToken: storage.getAccessToken() },
|
|
||||||
success: (uploadFileRes) => {
|
|
||||||
const data = JSON.parse(uploadFileRes.data)
|
|
||||||
form.face = data.result
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadAvatar(filePath: string) {
|
||||||
|
if (avatarUploading.value || saving.value) return
|
||||||
|
avatarUploading.value = true
|
||||||
|
uni.uploadFile({
|
||||||
|
url: upload,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
header: { accessToken: storage.getAccessToken() },
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(res.data)
|
||||||
|
if (res.statusCode !== 200 || !data.success || typeof data.result !== 'string' || !data.result) {
|
||||||
|
throw new Error('上传失败')
|
||||||
|
}
|
||||||
|
form.face = data.result
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '头像上传失败,请重新选择', icon: 'none' })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => uni.showToast({ title: '头像上传失败,请重新选择', icon: 'none' }),
|
||||||
|
complete: () => { avatarUploading.value = false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function selectTime(e: any) {
|
function selectTime(e: any) {
|
||||||
const timestamp = typeof e?.value === 'number' ? e.value : birthdayValue.value
|
const timestamp = typeof e?.value === 'number' ? e.value : birthdayValue.value
|
||||||
const formatted = formatBirthday(timestamp)
|
const formatted = formatBirthday(timestamp)
|
||||||
@@ -192,22 +269,11 @@ function navigateToBindMobile(username: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncUserInfo() {
|
onShow(() => {
|
||||||
const latest = storage.getUserInfo() || {}
|
const latest = storage.getUserInfo() || {}
|
||||||
form.nickName = latest.nickName || ''
|
// Refresh a newly bound phone without overwriting unsaved avatar/nickname edits.
|
||||||
form.birthday = latest.birthday || ''
|
|
||||||
form.face = latest.face || '/static/missing-face.png'
|
|
||||||
form.region = latest.region || []
|
|
||||||
form.sex = latest.sex != null ? String(latest.sex) : '1'
|
|
||||||
form.regionPath = latest.region
|
|
||||||
form.mobile = latest.mobile
|
form.mobile = latest.mobile
|
||||||
form.username = latest.username
|
form.username = latest.username
|
||||||
birthday.value = latest.birthday || ''
|
|
||||||
birthdayValue.value = parseBirthdayTimestamp(latest.birthday)
|
|
||||||
}
|
|
||||||
|
|
||||||
onShow(() => {
|
|
||||||
syncUserInfo()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -248,6 +314,23 @@ page {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-button {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
line-height: normal;
|
||||||
|
&::after { border: none; }
|
||||||
|
&[disabled] { background: #fff; color: #999; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 64rpx;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -341,5 +424,9 @@ page {
|
|||||||
font-size: 30rpx;
|
font-size: 30rpx;
|
||||||
background: $light-color;
|
background: $light-color;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
&::after { border: none; }
|
||||||
|
&[disabled] { opacity: .5; background: $light-color; color: #fff; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
></u-cell>
|
></u-cell>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
<u-cell is-link title="用户注销" v-if="userInfo.id" @click="logoff"></u-cell>
|
<u-cell is-link title="用户注销" v-if="userInfo.id" @click="logoff"></u-cell>
|
||||||
|
<u-cell
|
||||||
|
is-link
|
||||||
|
title="消息通知"
|
||||||
|
@click="navigateTo('/pages/mine/set/messageNotice')"
|
||||||
|
></u-cell>
|
||||||
<u-cell is-link title="意见反馈" @click="navigateTo('/pages/mine/set/feedBack')"></u-cell>
|
<u-cell is-link title="意见反馈" @click="navigateTo('/pages/mine/set/feedBack')"></u-cell>
|
||||||
<u-cell
|
<u-cell
|
||||||
is-link
|
is-link
|
||||||
|
|||||||
@@ -1,585 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view :style="themeStyle" class="content">
|
|
||||||
<view class="u-tabs-box">
|
|
||||||
<u-tabs
|
|
||||||
:list="list"
|
|
||||||
:scrollable="false"
|
|
||||||
v-model:current="current"
|
|
||||||
@change="change"
|
|
||||||
:lineColor="lightColor"
|
|
||||||
:activeStyle="{ color: lightColor }"
|
|
||||||
></u-tabs>
|
|
||||||
</view>
|
|
||||||
<div class="u-tabs-search">
|
|
||||||
<u-search
|
|
||||||
placeholder="请输入订单编号/商品名称/售后单号"
|
|
||||||
@search="submitSearchOrderList(current)"
|
|
||||||
@clear="clear(current)"
|
|
||||||
@custom="submitSearchOrderList(current)"
|
|
||||||
v-model="keywords"
|
|
||||||
>
|
|
||||||
</u-search>
|
|
||||||
</div>
|
|
||||||
<scroll-view class="body-view" scroll-y @scrolltolower="renderDate">
|
|
||||||
<view
|
|
||||||
class="seller-view"
|
|
||||||
v-for="(order, orderIndex) in orderList"
|
|
||||||
:key="orderIndex"
|
|
||||||
>
|
|
||||||
<!-- 店铺名称 -->
|
|
||||||
<view class="seller-info" v-if="current == 0">
|
|
||||||
<view class="seller-name wes">{{ order.storeName }}</view>
|
|
||||||
<view class="order-sn wes">订单编号:{{ order.sn }}</view>
|
|
||||||
</view>
|
|
||||||
<!-- 申请记录 选项卡 -->
|
|
||||||
<view class="seller-info" v-if="current != 0">
|
|
||||||
<view class="order-sn wes">售后单号:{{ order.service_sn || order.sn }}</view>
|
|
||||||
<view class="order-sn service-type">{{ order.serviceType_text }}</view>
|
|
||||||
</view>
|
|
||||||
<view v-for="(sku, goodsIndex) in order.orderItems" :key="goodsIndex">
|
|
||||||
<view class="goods-item-view" @click="onDetail(order, sku)">
|
|
||||||
<view class="goods-img">
|
|
||||||
<u-image
|
|
||||||
border-radius="6"
|
|
||||||
width="131rpx"
|
|
||||||
height="131rpx"
|
|
||||||
mode="aspectFill"
|
|
||||||
:src="getGoodsImage(sku, order, goodsIndex)"
|
|
||||||
></u-image>
|
|
||||||
</view>
|
|
||||||
<view class="goods-info">
|
|
||||||
<view class="goods-title">{{ getGoodsName(sku) }}</view>
|
|
||||||
<!-- 如果商品多个则不显示每个商品价格-->
|
|
||||||
<view class="goods-price" v-if="order.orderItems.length <= 1">
|
|
||||||
¥{{ unitPrice(order.flowPrice) }}
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="goods-num">
|
|
||||||
<view>x{{ sku.num }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="description">
|
|
||||||
<!-- 售后申请 -->
|
|
||||||
<view v-if="current === 0 && sku.afterSaleStatus">
|
|
||||||
<view
|
|
||||||
v-if="sku.afterSaleStatus.includes('ALREADY_APPLIED')"
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
>
|
|
||||||
<u-icon class="icon" name="info-circle-fill"></u-icon>
|
|
||||||
该商品已申请售后服务
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view v-if="current === 0 && sku.afterSaleStatus">
|
|
||||||
<view
|
|
||||||
v-if="sku.afterSaleStatus.includes('EXPIRED')"
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
@click="tipsShow = true"
|
|
||||||
>
|
|
||||||
<u-icon class="icon" name="info-circle-fill"></u-icon>
|
|
||||||
该商品无法申请售后
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<div v-if="current === 1 || current === 2">
|
|
||||||
<!-- 申请中 -->
|
|
||||||
<view
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
v-if="order.serviceType == 'RETURN_GOODS'"
|
|
||||||
>
|
|
||||||
退货处理-{{serviceStatusList(order.serviceStatus) }}</view
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
v-if="order.serviceType == 'SUPPLY_AGAIN_GOODS'"
|
|
||||||
>
|
|
||||||
补发商品-{{serviceStatusList(order.serviceStatus) }}</view
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
v-if="order.serviceType == 'RETURN_MONEY'"
|
|
||||||
>
|
|
||||||
退款-{{serviceStatusList(order.serviceStatus) }}</view
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
v-if="order.serviceType == 'EXCHANGE_GOODS'"
|
|
||||||
>
|
|
||||||
换货-{{serviceStatusList(order.serviceStatus) }}</view
|
|
||||||
>
|
|
||||||
<view
|
|
||||||
class="cannot_apply not_center"
|
|
||||||
v-if="order.serviceType == 'CANCEL'"
|
|
||||||
>
|
|
||||||
取消订单-{{serviceStatusList(order.serviceStatus) }}</view
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 申请记录 -->
|
|
||||||
</view>
|
|
||||||
<view class="btn-view">
|
|
||||||
<!-- 售后申请 -->
|
|
||||||
|
|
||||||
<div class="sale" v-if="current === 0 && sku.afterSaleStatus && !isECouponOrder(order.orderType)">
|
|
||||||
<div
|
|
||||||
v-if="
|
|
||||||
order.flowPrice != 0 &&
|
|
||||||
sku.afterSaleStatus.includes('NOT_APPLIED') ||
|
|
||||||
sku.afterSaleStatus.includes('PART_AFTER_SALE')
|
|
||||||
"
|
|
||||||
@click="applyService(sku.sn, order, sku)"
|
|
||||||
>
|
|
||||||
<view class="default-btn border"> 申请售后 </view>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<view class="after-line">
|
|
||||||
<!-- 申请中 -->
|
|
||||||
<view
|
|
||||||
class="default-btn border"
|
|
||||||
v-if="
|
|
||||||
current === 2 &&
|
|
||||||
order.serviceStatus &&
|
|
||||||
order.serviceStatus == 'PASS' &&
|
|
||||||
order.serviceType != 'RETURN_MONEY'
|
|
||||||
"
|
|
||||||
@click="onExpress(order, sku)"
|
|
||||||
>
|
|
||||||
提交物流
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
@click="close(order, sku)"
|
|
||||||
v-if="current === 1"
|
|
||||||
class="default-btn close"
|
|
||||||
>
|
|
||||||
取消售后
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
@click="afterDetails(order, sku)"
|
|
||||||
v-if="current === 1 || current === 2"
|
|
||||||
class="default-btn border"
|
|
||||||
>
|
|
||||||
售后详情
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
v-if="
|
|
||||||
current === 0 &&
|
|
||||||
order.groupAfterSaleStatus &&
|
|
||||||
order.groupAfterSaleStatus != 'ALREADY_APPLIED' &&
|
|
||||||
order.orderItems.length >= 1
|
|
||||||
"
|
|
||||||
class="btn-view order-total"
|
|
||||||
>
|
|
||||||
<!-- 多个商品显示订单总价格 -->
|
|
||||||
<view class="cannot_apply">
|
|
||||||
订单总金额:<span class="countMoney"
|
|
||||||
>¥{{unitPrice(order.flowPrice) }}</span
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<u-loadmore bg-color="#f8f8f8" :status="status" />
|
|
||||||
</scroll-view>
|
|
||||||
<u-modal
|
|
||||||
show-cancel-button
|
|
||||||
@confirm="closeService"
|
|
||||||
v-model:show="cancelShow"
|
|
||||||
content="确认取消售后"
|
|
||||||
></u-modal>
|
|
||||||
<u-modal
|
|
||||||
v-model:show="tipsShow"
|
|
||||||
content="当订单未确认收货|已过售后服务有效期|已申请售后服务时,不能申请售后"
|
|
||||||
></u-modal>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, computed } from 'vue'
|
|
||||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
|
||||||
import { useStore } from '@/store'
|
|
||||||
import { unitPrice, serviceStatusList, parseGoodsImageUrl } from '@/utils/filters.js'
|
|
||||||
// E_COUPON 订单列表不展示「申请售后」入口(FR-B-04)
|
|
||||||
import { isECouponOrder } from '@/utils/goodsType.js'
|
|
||||||
import { getAfterSaleList, cancelAfterSale } from '@/api/after-sale.js'
|
|
||||||
import { getOrderList } from '@/api/order.js'
|
|
||||||
import storage from '@/utils/storage'
|
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
|
||||||
|
|
||||||
const store = useStore()
|
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
|
||||||
|
|
||||||
const list = [
|
|
||||||
{ name: '售后申请' },
|
|
||||||
{ name: '申请中' },
|
|
||||||
{ name: '申请记录' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const current = ref(0)
|
|
||||||
const tipsShow = ref(false)
|
|
||||||
const cancelShow = ref(false)
|
|
||||||
const selectedOrder = ref<any>(null)
|
|
||||||
const orderList = ref<any[]>([])
|
|
||||||
const params = reactive<Record<string, any>>({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
sort: 'createTime',
|
|
||||||
flowPrice: 0,
|
|
||||||
order: 'desc',
|
|
||||||
})
|
|
||||||
const logParams = reactive<Record<string, any>>({
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
})
|
|
||||||
const status = ref('loadmore')
|
|
||||||
const keywords = ref('')
|
|
||||||
|
|
||||||
onLoad((options) => {
|
|
||||||
orderList.value = []
|
|
||||||
params.pageNumber = 1
|
|
||||||
if (options?.orderSn) params.keywords = options.orderSn
|
|
||||||
searchOrderList(current.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
|
||||||
change(current.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
function hideLoadingIfNeeded() {
|
|
||||||
if (store.state.isShowToast) uni.hideLoading()
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGoodsName(sku: any) {
|
|
||||||
return sku.goodsName || sku.name || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGoodsImage(goods: any, order: any, index: number) {
|
|
||||||
let image = goods.image || goods.goodsImage || goods.thumbnail
|
|
||||||
if (!image && order.groupImages) {
|
|
||||||
const images = String(order.groupImages).split(',')
|
|
||||||
image = images[index] || images[0]
|
|
||||||
}
|
|
||||||
return parseGoodsImageUrl(image)
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitSearchOrderList(tabIndex: number) {
|
|
||||||
params.pageNumber = 1
|
|
||||||
logParams.pageNumber = 1
|
|
||||||
orderList.value = []
|
|
||||||
searchOrderList(tabIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear(tabIndex: number) {
|
|
||||||
params.pageNumber = 1
|
|
||||||
logParams.pageNumber = 1
|
|
||||||
params.keywords = ''
|
|
||||||
orderList.value = []
|
|
||||||
searchOrderList(tabIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
function change(e: number | { index: number }) {
|
|
||||||
const index = typeof e === 'object' && e != null ? e.index : e
|
|
||||||
current.value = index
|
|
||||||
Object.assign(params, { pageNumber: 1, pageSize: 10 })
|
|
||||||
orderList.value = []
|
|
||||||
searchOrderList(index)
|
|
||||||
uni.stopPullDownRefresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchOrderList(index: number) {
|
|
||||||
if (index == 0) {
|
|
||||||
if (keywords.value) params.keywords = keywords.value
|
|
||||||
fetchOrderList()
|
|
||||||
} else {
|
|
||||||
Object.assign(logParams, {
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
sort: 'createTime',
|
|
||||||
order: 'desc',
|
|
||||||
})
|
|
||||||
if (index === 1) {
|
|
||||||
logParams.serviceStatus = 'APPLY'
|
|
||||||
}
|
|
||||||
if (keywords.value) logParams.keywords = keywords.value
|
|
||||||
orderList.value = []
|
|
||||||
fetchAfterSaleLogList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchOrderList() {
|
|
||||||
uni.showLoading({ title: '加载中', mask: true })
|
|
||||||
getOrderList(params).then((res) => {
|
|
||||||
hideLoadingIfNeeded()
|
|
||||||
const records = res.data.result.records
|
|
||||||
if (records.length > 0) {
|
|
||||||
orderList.value = orderList.value.concat(records)
|
|
||||||
params.pageNumber += 1
|
|
||||||
}
|
|
||||||
status.value = records.length < 10 ? 'nomore' : 'loading'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function close(order: any, _sku: any) {
|
|
||||||
selectedOrder.value = order
|
|
||||||
cancelShow.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function closeService() {
|
|
||||||
uni.showLoading({ title: '加载中' })
|
|
||||||
const res = await cancelAfterSale(selectedOrder.value.sn)
|
|
||||||
if (res.data.success) {
|
|
||||||
uni.showToast({ title: '取消成功!', duration: 2000, icon: 'none' })
|
|
||||||
}
|
|
||||||
orderList.value = []
|
|
||||||
searchOrderList(current.value)
|
|
||||||
hideLoadingIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
function afterDetails(order: any, _sku?: any) {
|
|
||||||
uni.navigateTo({ url: './applyDetail?sn=' + order.sn })
|
|
||||||
}
|
|
||||||
|
|
||||||
function fetchAfterSaleLogList() {
|
|
||||||
getAfterSaleList(logParams).then((res) => {
|
|
||||||
const afterSaleLogList = res.data.result.records
|
|
||||||
afterSaleLogList.forEach((item: any) => {
|
|
||||||
item.orderItems = [
|
|
||||||
{
|
|
||||||
image: item.goodsImage,
|
|
||||||
skuId: item.skuId,
|
|
||||||
name: item.goodsName,
|
|
||||||
num: item.num,
|
|
||||||
price: item.flowPrice,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
orderList.value = orderList.value.concat(afterSaleLogList)
|
|
||||||
status.value = afterSaleLogList.length < 10 ? 'nomore' : 'loading'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyService(sn: string, order: any, sku: any) {
|
|
||||||
storage.setAfterSaleData({ ...order, ...sku })
|
|
||||||
uni.navigateTo({ url: `/pages/order/afterSales/afterSalesSelect?sn=${sn}` })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onExpress(order: any, sku: any) {
|
|
||||||
sku.storeName = order.storeName
|
|
||||||
storage.setAfterSaleData({ ...order, ...sku })
|
|
||||||
uni.navigateTo({ url: `./afterSalesDetailExpress?serviceSn=${order.sn}` })
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDetail(goods: any, sku: any) {
|
|
||||||
if (current.value == 0) {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/product/goods?id=${sku.skuId}&goodsId=${sku.goodsId || sku.goodsId}`,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/product/goods?id=${goods.skuId}&goodsId=${goods.goodsId || goods.goodsId}`,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDate() {
|
|
||||||
if (current.value === 0) {
|
|
||||||
params.pageNumber += 1
|
|
||||||
fetchOrderList()
|
|
||||||
} else {
|
|
||||||
logParams.pageNumber += 1
|
|
||||||
fetchAfterSaleLogList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
page,
|
|
||||||
.content {
|
|
||||||
background: $page-color-base;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-view {
|
|
||||||
overflow-y: auto;
|
|
||||||
height: calc(100vh - 44px - 80rpx - 104rpx);
|
|
||||||
padding: 0 20rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.u-tabs-search {
|
|
||||||
padding: 20rpx;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.countMoney {
|
|
||||||
margin-left: 7rpx;
|
|
||||||
color: $main-color;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seller-view {
|
|
||||||
background-color: #fff;
|
|
||||||
margin: 20rpx 0;
|
|
||||||
padding: 0 20rpx 24rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
|
|
||||||
.seller-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
min-height: 70rpx;
|
|
||||||
padding: 20rpx 0 16rpx;
|
|
||||||
gap: 20rpx;
|
|
||||||
|
|
||||||
.seller-name {
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-width: 40%;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-sn {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #909399;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-type {
|
|
||||||
flex-shrink: 0;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-item-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 16rpx 0;
|
|
||||||
|
|
||||||
.goods-img {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 131rpx;
|
|
||||||
height: 131rpx;
|
|
||||||
border-radius: 6rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
|
|
||||||
.goods-title {
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
color: #333333;
|
|
||||||
font-size: 26rpx;
|
|
||||||
line-height: 1.4;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-specs {
|
|
||||||
font-size: 24rpx;
|
|
||||||
margin-bottom: 10rpx;
|
|
||||||
color: #cccccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-price {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #ff5a10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.goods-num {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 60rpx;
|
|
||||||
text-align: right;
|
|
||||||
color: $main-color;
|
|
||||||
font-size: 26rpx;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
padding: 16rpx 0 0;
|
|
||||||
|
|
||||||
.after-line {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 15rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-total {
|
|
||||||
justify-content: flex-start;
|
|
||||||
padding-top: 8rpx;
|
|
||||||
padding-bottom: 4rpx;
|
|
||||||
|
|
||||||
.cannot_apply {
|
|
||||||
text-align: left;
|
|
||||||
height: auto;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
color: #909399;
|
|
||||||
size: 25rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cannot_apply {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 22rpx;
|
|
||||||
|
|
||||||
color: #999999;
|
|
||||||
height: 70rpx;
|
|
||||||
line-height: 70rpx;
|
|
||||||
}
|
|
||||||
.not_center {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
margin-right: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sale {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.default-btn {
|
|
||||||
background-color: #ffffff;
|
|
||||||
margin-left: 15rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
line-height: 60rpx;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24rpx;
|
|
||||||
padding: 0 24rpx;
|
|
||||||
border-radius: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close {
|
|
||||||
color: $light-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border {
|
|
||||||
border: 2rpx solid $light-color;
|
|
||||||
color: $light-color;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -158,7 +158,7 @@ function onSubmit() {
|
|||||||
title: '提交成功',
|
title: '提交成功',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
back: true,
|
back: true,
|
||||||
url: '/pages/order/afterSales/afterSales',
|
url: '/pages/order/myOrder?status=5',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,80 +1,275 @@
|
|||||||
<template>
|
<template>
|
||||||
<view :style="themeStyle">
|
<view :style="themeStyle" class="apply-success-page">
|
||||||
<view class="header">
|
<u-navbar
|
||||||
<u-icon name='checkmark' size="150" color="#ff6b35"></u-icon>
|
title="提交成功"
|
||||||
<view class="success-text">售后申请提交成功</view>
|
:border="false"
|
||||||
<view class="btn-view">
|
:fixed="true"
|
||||||
<view class="btn-item">
|
:placeholder="true"
|
||||||
<u-button ripple class="btn1" shape="circle" @click="navigateToAfterSales()">查看记录</u-button>
|
:auto-back="false"
|
||||||
|
@leftClick="handleBack"
|
||||||
|
></u-navbar>
|
||||||
|
|
||||||
|
<view class="page-body">
|
||||||
|
<!-- 核心状态卡片 -->
|
||||||
|
<view class="result-card">
|
||||||
|
<view class="icon-halo">
|
||||||
|
<view class="icon-circle">
|
||||||
|
<u-icon name="checkmark" size="52rpx" color="#ffffff"></u-icon>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="btn-item">
|
|
||||||
<u-button ripple class="btn2" shape="circle" @click="navigateToHome()">回到主页</u-button>
|
<view class="success-title">售后申请提交成功</view>
|
||||||
|
<view class="success-sub">商家将尽快为您审核处理,请留意消息通知</view>
|
||||||
|
|
||||||
|
<view class="btn-group">
|
||||||
|
<view class="action-btn btn-outline" @click="navigateToHome()">
|
||||||
|
回到主页
|
||||||
|
</view>
|
||||||
|
<view class="action-btn btn-primary" @click="navigateToAfterSales()">
|
||||||
|
查看记录
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 温馨提示卡片 -->
|
||||||
|
<view class="tips-card">
|
||||||
|
<view class="tips-header">
|
||||||
|
<view class="tips-bar"></view>
|
||||||
|
<text class="tips-title">温馨提示</text>
|
||||||
|
</view>
|
||||||
|
<view class="tips-list">
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text class="tip-text">商品寄回地址将在商家审核通过后,在「我的订单 - 售后」详情中展示。</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text class="tip-text">提交服务单后,售后专员可能与您电话沟通,请保持手机畅通。</text>
|
||||||
|
</view>
|
||||||
|
<view class="tip-item">
|
||||||
|
<view class="tip-dot"></view>
|
||||||
|
<text class="tip-text">售后处理完成后,退款金额将原路退回至您的原支付账户中。</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
<view class="text-view">
|
|
||||||
<view style="display: flex; margin-bottom: 20rpx; align-items: center">温馨提示:</view>
|
|
||||||
<view>• 商品寄回地址将在审核通过后在申请记录中查询。</view>
|
|
||||||
<view>• 提交服务单后,售后专员可能与您电话沟通,请保持手机畅通。</view>
|
|
||||||
<view>• 退货处理成功后退款金额将原路返回到您的支持账户中;</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">import { computed } from 'vue'
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { useStore } from '@/store'
|
import { useStore } from '@/store'
|
||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
|
import {
|
||||||
|
prefetchMpMessageTemplates,
|
||||||
|
subscribeMpMessageByScenes,
|
||||||
|
} from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
prefetchMpMessageTemplates()
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
const doBack = () => {
|
||||||
|
uni.navigateBack({
|
||||||
|
fail: () => {
|
||||||
|
uni.redirectTo({
|
||||||
|
url: '/pages/order/myOrder?status=5',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['AFTER_SALE_AUDIT'], doBack)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
doBack()
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToAfterSales() {
|
function navigateToAfterSales() {
|
||||||
uni.redirectTo({
|
const goAfterSales = () => {
|
||||||
url: '/pages/order/afterSales/afterSales',
|
uni.redirectTo({
|
||||||
})
|
url: '/pages/order/myOrder?status=5',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['AFTER_SALE_AUDIT'], goAfterSales)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
goAfterSales()
|
||||||
|
// #endif
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToHome() {
|
function navigateToHome() {
|
||||||
uni.switchTab({
|
const goHome = () => {
|
||||||
url: '/pages/tabbar/home/index',
|
uni.switchTab({
|
||||||
})
|
url: '/pages/tabbar/home/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
subscribeMpMessageByScenes(['AFTER_SALE_AUDIT'], goHome)
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
goHome()
|
||||||
|
// #endif
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
page {
|
.apply-success-page {
|
||||||
background: $page-color-base;
|
min-height: 100vh;
|
||||||
padding-bottom: 100rpx;
|
background: #f5f6fa;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.btn1 {
|
|
||||||
background: $light-color;
|
.page-body {
|
||||||
color: #fff;
|
padding: 28rpx 24rpx 60rpx;
|
||||||
}
|
}
|
||||||
.btn2 {
|
|
||||||
color: $light-color;
|
.result-card {
|
||||||
}
|
background: #ffffff;
|
||||||
.header {
|
border-radius: 24rpx;
|
||||||
background-color: #fff;
|
padding: 64rpx 40rpx 52rpx;
|
||||||
height: 500rpx;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(17, 30, 79, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-halo {
|
||||||
|
width: 140rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 107, 53, 0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
.success-text {
|
}
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 400;
|
.icon-circle {
|
||||||
margin: 20rpx 0;
|
width: 104rpx;
|
||||||
}
|
height: 104rpx;
|
||||||
.btn-view {
|
border-radius: 50%;
|
||||||
display: flex;
|
background: linear-gradient(135deg, $main-color 0%, $light-color 100%);
|
||||||
flex-direction: row;
|
display: flex;
|
||||||
margin: 40rpx 0rpx;
|
align-items: center;
|
||||||
.btn-item {
|
justify-content: center;
|
||||||
margin: 0rpx 20rpx;
|
box-shadow: 0 8rpx 20rpx rgba(229, 50, 56, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.success-title {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1d2129;
|
||||||
|
letter-spacing: 0.5rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-sub {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 25rpx;
|
||||||
|
color: #86909c;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group {
|
||||||
|
margin-top: 48rpx;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 82rpx;
|
||||||
|
line-height: 82rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.text-view {
|
|
||||||
margin: 40rpx;
|
.btn-primary {
|
||||||
color: #909399;
|
background: linear-gradient(135deg, $main-color 0%, $light-color 100%);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(229, 50, 56, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: #fff6f3;
|
||||||
|
color: $main-color;
|
||||||
|
border: 2rpx solid rgba(229, 50, 56, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-card {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
padding: 32rpx 32rpx 36rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(17, 30, 79, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-bar {
|
||||||
|
width: 6rpx;
|
||||||
|
height: 26rpx;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
background: linear-gradient(180deg, $main-color 0%, $light-color 100%);
|
||||||
|
margin-right: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 10rpx;
|
||||||
|
height: 10rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #c9cdd4;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 25rpx;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: #4e5969;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -346,7 +346,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<u-row>
|
<u-row>
|
||||||
<u-col :span="6">活动优惠</u-col>
|
<u-col :span="6">活动优惠(含会员优惠)</u-col>
|
||||||
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
<u-col :span="6" class="tr tipsColor" textAlign="right">
|
||||||
<span v-if="orderMessage.priceDetailDTO.discountPrice"
|
<span v-if="orderMessage.priceDetailDTO.discountPrice"
|
||||||
>-¥{{unitPrice(orderMessage.priceDetailDTO.discountPrice)
|
>-¥{{unitPrice(orderMessage.priceDetailDTO.discountPrice)
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
<view class="btn-view u-flex u-row-between">
|
<view class="btn-view u-flex u-row-between">
|
||||||
<view class="description">{{ afterSaleTypeText(order.serviceType) }}</view>
|
<view class="description">{{ afterSaleTypeText(order.serviceType) }}</view>
|
||||||
<view class="goods-btn flex flex-a-c">
|
<view class="goods-btn flex flex-a-c">
|
||||||
|
<view class="cancel-btn" v-if="order.serviceStatus === 'APPLY'" @click="onCancelAfterSale(order)">取消售后</view>
|
||||||
|
<view class="pay-btn" v-if="order.serviceStatus === 'PASS' && order.serviceType !== 'RETURN_MONEY'" @click="onAfterSaleExpress(order)">提交物流</view>
|
||||||
<view class="pay-btn" @click="openOrder(order)">售后详情</view>
|
<view class="pay-btn" @click="openOrder(order)">售后详情</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -112,7 +114,7 @@
|
|||||||
<u-popup class="cancel-popup" v-model:show="cancelShow" mode="bottom" length="60%">
|
<u-popup class="cancel-popup" v-model:show="cancelShow" mode="bottom" length="60%">
|
||||||
<view class="header">取消订单</view>
|
<view class="header">取消订单</view>
|
||||||
<view class="body">
|
<view class="body">
|
||||||
<view class="title">取消订单后,本单享有的优惠可能会一并取消,是否继续?</view>
|
<view class="title">{{ cancelIsPickup ? '此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。是否继续?' : '取消订单后,本单享有的优惠可能会一并取消,是否继续?' }}</view>
|
||||||
<view>
|
<view>
|
||||||
<u-radio-group v-model="reason">
|
<u-radio-group v-model="reason">
|
||||||
<view class="value">
|
<view class="value">
|
||||||
@@ -131,6 +133,8 @@
|
|||||||
<u-toast ref="uToast" />
|
<u-toast ref="uToast" />
|
||||||
<u-modal :confirm-color="lightColor" v-model:show="rogShow" :show-cancel-button="true" :content="'是否确认收货?'"
|
<u-modal :confirm-color="lightColor" v-model:show="rogShow" :show-cancel-button="true" :content="'是否确认收货?'"
|
||||||
@confirm="confirmRog"></u-modal>
|
@confirm="confirmRog"></u-modal>
|
||||||
|
<u-modal :confirm-color="lightColor" v-model:show="cancelAfterSaleShow" :show-cancel-button="true" content="确认取消售后?"
|
||||||
|
@confirm="confirmCancelAfterSale"></u-modal>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -141,7 +145,8 @@ import { useStore } from '@/store'
|
|||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue'
|
import uniLoadMore from '@/components/uni-load-more/uni-load-more.vue'
|
||||||
import { getOrderList, cancelOrder, confirmReceipt } from '@/api/order.js'
|
import { getOrderList, cancelOrder, confirmReceipt } from '@/api/order.js'
|
||||||
import { getClearReason, getAfterSaleList } from '@/api/after-sale.js'
|
import { getClearReason, getAfterSaleList, cancelAfterSale } from '@/api/after-sale.js'
|
||||||
|
import storage from '@/utils/storage'
|
||||||
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
|
import LiLiWXPay from '@/js_sdk/lili-pay/wx-pay.js'
|
||||||
import {
|
import {
|
||||||
unitPrice,
|
unitPrice,
|
||||||
@@ -200,6 +205,8 @@ const orderSn = ref('')
|
|||||||
const reason = ref('')
|
const reason = ref('')
|
||||||
const cancelList = ref<any[]>([])
|
const cancelList = ref<any[]>([])
|
||||||
const rogShow = ref(false)
|
const rogShow = ref(false)
|
||||||
|
const cancelAfterSaleShow = ref(false)
|
||||||
|
const pendingAfterSaleSn = ref('')
|
||||||
|
|
||||||
function hideLoadingIfNeeded() {
|
function hideLoadingIfNeeded() {
|
||||||
if (store.state.isShowToast) uni.hideLoading()
|
if (store.state.isShowToast) uni.hideLoading()
|
||||||
@@ -292,7 +299,10 @@ function getOrderItemImage(goods: any, order: any, index: number) {
|
|||||||
return parseGoodsImageUrl(image)
|
return parseGoodsImageUrl(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cancelIsPickup = ref(false)
|
||||||
|
|
||||||
function onCancel(sn: string) {
|
function onCancel(sn: string) {
|
||||||
|
cancelIsPickup.value = !!navList.flatMap(tab => tab.orderList).find(order => order.sn === sn)?.pickupCardId
|
||||||
orderSn.value = sn
|
orderSn.value = sn
|
||||||
cancelShow.value = true
|
cancelShow.value = true
|
||||||
uni.showLoading({ title: '加载中' })
|
uni.showLoading({ title: '加载中' })
|
||||||
@@ -418,6 +428,30 @@ function openOrder(order: any) {
|
|||||||
navigateToOrderDetail(order.sn)
|
navigateToOrderDetail(order.sn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCancelAfterSale(order: any) {
|
||||||
|
pendingAfterSaleSn.value = order?.serviceSn || order?.sn || ''
|
||||||
|
cancelAfterSaleShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmCancelAfterSale() {
|
||||||
|
if (!pendingAfterSaleSn.value) return
|
||||||
|
uni.showLoading({ title: '加载中' })
|
||||||
|
const res = await cancelAfterSale(pendingAfterSaleSn.value)
|
||||||
|
hideLoadingIfNeeded()
|
||||||
|
if (res?.data?.success) {
|
||||||
|
uni.showToast({ title: '取消成功!', duration: 2000, icon: 'none' })
|
||||||
|
initData(tabCurrentIndex.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAfterSaleExpress(order: any) {
|
||||||
|
const sku = (order?.orderItems && order.orderItems[0]) || {}
|
||||||
|
storage.setAfterSaleData({ ...order, ...sku, storeName: order.storeName })
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `./afterSales/afterSalesDetailExpress?serviceSn=${order.serviceSn || order.sn}`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function changeTab(e: any) {
|
function changeTab(e: any) {
|
||||||
// H5 的 swiper change 把下标放在 detail.current。
|
// H5 的 swiper change 把下标放在 detail.current。
|
||||||
// 读 target.current 会得到 undefined,选中态被清掉,列表就停在「全部」。
|
// 读 target.current 会得到 undefined,选中态被清掉,列表就停在「全部」。
|
||||||
|
|||||||
@@ -257,7 +257,7 @@
|
|||||||
<u-popup class="cancel-popup" v-model:show="cancelShow" mode="bottom" length="60%">
|
<u-popup class="cancel-popup" v-model:show="cancelShow" mode="bottom" length="60%">
|
||||||
<view class="header">取消订单</view>
|
<view class="header">取消订单</view>
|
||||||
<view class="body">
|
<view class="body">
|
||||||
<view class="title">取消订单后,本单享有的优惠可能会一并取消,是否继续?</view>
|
<view class="title">{{ order.pickupCardId ? '此订单使用提货卡兑换,取消不退还提货权益,无法再次兑换。是否继续?' : '取消订单后,本单享有的优惠可能会一并取消,是否继续?' }}</view>
|
||||||
<view>
|
<view>
|
||||||
<u-radio-group v-model="reason">
|
<u-radio-group v-model="reason">
|
||||||
<view class="value">
|
<view class="value">
|
||||||
|
|||||||
@@ -70,7 +70,7 @@
|
|||||||
|
|
||||||
<GoodsSwiper id="main1" :res="imgList" :video="goodsDetail.goodsVideo" />
|
<GoodsSwiper id="main1" :res="imgList" :video="goodsDetail.goodsVideo" />
|
||||||
|
|
||||||
<!-- 促销活动条(秒杀/限时直降/拼团等,M-02 含 E_COUPON) -->
|
<!-- 促销活动条(秒杀/拼团等,M-02 含 E_COUPON) -->
|
||||||
<PromotionAssembleLayout v-if="PromotionList" :detail="goodsDetail" :res="PromotionList" />
|
<PromotionAssembleLayout v-if="PromotionList" :detail="goodsDetail" :res="PromotionList" />
|
||||||
|
|
||||||
<view class="card-box top-radius-0" id="main2">
|
<view class="card-box top-radius-0" id="main2">
|
||||||
@@ -139,6 +139,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="paidQuote && !isSeckill && !isGroup" class="card-box"><PaidMemberPrice :quote="paidQuote" /></view>
|
||||||
<!-- 促销/领券入口 -->
|
<!-- 促销/领券入口 -->
|
||||||
<view class="card-box">
|
<view class="card-box">
|
||||||
<view class="card-flex" @click="shutMask(1)">
|
<view class="card-flex" @click="shutMask(1)">
|
||||||
@@ -298,6 +299,8 @@ import PromotionAssembleLayout from './product/promotion/-promotion-assemble-pro
|
|||||||
import PromotionAssembleListLayout from './product/promotion/-promotion-assemble-list'
|
import PromotionAssembleListLayout from './product/promotion/-promotion-assemble-list'
|
||||||
import PromotionCoupon from './product/promotion/-promotion-coupon'
|
import PromotionCoupon from './product/promotion/-promotion-coupon'
|
||||||
import GoodsIntro from './product/goods/-goods-intro'
|
import GoodsIntro from './product/goods/-goods-intro'
|
||||||
|
import PaidMemberPrice from '@/components/paid-member-price/index.vue'
|
||||||
|
import { getPaidMemberQuotes } from '@/api/paid-member'
|
||||||
import GoodsRecommend from './product/goods/-goods-recommend'
|
import GoodsRecommend from './product/goods/-goods-recommend'
|
||||||
import storeLayout from './product/shop/-shop'
|
import storeLayout from './product/shop/-shop'
|
||||||
import Evaluation from './product/evaluation/-evaluation'
|
import Evaluation from './product/evaluation/-evaluation'
|
||||||
@@ -338,6 +341,19 @@ const scrollId = ref("1")
|
|||||||
const scrollFlag = ref(true)
|
const scrollFlag = ref(true)
|
||||||
const current = ref("1")
|
const current = ref("1")
|
||||||
const goodsDetail = ref<any>({})
|
const goodsDetail = ref<any>({})
|
||||||
|
const paidQuote = ref<any>(null)
|
||||||
|
let paidQuoteRequest = 0
|
||||||
|
async function loadPaidQuote() {
|
||||||
|
const id = ++paidQuoteRequest, memberId = String(isLogin()?.id || ''), skuId = goodsDetail.value?.id
|
||||||
|
paidQuote.value = null
|
||||||
|
if (!memberId || !skuId) return
|
||||||
|
try {
|
||||||
|
const res = await getPaidMemberQuotes([skuId])
|
||||||
|
if (id === paidQuoteRequest && String(isLogin()?.id || '') === memberId && res.data?.success) paidQuote.value = res.data.result?.[skuId] || null
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
watch(() => [goodsDetail.value?.id, store.state.userInfo?.id, store.state.hasLogin], loadPaidQuote)
|
||||||
|
onShow(loadPaidQuote)
|
||||||
const goodsSpec = ref<any>("")
|
const goodsSpec = ref<any>("")
|
||||||
const imgList = ref<any[]>([])
|
const imgList = ref<any[]>([])
|
||||||
const favorite = ref(false)
|
const favorite = ref(false)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
class="showBox"
|
class="showBox"
|
||||||
v-if="
|
v-if="
|
||||||
promotion.__key == 'SECKILL' ||
|
promotion.__key == 'SECKILL' ||
|
||||||
promotion.__key == 'FLASH_DISCOUNT' ||
|
|
||||||
promotion.__key == 'GROUPBUY' ||
|
promotion.__key == 'GROUPBUY' ||
|
||||||
promotion.__key == 'PINTUAN'
|
promotion.__key == 'PINTUAN'
|
||||||
"
|
"
|
||||||
@@ -37,29 +36,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 限时直降 -->
|
|
||||||
<view class="u-group-flex-left" v-if="promotion.__key == 'FLASH_DISCOUNT'">
|
|
||||||
<span
|
|
||||||
class="u-group-flex-left-span"
|
|
||||||
v-if="detail.promotionPrice != undefined"
|
|
||||||
>
|
|
||||||
¥
|
|
||||||
<span class="flex-price">
|
|
||||||
{{ goodsFormatPrice(detail.promotionPrice)[0] }}.{{
|
|
||||||
goodsFormatPrice(detail.promotionPrice)[1]
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<view class="u-group-flex" v-if="detail.price != undefined">
|
|
||||||
<span class="old-price"
|
|
||||||
>¥{{ goodsFormatPrice(detail.price)[0] }}.{{
|
|
||||||
goodsFormatPrice(detail.price)[1]
|
|
||||||
}}</span
|
|
||||||
>
|
|
||||||
<view class="promotion">限时直降</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 团购 -->
|
<!-- 团购 -->
|
||||||
<view class="u-group-flex-left" v-if="promotion.__key == 'GROUPBUY'">
|
<view class="u-group-flex-left" v-if="promotion.__key == 'GROUPBUY'">
|
||||||
<span class="u-group-flex-left-span">
|
<span class="u-group-flex-left-span">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<text v-else-if="item.scopeType == 'PORTION_GOODS'">部分商品</text>
|
<text v-else-if="item.scopeType == 'PORTION_GOODS'">部分商品</text>
|
||||||
<text v-else>{{ item.storeName == 'platform' ? '全平台' : item.storeName + '店铺' }}使用</text>
|
<text v-else>{{ item.storeName == 'platform' ? '全平台' : item.storeName + '店铺' }}使用</text>
|
||||||
</view>
|
</view>
|
||||||
<text>满{{ unitPrice(item.consumeThreshold) }}可用</text>
|
<text>{{ item.consumeThreshold > 0 ? '满' + unitPrice(item.consumeThreshold) + '可用' : '无门槛使用' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="lingqu-btn" @tap="getCoupon(item, index)">
|
<view class="lingqu-btn" @tap="getCoupon(item, index)">
|
||||||
<view class="lingqu-text" :class="{ cur: yhqFlag[index] }">
|
<view class="lingqu-text" :class="{ cur: yhqFlag[index] }">
|
||||||
|
|||||||
@@ -40,26 +40,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'FLASH_DISCOUNT'">
|
|
||||||
<view class="res_prom_item">
|
|
||||||
<view class="deg_tag">限时直降</view>
|
|
||||||
<text class="pro-text">{{ props.res[prom].promotionName || props.res[prom].title || '限时直降活动' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'NTH_ITEM_DISCOUNT'">
|
|
||||||
<view class="res_prom_item">
|
|
||||||
<view class="deg_tag">第N件优惠</view>
|
|
||||||
<text class="pro-text">
|
|
||||||
第{{ props.res[prom].nthNum || 'N' }}件
|
|
||||||
<text v-if="props.res[prom].discountType === 'FREE'">免费</text>
|
|
||||||
<text v-else-if="props.res[prom].discountType === 'HALF'">半价</text>
|
|
||||||
<text v-else-if="props.res[prom].discountValue">享{{ props.res[prom].discountValue }}折</text>
|
|
||||||
<text v-else>优惠</text>
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="getPromType(prom) == 'POINTS_GOODS'">
|
<view v-if="getPromType(prom) == 'POINTS_GOODS'">
|
||||||
<view class="res_prom_item">
|
<view class="res_prom_item">
|
||||||
<view class="deg_tag">积分活动</view>
|
<view class="deg_tag">积分活动</view>
|
||||||
@@ -93,7 +73,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const promotionKeys = computed(() => {
|
const promotionKeys = computed(() => {
|
||||||
if (!props.res || typeof props.res !== 'object') return []
|
if (!props.res || typeof props.res !== 'object') return []
|
||||||
return Object.keys(props.res).filter((key) => getPromType(key) !== 'COUPON')
|
return Object.keys(props.res).filter((key) =>
|
||||||
|
['FULL_DISCOUNT', 'PINTUAN', 'SECKILL', 'POINTS_GOODS', 'KANJIA'].includes(getPromType(key))
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
function getPromType(key: string) {
|
function getPromType(key: string) {
|
||||||
|
|||||||
@@ -2,18 +2,6 @@
|
|||||||
<view>
|
<view>
|
||||||
<view v-for="(promotionItem, promotionIndex) in promotion" :key="promotionIndex" class="promotion_row" @click="shutMask(1)">
|
<view v-for="(promotionItem, promotionIndex) in promotion" :key="promotionIndex" class="promotion_row" @click="shutMask(1)">
|
||||||
<view v-if="res != null" v-for="(item, index) in Object.keys(res)" :key="index">
|
<view v-if="res != null" v-for="(item, index) in Object.keys(res)" :key="index">
|
||||||
<div class="promotion_col" v-if="item.split('-')[0] == promotionItem.value && item.split('-')[0] == 'FLASH_DISCOUNT'">
|
|
||||||
<div class="flex">
|
|
||||||
<view class="deg_tag">{{ promotionItem.title }}</view>
|
|
||||||
<div class="text proText">{{ res[item].promotionName || res[item].title || '限时直降' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="promotion_col" v-if="item.split('-')[0] == promotionItem.value && item.split('-')[0] == 'NTH_ITEM_DISCOUNT'">
|
|
||||||
<div class="flex">
|
|
||||||
<view class="deg_tag">{{ promotionItem.title }}</view>
|
|
||||||
<div class="text proText">{{ res[item].promotionName || res[item].title || '第N件优惠' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="promotion_col" v-if="item.split('-')[0] == promotionItem.value && item.split('-')[0] == 'FULL_DISCOUNT'">
|
<div class="promotion_col" v-if="item.split('-')[0] == promotionItem.value && item.split('-')[0] == 'FULL_DISCOUNT'">
|
||||||
<!-- 满减,折扣 -->
|
<!-- 满减,折扣 -->
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ const promotion = [
|
|||||||
title: "砍价活动",
|
title: "砍价活动",
|
||||||
value: "KANJIA",
|
value: "KANJIA",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "限时直降",
|
|
||||||
value: "FLASH_DISCOUNT",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "第N件优惠",
|
|
||||||
value: "NTH_ITEM_DISCOUNT",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "团购",
|
title: "团购",
|
||||||
value: "GROUPBUY",
|
value: "GROUPBUY",
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
<view class="xian"></view>
|
<view class="xian"></view>
|
||||||
<view class="text">
|
<view class="text">
|
||||||
<text>{{ "领取优惠券" }}</text>
|
<text>{{ "领取优惠券" }}</text>
|
||||||
<text>满{{unitPrice(item.consumeThreshold) }}元可用</text>
|
<text>{{ item.consumeThreshold > 0 ? '满' + unitPrice(item.consumeThreshold) + '元可用' : '无门槛使用' }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -36,11 +36,12 @@
|
|||||||
}}</span
|
}}</span
|
||||||
>元</view
|
>元</view
|
||||||
>
|
>
|
||||||
<view
|
<view v-if="item.consumeThreshold > 0"
|
||||||
>满<span style="color: red; font-size: 32rpx"
|
>满<span style="color: red; font-size: 32rpx"
|
||||||
>{{unitPrice(item.consumeThreshold) }}元</span
|
>{{unitPrice(item.consumeThreshold) }}元</span
|
||||||
>可用</view
|
>可用</view
|
||||||
>
|
>
|
||||||
|
<view v-else>无门槛使用</view>
|
||||||
<view v-if="item.scopeType == 'ALL' && item.storeId == '0'"
|
<view v-if="item.scopeType == 'ALL' && item.storeId == '0'"
|
||||||
>全平台</view
|
>全平台</view
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<text class="coupon-value">{{ unitPrice(item.price) }}</text>
|
<text class="coupon-value">{{ unitPrice(item.price) }}</text>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
<view class="coupon-threshold">满{{ unitPrice(item.consumeThreshold) }}元可用</view>
|
<view class="coupon-threshold">{{ item.consumeThreshold > 0 ? '满' + unitPrice(item.consumeThreshold) + '元可用' : '无门槛使用' }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="coupon-right">
|
<view class="coupon-right">
|
||||||
<view class="coupon-name">
|
<view class="coupon-name">
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="user-page" :style="themeStyle">
|
<view class="user-page" :style="themeStyle">
|
||||||
<!-- 顶部沉浸式头部区域 -->
|
<!-- 顶部沉浸式头部区域 -->
|
||||||
<view class="hero-header" :style="{ paddingTop: headerPaddingTop + 'px' }">
|
<view class="hero-header" :style="heroHeaderStyle">
|
||||||
<!-- 用户信息区(沉降至胶囊安全区下方,舒展大气) -->
|
<image
|
||||||
|
v-if="gradeBackgroundUrl && !gradeBackgroundFailed"
|
||||||
|
:key="gradeBackgroundUrl"
|
||||||
|
class="grade-background"
|
||||||
|
:class="{ 'is-loaded': gradeBackgroundLoaded }"
|
||||||
|
:src="gradeBackgroundUrl"
|
||||||
|
mode="aspectFill"
|
||||||
|
@load="gradeBackgroundLoaded = true"
|
||||||
|
@error="onGradeBackgroundError"
|
||||||
|
/>
|
||||||
|
<!-- 用户信息区保持在微信胶囊安全区下方 -->
|
||||||
<view class="user-profile" @click="userDetail">
|
<view class="user-profile" @click="userDetail">
|
||||||
<view class="avatar-container">
|
<view class="avatar-container">
|
||||||
<image class="avatar-img" :src="userInfo.face || userImage" mode="aspectFill"></image>
|
<image class="avatar-img" :src="userInfo.face || userImage" mode="aspectFill"></image>
|
||||||
<view class="vip-tag-icon" v-if="userInfo.id">
|
|
||||||
<u-icon name="level" size="10" color="#ffffff"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="user-detail-info">
|
<view class="user-detail-info">
|
||||||
@@ -16,17 +23,19 @@
|
|||||||
<text class="user-nickname">{{ userInfo.id ? userInfo.nickName : '登录 / 注册' }}</text>
|
<text class="user-nickname">{{ userInfo.id ? userInfo.nickName : '登录 / 注册' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="user-sub-row">
|
<view class="user-sub-row">
|
||||||
<view class="badge-tag" v-if="userInfo.id">
|
<view class="badge-tag" v-if="userInfo.id" @click.stop="navigateTo('/pages/mine/memberBenefits')">
|
||||||
<text>普通会员</text>
|
<image v-if="gradeIconUrl && !gradeIconFailed" :key="gradeIconUrl" class="grade-icon" :src="gradeIconUrl" mode="aspectFit" @error="gradeIconFailed = true" />
|
||||||
|
<u-icon v-else name="level" size="16" :color="memberHeaderColor"></u-icon>
|
||||||
|
<text class="grade-name">{{ memberGradeName }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="user-sub-text" v-else>
|
<text class="user-sub-text" v-else>
|
||||||
登录享受更多会员特权与折扣
|
登录享受更多会员权益
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="profile-arrow">
|
<view class="profile-arrow">
|
||||||
<u-icon name="arrow-right" size="14" color="rgba(255, 255, 255, 0.75)"></u-icon>
|
<u-icon name="arrow-right" size="14" :color="memberHeaderColor"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -60,6 +69,13 @@
|
|||||||
|
|
||||||
<!-- 下方主内容卡片区域 -->
|
<!-- 下方主内容卡片区域 -->
|
||||||
<view class="main-container">
|
<view class="main-container">
|
||||||
|
<navigator v-if="showPaidMemberEntry" class="paid-entry" :style="{ color: paidEntryColor }" url="/pages/mine/paidMember" hover-class="none">
|
||||||
|
<image v-if="paidEntryBackground" class="entry-bg" :src="paidEntryBackground" mode="aspectFill" />
|
||||||
|
<view class="entry-content">
|
||||||
|
<view><text class="entry-title">{{ paidEntryTitle }}</text><text class="entry-subtitle">{{ paidEntrySubtitle }}</text></view>
|
||||||
|
<u-icon name="arrow-right" size="14" :color="paidEntryColor" />
|
||||||
|
</view>
|
||||||
|
</navigator>
|
||||||
<!-- 我的订单卡片 -->
|
<!-- 我的订单卡片 -->
|
||||||
<view class="card-box order-box">
|
<view class="card-box order-box">
|
||||||
<view class="card-title-bar">
|
<view class="card-title-bar">
|
||||||
@@ -109,13 +125,13 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 服务工具卡片(分组化重构) -->
|
<!-- 服务工具卡片(分组化重构) -->
|
||||||
<tool />
|
<tool :user-info="userInfo" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
onLoad,
|
onLoad,
|
||||||
onShow,
|
onShow,
|
||||||
@@ -123,6 +139,7 @@ import {
|
|||||||
onNavigationBarButtonTap,
|
onNavigationBarButtonTap,
|
||||||
} from '@dcloudio/uni-app'
|
} from '@dcloudio/uni-app'
|
||||||
import tool from '@/pages/tabbar/user/utils/tool.vue'
|
import tool from '@/pages/tabbar/user/utils/tool.vue'
|
||||||
|
import { getPaidMember } from '@/api/paid-member'
|
||||||
import { getCouponsNum, getFootprintNum } from '@/api/members.js'
|
import { getCouponsNum, getFootprintNum } from '@/api/members.js'
|
||||||
import { getUserWallet } from '@/api/members'
|
import { getUserWallet } from '@/api/members'
|
||||||
import configs from '@/config/config'
|
import configs from '@/config/config'
|
||||||
@@ -130,6 +147,13 @@ import { useStore } from '@/store'
|
|||||||
import { getThemeStyle } from '@/utils/theme'
|
import { getThemeStyle } from '@/utils/theme'
|
||||||
import { isLogin, navigateToLogin, unitPrice } from '@/utils/filters.js'
|
import { isLogin, navigateToLogin, unitPrice } from '@/utils/filters.js'
|
||||||
|
|
||||||
|
import { getCurrentMemberBenefits } from '@/api/members'
|
||||||
|
const memberGrade = ref<any>({})
|
||||||
|
const gradeOwnerId = ref('')
|
||||||
|
const gradeBackgroundLoaded = ref(false)
|
||||||
|
const gradeBackgroundFailed = ref(false)
|
||||||
|
const gradeIconFailed = ref(false)
|
||||||
|
let gradeRequestId = 0
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
const themeStyle = computed(() => getThemeStyle(store.state.theme))
|
||||||
const lightColor = computed(() => store.getters.lightColor)
|
const lightColor = computed(() => store.getters.lightColor)
|
||||||
@@ -138,12 +162,116 @@ const aiderLightColor = computed(() => store.getters.aiderLightColor)
|
|||||||
|
|
||||||
const userImage = configs.defaultUserPhoto
|
const userImage = configs.defaultUserPhoto
|
||||||
const userInfo = ref<any>({})
|
const userInfo = ref<any>({})
|
||||||
|
const paidMemberInfo = ref<any>(null)
|
||||||
|
let paidMemberRequestId = 0
|
||||||
|
const showPaidMemberEntry = computed(() => !!paidMemberInfo.value?.config
|
||||||
|
&& !!(paidMemberInfo.value.config.enabled || paidMemberInfo.value.active || paidMemberInfo.value.status === 'EXPIRED'))
|
||||||
|
const paidEntryTitle = computed(() => paidMemberInfo.value?.active
|
||||||
|
? paidMemberInfo.value.config?.openedText || '我的付费会员'
|
||||||
|
: paidMemberInfo.value?.config?.unopenedText || '开通付费会员,享专属权益')
|
||||||
|
const paidEntryColor = computed(() => (paidMemberInfo.value?.active
|
||||||
|
? paidMemberInfo.value.config?.openedFontColor
|
||||||
|
: paidMemberInfo.value?.config?.unopenedFontColor) || '#f4dfb9')
|
||||||
|
const paidEntryBackground = computed(() => paidMemberInfo.value?.active
|
||||||
|
? paidMemberInfo.value.config?.openedBackground
|
||||||
|
: paidMemberInfo.value?.config?.unopenedBackground)
|
||||||
|
const paidEntrySubtitle = computed(() => paidMemberInfo.value?.active
|
||||||
|
? '有效期至 ' + paidMemberInfo.value.endTime
|
||||||
|
: paidMemberInfo.value?.status === 'EXPIRED' ? '会员已到期,续费继续享权益' : '开通享商品优惠与专属权益')
|
||||||
const couponNum = ref<string | number>('')
|
const couponNum = ref<string | number>('')
|
||||||
const footNum = ref<string | number>('')
|
const footNum = ref<string | number>('')
|
||||||
const walletNum = ref<string | number>('')
|
const walletNum = ref<string | number>('')
|
||||||
|
|
||||||
|
const currentGrade = computed(() =>
|
||||||
|
userInfo.value.id && gradeOwnerId.value === String(userInfo.value.id) ? memberGrade.value : {}
|
||||||
|
)
|
||||||
|
const memberGradeName = computed(() => currentGrade.value.gradeName || '普通会员')
|
||||||
|
const gradeBackgroundUrl = computed(() => currentGrade.value.gradeBackground || '')
|
||||||
|
const gradeIconUrl = computed(() => currentGrade.value.gradeImage || '')
|
||||||
|
const memberHeaderColor = computed(() => {
|
||||||
|
if (!gradeBackgroundUrl.value || !gradeBackgroundLoaded.value || gradeBackgroundFailed.value) return '#ffffff'
|
||||||
|
const color = currentGrade.value.gradeFontColor || ''
|
||||||
|
return /^#[0-9a-f]{6}$/i.test(color) ? color : '#333333'
|
||||||
|
})
|
||||||
|
const heroHeaderStyle = computed(() => ({
|
||||||
|
paddingTop: headerPaddingTop.value + 'px',
|
||||||
|
'--member-header-color': memberHeaderColor.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
watch(gradeBackgroundUrl, () => {
|
||||||
|
gradeBackgroundLoaded.value = false
|
||||||
|
gradeBackgroundFailed.value = false
|
||||||
|
}, { flush: 'sync' })
|
||||||
|
watch(gradeIconUrl, () => { gradeIconFailed.value = false }, { flush: 'sync' })
|
||||||
|
|
||||||
|
function resetMemberGrade() {
|
||||||
|
gradeRequestId++
|
||||||
|
gradeOwnerId.value = ''
|
||||||
|
memberGrade.value = {}
|
||||||
|
gradeBackgroundLoaded.value = false
|
||||||
|
gradeBackgroundFailed.value = false
|
||||||
|
gradeIconFailed.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 个人中心显示时读取会员配置及资格;退出或切换账号后丢弃旧请求结果。 */
|
||||||
|
async function loadPaidMemberInfo() {
|
||||||
|
const requestId = ++paidMemberRequestId
|
||||||
|
const memberId = String(userInfo.value.id || '')
|
||||||
|
paidMemberInfo.value = null
|
||||||
|
if (!memberId) return
|
||||||
|
try {
|
||||||
|
const res = await getPaidMember()
|
||||||
|
if (requestId !== paidMemberRequestId
|
||||||
|
|| String(userInfo.value.id || '') !== memberId
|
||||||
|
|| String((isLogin() || {}).id || '') !== memberId) return
|
||||||
|
if (!res.data?.success) throw new Error(res.data?.message || '会员服务返回异常')
|
||||||
|
paidMemberInfo.value = res.data.result || null
|
||||||
|
} catch (error: any) {
|
||||||
|
if (requestId === paidMemberRequestId) {
|
||||||
|
console.warn('[付费会员] 入口加载失败', error?.message || '请求失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGradeBackgroundError() {
|
||||||
|
gradeBackgroundFailed.value = true
|
||||||
|
gradeBackgroundLoaded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMemberGrade() {
|
||||||
|
const memberId = String(userInfo.value.id || '')
|
||||||
|
if (!memberId) { resetMemberGrade(); return }
|
||||||
|
if (gradeOwnerId.value !== memberId) resetMemberGrade()
|
||||||
|
const requestId = ++gradeRequestId
|
||||||
|
const stillCurrent = () => requestId === gradeRequestId
|
||||||
|
&& String((isLogin() || {}).id || '') === memberId
|
||||||
|
&& String(userInfo.value.id || '') === memberId
|
||||||
|
try {
|
||||||
|
const res = await getCurrentMemberBenefits()
|
||||||
|
if (!stillCurrent()) return
|
||||||
|
gradeOwnerId.value = memberId
|
||||||
|
memberGrade.value = res.data?.success ? (res.data.result?.grade || {}) : {}
|
||||||
|
} catch {
|
||||||
|
if (stillCurrent()) resetMemberGrade()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保留页在退出登录/切换账号时立即清除上一账号的等级展示。
|
||||||
|
watch(() => [store.state.userInfo?.id, store.state.hasLogin], () => {
|
||||||
|
const account = store.state.hasLogin === false ? {} : (isLogin() || {})
|
||||||
|
if (String(account.id || '') !== String(userInfo.value.id || '')) {
|
||||||
|
userInfo.value = account
|
||||||
|
resetMemberGrade()
|
||||||
|
paidMemberRequestId++
|
||||||
|
paidMemberInfo.value = null
|
||||||
|
walletNum.value = 0
|
||||||
|
couponNum.value = 0
|
||||||
|
footNum.value = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// 动态计算顶部安全留白,避免顶到灵动岛/状态栏
|
// 动态计算顶部安全留白,避免顶到灵动岛/状态栏
|
||||||
const headerPaddingTop = ref<number>(88)
|
const headerPaddingTop = ref<number>(80)
|
||||||
|
|
||||||
function initSafeTop() {
|
function initSafeTop() {
|
||||||
try {
|
try {
|
||||||
@@ -152,17 +280,17 @@ function initSafeTop() {
|
|||||||
// #ifdef MP-WEIXIN
|
// #ifdef MP-WEIXIN
|
||||||
const menu = uni.getMenuButtonBoundingClientRect()
|
const menu = uni.getMenuButtonBoundingClientRect()
|
||||||
if (menu && menu.bottom) {
|
if (menu && menu.bottom) {
|
||||||
// 头像整行落到微信胶囊下方,预留 16px 呼吸空隙,彻底消除压迫感
|
// 胶囊下方保留 8px,减少空白且不遮挡用户信息
|
||||||
headerPaddingTop.value = menu.bottom + 16
|
headerPaddingTop.value = menu.bottom + 8
|
||||||
} else {
|
} else {
|
||||||
headerPaddingTop.value = statusBar + 54
|
headerPaddingTop.value = statusBar + 44
|
||||||
}
|
}
|
||||||
// #endif
|
// #endif
|
||||||
// #ifndef MP-WEIXIN
|
// #ifndef MP-WEIXIN
|
||||||
headerPaddingTop.value = statusBar + 32
|
headerPaddingTop.value = statusBar + 20
|
||||||
// #endif
|
// #endif
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
headerPaddingTop.value = 88
|
headerPaddingTop.value = 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,21 +298,26 @@ onLoad(() => {
|
|||||||
initSafeTop()
|
initSafeTop()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
async function refreshUserPage() {
|
||||||
userInfo.value = isLogin() || {}
|
userInfo.value = isLogin() || {}
|
||||||
if (isLogin('auth')) {
|
try {
|
||||||
getUserOrderNum()
|
if (isLogin('auth')) {
|
||||||
} else {
|
await Promise.allSettled([loadMemberGrade(), loadPaidMemberInfo(), getUserOrderNum()])
|
||||||
walletNum.value = 0
|
} else {
|
||||||
couponNum.value = 0
|
resetMemberGrade()
|
||||||
footNum.value = 0
|
paidMemberRequestId++
|
||||||
|
paidMemberInfo.value = null
|
||||||
|
walletNum.value = 0
|
||||||
|
couponNum.value = 0
|
||||||
|
footNum.value = 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onShow(refreshUserPage)
|
||||||
getUserOrderNum()
|
onPullDownRefresh(refreshUserPage)
|
||||||
userInfo.value = isLogin()
|
|
||||||
})
|
|
||||||
|
|
||||||
// #ifndef MP
|
// #ifndef MP
|
||||||
onNavigationBarButtonTap((e) => {
|
onNavigationBarButtonTap((e) => {
|
||||||
@@ -205,21 +338,34 @@ function userDetail() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getUserOrderNum() {
|
async function getUserOrderNum() {
|
||||||
uni.stopPullDownRefresh()
|
const memberId = String(userInfo.value.id || '')
|
||||||
|
try {
|
||||||
Promise.all([
|
// Each request can fail independently; a missing wallet must not break the other counts.
|
||||||
getCouponsNum(),
|
const results = await Promise.allSettled([
|
||||||
getFootprintNum(),
|
getCouponsNum(),
|
||||||
getUserWallet(),
|
getFootprintNum(),
|
||||||
]).then((res: any[]) => {
|
getUserWallet(),
|
||||||
couponNum.value = res[0].data.result
|
])
|
||||||
footNum.value = res[1].data.result
|
// The user may have logged out while these requests were running.
|
||||||
walletNum.value = res[2].data.result.memberWallet
|
if (String((isLogin() || {}).id || '') !== memberId || String(userInfo.value.id || '') !== memberId) return
|
||||||
})
|
const targets = [couponNum, footNum, walletNum]
|
||||||
|
results.forEach((result, index) => {
|
||||||
|
if (result.status !== 'fulfilled' || !result.value?.data?.success) return
|
||||||
|
const data = result.value.data.result
|
||||||
|
targets[index].value = index === 2 ? (data?.memberWallet ?? 0) : (data ?? 0)
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// 等级与资产请求独立,接口异常不影响页面其它内容。
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.paid-entry { position: relative; overflow: hidden; border-radius: 20rpx; background: linear-gradient(110deg, #423c36, #756149); margin-bottom: 20rpx; }
|
||||||
|
.entry-bg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
||||||
|
.entry-content { position: relative; display: flex; align-items: center; justify-content: space-between; gap: 20rpx; padding: 25rpx 28rpx; }
|
||||||
|
.entry-title { font-size: 27rpx; font-weight: 600; display: block; }
|
||||||
|
.entry-subtitle { font-size: 21rpx; opacity: .75; display: block; margin-top: 8rpx; }
|
||||||
.user-page {
|
.user-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f6f9;
|
background-color: #f5f6f9;
|
||||||
@@ -234,19 +380,35 @@ async function getUserOrderNum() {
|
|||||||
background: linear-gradient(175deg, #e53238 0%, #eb4448 45%, #ff5e62 100%);
|
background: linear-gradient(175deg, #e53238 0%, #eb4448 45%, #ff5e62 100%);
|
||||||
padding-left: 28rpx;
|
padding-left: 28rpx;
|
||||||
padding-right: 28rpx;
|
padding-right: 28rpx;
|
||||||
padding-bottom: 64rpx;
|
padding-bottom: 44rpx;
|
||||||
border-bottom-left-radius: 36rpx;
|
border-bottom-left-radius: 36rpx;
|
||||||
border-bottom-right-radius: 36rpx;
|
border-bottom-right-radius: 36rpx;
|
||||||
/* CSS 静态兜底:状态栏高度 + 充足的沉降留白 */
|
/* 静态兜底保留状态栏与导航操作区域 */
|
||||||
padding-top: calc(var(--status-bar-height, 44px) + 88rpx);
|
padding-top: calc(var(--status-bar-height, 44px) + 72rpx);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 用户信息行(移到胶囊下方,拥有充裕横纵向空间) */
|
.grade-background {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-background.is-loaded {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 紧凑用户信息行 */
|
||||||
.user-profile {
|
.user-profile {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 8rpx 6rpx;
|
padding: 4rpx 6rpx;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
@@ -258,8 +420,8 @@ async function getUserOrderNum() {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.avatar-img {
|
.avatar-img {
|
||||||
width: 124rpx;
|
width: 104rpx;
|
||||||
height: 124rpx;
|
height: 104rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 4rpx solid rgba(255, 255, 255, 0.92);
|
border: 4rpx solid rgba(255, 255, 255, 0.92);
|
||||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.16);
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.16);
|
||||||
@@ -267,19 +429,6 @@ async function getUserOrderNum() {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vip-tag-icon {
|
|
||||||
position: absolute;
|
|
||||||
right: -2rpx;
|
|
||||||
bottom: 2rpx;
|
|
||||||
width: 34rpx;
|
|
||||||
height: 34rpx;
|
|
||||||
background: linear-gradient(135deg, #f59e0b, #d97706);
|
|
||||||
border: 2rpx solid #ffffff;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-detail-info {
|
.user-detail-info {
|
||||||
@@ -292,9 +441,9 @@ async function getUserOrderNum() {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.user-nickname {
|
.user-nickname {
|
||||||
font-size: 38rpx;
|
font-size: 34rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: var(--member-header-color, #ffffff);
|
||||||
letter-spacing: 0.5rpx;
|
letter-spacing: 0.5rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -303,25 +452,46 @@ async function getUserOrderNum() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-sub-row {
|
.user-sub-row {
|
||||||
margin-top: 12rpx;
|
margin-top: 8rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
|
|
||||||
.badge-tag {
|
.badge-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 40rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #ffffff;
|
color: var(--member-header-color, #ffffff);
|
||||||
background: rgba(255, 255, 255, 0.22);
|
background: rgba(255, 255, 255, 0.22);
|
||||||
padding: 3rpx 14rpx;
|
padding: 2rpx 12rpx 2rpx 6rpx;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
border: 1rpx solid rgba(255, 255, 255, 0.35);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
|
.grade-icon {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-sub-text {
|
.user-sub-text {
|
||||||
font-size: 23rpx;
|
font-size: 23rpx;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
color: var(--member-header-color, #ffffff);
|
||||||
|
opacity: 0.88;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -336,12 +506,13 @@ async function getUserOrderNum() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 核心资产栏(沉浸在红色背景中) */
|
/* 核心资产栏 */
|
||||||
.hero-assets {
|
.hero-assets {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 44rpx;
|
margin-top: 24rpx;
|
||||||
padding: 0 12rpx;
|
padding: 0 12rpx;
|
||||||
|
|
||||||
.asset-col {
|
.asset-col {
|
||||||
@@ -360,7 +531,7 @@ async function getUserOrderNum() {
|
|||||||
.asset-number-row {
|
.asset-number-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
color: #ffffff;
|
color: var(--member-header-color, #ffffff);
|
||||||
|
|
||||||
.symbol {
|
.symbol {
|
||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
@@ -370,7 +541,7 @@ async function getUserOrderNum() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.num {
|
.num {
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.5rpx;
|
letter-spacing: 0.5rpx;
|
||||||
}
|
}
|
||||||
@@ -390,9 +561,10 @@ async function getUserOrderNum() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.asset-title {
|
.asset-title {
|
||||||
margin-top: 10rpx;
|
margin-top: 8rpx;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: rgba(255, 255, 255, 0.88);
|
color: var(--member-header-color, #ffffff);
|
||||||
|
opacity: 0.88;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,7 +572,8 @@ async function getUserOrderNum() {
|
|||||||
.asset-sep {
|
.asset-sep {
|
||||||
width: 2rpx;
|
width: 2rpx;
|
||||||
height: 38rpx;
|
height: 38rpx;
|
||||||
background-color: rgba(255, 255, 255, 0.22);
|
background-color: var(--member-header-color, #ffffff);
|
||||||
|
opacity: 0.22;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,11 @@
|
|||||||
<text class="grid-label">店铺入驻</text>
|
<text class="grid-label">店铺入驻</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view v-if="hasMerchantPermission" class="service-grid-item" @click="toMerchantWorkbench">
|
||||||
|
<image class="grid-icon" src="/static/mine/setting.png" mode="aspectFit"></image>
|
||||||
|
<text class="grid-label">商家工作台</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view class="service-grid-item" @click="linkMsgDetail()">
|
<view class="service-grid-item" @click="linkMsgDetail()">
|
||||||
<image class="grid-icon" src="/static/mine/mycommit.png" mode="aspectFit"></image>
|
<image class="grid-icon" src="/static/mine/mycommit.png" mode="aspectFit"></image>
|
||||||
<text class="grid-label">我的消息</text>
|
<text class="grid-label">我的消息</text>
|
||||||
@@ -115,7 +120,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { distribution as fetchDistribution } from '@/api/distribution'
|
import { distribution as fetchDistribution } from '@/api/distribution'
|
||||||
import configs from '@/config/config'
|
import configs from '@/config/config'
|
||||||
import storage from '@/utils/storage'
|
import storage from '@/utils/storage'
|
||||||
@@ -128,9 +133,47 @@ import {
|
|||||||
DISTRIBUTION_STATUS,
|
DISTRIBUTION_STATUS,
|
||||||
} from '@/utils/distributionStatus'
|
} from '@/utils/distributionStatus'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
userInfo?: any
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
userInfo: () => ({}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const sharingShow = ref(false)
|
const sharingShow = ref(false)
|
||||||
const sharingLink = ref('')
|
const sharingLink = ref('')
|
||||||
|
|
||||||
|
// 只有商家权限(店主 haveStore / 绑有 storeId 的店员 / 本地已有有效商家 token)的用户才展示入口
|
||||||
|
const hasMerchantPermission = computed(() => {
|
||||||
|
if (storage.getStoreAccessToken()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const u = (props.userInfo && props.userInfo.id) ? props.userInfo : (storage.getUserInfo() || {})
|
||||||
|
return Boolean(u.haveStore === true || u.haveStore === 'true' || u.storeId)
|
||||||
|
})
|
||||||
|
|
||||||
|
function toMerchantWorkbench() {
|
||||||
|
if (!hasMerchantPermission.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无商家权限',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const storeToken = storage.getStoreAccessToken()
|
||||||
|
if (storeToken) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages-sub/merchant/pages/index/index',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages-sub/merchant/pages/login/index',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleNavigate(url: string) {
|
function handleNavigate(url: string) {
|
||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ const FACE_LOGIN = isDev ? "face_login_dev" : "face_login";
|
|||||||
const FINGER_LOGIN = isDev ? "finger_login_dev" : "finger_login";
|
const FINGER_LOGIN = isDev ? "finger_login_dev" : "finger_login";
|
||||||
const CART_BACKBTN = isDev ? "cart_backbtn_dev" : "cart_backbtn";
|
const CART_BACKBTN = isDev ? "cart_backbtn_dev" : "cart_backbtn";
|
||||||
const AFTERSALE_DATA = isDev ? "aftersale_data_dev" : "aftersale_data";
|
const AFTERSALE_DATA = isDev ? "aftersale_data_dev" : "aftersale_data";
|
||||||
|
|
||||||
|
// 商家端专属 Key(与买家隔离)
|
||||||
|
const STORE_ACCESS_TOKEN = isDev ? "store_access_token_dev" : "store_access_token";
|
||||||
|
const STORE_REFRESH_TOKEN = isDev ? "store_refresh_token_dev" : "store_refresh_token";
|
||||||
|
const STORE_INFO = isDev ? "store_info_obj_dev" : "store_info_obj";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setInviter(val){
|
setInviter(val){
|
||||||
uni.setStorageSync('inviter', val)
|
uni.setStorageSync('inviter', val)
|
||||||
@@ -147,6 +153,41 @@ export default {
|
|||||||
uni.setStorageSync('imGoodId', val);
|
uni.setStorageSync('imGoodId', val);
|
||||||
},
|
},
|
||||||
getImGoodsLink() {
|
getImGoodsLink() {
|
||||||
return uni.getStorageSync('imGoodId');
|
return uni.getStorageSync('imGoodId');
|
||||||
|
},
|
||||||
|
|
||||||
|
// ============== 商家专属 Token 与鉴权方法 ==============
|
||||||
|
setStoreAccessToken(val) {
|
||||||
|
uni.setStorageSync(STORE_ACCESS_TOKEN, val);
|
||||||
|
},
|
||||||
|
getStoreAccessToken() {
|
||||||
|
return uni.getStorageSync(STORE_ACCESS_TOKEN);
|
||||||
|
},
|
||||||
|
removeStoreAccessToken() {
|
||||||
|
uni.removeStorageSync(STORE_ACCESS_TOKEN);
|
||||||
|
},
|
||||||
|
setStoreRefreshToken(val) {
|
||||||
|
uni.setStorageSync(STORE_REFRESH_TOKEN, val);
|
||||||
|
},
|
||||||
|
getStoreRefreshToken() {
|
||||||
|
return uni.getStorageSync(STORE_REFRESH_TOKEN);
|
||||||
|
},
|
||||||
|
removeStoreRefreshToken() {
|
||||||
|
uni.removeStorageSync(STORE_REFRESH_TOKEN);
|
||||||
|
},
|
||||||
|
setStoreInfo(val) {
|
||||||
|
uni.setStorageSync(STORE_INFO, val);
|
||||||
|
},
|
||||||
|
getStoreInfo() {
|
||||||
|
return uni.getStorageSync(STORE_INFO);
|
||||||
|
},
|
||||||
|
removeStoreInfo() {
|
||||||
|
uni.removeStorageSync(STORE_INFO);
|
||||||
|
},
|
||||||
|
/** 清理商家登录状态,保留买家会话 */
|
||||||
|
clearStoreAuth() {
|
||||||
|
uni.removeStorageSync(STORE_ACCESS_TOKEN);
|
||||||
|
uni.removeStorageSync(STORE_REFRESH_TOKEN);
|
||||||
|
uni.removeStorageSync(STORE_INFO);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
31
utils/wechatAddress.js
Normal file
31
utils/wechatAddress.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export function getWechatAddressErrorType(error) {
|
||||||
|
const message = String(error?.errMsg || error?.message || '')
|
||||||
|
if (/cancel/i.test(message)) return 'cancel'
|
||||||
|
if (/requiredPrivateInfos/i.test(message)) return 'configuration'
|
||||||
|
if (/privacy|隐私/i.test(message)) return 'privacy'
|
||||||
|
if (/auth deny|auth denied|authorize no response|auth reject/i.test(message)) return 'auth'
|
||||||
|
if (/not support|not supported|not available|not a function/i.test(message)) return 'unsupported'
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match each name under its parent; never guess an ID from a similarly named region.
|
||||||
|
export async function resolveWechatRegion(address, fetchRegions) {
|
||||||
|
const names = [address.provinceName, address.cityName, address.countyName, address.streetName]
|
||||||
|
const selected = []
|
||||||
|
let parentId = 0
|
||||||
|
for (let level = 0; level < 4; level++) {
|
||||||
|
const response = await fetchRegions(parentId)
|
||||||
|
if (!response?.data?.success || !Array.isArray(response.data.result)) {
|
||||||
|
throw new Error('地区加载失败')
|
||||||
|
}
|
||||||
|
const children = response.data.result
|
||||||
|
if (!children.length) return selected.length >= 3 ? selected : null
|
||||||
|
const name = String(names[level] || '').trim()
|
||||||
|
if (!name) return null
|
||||||
|
const matches = children.filter(item => String(item.name || item.localName || '').trim() === name)
|
||||||
|
if (matches.length !== 1 || !matches[0].id) return null
|
||||||
|
selected.push(matches[0])
|
||||||
|
parentId = matches[0].id
|
||||||
|
}
|
||||||
|
return selected
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user