初始化
|
@ -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>
|