chore: 初始开源发布
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# 统一使用 LF,避免跨平台协作时产生整文件 diff
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# 二进制文件不做行尾转换
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.pdf binary
|
||||||
|
*.exe binary
|
||||||
|
*.xlsx binary
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
.hbuilderx
|
||||||
|
|
||||||
|
.stylelintcache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
/docs/
|
||||||
|
|
||||||
|
src/types
|
||||||
|
# 单独把这个文件排除掉,用以解决部分电脑生成的 auto-import.d.ts 的API不完整导致类型提示报错问题
|
||||||
|
!src/types/auto-import.d.ts
|
||||||
|
src/manifest.json
|
||||||
|
src/pages.json
|
||||||
|
|
||||||
|
# 2025-10-15 by 菲鸽: lock 文件还是需要加入版本管理,今天又遇到版本不一致导致无法运行的问题了。
|
||||||
|
# pnpm-lock.yaml
|
||||||
|
# package-lock.json
|
||||||
|
|
||||||
|
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
|
||||||
|
# `git rm -r --cached .` 然后提交 commit 即可。
|
||||||
|
|
||||||
|
# git rm -r --cached file1 file2 ## 针对某些文件
|
||||||
|
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
|
||||||
|
# git rm -r --cached . ## 针对所有文件
|
||||||
|
|
||||||
|
# 更新 uni-app 官方版本
|
||||||
|
# npx @dcloudio/uvm@latest
|
||||||
|
*.log
|
||||||
|
*.exe
|
||||||
|
*.tar
|
||||||
|
*.zip
|
||||||
|
.vscode
|
||||||
|
.cursor
|
||||||
|
|
||||||
|
# Local upload files generated by the backend.
|
||||||
|
server/resource/upload/
|
||||||
|
server/resource/certs/*
|
||||||
|
!server/resource/certs/
|
||||||
|
server/upload/
|
||||||
|
server/.env
|
||||||
|
server/xiujie-wechat/
|
||||||
|
|
||||||
|
# Deployment host runtime configuration.
|
||||||
|
deploy/.env
|
||||||
|
|
||||||
|
# Local tooling / preview artifacts (not part of source)
|
||||||
|
.workbuddy/
|
||||||
|
app/.card-dom.html
|
||||||
|
app/.preview-*.png
|
||||||
|
app/_tmp_*
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 项目约束规范
|
||||||
|
## xiuAdmin框架说明文档
|
||||||
|
先阅读 http://xiuadmin.com/sys/catalog.html
|
||||||
|
## 范围界定
|
||||||
|
1. 本仓库包含两部分代码:
|
||||||
|
- 后端:基于 GoFrame v2 框架,存放于 `server/` 目录
|
||||||
|
- 前端:Vue + Vben Admin 技术栈,存放于 `app/` 目录
|
||||||
|
2. GoFrame 项目专属规范仅适用于 `server/` 下后端开发,或用户明确要求进行 GoFrame 开发的场景。
|
||||||
|
3. 仅修改前端代码时,不得套用 GoFrame 后端开发规范。
|
||||||
|
4. `vue/` 目录下前端开发需遵循 Vben Admin 5.x 开发规范,同步沿用 `vue/apps/web-antd` 中已落地的 Ant Design Vue 页面风格。
|
||||||
|
|
||||||
|
## GoFrame 后端开发规范
|
||||||
|
1. 控制器、服务、中间件、路由、配置、ORM、自动生成代码均严格遵循 GoFrame v2 官方编码惯例。
|
||||||
|
2. 禁止手动编辑自动生成的 DAO、DO、Entity 文件,统一使用项目内置代码生成流程更新。
|
||||||
|
3. 新增逻辑前优先复用项目现有服务、工具函数与通用设计模式,避免重复新增抽象层。
|
||||||
|
4. 数据库新增、更新操作必须使用自动生成的 DAO/DO 结构体;禁止将 `g.Map`、`map[string]interface{}` 作为 ORM 数据载体。
|
||||||
|
5. 后端新增业务报错统一使用 `gerror` 组件封装并返回错误信息。
|
||||||
|
6. 常规 ORM 操作交由 GoFrame 自动维护 `created_at`、`updated_at`、`deleted_at` 字段;逻辑删除统一调用 `Delete()` 方法。
|
||||||
|
|
||||||
|
|
||||||
|
## 前端开发规范
|
||||||
|
1. 必须使用中文进行对话以及编写文档。
|
||||||
|
2. 编写代码、修改文档或做业务判断前,必须先查询是否有可使用的本地 SKILL、项目文档、知识库、已有实现或现成工具,再决定行动方案。
|
||||||
|
3. 如果上下文过长、信息开始混乱或可能遗忘已确认内容,应提醒用户:建议压缩上下文。
|
||||||
|
4. 用户指定修改某个部分时,只处理指定范围;未经用户同意,不修改其他未指定部分。
|
||||||
|
5. 不要覆盖用户或其他工具已经产生的改动。若目标文件已有人工改动,应先理解并兼容现状。
|
||||||
|
6. 编写界面前必须先确认 Wot UI、uni-app 内置组件、项目现成组件和 UnoCSS 是否能满足需求;不能跳过这一步直接手搓样式或 UI。
|
||||||
|
7. 默认禁止手搓 CSS 和基础 UI 组件。只有在 UI 库、项目现成组件、UnoCSS 均无法满足,或存在明确平台兼容/业务封装需要时,才允许例外。
|
||||||
|
8. 使用例外时,必须在最终汇报中说明:为什么不能用 UnoCSS、为什么不能用 UI 库或现成组件、手写范围控制在哪里。
|
||||||
|
9. 若新增界面代码绕过 UnoCSS 或 UI 库且未说明例外依据,本轮任务视为未完成,必须先返工再汇报完成。
|
||||||
|
|
||||||
|
## 信息确认原则
|
||||||
|
1. 避免仅凭推测给出答案。回答或修改前,应优先基于以下来源确认信息:
|
||||||
|
- 当前项目文件、已有实现、配置、测试结果;
|
||||||
|
- 用户明确说明;
|
||||||
|
- `docs/README.md` 及相关业务文档;
|
||||||
|
- 管理平台后端和前端当前实现;
|
||||||
|
- 本地 SKILL、项目文档、知识库或现成工具;
|
||||||
|
- 官方文档或联网查询结果。
|
||||||
|
2. 当需求不明确、存在多种实现方式、影响范围较大、或可能破坏现有逻辑时,必须先向用户确认。
|
||||||
|
3. 当需求明确、范围清楚、风险较低时,可以直接执行,并在完成后说明处理内容和验证结果。
|
||||||
|
4. 涉及时效性信息,例如版本、法规、价格、接口文档、第三方库最新用法、新闻等,应优先使用官方来源或联网查询。
|
||||||
|
|
||||||
|
|
||||||
|
## 行为准则
|
||||||
|
|
||||||
|
1. 没有确切指令时,不得擅自替用户做高风险决策。
|
||||||
|
2. 在多轮对话中,已确认或用户未提出异议的内容必须保持一致。若用户变更需求,应明确记录并按新需求调整。
|
||||||
|
3. 需要参考案例或材料时,优先参考项目内类似实现;找不到或不确定时,再联网查询最新资料。
|
||||||
|
4. 对于多模块、高风险或探索性任务,应按模块推进,并在关键节点向用户确认。
|
||||||
|
5. 对于范围明确的常规任务,可以一次完成实现、校验和汇报。
|
||||||
|
6. 保持专业,坚持最佳实践。若用户需求明显不符合最佳实践,应说明风险并提供可选方案。
|
||||||
|
7. 当判断用户的方案、行动或前提存在明显问题时,以基于事实和最佳实践进行辩论为荣,以不分青红皂白附和用户为耻。
|
||||||
|
8. 保持客观、冷静、专业,避免过度奉承或无关情绪化表达。
|
||||||
|
|
||||||
|
|
||||||
|
## 系统菜单权限
|
||||||
|
1. 登录系统界面后,左侧菜单栏菜单都是动态路由菜单。菜单数据都放在 sys_menu表。菜单栏里有权限标识(不带前缀)。权限标识有相关的前缀。
|
||||||
|
"cpr:" // 角色权限码前缀+角色权限
|
||||||
|
"cpm:" // 菜单权限码前缀+菜单权限
|
||||||
|
"cpd:" // 部门权限码前缀+部门编码
|
||||||
|
"cpp:" // 岗位权限码前缀+岗位编码
|
||||||
|
"cpu:" // 用户权限码前缀+用户名
|
||||||
|
"cpc:current:user" // 当前登录用户默认权限
|
||||||
|
2. 如果登录的是超级管理员账号 admin,默认返回所有权限。
|
||||||
|
3. 如果登录的不是超级管理员账号:
|
||||||
|
1. 登录用户取得用户的所有角色;
|
||||||
|
2. 根据角色取得所有角色绑定的菜单;
|
||||||
|
3. 获取菜单中的权限标识。
|
||||||
|
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
`iotlcm-app` 是 IOTLCM 的用户侧小程序项目,定位为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
物联卡用户服务小程序 + 代理商轻量工作台
|
||||||
|
```
|
||||||
|
|
||||||
|
技术栈:
|
||||||
|
|
||||||
|
- uni-app
|
||||||
|
- Vue 3
|
||||||
|
- TypeScript
|
||||||
|
- Vite 5
|
||||||
|
- UnoCSS
|
||||||
|
- Wot UI v2
|
||||||
|
- unibest
|
||||||
|
|
||||||
|
输出端:
|
||||||
|
|
||||||
|
- H5
|
||||||
|
- 微信小程序
|
||||||
|
- APP
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# iotlcm-app
|
||||||
|
|
||||||
|
iotLCM 物联网卡全生命周期管理平台的**小程序端 + 用户端服务**。
|
||||||
|
|
||||||
|
- `app/` —— uni-app 小程序前端(基于 [unibest](https://github.com/unibest-tech/unibest),MIT)
|
||||||
|
- `server/` —— 用户端 GoFrame 服务(基于 XiuAdmin 框架,Apache-2.0)
|
||||||
|
|
||||||
|
配套管理后台:[iotlcm-admin-svr](https://git.5iot.com/iotlcm-public/iotlcm-admin-svr) / [iotlcm-admin-ui](https://git.5iot.com/iotlcm-public/iotlcm-admin-ui)
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
**小程序端**(`app/`)
|
||||||
|
|
||||||
|
- 公开查卡:输入卡号即可查状态、流量、到期时间
|
||||||
|
- 卡片详情与用量曲线(日/月分库)
|
||||||
|
- 一键诊断:拍卡号或手输,问题排查直达
|
||||||
|
- 账号体系:组织下个人账号、角色权限
|
||||||
|
|
||||||
|
**用户端服务**(`server/`)
|
||||||
|
|
||||||
|
- 小程序端的业务后端:卡映射、诊断、订单、余额、消息、工单
|
||||||
|
- 独立于管理后台的用户域数据与权限
|
||||||
|
- 对接上游通道完成真实查卡与诊断
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- 前端:uni-app + Vue 3 + TypeScript + Vite 5 + UnoCSS + wot-ui + z-paging
|
||||||
|
- 后端:Go 1.25 + GoFrame v2 + MySQL + Redis
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd server
|
||||||
|
go mod tidy
|
||||||
|
go run .
|
||||||
|
```
|
||||||
|
|
||||||
|
默认监听 `:8199`。
|
||||||
|
|
||||||
|
### 小程序前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd app
|
||||||
|
pnpm install
|
||||||
|
pnpm dev:mp-weixin # 微信小程序,产物用微信开发者工具打开 dist/dev/mp-weixin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
| 位置 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `server/config/config.yaml` | 后端配置(**公开版凭据为占位符**) |
|
||||||
|
| `app/env/.env*` | 前端环境变量(**公开版 AppID 为占位符**) |
|
||||||
|
|
||||||
|
后端配置占位:
|
||||||
|
|
||||||
|
| 配置项 | 占位 |
|
||||||
|
| --- | --- |
|
||||||
|
| `database.default.link` | `mysql:${DB_USER}:${DB_PASSWORD}@tcp(${DB_HOST}:3306)` |
|
||||||
|
| `redis.default.address` / `pass` | `${REDIS_ADDR}` / `${REDIS_PASSWORD}` |
|
||||||
|
| `jwt.signingKey` | `${JWT_SIGNING_KEY}` |
|
||||||
|
|
||||||
|
前端配置占位:
|
||||||
|
|
||||||
|
| 变量 | 占位 |
|
||||||
|
| --- | --- |
|
||||||
|
| `VITE_WX_APPID` | `YOUR_WX_APPID` |
|
||||||
|
| `VITE_UNI_APPID` | `__UNI__YOURAPPID` |
|
||||||
|
| `VITE_MP_SERVER_BASEURL` | `https://admin.iotlcm.example.com/app/api/v1` |
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
iotlcm-app/
|
||||||
|
├── app/ # uni-app 小程序前端
|
||||||
|
│ ├── src/pages/ # 页面(首页、查卡、诊断、登录等)
|
||||||
|
│ ├── src/constant/ # 常量与路由定义
|
||||||
|
│ ├── env/ # 环境变量
|
||||||
|
│ └── src/static/logo.svg # 应用图标
|
||||||
|
└── server/ # GoFrame 用户端服务
|
||||||
|
├── api/ # API 契约(app / common / gen)
|
||||||
|
├── internal/
|
||||||
|
│ ├── logic/app/ # 用户端业务逻辑
|
||||||
|
│ ├── model/ consts/ # 模型与常量
|
||||||
|
│ └── utility/ # 工具(含启动 banner)
|
||||||
|
├── config/ # 配置
|
||||||
|
└── storage/data/ # 框架基座初始化数据
|
||||||
|
```
|
||||||
|
|
||||||
|
## 初始化数据
|
||||||
|
|
||||||
|
`server/storage/data/xiu_admin.sql` 是框架基座初始化数据(系统菜单、字典、角色、示例账号),是运行起点的必要输入。
|
||||||
|
|
||||||
|
> **数据库 schema 不随本仓发布** —— 生产库的完整 DDL 已过时,请按需自建表结构。
|
||||||
|
|
||||||
|
## 上游与许可
|
||||||
|
|
||||||
|
| 部分 | 上游 | 许可 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `app/` | [unibest](https://github.com/unibest-tech/unibest) | MIT(见 [`app/LICENSE`](./app/LICENSE)) |
|
||||||
|
| `server/` | XiuAdmin(GoFrame 中后台框架基座) | Apache-2.0(见 [`server/LICENSE`](./server/LICENSE)) |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
根目录 [LICENSE](./LICENSE) 为 Apache-2.0。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.git
|
||||||
|
.idea
|
||||||
|
.hbuilderx
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
unpackage/
|
||||||
|
.vite/
|
||||||
|
.stylelintcache
|
||||||
|
.eslintcache
|
||||||
|
*.log
|
||||||
|
*.local
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*] # 表示所有文件适用
|
||||||
|
charset = utf-8 # 设置文件字符集为 utf-8
|
||||||
|
indent_style = space # 缩进风格(tab | space)
|
||||||
|
indent_size = 2 # 缩进大小
|
||||||
|
end_of_line = lf # 控制换行类型(lf | cr | crlf)
|
||||||
|
trim_trailing_whitespace = true # 去除行首的任意空白字符
|
||||||
|
insert_final_newline = true # 始终在文件末尾插入一个新行
|
||||||
|
|
||||||
|
[*.md] # 表示仅 md 文件适用以下规则
|
||||||
|
max_line_length = off # 关闭最大行长度限制
|
||||||
|
trim_trailing_whitespace = false # 关闭末尾空格修剪
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
private.*.key
|
||||||
|
private.key
|
||||||
|
|
||||||
|
# Local Claude API credentials
|
||||||
|
scripts/claude-instances.local.ps1
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# registry = https://registry.npmjs.org
|
||||||
|
registry = https://registry.npmmirror.com
|
||||||
|
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
|
shamefully-hoist=true
|
||||||
|
ignore-workspace-root-check=true
|
||||||
|
install-workspace-root=true
|
||||||
|
node-options=--max-old-space-size=8192
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
src/pages.json
|
||||||
|
src/manifest.json
|
||||||
|
src/service
|
||||||
|
auto-import.d.ts
|
||||||
|
uni-pages.d.ts
|
||||||
|
src/uni_modules
|
||||||
|
scripts
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
# Build context: app/
|
||||||
|
# Example:
|
||||||
|
# docker build -f Dockerfile -t iotlcm-app-web:latest .
|
||||||
|
|
||||||
|
FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/node:24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build/app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml .npmrc ./
|
||||||
|
|
||||||
|
RUN npm config set registry https://registry.npmmirror.com/ && \
|
||||||
|
npm install -g pnpm@10.10.0 --registry=https://registry.npmmirror.com/ && \
|
||||||
|
pnpm config set registry https://registry.npmmirror.com/ && \
|
||||||
|
pnpm install --frozen-lockfile --ignore-scripts
|
||||||
|
|
||||||
|
COPY index.html vite.config.ts manifest.config.ts pages.config.ts tsconfig.json ./
|
||||||
|
COPY uno.config.ts wot-ui-resolver.ts openapi-ts-request.config.ts ./
|
||||||
|
COPY folder-alias.json favicon.ico ./
|
||||||
|
COPY env ./env
|
||||||
|
COPY public ./public
|
||||||
|
COPY scripts ./scripts
|
||||||
|
COPY src ./src
|
||||||
|
COPY vite-plugins ./vite-plugins
|
||||||
|
|
||||||
|
ARG VITE_SERVER_BASEURL=/app/api/v1
|
||||||
|
ARG VITE_MP_SERVER_BASEURL=http://yznw2.xiujie.cn:8010/api/v1
|
||||||
|
ARG VITE_APP_PUBLIC_BASE=/app/
|
||||||
|
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
"VITE_SERVER_BASEURL='${VITE_SERVER_BASEURL}'" \
|
||||||
|
"VITE_MP_SERVER_BASEURL='${VITE_MP_SERVER_BASEURL}'" \
|
||||||
|
"VITE_APP_PUBLIC_BASE='${VITE_APP_PUBLIC_BASE}'" \
|
||||||
|
> env/.env.production.local
|
||||||
|
|
||||||
|
RUN pnpm init-baseFiles
|
||||||
|
RUN CI=true pnpm build:h5:prod
|
||||||
|
|
||||||
|
FROM crpi-svwhn8x8nvask31h.cn-beijing.personal.cr.aliyuncs.com/xjzlpublic/nginx:1.30.0-alpine AS runtime
|
||||||
|
|
||||||
|
COPY --from=builder /build/app/dist/build/h5 /usr/share/nginx/html
|
||||||
|
|
||||||
|
RUN printf '%s\n' \
|
||||||
|
'server {' \
|
||||||
|
' listen 80;' \
|
||||||
|
' server_name _;' \
|
||||||
|
' root /usr/share/nginx/html;' \
|
||||||
|
' index index.html;' \
|
||||||
|
' resolver 127.0.0.11 ipv6=off valid=30s;' \
|
||||||
|
' set $app_backend http://iotlcm-app-server:8010;' \
|
||||||
|
'' \
|
||||||
|
' client_max_body_size 200m;' \
|
||||||
|
'' \
|
||||||
|
' location = /app {' \
|
||||||
|
' return 301 /app/;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ^~ /app/api/ {' \
|
||||||
|
' rewrite ^/app/(.*)$ /$1 break;' \
|
||||||
|
' proxy_http_version 1.1;' \
|
||||||
|
' proxy_set_header Host $host;' \
|
||||||
|
' proxy_set_header X-Real-IP $remote_addr;' \
|
||||||
|
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
|
||||||
|
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
||||||
|
' proxy_pass $app_backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ^~ /app/upload/ {' \
|
||||||
|
' rewrite ^/app/(.*)$ /$1 break;' \
|
||||||
|
' proxy_http_version 1.1;' \
|
||||||
|
' proxy_set_header Host $host;' \
|
||||||
|
' proxy_set_header X-Real-IP $remote_addr;' \
|
||||||
|
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
|
||||||
|
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
||||||
|
' proxy_pass $app_backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ^~ /app/assets/ {' \
|
||||||
|
' rewrite ^/app/(.*)$ /$1 break;' \
|
||||||
|
' expires 30d;' \
|
||||||
|
' add_header Cache-Control "public, immutable";' \
|
||||||
|
' try_files $uri =404;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location ^~ /app/ {' \
|
||||||
|
' rewrite ^/app/(.*)$ /$1 break;' \
|
||||||
|
' try_files $uri $uri/ /index.html;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' gzip on;' \
|
||||||
|
' gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;' \
|
||||||
|
'' \
|
||||||
|
' location / {' \
|
||||||
|
' try_files $uri $uri/ /index.html;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location = /favicon.ico {' \
|
||||||
|
' alias /usr/share/nginx/html/static/images/shandong-yunze-logo.png;' \
|
||||||
|
' add_header Cache-Control "no-cache";' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location /api/ {' \
|
||||||
|
' proxy_http_version 1.1;' \
|
||||||
|
' proxy_set_header Host $host;' \
|
||||||
|
' proxy_set_header X-Real-IP $remote_addr;' \
|
||||||
|
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
|
||||||
|
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
||||||
|
' proxy_pass $app_backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location /upload/ {' \
|
||||||
|
' proxy_http_version 1.1;' \
|
||||||
|
' proxy_set_header Host $host;' \
|
||||||
|
' proxy_set_header X-Real-IP $remote_addr;' \
|
||||||
|
' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;' \
|
||||||
|
' proxy_set_header X-Forwarded-Proto $scheme;' \
|
||||||
|
' proxy_pass $app_backend;' \
|
||||||
|
' }' \
|
||||||
|
'' \
|
||||||
|
' location /assets/ {' \
|
||||||
|
' expires 30d;' \
|
||||||
|
' add_header Cache-Control "public, immutable";' \
|
||||||
|
' try_files $uri =404;' \
|
||||||
|
' }' \
|
||||||
|
'}' \
|
||||||
|
> /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 菲鸽
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img width="120" src="./src/static/logo.svg">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">iotlcm-app-ui</h1>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
iotLCM 物联网卡全生命周期管理平台的**小程序端**。
|
||||||
|
|
||||||
|
基于 [unibest](https://github.com/unibest-tech/unibest)(MIT License)二次开发。
|
||||||
|
|
||||||
|
[](https://nodejs.org)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
`uniapp` + `Vue3` + `TypeScript` + `Vite5` + `UnoCSS` + `wot-ui` + `z-paging`
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev:mp-weixin # 微信小程序
|
||||||
|
pnpm dev:h5 # H5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:mp-weixin # 微信小程序(产物在 dist/build/mp-weixin)
|
||||||
|
pnpm build:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
环境变量文件在 `env/`:
|
||||||
|
|
||||||
|
| 文件 | 用途 |
|
||||||
|
| --- | --- |
|
||||||
|
| `.env` | 通用配置(应用标题、端口、uni-app AppID、微信 AppID、公开路径) |
|
||||||
|
| `.env.development` | 开发环境(后端地址) |
|
||||||
|
| `.env.production` | 生产环境 |
|
||||||
|
| `.env.test` | 测试环境 |
|
||||||
|
|
||||||
|
**公开版里的标识是占位符**:
|
||||||
|
|
||||||
|
| 变量 | 占位 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `VITE_WX_APPID` | `YOUR_WX_APPID` | 微信小程序 AppID |
|
||||||
|
| `VITE_UNI_APPID` | `__UNI__YOURAPPID` | uni-app 应用标识 |
|
||||||
|
| `VITE_SERVER_BASEURL` | `http://localhost:8010/api/v1` | 后端地址 |
|
||||||
|
| `VITE_MP_SERVER_BASEURL` | `https://admin.iotlcm.example.com/app/api/v1` | 生产后端地址 |
|
||||||
|
|
||||||
|
## 上游与许可
|
||||||
|
|
||||||
|
本项目基于 **[unibest](https://github.com/unibest-tech/unibest)**(MIT License)二次开发。
|
||||||
|
|
||||||
|
- 上游版权声明保留在 [`LICENSE`](./LICENSE)
|
||||||
|
- iotLCM 相关的业务页面、路由、配置为本项目新增
|
||||||
|
- 上游仓库:[unibest-tech/unibest](https://github.com/unibest-tech/unibest)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE)
|
||||||
Vendored
+31
@@ -0,0 +1,31 @@
|
|||||||
|
VITE_APP_TITLE = '山东平台物联卡'
|
||||||
|
VITE_APP_PORT = 9000
|
||||||
|
|
||||||
|
VITE_UNI_APPID = '__UNI__YOURAPPID'
|
||||||
|
VITE_WX_APPID = 'YOUR_WX_APPID'
|
||||||
|
|
||||||
|
# 微信开发者工具 CLI 路径,仅当默认安装路径不正确时配置(就是当 pnpm dev:mp 无法自动打开微信开发者工具时,才需要配置,通常是你更改了默认的安装位置导致的,一般出现在windows系统)
|
||||||
|
# macOS 示例:
|
||||||
|
# WECHAT_DEVTOOLS_CLI_PATH = '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
|
||||||
|
# Windows 示例:
|
||||||
|
# WECHAT_DEVTOOLS_CLI_PATH = 'C:\Program Files (x86)\Tencent\微信web开发者工具\cli.bat'
|
||||||
|
|
||||||
|
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||||
|
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router
|
||||||
|
# 比如你要部署到 https://unibest.tech/doc/ ,则配置为 /doc/
|
||||||
|
VITE_APP_PUBLIC_BASE=/
|
||||||
|
|
||||||
|
# 默认后台请求地址
|
||||||
|
# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。
|
||||||
|
VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1'
|
||||||
|
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
|
||||||
|
|
||||||
|
# 微信小程序专用后台请求地址,构建后会直接请求这个地址。
|
||||||
|
# 不配置时会回退使用 VITE_SERVER_BASEURL。
|
||||||
|
VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1'
|
||||||
|
|
||||||
|
# 认证模式,'single' | 'double' ==> 单token | 双token
|
||||||
|
VITE_AUTH_MODE = 'single'
|
||||||
|
|
||||||
|
# 原生插件资源复制开关,启用后 App 构建会把根目录 nativeplugins 复制到 dist
|
||||||
|
VITE_COPY_NATIVE_RES_ENABLE = true
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
|
NODE_ENV = 'development'
|
||||||
|
# 是否去除console 和 debugger
|
||||||
|
VITE_DELETE_CONSOLE = false
|
||||||
|
# 是否开启sourcemap
|
||||||
|
VITE_SHOW_SOURCEMAP = false
|
||||||
|
|
||||||
|
# development mode 后台请求地址
|
||||||
|
VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1'
|
||||||
|
VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1'
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
|
NODE_ENV = 'production'
|
||||||
|
# 是否去除console 和 debugger
|
||||||
|
VITE_DELETE_CONSOLE = true
|
||||||
|
# 是否开启sourcemap
|
||||||
|
VITE_SHOW_SOURCEMAP = false
|
||||||
|
|
||||||
|
# production mode 后台请求地址
|
||||||
|
VITE_APP_PUBLIC_BASE = '/app/'
|
||||||
|
VITE_SERVER_BASEURL = '/app/api/v1'
|
||||||
|
VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1'
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
|
||||||
|
NODE_ENV = 'development'
|
||||||
|
# 是否去除console 和 debugger
|
||||||
|
VITE_DELETE_CONSOLE = false
|
||||||
|
# 是否开启sourcemap
|
||||||
|
VITE_SHOW_SOURCEMAP = false
|
||||||
|
|
||||||
|
# test mode 后台请求地址
|
||||||
|
VITE_SERVER_BASEURL = 'http://localhost:8010/api/v1'
|
||||||
|
VITE_MP_SERVER_BASEURL = 'https://admin.iotlcm.example.com/app/api/v1'
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import uniHelper from '@uni-helper/eslint-config'
|
||||||
|
|
||||||
|
export default uniHelper({
|
||||||
|
unocss: true,
|
||||||
|
vue: true,
|
||||||
|
markdown: false,
|
||||||
|
ignores: [
|
||||||
|
// 忽略uni_modules目录
|
||||||
|
'**/uni_modules/',
|
||||||
|
// 忽略原生插件目录
|
||||||
|
'**/nativeplugins/',
|
||||||
|
'dist',
|
||||||
|
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
|
||||||
|
'auto-import.d.ts',
|
||||||
|
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
|
||||||
|
'uni-pages.d.ts',
|
||||||
|
// 插件生成的文件
|
||||||
|
'src/pages.json',
|
||||||
|
'src/manifest.json',
|
||||||
|
// 忽略自动生成文件
|
||||||
|
'src/service/**',
|
||||||
|
],
|
||||||
|
// https://eslint-config.antfu.me/rules
|
||||||
|
rules: {
|
||||||
|
'no-useless-return': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'vue/no-unused-refs': 'off',
|
||||||
|
'unused-imports/no-unused-vars': 'off',
|
||||||
|
'eslint-comments/no-unlimited-disable': 'off',
|
||||||
|
'jsdoc/check-param-names': 'off',
|
||||||
|
'jsdoc/require-returns-description': 'off',
|
||||||
|
'ts/no-empty-object-type': 'off',
|
||||||
|
'no-extend-native': 'off',
|
||||||
|
// uni 条件编译注释可能包裹 import,自动排序会破坏平台条件边界
|
||||||
|
'perfectionist/sort-imports': 'off',
|
||||||
|
'vue/singleline-html-element-content-newline': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
externalIgnores: ['text'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// vue SFC 调换顺序改这里
|
||||||
|
'vue/block-order': ['error', {
|
||||||
|
order: [['script', 'template'], 'style'],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
formatters: {
|
||||||
|
/**
|
||||||
|
* Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
|
||||||
|
* By default uses Prettier
|
||||||
|
*/
|
||||||
|
css: true,
|
||||||
|
/**
|
||||||
|
* Format HTML files
|
||||||
|
* By default uses Prettier
|
||||||
|
*/
|
||||||
|
html: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"src/pages/home/components/CardOverview.vue": {
|
||||||
|
"description": "卡片概览"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html build-time="%BUILD_TIME%">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/static/images/shandong-yunze-logo.png?v=20260721-2" type="image/png" />
|
||||||
|
<link rel="shortcut icon" href="/static/images/shandong-yunze-logo.png?v=20260721-2" type="image/png" />
|
||||||
|
<link rel="apple-touch-icon" href="/static/images/shandong-yunze-logo.png?v=20260721-2" />
|
||||||
|
<script>
|
||||||
|
var coverSupport =
|
||||||
|
'CSS' in window &&
|
||||||
|
typeof CSS.supports === 'function' &&
|
||||||
|
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
|
||||||
|
document.write(
|
||||||
|
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||||
|
(coverSupport ? ', viewport-fit=cover' : '') +
|
||||||
|
'" />',
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
<title>%VITE_APP_TITLE%</title>
|
||||||
|
<!--preload-links-->
|
||||||
|
<!--app-context-->
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"><!--app-html--></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
// manifest.config.ts
|
||||||
|
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
|
||||||
|
import { loadEnv } from 'vite'
|
||||||
|
|
||||||
|
// 手动解析命令行参数获取 mode
|
||||||
|
function getMode() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const modeFlagIndex = args.findIndex(arg => arg === '--mode')
|
||||||
|
return modeFlagIndex !== -1 ? args[modeFlagIndex + 1] : args[0] === 'build' ? 'production' : 'development' // 默认 development
|
||||||
|
}
|
||||||
|
// 获取环境变量的范例
|
||||||
|
const env = loadEnv(getMode(), path.resolve(process.cwd(), 'env'))
|
||||||
|
const {
|
||||||
|
VITE_APP_TITLE,
|
||||||
|
VITE_UNI_APPID,
|
||||||
|
VITE_WX_APPID,
|
||||||
|
VITE_APP_PUBLIC_BASE,
|
||||||
|
VITE_FALLBACK_LOCALE,
|
||||||
|
} = env
|
||||||
|
// console.log('manifest.config.ts env:', env)
|
||||||
|
|
||||||
|
export default defineManifestConfig({
|
||||||
|
'name': VITE_APP_TITLE,
|
||||||
|
'appid': VITE_UNI_APPID,
|
||||||
|
'description': '',
|
||||||
|
'versionName': '1.0.0',
|
||||||
|
'versionCode': '100',
|
||||||
|
'transformPx': false,
|
||||||
|
'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans'
|
||||||
|
'h5': {
|
||||||
|
router: {
|
||||||
|
base: VITE_APP_PUBLIC_BASE,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* 5+App特有相关 */
|
||||||
|
'app-plus': {
|
||||||
|
usingComponents: true,
|
||||||
|
nvueStyleCompiler: 'uni-app',
|
||||||
|
compilerVersion: 3,
|
||||||
|
compatible: {
|
||||||
|
ignoreVersion: true,
|
||||||
|
},
|
||||||
|
splashscreen: {
|
||||||
|
alwaysShowBeforeRender: true,
|
||||||
|
waiting: true,
|
||||||
|
autoclose: true,
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
/* 模块配置 */
|
||||||
|
modules: {},
|
||||||
|
/* 应用发布信息 */
|
||||||
|
distribute: {
|
||||||
|
/* android打包配置 */
|
||||||
|
android: {
|
||||||
|
minSdkVersion: 21,
|
||||||
|
targetSdkVersion: 30,
|
||||||
|
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
|
||||||
|
permissions: [
|
||||||
|
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
|
||||||
|
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
|
||||||
|
'<uses-permission android:name="android.permission.VIBRATE"/>',
|
||||||
|
'<uses-permission android:name="android.permission.READ_LOGS"/>',
|
||||||
|
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
|
||||||
|
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
|
||||||
|
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
|
||||||
|
'<uses-permission android:name="android.permission.CAMERA"/>',
|
||||||
|
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
|
||||||
|
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
|
||||||
|
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
|
||||||
|
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
|
||||||
|
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
|
||||||
|
'<uses-feature android:name="android.hardware.camera"/>',
|
||||||
|
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
/* ios打包配置 */
|
||||||
|
ios: {},
|
||||||
|
/* SDK配置 */
|
||||||
|
sdkConfigs: {},
|
||||||
|
/* 图标配置 */
|
||||||
|
icons: {
|
||||||
|
android: {
|
||||||
|
hdpi: 'static/app/icons/72x72.png',
|
||||||
|
xhdpi: 'static/app/icons/96x96.png',
|
||||||
|
xxhdpi: 'static/app/icons/144x144.png',
|
||||||
|
xxxhdpi: 'static/app/icons/192x192.png',
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
appstore: 'static/app/icons/1024x1024.png',
|
||||||
|
ipad: {
|
||||||
|
'app': 'static/app/icons/76x76.png',
|
||||||
|
'app@2x': 'static/app/icons/152x152.png',
|
||||||
|
'notification': 'static/app/icons/20x20.png',
|
||||||
|
'notification@2x': 'static/app/icons/40x40.png',
|
||||||
|
'proapp@2x': 'static/app/icons/167x167.png',
|
||||||
|
'settings': 'static/app/icons/29x29.png',
|
||||||
|
'settings@2x': 'static/app/icons/58x58.png',
|
||||||
|
'spotlight': 'static/app/icons/40x40.png',
|
||||||
|
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||||
|
},
|
||||||
|
iphone: {
|
||||||
|
'app@2x': 'static/app/icons/120x120.png',
|
||||||
|
'app@3x': 'static/app/icons/180x180.png',
|
||||||
|
'notification@2x': 'static/app/icons/40x40.png',
|
||||||
|
'notification@3x': 'static/app/icons/60x60.png',
|
||||||
|
'settings@2x': 'static/app/icons/58x58.png',
|
||||||
|
'settings@3x': 'static/app/icons/87x87.png',
|
||||||
|
'spotlight@2x': 'static/app/icons/80x80.png',
|
||||||
|
'spotlight@3x': 'static/app/icons/120x120.png',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/* 快应用特有相关 */
|
||||||
|
'quickapp': {},
|
||||||
|
/* 小程序特有相关 */
|
||||||
|
'mp-weixin': {
|
||||||
|
appid: VITE_WX_APPID,
|
||||||
|
setting: {
|
||||||
|
urlCheck: false,
|
||||||
|
// 是否启用 ES6 转 ES5
|
||||||
|
es6: true,
|
||||||
|
minified: true,
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
subPackages: true,
|
||||||
|
},
|
||||||
|
// 是否合并组件虚拟节点外层属性,uni-app 3.5.1+ 开始支持。目前仅支持 style、class 属性。
|
||||||
|
// 默认不开启(undefined),这里设置为开启。
|
||||||
|
mergeVirtualHostAttributes: true,
|
||||||
|
styleIsolation: 'shared',
|
||||||
|
usingComponents: true,
|
||||||
|
lazyCodeLoading: 'requiredComponents',
|
||||||
|
// __usePrivacyCheck__: true,
|
||||||
|
},
|
||||||
|
'mp-alipay': {
|
||||||
|
usingComponents: true,
|
||||||
|
styleIsolation: 'shared',
|
||||||
|
optimization: {
|
||||||
|
subPackages: true,
|
||||||
|
},
|
||||||
|
// 解决支付宝小程序开发工具报错 【globalThis is not defined】
|
||||||
|
compileOptions: {
|
||||||
|
globalObjectMode: 'enable',
|
||||||
|
transpile: {
|
||||||
|
script: {
|
||||||
|
ignore: ['node_modules/**'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'mp-baidu': {
|
||||||
|
usingComponents: true,
|
||||||
|
},
|
||||||
|
'mp-toutiao': {
|
||||||
|
usingComponents: true,
|
||||||
|
},
|
||||||
|
'uniStatistics': {
|
||||||
|
enable: false,
|
||||||
|
},
|
||||||
|
'vueVersion': '3',
|
||||||
|
})
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'openapi-ts-request'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
describe: 'unibest-openapi-test',
|
||||||
|
schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
|
||||||
|
serversPath: './src/service',
|
||||||
|
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
|
||||||
|
requestOptionsType: 'CustomRequestOptions_',
|
||||||
|
isGenReactQuery: false,
|
||||||
|
reactQueryMode: 'vue',
|
||||||
|
isGenJavaScript: false,
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"name": "iotlcm-app-ui",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"unibest": {
|
||||||
|
"platforms": [
|
||||||
|
"h5",
|
||||||
|
"mp-weixin",
|
||||||
|
"app"
|
||||||
|
],
|
||||||
|
"uiLibrary": "wot-ui-v2",
|
||||||
|
"loginStrategy": true,
|
||||||
|
"i18n": true,
|
||||||
|
"chartLibraries": [
|
||||||
|
"ucharts"
|
||||||
|
],
|
||||||
|
"cliVersion": "4.0.16",
|
||||||
|
"unibestVersion": "4.4.1",
|
||||||
|
"unibestUpdateTime": "2026-05-22",
|
||||||
|
"createdAt": "2026-06-16 15:30:53"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.10.0",
|
||||||
|
"description": "unibest - 最好的 uniapp 开发模板",
|
||||||
|
"generate-time": "用户创建项目时生成",
|
||||||
|
"author": {
|
||||||
|
"name": "feige996",
|
||||||
|
"zhName": "菲鸽",
|
||||||
|
"email": "1020103647@example.com",
|
||||||
|
"github": "https://github.com/feige996",
|
||||||
|
"gitee": "https://gitee.com/feige996"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://unibest.tech",
|
||||||
|
"repository": "https://github.com/feige996/unibest",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/feige996/unibest/issues",
|
||||||
|
"url-old": "https://github.com/codercup/unibest/issues"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20",
|
||||||
|
"pnpm": ">=9"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"openapi": "openapi-ts",
|
||||||
|
"init-husky": "git init && husky",
|
||||||
|
"init-baseFiles": "node ./scripts/create-base-files.js",
|
||||||
|
"prepare": "pnpm init-husky & pnpm init-baseFiles",
|
||||||
|
"predev": "pnpm init-baseFiles",
|
||||||
|
"predev:app": "pnpm init-baseFiles",
|
||||||
|
"predev:mp": "pnpm init-baseFiles",
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"uvm": "npx @dcloudio/uvm@latest",
|
||||||
|
"uvm-rm": "node ./scripts/postupgrade.js",
|
||||||
|
"bump-version": "node ./scripts/bump-version.js",
|
||||||
|
"postuvm": "echo upgrade uni-app success!",
|
||||||
|
"dev:app": "uni -p app",
|
||||||
|
"dev:app:test": "uni -p app --mode test",
|
||||||
|
"dev:app:prod": "uni -p app --mode production",
|
||||||
|
"dev:app-android": "uni -p app-android",
|
||||||
|
"dev:app-ios": "uni -p app-ios",
|
||||||
|
"dev:custom": "uni -p",
|
||||||
|
"dev": "uni",
|
||||||
|
"dev:test": "uni --mode test",
|
||||||
|
"dev:prod": "uni --mode production",
|
||||||
|
"dev:h5": "uni",
|
||||||
|
"dev:h5:test": "uni --mode test",
|
||||||
|
"dev:h5:prod": "uni --mode production",
|
||||||
|
"dev:h5:ssr": "uni --ssr",
|
||||||
|
"dev:mp": "uni -p mp-weixin",
|
||||||
|
"dev:mp:test": "uni -p mp-weixin --mode test",
|
||||||
|
"dev:mp:prod": "uni -p mp-weixin --mode production",
|
||||||
|
"dev:mp-alipay": "uni -p mp-alipay",
|
||||||
|
"dev:mp-baidu": "uni -p mp-baidu",
|
||||||
|
"dev:mp-jd": "uni -p mp-jd",
|
||||||
|
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||||
|
"dev:mp-lark": "uni -p mp-lark",
|
||||||
|
"dev:mp-qq": "uni -p mp-qq",
|
||||||
|
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||||
|
"dev:mp-weixin": "uni -p mp-weixin",
|
||||||
|
"dev:mp-xhs": "uni -p mp-xhs",
|
||||||
|
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||||
|
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||||
|
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||||
|
"build:app": "uni build -p app",
|
||||||
|
"build:app:test": "uni build -p app --mode test",
|
||||||
|
"build:app:prod": "uni build -p app --mode production",
|
||||||
|
"build:app-android": "uni build -p app-android",
|
||||||
|
"build:app-ios": "uni build -p app-ios",
|
||||||
|
"build:custom": "uni build -p",
|
||||||
|
"build:h5": "uni build",
|
||||||
|
"build:h5:test": "uni build --mode test",
|
||||||
|
"build:h5:prod": "uni build --mode production",
|
||||||
|
"build": "uni build",
|
||||||
|
"build:test": "uni build --mode test",
|
||||||
|
"build:prod": "uni build --mode production",
|
||||||
|
"build:h5:ssr": "uni build --ssr",
|
||||||
|
"build:mp-alipay": "uni build -p mp-alipay",
|
||||||
|
"build:mp": "uni build -p mp-weixin",
|
||||||
|
"build:mp:test": "uni build -p mp-weixin --mode test",
|
||||||
|
"build:mp:prod": "uni build -p mp-weixin --mode production",
|
||||||
|
"build:mp-baidu": "uni build -p mp-baidu",
|
||||||
|
"build:mp-jd": "uni build -p mp-jd",
|
||||||
|
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||||
|
"build:mp-lark": "uni build -p mp-lark",
|
||||||
|
"build:mp-qq": "uni build -p mp-qq",
|
||||||
|
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||||
|
"build:mp-weixin": "uni build -p mp-weixin",
|
||||||
|
"build:mp-xhs": "uni build -p mp-xhs",
|
||||||
|
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||||
|
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||||
|
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||||
|
"upload:changeset": "pnpm changeset && pnpm changeset version",
|
||||||
|
"upload:mp": "node ./scripts/upload-weixin.js",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint",
|
||||||
|
"lint:fix": "eslint --fix",
|
||||||
|
"format": "prettier . --write",
|
||||||
|
"format:check": "prettier . --check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@alova/adapter-uniapp": "^2.0.14",
|
||||||
|
"@alova/shared": "^1.3.1",
|
||||||
|
"@changesets/cli": "^2.30.0",
|
||||||
|
"@dcloudio/uni-app": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-app-harmony": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-harmony": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
|
||||||
|
"@qiun/ucharts": "2.5.0-20230101",
|
||||||
|
"@wot-ui/ui": "latest",
|
||||||
|
"abortcontroller-polyfill": "^1.7.8",
|
||||||
|
"alova": "^3.3.3",
|
||||||
|
"dayjs": "1.11.10",
|
||||||
|
"go-captcha-uni": "^1.0.7",
|
||||||
|
"pinia": "2.0.36",
|
||||||
|
"pinia-plugin-persistedstate": "3.2.1",
|
||||||
|
"vue": "^3.4.21",
|
||||||
|
"vue-i18n": "9.1.9",
|
||||||
|
"vue-router": "4.5.1",
|
||||||
|
"z-paging": "2.8.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@dcloudio/types": "^3.4.8",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4070620250821001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4070620250821001",
|
||||||
|
"@iconify-json/carbon": "^1.2.4",
|
||||||
|
"@iconify/utils": "^3.0.2",
|
||||||
|
"@types/node": "^20.17.9",
|
||||||
|
"@uni-helper/eslint-config": "0.5.0",
|
||||||
|
"@uni-helper/plugin-uni": "0.1.0",
|
||||||
|
"@uni-helper/uni-env": "0.1.8",
|
||||||
|
"@uni-helper/uni-types": "1.0.0-alpha.6",
|
||||||
|
"@uni-helper/unocss-preset-uni": "0.2.11",
|
||||||
|
"@uni-helper/vite-plugin-uni-components": "0.2.3",
|
||||||
|
"@uni-helper/vite-plugin-uni-layouts": "0.1.11",
|
||||||
|
"@uni-helper/vite-plugin-uni-manifest": "0.2.12",
|
||||||
|
"@uni-helper/vite-plugin-uni-pages": "0.3.23",
|
||||||
|
"@uni-helper/vite-plugin-uni-platform": "0.0.5",
|
||||||
|
"@uni-ku/bundle-optimizer": "v1.3.15-beta.2",
|
||||||
|
"@uni-ku/root": "1.4.1",
|
||||||
|
"@unocss/eslint-plugin": "^66.2.3",
|
||||||
|
"@unocss/preset-legacy-compat": "66.0.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.1.0",
|
||||||
|
"@vue/runtime-core": "^3.4.21",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"cross-env": "^10.0.0",
|
||||||
|
"enquirer": "^2.4.1",
|
||||||
|
"eslint": "^9.31.0",
|
||||||
|
"eslint-plugin-format": "^1.0.1",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"miniprogram-api-typings": "^4.1.0",
|
||||||
|
"miniprogram-ci": "^2.1.26",
|
||||||
|
"openapi-ts-request": "^1.10.0",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"postcss-html": "^1.8.0",
|
||||||
|
"postcss-scss": "^4.0.9",
|
||||||
|
"prettier": "3.8.3",
|
||||||
|
"rollup-plugin-visualizer": "^6.0.3",
|
||||||
|
"sass": "1.77.8",
|
||||||
|
"std-env": "^3.9.0",
|
||||||
|
"typescript": "~5.8.0",
|
||||||
|
"unocss": "66.0.0",
|
||||||
|
"unplugin-auto-import": "^20.0.0",
|
||||||
|
"vite": "5.2.8",
|
||||||
|
"vite-plugin-restart": "^1.0.0",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.5",
|
||||||
|
"vue-tsc": "^3.0.6"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"unconfig": "7.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"unconfig": "7.3.2"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"bin-wrapper": "npm:bin-wrapper-china",
|
||||||
|
"unconfig": "7.3.2"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "echo 祝菲鸽身体健康"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
|
||||||
|
import { tabBar } from './src/tabbar/config'
|
||||||
|
|
||||||
|
export default defineUniPages({
|
||||||
|
globalStyle: {
|
||||||
|
navigationStyle: 'default',
|
||||||
|
navigationBarTitleText: '山东平台物联卡',
|
||||||
|
navigationBarBackgroundColor: '#f8f8f8',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
},
|
||||||
|
easycom: {
|
||||||
|
autoscan: true,
|
||||||
|
custom: {
|
||||||
|
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
|
||||||
|
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
|
||||||
|
'z-paging/components/z-paging$1/z-paging$1.vue',
|
||||||
|
'^wd-(.*)': '@wot-ui/ui/components/wd-$1/wd-$1.vue',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
|
||||||
|
// subPackages: [{
|
||||||
|
// root: 'pages-demo',
|
||||||
|
// pages: [],
|
||||||
|
// }],
|
||||||
|
tabBar: tabBar as any,
|
||||||
|
})
|
||||||
Generated
+22285
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"setting": {
|
||||||
|
"es6": true,
|
||||||
|
"postcss": true,
|
||||||
|
"minified": true,
|
||||||
|
"uglifyFileName": false,
|
||||||
|
"enhance": true,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"useCompilerPlugins": false,
|
||||||
|
"minifyWXML": true,
|
||||||
|
"compileWorklet": false,
|
||||||
|
"uploadWithSourceMap": true,
|
||||||
|
"packNpmManually": false,
|
||||||
|
"minifyWXSS": true,
|
||||||
|
"localPlugins": false,
|
||||||
|
"disableUseStrict": false,
|
||||||
|
"condition": false,
|
||||||
|
"swc": false,
|
||||||
|
"disableSWC": true
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"simulatorPluginLibVersion": {},
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"appid": "YOUR_WX_APPID",
|
||||||
|
"editorSetting": {},
|
||||||
|
"libVersion": "3.16.2"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"libVersion": "3.16.2",
|
||||||
|
"projectname": "app",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"coverView": true,
|
||||||
|
"lazyloadPlaceholderEnable": false,
|
||||||
|
"skylineRenderEnable": false,
|
||||||
|
"preloadBackgroundData": false,
|
||||||
|
"autoAudits": false,
|
||||||
|
"showShadowRootInWxmlPanel": true,
|
||||||
|
"compileHotReLoad": true,
|
||||||
|
"useApiHook": true,
|
||||||
|
"useApiHostProcess": true,
|
||||||
|
"useStaticServer": false,
|
||||||
|
"useLanDebug": false,
|
||||||
|
"showES6CompileOption": false,
|
||||||
|
"checkInvalidKey": true,
|
||||||
|
"ignoreDevUnusedFiles": true,
|
||||||
|
"bigPackageSizeSupport": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
@@ -0,0 +1,154 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import enquirer from 'enquirer'
|
||||||
|
import pc from 'picocolors'
|
||||||
|
|
||||||
|
const manifestPath = path.resolve(process.cwd(), 'manifest.config.ts')
|
||||||
|
// 获取是否只是测试运行
|
||||||
|
const dryRun = process.argv.includes('--dry-run')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将语义化版本号根据传递的类型进行递增
|
||||||
|
* @param {string} version 当前版本号,如 '1.1.0'
|
||||||
|
* @param {string} type 升级类型: 'patch' | 'minor' | 'major' | 'none'
|
||||||
|
* @returns {string} 新的版本号
|
||||||
|
*/
|
||||||
|
function bumpVersionName(version, type) {
|
||||||
|
if (type === 'none') return version
|
||||||
|
|
||||||
|
const parts = version.split('.')
|
||||||
|
let major = Number.parseInt(parts[0] || '0', 10)
|
||||||
|
let minor = Number.parseInt(parts[1] || '0', 10)
|
||||||
|
let patch = Number.parseInt(parts[2] || '0', 10)
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'major':
|
||||||
|
major += 1; minor = 0; patch = 0;
|
||||||
|
break;
|
||||||
|
case 'minor':
|
||||||
|
minor += 1; patch = 0;
|
||||||
|
break;
|
||||||
|
case 'patch':
|
||||||
|
patch += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return `${major}.${minor}.${patch}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const source = fs.readFileSync(manifestPath, 'utf8')
|
||||||
|
|
||||||
|
// 匹配 versionCode 和 versionName 的正则表达式,支持键名带有引号的情况,例如 'versionCode': '100'
|
||||||
|
const versionCodeRegex = /((?:['"])?versionCode(?:['"])?\s*:\s*)(['"])(\d+)\2/
|
||||||
|
const versionNameRegex = /((?:['"])?versionName(?:['"])?\s*:\s*)(['"])([\d\.]+)\2/
|
||||||
|
|
||||||
|
const codeMatch = source.match(versionCodeRegex)
|
||||||
|
const nameMatch = source.match(versionNameRegex)
|
||||||
|
|
||||||
|
if (!codeMatch || !nameMatch) {
|
||||||
|
console.error(pc.red('✖ [bump-version] 未在 manifest.config.ts 中找到合法的 versionCode 或 versionName'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentVersionCode = Number.parseInt(codeMatch[3], 10)
|
||||||
|
const currentVersionName = nameMatch[3]
|
||||||
|
const nextVersionCode = String(currentVersionCode + 1)
|
||||||
|
|
||||||
|
// 1. 检查命令行参数是否有 --type
|
||||||
|
const typeArgMatch = process.argv.find(arg => arg.startsWith('--type='))
|
||||||
|
let bumpType = typeArgMatch ? typeArgMatch.split('=')[1] : null
|
||||||
|
|
||||||
|
// 2. 环境判定:如果不在交互终端或者是CI环境,但是没有指定类型,则默认只升级 versionCode
|
||||||
|
const isInteractive = process.stdout.isTTY && !process.env.CI
|
||||||
|
|
||||||
|
if (!bumpType) {
|
||||||
|
if (!isInteractive) {
|
||||||
|
console.log(pc.yellow('⚠ [bump-version] 非交互环境且未指定参数,默认不修改 versionName'))
|
||||||
|
bumpType = 'none'
|
||||||
|
} else {
|
||||||
|
// 3. 在终端交互式询问用户怎么处理 versionName
|
||||||
|
console.log('')
|
||||||
|
console.log(pc.cyan('📦 准备发布新版本'))
|
||||||
|
console.log(`${pc.gray('当前版本:')} ${pc.bold(currentVersionName)} ${pc.gray(`(v${currentVersionCode})`)}`)
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
const response = await enquirer.prompt({
|
||||||
|
type: 'select',
|
||||||
|
name: 'selectedType',
|
||||||
|
message: '请选择如何升级版本名称 (versionName)?',
|
||||||
|
pointer: pc.cyan('❯'),
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: `${pc.bold('修复')} (Patch) ${pc.gray(currentVersionName)} → ${pc.green(bumpVersionName(currentVersionName, 'patch'))}`,
|
||||||
|
name: 'patch',
|
||||||
|
hint: pc.gray('修复Bug、极小的代码安全变动')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `${pc.bold('特性')} (Minor) ${pc.gray(currentVersionName)} → ${pc.cyan(bumpVersionName(currentVersionName, 'minor'))}`,
|
||||||
|
name: 'minor',
|
||||||
|
hint: pc.gray('新增功能、向下兼容的API更新')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `${pc.bold('重大')} (Major) ${pc.gray(currentVersionName)} → ${pc.magenta(bumpVersionName(currentVersionName, 'major'))}`,
|
||||||
|
name: 'major',
|
||||||
|
hint: pc.gray('重大重构、不兼容的API修改')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `${pc.bold('仅Code')} (None) ${pc.gray('保持 ' + currentVersionName)}`,
|
||||||
|
name: 'none',
|
||||||
|
hint: pc.gray('保持名称不变,仅升级构建号(versionCode)')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: `${pc.bold('取消')} (Cancel) ${pc.gray('完全不升级版本')}`,
|
||||||
|
name: 'cancel',
|
||||||
|
hint: pc.gray('跳过版本修改,直接进入后续打包流程')
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 如果用户按 ctrl+c 退出
|
||||||
|
onCancel: () => {
|
||||||
|
console.log(pc.red('✖ 取消操作并退出编译'))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
bumpType = response.selectedType
|
||||||
|
|
||||||
|
// 用户选择了完全不升级
|
||||||
|
if (bumpType === 'cancel') {
|
||||||
|
console.log('')
|
||||||
|
console.log(pc.green('✔ 已跳过版本升级操作'))
|
||||||
|
console.log('')
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 根据类型计算下一代版本并进行替换计算
|
||||||
|
const nextVersionName = bumpVersionName(currentVersionName, bumpType)
|
||||||
|
|
||||||
|
let updated = source.replace(versionCodeRegex, `${codeMatch[1]}${codeMatch[2]}${nextVersionCode}${codeMatch[2]}`)
|
||||||
|
updated = updated.replace(versionNameRegex, `${nameMatch[1]}${nameMatch[2]}${nextVersionName}${nameMatch[2]}`)
|
||||||
|
|
||||||
|
// 5. 回填文件内容
|
||||||
|
if (!dryRun) {
|
||||||
|
fs.writeFileSync(manifestPath, updated, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 美化成功提示输出
|
||||||
|
console.log('')
|
||||||
|
console.log(pc.green(`✔ ${dryRun ? '(模拟运行) ' : ''}版本更新成功!`))
|
||||||
|
|
||||||
|
if (bumpType !== 'none') {
|
||||||
|
console.log(` ${pc.gray('versionName:')} ${pc.strikethrough(pc.gray(currentVersionName))} → ${pc.bold(pc.green(nextVersionName))}`)
|
||||||
|
} else {
|
||||||
|
console.log(` ${pc.gray('versionName:')} ${pc.dim(currentVersionName)} (未更改)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(` ${pc.gray('versionCode:')} ${pc.strikethrough(pc.gray(currentVersionCode))} → ${pc.bold(pc.green(nextVersionCode))}`)
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(err => {
|
||||||
|
console.error(pc.red('✖ [bump-version] 发生错误:'), err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
// 基础配置文件生成脚本
|
||||||
|
// 此脚本用于生成 src/manifest.json 和 src/pages.json 基础文件
|
||||||
|
// 由于这两个配置文件会被添加到 .gitignore 中,因此需要通过此脚本确保项目能正常运行
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
// 获取当前文件的目录路径(替代 CommonJS 中的 __dirname)
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
|
||||||
|
// 最简可运行配置
|
||||||
|
const manifest = { }
|
||||||
|
const pages = {
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
path: 'pages/index/index',
|
||||||
|
type: 'home',
|
||||||
|
style: {
|
||||||
|
navigationStyle: 'custom',
|
||||||
|
navigationBarTitleText: '首页',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'pages/me/me',
|
||||||
|
type: 'page',
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '我的',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subPackages: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用修复后的 __dirname 来解析文件路径
|
||||||
|
const manifestPath = path.resolve(__dirname, '../src/manifest.json')
|
||||||
|
const pagesPath = path.resolve(__dirname, '../src/pages.json')
|
||||||
|
|
||||||
|
// 确保 src 目录存在
|
||||||
|
const srcDir = path.resolve(__dirname, '../src')
|
||||||
|
if (!fs.existsSync(srcDir)) {
|
||||||
|
fs.mkdirSync(srcDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_SIZE = `{ }`.length // 如果只有一个空对象,必定是不对的,需要重新生成
|
||||||
|
|
||||||
|
// 如果 src/manifest.json 不存在,就创建它;或者如果文件大小小于等于 MIN_SIZE,也重新创建
|
||||||
|
if (!fs.existsSync(manifestPath) || fs.statSync(manifestPath).size <= MIN_SIZE) {
|
||||||
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 src/pages.json 不存在,就创建它;或者如果文件大小小于等于 MIN_SIZE,也重新创建
|
||||||
|
if (!fs.existsSync(pagesPath) || fs.statSync(pagesPath).size <= MIN_SIZE) {
|
||||||
|
fs.writeFileSync(pagesPath, JSON.stringify(pages, null, 2))
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { exec } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开开发者工具
|
||||||
|
* @param {string} env - 环境,'dev' 或 'build'
|
||||||
|
* @param {object} options - 配置选项
|
||||||
|
* @param {string} options.wechatDevtoolsCliPath - 微信开发者工具 CLI 路径
|
||||||
|
*/
|
||||||
|
function _openDevTools(env = 'dev', options = {}) {
|
||||||
|
const { wechatDevtoolsCliPath } = options
|
||||||
|
const platform = process.platform // darwin, win32, linux
|
||||||
|
const { UNI_PLATFORM } = process.env // mp-weixin, mp-alipay, mp-lark
|
||||||
|
|
||||||
|
const uniPlatformText = UNI_PLATFORM === 'mp-weixin' ? '微信小程序' : UNI_PLATFORM === 'mp-alipay' ? '支付宝小程序' : UNI_PLATFORM === 'mp-lark' ? '抖音小程序' : '小程序'
|
||||||
|
|
||||||
|
// 项目路径(构建输出目录),根据环境选择不同目录
|
||||||
|
const outputDir = env === 'build' ? `dist/build/${UNI_PLATFORM}` : `dist/dev/${UNI_PLATFORM}`
|
||||||
|
const projectPath = path.resolve(process.cwd(), outputDir)
|
||||||
|
|
||||||
|
// 检查构建输出目录是否存在
|
||||||
|
if (!fs.existsSync(projectPath)) {
|
||||||
|
console.log(`❌ ${uniPlatformText}构建目录不存在:`, projectPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🚀 正在打开${uniPlatformText}开发者工具...`)
|
||||||
|
|
||||||
|
// 根据不同操作系统执行不同命令
|
||||||
|
let command = ''
|
||||||
|
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
if (UNI_PLATFORM === 'mp-weixin') {
|
||||||
|
const cliPath = wechatDevtoolsCliPath || '/Applications/wechatwebdevtools.app/Contents/MacOS/cli'
|
||||||
|
command = `"${cliPath}" -o "${projectPath}"`
|
||||||
|
}
|
||||||
|
else if (UNI_PLATFORM === 'mp-alipay') {
|
||||||
|
command = `/Applications/小程序开发者工具.app/Contents/MacOS/小程序开发者工具 --p "${projectPath}"`
|
||||||
|
}
|
||||||
|
else if (UNI_PLATFORM === 'mp-lark') {
|
||||||
|
command = `/Applications/抖音开发者工具.app/Contents/MacOS/抖音开发者工具 --p "${projectPath}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (platform === 'win32' || platform === 'win64') {
|
||||||
|
// Windows
|
||||||
|
if (UNI_PLATFORM === 'mp-weixin') {
|
||||||
|
const cliPath = wechatDevtoolsCliPath || 'C:\\Program Files (x86)\\Tencent\\微信web开发者工具\\cli.bat'
|
||||||
|
command = `"${cliPath}" -o "${projectPath}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Linux 或其他系统
|
||||||
|
console.log('❌ 当前系统不支持自动打开微信开发者工具')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(command, (error, stdout, stderr) => {
|
||||||
|
if (error) {
|
||||||
|
console.log(`❌ 打开${uniPlatformText}开发者工具失败:`, error.message)
|
||||||
|
if (UNI_PLATFORM === 'mp-weixin') {
|
||||||
|
console.log('💡 当前使用的微信开发者工具 CLI 命令:', command)
|
||||||
|
console.log('💡 如果安装位置不同,可以在 env/.env 配置 WECHAT_DEVTOOLS_CLI_PATH 为本机实际 CLI 路径')
|
||||||
|
}
|
||||||
|
console.log(`💡 请确保${uniPlatformText}开发者工具服务端口已启用`)
|
||||||
|
console.log(`💡 可以手动打开${uniPlatformText}开发者工具并导入项目:`, projectPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.log('⚠️ 警告:', stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ ${uniPlatformText}开发者工具已打开`)
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
console.log(stdout)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 Vite 插件,用于自动打开开发者工具
|
||||||
|
* @param {object} options - 配置选项
|
||||||
|
* @param {string} options.mode - 构建模式,'development' 或 'production'
|
||||||
|
* @param {string} options.wechatDevtoolsCliPath - 微信开发者工具 CLI 路径
|
||||||
|
*/
|
||||||
|
export default function openDevTools(options = {}) {
|
||||||
|
const { mode = 'development', wechatDevtoolsCliPath } = options
|
||||||
|
// 根据 mode 确定环境:development -> dev, production -> build
|
||||||
|
const env = mode === 'production' ? 'build' : 'dev'
|
||||||
|
|
||||||
|
// 首次构建标记
|
||||||
|
let isFirstBuild = true
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'uni-devtools',
|
||||||
|
writeBundle() {
|
||||||
|
if (isFirstBuild && process.env.UNI_PLATFORM?.includes('mp')) {
|
||||||
|
isFirstBuild = false
|
||||||
|
_openDevTools(env, { wechatDevtoolsCliPath })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
|
||||||
|
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
|
||||||
|
// # 只需要执行下面的命令即可
|
||||||
|
|
||||||
|
import { exec } from 'node:child_process'
|
||||||
|
import { promisify } from 'node:util'
|
||||||
|
|
||||||
|
// 日志控制开关,设置为 true 可以启用所有日志输出
|
||||||
|
const FG_LOG_ENABLE = true
|
||||||
|
|
||||||
|
// 将 exec 转换为返回 Promise 的函数
|
||||||
|
const execPromise = promisify(exec)
|
||||||
|
|
||||||
|
// 定义要执行的命令
|
||||||
|
const dependencies = [
|
||||||
|
// TODO: 如果不需要某个平台的小程序,请手动删除或注释掉
|
||||||
|
'@dcloudio/uni-mp-baidu',
|
||||||
|
'@dcloudio/uni-mp-jd',
|
||||||
|
'@dcloudio/uni-mp-kuaishou',
|
||||||
|
'@dcloudio/uni-mp-qq',
|
||||||
|
'@dcloudio/uni-mp-xhs',
|
||||||
|
'@dcloudio/uni-quickapp-webview',
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 带开关的日志输出函数
|
||||||
|
* @param {string} message 日志消息
|
||||||
|
* @param {string} type 日志类型 (log, error)
|
||||||
|
*/
|
||||||
|
function log(message, type = 'log') {
|
||||||
|
if (FG_LOG_ENABLE) {
|
||||||
|
if (type === 'error') {
|
||||||
|
console.error(message)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载单个依赖包
|
||||||
|
* @param {string} dep 依赖包名
|
||||||
|
* @returns {Promise<boolean>} 是否成功卸载
|
||||||
|
*/
|
||||||
|
async function uninstallDependency(dep) {
|
||||||
|
try {
|
||||||
|
log(`开始卸载依赖: ${dep}`)
|
||||||
|
const { stdout, stderr } = await execPromise(`pnpm un ${dep}`)
|
||||||
|
if (stdout) {
|
||||||
|
log(`stdout [${dep}]: ${stdout}`)
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
log(`stderr [${dep}]: ${stderr}`, 'error')
|
||||||
|
}
|
||||||
|
log(`成功卸载依赖: ${dep}`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// 单个依赖卸载失败不影响其他依赖
|
||||||
|
log(`卸载依赖 ${dep} 失败: ${error.message}`, 'error')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 串行卸载所有依赖包
|
||||||
|
*/
|
||||||
|
async function uninstallAllDependencies() {
|
||||||
|
log(`开始串行卸载 ${dependencies.length} 个依赖包...`)
|
||||||
|
|
||||||
|
let successCount = 0
|
||||||
|
let failedCount = 0
|
||||||
|
|
||||||
|
// 串行执行所有卸载命令
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
const success = await uninstallDependency(dep)
|
||||||
|
if (success) {
|
||||||
|
successCount++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
failedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为了避免命令执行过快导致的问题,添加短暂延迟
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
}
|
||||||
|
|
||||||
|
log(`卸载操作完成: 成功 ${successCount} 个, 失败 ${failedCount} 个`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行串行卸载
|
||||||
|
uninstallAllDependencies().catch((err) => {
|
||||||
|
log(`串行卸载过程中出现未捕获的错误: ${err}`, 'error')
|
||||||
|
})
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
/**
|
||||||
|
* 微信小程序 CLI 上传脚本
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* pnpm upload:mp # 版本号读取 package.json,描述使用最新 Git commit
|
||||||
|
* pnpm upload:mp --version=1.0.1 # 指定版本号(覆盖 package.json)
|
||||||
|
* pnpm upload:mp --desc="修复bug" # 指定版本描述(覆盖 Git commit)
|
||||||
|
* pnpm upload:mp --robot=2 # 指定机器人编号(1-30)
|
||||||
|
* pnpm upload:mp --version=2.0.0 --desc="重大更新" # 组合使用多个参数
|
||||||
|
*
|
||||||
|
* 版本号策略: 命令行参数 > package.json version
|
||||||
|
* 描述策略: 命令行参数 > Git 最新 commit > 默认时间戳
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* 1. 确保已在微信公众平台开通 "小程序代码上传" 权限
|
||||||
|
* 2. 确保私钥文件存在(private.${appid}.key),并且配置了上传IP白名单
|
||||||
|
* 3. 上传前会自动执行 build:mp:prod 构建 并跳过打开开发者工具
|
||||||
|
* 4. 秘钥文件的appid(VITE_WX_APPID)需要与微信公众平台的小程序appid一致
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import ci from 'miniprogram-ci'
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url)
|
||||||
|
const __dirname = path.dirname(__filename)
|
||||||
|
const ROOT_DIR = path.resolve(__dirname, '..')
|
||||||
|
|
||||||
|
// 从 package.json 读取版本号
|
||||||
|
function getPackageVersion() {
|
||||||
|
try {
|
||||||
|
const pkgPath = path.resolve(ROOT_DIR, 'package.json')
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
||||||
|
return pkg.version || '1.0.0'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return '1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新的 Git commit 信息
|
||||||
|
function getGitCommitMessage() {
|
||||||
|
try {
|
||||||
|
// 获取最新 commit 的作者和标题
|
||||||
|
const message = execSync('git log -1 --pretty="%an: %s"', {
|
||||||
|
cwd: ROOT_DIR,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
}).trim()
|
||||||
|
return message || null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成默认描述
|
||||||
|
function getDefaultDesc() {
|
||||||
|
// 优先使用 Git commit 信息
|
||||||
|
const gitMessage = getGitCommitMessage()
|
||||||
|
if (gitMessage) {
|
||||||
|
return gitMessage
|
||||||
|
}
|
||||||
|
// 回退到时间戳
|
||||||
|
return `上传于 ${new Date().toLocaleString('zh-CN')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析命令行参数
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
const params = {
|
||||||
|
version: null, // 稍后设置,优先级:命令行 > package.json
|
||||||
|
desc: null, // 稍后设置,优先级:命令行 > Git commit > 默认
|
||||||
|
robot: 1, // 机器人编号 1-30
|
||||||
|
}
|
||||||
|
|
||||||
|
args.forEach((arg) => {
|
||||||
|
if (arg.startsWith('--version=')) {
|
||||||
|
params.version = arg.split('=')[1]
|
||||||
|
}
|
||||||
|
else if (arg.startsWith('--desc=')) {
|
||||||
|
params.desc = arg.split('=')[1]
|
||||||
|
}
|
||||||
|
else if (arg.startsWith('--robot=')) {
|
||||||
|
params.robot = Number.parseInt(arg.split('=')[1], 10)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果命令行没有指定版本号,则读取 package.json
|
||||||
|
if (!params.version) {
|
||||||
|
params.version = getPackageVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果命令行没有指定描述,则读取 Git commit 或使用默认
|
||||||
|
if (!params.desc) {
|
||||||
|
params.desc = getDefaultDesc()
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取环境变量
|
||||||
|
function loadEnvFile(mode = 'production') {
|
||||||
|
const envPath = path.resolve(ROOT_DIR, 'env', `.env.${mode}`)
|
||||||
|
const defaultEnvPath = path.resolve(ROOT_DIR, 'env', '.env')
|
||||||
|
const localEnvPath = path.resolve(ROOT_DIR, 'env', '.env.local')
|
||||||
|
|
||||||
|
const envContent = {}
|
||||||
|
|
||||||
|
// 先读取默认 .env
|
||||||
|
if (fs.existsSync(defaultEnvPath)) {
|
||||||
|
const content = fs.readFileSync(defaultEnvPath, 'utf-8')
|
||||||
|
content.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
const [key, ...valueParts] = trimmed.split('=')
|
||||||
|
if (key) {
|
||||||
|
envContent[key.trim()] = valueParts.join('=').trim().replace(/^['"]|['"]$/g, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再读取对应模式的 .env 文件(会覆盖默认值)
|
||||||
|
if (fs.existsSync(localEnvPath)) {
|
||||||
|
const content = fs.readFileSync(localEnvPath, 'utf-8')
|
||||||
|
content.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
const [key, ...valueParts] = trimmed.split('=')
|
||||||
|
if (key) {
|
||||||
|
envContent[key.trim()] = valueParts.join('=').trim().replace(/^['"]|['"]$/g, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(envPath)) {
|
||||||
|
const content = fs.readFileSync(envPath, 'utf-8')
|
||||||
|
content.split('\n').forEach((line) => {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (trimmed && !trimmed.startsWith('#')) {
|
||||||
|
const [key, ...valueParts] = trimmed.split('=')
|
||||||
|
if (key) {
|
||||||
|
envContent[key.trim()] = valueParts.join('=').trim().replace(/^['"]|['"]$/g, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return envContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取私钥路径
|
||||||
|
function getPrivateKeyPath(appid) {
|
||||||
|
const configuredPath = envValue('WECHAT_UPLOAD_PRIVATE_KEY_PATH', 'MINIPROGRAM_UPLOAD_PRIVATE_KEY_PATH')
|
||||||
|
if (configuredPath) {
|
||||||
|
const privateKeyPath = path.isAbsolute(configuredPath)
|
||||||
|
? configuredPath
|
||||||
|
: path.resolve(ROOT_DIR, configuredPath)
|
||||||
|
if (fs.existsSync(privateKeyPath)) {
|
||||||
|
return privateKeyPath
|
||||||
|
}
|
||||||
|
throw new Error(`配置的微信小程序上传密钥文件不存在:${privateKeyPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找私钥文件
|
||||||
|
const keyPatterns = [
|
||||||
|
`private.${appid}.key`,
|
||||||
|
'private.key',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of keyPatterns) {
|
||||||
|
const keyPath = path.resolve(ROOT_DIR, pattern)
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
return keyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`未找到私钥文件,请确保项目根目录存在 private.${appid}.key 文件`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function envValue(...keys) {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = process.env[key]
|
||||||
|
if (typeof value === 'string' && value.trim()) {
|
||||||
|
return value.trim().replace(/^['"]|['"]$/g, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
console.log('\n🚀 开始微信小程序上传流程...\n')
|
||||||
|
|
||||||
|
const params = parseArgs()
|
||||||
|
const env = loadEnvFile('production')
|
||||||
|
Object.assign(process.env, env)
|
||||||
|
const appid = env.VITE_WX_APPID
|
||||||
|
|
||||||
|
if (!appid) {
|
||||||
|
throw new Error('未找到 VITE_WX_APPID 环境变量,请检查 env/.env 文件')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📱 AppID: ${appid}`)
|
||||||
|
console.log(`📌 版本号: ${params.version}`)
|
||||||
|
console.log(`📝 版本描述: ${params.desc}`)
|
||||||
|
console.log(`🤖 机器人编号: ${params.robot}`)
|
||||||
|
|
||||||
|
// 获取私钥路径
|
||||||
|
const privateKeyPath = getPrivateKeyPath(appid)
|
||||||
|
console.log(`🔑 私钥路径: ${privateKeyPath}`)
|
||||||
|
|
||||||
|
// 构建小程序(跳过自动打开开发者工具)
|
||||||
|
console.log('\n📦 正在构建小程序...(跳过自动打开开发者工具)\n')
|
||||||
|
try {
|
||||||
|
execSync('pnpm build:mp:prod', {
|
||||||
|
cwd: ROOT_DIR,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
SKIP_OPEN_DEVTOOLS: 'true', // 上传时跳过打开开发者工具
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('❌ 构建失败:', error.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小程序代码目录
|
||||||
|
const projectPath = path.resolve(ROOT_DIR, 'dist', 'build', 'mp-weixin')
|
||||||
|
|
||||||
|
if (!fs.existsSync(projectPath)) {
|
||||||
|
throw new Error(`构建产物不存在: ${projectPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`📂 项目路径: ${projectPath}`)
|
||||||
|
console.log('\n⬆️ 正在上传到微信服务器...\n')
|
||||||
|
|
||||||
|
// 创建项目实例
|
||||||
|
const project = new ci.Project({
|
||||||
|
appid,
|
||||||
|
type: 'miniProgram',
|
||||||
|
projectPath,
|
||||||
|
privateKeyPath,
|
||||||
|
ignores: ['node_modules/**/*'],
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 上传代码
|
||||||
|
const uploadResult = await ci.upload({
|
||||||
|
project,
|
||||||
|
version: params.version,
|
||||||
|
desc: params.desc,
|
||||||
|
robot: params.robot,
|
||||||
|
setting: {
|
||||||
|
es6: true,
|
||||||
|
es7: true,
|
||||||
|
minify: true,
|
||||||
|
autoPrefixWXSS: true,
|
||||||
|
minifyWXML: true,
|
||||||
|
minifyWXSS: true,
|
||||||
|
minifyJS: true,
|
||||||
|
},
|
||||||
|
onProgressUpdate: (task) => {
|
||||||
|
if (task._status === 'done') {
|
||||||
|
console.log(` ✅ ${task._msg}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('\n✅ 上传成功!')
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||||
|
console.log(` 📌 版本号: ${params.version}`)
|
||||||
|
console.log(` 📝 描述: ${params.desc}`)
|
||||||
|
console.log(` 🤖 机器人: ${params.robot}`)
|
||||||
|
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
||||||
|
console.log('\n📋 下一步操作:')
|
||||||
|
console.log(' 1. 登录微信公众平台: https://mp.weixin.qq.com')
|
||||||
|
console.log(' 2. 进入 "管理 -> 版本管理"')
|
||||||
|
console.log(' 3. 在 "开发版本" 中找到刚上传的版本')
|
||||||
|
console.log(' 4. 点击 "选为体验版" 按钮\n')
|
||||||
|
|
||||||
|
return uploadResult
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error('\n❌ 上传失败:', error.message)
|
||||||
|
if (error.message.includes('privateKeyPath')) {
|
||||||
|
console.log('\n💡 提示: 请确保已在微信公众平台配置代码上传密钥')
|
||||||
|
console.log(' 1. 登录微信公众平台')
|
||||||
|
console.log(' 2. 进入 "开发 -> 开发设置"')
|
||||||
|
console.log(' 3. 在 "小程序代码上传" 区域生成并下载密钥')
|
||||||
|
console.log(' 4. 在 "小程序代码上传" 区域配置上传IP白名单')
|
||||||
|
}
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error('❌ 执行出错:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
const root = process.cwd()
|
||||||
|
const dataPath = resolve(root, 'src/pages/message/data.ts')
|
||||||
|
const indexPath = resolve(root, 'src/pages/message/index.vue')
|
||||||
|
const source = readFileSync(dataPath, 'utf8')
|
||||||
|
const indexSource = readFileSync(indexPath, 'utf8')
|
||||||
|
|
||||||
|
const forbiddenWords = ['代理', '未来', '敬请期待', 'agent', 'future']
|
||||||
|
const forbiddenRoutePatterns = [
|
||||||
|
/url\s*:/,
|
||||||
|
/path\s*:/,
|
||||||
|
/route\s*:/,
|
||||||
|
/navigateTo/,
|
||||||
|
/switchTab/,
|
||||||
|
/redirectTo/,
|
||||||
|
/reLaunch/,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const word of forbiddenWords) {
|
||||||
|
if (source.includes(word) || indexSource.includes(word)) {
|
||||||
|
throw new Error(`消息页默认内容包含禁用词:${word}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const pattern of forbiddenRoutePatterns) {
|
||||||
|
if (source.match(pattern) || indexSource.match(pattern)) {
|
||||||
|
throw new Error(`消息页一级页面不应包含跳转配置或跳转调用:${pattern}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupCount = (source.match(/^\s{4}key:\s*'(system|card)',/gm) || []).length
|
||||||
|
if (groupCount !== 2) {
|
||||||
|
throw new Error(`消息页应只保留 2 个用户侧消息分类,当前为 ${groupCount} 个`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const required of ['系统消息', '卡片消息', '端午假期服务时间调整通知', '尾号 8821 流量不足']) {
|
||||||
|
if (!source.includes(required)) {
|
||||||
|
throw new Error(`消息页缺少来自暂存设计的用户侧内容:${required}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('message page scope checked')
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
const root = process.cwd()
|
||||||
|
|
||||||
|
const expectedPages = [
|
||||||
|
['business', 'renew', '套餐续费确认'],
|
||||||
|
['business', 'change', '套餐变更确认'],
|
||||||
|
['business', 'addon', '订购加油包'],
|
||||||
|
['business', 'diagnosis', '智能诊断'],
|
||||||
|
['business', 'workOrder', '工单列表'],
|
||||||
|
['business', 'workOrderCreate', '提交工单'],
|
||||||
|
['business', 'workOrderDetail', '工单详情'],
|
||||||
|
['card', 'detail', '卡片详情'],
|
||||||
|
['card', 'packageDetail', '套餐详情'],
|
||||||
|
['message', 'systemDetail', '系统消息详情'],
|
||||||
|
['message', 'cardDetail', '卡片消息详情'],
|
||||||
|
['user', 'profile', '账号资料'],
|
||||||
|
['user', 'realName', '实名信息'],
|
||||||
|
['user', 'notification', '通知设置'],
|
||||||
|
['user', 'feedback', '帮助与反馈'],
|
||||||
|
['user', 'contact', '联系客服'],
|
||||||
|
['user', 'agreement', '协议与隐私'],
|
||||||
|
['user', 'about', '关于我们'],
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [owner, name, title] of expectedPages) {
|
||||||
|
const file = resolve(root, `src/pages/${owner}/views/${name}/index.vue`)
|
||||||
|
if (!existsSync(file)) {
|
||||||
|
throw new Error(`缺少子界面文件:src/pages/${owner}/views/${name}/index.vue`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = readFileSync(file, 'utf8')
|
||||||
|
if (!source.includes('definePage({')) {
|
||||||
|
throw new Error(`子界面缺少 definePage:${file}`)
|
||||||
|
}
|
||||||
|
if (!source.includes(`navigationBarTitleText: '${title}'`)) {
|
||||||
|
throw new Error(`子界面标题不匹配:${file},期望 ${title}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryFiles = [
|
||||||
|
'src/pages/business/index.vue',
|
||||||
|
'src/pages/card/components/CardList.vue',
|
||||||
|
'src/pages/card/components/PackageList.vue',
|
||||||
|
'src/pages/message/components/MessageList.vue',
|
||||||
|
'src/pages/user/components/UserServiceCells.vue',
|
||||||
|
'src/pages/user/components/UserSupportCells.vue',
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const relative of entryFiles) {
|
||||||
|
const source = readFileSync(resolve(root, relative), 'utf8')
|
||||||
|
if (!source.includes('RouterPath')) {
|
||||||
|
throw new Error(`入口未使用 RouterPath 跳转:${relative}`)
|
||||||
|
}
|
||||||
|
if (!source.includes('navigateTo')) {
|
||||||
|
throw new Error(`入口未调用 navigateTo:${relative}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('sub pages checked')
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* @description 通过 vite 自定义条件动态导入 eruda
|
||||||
|
* @description Eruda 配置参考 https://eruda.liriliri.io/zh/docs/
|
||||||
|
* @param {object} options
|
||||||
|
* @param {boolean} [options.open] - 是否开启 eruda
|
||||||
|
* @param {object} [options.erudaOptions] - eruda 配置
|
||||||
|
* @param {string} [options.erudaUrl] - eruda 地址
|
||||||
|
*/
|
||||||
|
export default function vitePluginEruda(options = {}) {
|
||||||
|
const { open = true, erudaOptions = {}, erudaUrl = 'https://cdn.jsdelivr.net/npm/eruda' } = options
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'vite-plugin-eruda',
|
||||||
|
|
||||||
|
transformIndexHtml(html) {
|
||||||
|
const tags = [
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
attrs: {
|
||||||
|
src: erudaUrl,
|
||||||
|
},
|
||||||
|
injectTo: 'head',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tag: 'script',
|
||||||
|
children: `eruda.init(${JSON.stringify(erudaOptions)});`,
|
||||||
|
injectTo: 'head',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
return { html, tags }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import FgTabbar from '@/tabbar/index.vue'
|
||||||
|
import { isPageTabbar } from './tabbar/store'
|
||||||
|
import { currRoute } from './utils'
|
||||||
|
|
||||||
|
const isCurrentPageTabbar = ref(true)
|
||||||
|
onShow(() => {
|
||||||
|
const { path } = currRoute()
|
||||||
|
// “蜡笔小开心”提到本地是 '/pages/index/index',线上是 '/' 导致线上 tabbar 不见了
|
||||||
|
// 所以这里需要判断一下,如果是 '/' 就当做首页,也要显示 tabbar
|
||||||
|
if (path === '/') {
|
||||||
|
isCurrentPageTabbar.value = true
|
||||||
|
} else {
|
||||||
|
isCurrentPageTabbar.value = isPageTabbar(path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const helloKuRoot = ref('Hello AppKuVue')
|
||||||
|
|
||||||
|
const exposeRef = ref('this is form app.Ku.vue')
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
exposeRef,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="app-root">
|
||||||
|
<!-- 这个先隐藏了,知道这样用就行 -->
|
||||||
|
<view class="hidden text-center"> {{ helloKuRoot }},这里可以配置全局的东西 </view>
|
||||||
|
|
||||||
|
<KuRootView />
|
||||||
|
|
||||||
|
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.app-root {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
|
||||||
|
import { getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { navigateToInterceptor } from '@/router/interceptor'
|
||||||
|
import { tabbarStore } from '@/tabbar/store'
|
||||||
|
import { permission } from '@/router/permission'
|
||||||
|
|
||||||
|
const { proxy } = (getCurrentInstance() || {}) as any
|
||||||
|
const router = proxy?.$router
|
||||||
|
|
||||||
|
router && permission.install(router)
|
||||||
|
|
||||||
|
onLaunch((options: any) => {
|
||||||
|
// console.log('App.vue onLaunch', options)
|
||||||
|
})
|
||||||
|
onShow((options: any) => {
|
||||||
|
// console.log('App.vue onShow', options)
|
||||||
|
// 处理直接进入页面路由的情况:如h5直接输入路由、微信小程序分享后进入等
|
||||||
|
// https://github.com/unibest-tech/unibest/issues/192
|
||||||
|
if (options?.path) {
|
||||||
|
navigateToInterceptor.invoke({ url: `/${options.path}`, query: options.query })
|
||||||
|
} else {
|
||||||
|
navigateToInterceptor.invoke({ url: '/' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
onHide(() => {
|
||||||
|
// console.log('App Hide')
|
||||||
|
})
|
||||||
|
|
||||||
|
// #ifdef H5
|
||||||
|
function syncTabbarWhenPageVisible() {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
tabbarStore.syncCurIdxByCurrentPageAsync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('visibilitychange', syncTabbarWhenPageVisible)
|
||||||
|
window.addEventListener('pageshow', syncTabbarWhenPageVisible)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('visibilitychange', syncTabbarWhenPageVisible)
|
||||||
|
window.removeEventListener('pageshow', syncTabbarWhenPageVisible)
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss"></style>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type {
|
||||||
|
BalanceRechargeMockPayParams,
|
||||||
|
BalanceRechargeRefundParams,
|
||||||
|
BalanceRechargeStatusParams,
|
||||||
|
BalanceRechargeStatusRes,
|
||||||
|
BalanceRechargeSubmitParams,
|
||||||
|
BalanceRechargeSubmitRes,
|
||||||
|
} from './types/balance'
|
||||||
|
import type { AppOrder } from './types/order'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
BalanceRechargeMockPayParams,
|
||||||
|
BalanceRechargeRefundParams,
|
||||||
|
BalanceRechargeStatusParams,
|
||||||
|
BalanceRechargeStatusRes,
|
||||||
|
BalanceRechargeSubmitParams,
|
||||||
|
BalanceRechargeSubmitRes,
|
||||||
|
} from './types/balance'
|
||||||
|
|
||||||
|
export function balanceRechargeSubmit(data: BalanceRechargeSubmitParams) {
|
||||||
|
return http.post<BalanceRechargeSubmitRes>(
|
||||||
|
'/balance/recharge-submit',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeStatus(data: BalanceRechargeStatusParams) {
|
||||||
|
return http.get<BalanceRechargeStatusRes>('/balance/recharge-status', data, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeMockPay(data: BalanceRechargeMockPayParams) {
|
||||||
|
return http.post<BalanceRechargeStatusRes>(
|
||||||
|
'/balance/recharge-mock-pay',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeRefund(data: BalanceRechargeRefundParams) {
|
||||||
|
return http.post<AppOrder>(
|
||||||
|
'/balance/recharge-refund',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import type {
|
||||||
|
AllocateBatchPrepareParams,
|
||||||
|
AllocateBatchRes,
|
||||||
|
AllocateBatchSubmitParams,
|
||||||
|
AllocateTargetListParams,
|
||||||
|
AllocateTargetListRes,
|
||||||
|
BindCardParams,
|
||||||
|
CardLifecycleParams,
|
||||||
|
CardLifecycleRes,
|
||||||
|
CardLifecycleStatusParams,
|
||||||
|
CardListParams,
|
||||||
|
CardListRes,
|
||||||
|
CardRecognitionRes,
|
||||||
|
CardQueryRes,
|
||||||
|
ChangePrepareParams,
|
||||||
|
ChangePrepareRes,
|
||||||
|
ChangeSubmitParams,
|
||||||
|
ChangeSubmitRes,
|
||||||
|
PackageDetailRes,
|
||||||
|
PackageListParams,
|
||||||
|
PackageListRes,
|
||||||
|
PublicPackageDetailParams,
|
||||||
|
PublicQueryCardParams,
|
||||||
|
RenewBatchPrepareParams,
|
||||||
|
RenewBatchSubmitParams,
|
||||||
|
RenewPrepareParams,
|
||||||
|
RenewPrepareRes,
|
||||||
|
RenewSubmitParams,
|
||||||
|
RenewSubmitRes,
|
||||||
|
RestartCardParams,
|
||||||
|
RestartCardRes,
|
||||||
|
} from './types/card'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
import { getEnvBaseUrl } from '@/utils/index'
|
||||||
|
|
||||||
|
const CARD_REQUEST_OPTIONS = {
|
||||||
|
timeout: 15000,
|
||||||
|
hideErrorToast: true,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AllocateBatchRes,
|
||||||
|
AllocateTarget,
|
||||||
|
AppCard,
|
||||||
|
AppPackage,
|
||||||
|
AppRenewOption,
|
||||||
|
CardLifecycleRes,
|
||||||
|
ChangePackageOption,
|
||||||
|
ChangePrepareRes,
|
||||||
|
ChangeSubmitRes,
|
||||||
|
RenewPackageOption,
|
||||||
|
RenewPrepareRes,
|
||||||
|
RenewSubmitRes,
|
||||||
|
} from './types/card'
|
||||||
|
|
||||||
|
export function publicQueryCard(data: PublicQueryCardParams) {
|
||||||
|
return http.post<CardQueryRes>('/card/public-query', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recognizeCardImage(filePath: string) {
|
||||||
|
return new Promise<CardRecognitionRes>((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: `${getEnvBaseUrl()}/card/recognize`,
|
||||||
|
filePath,
|
||||||
|
name: 'file',
|
||||||
|
timeout: 30000,
|
||||||
|
success: (response) => {
|
||||||
|
let payload: { code?: number, message?: string, data?: CardRecognitionRes }
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(String(response.data || '{}'))
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
reject({ message: '图片识别响应格式错误' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300 || Number(payload.code) !== 0) {
|
||||||
|
reject({ message: payload.message || '图片识别失败' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(payload.data || {
|
||||||
|
code: 'RECOGNITION_FAILED',
|
||||||
|
message: '图片识别结果为空',
|
||||||
|
cardType: 'iccid',
|
||||||
|
cardValue: '',
|
||||||
|
confidence: 0,
|
||||||
|
candidates: [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject({ message: error.errMsg || '图片上传失败,请检查网络后重试' })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCardList(params: CardListParams) {
|
||||||
|
return http.get<CardListRes>('/card/list', params, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPackageList(params: PackageListParams) {
|
||||||
|
return http.get<PackageListRes>('/card/package-list', params, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publicPackageDetail(data: PublicPackageDetailParams) {
|
||||||
|
return http.post<PackageDetailRes>(
|
||||||
|
'/card/public-package-detail',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CARD_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCardDetail(cardValue: string) {
|
||||||
|
return http.get<CardQueryRes>('/card/detail', { cardValue }, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindCard(data: BindCardParams) {
|
||||||
|
return http.post<CardQueryRes>('/card/bind', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restartCard(data: RestartCardParams) {
|
||||||
|
return http.post<RestartCardRes>('/card/restart', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopCard(data: RestartCardParams) {
|
||||||
|
return http.post<CardLifecycleRes>('/card/stop', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startCard(data: RestartCardParams) {
|
||||||
|
return http.post<CardLifecycleRes>('/card/start', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startDataService(data: RestartCardParams) {
|
||||||
|
return lifecycleBatchSubmit({ cardValue: data.cardValue, operation: 'dataStart', remark: data.remark })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopDataService(data: RestartCardParams) {
|
||||||
|
return lifecycleBatchSubmit({ cardValue: data.cardValue, operation: 'dataStop', remark: data.remark })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lifecycleBatchSubmit(data: CardLifecycleParams) {
|
||||||
|
return http.post<CardLifecycleRes>(
|
||||||
|
'/card/lifecycle-batch-submit',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CARD_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lifecycleBatchStatus(data: CardLifecycleStatusParams) {
|
||||||
|
return http.get<CardLifecycleRes>('/card/lifecycle-batch-status', data, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewPrepare(data: RenewPrepareParams) {
|
||||||
|
return http.post<RenewPrepareRes>('/card/renew-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewBatchPrepare(data: RenewBatchPrepareParams) {
|
||||||
|
return http.post<RenewPrepareRes>('/card/renew-batch-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewSubmit(data: RenewSubmitParams) {
|
||||||
|
return http.post<RenewSubmitRes>('/card/renew-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renewBatchSubmit(data: RenewBatchSubmitParams) {
|
||||||
|
return http.post<RenewSubmitRes>('/card/renew-batch-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllocateTargets(params: AllocateTargetListParams) {
|
||||||
|
return http.get<AllocateTargetListRes>('/card/allocate-targets', params, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allocateBatchPrepare(data: AllocateBatchPrepareParams) {
|
||||||
|
return http.post<AllocateBatchRes>(
|
||||||
|
'/card/allocate-batch-prepare',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CARD_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allocateBatchSubmit(data: AllocateBatchSubmitParams) {
|
||||||
|
return http.post<AllocateBatchRes>(
|
||||||
|
'/card/allocate-batch-submit',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
CARD_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changePrepare(data: ChangePrepareParams) {
|
||||||
|
return http.post<ChangePrepareRes>('/card/change-prepare', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changeSubmit(data: ChangeSubmitParams) {
|
||||||
|
return http.post<ChangeSubmitRes>('/card/change-submit', data, undefined, undefined, CARD_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type {
|
||||||
|
CustomerDetailParams,
|
||||||
|
CustomerDetailRes,
|
||||||
|
CustomerListParams,
|
||||||
|
CustomerListRes,
|
||||||
|
} from './types/customer'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AppCustomer,
|
||||||
|
AppCustomerDetail,
|
||||||
|
AppCustomerType,
|
||||||
|
CustomerDetailParams,
|
||||||
|
CustomerListParams,
|
||||||
|
} from './types/customer'
|
||||||
|
|
||||||
|
export function getCustomerList(params: CustomerListParams) {
|
||||||
|
return http.get<CustomerListRes>('/customer/list', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCustomerDetail(params: CustomerDetailParams) {
|
||||||
|
return http.get<CustomerDetailRes>('/customer/detail', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import type {
|
||||||
|
DiagnosisFlowParams,
|
||||||
|
DiagnosisFlowRes,
|
||||||
|
DiagnosisInfoParams,
|
||||||
|
DiagnosisInfoRes,
|
||||||
|
} from './types/diagnosis'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DiagnosisFlowParams,
|
||||||
|
DiagnosisFlowRes,
|
||||||
|
DiagnosisInfoItem,
|
||||||
|
DiagnosisInfoParams,
|
||||||
|
DiagnosisInfoRes,
|
||||||
|
DiagnosisModuleItem,
|
||||||
|
DiagnosisPackageItem,
|
||||||
|
DiagnosisRateLimitApnItem,
|
||||||
|
DiagnosisRateLimitPccItem,
|
||||||
|
DiagnosisSessionItem,
|
||||||
|
} from './types/diagnosis'
|
||||||
|
|
||||||
|
export function getDiagnosisInfo(data: DiagnosisInfoParams) {
|
||||||
|
return http.post<DiagnosisInfoRes>('/diagnosis/info', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenDiagnosisInfo(data: DiagnosisInfoParams & { tenantId?: string }) {
|
||||||
|
return http.post<DiagnosisInfoRes>('/open/diagnosis/info', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDiagnosisFlow(data: DiagnosisFlowParams) {
|
||||||
|
return http.post<DiagnosisFlowRes>('/open/diagnosis/flow', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenDiagnosisFlow(data: DiagnosisFlowParams & { tenantId?: string }) {
|
||||||
|
return http.post<DiagnosisFlowRes>('/open/diagnosis/flow', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { http } from '@/http/alova'
|
||||||
|
|
||||||
|
export interface IFoo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return http.Get<IFoo>('/foo', {
|
||||||
|
params: {
|
||||||
|
name: '菲鸽',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export interface IFoo {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return http.Get<IFoo>('/foo', {
|
||||||
|
params: {
|
||||||
|
name: '菲鸽',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFooItem {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** GET 请求 */
|
||||||
|
export async function getFooAPI(name: string) {
|
||||||
|
return await http.get<IFooItem>('/foo', { name })
|
||||||
|
}
|
||||||
|
/** GET 请求;支持 传递 header 的范例 */
|
||||||
|
export function getFooAPI2(name: string) {
|
||||||
|
return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST 请求 */
|
||||||
|
export function postFooAPI(name: string) {
|
||||||
|
return http.post<IFooItem>('/foo', { name })
|
||||||
|
}
|
||||||
|
/** POST 请求;需要传递 query 参数的范例;微信小程序经常有同时需要query参数和body参数的场景 */
|
||||||
|
export function postFooAPI2(name: string) {
|
||||||
|
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 })
|
||||||
|
}
|
||||||
|
/** POST 请求;支持 传递 header 的范例 */
|
||||||
|
export function postFooAPI3(name: string) {
|
||||||
|
return http.post<IFooItem>('/foo', { name }, { a: 1, b: 2 }, { 'Content-Type-100': '100' })
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import type {
|
||||||
|
IAuthLoginRes,
|
||||||
|
ICheckSideCaptchaForm,
|
||||||
|
IForgotPasswordForm,
|
||||||
|
ILoginForm,
|
||||||
|
IRegisterForm,
|
||||||
|
ISendSmsForm,
|
||||||
|
ISendSmsRes,
|
||||||
|
ISideCaptcha,
|
||||||
|
IUpdateInfo,
|
||||||
|
IUserInfoRes,
|
||||||
|
IWechatBindForm,
|
||||||
|
IWechatBindPhoneForm,
|
||||||
|
IWechatBindPhoneRes,
|
||||||
|
IWechatBindRes,
|
||||||
|
IWechatLoginForm,
|
||||||
|
IWechatLoginRes,
|
||||||
|
} from './types/login'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type { IForgotPasswordForm, ILoginForm, IRegisterForm }
|
||||||
|
|
||||||
|
const AUTH_REQUEST_OPTIONS = {
|
||||||
|
timeout: 15000,
|
||||||
|
hideErrorToast: true,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function getSideCaptcha() {
|
||||||
|
return http.post<ISideCaptcha>(
|
||||||
|
'/auth/side-captcha',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkSideCaptcha(data: ICheckSideCaptchaForm) {
|
||||||
|
return http.post<{ success: boolean }>(
|
||||||
|
'/auth/check-side-captcha',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendSms(data: ISendSmsForm) {
|
||||||
|
return http.post<ISendSmsRes>('/auth/sms', data, undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(loginForm: ILoginForm) {
|
||||||
|
return http.post<IAuthLoginRes>(
|
||||||
|
'/auth/login',
|
||||||
|
loginForm,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wechatLogin(data: IWechatLoginForm) {
|
||||||
|
return http.post<IWechatLoginRes>(
|
||||||
|
'/auth/wechat-login',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wechatBindPhone(data: IWechatBindPhoneForm) {
|
||||||
|
return http.post<IWechatBindPhoneRes>(
|
||||||
|
'/auth/wechat-bind-phone',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function register(registerForm: IRegisterForm) {
|
||||||
|
return http.post<IAuthLoginRes>(
|
||||||
|
'/auth/register',
|
||||||
|
registerForm,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forgotPassword(data: IForgotPasswordForm) {
|
||||||
|
return http.post<{ success: boolean }>(
|
||||||
|
'/auth/forgot-password',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshToken(_refreshToken: string) {
|
||||||
|
return Promise.reject(new Error('当前 APP 接口使用单 token 模式,暂不支持刷新 token'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo() {
|
||||||
|
return http.get<IUserInfoRes>('/user/info', undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateInfo(data: IUpdateInfo) {
|
||||||
|
return http.post<IUserInfoRes>('/user/info', data, undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindWechat(data: IWechatBindForm) {
|
||||||
|
return http.post<IWechatBindRes>(
|
||||||
|
'/user/wechat/bind',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
AUTH_REQUEST_OPTIONS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserPassword() {
|
||||||
|
return Promise.reject(new Error('请使用找回密码流程重置密码'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppWechatLoginCodeRes {
|
||||||
|
code: string
|
||||||
|
errMsg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWxCode() {
|
||||||
|
return new Promise<AppWechatLoginCodeRes>((resolve, reject) => {
|
||||||
|
if (typeof wx !== 'undefined' && typeof wx.login === 'function') {
|
||||||
|
wx.login({
|
||||||
|
success: res => resolve({ code: res.code || '', errMsg: res.errMsg }),
|
||||||
|
fail: err => reject(new Error(err.errMsg || '微信登录失败')),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.login({
|
||||||
|
provider: 'weixin',
|
||||||
|
success: res => resolve({ code: res.code || '', errMsg: res.errMsg }),
|
||||||
|
fail: err => reject(new Error(err.errMsg)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import type { MessageCenterParams, MessageCenterRes } from './types/message'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export interface AppMessage {
|
||||||
|
id: number
|
||||||
|
messageId: number
|
||||||
|
messageLogId: number
|
||||||
|
messageType: number
|
||||||
|
messageTypeText?: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
link?: string
|
||||||
|
isRead: number
|
||||||
|
sendTime?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMessageListParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
isRead?: '' | '0' | '1'
|
||||||
|
messageType?: number
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMessageListRes {
|
||||||
|
items: AppMessage[]
|
||||||
|
total: number
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMessageDetailParams {
|
||||||
|
messageId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMessageDetailRes {
|
||||||
|
message: AppMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMessageReadParams {
|
||||||
|
ids: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageList(params: AppMessageListParams) {
|
||||||
|
return http.get<AppMessageListRes>('/message/list', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMessageCenter(params: MessageCenterParams = {}): Promise<MessageCenterRes> {
|
||||||
|
const unreadRes = await getMessageList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
isRead: '0',
|
||||||
|
keyword: params.keyword,
|
||||||
|
group: params.group,
|
||||||
|
} as AppMessageListParams)
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups: [],
|
||||||
|
totalUnread: Number(unreadRes.total || 0),
|
||||||
|
warnSummary: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMessageDetail(params: AppMessageDetailParams) {
|
||||||
|
return http.get<AppMessageDetailRes>('/message/detail', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readMessages(data: AppMessageReadParams) {
|
||||||
|
return http.post<void>('/message/read', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type {
|
||||||
|
OrderDetailParams,
|
||||||
|
OrderDetailRes,
|
||||||
|
OrderListParams,
|
||||||
|
OrderListRes,
|
||||||
|
} from './types/order'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type { AppOrder, AppOrderItem, OrderDetailParams, OrderListParams } from './types/order'
|
||||||
|
|
||||||
|
export function getOrderList(params: OrderListParams) {
|
||||||
|
return http.get<OrderListRes>('/order/list', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOrderDetail(params: OrderDetailParams) {
|
||||||
|
return http.get<OrderDetailRes>('/order/detail', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface AccessCodesResponse {
|
||||||
|
code?: number | string
|
||||||
|
data?:
|
||||||
|
| string[]
|
||||||
|
| {
|
||||||
|
data?: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAccessCodesResponse(body?: string[] | AccessCodesResponse | null) {
|
||||||
|
if (Array.isArray(body))
|
||||||
|
return body
|
||||||
|
if (Array.isArray(body?.data))
|
||||||
|
return body.data
|
||||||
|
const nested = body?.data
|
||||||
|
if (nested && !Array.isArray(nested) && Array.isArray(nested.data))
|
||||||
|
return nested.data
|
||||||
|
return []
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { normalizeAccessCodesResponse } from './system-normalize'
|
||||||
|
|
||||||
|
describe('normalizeAccessCodesResponse', () => {
|
||||||
|
it('supports xiuAdmin wrapped access code response', () => {
|
||||||
|
expect(
|
||||||
|
normalizeAccessCodesResponse({
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
data: ['cpm:iot:tSimCardInfo:view', 'cpm:iot:tSimCardRenew:input:prepare'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(['cpm:iot:tSimCardInfo:view', 'cpm:iot:tSimCardRenew:input:prepare'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps compatibility with direct array response', () => {
|
||||||
|
expect(
|
||||||
|
normalizeAccessCodesResponse({
|
||||||
|
code: 0,
|
||||||
|
data: ['cpc:current:user'],
|
||||||
|
}),
|
||||||
|
).toEqual(['cpc:current:user'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { AccessCodesResponse } from './system-normalize'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
import { normalizeAccessCodesResponse } from './system-normalize'
|
||||||
|
|
||||||
|
export interface AccessCodesParams {
|
||||||
|
isCache?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export { normalizeAccessCodesResponse }
|
||||||
|
|
||||||
|
export function getAccessCodes(params?: AccessCodesParams) {
|
||||||
|
return http.get<string[] | AccessCodesResponse>('/system/auth/codes', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
}).then(normalizeAccessCodesResponse)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export interface BalanceRechargeSubmitParams {
|
||||||
|
amount: number
|
||||||
|
buyerNote?: string
|
||||||
|
payProvider?: 'wechat' | 'mock'
|
||||||
|
payScene?: 'h5' | 'mini'
|
||||||
|
wxCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceRechargeSubmitRes {
|
||||||
|
orderId: number
|
||||||
|
orderSn: string
|
||||||
|
totalAmount: number
|
||||||
|
priceText: string
|
||||||
|
orderStatus: number
|
||||||
|
payStatus: number
|
||||||
|
status: string
|
||||||
|
paymentExpireAt?: string
|
||||||
|
paymentRemainingSeconds?: number
|
||||||
|
paymentExpired?: boolean
|
||||||
|
payProvider: string
|
||||||
|
payUrl: string
|
||||||
|
payMethod?: string
|
||||||
|
payGateway?: string
|
||||||
|
payParams?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceRechargeStatusParams {
|
||||||
|
orderSn: string
|
||||||
|
payScene?: 'h5' | 'mini'
|
||||||
|
wxCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceRechargeMockPayParams {
|
||||||
|
orderSn: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceRechargeRefundParams {
|
||||||
|
orderSn: string
|
||||||
|
refundReason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BalanceRechargeStatusRes {
|
||||||
|
orderId: number
|
||||||
|
orderSn: string
|
||||||
|
totalAmount: number
|
||||||
|
priceText: string
|
||||||
|
orderStatus: number
|
||||||
|
payStatus: number
|
||||||
|
status: string
|
||||||
|
paymentExpireAt?: string
|
||||||
|
paymentRemainingSeconds?: number
|
||||||
|
paymentExpired?: boolean
|
||||||
|
balanceAfter: number
|
||||||
|
balanceAfterText: string
|
||||||
|
payProvider: string
|
||||||
|
payTradeNo: string
|
||||||
|
payUrl: string
|
||||||
|
payMethod?: string
|
||||||
|
payGateway?: string
|
||||||
|
payParams?: Record<string, string>
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
export interface AppCard {
|
||||||
|
id?: number
|
||||||
|
iccid: string
|
||||||
|
msisdn?: string
|
||||||
|
imsi?: string
|
||||||
|
imei?: string
|
||||||
|
regionCode?: string
|
||||||
|
deptName?: string
|
||||||
|
currentPackageCode?: string
|
||||||
|
packageName?: string
|
||||||
|
packageType?: number
|
||||||
|
categoryCode?: string
|
||||||
|
categoryName?: string
|
||||||
|
packageStatus?: number
|
||||||
|
bossCardStatus?: number
|
||||||
|
abnormalReason?: string
|
||||||
|
abnormalReasons?: string[]
|
||||||
|
cardType?: number
|
||||||
|
netModel?: number
|
||||||
|
onlineStatus?: string | number
|
||||||
|
deviceStatus?: string | number
|
||||||
|
imeiStatus?: string | number
|
||||||
|
regionLimitStatus?: string | number
|
||||||
|
apnName?: string
|
||||||
|
apnId?: string
|
||||||
|
rat?: string
|
||||||
|
ipv4?: string
|
||||||
|
ipv6?: string
|
||||||
|
ipv6Prefix?: string
|
||||||
|
sessionCreateTime?: string
|
||||||
|
stopReason?: string
|
||||||
|
packageTotalFlowMb?: number
|
||||||
|
usedFlowMb?: number
|
||||||
|
remainFlowMb?: number
|
||||||
|
usedFlowPercent?: number
|
||||||
|
yesterdayFlowStatDate?: string
|
||||||
|
yesterdayDeptUsedFlowMb?: number
|
||||||
|
yesterdayDeptSms?: number
|
||||||
|
yesterdayDeptVoiceMin?: number
|
||||||
|
flowPkgBeginAt?: string
|
||||||
|
flowPkgEndAt?: string
|
||||||
|
dataServiceStatus?: number
|
||||||
|
packageTotalSms?: number
|
||||||
|
usedSms?: number
|
||||||
|
smsServiceStatus?: number
|
||||||
|
packageTotalVoice?: number
|
||||||
|
usedVoice?: number
|
||||||
|
voiceServiceStatus?: number
|
||||||
|
roamServiceStatus?: number
|
||||||
|
statusSyncTime?: string
|
||||||
|
flowSyncTime?: string
|
||||||
|
remark?: string
|
||||||
|
terminalName?: string
|
||||||
|
isBound?: boolean
|
||||||
|
canBusiness?: boolean
|
||||||
|
canAllocate?: boolean
|
||||||
|
ownerType?: 'public' | 'terminal' | 'agent' | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicQueryCardParams {
|
||||||
|
tenantId?: string
|
||||||
|
cardValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardRecognitionCode
|
||||||
|
= | 'CARD_RECOGNIZED'
|
||||||
|
| 'CARD_AMBIGUOUS'
|
||||||
|
| 'CARD_UNREADABLE'
|
||||||
|
| 'RECOGNITION_TIMEOUT'
|
||||||
|
| 'RECOGNITION_FAILED'
|
||||||
|
| 'RECOGNITION_DISABLED'
|
||||||
|
| 'RECOGNITION_RATE_LIMITED'
|
||||||
|
|
||||||
|
export interface CardRecognitionRes {
|
||||||
|
code: CardRecognitionCode | string
|
||||||
|
message: string
|
||||||
|
cardType: 'iccid' | string
|
||||||
|
cardValue: string
|
||||||
|
confidence: number
|
||||||
|
candidates: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
cardValue?: string
|
||||||
|
status?: string
|
||||||
|
categoryCode?: string
|
||||||
|
includeTotal?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppPackage {
|
||||||
|
id: number
|
||||||
|
subscriptionNo?: string
|
||||||
|
packageCode?: string
|
||||||
|
packageName?: string
|
||||||
|
packageType?: number
|
||||||
|
packageTypeName?: string
|
||||||
|
categoryCode?: string
|
||||||
|
categoryName?: string
|
||||||
|
iccid?: string
|
||||||
|
msisdn?: string
|
||||||
|
cardId?: number
|
||||||
|
cardValue?: string
|
||||||
|
cardRemark?: string
|
||||||
|
terminalName?: string
|
||||||
|
businessType?: number
|
||||||
|
businessTypeName?: string
|
||||||
|
effectiveStatus?: number
|
||||||
|
status?: string
|
||||||
|
statusType?: 'danger' | 'primary' | 'success' | 'warning' | string
|
||||||
|
filterValue?: string
|
||||||
|
effectiveTime?: string
|
||||||
|
expireTime?: string
|
||||||
|
validText?: string
|
||||||
|
effectText?: string
|
||||||
|
flowMb?: number
|
||||||
|
flowText?: string
|
||||||
|
ownerType?: 'public' | 'terminal' | 'agent' | string
|
||||||
|
canBusiness?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
cardValue?: string
|
||||||
|
subscriptionNo?: string
|
||||||
|
packageCode?: string
|
||||||
|
status?: string
|
||||||
|
categoryCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PublicPackageDetailParams {
|
||||||
|
tenantId?: string
|
||||||
|
cardValue: string
|
||||||
|
packageCode?: string
|
||||||
|
subscriptionNo?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BindCardParams {
|
||||||
|
tenantId?: string
|
||||||
|
cardValue: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestartCardParams {
|
||||||
|
cardValue: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardLifecycleParams {
|
||||||
|
cardValue?: string
|
||||||
|
cardValues?: string[]
|
||||||
|
operation: 'stop' | 'start' | 'restart' | string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardLifecycleStatusParams {
|
||||||
|
batchNo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewPrepareParams {
|
||||||
|
cardValue: string
|
||||||
|
packageCode?: string
|
||||||
|
subscriptionNo?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewBatchPrepareParams {
|
||||||
|
cardValues: string[]
|
||||||
|
packageCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewSubmitParams {
|
||||||
|
cardValue: string
|
||||||
|
packageCode?: string
|
||||||
|
subscriptionNo?: string
|
||||||
|
durationMonths?: number
|
||||||
|
durationYears?: number
|
||||||
|
buyerNote?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewBatchSubmitParams {
|
||||||
|
cardValues?: string[]
|
||||||
|
batchNo?: string
|
||||||
|
packageCode?: string
|
||||||
|
durationMonths?: number
|
||||||
|
durationYears?: number
|
||||||
|
buyerNote?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateTargetListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateTarget {
|
||||||
|
deptId: number
|
||||||
|
parentId?: number
|
||||||
|
deptKey?: string
|
||||||
|
deptName: string
|
||||||
|
deptCategory?: string
|
||||||
|
targetScope?: string
|
||||||
|
targetScopeName?: string
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateBatchPrepareParams {
|
||||||
|
cardValues: string[]
|
||||||
|
targetDeptId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateBatchSubmitParams {
|
||||||
|
cardValues?: string[]
|
||||||
|
batchNo: string
|
||||||
|
targetDeptId?: number
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePrepareParams {
|
||||||
|
cardValues: string[]
|
||||||
|
newPackageCode?: string
|
||||||
|
targetPackageCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeSubmitParams {
|
||||||
|
cardValues?: string[]
|
||||||
|
batchNo: string
|
||||||
|
newPackageCode: string
|
||||||
|
durationMonths: number
|
||||||
|
buyerNote?: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardQueryRes {
|
||||||
|
card: AppCard
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardListRes {
|
||||||
|
items: AppCard[]
|
||||||
|
total: number
|
||||||
|
hasMore?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageListRes {
|
||||||
|
items: AppPackage[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PackageDetailRes {
|
||||||
|
package: AppPackage
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestartCardRes {
|
||||||
|
batchNo: string
|
||||||
|
batchType: number
|
||||||
|
status: string
|
||||||
|
card: AppCard
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardLifecycleRes {
|
||||||
|
batchNo: string
|
||||||
|
batchType: number
|
||||||
|
operation: string
|
||||||
|
status: string
|
||||||
|
quantity: number
|
||||||
|
successCount?: number
|
||||||
|
failCount?: number
|
||||||
|
failReason?: string
|
||||||
|
cards: AppCard[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateTargetListRes {
|
||||||
|
items: AllocateTarget[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateBatchRes {
|
||||||
|
batchNo: string
|
||||||
|
sourceBatchNo?: string
|
||||||
|
batchType: number
|
||||||
|
quantity: number
|
||||||
|
totalCount?: number
|
||||||
|
validCount?: number
|
||||||
|
failCount?: number
|
||||||
|
failReason?: string
|
||||||
|
target: AllocateTarget
|
||||||
|
cards: AppCard[]
|
||||||
|
details?: Array<{
|
||||||
|
rowNo: number
|
||||||
|
itemId: string
|
||||||
|
status: string
|
||||||
|
errorReason?: string
|
||||||
|
}>
|
||||||
|
status: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePackageOption {
|
||||||
|
package: AppPackage
|
||||||
|
options: AppRenewOption[]
|
||||||
|
unitPrice: number
|
||||||
|
priceText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangePrepareRes {
|
||||||
|
batchNo: string
|
||||||
|
batchType: number
|
||||||
|
quantity: number
|
||||||
|
cards: AppCard[]
|
||||||
|
currentPackage: AppPackage
|
||||||
|
targetPackages: ChangePackageOption[]
|
||||||
|
effectiveRule: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeSubmitRes {
|
||||||
|
batchNo: string
|
||||||
|
batchType: number
|
||||||
|
orderId: number
|
||||||
|
orderSn: string
|
||||||
|
totalAmount: number
|
||||||
|
priceText: string
|
||||||
|
quantity: number
|
||||||
|
orderStatus: number
|
||||||
|
payStatus: number
|
||||||
|
status: string
|
||||||
|
effectiveRule: string
|
||||||
|
cards: AppCard[]
|
||||||
|
currentPackage: AppPackage
|
||||||
|
newPackage: AppPackage
|
||||||
|
option: AppRenewOption
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppRenewOption {
|
||||||
|
durationMonths: number
|
||||||
|
actualDurationMonths?: number
|
||||||
|
durationYears?: number
|
||||||
|
durationValue?: number
|
||||||
|
durationUnit?: string
|
||||||
|
durationUnitText?: string
|
||||||
|
label: string
|
||||||
|
unitPrice: number
|
||||||
|
totalAmount: number
|
||||||
|
priceText: string
|
||||||
|
expireAfter?: string
|
||||||
|
recommended?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewPackageOption {
|
||||||
|
package: AppPackage
|
||||||
|
packageCode: string
|
||||||
|
packageName: string
|
||||||
|
renewUnitPrice: number
|
||||||
|
unitPrice?: number
|
||||||
|
priceSource?: string
|
||||||
|
priceText?: string
|
||||||
|
renewableDurationOptions?: number[]
|
||||||
|
options: AppRenewOption[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewPrepareRes {
|
||||||
|
batchNo?: string
|
||||||
|
batchType?: number
|
||||||
|
card: AppCard
|
||||||
|
cards?: AppCard[]
|
||||||
|
quantity?: number
|
||||||
|
currentPackage?: AppPackage
|
||||||
|
package: AppPackage
|
||||||
|
renewablePackageCodes?: string[]
|
||||||
|
renewablePackages?: RenewPackageOption[]
|
||||||
|
options: AppRenewOption[]
|
||||||
|
priceSource: string
|
||||||
|
unitPrice: number
|
||||||
|
effectiveRule: string
|
||||||
|
recommendIndex: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenewSubmitRes {
|
||||||
|
batchNo?: string
|
||||||
|
batchType?: number
|
||||||
|
orderId: number
|
||||||
|
orderSn: string
|
||||||
|
totalAmount: number
|
||||||
|
priceText: string
|
||||||
|
quantity?: number
|
||||||
|
orderStatus: number
|
||||||
|
payStatus: number
|
||||||
|
status: string
|
||||||
|
effectiveRule: string
|
||||||
|
card: AppCard
|
||||||
|
cards?: AppCard[]
|
||||||
|
package: AppPackage
|
||||||
|
option: AppRenewOption
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import type { AppEnterpriseProfile, AppTerminalProfile } from './login'
|
||||||
|
|
||||||
|
export type AppCustomerType = 'all' | 'enterprise' | 'terminal'
|
||||||
|
|
||||||
|
export interface CustomerListParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
customerType?: AppCustomerType
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppCustomer {
|
||||||
|
customerType: AppCustomerType | string
|
||||||
|
customerTypeName: string
|
||||||
|
customerId: number
|
||||||
|
deptId?: number
|
||||||
|
terminalId?: number
|
||||||
|
parentDeptId?: number
|
||||||
|
customerCode?: string
|
||||||
|
customerName: string
|
||||||
|
deptCategory?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
status?: string
|
||||||
|
statusText: string
|
||||||
|
realNameStatus: string
|
||||||
|
balance?: number
|
||||||
|
cardCount: number
|
||||||
|
normalCardCount: number
|
||||||
|
address?: string
|
||||||
|
fullName?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppCustomerDetail extends AppCustomer {
|
||||||
|
enterpriseProfile?: AppEnterpriseProfile | null
|
||||||
|
terminalProfile?: AppTerminalProfile | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerListRes {
|
||||||
|
items: AppCustomer[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDetailParams {
|
||||||
|
customerType: Exclude<AppCustomerType, 'all'> | string
|
||||||
|
customerId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDetailRes {
|
||||||
|
customer: AppCustomerDetail
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type { AppCard } from './card'
|
||||||
|
|
||||||
|
export interface DiagnosisInfoParams {
|
||||||
|
queryCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisFlowParams {
|
||||||
|
queryCode: string
|
||||||
|
queryType: '7d' | '30d' | '3M' | '6M'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisInfoItem {
|
||||||
|
iccid?: string
|
||||||
|
imsi?: string
|
||||||
|
msisdn?: string
|
||||||
|
status?: string
|
||||||
|
dataStatus?: string
|
||||||
|
onlineStatus?: string
|
||||||
|
operatingStatus?: string
|
||||||
|
deviceStatus?: string
|
||||||
|
imeiStatus?: string
|
||||||
|
regionLimitStatus?: string
|
||||||
|
regionLimitOrderStatus?: string
|
||||||
|
regionLimitTriggerStatus?: string
|
||||||
|
regionLimitType?: string
|
||||||
|
regionLimitArea?: string
|
||||||
|
humanLimitStatus?: string
|
||||||
|
rateLimitStatus?: string
|
||||||
|
rateLimitReason?: string
|
||||||
|
rateLimitDetail?: string
|
||||||
|
rateLimitApnDetails?: DiagnosisRateLimitApnItem[]
|
||||||
|
packageTotalFlowMb?: number
|
||||||
|
usedFlowMb?: number
|
||||||
|
remainFlowMb?: number
|
||||||
|
flowSyncTime?: string
|
||||||
|
stopReason?: string
|
||||||
|
frozenStatus?: string
|
||||||
|
frozenReason?: string
|
||||||
|
frozenOperTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisRateLimitApnItem {
|
||||||
|
apnName?: string
|
||||||
|
apnStatus?: string
|
||||||
|
modifyTime?: string
|
||||||
|
overUseDetails?: string
|
||||||
|
apnServiceUsageState?: string
|
||||||
|
pccDetails?: DiagnosisRateLimitPccItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisRateLimitPccItem {
|
||||||
|
pccServiceCode?: string
|
||||||
|
pccName?: string
|
||||||
|
pccPolicyStatus?: string
|
||||||
|
pccServiceUsageState?: string
|
||||||
|
modifyTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisSessionItem {
|
||||||
|
apnId?: string
|
||||||
|
rat?: string
|
||||||
|
imei?: string
|
||||||
|
createTime?: string
|
||||||
|
ipv4?: string
|
||||||
|
ipv6?: string
|
||||||
|
ipv6Prefix?: string
|
||||||
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisPackageItem {
|
||||||
|
packageName?: string
|
||||||
|
packageType?: string
|
||||||
|
effectiveStatus?: string
|
||||||
|
packageTotalFlowMb?: number
|
||||||
|
usedFlowMb?: number
|
||||||
|
remainFlowMb?: number
|
||||||
|
durationMonths?: number
|
||||||
|
expectedExpireTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisInfoRes {
|
||||||
|
info?: DiagnosisInfoItem
|
||||||
|
session?: DiagnosisSessionItem
|
||||||
|
package?: DiagnosisPackageItem
|
||||||
|
modules?: Record<string, DiagnosisModuleItem>
|
||||||
|
card?: AppCard
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisModuleItem {
|
||||||
|
status: 'success' | 'error' | 'cached' | 'skipped' | string
|
||||||
|
message?: string
|
||||||
|
cached?: boolean
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisFlowRes {
|
||||||
|
times: string[]
|
||||||
|
flow: number[]
|
||||||
|
unit: string
|
||||||
|
granularity?: 'day' | 'month' | string
|
||||||
|
hasData?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
export type AuthMode = 'single' | 'double'
|
||||||
|
|
||||||
|
export interface ISingleTokenRes {
|
||||||
|
token: string
|
||||||
|
expiresIn: number
|
||||||
|
userInfo?: IUserInfoRes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDoubleTokenRes {
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
accessExpiresIn: number
|
||||||
|
refreshExpiresIn: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginCandidate {
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
phone: string
|
||||||
|
deptId?: number
|
||||||
|
deptName: string
|
||||||
|
userType: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginSelectRes {
|
||||||
|
needSelect: true
|
||||||
|
candidates: ILoginCandidate[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes | ILoginSelectRes
|
||||||
|
|
||||||
|
export interface IWechatNeedBindRes {
|
||||||
|
needBindPhone: true
|
||||||
|
bindTicket: string
|
||||||
|
bindExpireIn?: number
|
||||||
|
needSelect?: boolean
|
||||||
|
candidates?: ILoginCandidate[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IWechatLoginRes = ISingleTokenRes | IDoubleTokenRes | IWechatNeedBindRes
|
||||||
|
export type IWechatBindPhoneRes = ISingleTokenRes
|
||||||
|
| IDoubleTokenRes
|
||||||
|
| (ILoginSelectRes & Pick<IWechatNeedBindRes, 'needBindPhone' | 'bindTicket'>)
|
||||||
|
|
||||||
|
export type UserRole = string
|
||||||
|
|
||||||
|
export interface AppEnterpriseProfile {
|
||||||
|
deptId?: number
|
||||||
|
cardCount?: number
|
||||||
|
deptName: string
|
||||||
|
deptKey: string
|
||||||
|
phone: string
|
||||||
|
email: string
|
||||||
|
statusText: string
|
||||||
|
creditCode: string
|
||||||
|
areaCode: string
|
||||||
|
address: string
|
||||||
|
fullName: string
|
||||||
|
industry: string
|
||||||
|
legalPersonName: string
|
||||||
|
legalPersonIdCard: string
|
||||||
|
companyType: string
|
||||||
|
establishTime: string
|
||||||
|
bankName: string
|
||||||
|
bankAccount: string
|
||||||
|
realNameStatus: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppTerminalProfile {
|
||||||
|
cardCount?: number
|
||||||
|
terminalName: string
|
||||||
|
phone: string
|
||||||
|
email: string
|
||||||
|
statusText: string
|
||||||
|
creditCode: string
|
||||||
|
areaCode: string
|
||||||
|
address: string
|
||||||
|
fullName: string
|
||||||
|
industry: string
|
||||||
|
legalPersonName: string
|
||||||
|
legalPersonIdCard: string
|
||||||
|
companyType: string
|
||||||
|
establishTime: string
|
||||||
|
bankName: string
|
||||||
|
bankAccount: string
|
||||||
|
realNameStatus: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILoginForm {
|
||||||
|
tenantId: string
|
||||||
|
account: string
|
||||||
|
username?: string
|
||||||
|
password: string
|
||||||
|
selectedUserId?: number
|
||||||
|
wxCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatLoginForm {
|
||||||
|
tenantId: string
|
||||||
|
wxCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatBindPhoneForm {
|
||||||
|
tenantId: string
|
||||||
|
bindTicket: string
|
||||||
|
phone: string
|
||||||
|
captchaKey?: string
|
||||||
|
captchaPoint?: string
|
||||||
|
selectedUserId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IRegisterForm {
|
||||||
|
tenantId: string
|
||||||
|
phone: string
|
||||||
|
password: string
|
||||||
|
nickName: string
|
||||||
|
/** 短信验证码:当前注册未接入短信,图形验证通过后可为空 */
|
||||||
|
smsCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IForgotPasswordForm {
|
||||||
|
tenantId: string
|
||||||
|
phone: string
|
||||||
|
password: string
|
||||||
|
smsCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUserInfoRes {
|
||||||
|
userId?: number
|
||||||
|
tenantId?: string
|
||||||
|
deptId?: number
|
||||||
|
username: string
|
||||||
|
nickName: string
|
||||||
|
nickname?: string
|
||||||
|
phone: string
|
||||||
|
avatar?: string
|
||||||
|
sex?: '0' | '1' | '2' | string
|
||||||
|
userType?: string
|
||||||
|
isTerminal?: boolean
|
||||||
|
canEnterpriseBusiness?: boolean
|
||||||
|
wechatBound?: boolean
|
||||||
|
wechatBindText?: string
|
||||||
|
balance?: number
|
||||||
|
role?: UserRole
|
||||||
|
roleName?: string
|
||||||
|
roleKey?: string
|
||||||
|
roleNames?: string[]
|
||||||
|
roles?: UserRole[]
|
||||||
|
accessCodes?: string[]
|
||||||
|
enterpriseProfile?: AppEnterpriseProfile | null
|
||||||
|
terminalProfile?: AppTerminalProfile | null
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuthStorage {
|
||||||
|
mode: AuthMode
|
||||||
|
tokens: ISingleTokenRes | IDoubleTokenRes
|
||||||
|
userInfo?: IUserInfoRes
|
||||||
|
loginTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISideCaptcha {
|
||||||
|
captchaKey: string
|
||||||
|
imageBase64: string
|
||||||
|
tileBase64: string
|
||||||
|
tileWidth: number
|
||||||
|
tileHeight: number
|
||||||
|
tileX: number
|
||||||
|
tileY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICheckSideCaptchaForm {
|
||||||
|
captchaKey: string
|
||||||
|
point: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISendSmsForm {
|
||||||
|
tenantId: string
|
||||||
|
phone: string
|
||||||
|
scene: 'register' | 'forgotPassword'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISendSmsRes {
|
||||||
|
success: boolean
|
||||||
|
smsCode?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUploadSuccessInfo {
|
||||||
|
fileId: number
|
||||||
|
originalName: string
|
||||||
|
fileName: string
|
||||||
|
storagePath: string
|
||||||
|
fileHash: string
|
||||||
|
fileType: string
|
||||||
|
fileBusinessType: string
|
||||||
|
fileSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdateInfo {
|
||||||
|
nickName: string
|
||||||
|
avatar?: string
|
||||||
|
sex: string
|
||||||
|
terminalName?: string
|
||||||
|
phone?: string
|
||||||
|
email?: string
|
||||||
|
creditCode?: string
|
||||||
|
areaCode?: string
|
||||||
|
address?: string
|
||||||
|
fullName?: string
|
||||||
|
industry?: string
|
||||||
|
legalPersonName?: string
|
||||||
|
legalPersonIdCard?: string
|
||||||
|
companyType?: string
|
||||||
|
establishTime?: string
|
||||||
|
bankName?: string
|
||||||
|
bankAccount?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatBindForm {
|
||||||
|
wxCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatBindRes {
|
||||||
|
success: boolean
|
||||||
|
userInfo: IUserInfoRes
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUpdatePassword {
|
||||||
|
oldPassword: string
|
||||||
|
newPassword: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
|
||||||
|
return 'token' in tokenRes && !('refreshToken' in tokenRes)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
|
||||||
|
return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoginSelectRes(tokenRes: IAuthLoginRes): tokenRes is ILoginSelectRes {
|
||||||
|
return 'needSelect' in tokenRes && tokenRes.needSelect === true
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
export type MessageGroupKey = 'system' | 'card' | string
|
||||||
|
|
||||||
|
export type MessageTagType = 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'volcano' | 'lightblue' | 'cyan' | 'pink' | 'purple'
|
||||||
|
|
||||||
|
export interface MessageItem {
|
||||||
|
messageId?: number
|
||||||
|
group?: MessageGroupKey
|
||||||
|
source?: 'user_message' | 'card_warn' | string
|
||||||
|
title: string
|
||||||
|
summary: string
|
||||||
|
time: string
|
||||||
|
unread: boolean
|
||||||
|
tag?: string
|
||||||
|
tagType?: MessageTagType
|
||||||
|
link?: string
|
||||||
|
cardValue?: string
|
||||||
|
warnType?: string
|
||||||
|
severity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageGroup {
|
||||||
|
key: MessageGroupKey
|
||||||
|
label: string
|
||||||
|
unreadCount: number
|
||||||
|
searchPlaceholder: string
|
||||||
|
tag: string
|
||||||
|
tagType: MessageTagType
|
||||||
|
list: MessageItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarnSummary {
|
||||||
|
total: number
|
||||||
|
critical: number
|
||||||
|
warning: number
|
||||||
|
reminder: number
|
||||||
|
latestTime?: string
|
||||||
|
items?: MessageItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageCenterParams {
|
||||||
|
page?: number
|
||||||
|
pageSize?: number
|
||||||
|
group?: MessageGroupKey
|
||||||
|
keyword?: string
|
||||||
|
timeRange?: MessageTimeRange
|
||||||
|
sortBy?: MessageSortBy
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MessageTimeRange = 'all' | '30d'
|
||||||
|
|
||||||
|
export type MessageSortBy = 'none' | 'time_desc' | 'time_asc'
|
||||||
|
|
||||||
|
export interface MessageCenterRes {
|
||||||
|
groups: MessageGroup[]
|
||||||
|
totalUnread: number
|
||||||
|
warnSummary?: WarnSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageReadParams {
|
||||||
|
messageIds: number[]
|
||||||
|
group?: MessageGroupKey
|
||||||
|
source?: MessageItem['source']
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
export interface OrderListParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
orderType?: number
|
||||||
|
payStatus?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppOrderItem {
|
||||||
|
itemId: number
|
||||||
|
productName: string
|
||||||
|
productSn: string
|
||||||
|
price: number
|
||||||
|
priceText: string
|
||||||
|
quantity: number
|
||||||
|
originPrice: number
|
||||||
|
originPriceText: string
|
||||||
|
productType: number
|
||||||
|
serviceBeginTime?: string
|
||||||
|
serviceEndTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppOrder {
|
||||||
|
orderId: number
|
||||||
|
orderSn: string
|
||||||
|
orderType: number
|
||||||
|
orderTypeText: string
|
||||||
|
orderStatus: number
|
||||||
|
orderStatusText: string
|
||||||
|
payType: number
|
||||||
|
payTypeText: string
|
||||||
|
payStatus: number
|
||||||
|
payStatusText: string
|
||||||
|
payOrderSn: string
|
||||||
|
returnStatus: number
|
||||||
|
returnStatusText: string
|
||||||
|
paymentExpireAt?: string
|
||||||
|
paymentRemainingSeconds?: number
|
||||||
|
paymentExpired?: boolean
|
||||||
|
totalAmount: number
|
||||||
|
totalAmountText: string
|
||||||
|
paidAmount: number
|
||||||
|
paidAmountText: string
|
||||||
|
onlinePaidAmount: number
|
||||||
|
onlinePaidAmountText: string
|
||||||
|
balance: number
|
||||||
|
balanceText: string
|
||||||
|
unpaidAmount: number
|
||||||
|
unpaidAmountText: string
|
||||||
|
unrefundAmount: number
|
||||||
|
unrefundAmountText: string
|
||||||
|
buyerNote: string
|
||||||
|
remark: string
|
||||||
|
createdAt?: string
|
||||||
|
payTime?: string
|
||||||
|
itemSummary: string
|
||||||
|
itemCount: number
|
||||||
|
items?: AppOrderItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderListRes {
|
||||||
|
items: AppOrder[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderDetailParams {
|
||||||
|
orderSn: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderDetailRes {
|
||||||
|
order: AppOrder
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import type { AppCard } from './card'
|
||||||
|
|
||||||
|
export interface WarningConfig {
|
||||||
|
id?: number
|
||||||
|
itemType?: number
|
||||||
|
itemId?: number
|
||||||
|
itemCode?: string
|
||||||
|
warnType?: number
|
||||||
|
warnUnit: number
|
||||||
|
warnTitle?: string
|
||||||
|
unitText?: string
|
||||||
|
firstWarn: number
|
||||||
|
secondWarn?: number
|
||||||
|
thirdWarn?: number
|
||||||
|
autoAction?: number
|
||||||
|
isEnabled: number
|
||||||
|
appNotifyEnabled?: number
|
||||||
|
wxTemplateId?: string
|
||||||
|
remark?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigListParams {
|
||||||
|
cardValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigListRes {
|
||||||
|
card: AppCard
|
||||||
|
items: WarningConfig[]
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigSaveParams {
|
||||||
|
cardValue: string
|
||||||
|
warnUnit: number
|
||||||
|
firstWarn: number
|
||||||
|
secondWarn?: number
|
||||||
|
thirdWarn?: number
|
||||||
|
isEnabled: number
|
||||||
|
appNotifyEnabled: number
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigSaveRes {
|
||||||
|
id: number
|
||||||
|
config: WarningConfig
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigNotifyParams {
|
||||||
|
cardValue: string
|
||||||
|
warnUnit?: number
|
||||||
|
appNotifyEnabled: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigNotifyRes {
|
||||||
|
appNotifyEnabled: number
|
||||||
|
config?: WarningConfig
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigMetaRes {
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
appNotifyEnabled: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchSaveParams {
|
||||||
|
cardValues: string[]
|
||||||
|
configs: WarningConfigBatchItemParams[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchItemParams {
|
||||||
|
warnUnit: number
|
||||||
|
firstWarn: number
|
||||||
|
secondWarn?: number
|
||||||
|
thirdWarn?: number
|
||||||
|
isEnabled: number
|
||||||
|
appNotifyEnabled: number
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchFailItem {
|
||||||
|
cardValue: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchSaveRes {
|
||||||
|
batchNo: string
|
||||||
|
totalCount: number
|
||||||
|
successCount: number
|
||||||
|
failCount: number
|
||||||
|
failItems: WarningConfigBatchFailItem[]
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchNotifyParams {
|
||||||
|
cardValues: string[]
|
||||||
|
warnUnit?: number
|
||||||
|
appNotifyEnabled: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningConfigBatchNotifyRes {
|
||||||
|
appNotifyEnabled: number
|
||||||
|
totalCount: number
|
||||||
|
successCount: number
|
||||||
|
failCount: number
|
||||||
|
failItems: WarningConfigBatchFailItem[]
|
||||||
|
templateId: string
|
||||||
|
templateReady: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
wechatBindText: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export interface WorkOrderListParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
currentStatus?: number
|
||||||
|
woType?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderDetailParams {
|
||||||
|
woNo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderCreateParams {
|
||||||
|
woType: number
|
||||||
|
relatedId?: number
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppWorkOrderCommunication {
|
||||||
|
id: number
|
||||||
|
woNo: string
|
||||||
|
commType: number
|
||||||
|
commTypeText: string
|
||||||
|
senderName: string
|
||||||
|
content: string
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppWorkOrder {
|
||||||
|
id: number
|
||||||
|
woNo: string
|
||||||
|
woType: number
|
||||||
|
woTypeText: string
|
||||||
|
relatedId: number
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
creatorType: number
|
||||||
|
creatorName: string
|
||||||
|
creatorPhone: string
|
||||||
|
currentStatus: number
|
||||||
|
currentStatusText: string
|
||||||
|
statusType: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
communications?: AppWorkOrderCommunication[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderListRes {
|
||||||
|
items: AppWorkOrder[]
|
||||||
|
total: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderDetailRes {
|
||||||
|
workOrder: AppWorkOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderCreateRes {
|
||||||
|
workOrder: AppWorkOrder
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type {
|
||||||
|
WarningConfigBatchNotifyParams,
|
||||||
|
WarningConfigBatchNotifyRes,
|
||||||
|
WarningConfigBatchSaveParams,
|
||||||
|
WarningConfigBatchSaveRes,
|
||||||
|
WarningConfigListParams,
|
||||||
|
WarningConfigListRes,
|
||||||
|
WarningConfigMetaRes,
|
||||||
|
WarningConfigNotifyParams,
|
||||||
|
WarningConfigNotifyRes,
|
||||||
|
WarningConfigSaveParams,
|
||||||
|
WarningConfigSaveRes,
|
||||||
|
} from './types/warning'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
WarningConfig,
|
||||||
|
WarningConfigBatchItemParams,
|
||||||
|
WarningConfigBatchNotifyParams,
|
||||||
|
WarningConfigBatchNotifyRes,
|
||||||
|
WarningConfigBatchSaveParams,
|
||||||
|
WarningConfigBatchSaveRes,
|
||||||
|
WarningConfigListRes,
|
||||||
|
WarningConfigNotifyParams,
|
||||||
|
WarningConfigNotifyRes,
|
||||||
|
WarningConfigSaveParams,
|
||||||
|
} from './types/warning'
|
||||||
|
|
||||||
|
export function getWarningConfigList(params: WarningConfigListParams) {
|
||||||
|
return http.get<WarningConfigListRes>('/warning/config/list', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveWarningConfig(data: WarningConfigSaveParams) {
|
||||||
|
return http.post<WarningConfigSaveRes>('/warning/config/save', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWarningConfigNotify(data: WarningConfigNotifyParams) {
|
||||||
|
return http.post<WarningConfigNotifyRes>(
|
||||||
|
'/warning/config/notify',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWarningConfigMeta() {
|
||||||
|
return http.get<WarningConfigMetaRes>('/warning/config/meta', undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveWarningConfigBatch(data: WarningConfigBatchSaveParams) {
|
||||||
|
return http.post<WarningConfigBatchSaveRes>(
|
||||||
|
'/warning/config/batch-save',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateWarningConfigNotifyBatch(data: WarningConfigBatchNotifyParams) {
|
||||||
|
return http.post<WarningConfigBatchNotifyRes>(
|
||||||
|
'/warning/config/batch-notify',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type {
|
||||||
|
WorkOrderCreateParams,
|
||||||
|
WorkOrderCreateRes,
|
||||||
|
WorkOrderDetailParams,
|
||||||
|
WorkOrderDetailRes,
|
||||||
|
WorkOrderListParams,
|
||||||
|
WorkOrderListRes,
|
||||||
|
} from './types/workOrder'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AppWorkOrder,
|
||||||
|
AppWorkOrderCommunication,
|
||||||
|
WorkOrderCreateParams,
|
||||||
|
WorkOrderDetailParams,
|
||||||
|
WorkOrderListParams,
|
||||||
|
} from './types/workOrder'
|
||||||
|
|
||||||
|
export function getWorkOrderList(params: WorkOrderListParams) {
|
||||||
|
return http.get<WorkOrderListRes>('/work-order/list', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWorkOrderDetail(params: WorkOrderDetailParams) {
|
||||||
|
return http.get<WorkOrderDetailRes>('/work-order/detail', params, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWorkOrder(data: WorkOrderCreateParams) {
|
||||||
|
return http.post<WorkOrderCreateRes>('/work-order/create', data, undefined, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { getCurrentPageI18nKey, isCurrentPageTabbar } from '@/utils'
|
||||||
|
defineProps<{
|
||||||
|
desc?: string
|
||||||
|
}>()
|
||||||
|
/** ==================== 数据 ==================== */
|
||||||
|
const title = ref('')
|
||||||
|
const isTabbarPage = ref(true)
|
||||||
|
/** ==================== 方法 ==================== */
|
||||||
|
function handleClickLeft() {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ==================== 周期 ==================== */
|
||||||
|
onMounted(() => {
|
||||||
|
title.value = getCurrentPageI18nKey()
|
||||||
|
isTabbarPage.value = isCurrentPageTabbar()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<wd-navbar
|
||||||
|
:fixed="true"
|
||||||
|
safe-area-inset-top
|
||||||
|
placeholder
|
||||||
|
custom-class="navbar"
|
||||||
|
>
|
||||||
|
<template #left>
|
||||||
|
<view
|
||||||
|
v-if="!isTabbarPage"
|
||||||
|
class="nav-back"
|
||||||
|
@click="handleClickLeft"
|
||||||
|
>
|
||||||
|
<wd-icon name="left" size="16px" custom-class="nav-back-ico" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<view class="nav-title-row">
|
||||||
|
<view class="nav-title">{{ title }}</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</wd-navbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.navbar) {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
.wd-navbar__content {
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding-top: 4rpx !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-navbar__left {
|
||||||
|
padding: 0;
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-navbar__title {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wd-navbar__right {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-back {
|
||||||
|
height: 52rpx;
|
||||||
|
width: 52rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.nav-back-ico) {
|
||||||
|
color: #2563eb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title-row {
|
||||||
|
display: flex;
|
||||||
|
height: 64rpx;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-title {
|
||||||
|
max-width: 360rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 32px;
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import GoCaptchaUni from 'go-captcha-uni'
|
||||||
|
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { getSideCaptcha } from '@/api/login'
|
||||||
|
|
||||||
|
defineOptions({ name: 'SideCaptcha' })
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
confirm: [{ captchaKey: string, posX: number, posY: number }]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
const DEFAULT_WIDTH = 300
|
||||||
|
const DEFAULT_HEIGHT = 220
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const captchaKey = ref('')
|
||||||
|
const scaleRatio = ref(1)
|
||||||
|
const captchaRef = ref<InstanceType<typeof GoCaptchaUni> | null>(null)
|
||||||
|
let requestSerial = 0
|
||||||
|
|
||||||
|
const captchaConfig = reactive({
|
||||||
|
width: DEFAULT_WIDTH,
|
||||||
|
height: DEFAULT_HEIGHT,
|
||||||
|
showTheme: false,
|
||||||
|
verticalPadding: 0,
|
||||||
|
horizontalPadding: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const captchaData = reactive({
|
||||||
|
thumbX: 0,
|
||||||
|
thumbY: 0,
|
||||||
|
thumbWidth: 0,
|
||||||
|
thumbHeight: 0,
|
||||||
|
image: '',
|
||||||
|
thumb: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const captchaReady = computed(() =>
|
||||||
|
!loading.value
|
||||||
|
&& !errorMessage.value
|
||||||
|
&& !!captchaKey.value
|
||||||
|
&& !!captchaData.image
|
||||||
|
&& !!captchaData.thumb
|
||||||
|
&& captchaData.thumbWidth > 0
|
||||||
|
&& captchaData.thumbHeight > 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
function toBase64(src?: string) {
|
||||||
|
return src
|
||||||
|
? (src.startsWith('data:image') ? src : `data:image/png;base64,${src}`)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripBase64Prefix(src: string) {
|
||||||
|
return src.replace(/^data:image\/\w+;base64,/, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCaptchaImage(src: string, name: string) {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const dataUri = toBase64(src)
|
||||||
|
if (!dataUri) {
|
||||||
|
reject(new Error('验证码图片为空'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
const userDataPath = wx.env?.USER_DATA_PATH
|
||||||
|
if (!userDataPath) {
|
||||||
|
reject(new Error('小程序本地图片目录不可用'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const filePath = `${userDataPath}/${name}-${Date.now()}-${Math.random().toString(36).slice(2)}.png`
|
||||||
|
wx.getFileSystemManager().writeFile({
|
||||||
|
filePath,
|
||||||
|
data: stripBase64Prefix(dataUri),
|
||||||
|
encoding: 'base64',
|
||||||
|
success: () => resolve(filePath),
|
||||||
|
fail: error => reject(new Error(error.errMsg || '验证码图片写入失败')),
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
resolve(dataUri)
|
||||||
|
// #endif
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function measureContainerWidth(): Promise<number> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!instance) {
|
||||||
|
resolve(DEFAULT_WIDTH)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni
|
||||||
|
.createSelectorQuery()
|
||||||
|
.in(instance)
|
||||||
|
.select('.side-captcha-host')
|
||||||
|
.boundingClientRect((rect) => {
|
||||||
|
resolve(!Array.isArray(rect) && rect?.width ? Math.floor(rect.width) : DEFAULT_WIDTH)
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImageSize(src: string) {
|
||||||
|
return new Promise<{ width: number, height: number }>((resolve) => {
|
||||||
|
uni.getImageInfo({
|
||||||
|
src,
|
||||||
|
success: ({ width, height }) => resolve({ width, height }),
|
||||||
|
fail: () => resolve({ width: 0, height: 0 }),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCaptcha() {
|
||||||
|
const currentRequest = ++requestSerial
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
captchaKey.value = ''
|
||||||
|
Object.assign(captchaData, {
|
||||||
|
thumbX: 0,
|
||||||
|
thumbY: 0,
|
||||||
|
thumbWidth: 0,
|
||||||
|
thumbHeight: 0,
|
||||||
|
image: '',
|
||||||
|
thumb: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getSideCaptcha()
|
||||||
|
const imageSrc = await normalizeCaptchaImage(res.imageBase64, 'side-captcha-bg')
|
||||||
|
const thumbSrc = await normalizeCaptchaImage(res.tileBase64, 'side-captcha-tile')
|
||||||
|
if (currentRequest !== requestSerial)
|
||||||
|
return
|
||||||
|
|
||||||
|
const captchaKeyValue = String(res.captchaKey || '').trim()
|
||||||
|
const tileWidth = Number(res.tileWidth || 0)
|
||||||
|
const tileHeight = Number(res.tileHeight || 0)
|
||||||
|
if (!captchaKeyValue || !imageSrc || !thumbSrc || tileWidth <= 0 || tileHeight <= 0)
|
||||||
|
throw new Error('验证码返回数据不完整')
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
const containerW = await measureContainerWidth()
|
||||||
|
const { width: srcW, height: srcH } = await getImageSize(imageSrc)
|
||||||
|
const sourceWidth = srcW || 280
|
||||||
|
const sourceHeight = srcH || 170
|
||||||
|
const availableWidth = containerW > 0 ? containerW : DEFAULT_WIDTH
|
||||||
|
const displayWidth = Math.max(240, Math.min(availableWidth, sourceWidth))
|
||||||
|
const ratio = displayWidth / sourceWidth
|
||||||
|
|
||||||
|
scaleRatio.value = ratio
|
||||||
|
captchaConfig.width = displayWidth
|
||||||
|
captchaConfig.height = Math.round(sourceHeight * ratio) || DEFAULT_HEIGHT
|
||||||
|
|
||||||
|
captchaKey.value = captchaKeyValue
|
||||||
|
captchaData.image = imageSrc
|
||||||
|
captchaData.thumb = thumbSrc
|
||||||
|
captchaData.thumbX = Math.round((res.tileX || 0) * ratio)
|
||||||
|
captchaData.thumbY = Math.round((res.tileY || 0) * ratio)
|
||||||
|
captchaData.thumbWidth = Math.round(tileWidth * ratio)
|
||||||
|
captchaData.thumbHeight = Math.round(tileHeight * ratio)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (currentRequest !== requestSerial)
|
||||||
|
return
|
||||||
|
console.error('获取滑块验证码失败:', error)
|
||||||
|
errorMessage.value = '验证码加载失败,请点击重新获取'
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (currentRequest === requestSerial)
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCaptchaConfirm(point: { x: number, y: number }) {
|
||||||
|
if (!captchaReady.value)
|
||||||
|
return
|
||||||
|
const posX = Math.round(point.x / (scaleRatio.value || 1))
|
||||||
|
const posY = Math.round(point.y / (scaleRatio.value || 1))
|
||||||
|
if (!Number.isFinite(posX) || !Number.isFinite(posY) || posX < 0 || posY < 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('confirm', { captchaKey: captchaKey.value, posX, posY })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
captchaRef.value?.clear()
|
||||||
|
await nextTick()
|
||||||
|
await fetchCaptcha()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ refresh })
|
||||||
|
|
||||||
|
onMounted(fetchCaptcha)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="side-captcha-host w-full flex justify-center">
|
||||||
|
<view v-if="loading" class="min-h-[260px] flex items-center justify-center">
|
||||||
|
<wd-loading />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<go-captcha-uni
|
||||||
|
v-else-if="captchaReady"
|
||||||
|
ref="captchaRef"
|
||||||
|
type="slide"
|
||||||
|
class="captcha"
|
||||||
|
:config="captchaConfig"
|
||||||
|
:data="captchaData"
|
||||||
|
@event-confirm="onCaptchaConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view v-else class="captcha-error">
|
||||||
|
<view class="i-carbon-warning-alt captcha-error__icon" />
|
||||||
|
<text class="captcha-error__title">验证码暂时无法显示</text>
|
||||||
|
<text class="captcha-error__desc">{{ errorMessage || '请重新获取验证码' }}</text>
|
||||||
|
<wd-button size="small" type="primary" @click="refresh">
|
||||||
|
重新获取
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.captcha) {
|
||||||
|
.gc-header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-error {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 260px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
background: #f8fafc;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-error__icon {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 42rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-error__title {
|
||||||
|
color: #334155;
|
||||||
|
font-size: 25rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.captcha-error__desc {
|
||||||
|
max-width: 90%;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/*******************************
|
||||||
|
* 此文件由脚本自动生成,请勿手动修改 *
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面路径常量
|
||||||
|
*/
|
||||||
|
const RouterPath = {
|
||||||
|
/** 山东平台物联卡 */
|
||||||
|
Home: '/pages/home/index',
|
||||||
|
/** 办理 */
|
||||||
|
Business: '/pages/business/index',
|
||||||
|
/** 卡片 */
|
||||||
|
Card: '/pages/card/index',
|
||||||
|
/** 山东平台物联卡 */
|
||||||
|
Login: '/pages/login/index',
|
||||||
|
/** 消息 */
|
||||||
|
Message: '/pages/message/index',
|
||||||
|
/** 我的 */
|
||||||
|
User: '/pages/user/index',
|
||||||
|
/** 找回密码 */
|
||||||
|
'LoginForgot-password': '/pages/login/forgot-password/index',
|
||||||
|
/** 注册账号 */
|
||||||
|
LoginRegister: '/pages/login/register/index',
|
||||||
|
/** 加油包 */
|
||||||
|
BusinessViewsAddon: '/pages/business/views/addon/index',
|
||||||
|
/** 批量卡片划拨 */
|
||||||
|
BusinessViewsAllocate: '/pages/business/views/allocate/index',
|
||||||
|
/** 余额充值 */
|
||||||
|
BusinessViewsBalanceRecharge: '/pages/business/views/balanceRecharge/index',
|
||||||
|
/** 单卡套餐续费 */
|
||||||
|
BusinessViewsCardRenew: '/pages/business/views/cardRenew/index',
|
||||||
|
/** 套餐变更 */
|
||||||
|
BusinessViewsChange: '/pages/business/views/change/index',
|
||||||
|
/** 客户管理 */
|
||||||
|
BusinessViewsCustomer: '/pages/business/views/customer/index',
|
||||||
|
/** 客户详情 */
|
||||||
|
BusinessViewsCustomerDetail: '/pages/business/views/customerDetail/index',
|
||||||
|
/** 卡片排查 */
|
||||||
|
BusinessViewsDiagnosis: '/pages/business/views/diagnosis/index',
|
||||||
|
/** 我的订单 */
|
||||||
|
BusinessViewsOrder: '/pages/business/views/order/index',
|
||||||
|
/** 批量套餐续费 */
|
||||||
|
BusinessViewsRenew: '/pages/business/views/renew/index',
|
||||||
|
/** 预警配置 */
|
||||||
|
BusinessViewsWarning: '/pages/business/views/warning/index',
|
||||||
|
/** 工单列表 */
|
||||||
|
BusinessViewsWorkOrder: '/pages/business/views/workOrder/index',
|
||||||
|
/** 提交工单 */
|
||||||
|
BusinessViewsWorkOrderCreate: '/pages/business/views/workOrderCreate/index',
|
||||||
|
/** 工单详情 */
|
||||||
|
BusinessViewsWorkOrderDetail: '/pages/business/views/workOrderDetail/index',
|
||||||
|
/** 卡片详情 */
|
||||||
|
CardViewsDetail: '/pages/card/views/detail/index',
|
||||||
|
/** 全部设备 */
|
||||||
|
CardViewsManagement: '/pages/card/views/management/index',
|
||||||
|
/** 卡片详情 */
|
||||||
|
CardViewsPackageDetail: '/pages/card/views/packageDetail/index',
|
||||||
|
/** 预警配置 */
|
||||||
|
CardViewsWarning: '/pages/card/views/warning/index',
|
||||||
|
/** 消息详情 */
|
||||||
|
MessageViewsCardDetail: '/pages/message/views/cardDetail/index',
|
||||||
|
/** 消息详情 */
|
||||||
|
MessageViewsSystemDetail: '/pages/message/views/systemDetail/index',
|
||||||
|
/** 关于我们 */
|
||||||
|
UserViewsAbout: '/pages/user/views/about/index',
|
||||||
|
/** 协议与隐私 */
|
||||||
|
UserViewsAgreement: '/pages/user/views/agreement/index',
|
||||||
|
/** 联系客服 */
|
||||||
|
UserViewsContact: '/pages/user/views/contact/index',
|
||||||
|
/** 帮助与反馈 */
|
||||||
|
UserViewsFeedback: '/pages/user/views/feedback/index',
|
||||||
|
/** 微信提醒 */
|
||||||
|
UserViewsNotification: '/pages/user/views/notification/index',
|
||||||
|
/** 账号资料 */
|
||||||
|
UserViewsProfile: '/pages/user/views/profile/index',
|
||||||
|
/** 实名信息 */
|
||||||
|
UserViewsRealName: '/pages/user/views/realName/index',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouterPath
|
||||||
Vendored
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="vite-svg-loader" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@qiun/ucharts' {
|
||||||
|
export default class UCharts {
|
||||||
|
constructor(options: Record<string, any>)
|
||||||
|
updateData(options?: Record<string, any>): void
|
||||||
|
showToolTip(event: unknown, options?: Record<string, any>): void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
/** 网站标题,应用名称 */
|
||||||
|
readonly VITE_APP_TITLE: string
|
||||||
|
/** 服务端口号 */
|
||||||
|
readonly VITE_APP_PORT: string
|
||||||
|
/** 后台接口地址 */
|
||||||
|
readonly VITE_SERVER_BASEURL: string
|
||||||
|
/** 微信小程序后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||||
|
readonly VITE_MP_SERVER_BASEURL?: string
|
||||||
|
/** 后端是否有统一前缀 /api */
|
||||||
|
readonly VITE_SERVER_HAS_API_PREFIX: 'true' | 'false'
|
||||||
|
/** 认证模式,'single' | 'double' ==> 单token | 双token */
|
||||||
|
readonly VITE_AUTH_MODE: 'single' | 'double'
|
||||||
|
/** 是否清除console */
|
||||||
|
readonly VITE_DELETE_CONSOLE: string
|
||||||
|
// 更多环境变量...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
interface IUseRequestOptions<T> {
|
||||||
|
/** 是否立即执行 */
|
||||||
|
immediate?: boolean
|
||||||
|
/** 初始化数据 */
|
||||||
|
initialData?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IUseRequestReturn<T, P = undefined> {
|
||||||
|
loading: Ref<boolean>
|
||||||
|
error: Ref<boolean | Error>
|
||||||
|
data: Ref<T | undefined>
|
||||||
|
run: (args?: P) => Promise<T | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
|
||||||
|
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
|
||||||
|
* @param options 包含请求选项的对象 {immediate, initialData}。
|
||||||
|
* @param options.immediate 是否立即执行请求,默认为false。
|
||||||
|
* @param options.initialData 初始化数据,默认为undefined。
|
||||||
|
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
|
||||||
|
*/
|
||||||
|
export default function useRequest<T, P = undefined>(
|
||||||
|
func: (args?: P) => Promise<T>,
|
||||||
|
options: IUseRequestOptions<T> = { immediate: false },
|
||||||
|
): IUseRequestReturn<T, P> {
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref(false)
|
||||||
|
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
|
||||||
|
const run = async (args?: P) => {
|
||||||
|
loading.value = true
|
||||||
|
return func(args)
|
||||||
|
.then((res) => {
|
||||||
|
data.value = res
|
||||||
|
error.value = false
|
||||||
|
return data.value
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error.value = err
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.immediate) {
|
||||||
|
(run as (args: P) => Promise<T | undefined>)({} as P)
|
||||||
|
}
|
||||||
|
return { loading, error, data, run }
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# 上拉刷新和下拉加载更多
|
||||||
|
|
||||||
|
在 unibest 框架中,我们通过组合 `useScroll` Hook 可结合 `scroll-view` 组件来轻松实现上拉刷新和下拉加载更多的功能。
|
||||||
|
场景一 页面滚动
|
||||||
|
|
||||||
|
```
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '上拉刷新和下拉加载更多',
|
||||||
|
enablePullDownRefresh: true,
|
||||||
|
onReachBottomDistance: 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
场景二 局部滚动 结合 `scroll-view`
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
|
||||||
|
- `src/hooks/useScroll.ts`: 提供了核心的滚动逻辑处理 Hook。
|
||||||
|
- `src/pages-sub/demo/scroll.vue`: 一个具体的实现示例页面。
|
||||||
|
|
||||||
|
## `useScroll` Hook
|
||||||
|
|
||||||
|
`useScroll` 是一个 Vue Composition API Hook,它封装了处理下拉刷新和上拉加载的通用逻辑。
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
|
||||||
|
- **管理加载状态**: 自动处理 `loading`(加载中)、`finished`(已加载全部)和 `error`(加载失败)等状态。
|
||||||
|
- **分页逻辑**: 内部维护分页参数(页码 `page` 和每页数量 `pageSize`)。
|
||||||
|
- **事件处理**: 提供 `onScrollToLower`(滚动到底部)、`onRefresherRefresh`(下拉刷新)等方法,用于在视图层触发。
|
||||||
|
- **数据合并**: 自动将新加载的数据追加到现有列表 `list` 中。
|
||||||
|
|
||||||
|
### 使用方法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { useScroll } from '@/hooks/useScroll'
|
||||||
|
import { getList } from '@/service/list' // 你的数据请求API
|
||||||
|
|
||||||
|
const {
|
||||||
|
list, // 响应式的数据列表
|
||||||
|
loading, // 是否加载中
|
||||||
|
finished, // 是否已全部加载
|
||||||
|
error, // 是否加载失败
|
||||||
|
onScrollToLower, // 滚动到底部时触发的事件
|
||||||
|
onRefresherRefresh, // 下拉刷新时触发的事件
|
||||||
|
} = useScroll(getList) // 将获取数据的API函数传入
|
||||||
|
```
|
||||||
|
|
||||||
|
## `scroll-view` 组件
|
||||||
|
|
||||||
|
`scroll-view` 是 uni-app 提供的可滚动视图区域组件,它提供了一系列属性来支持下拉刷新和上拉加载。
|
||||||
|
|
||||||
|
### 关键属性
|
||||||
|
|
||||||
|
- `scroll-y`: 允许纵向滚动。
|
||||||
|
- `refresher-enabled`: 启用下拉刷新。
|
||||||
|
- `refresher-triggered`: 控制下拉刷新动画的显示与隐藏,通过 `loading` 状态绑定。
|
||||||
|
- `@scrolltolower`: 滚动到底部时触发的事件,绑定 `onScrollToLower` 方法。
|
||||||
|
- `@refresherrefresh`: 触发下拉刷新时触发的事件,绑定 `onRefresherRefresh` 方法。
|
||||||
|
|
||||||
|
## 示例代码
|
||||||
|
|
||||||
|
以下是 `src/pages-sub/demo/scroll.vue` 中的核心代码,展示了如何将 `useScroll` 和 `scroll-view` 结合使用。
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<view class="scroll-page">
|
||||||
|
<scroll-view
|
||||||
|
class="scroll-view"
|
||||||
|
scroll-y
|
||||||
|
:refresher-enabled="true"
|
||||||
|
:refresher-triggered="loading"
|
||||||
|
@scrolltolower="onScrollToLower"
|
||||||
|
@refresherrefresh="onRefresherRefresh"
|
||||||
|
>
|
||||||
|
<view v-for="item in list" :key="item.id" class="scroll-item">
|
||||||
|
{{ item.name }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态提示 -->
|
||||||
|
<view v-if="loading" class="loading-tip">加载中...</view>
|
||||||
|
<view v-if="finished" class="finished-tip">没有更多了</view>
|
||||||
|
<view v-if="error" class="error-tip">加载失败,请重试</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useScroll } from '@/hooks/useScroll'
|
||||||
|
import { getList } from '@/service/list'
|
||||||
|
|
||||||
|
const { list, loading, finished, error, onScrollToLower, onRefresherRefresh } = useScroll(getList)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 样式省略 */
|
||||||
|
.scroll-page, .scroll-view {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现步骤总结
|
||||||
|
|
||||||
|
1. **创建API**: 确保你有一个返回分页数据的API请求函数(例如 `getList`),它应该接受页码和页面大小作为参数。
|
||||||
|
2. **调用 `useScroll`**: 在你的页面脚本中,导入并调用 `useScroll` Hook,将你的API函数作为参数传入。
|
||||||
|
3. **模板绑定**:
|
||||||
|
- 使用 `scroll-view` 组件作为滚动容器。
|
||||||
|
- 将其 `refresher-triggered` 属性绑定到 `useScroll` 返回的 `loading` 状态。
|
||||||
|
- 将其 `@scrolltolower` 事件绑定到 `onScrollToLower` 方法。
|
||||||
|
- 将其 `@refresherrefresh` 事件绑定到 `onRefresherRefresh` 方法。
|
||||||
|
4. **渲染列表**: 使用 `v-for` 指令渲染 `useScroll` 返回的 `list` 数组。
|
||||||
|
5. **添加加载提示**: 根据 `loading`, `finished`, `error` 状态,在列表底部显示不同的提示信息,提升用户体验。
|
||||||
|
|
||||||
|
通过以上步骤,你就可以在项目中快速集成一个功能完善、体验良好的上拉刷新和下拉加载列表。
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import type { Ref } from 'vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
interface UseScrollOptions<T> {
|
||||||
|
fetchData: (page: number, pageSize: number) => Promise<T[]>
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseScrollReturn<T> {
|
||||||
|
list: Ref<T[]>
|
||||||
|
loading: Ref<boolean>
|
||||||
|
finished: Ref<boolean>
|
||||||
|
error: Ref<any>
|
||||||
|
refresh: () => Promise<void>
|
||||||
|
loadMore: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScroll<T>({
|
||||||
|
fetchData,
|
||||||
|
pageSize = 10,
|
||||||
|
}: UseScrollOptions<T>): UseScrollReturn<T> {
|
||||||
|
const list = ref<T[]>([]) as Ref<T[]>
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const error = ref<any>(null)
|
||||||
|
const page = ref(1)
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
if (loading.value || finished.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchData(page.value, pageSize)
|
||||||
|
if (data.length < pageSize) {
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
list.value.push(...data)
|
||||||
|
page.value++
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
error.value = err
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
page.value = 1
|
||||||
|
finished.value = false
|
||||||
|
list.value = []
|
||||||
|
await loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
await loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
list,
|
||||||
|
loading,
|
||||||
|
finished,
|
||||||
|
error,
|
||||||
|
refresh,
|
||||||
|
loadMore,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { getEnvBaseUrl } from '@/utils/index'
|
||||||
|
|
||||||
|
const VITE_UPLOAD_BASEURL = `${getEnvBaseUrl()}/upload`
|
||||||
|
|
||||||
|
type TfileType = 'image' | 'file'
|
||||||
|
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
|
||||||
|
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
|
||||||
|
|
||||||
|
interface TOptions<T extends TfileType> {
|
||||||
|
formData?: Record<string, any>
|
||||||
|
maxSize?: number
|
||||||
|
accept?: T extends 'image' ? TImage[] : TFile[]
|
||||||
|
fileType?: T
|
||||||
|
success?: (params: any) => void
|
||||||
|
error?: (err: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
|
||||||
|
const {
|
||||||
|
formData = {},
|
||||||
|
maxSize = 5 * 1024 * 1024,
|
||||||
|
accept = ['*'],
|
||||||
|
fileType = 'image',
|
||||||
|
success,
|
||||||
|
error: onError,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref<Error | null>(null)
|
||||||
|
const data = ref<any>(null)
|
||||||
|
|
||||||
|
const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => {
|
||||||
|
if (size > maxSize) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase()
|
||||||
|
// const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension)
|
||||||
|
|
||||||
|
// if (!isTypeValid) {
|
||||||
|
// uni.showToast({
|
||||||
|
// title: `仅支持 ${accept.join(', ')} 格式的文件`,
|
||||||
|
// icon: 'none',
|
||||||
|
// })
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
uploadFile({
|
||||||
|
tempFilePath,
|
||||||
|
formData,
|
||||||
|
onSuccess: (res) => {
|
||||||
|
// 修改这里的解析逻辑,适应不同平台的返回格式
|
||||||
|
let parsedData = res
|
||||||
|
try {
|
||||||
|
// 尝试解析为JSON
|
||||||
|
const jsonData = JSON.parse(res)
|
||||||
|
// 检查是否包含data字段
|
||||||
|
parsedData = jsonData.data || jsonData
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// 如果解析失败,使用原始数据
|
||||||
|
console.log('Response is not JSON, using raw data:', res)
|
||||||
|
}
|
||||||
|
data.value = parsedData
|
||||||
|
// console.log('上传成功', res)
|
||||||
|
success?.(parsedData)
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
error.value = err
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
loading.value = false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = () => {
|
||||||
|
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
|
||||||
|
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
|
||||||
|
const chooseFileOptions = {
|
||||||
|
count: 1,
|
||||||
|
success: (res: any) => {
|
||||||
|
console.log('File selected successfully:', res)
|
||||||
|
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
|
||||||
|
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
|
||||||
|
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
|
||||||
|
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
|
||||||
|
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
|
||||||
|
let tempFilePath = ''
|
||||||
|
let size = 0
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
tempFilePath = res.tempFiles[0].tempFilePath
|
||||||
|
size = res.tempFiles[0].size
|
||||||
|
// #endif
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
tempFilePath = res.tempFilePaths[0]
|
||||||
|
size = res.tempFiles[0].size
|
||||||
|
// #endif
|
||||||
|
handleFileChoose({ tempFilePath, size })
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
console.error('File selection failed:', err)
|
||||||
|
error.value = err
|
||||||
|
onError?.(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType === 'image') {
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.chooseMedia({
|
||||||
|
...chooseFileOptions,
|
||||||
|
mediaType: ['image'],
|
||||||
|
})
|
||||||
|
// #endif
|
||||||
|
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.chooseImage(chooseFileOptions)
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.chooseFile({
|
||||||
|
...chooseFileOptions,
|
||||||
|
type: 'all',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loading, error, data, run }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile({
|
||||||
|
tempFilePath,
|
||||||
|
formData,
|
||||||
|
onSuccess,
|
||||||
|
onError,
|
||||||
|
onComplete,
|
||||||
|
}: {
|
||||||
|
tempFilePath: string
|
||||||
|
formData: Record<string, any>
|
||||||
|
onSuccess: (data: any) => void
|
||||||
|
onError: (err: any) => void
|
||||||
|
onComplete: () => void
|
||||||
|
}) {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: VITE_UPLOAD_BASEURL,
|
||||||
|
filePath: tempFilePath,
|
||||||
|
name: 'file',
|
||||||
|
formData,
|
||||||
|
success: (uploadFileRes) => {
|
||||||
|
try {
|
||||||
|
const data = uploadFileRes.data
|
||||||
|
onSuccess(data)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
onError(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err) => {
|
||||||
|
console.error('Upload failed:', err)
|
||||||
|
onError(err)
|
||||||
|
},
|
||||||
|
complete: onComplete,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# 请求库
|
||||||
|
|
||||||
|
目前unibest支持3种请求库:
|
||||||
|
- 菲鸽简单封装的 `简单版本http`,路径(src/http/http.ts),对应的示例在 src/api/foo.ts
|
||||||
|
- `alova 的 http`,路径(src/http/alova.ts),对应的示例在 src/api/foo-alova.ts
|
||||||
|
- `vue-query`, 路径(src/http/vue-query.ts), 目前主要用在自动生成接口,详情看(https://unibest.tech/base/17-generate),示例在 src/service/app 文件夹
|
||||||
|
|
||||||
|
## 如何选择
|
||||||
|
如果您以前用过 alova 或者 vue-query,可以优先使用您熟悉的。
|
||||||
|
如果您的项目简单,简单版本的http 就够了,也不会增加包体积。(发版的时候可以去掉alova和vue-query,如果没有超过包体积,留着也无所谓 ^_^)
|
||||||
|
|
||||||
|
## roadmap
|
||||||
|
菲鸽最近在优化脚手架,后续可以选择是否使用第三方的请求库,以及选择什么请求库。还在开发中,大概月底出来(8月31号)。
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import type { uniappRequestAdapter } from '@alova/adapter-uniapp'
|
||||||
|
import type { IResponse } from './types'
|
||||||
|
import AdapterUniapp from '@alova/adapter-uniapp'
|
||||||
|
import { createAlova } from 'alova'
|
||||||
|
import { createServerTokenAuthentication } from 'alova/client'
|
||||||
|
import VueHook from 'alova/vue'
|
||||||
|
import { toLoginPage } from '@/utils/toLoginPage'
|
||||||
|
import { getEnvBaseUrl } from '@/utils'
|
||||||
|
import { ContentTypeEnum, getHttpErrorMessage, REQUEST_ERROR_MESSAGE, ResultEnum, ShowMessage } from './tools/enum'
|
||||||
|
|
||||||
|
// 配置动态Tag
|
||||||
|
export const API_DOMAINS = {
|
||||||
|
DEFAULT: getEnvBaseUrl(),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建请求实例
|
||||||
|
*/
|
||||||
|
const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
|
||||||
|
typeof VueHook,
|
||||||
|
typeof uniappRequestAdapter
|
||||||
|
>({
|
||||||
|
// 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
|
||||||
|
refreshTokenOnError: {
|
||||||
|
isExpired: (error) => {
|
||||||
|
return error.response?.status === ResultEnum.Unauthorized
|
||||||
|
},
|
||||||
|
handler: async () => {
|
||||||
|
try {
|
||||||
|
// await authLogin();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
// 切换到登录页
|
||||||
|
toLoginPage({ mode: 'reLaunch' })
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* alova 请求实例
|
||||||
|
*/
|
||||||
|
const alovaInstance = createAlova({
|
||||||
|
baseURL: API_DOMAINS.DEFAULT,
|
||||||
|
...AdapterUniapp(),
|
||||||
|
timeout: 5000,
|
||||||
|
statesHook: VueHook,
|
||||||
|
|
||||||
|
beforeRequest: onAuthRequired((method) => {
|
||||||
|
// 设置默认 Content-Type
|
||||||
|
method.config.headers = {
|
||||||
|
ContentType: ContentTypeEnum.JSON,
|
||||||
|
Accept: 'application/json, text/plain, */*',
|
||||||
|
...method.config.headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { config } = method
|
||||||
|
const ignoreAuth = !config.meta?.ignoreAuth
|
||||||
|
console.log('ignoreAuth===>', ignoreAuth)
|
||||||
|
// 处理认证信息 自行处理认证问题
|
||||||
|
if (ignoreAuth) {
|
||||||
|
const token = 'getToken()'
|
||||||
|
if (!token) {
|
||||||
|
throw new Error('[请求错误]:未登录')
|
||||||
|
}
|
||||||
|
// method.config.headers.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理动态域名
|
||||||
|
if (config.meta?.domain) {
|
||||||
|
method.baseURL = config.meta.domain
|
||||||
|
console.log('当前域名', method.baseURL)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
responded: onResponseRefreshToken((response, method) => {
|
||||||
|
const { config } = method
|
||||||
|
const { requestType } = config
|
||||||
|
const {
|
||||||
|
statusCode,
|
||||||
|
data: rawData,
|
||||||
|
errMsg,
|
||||||
|
} = response as UniNamespace.RequestSuccessCallbackResult
|
||||||
|
|
||||||
|
// 处理特殊请求类型(上传/下载)
|
||||||
|
if (requestType === 'upload' || requestType === 'download') {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 HTTP 状态码错误
|
||||||
|
if (statusCode !== 200) {
|
||||||
|
const errorMessage = ShowMessage(statusCode) || REQUEST_ERROR_MESSAGE
|
||||||
|
console.error('errorMessage===>', errorMessage)
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMessage,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
console.error('[ALOVA REQUEST ERROR]', { statusCode, errMsg, url: method.url })
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理业务逻辑错误
|
||||||
|
const { code, message, data } = rawData as IResponse
|
||||||
|
// 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
|
||||||
|
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||||
|
const errorMessage = getHttpErrorMessage(rawData, REQUEST_ERROR_MESSAGE)
|
||||||
|
if (config.meta?.toast !== false) {
|
||||||
|
uni.showToast({
|
||||||
|
title: errorMessage,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.error('[ALOVA BUSINESS ERROR]', { code, message, url: method.url })
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
// 处理成功响应,返回业务数据
|
||||||
|
return data
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const http = alovaInstance
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export {
|
||||||
|
getHttpErrorMessage,
|
||||||
|
NETWORK_ERROR_MESSAGE,
|
||||||
|
normalizeErrorText,
|
||||||
|
REQUEST_ERROR_MESSAGE,
|
||||||
|
toUserRequestError,
|
||||||
|
} from './tools/enum'
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import type { IDoubleTokenRes } from '@/api/types/login'
|
||||||
|
import type { CustomRequestOptions, IResponse } from '@/http/types'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { isDoubleTokenMode } from '@/utils'
|
||||||
|
import { toLoginPage } from '@/utils/toLoginPage'
|
||||||
|
import { getHttpErrorMessage, NETWORK_ERROR_MESSAGE, REQUEST_ERROR_MESSAGE, toUserRequestError } from './error'
|
||||||
|
import { ResultEnum } from './tools/enum'
|
||||||
|
|
||||||
|
let refreshing = false
|
||||||
|
let taskQueue: (() => void)[] = []
|
||||||
|
const DEFAULT_REQUEST_TIMEOUT = 30000
|
||||||
|
const REQUEST_TIMEOUT_BUFFER = 1000
|
||||||
|
|
||||||
|
function resolveRequestTimeout(timeout?: number) {
|
||||||
|
const value = Number(timeout)
|
||||||
|
return Number.isFinite(value) && value > 0 ? value : DEFAULT_REQUEST_TIMEOUT
|
||||||
|
}
|
||||||
|
|
||||||
|
function showErrorToast(options: CustomRequestOptions, message: string) {
|
||||||
|
if (options.hideErrorToast)
|
||||||
|
return
|
||||||
|
uni.showToast({
|
||||||
|
icon: 'none',
|
||||||
|
title: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRequestFailDebug(options: CustomRequestOptions, err: UniApp.GeneralCallbackResult) {
|
||||||
|
console.error('[APP REQUEST FAIL]', {
|
||||||
|
url: options.url,
|
||||||
|
method: options.method,
|
||||||
|
errMsg: err.errMsg,
|
||||||
|
err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function http<T>(options: CustomRequestOptions) {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
let settled = false
|
||||||
|
let requestTask: UniApp.RequestTask | undefined
|
||||||
|
let timeoutTimer: ReturnType<typeof setTimeout> | undefined
|
||||||
|
const timeout = resolveRequestTimeout(options.timeout)
|
||||||
|
|
||||||
|
function clearRequestTimeout() {
|
||||||
|
if (timeoutTimer) {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
timeoutTimer = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishResolve(value: T | PromiseLike<T>) {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
settled = true
|
||||||
|
clearRequestTimeout()
|
||||||
|
resolve(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function finishReject(error: unknown) {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
settled = true
|
||||||
|
clearRequestTimeout()
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOptions: CustomRequestOptions = {
|
||||||
|
...options,
|
||||||
|
timeout,
|
||||||
|
dataType: 'json',
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
responseType: 'json',
|
||||||
|
// #endif
|
||||||
|
success: async (res) => {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
try {
|
||||||
|
const responseData = res.data as IResponse<T>
|
||||||
|
const code = Number((responseData as any)?.code)
|
||||||
|
const isTokenExpired = res.statusCode === 401 || code === 401
|
||||||
|
|
||||||
|
if (isTokenExpired) {
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
if (!isDoubleTokenMode) {
|
||||||
|
await tokenStore.logout()
|
||||||
|
toLoginPage()
|
||||||
|
return finishReject(responseData || res)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { refreshToken } = (tokenStore.tokenInfo as IDoubleTokenRes) || {}
|
||||||
|
if (refreshToken) {
|
||||||
|
taskQueue.push(() => {
|
||||||
|
finishResolve(http<T>(options))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshToken && !refreshing) {
|
||||||
|
refreshing = true
|
||||||
|
try {
|
||||||
|
await tokenStore.refreshToken()
|
||||||
|
refreshing = false
|
||||||
|
nextTick(() => {
|
||||||
|
uni.hideToast()
|
||||||
|
})
|
||||||
|
taskQueue.forEach(task => task())
|
||||||
|
}
|
||||||
|
catch (refreshErr) {
|
||||||
|
console.error('刷新 token 失败:', refreshErr)
|
||||||
|
refreshing = false
|
||||||
|
showErrorToast(options, '登录已过期,请重新登录')
|
||||||
|
await tokenStore.logout()
|
||||||
|
setTimeout(() => {
|
||||||
|
toLoginPage()
|
||||||
|
}, 1200)
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
taskQueue = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return finishReject(responseData || res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
|
||||||
|
const message = getHttpErrorMessage(responseData, REQUEST_ERROR_MESSAGE)
|
||||||
|
showErrorToast(options, message)
|
||||||
|
return finishReject({ ...responseData, msg: message })
|
||||||
|
}
|
||||||
|
return finishResolve(responseData.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = getHttpErrorMessage(res.data, REQUEST_ERROR_MESSAGE, res.statusCode)
|
||||||
|
showErrorToast(options, message)
|
||||||
|
finishReject({ ...res, msg: message, message, errMsg: message })
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = getHttpErrorMessage(error, REQUEST_ERROR_MESSAGE)
|
||||||
|
showErrorToast(options, message)
|
||||||
|
finishReject(toUserRequestError(error, message))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail(err) {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
showRequestFailDebug(requestOptions, err)
|
||||||
|
const message = getHttpErrorMessage(err, NETWORK_ERROR_MESSAGE)
|
||||||
|
showErrorToast(requestOptions, message)
|
||||||
|
finishReject(toUserRequestError(err, message))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutTimer = setTimeout(() => {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
settled = true
|
||||||
|
clearRequestTimeout()
|
||||||
|
const error = { errMsg: `request:fail timeout after ${timeout}ms` }
|
||||||
|
requestTask?.abort?.()
|
||||||
|
showRequestFailDebug(requestOptions, error)
|
||||||
|
showErrorToast(requestOptions, NETWORK_ERROR_MESSAGE)
|
||||||
|
reject(toUserRequestError(error, NETWORK_ERROR_MESSAGE))
|
||||||
|
}, timeout + REQUEST_TIMEOUT_BUFFER)
|
||||||
|
|
||||||
|
try {
|
||||||
|
requestTask = uni.request(requestOptions) as unknown as UniApp.RequestTask
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = getHttpErrorMessage(error, NETWORK_ERROR_MESSAGE)
|
||||||
|
showErrorToast(requestOptions, message)
|
||||||
|
finishReject(toUserRequestError(error, message))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
query,
|
||||||
|
method: 'GET',
|
||||||
|
header,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
query,
|
||||||
|
data,
|
||||||
|
method: 'POST',
|
||||||
|
header,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
query,
|
||||||
|
method: 'PUT',
|
||||||
|
header,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function httpDelete<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
|
||||||
|
return http<T>({
|
||||||
|
url,
|
||||||
|
query,
|
||||||
|
method: 'DELETE',
|
||||||
|
header,
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
http.get = httpGet
|
||||||
|
http.post = httpPost
|
||||||
|
http.put = httpPut
|
||||||
|
http.delete = httpDelete
|
||||||
|
|
||||||
|
http.Get = httpGet
|
||||||
|
http.Post = httpPost
|
||||||
|
http.Put = httpPut
|
||||||
|
http.Delete = httpDelete
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import type { CustomRequestOptions } from '@/http/types'
|
||||||
|
import { useTokenStore } from '@/store'
|
||||||
|
import { getEnvBaseUrl } from '@/utils'
|
||||||
|
import { stringifyQuery } from './tools/queryString'
|
||||||
|
|
||||||
|
// 请求基准地址
|
||||||
|
const baseUrl = getEnvBaseUrl()
|
||||||
|
|
||||||
|
// 拦截器配置
|
||||||
|
const httpInterceptor = {
|
||||||
|
// 拦截前触发
|
||||||
|
invoke(options: CustomRequestOptions) {
|
||||||
|
// 如果您使用了alova,则请把下面的代码放开注释
|
||||||
|
// alova 执行流程:alova beforeRequest --> 本拦截器 --> alova responded
|
||||||
|
// return options
|
||||||
|
|
||||||
|
// 非 alova 请求,正常执行
|
||||||
|
// 接口请求支持通过 query 参数配置 queryString
|
||||||
|
if (options.query) {
|
||||||
|
const queryStr = stringifyQuery(options.query)
|
||||||
|
if (options.url.includes('?')) {
|
||||||
|
options.url += `&${queryStr}`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
options.url += `?${queryStr}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 非 http 开头需拼接地址
|
||||||
|
if (!options.url.startsWith('http')) {
|
||||||
|
options.url = baseUrl + options.url
|
||||||
|
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
|
||||||
|
}
|
||||||
|
// 1. 请求超时:保留业务请求显式设置的超时时间。
|
||||||
|
if (!options.timeout || options.timeout <= 0)
|
||||||
|
options.timeout = 60000
|
||||||
|
// 2. (可选)添加小程序端请求头标识
|
||||||
|
options.header = {
|
||||||
|
...options.header,
|
||||||
|
}
|
||||||
|
// 3. 添加 token 请求头标识
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
const token = tokenStore.updateNowTime().validToken
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
options.header.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestInterceptor = {
|
||||||
|
install() {
|
||||||
|
// 拦截 request 请求
|
||||||
|
uni.addInterceptor('request', httpInterceptor)
|
||||||
|
// 拦截 uploadFile 文件上传
|
||||||
|
uni.addInterceptor('uploadFile', httpInterceptor)
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
export enum ResultEnum {
|
||||||
|
// 0和200当做成功都很普遍,这里直接兼容两者(PS:0和200通常都不会当做错误码,但是有的接口会返回0,有的接口会返回200)
|
||||||
|
Success0 = 0, // 成功
|
||||||
|
Success200 = 200, // 成功
|
||||||
|
Error = 400, // 错误
|
||||||
|
Unauthorized = 401, // 未授权
|
||||||
|
Forbidden = 403, // 禁止访问(原为forbidden)
|
||||||
|
NotFound = 404, // 未找到(原为notFound)
|
||||||
|
MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed)
|
||||||
|
RequestTimeout = 408, // 请求超时(原为requestTimeout)
|
||||||
|
InternalServerError = 500, // 服务器错误(原为internalServerError)
|
||||||
|
NotImplemented = 501, // 未实现(原为notImplemented)
|
||||||
|
BadGateway = 502, // 网关错误(原为badGateway)
|
||||||
|
ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable)
|
||||||
|
GatewayTimeout = 504, // 网关超时(原为gatewayTimeout)
|
||||||
|
HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported)
|
||||||
|
}
|
||||||
|
export enum ContentTypeEnum {
|
||||||
|
JSON = 'application/json;charset=UTF-8',
|
||||||
|
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NETWORK_ERROR_MESSAGE = '网络异常,请检查网络后重试'
|
||||||
|
export const REQUEST_ERROR_MESSAGE = '请求处理失败,请稍后重试'
|
||||||
|
|
||||||
|
const networkErrorPattern
|
||||||
|
= /request:fail|timeout|failed to fetch|networkerror|err_connection|econnrefused|econnreset|econnaborted|enotfound|net::|socket|connect|refused|abort|not in domain list|url not in domain list|ssl|tls|certificate|网络|超时/i
|
||||||
|
|
||||||
|
const technicalDetailPattern
|
||||||
|
= /https?:\/\/|\/api\/|接口[::]|errMsg|statusCode|StatusCode|request:|XMLHttpRequest|TypeError:|Error:/i
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据状态码,生成对应的错误信息
|
||||||
|
* @param {number|string} status 状态码
|
||||||
|
* @returns {string} 错误信息
|
||||||
|
*/
|
||||||
|
export function ShowMessage(status: number | string): string {
|
||||||
|
let message: string
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
message = '请求错误(400)'
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
message = '未授权,请重新登录(401)'
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
message = '拒绝访问(403)'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
message = '请求出错(404)'
|
||||||
|
break
|
||||||
|
case 408:
|
||||||
|
message = '请求超时(408)'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
message = '服务器错误(500)'
|
||||||
|
break
|
||||||
|
case 501:
|
||||||
|
message = '服务未实现(501)'
|
||||||
|
break
|
||||||
|
case 502:
|
||||||
|
message = '网络错误(502)'
|
||||||
|
break
|
||||||
|
case 503:
|
||||||
|
message = '服务不可用(503)'
|
||||||
|
break
|
||||||
|
case 504:
|
||||||
|
message = '网络超时(504)'
|
||||||
|
break
|
||||||
|
case 505:
|
||||||
|
message = 'HTTP版本不受支持(505)'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
message = `连接出错(${status})!`
|
||||||
|
}
|
||||||
|
return `${message},请检查网络或联系管理员!`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeErrorText(value: unknown, fallback = REQUEST_ERROR_MESSAGE) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
const message = value.trim().replace(/^Error:\s*/i, '')
|
||||||
|
if (!message) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
if (/^\d+$/.test(message)) {
|
||||||
|
return ShowMessage(Number(message))
|
||||||
|
}
|
||||||
|
if (networkErrorPattern.test(message)) {
|
||||||
|
return NETWORK_ERROR_MESSAGE
|
||||||
|
}
|
||||||
|
if (technicalDetailPattern.test(message)) {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHttpErrorMessage(data: any, fallback = REQUEST_ERROR_MESSAGE, statusCode?: number) {
|
||||||
|
if (data instanceof Error) {
|
||||||
|
return normalizeErrorText(data.message, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
const directMessage = data?.msg || data?.message || data?.data?.msg || data?.data?.message
|
||||||
|
if (directMessage) {
|
||||||
|
return normalizeErrorText(directMessage, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawErrMsg = data?.errMsg
|
||||||
|
if (rawErrMsg) {
|
||||||
|
return normalizeErrorText(rawErrMsg, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = Number(data?.code || statusCode || data?.statusCode)
|
||||||
|
if (Number.isFinite(code) && code >= 400) {
|
||||||
|
return ShowMessage(code)
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toUserRequestError(error: unknown, message: string) {
|
||||||
|
return {
|
||||||
|
rawError: error,
|
||||||
|
msg: message,
|
||||||
|
message,
|
||||||
|
errMsg: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
|
||||||
|
* 支持基本类型值和数组,不支持嵌套对象
|
||||||
|
* @param obj 要序列化的对象
|
||||||
|
* @returns 序列化后的查询字符串
|
||||||
|
*/
|
||||||
|
export function stringifyQuery(obj: Record<string, any>): string {
|
||||||
|
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
|
||||||
|
return ''
|
||||||
|
|
||||||
|
return Object.entries(obj)
|
||||||
|
.filter(([_, value]) => value !== undefined && value !== null)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
// 对键进行编码
|
||||||
|
const encodedKey = encodeURIComponent(key)
|
||||||
|
|
||||||
|
// 处理数组类型
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
.filter(item => item !== undefined && item !== null)
|
||||||
|
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理基本类型
|
||||||
|
return `${encodedKey}=${encodeURIComponent(value)}`
|
||||||
|
})
|
||||||
|
.join('&')
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 在 uniapp 的 RequestOptions 和 IUniUploadFileOptions 基础上,添加自定义参数
|
||||||
|
*/
|
||||||
|
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||||
|
query?: Record<string, any>
|
||||||
|
/** 出错时是否隐藏错误提示 */
|
||||||
|
hideErrorToast?: boolean
|
||||||
|
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
|
||||||
|
|
||||||
|
/** 主要提供给 openapi-ts-request 生成的代码使用 */
|
||||||
|
export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
|
||||||
|
|
||||||
|
export interface HttpRequestResult<T> {
|
||||||
|
promise: Promise<T>
|
||||||
|
requestTask: UniApp.RequestTask
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用响应格式(兼容 msg + message 字段)
|
||||||
|
export type IResponse<T = any> = {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
message: string
|
||||||
|
[key: string]: any // 允许额外属性
|
||||||
|
} | {
|
||||||
|
code: number
|
||||||
|
data: T
|
||||||
|
msg: string
|
||||||
|
[key: string]: any // 允许额外属性
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页请求参数
|
||||||
|
export interface PageParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页响应数据
|
||||||
|
export interface PageResult<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import type { CustomRequestOptions } from '@/http/types'
|
||||||
|
import { http } from './http'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* openapi-ts-request 工具的 request 跨客户端适配方法
|
||||||
|
*/
|
||||||
|
export default function request<T extends { data?: any }>(
|
||||||
|
url: string,
|
||||||
|
options: Omit<CustomRequestOptions, 'url'> & {
|
||||||
|
params?: Record<string, unknown>
|
||||||
|
headers?: Record<string, unknown>
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const requestOptions = {
|
||||||
|
url,
|
||||||
|
...options,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.params) {
|
||||||
|
requestOptions.query = requestOptions.params
|
||||||
|
delete requestOptions.params
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.headers) {
|
||||||
|
requestOptions.header = options.headers
|
||||||
|
delete requestOptions.headers
|
||||||
|
}
|
||||||
|
|
||||||
|
return http<T['data']>(requestOptions)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="w-screen flex flex-1 flex-col">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<view class="layout-default w-screen flex flex-1 flex-col">
|
||||||
|
<slot />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# 注意事项
|
||||||
|
|
||||||
|
> 文件夹名字必须为 `locale`, 这是 `uniapp` 官方约定的,如果改为别的,标题将不能正常切换多语言(其他内容还是正常)。
|
||||||
|
>
|
||||||
|
> `xxx.json` 的 `xxx` 多语言标识必须与 `uniapp` 官方约定的一致,否则也会出现 BUG。
|
||||||
|
>
|
||||||
|
> 查看截图 `screenshots/i18n.png`。
|
||||||
|
|
||||||
|
## 参考文档
|
||||||
|
|
||||||
|
[uniapp 国际化开发指南](https://uniapp.dcloud.net.cn/tutorial/i18n.html)
|
||||||
|
[uniapp 国际化-注意事项](https://uniapp.dcloud.net.cn/api/ui/locale.html#onlocalechange) 最下面的注意事项
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tabbar.home": "Home",
|
||||||
|
"tabbar.about": "About",
|
||||||
|
"tabbar.me": "Me",
|
||||||
|
"tabbar.i18n": "I18n",
|
||||||
|
"i18n.title": "En Title",
|
||||||
|
"alova.title": "Alova Request",
|
||||||
|
"weight": "{heavy}KG",
|
||||||
|
"detail": "{0}cm, {1}KG",
|
||||||
|
"introduction": "I am {name},height:{detail.height},weight:{detail.weight}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import en from './en.json'
|
||||||
|
import zhHans from './zh-Hans.json' // 简体中文
|
||||||
|
|
||||||
|
const messages = {
|
||||||
|
en,
|
||||||
|
'zh-Hans': zhHans, // key 不能乱写,查看uniapp官网
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
locale: uni.getLocale(), // 获取已设置的语言,fallback 语言需要再 manifest.config.ts 中设置
|
||||||
|
messages,
|
||||||
|
allowComposition: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(uni.getLocale())
|
||||||
|
console.log(i18n.global.locale)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可以拿到原始的语言模板,非 vue 文件使用这个方法,
|
||||||
|
* @param { string } key 多语言的key,eg: "app.name"
|
||||||
|
* @returns {string} 返回原始的多语言模板,eg: "{heavy}KG"
|
||||||
|
*/
|
||||||
|
export function getTemplateByKey(key: string) {
|
||||||
|
if (!key) {
|
||||||
|
console.error(`[i18n] Function getTemplateByKey(), key param is required`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const locale = uni.getLocale()
|
||||||
|
console.log('locale:', locale)
|
||||||
|
|
||||||
|
const message = messages[locale] // 拿到某个多语言的所有模板(是一个对象)
|
||||||
|
if (Object.keys(message).includes(key)) {
|
||||||
|
return message[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyList = key.split('.')
|
||||||
|
return keyList.reduce((pre, cur) => {
|
||||||
|
return pre[cur]
|
||||||
|
}, message)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(`[i18n] Function getTemplateByKey(), key param ${key} is not existed.`)
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
|
||||||
|
* 暂不支持数组
|
||||||
|
* @param template 多语言模板字符串,eg: `我是{name}`
|
||||||
|
* @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}`
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function formatI18n(template: string, data?: any) {
|
||||||
|
return template.replace(/\{([^}]+)\}/g, (match, key: string) => {
|
||||||
|
// console.log( match, key) // => { detail.height } detail.height
|
||||||
|
const arr = key.trim().split('.')
|
||||||
|
let result = data
|
||||||
|
while (arr.length) {
|
||||||
|
const first = arr.shift()
|
||||||
|
result = result[first]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* t('introduction',{name:'张三',detail:{height:178,weight:'75kg'}})
|
||||||
|
* => formatI18n('我是{name},身高{detail.height},体重{detail.weight}',{name:'张三',detail:{height:178,weight:'75kg'}})
|
||||||
|
* 没有key的,可以不传 data;暂不支持数组
|
||||||
|
* @param template 多语言模板字符串,eg: `我是{name}`
|
||||||
|
* @param {object | undefined} data 需要传递的数据对象,里面的key与多语言字符串对应,eg: `{name:'菲鸽'}`
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function t(key, data?) {
|
||||||
|
return formatI18n(getTemplateByKey(key), data)
|
||||||
|
}
|
||||||
|
export default i18n
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tabbar.home": "首页",
|
||||||
|
"tabbar.about": "关于",
|
||||||
|
"tabbar.me": "我的",
|
||||||
|
"tabbar.i18n": "多语言",
|
||||||
|
"i18n.title": "中文标题",
|
||||||
|
"alova.title": "Alova 请求",
|
||||||
|
"weight": "{heavy}公斤",
|
||||||
|
"detail": "{0}cm, {1}公斤",
|
||||||
|
"introduction": "我是 {name},身高:{detail.height},体重:{detail.weight}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { requestInterceptor } from './http/interceptor'
|
||||||
|
import { routeInterceptor } from './router/interceptor'
|
||||||
|
|
||||||
|
import store from './store'
|
||||||
|
import '@/style/index.scss'
|
||||||
|
import 'go-captcha-uni/assets/css/gocaptcha.css'
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
import i18n from './locale/index'
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
app.use(store)
|
||||||
|
app.use(routeInterceptor)
|
||||||
|
app.use(requestInterceptor)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
{
|
||||||
|
"name": "山东平台物联卡",
|
||||||
|
"appid": "__UNI__YOURAPPID",
|
||||||
|
"description": "",
|
||||||
|
"versionName": "1.0.0",
|
||||||
|
"versionCode": "100",
|
||||||
|
"transformPx": false,
|
||||||
|
"app-plus": {
|
||||||
|
"usingComponents": true,
|
||||||
|
"nvueStyleCompiler": "uni-app",
|
||||||
|
"compilerVersion": 3,
|
||||||
|
"splashscreen": {
|
||||||
|
"alwaysShowBeforeRender": true,
|
||||||
|
"waiting": true,
|
||||||
|
"autoclose": true,
|
||||||
|
"delay": 0
|
||||||
|
},
|
||||||
|
"modules": {},
|
||||||
|
"distribute": {
|
||||||
|
"android": {
|
||||||
|
"permissions": [
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
|
||||||
|
"<uses-feature android:name=\"android.hardware.camera\"/>",
|
||||||
|
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
|
||||||
|
],
|
||||||
|
"minSdkVersion": 21,
|
||||||
|
"targetSdkVersion": 30,
|
||||||
|
"abiFilters": [
|
||||||
|
"armeabi-v7a",
|
||||||
|
"arm64-v8a"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ios": {},
|
||||||
|
"sdkConfigs": {},
|
||||||
|
"icons": {
|
||||||
|
"android": {
|
||||||
|
"hdpi": "static/app/icons/72x72.png",
|
||||||
|
"xhdpi": "static/app/icons/96x96.png",
|
||||||
|
"xxhdpi": "static/app/icons/144x144.png",
|
||||||
|
"xxxhdpi": "static/app/icons/192x192.png"
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"appstore": "static/app/icons/1024x1024.png",
|
||||||
|
"ipad": {
|
||||||
|
"app": "static/app/icons/76x76.png",
|
||||||
|
"app@2x": "static/app/icons/152x152.png",
|
||||||
|
"notification": "static/app/icons/20x20.png",
|
||||||
|
"notification@2x": "static/app/icons/40x40.png",
|
||||||
|
"proapp@2x": "static/app/icons/167x167.png",
|
||||||
|
"settings": "static/app/icons/29x29.png",
|
||||||
|
"settings@2x": "static/app/icons/58x58.png",
|
||||||
|
"spotlight": "static/app/icons/40x40.png",
|
||||||
|
"spotlight@2x": "static/app/icons/80x80.png"
|
||||||
|
},
|
||||||
|
"iphone": {
|
||||||
|
"app@2x": "static/app/icons/120x120.png",
|
||||||
|
"app@3x": "static/app/icons/180x180.png",
|
||||||
|
"notification@2x": "static/app/icons/40x40.png",
|
||||||
|
"notification@3x": "static/app/icons/60x60.png",
|
||||||
|
"settings@2x": "static/app/icons/58x58.png",
|
||||||
|
"settings@3x": "static/app/icons/87x87.png",
|
||||||
|
"spotlight@2x": "static/app/icons/80x80.png",
|
||||||
|
"spotlight@3x": "static/app/icons/120x120.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compatible": {
|
||||||
|
"ignoreVersion": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"app-harmony": {
|
||||||
|
"distribute": {}
|
||||||
|
},
|
||||||
|
"mp-harmony": {
|
||||||
|
"distribute": {}
|
||||||
|
},
|
||||||
|
"quickapp": {},
|
||||||
|
"mp-weixin": {
|
||||||
|
"appid": "YOUR_WX_APPID",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": false,
|
||||||
|
"es6": true,
|
||||||
|
"minified": true
|
||||||
|
},
|
||||||
|
"usingComponents": true,
|
||||||
|
"optimization": {
|
||||||
|
"subPackages": true
|
||||||
|
},
|
||||||
|
"mergeVirtualHostAttributes": true,
|
||||||
|
"styleIsolation": "shared",
|
||||||
|
"lazyCodeLoading": "requiredComponents"
|
||||||
|
},
|
||||||
|
"mp-alipay": {
|
||||||
|
"usingComponents": true,
|
||||||
|
"styleIsolation": "shared",
|
||||||
|
"optimization": {
|
||||||
|
"subPackages": true
|
||||||
|
},
|
||||||
|
"compileOptions": {
|
||||||
|
"globalObjectMode": "enable",
|
||||||
|
"transpile": {
|
||||||
|
"script": {
|
||||||
|
"ignore": [
|
||||||
|
"node_modules/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mp-baidu": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"mp-toutiao": {
|
||||||
|
"usingComponents": true
|
||||||
|
},
|
||||||
|
"uniStatistics": {
|
||||||
|
"enable": false
|
||||||
|
},
|
||||||
|
"vueVersion": "3",
|
||||||
|
"h5": {
|
||||||
|
"router": {
|
||||||
|
"base": "/app/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,345 @@
|
|||||||
|
{
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"navigationBarTitleText": "山东平台物联卡",
|
||||||
|
"navigationBarBackgroundColor": "#f8f8f8",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"backgroundColor": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^fg-(.*)": "@/components/fg-$1/fg-$1.vue",
|
||||||
|
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue",
|
||||||
|
"^wd-(.*)": "@wot-ui/ui/components/wd-$1/wd-$1.vue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
// #ifdef H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"path": "pages/home/index",
|
||||||
|
"type": "home",
|
||||||
|
"style": {
|
||||||
|
"navigationStyle": "default",
|
||||||
|
"navigationBarTitleText": "山东平台物联卡",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
},
|
||||||
|
"layout": false
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
{
|
||||||
|
"path": "pages/business/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "办理"
|
||||||
|
},
|
||||||
|
"layout": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/card/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "卡片"
|
||||||
|
},
|
||||||
|
"layout": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"type": "page",
|
||||||
|
"name": "Login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "山东平台物联卡",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"disableScroll": true
|
||||||
|
},
|
||||||
|
"layout": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/message/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "消息"
|
||||||
|
},
|
||||||
|
"layout": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/user/index",
|
||||||
|
"type": "page",
|
||||||
|
"excludeLoginPath": true,
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff",
|
||||||
|
"navigationBarTextStyle": "black"
|
||||||
|
},
|
||||||
|
"layout": "blank"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/forgot-password/index",
|
||||||
|
"type": "page",
|
||||||
|
"name": "LoginForgotPassword",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "找回密码"
|
||||||
|
},
|
||||||
|
"layout": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/register/index",
|
||||||
|
"type": "page",
|
||||||
|
"name": "LoginRegister",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "注册账号"
|
||||||
|
},
|
||||||
|
"layout": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "pages/business/views",
|
||||||
|
"pages": [
|
||||||
|
// GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"path": "addon/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "加油包"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "allocate/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "批量卡片划拨"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "balanceRecharge/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "余额充值"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "cardRenew/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "单卡套餐续费"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "change/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "套餐变更"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "customer/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "客户管理"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "customerDetail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "客户详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "diagnosis/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "卡片排查"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "order/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的订单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "renew/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "批量套餐续费"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "warning/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "预警配置"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "workOrder/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "工单列表"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "workOrderCreate/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "提交工单"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "workOrderDetail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "工单详情"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/card/views",
|
||||||
|
"pages": [
|
||||||
|
// GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"path": "detail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "卡片详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "management/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "全部设备"
|
||||||
|
},
|
||||||
|
"layout": "default"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "packageDetail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "卡片详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "warning/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "预警配置"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/message/views",
|
||||||
|
"pages": [
|
||||||
|
// GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"path": "cardDetail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "消息详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "systemDetail/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "消息详情"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"root": "pages/user/views",
|
||||||
|
"pages": [
|
||||||
|
// GENERATED BY UNI-PAGES, PLATFORM: H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"path": "about/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "关于我们"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "agreement/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "协议与隐私"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "contact/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "联系客服"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "feedback/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "帮助与反馈"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "notification/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "微信提醒"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "profile/index",
|
||||||
|
"type": "page",
|
||||||
|
"excludeLoginPath": true,
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "账号资料"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "realName/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "实名信息"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tabBar": {
|
||||||
|
"custom": true,
|
||||||
|
"color": "#94a3b8",
|
||||||
|
"selectedColor": "#2563eb",
|
||||||
|
"backgroundColor": "#F8F8F8",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"height": "50px",
|
||||||
|
"fontSize": "10px",
|
||||||
|
"iconWidth": "24px",
|
||||||
|
"spacing": "3px",
|
||||||
|
"list": [
|
||||||
|
// #ifdef H5 || MP-WEIXIN
|
||||||
|
{
|
||||||
|
"text": "首页",
|
||||||
|
"pagePath": "pages/home/index"
|
||||||
|
},
|
||||||
|
// #endif
|
||||||
|
{
|
||||||
|
"text": "设备",
|
||||||
|
"pagePath": "pages/card/index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "办理",
|
||||||
|
"pagePath": "pages/business/index"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "我的",
|
||||||
|
"pagePath": "pages/user/index"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/** ==================== 常量 ==================== */
|
||||||
|
const emit = defineEmits<{
|
||||||
|
action: [action: 'addon' | 'renew']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/** ==================== 数据 ==================== */
|
||||||
|
/** ==================== 方法 ==================== */
|
||||||
|
function handleAction(action: 'addon' | 'renew') {
|
||||||
|
emit('action', action)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ==================== 组件 ==================== */
|
||||||
|
/** ==================== 周期 ==================== */
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<wd-card custom-class="!mx-0 !mb-0" custom-content-class="!p-32rpx">
|
||||||
|
<view class="flex flex-col gap-24rpx">
|
||||||
|
<view class="flex flex-col gap-10rpx">
|
||||||
|
<text class="text-40rpx text-#111827 font-900 leading-48rpx">
|
||||||
|
常用业务
|
||||||
|
</text>
|
||||||
|
<text class="text-26rpx text-#64748b leading-38rpx">
|
||||||
|
办理套餐续费、加油包订购和卡片自助服务。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grid grid-cols-2 gap-16rpx">
|
||||||
|
<wd-button type="primary" block @click="handleAction('renew')">
|
||||||
|
套餐续费
|
||||||
|
</wd-button>
|
||||||
|
<wd-button type="primary" plain block @click="handleAction('addon')">
|
||||||
|
加油包
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</wd-card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { BusinessServiceItem, BusinessServiceSection, BusinessTone } from '../data'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
section: BusinessServiceSection
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
action: [item: BusinessServiceItem]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toneClass: Record<BusinessTone, string> = {
|
||||||
|
primary: 'tone-primary',
|
||||||
|
blue: 'tone-blue',
|
||||||
|
green: 'tone-green',
|
||||||
|
orange: 'tone-orange',
|
||||||
|
red: 'tone-red',
|
||||||
|
purple: 'tone-purple',
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(item: BusinessServiceItem) {
|
||||||
|
emit('action', item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function visibleRoleTag(item: BusinessServiceItem) {
|
||||||
|
if (item.agentOnly)
|
||||||
|
return '代理商'
|
||||||
|
if (item.terminalOnly)
|
||||||
|
return '用户'
|
||||||
|
if (item.terminalHidden)
|
||||||
|
return '企业'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="biz-section">
|
||||||
|
<text class="biz-group-title">{{ props.section.title }}</text>
|
||||||
|
|
||||||
|
<view class="biz-list">
|
||||||
|
<view
|
||||||
|
v-for="item in props.section.items"
|
||||||
|
:key="item.title"
|
||||||
|
class="biz-item"
|
||||||
|
@click="handleClick(item)"
|
||||||
|
>
|
||||||
|
<view class="biz-ico" :class="toneClass[item.tone]">
|
||||||
|
<view :class="[item.icon]" />
|
||||||
|
</view>
|
||||||
|
<view class="biz-mid">
|
||||||
|
<view class="biz-title-row">
|
||||||
|
<text class="biz-title">{{ item.title }}</text>
|
||||||
|
<view v-if="visibleRoleTag(item)" class="app-tag app-tag--info biz-role-tag">
|
||||||
|
{{ visibleRoleTag(item) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text class="biz-desc">{{ item.desc }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="i-carbon-chevron-right biz-chev" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.biz-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-group-title {
|
||||||
|
padding: 0 4rpx;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 23rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-list {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 6rpx 22rpx;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(17, 24, 39, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 18rpx;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-item:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-ico {
|
||||||
|
display: flex;
|
||||||
|
width: 76rpx;
|
||||||
|
height: 76rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-ico > view {
|
||||||
|
font-size: 38rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-mid {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-title-row {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-title {
|
||||||
|
min-width: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 34rpx;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-role-tag {
|
||||||
|
flex-shrink: 0;
|
||||||
|
max-width: 118rpx;
|
||||||
|
padding: 2rpx 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 20rpx;
|
||||||
|
line-height: 28rpx;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 21rpx;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.biz-chev {
|
||||||
|
color: #cbd5e1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-primary {
|
||||||
|
background: #eff6ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-primary > view {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-blue {
|
||||||
|
background: #e0f2fe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-blue > view {
|
||||||
|
color: #0369a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-green {
|
||||||
|
background: #dcfce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-green > view {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-orange {
|
||||||
|
background: #fef3c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-orange > view {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-red {
|
||||||
|
background: #fee2e2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-red > view {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-purple {
|
||||||
|
background: #e0e7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tone-purple > view {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { businessSections, getUserBusinessSections } from './data'
|
||||||
|
|
||||||
|
const businessDir = fileURLToPath(new URL('.', import.meta.url))
|
||||||
|
|
||||||
|
const expectedSubViews = [
|
||||||
|
'diagnosis',
|
||||||
|
'renew',
|
||||||
|
'allocate',
|
||||||
|
'change',
|
||||||
|
'addon',
|
||||||
|
'customer',
|
||||||
|
'customerDetail',
|
||||||
|
'order',
|
||||||
|
'warning',
|
||||||
|
'workOrder',
|
||||||
|
'workOrderCreate',
|
||||||
|
'workOrderDetail',
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('getUserBusinessSections', () => {
|
||||||
|
it('只保留用户侧一级业务入口', () => {
|
||||||
|
const sections = getUserBusinessSections(businessSections)
|
||||||
|
const titles = sections.flatMap(section => section.items.map(item => item.title))
|
||||||
|
|
||||||
|
expect(titles).toContain('套餐续费')
|
||||||
|
expect(titles).not.toContain('余额充值')
|
||||||
|
expect(titles).toContain('重启卡')
|
||||||
|
expect(titles).toContain('停机 / 复机')
|
||||||
|
expect(titles).toContain('加油包')
|
||||||
|
expect(titles).toContain('预警配置')
|
||||||
|
expect(titles).toContain('卡片排查')
|
||||||
|
expect(titles).toContain('我的卡片')
|
||||||
|
expect(titles).toContain('我的订单')
|
||||||
|
expect(titles).toContain('工单服务')
|
||||||
|
expect(titles).not.toContain('进入商城')
|
||||||
|
expect(titles).not.toContain('订单记录')
|
||||||
|
expect(titles).not.toContain('流量池预警(代理商独有)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('可办理业务都有明确动作', () => {
|
||||||
|
const sections = getUserBusinessSections(businessSections)
|
||||||
|
const actionableItems = sections
|
||||||
|
.flatMap(section => section.items)
|
||||||
|
.filter(item => item.action)
|
||||||
|
|
||||||
|
expect(actionableItems.map(item => [item.title, item.action])).toEqual([
|
||||||
|
['套餐续费', 'renew'],
|
||||||
|
['加油包', 'addon'],
|
||||||
|
['停机 / 复机', 'cardList'],
|
||||||
|
['重启卡', 'cardList'],
|
||||||
|
['卡片排查', 'diagnosis'],
|
||||||
|
['预警配置', 'warning'],
|
||||||
|
['我的订单', 'order'],
|
||||||
|
['工单服务', 'workOrder'],
|
||||||
|
['我的卡片', 'cardList'],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('卡片排查完全开放,不受业务权限过滤', () => {
|
||||||
|
const sections = getUserBusinessSections({
|
||||||
|
userId: 18,
|
||||||
|
deptId: 100,
|
||||||
|
userType: 'sys_user',
|
||||||
|
appAccess: {
|
||||||
|
configured: true,
|
||||||
|
permissions: [],
|
||||||
|
features: [],
|
||||||
|
routes: [],
|
||||||
|
menus: [],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
const titles = sections.flatMap(section => section.items.map(item => item.title))
|
||||||
|
|
||||||
|
expect(titles).toContain('卡片排查')
|
||||||
|
expect(titles).toContain('我的订单')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('企业用户显示卡片划拨、套餐变更和客户管理入口', () => {
|
||||||
|
const enterpriseUserTypes = ['sys_user', '']
|
||||||
|
enterpriseUserTypes.forEach((userType, index) => {
|
||||||
|
const sections = getUserBusinessSections({
|
||||||
|
userId: 10 + index,
|
||||||
|
deptId: 100,
|
||||||
|
userType,
|
||||||
|
} as any)
|
||||||
|
const titles = sections.flatMap(section => section.items.map(item => item.title))
|
||||||
|
|
||||||
|
expect(titles).toContain('卡片划拨')
|
||||||
|
expect(titles).toContain('套餐变更')
|
||||||
|
expect(titles).toContain('客户管理')
|
||||||
|
expect(titles).toContain('停机 / 复机')
|
||||||
|
expect(titles).toContain('重启卡')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('终端用户不显示卡片划拨、套餐变更和客户管理入口', () => {
|
||||||
|
const sections = getUserBusinessSections({
|
||||||
|
userId: 11,
|
||||||
|
deptId: 147,
|
||||||
|
userType: 'app_user',
|
||||||
|
} as any)
|
||||||
|
const titles = sections.flatMap(section => section.items.map(item => item.title))
|
||||||
|
|
||||||
|
expect(titles).not.toContain('卡片划拨')
|
||||||
|
expect(titles).not.toContain('套餐变更')
|
||||||
|
expect(titles).not.toContain('客户管理')
|
||||||
|
expect(titles).toContain('停机 / 复机')
|
||||||
|
expect(titles).toContain('重启卡')
|
||||||
|
expect(titles).toContain('加油包')
|
||||||
|
expect(titles).toContain('预警配置')
|
||||||
|
expect(titles).toContain('卡片排查')
|
||||||
|
expect(titles).toContain('我的卡片')
|
||||||
|
expect(titles).toContain('工单服务')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('配置权限时只展示已授权业务入口', () => {
|
||||||
|
const sections = getUserBusinessSections({
|
||||||
|
userId: 12,
|
||||||
|
deptId: 100,
|
||||||
|
userType: 'sys_user',
|
||||||
|
appAccess: {
|
||||||
|
configured: true,
|
||||||
|
permissions: [],
|
||||||
|
features: ['card:list', 'customer:list'],
|
||||||
|
routes: [],
|
||||||
|
menus: [],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
const titles = sections.flatMap(section => section.items.map(item => item.title))
|
||||||
|
|
||||||
|
expect(titles).toContain('我的卡片')
|
||||||
|
expect(titles).toContain('客户管理')
|
||||||
|
expect(titles).toContain('卡片排查')
|
||||||
|
expect(titles).toContain('我的订单')
|
||||||
|
expect(titles).not.toContain('套餐续费')
|
||||||
|
expect(titles).not.toContain('卡片划拨')
|
||||||
|
expect(titles).not.toContain('套餐变更')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('控制台权限标识直接决定可见业务入口', () => {
|
||||||
|
const sections = getUserBusinessSections({
|
||||||
|
userId: 13,
|
||||||
|
deptId: 100,
|
||||||
|
userType: 'sys_user',
|
||||||
|
appAccess: {
|
||||||
|
configured: true,
|
||||||
|
permissions: ['cpm:iot:tSimCardInfo:list', 'cpm:agent:tAgentEnterprise:list'],
|
||||||
|
features: [],
|
||||||
|
routes: [],
|
||||||
|
menus: [],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
const actions = sections.flatMap(section => section.items.map(item => item.action))
|
||||||
|
|
||||||
|
expect(actions).toContain('cardList')
|
||||||
|
expect(actions).toContain('customer')
|
||||||
|
expect(actions).not.toContain('renew')
|
||||||
|
expect(actions).not.toContain('allocate')
|
||||||
|
expect(actions).not.toContain('change')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('有工单列表或新增权限都展示工单服务入口', () => {
|
||||||
|
const listSections = getUserBusinessSections({
|
||||||
|
userId: 14,
|
||||||
|
deptId: 100,
|
||||||
|
userType: 'sys_user',
|
||||||
|
appAccess: {
|
||||||
|
configured: true,
|
||||||
|
permissions: ['cpm:gen:tWoMain:list'],
|
||||||
|
features: [],
|
||||||
|
routes: [],
|
||||||
|
menus: [],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
const createSections = getUserBusinessSections({
|
||||||
|
userId: 15,
|
||||||
|
deptId: 100,
|
||||||
|
userType: 'sys_user',
|
||||||
|
appAccess: {
|
||||||
|
configured: true,
|
||||||
|
permissions: ['cpm:gen:tWoMain:edit'],
|
||||||
|
features: [],
|
||||||
|
routes: [],
|
||||||
|
menus: [],
|
||||||
|
},
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
expect(listSections.flatMap(section => section.items.map(item => item.title))).toContain('工单服务')
|
||||||
|
expect(createSections.flatMap(section => section.items.map(item => item.title))).toContain('工单服务')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('业务子界面容器文件都存在', () => {
|
||||||
|
expectedSubViews.forEach((viewName) => {
|
||||||
|
expect(fs.existsSync(`${businessDir}views/${viewName}/index.vue`)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import type { IUserInfoRes } from '@/api/types/login'
|
||||||
|
import type { AppPermissionAction } from '@/utils/permission'
|
||||||
|
import { hasUserPermission } from '@/utils/permission'
|
||||||
|
import { getAccountWorkspace, isTerminalUserInfo } from '@/utils/userIdentity'
|
||||||
|
|
||||||
|
export type BusinessTone = 'blue' | 'green' | 'orange' | 'primary' | 'purple' | 'red'
|
||||||
|
|
||||||
|
export interface BusinessServiceItem {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
tag: string
|
||||||
|
icon: string
|
||||||
|
tone: BusinessTone
|
||||||
|
action?:
|
||||||
|
| 'addon'
|
||||||
|
| 'allocate'
|
||||||
|
| 'balanceRecharge'
|
||||||
|
| 'cardList'
|
||||||
|
| 'change'
|
||||||
|
| 'customer'
|
||||||
|
| 'diagnosis'
|
||||||
|
| 'order'
|
||||||
|
| 'renew'
|
||||||
|
| 'warning'
|
||||||
|
| 'workOrder'
|
||||||
|
agentOnly?: boolean
|
||||||
|
terminalOnly?: boolean
|
||||||
|
terminalHidden?: boolean
|
||||||
|
permissionAction?: AppPermissionAction | AppPermissionAction[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BusinessServiceSection {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
type: 'grid' | 'list'
|
||||||
|
items: BusinessServiceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ==================== 常量 ==================== */
|
||||||
|
export const businessSections: BusinessServiceSection[] = [
|
||||||
|
{
|
||||||
|
title: '套餐与流量',
|
||||||
|
desc: '续费、变更与加油包',
|
||||||
|
type: 'grid',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '套餐续费',
|
||||||
|
desc: '续约当前套餐,避免停机',
|
||||||
|
tag: '续费',
|
||||||
|
icon: 'i-carbon-renew',
|
||||||
|
tone: 'green',
|
||||||
|
action: 'renew',
|
||||||
|
permissionAction: 'renew',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '加油包',
|
||||||
|
desc: '流量加餐,按需购买',
|
||||||
|
tag: '流量',
|
||||||
|
icon: 'i-carbon-package',
|
||||||
|
tone: 'blue',
|
||||||
|
action: 'addon',
|
||||||
|
permissionAction: 'addon',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '套餐变更',
|
||||||
|
desc: '更换套餐档位',
|
||||||
|
tag: '变更',
|
||||||
|
icon: 'i-carbon-change-catalog',
|
||||||
|
tone: 'primary',
|
||||||
|
action: 'change',
|
||||||
|
agentOnly: true,
|
||||||
|
permissionAction: 'change',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '生命周期操作',
|
||||||
|
desc: '停复机与联网操作',
|
||||||
|
type: 'grid',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '停机 / 复机',
|
||||||
|
desc: '进入卡片选择暂停或恢复服务',
|
||||||
|
tag: '生命周期',
|
||||||
|
icon: 'i-carbon-pause-outline',
|
||||||
|
tone: 'orange',
|
||||||
|
action: 'cardList',
|
||||||
|
permissionAction: ['stop', 'start'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '重启卡',
|
||||||
|
desc: '重新建立网络会话',
|
||||||
|
tag: '重启',
|
||||||
|
icon: 'i-carbon-restart',
|
||||||
|
tone: 'primary',
|
||||||
|
action: 'cardList',
|
||||||
|
permissionAction: 'restart',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '管理与预警',
|
||||||
|
desc: '状态排查、预警与分配',
|
||||||
|
type: 'grid',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '卡片排查',
|
||||||
|
desc: '状态、会话和流量异常',
|
||||||
|
tag: '排查',
|
||||||
|
icon: 'i-carbon-stethoscope',
|
||||||
|
tone: 'green',
|
||||||
|
action: 'diagnosis',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '预警配置',
|
||||||
|
desc: '流量阈值与微信订阅',
|
||||||
|
tag: '预警',
|
||||||
|
icon: 'i-carbon-warning-alt',
|
||||||
|
tone: 'orange',
|
||||||
|
action: 'warning',
|
||||||
|
permissionAction: 'warning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卡片划拨',
|
||||||
|
desc: '分配给下游客户/子账号',
|
||||||
|
tag: '划拨',
|
||||||
|
icon: 'i-carbon-share-knowledge',
|
||||||
|
tone: 'blue',
|
||||||
|
action: 'allocate',
|
||||||
|
agentOnly: true,
|
||||||
|
permissionAction: 'allocate',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '服务记录',
|
||||||
|
desc: '订单与工单',
|
||||||
|
type: 'grid',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: '我的订单',
|
||||||
|
desc: '续费/加油包办理记录',
|
||||||
|
tag: '订单',
|
||||||
|
icon: 'i-carbon-receipt',
|
||||||
|
tone: 'blue',
|
||||||
|
action: 'order',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '工单服务',
|
||||||
|
desc: '反馈问题和查看进度',
|
||||||
|
tag: '工单',
|
||||||
|
icon: 'i-carbon-chat',
|
||||||
|
tone: 'orange',
|
||||||
|
action: 'workOrder',
|
||||||
|
permissionAction: ['workOrder', 'workOrderCreate'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '我的卡片',
|
||||||
|
desc: '查看卡信息、套餐和流量',
|
||||||
|
tag: '进入',
|
||||||
|
icon: 'i-carbon-sim-card',
|
||||||
|
tone: 'primary',
|
||||||
|
action: 'cardList',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '客户管理',
|
||||||
|
desc: '直属客户与客户详情',
|
||||||
|
tag: '客户',
|
||||||
|
icon: 'i-carbon-user-multiple',
|
||||||
|
tone: 'green',
|
||||||
|
action: 'customer',
|
||||||
|
agentOnly: true,
|
||||||
|
permissionAction: 'customer',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
/** ==================== 方法 ==================== */
|
||||||
|
export function getUserBusinessSections(
|
||||||
|
userInfoOrSections?: IUserInfoRes | BusinessServiceSection[],
|
||||||
|
sections: BusinessServiceSection[] = businessSections,
|
||||||
|
) {
|
||||||
|
const userInfo = Array.isArray(userInfoOrSections) ? undefined : userInfoOrSections
|
||||||
|
const sourceSections = Array.isArray(userInfoOrSections) ? userInfoOrSections : sections
|
||||||
|
const workspace = getAccountWorkspace(userInfo)
|
||||||
|
const isAgentUser = Boolean(userInfo) && workspace !== 'terminal'
|
||||||
|
const isTerminalUser = !userInfo || isTerminalUserInfo(userInfo)
|
||||||
|
|
||||||
|
return sourceSections
|
||||||
|
.map(section => ({
|
||||||
|
...section,
|
||||||
|
items: section.items.filter((item) => {
|
||||||
|
if (item.agentOnly && !isAgentUser)
|
||||||
|
return false
|
||||||
|
if (item.terminalOnly && !isTerminalUser)
|
||||||
|
return false
|
||||||
|
if (item.terminalHidden && isTerminalUser)
|
||||||
|
return false
|
||||||
|
if (userInfo && item.permissionAction && !hasBusinessItemPermission(userInfo, item.permissionAction))
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
.filter(section => section.items.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBusinessItemPermission(
|
||||||
|
userInfo: IUserInfoRes,
|
||||||
|
action: AppPermissionAction | AppPermissionAction[],
|
||||||
|
) {
|
||||||
|
const actions = Array.isArray(action) ? action : [action]
|
||||||
|
return actions.some(item => hasUserPermission(userInfo, item))
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import RouterPath from '@/constant/RouterPath'
|
||||||
|
import type { IUserInfoRes } from '@/api/types/login'
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import type { AppPermissionAction } from '@/utils/permission'
|
||||||
|
import { hasUserPermission } from '@/utils/permission'
|
||||||
|
import BusinessSection from './components/BusinessSection.vue'
|
||||||
|
import { getUserBusinessSections } from './data'
|
||||||
|
import type { BusinessServiceItem } from './data'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '办理',
|
||||||
|
},
|
||||||
|
layout: 'default',
|
||||||
|
})
|
||||||
|
|
||||||
|
/** ==================== 常量 ==================== */
|
||||||
|
const actionRouteMap: Partial<Record<NonNullable<BusinessServiceItem['action']>, string>> = {
|
||||||
|
renew: RouterPath.BusinessViewsRenew,
|
||||||
|
allocate: RouterPath.BusinessViewsAllocate,
|
||||||
|
balanceRecharge: RouterPath.BusinessViewsBalanceRecharge,
|
||||||
|
change: RouterPath.BusinessViewsChange,
|
||||||
|
customer: RouterPath.BusinessViewsCustomer,
|
||||||
|
addon: RouterPath.BusinessViewsAddon,
|
||||||
|
diagnosis: RouterPath.BusinessViewsDiagnosis,
|
||||||
|
order: RouterPath.BusinessViewsOrder,
|
||||||
|
warning: RouterPath.BusinessViewsWarning,
|
||||||
|
workOrder: RouterPath.BusinessViewsWorkOrder,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ==================== 数据 ==================== */
|
||||||
|
const tokenStore = useTokenStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
let refreshUserPromise: Promise<void | IUserInfoRes> | undefined
|
||||||
|
const sections = computed(() => getUserBusinessSections(userStore.userInfo))
|
||||||
|
|
||||||
|
/** ==================== 方法 ==================== */
|
||||||
|
async function refreshUserInfoIfNeeded() {
|
||||||
|
if (!tokenStore.updateNowTime().hasLogin)
|
||||||
|
return
|
||||||
|
if (!refreshUserPromise) {
|
||||||
|
refreshUserPromise = userStore
|
||||||
|
.fetchUserInfo()
|
||||||
|
.catch(() => {
|
||||||
|
// 保留本地已缓存的用户信息,避免网络抖动时业务入口反复隐藏。
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
refreshUserPromise = undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await refreshUserPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBusinessAction(item: BusinessServiceItem) {
|
||||||
|
await refreshUserInfoIfNeeded()
|
||||||
|
if (item.permissionAction && !hasBusinessItemPermission(item.permissionAction)) {
|
||||||
|
uni.showToast({ title: '暂无操作权限', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.action === 'cardList') {
|
||||||
|
uni.switchTab({ url: RouterPath.Card })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = item.action ? actionRouteMap[item.action] : ''
|
||||||
|
if (url) {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: item.title,
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goDiagnosis() {
|
||||||
|
uni.navigateTo({ url: RouterPath.BusinessViewsDiagnosis })
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasBusinessItemPermission(action: AppPermissionAction | AppPermissionAction[]) {
|
||||||
|
const actions = Array.isArray(action) ? action : [action]
|
||||||
|
return actions.some(item => hasUserPermission(userStore.userInfo, item))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ==================== 组件 ==================== */
|
||||||
|
/** ==================== 周期 ==================== */
|
||||||
|
onShow(() => {
|
||||||
|
void refreshUserInfoIfNeeded()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="business-page">
|
||||||
|
<view class="business-stack">
|
||||||
|
<view class="assist-card" @click="goDiagnosis">
|
||||||
|
<view class="assist-icon">
|
||||||
|
<view class="i-carbon-stethoscope text-32rpx" />
|
||||||
|
</view>
|
||||||
|
<view class="min-w-0 flex-1">
|
||||||
|
<text class="assist-title">不确定办哪项?先查卡状态</text>
|
||||||
|
<text class="assist-sub">按卡片状态、套餐、流量和会话信息整理处理方向</text>
|
||||||
|
<text class="mt-4rpx block text-20rpx text-#2563eb leading-30rpx">
|
||||||
|
排查建议仅供辅助判断,以实际业务处理结果为准
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="i-carbon-chevron-right assist-chev" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<BusinessSection
|
||||||
|
v-for="section in sections"
|
||||||
|
:key="section.title"
|
||||||
|
:section="section"
|
||||||
|
@action="handleBusinessAction"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
page {
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.business-page {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 20rpx 28rpx calc(128rpx + env(safe-area-inset-bottom));
|
||||||
|
background: #f6f8fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.business-stack {
|
||||||
|
display: flex;
|
||||||
|
max-width: 920rpx;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18rpx;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assist-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 18rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border: 1px solid #dbeafe;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(120deg, #eef5ff 0%, #f3f9ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.assist-icon {
|
||||||
|
display: flex;
|
||||||
|
width: 56rpx;
|
||||||
|
height: 56rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assist-title {
|
||||||
|
display: block;
|
||||||
|
font-size: 25rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assist-sub {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5rpx;
|
||||||
|
font-size: 21rpx;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 32rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assist-chev {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #93c5fd;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { useLastQueriedCardStore, useUserStore } from '@/store'
|
||||||
|
import { hasAnyFeature } from '@/utils/permission'
|
||||||
|
import { displayCardValue, displayPackageName, formatDateTime, formatFlowPair, getBossCardStatusText } from '@/pages/card/data'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '加油包',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type AddonOption = {
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
desc: string
|
||||||
|
flowText: string
|
||||||
|
badge?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const addonOptions: AddonOption[] = [
|
||||||
|
{ name: '1GB 加油包', price: '¥8.00', desc: '适合短时补量', flowText: '1GB', badge: '推荐' },
|
||||||
|
{ name: '3GB 加油包', price: '¥18.00', desc: '日常轻量补充', flowText: '3GB' },
|
||||||
|
{ name: '5GB 加油包', price: '¥28.00', desc: '中等用量设备', flowText: '5GB' },
|
||||||
|
{ name: '10GB 加油包', price: '¥48.00', desc: '持续在线设备', flowText: '10GB' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const lastCardStore = useLastQueriedCardStore()
|
||||||
|
const noPermission = ref(false)
|
||||||
|
const selectedIndex = ref(0)
|
||||||
|
const canUseAddon = computed(() => hasAnyFeature(userStore.userInfo, 'card:addon'))
|
||||||
|
const currentCard = computed(() => lastCardStore.card)
|
||||||
|
const selectedOption = computed(() => addonOptions[selectedIndex.value] || addonOptions[0])
|
||||||
|
const selectedAmountText = computed(() => selectedOption.value?.price || '¥0.00')
|
||||||
|
|
||||||
|
const cardValueText = computed(() => {
|
||||||
|
const card = currentCard.value
|
||||||
|
return card ? displayCardValue(card) : '请选择卡片'
|
||||||
|
})
|
||||||
|
const packageText = computed(() => {
|
||||||
|
const card = currentCard.value
|
||||||
|
return card ? displayPackageName(card) : '当前套餐'
|
||||||
|
})
|
||||||
|
const flowText = computed(() => {
|
||||||
|
const card = currentCard.value
|
||||||
|
return card ? formatFlowPair(card) : '暂无'
|
||||||
|
})
|
||||||
|
const expireText = computed(() => {
|
||||||
|
const card = currentCard.value
|
||||||
|
return card ? formatDateTime(card.flowPkgEndAt) : '-'
|
||||||
|
})
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const card = currentCard.value
|
||||||
|
return card ? getBossCardStatusText(card.bossCardStatus) : '未选择'
|
||||||
|
})
|
||||||
|
|
||||||
|
function selectOption(index: number) {
|
||||||
|
selectedIndex.value = index
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePay() {
|
||||||
|
if (!canUseAddon.value) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '暂无加油包订购权限',
|
||||||
|
icon: 'none',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认订购',
|
||||||
|
content: `卡号:${cardValueText.value}\n加油包:${selectedOption.value.name}\n应付金额:${selectedAmountText.value}\n订购后将立即生效。`,
|
||||||
|
confirmText: '确认',
|
||||||
|
cancelText: '再看看',
|
||||||
|
success: (res) => {
|
||||||
|
if (!res.confirm)
|
||||||
|
return
|
||||||
|
uni.showToast({
|
||||||
|
title: '已选择加油包',
|
||||||
|
icon: 'success',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await userStore.fetchUserInfo().catch(() => undefined)
|
||||||
|
if (!canUseAddon.value) {
|
||||||
|
noPermission.value = true
|
||||||
|
uni.showToast({ title: '当前账号不能订购加油包', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="biz-page">
|
||||||
|
<view class="biz-stack">
|
||||||
|
<view v-if="noPermission" class="biz-card">
|
||||||
|
<view class="biz-card__body text-26rpx text-#64748b leading-38rpx">
|
||||||
|
当前账号不能订购加油包。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="biz-hero">
|
||||||
|
<view class="biz-hero__top">
|
||||||
|
<view class="min-w-0 flex items-center gap-16rpx">
|
||||||
|
<view class="biz-hero__icon">
|
||||||
|
<view class="i-carbon-flash" />
|
||||||
|
</view>
|
||||||
|
<view class="min-w-0">
|
||||||
|
<text class="biz-hero__title">加油包</text>
|
||||||
|
<text class="biz-hero__sub">按需补充流量,选定后直接进入支付确认</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<wd-tag type="primary" variant="plain">
|
||||||
|
订购
|
||||||
|
</wd-tag>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-stat-grid">
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">当前卡片</text>
|
||||||
|
<text class="biz-stat__value">{{ cardValueText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">当前套餐</text>
|
||||||
|
<text class="biz-stat__value">{{ packageText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">剩余流量</text>
|
||||||
|
<text class="biz-stat__value">{{ flowText }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">卡片状态</text>
|
||||||
|
<text class="biz-stat__value">{{ statusText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-card">
|
||||||
|
<view class="biz-card__body">
|
||||||
|
<view class="biz-section-head">
|
||||||
|
<view>
|
||||||
|
<text class="biz-section-title">选择加油包</text>
|
||||||
|
<text class="biz-section-sub">按流量档位单选,价格与生效时间由后端统一控制</text>
|
||||||
|
</view>
|
||||||
|
<wd-tag type="success" variant="plain">
|
||||||
|
{{ selectedOption.name }}
|
||||||
|
</wd-tag>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="grid gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-for="(item, index) in addonOptions"
|
||||||
|
:key="item.name"
|
||||||
|
class="biz-option"
|
||||||
|
:class="{ 'is-active': index === selectedIndex }"
|
||||||
|
@click="selectOption(index)"
|
||||||
|
>
|
||||||
|
<view class="biz-option__main">
|
||||||
|
<view class="flex items-center gap-10rpx">
|
||||||
|
<text class="biz-option__title">{{ item.name }}</text>
|
||||||
|
<wd-tag
|
||||||
|
v-if="item.badge"
|
||||||
|
type="warning"
|
||||||
|
size="small"
|
||||||
|
variant="plain"
|
||||||
|
>
|
||||||
|
{{ item.badge }}
|
||||||
|
</wd-tag>
|
||||||
|
</view>
|
||||||
|
<text class="biz-option__meta">
|
||||||
|
{{ item.flowText }} · {{ item.desc }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<text class="biz-option__price">{{ item.price }}</text>
|
||||||
|
<view class="biz-radio" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-card">
|
||||||
|
<view class="biz-card__body">
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">已选加油包</text>
|
||||||
|
<text class="biz-row__value">{{ selectedOption.name }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">付款方式</text>
|
||||||
|
<text class="biz-row__value">微信支付</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">生效时间</text>
|
||||||
|
<text class="biz-row__value">订购成功后立即生效</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">到期时间</text>
|
||||||
|
<text class="biz-row__value">{{ expireText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="!noPermission" class="biz-paybar">
|
||||||
|
<view class="biz-paybar__inner">
|
||||||
|
<view class="biz-paybar__summary">
|
||||||
|
<text class="biz-paybar__label">{{ cardValueText }}</text>
|
||||||
|
<text class="biz-paybar__value">应付 {{ selectedAmountText }}</text>
|
||||||
|
</view>
|
||||||
|
<wd-button block type="primary" @click="handlePay">
|
||||||
|
立即订购 {{ selectedAmountText }}
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,740 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { AllocateBatchRes, AllocateTarget, AppCard } from '@/api/card'
|
||||||
|
import {
|
||||||
|
allocateBatchPrepare,
|
||||||
|
allocateBatchSubmit,
|
||||||
|
getAllocateTargets,
|
||||||
|
getCardList,
|
||||||
|
} from '@/api/card'
|
||||||
|
import {
|
||||||
|
displayCardValue,
|
||||||
|
displayPackageName,
|
||||||
|
formatDateTime,
|
||||||
|
formatFlowPair,
|
||||||
|
} from '@/pages/card/data'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { hasUserPermission } from '@/utils/permission'
|
||||||
|
import { getAccountWorkspace } from '@/utils/userIdentity'
|
||||||
|
|
||||||
|
definePage({
|
||||||
|
style: {
|
||||||
|
navigationBarTitleText: '批量卡片划拨',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const keyword = ref('')
|
||||||
|
const targetKeyword = ref('')
|
||||||
|
const cards = ref<AppCard[]>([])
|
||||||
|
const targets = ref<AllocateTarget[]>([])
|
||||||
|
const selectedCardValues = ref<string[]>([])
|
||||||
|
const selectedTargetId = ref<number>()
|
||||||
|
const loadingCards = ref(false)
|
||||||
|
const loadingTargets = ref(false)
|
||||||
|
const preparing = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const noPermission = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const targetPopupVisible = ref(false)
|
||||||
|
const prepare = ref<AllocateBatchRes>()
|
||||||
|
const result = ref<AllocateBatchRes>()
|
||||||
|
const initialCardValue = ref('')
|
||||||
|
let targetLoadSeq = 0
|
||||||
|
|
||||||
|
const workspace = computed(() => getAccountWorkspace(userStore.userInfo))
|
||||||
|
const isAgentUser = computed(() => workspace.value === 'enterprise' || workspace.value === 'agent' || workspace.value === 'admin')
|
||||||
|
const canUseAllocate = computed(() => isAgentUser.value && hasUserPermission(userStore.userInfo, 'allocate'))
|
||||||
|
const selectedTarget = computed(() =>
|
||||||
|
targets.value.find(item => Number(item.deptId) === Number(selectedTargetId.value)),
|
||||||
|
)
|
||||||
|
const allocatableCards = computed(() => cards.value.filter(item => item.canAllocate !== false))
|
||||||
|
const visibleCards = computed(() => {
|
||||||
|
const value = keyword.value.trim()
|
||||||
|
if (!value)
|
||||||
|
return allocatableCards.value
|
||||||
|
return allocatableCards.value.filter((item) => {
|
||||||
|
return (
|
||||||
|
allocateCardValue(item).includes(value)
|
||||||
|
|| (item.msisdn || '').includes(value)
|
||||||
|
|| (item.imsi || '').includes(value)
|
||||||
|
|| displayPackageName(item).includes(value)
|
||||||
|
|| (item.remark || '').includes(value)
|
||||||
|
|| (item.terminalName || '').includes(value)
|
||||||
|
|| ownerDeptText(item).includes(value)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const canPrepare = computed(
|
||||||
|
() => canUseAllocate.value && !!selectedTarget.value && !!selectedCardValues.value.length,
|
||||||
|
)
|
||||||
|
const canSubmit = computed(() => {
|
||||||
|
return !!prepare.value?.batchNo
|
||||||
|
&& !submitting.value
|
||||||
|
&& !result.value
|
||||||
|
&& (prepare.value.failCount || 0) === 0
|
||||||
|
&& prepare.value.status !== 'failed'
|
||||||
|
})
|
||||||
|
const isFixedCardMode = computed(() => !!initialCardValue.value)
|
||||||
|
|
||||||
|
async function openTargetPopup() {
|
||||||
|
if (noPermission.value)
|
||||||
|
return
|
||||||
|
targetPopupVisible.value = true
|
||||||
|
await refreshTargetPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTargetPopup() {
|
||||||
|
targetKeyword.value = ''
|
||||||
|
targets.value = []
|
||||||
|
await loadTargets('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchTargetPopup() {
|
||||||
|
await loadTargets(targetKeyword.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
await userStore.fetchUserInfo().catch(() => undefined)
|
||||||
|
if (!canUseAllocate.value) {
|
||||||
|
noPermission.value = true
|
||||||
|
uni.showToast({ title: '当前账号不能使用卡片划拨', icon: 'none' })
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 800)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await Promise.all([loadTargets(), loadCards()])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTargets(keyword?: string) {
|
||||||
|
const seq = ++targetLoadSeq
|
||||||
|
loadingTargets.value = true
|
||||||
|
try {
|
||||||
|
const actualKeyword = typeof keyword === 'string' ? keyword : targetKeyword.value
|
||||||
|
const res = await getAllocateTargets({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1000,
|
||||||
|
keyword: actualKeyword.trim() || undefined,
|
||||||
|
})
|
||||||
|
if (seq !== targetLoadSeq)
|
||||||
|
return
|
||||||
|
targets.value = (res.items || []).map(item => ({
|
||||||
|
...item,
|
||||||
|
deptId: Number(item.deptId),
|
||||||
|
parentId: item.parentId == null ? undefined : Number(item.parentId),
|
||||||
|
})).filter(isSelectableTarget)
|
||||||
|
if (
|
||||||
|
selectedTargetId.value
|
||||||
|
&& !targets.value.some(item => Number(item.deptId) === Number(selectedTargetId.value))
|
||||||
|
) {
|
||||||
|
selectedTargetId.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (seq !== targetLoadSeq)
|
||||||
|
return
|
||||||
|
targets.value = []
|
||||||
|
uni.showToast({ title: getErrorMessage(error, '目标客户加载失败'), icon: 'none' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (seq === targetLoadSeq)
|
||||||
|
loadingTargets.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCards() {
|
||||||
|
loadingCards.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
try {
|
||||||
|
const res = await getCardList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 100,
|
||||||
|
cardValue: keyword.value.trim() || undefined,
|
||||||
|
})
|
||||||
|
cards.value = res.items || []
|
||||||
|
applyInitialCardSelection()
|
||||||
|
const nextKeys = new Set(allocatableCards.value.map(allocateCardValue))
|
||||||
|
selectedCardValues.value = selectedCardValues.value.filter(value => nextKeys.has(value))
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
cards.value = []
|
||||||
|
selectedCardValues.value = []
|
||||||
|
errorMessage.value = getErrorMessage(error, '卡片列表加载失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loadingCards.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function allocateCardValue(item: AppCard) {
|
||||||
|
return item.iccid || item.msisdn || item.imsi || item.imei || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function ownerDeptText(item: AppCard) {
|
||||||
|
return item.deptName || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardMatchesValue(item: AppCard, value: string) {
|
||||||
|
return [item.iccid, item.msisdn, item.imsi, item.imei].filter(Boolean).includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInitialCardSelection() {
|
||||||
|
const value = initialCardValue.value.trim()
|
||||||
|
if (!value)
|
||||||
|
return
|
||||||
|
const item = allocatableCards.value.find(cardItem => cardMatchesValue(cardItem, value))
|
||||||
|
if (!item)
|
||||||
|
return
|
||||||
|
const selectedValue = allocateCardValue(item)
|
||||||
|
if (selectedValue) {
|
||||||
|
selectedCardValues.value = [selectedValue]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(item: AppCard) {
|
||||||
|
return selectedCardValues.value.includes(allocateCardValue(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectVisibleCards() {
|
||||||
|
const values = visibleCards.value.map(allocateCardValue).filter(Boolean)
|
||||||
|
selectedCardValues.value = Array.from(new Set([...selectedCardValues.value, ...values]))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedCardValues.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectTarget(item: AllocateTarget) {
|
||||||
|
selectedTargetId.value = Number(item.deptId)
|
||||||
|
targetPopupVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetLabel(item?: AllocateTarget) {
|
||||||
|
if (!item)
|
||||||
|
return '请选择划拨目标客户'
|
||||||
|
return item.label || item.deptName || '未命名客户'
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetDescription(item?: AllocateTarget) {
|
||||||
|
if (!item)
|
||||||
|
return ''
|
||||||
|
return item.deptKey ? `客户编码:${item.deptKey}` : targetScopeName(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetScopeName(item?: AllocateTarget) {
|
||||||
|
return item?.targetScopeName || (item?.targetScope === 'direct_child' ? '直属下级' : '当前机构')
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetScopeTagType(item?: AllocateTarget) {
|
||||||
|
return item?.targetScope === 'direct_child' ? 'primary' : 'success'
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelectableTarget(item: AllocateTarget) {
|
||||||
|
const currentDeptId = Number(userStore.userInfo?.deptId || userStore.userInfo?.enterpriseProfile?.deptId || 0)
|
||||||
|
if (currentDeptId && Number(item.deptId) === currentDeptId)
|
||||||
|
return false
|
||||||
|
if (item.targetScope === 'current')
|
||||||
|
return false
|
||||||
|
if (item.targetScope === 'direct_child')
|
||||||
|
return true
|
||||||
|
if (currentDeptId && Number(item.parentId) === currentDeptId)
|
||||||
|
return true
|
||||||
|
return String(item.targetScopeName || '').includes('下级')
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareStatusText(item?: AllocateBatchRes) {
|
||||||
|
if (!item)
|
||||||
|
return '-'
|
||||||
|
if (result.value)
|
||||||
|
return '已划拨'
|
||||||
|
if (item.status === 'failed' || (item.failCount || 0) > 0)
|
||||||
|
return '校验失败'
|
||||||
|
return '已校验'
|
||||||
|
}
|
||||||
|
|
||||||
|
function failedAllocateDetails(item?: AllocateBatchRes) {
|
||||||
|
return item?.details?.filter(detail => detail.status === 'failed') || []
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPrepareFailedModal(item: AllocateBatchRes) {
|
||||||
|
const failedDetails = failedAllocateDetails(item)
|
||||||
|
const failCount = item.failCount || failedDetails.length
|
||||||
|
const lines = failedDetails.slice(0, 6).map((detail) => {
|
||||||
|
const cardNo = detail.itemId || `第${detail.rowNo}行`
|
||||||
|
return `${cardNo}:${detail.errorReason || '校验失败'}`
|
||||||
|
})
|
||||||
|
if (failCount > lines.length)
|
||||||
|
lines.push(`还有 ${failCount - lines.length} 条失败明细,请在页面下方查看。`)
|
||||||
|
uni.showModal({
|
||||||
|
title: `校验失败 ${failCount} 张`,
|
||||||
|
content: lines.join('\n') || item.failReason || '存在校验失败卡,请调整后重新校验。',
|
||||||
|
confirmText: '知道了',
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePrepare() {
|
||||||
|
if (!canPrepare.value || preparing.value) {
|
||||||
|
uni.showToast({ title: '请先选择目标客户和需要划拨的卡', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
preparing.value = true
|
||||||
|
try {
|
||||||
|
const res = await allocateBatchPrepare({
|
||||||
|
cardValues: selectedCardValues.value,
|
||||||
|
targetDeptId: selectedTargetId.value as number,
|
||||||
|
})
|
||||||
|
prepare.value = res
|
||||||
|
result.value = undefined
|
||||||
|
if ((res.failCount || 0) > 0 || res.status === 'failed') {
|
||||||
|
showPrepareFailedModal(res)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.showToast({ title: '校验通过', icon: 'success' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
prepare.value = undefined
|
||||||
|
uni.showToast({ title: getErrorMessage(error, '划拨校验失败'), icon: 'none' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
preparing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
if (!prepare.value?.batchNo || !selectedTarget.value) {
|
||||||
|
uni.showToast({ title: '请先校验划拨批次', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (submitting.value)
|
||||||
|
return
|
||||||
|
const confirmed = await confirmAllocateSubmit()
|
||||||
|
if (!confirmed)
|
||||||
|
return
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const res = await allocateBatchSubmit({
|
||||||
|
batchNo: prepare.value.batchNo,
|
||||||
|
cardValues: selectedCardValues.value,
|
||||||
|
targetDeptId: selectedTarget.value.deptId,
|
||||||
|
})
|
||||||
|
result.value = res
|
||||||
|
prepare.value = res
|
||||||
|
uni.showModal({
|
||||||
|
title: '划拨成功',
|
||||||
|
content: `划拨批次:${res.batchNo || '-'}\n目标客户:${targetLabel(res.target)}\n划拨卡数:${res.quantity || selectedCardValues.value.length} 张\n当前企业保留查看和其他办理权限,划拨权交由目标客户。`,
|
||||||
|
showCancel: false,
|
||||||
|
})
|
||||||
|
initialCardValue.value = ''
|
||||||
|
clearSelection()
|
||||||
|
await loadCards()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
uni.showToast({ title: getErrorMessage(error, '划拨提交失败'), icon: 'none' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmAllocateSubmit() {
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认划拨',
|
||||||
|
content: `目标客户:${targetLabel(selectedTarget.value)}\n划拨卡数:${prepare.value?.quantity || selectedCardValues.value.length} 张\n确认后更新卡片归属,当前企业保留查看和其他办理权限,划拨权交由目标客户。`,
|
||||||
|
confirmText: '确认划拨',
|
||||||
|
cancelText: '再看一下',
|
||||||
|
success: res => resolve(!!res.confirm),
|
||||||
|
fail: () => resolve(false),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown, fallback: string) {
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const data = error as {
|
||||||
|
msg?: unknown
|
||||||
|
message?: unknown
|
||||||
|
errMsg?: unknown
|
||||||
|
statusCode?: unknown
|
||||||
|
data?: { msg?: unknown, message?: unknown }
|
||||||
|
}
|
||||||
|
if (typeof data.msg === 'string' && data.msg)
|
||||||
|
return data.msg
|
||||||
|
if (typeof data.message === 'string' && data.message)
|
||||||
|
return data.message
|
||||||
|
if (typeof data.data?.msg === 'string' && data.data.msg)
|
||||||
|
return data.data.msg
|
||||||
|
if (typeof data.data?.message === 'string' && data.data.message)
|
||||||
|
return data.data.message
|
||||||
|
if (typeof data.statusCode === 'number')
|
||||||
|
return fallback
|
||||||
|
if (typeof data.errMsg === 'string' && data.errMsg)
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([selectedCardValues, selectedTargetId], () => {
|
||||||
|
prepare.value = undefined
|
||||||
|
result.value = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void bootstrap()
|
||||||
|
})
|
||||||
|
|
||||||
|
onLoad((query) => {
|
||||||
|
const value = normalizeQueryValue(query?.cardValue || query?.cardNo || query?.iccid)
|
||||||
|
if (!value)
|
||||||
|
return
|
||||||
|
initialCardValue.value = value
|
||||||
|
keyword.value = value
|
||||||
|
})
|
||||||
|
|
||||||
|
function normalizeQueryValue(value: string | string[] | undefined) {
|
||||||
|
return Array.isArray(value) ? value[0] : String(value || '')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<scroll-view scroll-y class="h-screen">
|
||||||
|
<view class="biz-page">
|
||||||
|
<view class="biz-stack">
|
||||||
|
<view v-if="noPermission" class="biz-card">
|
||||||
|
<view class="biz-card__body text-26rpx text-#64748b leading-38rpx">
|
||||||
|
当前账号不能使用卡片划拨。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<view class="biz-hero">
|
||||||
|
<view class="biz-hero__top">
|
||||||
|
<view class="min-w-0 flex items-center gap-16rpx">
|
||||||
|
<view class="biz-hero__icon">
|
||||||
|
<view class="i-carbon-user-multiple" />
|
||||||
|
</view>
|
||||||
|
<view class="min-w-0">
|
||||||
|
<text class="biz-hero__title">卡片划拨</text>
|
||||||
|
<text class="biz-hero__sub">选择可划拨卡片,校验后转给目标下级客户</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="app-tag app-tag--info shrink-0 whitespace-nowrap">
|
||||||
|
代理商
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat-grid">
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">目标客户</text>
|
||||||
|
<text class="biz-stat__value">{{ targetLabel(selectedTarget) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">已选卡片</text>
|
||||||
|
<text class="biz-stat__value">{{ selectedCardValues.length }} 张</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">校验状态</text>
|
||||||
|
<text class="biz-stat__value">{{ prepareStatusText(prepare) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-stat">
|
||||||
|
<text class="biz-stat__label">失败卡数</text>
|
||||||
|
<text class="biz-stat__value">{{ prepare?.failCount || 0 }} 张</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-card">
|
||||||
|
<wd-cell
|
||||||
|
title="目标客户"
|
||||||
|
:value="targetLabel(selectedTarget)"
|
||||||
|
clickable
|
||||||
|
is-link
|
||||||
|
center
|
||||||
|
@click="openTargetPopup"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-card">
|
||||||
|
<view class="biz-card__body">
|
||||||
|
<wd-input
|
||||||
|
v-model="keyword"
|
||||||
|
placeholder="搜索卡号、套餐、备注或客户"
|
||||||
|
:clearable="!isFixedCardMode"
|
||||||
|
@confirm="loadCards"
|
||||||
|
/>
|
||||||
|
<view class="biz-mini-action-grid mt-16rpx">
|
||||||
|
<wd-button size="small" type="primary" plain block custom-class="biz-mini-action-btn" :loading="loadingCards" @click="loadCards">
|
||||||
|
<view class="biz-mini-action-btn__inner">
|
||||||
|
<view class="i-carbon-renew biz-mini-action-btn__icon biz-mini-action-btn__icon--blue" />
|
||||||
|
<text class="biz-mini-action-btn__text">刷新</text>
|
||||||
|
</view>
|
||||||
|
</wd-button>
|
||||||
|
<wd-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
block
|
||||||
|
custom-class="biz-mini-action-btn"
|
||||||
|
:disabled="!visibleCards.length"
|
||||||
|
@click="selectVisibleCards"
|
||||||
|
>
|
||||||
|
<view class="biz-mini-action-btn__inner">
|
||||||
|
<view class="i-carbon-checkmark-outline biz-mini-action-btn__icon biz-mini-action-btn__icon--green" />
|
||||||
|
<text class="biz-mini-action-btn__text">全选</text>
|
||||||
|
</view>
|
||||||
|
</wd-button>
|
||||||
|
<wd-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
block
|
||||||
|
custom-class="biz-mini-action-btn"
|
||||||
|
:disabled="!selectedCardValues.length"
|
||||||
|
@click="clearSelection"
|
||||||
|
>
|
||||||
|
<view class="biz-mini-action-btn__inner">
|
||||||
|
<view class="i-carbon-close biz-mini-action-btn__icon biz-mini-action-btn__icon--gray" />
|
||||||
|
<text class="biz-mini-action-btn__text">清空</text>
|
||||||
|
</view>
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="biz-section-head">
|
||||||
|
<view>
|
||||||
|
<text class="biz-section-title">当前可划拨卡</text>
|
||||||
|
<text class="biz-section-sub">划拨前会生成 ALC 批次进行归属和状态校验</text>
|
||||||
|
</view>
|
||||||
|
<view class="app-tag app-tag--info shrink-0 whitespace-nowrap">
|
||||||
|
已选 {{ selectedCardValues.length }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="loadingCards" class="biz-card">
|
||||||
|
<view class="biz-card__body">
|
||||||
|
<wd-skeleton theme="paragraph" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else-if="errorMessage" class="biz-card">
|
||||||
|
<view class="biz-card__body grid gap-16rpx">
|
||||||
|
<view class="text-26rpx text-#b91c1c leading-38rpx">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</view>
|
||||||
|
<wd-button size="small" type="primary" @click="loadCards">
|
||||||
|
重试
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-else class="biz-card">
|
||||||
|
<view v-if="visibleCards.length" class="biz-card__body">
|
||||||
|
<wd-checkbox-group v-model="selectedCardValues" type="square" checked-color="#2563eb">
|
||||||
|
<view class="grid gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-for="item in visibleCards"
|
||||||
|
:key="allocateCardValue(item)"
|
||||||
|
class="biz-check-row"
|
||||||
|
:class="{ 'is-active': isSelected(item) }"
|
||||||
|
>
|
||||||
|
<wd-checkbox
|
||||||
|
:name="allocateCardValue(item)"
|
||||||
|
custom-label-class="!block !w-full"
|
||||||
|
>
|
||||||
|
<view class="min-w-0 pr-16rpx">
|
||||||
|
<view class="truncate text-30rpx text-#111827 font-900">
|
||||||
|
{{ displayCardValue(item) }}
|
||||||
|
</view>
|
||||||
|
<view class="mt-8rpx truncate text-24rpx text-#64748b">
|
||||||
|
{{ displayPackageName(item) }}
|
||||||
|
</view>
|
||||||
|
<view class="mt-8rpx text-22rpx text-#64748b leading-32rpx">
|
||||||
|
{{ formatFlowPair(item) }} · 到期 {{ formatDateTime(item.flowPkgEndAt) }}
|
||||||
|
</view>
|
||||||
|
<view class="mt-8rpx text-22rpx text-#64748b leading-32rpx">
|
||||||
|
归属客户:{{ ownerDeptText(item) }}
|
||||||
|
</view>
|
||||||
|
<view v-if="item.terminalName" class="mt-8rpx text-22rpx text-#64748b leading-32rpx">
|
||||||
|
终端绑定:{{ item.terminalName }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</wd-checkbox>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</wd-checkbox-group>
|
||||||
|
</view>
|
||||||
|
<view v-else class="biz-card__body text-center text-26rpx text-#64748b">
|
||||||
|
暂无可划拨卡片
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="prepare" class="biz-card">
|
||||||
|
<view class="biz-card__body">
|
||||||
|
<view class="biz-section-head mb-8rpx">
|
||||||
|
<text class="biz-section-title">校验结果</text>
|
||||||
|
<wd-tag :type="prepare.failCount ? 'warning' : 'success'" variant="plain">
|
||||||
|
{{ prepareStatusText(prepare) }}
|
||||||
|
</wd-tag>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">划拨批次</text>
|
||||||
|
<text class="biz-row__value mono">{{ prepare.batchNo || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">目标客户</text>
|
||||||
|
<text class="biz-row__value">{{ targetLabel(prepare.target) }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">校验卡数</text>
|
||||||
|
<text class="biz-row__value">
|
||||||
|
{{ prepare.validCount ?? prepare.quantity ?? 0 }}/{{ prepare.totalCount ?? selectedCardValues.length }} 张
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-row">
|
||||||
|
<text class="biz-row__label">失败卡数</text>
|
||||||
|
<text class="biz-row__value">{{ prepare.failCount || 0 }} 张</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-if="prepare.failCount"
|
||||||
|
class="border-t border-#eef2f7 border-solid px-24rpx py-20rpx"
|
||||||
|
>
|
||||||
|
<view class="mb-14rpx text-26rpx text-#991b1b font-800">
|
||||||
|
失败明细
|
||||||
|
</view>
|
||||||
|
<view class="grid gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-for="item in failedAllocateDetails(prepare)"
|
||||||
|
:key="`${item.rowNo}-${item.itemId}`"
|
||||||
|
class="rounded-8rpx bg-#fef2f2 px-18rpx py-16rpx"
|
||||||
|
>
|
||||||
|
<view class="text-24rpx text-#7f1d1d font-700">
|
||||||
|
第 {{ item.rowNo }} 行:{{ item.itemId || '-' }}
|
||||||
|
</view>
|
||||||
|
<view class="mt-6rpx text-23rpx text-#991b1b leading-32rpx">
|
||||||
|
{{ item.errorReason || '校验失败' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<wd-popup v-model="targetPopupVisible" position="bottom" round safe-area-inset-bottom>
|
||||||
|
<view class="max-h-84vh flex flex-col p-24rpx">
|
||||||
|
<view class="text-32rpx text-#111827 font-900">
|
||||||
|
选择目标客户
|
||||||
|
</view>
|
||||||
|
<view class="mt-20rpx">
|
||||||
|
<wd-input
|
||||||
|
v-model="targetKeyword"
|
||||||
|
label="搜索"
|
||||||
|
label-width="72px"
|
||||||
|
placeholder="客户名称或编码"
|
||||||
|
clearable
|
||||||
|
@confirm="searchTargetPopup"
|
||||||
|
/>
|
||||||
|
<view class="mt-16rpx">
|
||||||
|
<wd-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
block
|
||||||
|
custom-class="biz-mini-action-btn"
|
||||||
|
:loading="loadingTargets"
|
||||||
|
@click="refreshTargetPopup"
|
||||||
|
>
|
||||||
|
<view class="biz-mini-action-btn__inner">
|
||||||
|
<view class="i-carbon-renew biz-mini-action-btn__icon biz-mini-action-btn__icon--blue" />
|
||||||
|
<text class="biz-mini-action-btn__text">刷新目标客户</text>
|
||||||
|
</view>
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="mt-16rpx text-24rpx text-#64748b">
|
||||||
|
{{ loadingTargets ? '正在加载下级代理...' : `已加载 ${targets.length} 个下级代理` }}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view scroll-y class="mt-20rpx h-620rpx min-h-260rpx">
|
||||||
|
<view v-if="targets.length" class="grid gap-12rpx">
|
||||||
|
<view
|
||||||
|
v-for="item in targets"
|
||||||
|
:key="item.deptId"
|
||||||
|
class="border rounded-8rpx border-solid p-20rpx"
|
||||||
|
:class="
|
||||||
|
Number(item.deptId) === Number(selectedTargetId)
|
||||||
|
? 'border-#2563eb bg-#eff6ff'
|
||||||
|
: 'border-#e5e7eb bg-#f8fafc'
|
||||||
|
"
|
||||||
|
@click="selectTarget(item)"
|
||||||
|
>
|
||||||
|
<view class="flex items-center justify-between gap-16rpx">
|
||||||
|
<view class="min-w-0">
|
||||||
|
<view class="truncate text-28rpx text-#111827 font-900">
|
||||||
|
{{ item.deptName }}
|
||||||
|
</view>
|
||||||
|
<view class="mt-8rpx text-23rpx text-#64748b">
|
||||||
|
{{ targetDescription(item) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex shrink-0 items-center gap-8rpx">
|
||||||
|
<wd-tag :type="targetScopeTagType(item)" variant="plain">
|
||||||
|
{{ targetScopeName(item) }}
|
||||||
|
</wd-tag>
|
||||||
|
<wd-tag v-if="Number(item.deptId) === Number(selectedTargetId)" type="primary" variant="plain">
|
||||||
|
已选择
|
||||||
|
</wd-tag>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="p-32rpx text-center text-26rpx text-#64748b">
|
||||||
|
暂无可划拨下级代理
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</wd-popup>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<view
|
||||||
|
v-if="!noPermission && !targetPopupVisible"
|
||||||
|
class="biz-paybar z-1100"
|
||||||
|
>
|
||||||
|
<view class="biz-paybar__inner">
|
||||||
|
<view class="biz-paybar__summary">
|
||||||
|
<text class="biz-paybar__label">已选 {{ selectedCardValues.length }} 张</text>
|
||||||
|
<text class="biz-paybar__value">
|
||||||
|
{{ selectedTarget ? `${targetScopeName(selectedTarget)} · ${targetLabel(selectedTarget)}` : '请选择目标客户' }}
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view class="biz-paybar-actions">
|
||||||
|
<wd-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
block
|
||||||
|
custom-class="biz-paybar-btn"
|
||||||
|
:loading="preparing"
|
||||||
|
:disabled="!canPrepare || preparing"
|
||||||
|
@click="handlePrepare"
|
||||||
|
>
|
||||||
|
校验划拨
|
||||||
|
</wd-button>
|
||||||
|
<wd-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
custom-class="biz-paybar-btn"
|
||||||
|
:loading="submitting"
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="handleSubmit"
|
||||||
|
>
|
||||||
|
确认划拨
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type {
|
||||||
|
BalanceRechargeMockPayParams,
|
||||||
|
BalanceRechargeRefundParams,
|
||||||
|
BalanceRechargeStatusParams,
|
||||||
|
BalanceRechargeStatusRes,
|
||||||
|
BalanceRechargeSubmitParams,
|
||||||
|
BalanceRechargeSubmitRes,
|
||||||
|
} from '@/api/types/balance'
|
||||||
|
import type { AppOrder } from '@/api/types/order'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
BalanceRechargeMockPayParams,
|
||||||
|
BalanceRechargeRefundParams,
|
||||||
|
BalanceRechargeStatusParams,
|
||||||
|
BalanceRechargeStatusRes,
|
||||||
|
BalanceRechargeSubmitParams,
|
||||||
|
BalanceRechargeSubmitRes,
|
||||||
|
} from '@/api/types/balance'
|
||||||
|
|
||||||
|
export function balanceRechargeSubmit(data: BalanceRechargeSubmitParams) {
|
||||||
|
return http.post<BalanceRechargeSubmitRes>(
|
||||||
|
'/balance/recharge-submit',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeStatus(data: BalanceRechargeStatusParams) {
|
||||||
|
return http.get<BalanceRechargeStatusRes>('/balance/recharge-status', data, undefined, {
|
||||||
|
hideErrorToast: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeMockPay(data: BalanceRechargeMockPayParams) {
|
||||||
|
return http.post<BalanceRechargeStatusRes>(
|
||||||
|
'/balance/recharge-mock-pay',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function balanceRechargeRefund(data: BalanceRechargeRefundParams) {
|
||||||
|
return http.post<AppOrder>(
|
||||||
|
'/balance/recharge-refund',
|
||||||
|
data,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
hideErrorToast: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user