chore: 移除第三方文档目录及其相关文件
移除doc/thirdParty目录及其所有内容,包括Vue组件、样式、配置文件和测试文件等
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(mkdir:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
# 配置项文档:https://editorconfig.org(修改配置后重启编辑器)
|
||||
|
||||
## 告知 EditorConfig 插件,当前即是根文件
|
||||
root = true
|
||||
|
||||
## 适用全部文件
|
||||
[*]
|
||||
### 设置字符集
|
||||
charset = utf-8
|
||||
### 缩进风格 space | tab,建议 space
|
||||
indent_style = space
|
||||
### 缩进的空格数
|
||||
indent_size = 2
|
||||
### 换行符类型 lf | cr | crlf,一般都是设置为 lf
|
||||
end_of_line = lf
|
||||
### 是否在文件末尾插入空白行
|
||||
insert_final_newline = true
|
||||
### 是否删除一行中的前后空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
## 适用 .md 文件
|
||||
[*.md]
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# 所有环境的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 项目标题
|
||||
VITE_APP_TITLE = 智柜通
|
||||
|
||||
## 路由模式 hash 或 html5
|
||||
VITE_ROUTER_HISTORY = hash
|
||||
|
||||
## 是否开启 console 调试工具
|
||||
VITE_CONSOLE = false
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# 开发环境的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
|
||||
VITE_BASE_URL = http://localhost:8090/api
|
||||
|
||||
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
|
||||
VITE_PUBLIC_PATH = /
|
||||
|
||||
# 后端地址
|
||||
VITE_APP_BASE_API = '/dev-api'
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# 生产环境的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||
VITE_BASE_URL = '/shop-api/api'
|
||||
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/mobvue/ 域名下就需要填写 /mobvue/)
|
||||
VITE_PUBLIC_PATH = /shop/
|
||||
|
||||
# 后端地址
|
||||
VITE_APP_BASE_API = '/shop-back-end'
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
# 预发布环境的环境变量(命名必须以 VITE_ 开头)
|
||||
|
||||
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
|
||||
VITE_BASE_URL = '/shop-api/api'
|
||||
|
||||
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /)
|
||||
VITE_PUBLIC_PATH = /shop/
|
||||
|
||||
# 后端地址
|
||||
VITE_APP_BASE_API = '/shop-back-end'
|
||||
|
|
@ -1 +0,0 @@
|
|||
custom: https://github.com/un-pany/mobvue/issues/1
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
name: Build And Deploy MobVue
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@master
|
||||
with:
|
||||
node-version: 22.12.0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 10.2.0
|
||||
|
||||
- name: Build
|
||||
run: pnpm install && pnpm build
|
||||
|
||||
- name: Deploy
|
||||
uses: JamesIves/github-pages-deploy-action@releases/v3
|
||||
with:
|
||||
ACCESS_TOKEN: ${{ secrets.MOBVUE }}
|
||||
BRANCH: gh-pages
|
||||
FOLDER: dist
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
name: Release
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
node-version: lts/*
|
||||
|
||||
- run: npx changelogithub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.MOBVUE }}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
# Common
|
||||
dist
|
||||
node_modules
|
||||
.eslintcache
|
||||
vite.config.*.timestamp*
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
# Local env files
|
||||
*.local
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Use the pnpm
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
/.svn
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
# 全局 ts 类型检查(此操作会增加 git commit 时长)
|
||||
# npx vue-tsc
|
||||
# 执行 lint-staged 中配置的任务
|
||||
# npx lint-staged
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# China mirror of npm
|
||||
registry = https://registry.npmmirror.com
|
||||
|
||||
# 安装依赖时锁定版本号
|
||||
save-exact = true
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025-present pany <https://github.com/pany-ang>
|
||||
|
||||
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.
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
<div align="center">
|
||||
<img alt="logo" width="120" height="120" src="./public/favicon.ico">
|
||||
<h1>Mobile + Vue = MobVue</h1>
|
||||
</div>
|
||||
|
||||
[](https://github.com/un-pany/mobvue/releases)
|
||||
[](https://github.com/un-pany/mobvue/stargazers)
|
||||
[](https://gitee.com/un-pany/mobvue/stargazers)
|
||||
|
||||
<b>English | <a href="./README.zh-CN.md">中文</a></b>
|
||||
|
||||
## Introduction
|
||||
|
||||
MobVue is a well-crafted mobile web app template, built with popular technologies such as Vue3, Vite, TypeScript, and Vant
|
||||
|
||||
## Notifications
|
||||
|
||||
> [!NOTE]
|
||||
> Powered by love! All source code is free and open-source. If you find it helpful, feel free to give a star to support!
|
||||
|
||||
> [!TIP]
|
||||
> Paid services are officially launched! If you don’t want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/mobvue/issues/2)
|
||||
|
||||
## Usage
|
||||
|
||||
<details>
|
||||
<summary>Recommended Environment</summary>
|
||||
|
||||
<br>
|
||||
|
||||
- Latest version of `Visual Studio Code`
|
||||
- Install the recommended plugins in the `.vscode/extensions.json` file
|
||||
- `node` 20.x or 22+
|
||||
- `pnpm` 9.x or 10+
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Local Development</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
pnpm i
|
||||
|
||||
# Start the development server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Build</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# Build for the staging environment
|
||||
pnpm build:staging
|
||||
|
||||
# Build for the production environment
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Local Preview</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# Execute the build command first to generate the dist directory, then run the preview command
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Code Check</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# Code linting and formatting
|
||||
pnpm lint
|
||||
|
||||
# Unit tests
|
||||
pnpm test
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Commit Guidelines</summary>
|
||||
|
||||
<br>
|
||||
|
||||
`feat` New feature
|
||||
|
||||
`fix` Bug fix
|
||||
|
||||
`perf` Performance improvement
|
||||
|
||||
`refactor` Code refactoring
|
||||
|
||||
`docs` Documentation and comments
|
||||
|
||||
`types` Type-related changes
|
||||
|
||||
`test` Unit tests related
|
||||
|
||||
`ci` Continuous integration, workflows
|
||||
|
||||
`revert` Revert changes
|
||||
|
||||
`chore` Chores (update dependencies, modify configurations, etc)
|
||||
|
||||
</details>
|
||||
|
||||
## Links
|
||||
|
||||
**Online Preview**:[github-pages](https://un-pany.github.io/mobvue)
|
||||
|
||||
**Documentation and Tutorials**:[link](https://juejin.cn/column/7472609448201666599)
|
||||
|
||||
**Chinese Repository**:[gitee](https://gitee.com/un-pany/mobvue)
|
||||
|
||||
**Chat Group**:[check how to join](https://github.com/un-pany/mobvue/issues/3)
|
||||
|
||||
**Donations**:[buy a coffee for the author](https://github.com/un-pany/mobvue/issues/1)
|
||||
|
||||
**Releases & Changelog**:[releases](https://github.com/un-pany/mobvue/releases)
|
||||
|
||||
## Features
|
||||
|
||||
🔥 Latest [Syntax](https://vuejs.org/api/sfc-script-setup.html), [Configuration](./vite.config.ts), [Dependencies](./package.json)
|
||||
|
||||
📍 [Pure Level 1 Route Design](./src/router/index.ts) - Clear and Cache-Friendly
|
||||
|
||||
📱 Mobile Adaptation [px2vw](./postcss.config.ts) - Also Wide-Screen Friendly
|
||||
|
||||
🌐 Browser Compatibility [@vitejs/plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) + [autoprefixer](https://github.com/postcss/autoprefixer) + [browserslist](https://github.com/browserslist/browserslist) - Compatible with multiple browsers and lower versions
|
||||
|
||||
🧩 [Layout System](./src/layout) - Configurable
|
||||
|
||||
🔒 Permission Control [Page Level](./src/router/guard.ts), [Button Level](./src/pages/demo/permission.vue)
|
||||
|
||||
🌗 Theme Mode [Dark Mode](./src/common/assets/styles/variables.css)
|
||||
|
||||
🫧 [Embrace Atomic CSS](./uno.config.ts)
|
||||
|
||||
🔧 [Components](https://github.com/unplugin/unplugin-vue-components) and [API](https://github.com/unplugin/unplugin-auto-import) Auto Import on Demand
|
||||
|
||||
🔎 [Husky](./.husky/pre-commit) + [lint-staged](./package.json) + [ESLint](./eslint.config.js) - Code Standardization
|
||||
|
||||
💪🏻 Still [TypeScript](./tsconfig.json) - Strict Mode with No `any`
|
||||
|
||||
👀 More Features - [Route Cache](./src/pinia/stores/keep-alive.ts), [Defensive Watermark](./src/common/composables/useWatermark.ts), [Grayscale and Colorblind Mode](./src/common/composables/useGrayscaleAndColorblind.ts), [SVG Loader](https://github.com/jpkleemans/vite-svg-loader), [VConsole](./src/plugins/console.ts), [White Screen Loading Animation](./public/app-loading.css), [Unit Tests](./tests)
|
||||
|
||||
## Tech Stack
|
||||
|
||||
**Vue3**: Vue3 + script setup with the latest Vue3 Composition API
|
||||
|
||||
**Vant**:A lightweight, customizable Vue UI library for mobile web apps
|
||||
|
||||
**Pinia**: The legendary Vuex5
|
||||
|
||||
**Vite**: Really fast
|
||||
|
||||
**Vue Router**:The routing system
|
||||
|
||||
**TypeScript**:A superset of JavaScript
|
||||
|
||||
**pnpm**:A faster, disk-space-saving package manager
|
||||
|
||||
**ESlint**:Code linting and formatting
|
||||
|
||||
**Axios**:Sends network requests
|
||||
|
||||
**UnoCSS**:A high-performance, flexible atomic CSS engine
|
||||
|
||||
## Project Preview Image
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License © 2025-PRESENT [pany](https://github.com/pany-ang)
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
<div align="center">
|
||||
<img alt="logo" width="120" height="120" src="./public/favicon.ico">
|
||||
<h1>Mobile + Vue = MobVue</h1>
|
||||
</div>
|
||||
|
||||
[](https://github.com/un-pany/mobvue/releases)
|
||||
[](https://github.com/un-pany/mobvue/stargazers)
|
||||
[](https://gitee.com/un-pany/mobvue/stargazers)
|
||||
|
||||
<b><a href="./README.md">English</a> | 中文</b>
|
||||
|
||||
## 简介
|
||||
|
||||
MobVue 是一个精心制作的移动端 H5 模板,基于 Vue3、Vite、TypeScript、Vant 等主流技术
|
||||
|
||||
## 通知
|
||||
|
||||
> [!NOTE]
|
||||
> 为爱发电!所有源码均免费开源,如果对你有帮助,欢迎点个 Star 支持一下!
|
||||
|
||||
> [!TIP]
|
||||
> 正式推出付费服务,如果不想自己动手,但想移除 TS 或其他模块?试试懒人套餐
|
||||
|
||||
## 使用
|
||||
|
||||
<details>
|
||||
<summary>推荐环境</summary>
|
||||
|
||||
<br>
|
||||
|
||||
- 新版 `Visual Studio Code`
|
||||
- 安装 `.vscode/extensions.json` 文件中推荐的插件
|
||||
- `node` 20.x 或 22+
|
||||
- `pnpm` 9.x 或 10+
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>本地开发</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
|
||||
# 启动服务
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>打包构建</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# 打包构建预发布环境
|
||||
pnpm build:staging
|
||||
|
||||
# 打包构建生产环境
|
||||
pnpm build
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>本地预览</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# 先执行打包构建命令生成 dist 目录后再执行以下预览命令
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>代码检查</summary>
|
||||
|
||||
<br>
|
||||
|
||||
```bash
|
||||
# 代码校验与格式化
|
||||
pnpm lint
|
||||
|
||||
# 单元测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>代码提交规范</summary>
|
||||
|
||||
<br>
|
||||
|
||||
`feat` 新功能
|
||||
|
||||
`fix` 修复错误
|
||||
|
||||
`perf` 性能优化
|
||||
|
||||
`refactor` 重构代码
|
||||
|
||||
`docs` 文档和注释
|
||||
|
||||
`types` 类型相关
|
||||
|
||||
`test` 单测相关
|
||||
|
||||
`ci` 持续集成、工作流
|
||||
|
||||
`revert` 撤销更改
|
||||
|
||||
`chore` 琐事(更新依赖、修改配置等)
|
||||
|
||||
</details>
|
||||
|
||||
## 链接
|
||||
|
||||
**在线预览**:[github-pages](https://un-pany.github.io/mobvue)
|
||||
|
||||
**文档教程**:[链接](https://juejin.cn/column/7472609448201666599)
|
||||
|
||||
**国内仓库**:[gitee](https://gitee.com/un-pany/mobvue)
|
||||
|
||||
**交流群**:[查看进群方式](https://github.com/un-pany/mobvue/issues/3)
|
||||
|
||||
**捐赠**:[请作者喝咖啡](https://github.com/un-pany/mobvue/issues/1)
|
||||
|
||||
**发行版 & 更新日志**:[releases](https://github.com/un-pany/mobvue/releases)
|
||||
|
||||
## 特性
|
||||
|
||||
🔥 最新的 [语法](https://vuejs.org/api/sfc-script-setup.html)、[配置](./vite.config.ts)、[依赖](./package.json)
|
||||
|
||||
📍 [纯一级路由设计](./src/router/index.ts) - 清晰且缓存友好
|
||||
|
||||
📱 移动端适配 [px2vw](./postcss.config.ts) - 并且宽屏友好
|
||||
|
||||
🌐 浏览器适配 [@vitejs/plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) + [autoprefixer](https://github.com/postcss/autoprefixer) + [browserslist](https://github.com/browserslist/browserslist) - 兼容多种浏览器和低版本浏览器
|
||||
|
||||
🧩 [布局系统](./src/layout) - 配置化的
|
||||
|
||||
🔒 权限控制 [页面级](./src/router/guard.ts)、[按钮级](./src/pages/demo/permission.vue)
|
||||
|
||||
🌗 主题模式 [Dark Mode](./src/common/assets/styles/variables.css)
|
||||
|
||||
🫧 [拥抱原子化 CSS](./uno.config.ts)
|
||||
|
||||
🔧 [组件](https://github.com/unplugin/unplugin-vue-components) 和 [API](https://github.com/unplugin/unplugin-auto-import) 自动按需导入
|
||||
|
||||
🔎 [Husky](./.husky/pre-commit) + [lint-staged](./package.json) + [ESLint](./eslint.config.js) - 规范代码
|
||||
|
||||
💪🏻 依然 [TypeScript](./tsconfig.json) - 严格模式且无 `any`
|
||||
|
||||
👀 更多功能 - [路由缓存](./src/pinia/stores/keep-alive.ts)、[带防御的水印](./src/common/composables/useWatermark.ts)、[灰色模式, 色弱模式](./src/common/composables/useGrayscaleAndColorblind.ts)、[SVG Loader](https://github.com/jpkleemans/vite-svg-loader)、[VConsole](./src/plugins/console.ts)、[白屏加载动画](./public/app-loading.css)、[单元测试](./tests)
|
||||
|
||||
## 技术栈
|
||||
|
||||
**Vue3**:采用 Vue3 + script setup 最新的 Vue3 组合式 API
|
||||
|
||||
**Vant**:轻量、可定制的移动端 Vue 组件库
|
||||
|
||||
**Pinia**: 传说中的 Vuex5
|
||||
|
||||
**Vite**:真的很快
|
||||
|
||||
**Vue Router**:路由路由
|
||||
|
||||
**TypeScript**:JavaScript 语言的超集
|
||||
|
||||
**pnpm**:更快速的,节省磁盘空间的包管理工具
|
||||
|
||||
**ESlint**:代码校验与格式化
|
||||
|
||||
**Axios**:发送网络请求(已封装好)
|
||||
|
||||
**UnoCSS**:具有高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
|
||||
## 项目预览图
|
||||
|
||||

|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE) License © 2025-PRESENT [pany](https://github.com/pany-ang)
|
||||
|
|
@ -1,385 +0,0 @@
|
|||
# API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细描述了 MobVue 项目的 API 接口规范和使用方法。
|
||||
|
||||
## 基础配置
|
||||
|
||||
### 请求配置
|
||||
- **基础 URL**: 通过环境变量 `VITE_BASE_URL` 配置
|
||||
- **超时时间**: 10 秒
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
### 响应格式
|
||||
```typescript
|
||||
interface ApiResponse<T> {
|
||||
code: number; // 业务状态码
|
||||
data: T; // 响应数据
|
||||
msg?: string; // 响应消息
|
||||
message?: string; // 响应消息(兼容字段)
|
||||
}
|
||||
```
|
||||
|
||||
### 状态码说明
|
||||
- `0`: 成功
|
||||
- `401`: 登录过期
|
||||
- `403`: 拒绝访问
|
||||
- `500`: 服务器内部错误
|
||||
|
||||
## 用户认证模块
|
||||
|
||||
### 微信登录
|
||||
|
||||
#### 获取 OpenID
|
||||
```typescript
|
||||
// 接口: GET /api/v1/wx/getOpenId
|
||||
interface GetOpenIdParams {
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface GetOpenIdResponse {
|
||||
openid: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 企业微信登录
|
||||
```typescript
|
||||
// 接口: POST /api/v1/wx/qyLogin
|
||||
interface QyLoginParams {
|
||||
corpid: string;
|
||||
code: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
interface QyLoginResponse {
|
||||
userid: string;
|
||||
openid: string;
|
||||
isCabinetAdmin: number;
|
||||
name: string;
|
||||
qyUserId: number;
|
||||
ab98User: Ab98UserDTO;
|
||||
}
|
||||
```
|
||||
|
||||
### AB98 用户系统
|
||||
|
||||
#### Token 登录
|
||||
```typescript
|
||||
// 接口: POST /api/v1/ab98/tokenLogin
|
||||
interface TokenLoginParams {
|
||||
token: string;
|
||||
userid: string;
|
||||
openid: string;
|
||||
}
|
||||
|
||||
interface LoginData {
|
||||
face_img: string;
|
||||
success: boolean;
|
||||
sex: string;
|
||||
name: string;
|
||||
userid: string;
|
||||
registered: boolean;
|
||||
tel: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 商品模块
|
||||
|
||||
### 商品列表
|
||||
```typescript
|
||||
// 接口: GET /api/v1/product/list
|
||||
interface ProductListResponse {
|
||||
products: Product[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
image: string;
|
||||
description: string;
|
||||
stock: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 商品详情
|
||||
```typescript
|
||||
// 接口: GET /api/v1/product/{id}
|
||||
interface ProductDetailResponse extends Product {
|
||||
specifications: Specification[];
|
||||
images: string[];
|
||||
}
|
||||
|
||||
interface Specification {
|
||||
id: number;
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 订单模块
|
||||
|
||||
### 创建订单
|
||||
```typescript
|
||||
// 接口: POST /api/v1/order/create
|
||||
interface CreateOrderParams {
|
||||
productId: number;
|
||||
quantity: number;
|
||||
specifications?: Record<string, any>;
|
||||
deliveryInfo?: DeliveryInfo;
|
||||
}
|
||||
|
||||
interface CreateOrderResponse {
|
||||
orderId: string;
|
||||
totalAmount: number;
|
||||
status: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 订单列表
|
||||
```typescript
|
||||
// 接口: GET /api/v1/order/list
|
||||
interface OrderListParams {
|
||||
page?: number;
|
||||
size?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface OrderListResponse {
|
||||
orders: Order[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface Order {
|
||||
id: string;
|
||||
productName: string;
|
||||
totalAmount: number;
|
||||
status: string;
|
||||
createTime: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 订单详情
|
||||
```typescript
|
||||
// 接口: GET /api/v1/order/{id}
|
||||
interface OrderDetailResponse extends Order {
|
||||
items: OrderItem[];
|
||||
deliveryInfo: DeliveryInfo;
|
||||
paymentInfo: PaymentInfo;
|
||||
}
|
||||
|
||||
interface OrderItem {
|
||||
productId: number;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 柜机管理模块
|
||||
|
||||
### 柜机列表
|
||||
```typescript
|
||||
// 接口: GET /api/v1/cabinet/list
|
||||
interface CabinetListResponse {
|
||||
cabinets: Cabinet[];
|
||||
}
|
||||
|
||||
interface Cabinet {
|
||||
id: number;
|
||||
name: string;
|
||||
location: string;
|
||||
status: string;
|
||||
capacity: number;
|
||||
usedCapacity: number;
|
||||
}
|
||||
```
|
||||
|
||||
### 绑定商品
|
||||
```typescript
|
||||
// 接口: POST /api/v1/cabinet/bind
|
||||
interface BindGoodsParams {
|
||||
cabinetId: number;
|
||||
productId: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface BindGoodsResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 审批模块
|
||||
|
||||
### 提交审批
|
||||
```typescript
|
||||
// 接口: POST /api/v1/approval/submit
|
||||
interface SubmitApprovalParams {
|
||||
type: string;
|
||||
content: Record<string, any>;
|
||||
attachments?: string[];
|
||||
}
|
||||
|
||||
interface SubmitApprovalResponse {
|
||||
approvalId: string;
|
||||
status: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 审批列表
|
||||
```typescript
|
||||
// 接口: GET /api/v1/approval/list
|
||||
interface ApprovalListResponse {
|
||||
approvals: Approval[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Approval {
|
||||
id: string;
|
||||
type: string;
|
||||
status: string;
|
||||
submitter: string;
|
||||
submitTime: string;
|
||||
content: Record<string, any>;
|
||||
}
|
||||
```
|
||||
|
||||
### 处理审批
|
||||
```typescript
|
||||
// 接口: POST /api/v1/approval/handle
|
||||
interface HandleApprovalParams {
|
||||
approvalId: string;
|
||||
action: 'approve' | 'reject';
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
interface HandleApprovalResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 余额管理
|
||||
|
||||
### 获取余额
|
||||
```typescript
|
||||
// 接口: GET /api/v1/balance
|
||||
interface GetBalanceParams {
|
||||
corpid: string;
|
||||
openid: string;
|
||||
}
|
||||
|
||||
interface GetBalanceResponse {
|
||||
balance: number;
|
||||
useBalance: number;
|
||||
balanceLimit: number;
|
||||
userid: string;
|
||||
corpid: string;
|
||||
ab98User?: Ab98UserDTO;
|
||||
}
|
||||
```
|
||||
|
||||
### 企业微信用户余额
|
||||
```typescript
|
||||
// 接口: GET /api/v1/balance/qyUser
|
||||
interface GetBalanceByQyUseridParams {
|
||||
corpid: string;
|
||||
userid: string;
|
||||
}
|
||||
|
||||
interface GetBalanceByQyUseridResponse extends GetBalanceResponse {
|
||||
// 继承 GetBalanceResponse 的所有字段
|
||||
}
|
||||
```
|
||||
|
||||
## 数据模型
|
||||
|
||||
### Ab98UserDTO
|
||||
```typescript
|
||||
interface Ab98UserDTO {
|
||||
faceImg?: string;
|
||||
sex?: string;
|
||||
name?: string;
|
||||
userid?: string;
|
||||
tel?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### DeliveryInfo
|
||||
```typescript
|
||||
interface DeliveryInfo {
|
||||
recipient: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
deliveryTime?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### PaymentInfo
|
||||
```typescript
|
||||
interface PaymentInfo {
|
||||
paymentMethod: string;
|
||||
paymentTime?: string;
|
||||
transactionId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误码
|
||||
- `400`: 请求参数错误
|
||||
- `401`: 未授权访问
|
||||
- `403`: 权限不足
|
||||
- `404`: 资源不存在
|
||||
- `500`: 服务器内部错误
|
||||
|
||||
### 错误处理示例
|
||||
```typescript
|
||||
try {
|
||||
const response = await getDataApi(params);
|
||||
// 处理成功响应
|
||||
} catch (error) {
|
||||
// 处理错误
|
||||
console.error('API 调用失败:', error.message);
|
||||
// 显示错误提示给用户
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 调用 API
|
||||
```typescript
|
||||
import { getProductListApi } from '@/common/apis/product';
|
||||
|
||||
// 获取商品列表
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
const response = await getProductListApi({
|
||||
page: 1,
|
||||
size: 20
|
||||
});
|
||||
if (response.code === 0) {
|
||||
return response.data.products;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取商品列表失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 状态管理集成
|
||||
```typescript
|
||||
import { useProductStore } from '@/pinia/stores/product';
|
||||
|
||||
const productStore = useProductStore();
|
||||
|
||||
// 在组件中使用
|
||||
const loadProducts = async () => {
|
||||
await productStore.fetchProducts();
|
||||
};
|
||||
```
|
||||
|
|
@ -1,529 +0,0 @@
|
|||
# 开发指南
|
||||
|
||||
## 开发环境设置
|
||||
|
||||
### 1. 环境要求
|
||||
- **Node.js**: 20.x 或 22+
|
||||
- **包管理器**: pnpm 9.x 或 10+
|
||||
- **编辑器**: Visual Studio Code (推荐)
|
||||
- **浏览器**: Chrome 或 Safari (移动端调试)
|
||||
|
||||
### 2. 推荐 VS Code 插件
|
||||
- Vue Language Features (Volar)
|
||||
- TypeScript Vue Plugin (Volar)
|
||||
- ESLint
|
||||
- UnoCSS
|
||||
- Auto Rename Tag
|
||||
- GitLens
|
||||
|
||||
### 3. 项目初始化
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone <repository-url>
|
||||
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## 项目架构
|
||||
|
||||
### 目录结构详解
|
||||
|
||||
```
|
||||
src/
|
||||
├── common/ # 通用模块
|
||||
│ ├── apis/ # API 接口定义
|
||||
│ │ ├── users/ # 用户相关接口
|
||||
│ │ ├── shop/ # 店铺相关接口
|
||||
│ │ ├── cabinet/ # 柜机相关接口
|
||||
│ │ ├── approval/ # 审批相关接口
|
||||
│ │ └── ab98/ # AB98系统接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ │ └── styles/ # 样式文件
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ │ ├── useDark.ts # 暗黑模式
|
||||
│ │ ├── useWatermark.ts # 水印功能
|
||||
│ │ └── useGrayscaleAndColorblind.ts # 无障碍模式
|
||||
│ ├── components/ # 通用组件
|
||||
│ └── utils/ # 工具函数
|
||||
│ ├── cache/ # 缓存工具
|
||||
│ ├── permission.ts # 权限工具
|
||||
│ └── wx.ts # 微信工具
|
||||
├── layout/ # 布局组件
|
||||
│ ├── index.vue # 主布局
|
||||
│ ├── components/
|
||||
│ │ ├── NavBar.vue # 导航栏
|
||||
│ │ ├── Tabbar.vue # 标签栏
|
||||
│ │ └── Footer.vue # 底部
|
||||
├── pages/ # 页面组件
|
||||
│ ├── product/ # 商品相关页面
|
||||
│ ├── order/ # 订单相关页面
|
||||
│ ├── cabinet/ # 柜机相关页面
|
||||
│ ├── approval/ # 审批相关页面
|
||||
│ ├── me/ # 个人中心
|
||||
│ └── error/ # 错误页面
|
||||
├── pinia/ # 状态管理
|
||||
│ ├── index.ts # Pinia 实例
|
||||
│ └── stores/ # Store 定义
|
||||
│ ├── user.ts # 用户状态
|
||||
│ ├── ab98-user.ts # AB98用户状态
|
||||
│ ├── wx.ts # 微信状态
|
||||
│ ├── product.ts # 商品状态
|
||||
│ ├── cart.ts # 购物车状态
|
||||
│ ├── order.ts # 订单状态
|
||||
│ └── approval.ts # 审批状态
|
||||
├── router/ # 路由配置
|
||||
│ ├── index.ts # 路由定义
|
||||
│ ├── guard.ts # 路由守卫
|
||||
│ └── whitelist.ts # 白名单
|
||||
├── http/ # HTTP 请求
|
||||
│ └── axios.ts # Axios 配置
|
||||
└── plugins/ # 插件配置
|
||||
├── index.ts # 插件安装
|
||||
├── permission-directive.ts # 权限指令
|
||||
└── console.ts # 控制台工具
|
||||
```
|
||||
|
||||
## 开发规范
|
||||
|
||||
### 1. 代码风格
|
||||
|
||||
#### TypeScript 规范
|
||||
- 使用严格模式,避免 `any` 类型
|
||||
- 为所有函数和变量提供明确的类型定义
|
||||
- 使用接口定义复杂的数据结构
|
||||
|
||||
```typescript
|
||||
// ✅ 推荐
|
||||
interface UserInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const getUserInfo = async (id: number): Promise<UserInfo> => {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ 避免
|
||||
const getUserInfo = async (id: any): Promise<any> => {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue 组件规范
|
||||
- 使用 script setup 语法
|
||||
- 组件名使用 PascalCase
|
||||
- Props 和 Emits 使用 TypeScript 定义
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
title: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:count', value: number): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const handleClick = () => {
|
||||
emit('update:count', (props.count || 0) + 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="my-component">
|
||||
<h3>{{ title }}</h3>
|
||||
<button @click="handleClick">
|
||||
点击 {{ count }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 2. 状态管理规范
|
||||
|
||||
#### Store 定义
|
||||
- Store 名使用 camelCase
|
||||
- 使用组合式 API 风格
|
||||
- 提供清晰的类型定义
|
||||
|
||||
```typescript
|
||||
import { pinia } from "@/pinia";
|
||||
|
||||
export const useProductStore = defineStore("product", () => {
|
||||
// State
|
||||
const products = ref<Product[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// Getters
|
||||
const availableProducts = computed(() =>
|
||||
products.value.filter(p => p.stock > 0)
|
||||
);
|
||||
|
||||
// Actions
|
||||
const fetchProducts = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getProductListApi();
|
||||
if (response.code === 0) {
|
||||
products.value = response.data.products;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
products,
|
||||
loading,
|
||||
availableProducts,
|
||||
fetchProducts
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 3. API 开发规范
|
||||
|
||||
#### API 文件结构
|
||||
```typescript
|
||||
// src/common/apis/product/index.ts
|
||||
import { request } from "@/http/axios";
|
||||
import type { ProductListResponse, ProductDetailResponse } from "./type";
|
||||
|
||||
export const getProductListApi = (params?: { page?: number; size?: number }) => {
|
||||
return request<ProductListResponse>({
|
||||
url: "/api/v1/product/list",
|
||||
method: "GET",
|
||||
params
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductDetailApi = (id: number) => {
|
||||
return request<ProductDetailResponse>({
|
||||
url: `/api/v1/product/${id}`,
|
||||
method: "GET"
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 类型定义文件
|
||||
```typescript
|
||||
// src/common/apis/product/type.ts
|
||||
export interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
image: string;
|
||||
description: string;
|
||||
stock: number;
|
||||
category: string;
|
||||
}
|
||||
|
||||
export interface ProductListResponse {
|
||||
products: Product[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ProductDetailResponse extends Product {
|
||||
specifications: Specification[];
|
||||
images: string[];
|
||||
details: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 路由配置规范
|
||||
|
||||
#### 路由定义
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/product/:id",
|
||||
component: () => import("@/pages/product/ProductDetail.vue"),
|
||||
name: "ProductDetail",
|
||||
meta: {
|
||||
title: "商品详情",
|
||||
keepAlive: true,
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
},
|
||||
tabbar: {
|
||||
showTabbar: false
|
||||
}
|
||||
},
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 5. 样式开发规范
|
||||
|
||||
#### UnoCSS 使用
|
||||
- 优先使用预设的原子类
|
||||
- 自定义规则在 `uno.config.ts` 中定义
|
||||
- 使用属性化模式(un-前缀)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="product-card un-p-4 un-bg-white un-rounded-lg un-shadow-sm">
|
||||
<img
|
||||
:src="product.image"
|
||||
class="un-w-full un-h-40 un-object-cover un-rounded"
|
||||
/>
|
||||
<h3 class="un-text-lg un-font-medium un-mt-2 un-text-gray-900">
|
||||
{{ product.name }}
|
||||
</h3>
|
||||
<p class="un-text-primary un-font-bold un-mt-1">
|
||||
¥{{ product.price }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
#### CSS 变量
|
||||
在 `src/common/assets/styles/variables.css` 中定义主题变量:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--mobvue-primary-color: #1989fa;
|
||||
--mobvue-bg-color: #ffffff;
|
||||
--mobvue-text-color: #323233;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--mobvue-primary-color: #2d8cf0;
|
||||
--mobvue-bg-color: #1a1a1a;
|
||||
--mobvue-text-color: #e5e5e5;
|
||||
}
|
||||
```
|
||||
|
||||
## 功能开发指南
|
||||
|
||||
### 1. 添加新页面
|
||||
|
||||
#### 步骤 1: 创建页面组件
|
||||
```vue
|
||||
<!-- src/pages/example/ExamplePage.vue -->
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 页面逻辑...
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="example-page">
|
||||
<h1>示例页面</h1>
|
||||
<!-- 页面内容 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.example-page {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
#### 步骤 2: 配置路由
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/example",
|
||||
component: () => import("@/pages/example/ExamplePage.vue"),
|
||||
name: "ExamplePage",
|
||||
meta: {
|
||||
title: "示例页面",
|
||||
layout: {
|
||||
navBar: {
|
||||
showNavBar: true,
|
||||
showLeftArrow: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
### 2. 添加新 Store
|
||||
|
||||
```typescript
|
||||
// src/pinia/stores/example.ts
|
||||
import { pinia } from "@/pinia";
|
||||
|
||||
export const useExampleStore = defineStore("example", () => {
|
||||
const data = ref<string[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// API 调用...
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
fetchData
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 添加新 API
|
||||
|
||||
```typescript
|
||||
// src/common/apis/example/index.ts
|
||||
import { request } from "@/http/axios";
|
||||
import type { ExampleResponse } from "./type";
|
||||
|
||||
export const getExampleDataApi = () => {
|
||||
return request<ExampleResponse>({
|
||||
url: "/api/v1/example/data",
|
||||
method: "GET"
|
||||
});
|
||||
};
|
||||
|
||||
// src/common/apis/example/type.ts
|
||||
export interface ExampleResponse {
|
||||
data: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 移动端调试
|
||||
- 使用 Chrome DevTools 的设备模拟
|
||||
- 启用移动端触摸模拟
|
||||
- 测试不同屏幕尺寸
|
||||
|
||||
### 2. 微信调试
|
||||
- 使用微信开发者工具
|
||||
- 配置正确的回调域名
|
||||
- 检查 openid 获取逻辑
|
||||
|
||||
### 3. 状态调试
|
||||
- 使用 Vue DevTools
|
||||
- 查看 Pinia Store 状态
|
||||
- 监控组件生命周期
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 代码分割
|
||||
- 路由级别的懒加载
|
||||
- 组件按需导入
|
||||
- 第三方库独立打包
|
||||
|
||||
### 2. 图片优化
|
||||
- 使用 WebP 格式
|
||||
- 实现懒加载
|
||||
- 压缩图片大小
|
||||
|
||||
### 3. 缓存策略
|
||||
- 合理使用浏览器缓存
|
||||
- API 响应缓存
|
||||
- 组件 keep-alive
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 1. 单元测试
|
||||
```typescript
|
||||
// tests/example.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import ExampleComponent from "@/components/ExampleComponent.vue";
|
||||
|
||||
describe("ExampleComponent", () => {
|
||||
it("renders correctly", () => {
|
||||
const wrapper = mount(ExampleComponent, {
|
||||
props: {
|
||||
title: "Test Title"
|
||||
}
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain("Test Title");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. E2E 测试
|
||||
- 使用 Playwright 或 Cypress
|
||||
- 模拟用户操作流程
|
||||
- 测试关键业务路径
|
||||
|
||||
## 部署指南
|
||||
|
||||
### 1. 环境变量配置
|
||||
```env
|
||||
# .env.production
|
||||
VITE_BASE_URL=https://api.example.com
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
```
|
||||
|
||||
### 2. 构建优化
|
||||
```bash
|
||||
# 生产环境构建
|
||||
pnpm build
|
||||
|
||||
# 预发布环境构建
|
||||
pnpm build:staging
|
||||
```
|
||||
|
||||
### 3. 部署检查清单
|
||||
- [ ] 环境变量配置正确
|
||||
- [ ] 静态资源路径正确
|
||||
- [ ] API 接口可访问
|
||||
- [ ] 路由配置正确
|
||||
- [ ] 移动端适配正常
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 1. 微信认证失败
|
||||
- 检查回调域名配置
|
||||
- 验证 corpid 和 secret
|
||||
- 查看网络请求日志
|
||||
|
||||
### 2. 路由跳转问题
|
||||
- 检查路由守卫逻辑
|
||||
- 验证权限配置
|
||||
- 查看路由历史模式
|
||||
|
||||
### 3. 样式显示异常
|
||||
- 检查 UnoCSS 配置
|
||||
- 验证 CSS 变量定义
|
||||
- 测试不同主题模式
|
||||
|
||||
## 贡献指南
|
||||
|
||||
### 1. 代码提交
|
||||
- 遵循提交信息规范
|
||||
- 一个功能一个提交
|
||||
- 提供清晰的提交描述
|
||||
|
||||
### 2. 代码审查
|
||||
- 检查代码规范
|
||||
- 验证功能完整性
|
||||
- 测试边界情况
|
||||
|
||||
### 3. 文档更新
|
||||
- 更新相关文档
|
||||
- 添加使用示例
|
||||
- 记录变更内容
|
||||
|
|
@ -1,646 +0,0 @@
|
|||
# 微信集成文档
|
||||
|
||||
## 概述
|
||||
|
||||
本文档详细描述了 MobVue 项目中微信和企业微信的集成方案,包括认证流程、API 调用、状态管理等。
|
||||
|
||||
## 微信认证流程
|
||||
|
||||
### 1. 微信公众号认证
|
||||
|
||||
#### 认证流程图
|
||||
```
|
||||
用户访问应用
|
||||
↓
|
||||
检查是否已认证
|
||||
↓
|
||||
未认证 → 跳转微信授权页面
|
||||
↓
|
||||
用户授权
|
||||
↓
|
||||
微信回调携带 code
|
||||
↓
|
||||
使用 code 换取 openid
|
||||
↓
|
||||
获取用户信息
|
||||
↓
|
||||
完成认证
|
||||
```
|
||||
|
||||
#### 代码实现
|
||||
```typescript
|
||||
// src/App.vue - 微信回调处理
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
|
||||
if (code) {
|
||||
wxStore.handleWxCallback({ code, state });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 企业微信认证
|
||||
|
||||
#### 认证流程图
|
||||
```
|
||||
用户访问应用
|
||||
↓
|
||||
检查企业微信环境
|
||||
↓
|
||||
跳转企业微信授权
|
||||
↓
|
||||
用户授权
|
||||
↓
|
||||
企业微信回调携带 code
|
||||
↓
|
||||
使用 code 换取用户信息
|
||||
↓
|
||||
获取企业微信用户详情
|
||||
↓
|
||||
关联 AB98 用户系统
|
||||
↓
|
||||
完成认证
|
||||
```
|
||||
|
||||
#### 代码实现
|
||||
```typescript
|
||||
// src/pinia/stores/wx.ts - 企业微信登录
|
||||
const qyLogin = async ({ corpid, code, state }: QyLoginParams) => {
|
||||
const res = await qyLoginApi({ corpid, code, state });
|
||||
if (res && res.code == 0) {
|
||||
userid.value = res.data.userid;
|
||||
openid.value = res.data.openid;
|
||||
isCabinetAdmin.value = res.data.isCabinetAdmin === 1;
|
||||
name.value = res.data.name;
|
||||
qyUserId.value = res.data.qyUserId;
|
||||
setAb98User(res.data.ab98User);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 状态管理
|
||||
|
||||
### 微信状态 Store (src/pinia/stores/wx.ts)
|
||||
|
||||
#### 状态定义
|
||||
```typescript
|
||||
export const useWxStore = defineStore("wx", () => {
|
||||
// 微信授权 code
|
||||
const code = ref<string>("");
|
||||
|
||||
// 防止 CSRF 攻击的 state 参数
|
||||
const state = ref<string>("");
|
||||
|
||||
// 用户 openid
|
||||
const openid = ref<string>("");
|
||||
|
||||
// 用户 userid
|
||||
const userid = ref<string>("");
|
||||
|
||||
// 企业 ID
|
||||
const corpid = ref<string>("");
|
||||
|
||||
// 是否企业微信登录
|
||||
const corpidLogin = ref<boolean>(false);
|
||||
|
||||
// 是否是柜子管理员
|
||||
const isCabinetAdmin = ref<boolean>(false);
|
||||
|
||||
// 企业微信用户姓名
|
||||
const name = ref<string>("");
|
||||
|
||||
// 企业微信用户 ID
|
||||
const qyUserId = ref<number>(0);
|
||||
|
||||
// 汇邦云用户信息
|
||||
const ab98User = ref<ab98UserDTO | null>(null);
|
||||
|
||||
// 余额信息
|
||||
const balance = ref<number>(0);
|
||||
const useBalance = ref<number>(0);
|
||||
const balanceLimit = ref<number>(0);
|
||||
|
||||
// 处理状态
|
||||
const isHandleWxCallbackComplete = ref<boolean>(false);
|
||||
});
|
||||
```
|
||||
|
||||
#### 核心方法
|
||||
```typescript
|
||||
// 处理微信回调
|
||||
const handleWxCallback = async (params: {
|
||||
corpid?: string;
|
||||
code?: string;
|
||||
state?: string;
|
||||
}) => {
|
||||
isHandleWxCallbackComplete.value = false;
|
||||
|
||||
if (params.code) {
|
||||
code.value = params.code;
|
||||
state.value = params.state || state.value;
|
||||
corpid.value = params.corpid || corpid.value;
|
||||
corpidLogin.value = !!corpid.value;
|
||||
|
||||
try {
|
||||
if (!corpid.value) {
|
||||
// 微信公众号登录
|
||||
const res = await getOpenIdApi({ code: params.code });
|
||||
if (res && res.code == 0) {
|
||||
openid.value = res.data;
|
||||
}
|
||||
} else {
|
||||
// 企业微信登录
|
||||
await qyLogin({
|
||||
corpid: corpid.value,
|
||||
code: params.code,
|
||||
state: params.state
|
||||
});
|
||||
}
|
||||
|
||||
// 获取余额信息
|
||||
if (openid.value) {
|
||||
await refreshBalance();
|
||||
}
|
||||
} finally {
|
||||
isHandleWxCallbackComplete.value = true;
|
||||
}
|
||||
} else {
|
||||
isHandleWxCallbackComplete.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新余额
|
||||
const refreshBalance = async () => {
|
||||
if (corpid.value && userid.value) {
|
||||
const res = await getBalanceByQyUserid(corpid.value, userid.value);
|
||||
if (res && res.code == 0) {
|
||||
balance.value = res.data.balance;
|
||||
useBalance.value = res.data.useBalance;
|
||||
balanceLimit.value = res.data.balanceLimit;
|
||||
|
||||
if (res.data.ab98User) {
|
||||
setAb98User(res.data.ab98User);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### AB98 用户状态 Store (src/pinia/stores/ab98-user.ts)
|
||||
|
||||
#### 状态定义
|
||||
```typescript
|
||||
export const useAb98UserStore = defineStore("ab98User", () => {
|
||||
// 用户基本信息
|
||||
const face_img = ref<string>('');
|
||||
const sex = ref<string>('');
|
||||
const name = ref<string>('');
|
||||
const userid = ref<string>("");
|
||||
const registered = ref<boolean>(false);
|
||||
const tel = ref<string>("");
|
||||
const token = ref<string>("");
|
||||
|
||||
// 登录状态
|
||||
const isLogin = ref<boolean>(false);
|
||||
const tokenLogin = ref<string>("");
|
||||
const loginCode = '1'; // 默认验证码
|
||||
});
|
||||
```
|
||||
|
||||
#### 核心方法
|
||||
```typescript
|
||||
// 设置用户信息
|
||||
const setUserInfo = (data: LoginData) => {
|
||||
face_img.value = data.face_img;
|
||||
localStorage.setItem(STORAGE_KEYS.FACE, encodeURIComponent(data.face_img));
|
||||
sex.value = data.sex;
|
||||
localStorage.setItem(STORAGE_KEYS.SEX, encodeURIComponent(data.sex));
|
||||
name.value = data.name;
|
||||
localStorage.setItem(STORAGE_KEYS.NAME, encodeURIComponent(data.name));
|
||||
userid.value = data.userid;
|
||||
localStorage.setItem(STORAGE_KEYS.USERID, encodeURIComponent(data.userid));
|
||||
registered.value = data.registered;
|
||||
localStorage.setItem(STORAGE_KEYS.REGISTERED, JSON.stringify(data.registered));
|
||||
tel.value = data.tel;
|
||||
localStorage.setItem(STORAGE_KEYS.TEL, encodeURIComponent(data.tel));
|
||||
localStorage.setItem(STORAGE_KEYS.LOGIN_CODE, encodeURIComponent(loginCode));
|
||||
};
|
||||
|
||||
// Token 登录
|
||||
const tokenLogin = async (token: string, userid: string, openid: string) => {
|
||||
const res = await tokenLoginApi({ token, userid, openid });
|
||||
if (res?.code === 0 && res.data?.success) {
|
||||
setTel(res.data.tel);
|
||||
setUserInfo(res.data);
|
||||
if (!isLogin.value) {
|
||||
setIsLogin(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## API 接口
|
||||
|
||||
### 微信相关接口 (src/common/apis/shop/)
|
||||
|
||||
#### 获取 OpenID
|
||||
```typescript
|
||||
// 接口: GET /api/v1/wx/getOpenId
|
||||
export const getOpenIdApi = (params: { code: string }) => {
|
||||
return request<string>({
|
||||
url: "/api/v1/wx/getOpenId",
|
||||
method: "GET",
|
||||
params
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 企业微信登录
|
||||
```typescript
|
||||
// 接口: POST /api/v1/wx/qyLogin
|
||||
export const qyLogin = (params: {
|
||||
corpid: string;
|
||||
code: string;
|
||||
state?: string;
|
||||
}) => {
|
||||
return request<QyLoginResponse>({
|
||||
url: "/api/v1/wx/qyLogin",
|
||||
method: "POST",
|
||||
data: params
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
#### 获取余额
|
||||
```typescript
|
||||
// 接口: GET /api/v1/balance
|
||||
export const getBalanceApi = (corpid: string, openid: string) => {
|
||||
return request<GetBalanceResponse>({
|
||||
url: "/api/v1/balance",
|
||||
method: "GET",
|
||||
params: { corpid, openid }
|
||||
});
|
||||
};
|
||||
|
||||
// 企业微信用户余额
|
||||
// 接口: GET /api/v1/balance/qyUser
|
||||
export const getBalanceByQyUserid = (corpid: string, userid: string) => {
|
||||
return request<GetBalanceResponse>({
|
||||
url: "/api/v1/balance/qyUser",
|
||||
method: "GET",
|
||||
params: { corpid, userid }
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### AB98 系统接口 (src/common/apis/ab98/)
|
||||
|
||||
#### Token 登录
|
||||
```typescript
|
||||
// 接口: POST /api/v1/ab98/tokenLogin
|
||||
export const tokenLogin = (params: {
|
||||
token: string;
|
||||
userid: string;
|
||||
openid: string;
|
||||
}) => {
|
||||
return request<LoginData>({
|
||||
url: "/api/v1/ab98/tokenLogin",
|
||||
method: "POST",
|
||||
data: params
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 微信配置
|
||||
|
||||
#### 公众号配置
|
||||
```typescript
|
||||
// 微信公众号配置
|
||||
const wxConfig = {
|
||||
appId: 'YOUR_APP_ID',
|
||||
redirectUri: encodeURIComponent('https://your-domain.com/callback'),
|
||||
scope: 'snsapi_userinfo', // 或 snsapi_base
|
||||
state: 'STATE_PARAM'
|
||||
};
|
||||
|
||||
// 生成授权 URL
|
||||
const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${wxConfig.appId}&redirect_uri=${wxConfig.redirectUri}&response_type=code&scope=${wxConfig.scope}&state=${wxConfig.state}#wechat_redirect`;
|
||||
```
|
||||
|
||||
#### 企业微信配置
|
||||
```typescript
|
||||
// 企业微信配置
|
||||
const qywxConfig = {
|
||||
corpid: 'YOUR_CORP_ID',
|
||||
agentId: 'YOUR_AGENT_ID',
|
||||
redirectUri: encodeURIComponent('https://your-domain.com/qy-callback'),
|
||||
state: 'STATE_PARAM'
|
||||
};
|
||||
|
||||
// 生成企业微信授权 URL
|
||||
const qyAuthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${qywxConfig.corpid}&redirect_uri=${qywxConfig.redirectUri}&response_type=code&scope=snsapi_base&state=${qywxConfig.state}#wechat_redirect`;
|
||||
```
|
||||
|
||||
### 环境变量配置
|
||||
|
||||
#### 开发环境 (.env.development)
|
||||
```env
|
||||
# 微信配置
|
||||
VITE_WX_APP_ID=your_dev_app_id
|
||||
VITE_WX_CORP_ID=your_dev_corp_id
|
||||
|
||||
# API 基础地址
|
||||
VITE_BASE_URL=https://apifoxmock.com/m1/2930465-2145633-default
|
||||
```
|
||||
|
||||
#### 生产环境 (.env.production)
|
||||
```env
|
||||
# 微信配置
|
||||
VITE_WX_APP_ID=your_prod_app_id
|
||||
VITE_WX_CORP_ID=your_prod_corp_id
|
||||
|
||||
# API 基础地址
|
||||
VITE_BASE_URL=https://api.example.com
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 页面中使用微信状态
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="user-info">
|
||||
<!-- 显示用户信息 -->
|
||||
<div v-if="ab98UserStore.isLogin">
|
||||
<img :src="ab98UserStore.face_img" class="avatar" />
|
||||
<div class="info">
|
||||
<h3>{{ ab98UserStore.name }}</h3>
|
||||
<p>手机号: {{ ab98UserStore.tel }}</p>
|
||||
<p>余额: ¥{{ wxStore.balance }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 未登录状态 -->
|
||||
<div v-else class="login-prompt">
|
||||
<p>请先登录</p>
|
||||
<button @click="handleLogin">微信登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWxStore } from '@/pinia/stores/wx';
|
||||
import { useAb98UserStore } from '@/pinia/stores/ab98-user';
|
||||
|
||||
const wxStore = useWxStore();
|
||||
const ab98UserStore = useAb98UserStore();
|
||||
|
||||
// 处理登录
|
||||
const handleLogin = () => {
|
||||
// 跳转到微信授权页面
|
||||
const authUrl = generateWxAuthUrl();
|
||||
window.location.href = authUrl;
|
||||
};
|
||||
|
||||
// 生成微信授权 URL
|
||||
const generateWxAuthUrl = () => {
|
||||
const appId = import.meta.env.VITE_WX_APP_ID;
|
||||
const redirectUri = encodeURIComponent(window.location.origin + '/callback');
|
||||
return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=wx_auth#wechat_redirect`;
|
||||
};
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. 等待微信回调完成
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="loading-container" v-if="!wxStore.isHandleWxCallbackComplete">
|
||||
<van-loading>微信认证中...</van-loading>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<!-- 正常页面内容 -->
|
||||
<ProductList />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useWxStore } from '@/pinia/stores/wx';
|
||||
|
||||
const wxStore = useWxStore();
|
||||
|
||||
// 等待微信回调处理完成
|
||||
const waitForWxCallback = async () => {
|
||||
const success = await wxStore.waitForHandleWxCallbackComplete();
|
||||
if (!success) {
|
||||
// 处理超时情况
|
||||
console.error('微信认证超时');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
waitForWxCallback();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. 微信认证失败
|
||||
|
||||
#### 可能原因
|
||||
- 回调域名未配置
|
||||
- AppID 或 Secret 错误
|
||||
- 网络问题导致 API 调用失败
|
||||
|
||||
#### 解决方案
|
||||
```typescript
|
||||
// 检查回调域名
|
||||
const checkCallbackDomain = () => {
|
||||
const currentDomain = window.location.hostname;
|
||||
// 确保当前域名在微信白名单中
|
||||
};
|
||||
|
||||
// 重试机制
|
||||
const retryWxAuth = async (maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
await wxStore.handleWxCallback(params);
|
||||
if (wxStore.isHandleWxCallbackComplete) {
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`微信认证重试 ${i + 1} 失败:`, error);
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2. OpenID 获取失败
|
||||
|
||||
#### 调试方法
|
||||
```typescript
|
||||
// 检查网络请求
|
||||
const debugWxAuth = async () => {
|
||||
console.log('当前 URL 参数:', window.location.search);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
|
||||
if (!code) {
|
||||
console.error('未获取到微信 code');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('获取到 code:', code);
|
||||
|
||||
// 手动调用获取 OpenID
|
||||
try {
|
||||
const response = await getOpenIdApi({ code });
|
||||
console.log('OpenID 响应:', response);
|
||||
} catch (error) {
|
||||
console.error('获取 OpenID 失败:', error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 余额信息不更新
|
||||
|
||||
#### 刷新策略
|
||||
```typescript
|
||||
// 定期刷新余额
|
||||
const startBalanceRefresh = () => {
|
||||
// 每 30 秒刷新一次余额
|
||||
setInterval(() => {
|
||||
if (wxStore.corpid && wxStore.userid) {
|
||||
wxStore.refreshBalance();
|
||||
}
|
||||
}, 30000);
|
||||
};
|
||||
|
||||
// 在关键操作后刷新余额
|
||||
const afterOrderCreate = async () => {
|
||||
// 创建订单后刷新余额
|
||||
await wxStore.refreshBalance();
|
||||
};
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 1. Token 安全
|
||||
|
||||
#### 存储安全
|
||||
```typescript
|
||||
// 敏感信息加密存储
|
||||
const encryptToken = (token: string) => {
|
||||
return btoa(encodeURIComponent(token));
|
||||
};
|
||||
|
||||
const decryptToken = (encrypted: string) => {
|
||||
return decodeURIComponent(atob(encrypted));
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 防 CSRF 攻击
|
||||
|
||||
#### State 参数验证
|
||||
```typescript
|
||||
// 生成随机的 state 参数
|
||||
const generateState = () => {
|
||||
return Math.random().toString(36).substring(2, 15);
|
||||
};
|
||||
|
||||
// 验证 state 参数
|
||||
const validateState = (receivedState: string, expectedState: string) => {
|
||||
return receivedState === expectedState;
|
||||
};
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 减少不必要的 API 调用
|
||||
|
||||
```typescript
|
||||
// 缓存用户信息
|
||||
const cachedUserInfo = ref<Ab98UserDTO | null>(null);
|
||||
|
||||
const getUserInfo = async (forceRefresh = false) => {
|
||||
if (cachedUserInfo.value && !forceRefresh) {
|
||||
return cachedUserInfo.value;
|
||||
}
|
||||
|
||||
// 调用 API 获取用户信息
|
||||
const userInfo = await fetchUserInfo();
|
||||
cachedUserInfo.value = userInfo;
|
||||
return userInfo;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 并行请求优化
|
||||
|
||||
```typescript
|
||||
// 并行获取用户信息和余额
|
||||
const loadUserData = async () => {
|
||||
const [userInfo, balanceInfo] = await Promise.all([
|
||||
getUserInfo(),
|
||||
getBalanceInfo()
|
||||
]);
|
||||
|
||||
return { userInfo, balanceInfo };
|
||||
};
|
||||
```
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 1. 单元测试
|
||||
|
||||
```typescript
|
||||
// tests/stores/wx.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { setActivePinia, createPinia } from "pinia";
|
||||
import { useWxStore } from "@/pinia/stores/wx";
|
||||
|
||||
describe("Wx Store", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it("should handle wx callback", async () => {
|
||||
const wxStore = useWxStore();
|
||||
|
||||
await wxStore.handleWxCallback({
|
||||
code: "test_code",
|
||||
state: "test_state"
|
||||
});
|
||||
|
||||
expect(wxStore.code).toBe("test_code");
|
||||
expect(wxStore.state).toBe("test_state");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
|
||||
```typescript
|
||||
// tests/integration/wx-auth.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("微信认证集成测试", () => {
|
||||
it("应该完成完整的微信认证流程", async () => {
|
||||
// 模拟微信回调
|
||||
// 验证状态更新
|
||||
// 检查用户信息
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
通过本文档,您可以全面了解 MobVue 项目中微信集成的各个方面,包括认证流程、状态管理、API 接口、配置说明和常见问题解决方案。
|
||||
|
|
@ -1,586 +0,0 @@
|
|||
# 系统架构文档
|
||||
|
||||
## 架构概述
|
||||
|
||||
MobVue 是一个基于 Vue3 的移动端 H5 应用,采用现代化的前端技术栈,具备清晰的模块化设计和可扩展的架构。
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ MobVue 应用 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Vue 3 + Composition API + TypeScript + Vant UI │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Pinia (状态管理) │ Vue Router (路由) │ UnoCSS (样式) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Vite (构建工具) │ Axios (HTTP客户端) │ ESLint (代码规范) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 核心依赖关系
|
||||
|
||||
```
|
||||
App.vue
|
||||
├── Layout (布局系统)
|
||||
│ ├── NavBar (导航栏)
|
||||
│ ├── Tabbar (标签栏)
|
||||
│ └── RouterView (页面渲染)
|
||||
├── Pinia Stores (状态管理)
|
||||
│ ├── User Store (用户状态)
|
||||
│ ├── Wx Store (微信状态)
|
||||
│ ├── Product Store (商品状态)
|
||||
│ └── Cart Store (购物车状态)
|
||||
└── Router (路由系统)
|
||||
├── Route Guards (路由守卫)
|
||||
└── Route Configuration (路由配置)
|
||||
```
|
||||
|
||||
## 模块架构
|
||||
|
||||
### 1. 核心模块
|
||||
|
||||
#### 应用入口 (src/main.ts)
|
||||
```typescript
|
||||
// 应用初始化流程
|
||||
1. 创建 Vue 应用实例
|
||||
2. 安装插件系统
|
||||
3. 配置状态管理 (Pinia)
|
||||
4. 配置路由系统 (Vue Router)
|
||||
5. 挂载应用到 DOM
|
||||
```
|
||||
|
||||
#### 应用根组件 (src/App.vue)
|
||||
```typescript
|
||||
// 主要职责
|
||||
1. 全局配置提供器 (ConfigProvider)
|
||||
2. 主题模式管理 (暗黑/亮色)
|
||||
3. 微信回调处理
|
||||
4. 用户认证状态管理
|
||||
5. 布局系统容器
|
||||
```
|
||||
|
||||
### 2. 布局系统 (src/layout/)
|
||||
|
||||
#### 布局组件结构
|
||||
```
|
||||
Layout (主布局)
|
||||
├── NavBar (导航栏)
|
||||
│ ├── 标题显示
|
||||
│ ├── 返回按钮
|
||||
│ └── 操作按钮
|
||||
├── Tabbar (标签栏)
|
||||
│ ├── 首页
|
||||
│ ├── 商品列表
|
||||
│ ├── 审批中心
|
||||
│ └── 个人中心
|
||||
└── RouterView (页面内容)
|
||||
```
|
||||
|
||||
#### 布局配置
|
||||
```typescript
|
||||
interface LayoutConfig {
|
||||
navBar: {
|
||||
showNavBar: boolean; // 是否显示导航栏
|
||||
showLeftArrow: boolean; // 是否显示返回箭头
|
||||
title?: string; // 导航栏标题
|
||||
};
|
||||
tabbar: {
|
||||
showTabbar: boolean; // 是否显示标签栏
|
||||
icon?: string; // 标签图标
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 状态管理架构 (src/pinia/)
|
||||
|
||||
#### Store 设计模式
|
||||
```typescript
|
||||
// Store 通用结构
|
||||
export const useXxxStore = defineStore("xxx", () => {
|
||||
// 1. State (状态)
|
||||
const state = ref<StateType>(initialValue);
|
||||
|
||||
// 2. Getters (计算属性)
|
||||
const computedState = computed(() => {
|
||||
return state.value.filter(...);
|
||||
});
|
||||
|
||||
// 3. Actions (操作)
|
||||
const fetchData = async () => {
|
||||
// 异步操作
|
||||
};
|
||||
|
||||
// 4. 返回暴露的属性和方法
|
||||
return { state, computedState, fetchData };
|
||||
});
|
||||
```
|
||||
|
||||
#### Store 依赖关系
|
||||
```
|
||||
User Store
|
||||
├── 用户认证状态
|
||||
├── 用户权限信息
|
||||
└── 用户基本信息
|
||||
|
||||
Wx Store
|
||||
├── 微信认证状态
|
||||
├── 用户 OpenID
|
||||
├── 企业微信信息
|
||||
└── 余额信息
|
||||
|
||||
Product Store
|
||||
├── 商品列表
|
||||
├── 商品详情
|
||||
└── 商品分类
|
||||
|
||||
Cart Store
|
||||
├── 购物车商品
|
||||
├── 选中状态
|
||||
└── 结算信息
|
||||
|
||||
Order Store
|
||||
├── 订单列表
|
||||
├── 订单详情
|
||||
└── 订单状态
|
||||
```
|
||||
|
||||
### 4. 路由架构 (src/router/)
|
||||
|
||||
#### 路由分层设计
|
||||
```typescript
|
||||
// 系统路由 (错误页面等)
|
||||
export const systemRoutes: RouteRecordRaw[] = [
|
||||
{ path: "/403", component: Error403 },
|
||||
{ path: "/404", component: Error404 }
|
||||
];
|
||||
|
||||
// 业务路由 (主要功能页面)
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
{ path: "/", component: ProductList },
|
||||
{ path: "/me", component: UserCenter },
|
||||
{ path: "/order/:id", component: OrderDetail }
|
||||
];
|
||||
```
|
||||
|
||||
#### 路由守卫流程
|
||||
```typescript
|
||||
// 路由导航守卫流程
|
||||
1. 检查白名单 (无需认证的页面)
|
||||
2. 验证用户认证状态
|
||||
3. 检查页面访问权限
|
||||
4. 设置页面标题
|
||||
5. 处理布局配置
|
||||
6. 执行路由跳转
|
||||
```
|
||||
|
||||
### 5. API 架构 (src/common/apis/)
|
||||
|
||||
#### API 模块化组织
|
||||
```
|
||||
apis/
|
||||
├── users/ # 用户相关接口
|
||||
│ ├── index.ts # 接口方法
|
||||
│ └── type.ts # 类型定义
|
||||
├── shop/ # 店铺相关接口
|
||||
├── cabinet/ # 柜机相关接口
|
||||
├── approval/ # 审批相关接口
|
||||
└── ab98/ # AB98系统接口
|
||||
```
|
||||
|
||||
#### HTTP 客户端架构
|
||||
```typescript
|
||||
// Axios 配置层次
|
||||
1. 创建实例配置
|
||||
2. 请求拦截器 (添加 Token 等)
|
||||
3. 响应拦截器 (统一错误处理)
|
||||
4. 业务状态码处理
|
||||
5. 全局错误处理
|
||||
```
|
||||
|
||||
### 6. 工具函数架构 (src/common/utils/)
|
||||
|
||||
#### 工具模块分类
|
||||
```
|
||||
utils/
|
||||
├── cache/ # 缓存工具
|
||||
│ ├── cookies.ts # Cookie 操作
|
||||
│ └── local-storage.ts # 本地存储
|
||||
├── permission.ts # 权限工具
|
||||
├── wx.ts # 微信工具
|
||||
├── validate.ts # 验证工具
|
||||
└── datetime.ts # 日期时间工具
|
||||
```
|
||||
|
||||
### 7. 组合式函数架构 (src/common/composables/)
|
||||
|
||||
#### 可复用逻辑
|
||||
```typescript
|
||||
// 组合式函数设计模式
|
||||
export function useXxx() {
|
||||
// 1. 响应式状态
|
||||
const state = ref(initialValue);
|
||||
|
||||
// 2. 操作方法
|
||||
const action = () => {
|
||||
// 业务逻辑
|
||||
};
|
||||
|
||||
// 3. 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化逻辑
|
||||
});
|
||||
|
||||
// 4. 返回暴露的 API
|
||||
return { state, action };
|
||||
}
|
||||
```
|
||||
|
||||
## 数据流架构
|
||||
|
||||
### 1. 组件间通信
|
||||
|
||||
#### Props/Emits (父子组件)
|
||||
```vue
|
||||
<!-- 父组件 -->
|
||||
<ChildComponent
|
||||
:title="parentTitle"
|
||||
@update:title="handleTitleUpdate"
|
||||
/>
|
||||
|
||||
<!-- 子组件 -->
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ title: string }>();
|
||||
const emit = defineEmits<{ 'update:title': [value: string] }>();
|
||||
</script>
|
||||
```
|
||||
|
||||
#### Provide/Inject (跨层级组件)
|
||||
```typescript
|
||||
// 祖先组件
|
||||
const theme = ref('light');
|
||||
provide('theme', theme);
|
||||
|
||||
// 后代组件
|
||||
const theme = inject<Ref<string>>('theme');
|
||||
```
|
||||
|
||||
#### Pinia Store (全局状态)
|
||||
```typescript
|
||||
// 在任何组件中使用
|
||||
const userStore = useUserStore();
|
||||
const { token, username } = storeToRefs(userStore);
|
||||
```
|
||||
|
||||
### 2. API 数据流
|
||||
|
||||
#### 数据获取流程
|
||||
```typescript
|
||||
// 1. 组件触发 Action
|
||||
const productStore = useProductStore();
|
||||
|
||||
// 2. Store 调用 API
|
||||
const fetchProducts = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await getProductListApi();
|
||||
if (response.code === 0) {
|
||||
products.value = response.data.products;
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 组件监听状态变化
|
||||
watchEffect(() => {
|
||||
if (productStore.products.length > 0) {
|
||||
// 更新 UI
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 用户交互流程
|
||||
|
||||
#### 典型用户操作流程
|
||||
```
|
||||
用户打开应用
|
||||
↓
|
||||
微信认证处理
|
||||
↓
|
||||
获取用户信息
|
||||
↓
|
||||
显示商品列表
|
||||
↓
|
||||
用户选择商品
|
||||
↓
|
||||
加入购物车
|
||||
↓
|
||||
创建订单
|
||||
↓
|
||||
支付结算
|
||||
↓
|
||||
订单完成
|
||||
```
|
||||
|
||||
## 安全架构
|
||||
|
||||
### 1. 认证机制
|
||||
|
||||
#### 微信 OAuth2.0 认证流程
|
||||
```typescript
|
||||
// 微信认证流程
|
||||
1. 重定向到微信授权页面
|
||||
2. 用户授权后返回 code
|
||||
3. 使用 code 换取 access_token
|
||||
4. 获取用户 OpenID
|
||||
5. 验证用户权限
|
||||
6. 生成应用内 Token
|
||||
```
|
||||
|
||||
#### Token 管理
|
||||
```typescript
|
||||
// Token 存储策略
|
||||
1. HTTP Only Cookie 存储
|
||||
2. 自动续期机制
|
||||
3. 安全退出清理
|
||||
4. 跨域请求携带
|
||||
```
|
||||
|
||||
### 2. 权限控制
|
||||
|
||||
#### 路由级权限
|
||||
```typescript
|
||||
// 路由守卫权限检查
|
||||
const hasPermission = (route: RouteLocationNormalized) => {
|
||||
if (route.meta?.requiresAuth && !isAuthenticated) {
|
||||
return false;
|
||||
}
|
||||
if (route.meta?.roles && !hasRole(route.meta.roles)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
#### 组件级权限
|
||||
```vue
|
||||
<!-- 权限指令使用 -->
|
||||
<button v-permission="['admin']">管理员操作</button>
|
||||
```
|
||||
|
||||
### 3. 数据安全
|
||||
|
||||
#### 敏感信息保护
|
||||
```typescript
|
||||
// 本地存储加密
|
||||
const encryptData = (data: string) => {
|
||||
return btoa(encodeURIComponent(data));
|
||||
};
|
||||
|
||||
const decryptData = (encrypted: string) => {
|
||||
return decodeURIComponent(atob(encrypted));
|
||||
};
|
||||
```
|
||||
|
||||
## 性能架构
|
||||
|
||||
### 1. 代码分割
|
||||
|
||||
#### 路由级懒加载
|
||||
```typescript
|
||||
// 动态导入组件
|
||||
const ProductList = () => import('@/pages/product/ProductList.vue');
|
||||
```
|
||||
|
||||
#### 第三方库分割
|
||||
```typescript
|
||||
// Vite 配置手动分块
|
||||
manualChunks: {
|
||||
vue: ['vue', 'vue-router', 'pinia']
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 缓存策略
|
||||
|
||||
#### 组件缓存
|
||||
```vue
|
||||
<!-- KeepAlive 缓存 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="cachedRoutes">
|
||||
<component :is="Component" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
```
|
||||
|
||||
#### API 响应缓存
|
||||
```typescript
|
||||
// 接口数据缓存
|
||||
const cache = new Map();
|
||||
|
||||
const getCachedData = async (key: string, fetcher: () => Promise<any>) => {
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const data = await fetcher();
|
||||
cache.set(key, data);
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 资源优化
|
||||
|
||||
#### 图片优化
|
||||
```vue
|
||||
<!-- 图片懒加载 -->
|
||||
<img
|
||||
:src="product.image"
|
||||
loading="lazy"
|
||||
:alt="product.name"
|
||||
/>
|
||||
```
|
||||
|
||||
#### CSS 优化
|
||||
```css
|
||||
/* UnoCSS 原子化类名 */
|
||||
.un-p-4 { padding: 1rem; }
|
||||
.un-bg-white { background-color: white; }
|
||||
.un-rounded-lg { border-radius: 0.5rem; }
|
||||
```
|
||||
|
||||
## 扩展性设计
|
||||
|
||||
### 1. 插件系统
|
||||
|
||||
#### 插件注册机制
|
||||
```typescript
|
||||
// 插件安装函数
|
||||
export const installPlugins = (app: App) => {
|
||||
// 注册全局组件
|
||||
app.component('CustomComponent', CustomComponent);
|
||||
|
||||
// 注册自定义指令
|
||||
app.directive('permission', permissionDirective);
|
||||
|
||||
// 注册全局属性
|
||||
app.config.globalProperties.$utils = utils;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. 配置系统
|
||||
|
||||
#### 环境配置
|
||||
```typescript
|
||||
// 多环境配置支持
|
||||
const config = {
|
||||
development: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
debug: true
|
||||
},
|
||||
production: {
|
||||
baseURL: 'https://api.example.com',
|
||||
debug: false
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 主题系统
|
||||
|
||||
#### 动态主题切换
|
||||
```typescript
|
||||
// 主题管理
|
||||
const useTheme = () => {
|
||||
const isDark = ref(false);
|
||||
|
||||
const toggleTheme = () => {
|
||||
isDark.value = !isDark.value;
|
||||
document.documentElement.classList.toggle('dark', isDark.value);
|
||||
};
|
||||
|
||||
return { isDark, toggleTheme };
|
||||
};
|
||||
```
|
||||
|
||||
## 监控和调试
|
||||
|
||||
### 1. 开发工具
|
||||
|
||||
#### Vue DevTools 集成
|
||||
```typescript
|
||||
// 开发环境启用
|
||||
if (import.meta.env.DEV) {
|
||||
// 启用 Vue DevTools
|
||||
}
|
||||
```
|
||||
|
||||
#### 控制台调试
|
||||
```typescript
|
||||
// VConsole 移动端调试
|
||||
import VConsole from 'vconsole';
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
new VConsole();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 错误监控
|
||||
|
||||
#### 全局错误处理
|
||||
```typescript
|
||||
// Vue 错误处理
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Vue 错误:', err, info);
|
||||
// 发送错误报告
|
||||
};
|
||||
|
||||
// 未处理 Promise 拒绝
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('未处理的 Promise 拒绝:', event.reason);
|
||||
});
|
||||
```
|
||||
|
||||
## 部署架构
|
||||
|
||||
### 1. 构建优化
|
||||
|
||||
#### 生产环境构建
|
||||
```typescript
|
||||
// Vite 生产配置
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vue: ['vue', 'vue-router', 'pinia']
|
||||
}
|
||||
}
|
||||
},
|
||||
reportCompressedSize: false,
|
||||
chunkSizeWarningLimit: 2048
|
||||
}
|
||||
```
|
||||
|
||||
### 2. CDN 部署
|
||||
|
||||
#### 静态资源优化
|
||||
```typescript
|
||||
// 静态资源 CDN 配置
|
||||
base: process.env.NODE_ENV === 'production'
|
||||
? 'https://cdn.example.com/mobvue/'
|
||||
: '/'
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
MobVue 采用现代化的前端架构设计,具备以下特点:
|
||||
|
||||
1. **模块化设计**: 清晰的模块边界和职责分离
|
||||
2. **类型安全**: 全面的 TypeScript 支持
|
||||
3. **状态管理**: 集中式的状态管理方案
|
||||
4. **性能优化**: 多层次的性能优化策略
|
||||
5. **安全可靠**: 完善的安全机制和错误处理
|
||||
6. **可扩展性**: 灵活的插件和配置系统
|
||||
7. **开发体验**: 优秀的开发工具和调试支持
|
||||
|
||||
这种架构设计确保了项目的可维护性、可扩展性和高性能,为移动端 H5 应用开发提供了坚实的基础。
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
# 部署指南
|
||||
|
||||
## 部署概述
|
||||
|
||||
本文档详细描述了 MobVue 项目的部署流程和配置说明,涵盖开发环境、预发布环境和生产环境的部署步骤。
|
||||
|
||||
## 环境要求
|
||||
|
||||
### 服务器要求
|
||||
- **操作系统**: Linux (推荐 Ubuntu 20.04+ 或 CentOS 7+)
|
||||
- **Node.js**: 20.x 或 22+
|
||||
- **Web 服务器**: Nginx 或 Apache
|
||||
- **内存**: 至少 1GB RAM
|
||||
- **存储**: 至少 10GB 可用空间
|
||||
|
||||
### 网络要求
|
||||
- **域名**: 配置正确的域名和 SSL 证书
|
||||
- **防火墙**: 开放 80 (HTTP) 和 443 (HTTPS) 端口
|
||||
- **CDN**: 推荐使用 CDN 加速静态资源
|
||||
|
||||
## 环境配置
|
||||
|
||||
### 1. 环境变量
|
||||
|
||||
#### 开发环境 (.env.development)
|
||||
```env
|
||||
# 开发环境配置
|
||||
VITE_BASE_URL=https://apifoxmock.com/m1/2930465-2145633-default
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
```
|
||||
|
||||
#### 预发布环境 (.env.staging)
|
||||
```env
|
||||
# 预发布环境配置
|
||||
VITE_BASE_URL=https://staging-api.example.com
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
```
|
||||
|
||||
#### 生产环境 (.env.production)
|
||||
```env
|
||||
# 生产环境配置
|
||||
VITE_BASE_URL=https://api.example.com
|
||||
VITE_PUBLIC_PATH=/
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
```
|
||||
|
||||
### 2. 微信配置
|
||||
|
||||
#### 微信公众号配置
|
||||
- **AppID**: 微信公众号应用 ID
|
||||
- **AppSecret**: 微信公众号应用密钥
|
||||
- **回调域名**: 配置为部署的域名
|
||||
|
||||
#### 企业微信配置
|
||||
- **CorpID**: 企业微信企业 ID
|
||||
- **AgentID**: 企业微信应用 ID
|
||||
- **Secret**: 企业微信应用密钥
|
||||
- **回调域名**: 配置为部署的域名
|
||||
|
||||
## 构建流程
|
||||
|
||||
### 1. 开发环境构建
|
||||
```bash
|
||||
# 安装依赖
|
||||
pnpm i
|
||||
|
||||
# 启动开发服务器
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 2. 预发布环境构建
|
||||
```bash
|
||||
# 构建预发布版本
|
||||
pnpm build:staging
|
||||
|
||||
# 构建输出目录: dist/
|
||||
```
|
||||
|
||||
### 3. 生产环境构建
|
||||
```bash
|
||||
# 构建生产版本
|
||||
pnpm build
|
||||
|
||||
# 构建输出目录: dist/
|
||||
```
|
||||
|
||||
### 构建优化
|
||||
- **代码分割**: Vue 相关库独立打包
|
||||
- **压缩优化**: 移除 console 和 debugger
|
||||
- **浏览器兼容**: 传统浏览器支持
|
||||
- **资源优化**: 图片和字体文件压缩
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 1. 静态文件部署
|
||||
|
||||
#### Nginx 配置示例
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# 重定向到 HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your-domain.com;
|
||||
|
||||
# SSL 配置
|
||||
ssl_certificate /path/to/certificate.crt;
|
||||
ssl_certificate_key /path/to/private.key;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
# 静态文件服务
|
||||
root /var/www/mobvue/dist;
|
||||
index index.html;
|
||||
|
||||
# Gzip 压缩
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
||||
|
||||
# 缓存配置
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# SPA 路由支持
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api/ {
|
||||
proxy_pass https://api.example.com;
|
||||
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;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Apache 配置示例
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName your-domain.com
|
||||
DocumentRoot /var/www/mobvue/dist
|
||||
|
||||
# 重定向到 HTTPS
|
||||
Redirect permanent / https://your-domain.com/
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
ServerName your-domain.com
|
||||
DocumentRoot /var/www/mobvue/dist
|
||||
|
||||
# SSL 配置
|
||||
SSLEngine on
|
||||
SSLCertificateFile /path/to/certificate.crt
|
||||
SSLCertificateKeyFile /path/to/private.key
|
||||
|
||||
# SPA 路由支持
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule . /index.html [L]
|
||||
|
||||
# 缓存配置
|
||||
<FilesMatch "\.(js|css|png|jpg|jpeg|gif|ico|svg)$">
|
||||
ExpiresActive On
|
||||
ExpiresDefault "access plus 1 year"
|
||||
Header append Cache-Control "public, immutable"
|
||||
</FilesMatch>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
### 2. Docker 部署
|
||||
|
||||
#### Dockerfile
|
||||
```dockerfile
|
||||
# 构建阶段
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 package.json 和 lock 文件
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm install -g pnpm && pnpm i
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN pnpm build
|
||||
|
||||
# 生产阶段
|
||||
FROM nginx:alpine
|
||||
|
||||
# 复制构建结果
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制 Nginx 配置
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
```
|
||||
|
||||
#### docker-compose.yml
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mobvue:
|
||||
build: .
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
|
||||
# 可选:添加反向代理
|
||||
nginx-proxy:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx-proxy.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./ssl:/etc/nginx/ssl
|
||||
depends_on:
|
||||
- mobvue
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
### 3. 云平台部署
|
||||
|
||||
#### Vercel 部署
|
||||
1. 连接 GitHub 仓库
|
||||
2. 配置环境变量
|
||||
3. 自动部署
|
||||
|
||||
#### Netlify 部署
|
||||
1. 连接 Git 仓库
|
||||
2. 构建命令: `pnpm build`
|
||||
3. 发布目录: `dist`
|
||||
4. 配置重定向规则
|
||||
|
||||
#### 阿里云/腾讯云部署
|
||||
1. 上传构建文件到对象存储
|
||||
2. 配置 CDN 加速
|
||||
3. 配置域名和 SSL
|
||||
|
||||
## 部署检查清单
|
||||
|
||||
### 构建前检查
|
||||
- [ ] 环境变量配置正确
|
||||
- [ ] 依赖安装完成
|
||||
- [ ] 代码 lint 通过
|
||||
- [ ] 单元测试通过
|
||||
|
||||
### 部署前检查
|
||||
- [ ] 构建产物完整
|
||||
- [ ] 静态资源路径正确
|
||||
- [ ] API 接口可访问
|
||||
- [ ] 路由配置正确
|
||||
|
||||
### 部署后验证
|
||||
- [ ] 首页可正常访问
|
||||
- [ ] 路由跳转正常
|
||||
- [ ] API 调用正常
|
||||
- [ ] 移动端适配正常
|
||||
- [ ] 微信登录正常
|
||||
- [ ] 图片资源加载正常
|
||||
|
||||
## 监控和维护
|
||||
|
||||
### 1. 性能监控
|
||||
- **Web Vitals**: 监控核心性能指标
|
||||
- **错误监控**: 使用 Sentry 或类似工具
|
||||
- **用户行为分析**: 使用 Google Analytics
|
||||
|
||||
### 2. 日志管理
|
||||
- **访问日志**: Nginx 访问日志
|
||||
- **错误日志**: 应用错误日志
|
||||
- **API 日志**: 后端接口调用日志
|
||||
|
||||
### 3. 备份策略
|
||||
- **代码备份**: Git 仓库备份
|
||||
- **数据库备份**: 定期备份数据库
|
||||
- **配置文件备份**: 环境配置备份
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 白屏问题
|
||||
- 检查静态资源路径
|
||||
- 验证路由配置
|
||||
- 查看浏览器控制台错误
|
||||
|
||||
#### 2. API 调用失败
|
||||
- 检查网络连接
|
||||
- 验证 API 地址配置
|
||||
- 查看跨域配置
|
||||
|
||||
#### 3. 微信登录失败
|
||||
- 检查回调域名配置
|
||||
- 验证 AppID 和 Secret
|
||||
- 查看微信授权日志
|
||||
|
||||
#### 4. 移动端适配问题
|
||||
- 测试不同设备尺寸
|
||||
- 检查 viewport 配置
|
||||
- 验证 CSS 媒体查询
|
||||
|
||||
### 调试工具
|
||||
- **浏览器 DevTools**: 调试前端代码
|
||||
- **网络监控**: 检查 API 请求
|
||||
- **移动端模拟**: 测试移动端体验
|
||||
- **性能分析**: 分析页面性能
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 1. 网络安全
|
||||
- 使用 HTTPS 加密传输
|
||||
- 配置 CSP 安全策略
|
||||
- 启用 HSTS 强制 HTTPS
|
||||
|
||||
### 2. 应用安全
|
||||
- 输入验证和过滤
|
||||
- XSS 防护
|
||||
- CSRF 防护
|
||||
|
||||
### 3. 数据安全
|
||||
- 敏感信息加密存储
|
||||
- 定期安全扫描
|
||||
- 访问权限控制
|
||||
|
||||
## 版本管理
|
||||
|
||||
### 1. 版本号规范
|
||||
- 遵循语义化版本规范 (SemVer)
|
||||
- 主版本号.次版本号.修订版本号
|
||||
- 预发布版本使用标签 (如: 1.0.0-beta.1)
|
||||
|
||||
### 2. 发布流程
|
||||
1. 功能开发和测试
|
||||
2. 预发布环境验证
|
||||
3. 生产环境部署
|
||||
4. 监控和反馈
|
||||
|
||||
### 3. 回滚策略
|
||||
- 保留历史版本构建
|
||||
- 快速回滚到稳定版本
|
||||
- 数据库版本兼容性检查
|
||||
|
||||
## 附录
|
||||
|
||||
### 常用命令
|
||||
```bash
|
||||
# 开发
|
||||
pnpm dev
|
||||
|
||||
# 构建
|
||||
pnpm build
|
||||
pnpm build:staging
|
||||
|
||||
# 代码检查
|
||||
pnpm lint
|
||||
pnpm test
|
||||
|
||||
# 依赖管理
|
||||
pnpm i
|
||||
pnpm update
|
||||
```
|
||||
|
||||
### 配置文件说明
|
||||
- `vite.config.ts`: Vite 构建配置
|
||||
- `uno.config.ts`: UnoCSS 配置
|
||||
- `tsconfig.json`: TypeScript 配置
|
||||
- `eslint.config.js`: ESLint 配置
|
||||
|
||||
### 相关文档
|
||||
- [Vue 3 官方文档](https://vuejs.org/)
|
||||
- [Vite 官方文档](https://vitejs.dev/)
|
||||
- [Vant 官方文档](https://vant-ui.github.io/vant/)
|
||||
- [UnoCSS 官方文档](https://unocss.dev/)
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
# MobVue 项目文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
MobVue 是一个精心制作的移动端 H5 应用模板,基于 Vue3、Vite、TypeScript、Vant 等主流技术栈开发。本项目是一个电商/柜机管理系统,支持微信和企业微信登录,具备完整的用户认证、商品管理、订单处理、审批流程等功能。
|
||||
|
||||
## 项目特性
|
||||
|
||||
### 🔥 技术特性
|
||||
- **Vue3**: 采用最新的 Composition API 和 script setup 语法
|
||||
- **TypeScript**: 严格模式,无 `any` 类型
|
||||
- **Vite**: 极速的开发体验和构建性能
|
||||
- **Vant**: 轻量、可定制的移动端 Vue 组件库
|
||||
- **Pinia**: 现代化的状态管理
|
||||
- **UnoCSS**: 高性能的原子化 CSS 引擎
|
||||
|
||||
### 📱 移动端适配
|
||||
- **Px2Vw**: 自动将 px 转换为 vw 单位
|
||||
- **宽屏友好**: 适配各种屏幕尺寸
|
||||
- **浏览器兼容**: 支持多种浏览器和低版本
|
||||
|
||||
### 🔒 安全与权限
|
||||
- **页面级权限**: 路由守卫控制页面访问
|
||||
- **按钮级权限**: 细粒度的操作权限控制
|
||||
- **微信认证**: 支持微信和企业微信登录
|
||||
- **防御水印**: 防止敏感信息泄露
|
||||
|
||||
### 🧩 业务功能
|
||||
- **商品管理**: 商品列表、详情展示
|
||||
- **订单系统**: 订单创建、结算、详情查看
|
||||
- **柜机管理**: 智能柜机绑定和管理
|
||||
- **审批流程**: 审批申请、处理、核销
|
||||
- **用户中心**: 个人信息、余额管理
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── common/ # 通用模块
|
||||
│ ├── apis/ # API 接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── composables/ # 组合式函数
|
||||
│ ├── components/ # 通用组件
|
||||
│ └── utils/ # 工具函数
|
||||
├── layout/ # 布局组件
|
||||
├── pages/ # 页面组件
|
||||
├── pinia/ # 状态管理
|
||||
├── router/ # 路由配置
|
||||
├── http/ # HTTP 请求
|
||||
└── plugins/ # 插件配置
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
- Node.js 20.x 或 22+
|
||||
- pnpm 9.x 或 10+
|
||||
- 最新版 Visual Studio Code
|
||||
|
||||
### 安装依赖
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### 开发环境
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 构建项目
|
||||
```bash
|
||||
# 预发布环境
|
||||
pnpm build:staging
|
||||
|
||||
# 生产环境
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
```bash
|
||||
# 代码校验与格式化
|
||||
pnpm lint
|
||||
|
||||
# 单元测试
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## 核心模块说明
|
||||
|
||||
### 1. 路由系统 (src/router/)
|
||||
- **一级路由设计**: 清晰且缓存友好
|
||||
- **权限守卫**: 页面访问权限控制
|
||||
- **布局配置**: 可配置的导航栏和标签栏
|
||||
|
||||
### 2. 状态管理 (src/pinia/)
|
||||
- **用户状态**: 用户信息、登录状态
|
||||
- **微信状态**: 微信认证相关状态
|
||||
- **购物车**: 商品选择和结算
|
||||
- **订单状态**: 订单处理流程
|
||||
|
||||
### 3. API 管理 (src/common/apis/)
|
||||
- **用户接口**: 登录、用户信息
|
||||
- **商品接口**: 商品列表、详情
|
||||
- **订单接口**: 订单创建、查询
|
||||
- **柜机接口**: 柜机管理
|
||||
- **审批接口**: 审批流程
|
||||
|
||||
### 4. 布局系统 (src/layout/)
|
||||
- **导航栏**: 自定义标题和返回按钮
|
||||
- **标签栏**: 底部导航切换
|
||||
- **响应式**: 适配不同页面需求
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新页面
|
||||
1. 在 `src/pages/` 下创建 Vue 组件
|
||||
2. 在 `src/router/index.ts` 中添加路由配置
|
||||
3. 配置页面元信息 (meta)
|
||||
|
||||
### 状态管理
|
||||
使用 Pinia 进行状态管理,遵循组合式 API 规范:
|
||||
|
||||
```typescript
|
||||
export const useMyStore = defineStore("myStore", () => {
|
||||
const state = ref<StateType>(initialValue)
|
||||
|
||||
const action = () => {
|
||||
// 业务逻辑
|
||||
}
|
||||
|
||||
return { state, action }
|
||||
})
|
||||
```
|
||||
|
||||
### API 开发
|
||||
API 接口统一在 `src/common/apis/` 目录下管理:
|
||||
|
||||
```typescript
|
||||
export const getDataApi = (params: ParamsType) => {
|
||||
return request<ResponseType>({
|
||||
url: "/api/data",
|
||||
method: "GET",
|
||||
params
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 样式规范
|
||||
- 使用 UnoCSS 原子化类名
|
||||
- 优先使用预设的快捷方式
|
||||
- 自定义样式在 CSS 变量中定义
|
||||
|
||||
## 部署说明
|
||||
|
||||
### 环境变量
|
||||
项目支持多环境配置,通过 `.env` 文件管理:
|
||||
|
||||
```env
|
||||
VITE_BASE_URL=接口基础地址
|
||||
VITE_PUBLIC_PATH=公共路径
|
||||
VITE_ROUTER_HISTORY=路由模式
|
||||
```
|
||||
|
||||
### 构建优化
|
||||
- 代码分割:Vue 相关库独立打包
|
||||
- 压缩优化:移除 console 和 debugger
|
||||
- 浏览器兼容:传统浏览器支持
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 微信认证问题
|
||||
- 确保微信回调地址配置正确
|
||||
- 检查企业微信 corpid 配置
|
||||
- 验证 openid 获取逻辑
|
||||
|
||||
### 权限控制
|
||||
- 页面权限在路由守卫中控制
|
||||
- 按钮权限使用自定义指令
|
||||
- 角色权限在用户信息中配置
|
||||
|
||||
### 移动端适配
|
||||
- 使用 vw 单位进行响应式设计
|
||||
- 测试不同屏幕尺寸的显示效果
|
||||
- 确保触摸交互的流畅性
|
||||
|
||||
## 贡献指南
|
||||
|
||||
### 代码提交规范
|
||||
- `feat`: 新功能
|
||||
- `fix`: 修复错误
|
||||
- `perf`: 性能优化
|
||||
- `refactor`: 重构代码
|
||||
- `docs`: 文档和注释
|
||||
- `test`: 单元测试相关
|
||||
|
||||
### 开发流程
|
||||
1. Fork 项目
|
||||
2. 创建功能分支
|
||||
3. 提交代码变更
|
||||
4. 创建 Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
[MIT License](./LICENSE) © 2025-PRESENT [pany](https://github.com/pany-ang)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import antfu from "@antfu/eslint-config"
|
||||
|
||||
// 更多自定义配置可查阅仓库:https://github.com/antfu/eslint-config
|
||||
export default antfu(
|
||||
{
|
||||
// 使用外部格式化程序格式化 css、html、markdown 等文件
|
||||
formatters: true,
|
||||
// 启用样式规则
|
||||
stylistic: {
|
||||
// 缩进级别
|
||||
indent: 2,
|
||||
// 引号风格 'single' | 'double'
|
||||
quotes: "double",
|
||||
// 是否启用分号
|
||||
semi: false
|
||||
},
|
||||
// 忽略文件
|
||||
ignores: []
|
||||
},
|
||||
{
|
||||
// 对所有文件都生效的规则
|
||||
rules: {
|
||||
// vue
|
||||
"vue/block-order": ["error", { order: ["script", "template", "style"] }],
|
||||
"vue/attributes-order": "off",
|
||||
// ts
|
||||
"ts/no-use-before-define": "off",
|
||||
// node
|
||||
"node/prefer-global/process": "off",
|
||||
// style
|
||||
"style/comma-dangle": ["error", "never"],
|
||||
"style/brace-style": ["error", "1tbs"],
|
||||
// regexp
|
||||
"regexp/no-unused-capturing-group": "off",
|
||||
// other
|
||||
"no-console": "off",
|
||||
"no-debugger": "off",
|
||||
"symbol-description": "off",
|
||||
"antfu/if-newline": "off"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" type="image/png" />
|
||||
<link rel="stylesheet" href="/app-loading.css" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
<body ontouchstart>
|
||||
<div id="app">
|
||||
<div id="app-loading"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
{
|
||||
"name": "mobvue",
|
||||
"type": "module",
|
||||
"version": "0.6.1",
|
||||
"description": "A crafted mobile template, built with Vue3, Vite, TypeScript, Vant, and more",
|
||||
"author": "pany <939630029@qq.com> (https://github.com/pany-ang)",
|
||||
"repository": "https://github.com/un-pany/mobvue",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:staging": "vue-tsc && vite build --mode staging",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix",
|
||||
"prepare": "husky",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vant/touch-emulator": "1.4.0",
|
||||
"axios": "1.7.9",
|
||||
"compressorjs": "1.2.1",
|
||||
"dayjs": "1.11.13",
|
||||
"js-cookie": "3.0.5",
|
||||
"lodash-es": "4.17.21",
|
||||
"normalize.css": "8.0.1",
|
||||
"pinia": "3.0.1",
|
||||
"unocss": "66.0.0",
|
||||
"vant": "4.9.17",
|
||||
"vconsole": "3.15.1",
|
||||
"vue": "3.5.13",
|
||||
"vue-router": "4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "4.3.0",
|
||||
"@types/js-cookie": "3.0.6",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/nprogress": "0.2.3",
|
||||
"@unocss/preset-rem-to-px": "66.0.0",
|
||||
"@vant/auto-import-resolver": "1.2.1",
|
||||
"@vitejs/plugin-legacy": "6.0.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"autoprefixer": "10.4.20",
|
||||
"eslint": "9.21.0",
|
||||
"eslint-plugin-format": "1.0.1",
|
||||
"happy-dom": "17.1.2",
|
||||
"husky": "9.1.7",
|
||||
"lint-staged": "15.4.3",
|
||||
"nprogress": "0.2.0",
|
||||
"postcss-mobile-forever": "4.4.0",
|
||||
"typescript": "5.7.3",
|
||||
"unplugin-auto-import": "19.1.0",
|
||||
"unplugin-vue-components": "28.4.0",
|
||||
"vite": "6.1.1",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "3.0.6",
|
||||
"vue-tsc": "2.2.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
]
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
// 修改配置后重启服务生效
|
||||
export default {
|
||||
plugins: {
|
||||
// 自动添加浏览器前缀
|
||||
"autoprefixer": {},
|
||||
// 移动端适配插件
|
||||
"postcss-mobile-forever": {
|
||||
// UI 设计稿宽度
|
||||
viewportWidth: (file: string) => file.includes("vant") ? 375 : 375,
|
||||
// 限制视图的最大宽度
|
||||
maxDisplayWidth: 750,
|
||||
// 页面最外层选择器
|
||||
appSelector: "#app",
|
||||
// 是否对「页面最外层选择器」对应的元素进行描边
|
||||
border: true,
|
||||
// 转换单位后保留的小数点位数
|
||||
unitPrecision: 3,
|
||||
// 转换后的单位
|
||||
mobileUnit: "vw",
|
||||
// 需要转换的属性
|
||||
propList: ["*"],
|
||||
// 忽略的选择器
|
||||
selectorBlackList: [".ignore", "keep-px"],
|
||||
// 忽略的属性
|
||||
propertyBlackList: {
|
||||
".van-icon": "font"
|
||||
},
|
||||
// 忽略的属性值
|
||||
valueBlackList: ["1px"],
|
||||
// 忽略的目录或文件
|
||||
exclude: [],
|
||||
// 包含块是根元素的选择器列表
|
||||
rootContainingBlockSelectorList: ["van-tabbar", "van-popup"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"appid": "wxa9f934a1036c971d",
|
||||
"compileType": "miniprogram",
|
||||
"libVersion": "3.7.8",
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"setting": {
|
||||
"coverView": true,
|
||||
"es6": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"enhance": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmRelationList": [],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
}
|
||||
},
|
||||
"condition": {},
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "mobvue",
|
||||
"setting": {
|
||||
"compileHotReLoad": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/* 白屏阶段会执行的 CSS 加载动画 */
|
||||
|
||||
#app-loading {
|
||||
position: relative;
|
||||
top: 45vh;
|
||||
margin: 0 auto;
|
||||
color: var(--mobvue-primary-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
#app-loading,
|
||||
#app-loading::before,
|
||||
#app-loading::after {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-radius: 50%;
|
||||
animation: 2s ease-in-out infinite app-loading-animation;
|
||||
}
|
||||
|
||||
#app-loading::before,
|
||||
#app-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#app-loading::before {
|
||||
left: -4em;
|
||||
animation-delay: -0.2s;
|
||||
}
|
||||
|
||||
#app-loading::after {
|
||||
left: 4em;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
@keyframes app-loading-animation {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 2em 0 -2em;
|
||||
}
|
||||
40% {
|
||||
box-shadow: 0 2em 0 0;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 8.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="5" width="90" height="90" rx="10" fill="#E8F5E9" stroke="#81C784"
|
||||
stroke-width="2" />
|
||||
<text x="50" y="60" font-family="Arial, sans-serif" font-size="24" font-weight="bold"
|
||||
fill="#2E7D32" text-anchor="middle">空闲</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 473 B |
|
|
@ -1,84 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import Layout from "@/layout/index.vue"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
import { useDark } from "@@/composables/useDark"
|
||||
import { useWxStore } from "@/pinia/stores/wx"
|
||||
import { tokenLogin } from '@/common/apis/ab98'
|
||||
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
|
||||
import { useProductStore } from "./pinia/stores/product"
|
||||
|
||||
// const userStore = useUserStore()
|
||||
const wxStore = useWxStore();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const ab98UserStore = useAb98UserStore();
|
||||
const productStore = useProductStore();
|
||||
|
||||
const { isDark, initDark } = useDark()
|
||||
|
||||
const isLoading = false;
|
||||
// const isLoading = computed(() => userStore.token && !userStore.username)
|
||||
|
||||
// watch(
|
||||
// () => userStore.token,
|
||||
// (newVal) => {
|
||||
// newVal && userStore.getInfo()
|
||||
// },
|
||||
// {
|
||||
// immediate: true
|
||||
// }
|
||||
// )
|
||||
|
||||
initDark()
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
console.log('urlParams', urlParams);
|
||||
const code = urlParams.get('code') || undefined;
|
||||
const state = urlParams.get('state') || undefined;
|
||||
const corpid = urlParams.get('corpid') || undefined;
|
||||
const isAdmin = urlParams.get('isAdmin') || undefined;
|
||||
|
||||
if (state && state.indexOf('token') !== -1) {
|
||||
const token = state.split('token_')[1];
|
||||
if (token) {
|
||||
ab98UserStore.setTokenLogin(token);
|
||||
|
||||
watch(
|
||||
() => wxStore.userid,
|
||||
(newVal) => {
|
||||
const isLogin = ab98UserStore.isLogin;
|
||||
if (newVal) {
|
||||
tokenLogin(ab98UserStore.tokenLogin, newVal, wxStore.openid).then(res => {
|
||||
if (res?.code === 0 && res.data?.success) {
|
||||
ab98UserStore.setTel(res.data.tel)
|
||||
ab98UserStore.setUserInfo(res.data)
|
||||
if (!isLogin) {
|
||||
ab98UserStore.setIsLogin(true)
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdmin == '1') {
|
||||
wxStore.setIsCabinetAdmin(true);
|
||||
}
|
||||
if (code || state) {
|
||||
wxStore.handleWxCallback({ corpid, code, state })
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-config-provider :theme="isDark ? 'dark' : 'light'" un-h-full>
|
||||
<van-loading v-if="isLoading" un-h-full un-flex-center>
|
||||
加载中...
|
||||
</van-loading>
|
||||
<Layout v-else />
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { request } from '@/http/axios'
|
||||
import {
|
||||
BindQyUserCommand,
|
||||
GetTokenParams,
|
||||
LoginData,
|
||||
LogoutResponse,
|
||||
SmsSendResponse,
|
||||
TokenResponse,
|
||||
VerifySmsParams,
|
||||
WechatQrCodeParams
|
||||
} from './type'
|
||||
import { ab98UserDTO } from '../shop/type'
|
||||
|
||||
/** 获取临时令牌 */
|
||||
export function getTokenApi(appName: string) {
|
||||
return request<ApiResponseData<TokenResponse>>({
|
||||
url: '/wx/login/getToken',
|
||||
method: 'get',
|
||||
params: { appName }
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取微信登录二维码 */
|
||||
export function getWechatQrCodeApi(token: string) {
|
||||
return request<ApiResponseData<string>>({
|
||||
url: '/wx/login/wechat/qrcode',
|
||||
method: 'get',
|
||||
params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
/** 发送短信验证码 */
|
||||
export function sendSmsApi(token: string, tel: string) {
|
||||
return request<ApiResponseData<SmsSendResponse>>({
|
||||
url: '/wx/login/sendSms',
|
||||
method: 'post',
|
||||
params: { token, tel }
|
||||
})
|
||||
}
|
||||
|
||||
/** 验证短信验证码 */
|
||||
export function verifySmsApi(params: VerifySmsParams) {
|
||||
return request<ApiResponseData<LoginData>>({
|
||||
url: '/wx/login/verifySms',
|
||||
method: 'post',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/** 用户退出登录 */
|
||||
export function logoutApi(token: string) {
|
||||
return request<ApiResponseData<LogoutResponse>>({
|
||||
url: '/wx/login/logout',
|
||||
method: 'post',
|
||||
params: { token }
|
||||
})
|
||||
}
|
||||
|
||||
/** ab98Token登录 */
|
||||
export function tokenLogin(token: string, userid: string, openid: string) {
|
||||
return request<ApiResponseData<LoginData>>({
|
||||
url: '/wx/login/tokenLogin',
|
||||
method: 'get',
|
||||
params: { token, userid, openid }
|
||||
})
|
||||
}
|
||||
|
||||
export function bindQyUserApi(data: BindQyUserCommand) {
|
||||
return request<ApiResponseData<ab98UserDTO>>({
|
||||
url: '/wx/login/bindQyUser',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/** 令牌响应 */
|
||||
export interface TokenResponse {
|
||||
/** 认证令牌 */
|
||||
token: string
|
||||
}
|
||||
|
||||
/** 退出登录响应 */
|
||||
export interface LogoutResponse {
|
||||
/** 是否成功 */
|
||||
success: boolean
|
||||
}
|
||||
|
||||
/** 短信发送响应 */
|
||||
export interface SmsSendResponse {
|
||||
/** 发送状态 */
|
||||
success: boolean
|
||||
/** 错误信息 */
|
||||
errMsg?: string
|
||||
}
|
||||
|
||||
/** 登录数据 */
|
||||
export interface LoginData {
|
||||
/** 用户头像 */
|
||||
face_img: string
|
||||
/** 登录状态 */
|
||||
success: boolean
|
||||
/** 用户性别 */
|
||||
sex: string
|
||||
/** 用户姓名 */
|
||||
name: string
|
||||
/** 用户ID */
|
||||
userid: string
|
||||
/** 是否已注册 */
|
||||
registered: boolean
|
||||
/** 联系电话 */
|
||||
tel: string
|
||||
}
|
||||
|
||||
/** 获取令牌参数 */
|
||||
export type GetTokenParams = {
|
||||
/** 应用名称 */
|
||||
appName: string
|
||||
}
|
||||
|
||||
/** 微信二维码参数 */
|
||||
export type WechatQrCodeParams = {
|
||||
/** 认证令牌 */
|
||||
token: string
|
||||
}
|
||||
|
||||
/** 短信验证参数 */
|
||||
export type VerifySmsParams = {
|
||||
/** 认证令牌 */
|
||||
token: string
|
||||
/** 手机号码 */
|
||||
tel: string
|
||||
/** 验证码 */
|
||||
vcode: string
|
||||
userid: string
|
||||
openid: string
|
||||
}
|
||||
|
||||
export interface BindQyUserCommand {
|
||||
qyUserId: number;
|
||||
name: string;
|
||||
idNum: string;
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
import { request } from '@/http/axios'
|
||||
import { SubmitApprovalRequestData, SubmitApprovalResponseData, SearchApiReturnApprovalQuery, ApiResponsePageData, ReturnApprovalEntity, HandleApprovalRequestData, SearchReturnApprovalAssetQuery, ReturnApprovalAssetDTO, HandleApprovalAssetRequestData, ApprovalGoodsCellEntity, ReturnApprovalDetailDTO } from './type'
|
||||
import { OpenCabinetApiData, ShopOrderGoodsEntity } from '../shop/type'
|
||||
|
||||
export const getApprovalListApi = (params: SearchApiReturnApprovalQuery) => {
|
||||
return request<ApiResponsePageData<ReturnApprovalEntity>>({
|
||||
url: 'approval/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const getApprovalAssetListApi = (params: SearchReturnApprovalAssetQuery) => {
|
||||
return request<ApiResponsePageData<ReturnApprovalAssetDTO>>({
|
||||
url: 'approval/list/asset',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const checkApprovalCodeApi = (params: {
|
||||
corpid: string,
|
||||
approvalType: number,
|
||||
code: string
|
||||
}) => {
|
||||
return request<ApiResponseMsgData<string>>(
|
||||
{
|
||||
url: 'approval/checkCode',
|
||||
method: 'post',
|
||||
params
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export const submitApprovalApi = (data: SubmitApprovalRequestData) => {
|
||||
return request<SubmitApprovalResponseData>({
|
||||
url: 'approval/submit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const handleApprovalApi = (data: HandleApprovalRequestData) => {
|
||||
return request<ApiResponseMsgData<string>>({
|
||||
url: 'approval/handle',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const handleApprovalAssetApi = (data: HandleApprovalRequestData) => {
|
||||
return request<ApiResponseMsgData<string>>({
|
||||
url: 'approval/handle/asset',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const allocateApprovalGoods = (data: HandleApprovalAssetRequestData) => {
|
||||
return request<ApiResponseMsgData<string>>({
|
||||
url: 'approval/handle/allocateApprovalGoods',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const getApprovalOrderGoodsApi = (approvalId: number) => {
|
||||
return request<ApiResponseMsgData<ShopOrderGoodsEntity[]>>({
|
||||
url: 'approval/getApprovalOrderGoods',
|
||||
method: 'get',
|
||||
params: { approvalId }
|
||||
})
|
||||
}
|
||||
|
||||
export const getApprovalGoodsCellApi = (approvalId: number) => {
|
||||
return request<ApiResponseMsgData<ApprovalGoodsCellEntity[]>>(
|
||||
{
|
||||
url: 'approval/getApprovalGoodsCell',
|
||||
method: 'get',
|
||||
params: { approvalId }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const getApprovalDetailAssetApi = (approvalId: number) => {
|
||||
return request<ApiResponseMsgData<ReturnApprovalDetailDTO>>(
|
||||
{
|
||||
url: 'approval/detail/asset',
|
||||
method: 'get',
|
||||
params: { approval_id: approvalId }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 打开储物柜接口 */
|
||||
export function openCabinetApi(approvalGoodsCellId: number, data: OpenCabinetApiData) {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `approval/openCabinet/${approvalGoodsCellId}`,
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
export interface SubmitApprovalRequestData {
|
||||
orderGoodsId: number
|
||||
returnQuantity: number
|
||||
returnImages: string
|
||||
returnRemark: string
|
||||
corpid: string
|
||||
applyUserid: string
|
||||
}
|
||||
|
||||
export interface HandleApprovalRequestData {
|
||||
/** 审批ID */
|
||||
approvalId: number
|
||||
/** 审批状态 */
|
||||
status: number
|
||||
returnAmount: number
|
||||
auditImages: string
|
||||
auditRemark: string
|
||||
userid: string
|
||||
corpid: string
|
||||
auditUserid: string
|
||||
}
|
||||
|
||||
export interface HandleApprovalAssetRequestData extends HandleApprovalRequestData {
|
||||
/** 审批商品ID */
|
||||
approvalGoodsList: ApprovalGoodsEntity[]
|
||||
}
|
||||
|
||||
export interface SearchApiReturnApprovalQuery {
|
||||
pageNum: number
|
||||
pageSize: number
|
||||
approvalId?: number
|
||||
orderId?: number
|
||||
goodsId?: number
|
||||
status?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
approvalType?: number
|
||||
corpid?: string
|
||||
handleStatus?: number;
|
||||
searchStr?: string;
|
||||
}
|
||||
|
||||
export interface ApiResponsePageData<T> {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
total: number
|
||||
rows: T[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface ReturnApprovalEntity {
|
||||
/** 审批编号 */
|
||||
approvalId: number
|
||||
/** 关联订单ID */
|
||||
orderId: number
|
||||
/** 关联商品ID */
|
||||
goodsId: number
|
||||
/** 关联订单商品ID */
|
||||
orderGoodsId: number
|
||||
/** 外部归属类型的商品ID */
|
||||
externalGoodsId?: number
|
||||
/** 外部归属类型的审批ID */
|
||||
externalApprovalId?: number
|
||||
/** 审批码 */
|
||||
code?: string
|
||||
/** 审批码校验状态(0未核销 1已核销) */
|
||||
codeCheck?: number
|
||||
/** 企业微信id */
|
||||
corpid?: string
|
||||
/** 申请人企业UserID */
|
||||
applyUserid?: string
|
||||
/** 申请人姓名 */
|
||||
applyUserName?: string
|
||||
/** 审批人企业UserID */
|
||||
auditUserid?: string
|
||||
/** 申请数量 */
|
||||
applyQuantity?: number
|
||||
/** 审批类型(0为借还柜 1为固资通) */
|
||||
approvalType?: number
|
||||
/** 申请说明 */
|
||||
applyRemark?: string
|
||||
/** 归还数量 */
|
||||
returnQuantity: number
|
||||
/** 商品单价 */
|
||||
goodsPrice: number
|
||||
/** 退还金额 */
|
||||
returnAmount: number
|
||||
/** 归还图片路径数组 */
|
||||
returnImages: string
|
||||
/** 审核图片路径数组 */
|
||||
auditImages: string
|
||||
/** 归还说明 */
|
||||
returnRemark: string
|
||||
/** 审核说明 */
|
||||
auditRemark: string
|
||||
/** 审批人姓名 */
|
||||
auditName: string
|
||||
/** 审批状态(1待审核 2已通过 3已驳回 4开柜中) */
|
||||
status: number
|
||||
/** 审批时间 */
|
||||
approvalTime?: string
|
||||
createTime: string
|
||||
updateTime: string
|
||||
/** 商品名称 */
|
||||
goodsName: string
|
||||
/** 封面图URL */
|
||||
coverImg: string
|
||||
/** 手机号码 */
|
||||
mobile: string
|
||||
/** 企业微信用户ID或汇邦云用户ID */
|
||||
userid: string
|
||||
/** 用户姓名 */
|
||||
name: string
|
||||
/** 是否内部用户(0否 1汇邦云用户 2企业微信用户) */
|
||||
isInternal: number
|
||||
/** 支付方式 */
|
||||
paymentMethod?: string
|
||||
}
|
||||
|
||||
export interface ApprovalGoodsEntity {
|
||||
approvalGoodsId: number;
|
||||
approvalId: number;
|
||||
goodsName: string;
|
||||
goodsId: number;
|
||||
externalGoodsId?: number;
|
||||
corpid?: string;
|
||||
belongType: number;
|
||||
price: number;
|
||||
applyQuantity: number;
|
||||
approvalQuantity?: number;
|
||||
coverImg?: string;
|
||||
}
|
||||
|
||||
export interface ReturnApprovalAssetDTO extends ReturnApprovalEntity {
|
||||
goodsList?: ApprovalGoodsEntity[];
|
||||
}
|
||||
|
||||
export interface ReturnApprovalDetailDTO extends ReturnApprovalAssetDTO {
|
||||
statusStr: string;
|
||||
approvalGoodsCellList?: ApprovalGoodsCellEntity[];
|
||||
}
|
||||
|
||||
export interface SearchReturnApprovalAssetQuery {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
approvalId?: number;
|
||||
orderId?: number;
|
||||
goodsId?: number;
|
||||
status?: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
approvalType?: number;
|
||||
corpid?: string;
|
||||
code?: string;
|
||||
codeCheck?: number;
|
||||
handleStatus?: number;
|
||||
searchStr?: string;
|
||||
}
|
||||
|
||||
export interface ApprovalGoodsCellEntity {
|
||||
/** 主键ID */
|
||||
approvalGoodsCellId: number;
|
||||
/** 审批ID */
|
||||
approvalId: number;
|
||||
/** 申请领用商品ID */
|
||||
approvalGoodsId: number;
|
||||
/** 商店ID */
|
||||
shopId: number;
|
||||
/** 柜机ID */
|
||||
cabinetId: number;
|
||||
/** 格口ID */
|
||||
cellId: number;
|
||||
/** 分配数量 */
|
||||
allocateQuantity: number;
|
||||
/** 商店名称 */
|
||||
shopName: string;
|
||||
/** 柜机名称 */
|
||||
cabinetName: string;
|
||||
/** 格口号 */
|
||||
cellNo: number;
|
||||
}
|
||||
|
||||
export type SubmitApprovalResponseData = ApiResponseMsgData<{
|
||||
approvalId: number
|
||||
status: number
|
||||
}>
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { request } from '@/http/axios'
|
||||
import type { CabinetDetailResponse, RentingCabinetDetailDTO } from './type'
|
||||
import { OpenCabinetApiData } from '../shop/type'
|
||||
|
||||
/** 获取智能柜详情接口 */
|
||||
export function getCabinetDetailApi(shopId: number) {
|
||||
return request<CabinetDetailResponse>({
|
||||
url: 'cabinet/detail',
|
||||
method: 'get',
|
||||
params: {
|
||||
shopId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取出租中的智能柜详情接口 */
|
||||
export function getRentingCabinetDetailApi(shopId: number) {
|
||||
return request<ApiResponseData<RentingCabinetDetailDTO[]>>({
|
||||
url: 'cabinet/detail/renting',
|
||||
method: 'get',
|
||||
params: {
|
||||
shopId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取自己租用中的智能柜详情接口 */
|
||||
export function getUserRentedCabinetListApi(corpid:string, ab98UserId: number) {
|
||||
return request<ApiResponseData<RentingCabinetDetailDTO[]>>({
|
||||
url: 'cabinet/detail/user',
|
||||
method: 'get',
|
||||
params: {
|
||||
corpid,
|
||||
ab98UserId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function openCabinet(cabinetId: number, pinNo: number, data: OpenCabinetApiData) {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `cabinet/openCabinet/${cabinetId}/${pinNo}`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const configureGoodsCellsStock = (cellId: number, goodsId: number, stock: number) => {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `/cabinet/configureGoodsCellsStock/${cellId}/${goodsId}/${stock}`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
export const changeGoodsCellsStock = (cellId: number, stock: number) => {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `/cabinet/changeGoodsCellsStock/${cellId}/${stock}`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
export const clearGoodsCells = (cellId: number) => {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `/cabinet/clearGoodsCells/${cellId}`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
export interface CabinetDetailDTO {
|
||||
cabinetId: number
|
||||
cabinetName: string
|
||||
lockControlNo: number
|
||||
cells: CellInfoDTO[]
|
||||
}
|
||||
|
||||
/** 租用中的智能柜详情DTO */
|
||||
export interface RentingCabinetDetailDTO {
|
||||
/** 柜机ID */
|
||||
cabinetId: number
|
||||
/** 柜机名称 */
|
||||
cabinetName: string
|
||||
/** 锁控编号 */
|
||||
lockControlNo: number
|
||||
/** 柜格列表 */
|
||||
cells: RetingCellEntity[]
|
||||
}
|
||||
|
||||
export interface RetingCellEntity extends CabinetCellEntity {
|
||||
orderId: number;
|
||||
orderGoodsId: number;
|
||||
}
|
||||
|
||||
/** 智能柜格口实体类 */
|
||||
export interface CabinetCellEntity {
|
||||
/** 格口唯一ID */
|
||||
cellId: number
|
||||
/** 关联柜机ID */
|
||||
cabinetId: number
|
||||
/** 主板ID */
|
||||
mainboardId?: number
|
||||
/** 格口号 */
|
||||
cellNo: number
|
||||
/** 针脚序号 */
|
||||
pinNo: number
|
||||
/** 库存数量 */
|
||||
stock: number
|
||||
/** 格口价格 */
|
||||
cellPrice?: number
|
||||
/** 是否已租用:0-未租用,1-已租用 */
|
||||
isRented: number
|
||||
/** 格口类型(1小格 2中格 3大格 4超大格) */
|
||||
cellType: number
|
||||
/** 使用状态:1空闲 2已占用 */
|
||||
usageStatus: number
|
||||
/** 可用状态:1正常 2故障 */
|
||||
availableStatus: number
|
||||
/** 商品ID */
|
||||
goodsId?: number
|
||||
}
|
||||
|
||||
export interface CellInfoDTO {
|
||||
cellId: number
|
||||
cellNo: number
|
||||
pinNo: number
|
||||
stock: number
|
||||
product?: ProductInfoDTO
|
||||
}
|
||||
|
||||
export interface ProductInfoDTO {
|
||||
goodsId: number
|
||||
goodsName: string
|
||||
price: number
|
||||
coverImg: string
|
||||
}
|
||||
|
||||
export type CabinetDetailResponse = ApiResponseData<CabinetDetailDTO[]>
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import { request } from "@/http/axios"
|
||||
import { PageDTO, ResponseData, BasePageQuery } from "../type"
|
||||
|
||||
export interface ShopGoodsDTO {
|
||||
goodsId?: number
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
categoryName?: string
|
||||
price?: number
|
||||
stock?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
coverImg?: string
|
||||
creatorId?: number
|
||||
creatorName?: string
|
||||
createTime?: string
|
||||
remark?: string
|
||||
cabinetName?: string
|
||||
cellNo?: number
|
||||
cellNoStr?: string
|
||||
totalStock?: number
|
||||
usageInstruction?: string
|
||||
}
|
||||
|
||||
export interface SearchShopGoodsQuery extends BasePageQuery {
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
minPrice?: number
|
||||
maxPrice?: number
|
||||
}
|
||||
|
||||
/** 获取商品列表 */
|
||||
export function getGoodsList(query: SearchShopGoodsQuery) {
|
||||
return request<ResponseData<PageDTO<ShopGoodsDTO>>>({
|
||||
url: "manage/goods/list",
|
||||
method: "get",
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增商品 */
|
||||
export function addGoods(data: {
|
||||
goodsName: string
|
||||
categoryId: number
|
||||
price: number
|
||||
stock: number
|
||||
status: number
|
||||
autoApproval: number
|
||||
coverImg: string
|
||||
goodsDetail?: string
|
||||
usageInstruction?: string
|
||||
}) {
|
||||
return request<ResponseData<void>>({
|
||||
url: "manage/goods",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除商品 */
|
||||
export function deleteGoods(goodsIds: number[]) {
|
||||
return request<ResponseData<void>>({
|
||||
url: `manage/goods/${goodsIds.join(',')}`,
|
||||
method: "delete"
|
||||
})
|
||||
}
|
||||
|
||||
/** 修改商品 */
|
||||
export function updateGoods(goodsId: number, data: {
|
||||
goodsName?: string
|
||||
categoryId?: number
|
||||
price?: number
|
||||
stock?: number
|
||||
status?: number
|
||||
autoApproval?: number
|
||||
coverImg?: string
|
||||
goodsDetail?: string
|
||||
usageInstruction?: string
|
||||
}) {
|
||||
return request<ResponseData<void>>({
|
||||
url: `manage/goods/${goodsId}`,
|
||||
method: "put",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取单个商品信息 */
|
||||
export function getGoodsInfo(goodsId: number) {
|
||||
return request<ResponseData<ShopGoodsDTO>>({
|
||||
url: "manage/goods/getGoodsInfo",
|
||||
method: "get",
|
||||
params: { goodsId }
|
||||
})
|
||||
}
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import { request } from "@/http/axios"
|
||||
import { GetBalanceResponse, GetOrdersByOpenIdDTO, OpenCabinetApiData, QyLoginDTO, QyLoginRequestParams, SearchGoodsDO, ShopEntity, ShopGoodsEntity, ShopGoodsResponseData, SubmitOrderRequestData, SubmitOrderResponseData } from './type'
|
||||
import { GetOpenIdRequestParams } from './type'
|
||||
|
||||
|
||||
|
||||
/** 获取商品列表 */
|
||||
export function getShopGoodsListApi(corpid: string, belongType: number) {
|
||||
return request<ApiResponseData<SearchGoodsDO[]>>({
|
||||
url: "shop/goods/list",
|
||||
method: "get",
|
||||
params: { corpid, belongType }
|
||||
});
|
||||
}
|
||||
|
||||
export function getShopGoodsApi(shopId: number|null) {
|
||||
return request<ShopGoodsResponseData>({
|
||||
url: "shop/goods",
|
||||
method: "get",
|
||||
params: {
|
||||
shopId: shopId ? shopId : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** 提交订单接口 */
|
||||
export function submitOrderApi(data: SubmitOrderRequestData) {
|
||||
return request<SubmitOrderResponseData>({
|
||||
url: "order/submit",
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取微信openid */
|
||||
export function getOpenIdApi(params: GetOpenIdRequestParams) {
|
||||
return request<ApiResponseData<string>>({
|
||||
url: "payment/getOpenId",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
/** 企业微信登录 */
|
||||
export function qyLogin(params: QyLoginRequestParams) {
|
||||
return request<ApiResponseData<QyLoginDTO>>({
|
||||
url: "payment/login/qy",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
export function fakeQyLoginApi(params: {corpid: string, userid: string}) {
|
||||
return request<ApiResponseData<QyLoginDTO>>({
|
||||
url: "payment/login/qy/fake",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据openid获取用户订单信息 */
|
||||
export function getOrdersByOpenIdApi(corpid: string, openid: string, hasReturn: number) {
|
||||
return request<ApiResponseData<GetOrdersByOpenIdDTO>>({
|
||||
url: `order/user/${openid}`,
|
||||
method: "get",
|
||||
params: { corpid, hasReturn }
|
||||
})
|
||||
}
|
||||
|
||||
/** 根据openid获取用户订单信息 */
|
||||
export function getOrdersByQyUserIdApi(qyUserId: number, hasReturn: number) {
|
||||
return request<ApiResponseData<GetOrdersByOpenIdDTO>>({
|
||||
url: `order/user/qy/${qyUserId}`,
|
||||
method: "get",
|
||||
params: { hasReturn }
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开储物柜接口 */
|
||||
export function openCabinetApi(orderId: number, orderGoodsId: number, data: OpenCabinetApiData) {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `order/openCabinet/${orderId}/${orderGoodsId}`,
|
||||
method: "post",
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 获取用户余额接口 */
|
||||
export function getBalanceApi(corpid: string, openid: string) {
|
||||
return request<ApiResponseData<GetBalanceResponse>>({
|
||||
url: "payment/getBalance",
|
||||
method: "get",
|
||||
params: { corpid, openid }
|
||||
})
|
||||
}
|
||||
export function getBalanceByQyUserid(corpid: string, userid: string) {
|
||||
return request<ApiResponseData<GetBalanceResponse>>({
|
||||
url: "payment/getBalanceByQyUserid",
|
||||
method: "get",
|
||||
params: { corpid, userid }
|
||||
})
|
||||
}
|
||||
|
||||
export function getShopListApi(corpid: string, mode?: number) {
|
||||
const params: any = {
|
||||
corpid
|
||||
};
|
||||
if (typeof mode !== 'undefined') {
|
||||
params.mode = mode;
|
||||
}
|
||||
return request<ApiResponseData<ShopEntity[]>>({
|
||||
url: "shop/list",
|
||||
method: "get",
|
||||
params
|
||||
})
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
export type Goods = {
|
||||
goodsId: number,
|
||||
goodsName: string,
|
||||
categoryId: number,
|
||||
price: number,
|
||||
stock: number,
|
||||
status: number,
|
||||
coverImg: string,
|
||||
goodsDetail: string,
|
||||
usageInstruction: string,
|
||||
cellId: number,
|
||||
belongType: number
|
||||
}
|
||||
|
||||
export interface ShopGoodsEntity {
|
||||
/** 商品唯一ID */
|
||||
goodsId: number;
|
||||
/** 商品名称 */
|
||||
goodsName: string;
|
||||
/** 商品分类ID */
|
||||
categoryId: number;
|
||||
/** 外部归属类型的商品ID */
|
||||
externalGoodsId?: number;
|
||||
/** 企业微信id */
|
||||
corpid?: string;
|
||||
/** 每人每月限购数量 */
|
||||
monthlyPurchaseLimit?: number;
|
||||
/** 销售价格 */
|
||||
price: number;
|
||||
/** 库存数量 */
|
||||
stock: number;
|
||||
/** 商品状态(1上架 2下架) */
|
||||
status: number;
|
||||
/** 免审批(0否 1是) */
|
||||
autoApproval?: number;
|
||||
/** 封面图URL */
|
||||
coverImg?: string;
|
||||
/** 商品详情(支持2000汉字+10个图片链接) */
|
||||
goodsDetail?: string;
|
||||
/** 备注 */
|
||||
remark?: string;
|
||||
/** 商品使用说明 */
|
||||
usageInstruction?: string;
|
||||
/** 归属类型(0-借还柜 1-固资通) */
|
||||
belongType?: number;
|
||||
}
|
||||
|
||||
export type category = {
|
||||
categoryId: number,
|
||||
categoryName: string,
|
||||
sort: number
|
||||
}
|
||||
|
||||
export interface SubmitOrderRequestData {
|
||||
/** 微信用户唯一标识 */
|
||||
openid: string;
|
||||
/** 系统用户ID */
|
||||
userid: string;
|
||||
/** 企业ID */
|
||||
corpid: string;
|
||||
/** 支付类型 wechat:微信 balance:余额 */
|
||||
paymentType: 'wechat' | 'balance' | "approval";
|
||||
/** 联系电话 */
|
||||
mobile: string;
|
||||
/** 用户姓名 */
|
||||
name: string;
|
||||
/** 企业微信用户ID或汇邦云用户ID */
|
||||
qyUserid: string;
|
||||
/** 是否内部订单 0否 1汇邦云用户 2企业微信用户 */
|
||||
isInternal: number;
|
||||
applyRemark: string;
|
||||
/** 运行模式(0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
|
||||
mode: number;
|
||||
/** 订单商品明细列表 */
|
||||
goodsList: Array<{
|
||||
goodsId?: number
|
||||
quantity: number
|
||||
cellId: number
|
||||
/** 运行模式(0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
|
||||
mode: number;
|
||||
}>
|
||||
}
|
||||
|
||||
export type SubmitOrderResponseData = ApiResponseMsgData<{
|
||||
orderId: number
|
||||
totalAmount: number
|
||||
newBalance: number
|
||||
paymentInfo: WxJsApiPreCreateResponse
|
||||
}>
|
||||
|
||||
export type ShopGoodsResponseData = ApiResponseMsgData<{
|
||||
goodsList: Goods[],
|
||||
categoryList: category[]
|
||||
}>
|
||||
|
||||
export interface WxJsApiPreCreateResponse {
|
||||
appId: string
|
||||
timeStamp: string
|
||||
nonceStr: string
|
||||
package: string
|
||||
signType: string
|
||||
paySign: string
|
||||
}
|
||||
|
||||
export interface GetOpenIdRequestParams {
|
||||
code: string
|
||||
}
|
||||
export interface QyLoginRequestParams {
|
||||
corpid: string
|
||||
code: string
|
||||
state?: string
|
||||
}
|
||||
|
||||
export interface ShopOrderEntity {
|
||||
orderId: number
|
||||
openid: string
|
||||
totalAmount: number
|
||||
status: number
|
||||
payStatus: number
|
||||
paymentMethod: string
|
||||
payTime: string
|
||||
}
|
||||
|
||||
export interface ShopOrderGoodsEntity {
|
||||
/** 订单商品唯一ID */
|
||||
orderGoodsId: number
|
||||
/** 关联订单ID */
|
||||
orderId: number
|
||||
/** 审批ID */
|
||||
approvalId: number
|
||||
/** 关联商品ID */
|
||||
goodsId: number
|
||||
/** 关联格口ID */
|
||||
cellId: number
|
||||
/** 购买数量 */
|
||||
quantity: number
|
||||
/** 购买时单价 */
|
||||
price: number
|
||||
/** 商品总金额 */
|
||||
totalAmount: number
|
||||
/** 商品名称 */
|
||||
goodsName: string
|
||||
/** 封面图URL */
|
||||
coverImg: string
|
||||
/** 商品状态(1正常 2已退货 3已换货 4已完成 5审核中 6退货未通过) */
|
||||
status: number
|
||||
/** 企业微信id */
|
||||
corpid: string
|
||||
}
|
||||
|
||||
export interface GetOrdersByOpenIdDTO {
|
||||
orders: ShopOrderEntity[]
|
||||
orderGoods: ShopOrderGoodsEntity[]
|
||||
goods: Goods[]
|
||||
}
|
||||
|
||||
export interface GetBalanceResponse {
|
||||
userid: string
|
||||
corpid: string
|
||||
/** 剩余借呗 */
|
||||
balance: number
|
||||
/** 已用借呗 */
|
||||
useBalance: number
|
||||
/** 借呗总额 */
|
||||
balanceLimit: number
|
||||
ab98User: ab98UserDTO;
|
||||
}
|
||||
|
||||
export interface QyLoginDTO {
|
||||
userid: string;
|
||||
openid: string;
|
||||
isCabinetAdmin: number;
|
||||
qyUserId: number;
|
||||
name: string;
|
||||
ab98User: ab98UserDTO;
|
||||
}
|
||||
|
||||
export interface ab98UserDTO {
|
||||
/** 主键ID */
|
||||
ab98UserId?: number;
|
||||
/** openid */
|
||||
openid?: string;
|
||||
/** 汇邦云用户唯一ID */
|
||||
userid?: string;
|
||||
/** 真实姓名 */
|
||||
name?: string;
|
||||
/** 手机号码 */
|
||||
tel?: string;
|
||||
/** 身份证号码 */
|
||||
idnum?: string;
|
||||
/** 性别(男 女) */
|
||||
sex?: string;
|
||||
/** 人脸照片地址 */
|
||||
faceImg?: string;
|
||||
/** 身份证正面地址 */
|
||||
idcardFront?: string;
|
||||
/** 身份证背面地址 */
|
||||
idcardBack?: string;
|
||||
/** 身份证登记地址 */
|
||||
address?: string;
|
||||
/** 是否已注册(0未注册 1已注册) */
|
||||
registered?: boolean;
|
||||
/** 借呗余额 单位分 */
|
||||
ab98Balance?: number;
|
||||
}
|
||||
|
||||
export interface OpenCabinetApiData {
|
||||
// 格口ID
|
||||
cellId?: number
|
||||
// 用户ID
|
||||
userid: string
|
||||
// 是否内部用户(0否 1汇邦云用户 2企业微信用户)
|
||||
isInternal: number
|
||||
// 姓名
|
||||
name: string
|
||||
// 联系电话
|
||||
mobile: string
|
||||
// 操作类型(1用户 2管理员)
|
||||
operationType: number
|
||||
}
|
||||
|
||||
export interface ShopEntity {
|
||||
/** 主键ID */
|
||||
shopId: number;
|
||||
/** 商店名称 */
|
||||
shopName: string;
|
||||
/** 企业微信id */
|
||||
corpid: string;
|
||||
/** 运行模式(0-支付模式 1-审批模式 2-借还模式 3-会员模式 4-耗材模式) */
|
||||
mode?: number;
|
||||
/** 借呗支付(1-正常使用 0-禁止使用) */
|
||||
balanceEnable?: number;
|
||||
/** 封面图URL */
|
||||
coverImg?: string;
|
||||
}
|
||||
|
||||
export interface SearchGoodsDO extends ShopGoodsEntity {
|
||||
/** 分类名称 */
|
||||
categoryName?: string;
|
||||
/** 柜子ID */
|
||||
cabinetId?: number;
|
||||
/** 柜子名称 */
|
||||
cabinetName?: string;
|
||||
/** 商店名称字符串 */
|
||||
shopNameStr?: string;
|
||||
/** 格口编号 */
|
||||
cellNo?: number;
|
||||
/** 格口编号字符串 */
|
||||
cellNoStr?: string;
|
||||
/** 已分配库存 */
|
||||
totalStock?: number;
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
|
||||
export type ResponseData<T> = {
|
||||
code: number;
|
||||
msg: string;
|
||||
data: T;
|
||||
};
|
||||
|
||||
export type PageDTO<T> = {
|
||||
total: number;
|
||||
rows: Array<T>;
|
||||
};
|
||||
|
||||
export interface BasePageQuery extends BaseQuery {
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface BaseQuery {
|
||||
beginTime?: string;
|
||||
endTime?: string;
|
||||
orderColumn?: string;
|
||||
orderDirection?: string;
|
||||
timeRangeColumn?: string;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import type * as Users from "./type"
|
||||
import { request } from "@/http/axios"
|
||||
|
||||
/** 获取当前登录用户详情 */
|
||||
export function getCurrentUserApi() {
|
||||
return request<Users.CurrentUserResponseData>({
|
||||
url: "users/me",
|
||||
method: "get"
|
||||
})
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>
|
||||
|
Before Width: | Height: | Size: 198 KiB |
|
|
@ -1,56 +0,0 @@
|
|||
/* 全局 CSS 变量 */
|
||||
@import url("./variables.css");
|
||||
/* View Transition */
|
||||
@import url("./view-transition.css");
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 灰色模式 */
|
||||
html.grayscale-mode {
|
||||
filter: grayscale(1);
|
||||
}
|
||||
|
||||
/* 色弱模式 */
|
||||
html.colorblind-mode {
|
||||
filter: invert(0.8);
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
color: var(--mobvue-body-text-color);
|
||||
background-color: var(--mobvue-body-bg-color);
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-family: var(--van-base-font);
|
||||
/* 禁止 iOS 和 macOS 系统默认的橡皮筋效果 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
/* 隐藏滚动条样式 */
|
||||
*::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
a,
|
||||
a:focus,
|
||||
a:hover {
|
||||
color: inherit;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/* 全局 CSS 变量,这种变量不仅可以在 CSS 中使用,还可以导入到 JS 中使用 */
|
||||
|
||||
/* Light Mode */
|
||||
:root {
|
||||
/* Body Colors */
|
||||
--mobvue-body-text-color: #323233;
|
||||
--mobvue-body-bg-color: #f7f8fa;
|
||||
/* Component Colors */
|
||||
--mobvue-primary-color: #1989fa;
|
||||
--mobvue-bg-color: #ffffff;
|
||||
|
||||
--van-toast-text-color: fade(var(--van-black), 70%) !important;
|
||||
}
|
||||
|
||||
/* Dark Mode */
|
||||
html.dark {
|
||||
/* Body Colors */
|
||||
--mobvue-body-text-color: #f5f5f5;
|
||||
--mobvue-body-bg-color: #000000;
|
||||
/* Component Colors */
|
||||
--mobvue-primary-color: #1989fa;
|
||||
--mobvue-bg-color: #1c1c1e;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/* 控制切换主题时的动画效果(只在较新的浏览器上生效,例如 Chrome 111+) */
|
||||
|
||||
::view-transition-old(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
animation: 0.5s ease-in clip-animation;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
@keyframes clip-animation {
|
||||
from {
|
||||
clip-path: circle(0px at var(--mobvue-dark-x) var(--mobvue-dark-y));
|
||||
}
|
||||
to {
|
||||
clip-path: circle(var(--mobvue-dark-r) at var(--mobvue-dark-x) var(--mobvue-dark-y));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
interface Props {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
text: "一个精心制作的移动端 H5 模板"
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h1 un-flex-y-center>
|
||||
<img src="/favicon.ico" un-w-38px un-h-38px>
|
||||
<span un-ml-16px un-text-32px un-fw400>MobVue</span>
|
||||
</h1>
|
||||
<h2 un-color-hex-969799 un-text-14px un-fw400>
|
||||
{{ props.text }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { getIsDark, setIsDark } from "@@/utils/cache/local-storage"
|
||||
import { setCssVar } from "@@/utils/css"
|
||||
|
||||
const isDark = ref<boolean>(getIsDark() === "true")
|
||||
|
||||
function _handler() {
|
||||
isDark.value = !isDark.value
|
||||
}
|
||||
|
||||
function changeDark({ clientX, clientY }: MouseEvent) {
|
||||
const maxRadius = Math.hypot(
|
||||
Math.max(clientX, window.innerWidth - clientX),
|
||||
Math.max(clientY, window.innerHeight - clientY)
|
||||
)
|
||||
setCssVar("--mobvue-dark-x", `${clientX}px`)
|
||||
setCssVar("--mobvue-dark-y", `${clientY}px`)
|
||||
setCssVar("--mobvue-dark-r", `${maxRadius}px`)
|
||||
document.startViewTransition ? document.startViewTransition(_handler) : _handler()
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
function initDark() {
|
||||
// watchEffect 来收集副作用
|
||||
watchEffect(() => {
|
||||
document.documentElement.classList.toggle("dark", isDark.value)
|
||||
setIsDark(isDark.value)
|
||||
})
|
||||
}
|
||||
|
||||
/** 黑暗模式 Composable */
|
||||
export function useDark() {
|
||||
return { isDark, changeDark, initDark }
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
export type Mode = "" | "grayscale" | "colorblind"
|
||||
|
||||
const GRAYSCALE_MODE = "grayscale-mode"
|
||||
|
||||
const COLORBLIND_MODE = "colorblind-mode"
|
||||
|
||||
const classList = document.documentElement.classList
|
||||
|
||||
const mode = ref<Mode>("")
|
||||
|
||||
function setMode(_mdoe: Mode) {
|
||||
mode.value = _mdoe
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
classList.toggle(GRAYSCALE_MODE, mode.value === "grayscale")
|
||||
classList.toggle(COLORBLIND_MODE, mode.value === "colorblind")
|
||||
})
|
||||
|
||||
/** 灰色模式和色弱模式 Composable */
|
||||
export function useGrayscaleAndColorblind() {
|
||||
return { mode, setMode }
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/** 项目标题 */
|
||||
const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE ?? "MobVue"
|
||||
|
||||
/** 动态标题 */
|
||||
const dynamicTitle = ref<string>("")
|
||||
|
||||
/** 设置标题 */
|
||||
function setTitle(title?: string) {
|
||||
dynamicTitle.value = title ? `${VITE_APP_TITLE} | ${title}` : VITE_APP_TITLE
|
||||
}
|
||||
|
||||
// 监听标题变化
|
||||
watch(dynamicTitle, (value, oldValue) => {
|
||||
if (document && value !== oldValue) {
|
||||
document.title = value
|
||||
}
|
||||
})
|
||||
|
||||
/** 标题 Composable */
|
||||
export function useTitle() {
|
||||
return { setTitle }
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
import type { Ref } from "vue"
|
||||
import { debounce } from "lodash-es"
|
||||
|
||||
/** 默认配置 */
|
||||
const DEFAULT_CONFIG = {
|
||||
/** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */
|
||||
defense: true,
|
||||
/** 文本颜色 */
|
||||
color: "#c0c4cc",
|
||||
/** 文本透明度 */
|
||||
opacity: 0.5,
|
||||
/** 文本字体大小 */
|
||||
size: 16,
|
||||
/** 文本字体 */
|
||||
family: "serif",
|
||||
/** 文本倾斜角度 */
|
||||
angle: -20,
|
||||
/** 一处水印所占宽度(数值越大水印密度越低) */
|
||||
width: 300,
|
||||
/** 一处水印所占高度(数值越大水印密度越低) */
|
||||
height: 200
|
||||
}
|
||||
|
||||
type DefaultConfig = typeof DEFAULT_CONFIG
|
||||
|
||||
interface Observer {
|
||||
watermarkElMutationObserver?: MutationObserver
|
||||
parentElMutationObserver?: MutationObserver
|
||||
parentElResizeObserver?: ResizeObserver
|
||||
}
|
||||
|
||||
/** body 元素 */
|
||||
const bodyEl = ref<HTMLElement>(document.body)
|
||||
|
||||
/**
|
||||
* @name 水印 Composable
|
||||
* @description 1. 可以选择传入挂载水印的容器元素,默认是 body
|
||||
* @description 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
|
||||
*/
|
||||
export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
||||
// 备份文本
|
||||
let backupText: string
|
||||
// 最终配置
|
||||
let mergeConfig: DefaultConfig
|
||||
// 水印元素
|
||||
let watermarkEl: HTMLElement | null = null
|
||||
// 观察器
|
||||
const observer: Observer = {
|
||||
watermarkElMutationObserver: undefined,
|
||||
parentElMutationObserver: undefined,
|
||||
parentElResizeObserver: undefined
|
||||
}
|
||||
|
||||
// 设置水印
|
||||
const setWatermark = (text: string, config: Partial<DefaultConfig> = {}) => {
|
||||
if (!parentEl.value) return console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
|
||||
// 备份文本
|
||||
backupText = text
|
||||
// 合并配置
|
||||
mergeConfig = { ...DEFAULT_CONFIG, ...config }
|
||||
// 创建或更新水印元素
|
||||
watermarkEl ? updateWatermarkEl() : createWatermarkEl()
|
||||
// 监听水印元素和容器元素的变化
|
||||
addElListener(parentEl.value)
|
||||
}
|
||||
|
||||
// 创建水印元素
|
||||
const createWatermarkEl = () => {
|
||||
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
|
||||
const watermarkElPosition = isBody ? "fixed" : "absolute"
|
||||
const parentElPosition = isBody ? "" : "relative"
|
||||
watermarkEl = document.createElement("div")
|
||||
watermarkEl.style.pointerEvents = "none"
|
||||
watermarkEl.style.top = "0"
|
||||
watermarkEl.style.left = "0"
|
||||
watermarkEl.style.position = watermarkElPosition
|
||||
watermarkEl.style.zIndex = "99999"
|
||||
const { clientWidth, clientHeight } = parentEl.value!
|
||||
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
||||
// 设置水印容器为相对定位
|
||||
parentEl.value!.style.position = parentElPosition
|
||||
// 将水印元素添加到水印容器中
|
||||
parentEl.value!.appendChild(watermarkEl)
|
||||
}
|
||||
|
||||
// 更新水印元素
|
||||
const updateWatermarkEl = (
|
||||
options: Partial<{
|
||||
width: number
|
||||
height: number
|
||||
}> = {}
|
||||
) => {
|
||||
if (!watermarkEl) return
|
||||
backupText && (watermarkEl.style.background = `url(${createBase64()}) left top repeat`)
|
||||
options.width && (watermarkEl.style.width = `${options.width}px`)
|
||||
options.height && (watermarkEl.style.height = `${options.height}px`)
|
||||
}
|
||||
|
||||
// 创建 base64 图片
|
||||
const createBase64 = () => {
|
||||
const { color, opacity, size, family, angle, width, height } = mergeConfig
|
||||
const canvasEl = document.createElement("canvas")
|
||||
canvasEl.width = width
|
||||
canvasEl.height = height
|
||||
const ctx = canvasEl.getContext("2d")
|
||||
if (ctx) {
|
||||
ctx.fillStyle = color
|
||||
ctx.globalAlpha = opacity
|
||||
ctx.font = `${size}px ${family}`
|
||||
ctx.rotate((Math.PI / 180) * angle)
|
||||
ctx.fillText(backupText, 0, height / 2)
|
||||
}
|
||||
return canvasEl.toDataURL()
|
||||
}
|
||||
|
||||
// 清除水印
|
||||
const clearWatermark = () => {
|
||||
if (!parentEl.value || !watermarkEl) return
|
||||
// 移除对水印元素和容器元素的监听
|
||||
removeListener()
|
||||
// 移除水印元素
|
||||
try {
|
||||
parentEl.value.removeChild(watermarkEl)
|
||||
} catch {
|
||||
// 比如在无防御情况下,用户打开控制台删除了这个元素
|
||||
console.warn("水印元素已不存在,请重新创建")
|
||||
} finally {
|
||||
watermarkEl = null
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新水印(防御时调用)
|
||||
const updateWatermark = debounce(() => {
|
||||
clearWatermark()
|
||||
createWatermarkEl()
|
||||
addElListener(parentEl.value!)
|
||||
}, 100)
|
||||
|
||||
// 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化)
|
||||
const addElListener = (targetNode: HTMLElement) => {
|
||||
// 判断是否开启防御
|
||||
if (mergeConfig.defense) {
|
||||
// 防止重复添加监听
|
||||
if (!observer.watermarkElMutationObserver && !observer.parentElMutationObserver) {
|
||||
// 监听 DOM 变化
|
||||
addMutationListener(targetNode)
|
||||
}
|
||||
} else {
|
||||
// 无防御时不需要 mutation 监听
|
||||
removeListener("mutation")
|
||||
}
|
||||
// 防止重复添加监听
|
||||
if (!observer.parentElResizeObserver) {
|
||||
// 监听 DOM 大小变化
|
||||
addResizeListener(targetNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听
|
||||
const removeListener = (kind: "mutation" | "resize" | "all" = "all") => {
|
||||
// 移除 mutation 监听
|
||||
if (kind === "mutation" || kind === "all") {
|
||||
observer.watermarkElMutationObserver?.disconnect()
|
||||
observer.watermarkElMutationObserver = undefined
|
||||
observer.parentElMutationObserver?.disconnect()
|
||||
observer.parentElMutationObserver = undefined
|
||||
}
|
||||
// 移除 resize 监听
|
||||
if (kind === "resize" || kind === "all") {
|
||||
observer.parentElResizeObserver?.disconnect()
|
||||
observer.parentElResizeObserver = undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 DOM 变化
|
||||
const addMutationListener = (targetNode: HTMLElement) => {
|
||||
// 当观察到变动时执行的回调
|
||||
const mutationCallback = debounce((mutationList: MutationRecord[]) => {
|
||||
// 水印的防御(防止用户手动删除水印元素或通过 CSS 隐藏水印)
|
||||
mutationList.forEach(
|
||||
debounce((mutation: MutationRecord) => {
|
||||
switch (mutation.type) {
|
||||
case "attributes":
|
||||
mutation.target === watermarkEl && updateWatermark()
|
||||
break
|
||||
case "childList":
|
||||
mutation.removedNodes.forEach((item) => {
|
||||
item === watermarkEl && targetNode.appendChild(watermarkEl)
|
||||
})
|
||||
break
|
||||
}
|
||||
}, 100)
|
||||
)
|
||||
}, 100)
|
||||
// 创建观察器实例并传入回调
|
||||
observer.watermarkElMutationObserver = new MutationObserver(mutationCallback)
|
||||
observer.parentElMutationObserver = new MutationObserver(mutationCallback)
|
||||
// 以上述配置开始观察目标节点
|
||||
observer.watermarkElMutationObserver.observe(watermarkEl!, {
|
||||
// 观察目标节点属性是否变动,默认为 true
|
||||
attributes: true,
|
||||
// 观察目标子节点是否有添加或者删除,默认为 false
|
||||
childList: false,
|
||||
// 是否拓展到观察所有后代节点,默认为 false
|
||||
subtree: false
|
||||
})
|
||||
observer.parentElMutationObserver.observe(targetNode, {
|
||||
attributes: false,
|
||||
childList: true,
|
||||
subtree: false
|
||||
})
|
||||
}
|
||||
|
||||
// 监听 DOM 大小变化
|
||||
const addResizeListener = (targetNode: HTMLElement) => {
|
||||
// 当 targetNode 元素大小变化时去更新整个水印的大小
|
||||
const resizeCallback = debounce(() => {
|
||||
const { clientWidth, clientHeight } = targetNode
|
||||
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
||||
}, 500)
|
||||
// 创建一个观察器实例并传入回调
|
||||
observer.parentElResizeObserver = new ResizeObserver(resizeCallback)
|
||||
// 开始观察目标节点
|
||||
observer.parentElResizeObserver.observe(targetNode)
|
||||
}
|
||||
|
||||
// 在组件卸载前移除水印以及各种监听
|
||||
onBeforeUnmount(() => {
|
||||
clearWatermark()
|
||||
})
|
||||
|
||||
return { setWatermark, clearWatermark }
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
const SYSTEM_NAME = "mobvue"
|
||||
|
||||
/** 缓存数据时用到的 Key */
|
||||
export class CacheKey {
|
||||
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
|
||||
static readonly IS_DARK = `${SYSTEM_NAME}-is-dark-key`
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
// 统一处理 Cookie
|
||||
|
||||
import { CacheKey } from "@@/constants/cache-key"
|
||||
import Cookies from "js-cookie"
|
||||
|
||||
export function getToken() {
|
||||
return Cookies.get(CacheKey.TOKEN)
|
||||
}
|
||||
|
||||
export function setToken(token: string) {
|
||||
Cookies.set(CacheKey.TOKEN, token)
|
||||
}
|
||||
|
||||
export function removeToken() {
|
||||
Cookies.remove(CacheKey.TOKEN)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { CacheKey } from "@@/constants/cache-key"
|
||||
|
||||
export function getIsDark() {
|
||||
return localStorage.getItem(CacheKey.IS_DARK)
|
||||
}
|
||||
|
||||
export function setIsDark(isDark: boolean) {
|
||||
localStorage.setItem(CacheKey.IS_DARK, isDark.toString())
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
/** 获取指定元素(默认全局)上的 CSS 变量的值 */
|
||||
export function getCssVar(varName: string, element: HTMLElement = document.documentElement) {
|
||||
if (!varName?.startsWith("--")) {
|
||||
console.error("CSS 变量名应以 '--' 开头")
|
||||
return ""
|
||||
}
|
||||
// 没有拿到值时,会返回空串
|
||||
return getComputedStyle(element).getPropertyValue(varName)
|
||||
}
|
||||
|
||||
/** 设置指定元素(默认全局)上的 CSS 变量的值 */
|
||||
export function setCssVar(varName: string, value: string, element: HTMLElement = document.documentElement) {
|
||||
if (!varName?.startsWith("--")) {
|
||||
console.error("CSS 变量名应以 '--' 开头")
|
||||
return
|
||||
}
|
||||
element.style.setProperty(varName, value)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import dayjs from "dayjs"
|
||||
|
||||
const INVALID_DATE = "N/A"
|
||||
|
||||
/** 格式化日期时间 */
|
||||
export function formatDateTime(datetime: string | number | Date = "", template: string = "YYYY-MM-DD HH:mm:ss") {
|
||||
const day = dayjs(datetime)
|
||||
return day.isValid() ? day.format(template) : INVALID_DATE
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
export const paymentMethodOptions = [
|
||||
{ label: '微信支付', value: 0, type: 'primary' },
|
||||
{ label: '借呗支付', value: 1, type: 'success' },
|
||||
{ label: '要呗支付', value: 2, type: 'info' },
|
||||
{ label: '余额支付', value: 3, type: 'warning' },
|
||||
];
|
||||
|
||||
export const modeToPaymentMethodMap: Record<number, number[]> = {
|
||||
0: [0],
|
||||
1: [0, 1],
|
||||
2: [0, 1],
|
||||
3: [0],
|
||||
4: [2],
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export const publicPath = import.meta.env.BASE_URL
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { useUserStore } from "@/pinia/stores/user"
|
||||
import { isArray } from "@@/utils/validate"
|
||||
|
||||
/** 全局权限判断函数,和权限指令 v-permission 功能类似 */
|
||||
export function checkPermission(permissionRoles: string[]): boolean {
|
||||
if (isArray(permissionRoles) && permissionRoles.length > 0) {
|
||||
const { roles } = useUserStore()
|
||||
return roles.some(role => permissionRoles.includes(role))
|
||||
} else {
|
||||
console.error("参数必须是一个数组且长度大于 0,参考:checkPermission(['admin', 'editor'])")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/** 判断是否为数组 */
|
||||
export function isArray<T>(arg: T) {
|
||||
return Array.isArray ? Array.isArray(arg) : Object.prototype.toString.call(arg) === "[object Array]"
|
||||
}
|
||||
|
||||
/** 判断是否为字符串 */
|
||||
export function isString(str: unknown) {
|
||||
return typeof str === "string" || str instanceof String
|
||||
}
|
||||
|
||||
/** 判断是否为外链 */
|
||||
export function isExternal(path: string) {
|
||||
const reg = /^(https?:|mailto:|tel:)/
|
||||
return reg.test(path)
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
function openInWeChat(url: string) {
|
||||
const weChatUrl = `weixin://dl/business/?t=${encodeURIComponent(url)}`;
|
||||
window.location.href = weChatUrl;
|
||||
// 检测跳转是否成功
|
||||
setTimeout(() => {
|
||||
if (document.hidden) return;
|
||||
alert("未检测到微信,请手动打开微信并访问链接。");
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function checkInWeChat(targetUrl: string) {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
if (!ua.includes('micromessenger')) {
|
||||
openInWeChat(targetUrl); // 调用上述跳转函数
|
||||
} else {
|
||||
console.log("已在微信内,无需跳转");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import type { AxiosInstance, AxiosRequestConfig } from "axios"
|
||||
import { useUserStore } from "@/pinia/stores/user"
|
||||
import { getToken } from "@@/utils/cache/cookies"
|
||||
import axios from "axios"
|
||||
import { get, merge } from "lodash-es"
|
||||
|
||||
/** 退出登录并强制刷新页面(会重定向到登录页) */
|
||||
function logout() {
|
||||
useUserStore().resetToken()
|
||||
location.reload()
|
||||
}
|
||||
|
||||
/** 创建请求实例 */
|
||||
function createInstance() {
|
||||
// 创建一个 axios 实例命名为 instance
|
||||
const instance = axios.create()
|
||||
// 请求拦截器
|
||||
instance.interceptors.request.use(
|
||||
// 发送之前
|
||||
config => config,
|
||||
// 发送失败
|
||||
error => Promise.reject(error)
|
||||
)
|
||||
// 响应拦截器(可根据具体业务作出相应的调整)
|
||||
instance.interceptors.response.use(
|
||||
(response) => {
|
||||
// apiData 是 api 返回的数据
|
||||
const apiData = response.data
|
||||
// 二进制数据则直接返回
|
||||
const responseType = response.request?.responseType
|
||||
if (responseType === "blob" || responseType === "arraybuffer") return apiData
|
||||
// 这个 code 是和后端约定的业务 code
|
||||
const code = apiData.code
|
||||
// 如果没有 code, 代表这不是项目后端开发的 api
|
||||
if (code === undefined) {
|
||||
return Promise.reject(new Error("非本系统的接口"))
|
||||
}
|
||||
switch (code) {
|
||||
case 0:
|
||||
// 本系统采用 code === 0 来表示没有业务错误
|
||||
return apiData
|
||||
case 401:
|
||||
// 登录过期
|
||||
return logout()
|
||||
default:
|
||||
// 不是正确的 code
|
||||
return Promise.reject(new Error(apiData.msg || apiData.message || "Error"))
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
// status 是 HTTP 状态码
|
||||
const status = get(error, "response.status")
|
||||
const message = get(error, "response.data.message")
|
||||
switch (status) {
|
||||
case 400:
|
||||
error.message = "请求错误"
|
||||
break
|
||||
case 401:
|
||||
// 登录过期
|
||||
error.message = message || "登录过期"
|
||||
logout()
|
||||
break
|
||||
case 403:
|
||||
error.message = message || "拒绝访问"
|
||||
break
|
||||
case 404:
|
||||
error.message = "请求地址出错"
|
||||
break
|
||||
case 408:
|
||||
error.message = "请求超时"
|
||||
break
|
||||
case 500:
|
||||
error.message = "服务器内部错误"
|
||||
break
|
||||
case 501:
|
||||
error.message = "服务未实现"
|
||||
break
|
||||
case 502:
|
||||
error.message = "网关错误"
|
||||
break
|
||||
case 503:
|
||||
error.message = "服务不可用"
|
||||
break
|
||||
case 504:
|
||||
error.message = "网关超时"
|
||||
break
|
||||
case 505:
|
||||
error.message = "HTTP 版本不受支持"
|
||||
break
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return instance
|
||||
}
|
||||
|
||||
/** 创建请求方法 */
|
||||
function createRequest(instance: AxiosInstance) {
|
||||
return <T>(config: AxiosRequestConfig): Promise<T> => {
|
||||
const token = getToken()
|
||||
// 默认配置
|
||||
const defaultConfig: AxiosRequestConfig = {
|
||||
// 接口地址
|
||||
baseURL: import.meta.env.VITE_BASE_URL,
|
||||
// 请求头
|
||||
headers: {
|
||||
// 携带 Token
|
||||
// "Authorization": token ? `Bearer ${token}` : undefined,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
// 请求体
|
||||
data: {},
|
||||
// 请求超时
|
||||
timeout: 10000,
|
||||
// 跨域请求时是否携带 Cookies
|
||||
withCredentials: false
|
||||
}
|
||||
// 将默认配置 defaultConfig 和传入的自定义配置 config 进行合并成为 mergeConfig
|
||||
const mergeConfig = merge(defaultConfig, config)
|
||||
return instance(mergeConfig)
|
||||
}
|
||||
}
|
||||
|
||||
/** 用于请求的实例 */
|
||||
const instance = createInstance()
|
||||
|
||||
/** 用于请求的方法 */
|
||||
export const request = createRequest(instance)
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const VITE_APP_TITLE = import.meta.env.VITE_APP_TITLE
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer un-flex-center un-mb-20px un-text-14px un-color-hex-969799>
|
||||
MIT © 2025-PRESENT {{ VITE_APP_TITLE }}
|
||||
</footer>
|
||||
</template>
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const title = computed(() => route.meta.title)
|
||||
|
||||
const showLeftArrow = computed(() => route.meta.layout?.navBar?.showLeftArrow)
|
||||
|
||||
function onClickLeft() {
|
||||
history.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-nav-bar
|
||||
:title="title"
|
||||
:left-arrow="showLeftArrow"
|
||||
fixed
|
||||
placeholder
|
||||
safe-area-inset-top
|
||||
@click-left="onClickLeft"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
|
||||
const tabbarItemList = computed(() => {
|
||||
const routes = router.getRoutes()
|
||||
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar
|
||||
&& route.path !== '/cabinet'
|
||||
&& route.path!== '/approval/list'
|
||||
&& route.path!== '/approvalAsset/list'
|
||||
)
|
||||
.map(route => ({
|
||||
title: route.meta.title,
|
||||
icon: route.meta.layout?.tabbar?.icon,
|
||||
path: route.path
|
||||
}))
|
||||
})
|
||||
|
||||
import { useWxStoreOutside } from '@/pinia/stores/wx'
|
||||
const wxStore = useWxStoreOutside()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<van-tabbar
|
||||
route
|
||||
fixed
|
||||
placeholder
|
||||
safe-area-inset-bottom
|
||||
>
|
||||
<van-tabbar-item
|
||||
v-for="item in tabbarItemList"
|
||||
:key="item.path"
|
||||
:icon="item.icon"
|
||||
:to="item.path"
|
||||
replace
|
||||
>
|
||||
{{ item.title }}
|
||||
</van-tabbar-item>
|
||||
</van-tabbar>
|
||||
</template>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { useKeepAliveStore } from "@/pinia/stores/keep-alive"
|
||||
import Footer from "./components/Footer.vue"
|
||||
import NavBar from "./components/NavBar.vue"
|
||||
import Tabbar from "./components/Tabbar.vue"
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const showNavBar = computed(() => route.meta.layout?.navBar?.showNavBar)
|
||||
|
||||
const showTabbar = computed(() => route.meta.layout?.tabbar?.showTabbar)
|
||||
|
||||
const showFooter = computed(() => route.meta.layout?.footer)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div un-h-full un-flex un-flex-col>
|
||||
<NavBar v-if="showNavBar" />
|
||||
<div un-flex-1 un-overflow-y-auto>
|
||||
<!-- key 采用 route.path 和 route.fullPath 有着不同的效果,大多数时候 path 更通用 -->
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive :include="keepAliveStore.cachedRoutes">
|
||||
<component :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<Footer v-if="showFooter" />
|
||||
</div>
|
||||
<Tabbar v-if="showTabbar" />
|
||||
</div>
|
||||
</template>
|
||||