项目初始化

This commit is contained in:
scout 2025-01-08 13:26:12 +08:00
commit dc07b81438
48 changed files with 11779 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NUXT_PUBLIC_API_BASE=https://easyapi.devv.zone

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea

1
.npmrc Normal file
View File

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

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

@ -0,0 +1,10 @@
{
"recommendations": [
"nuxtr.nuxtr-vscode",
"vue.vscode-typescript-vue-plugin",
"vue.volar",
"dbaeumer.vscode-eslint",
"antfu.iconify",
"lokalise.i18n-ally"
]
}

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

@ -0,0 +1,51 @@
{
"files.associations": {
"*.css": "postcss"
},
"i18n-ally.keystyle": "nested",
"i18n-ally.localesPaths": [
"i18n/locales"
],
"i18n-ally.sourceLanguage": "zh-CN",
"i18n-ally.displayLanguage": "zh-CN",
"typescript.tsdk": "node_modules/typescript/lib",
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

21
LICENSE Normal file
View File

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

136
README.md Normal file
View File

@ -0,0 +1,136 @@
<!-- markdownlint-disable MD033 MD041 -->
<div id="top" align="center">
<img src="https://cdn.jsdelivr.net/gh/easy-temps/easy-static/cover.png" alt="cover" />
<h1 align="center">nuxt-vant-mobile</h1>
An mobile web apps template based on the Nuxt _⁴_ ecosystem.
一个基于 Nuxt _⁴_ 生态系统的移动 web 应用模板,帮助你快速完成业务开发。
<p>
<img src="https://img.shields.io/github/license/easy-temps/nuxt-vant-mobile" alt="license" />
<img src="https://img.shields.io/github/package-json/v/easy-temps/nuxt-vant-mobile" alt="version" />
<img src="https://img.shields.io/github/repo-size/easy-temps/nuxt-vant-mobile" alt="repo-size" />
<img src="https://img.shields.io/github/languages/top/easy-temps/nuxt-vant-mobile" alt="languages" />
<img src="https://img.shields.io/github/issues-closed/easy-temps/nuxt-vant-mobile" alt="issues" />
</p>
[文档](https://easy-temps.github.io/easy-docs/nuxt3-vant-mobile/) / [交流](https://github.com/easy-temps/vue3-vant-mobile/issues/56) / [反馈](https://github.com/easy-temps/nuxt-vant-mobile/issues)
🖥 <a href="https://nuxt-vant-mobile.netlify.app">Online Preview</a>
[![Netlify Status](https://api.netlify.com/api/v1/badges/1eb0d3f7-69a1-4972-a2b7-9e317ffa5c63/deploy-status)](https://app.netlify.com/sites/nuxt-vant-mobile/deploys)
</div>
## Features
- 💚 [Nuxt](https://nuxt.com/) - SSR, ESR, File-based routing, components auto importing, modules, etc
- ⚡️ Vite - Instant HMR
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand atomic CSS engine
- 😃 Use icons from any icon sets in Pure CSS, powered by [UnoCSS](https://github.com/unocss/unocss)
- 🔥 The `<script setup>` syntax
- 🌍 [I18n ready](./i18n/locales)
- 🍍 [State Management via Pinia](https://github.com/vuejs/pinia), see [./app/composables/counter.ts](./app/composables/counter.ts)
- 📑 [Layout system](./app/layouts)
- 📥 APIs auto importing - for Composition API and custom composables
- 🦾 TypeScript, of course
- ☁️ Deploy on [Netlify](https://www.netlify.com), zero-config
## Nuxt Modules
- [Vant](https://github.com/youzan/vant) - Vue UI library for mobile web apps
- [Nuxt ESLint](https://github.com/nuxt/eslint) - collection of ESLint-related packages for Nuxt
- [i18n](https://github.com/nuxt-modules/i18n) - i18n module for Nuxt
- [ColorMode](https://github.com/nuxt-modules/color-mode) - dark and Light mode with auto detection made easy with Nuxt
- [UnoCSS](https://github.com/unocss/unocss) - the instant on-demand atomic CSS engine
- [Pinia](https://github.com/vuejs/pinia) - intuitive, type safe, light and flexible Store for Vue
- [Pinia Persistedstate](https://github.com/prazdevs/pinia-plugin-persistedstate) - configurable persistence and rehydration of Pinia stores
- [DevTools](https://github.com/nuxt/devtools) - unleash Nuxt Developer Experience
## IDE
We recommend using [VS Code](https://code.visualstudio.com/) with [Volar](https://github.com/johnsoncodehk/volar) to get the best experience (You might want to disable [Vetur](https://vuejs.github.io/vetur/) if you have it)
## Try it now
### GitHub Template
[Create a repo from this template on GitHub](https://github.com/easy-temps/nuxt-vant-mobile/generate)
### Clone to local
If you prefer to do it manually with the cleaner git history
```bash
npx tiged easy-temps/nuxt-vant-mobile my-nuxt-app
cd my-nuxt-app
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
```
## 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 `.output` 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/easy-temps/nuxt-vant-mobile/issues) be used for problem feedback, or [Wechat group](https://github.com/easy-temps/vue3-vant-mobile/issues/56).
## Donation ☕
[Buy Me a Coffee](https://github.com/CharleeWa/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>
</p>
## License
[MIT](./LICENSE) License
<p align="right">
<a href="#top">⬆️ Back to Top</a>
</p>

55
app/api/http.ts Normal file
View File

@ -0,0 +1,55 @@
import type { $Fetch } from 'ofetch'
import { useRuntimeConfig } from '#app'
import { ofetch } from 'ofetch'
type HttpStatusErrorHandler = (message: string, statusCode: number) => void
let httpStatusErrorHandler: HttpStatusErrorHandler
let http: $Fetch
export function setupHttp() {
if (http)
return http
const config = useRuntimeConfig()
const baseURL = config.public.apiBase as string
http = ofetch.create({
baseURL,
headers: { 'Content-Type': 'application/json' },
async onRequest({ options }) {
const token = localStorage.getItem('token')
options.headers = {
...options.headers,
...(token && { Authorization: `Bearer ${token}` }),
}
},
async onResponseError({ response }) {
const { message } = response._data
if (Array.isArray(message)) {
message.forEach((item) => {
httpStatusErrorHandler?.(item, response.status)
})
}
else {
httpStatusErrorHandler?.(message, response.status)
}
return Promise.reject(response._data)
},
retry: 3,
retryDelay: 1000,
})
}
export function injectHttpStatusErrorHandler(handler: HttpStatusErrorHandler) {
httpStatusErrorHandler = handler
}
export function getHttp() {
if (!http) {
throw new Error('HTTP client not initialized. Call setupHttp first.')
}
return http
}

8
app/api/prose.ts Normal file
View File

@ -0,0 +1,8 @@
import { getHttp } from './http'
export async function getProse() {
const http = getHttp()
return await http('/api/prose', {
method: 'GET',
})
}

28
app/app.vue Normal file
View File

@ -0,0 +1,28 @@
<script setup lang="ts">
import type { ConfigProviderTheme } from 'vant'
import useKeepalive from '~/composables/keepalive'
import { appName } from '~/constants'
useHead({
title: appName,
})
const color = useColorMode()
const mode = computed(() => {
return color.value as ConfigProviderTheme
})
const keepAliveRouteNames = computed(() => {
return useKeepalive().routeCaches as string[]
})
</script>
<template>
<VanConfigProvider :theme="mode">
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
<NuxtLayout>
<NuxtPage :keepalive="{ include: keepAliveRouteNames }" />
</NuxtLayout>
</VanConfigProvider>
</template>

View File

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

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { useAppFooterRouteNames as routeWhiteList } from '~/config'
const route = useRoute()
const router = useRouter()
function onBack() {
if (window.history.state.back)
history.back()
else
router.replace('/')
}
const { t } = useI18n()
const title = computed(() => {
if (!route.meta)
return ''
return route.meta.i18n ? t(route.meta.i18n) : (route.meta.title || '')
})
const showLeftArrow = computed(() => route.name && routeWhiteList.includes(route.name))
</script>
<template>
<VanNavBar
:title="title"
:left-arrow="!showLeftArrow"
placeholder clickable fixed
@click-left="onBack"
/>
</template>

View File

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

View File

@ -0,0 +1,24 @@
import type { RouteLocationNormalized, RouteRecordName } from 'vue-router'
import { defineStore } from 'pinia'
const useKeepalive = defineStore('keepalive', () => {
const routeCaches = ref<RouteRecordName[]>([])
const addRoute = (route: RouteLocationNormalized) => {
if (!route.name)
return
if (routeCaches.value.includes(route.name))
return
if (route?.meta?.keepalive)
routeCaches.value.push(route.name)
}
return {
routeCaches,
addRoute,
}
})
export default useKeepalive

6
app/config/index.ts Normal file
View File

@ -0,0 +1,6 @@
import type { RouteRecordName } from 'vue-router'
/**
* Use the AppFooter routing whitelist
*/
export const useAppFooterRouteNames: RouteRecordName[] = ['index', 'profile']

2
app/constants/index.ts Normal file
View File

@ -0,0 +1,2 @@
export const appName = 'nuxt-vant-mobile'
export const appDescription = 'Nuxt H5 Starter Template'

23
app/layouts/404.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
const router = useRouter()
function onBack() {
if (window.history.state.back)
history.back()
else
router.replace('/')
}
</script>
<template>
<main text="center gray-300 dark:gray-200 18" py="20">
<van-icon name="warn-o" size="3em" />
<slot />
<div class="mt-10">
<button van-haptics-feedback btn m="3 t8" @click="onBack">
{{ $t('error_page.back_btn') }}
</button>
</div>
</main>
</template>

15
app/layouts/README.md Normal file
View File

@ -0,0 +1,15 @@
# Layouts
Vue components in this dir are used as layouts.
By default, `default.vue` will be used unless an alternative is specified in the route meta.
```vue
<script setup lang="ts">
definePageMeta({
layout: 'home',
})
</script>
```
Learn more on <https://nuxt.com/docs/guide/directory-structure/layouts>

11
app/layouts/default.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<main class="flex flex-col min-h-svh">
<AppHeader class="h-[var(--van-nav-bar-height)]" />
<div class="flex-1 p-16 pb-[var(--van-nav-bar-height)]">
<slot />
</div>
<AppFooter />
</main>
</template>

View File

@ -0,0 +1,7 @@
import type { RouteLocationNormalized } from 'vue-router'
import useKeepalive from '~/composables/keepalive'
export default defineNuxtRouteMiddleware((to: RouteLocationNormalized) => {
if (to.meta && to.meta.keepalive)
useKeepalive().addRoute(to)
})

9
app/pages/[...all].vue Normal file
View File

@ -0,0 +1,9 @@
<script setup lang="ts">
definePageMeta({
layout: '404',
})
</script>
<template>
<div> {{ $t('error_page.txt') }} </div>
</template>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import useCounter from '~/composables/counter'
definePageMeta({
title: '🍍 持久化 Pinia 状态',
i18n: 'menu.persistPiniaState',
})
const counter = useCounter()
function add() {
counter.increment()
}
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
Hello, Pinia!
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('counter_page.label') }}
</p>
<p class="mt-10">
{{ $t('counter_page.label_num') }}:
<strong class="text-green-500"> {{ counter.count }} </strong>
</p>
<button class="mt-10 btn" @click="add">
{{ $t('counter_page.btn_add') }}
</button>
</div>
</template>

95
app/pages/index.vue Normal file
View File

@ -0,0 +1,95 @@
<script setup lang="ts">
import type { LocaleObject } from '@nuxtjs/i18n'
import type { PickerColumn } from 'vant'
import type { ComputedRef } from 'vue'
import { Locale } from 'vant'
definePageMeta({
layout: 'default',
title: '主页',
i18n: 'menu.home',
})
const color = useColorMode()
useHead({
meta: [{
id: 'theme-color',
name: 'theme-color',
content: () => color.value === 'dark' ? '#222222' : '#ffffff',
}],
})
const checked = computed({
get: () => color.value === 'dark',
set: (val: boolean) => {
color.preference = val ? 'dark' : 'light'
},
})
const { setLocale, t } = useI18n()
const i18n = useNuxtApp().$i18n
const showLanguagePicker = ref(false)
const languageValues = ref<string[]>([i18n.locale.value])
const { locales } = useI18n() as { locales: ComputedRef<LocaleObject[]> }
const menus = computed(() => [
{ title: t('menu.unocssExample'), route: 'unocss' },
{ title: t('menu.keepAlive'), route: 'keepalive' },
{ title: t('menu.persistPiniaState'), route: 'counter' },
{ title: t('menu.fetch'), route: 'prose' },
{ title: t('menu.404Demo'), route: 'unknown' },
])
function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
const lang = event.selectedOptions[0]?.code
setLocale(lang)
Locale.use(lang)
localStorage.setItem('lang', lang)
showLanguagePicker.value = false
}
</script>
<template>
<div>
<VanCellGroup inset>
<VanCell :title="$t('menu.darkMode')" center>
<template #right-icon>
<ClientOnly>
<VanSwitch
v-model="checked"
size="20px"
aria-label="on/off Dark Mode"
/>
</ClientOnly>
</template>
</VanCell>
<VanCell
:title="$t('menu.language')"
:value="locales.find(i => i.code === i18n.locale.value)?.name"
is-link
@click="showLanguagePicker = true"
/>
<template v-for="item in menus" :key="item.route">
<VanCell :title="item.title" :to="item.route" is-link />
</template>
</VanCellGroup>
<van-popup v-model:show="showLanguagePicker" position="bottom">
<van-picker
v-model="languageValues"
:columns="locales"
:columns-field-names="{ text: 'name', value: 'code' }"
@confirm="onLanguageConfirm"
@cancel="showLanguagePicker = false"
/>
</van-popup>
</div>
</template>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
defineOptions({
name: 'Keepalive',
})
definePageMeta({
name: 'Keepalive',
keepalive: true,
title: '🧡 KeepAlive',
i18n: 'menu.keepAlive',
})
const value = ref(1)
</script>
<template>
<div>
<p> {{ $t('keepalive_page.label') }} </p>
<van-stepper v-model="value" class="mt-10" />
</div>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
definePageMeta({
layout: 'default',
title: '我的',
i18n: 'menu.profile',
})
</script>
<template>
<div mx-auto mb-60 pt-15 text-center text-16 text-dark dark:text-white>
{{ $t('profile_page.txt') }}
</div>
</template>

42
app/pages/prose/index.vue Normal file
View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import { useProseStore } from '~/stores/prose'
definePageMeta({
layout: 'default',
title: '随笔',
i18n: 'menu.fetch',
})
const proseStore = useProseStore()
function fetch() {
proseStore.fetchProse()
}
function clear() {
proseStore.clearProse()
}
</script>
<template>
<div>
<div class="h-300 flex items-center justify-center rounded-15 bg-white p-16 dark:bg-[--van-background-2]">
<div v-if="proseStore.prose" class="text-16 leading-26">
{{ proseStore.prose }}
</div>
<ClientOnly v-else>
<van-empty :description="$t('prose_page.btn_empty_desc')" />
</ClientOnly>
</div>
<van-space class="m-10" direction="vertical" fill>
<van-button type="primary" round block @click="fetch">
{{ $t('prose_page.btn_fetch') }}
</van-button>
<van-button type="default" round block @click="clear">
{{ $t('prose_page.btn_clear') }}
</van-button>
</van-space>
</div>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
definePageMeta({
title: '🎨 Unocss 示例',
i18n: 'menu.unocssExample',
})
</script>
<template>
<div>
<h1 class="text-6xl color-pink font-semibold">
{{ $t('unocss_page.hello', ['Unocss!']) }}
</h1>
<p class="mt-10 text-gray-700 dark:text-white">
{{ $t('unocss_page.desc') }}
</p>
<button class="mt-10 btn">
{{ $t('unocss_page.btn_txt') }}
</button>
</div>
</template>

5
app/plugins/http.ts Normal file
View File

@ -0,0 +1,5 @@
import { setupHttp } from '~/api/http'
export default defineNuxtPlugin(() => {
setupHttp()
})

26
app/plugins/i18n.ts Normal file
View File

@ -0,0 +1,26 @@
import type { Locale as TypeLocale } from '#i18n'
import { Locale } from 'vant'
import enUS from 'vant/es/locale/lang/en-US'
import zhCN from 'vant/es/locale/lang/zh-CN'
export default defineNuxtPlugin(() => {
// 载入 vant 语言包
Locale.use('zh-CN', zhCN)
Locale.use('en-US', enUS)
if (import.meta.client) {
const i18n = useNuxtApp().$i18n
const { setLocale } = i18n
const lang = localStorage.getItem('lang')
if (lang) {
setLocale(lang as TypeLocale)
Locale.use(lang)
}
else {
setLocale(i18n.locale.value)
Locale.use(i18n.locale.value)
}
}
})

33
app/stores/prose.ts Normal file
View File

@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { getProse } from '~/api/prose'
export const useProseStore = defineStore(
'prose',
() => {
const prose = ref<string>('')
function initProse(val: string) {
if (!prose.value) {
prose.value = ''
}
prose.value = val
}
function clearProse() {
prose.value = ''
}
async function fetchProse() {
const res = await getProse()
initProse(res.result)
}
return {
prose,
initProse,
clearProse,
fetchProse,
}
},
)

View File

@ -0,0 +1,4 @@
:root:root {
--van-primary-color: var(--c-primary);
--van-cell-group-inset-padding: 0;
}

14
app/styles/global.css Normal file
View File

@ -0,0 +1,14 @@
#__nuxt {
margin: 0;
padding: 0;
}
html {
background: var(--van-gray-1);
color-scheme: light;
}
html.dark {
background: #222;
color-scheme: dark;
}

15
app/styles/vars.css Normal file
View File

@ -0,0 +1,15 @@
:root {
--c-primary: rgb(var(--c-primary-500));
--c-primary-active: rgb(var(--c-primary-600));
/* main color ratio */
--c-primary-100: 217 251 232;
--c-primary-200: 179 245 209;
--c-primary-300: 117 237 174;
--c-primary-400: 0 220 130;
--c-primary-500: 0 193 106;
--c-primary-600: 0 161 85;
--c-primary-700: 0 127 69;
--c-primary-800: 1 101 56;
--c-primary-900: 10 83 49;
}

10
app/types/vue-router.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
declare module 'vue-router' {
interface RouteMeta {
/** page title */
title?: string
/** i18n key */
i18n?: string
}
}
export {}

10
app/utils/preload.ts Normal file
View File

@ -0,0 +1,10 @@
export default function preload() {
return `
;(function() {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const setting = localStorage.getItem('nuxt-color-mode') || 'auto';
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('van-theme-dark', true);
})()
`
}

32
commitlint.config.ts Normal file
View File

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

9
eslint.config.js Normal file
View File

@ -0,0 +1,9 @@
import antfu from '@antfu/eslint-config'
import nuxt from './.nuxt/eslint.config.mjs'
export default nuxt(
antfu({
unocss: true,
formatters: true,
}),
)

11
i18n/i18n.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { currentLocales } from './i18n'
export default defineI18nConfig(() => {
return {
legacy: false,
availableLocales: currentLocales.map(l => l.code),
fallbackLocale: 'zh-CN',
fallbackWarn: true,
missingWarn: true,
}
})

26
i18n/i18n.ts Normal file
View File

@ -0,0 +1,26 @@
import type { LocaleObject } from '@nuxtjs/i18n'
const locales: LocaleObject[] = [
{
code: 'zh-CN',
file: 'zh-CN.json',
name: '简体中文',
},
{
code: 'en-US',
file: 'en-US.json',
name: 'English',
},
]
function buildLocales() {
const useLocales = Object.values(locales).reduce((acc, data) => {
acc.push(data)
return acc
}, <LocaleObject[]>[])
return useLocales.sort((a, b) => a.code.localeCompare(b.code))
}
export const currentLocales = buildLocales()

42
i18n/locales/en-US.json Normal file
View File

@ -0,0 +1,42 @@
{
"menu": {
"home": "Home",
"profile": "Profile",
"darkMode": "🌗 Dark Mode",
"language": "📚 Language",
"404Demo": "🙅 Page 404 Demo",
"unocssExample": "🎨 Unocss example",
"keepAlive": "🧡 KeepAlive Demo",
"persistPiniaState": "💾 Persist Pinia State",
"fetch": "🏄 Network Request"
},
"tabbar": {
"home": "Home",
"profile": "Profile"
},
"unocss_page": {
"hello": "Hello {0}",
"desc": "This is a simple example of Unocss in action.",
"btn_txt": "Button"
},
"error_page": {
"back_btn": "Back",
"txt": "Not found"
},
"profile_page": {
"txt": "WIP"
},
"keepalive_page": {
"label": "The current component will be cached"
},
"counter_page": {
"label": "This is a simple example of persisting Pinia state. To verify its effectiveness, you can refresh the interface and observe it.",
"label_num": "Number",
"btn_add": "Add"
},
"prose_page": {
"btn_fetch": "Fetch",
"btn_clear": "Clear",
"btn_empty_desc": "No data"
}
}

42
i18n/locales/zh-CN.json Normal file
View File

@ -0,0 +1,42 @@
{
"menu": {
"home": "主页",
"profile": "我的",
"darkMode": "🌗 暗黑模式",
"language": "📚 语言",
"404Demo": "🙅 404页 演示",
"unocssExample": "🎨 Unocss 示例",
"keepAlive": "🧡 KeepAlive 演示",
"persistPiniaState": "💾 持久化 Pinia 状态",
"fetch": "🏄 网络请求"
},
"tabbar": {
"home": "主页",
"profile": "我的"
},
"unocss_page": {
"hello": "你好 {0}",
"desc": "这是 unocss 一个简单例子。",
"btn_txt": "按钮"
},
"error_page": {
"back_btn": "返回",
"txt": "没有找到"
},
"profile_page": {
"txt": "未完成"
},
"keepalive_page": {
"label": "当前组件将会被缓存"
},
"counter_page": {
"label": "这是一个简单的持久化 Pinia 状态的例子。为了验证其有效性,你可以刷新界面并观察它。",
"label_num": "数字",
"btn_add": "增加"
},
"prose_page": {
"btn_fetch": "拉取",
"btn_clear": "清空",
"btn_empty_desc": "暂无数据"
}
}

129
nuxt.config.ts Normal file
View File

@ -0,0 +1,129 @@
import process from 'node:process'
import { appDescription } from './app/constants/index'
import preload from './app/utils/preload'
import { currentLocales } from './i18n/i18n'
export default defineNuxtConfig({
modules: [
'@vant/nuxt',
'@unocss/nuxt',
'@nuxtjs/color-mode',
'@nuxt/eslint',
'@nuxtjs/i18n',
'@pinia/nuxt',
'pinia-plugin-persistedstate/nuxt',
],
runtimeConfig: {
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE,
},
},
css: [
'@unocss/reset/tailwind.css',
'./app/styles/vars.css',
'./app/styles/global.css',
'./app/styles/default-theme.css',
],
postcss: {
plugins: {
'autoprefixer': {},
// https://github.com/wswmsword/postcss-mobile-forever
'postcss-mobile-forever': {
appSelector: '#__nuxt',
viewportWidth: 375,
maxDisplayWidth: 600,
// devtools excluded
exclude: /@nuxt/,
border: true,
rootContainingBlockSelectorList: [
'van-tabbar',
'van-popup',
],
},
},
},
colorMode: {
classSuffix: '',
preference: 'system',
fallback: 'light',
storageKey: 'nuxt-color-mode',
},
i18n: {
locales: currentLocales,
lazy: true,
strategy: 'no_prefix',
detectBrowserLanguage: {
useCookie: true,
},
langDir: 'locales',
defaultLocale: 'zh-CN',
vueI18n: './i18n/i18n.config.ts',
},
app: {
head: {
viewport: 'width=device-width,initial-scale=1,viewport-fit=cover',
link: [
{ rel: 'icon', href: '/favicon.ico', sizes: 'any' },
],
meta: [
{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' },
{ name: 'description', content: appDescription },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
{ name: 'theme-color', media: '(prefers-color-scheme: light)', content: '#ffffff' },
{ name: 'theme-color', media: '(prefers-color-scheme: dark)', content: '#222222' },
],
script: [
{ innerHTML: preload(), type: 'text/javascript', tagPosition: 'head' },
],
},
},
vite: {
build: {
target: 'esnext',
},
optimizeDeps: {
include: [
'@intlify/core-base',
'@intlify/shared',
'is-https',
],
},
},
experimental: {
typedPages: true,
},
devtools: {
enabled: true,
},
typescript: {
shim: false,
},
features: {
// For UnoCSS
inlineStyles: false,
},
eslint: {
config: {
standalone: false,
},
},
future: {
compatibilityVersion: 4,
},
compatibilityDate: '2024-09-24',
})

61
package.json Normal file
View File

@ -0,0 +1,61 @@
{
"name": "nuxt-vant-mobile",
"type": "module",
"version": "0.3.0",
"packageManager": "pnpm@9.15.1",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"typecheck": "vue-tsc --noEmit",
"release": "bumpp --commit --push --tag"
},
"dependencies": {
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "^9.1.1",
"nuxt": "^3.15.0",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@antfu/eslint-config": "^3.12.1",
"@iconify-json/carbon": "^1.2.5",
"@nuxt/eslint": "^0.7.4",
"@pinia/nuxt": "^0.9.0",
"@unocss/eslint-plugin": "0.65.2",
"@unocss/nuxt": "0.65.2",
"@unocss/preset-rem-to-px": "0.65.2",
"@vant/nuxt": "^1.0.6",
"bumpp": "^9.9.2",
"pinia": "^2.3.0",
"postcss-mobile-forever": "^4.3.1",
"typescript": "~5.7.2",
"vant": "^4.9.15"
},
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"meow": "^12.x",
"@intlify/shared": "^11.0.0"
}
},
"allowedDeprecatedVersions": {
"glob": "*",
"are-we-there-yet": "2",
"gauge": "3",
"inflight": "1",
"npmlog": "5",
"rimraf": "3"
}
},
"resolutions": {
"vite": "^6.0.5"
},
"browserslist": [
"defaults"
]
}

10523
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
server/tsconfig.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

4
tsconfig.json Normal file
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

45
uno.config.ts Normal file
View File

@ -0,0 +1,45 @@
import presetRemToPx from '@unocss/preset-rem-to-px'
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
// https://unocss.dev/guide/config-file
export default defineConfig({
shortcuts: [
// shortcuts to multiple utilities
['btn', 'px-6 py-3 rounded-3 inline-block bg-primary text-white cursor-pointer hover:bg-primary-hover disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
],
presets: [
presetUno(),
presetAttributify(),
presetIcons(),
presetTypography(),
presetWebFonts(),
presetRemToPx({
baseFontSize: 4,
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
theme: {
colors: {
primary: {
DEFAULT: 'var(--c-primary)',
hover: 'var(--c-primary-active)',
},
},
},
})