This commit is contained in:
dzq 2025-12-17 16:49:16 +08:00
commit 9edd33b062
92 changed files with 14126 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root=true
[*]
charset=utf-8
indent_style=space
indent_size=2
end_of_line=lf
insert_final_newline=true
trim_trailing_whitespace=true

5
.env Normal file
View File

@ -0,0 +1,5 @@
VITE_APP_PUBLIC_PATH=/
VITE_APP_PREVIEW=true
VITE_APP_API_BASE_URL=/api
VITE_APP_OUT_DIR=dist
VITE_APP_VCONSOLE=false

3
.env.development Normal file
View File

@ -0,0 +1,3 @@
NODE_ENV=development
VITE_APP_PREVIEW=true
VITE_APP_API_BASE_URL=/api

2
.env.production Normal file
View File

@ -0,0 +1,2 @@
VITE_APP_PREVIEW=false
VITE_APP_API_BASE_URL=https://easyapi.devv.zone/api

77
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,77 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to make participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies within all project spaces, and it also applies when
an individual is representing the project or its community in public spaces.
Examples of representing a project or community include using an official
project e-mail address, posting via an official social media account, or acting
as an appointed representative at an online or offline event. Representation of
a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [18888351756@163.com]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html>
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
<https://www.contributor-covenant.org/faq>

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

@ -0,0 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set node
uses: actions/setup-node@v3
with:
node-version: 20.x
- run: npx conventional-github-releaser -p angular
env:
CONVENTIONAL_GITHUB_RELEASER_TOKEN: ${{secrets.GITHUB_TOKEN}}

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
stats.html
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Misc
.DS_Store
.history

1
.node-version Normal file
View File

@ -0,0 +1 @@
20.19.0

1
.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

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

@ -0,0 +1,12 @@
{
"recommendations": [
"Vue.volar",
"antfu.unocss",
"antfu.goto-alias",
"antfu.iconify",
"antfu.file-nesting",
"lokalise.i18n-ally",
"blueglassblock.better-json5",
"dbaeumer.vscode-eslint"
]
}

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

