init
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
NODE_ENV=development
|
||||
VITE_APP_PREVIEW=true
|
||||
VITE_APP_API_BASE_URL=/api
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
VITE_APP_PREVIEW=false
|
||||
VITE_APP_API_BASE_URL=https://easyapi.devv.zone/api
|
||||
|
|
@ -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>
|
||||
|
|
@ -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}}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
20.19.0
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
|
||||
[](https://deepwiki.com/vue-zone/vue3-vant-mobile) [](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>
|
||||
|
|
@ -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)
|
||||
|
||||
[](https://deepwiki.com/vue-zone/vue3-vant-mobile) [](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>
|
||||
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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();
|
||||
`,
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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/**',
|
||||
],
|
||||
},
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { defineMockData } from 'vite-plugin-mock-dev-server'
|
||||
|
||||
// defineMockData,用于在 mock 文件中使用 data.ts 作为共享数据源。
|
||||
export default defineMockData('proses', [
|
||||
'🔖 躲在某一时间,想念一段时光的掌纹;躲在某一地点,想念一个站在来路也站在去路的,让我牵挂的人。',
|
||||
'🔖 天空一碧如洗,灿烂的阳光正从密密的松针的缝隙间射下来,形成一束束粗粗细细的光柱,把飘荡着轻纱般薄雾的林荫照得通亮。',
|
||||
'🔖 这一次相遇,美得彻骨,美得震颤,美得孤绝,美得惊艳。',
|
||||
'🔖 沉默的状态,能让我感觉到呼吸的自由和自己原来就处于的本色位置。',
|
||||
'🔖 青春,是一包象征着阳光的向日葵种子,在现在洒下,就会在未来得到收获,那一株株饱含青春的花朵。',
|
||||
'🔖 燕子去了,有再来的时候;杨柳枯了,有再青的时候;桃花谢了,有再开的时候。但是,聪明的,你告诉我,我们的日子为什么一去不复返呢?',
|
||||
'🔖 毕业了,青春在无形之中离去,我们即将翻开人生的另一页。',
|
||||
'🔖 成长,是每个孩子的权力,也是他们必经的征程,或平坦、或崎岖,有悲欢,有离合。',
|
||||
'🔖 旧时光里的人和事,琐碎而零乱。我的记忆很模糊,好像大部分都成了一种温馨的符号,静静的沉在我心底。',
|
||||
'🔖 生活是一部大百科全书,包罗万象;生活是一把六弦琴,弹奏出多重美妙的旋律:生活是一座飞马牌大钟,上紧发条,便会使人获得浓缩的生命。',
|
||||
'🔖 毕业了,身边的朋友一个个各奔东西,开始学会自己撑起生命的暖色。',
|
||||
'🔖 已经走到尽头的东西,重生也不过是再一次的消亡。就像所有的开始,其实都只是一个写好了的结局。',
|
||||
'🔖 下午茶的芬香熏陶着房内的任何一个角落,午后的阳光透过窗帘的间隙洒在木制的桌面上,一份思念随着红茶顺滑至心中。',
|
||||
'🔖 这里再不是我们的校园,当我们就此离开我们的青葱岁月。',
|
||||
'🔖 很久找你,一直没有找到,微风吹过的时候,我深深的呼吸,才感觉到你也在陪伴着我呼吸。',
|
||||
])
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import prose from './modules/prose.mock'
|
||||
import user from './modules/user.mock'
|
||||
|
||||
export default {
|
||||
...prose,
|
||||
...user,
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -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 |
|
After Width: | Height: | Size: 41 KiB |
|
|
@ -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 |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import request from '@/utils/request'
|
||||
|
||||
export async function queryProse(): Promise<any> {
|
||||
return request('/prose')
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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).
|
||||
|
|
@ -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>
|
||||
|
|
@ -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()
|
||||
|
|
@ -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',
|
||||
]
|
||||
|
|
@ -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')
|
||||
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "确认退出?"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 中有个别组件是以函数的形式提供的,
|
||||
包括 Toast,Dialog,Notify 和 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')
|
||||
|
|
@ -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) 自定义块定义每个页面的路由名称和元信息,可以轻松控制每个路由的过渡动画。
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
})
|
||||
```
|
||||
|
|
@ -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 👌🏻
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)了解更多细节。
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
|
||||
export type EnhancedRouteLocation = RouteLocationNormalized & {
|
||||
meta: {
|
||||
keepAlive?: boolean
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export const STORAGE_TOKEN_KEY = 'access_token'
|
||||
export const STORAGE_LANG_KEY = 'app_lang'
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
注意:为什么要写两个重复的 :root?
|
||||
由于 vant 中的主题变量也是在 :root 下声明的,所以在有些情况下会由于优先级的问题无法成功覆盖。
|
||||
通过 :root:root 可以显式地让你所写内容的优先级更高一些,从而确保主题变量的成功覆盖。
|
||||
**/
|
||||
|
||||
:root:root {
|
||||
// van-cell-group
|
||||
--van-cell-group-inset-padding: 0;
|
||||
}
|
||||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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']
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
/** page title */
|
||||
title?: string
|
||||
/** keepalive */
|
||||
keepAlive?: boolean
|
||||
}
|
||||
}
|
||||
export {}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)`
|
||||
}
|
||||
|
|
@ -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-Token,Authorization
|
||||
// 需要注意的是,请尽量保证使用中横线`-` 来作为分隔符,
|
||||
// 避免被 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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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(),
|
||||
],
|
||||
})
|
||||
|
|
@ -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 },
|
||||
}
|
||||
}
|
||||