初始化

This commit is contained in:
dqz 2025-03-05 09:22:29 +08:00
commit 9a4038af05
130 changed files with 11981 additions and 0 deletions

24
.editorconfig Normal file
View File

@ -0,0 +1,24 @@
# 配置项文档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
.env Normal file
View File

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

7
.env.development Normal file
View File

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

7
.env.production Normal file
View File

@ -0,0 +1,7 @@
# 生产环境的环境变量(命名必须以 VITE_ 开头)
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/mobvue/ 域名下就需要填写 /mobvue/
VITE_PUBLIC_PATH = /mobvue/

7
.env.staging Normal file
View File

@ -0,0 +1,7 @@
# 预发布环境的环境变量(命名必须以 VITE_ 开头)
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /
VITE_PUBLIC_PATH = /

1
.github/FUNDING.yml vendored Normal file
View File

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

35
.github/workflows/deploy.yml vendored Normal file
View File

@ -0,0 +1,35 @@
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

27
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,27 @@
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 }}

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
# 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

4
.husky/pre-commit Normal file
View File

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

5
.npmrc Normal file
View File

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

9
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"recommendations": [
"vue.volar",
"editorconfig.editorconfig",
"dbaeumer.vscode-eslint",
"antfu.unocss",
"vitest.explorer"
]
}

15
.vscode/hook.code-snippets vendored Normal file
View File

@ -0,0 +1,15 @@
{
"Vue3 Composable 代码结构一键生成": {
"prefix": "Vue3 Composable",
"body": [
"const refName1 = ref<string>(\"这是一个响应式变量\")\n",
"export function useName() {",
"\tconst refName2 = ref<string>(\"这是一个响应式变量\")\n",
"\tconst fnName = () => {}\n",
"\treturn { refName1, refName2, fnName }",
"}",
"$1"
],
"description": "Vue3 Composable"
}
}

54
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,54 @@
{
// Use workspace TypeScript version
"typescript.tsdk": "node_modules/typescript/lib",
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"marscode.chatLanguage": "cn"
}

16
.vscode/vue.code-snippets vendored Normal file
View File

@ -0,0 +1,16 @@
{
"Vue3 SFC 代码结构一键生成": {
"prefix": "Vue3 SFC",
"body": [
"<script setup lang=\"ts\"></script>\n",
"<template>",
"\t<div>",
"\t\t...",
"\t</div>",
"</template>\n",
"<style scoped></style>",
"$1"
],
"description": "Vue3 SFC"
}
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
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.

190
README.md Normal file
View File

@ -0,0 +1,190 @@
<div align="center">
<img alt="logo" width="120" height="120" src="./public/favicon.png">
<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)

190
README.zh-CN.md Normal file
View File

@ -0,0 +1,190 @@
<div align="center">
<img alt="logo" width="120" height="120" src="./public/favicon.png">
<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)

42
eslint.config.js Normal file
View File

@ -0,0 +1,42 @@
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"
}
}
)

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!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.png" 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>

64
package.json Normal file
View File

@ -0,0 +1,64 @@
{
"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",
"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"
]
}

7731
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

36
postcss.config.ts Normal file
View File

@ -0,0 +1,36 @@
// 修改配置后重启服务生效
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"]
}
}
}

28
project.config.json Normal file
View File

@ -0,0 +1,28 @@
{
"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

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

45
public/app-loading.css Normal file
View File

@ -0,0 +1,45 @@
/* 白屏阶段会执行的 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;
}
}

BIN
public/c63.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/img/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/img/10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
public/img/11.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
public/img/12.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/img/13.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/img/14.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
public/img/15.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
public/img/16.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
public/img/17.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
public/img/18.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
public/img/19.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/img/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
public/img/20.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/img/21.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

BIN
public/img/22.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
public/img/23.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/img/24.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
public/img/25.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
public/img/26.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/img/27.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/img/28.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/img/29.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/img/3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/img/30.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
public/img/4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
public/img/5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/img/6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
public/img/7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
public/img/8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
public/img/9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

33
src/App.vue Normal file
View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import Layout from "@/layout/index.vue"
import { useUserStore } from "@/pinia/stores/user"
import { useDark } from "@@/composables/useDark"
// const userStore = useUserStore()
const { isDark, initDark } = useDark()
const isLoading = false;
// const isLoading = computed(() => userStore.token && !userStore.username)
// watch(
// () => userStore.token,
// (newVal) => {
// newVal && userStore.getInfo()
// },
// {
// immediate: true
// }
// )
initDark()
</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

@ -0,0 +1,10 @@
import { request } from "@/http/axios"
import { ShopGoodsResponseData } from './type'
/** 获取当前登录用户详情 */
export function getShopGoodsApi() {
return request<ShopGoodsResponseData>({
url: "shop/goods",
method: "get"
})
}

View File

@ -0,0 +1,21 @@
export type Goods = {
goodsId: number,
goodsName: string,
categoryId: number,
price: number,
stock: number,
status: number,
coverImg: string,
goodsDetail: string
}
export type category = {
categoryId: number,
categoryName: string,
sort: number
}
export type ShopGoodsResponseData = ApiResponseMsgData<{
goodsList: Goods[],
categoryList: category[]
}>

View File

@ -0,0 +1,10 @@
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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

View File

@ -0,0 +1,56 @@
/* 全局 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

@ -0,0 +1,21 @@
/* 全局 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;
}
/* 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

@ -0,0 +1,20 @@
/* 控制切换主题时的动画效果(只在较新的浏览器上生效,例如 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

@ -0,0 +1,21 @@
<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.png" 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

@ -0,0 +1,33 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,22 @@
/** 项目标题 */
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

@ -0,0 +1,233 @@
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

@ -0,0 +1,7 @@
const SYSTEM_NAME = "mobvue"
/** 缓存数据时用到的 Key */
export class CacheKey {
static readonly TOKEN = `${SYSTEM_NAME}-token-key`
static readonly IS_DARK = `${SYSTEM_NAME}-is-dark-key`
}

16
src/common/utils/cache/cookies.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// 统一处理 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

@ -0,0 +1,9 @@
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())
}

18
src/common/utils/css.ts Normal file
View File

@ -0,0 +1,18 @@
/** 获取指定元素(默认全局)上的 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

@ -0,0 +1,9 @@
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

@ -0,0 +1,13 @@
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

@ -0,0 +1,15 @@
/** 判断是否为数组 */
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)
}

128
src/http/axios.ts Normal file
View File

@ -0,0 +1,128 @@
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.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: 5000,
// 跨域请求时是否携带 Cookies
withCredentials: false
}
// 将默认配置 defaultConfig 和传入的自定义配置 config 进行合并成为 mergeConfig
const mergeConfig = merge(defaultConfig, config)
return instance(mergeConfig)
}
}
/** 用于请求的实例 */
const instance = createInstance()
/** 用于请求的方法 */
export const request = createRequest(instance)

View File

@ -0,0 +1,9 @@
<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

@ -0,0 +1,22 @@
<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

@ -0,0 +1,31 @@
<script setup lang="ts">
const router = useRouter()
const tabbarItemList = computed(() => {
const routes = router.getRoutes()
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar).map(route => ({
title: route.meta.title,
icon: route.meta.layout?.tabbar?.icon,
path: route.path
}))
})
</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>

32
src/layout/index.vue Normal file
View File

@ -0,0 +1,32 @@
<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>

29
src/main.ts Normal file
View File

@ -0,0 +1,29 @@
/* eslint-disable perfectionist/sort-imports */
// core
import { pinia } from "@/pinia"
import { router } from "@/router"
import { installPlugins } from "@/plugins"
import App from "@/App.vue"
// vant
import "@vant/touch-emulator"
// css
import "normalize.css"
import "nprogress/nprogress.css"
import "@@/assets/styles/index.css"
import "virtual:uno.css"
import "vant/lib/index.css"
// 创建应用实例
const app = createApp(App)
// 安装插件(全局组件、自定义指令等)
installPlugins(app)
// 安装 pinia 和 router
app.use(pinia).use(router)
// router 准备就绪后挂载应用
router.isReady().then(() => {
app.mount("#app")
})

5
src/pages/demo/chart.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
</template>

38
src/pages/demo/color.vue Normal file
View File

@ -0,0 +1,38 @@
<script setup lang="ts">
import type { Mode } from "@@/composables/useGrayscaleAndColorblind"
import { useGrayscaleAndColorblind } from "@@/composables/useGrayscaleAndColorblind"
import NoticeBar from "./components/NoticeBar.vue"
const { mode, setMode } = useGrayscaleAndColorblind()
const checked = ref<Mode>(mode.value)
function onChange(name: Mode) {
setMode(name)
}
</script>
<template>
<div un-mb-20px>
<NoticeBar text="针对哀悼场景和色弱色盲人群" />
<van-radio-group v-model="checked" @change="onChange">
<van-cell-group title="切换模式" inset>
<van-cell title="无" @click="checked = ''">
<template #right-icon>
<van-radio name="" />
</template>
</van-cell>
<van-cell title="灰色模式" @click="checked = 'grayscale'">
<template #right-icon>
<van-radio name="grayscale" />
</template>
</van-cell>
<van-cell title="色弱模式" @click="checked = 'colorblind'">
<template #right-icon>
<van-radio name="colorblind" />
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
</div>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
interface Props {
text: string
}
const props = defineProps<Props>()
</script>
<template>
<van-notice-bar
color="var(--mobvue-primary-color)"
background="#ecf9ff"
left-icon="bulb-o"
:text="props.text"
/>
</template>

5
src/pages/demo/i18n.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
</template>

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import NoticeBar from "./components/NoticeBar.vue"
defineOptions({
name: "KeepAlive"
})
const value = ref<string>("")
</script>
<template>
<div un-mb-20px>
<NoticeBar text="基于 KeepAlive 组件实现的路由缓存,但不受 Layout 影响" />
<van-cell-group inset un-mt-20px>
<van-field v-model="value" placeholder="请输入" />
</van-cell-group>
</div>
</template>

View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
</template>

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { useUserStore } from "@/pinia/stores/user"
import { checkPermission } from "@@/utils/permission"
import NoticeBar from "./components/NoticeBar.vue"
const userStore = useUserStore()
const checked = ref<string>(userStore.roles[0])
function onChange(name: string) {
userStore.changeRoles(name)
}
</script>
<template>
<div un-mb-20px>
<NoticeBar text="基于权限指令、权限函数实现的按钮级控制" />
<van-radio-group v-model="checked" @change="onChange">
<van-cell-group title="切换用户" inset>
<van-cell title="Admin 用户" @click="checked = 'admin'">
<template #right-icon>
<van-radio name="admin" />
</template>
</van-cell>
<van-cell title="Editor 用户" @click="checked = 'editor'">
<template #right-icon>
<van-radio name="editor" />
</template>
</van-cell>
</van-cell-group>
</van-radio-group>
<van-cell-group title="权限指令" inset>
<van-cell v-permission="['admin']" title="Admin 可见" value="Role admin" />
<van-cell v-permission="['admin', 'editor']" title="Admin 或 Editor 可见" value="Role admin or editor" />
</van-cell-group>
<van-cell-group title="权限函数" inset>
<van-cell v-if="checkPermission(['admin'])" title="Admin 可见" value="Role admin" />
<van-cell v-if="checkPermission(['admin', 'editor'])" title="Admin 或 Editor 可见" value="Role admin or editor" />
</van-cell-group>
</div>
</template>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { useWatermark } from "@@/composables/useWatermark"
import NoticeBar from "./components/NoticeBar.vue"
const localRef = ref<HTMLElement | null>(null)
const { setWatermark, clearWatermark } = useWatermark(localRef)
const { setWatermark: setGlobalWatermark, clearWatermark: clearGlobalWatermark } = useWatermark()
</script>
<template>
<div un-mb-20px>
<NoticeBar text="支持局部、全局、自定义样式,并自带防御和自适应功能" />
<div
ref="localRef"
class="local"
un-mt-40px
un-h-35vh
un-b-2px
un-b-dashed
un-b-primary
/>
<div un-mt-40px un-flex-x-center>
<van-button
type="primary"
size="small"
square
@click="setWatermark('局部水印', { color: '#409eff', width: 200, height: 150 })"
>
创建局部水印
</van-button>
<van-button
type="warning"
size="small"
square
@click="setWatermark('没有防御功能的局部水印', { color: '#e6a23c', defense: false, width: 200, height: 150 })"
>
创建无防御局部水印
</van-button>
<van-button
type="danger"
size="small"
square
@click="clearWatermark"
>
清除局部水印
</van-button>
</div>
<div un-mt-20px un-flex-x-center>
<van-button
type="primary"
size="small"
square
@click="setGlobalWatermark('全局水印', { color: '#409eff', width: 200, height: 150 })"
>
创建全局水印
</van-button>
<van-button
type="warning"
size="small"
square
@click="setGlobalWatermark('没有防御功能的全局水印', { color: '#e6a23c', defense: false, width: 200, height: 150 })"
>
创建无防御全局水印
</van-button>
<van-button
type="danger"
size="small"
square
@click="clearGlobalWatermark"
>
清除全局水印
</van-button>
</div>
</div>
</template>

10
src/pages/error/403.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import Layout from "./components/Layout.vue"
import Svg403 from "./images/403.svg?component" // vite-svg-loader
</script>
<template>
<Layout>
<Svg403 />
</Layout>
</template>

10
src/pages/error/404.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup lang="ts">
import Layout from "./components/Layout.vue"
import Svg404 from "./images/404.svg?component" // vite-svg-loader
</script>
<template>
<Layout>
<Svg404 />
</Layout>
</template>

View File

@ -0,0 +1,10 @@
<template>
<div un-h-full un-flex-center un-flex-col>
<div un-w-250px un-mb-50px>
<slot />
</div>
<van-button type="primary" to="/" replace>
回到首页
</van-button>
</div>
</template>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 60 KiB

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { router } from "@/router"
interface Props {
title: string
path: string
}
const props = defineProps<Props>()
function onClick() {
router.push(props.path)
}
</script>
<template>
<div
@click="onClick"
un-h-40px
un-px-20px
un-rounded-99px
un-bg-bg
un-color-hex-34495e
un-dark:color-hex-ffffffbf
un-text-14px
un-fw-600
un-flex-y-center
un-justify-between
un-transition-opacity-300
un-active-opacity-60
>
<span>{{ props.title }}</span>
<van-icon name="arrow" color="#B6C3D2" />
</div>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
interface Props {
text: string
}
const props = defineProps<Props>()
</script>
<template>
<span un-pl-16px un-color-hex-969799 un-text-14px un-fw400>
{{ props.text }}
</span>
</template>

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