chore: 移除第三方文档目录及其相关文件

移除doc/thirdParty目录及其所有内容,包括Vue组件、样式、配置文件和测试文件等
This commit is contained in:
dzq 2025-11-07 17:25:32 +08:00
parent 3b3dd32155
commit 8100608f94
168 changed files with 0 additions and 22065 deletions

View File

@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)"
],
"deny": [],
"ask": []
}
}

View File

@ -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

10
doc/thirdParty/.env vendored
View File

@ -1,10 +0,0 @@
# 所有环境的环境变量(命名必须以 VITE_ 开头)
## 项目标题
VITE_APP_TITLE = 智柜通
## 路由模式 hash 或 html5
VITE_ROUTER_HISTORY = hash
## 是否开启 console 调试工具
VITE_CONSOLE = false

View File

@ -1,10 +0,0 @@
# 开发环境的环境变量(命名必须以 VITE_ 开头)
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
VITE_BASE_URL = http://localhost:8090/api
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
VITE_PUBLIC_PATH = /
# 后端地址
VITE_APP_BASE_API = '/dev-api'

View File

@ -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'

View File

@ -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'

View File

@ -1 +0,0 @@
custom: https://github.com/un-pany/mobvue/issues/1

View File

@ -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

View File

@ -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 }}

View File

@ -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

View File

@ -1,4 +0,0 @@
# 全局 ts 类型检查(此操作会增加 git commit 时长)
# npx vue-tsc
# 执行 lint-staged 中配置的任务
# npx lint-staged

View File

@ -1,5 +0,0 @@
# China mirror of npm
registry = https://registry.npmmirror.com
# 安装依赖时锁定版本号
save-exact = true

View File

@ -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.

View File

@ -1,190 +0,0 @@
<div align="center">
<img alt="logo" width="120" height="120" src="./public/favicon.ico">
<h1>Mobile + Vue = MobVue</h1>
</div>
[![github release](https://img.shields.io/github/v/release/un-pany/mobvue?style=flat)](https://github.com/un-pany/mobvue/releases)
[![github stars](https://img.shields.io/github/stars/un-pany/mobvue?style=flat)](https://github.com/un-pany/mobvue/stargazers)
[![gitee stars](https://gitee.com/un-pany/mobvue/badge/star.svg)](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 dont 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
![preview](./src/common/assets/images/preview.png)
## License
[MIT](./LICENSE) License © 2025-PRESENT [pany](https://github.com/pany-ang)

View File

@ -1,190 +0,0 @@
<div align="center">
<img alt="logo" width="120" height="120" src="./public/favicon.ico">
<h1>Mobile + Vue = MobVue</h1>
</div>
[![github release](https://img.shields.io/github/v/release/un-pany/mobvue?style=flat)](https://github.com/un-pany/mobvue/releases)
[![github stars](https://img.shields.io/github/stars/un-pany/mobvue?style=flat)](https://github.com/un-pany/mobvue/stargazers)
[![gitee stars](https://gitee.com/un-pany/mobvue/badge/star.svg)](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 或其他模块?试试懒人套餐![点击看看](https://github.com/un-pany/mobvue/issues/2)
## 使用
<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 引擎
## 项目预览图
![preview](./src/common/assets/images/preview.png)
## License
[MIT](./LICENSE) License © 2025-PRESENT [pany](https://github.com/pany-ang)

View File

@ -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();
};
```

View File

@ -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. 文档更新
- 更新相关文档
- 添加使用示例
- 记录变更内容

View File

@ -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 接口、配置说明和常见问题解决方案。

View File

@ -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 应用开发提供了坚实的基础。

View File

@ -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/)

View File

@ -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)

View File

@ -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"
}
}
)

View File

@ -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>

View File

@ -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"
]
}

File diff suppressed because it is too large Load Diff

View File

@ -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"]
}
}
}

View File

@ -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
}
}

View File

@ -1,7 +0,0 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "mobvue",
"setting": {
"compileHotReLoad": true
}
}

View File

@ -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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -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

View File

@ -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>

View File

@ -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
})
}

View File

@ -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;
}

View File

@ -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
})
}

View File

@ -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
}>

View File

@ -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'
});
};

View File

@ -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[]>

View File

@ -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 }
})
}

View File

@ -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
})
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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"
})
}

View File

@ -1 +0,0 @@
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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));
}
}

View File

@ -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>

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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 }
}

View File

@ -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`
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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],
};

View File

@ -1 +0,0 @@
export const publicPath = import.meta.env.BASE_URL

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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("已在微信内,无需跳转");
}
}

View File

@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More