@ -0,0 +1,66 @@
{
// 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",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
// Configuration of i18n i18n-ally
"i18n-ally.enabledParsers": ["json"],
"i18n-ally.displayLanguage": "zh-CN",
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.keystyle": "nested",
// Markdownlint rules
"markdownlint.config": {
"default": true,
"MD033": false,
"MD041": false
}
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Charlie Wang
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.

241
README.md Normal file
View File

@ -0,0 +1,241 @@
<div id="top" align="center">
<img src="https://cdn.jsdelivr.net/gh/vue-zone/static/cover.png" alt="cover" />
<h1 align="center">vue3-vant-mobile</h1>
English / [简体中文](./README.zh-CN.md)
An mobile web apps template based on the Vue 3 ecosystem.
一个基于 Vue 3 生态系统的移动 web 应用模板,帮助你快速完成业务开发。
<p>
<img src="https://img.shields.io/github/license/vue-zone/vue3-vant-mobile" alt="license" />
<img src="https://img.shields.io/github/package-json/v/vue-zone/vue3-vant-mobile" alt="version" />
<img src="https://img.shields.io/github/repo-size/vue-zone/vue3-vant-mobile" alt="repo-size" />
<img src="https://img.shields.io/github/languages/top/vue-zone/vue3-vant-mobile" alt="languages" />
<img src="https://img.shields.io/github/issues-closed/vue-zone/vue3-vant-mobile" alt="issues" />
</p>
[🌐预览](https://vue3-vant-mobile.netlify.app) / [📖文档](https://vue-zone.github.io/docs/vue3-vant-mobile/) / [🗨️交流](https://github.com/vue-zone/vue3-vant-mobile/issues/56) / [📝反馈](https://github.com/vue-zone/vue3-vant-mobile/issues)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/vue-zone/vue3-vant-mobile) [![Netlify Status](https://api.netlify.com/api/v1/badges/e6828bd2-2904-4c3e-a67c-b97d32aa1275/deploy-status)](https://app.netlify.com/sites/vue3-vant-mobile/deploys)
</div>
<br>
## Features
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite 7](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - born with fastness
- 🗂 [File based routing](./src/router)
- 📦 [Components auto importing](./src/components)
- 🍍 [State Management via Pinia](https://pinia.vuejs.org)
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
- 🎨 [UnoCSS](https://github.com/antfu/unocss) - the instant on-demand atomic CSS engine
- 🌍 [I18n ready](./src/locales)
- 🔥 Use the [new `<script setup>` syntax](https://github.com/vuejs/rfcs/pull/227)
- 📥 [APIs auto importing](https://github.com/antfu/unplugin-auto-import) - use Composition API and others directly
- 💪 TypeScript, of course
- 💾 [Mock](https://github.com/pengzhanbo/vite-plugin-mock-dev-server) server Support
- 🌈 Git hooks - lint and commit
- 🪶 [Vant](https://github.com/youzan/vant) - Vue UI library for mobile web apps
- 🔭 [vConsole](https://github.com/vadxq/vite-plugin-vconsole) - the developer tool for mobile web page
- 📱 Browser adaptation - use viewport vw/vh units
- 💻 [Desktop optimization](https://github.com/wswmsword/postcss-mobile-forever) - the mobile area
- 🌓 Dark Mode Support
- 🛡️ Configure [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) as default
- ☁️ Deploy on [Netlify](https://www.netlify.com), zero-config
<br>
## Pre-packed
### UI Frameworks
- [UnoCSS](https://github.com/antfu/unocss) - The instant on-demand atomic CSS engine
- [Vant](https://github.com/youzan/vant) - Vue UI library for mobile web apps
- [`vant-touch-emulator`](https://github.com/youzan/vant/tree/main/packages/vant-touch-emulator) - Simulate mobile touch events on the desktop
- [`vant-use`](https://github.com/youzan/vant/tree/main/packages/vant-use) - Built-in composition APIs of Vant
### Plugins
- [Vue Router](https://github.com/vuejs/router)
- [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) - file system based routing
- [Pinia](https://pinia.vuejs.org) - Intuitive, type safe, light and flexible Store for Vue using the composition api
- [`pinia-plugin-persistedstate`](https://github.com/prazdevs/pinia-plugin-persistedstate) - Configurable persistence and rehydration of Pinia stores
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization
- [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - unplugin for Vue I18n
- [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) - components auto import
- [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import) - Directly use Vue Composition API and others without importing
- [vite-plugin-vconsole](https://github.com/vadxq/vite-plugin-vconsole) - A lightweight, extendable front-end developer tool for mobile web page
- [vite-plugin-mock-dev-server](https://github.com/pengzhanbo/vite-plugin-mock-dev-server) - Vite Plugin for API mock dev server
- [postcss-mobile-forever](https://github.com/wswmsword/postcss-mobile-forever) - To adapt different displays by one mobile viewport
- [vite-plugin-vue-devtools](https://github.com/vuejs/devtools-next) - Designed to enhance the Vue developer experience
- [vueuse](https://github.com/antfu/vueuse) - collection of useful composition APIs
- [@unhead/vue v2](https://github.com/unjs/unhead) - manipulate document head reactively
- [vite-plugin-pwa](https://github.com/antfu/vite-plugin-pwa) - PWA
- [vite-plugin-sitemap](https://github.com/jbaubree/vite-plugin-sitemap) - sitemap and robots generator
### Coding Style
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
- [ESLint](https://eslint.org/) with [@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi
### Dev tools
- [TypeScript](https://www.typescriptlang.org/)
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
- [Netlify](https://www.netlify.com/) - zero-config deployment
- [VS Code Extensions](./.vscode/extensions.json)
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 3 `<script setup>` IDE support
- [Unocss](https://marketplace.visualstudio.com/items?itemName=antfu.unocss) - UnoCSS for VS Code
- [Goto Alias](https://marketplace.visualstudio.com/items?itemName=antfu.goto-alias) - Go to Definition following alias redirections
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - Icon inline display and autocomplete
- [File Nesting](https://marketplace.visualstudio.com/items?itemName=antfu.file-nesting) - Config of File Nesting for VS Code
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - All in one i18n support
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - Eslint support
- [Better JSON5](https://marketplace.visualstudio.com/items?itemName=blueglassblock.better-json5) - JSON5 support
## Try it now
> vue3-vant-mobile requires Node 20+
### GitHub Template
[Create a repo from this template on GitHub](https://github.com/vue-zone/vue3-vant-mobile/generate)
### Clone to local
If you prefer to do it manually with the cleaner git history
```bash
npx tiged vue-zone/vue3-vant-mobile my-mobile-app
cd my-mobile-app
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
```
## Checklist
When you use this template, try follow the checklist to update your info properly
- [ ] Change the author name in `LICENSE`
- [ ] Change the title in `index.html`
- [ ] Change the hostname in `vite.config.ts`
- [ ] Change the favicon in `public`
- [ ] Clean up the READMEs and remove routes
And, enjoy :)
## Usage
### Development
Just run and visit <http://localhost:3000>
```bash
pnpm dev
```
### Build
To build the App, run
```bash
pnpm build
```
And you will see the generated file in `dist` that ready to be served.
### Deploy on Netlify
Go to [Netlify](https://app.netlify.com/start) and select your clone, `OK` along the way, and your App will be live in a minute.
## Community 👏
We recommend that [issue](https://github.com/vue-zone/vue3-vant-mobile/issues) be used for problem feedback, or [Wechat group](https://github.com/vue-zone/vue3-vant-mobile/issues/56).
## Donation ☕
[Buy Me a Coffee](https://github.com/cwandev/sponsor)
<h2 align="center">💝 Our Sponsors 💝</h2>
<p align="center">
Your sponsorship will help us continue to iterate on this exciting project! 🚀
</p>
<p align="center">
<a href="https://github.com/keyFeng"><img src="https://avatars.githubusercontent.com/u/52267976?v=4" width="60px" alt="keyFeng" /></a>
<a href="https://github.com/ljt990218"><img src="https://avatars.githubusercontent.com/u/50509815?v=4" width="60px" alt="ljt990218" /></a>
<a href="https://github.com/heked"><img src="https://avatars.githubusercontent.com/u/14127731?v=4" width="60px" alt="heked" /></a>
<a href="https://github.com/topcnm"><img src="https://avatars.githubusercontent.com/u/8057893?v=4" width="60px" alt="topcnm" /></a>
<a href="https://github.com/qiyue2015"><img src="https://avatars.githubusercontent.com/u/11554433?v=4" width="60px" alt="qiyue2015" /></a>
<a href="https://github.com/scc0"><img src="https://avatars.githubusercontent.com/u/45963033?v=4" width="60px" alt="scc0" /></a>
<a href="https://github.com/xiaminxi"><img src="https://avatars.githubusercontent.com/u/37994820?v=4" width="60px" alt="xiaminxi" /></a>
<a href="https://github.com/wangpeng00544"><img src="https://avatars.githubusercontent.com/u/54630102?v=4" width="60px" alt="wangpeng00544" /></a>
<a href="https://github.com/ljgx"><img src="https://avatars.githubusercontent.com/u/59424192?v=4" width="60px" alt="ljgx" /></a>
<a href="https://github.com/3026546679"><img src="https://avatars.githubusercontent.com/u/36257162?v=4" width="60px" alt="3026546679" /></a>
<a href="https://github.com/shuilong001"><img src="https://avatars.githubusercontent.com/u/219820297?v=4" width="60px" alt="shuilong001" /></a>
</p>
<h2 align="center">
💪 Contributors 💪
</h2>
<p align="center">
Our contributors have made this project better. Thank you! 🙏
</p>
<p align="center">
<a href="https://github.com/cwandev"><img src="https://avatars.githubusercontent.com/u/22477554?v=4" width="60px" alt="cwandev" /></a>
<a href="https://github.com/ljt990218"><img src="https://avatars.githubusercontent.com/u/50509815?v=4" width="60px" alt="ljt990218" /></a>
<a href="https://github.com/wswmsword"><img src="https://avatars.githubusercontent.com/u/26893092?v=4" width="60px" alt="wswmsword" /></a>
<a href="https://github.com/weiq"><img src="https://avatars.githubusercontent.com/u/1697158?v=4" width="60px" alt="weiq" /></a>
<a href="https://github.com/SublimeCT"><img src="https://avatars.githubusercontent.com/u/20380890?v=4" width="60px" alt="SublimeCT" /></a>
<a href="https://github.com/ReginYuan"><img src="https://avatars.githubusercontent.com/u/49477488?v=4" width="60px" alt="ReginYuan" /></a>
<a href="https://github.com/smartsf"><img src="https://avatars.githubusercontent.com/u/19995400?v=4" width="60px" alt="smartsf" /></a>
<a href="https://github.com/Kysen777"><img src="https://avatars.githubusercontent.com/u/63892082?v=4" width="60px" alt="Kysen777" /></a>
<a href="https://github.com/Leezon"><img src="https://avatars.githubusercontent.com/u/38120280?v=4" width="60px" alt="Leezon" /></a>
<a href="https://github.com/AlphaYoung111"><img src="https://avatars.githubusercontent.com/u/54132313?v=4" width="60px" alt="AlphaYoung111" /></a>
<a href="https://github.com/leo4developer"><img src="https://avatars.githubusercontent.com/u/15160478?v=4" width="60px" alt="leo4developer" /></a>
<a href="https://github.com/InsHomePgup"><img src="https://avatars.githubusercontent.com/u/47906083?v=4" width="60px" alt="InsHomePgup" /></a>
<a href="https://github.com/wowping"><img src="https://avatars.githubusercontent.com/u/137802961?v=4" width="60px" alt="wowping" /></a>
<a href="https://github.com/ChunyuPCY"><img src="https://avatars.githubusercontent.com/u/21986942?v=4" width="60px" alt="ChunyuPCY" /></a>
<a href="https://github.com/qiyue2015"><img src="https://avatars.githubusercontent.com/u/11554433?v=4" width="60px" alt="qiyue2015" /></a>
<a href="https://github.com/pyQianYi"><img src="https://avatars.githubusercontent.com/u/57526688?v=4" width="60px" alt="pyQianYi" /></a>
<a href="https://github.com/xyy94813"><img src="https://avatars.githubusercontent.com/u/17971352?v=4" width="60px" alt="xyy94813" /></a>
<a href="https://github.com/faukwaa"><img src="https://avatars.githubusercontent.com/u/133618995?v=4" width="60px" alt="faukwaa" /></a>
<a href="https://github.com/chensongni"><img src="https://avatars.githubusercontent.com/u/18071921?v=4" width="60px" alt="chensongni" /></a>
<a href="https://github.com/csheng-github"><img src="https://avatars.githubusercontent.com/u/88492404?v=4" width="60px" alt="csheng-github" /></a>
<a href="https://github.com/LostElkByte"><img src="https://avatars.githubusercontent.com/u/24487727?v=4" width="60px" alt="LostElkByte" /></a>
<a href="https://github.com/xuxichen"><img src="https://avatars.githubusercontent.com/u/18108140?v=4" width="60px" alt="xuxichen" /></a>
<a href="https://github.com/1411430556"><img src="https://avatars.githubusercontent.com/u/67215517?v=4" width="60px" alt="1411430556" /></a>
</p>
## License
[MIT](./LICENSE) License
<p align="right">
<a href="#top">⬆️ Back to Top</a>
</p>

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

@ -0,0 +1,241 @@
<div id="top" align="center">
<img src="https://cdn.jsdelivr.net/gh/vue-zone/static/cover.png" alt="cover" />
<h1 align="center">vue3-vant-mobile</h1>
[English](./README.md) / 简体中文
An mobile web apps template based on the Vue 3 ecosystem.
一个基于 Vue 3 生态系统的移动 web 应用模板,帮助你快速完成业务开发。
<p>
<img src="https://img.shields.io/github/license/vue-zone/vue3-vant-mobile" alt="license" />
<img src="https://img.shields.io/github/package-json/v/vue-zone/vue3-vant-mobile" alt="version" />
<img src="https://img.shields.io/github/repo-size/vue-zone/vue3-vant-mobile" alt="repo-size" />
<img src="https://img.shields.io/github/languages/top/vue-zone/vue3-vant-mobile" alt="languages" />
<img src="https://img.shields.io/github/issues-closed/vue-zone/vue3-vant-mobile" alt="issues" />
</p>
[🌐预览](https://vue3-vant-mobile.netlify.app) / [📖文档](https://vue-zone.github.io/docs/vue3-vant-mobile/) / [🗨️交流](https://github.com/vue-zone/vue3-vant-mobile/issues/56) / [📝反馈](https://github.com/vue-zone/vue3-vant-mobile/issues)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/vue-zone/vue3-vant-mobile) [![Netlify Status](https://api.netlify.com/api/v1/badges/e6828bd2-2904-4c3e-a67c-b97d32aa1275/deploy-status)](https://app.netlify.com/sites/vue3-vant-mobile/deploys)
</div>
<br>
## Features
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite 7](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
- 🗂 [基于文件的路由](./src/router)
- 📦 [组件自动化加载](./src/components)
- 🍍 [使用 Pinia 的状态管理](https://pinia.vuejs.org)
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
- 🎨 [UnoCSS](https://github.com/antfu/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
- 🌍 [I18n 国际化开箱即用](./src/locales)
- 🔥 使用 [新的 `<script setup>` 语法](https://github.com/vuejs/rfcs/pull/227)
- 📥 [API 自动加载](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 无需引入
- 💪 TypeScript, 当然
- 💾 [本地数据模拟](https://github.com/pengzhanbo/vite-plugin-mock-dev-server)的支持
- 🌈 Git hooks - 提交代码 eslint 检测 和 提交规范检测
- 🪶 [Vant](https://github.com/youzan/vant) - 移动端 Vue 组件库
- 🔭 [vConsole](https://github.com/vadxq/vite-plugin-vconsole) - 移动端网页开发工具
- 📱 浏览器适配 - 使用 viewport vw/vh 单位布局
- 💻 [桌面端优化](https://github.com/wswmsword/postcss-mobile-forever) - 处理为移动端视图
- 🌓 支持深色模式
- 🛡️ 将 [ESM](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) 设为默认
- ☁️ 零配置部署 [Netlify](https://www.netlify.com)
<br>
## 预配置
### UI 框架
- [UnoCSS](https://github.com/antfu/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
- [Vant](https://github.com/youzan/vant) - 移动端 Vue 组件库
- [`vant-touch-emulator`](https://github.com/youzan/vant/tree/main/packages/vant-touch-emulator) - 在桌面端上模拟移动端 touch 事件
- [`vant-use`](https://github.com/youzan/vant/tree/main/packages/vant-use) - Vant 内置的组合式 API
### 插件
- [Vue Router](https://github.com/vuejs/router)
- [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) - 以文件系统为基础的路由
- [Pinia](https://pinia.vuejs.org) - 直接的, 类型安全的, 使用 Composition API 的轻便灵活的 Vue 状态管理库
- [`pinia-plugin-persistedstate`](https://github.com/prazdevs/pinia-plugin-persistedstate) - 适用于 Pinia 的持久化存储插件
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - 国际化
- [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - Vue I18n 的 Vite 插件
- [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) - 自动加载组件
- [unplugin-auto-import](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 等,无需导入
- [vite-plugin-vconsole](https://github.com/vadxq/vite-plugin-vconsole) - vConsole 的 vite 插件
- [vite-plugin-mock-dev-server](https://github.com/pengzhanbo/vite-plugin-mock-dev-server) - vite mock 开发服务mock-dev-server插件
- [postcss-mobile-forever](https://github.com/wswmsword/postcss-mobile-forever) - 一款 PostCSS 插件,将固定尺寸的移动端视图转为具有最大宽度的可伸缩的移动端视图
- [vite-plugin-vue-devtools](https://github.com/vuejs/devtools-next) - 旨在增强Vue开发者体验的Vite插件
- [vueuse](https://github.com/antfu/vueuse) - 实用的 Composition API 工具合集
- [@unhead/vue v2](https://github.com/unjs/unhead) - 响应式地操作文档头信息
- [vite-plugin-pwa](https://github.com/antfu/vite-plugin-pwa) - PWA
- [vite-plugin-sitemap](https://github.com/jbaubree/vite-plugin-sitemap) - sitemap 和 robots 生成器
### 编码风格
- 使用 Composition API 地 [`<script setup>` SFC 语法](https://github.com/vuejs/rfcs/pull/227)
- [ESLint](https://eslint.org/) 配置为 [@antfu/eslint-config](https://github.com/antfu/eslint-config), 单引号, 无分号
### 开发工具
- [TypeScript](https://www.typescriptlang.org/)
- [pnpm](https://pnpm.js.org/) - 快, 节省磁盘空间的包管理器
- [Netlify](https://www.netlify.com/) - 零配置的部署
- [VS Code Extensions](./.vscode/extensions.json)
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 3 `<script setup>` IDE 支持
- [Unocss](https://marketplace.visualstudio.com/items?itemName=antfu.unocss) - Unocss 智能提示
- [Goto Alias](https://marketplace.visualstudio.com/items?itemName=antfu.goto-alias) - 跳转到定义
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - 图标内联显示和自动补全
- [File Nesting](https://marketplace.visualstudio.com/items?itemName=antfu.file-nesting) - 文件嵌套
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - 多合一的 I18n 支持
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) - ESLint 支持
- [Better JSON5](https://marketplace.visualstudio.com/items?itemName=blueglassblock.better-json5) - JSON5 支持
## 现在可以试试
> vue3-vant-mobile 需要 Node 版本 20+
### GitHub 模板
[使用这个模板创建仓库](https://github.com/vue-zone/vue3-vant-mobile/generate)
### 克隆到本地
如果您更喜欢使用更干净的 git 历史记录手动执行此操作
```bash
npx tiged vue-zone/vue3-vant-mobile my-mobile-app
cd my-mobile-app
pnpm i # 如果你没装过 pnpm, 可以先运行: npm install -g pnpm
```
## 清单
使用此模板时,请尝试按照清单正确更新您自己的信息
- [ ] 在 `LICENSE` 中改变作者名
- [ ] 在 `index.html` 中改变标题
- [ ] 在 `vite.config.ts` 更改主机名
- [ ] 在 `public` 目录下改变favicon
- [ ] 整理 README 并删除路由
紧接着, 享受吧 :)
## 使用
### 开发
只需要执行以下命令就可以在 <http://localhost:3000> 中看到
```bash
pnpm dev
```
### 构建
构建该应用只需要执行以下命令
```bash
pnpm build
```
然后你会看到用于发布的 `dist` 文件夹被生成。
### 部署到 Netlify
前往 [Netlify](https://app.netlify.com/start) 并选择你的仓库, 一路 `OK` 下去,稍等一下后,你的应用将被创建。
## 社区 👏
我们推荐使用 [议题](https://github.com/vue-zone/vue3-vant-mobile/issues) 来反馈问题, 或者您也可以通过 [微信交流群](https://github.com/vue-zone/vue3-vant-mobile/issues/56) 联系我们。
## 捐赠 ☕
[请我喝一杯咖啡](https://github.com/cwandev/sponsor)
<h2 align="center">💝 我们的赞助者 💝</h2>
<p align="center">
您的赞助将帮助我们继续迭代这个令人兴奋的项目! 🚀
</p>
<p align="center">
<a href="https://github.com/keyFeng"><img src="https://avatars.githubusercontent.com/u/52267976?v=4" width="60px" alt="keyFeng" /></a>
<a href="https://github.com/ljt990218"><img src="https://avatars.githubusercontent.com/u/50509815?v=4" width="60px" alt="ljt990218" /></a>
<a href="https://github.com/heked"><img src="https://avatars.githubusercontent.com/u/14127731?v=4" width="60px" alt="heked" /></a>
<a href="https://github.com/topcnm"><img src="https://avatars.githubusercontent.com/u/8057893?v=4" width="60px" alt="topcnm" /></a>
<a href="https://github.com/qiyue2015"><img src="https://avatars.githubusercontent.com/u/11554433?v=4" width="60px" alt="qiyue2015" /></a>
<a href="https://github.com/scc0"><img src="https://avatars.githubusercontent.com/u/45963033?v=4" width="60px" alt="scc0" /></a>
<a href="https://github.com/xiaminxi"><img src="https://avatars.githubusercontent.com/u/37994820?v=4" width="60px" alt="xiaminxi" /></a>
<a href="https://github.com/wangpeng00544"><img src="https://avatars.githubusercontent.com/u/54630102?v=4" width="60px" alt="wangpeng00544" /></a>
<a href="https://github.com/ljgx"><img src="https://avatars.githubusercontent.com/u/59424192?v=4" width="60px" alt="ljgx" /></a>
<a href="https://github.com/3026546679"><img src="https://avatars.githubusercontent.com/u/36257162?v=4" width="60px" alt="3026546679" /></a>
<a href="https://github.com/shuilong001"><img src="https://avatars.githubusercontent.com/u/219820297?v=4" width="60px" alt="shuilong001" /></a>
</p>
<h2 align="center">
💪 贡献者 💪
</h2>
<p align="center">
我们的贡献者使这个项目变得更好。谢谢你! 🙏
</p>
<p align="center">
<a href="https://github.com/cwandev"><img src="https://avatars.githubusercontent.com/u/22477554?v=4" width="60px" alt="cwandev" /></a>
<a href="https://github.com/ljt990218"><img src="https://avatars.githubusercontent.com/u/50509815?v=4" width="60px" alt="ljt990218" /></a>
<a href="https://github.com/wswmsword"><img src="https://avatars.githubusercontent.com/u/26893092?v=4" width="60px" alt="wswmsword" /></a>
<a href="https://github.com/weiq"><img src="https://avatars.githubusercontent.com/u/1697158?v=4" width="60px" alt="weiq" /></a>
<a href="https://github.com/SublimeCT"><img src="https://avatars.githubusercontent.com/u/20380890?v=4" width="60px" alt="SublimeCT" /></a>
<a href="https://github.com/ReginYuan"><img src="https://avatars.githubusercontent.com/u/49477488?v=4" width="60px" alt="ReginYuan" /></a>
<a href="https://github.com/smartsf"><img src="https://avatars.githubusercontent.com/u/19995400?v=4" width="60px" alt="smartsf" /></a>
<a href="https://github.com/Kysen777"><img src="https://avatars.githubusercontent.com/u/63892082?v=4" width="60px" alt="Kysen777" /></a>
<a href="https://github.com/Leezon"><img src="https://avatars.githubusercontent.com/u/38120280?v=4" width="60px" alt="Leezon" /></a>
<a href="https://github.com/AlphaYoung111"><img src="https://avatars.githubusercontent.com/u/54132313?v=4" width="60px" alt="AlphaYoung111" /></a>
<a href="https://github.com/leo4developer"><img src="https://avatars.githubusercontent.com/u/15160478?v=4" width="60px" alt="leo4developer" /></a>
<a href="https://github.com/InsHomePgup"><img src="https://avatars.githubusercontent.com/u/47906083?v=4" width="60px" alt="InsHomePgup" /></a>
<a href="https://github.com/wowping"><img src="https://avatars.githubusercontent.com/u/137802961?v=4" width="60px" alt="wowping" /></a>
<a href="https://github.com/ChunyuPCY"><img src="https://avatars.githubusercontent.com/u/21986942?v=4" width="60px" alt="ChunyuPCY" /></a>
<a href="https://github.com/qiyue2015"><img src="https://avatars.githubusercontent.com/u/11554433?v=4" width="60px" alt="qiyue2015" /></a>
<a href="https://github.com/pyQianYi"><img src="https://avatars.githubusercontent.com/u/57526688?v=4" width="60px" alt="pyQianYi" /></a>
<a href="https://github.com/xyy94813"><img src="https://avatars.githubusercontent.com/u/17971352?v=4" width="60px" alt="xyy94813" /></a>
<a href="https://github.com/faukwaa"><img src="https://avatars.githubusercontent.com/u/133618995?v=4" width="60px" alt="faukwaa" /></a>
<a href="https://github.com/chensongni"><img src="https://avatars.githubusercontent.com/u/18071921?v=4" width="60px" alt="chensongni" /></a>
<a href="https://github.com/csheng-github"><img src="https://avatars.githubusercontent.com/u/88492404?v=4" width="60px" alt="csheng-github" /></a>
<a href="https://github.com/LostElkByte"><img src="https://avatars.githubusercontent.com/u/24487727?v=4" width="60px" alt="LostElkByte" /></a>
<a href="https://github.com/xuxichen"><img src="https://avatars.githubusercontent.com/u/18108140?v=4" width="60px" alt="xuxichen" /></a>
<a href="https://github.com/1411430556"><img src="https://avatars.githubusercontent.com/u/67215517?v=4" width="60px" alt="1411430556" /></a>
</p>
## License
[MIT](./LICENSE) License
<p align="right">
<a href="#top">⬆️ 回到顶部</a>
</p>

124
build/vite/index.ts Normal file
View File

@ -0,0 +1,124 @@
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import process from 'node:process'
import { unheadVueComposablesImports } from '@unhead/vue'
import legacy from '@vitejs/plugin-legacy'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { VantResolver } from '@vant/auto-import-resolver'
import Components from 'unplugin-vue-components/vite'
import { VueRouterAutoImports } from 'unplugin-vue-router'
import VueRouter from 'unplugin-vue-router/vite'
import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'
import { VitePWA } from 'vite-plugin-pwa'
import Sitemap from 'vite-plugin-sitemap'
import VueDevTools from 'vite-plugin-vue-devtools'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import { loadEnv } from 'vite'
import { createViteVConsole } from './vconsole'
export function createVitePlugins(mode: string) {
const env = loadEnv(mode, process.cwd())
return [
// https://github.com/posva/unplugin-vue-router
VueRouter({
extensions: ['.vue'],
routesFolder: 'src/pages',
dts: 'src/types/typed-router.d.ts',
}),
vue(),
// https://github.com/jbaubree/vite-plugin-sitemap
Sitemap({
outDir: env.VITE_APP_OUT_DIR || 'dist',
}),
// https://github.com/pengzhanbo/vite-plugin-mock-dev-server
mockDevServerPlugin(),
// https://github.com/antfu/unplugin-vue-components
Components({
extensions: ['vue'],
resolvers: [VantResolver()],
include: [/\.vue$/, /\.vue\?vue/],
dts: 'src/types/components.d.ts',
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
include: [
/\.[tj]sx?$/,
/\.vue$/,
/\.vue\?vue/,
],
imports: [
'vue',
'@vueuse/core',
VueRouterAutoImports,
{
'vue-router/auto': ['useLink'],
'@/utils/i18n': ['i18n', 'locale'],
'vue-i18n': ['useI18n'],
},
unheadVueComposablesImports,
],
dts: 'src/types/auto-imports.d.ts',
dirs: [
'src/composables',
],
resolvers: [VantResolver()],
}),
// https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n
VueI18nPlugin({
// locale messages resource pre-compile option
include: resolve(dirname(fileURLToPath(import.meta.url)), '../../src/locales/**'),
}),
legacy({
targets: ['defaults', 'not IE 11'],
}),
// https://github.com/antfu/unocss
// see uno.config.ts for config
UnoCSS(),
// https://github.com/vadxq/vite-plugin-vconsole
createViteVConsole(mode),
// https://github.com/vuejs/devtools-next
VueDevTools(),
// https://github.com/antfu/vite-plugin-pwa
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.svg', 'safari-pinned-tab.svg'],
manifest: {
name: 'vue3-vant-mobile',
short_name: 'vue3-vant-mobile',
theme_color: '#ffffff',
icons: [
{
src: '/pwa-192x192.png',
sizes: '192x192',
type: 'image/png',
},
{
src: '/pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
},
{
src: '/pwa-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any maskable',
},
],
},
}),
]
}

36
build/vite/optimize.ts Normal file
View File

@ -0,0 +1,36 @@
const include = [
'axios',
'echarts',
'lodash-es',
'resize-detector',
'vant/es',
'vant/es/cell-group/style/index',
'vant/es/popup/style/index',
'vant/es/picker/style/index',
'vant/es/cell/style/index',
'vant/es/switch/style/index',
'vant/es/space/style/index',
'vant/es/button/style/index',
'vant/es/empty/style/index',
'vant/es/icon/style/index',
'vant/es/stepper/style/index',
'vant/es/image/style/index',
'vant/es/form/style/index',
'vant/es/field/style/index',
'vant/es/notify/style/index',
'vant/es/config-provider/style/index',
'vant/es/nav-bar/style/index',
'vant/es/tabbar/style/index',
'vant/es/tabbar-item/style/index',
'vant/es/list/style/index',
'vant/es/text-ellipsis/style/index',
'unplugin-vue-router/runtime',
'unplugin-vue-router/data-loaders',
'unplugin-vue-router/data-loaders/basic',
]
const exclude = [
'@iconify-json/carbon',
]
export { include, exclude }

47
build/vite/vconsole.ts Normal file
View File

@ -0,0 +1,47 @@
import path from 'node:path'
import process from 'node:process'
import { loadEnv } from 'vite'
import { viteVConsole } from 'vite-plugin-vconsole'
export function createViteVConsole(mode: string) {
const env = loadEnv(mode, process.cwd())
return viteVConsole({
entry: [path.resolve('src/main.ts')],
enabled: env.VITE_APP_VCONSOLE === 'true',
config: {
maxLogNumber: 1000,
theme: 'light',
},
// https://github.com/vadxq/vite-plugin-vconsole/issues/21
dynamicConfig: {
theme: `document.documentElement.classList.contains('dark') ? 'dark' : 'light'`,
},
eventListener: `
const targetElement = document.querySelector('html'); // 择要监听的元素
const observerOptions = {
attributes: true, // 监听属性变化
attributeFilter: ['class'] // 只监听class属性变化
};
// 定义回调函数来处理观察到的变化
function handleAttributeChange(mutationsList) {
for(let mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
if (window && window.vConsole) {
window.vConsole.dynamicChange.value = new Date().getTime();
}
}
}
}
// 创建观察者实例并传入回调函数
const observer = new MutationObserver(handleAttributeChange);
// 开始观察目标元素
observer.observe(targetElement, observerOptions);
// 当不再需要观察时,停止观察
// observer.disconnect();
`,
})
}

32
commitlint.config.ts Normal file
View File

@ -0,0 +1,32 @@
import type { UserConfig } from '@commitlint/types'
import { RuleConfigSeverity } from '@commitlint/types'
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
formatter: '@commitlint/format',
rules: {
'type-enum': [
RuleConfigSeverity.Error,
'always',
[
'feat',
'fix',
'perf',
'style',
'docs',
'test',
'refactor',
'build',
'ci',
'chore',
'revert',
'wip',
'workflow',
'types',
'release',
],
],
},
}
export default Configuration

25
eslint.config.ts Normal file
View File

@ -0,0 +1,25 @@
import antfu from '@antfu/eslint-config'
export default antfu(
{
vue: true,
typescript: true,
// Enable UnoCSS support
// https://unocss.dev/integrations/vscode
unocss: true,
formatters: true,
},
{
rules: {
'perfectionist/sort-imports': 'off',
'perfectionist/sort-exports': 'off',
'perfectionist/sort-named-exports': 'off',
},
},
{
ignores: [
'.github/**',
],
},
)

23
index.html Normal file
View File

@ -0,0 +1,23 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#f6d2d2" />
<meta name="msapplication-TileColor" content="#f6d2d2" />
<script>
;(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
</body>
</html>

20
mock/data.ts Normal file
View File

@ -0,0 +1,20 @@
import { defineMockData } from 'vite-plugin-mock-dev-server'
// defineMockData用于在 mock 文件中使用 data.ts 作为共享数据源。
export default defineMockData('proses', [
'🔖 躲在某一时间,想念一段时光的掌纹;躲在某一地点,想念一个站在来路也站在去路的,让我牵挂的人。',
'🔖 天空一碧如洗,灿烂的阳光正从密密的松针的缝隙间射下来,形成一束束粗粗细细的光柱,把飘荡着轻纱般薄雾的林荫照得通亮。',
'🔖 这一次相遇,美得彻骨,美得震颤,美得孤绝,美得惊艳。',
'🔖 沉默的状态,能让我感觉到呼吸的自由和自己原来就处于的本色位置。',
'🔖 青春,是一包象征着阳光的向日葵种子,在现在洒下,就会在未来得到收获,那一株株饱含青春的花朵。',
'🔖 燕子去了,有再来的时候;杨柳枯了,有再青的时候;桃花谢了,有再开的时候。但是,聪明的,你告诉我,我们的日子为什么一去不复返呢?',
'🔖 毕业了,青春在无形之中离去,我们即将翻开人生的另一页。',
'🔖 成长,是每个孩子的权力,也是他们必经的征程,或平坦、或崎岖,有悲欢,有离合。',
'🔖 旧时光里的人和事,琐碎而零乱。我的记忆很模糊,好像大部分都成了一种温馨的符号,静静的沉在我心底。',
'🔖 生活是一部大百科全书,包罗万象;生活是一把六弦琴,弹奏出多重美妙的旋律:生活是一座飞马牌大钟,上紧发条,便会使人获得浓缩的生命。',
'🔖 毕业了,身边的朋友一个个各奔东西,开始学会自己撑起生命的暖色。',
'🔖 已经走到尽头的东西,重生也不过是再一次的消亡。就像所有的开始,其实都只是一个写好了的结局。',
'🔖 下午茶的芬香熏陶着房内的任何一个角落,午后的阳光透过窗帘的间隙洒在木制的桌面上,一份思念随着红茶顺滑至心中。',
'🔖 这里再不是我们的校园,当我们就此离开我们的青葱岁月。',
'🔖 很久找你,一直没有找到,微风吹过的时候,我深深的呼吸,才感觉到你也在陪伴着我呼吸。',
])

7
mock/index.ts Normal file
View File

@ -0,0 +1,7 @@
import prose from './modules/prose.mock'
import user from './modules/user.mock'
export default {
...prose,
...user,
}

View File

@ -0,0 +1,13 @@
import { defineMock } from 'vite-plugin-mock-dev-server'
import { builder } from '../util'
import proses from '../data'
export default defineMock({
url: '/api/prose',
delay: 100,
body: () => {
const rand = Math.floor(Math.random() * proses.value.length)
const prose = proses.value[rand]
return builder(prose)
},
})

67
mock/modules/user.mock.ts Normal file
View File

@ -0,0 +1,67 @@
import { defineMock } from 'vite-plugin-mock-dev-server'
import { builder } from '../util'
export default defineMock([
{
url: '/api/auth/login',
delay: 500,
body: () => {
return {
code: 0,
data: {
token: 'admin',
},
msg: 'success',
}
},
},
{
url: '/api/user/me',
delay: 100,
body: () => {
return {
code: 0,
data: {
uid: 1,
name: 'admin',
avatar: 'https://iconfont.alicdn.com/p/user/eZQFvSX6g8f1/f0d9fd95-a5f0-474d-98b0-d51e8450f2cf.png',
},
msg: 'success',
}
},
},
{
url: '/api/user/logout',
delay: 500,
body: () => {
return {
code: 0,
msg: 'success',
}
},
},
{
url: '/api/user/email-code',
delay: 1000,
body: () => {
const code = '123456'
return builder(code)
},
},
{
url: '/api/user/reset-password',
delay: 1000,
body: () => {
const res = true
return builder(res)
},
},
{
url: '/api/user/register',
delay: 1000,
body: () => {
const res = true
return builder(res)
},
},
])

19
mock/util.ts Normal file
View File

@ -0,0 +1,19 @@
const responseBody = {
message: '',
timestamp: 0,
result: null as unknown,
code: 0,
}
export function builder(data: unknown, message = 'success', code = 0) {
responseBody.result = data
if (message !== undefined && message !== null)
responseBody.message = message
if (code !== undefined && code !== 0)
responseBody.code = code
responseBody.timestamp = new Date().getTime()
return responseBody
}

12
netlify.toml Normal file
View File

@ -0,0 +1,12 @@
[build]
base = "/"
publish = "dist"
command = "pnpm run build:pro"
[build.environment]
NODE_VERSION = "20"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

109
package.json Normal file
View File

@ -0,0 +1,109 @@
{
"name": "vue3-vant-mobile",
"type": "module",
"version": "3.14.0",
"packageManager": "pnpm@10.25.0",
"description": "An mobile web apps template based on the Vue 3 ecosystem",
"license": "MIT",
"engines": {
"node": ">=20.19.0"
},
"scripts": {
"dev": "cross-env MOCK_SERVER_PORT=8086 vite",
"build:dev": "vue-tsc --noEmit && vite build --mode development",
"build:pro": "vue-tsc --noEmit && vite build --mode production",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"release": "bumpp --commit --push --tag",
"typecheck": "vue-tsc --noEmit",
"commitlint": "commitlint --edit",
"prepare": "simple-git-hooks"
},
"dependencies": {
"@unhead/vue": "2.0.19",
"@vant/touch-emulator": "^1.4.0",
"@vant/use": "^1.6.0",
"@vueuse/core": "^14.1.0",
"axios": "^1.13.2",
"echarts": "^6.0.0",
"lodash-es": "^4.17.21",
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"resize-detector": "^0.3.0",
"vant": "^4.9.21",
"vconsole": "^3.15.1",
"vue": "^3.5.25",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@antfu/eslint-config": "6.6.1",
"@commitlint/cli": "^20.2.0",
"@commitlint/config-conventional": "^20.2.0",
"@commitlint/types": "^20.2.0",
"@iconify-json/carbon": "^1.2.15",
"@intlify/unplugin-vue-i18n": "^11.0.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.2",
"@types/nprogress": "^0.2.3",
"@unocss/eslint-config": "66.5.10",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-legacy": "^7.2.1",
"@vitejs/plugin-vue": "^6.0.2",
"autoprefixer": "^10.4.22",
"bumpp": "^10.3.2",
"consola": "^3.4.2",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-plugin-format": "^1.1.0",
"less": "^4.4.2",
"lint-staged": "^16.2.7",
"mockjs": "^1.1.0",
"postcss-mobile-forever": "^5.0.0",
"rollup": "^4.53.3",
"simple-git-hooks": "^2.13.1",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"unocss": "66.5.10",
"unplugin-auto-import": "^20.3.0",
"unplugin-vue-components": "^30.0.0",
"unplugin-vue-router": "^0.19.0",
"vite": "^7.2.7",
"vite-plugin-mock-dev-server": "^2.0.6",
"vite-plugin-pwa": "^1.2.0",
"vite-plugin-sitemap": "^0.8.2",
"vite-plugin-vconsole": "^2.1.1",
"vite-plugin-vue-devtools": "^8.0.5",
"vue-tsc": "^3.1.8"
},
"pnpm": {
"allowedDeprecatedVersions": {
"glob": "7.2.3",
"inflight": "1.0.6",
"sourcemap-codec": "1.4.8",
"source-map": "0.8.0-beta.0",
"keygrip": "1.1.0"
},
"peerDependencyRules": {
"allowedVersions": {}
},
"onlyBuiltDependencies": [
"core-js",
"esbuild",
"simple-git-hooks",
"unrs-resolver"
]
},
"resolutions": {
"vite": "^7.2.7"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
"commit-msg": "pnpm commitlint $1"
},
"lint-staged": {
"*": "eslint --fix"
}
}

9842
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

27
postcss.config.ts Normal file
View File

@ -0,0 +1,27 @@
// 此文件不支持热更新,修改后需要重启生效
// 需要转换的 fixed 选择器列表
const rootContainingBlockSelectorList = [
'.van-tabbar',
'.van-popup',
'.van-popup--bottom',
'.van-popup--top',
'.van-popup--left',
'.van-popup--right',
// 在这里添加你的选择器
]
export default {
plugins: {
'autoprefixer': {},
// https://github.com/wswmsword/postcss-mobile-forever
'postcss-mobile-forever': {
appSelector: '#app',
viewportWidth: 375,
maxDisplayWidth: 600,
border: true,
rootContainingBlockSelectorList,
},
},
}

4
public/favicon-dark.svg Normal file
View File

@ -0,0 +1,4 @@
<svg t="1709866807903" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4913" width="32" height="32">
<path d="M512 598.528a111.232 111.232 0 0 0-111.232 111.2064V870.4h222.464v-160.6656c0-61.44-49.792-111.232-111.232-111.232z m0 58.0608c-30.72 0-55.6032 24.9088-55.6032 55.6288v105.0368h111.2064v-105.0368c0-30.72-24.8832-55.6288-55.6032-55.6288z" fill="#FB4D31" p-id="4914"></path>
<path d="M542.08 270.208l45.2608-78.5408a24.5248 24.5248 0 0 0 0-25.6A26.4448 26.4448 0 0 0 564.1472 153.6a26.3424 26.3424 0 0 0-22.5792 13.44L512 217.6256l-29.5424-50.5344c-4.5824-8.192-13.184-13.312-22.5536-13.4912a26.4448 26.4448 0 0 0-23.2448 12.4928 24.5248 24.5248 0 0 0 0 25.6l45.2352 78.5152L156.928 832.768a24.4736 24.4736 0 0 0 0.3328 25.088c4.8384 7.8336 13.3888 12.544 22.5792 12.5184h664.3456c9.3184 0 17.92-4.7616 22.6304-12.4672a24.4736 24.4736 0 0 0 0.3072-25.088l-325.0176-562.688v0.0512zM619.52 816.64h-33.3568c-12.3648 0-9.6768 0.1024-65.0496 0H512c-55.3728 0.1024-24.704 0-61.7984 0H225.28L512 322.1248 798.72 816.64h-179.2z" fill="#ffffff" p-id="4915"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

4
public/favicon.svg Normal file
View File

@ -0,0 +1,4 @@
<svg t="1709866807903" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4913" width="32" height="32">
<path d="M512 598.528a111.232 111.232 0 0 0-111.232 111.2064V870.4h222.464v-160.6656c0-61.44-49.792-111.232-111.232-111.232z m0 58.0608c-30.72 0-55.6032 24.9088-55.6032 55.6288v105.0368h111.2064v-105.0368c0-30.72-24.8832-55.6288-55.6032-55.6288z" fill="#FB4D31" p-id="4914"></path>
<path d="M542.08 270.208l45.2608-78.5408a24.5248 24.5248 0 0 0 0-25.6A26.4448 26.4448 0 0 0 564.1472 153.6a26.3424 26.3424 0 0 0-22.5792 13.44L512 217.6256l-29.5424-50.5344c-4.5824-8.192-13.184-13.312-22.5536-13.4912a26.4448 26.4448 0 0 0-23.2448 12.4928 24.5248 24.5248 0 0 0 0 25.6l45.2352 78.5152L156.928 832.768a24.4736 24.4736 0 0 0 0.3328 25.088c4.8384 7.8336 13.3888 12.544 22.5792 12.5184h664.3456c9.3184 0 17.92-4.7616 22.6304-12.4672a24.4736 24.4736 0 0 0 0.3072-25.088l-325.0176-562.688v0.0512zM619.52 816.64h-33.3568c-12.3648 0-9.6768 0.1024-65.0496 0H512c-55.3728 0.1024-24.704 0-61.7984 0H225.28L512 322.1248 798.72 816.64h-179.2z" fill="#000000" p-id="4915"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/pwa-192x192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
public/pwa-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,4 @@
<svg width="683" height="683" viewBox="0 0 683 683" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M297.6 104.8C291.6 107.867 288.667 113.733 289.2 122.133C289.2 123.6 296.533 137.2 305.333 152.4L321.333 180L296.533 222.933C282.8 246.667 266 275.867 258.933 288C251.867 300.133 244.267 313.333 242 317.333C239.733 321.333 219.6 356.133 197.333 394.667C175.067 433.2 156 466.133 155.067 468C154 469.867 151.733 473.733 150 476.667C148.267 479.6 145.733 484.133 144.267 486.667C141.733 491.333 117.2 533.867 108.4 548.667C100 562.8 101.333 572.4 112.4 578.267C116.267 580.267 135.2 580.4 341.333 580.4C588.667 580.4 570.533 581.067 577.2 572.533C582.133 566.267 580.933 560.933 570.533 542.667C565.333 533.467 559.867 523.867 558.4 521.333C554.933 515.333 526.933 466.8 496.4 414C483.2 391.2 470.933 370 469.067 366.667C467.2 363.333 458.933 348.933 450.667 334.667C437.867 312.667 422.667 286.267 413.333 270C412 267.733 400.933 248.533 388.667 227.333C376.4 206 365.2 186.667 363.867 184.267L361.333 180L375.867 154.933C394.533 122.667 393.6 124.4 393.6 118.533C393.467 109.333 385.867 102.667 375.467 102.667C367.733 102.667 363.733 106.667 352.533 125.867C346.8 135.867 341.6 144 341.2 144C340.667 144 339.467 142.4 338.533 140.267C332.4 128.267 319.067 107.467 316.4 105.733C311.733 102.667 302.8 102.133 297.6 104.8ZM349.333 228.267C353.333 235.067 357.867 243.067 359.467 246C361.067 248.933 368.533 261.867 376 274.667C383.467 287.467 390.933 300.4 392.667 303.333C394.267 306.267 400 316.133 405.333 325.333C410.667 334.533 417.467 346.133 420.4 351.333C423.333 356.4 437.2 380.4 451.333 404.667C465.333 428.8 478.4 451.333 480.267 454.667C482.133 458 490.133 471.733 498 485.333C506 498.933 513.467 511.733 514.667 514C522.933 528.267 531.067 542.133 532 543.333C532.8 544.267 513.333 544.667 474.267 544.533L415.467 544.4L415.333 502.533C415.2 466.133 414.8 459.733 412.8 453.333C406.667 434.933 399.733 424.667 385.867 414.4C377.467 408.133 367.333 402.8 363.067 402.533C362.8 402.4 359.733 401.733 356.267 400.667C343.6 397.333 320.133 400.4 308 406.933C287.6 417.867 272.933 437.333 268.533 458.933C268 461.333 267.6 481.6 267.467 503.867V544.495L415.467 544.4L208.4 544.533C169.333 544.667 149.867 544.267 150.667 543.333C151.6 542.133 160.933 526.4 168.667 512.667C170.4 509.733 185.733 483.333 202.667 454C219.733 424.667 234.8 398.533 236.267 396C246.133 378.8 265.733 345.067 272.8 332.933C277.467 325.067 281.333 318.4 281.333 318.133C281.333 317.733 284.267 312.8 287.867 306.933C291.467 301.067 294.533 295.733 294.8 295.067C296.133 291.6 340.667 216 341.333 216C341.733 216 345.333 221.6 349.333 228.267Z" fill="black"/>
<path d="M415.333 502.533L415.467 544.4L267.467 544.495V503.867C267.6 481.6 268 461.333 268.533 458.933C272.933 437.333 287.6 417.867 308 406.933C320.133 400.4 343.6 397.333 356.267 400.667C359.733 401.733 362.8 402.4 363.067 402.533C367.333 402.8 377.467 408.133 385.867 414.4C399.733 424.667 406.667 434.933 412.8 453.333C414.8 459.733 415.2 466.133 415.333 502.533Z" fill="#FB4D31"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

59
src/App.vue Normal file
View File

@ -0,0 +1,59 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouteCacheStore } from '@/stores'
const { t } = useI18n()
useHead({
title: () => t('app.name'),
meta: [
{
name: 'description',
content: () => t('app.description'),
},
{
name: 'theme-color',
content: () => isDark.value ? '#0B0A0A' : '#ffffff',
},
],
link: [
{
rel: 'icon',
type: 'image/svg+xml',
href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg',
},
],
})
const routeCacheStore = useRouteCacheStore()
const keepAliveRouteNames = computed(() => {
return routeCacheStore.routeCaches
})
const mode = computed(() => {
return isDark.value ? 'dark' : 'light'
})
</script>
<template>
<van-config-provider :theme="mode">
<nav-bar />
<router-view v-slot="{ Component }">
<section class="app-wrapper">
<keep-alive :include="keepAliveRouteNames">
<component :is="Component" />
</keep-alive>
</section>
</router-view>
<tab-bar />
</van-config-provider>
</template>
<style scoped>
.app-wrapper {
width: 100%;
position: relative;
padding: 16px;
}
</style>

5
src/api/index.ts Normal file
View File

@ -0,0 +1,5 @@
import request from '@/utils/request'
export async function queryProse(): Promise<any> {
return request('/prose')
}

40
src/api/user.ts Normal file
View File

@ -0,0 +1,40 @@
import request from '@/utils/request'
export interface LoginData {
email: string
password: string
}
export interface LoginRes {
token: string
}
export interface UserState {
uid?: number
name?: string
avatar?: string
}
export function login(data: LoginData): Promise<any> {
return request.post<LoginRes>('/auth/login', data)
}
export function logout() {
return request.post('/user/logout')
}
export function getUserInfo() {
return request<UserState>('/user/me')
}
export function getEmailCode(): Promise<any> {
return request.get('/user/email-code')
}
export function resetPassword(): Promise<any> {
return request.post('/user/reset-password')
}
export function register(): Promise<any> {
return request.post('/user/register')
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1713850165468" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6560" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M21.333 512a490.667 490.667 0 1 0 981.334 0 490.667 490.667 0 1 0-981.334 0z" fill="#E1E0FC" p-id="6561" data-spm-anchor-id="a313x.search_index.0.i0.216a3a81XzpQWQ"></path><path d="M155.733 849.067c8.534-27.734 19.2-46.934 32-57.6C204.8 774.4 403.2 710.4 428.8 682.667c25.6-27.734 19.2-59.734 14.933-74.667-4.266-14.933-49.066-59.733-59.733-106.667-27.733-27.733-34.133-78.933-21.333-87.466-12.8-76.8-6.4-108.8 8.533-132.267s27.733-83.2 87.467-98.133 87.466 12.8 125.866 10.666C620.8 192 620.8 183.467 640 192c19.2 8.533 2.133 53.333 19.2 93.867 17.067 40.533 10.667 61.866 6.4 123.733 21.333 34.133-6.4 78.933-21.333 91.733C633.6 554.667 590.933 595.2 586.667 608c-2.134 12.8-10.667 72.533 57.6 104.533 68.266 32 170.666 61.867 198.4 78.934 17.066 12.8 27.733 32 27.733 57.6C772.267 953.6 652.8 1004.8 512 1002.667c-140.8-2.134-260.267-53.334-356.267-153.6z" fill="#F0EEFF" p-id="6562"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.287 18.4129C51.2328 18.9517 50.7175 19.9824 50.8111 21.4582C50.8111 21.716 52.0995 24.1054 53.6456 26.776L56.4568 31.6252L52.0995 39.1683C49.6867 43.3383 46.735 48.4686 45.4934 50.6003C44.2519 52.732 42.9166 55.0512 42.5183 55.754C42.12 56.4567 38.5827 62.5709 34.6705 69.3412C30.7585 76.1113 27.4085 81.8974 27.2446 82.2255C27.0571 82.5535 26.6588 83.2327 26.3543 83.7482C26.0498 84.2635 25.6046 85.06 25.3471 85.5052C24.9018 86.325 20.5915 93.798 19.0454 96.3983C17.5695 98.8814 17.8037 100.568 19.7482 101.599C20.4276 101.95 23.754 101.974 59.9707 101.974C103.426 101.974 100.24 102.091 101.411 100.591C102.278 99.4905 102.067 98.5534 100.24 95.3441C99.3264 93.7277 98.3661 92.041 98.1083 91.5958C97.4992 90.5417 92.5797 82.0146 87.2152 72.7379C84.896 68.7321 82.7408 65.0073 82.4129 64.4217C82.0849 63.836 80.6324 61.3059 79.1801 58.7995C76.9312 54.9342 74.2607 50.2958 72.6207 47.4378C72.3865 47.0395 70.4421 43.6661 68.287 39.9414C66.1318 36.1933 64.164 32.7965 63.9298 32.3749L63.4846 31.6252L66.0381 27.221C69.3177 21.552 69.1537 21.8565 69.1537 20.8257C69.1304 19.2093 67.7951 18.0381 65.9678 18.0381C64.609 18.0381 63.9062 18.7409 61.9384 22.1143C60.9312 23.8712 60.0176 25.3001 59.9473 25.3001C59.8536 25.3001 59.6428 25.019 59.4787 24.6443C58.4012 22.5359 56.0586 18.8815 55.59 18.5768C54.7701 18.0381 53.2006 17.9443 52.287 18.4129ZM61.3762 40.1055C62.079 41.3002 62.8756 42.7058 63.1567 43.2211C63.4378 43.7364 64.7496 46.0088 66.0615 48.2577C67.3734 50.5066 68.6851 52.7789 68.9898 53.2942C69.2709 53.8097 70.2782 55.5431 71.2152 57.1595C72.1523 58.7759 73.3471 60.814 73.8624 61.7276C74.3777 62.6179 76.8141 66.8345 79.2972 71.0981C81.7569 75.3382 84.0527 79.2972 84.3807 79.8829C84.7086 80.4685 86.1141 82.8813 87.4963 85.2708C88.9019 87.6603 90.2138 89.9092 90.4247 90.3075C91.877 92.8141 93.3061 95.2503 93.47 95.4611C93.6105 95.6252 90.1903 95.6955 83.3266 95.672L72.9957 95.6486L72.9721 88.2928C72.9488 81.8974 72.8785 80.773 72.5271 79.6485C71.4495 76.4157 70.2313 74.6121 67.7951 72.8082C66.3192 71.7071 64.5387 70.7701 63.7892 70.7232C63.7423 70.6998 63.2035 70.5827 62.5945 70.3954C60.369 69.8096 56.2459 70.3485 54.1142 71.4963C50.53 73.4173 47.9531 76.8374 47.18 80.6324C47.0864 81.0541 47.0161 84.6149 46.9927 88.5271V95.6653L72.9957 95.6486L36.6149 95.672C29.751 95.6955 26.331 95.6252 26.4715 95.4611C26.6354 95.2503 28.2752 92.4861 29.634 90.0733C29.9385 89.5578 32.6324 84.9194 35.6077 79.7657C38.6061 74.6121 41.2533 70.0204 41.511 69.5754C43.2445 66.5534 46.6881 60.6267 47.9297 58.4948C48.7497 57.1128 49.4289 55.9414 49.4289 55.8945C49.4289 55.8242 49.9444 54.9575 50.5769 53.9267C51.2094 52.8961 51.7481 51.9589 51.795 51.8419C52.0292 51.2328 59.8536 37.9502 59.9707 37.9502C60.0409 37.9502 60.6734 38.9341 61.3762 40.1055Z" fill="#CCCCCC"/>
<path d="M72.9721 88.2928L72.9957 95.6486L46.9927 95.6653V88.5271C47.0161 84.6149 47.0864 81.0541 47.18 80.6324C47.9531 76.8374 50.53 73.4173 54.1142 71.4963C56.2459 70.3484 60.369 69.8096 62.5945 70.3954C63.2035 70.5826 63.7423 70.6998 63.7892 70.7232C64.5387 70.7701 66.3192 71.7071 67.7951 72.8082C70.2313 74.612 71.4495 76.4157 72.5271 79.6485C72.8785 80.773 72.9488 81.8974 72.9721 88.2928Z" fill="#FB4D31"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="120" height="120" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.287 18.4129C51.2328 18.9517 50.7175 19.9824 50.8111 21.4582C50.8111 21.716 52.0995 24.1054 53.6456 26.776L56.4568 31.6252L52.0995 39.1683C49.6867 43.3383 46.735 48.4686 45.4934 50.6003C44.2519 52.732 42.9166 55.0512 42.5183 55.754C42.12 56.4567 38.5827 62.5709 34.6705 69.3412C30.7585 76.1113 27.4085 81.8974 27.2446 82.2255C27.0571 82.5535 26.6588 83.2327 26.3543 83.7482C26.0498 84.2635 25.6046 85.06 25.3471 85.5052C24.9018 86.325 20.5915 93.798 19.0454 96.3983C17.5695 98.8814 17.8038 100.568 19.7482 101.599C20.4276 101.95 23.754 101.974 59.9707 101.974C103.426 101.974 100.24 102.091 101.411 100.591C102.278 99.4905 102.067 98.5534 100.24 95.3441C99.3264 93.7277 98.3661 92.041 98.1084 91.5958C97.4992 90.5417 92.5797 82.0146 87.2152 72.7379C84.896 68.7321 82.7408 65.0073 82.4129 64.4217C82.0849 63.836 80.6324 61.3059 79.1801 58.7995C76.9312 54.9342 74.2607 50.2958 72.6207 47.4378C72.3865 47.0395 70.4421 43.6661 68.287 39.9414C66.1318 36.1933 64.164 32.7965 63.9298 32.3749L63.4846 31.6252L66.0381 27.221C69.3177 21.552 69.1537 21.8565 69.1537 20.8257C69.1304 19.2093 67.7951 18.0381 65.9679 18.0381C64.609 18.0381 63.9062 18.7409 61.9385 22.1143C60.9312 23.8712 60.0176 25.3001 59.9473 25.3001C59.8537 25.3001 59.6428 25.019 59.4787 24.6443C58.4012 22.5359 56.0586 18.8815 55.59 18.5768C54.7701 18.0381 53.2006 17.9443 52.287 18.4129ZM61.3762 40.1055C62.079 41.3002 62.8756 42.7058 63.1567 43.2211C63.4378 43.7364 64.7496 46.0088 66.0615 48.2577C67.3734 50.5066 68.6852 52.7789 68.9898 53.2942C69.2709 53.8097 70.2782 55.5431 71.2152 57.1595C72.1523 58.7759 73.3471 60.814 73.8624 61.7276C74.3777 62.6179 76.8141 66.8345 79.2972 71.0981C81.7569 75.3382 84.0527 79.2972 84.3807 79.8829C84.7086 80.4685 86.1141 82.8813 87.4963 85.2708C88.9019 87.6603 90.2138 89.9092 90.4247 90.3075C91.877 92.8141 93.3061 95.2503 93.47 95.4611C93.6105 95.6252 90.1903 95.6955 83.3266 95.672L72.9957 95.6486L72.9721 88.2928C72.9488 81.8974 72.8785 80.773 72.5271 79.6485C71.4495 76.4157 70.2313 74.6121 67.7951 72.8082C66.3192 71.7071 64.5387 70.7701 63.7892 70.7232C63.7423 70.6998 63.2035 70.5827 62.5945 70.3954C60.369 69.8096 56.2459 70.3485 54.1142 71.4963C50.53 73.4173 47.9531 76.8374 47.18 80.6324C47.0864 81.0541 47.0161 84.6149 46.9927 88.5271V95.6653L72.9957 95.6486L36.6149 95.672C29.751 95.6955 26.331 95.6252 26.4715 95.4611C26.6354 95.2503 28.2752 92.4861 29.634 90.0733C29.9385 89.5578 32.6324 84.9194 35.6077 79.7657C38.6061 74.6121 41.2533 70.0204 41.511 69.5754C43.2445 66.5534 46.6881 60.6267 47.9297 58.4948C48.7497 57.1128 49.4289 55.9414 49.4289 55.8945C49.4289 55.8242 49.9444 54.9575 50.5769 53.9267C51.2094 52.8961 51.7481 51.9589 51.795 51.8419C52.0292 51.2328 59.8537 37.9502 59.9707 37.9502C60.0409 37.9502 60.6734 38.9341 61.3762 40.1055Z" fill="black"/>
<path d="M72.9721 88.2928L72.9957 95.6486L46.9927 95.6653V88.5271C47.0161 84.6149 47.0864 81.0541 47.18 80.6324C47.9531 76.8374 50.53 73.4173 54.1142 71.4963C56.2459 70.3484 60.369 69.8096 62.5945 70.3954C63.2035 70.5826 63.7423 70.6998 63.7892 70.7232C64.5387 70.7701 66.3192 71.7071 67.7951 72.8082C70.2313 74.612 71.4495 76.4157 72.5271 79.6485C72.8785 80.773 72.9488 81.8974 72.9721 88.2928Z" fill="#FB4D31"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,179 @@
const contrastColor = 'rgba(255, 255, 255, 0.65)'
const backgroundColor = 'transparent'
function axisCommon() {
return {
axisLine: {
lineStyle: {
color: contrastColor,
},
},
splitLine: {
lineStyle: {
color: '#484753',
},
},
splitArea: {
areaStyle: {
color: ['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)'],
},
},
minorSplitLine: {
lineStyle: {
color: '#20203B',
},
},
}
}
const colorPalette = [
'#4992ff',
'#7cffb2',
'#fddd60',
'#ff6e76',
'#58d9f9',
'#05c091',
'#ff8a45',
'#8d48e3',
'#dd79ff',
]
const theme: any = {
color: colorPalette,
backgroundColor,
axisPointer: {
lineStyle: {
color: '#817f91',
},
crossStyle: {
color: '#817f91',
},
label: {
// TODO Contrast of label backgorundColor
color: '#fff',
},
},
legend: {
textStyle: {
color: contrastColor,
},
},
textStyle: {
color: contrastColor,
},
title: {
textStyle: {
color: 'red',
},
subtextStyle: {
color: 'rgba(255, 255, 255, 0.65)',
},
},
toolbox: {
iconStyle: {
borderColor: contrastColor,
},
},
dataZoom: {
borderColor: '#71708A',
textStyle: {
color: contrastColor,
},
brushStyle: {
color: 'rgba(135,163,206,0.3)',
},
handleStyle: {
color: '#353450',
borderColor: '#C5CBE3',
},
moveHandleStyle: {
color: '#B0B6C3',
opacity: 0.3,
},
fillerColor: 'rgba(135,163,206,0.2)',
emphasis: {
handleStyle: {
borderColor: '#91B7F2',
color: '#4D587D',
},
moveHandleStyle: {
color: '#636D9A',
opacity: 0.7,
},
},
dataBackground: {
lineStyle: {
color: '#71708A',
width: 1,
},
areaStyle: {
color: '#71708A',
},
},
selectedDataBackground: {
lineStyle: {
color: '#87A3CE',
},
areaStyle: {
color: '#87A3CE',
},
},
},
visualMap: {
textStyle: {
color: contrastColor,
},
},
timeline: {
lineStyle: {
color: contrastColor,
},
label: {
color: contrastColor,
},
controlStyle: {
color: contrastColor,
borderColor: contrastColor,
},
},
calendar: {
itemStyle: {
color: backgroundColor,
},
dayLabel: {
color: contrastColor,
},
monthLabel: {
color: contrastColor,
},
yearLabel: {
color: contrastColor,
},
},
timeAxis: axisCommon(),
logAxis: axisCommon(),
valueAxis: axisCommon(),
categoryAxis: axisCommon(),
line: {
symbol: 'circle',
},
graph: {
color: colorPalette,
},
gauge: {
title: {
color: contrastColor,
},
},
candlestick: {
itemStyle: {
color: '#FD1050',
color0: '#0CF49B',
borderColor: '#FD1050',
borderColor0: '#0CF49B',
},
},
}
theme.categoryAxis.splitLine.show = false
export default theme

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue'
import type { ECharts } from 'echarts'
import * as echarts from 'echarts'
import { debounce } from 'lodash-es'
import { addListener, removeListener } from 'resize-detector'
import dark from './dark'
const props = defineProps({
option: {
type: Object,
default: () => {
return {}
},
},
})
echarts.registerTheme('dark-chart', dark)
const chartDom = ref<HTMLDivElement>()
let chart: ECharts | null = null
const isRealDark = ref(isDark.value)
function resizeChart() {
chart?.resize()
}
const resize = debounce(resizeChart, 300)
function disposeChart() {
if (chartDom.value)
removeListener(chartDom.value, resize)
chart?.dispose()
chart = null
}
function initChart() {
disposeChart()
if (chartDom.value) {
// init echarts
chart = echarts.init(chartDom.value, isRealDark.value ? 'dark-chart' : undefined)
chart.setOption(props.option)
addListener(chartDom.value, resize)
}
}
watch(isRealDark, (newValue) => {
if (chart) {
chart.setTheme(newValue ? 'dark-chart' : undefined)
}
else {
initChart()
}
}, {
flush: 'post',
})
onMounted(() => {
watch(() => props.option, () => {
chart?.setOption(props.option)
}, {
deep: true,
flush: 'post',
})
initChart()
})
onUnmounted(() => {
disposeChart()
})
</script>
<template>
<div ref="chartDom" />
</template>

View File

@ -0,0 +1,12 @@
<template>
<van-button size="small" plain type="primary">
<slot />
</van-button>
</template>
<style scoped>
.van-button {
--van-button-border-width: 0;
--van-button-plain-background: opacity;
}
</style>

50
src/components/NavBar.vue Normal file
View File

@ -0,0 +1,50 @@
<script setup lang="ts">
import { rootRouteList } from '@/config/routes'
const route = useRoute()
const router = useRouter()
const { t } = useI18n()
/**
* Get page title
* Located in src/locales/json
*/
const title = computed(() => {
if (route.name) {
return t(`navbar.${route.name}`)
}
return t('navbar.Undefined')
})
/**
* Show the left arrow
* If route name is in rootRouteList, hide left arrow
*/
const showLeftArrow = computed(() => {
if (route.name && rootRouteList.includes(route.name)) {
return false
}
return true
})
function onBack() {
if (window.history.state.back) {
history.back()
}
else {
router.replace('/')
}
}
</script>
<template>
<VanNavBar
:title="title"
:fixed="true"
:left-arrow="showLeftArrow"
placeholder clickable
@click-left="onBack"
/>
</template>

3
src/components/README.md Normal file
View File

@ -0,0 +1,3 @@
# Components
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).

30
src/components/TabBar.vue Normal file
View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { rootRouteList } from '@/config/routes'
const active = ref(0)
const route = useRoute()
const show = computed(() => {
if (route.name && rootRouteList.includes(route.name)) {
return true
}
return false
})
</script>
<template>
<van-tabbar v-if="show" v-model="active" route placeholder>
<van-tabbar-item replace to="/">
{{ $t('tabbar.home') }}
<template #icon>
<div class="i-carbon:home" />
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
{{ $t('tabbar.profile') }}
<template #icon>
<div class="i-carbon:user" />
</template>
</van-tabbar-item>
</van-tabbar>
</template>

4
src/composables/dark.ts Normal file
View File

@ -0,0 +1,4 @@
// these APIs are auto-imported from @vueuse/core
export const isDark = useDark()
export const toggleDark = useToggle(isDark)
export const preferredDark = usePreferredDark()

9
src/config/routes.ts Normal file
View File

@ -0,0 +1,9 @@
/**
* List of root-level route names.
* In the Navbar component, the left arrow is hidden for these routes.
* However, the Tabbar is shown on these routes.
*/
export const rootRouteList: readonly string[] = [
'Home',
'Profile',
]

4
src/constants/index.ts Normal file
View File

@ -0,0 +1,4 @@
import { i18n } from '@/utils/i18n'
export const appName = () => i18n.global.t('app.name')
export const appDescription = () => i18n.global.t('app.description')

133
src/locales/en-US.json Normal file
View File

@ -0,0 +1,133 @@
{
"app": {
"name": "Vue3 Vant Mobile",
"description": "An mobile web apps template based on the Vue 3 ecosystem"
},
"navbar": {
"Home": "Home",
"Profile": "Profile",
"Mock": "🗂️ Mock",
"Charts": "📊 Charts",
"UnoCSS": "⚡ UnoCSS",
"Counter": "🍍 Persistent State",
"KeepAlive": "♻️ Page Cache",
"ScrollCache": "📍 Scroll Cache",
"Login": "🧑‍💻 Login",
"Register": "🧑‍💻 Register",
"ForgotPassword": "❓ Forgot Password",
"Settings": "⚙️ Settings",
"404": "⚠️ Page 404",
"Undefined": "🤷 Undefined title"
},
"tabbar": {
"home": "HOME",
"profile": "PROFILE"
},
"home": {
"darkMode": "🌗 Dark Mode",
"language": "📚 Language",
"settings": "Settings",
"examples": "Examples"
},
"profile": {
"login": "Login",
"settings": "Settings",
"docs": "Docs"
},
"mock": {
"fromAsyncData": "Data from asynchronous requests",
"noData": "No data",
"pull": "Pull",
"reset": "Reset"
},
"charts": {
"January": "Jan",
"February": "Feb",
"March": "Mar",
"April": "Apr",
"May": "May",
"June": "Jun"
},
"counter": {
"description": "This counter's state is persisted via Pinia. Try refreshing the page to see it in action."
},
"unocss": {
"title": "Hello, Unocss!",
"description": "This is a simple example of Unocss in action.",
"button": "Button"
},
"keepAlive": {
"label": "The current component will be cached"
},
"scrollCache": {
"sectionTitle": "Section title",
"sectionText": "Section text text text text text text text text text text",
"finished": "Already at the bottom ~",
"loading": "Loading..."
},
"login": {
"login": "Sign In",
"logout": "Sign Out",
"email": "Email",
"password": "Password",
"pleaseEnterEmail": "Please enter email",
"pleaseEnterPassword": "Please enter password",
"signUp": "Click to sign up",
"forgotPassword": "Forgot password?"
},
"forgotPassword": {
"email": "Email",
"code": "Code",
"password": "Password",
"confirmPassword": "Password again",
"pleaseEnterEmail": "Please enter email",
"pleaseEnterCode": "Please enter code",
"pleaseEnterPassword": "Please enter password",
"pleaseEnterConfirmPassword": "Please enter password again",
"passwordsDoNotMatch": "Passwords do not match",
"confirm": "Confirm",
"backToLogin": "Back to login",
"getCode": "Get code",
"gettingCode": "Getting code",
"sendCodeSuccess": "Sent, the code is",
"passwordResetSuccess": "Password reset succeeded"
},
"register": {
"email": "Email",
"code": "Code",
"nickname": "Nickname",
"password": "Password",
"confirmPassword": "Password again",
"pleaseEnterEmail": "Please enter email",
"pleaseEnterCode": "Please enter code",
"pleaseEnterNickname": "Please enter nickname",
"pleaseEnterPassword": "Please enter password",
"pleaseEnterConfirmPassword": "Please enter password again",
"passwordsDoNotMatch": "Passwords do not match",
"confirm": "Confirm",
"backToLogin": "Back to login",
"getCode": "Get code",
"gettingCode": "Getting code",
"sendCodeSuccess": "Sent, the code is",
"registerSuccess": "Register succeeded"
},
"settings": {
"logout": "Sign Out",
"currentVersion": "Current Version",
"confirmTitle": "Confirm Exit?"
}
}

133
src/locales/zh-CN.json Normal file
View File

@ -0,0 +1,133 @@
{
"app": {
"name": "Vue3 移动端模板",
"description": "一个基于 Vue 3 生态系统的移动 web 应用模板"
},
"navbar": {
"Home": "主页",
"Profile": "我的",
"Mock": "🗂️ Mock",
"Charts": "📊 图表",
"UnoCSS": "⚡ UnoCSS",
"Counter": "🍍 状态持久化",
"KeepAlive": "♻️ 页面缓存",
"ScrollCache": "📍 滚动缓存",
"Login": "🧑‍💻 登录",
"Register": "🧑‍💻 注册",
"ForgotPassword": "❓ 忘记密码",
"Settings": "⚙️ 设置",
"404": "⚠️ 404 页面",
"Undefined": "🤷 未定义标题"
},
"tabbar": {
"home": "首页",
"profile": "我的"
},
"home": {
"darkMode": "🌗 深色模式",
"language": "📚 多语言",
"settings": "设置",
"examples": "示例"
},
"profile": {
"login": "登录",
"settings": "设置",
"docs": "文档"
},
"mock": {
"fromAsyncData": "来自异步请求的数据",
"noData": "暂无数据",
"pull": "请求",
"reset": "清空"
},
"charts": {
"January": "1月",
"February": "2月",
"March": "3月",
"April": "4月",
"May": "5月",
"June": "6月"
},
"counter": {
"description": "该计数器的状态通过 Pinia 持久化。刷新页面试试看!"
},
"unocss": {
"title": "你好, Unocss!",
"description": "这是一个简单的 Unocss 使用示例。",
"button": "按钮"
},
"keepAlive": {
"label": "当前组件将会被缓存"
},
"scrollCache": {
"sectionTitle": "段落标题",
"sectionText": "段落内容段落内容段落内容段落内容段落内容段落内容",
"finished": "已经到底啦 ~",
"loading": "加载中..."
},
"login": {
"login": "登录",
"logout": "退出登录",
"email": "邮箱",
"password": "密码",
"pleaseEnterEmail": "请输入邮箱",
"pleaseEnterPassword": "请输入密码",
"signUp": "还没有账号?点击注册",
"forgotPassword": "忘记密码?"
},
"forgotPassword": {
"email": "邮箱",
"code": "验证码",
"password": "密码",
"confirmPassword": "再次输入密码",
"pleaseEnterEmail": "请输入邮箱",
"pleaseEnterCode": "请输入验证码",
"pleaseEnterPassword": "请输入密码",
"pleaseEnterConfirmPassword": "请再次输入密码",
"passwordsDoNotMatch": "两次输入的密码不一致",
"confirm": "确认",
"backToLogin": "返回登录",
"getCode": "获取验证码",
"gettingCode": "获取中",
"sendCodeSuccess": "已发送,验证码为",
"passwordResetSuccess": "密码重置成功"
},
"register": {
"email": "邮箱",
"code": "验证码",
"nickname": "昵称",
"password": "密码",
"confirmPassword": "再次输入密码",
"pleaseEnterEmail": "请输入邮箱",
"pleaseEnterCode": "请输入验证码",
"pleaseEnterNickname": "请输入昵称",
"pleaseEnterPassword": "请输入密码",
"pleaseEnterConfirmPassword": "请再次输入密码",
"passwordsDoNotMatch": "两次输入的密码不一致",
"confirm": "确认",
"backToLogin": "返回登录",
"getCode": "获取验证码",
"gettingCode": "获取中",
"sendCodeSuccess": "已发送,验证码为",
"registerSuccess": "注册成功"
},
"settings": {
"logout": "退出登录",
"currentVersion": "当前版本",
"confirmTitle": "确认退出?"
}
}

33
src/main.ts Normal file
View File

@ -0,0 +1,33 @@
import { createApp } from 'vue'
import { createHead } from '@unhead/vue/client'
import App from '@/App.vue'
import router from '@/router'
import pinia from '@/stores'
import 'virtual:uno.css'
import '@/styles/app.less'
import '@/styles/var.less'
import { i18n } from '@/utils/i18n'
// Vant 桌面端适配
import '@vant/touch-emulator'
/* --------------------------------
Vant
ToastDialogNotify ImagePreview
使unplugin-vue-components
------------------------------------- */
import 'vant/es/toast/style'
import 'vant/es/dialog/style'
import 'vant/es/notify/style'
import 'vant/es/image-preview/style'
const app = createApp(App)
const head = createHead()
app.use(head)
app.use(router)
app.use(pinia)
app.use(i18n)
app.mount('#app')

5
src/pages/README.md Normal file
View File

@ -0,0 +1,5 @@
# SFC `<route>` custom block
We used SFC <route> [`<route>`](https://uvr.esm.is/guide/extending-routes.html#sfc-route-custom-block) custom block to define the route name and meta information for each page, making it easy to control the transition animations for each route.
我们使用 SFC [`<route>`](https://uvr.esm.is/guide/extending-routes.html#sfc-route-custom-block) 自定义块定义每个页面的路由名称和元信息,可以轻松控制每个路由的过渡动画。

29
src/pages/[...all].vue Normal file
View File

@ -0,0 +1,29 @@
<script setup lang="ts">
const router = useRouter()
function onBack() {
if (window.history.state.back)
history.back()
else
router.replace('/')
}
</script>
<template>
<div text="center gray-300 dark:gray-200">
<van-icon name="warn-o" size="3em" />
<div> Not found </div>
<div class="mt-2">
<button van-haptics-feedback class="btn" @click="onBack">
Back
</button>
</div>
</div>
</template>
<route lang="json5">
{
name: '404'
}
</route>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
const { t } = useI18n()
const barOption = ref({
title: {},
tooltip: {},
xAxis: {
data: [t('charts.January'), t('charts.February'), t('charts.March'), t('charts.April'), t('charts.May'), t('charts.June')],
},
yAxis: {},
series: [
{
name: 'sales',
type: 'bar',
data: [5, 20, 36, 10, 10, 20],
},
],
})
const lineOption = ref({
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
},
yAxis: {
type: 'value',
},
series: [
{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line',
},
],
})
const scoreOption = ref({
tooltip: {
formatter: '{a} <br/>{b} : {c}%',
},
series: [
{
name: 'Pressure',
type: 'gauge',
detail: {
formatter: '{value}',
},
data: [
{
value: 50,
name: 'SCORE',
},
],
},
],
})
</script>
<template>
<Chart :option="barOption" :style="{ height: '330px' }" />
<Chart :option="lineOption" :style="{ height: '330px' }" />
<Chart :option="scoreOption" :style="{ height: '330px' }" />
</template>
<route lang="json5">
{
name: 'Charts'
}
</route>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores'
const counterStore = useCounterStore()
const { counter } = storeToRefs(counterStore)
</script>
<template>
<div class="text-sm space-y-2">
<p> {{ $t('counter.description') }}</p>
<van-stepper v-model="counter" />
</div>
</template>
<route lang="json5">
{
name: 'Counter'
}
</route>

View File

@ -0,0 +1,144 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { FieldRule } from 'vant'
import { showNotify } from 'vant'
import { useUserStore } from '@/stores'
import vw from '@/utils/inline-px-to-vw'
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const postData = reactive({
email: '',
code: '',
password: '',
confirmPassword: '',
})
const validatorPassword = (val: string) => val === postData.password
const rules = reactive({
email: [
{ required: true, message: t('forgotPassword.pleaseEnterEmail') },
],
code: [
{ required: true, message: t('forgotPassword.pleaseEnterCode') },
],
password: [
{ required: true, message: t('forgotPassword.pleaseEnterPassword') },
],
confirmPassword: [
{ required: true, message: t('forgotPassword.pleaseEnterConfirmPassword') },
{ required: true, validator: validatorPassword, message: t('forgotPassword.passwordsDoNotMatch') },
] as FieldRule[],
})
async function reset() {
try {
loading.value = true
const res = await userStore.reset()
if (res.code === 0) {
showNotify({ type: 'success', message: t('forgotPassword.passwordResetSuccess') })
router.push({ name: 'Login' })
}
}
finally {
loading.value = false
}
}
const isGettingCode = ref(false)
const buttonText = computed(() => {
return isGettingCode.value ? t('forgotPassword.gettingCode') : t('forgotPassword.getCode')
})
async function getCode() {
if (!postData.email) {
showNotify({ type: 'warning', message: t('forgotPassword.pleaseEnterEmail') })
return
}
isGettingCode.value = true
const res = await userStore.getCode()
if (res.code === 0)
showNotify({ type: 'success', message: `${t('forgotPassword.sendCodeSuccess')}: ${res.result}` })
isGettingCode.value = false
}
</script>
<template>
<div class="mx-auto p-3 text-center w-full">
<van-form :model="postData" :rules="rules" validate-trigger="onSubmit" @submit="reset">
<div class="rounded-md overflow-hidden">
<van-field
v-model.trim="postData.email"
:rules="rules.email"
name="email"
:placeholder="$t('forgotPassword.email')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.code"
:rules="rules.code"
name="code"
:placeholder="$t('forgotPassword.code')"
>
<template #button>
<van-button size="small" type="primary" plain @click="getCode">
{{ buttonText }}
</van-button>
</template>
</van-field>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.password"
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('forgotPassword.password')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.confirmPassword"
type="password"
:rules="rules.confirmPassword"
name="confirmPassword"
:placeholder="$t('forgotPassword.confirmPassword')"
/>
</div>
<div class="mt-4">
<van-button
:loading="loading"
type="primary"
native-type="submit"
round block
>
{{ $t('forgotPassword.confirm') }}
</van-button>
</div>
</van-form>
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
{{ $t('forgotPassword.backToLogin') }}
</GhostButton>
</div>
</template>
<route lang="json5">
{
name: 'ForgotPassword'
}
</route>

72
src/pages/index.vue Normal file
View File

@ -0,0 +1,72 @@
<script setup lang="ts">
import type { PickerColumn } from 'vant'
import { languageColumns, locale } from '@/utils/i18n'
const { t } = useI18n()
const checked = computed({
get: () => isDark.value,
set: () => toggleDark(),
})
const menuItems = computed(() => ([
{ title: t('navbar.Mock'), route: 'mock' },
{ title: t('navbar.Charts'), route: 'charts' },
{ title: t('navbar.UnoCSS'), route: 'unocss' },
{ title: t('navbar.Counter'), route: 'counter' },
{ title: t('navbar.KeepAlive'), route: 'keepalive' },
{ title: t('navbar.ScrollCache'), route: 'scroll-cache' },
{ title: t('navbar.404'), route: 'unknown' },
]))
const showLanguagePicker = ref(false)
const languageValues = ref<Array<string>>([locale.value])
const language = computed(() => languageColumns.find(l => l.value === locale.value).text)
function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
locale.value = event.selectedOptions[0].value as string
showLanguagePicker.value = false
}
</script>
<template>
<van-cell-group :title="$t('home.settings')" :border="false" :inset="true">
<van-cell center :title="$t('home.darkMode')">
<template #right-icon>
<van-switch
v-model="checked"
size="20px"
aria-label="on/off Dark Mode"
/>
</template>
</van-cell>
<van-cell
is-link
:title="$t('home.language')"
:value="language"
@click="showLanguagePicker = true"
/>
</van-cell-group>
<van-cell-group :title="$t('home.examples')" :border="false" :inset="true">
<template v-for="item in menuItems" :key="item.route">
<van-cell :title="item.title" :to="item.route" is-link />
</template>
</van-cell-group>
<van-popup v-model:show="showLanguagePicker" position="bottom">
<van-picker
v-model="languageValues"
:columns="languageColumns"
@confirm="onLanguageConfirm"
@cancel="showLanguagePicker = false"
/>
</van-popup>
</template>
<route lang="json5">
{
name: 'Home'
}
</route>

View File

@ -0,0 +1,23 @@
<script setup lang="ts">
defineOptions({
name: 'KeepAlive',
})
const value = ref(0)
</script>
<template>
<div class="text-sm space-y-2">
<p>{{ $t('keepAlive.label') }}</p>
<van-stepper v-model="value" />
</div>
</template>
<route lang="json5">
{
name: 'KeepAlive',
meta: {
keepAlive: true
},
}
</route>

108
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { RouteMap } from 'vue-router'
import { useUserStore } from '@/stores'
import logo from '~/images/logo.svg'
import logoDark from '~/images/logo-dark.svg'
import vw from '@/utils/inline-px-to-vw'
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const dark = ref<boolean>(isDark.value)
watch(
() => isDark.value,
(newMode) => {
dark.value = newMode
},
)
const postData = reactive({
email: '',
password: '',
})
const rules = reactive({
email: [
{ required: true, message: t('login.pleaseEnterEmail') },
],
password: [
{ required: true, message: t('login.pleaseEnterPassword') },
],
})
async function login(values: any) {
try {
loading.value = true
await userStore.login({ ...postData, ...values })
const { redirect, ...othersQuery } = router.currentRoute.value.query
router.push({
name: (redirect as keyof RouteMap) || 'Home',
query: {
...othersQuery,
},
})
}
finally {
loading.value = false
}
}
</script>
<template>
<div class="mx-auto p-3 text-center w-full">
<div class="mb-8 mt-2">
<van-image :src="dark ? logoDark : logo" class="h-30 w-30" alt="brand logo" />
</div>
<van-form :model="postData" :rules="rules" validate-trigger="onSubmit" @submit="login">
<div class="rounded-md overflow-hidden">
<van-field
v-model="postData.email"
:rules="rules.email"
name="email"
:placeholder="$t('login.email')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model="postData.password"
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('login.password')"
/>
</div>
<div class="mt-4">
<van-button
:loading="loading"
type="primary"
native-type="submit"
round block
>
{{ $t('login.login') }}
</van-button>
</div>
</van-form>
<GhostButton block to="register" :style="{ 'margin-top': vw(18) }">
{{ $t('login.signUp') }}
</GhostButton>
<GhostButton block to="forgot-password" class="mt-2">
{{ $t('login.forgotPassword') }}
</GhostButton>
</div>
</template>
<route lang="json5">
{
name: 'Login'
}
</route>

62
src/pages/mock/index.vue Normal file
View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { queryProse } from '@/api'
const messages = ref<string>('')
function pull() {
queryProse().then(({ code, result }) => {
if (code === 0)
messages.value = result
})
}
</script>
<template>
<div class="data-label">
{{ $t('mock.fromAsyncData') }}
</div>
<div class="data-content bg-white dark:bg-[--van-background-2]">
<div v-if="messages">
{{ messages }}
</div>
<VanEmpty v-else :description="$t('mock.noData')" />
</div>
<van-space class="m-2" direction="vertical" fill>
<VanButton type="primary" round block @click="pull">
{{ $t('mock.pull') }}
</VanButton>
<VanButton type="default" round block @click="messages = ''">
{{ $t('mock.reset') }}
</VanButton>
</van-space>
</template>
<route lang="json5">
{
name: 'Mock',
}
</route>
<style lang="less" scoped>
.data-label {
color: #969799;
font-weight: 400;
font-size: 14px;
line-height: 16px;
margin-top: 10px;
}
.data-content {
height: 300px;
padding: 20px;
line-height: 30px;
margin-top: 20px;
font-size: 16px;
border-radius: 15px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import router from '@/router'
import { useUserStore } from '@/stores'
import defaultAvatar from '@/assets/images/default-avatar.svg'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const isLogin = computed(() => !!userInfo.value.uid)
function login() {
if (isLogin.value)
return
router.push({ name: 'Login', query: { redirect: 'Profile' } })
}
</script>
<template>
<div>
<VanCellGroup :inset="true">
<van-cell center :is-link="!isLogin" @click="login">
<template #title>
<van-image :src="userInfo.avatar || defaultAvatar" round class="h-14 w-14" />
</template>
<template #value>
<span v-if="isLogin">{{ userInfo.name }}</span>
<span v-else>{{ $t('profile.login') }}</span>
</template>
</van-cell>
</VanCellGroup>
<VanCellGroup :inset="true" class="!mt-4">
<van-cell :title="$t('profile.settings')" icon="setting-o" is-link to="/settings">
<template #icon>
<div class="i-carbon:settings text-gray-400 mr-2 self-center" />
</template>
</van-cell>
<van-cell :title="$t('profile.docs')" is-link url="https://vue-zone.github.io/docs/vue3-vant-mobile/">
<template #icon>
<div class="i-carbon:doc text-gray-400 mr-2 self-center" />
</template>
</van-cell>
</VanCellGroup>
</div>
</template>
<route lang="json5">
{
name: 'Profile'
}
</route>

View File

@ -0,0 +1,157 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import type { FieldRule } from 'vant'
import { showNotify } from 'vant'
import { useUserStore } from '@/stores'
import vw from '@/utils/inline-px-to-vw'
const { t } = useI18n()
const router = useRouter()
const userStore = useUserStore()
const loading = ref(false)
const postData = reactive({
email: '',
code: '',
nickname: '',
password: '',
confirmPassword: '',
})
const validatorPassword = (val: string) => val === postData.password
const rules = reactive({
email: [
{ required: true, message: t('register.pleaseEnterEmail') },
],
code: [
{ required: true, message: t('register.pleaseEnterCode') },
],
nickname: [
{ required: true, message: t('register.pleaseEnterNickname') },
],
password: [
{ required: true, message: t('register.pleaseEnterPassword') },
],
confirmPassword: [
{ required: true, message: t('register.pleaseEnterConfirmPassword') },
{ required: true, validator: validatorPassword, message: t('register.passwordsDoNotMatch') },
] as FieldRule[],
})
async function register() {
try {
loading.value = true
const res = await userStore.register()
if (res.code === 0) {
showNotify({ type: 'success', message: t('register.registerSuccess') })
router.push({ name: 'Login' })
}
}
finally {
loading.value = false
}
}
const isGettingCode = ref(false)
const buttonText = computed(() => {
return isGettingCode.value ? t('register.gettingCode') : t('register.getCode')
})
async function getCode() {
if (!postData.email) {
showNotify({ type: 'warning', message: t('register.pleaseEnterEmail') })
return
}
isGettingCode.value = true
const res = await userStore.getCode()
if (res.code === 0)
showNotify({ type: 'success', message: `${t('register.sendCodeSuccess')}: ${res.result}` })
isGettingCode.value = false
}
</script>
<template>
<div class="mx-auto p-3 text-center w-full">
<van-form :model="postData" :rules="rules" validate-trigger="onSubmit" @submit="register">
<div class="rounded-md overflow-hidden">
<van-field
v-model.trim="postData.email"
:rules="rules.email"
name="email"
:placeholder="$t('register.email')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.code"
:rules="rules.code"
name="code"
:placeholder="$t('register.code')"
>
<template #button>
<van-button size="small" type="primary" plain @click="getCode">
{{ buttonText }}
</van-button>
</template>
</van-field>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.nickname"
:rules="rules.nickname"
name="nickname"
:placeholder="$t('register.nickname')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.password"
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('register.password')"
/>
</div>
<div class="mt-4 rounded-md overflow-hidden">
<van-field
v-model.trim="postData.confirmPassword"
type="password"
:rules="rules.confirmPassword"
name="confirmPassword"
:placeholder="$t('register.confirmPassword')"
/>
</div>
<div class="mt-4">
<van-button
:loading="loading"
type="primary"
native-type="submit"
round block
>
{{ $t('register.confirm') }}
</van-button>
</div>
</van-form>
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
{{ $t('register.backToLogin') }}
</GhostButton>
</div>
</template>
<route lang="json5">
{
name: 'Register'
}
</route>

View File

@ -0,0 +1,78 @@
# Page Scroll Position Save and Restore Guide
If you want to save the current scroll position when leaving a page and restore it upon return, you can follow the approach outlined below.
## Basic Approach
1. **Cache the Component**:
Set `keepAlive` to `true` to cache the component.
2. **Save Scroll Position**:
Use the `onBeforeRouteLeave` hook to save the current scroll position when leaving the page.
3. **Restore Scroll Position**:
Use the `onActivated` hook to restore the last saved scroll position when the page is activated.
## Example Code
```js
// Define a ref to store the scroll position
const scrollTop = ref(0)
// When a component with keepAlive set to true is activated, scroll to the saved position
onActivated(() => {
window.scrollTo(0, scrollTop.value)
})
// Before leaving the route, save the current scroll position
onBeforeRouteLeave(() => {
scrollTop.value
= window.scrollY
|| document.documentElement.scrollTop
|| document.body.scrollTop
})
```
# Handling a Specific Scroll Container
If you need to save and restore the scroll position for a specific element (instead of the entire window), follow these steps:
## 1. Add a ref in the Template
In your template, add a `ref` attribute to the scroll container element. For example:
```html
<div ref="scrollContainer" class="...">...</div>
```
## 2. In the setup Function
Use a ref to obtain the element's reference:
```js
const scrollContainer = ref(null)
```
## 3. In the onBeforeRouteLeave Hook
Save the scroll container's scroll position:
```js
onBeforeRouteLeave(() => {
if (scrollContainer.value) {
scrollTop.value = scrollContainer.value.scrollTop
}
})
```
## 3. In the onActivated Hook
Restore the scroll container's scroll position:
```js
onActivated(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollTop.value
}
})
```

View File

@ -0,0 +1,80 @@
# 页面滚动位置保存与恢复指南
如果你希望在离开页面时保存当前的滚动位置,并在返回时恢复它,可以参考下面的实现思路:
## 基本思路
1. **缓存组件**
设置 `keepAlive``true` 以缓存组件。
2. **保存滚动位置**
在页面离开时,使用 `onBeforeRouteLeave` 钩子保存当前滚动位置。
3. **恢复滚动位置**
在页面激活时,使用 `onActivated` 钩子恢复上次的滚动位置。
## 示例代码
```js
// 定义一个 ref 用于存储滚动位置
const scrollTop = ref(0)
// 当 keepAlive 为 true 的组件被激活时,滚动到保存的位置
onActivated(() => {
window.scrollTo(0, scrollTop.value)
})
// 在路由离开前,保存当前的滚动位置
onBeforeRouteLeave(() => {
scrollTop.value
= window.scrollY
|| document.documentElement.scrollTop
|| document.body.scrollTop
})
```
# 针对指定滚动容器的处理
如果你需要对特定元素(而非整个窗口)进行滚动位置的保存和恢复,可按以下步骤操作:
## 1. 在 Template 中添加 ref
在模板中,给滚动容器元素添加 `ref` 属性。例如:
```html
<div ref="scrollContainer" class="...">...</div>
```
## 2. 在 setup 中:
使用 ref 获取该元素的引用:
```js
const scrollContainer = ref(null)
```
## 3. 在 onBeforeRouteLeave 钩子中:
保存滚动容器的滚动位置:
```js
onBeforeRouteLeave(() => {
if (scrollContainer.value) {
scrollTop.value = scrollContainer.value.scrollTop
}
})
```
## 3. 在 onActivated 钩子中:
恢复滚动容器的滚动位置:
```js
onActivated(() => {
if (scrollContainer.value) {
scrollContainer.value.scrollTop = scrollTop.value
}
})
```
万事 OK 👌🏻

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
defineOptions({
name: 'ScrollCache',
})
const loading = ref(false)
const finished = ref(false)
const list = ref([])
function onLoad() {
setTimeout(() => {
for (let i = 0; i < 10; i++) {
list.value.push(`${list.value.length + 1}`)
}
loading.value = false
if (list.value.length >= 40) {
finished.value = true
}
}, 1000)
}
const scrollTop = ref(0)
onActivated(() => {
window.scrollTo(0, scrollTop.value)
})
onBeforeRouteLeave(() => {
scrollTop.value
= window.scrollY
|| document.documentElement.scrollTop
|| document.body.scrollTop
})
</script>
<template>
<van-list
v-model:loading="loading"
:finished="finished"
:finished-text="$t('scrollCache.finished')"
:loading-text="$t('scrollCache.loading')"
@load="onLoad"
>
<ul class="space-y-2">
<li v-for="item in list" :key="item" class="p-1 flex gap-3">
<div class="shrink-0">
<div class="rounded-full bg-gray-500/20 flex h-12 w-12 items-center justify-center overflow-hidden">
<span class="text-base text-zinc-600 tabular-nums dark:text-zinc-400"> {{ item }} </span>
</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex flex-row gap-2 w-full justify-between">
<h3 class="text-base text-zinc-600 tracking-tight font-semibold w-1/2 dark:text-white">
<van-text-ellipsis :content="`${$t('scrollCache.sectionTitle')}`" />
</h3>
<time class="text-xs text-zinc-400 tabular-nums">2025-05-16</time>
</div>
<p class="text-sm text-zinc-500">
<van-text-ellipsis :rows="2" :content="$t('scrollCache.sectionText')" />
</p>
</div>
</li>
</ul>
</van-list>
</template>
<route lang="json5">
{
name: 'ScrollCache',
meta: {
keepAlive: true
},
}
</route>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { showConfirmDialog } from 'vant'
import router from '@/router'
import { useUserStore } from '@/stores'
import { version } from '~root/package.json'
const { t } = useI18n()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
function Logout() {
showConfirmDialog({
title: t('settings.confirmTitle'),
})
.then(() => {
userStore.logout()
router.push({ name: 'Home' })
})
.catch(() => {})
}
</script>
<template>
<div class="text-center">
<VanCellGroup :inset="true">
<van-cell v-if="userInfo.uid" :title="$t('settings.logout')" clickable class="van-text-color" @click="Logout" />
</VanCellGroup>
<div class="text-gray mt-2">
{{ $t("settings.currentVersion") }}: v{{ version }}
</div>
</div>
</template>
<style scoped>
.van-text-color {
--van-cell-text-color: var(--van-red);
}
</style>
<route lang="json5">
{
name: 'Settings'
}
</route>

View File

@ -0,0 +1,19 @@
<template>
<h1 class="text-base color-pink font-semibold">
{{ $t('unocss.title') }}
</h1>
<p class="text-gray-700 mt-2 dark:text-white">
{{ $t('unocss.description') }}
</p>
<button class="btn mt-2">
{{ $t('unocss.button') }}
</button>
</template>
<route lang="json5">
{
name: 'UnoCSS'
}
</route>

8
src/router/README.md Normal file
View File

@ -0,0 +1,8 @@
# `File-based Routing`
Routes will be auto-generated for Vue files in the **src/pages** dir with the same file structure.
Check out [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) for more details.
**src/pages** 目录下的 Vue 文件会自动生成相同结构的路由。
查看[`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router)了解更多细节。

44
src/router/index.ts Normal file
View File

@ -0,0 +1,44 @@
import { createRouter, createWebHistory } from 'vue-router'
import { handleHotUpdate, routes } from 'vue-router/auto-routes'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import type { EnhancedRouteLocation } from './types'
import { useRouteCacheStore, useUserStore } from '@/stores'
import { isLogin } from '@/utils/auth'
import setPageTitle from '@/utils/set-page-title'
NProgress.configure({ showSpinner: true, parent: '#app' })
const router = createRouter({
history: createWebHistory(import.meta.env.VITE_APP_PUBLIC_PATH),
routes,
})
// This will update routes at runtime without reloading the page
if (import.meta.hot)
handleHotUpdate(router)
router.beforeEach(async (to: EnhancedRouteLocation) => {
NProgress.start()
const routeCacheStore = useRouteCacheStore()
const userStore = useUserStore()
// Route cache
routeCacheStore.addRoute(to)
// Set page title
setPageTitle(to.name)
if (isLogin() && !userStore.userInfo?.uid)
await userStore.info()
})
router.afterEach(() => {
NProgress.done()
})
export default router

7
src/router/types.ts Normal file
View File

@ -0,0 +1,7 @@
import type { RouteLocationNormalized } from 'vue-router'
export type EnhancedRouteLocation = RouteLocationNormalized & {
meta: {
keepAlive?: boolean
}
}

12
src/stores/index.ts Normal file
View File

@ -0,0 +1,12 @@
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import useUserStore from './modules/user'
import useCounterStore from './modules/counter'
import useRouteCacheStore from './modules/routeCache'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export { useUserStore, useCounterStore, useRouteCacheStore }
export default pinia

View File

@ -0,0 +1,18 @@
import { defineStore } from 'pinia'
const useCounterStore = defineStore('counter', () => {
const counter = ref(0)
const increment = () => {
counter.value++
}
return {
counter,
increment,
}
}, {
persist: true,
})
export default useCounterStore

View File

@ -0,0 +1,22 @@
import { defineStore } from 'pinia'
import type { RouteRecordName } from 'vue-router'
import type { EnhancedRouteLocation } from '@/router/types'
const useRouteCacheStore = defineStore('route-cache', () => {
const routeCaches = ref<RouteRecordName[]>([])
const addRoute = (route: EnhancedRouteLocation) => {
if (routeCaches.value.includes(route.name))
return
if (route?.meta?.keepAlive)
routeCaches.value.push(route.name)
}
return {
routeCaches,
addRoute,
}
})
export default useRouteCacheStore

View File

@ -0,0 +1,97 @@
import { defineStore } from 'pinia'
import type { LoginData, UserState } from '@/api/user'
import { clearToken, setToken } from '@/utils/auth'
import {
getEmailCode,
getUserInfo,
resetPassword,
login as userLogin,
logout as userLogout,
register as userRegister,
} from '@/api/user'
const InitUserInfo = {
uid: 0,
nickname: '',
avatar: '',
}
export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserState>({ ...InitUserInfo })
// Set user's information
const setInfo = (partial: Partial<UserState>) => {
userInfo.value = { ...partial }
}
const login = async (loginForm: LoginData) => {
try {
const { data } = await userLogin(loginForm)
setToken(data.token)
}
catch (error) {
clearToken()
throw error
}
}
const info = async () => {
try {
const { data } = await getUserInfo()
setInfo(data)
}
catch (error) {
clearToken()
throw error
}
}
const logout = async () => {
try {
await userLogout()
}
finally {
clearToken()
setInfo({ ...InitUserInfo })
}
}
const getCode = async () => {
try {
const data = await getEmailCode()
return data
}
catch {}
}
const reset = async () => {
try {
const data = await resetPassword()
return data
}
catch {}
}
const register = async () => {
try {
const data = await userRegister()
return data
}
catch {}
}
return {
userInfo,
info,
login,
logout,
getCode,
reset,
register,
}
}, {
persist: true,
})
export default useUserStore

View File

@ -0,0 +1,2 @@
export const STORAGE_TOKEN_KEY = 'access_token'
export const STORAGE_LANG_KEY = 'app_lang'

26
src/styles/app.less Normal file
View File

@ -0,0 +1,26 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
background: var(--van-gray-1);
font-size: var(--van-font-size-lg);
color-scheme: light;
}
html.dark {
background: #222;
color-scheme: dark;
}
#app {
height: 100%;
position: relative;
}
::-webkit-scrollbar {
width: 0;
background: transparent;
}

10
src/styles/var.less Normal file
View File

@ -0,0 +1,10 @@
/**
注意:为什么要写两个重复的 :root
由于 vant 中的主题变量也是在 :root 下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。
通过 :root:root 可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。
**/
:root:root {
// van-cell-group
--van-cell-group-inset-padding: 0;
}

320
src/types/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,320 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const asyncComputed: typeof import('@vueuse/core').asyncComputed
const autoResetRef: typeof import('@vueuse/core').autoResetRef
const computed: typeof import('vue').computed
const computedAsync: typeof import('@vueuse/core').computedAsync
const computedEager: typeof import('@vueuse/core').computedEager
const computedInject: typeof import('@vueuse/core').computedInject
const computedWithControl: typeof import('@vueuse/core').computedWithControl
const controlledComputed: typeof import('@vueuse/core').controlledComputed
const controlledRef: typeof import('@vueuse/core').controlledRef
const createApp: typeof import('vue').createApp
const createEventHook: typeof import('@vueuse/core').createEventHook
const createGlobalState: typeof import('@vueuse/core').createGlobalState
const createInjectionState: typeof import('@vueuse/core').createInjectionState
const createReactiveFn: typeof import('@vueuse/core').createReactiveFn
const createRef: typeof import('@vueuse/core').createRef
const createReusableTemplate: typeof import('@vueuse/core').createReusableTemplate
const createSharedComposable: typeof import('@vueuse/core').createSharedComposable
const createTemplatePromise: typeof import('@vueuse/core').createTemplatePromise
const createUnrefFn: typeof import('@vueuse/core').createUnrefFn
const customRef: typeof import('vue').customRef
const debouncedRef: typeof import('@vueuse/core').debouncedRef
const debouncedWatch: typeof import('@vueuse/core').debouncedWatch
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const eagerComputed: typeof import('@vueuse/core').eagerComputed
const effectScope: typeof import('vue').effectScope
const extendRef: typeof import('@vueuse/core').extendRef
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const i18n: typeof import('@/utils/i18n').i18n
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectHead: typeof import('@unhead/vue').injectHead
const injectLocal: typeof import('@vueuse/core').injectLocal
const isDark: typeof import('../composables/dark').isDark
const isDefined: typeof import('@vueuse/core').isDefined
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const locale: typeof import('@/utils/i18n').locale
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
const manualResetRef: typeof import('@vueuse/core').manualResetRef
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeRouteLeave: typeof import('vue-router').onBeforeRouteLeave
const onBeforeRouteUpdate: typeof import('vue-router').onBeforeRouteUpdate
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onClickOutside: typeof import('@vueuse/core').onClickOutside
const onDeactivated: typeof import('vue').onDeactivated
const onElementRemoval: typeof import('@vueuse/core').onElementRemoval
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onKeyStroke: typeof import('@vueuse/core').onKeyStroke
const onLongPress: typeof import('@vueuse/core').onLongPress
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onStartTyping: typeof import('@vueuse/core').onStartTyping
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const pausableWatch: typeof import('@vueuse/core').pausableWatch
const preferredDark: typeof import('../composables/dark').preferredDark
const provide: typeof import('vue').provide
const provideLocal: typeof import('@vueuse/core').provideLocal
const reactify: typeof import('@vueuse/core').reactify
const reactifyObject: typeof import('@vueuse/core').reactifyObject
const reactive: typeof import('vue').reactive
const reactiveComputed: typeof import('@vueuse/core').reactiveComputed
const reactiveOmit: typeof import('@vueuse/core').reactiveOmit
const reactivePick: typeof import('@vueuse/core').reactivePick
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const refAutoReset: typeof import('@vueuse/core').refAutoReset
const refDebounced: typeof import('@vueuse/core').refDebounced
const refDefault: typeof import('@vueuse/core').refDefault
const refManualReset: typeof import('@vueuse/core').refManualReset
const refThrottled: typeof import('@vueuse/core').refThrottled
const refWithControl: typeof import('@vueuse/core').refWithControl
const resolveComponent: typeof import('vue').resolveComponent
const resolveRef: typeof import('@vueuse/core').resolveRef
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs
const templateRef: typeof import('@vueuse/core').templateRef
const throttledRef: typeof import('@vueuse/core').throttledRef
const throttledWatch: typeof import('@vueuse/core').throttledWatch
const toRaw: typeof import('vue').toRaw
const toReactive: typeof import('@vueuse/core').toReactive
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const toggleDark: typeof import('../composables/dark').toggleDark
const triggerRef: typeof import('vue').triggerRef
const tryOnBeforeMount: typeof import('@vueuse/core').tryOnBeforeMount
const tryOnBeforeUnmount: typeof import('@vueuse/core').tryOnBeforeUnmount
const tryOnMounted: typeof import('@vueuse/core').tryOnMounted
const tryOnScopeDispose: typeof import('@vueuse/core').tryOnScopeDispose
const tryOnUnmounted: typeof import('@vueuse/core').tryOnUnmounted
const unref: typeof import('vue').unref
const unrefElement: typeof import('@vueuse/core').unrefElement
const until: typeof import('@vueuse/core').until
const useActiveElement: typeof import('@vueuse/core').useActiveElement
const useAnimate: typeof import('@vueuse/core').useAnimate
const useArrayDifference: typeof import('@vueuse/core').useArrayDifference
const useArrayEvery: typeof import('@vueuse/core').useArrayEvery
const useArrayFilter: typeof import('@vueuse/core').useArrayFilter
const useArrayFind: typeof import('@vueuse/core').useArrayFind
const useArrayFindIndex: typeof import('@vueuse/core').useArrayFindIndex
const useArrayFindLast: typeof import('@vueuse/core').useArrayFindLast
const useArrayIncludes: typeof import('@vueuse/core').useArrayIncludes
const useArrayJoin: typeof import('@vueuse/core').useArrayJoin
const useArrayMap: typeof import('@vueuse/core').useArrayMap
const useArrayReduce: typeof import('@vueuse/core').useArrayReduce
const useArraySome: typeof import('@vueuse/core').useArraySome
const useArrayUnique: typeof import('@vueuse/core').useArrayUnique
const useAsyncQueue: typeof import('@vueuse/core').useAsyncQueue
const useAsyncState: typeof import('@vueuse/core').useAsyncState
const useAttrs: typeof import('vue').useAttrs
const useBase64: typeof import('@vueuse/core').useBase64
const useBattery: typeof import('@vueuse/core').useBattery
const useBluetooth: typeof import('@vueuse/core').useBluetooth
const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
const useBrowserLocation: typeof import('@vueuse/core').useBrowserLocation
const useCached: typeof import('@vueuse/core').useCached
const useClipboard: typeof import('@vueuse/core').useClipboard
const useClipboardItems: typeof import('@vueuse/core').useClipboardItems
const useCloned: typeof import('@vueuse/core').useCloned
const useColorMode: typeof import('@vueuse/core').useColorMode
const useConfirmDialog: typeof import('@vueuse/core').useConfirmDialog
const useCountdown: typeof import('@vueuse/core').useCountdown
const useCounter: typeof import('@vueuse/core').useCounter
const useCssModule: typeof import('vue').useCssModule
const useCssVar: typeof import('@vueuse/core').useCssVar
const useCssVars: typeof import('vue').useCssVars
const useCurrentElement: typeof import('@vueuse/core').useCurrentElement
const useCycleList: typeof import('@vueuse/core').useCycleList
const useDark: typeof import('@vueuse/core').useDark
const useDateFormat: typeof import('@vueuse/core').useDateFormat
const useDebounce: typeof import('@vueuse/core').useDebounce
const useDebounceFn: typeof import('@vueuse/core').useDebounceFn
const useDebouncedRefHistory: typeof import('@vueuse/core').useDebouncedRefHistory
const useDeviceMotion: typeof import('@vueuse/core').useDeviceMotion
const useDeviceOrientation: typeof import('@vueuse/core').useDeviceOrientation
const useDevicePixelRatio: typeof import('@vueuse/core').useDevicePixelRatio
const useDevicesList: typeof import('@vueuse/core').useDevicesList
const useDisplayMedia: typeof import('@vueuse/core').useDisplayMedia
const useDocumentVisibility: typeof import('@vueuse/core').useDocumentVisibility
const useDraggable: typeof import('@vueuse/core').useDraggable
const useDropZone: typeof import('@vueuse/core').useDropZone
const useElementBounding: typeof import('@vueuse/core').useElementBounding
const useElementByPoint: typeof import('@vueuse/core').useElementByPoint
const useElementHover: typeof import('@vueuse/core').useElementHover
const useElementSize: typeof import('@vueuse/core').useElementSize
const useElementVisibility: typeof import('@vueuse/core').useElementVisibility
const useEventBus: typeof import('@vueuse/core').useEventBus
const useEventListener: typeof import('@vueuse/core').useEventListener
const useEventSource: typeof import('@vueuse/core').useEventSource
const useEyeDropper: typeof import('@vueuse/core').useEyeDropper
const useFavicon: typeof import('@vueuse/core').useFavicon
const useFetch: typeof import('@vueuse/core').useFetch
const useFileDialog: typeof import('@vueuse/core').useFileDialog
const useFileSystemAccess: typeof import('@vueuse/core').useFileSystemAccess
const useFocus: typeof import('@vueuse/core').useFocus
const useFocusWithin: typeof import('@vueuse/core').useFocusWithin
const useFps: typeof import('@vueuse/core').useFps
const useFullscreen: typeof import('@vueuse/core').useFullscreen
const useGamepad: typeof import('@vueuse/core').useGamepad
const useGeolocation: typeof import('@vueuse/core').useGeolocation
const useHead: typeof import('@unhead/vue').useHead
const useHeadSafe: typeof import('@unhead/vue').useHeadSafe
const useI18n: typeof import('vue-i18n').useI18n
const useId: typeof import('vue').useId
const useIdle: typeof import('@vueuse/core').useIdle
const useImage: typeof import('@vueuse/core').useImage
const useInfiniteScroll: typeof import('@vueuse/core').useInfiniteScroll
const useIntersectionObserver: typeof import('@vueuse/core').useIntersectionObserver
const useInterval: typeof import('@vueuse/core').useInterval
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
const useKeyModifier: typeof import('@vueuse/core').useKeyModifier
const useLastChanged: typeof import('@vueuse/core').useLastChanged
const useLink: typeof import('vue-router/auto').useLink
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
const useMagicKeys: typeof import('@vueuse/core').useMagicKeys
const useManualRefHistory: typeof import('@vueuse/core').useManualRefHistory
const useMediaControls: typeof import('@vueuse/core').useMediaControls
const useMediaQuery: typeof import('@vueuse/core').useMediaQuery
const useMemoize: typeof import('@vueuse/core').useMemoize
const useMemory: typeof import('@vueuse/core').useMemory
const useModel: typeof import('vue').useModel
const useMounted: typeof import('@vueuse/core').useMounted
const useMouse: typeof import('@vueuse/core').useMouse
const useMouseInElement: typeof import('@vueuse/core').useMouseInElement
const useMousePressed: typeof import('@vueuse/core').useMousePressed
const useMutationObserver: typeof import('@vueuse/core').useMutationObserver
const useNavigatorLanguage: typeof import('@vueuse/core').useNavigatorLanguage
const useNetwork: typeof import('@vueuse/core').useNetwork
const useNow: typeof import('@vueuse/core').useNow
const useObjectUrl: typeof import('@vueuse/core').useObjectUrl
const useOffsetPagination: typeof import('@vueuse/core').useOffsetPagination
const useOnline: typeof import('@vueuse/core').useOnline
const usePageLeave: typeof import('@vueuse/core').usePageLeave
const useParallax: typeof import('@vueuse/core').useParallax
const useParentElement: typeof import('@vueuse/core').useParentElement
const usePerformanceObserver: typeof import('@vueuse/core').usePerformanceObserver
const usePermission: typeof import('@vueuse/core').usePermission
const usePointer: typeof import('@vueuse/core').usePointer
const usePointerLock: typeof import('@vueuse/core').usePointerLock
const usePointerSwipe: typeof import('@vueuse/core').usePointerSwipe
const usePreferredColorScheme: typeof import('@vueuse/core').usePreferredColorScheme
const usePreferredContrast: typeof import('@vueuse/core').usePreferredContrast
const usePreferredDark: typeof import('@vueuse/core').usePreferredDark
const usePreferredLanguages: typeof import('@vueuse/core').usePreferredLanguages
const usePreferredReducedMotion: typeof import('@vueuse/core').usePreferredReducedMotion
const usePreferredReducedTransparency: typeof import('@vueuse/core').usePreferredReducedTransparency
const usePrevious: typeof import('@vueuse/core').usePrevious
const useRafFn: typeof import('@vueuse/core').useRafFn
const useRefHistory: typeof import('@vueuse/core').useRefHistory
const useResizeObserver: typeof import('@vueuse/core').useResizeObserver
const useRoute: typeof import('vue-router').useRoute
const useRouter: typeof import('vue-router').useRouter
const useSSRWidth: typeof import('@vueuse/core').useSSRWidth
const useScreenOrientation: typeof import('@vueuse/core').useScreenOrientation
const useScreenSafeArea: typeof import('@vueuse/core').useScreenSafeArea
const useScriptTag: typeof import('@vueuse/core').useScriptTag
const useScroll: typeof import('@vueuse/core').useScroll
const useScrollLock: typeof import('@vueuse/core').useScrollLock
const useSeoMeta: typeof import('@unhead/vue').useSeoMeta
const useServerHead: typeof import('@unhead/vue').useServerHead
const useServerHeadSafe: typeof import('@unhead/vue').useServerHeadSafe
const useServerSeoMeta: typeof import('@unhead/vue').useServerSeoMeta
const useSessionStorage: typeof import('@vueuse/core').useSessionStorage
const useShare: typeof import('@vueuse/core').useShare
const useSlots: typeof import('vue').useSlots
const useSorted: typeof import('@vueuse/core').useSorted
const useSpeechRecognition: typeof import('@vueuse/core').useSpeechRecognition
const useSpeechSynthesis: typeof import('@vueuse/core').useSpeechSynthesis
const useStepper: typeof import('@vueuse/core').useStepper
const useStorage: typeof import('@vueuse/core').useStorage
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
const useStyleTag: typeof import('@vueuse/core').useStyleTag
const useSupported: typeof import('@vueuse/core').useSupported
const useSwipe: typeof import('@vueuse/core').useSwipe
const useTemplateRef: typeof import('vue').useTemplateRef
const useTemplateRefsList: typeof import('@vueuse/core').useTemplateRefsList
const useTextDirection: typeof import('@vueuse/core').useTextDirection
const useTextSelection: typeof import('@vueuse/core').useTextSelection
const useTextareaAutosize: typeof import('@vueuse/core').useTextareaAutosize
const useThrottle: typeof import('@vueuse/core').useThrottle
const useThrottleFn: typeof import('@vueuse/core').useThrottleFn
const useThrottledRefHistory: typeof import('@vueuse/core').useThrottledRefHistory
const useTimeAgo: typeof import('@vueuse/core').useTimeAgo
const useTimeAgoIntl: typeof import('@vueuse/core').useTimeAgoIntl
const useTimeout: typeof import('@vueuse/core').useTimeout
const useTimeoutFn: typeof import('@vueuse/core').useTimeoutFn
const useTimeoutPoll: typeof import('@vueuse/core').useTimeoutPoll
const useTimestamp: typeof import('@vueuse/core').useTimestamp
const useTitle: typeof import('@vueuse/core').useTitle
const useToNumber: typeof import('@vueuse/core').useToNumber
const useToString: typeof import('@vueuse/core').useToString
const useToggle: typeof import('@vueuse/core').useToggle
const useTransition: typeof import('@vueuse/core').useTransition
const useUrlSearchParams: typeof import('@vueuse/core').useUrlSearchParams
const useUserMedia: typeof import('@vueuse/core').useUserMedia
const useVModel: typeof import('@vueuse/core').useVModel
const useVModels: typeof import('@vueuse/core').useVModels
const useVibrate: typeof import('@vueuse/core').useVibrate
const useVirtualList: typeof import('@vueuse/core').useVirtualList
const useWakeLock: typeof import('@vueuse/core').useWakeLock
const useWebNotification: typeof import('@vueuse/core').useWebNotification
const useWebSocket: typeof import('@vueuse/core').useWebSocket
const useWebWorker: typeof import('@vueuse/core').useWebWorker
const useWebWorkerFn: typeof import('@vueuse/core').useWebWorkerFn
const useWindowFocus: typeof import('@vueuse/core').useWindowFocus
const useWindowScroll: typeof import('@vueuse/core').useWindowScroll
const useWindowSize: typeof import('@vueuse/core').useWindowSize
const watch: typeof import('vue').watch
const watchArray: typeof import('@vueuse/core').watchArray
const watchAtMost: typeof import('@vueuse/core').watchAtMost
const watchDebounced: typeof import('@vueuse/core').watchDebounced
const watchDeep: typeof import('@vueuse/core').watchDeep
const watchEffect: typeof import('vue').watchEffect
const watchIgnorable: typeof import('@vueuse/core').watchIgnorable
const watchImmediate: typeof import('@vueuse/core').watchImmediate
const watchOnce: typeof import('@vueuse/core').watchOnce
const watchPausable: typeof import('@vueuse/core').watchPausable
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const watchThrottled: typeof import('@vueuse/core').watchThrottled
const watchTriggerable: typeof import('@vueuse/core').watchTriggerable
const watchWithFilter: typeof import('@vueuse/core').watchWithFilter
const whenever: typeof import('@vueuse/core').whenever
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

40
src/types/components.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
Chart: typeof import('./../components/Chart/index.vue')['default']
GhostButton: typeof import('./../components/GhostButton.vue')['default']
NavBar: typeof import('./../components/NavBar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
TabBar: typeof import('./../components/TabBar.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCell: typeof import('vant/es')['Cell']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanEmpty: typeof import('vant/es')['Empty']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar']
VanPicker: typeof import('vant/es')['Picker']
VanPopup: typeof import('vant/es')['Popup']
VanSpace: typeof import('vant/es')['Space']
VanStepper: typeof import('vant/es')['Stepper']
VanSwitch: typeof import('vant/es')['Switch']
VanTabbar: typeof import('vant/es')['Tabbar']
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTextEllipsis: typeof import('vant/es')['TextEllipsis']
}
}

8
src/types/env.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<Record<string, never>, Record<string, never>, any>
export default component
}

220
src/types/typed-router.d.ts vendored Normal file
View File

@ -0,0 +1,220 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection ES6UnusedImports
// Generated by unplugin-vue-router. !! DO NOT MODIFY THIS FILE !!
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-resolver' {
export type ParamParserCustom = never
}
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'Home': RouteRecordInfo<
'Home',
'/',
Record<never, never>,
Record<never, never>,
| never
>,
'404': RouteRecordInfo<
'404',
'/:all(.*)',
{ all: ParamValue<true> },
{ all: ParamValue<false> },
| never
>,
'Charts': RouteRecordInfo<
'Charts',
'/charts',
Record<never, never>,
Record<never, never>,
| never
>,
'Counter': RouteRecordInfo<
'Counter',
'/counter',
Record<never, never>,
Record<never, never>,
| never
>,
'ForgotPassword': RouteRecordInfo<
'ForgotPassword',
'/forgot-password',
Record<never, never>,
Record<never, never>,
| never
>,
'KeepAlive': RouteRecordInfo<
'KeepAlive',
'/keepalive',
Record<never, never>,
Record<never, never>,
| never
>,
'Login': RouteRecordInfo<
'Login',
'/login',
Record<never, never>,
Record<never, never>,
| never
>,
'Mock': RouteRecordInfo<
'Mock',
'/mock',
Record<never, never>,
Record<never, never>,
| never
>,
'Profile': RouteRecordInfo<
'Profile',
'/profile',
Record<never, never>,
Record<never, never>,
| never
>,
'Register': RouteRecordInfo<
'Register',
'/register',
Record<never, never>,
Record<never, never>,
| never
>,
'ScrollCache': RouteRecordInfo<
'ScrollCache',
'/scroll-cache',
Record<never, never>,
Record<never, never>,
| never
>,
'Settings': RouteRecordInfo<
'Settings',
'/settings',
Record<never, never>,
Record<never, never>,
| never
>,
'UnoCSS': RouteRecordInfo<
'UnoCSS',
'/unocss',
Record<never, never>,
Record<never, never>,
| never
>,
}
/**
* Route file to route info map by unplugin-vue-router.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* Each key is a file path relative to the project root with 2 properties:
* - routes: union of route names of the possible routes when in this page (passed to useRoute<...>())
* - views: names of nested views (can be passed to <RouterView name="...">)
*
* @internal
*/
export interface _RouteFileInfoMap {
'src/pages/index.vue': {
routes:
| 'Home'
views:
| never
}
'src/pages/[...all].vue': {
routes:
| '404'
views:
| never
}
'src/pages/charts/index.vue': {
routes:
| 'Charts'
views:
| never
}
'src/pages/counter/index.vue': {
routes:
| 'Counter'
views:
| never
}
'src/pages/forgot-password/index.vue': {
routes:
| 'ForgotPassword'
views:
| never
}
'src/pages/keepalive/index.vue': {
routes:
| 'KeepAlive'
views:
| never
}
'src/pages/login/index.vue': {
routes:
| 'Login'
views:
| never
}
'src/pages/mock/index.vue': {
routes:
| 'Mock'
views:
| never
}
'src/pages/profile/index.vue': {
routes:
| 'Profile'
views:
| never
}
'src/pages/register/index.vue': {
routes:
| 'Register'
views:
| never
}
'src/pages/scroll-cache/index.vue': {
routes:
| 'ScrollCache'
views:
| never
}
'src/pages/settings/index.vue': {
routes:
| 'Settings'
views:
| never
}
'src/pages/unocss/index.vue': {
routes:
| 'UnoCSS'
views:
| never
}
}
/**
* Get a union of possible route names in a certain route component file.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* @internal
*/
export type _RouteNamesForFilePath<FilePath extends string> =
_RouteFileInfoMap extends Record<FilePath, infer Info>
? Info['routes']
: keyof RouteNamedMap
}

9
src/types/vue-router.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module 'vue-router' {
interface RouteMeta {
/** page title */
title?: string
/** keepalive */
keepAlive?: boolean
}
}
export {}

22
src/utils/auth.ts Normal file
View File

@ -0,0 +1,22 @@
import { STORAGE_TOKEN_KEY } from '@/stores/mutation-type'
import { useLocalStorage } from '@vueuse/core'
const token = useLocalStorage(STORAGE_TOKEN_KEY, '')
function isLogin() {
return !!token.value
}
function getToken() {
return token.value
}
function setToken(newToken: string) {
token.value = newToken
}
function clearToken() {
token.value = null
}
export { isLogin, getToken, setToken, clearToken }

71
src/utils/i18n.ts Normal file
View File

@ -0,0 +1,71 @@
import { createI18n } from 'vue-i18n'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
import { Locale } from 'vant'
import type { PickerColumn } from 'vant'
const FALLBACK_LOCALE = 'zh-CN'
const vantLocales = {
'zh-CN': zhCN,
'en-US': enUS,
}
export const languageColumns: PickerColumn = [
{ text: '简体中文', value: 'zh-CN' },
{ text: 'English', value: 'en-US' },
]
export const i18n = setupI18n()
type I18n = typeof i18n
export const locale = computed({
get() {
return i18n.global.locale.value
},
set(language: string) {
setLang(language, i18n)
},
})
function setupI18n() {
const locale = getI18nLocale()
const i18n = createI18n({
locale,
legacy: false,
})
setLang(locale, i18n)
return i18n
}
async function setLang(lang: string, i18n: I18n) {
await loadLocaleMsg(lang, i18n)
document.querySelector('html').setAttribute('lang', lang)
localStorage.setItem('language', lang)
i18n.global.locale.value = lang
// 设置 vant 组件语言包
Locale.use(lang, vantLocales[lang])
}
// 加载本地语言包
async function loadLocaleMsg(locale: string, i18n: I18n) {
const messages = await import(`../locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
}
// 获取当前语言对应的语言包名称
function getI18nLocale() {
const storedLocale = localStorage.getItem('language') || navigator.language
const langs = languageColumns.map(v => v.value as string)
// 存在当前语言的语言包 或 存在当前语言的任意地区的语言包
const foundLocale = langs.find(v => v === storedLocale || v.indexOf(storedLocale) === 0)
// 若未找到,则使用 默认语言包
const locale = foundLocale || FALLBACK_LOCALE
return locale
}

View File

@ -0,0 +1,28 @@
/**
* px vw
* scale-view
* https://github.com/wswmsword/scale-view
* @wswmsword
*/
import { round } from 'lodash-es'
// 理想宽度,设计稿的宽度
const idealWidth = 375
// 表示伸缩视图的最大宽度
const maxWidth = 600
/**
* vw
* @param {number} n
*/
export default function vw(n: number) {
if (n === 0)
return n
const vwN = round(n * 100 / idealWidth, 3)
const maxN = round(n * maxWidth / idealWidth, 3)
const cssF = n > 0 ? 'min' : 'max'
return `${cssF}(${vwN}vw, ${maxN}px)`
}

71
src/utils/request.ts Normal file
View File

@ -0,0 +1,71 @@
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'
import { showNotify } from 'vant'
import { STORAGE_TOKEN_KEY } from '@/stores/mutation-type'
// 这里是用于设定请求后端时,所用的 Token KEY
// 可以根据自己的需要修改,常见的如 Access-TokenAuthorization
// 需要注意的是,请尽量保证使用中横线`-` 来作为分隔符,
// 避免被 nginx 等负载均衡器丢弃了自定义的请求头
export const REQUEST_TOKEN_KEY = 'Access-Token'
// 创建 axios 实例
const request = axios.create({
// API 请求的默认前缀
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
timeout: 6000, // 请求超时时间
})
export type RequestError = AxiosError<{
message?: string
result?: any
errorMessage?: string
}>
// 异常拦截处理器
function errorHandler(error: RequestError): Promise<any> {
if (error.response) {
const { data = {}, status, statusText } = error.response
// 403 无权限
if (status === 403) {
showNotify({
type: 'danger',
message: (data && data.message) || statusText,
})
}
// 401 未登录/未授权
if (status === 401 && data.result && data.result.isLogin) {
showNotify({
type: 'danger',
message: 'Authorization verification failed',
})
// 如果你需要直接跳转登录页面
// location.replace(loginRoutePath)
}
}
return Promise.reject(error)
}
// 请求拦截器
function requestHandler(config: InternalAxiosRequestConfig): InternalAxiosRequestConfig | Promise<InternalAxiosRequestConfig> {
const savedToken = localStorage.getItem(STORAGE_TOKEN_KEY)
// 如果 token 存在
// 让每个请求携带自定义 token, 请根据实际情况修改
if (savedToken)
config.headers[REQUEST_TOKEN_KEY] = savedToken
return config
}
// Add a request interceptor
request.interceptors.request.use(requestHandler, errorHandler)
// 响应拦截器
function responseHandler(response: { data: any }) {
return response.data
}
// Add a response interceptor
request.interceptors.response.use(responseHandler, errorHandler)
export default request

View File

@ -0,0 +1,6 @@
import { appName } from '@/constants'
import { i18n } from '@/utils/i18n'
export default function setPageTitle(name?: string): void {
window.document.title = name ? `${i18n.global.t(`navbar.${name}`)} - ${appName()}` : appName()
}

41
tsconfig.json Normal file
View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "esnext",
"jsx": "preserve",
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"experimentalDecorators": true,
"baseUrl": ".",
"module": "esnext",
"moduleResolution": "Bundler",
"paths": {
"@/*": ["src/*"],
"~root/*": ["./*"]
},
"types": [
"node",
"unplugin-vue-router/client",
"vite-plugin-pwa/client",
"@intlify/unplugin-vue-i18n/messages"
],
"allowJs": true,
"strictNullChecks": false,
"noImplicitAny": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"importHelpers": true,
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": [
"src/App.vue",
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"src/types/components.d.ts",
"src/types/auto-imports.d.ts",
"src/types/typed-router.d.ts"
]
}

36
uno.config.ts Normal file
View File

@ -0,0 +1,36 @@
import { createRemToPxProcessor } from '@unocss/preset-wind4/utils'
import {
defineConfig,
presetAttributify,
presetIcons,
presetWind4,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
shortcuts: [
['btn', 'px-2 py-1 rounded-1 border-none inline-block bg-green-400 text-white cursor-pointer outline-hidden hover:bg-green-600 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
],
presets: [
presetWind4({
preflights: {
theme: {
process: createRemToPxProcessor(),
},
},
}),
presetAttributify(),
presetIcons({
scale: 1.2,
}),
],
postprocess: [
createRemToPxProcessor(),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
})

45
vite.config.ts Normal file
View File

@ -0,0 +1,45 @@
import path from 'node:path'
import process from 'node:process'
import { loadEnv } from 'vite'
import type { ConfigEnv, UserConfig } from 'vite'
import { createVitePlugins } from './build/vite'
import { exclude, include } from './build/vite/optimize'
export default ({ mode }: ConfigEnv): UserConfig => {
const root = process.cwd()
const env = loadEnv(mode, root)
return {
base: env.VITE_APP_PUBLIC_PATH,
plugins: createVitePlugins(mode),
server: {
host: true,
port: 3000,
proxy: {
'/api': {
target: '', // Your backend API base URL
ws: false,
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
resolve: {
alias: {
'@': path.join(__dirname, './src'),
'~': path.join(__dirname, './src/assets'),
'~root': path.join(__dirname, '.'),
},
},
build: {
cssCodeSplit: false,
chunkSizeWarningLimit: 2048,
outDir: env.VITE_APP_OUT_DIR || 'dist',
},
optimizeDeps: { include, exclude },
}
}