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