chore: 并入 iotlcm-app 历史到 app/(原提交 f40bda9)
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
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: mirror-to-gitee
|
||||||
|
|
||||||
|
# 把本仓同步到 Gitee 企业仓 snowgoat/<同名仓库>。
|
||||||
|
# 触发:main 分支推送、v* tag 推送、手动;另有每日兜底重试。
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
schedule:
|
||||||
|
# Gitea Actions 的 cron 用 UTC:18:30 UTC = 次日 02:30 北京时间
|
||||||
|
- cron: '30 18 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: mirror-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mirror:
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: 同步到 Gitee
|
||||||
|
env:
|
||||||
|
SRC_TOKEN: ${{ secrets.SRC_GIT_TOKEN }}
|
||||||
|
DST_TOKEN: ${{ secrets.GITEE_TOKEN }}
|
||||||
|
DST_OWNER: ${{ secrets.GITEE_OWNER }}
|
||||||
|
DST_USER: ${{ secrets.GITEE_USER }}
|
||||||
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
|
run: |
|
||||||
|
set -u
|
||||||
|
|
||||||
|
if [ -z "${REPO_NAME:-}" ]; then
|
||||||
|
echo "未取到仓库名,退出"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
echo "源:git.5iot.com/iotlcm-public/$REPO_NAME"
|
||||||
|
echo "目标:gitee.com/$DST_OWNER/$REPO_NAME"
|
||||||
|
|
||||||
|
git clone --mirror "https://jenkins:${SRC_TOKEN}@git.5iot.com/iotlcm-public/${REPO_NAME}.git" "$WORK/repo.git"
|
||||||
|
cd "$WORK/repo.git"
|
||||||
|
echo "本地 ref:$(git for-each-ref --format='%(refname:short)' | tr '\n' ' ')"
|
||||||
|
|
||||||
|
# Gitee 在推送密集时会返回 429 Pending queue overflowed / 408 Authorization server timeout,
|
||||||
|
# 属服务端限流,退避重试即可。
|
||||||
|
attempt=0
|
||||||
|
while [ "$attempt" -lt 5 ]; do
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
echo "----- 推送尝试 $attempt/5"
|
||||||
|
if git push --mirror "https://${DST_USER}:${DST_TOKEN}@gitee.com/${DST_OWNER}/${REPO_NAME}.git" 2>&1; then
|
||||||
|
echo "✅ 已同步到 Gitee"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
wait_s=$((attempt * 30))
|
||||||
|
echo "推送失败,${wait_s}s 后重试"
|
||||||
|
sleep "$wait_s"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ 连续 5 次推送失败,请检查 Gitee 状态或令牌"
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
name: trigger-jenkins-oss
|
||||||
|
|
||||||
|
# 打 tag 时触发 Jenkins 构建开源镜像并推送到镜像仓库。
|
||||||
|
# 仅在 tag 推送时触发;main 分支的日常提交不触发。
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# 同一 ref 连续触发时取消旧任务,只跑最新的
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger:
|
||||||
|
runs-on: host
|
||||||
|
steps:
|
||||||
|
- name: 触发 Jenkins 镜像构建
|
||||||
|
env:
|
||||||
|
JU: ${{ secrets.JENKINS_USER }}
|
||||||
|
JT: ${{ secrets.JENKINS_API_TOKEN }}
|
||||||
|
JURL: ${{ secrets.JENKINS_URL }}
|
||||||
|
JOB_PATH: ${{ secrets.JENKINS_JOB_PATH }}
|
||||||
|
REPO_NAME: ${{ github.event.repository.name }}
|
||||||
|
TAG_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# 只有 tag 推送才构建;手动触发时按 tag 取,没有就退出
|
||||||
|
case "${GITHUB_REF:-}" in
|
||||||
|
refs/tags/v*) ;;
|
||||||
|
*)
|
||||||
|
if [ "${GITHUB_EVENT_NAME:-}" != "workflow_dispatch" ]; then
|
||||||
|
echo "非 tag 推送,跳过:${GITHUB_REF:-unknown}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${TAG_NAME:-}" ]; then
|
||||||
|
echo "未取到 tag 名,退出"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# job 在 Jenkins 文件夹下的路径(组织密钥 JENKINS_JOB_PATH),
|
||||||
|
# 例如 job/iotlcm-opensrc/job/iotlcm-oss-build。
|
||||||
|
# 用密钥而非硬编码,Jenkins 里移动 job 时不用改 11 个仓。
|
||||||
|
JP="${JOB_PATH:-job/iotlcm-oss-build}"
|
||||||
|
|
||||||
|
echo "仓库:$REPO_NAME 版本:$TAG_NAME job:$JP"
|
||||||
|
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$WORK"' EXIT
|
||||||
|
|
||||||
|
# 取 CSRF crumb
|
||||||
|
curl -s -c "$WORK/cj" -u "$JU:$JT" "$JURL/crumbIssuer/api/json" -o "$WORK/crumb.json"
|
||||||
|
CR=$(python3 -c "import json;print(json.load(open('$WORK/crumb.json'))['crumb'])")
|
||||||
|
|
||||||
|
# 触发参数化任务:SERVICES=仓库名,VERSION=tag
|
||||||
|
code=$(curl -s -o "$WORK/out" -w '%{http_code}' -X POST -b "$WORK/cj" -u "$JU:$JT" \
|
||||||
|
-H "Jenkins-Crumb: $CR" \
|
||||||
|
--data-urlencode "SERVICES=$REPO_NAME" \
|
||||||
|
--data-urlencode "VERSION=$TAG_NAME" \
|
||||||
|
--data-urlencode "FORCE=true" \
|
||||||
|
"$JURL/$JP/buildWithParameters")
|
||||||
|
|
||||||
|
echo "Jenkins 触发 HTTP $code"
|
||||||
|
if [ "$code" != "201" ] && [ "$code" != "200" ] && [ "$code" != "302" ] && [ "$code" != "303" ]; then
|
||||||
|
echo "触发失败:"
|
||||||
|
cat "$WORK/out"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
LINK="$JURL/$JP/"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Jenkins 构建:$LINK"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "::notice title=Jenkins 镜像构建::[$LINK]($LINK)"
|
||||||
@@ -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_*
|
||||||
+201
@@ -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.
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
将 `server/config/config.example.yaml` 复制为 `server/config/config.yaml` 并填写自己的凭据;使用代码生成前同样将 `server/hack/config.example.yaml` 复制为 `server/hack/config.yaml`。
|
||||||
|
|
||||||
|
|
||||||
|
```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.example.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
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Build context: app/
|
||||||
|
# Example:
|
||||||
|
# docker build -f Dockerfile -t iotlcm-app-web:latest .
|
||||||
|
|
||||||
|
# 基础镜像仓库。默认使用 Docker Hub 官方镜像;内网环境可替换为自己的仓库:
|
||||||
|
# docker build --build-arg BASE_REGISTRY=harbor.example.com/library ...
|
||||||
|
ARG BASE_REGISTRY=docker.io/library
|
||||||
|
|
||||||
|
FROM ${BASE_REGISTRY}/node:24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build/app
|
||||||
|
|
||||||
|
COPY package.json pnpm-lock.yaml .npmrc ./
|
||||||
|
COPY patches ./patches
|
||||||
|
|
||||||
|
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=/app/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 ${BASE_REGISTRY}/nginx:1.30-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;"]
|
||||||
@@ -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.2',
|
||||||
|
'versionCode': '102',
|
||||||
|
'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,227 @@
|
|||||||
|
{
|
||||||
|
"name": "iotlcm-app-ui",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"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": "xxx@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",
|
||||||
|
"test:run": "vitest run",
|
||||||
|
"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",
|
||||||
|
"vitest": "3.2.4",
|
||||||
|
"vue-tsc": "^3.0.6"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"unconfig": "7.3.2"
|
||||||
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@wot-ui/ui@2.1.0": "patches/@wot-ui__ui@2.1.0.patch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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,
|
||||||
|
})
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
diff --git a/components/wd-button/wd-button.vue b/components/wd-button/wd-button.vue
|
||||||
|
index 8b2610d90c8ebe78e2726179bb83f2b688ec0d96..c25b26965ccfb9470cc3c2ab9e3b2a1576c3220d 100644
|
||||||
|
--- a/components/wd-button/wd-button.vue
|
||||||
|
+++ b/components/wd-button/wd-button.vue
|
||||||
|
@@ -18,7 +18,7 @@
|
||||||
|
]"
|
||||||
|
:hover-start-time="hoverStartTime"
|
||||||
|
:hover-stay-time="hoverStayTime"
|
||||||
|
- :open-type="openTypeValue"
|
||||||
|
+ :open-type="openTypeValue as UniHelper.ButtonOpenType"
|
||||||
|
:send-message-title="sendMessageTitle"
|
||||||
|
:send-message-path="sendMessagePath"
|
||||||
|
:send-message-img="sendMessageImg"
|
||||||
|
diff --git a/components/wd-fab/types.ts b/components/wd-fab/types.ts
|
||||||
|
index 50ca94f1a95a54c95fe68d9ed3535b45f1e36bcc..57b5ceb1143a0e9385cc280fe8054ed87f255807 100644
|
||||||
|
--- a/components/wd-fab/types.ts
|
||||||
|
+++ b/components/wd-fab/types.ts
|
||||||
|
@@ -87,7 +87,7 @@ export const fabProps = {
|
||||||
|
*/
|
||||||
|
gap: {
|
||||||
|
type: Object as PropType<FabGap>,
|
||||||
|
- default: () => ({})
|
||||||
|
+ default: (): FabGap => ({})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 点击时是否展开菜单
|
||||||
|
diff --git a/components/wd-swiper/wd-swiper.vue b/components/wd-swiper/wd-swiper.vue
|
||||||
|
index c662e565171dad9c350b7215dddd3e8fc11320ab..54a5260a40ed1d6554a7e1b2158f94d8598b1c10 100644
|
||||||
|
--- a/components/wd-swiper/wd-swiper.vue
|
||||||
|
+++ b/components/wd-swiper/wd-swiper.vue
|
||||||
|
@@ -158,9 +158,9 @@ const swiperIndicator = computed(() => {
|
||||||
|
indicatorPosition: indicatorPosition || 'bottom'
|
||||||
|
}
|
||||||
|
if (isObj(indicator)) {
|
||||||
|
- swiperIndicator.type = indicator.type || 'dots'
|
||||||
|
- swiperIndicator.minShowNum = indicator.minShowNum || 2
|
||||||
|
- swiperIndicator.showControls = indicator.showControls || false
|
||||||
|
+ swiperIndicator.type = (indicator as Partial<SwiperNavProps>).type || 'dots'
|
||||||
|
+ swiperIndicator.minShowNum = (indicator as Partial<SwiperNavProps>).minShowNum || 2
|
||||||
|
+ swiperIndicator.showControls = (indicator as Partial<SwiperNavProps>).showControls || false
|
||||||
|
}
|
||||||
|
return swiperIndicator
|
||||||
|
})
|
||||||
|
diff --git a/components/wd-tour/wd-tour.vue b/components/wd-tour/wd-tour.vue
|
||||||
|
index d1c6e895a819a4e15178930c3869a7034d741887..2eacba3bb4b20621d94465d17a94d88ccea6195e 100644
|
||||||
|
--- a/components/wd-tour/wd-tour.vue
|
||||||
|
+++ b/components/wd-tour/wd-tour.vue
|
||||||
|
@@ -69,7 +69,7 @@ export default {
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, computed, watch, nextTick, type CSSProperties } from 'vue'
|
||||||
|
import { addUnit, getRect, getSystemInfo, isDef, objToStyle } from '../../common/util'
|
||||||
|
-import { tourProps, type TourEmits, type TourExpose } from './types'
|
||||||
|
+import { tourProps, type TourEmits, type TourExpose, type TourStep } from './types'
|
||||||
|
import { useRaf } from '../../composables/useRaf'
|
||||||
|
import { useTranslate } from '../../composables/useTranslate'
|
||||||
|
import wdButton from '../wd-button/wd-button.vue'
|
||||||
|
@@ -122,7 +122,7 @@ const highlightClass = computed(() => {
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
-const currentStep = computed(() => {
|
||||||
|
+const currentStep = computed<Partial<TourStep>>(() => {
|
||||||
|
return props.steps[currentIndex.value] || {}
|
||||||
|
})
|
||||||
|
// 提取公共的默认样式函数
|
||||||
|
diff --git a/composables/useUpload.ts b/composables/useUpload.ts
|
||||||
|
index acad05a461562cb197299623100371280d559918..83b02be95ebb380b8d1cd9f03a742a8727ece502 100644
|
||||||
|
--- a/composables/useUpload.ts
|
||||||
|
+++ b/composables/useUpload.ts
|
||||||
|
@@ -174,7 +174,7 @@ export function useUpload(): UseUploadReturn {
|
||||||
|
path: item.path || '',
|
||||||
|
name: item.name || '',
|
||||||
|
size: item.size,
|
||||||
|
- type: 'image',
|
||||||
|
+ type: 'image' as const,
|
||||||
|
thumb: item.path || ''
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
@@ -183,7 +183,7 @@ export function useUpload(): UseUploadReturn {
|
||||||
|
path: (res.tempFiles as any).path || '',
|
||||||
|
name: (res.tempFiles as any).name || '',
|
||||||
|
size: (res.tempFiles as any).size,
|
||||||
|
- type: 'image',
|
||||||
|
+ type: 'image' as const,
|
||||||
|
thumb: (res.tempFiles as any).path || ''
|
||||||
|
}
|
||||||
|
]
|
||||||
Generated
+22293
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,49 @@
|
|||||||
|
<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,52 @@
|
|||||||
|
<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'
|
||||||
|
import { useMessageStore } from '@/store/message'
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
void useMessageStore().refreshUnreadCount()
|
||||||
|
// 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,220 @@
|
|||||||
|
import type {
|
||||||
|
AllocateBatchPrepareParams,
|
||||||
|
AllocateBatchRes,
|
||||||
|
AllocateBatchSubmitParams,
|
||||||
|
AllocateTargetListParams,
|
||||||
|
AllocateTargetListRes,
|
||||||
|
BindCardParams,
|
||||||
|
CardLifecycleParams,
|
||||||
|
CardLifecycleRes,
|
||||||
|
CardLifecycleStatusParams,
|
||||||
|
CardListParams,
|
||||||
|
CardListRes,
|
||||||
|
CardQueryRes,
|
||||||
|
CardRecognitionRes,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CardRecognitionPayload {
|
||||||
|
code?: number
|
||||||
|
message?: string
|
||||||
|
data?: CardRecognitionRes
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCardRecognitionPayload(data: unknown): CardRecognitionPayload {
|
||||||
|
if (data && typeof data === 'object')
|
||||||
|
return data as CardRecognitionPayload
|
||||||
|
|
||||||
|
const text = String(data ?? '').replace(/^\uFEFF/, '').trim()
|
||||||
|
return text ? JSON.parse(text) as CardRecognitionPayload : {}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: CardRecognitionPayload
|
||||||
|
try {
|
||||||
|
payload = parseCardRecognitionPayload(response.data)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
const headers = (response as typeof response & { header?: Record<string, string> }).header
|
||||||
|
const contentType = headers?.['content-type'] || headers?.['Content-Type']
|
||||||
|
const responseType = contentType ? `,${contentType}` : ''
|
||||||
|
reject(new Error(`图片识别响应格式错误(HTTP ${response.statusCode}${responseType})`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode < 200 || response.statusCode >= 300 || Number(payload.code) !== 0) {
|
||||||
|
reject(new Error(payload.message || '图片识别失败'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve(payload.data || {
|
||||||
|
code: 'RECOGNITION_FAILED',
|
||||||
|
message: '图片识别结果为空',
|
||||||
|
cardType: 'iccid',
|
||||||
|
cardValue: '',
|
||||||
|
confidence: 0,
|
||||||
|
candidates: [],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
reject(new Error(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>('/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,57 @@
|
|||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export interface AllocationUnits {
|
||||||
|
sourceDeptId?: number
|
||||||
|
sourceDeptName?: string
|
||||||
|
targetDeptId?: number
|
||||||
|
targetDeptName?: string
|
||||||
|
targetDeptMode?: 'single' | 'per_row'
|
||||||
|
}
|
||||||
|
export function allocationUnitName(name?: string, id?: number) {
|
||||||
|
return name || (id ? '单位已删除或信息未记录' : '单位信息未记录')
|
||||||
|
}
|
||||||
|
export function receivingUnitName(item: AllocationUnits) {
|
||||||
|
return item.targetDeptMode === 'per_row' ? '按卡片明细接收' : allocationUnitName(item.targetDeptName, item.targetDeptId)
|
||||||
|
}
|
||||||
|
export interface ExecutionLog extends AllocationUnits {
|
||||||
|
batchNo: string
|
||||||
|
batchType: number
|
||||||
|
sourceChannel: string
|
||||||
|
executionMode: number
|
||||||
|
sourceType: number | null
|
||||||
|
batchAsyncStatus: number
|
||||||
|
totalCount: number
|
||||||
|
runResult: number
|
||||||
|
failReason?: string
|
||||||
|
createdAt?: string
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
export interface ExecutionDetail extends AllocationUnits {
|
||||||
|
itemId: string
|
||||||
|
rowNo: number
|
||||||
|
detailStatus: number
|
||||||
|
errorCode?: string
|
||||||
|
errorReason?: string
|
||||||
|
}
|
||||||
|
export const channelLabels: Record<string, string> = { console: '控制台', mini_program: '小程序', api: '开放API', system: '系统任务', legacy: '历史未知' }
|
||||||
|
export const modeLabels = ['未知', '单卡', '批量']
|
||||||
|
export const inputLabels = ['选择', '连号', '输入', '文件', '定时任务']
|
||||||
|
export const statusLabels = ['已创建', '待处理', '处理中', '成功', '失败', '部分成功']
|
||||||
|
export const detailLabels = ['待处理', '处理中', '成功', '失败']
|
||||||
|
export function listExecutionLogs(params: Record<string, unknown>) {
|
||||||
|
return http.get<{ items: ExecutionLog[], total: number }>('/execution-log/list', params, undefined, { hideErrorToast: true })
|
||||||
|
}
|
||||||
|
export function executionSummary(batchNo: string) {
|
||||||
|
return http.get<ExecutionLog>('/execution-log/summary', { batchNo }, undefined, { hideErrorToast: true })
|
||||||
|
}
|
||||||
|
export function executionDetails(params: Record<string, unknown>) {
|
||||||
|
return http.get<{ items: ExecutionDetail[], total: number }>('/execution-log/details', params, undefined, { hideErrorToast: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allocateSummary(params: { batchNo?: string, requestId?: string }) {
|
||||||
|
return http.get<ExecutionLog>('/card/allocate-result', params, undefined, { hideErrorToast: true })
|
||||||
|
}
|
||||||
|
export function allocateDetails(params: Record<string, unknown>) {
|
||||||
|
return http.get<{ items: ExecutionDetail[], total: number }>('/card/allocate-details', params, 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,180 @@
|
|||||||
|
import type {
|
||||||
|
IAuthLoginRes,
|
||||||
|
ICheckSideCaptchaForm,
|
||||||
|
IForgotPasswordForm,
|
||||||
|
ILoginForm,
|
||||||
|
IPhoneSmsLoginForm,
|
||||||
|
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 phoneSmsLogin(data: IPhoneSmsLoginForm) {
|
||||||
|
return http.post<IAuthLoginRes>(
|
||||||
|
'/auth/phone-login',
|
||||||
|
data,
|
||||||
|
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)),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WarningPreference {
|
||||||
|
enabled: boolean
|
||||||
|
wechatBound: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWarningPreference() {
|
||||||
|
return http.get<WarningPreference>('/user/wechat/warning-preference', undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveWarningPreference(enabled: boolean) {
|
||||||
|
return http.post<WarningPreference>('/user/wechat/warning-preference', { enabled }, undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unbindWechat() {
|
||||||
|
return http.post<IWechatBindRes>('/user/wechat/unbind', {}, undefined, undefined, AUTH_REQUEST_OPTIONS)
|
||||||
|
}
|
||||||
@@ -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,399 @@
|
|||||||
|
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
|
||||||
|
allocateOnly?: boolean
|
||||||
|
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 {
|
||||||
|
executionMode?: number
|
||||||
|
cardValues: string[]
|
||||||
|
targetDeptId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllocateBatchSubmitParams {
|
||||||
|
requestId?: string
|
||||||
|
executionMode?: number
|
||||||
|
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 {
|
||||||
|
requestId?: string
|
||||||
|
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,60 @@
|
|||||||
|
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
|
||||||
|
cardStatusCounts?: { status: number, count: number }[]
|
||||||
|
packageStatusCounts?: { status: number, count: 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
|
||||||
|
summary: {
|
||||||
|
total: number
|
||||||
|
normalCount: number
|
||||||
|
pendingProfileCount: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDetailParams {
|
||||||
|
customerType: Exclude<AppCustomerType, 'all'> | string
|
||||||
|
customerId: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomerDetailRes {
|
||||||
|
customer: AppCustomerDetail
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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
|
||||||
|
imei?: string
|
||||||
|
status?: string
|
||||||
|
dataStatus?: string
|
||||||
|
dataServiceSource?: 'apn' | 'network_block'
|
||||||
|
netBlockStatus?: string
|
||||||
|
netBlockReason?: 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
|
||||||
|
reason?: 'not_subscribed' | 'unsupported' | 'no_data' | 'query_error' | string
|
||||||
|
message?: string
|
||||||
|
cached?: boolean
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DiagnosisFlowRes {
|
||||||
|
times: string[]
|
||||||
|
flow: number[]
|
||||||
|
flowAvailable?: boolean[]
|
||||||
|
unit: string
|
||||||
|
granularity?: 'day' | 'month' | string
|
||||||
|
hasData?: boolean
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
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 IPhoneSmsLoginForm {
|
||||||
|
tenantId: string
|
||||||
|
phone: string
|
||||||
|
smsCode: string
|
||||||
|
selectedUserId?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatLoginForm {
|
||||||
|
tenantId: string
|
||||||
|
wxCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWechatBindPhoneForm {
|
||||||
|
tenantId: string
|
||||||
|
bindTicket: string
|
||||||
|
phone: string
|
||||||
|
smsCode: 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 {
|
||||||
|
isTopLevelEnterprise?: boolean
|
||||||
|
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' | 'login' | 'forgotPassword' | 'wechatBindPhone'
|
||||||
|
captchaKey: string
|
||||||
|
captchaPoint: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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,129 @@
|
|||||||
|
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
|
||||||
|
activeConfigCount?: number
|
||||||
|
notifyConfigCount?: 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,95 @@
|
|||||||
|
export interface WorkOrderListParams {
|
||||||
|
scope?: 'all' | 'submitted' | 'received'
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
keyword?: string
|
||||||
|
currentStatus?: number
|
||||||
|
woType?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderDetailParams {
|
||||||
|
beforeId?: number
|
||||||
|
woNo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderCreateParams {
|
||||||
|
creatorPhone: string
|
||||||
|
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 type WorkOrderAction = 'reply' | 'reopen' | 'confirm' | 'cancel' | 'evaluate' | 'transfer' | 'resolve' | 'close'
|
||||||
|
|
||||||
|
export interface WorkOrderActionParams {
|
||||||
|
woNo: string
|
||||||
|
action: WorkOrderAction
|
||||||
|
content?: string
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
satisfactionLevel?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkOrderProgress {
|
||||||
|
id: number
|
||||||
|
operationType: number
|
||||||
|
operationText: string
|
||||||
|
beforeStatus: number
|
||||||
|
afterStatus: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppWorkOrder {
|
||||||
|
attachmentNames?: Record<string, string>
|
||||||
|
isCreator?: boolean
|
||||||
|
hasMoreCommunications?: boolean
|
||||||
|
attachmentUrls?: string[]
|
||||||
|
actions: WorkOrderAction[]
|
||||||
|
records?: WorkOrderProgress[]
|
||||||
|
resolveTime?: string
|
||||||
|
closeTime?: string
|
||||||
|
satisfactionLevel: number
|
||||||
|
evalContent?: string
|
||||||
|
evalTime?: string
|
||||||
|
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,41 @@
|
|||||||
|
import type {
|
||||||
|
WorkOrderActionParams,
|
||||||
|
WorkOrderCreateParams,
|
||||||
|
WorkOrderCreateRes,
|
||||||
|
WorkOrderDetailParams,
|
||||||
|
WorkOrderDetailRes,
|
||||||
|
WorkOrderListParams,
|
||||||
|
WorkOrderListRes,
|
||||||
|
} from './types/workOrder'
|
||||||
|
import { http } from '@/http/http'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AppWorkOrder,
|
||||||
|
AppWorkOrderCommunication,
|
||||||
|
WorkOrderActionParams,
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function actOnWorkOrder(data: WorkOrderActionParams) {
|
||||||
|
return http.post('/work-order/action', data, undefined, undefined, { hideErrorToast: true })
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<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,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { isWorkOrderImage } from '@/utils/workOrderAttachment'
|
||||||
|
|
||||||
|
const props = defineProps<{ url: string, name: string }>()
|
||||||
|
const emit = defineEmits<{ open: [] }>()
|
||||||
|
const failed = ref(false)
|
||||||
|
const loaded = ref(false)
|
||||||
|
const isImage = computed(() => isWorkOrderImage(props.url, props.name))
|
||||||
|
watch(() => props.url, () => {
|
||||||
|
failed.value = false
|
||||||
|
loaded.value = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view v-if="isImage" class="w-full overflow-hidden rounded-12rpx bg-#f1f5f9" @click="emit('open')">
|
||||||
|
<view class="relative h-176rpx w-full">
|
||||||
|
<view v-if="!loaded || failed" class="absolute inset-0 flex flex-col items-center justify-center gap-8rpx text-#64748b">
|
||||||
|
<wd-icon name="image" size="28" />
|
||||||
|
<text class="text-22rpx">{{ failed ? '点击查看原图' : '图片加载中' }}</text>
|
||||||
|
</view>
|
||||||
|
<image
|
||||||
|
v-if="!failed"
|
||||||
|
:src="url"
|
||||||
|
mode="aspectFill"
|
||||||
|
class="relative h-176rpx w-full"
|
||||||
|
@load="loaded = true"
|
||||||
|
@error="failed = true"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="truncate px-12rpx py-10rpx text-22rpx text-#475569">
|
||||||
|
{{ name }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="w-full overflow-hidden rounded-12rpx bg-#f1f5f9">
|
||||||
|
<wd-cell :title="name" icon="file" is-link custom-class="!bg-transparent" custom-title-class="!truncate !text-24rpx" @click="emit('open')" />
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { createContext, runInContext } from 'node:vm'
|
||||||
|
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import { workOrderAttachmentName } from '@/utils/workOrderAttachment'
|
||||||
|
|
||||||
|
const source = readFileSync(new URL('./WorkOrderAttachments.vue', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
describe('工单上传原始文件名', () => {
|
||||||
|
it('微信上传携带原名,并保留URL字符串协议', async () => {
|
||||||
|
const emit = vi.fn()
|
||||||
|
const upload = vi.fn((options) => {
|
||||||
|
options.success({ statusCode: 200, data: JSON.stringify({ code: 0, data: { url: 'https://files/random.pdf', originalName: '说明.pdf' } }) })
|
||||||
|
})
|
||||||
|
const context = createContext({
|
||||||
|
exports: {},
|
||||||
|
require: (name: string) => name === '@/utils/workOrderAttachment' ? { workOrderAttachmentName } : name === '@/utils' ? { getEnvBaseUrl: () => 'https://api/api/v1' } : {},
|
||||||
|
defineProps: () => ({ modelValue: [], disabled: false }),
|
||||||
|
defineEmits: () => emit,
|
||||||
|
ref: (value: unknown) => ({ value }),
|
||||||
|
uni: { uploadFile: upload, showToast: vi.fn() },
|
||||||
|
})
|
||||||
|
const script = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1]
|
||||||
|
if (!script)
|
||||||
|
throw new Error('未找到附件上传脚本')
|
||||||
|
runInContext(transpileModule(script, { compilerOptions: { module: ModuleKind.CommonJS, target: ScriptTarget.ES2020 } }).outputText, context)
|
||||||
|
await runInContext('upload("wxfile://random.pdf", "说明.pdf", 20)', context)
|
||||||
|
expect(upload).toHaveBeenCalledWith(expect.objectContaining({ url: '/work-order/upload', formData: { fileType: 'file', originalName: '说明.pdf' } }))
|
||||||
|
expect(emit).toHaveBeenCalledWith('update:modelValue', ['https://files/random.pdf'])
|
||||||
|
expect(runInContext('nameOf("https://files/random.pdf")', context)).toBe('说明.pdf')
|
||||||
|
expect(source).toContain('form.append(\'originalName\', name)')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useTokenStore } from '@/store/token'
|
||||||
|
import { getEnvBaseUrl } from '@/utils'
|
||||||
|
import { workOrderAttachmentName } from '@/utils/workOrderAttachment'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: string[], disabled?: boolean }>()
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string[]]
|
||||||
|
'uploading': [value: boolean]
|
||||||
|
}>()
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
const attachmentNames = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
function nameOf(url: string) {
|
||||||
|
return workOrderAttachmentName(url, attachmentNames.value[url])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(file: string | File, name: string, size?: number) {
|
||||||
|
if (uploading.value || props.disabled)
|
||||||
|
return
|
||||||
|
if ((size || 0) > 10 * 1024 * 1024) {
|
||||||
|
uni.showToast({ title: '单个附件不能超过10MB', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploading.value = true
|
||||||
|
emit('uploading', true)
|
||||||
|
try {
|
||||||
|
let body: { code: number, msg?: string, message?: string, data?: { url?: string, path?: string, originalName?: string } }
|
||||||
|
// #ifdef H5
|
||||||
|
if (typeof file !== 'string') {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('file', file, name)
|
||||||
|
form.append('fileType', 'file')
|
||||||
|
form.append('originalName', name)
|
||||||
|
const token = useTokenStore().updateNowTime().validToken
|
||||||
|
const response = await fetch(`${getEnvBaseUrl()}/work-order/upload`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
|
||||||
|
body: form,
|
||||||
|
})
|
||||||
|
if (!response.ok)
|
||||||
|
throw new Error('附件上传失败')
|
||||||
|
body = await response.json()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// #endif
|
||||||
|
body = await new Promise((resolve, reject) => {
|
||||||
|
uni.uploadFile({
|
||||||
|
url: '/work-order/upload',
|
||||||
|
filePath: file as string,
|
||||||
|
name: 'file',
|
||||||
|
formData: { fileType: 'file', originalName: name },
|
||||||
|
success: (res) => {
|
||||||
|
try {
|
||||||
|
if (res.statusCode < 200 || res.statusCode >= 300)
|
||||||
|
throw new Error('附件上传失败')
|
||||||
|
resolve(JSON.parse(res.data))
|
||||||
|
}
|
||||||
|
catch (error) { reject(error) }
|
||||||
|
},
|
||||||
|
fail: reject,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// #ifdef H5
|
||||||
|
}
|
||||||
|
// #endif
|
||||||
|
if (Number(body.code) !== 0)
|
||||||
|
throw new Error(body.message || body.msg || '附件上传失败')
|
||||||
|
const url = body.data?.url || body.data?.path
|
||||||
|
if (!url)
|
||||||
|
throw new Error('上传接口未返回文件地址')
|
||||||
|
attachmentNames.value[url] = body.data?.originalName || name
|
||||||
|
emit('update:modelValue', [...props.modelValue, url])
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
uni.showToast({ title: error instanceof Error ? error.message : '附件上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
uploading.value = false
|
||||||
|
emit('uploading', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseFile() {
|
||||||
|
// #ifdef H5
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files?.[0]
|
||||||
|
if (file)
|
||||||
|
void upload(file, file.name, file.size)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
return
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP-WEIXIN
|
||||||
|
uni.chooseMessageFile({
|
||||||
|
count: 1,
|
||||||
|
type: 'all',
|
||||||
|
success: (res) => {
|
||||||
|
const file = res.tempFiles[0]
|
||||||
|
if (file)
|
||||||
|
void upload(file.path, file.name, file.size)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
// #endif
|
||||||
|
// #ifndef H5
|
||||||
|
// #ifndef MP-WEIXIN
|
||||||
|
uni.showToast({ title: '当前平台请使用图片附件', icon: 'none' })
|
||||||
|
// #endif
|
||||||
|
// #endif
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose() {
|
||||||
|
if (uploading.value || props.disabled)
|
||||||
|
return
|
||||||
|
if (props.modelValue.length >= 6) {
|
||||||
|
uni.showToast({ title: '最多上传6个附件', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showActionSheet({
|
||||||
|
itemList: ['拍照或选择图片', '选择文件'],
|
||||||
|
success: ({ tapIndex }) => {
|
||||||
|
if (tapIndex === 1) {
|
||||||
|
chooseFile()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.chooseImage({
|
||||||
|
count: 1,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['album', 'camera'],
|
||||||
|
success: (res) => {
|
||||||
|
const files = res.tempFiles as UniApp.ChooseImageSuccessCallbackResult['tempFiles']
|
||||||
|
const file = Array.isArray(files) ? files[0] : undefined
|
||||||
|
const path = res.tempFilePaths[0]
|
||||||
|
if (path)
|
||||||
|
void upload(path, workOrderAttachmentName(path), file?.size)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="flex flex-col gap-12rpx">
|
||||||
|
<view class="flex items-center justify-between gap-16rpx">
|
||||||
|
<text class="text-24rpx text-#64748b">附件 {{ modelValue.length }}/6,单个不超过10MB</text>
|
||||||
|
<wd-button size="small" type="primary" variant="text" icon="add" :disabled="disabled || modelValue.length >= 6" :loading="uploading" @click="choose">
|
||||||
|
添加附件
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
<view v-for="(url, index) in modelValue" :key="`${url}-${index}`" class="flex items-center justify-between gap-16rpx">
|
||||||
|
<text class="min-w-0 flex-1 truncate text-24rpx text-#334155">{{ nameOf(url) }}</text>
|
||||||
|
<wd-button size="small" type="danger" variant="text" :disabled="disabled || uploading" @click="emit('update:modelValue', modelValue.filter((_, i) => i !== index))">
|
||||||
|
移除
|
||||||
|
</wd-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*******************************
|
||||||
|
* 此文件由脚本自动生成,请勿手动修改 *
|
||||||
|
*******************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面路径常量
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
/** 批次执行日志 */
|
||||||
|
'UserViewsExecutionLog': '/pages/user/views/executionLog/index',
|
||||||
|
/** 执行日志详情 */
|
||||||
|
'UserViewsExecutionLogDetail': '/pages/user/views/executionLogDetail/index',
|
||||||
|
/** 帮助与反馈 */
|
||||||
|
'UserViewsFeedback': '/pages/user/views/feedback/index',
|
||||||
|
/** 使用手册 */
|
||||||
|
'UserViewsManual': '/pages/user/views/manual/index',
|
||||||
|
/** 消息提醒 */
|
||||||
|
'UserViewsNotification': '/pages/user/views/notification/index',
|
||||||
|
/** 个人资料 */
|
||||||
|
'UserViewsProfile': '/pages/user/views/profile/index',
|
||||||
|
/** 实名信息 */
|
||||||
|
'UserViewsRealName': '/pages/user/views/realName/index',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouterPath
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* v1.0.2 front-end release scope (legacy export names retained).
|
||||||
|
*
|
||||||
|
* Business, customer management and authorized allocation are now available.
|
||||||
|
* Other unreleased lifecycle actions remain hidden. The API layer is unchanged;
|
||||||
|
* these switches keep every front-end entry point consistent.
|
||||||
|
*/
|
||||||
|
export const V101_RELEASE = {
|
||||||
|
showWechatLogin: false,
|
||||||
|
showRegister: false,
|
||||||
|
showBindCard: false,
|
||||||
|
showCardMoreActions: false,
|
||||||
|
showBusinessTab: true,
|
||||||
|
showUserBusinessEntries: false,
|
||||||
|
showDiagnosisFlowForNonAdmin: false,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
/** Routes that must remain unreachable from direct front-end navigation. */
|
||||||
|
export const V101_HIDDEN_ROUTE_PATHS = new Set([
|
||||||
|
'/pages/business/views/addon/index',
|
||||||
|
'/pages/business/views/balanceRecharge/index',
|
||||||
|
'/pages/business/views/cardRenew/index',
|
||||||
|
'/pages/business/views/change/index',
|
||||||
|
'/pages/business/views/order/index',
|
||||||
|
'/pages/business/views/renew/index',
|
||||||
|
'/pages/business/views/warning/index',
|
||||||
|
'/pages/card/views/packageDetail/index',
|
||||||
|
'/pages/card/views/warning/index',
|
||||||
|
])
|
||||||
|
|
||||||
|
export function isV101HiddenRoute(path: string) {
|
||||||
|
return V101_HIDDEN_ROUTE_PATHS.has(path)
|
||||||
|
}
|
||||||
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,39 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
const config = vi.hoisted(() => ({ baseUrl: '/app/api/v1' }))
|
||||||
|
|
||||||
|
vi.mock('@/utils', () => ({ getEnvBaseUrl: () => config.baseUrl }))
|
||||||
|
vi.mock('@/store', () => ({
|
||||||
|
useTokenStore: () => ({ updateNowTime: () => ({ validToken: 'test-token' }) }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
vi.resetModules()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('工单附件上传地址', () => {
|
||||||
|
it.each([
|
||||||
|
'/app/api/v1',
|
||||||
|
'http://localhost:8010/api/v1',
|
||||||
|
'https://example.com/app/api/v1',
|
||||||
|
])('基础地址 %s 只拼接一次,并保留鉴权', async (baseUrl) => {
|
||||||
|
config.baseUrl = baseUrl
|
||||||
|
const addInterceptor = vi.fn()
|
||||||
|
vi.stubGlobal('uni', { addInterceptor })
|
||||||
|
const { requestInterceptor } = await import('./interceptor')
|
||||||
|
requestInterceptor.install()
|
||||||
|
const uploadInterceptor = addInterceptor.mock.calls.find(([name]) => name === 'uploadFile')?.[1]
|
||||||
|
expect(uploadInterceptor).toBeDefined()
|
||||||
|
const options = uploadInterceptor.invoke({ url: '/work-order/upload' })
|
||||||
|
expect(options.url).toBe(`${baseUrl}/work-order/upload`)
|
||||||
|
expect(options.header.Authorization).toBe('Bearer test-token')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('原生上传交由拦截器拼接,H5 fetch 保留基础地址', () => {
|
||||||
|
const source = readFileSync(new URL('../components/WorkOrderAttachments.vue', import.meta.url), 'utf8')
|
||||||
|
expect(source).toMatch(/uni\.uploadFile\(\{\s*url: '\/work-order\/upload'/)
|
||||||
|
expect(source).toMatch(/fetch\(`\$\{getEnvBaseUrl\(\)\}\/work-order\/upload`/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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|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.2",
|
||||||
|
"versionCode": "102",
|
||||||
|
"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,367 @@
|
|||||||
|
{
|
||||||
|
"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": "工单列表",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "实名信息"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "manual/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "使用手册"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "executionLog/index",
|
||||||
|
"type": "page",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "批次执行日志"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "executionLogDetail/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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user