refactor: remove i18n dependency and hardcode strings in components
- Updated ChatInput.vue to remove i18n and handle model value updates directly. - Refactored NavBar.vue to use a static title map instead of i18n. - Simplified TabBar.vue by replacing i18n calls with hardcoded titles. - Removed i18n usage in various pages (charts, counter, forgot-password, index, keepalive, llm-chat, login, mock, profile, register, scroll-cache, settings, unocss). - Deleted localization files (en-US.json, zh-CN.json) and i18n utility functions. - Updated constants to provide static app name and description. - Adjusted page titles in set-page-title.ts to use static names. - Cleaned up TypeScript types by removing unused i18n types.
This commit is contained in:
parent
3f8689f7ad
commit
118d10208b
|
|
@ -14,7 +14,6 @@ import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import Sitemap from 'vite-plugin-sitemap'
|
import Sitemap from 'vite-plugin-sitemap'
|
||||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
|
||||||
import { loadEnv } from 'vite'
|
import { loadEnv } from 'vite'
|
||||||
import { createViteVConsole } from './vconsole'
|
import { createViteVConsole } from './vconsole'
|
||||||
|
|
||||||
|
|
@ -60,8 +59,6 @@ export function createVitePlugins(mode: string) {
|
||||||
VueRouterAutoImports,
|
VueRouterAutoImports,
|
||||||
{
|
{
|
||||||
'vue-router/auto': ['useLink'],
|
'vue-router/auto': ['useLink'],
|
||||||
'@/utils/i18n': ['i18n', 'locale'],
|
|
||||||
'vue-i18n': ['useI18n'],
|
|
||||||
},
|
},
|
||||||
unheadVueComposablesImports,
|
unheadVueComposablesImports,
|
||||||
],
|
],
|
||||||
|
|
@ -72,12 +69,6 @@ export function createVitePlugins(mode: string) {
|
||||||
resolvers: [VantResolver()],
|
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({
|
legacy({
|
||||||
targets: ['defaults', 'not IE 11'],
|
targets: ['defaults', 'not IE 11'],
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -34,13 +34,11 @@
|
||||||
"vant": "^4.9.21",
|
"vant": "^4.9.21",
|
||||||
"vconsole": "^3.15.1",
|
"vconsole": "^3.15.1",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-i18n": "^11.2.2",
|
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "6.6.1",
|
"@antfu/eslint-config": "6.6.1",
|
||||||
"@iconify-json/carbon": "^1.2.15",
|
"@iconify-json/carbon": "^1.2.15",
|
||||||
"@intlify/unplugin-vue-i18n": "^11.0.1",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^24.10.2",
|
"@types/node": "^24.10.2",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "^0.2.3",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouteCacheStore } from '@/stores'
|
import { useRouteCacheStore } from '@/stores'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
title: () => t('app.name'),
|
title: 'Vue3 Vant Mobile',
|
||||||
meta: [
|
meta: [
|
||||||
{
|
{
|
||||||
name: 'description',
|
name: 'description',
|
||||||
content: () => t('app.description'),
|
content: 'An mobile web apps template based on the Vue 3 ecosystem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'theme-color',
|
name: 'theme-color',
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,16 @@ import type { ChatBubbleProps } from '@/types/chat'
|
||||||
const props = defineProps<ChatBubbleProps>()
|
const props = defineProps<ChatBubbleProps>()
|
||||||
|
|
||||||
const messageClasses = computed(() => {
|
const messageClasses = computed(() => {
|
||||||
const base = 'rounded-2xl p-3 max-w-80'
|
const base = 'rounded-2xl p-3 max-w-80 break-words'
|
||||||
const sender = props.message.sender === 'user'
|
const sender = props.message.sender === 'user'
|
||||||
? 'bg-blue-100 dark:bg-blue-900 ml-auto text-right'
|
? 'bg-blue-100 dark:bg-blue-800'
|
||||||
: 'bg-gray-100 dark:bg-gray-800 mr-auto text-left'
|
: 'bg-gray-100 dark:bg-gray-700'
|
||||||
return `${base} ${sender}`
|
return `${base} ${sender}`
|
||||||
})
|
})
|
||||||
|
|
||||||
const formattedTime = computed(() => {
|
const formattedTime = computed(() => {
|
||||||
if (!props.message.timestamp) return ''
|
if (!props.message.timestamp)
|
||||||
|
return ''
|
||||||
const date = typeof props.message.timestamp === 'string'
|
const date = typeof props.message.timestamp === 'string'
|
||||||
? new Date(props.message.timestamp)
|
? new Date(props.message.timestamp)
|
||||||
: props.message.timestamp
|
: props.message.timestamp
|
||||||
|
|
@ -21,37 +22,57 @@ const formattedTime = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-end gap-2 mb-4" :class="{ 'flex-row-reverse': message.sender === 'user' }">
|
<div class="mb-4 flex gap-2" :class="message.sender === 'user' ? 'justify-end' : 'justify-start'">
|
||||||
<!-- Avatar -->
|
<!-- Avatar for AI messages (left side) -->
|
||||||
|
<div v-if="message.sender === 'ai'">
|
||||||
<van-image
|
<van-image
|
||||||
v-if="avatar"
|
v-if="avatar"
|
||||||
:src="avatar"
|
:src="avatar"
|
||||||
class="w-8 h-8 rounded-full flex-shrink-0"
|
class="rounded-full flex-shrink-0 h-8 w-8"
|
||||||
round
|
round
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center"
|
<div v-else class="text-gray-600 rounded-full bg-gray-100 flex flex-shrink-0 h-8 w-8 items-center justify-center dark:text-gray-300 dark:bg-gray-700">
|
||||||
:class="message.sender === 'user' ? 'bg-blue-100 dark:bg-blue-900' : 'bg-gray-100 dark:bg-gray-800'">
|
<van-icon name="robot-o" class="text-lg" />
|
||||||
<van-icon :name="message.sender === 'user' ? 'user-circle-o' : 'robot-o'" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message bubble -->
|
<!-- Message bubble and timestamp -->
|
||||||
<div class="flex flex-col" :class="{ 'items-end': message.sender === 'user', 'items-start': message.sender === 'ai' }">
|
<div class="flex flex-col max-w-[80%]">
|
||||||
<div :class="messageClasses">
|
<div :class="messageClasses">
|
||||||
<p class="text-sm">{{ message.content }}</p>
|
<p class="text-sm">
|
||||||
|
{{ message.content }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<!-- Status indicator -->
|
<!-- Status indicator -->
|
||||||
<div v-if="message.status === 'sending'" class="mt-1">
|
<div v-if="message.status === 'sending'" class="mt-1">
|
||||||
<van-icon name="clock-o" class="animate-pulse" />
|
<van-icon name="clock-o" class="animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="message.status === 'error'" class="mt-1 text-red-500">
|
<div v-else-if="message.status === 'error'" class="text-red-500 mt-1">
|
||||||
<van-icon name="warning-o" />
|
<van-icon name="warning-o" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timestamp -->
|
<!-- Timestamp -->
|
||||||
<div v-if="showTimestamp && formattedTime" class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
<div
|
||||||
|
v-if="showTimestamp && formattedTime"
|
||||||
|
class="text-xs text-gray-500 mt-1 dark:text-gray-400"
|
||||||
|
:class="message.sender === 'user' ? 'text-right' : 'text-left'"
|
||||||
|
>
|
||||||
{{ formattedTime }}
|
{{ formattedTime }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Avatar for user messages (right side) -->
|
||||||
|
<div v-if="message.sender === 'user'">
|
||||||
|
<van-image
|
||||||
|
v-if="avatar"
|
||||||
|
:src="avatar"
|
||||||
|
class="rounded-full flex-shrink-0 h-8 w-8"
|
||||||
|
round
|
||||||
|
/>
|
||||||
|
<div v-else class="text-blue-600 rounded-full bg-blue-100 flex flex-shrink-0 h-8 w-8 items-center justify-center dark:text-blue-300 dark:bg-blue-800">
|
||||||
|
<van-icon name="user-circle-o" class="text-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ChatInputProps, ChatInputEmits } from '@/types/chat'
|
import type { ChatInputEmits, ChatInputProps } from '@/types/chat'
|
||||||
|
|
||||||
const props = defineProps<ChatInputProps>()
|
const props = defineProps<ChatInputProps>()
|
||||||
const emit = defineEmits<ChatInputEmits>()
|
const emit = defineEmits<ChatInputEmits>()
|
||||||
|
|
@ -7,11 +7,6 @@ const emit = defineEmits<ChatInputEmits>()
|
||||||
const inputRef = ref<HTMLTextAreaElement>()
|
const inputRef = ref<HTMLTextAreaElement>()
|
||||||
const isComposing = ref(false)
|
const isComposing = ref(false)
|
||||||
|
|
||||||
function handleInput(e: Event) {
|
|
||||||
const target = e.target as HTMLInputElement
|
|
||||||
emit('update:modelValue', target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
emit('keydown', e)
|
emit('keydown', e)
|
||||||
|
|
||||||
|
|
@ -38,10 +33,10 @@ defineExpose({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center gap-2 p-3 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
<div class="p-3 border-t border-gray-200 bg-white flex gap-2 items-center dark:border-gray-700 dark:bg-gray-900">
|
||||||
<van-field
|
<van-field
|
||||||
ref="inputRef"
|
ref="inputRef"
|
||||||
v-model="props.modelValue"
|
:model-value="props.modelValue"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
rows="1"
|
rows="1"
|
||||||
autosize
|
autosize
|
||||||
|
|
@ -49,7 +44,7 @@ defineExpose({
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:readonly="loading"
|
:readonly="loading"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
@input="handleInput"
|
@update:model-value="(value) => emit('update:modelValue', value)"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
@compositionstart="isComposing = true"
|
@compositionstart="isComposing = true"
|
||||||
@compositionend="isComposing = false"
|
@compositionend="isComposing = false"
|
||||||
|
|
@ -60,8 +55,8 @@ defineExpose({
|
||||||
round
|
round
|
||||||
:disabled="!props.modelValue.trim() || disabled"
|
:disabled="!props.modelValue.trim() || disabled"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="handleSend"
|
|
||||||
class="flex-shrink-0"
|
class="flex-shrink-0"
|
||||||
|
@click="handleSend"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<van-icon name="send" />
|
<van-icon name="send" />
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,33 @@ import { rootRouteList } from '@/config/routes'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useI18n()
|
|
||||||
|
const routeTitleMap: Record<string, string> = {
|
||||||
|
'home': 'Home',
|
||||||
|
'profile': 'Profile',
|
||||||
|
'mock': 'Mock',
|
||||||
|
'charts': 'Charts',
|
||||||
|
'unocss': 'UnoCSS',
|
||||||
|
'counter': 'Counter',
|
||||||
|
'keepalive': 'KeepAlive',
|
||||||
|
'scroll-cache': 'ScrollCache',
|
||||||
|
'login': 'Login',
|
||||||
|
'register': 'Register',
|
||||||
|
'forgot-password': 'Forgot Password',
|
||||||
|
'settings': 'Settings',
|
||||||
|
'llm-chat': 'AI Chat',
|
||||||
|
'unknown': '404',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page title
|
* Get page title
|
||||||
* Located in src/locales/json
|
|
||||||
*/
|
*/
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
if (route.name) {
|
if (route.name) {
|
||||||
return t(`navbar.${route.name}`)
|
return routeTitleMap[route.name.toString()] || route.name.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
return t('navbar.Undefined')
|
return 'Undefined'
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ const show = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<van-tabbar v-if="show" v-model="active" route placeholder>
|
<van-tabbar v-if="show" v-model="active" route placeholder>
|
||||||
<van-tabbar-item replace to="/">
|
<van-tabbar-item replace to="/">
|
||||||
{{ $t('tabbar.home') }}
|
Home
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div class="i-carbon:home" />
|
<div class="i-carbon:home" />
|
||||||
</template>
|
</template>
|
||||||
</van-tabbar-item>
|
</van-tabbar-item>
|
||||||
<van-tabbar-item replace to="/profile">
|
<van-tabbar-item replace to="/profile">
|
||||||
{{ $t('tabbar.profile') }}
|
Profile
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div class="i-carbon:user" />
|
<div class="i-carbon:user" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,2 @@
|
||||||
import { i18n } from '@/utils/i18n'
|
export const appName = () => 'Vue3 Vant Mobile'
|
||||||
|
export const appDescription = () => 'An mobile web apps template based on the Vue 3 ecosystem'
|
||||||
export const appName = () => i18n.global.t('app.name')
|
|
||||||
export const appDescription = () => i18n.global.t('app.description')
|
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
{
|
|
||||||
"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",
|
|
||||||
"AIChat": "🤖 AI Chat",
|
|
||||||
"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?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
{
|
|
||||||
"app": {
|
|
||||||
"name": "Vue3 移动端模板",
|
|
||||||
"description": "一个基于 Vue 3 生态系统的移动 web 应用模板"
|
|
||||||
},
|
|
||||||
|
|
||||||
"navbar": {
|
|
||||||
"Home": "主页",
|
|
||||||
"Profile": "我的",
|
|
||||||
"Mock": "🗂️ Mock",
|
|
||||||
"Charts": "📊 图表",
|
|
||||||
"UnoCSS": "⚡ UnoCSS",
|
|
||||||
"Counter": "🍍 状态持久化",
|
|
||||||
"KeepAlive": "♻️ 页面缓存",
|
|
||||||
"ScrollCache": "📍 滚动缓存",
|
|
||||||
"Login": "🧑💻 登录",
|
|
||||||
"Register": "🧑💻 注册",
|
|
||||||
"ForgotPassword": "❓ 忘记密码",
|
|
||||||
"Settings": "⚙️ 设置",
|
|
||||||
"AIChat": "🤖 AI聊天",
|
|
||||||
"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": "确认退出?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ import pinia from '@/stores'
|
||||||
import 'virtual:uno.css'
|
import 'virtual:uno.css'
|
||||||
import '@/styles/app.less'
|
import '@/styles/app.less'
|
||||||
import '@/styles/var.less'
|
import '@/styles/var.less'
|
||||||
import { i18n } from '@/utils/i18n'
|
|
||||||
|
|
||||||
// Vant 桌面端适配
|
// Vant 桌面端适配
|
||||||
import '@vant/touch-emulator'
|
import '@vant/touch-emulator'
|
||||||
|
|
@ -28,6 +27,5 @@ const head = createHead()
|
||||||
app.use(head)
|
app.use(head)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(pinia)
|
app.use(pinia)
|
||||||
app.use(i18n)
|
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const barOption = ref({
|
const barOption = ref({
|
||||||
title: {},
|
title: {},
|
||||||
tooltip: {},
|
tooltip: {},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
data: [t('charts.January'), t('charts.February'), t('charts.March'), t('charts.April'), t('charts.May'), t('charts.June')],
|
data: ['January', 'February', 'March', 'April', 'May', 'June'],
|
||||||
},
|
},
|
||||||
yAxis: {},
|
yAxis: {},
|
||||||
series: [
|
series: [
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const { counter } = storeToRefs(counterStore)
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-sm space-y-2">
|
<div class="text-sm space-y-2">
|
||||||
<p> {{ $t('counter.description') }}</p>
|
<p>Counter Demo</p>
|
||||||
<van-stepper v-model="counter" />
|
<van-stepper v-model="counter" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { showNotify } from 'vant'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import vw from '@/utils/inline-px-to-vw'
|
import vw from '@/utils/inline-px-to-vw'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
@ -21,17 +20,17 @@ const validatorPassword = (val: string) => val === postData.password
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
email: [
|
email: [
|
||||||
{ required: true, message: t('forgotPassword.pleaseEnterEmail') },
|
{ required: true, message: 'Please enter email' },
|
||||||
],
|
],
|
||||||
code: [
|
code: [
|
||||||
{ required: true, message: t('forgotPassword.pleaseEnterCode') },
|
{ required: true, message: 'Please enter code' },
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: t('forgotPassword.pleaseEnterPassword') },
|
{ required: true, message: 'Please enter password' },
|
||||||
],
|
],
|
||||||
confirmPassword: [
|
confirmPassword: [
|
||||||
{ required: true, message: t('forgotPassword.pleaseEnterConfirmPassword') },
|
{ required: true, message: 'Please enter password again' },
|
||||||
{ required: true, validator: validatorPassword, message: t('forgotPassword.passwordsDoNotMatch') },
|
{ required: true, validator: validatorPassword, message: 'Passwords do not match' },
|
||||||
] as FieldRule[],
|
] as FieldRule[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -42,7 +41,7 @@ async function reset() {
|
||||||
const res = await userStore.reset()
|
const res = await userStore.reset()
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
showNotify({ type: 'success', message: t('forgotPassword.passwordResetSuccess') })
|
showNotify({ type: 'success', message: 'Password reset succeeded' })
|
||||||
router.push({ name: 'Login' })
|
router.push({ name: 'Login' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -54,19 +53,19 @@ async function reset() {
|
||||||
const isGettingCode = ref(false)
|
const isGettingCode = ref(false)
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
return isGettingCode.value ? t('forgotPassword.gettingCode') : t('forgotPassword.getCode')
|
return isGettingCode.value ? 'Getting code' : 'Get code'
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getCode() {
|
async function getCode() {
|
||||||
if (!postData.email) {
|
if (!postData.email) {
|
||||||
showNotify({ type: 'warning', message: t('forgotPassword.pleaseEnterEmail') })
|
showNotify({ type: 'warning', message: 'Please enter email' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isGettingCode.value = true
|
isGettingCode.value = true
|
||||||
const res = await userStore.getCode()
|
const res = await userStore.getCode()
|
||||||
if (res.code === 0)
|
if (res.code === 0)
|
||||||
showNotify({ type: 'success', message: `${t('forgotPassword.sendCodeSuccess')}: ${res.result}` })
|
showNotify({ type: 'success', message: `Sent, the code is: ${res.result}` })
|
||||||
|
|
||||||
isGettingCode.value = false
|
isGettingCode.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +79,7 @@ async function getCode() {
|
||||||
v-model.trim="postData.email"
|
v-model.trim="postData.email"
|
||||||
:rules="rules.email"
|
:rules="rules.email"
|
||||||
name="email"
|
name="email"
|
||||||
:placeholder="$t('forgotPassword.email')"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -89,7 +88,7 @@ async function getCode() {
|
||||||
v-model.trim="postData.code"
|
v-model.trim="postData.code"
|
||||||
:rules="rules.code"
|
:rules="rules.code"
|
||||||
name="code"
|
name="code"
|
||||||
:placeholder="$t('forgotPassword.code')"
|
placeholder="Code"
|
||||||
>
|
>
|
||||||
<template #button>
|
<template #button>
|
||||||
<van-button size="small" type="primary" plain @click="getCode">
|
<van-button size="small" type="primary" plain @click="getCode">
|
||||||
|
|
@ -105,7 +104,7 @@ async function getCode() {
|
||||||
type="password"
|
type="password"
|
||||||
:rules="rules.password"
|
:rules="rules.password"
|
||||||
name="password"
|
name="password"
|
||||||
:placeholder="$t('forgotPassword.password')"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -115,7 +114,7 @@ async function getCode() {
|
||||||
type="password"
|
type="password"
|
||||||
:rules="rules.confirmPassword"
|
:rules="rules.confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
:placeholder="$t('forgotPassword.confirmPassword')"
|
placeholder="Password again"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -126,13 +125,13 @@ async function getCode() {
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
round block
|
round block
|
||||||
>
|
>
|
||||||
{{ $t('forgotPassword.confirm') }}
|
Confirm
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|
||||||
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
|
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
|
||||||
{{ $t('forgotPassword.backToLogin') }}
|
Back to login
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,24 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { PickerColumn } from 'vant'
|
|
||||||
import { languageColumns, locale } from '@/utils/i18n'
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
const checked = computed({
|
const checked = computed({
|
||||||
get: () => isDark.value,
|
get: () => isDark.value,
|
||||||
set: () => toggleDark(),
|
set: () => toggleDark(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const menuItems = computed(() => ([
|
const menuItems = computed(() => ([
|
||||||
{ title: t('navbar.Mock'), route: 'mock' },
|
{ title: 'Mock', route: 'mock' },
|
||||||
{ title: t('navbar.Charts'), route: 'charts' },
|
{ title: 'Charts', route: 'charts' },
|
||||||
{ title: t('navbar.UnoCSS'), route: 'unocss' },
|
{ title: 'UnoCSS', route: 'unocss' },
|
||||||
{ title: t('navbar.Counter'), route: 'counter' },
|
{ title: 'Counter', route: 'counter' },
|
||||||
{ title: t('navbar.KeepAlive'), route: 'keepalive' },
|
{ title: 'KeepAlive', route: 'keepalive' },
|
||||||
{ title: t('navbar.ScrollCache'), route: 'scroll-cache' },
|
{ title: 'ScrollCache', route: 'scroll-cache' },
|
||||||
{ title: t('navbar.AIChat'), route: 'llm-chat' },
|
{ title: 'AI Chat', route: 'llm-chat' },
|
||||||
{ title: t('navbar.404'), route: 'unknown' },
|
{ title: '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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<van-cell-group :title="$t('home.settings')" :border="false" :inset="true">
|
<van-cell-group title="Settings" :border="false" :inset="true">
|
||||||
<van-cell center :title="$t('home.darkMode')">
|
<van-cell center title="Dark Mode">
|
||||||
<template #right-icon>
|
<template #right-icon>
|
||||||
<van-switch
|
<van-switch
|
||||||
v-model="checked"
|
v-model="checked"
|
||||||
|
|
@ -41,29 +27,13 @@ function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
|
|
||||||
<van-cell
|
|
||||||
is-link
|
|
||||||
:title="$t('home.language')"
|
|
||||||
:value="language"
|
|
||||||
@click="showLanguagePicker = true"
|
|
||||||
/>
|
|
||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
|
|
||||||
<van-cell-group :title="$t('home.examples')" :border="false" :inset="true">
|
<van-cell-group title="Examples" :border="false" :inset="true">
|
||||||
<template v-for="item in menuItems" :key="item.route">
|
<template v-for="item in menuItems" :key="item.route">
|
||||||
<van-cell :title="item.title" :to="item.route" is-link />
|
<van-cell :title="item.title" :to="item.route" is-link />
|
||||||
</template>
|
</template>
|
||||||
</van-cell-group>
|
</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>
|
</template>
|
||||||
|
|
||||||
<route lang="json5">
|
<route lang="json5">
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const value = ref(0)
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-sm space-y-2">
|
<div class="text-sm space-y-2">
|
||||||
<p>{{ $t('keepAlive.label') }}</p>
|
<p>KeepAlive Demo</p>
|
||||||
<van-stepper v-model="value" />
|
<van-stepper v-model="value" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import type { ChatMessage } from '@/types/chat'
|
||||||
import { useScroll } from '@vueuse/core'
|
import { useScroll } from '@vueuse/core'
|
||||||
import { isDark, toggleDark } from '@/composables/dark'
|
import { isDark, toggleDark } from '@/composables/dark'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
|
|
||||||
// Sample chat data for demonstration
|
// Sample chat data for demonstration
|
||||||
const messages = ref<ChatMessage[]>([
|
const messages = ref<ChatMessage[]>([
|
||||||
|
|
@ -48,7 +47,8 @@ watch(messages, () => {
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
function handleSend(text: string) {
|
function handleSend(text: string) {
|
||||||
if (!text.trim()) return
|
if (!text.trim())
|
||||||
|
return
|
||||||
|
|
||||||
// Add user message
|
// Add user message
|
||||||
const userMessage: ChatMessage = {
|
const userMessage: ChatMessage = {
|
||||||
|
|
@ -56,7 +56,7 @@ function handleSend(text: string) {
|
||||||
content: text,
|
content: text,
|
||||||
sender: 'user',
|
sender: 'user',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
status: 'sending',
|
status: 'sent',
|
||||||
}
|
}
|
||||||
messages.value.push(userMessage)
|
messages.value.push(userMessage)
|
||||||
|
|
||||||
|
|
@ -87,13 +87,12 @@ function handleKeydown(e: KeyboardEvent) {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-screen flex flex-col bg-gray-50 dark:bg-gray-950">
|
<div class="bg-gray-50 flex flex-col h-screen dark:bg-gray-950">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
:title="t('llmChat.title', 'AI Chat')"
|
title="AI Chat"
|
||||||
fixed
|
|
||||||
placeholder
|
placeholder safe-area-inset-top fixed
|
||||||
safe-area-inset-top
|
|
||||||
>
|
>
|
||||||
<template #right>
|
<template #right>
|
||||||
<van-icon
|
<van-icon
|
||||||
|
|
@ -107,7 +106,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||||
<!-- Messages container -->
|
<!-- Messages container -->
|
||||||
<van-list
|
<van-list
|
||||||
ref="messagesContainer"
|
ref="messagesContainer"
|
||||||
class="flex-1 p-4 pt-16 pb-24"
|
class="p-4 pb-24 pt-16 flex-1"
|
||||||
:finished="true"
|
:finished="true"
|
||||||
:loading="false"
|
:loading="false"
|
||||||
finished-text=""
|
finished-text=""
|
||||||
|
|
@ -130,7 +129,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||||
</van-list>
|
</van-list>
|
||||||
|
|
||||||
<!-- Input area -->
|
<!-- Input area -->
|
||||||
<div class="fixed bottom-0 left-0 right-0 z-10">
|
<div class="bottom-0 left-0 right-0 fixed z-10">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
v-model="inputText"
|
v-model="inputText"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading"
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import logo from '~/images/logo.svg'
|
||||||
import logoDark from '~/images/logo-dark.svg'
|
import logoDark from '~/images/logo-dark.svg'
|
||||||
import vw from '@/utils/inline-px-to-vw'
|
import vw from '@/utils/inline-px-to-vw'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
@ -28,10 +27,10 @@ const postData = reactive({
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
email: [
|
email: [
|
||||||
{ required: true, message: t('login.pleaseEnterEmail') },
|
{ required: true, message: 'Please enter email' },
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: t('login.pleaseEnterPassword') },
|
{ required: true, message: 'Please enter password' },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -65,7 +64,7 @@ async function login(values: any) {
|
||||||
v-model="postData.email"
|
v-model="postData.email"
|
||||||
:rules="rules.email"
|
:rules="rules.email"
|
||||||
name="email"
|
name="email"
|
||||||
:placeholder="$t('login.email')"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -75,7 +74,7 @@ async function login(values: any) {
|
||||||
type="password"
|
type="password"
|
||||||
:rules="rules.password"
|
:rules="rules.password"
|
||||||
name="password"
|
name="password"
|
||||||
:placeholder="$t('login.password')"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -86,17 +85,17 @@ async function login(values: any) {
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
round block
|
round block
|
||||||
>
|
>
|
||||||
{{ $t('login.login') }}
|
Sign In
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|
||||||
<GhostButton block to="register" :style="{ 'margin-top': vw(18) }">
|
<GhostButton block to="register" :style="{ 'margin-top': vw(18) }">
|
||||||
{{ $t('login.signUp') }}
|
Click to sign up
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
|
|
||||||
<GhostButton block to="forgot-password" class="mt-2">
|
<GhostButton block to="forgot-password" class="mt-2">
|
||||||
{{ $t('login.forgotPassword') }}
|
Forgot password?
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -13,22 +13,22 @@ function pull() {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="data-label">
|
<div class="data-label">
|
||||||
{{ $t('mock.fromAsyncData') }}
|
Data from async request
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="data-content bg-white dark:bg-[--van-background-2]">
|
<div class="data-content bg-white dark:bg-[--van-background-2]">
|
||||||
<div v-if="messages">
|
<div v-if="messages">
|
||||||
{{ messages }}
|
{{ messages }}
|
||||||
</div>
|
</div>
|
||||||
<VanEmpty v-else :description="$t('mock.noData')" />
|
<VanEmpty v-else description="No data" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-space class="m-2" direction="vertical" fill>
|
<van-space class="m-2" direction="vertical" fill>
|
||||||
<VanButton type="primary" round block @click="pull">
|
<VanButton type="primary" round block @click="pull">
|
||||||
{{ $t('mock.pull') }}
|
Pull data
|
||||||
</VanButton>
|
</VanButton>
|
||||||
<VanButton type="default" round block @click="messages = ''">
|
<VanButton type="default" round block @click="messages = ''">
|
||||||
{{ $t('mock.reset') }}
|
Reset
|
||||||
</VanButton>
|
</VanButton>
|
||||||
</van-space>
|
</van-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,18 @@ function login() {
|
||||||
|
|
||||||
<template #value>
|
<template #value>
|
||||||
<span v-if="isLogin">{{ userInfo.name }}</span>
|
<span v-if="isLogin">{{ userInfo.name }}</span>
|
||||||
<span v-else>{{ $t('profile.login') }}</span>
|
<span v-else>Login</span>
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
</VanCellGroup>
|
</VanCellGroup>
|
||||||
|
|
||||||
<VanCellGroup :inset="true" class="!mt-4">
|
<VanCellGroup :inset="true" class="!mt-4">
|
||||||
<van-cell :title="$t('profile.settings')" icon="setting-o" is-link to="/settings">
|
<van-cell title="Settings" icon="setting-o" is-link to="/settings">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div class="i-carbon:settings text-gray-400 mr-2 self-center" />
|
<div class="i-carbon:settings text-gray-400 mr-2 self-center" />
|
||||||
</template>
|
</template>
|
||||||
</van-cell>
|
</van-cell>
|
||||||
<van-cell :title="$t('profile.docs')" is-link url="https://vue-zone.github.io/docs/vue3-vant-mobile/">
|
<van-cell title="Documentation" is-link url="https://vue-zone.github.io/docs/vue3-vant-mobile/">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<div class="i-carbon:doc text-gray-400 mr-2 self-center" />
|
<div class="i-carbon:doc text-gray-400 mr-2 self-center" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { showNotify } from 'vant'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import vw from '@/utils/inline-px-to-vw'
|
import vw from '@/utils/inline-px-to-vw'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
@ -22,20 +21,20 @@ const validatorPassword = (val: string) => val === postData.password
|
||||||
|
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
email: [
|
email: [
|
||||||
{ required: true, message: t('register.pleaseEnterEmail') },
|
{ required: true, message: 'Please enter email' },
|
||||||
],
|
],
|
||||||
code: [
|
code: [
|
||||||
{ required: true, message: t('register.pleaseEnterCode') },
|
{ required: true, message: 'Please enter code' },
|
||||||
],
|
],
|
||||||
nickname: [
|
nickname: [
|
||||||
{ required: true, message: t('register.pleaseEnterNickname') },
|
{ required: true, message: 'Please enter nickname' },
|
||||||
],
|
],
|
||||||
password: [
|
password: [
|
||||||
{ required: true, message: t('register.pleaseEnterPassword') },
|
{ required: true, message: 'Please enter password' },
|
||||||
],
|
],
|
||||||
confirmPassword: [
|
confirmPassword: [
|
||||||
{ required: true, message: t('register.pleaseEnterConfirmPassword') },
|
{ required: true, message: 'Please enter password again' },
|
||||||
{ required: true, validator: validatorPassword, message: t('register.passwordsDoNotMatch') },
|
{ required: true, validator: validatorPassword, message: 'Passwords do not match' },
|
||||||
] as FieldRule[],
|
] as FieldRule[],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -46,7 +45,7 @@ async function register() {
|
||||||
const res = await userStore.register()
|
const res = await userStore.register()
|
||||||
|
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
showNotify({ type: 'success', message: t('register.registerSuccess') })
|
showNotify({ type: 'success', message: 'Registration succeeded' })
|
||||||
router.push({ name: 'Login' })
|
router.push({ name: 'Login' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,19 +57,19 @@ async function register() {
|
||||||
const isGettingCode = ref(false)
|
const isGettingCode = ref(false)
|
||||||
|
|
||||||
const buttonText = computed(() => {
|
const buttonText = computed(() => {
|
||||||
return isGettingCode.value ? t('register.gettingCode') : t('register.getCode')
|
return isGettingCode.value ? 'Getting code' : 'Get code'
|
||||||
})
|
})
|
||||||
|
|
||||||
async function getCode() {
|
async function getCode() {
|
||||||
if (!postData.email) {
|
if (!postData.email) {
|
||||||
showNotify({ type: 'warning', message: t('register.pleaseEnterEmail') })
|
showNotify({ type: 'warning', message: 'Please enter email' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isGettingCode.value = true
|
isGettingCode.value = true
|
||||||
const res = await userStore.getCode()
|
const res = await userStore.getCode()
|
||||||
if (res.code === 0)
|
if (res.code === 0)
|
||||||
showNotify({ type: 'success', message: `${t('register.sendCodeSuccess')}: ${res.result}` })
|
showNotify({ type: 'success', message: `Sent, the code is: ${res.result}` })
|
||||||
|
|
||||||
isGettingCode.value = false
|
isGettingCode.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -84,7 +83,7 @@ async function getCode() {
|
||||||
v-model.trim="postData.email"
|
v-model.trim="postData.email"
|
||||||
:rules="rules.email"
|
:rules="rules.email"
|
||||||
name="email"
|
name="email"
|
||||||
:placeholder="$t('register.email')"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -93,7 +92,7 @@ async function getCode() {
|
||||||
v-model.trim="postData.code"
|
v-model.trim="postData.code"
|
||||||
:rules="rules.code"
|
:rules="rules.code"
|
||||||
name="code"
|
name="code"
|
||||||
:placeholder="$t('register.code')"
|
placeholder="Code"
|
||||||
>
|
>
|
||||||
<template #button>
|
<template #button>
|
||||||
<van-button size="small" type="primary" plain @click="getCode">
|
<van-button size="small" type="primary" plain @click="getCode">
|
||||||
|
|
@ -108,7 +107,7 @@ async function getCode() {
|
||||||
v-model.trim="postData.nickname"
|
v-model.trim="postData.nickname"
|
||||||
:rules="rules.nickname"
|
:rules="rules.nickname"
|
||||||
name="nickname"
|
name="nickname"
|
||||||
:placeholder="$t('register.nickname')"
|
placeholder="Nickname"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -118,7 +117,7 @@ async function getCode() {
|
||||||
type="password"
|
type="password"
|
||||||
:rules="rules.password"
|
:rules="rules.password"
|
||||||
name="password"
|
name="password"
|
||||||
:placeholder="$t('register.password')"
|
placeholder="Password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -128,7 +127,7 @@ async function getCode() {
|
||||||
type="password"
|
type="password"
|
||||||
:rules="rules.confirmPassword"
|
:rules="rules.confirmPassword"
|
||||||
name="confirmPassword"
|
name="confirmPassword"
|
||||||
:placeholder="$t('register.confirmPassword')"
|
placeholder="Password again"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -139,13 +138,13 @@ async function getCode() {
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
round block
|
round block
|
||||||
>
|
>
|
||||||
{{ $t('register.confirm') }}
|
Confirm
|
||||||
</van-button>
|
</van-button>
|
||||||
</div>
|
</div>
|
||||||
</van-form>
|
</van-form>
|
||||||
|
|
||||||
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
|
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
|
||||||
{{ $t('register.backToLogin') }}
|
Back to login
|
||||||
</GhostButton>
|
</GhostButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ onBeforeRouteLeave(() => {
|
||||||
<van-list
|
<van-list
|
||||||
v-model:loading="loading"
|
v-model:loading="loading"
|
||||||
:finished="finished"
|
:finished="finished"
|
||||||
:finished-text="$t('scrollCache.finished')"
|
finished-text="No more data"
|
||||||
:loading-text="$t('scrollCache.loading')"
|
loading-text="Loading..."
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
>
|
>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
|
|
@ -54,14 +54,14 @@ onBeforeRouteLeave(() => {
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex flex-row gap-2 w-full justify-between">
|
<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">
|
<h3 class="text-base text-zinc-600 tracking-tight font-semibold w-1/2 dark:text-white">
|
||||||
<van-text-ellipsis :content="`${$t('scrollCache.sectionTitle')}`" />
|
<van-text-ellipsis content="Section Title" />
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<time class="text-xs text-zinc-400 tabular-nums">2025-05-16</time>
|
<time class="text-xs text-zinc-400 tabular-nums">2025-05-16</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-zinc-500">
|
<p class="text-sm text-zinc-500">
|
||||||
<van-text-ellipsis :rows="2" :content="$t('scrollCache.sectionText')" />
|
<van-text-ellipsis rows="2" content="This is a sample section text for demonstration purposes. It can be longer and will be ellipsized." />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,12 @@ import router from '@/router'
|
||||||
import { useUserStore } from '@/stores'
|
import { useUserStore } from '@/stores'
|
||||||
import { version } from '~root/package.json'
|
import { version } from '~root/package.json'
|
||||||
|
|
||||||
const { t } = useI18n()
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const userInfo = computed(() => userStore.userInfo)
|
const userInfo = computed(() => userStore.userInfo)
|
||||||
|
|
||||||
function Logout() {
|
function Logout() {
|
||||||
showConfirmDialog({
|
showConfirmDialog({
|
||||||
title: t('settings.confirmTitle'),
|
title: 'Confirm logout?',
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
userStore.logout()
|
userStore.logout()
|
||||||
|
|
@ -23,11 +22,11 @@ function Logout() {
|
||||||
<template>
|
<template>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<VanCellGroup :inset="true">
|
<VanCellGroup :inset="true">
|
||||||
<van-cell v-if="userInfo.uid" :title="$t('settings.logout')" clickable class="van-text-color" @click="Logout" />
|
<van-cell v-if="userInfo.uid" title="Logout" clickable class="van-text-color" @click="Logout" />
|
||||||
</VanCellGroup>
|
</VanCellGroup>
|
||||||
|
|
||||||
<div class="text-gray mt-2">
|
<div class="text-gray mt-2">
|
||||||
{{ $t("settings.currentVersion") }}: v{{ version }}
|
Current version: v{{ version }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<h1 class="text-base color-pink font-semibold">
|
<h1 class="text-base color-pink font-semibold">
|
||||||
{{ $t('unocss.title') }}
|
UnoCSS Demo
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="text-gray-700 mt-2 dark:text-white">
|
<p class="text-gray-700 mt-2 dark:text-white">
|
||||||
{{ $t('unocss.description') }}
|
This is a demo of UnoCSS.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button class="btn mt-2">
|
<button class="btn mt-2">
|
||||||
{{ $t('unocss.button') }}
|
Click me
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ declare global {
|
||||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
const h: typeof import('vue').h
|
const h: typeof import('vue').h
|
||||||
const i18n: typeof import('@/utils/i18n').i18n
|
|
||||||
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
||||||
const inject: typeof import('vue').inject
|
const inject: typeof import('vue').inject
|
||||||
const injectHead: typeof import('@unhead/vue').injectHead
|
const injectHead: typeof import('@unhead/vue').injectHead
|
||||||
|
|
@ -50,7 +49,6 @@ declare global {
|
||||||
const isReadonly: typeof import('vue').isReadonly
|
const isReadonly: typeof import('vue').isReadonly
|
||||||
const isRef: typeof import('vue').isRef
|
const isRef: typeof import('vue').isRef
|
||||||
const isShallow: typeof import('vue').isShallow
|
const isShallow: typeof import('vue').isShallow
|
||||||
const locale: typeof import('@/utils/i18n').locale
|
|
||||||
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
|
const makeDestructurable: typeof import('@vueuse/core').makeDestructurable
|
||||||
const manualResetRef: typeof import('@vueuse/core').manualResetRef
|
const manualResetRef: typeof import('@vueuse/core').manualResetRef
|
||||||
const markRaw: typeof import('vue').markRaw
|
const markRaw: typeof import('vue').markRaw
|
||||||
|
|
@ -190,7 +188,6 @@ declare global {
|
||||||
const useGeolocation: typeof import('@vueuse/core').useGeolocation
|
const useGeolocation: typeof import('@vueuse/core').useGeolocation
|
||||||
const useHead: typeof import('@unhead/vue').useHead
|
const useHead: typeof import('@unhead/vue').useHead
|
||||||
const useHeadSafe: typeof import('@unhead/vue').useHeadSafe
|
const useHeadSafe: typeof import('@unhead/vue').useHeadSafe
|
||||||
const useI18n: typeof import('vue-i18n').useI18n
|
|
||||||
const useId: typeof import('vue').useId
|
const useId: typeof import('vue').useId
|
||||||
const useIdle: typeof import('@vueuse/core').useIdle
|
const useIdle: typeof import('@vueuse/core').useIdle
|
||||||
const useImage: typeof import('@vueuse/core').useImage
|
const useImage: typeof import('@vueuse/core').useImage
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { appName } from '@/constants'
|
import { appName } from '@/constants'
|
||||||
import { i18n } from '@/utils/i18n'
|
|
||||||
|
|
||||||
export default function setPageTitle(name?: string): void {
|
export default function setPageTitle(name?: string): void {
|
||||||
window.document.title = name ? `${i18n.global.t(`navbar.${name}`)} - ${appName()}` : appName()
|
window.document.title = name ? `${name} - ${appName()}` : appName()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@
|
||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"unplugin-vue-router/client",
|
"unplugin-vue-router/client",
|
||||||
"vite-plugin-pwa/client",
|
"vite-plugin-pwa/client"
|
||||||
"@intlify/unplugin-vue-i18n/messages"
|
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue