初始化
|
@ -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
|
|
@ -0,0 +1,10 @@
|
||||||
|
# 所有环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
|
## 项目标题
|
||||||
|
VITE_APP_TITLE = MobVue
|
||||||
|
|
||||||
|
## 路由模式 hash 或 html5
|
||||||
|
VITE_ROUTER_HISTORY = hash
|
||||||
|
|
||||||
|
## 是否开启 console 调试工具
|
||||||
|
VITE_CONSOLE = true
|
|
@ -0,0 +1,7 @@
|
||||||
|
# 开发环境的环境变量(命名必须以 VITE_ 开头)
|
||||||
|
|
||||||
|
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
|
||||||
|
VITE_BASE_URL = http://localhost:8090/api
|
||||||
|
|
||||||
|
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
|
||||||
|
VITE_PUBLIC_PATH = /
|
|
@ -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/
|
|
@ -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 = /
|
|
@ -0,0 +1 @@
|
||||||
|
custom: https://github.com/un-pany/mobvue/issues/1
|
|
@ -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
|
|
@ -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 }}
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
||||||
|
# 全局 ts 类型检查(此操作会增加 git commit 时长)
|
||||||
|
npx vue-tsc
|
||||||
|
# 执行 lint-staged 中配置的任务
|
||||||
|
npx lint-staged
|
|
@ -0,0 +1,5 @@
|
||||||
|
# China mirror of npm
|
||||||
|
registry = https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 安装依赖时锁定版本号
|
||||||
|
save-exact = true
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vue.volar",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"antfu.unocss",
|
||||||
|
"vitest.explorer"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
|
@ -0,0 +1,190 @@
|
||||||
|
<div align="center">
|
||||||
|
<img alt="logo" width="120" height="120" src="./public/favicon.png">
|
||||||
|
<h1>Mobile + Vue = MobVue</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
[](https://github.com/un-pany/mobvue/releases)
|
||||||
|
[](https://github.com/un-pany/mobvue/stargazers)
|
||||||
|
[](https://gitee.com/un-pany/mobvue/stargazers)
|
||||||
|
|
||||||
|
<b>English | <a href="./README.zh-CN.md">中文</a></b>
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
MobVue is a well-crafted mobile web app template, built with popular technologies such as Vue3, Vite, TypeScript, and Vant
|
||||||
|
|
||||||
|
## Notifications
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Powered by love! All source code is free and open-source. If you find it helpful, feel free to give a star to support!
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> Paid services are officially launched! If you don’t want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/mobvue/issues/2)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Recommended Environment</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
- Latest version of `Visual Studio Code`
|
||||||
|
- Install the recommended plugins in the `.vscode/extensions.json` file
|
||||||
|
- `node` 20.x or 22+
|
||||||
|
- `pnpm` 9.x or 10+
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Local Development</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
pnpm i
|
||||||
|
|
||||||
|
# Start the development server
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Build</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for the staging environment
|
||||||
|
pnpm build:staging
|
||||||
|
|
||||||
|
# Build for the production environment
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Local Preview</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute the build command first to generate the dist directory, then run the preview command
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Code Check</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Code linting and formatting
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Unit tests
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Commit Guidelines</summary>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
`feat` New feature
|
||||||
|
|
||||||
|
`fix` Bug fix
|
||||||
|
|
||||||
|
`perf` Performance improvement
|
||||||
|
|
||||||
|
`refactor` Code refactoring
|
||||||
|
|
||||||
|
`docs` Documentation and comments
|
||||||
|
|
||||||
|
`types` Type-related changes
|
||||||
|
|
||||||
|
`test` Unit tests related
|
||||||
|
|
||||||
|
`ci` Continuous integration, workflows
|
||||||
|
|
||||||
|
`revert` Revert changes
|
||||||
|
|
||||||
|
`chore` Chores (update dependencies, modify configurations, etc)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
**Online Preview**:[github-pages](https://un-pany.github.io/mobvue)
|
||||||
|
|
||||||
|
**Documentation and Tutorials**:[link](https://juejin.cn/column/7472609448201666599)
|
||||||
|
|
||||||
|
**Chinese Repository**:[gitee](https://gitee.com/un-pany/mobvue)
|
||||||
|
|
||||||
|
**Chat Group**:[check how to join](https://github.com/un-pany/mobvue/issues/3)
|
||||||
|
|
||||||
|
**Donations**:[buy a coffee for the author](https://github.com/un-pany/mobvue/issues/1)
|
||||||
|
|
||||||
|
**Releases & Changelog**:[releases](https://github.com/un-pany/mobvue/releases)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
🔥 Latest [Syntax](https://vuejs.org/api/sfc-script-setup.html), [Configuration](./vite.config.ts), [Dependencies](./package.json)
|
||||||
|
|
||||||
|
📍 [Pure Level 1 Route Design](./src/router/index.ts) - Clear and Cache-Friendly
|
||||||
|
|
||||||
|
📱 Mobile Adaptation [px2vw](./postcss.config.ts) - Also Wide-Screen Friendly
|
||||||
|
|
||||||
|
🌐 Browser Compatibility [@vitejs/plugin-legacy](https://github.com/vitejs/vite/tree/main/packages/plugin-legacy) + [autoprefixer](https://github.com/postcss/autoprefixer) + [browserslist](https://github.com/browserslist/browserslist) - Compatible with multiple browsers and lower versions
|
||||||
|
|
||||||
|
🧩 [Layout System](./src/layout) - Configurable
|
||||||
|
|
||||||
|
🔒 Permission Control [Page Level](./src/router/guard.ts), [Button Level](./src/pages/demo/permission.vue)
|
||||||
|
|
||||||
|
🌗 Theme Mode [Dark Mode](./src/common/assets/styles/variables.css)
|
||||||
|
|
||||||
|
🫧 [Embrace Atomic CSS](./uno.config.ts)
|
||||||
|
|
||||||
|
🔧 [Components](https://github.com/unplugin/unplugin-vue-components) and [API](https://github.com/unplugin/unplugin-auto-import) Auto Import on Demand
|
||||||
|
|
||||||
|
🔎 [Husky](./.husky/pre-commit) + [lint-staged](./package.json) + [ESLint](./eslint.config.js) - Code Standardization
|
||||||
|
|
||||||
|
💪🏻 Still [TypeScript](./tsconfig.json) - Strict Mode with No `any`
|
||||||
|
|
||||||
|
👀 More Features - [Route Cache](./src/pinia/stores/keep-alive.ts), [Defensive Watermark](./src/common/composables/useWatermark.ts), [Grayscale and Colorblind Mode](./src/common/composables/useGrayscaleAndColorblind.ts), [SVG Loader](https://github.com/jpkleemans/vite-svg-loader), [VConsole](./src/plugins/console.ts), [White Screen Loading Animation](./public/app-loading.css), [Unit Tests](./tests)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
**Vue3**: Vue3 + script setup with the latest Vue3 Composition API
|
||||||
|
|
||||||
|
**Vant**:A lightweight, customizable Vue UI library for mobile web apps
|
||||||
|
|
||||||
|
**Pinia**: The legendary Vuex5
|
||||||
|
|
||||||
|
**Vite**: Really fast
|
||||||
|
|
||||||
|
**Vue Router**:The routing system
|
||||||
|
|
||||||
|
**TypeScript**:A superset of JavaScript
|
||||||
|
|
||||||
|
**pnpm**:A faster, disk-space-saving package manager
|
||||||
|
|
||||||
|
**ESlint**:Code linting and formatting
|
||||||
|
|
||||||
|
**Axios**:Sends network requests
|
||||||
|
|
||||||
|
**UnoCSS**:A high-performance, flexible atomic CSS engine
|
||||||
|
|
||||||
|
## Project Preview Image
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE) License © 2025-PRESENT [pany](https://github.com/pany-ang)
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
|
@ -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>
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||||
|
"projectname": "mobvue",
|
||||||
|
"setting": {
|
||||||
|
"compileHotReLoad": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 367 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 6.2 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 7.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 8.1 KiB |
After Width: | Height: | Size: 6.9 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 7.4 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 9.5 KiB |
After Width: | Height: | Size: 8.2 KiB |
|
@ -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>
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { request } from "@/http/axios"
|
||||||
|
import { ShopGoodsResponseData } from './type'
|
||||||
|
|
||||||
|
/** 获取当前登录用户详情 */
|
||||||
|
export function getShopGoodsApi() {
|
||||||
|
return request<ShopGoodsResponseData>({
|
||||||
|
url: "shop/goods",
|
||||||
|
method: "get"
|
||||||
|
})
|
||||||
|
}
|
|
@ -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[]
|
||||||
|
}>
|
|
@ -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"
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>
|
After Width: | Height: | Size: 198 KiB |
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
|
@ -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`
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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")
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<van-empty description="作者正在努力,点个 Star 为作者加速!" />
|
||||||
|
</template>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 60 KiB |
|
@ -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>
|
|
@ -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>
|