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:
dzq 2025-12-19 10:09:47 +08:00
parent 3f8689f7ad
commit 118d10208b
29 changed files with 156 additions and 524 deletions

View File

@ -14,7 +14,6 @@ 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'
@ -60,8 +59,6 @@ export function createVitePlugins(mode: string) {
VueRouterAutoImports,
{
'vue-router/auto': ['useLink'],
'@/utils/i18n': ['i18n', 'locale'],
'vue-i18n': ['useI18n'],
},
unheadVueComposablesImports,
],
@ -72,12 +69,6 @@ export function createVitePlugins(mode: string) {
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'],
}),

View File

@ -34,13 +34,11 @@
"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",
"@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",

View File

@ -1,15 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { useRouteCacheStore } from '@/stores'
const { t } = useI18n()
useHead({
title: () => t('app.name'),
title: 'Vue3 Vant Mobile',
meta: [
{
name: 'description',
content: () => t('app.description'),
content: 'An mobile web apps template based on the Vue 3 ecosystem',
},
{
name: 'theme-color',

View File

@ -4,15 +4,16 @@ import type { ChatBubbleProps } from '@/types/chat'
const props = defineProps<ChatBubbleProps>()
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'
? 'bg-blue-100 dark:bg-blue-900 ml-auto text-right'
: 'bg-gray-100 dark:bg-gray-800 mr-auto text-left'
? 'bg-blue-100 dark:bg-blue-800'
: 'bg-gray-100 dark:bg-gray-700'
return `${base} ${sender}`
})
const formattedTime = computed(() => {
if (!props.message.timestamp) return ''
if (!props.message.timestamp)
return ''
const date = typeof props.message.timestamp === 'string'
? new Date(props.message.timestamp)
: props.message.timestamp
@ -21,37 +22,57 @@ const formattedTime = computed(() => {
</script>
<template>
<div class="flex items-end gap-2 mb-4" :class="{ 'flex-row-reverse': message.sender === 'user' }">
<!-- Avatar -->
<van-image
v-if="avatar"
:src="avatar"
class="w-8 h-8 rounded-full flex-shrink-0"
round
/>
<div v-else class="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center"
:class="message.sender === 'user' ? 'bg-blue-100 dark:bg-blue-900' : 'bg-gray-100 dark:bg-gray-800'">
<van-icon :name="message.sender === 'user' ? 'user-circle-o' : 'robot-o'" />
<div class="mb-4 flex gap-2" :class="message.sender === 'user' ? 'justify-end' : 'justify-start'">
<!-- Avatar for AI messages (left side) -->
<div v-if="message.sender === 'ai'">
<van-image
v-if="avatar"
:src="avatar"
class="rounded-full flex-shrink-0 h-8 w-8"
round
/>
<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">
<van-icon name="robot-o" class="text-lg" />
</div>
</div>
<!-- Message bubble -->
<div class="flex flex-col" :class="{ 'items-end': message.sender === 'user', 'items-start': message.sender === 'ai' }">
<!-- Message bubble and timestamp -->
<div class="flex flex-col max-w-[80%]">
<div :class="messageClasses">
<p class="text-sm">{{ message.content }}</p>
<p class="text-sm">
{{ message.content }}
</p>
<!-- Status indicator -->
<div v-if="message.status === 'sending'" class="mt-1">
<van-icon name="clock-o" class="animate-pulse" />
</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" />
</div>
</div>
<!-- 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 }}
</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>
</template>
</template>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { ChatInputProps, ChatInputEmits } from '@/types/chat'
import type { ChatInputEmits, ChatInputProps } from '@/types/chat'
const props = defineProps<ChatInputProps>()
const emit = defineEmits<ChatInputEmits>()
@ -7,11 +7,6 @@ const emit = defineEmits<ChatInputEmits>()
const inputRef = ref<HTMLTextAreaElement>()
const isComposing = ref(false)
function handleInput(e: Event) {
const target = e.target as HTMLInputElement
emit('update:modelValue', target.value)
}
function handleKeydown(e: KeyboardEvent) {
emit('keydown', e)
@ -38,10 +33,10 @@ defineExpose({
</script>
<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
ref="inputRef"
v-model="props.modelValue"
:model-value="props.modelValue"
type="textarea"
rows="1"
autosize
@ -49,7 +44,7 @@ defineExpose({
:disabled="disabled"
:readonly="loading"
class="flex-1"
@input="handleInput"
@update:model-value="(value) => emit('update:modelValue', value)"
@keydown="handleKeydown"
@compositionstart="isComposing = true"
@compositionend="isComposing = false"
@ -60,12 +55,12 @@ defineExpose({
round
:disabled="!props.modelValue.trim() || disabled"
:loading="loading"
@click="handleSend"
class="flex-shrink-0"
@click="handleSend"
>
<template #icon>
<van-icon name="send" />
</template>
</van-button>
</div>
</template>
</template>

View File

@ -3,18 +3,33 @@ import { rootRouteList } from '@/config/routes'
const route = useRoute()
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
* Located in src/locales/json
*/
const title = computed(() => {
if (route.name) {
return t(`navbar.${route.name}`)
return routeTitleMap[route.name.toString()] || route.name.toString()
}
return t('navbar.Undefined')
return 'Undefined'
})
/**

View File

@ -15,13 +15,13 @@ const show = computed(() => {
<template>
<van-tabbar v-if="show" v-model="active" route placeholder>
<van-tabbar-item replace to="/">
{{ $t('tabbar.home') }}
Home
<template #icon>
<div class="i-carbon:home" />
</template>
</van-tabbar-item>
<van-tabbar-item replace to="/profile">
{{ $t('tabbar.profile') }}
Profile
<template #icon>
<div class="i-carbon:user" />
</template>

View File

@ -1,4 +1,2 @@
import { i18n } from '@/utils/i18n'
export const appName = () => i18n.global.t('app.name')
export const appDescription = () => i18n.global.t('app.description')
export const appName = () => 'Vue3 Vant Mobile'
export const appDescription = () => 'An mobile web apps template based on the Vue 3 ecosystem'

View File

@ -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?"
}
}

View File

@ -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": "确认退出?"
}
}

View File

@ -6,7 +6,6 @@ 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'
@ -28,6 +27,5 @@ const head = createHead()
app.use(head)
app.use(router)
app.use(pinia)
app.use(i18n)
app.mount('#app')

View File

@ -1,11 +1,9 @@
<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')],
data: ['January', 'February', 'March', 'April', 'May', 'June'],
},
yAxis: {},
series: [

View File

@ -8,7 +8,7 @@ const { counter } = storeToRefs(counterStore)
<template>
<div class="text-sm space-y-2">
<p> {{ $t('counter.description') }}</p>
<p>Counter Demo</p>
<van-stepper v-model="counter" />
</div>
</template>

View File

@ -5,7 +5,6 @@ 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)
@ -21,17 +20,17 @@ const validatorPassword = (val: string) => val === postData.password
const rules = reactive({
email: [
{ required: true, message: t('forgotPassword.pleaseEnterEmail') },
{ required: true, message: 'Please enter email' },
],
code: [
{ required: true, message: t('forgotPassword.pleaseEnterCode') },
{ required: true, message: 'Please enter code' },
],
password: [
{ required: true, message: t('forgotPassword.pleaseEnterPassword') },
{ required: true, message: 'Please enter password' },
],
confirmPassword: [
{ required: true, message: t('forgotPassword.pleaseEnterConfirmPassword') },
{ required: true, validator: validatorPassword, message: t('forgotPassword.passwordsDoNotMatch') },
{ required: true, message: 'Please enter password again' },
{ required: true, validator: validatorPassword, message: 'Passwords do not match' },
] as FieldRule[],
})
@ -42,7 +41,7 @@ async function reset() {
const res = await userStore.reset()
if (res.code === 0) {
showNotify({ type: 'success', message: t('forgotPassword.passwordResetSuccess') })
showNotify({ type: 'success', message: 'Password reset succeeded' })
router.push({ name: 'Login' })
}
}
@ -54,19 +53,19 @@ async function reset() {
const isGettingCode = ref(false)
const buttonText = computed(() => {
return isGettingCode.value ? t('forgotPassword.gettingCode') : t('forgotPassword.getCode')
return isGettingCode.value ? 'Getting code' : 'Get code'
})
async function getCode() {
if (!postData.email) {
showNotify({ type: 'warning', message: t('forgotPassword.pleaseEnterEmail') })
showNotify({ type: 'warning', message: 'Please enter email' })
return
}
isGettingCode.value = true
const res = await userStore.getCode()
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
}
@ -80,7 +79,7 @@ async function getCode() {
v-model.trim="postData.email"
:rules="rules.email"
name="email"
:placeholder="$t('forgotPassword.email')"
placeholder="Email"
/>
</div>
@ -89,7 +88,7 @@ async function getCode() {
v-model.trim="postData.code"
:rules="rules.code"
name="code"
:placeholder="$t('forgotPassword.code')"
placeholder="Code"
>
<template #button>
<van-button size="small" type="primary" plain @click="getCode">
@ -105,7 +104,7 @@ async function getCode() {
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('forgotPassword.password')"
placeholder="Password"
/>
</div>
@ -115,7 +114,7 @@ async function getCode() {
type="password"
:rules="rules.confirmPassword"
name="confirmPassword"
:placeholder="$t('forgotPassword.confirmPassword')"
placeholder="Password again"
/>
</div>
@ -126,13 +125,13 @@ async function getCode() {
native-type="submit"
round block
>
{{ $t('forgotPassword.confirm') }}
Confirm
</van-button>
</div>
</van-form>
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
{{ $t('forgotPassword.backToLogin') }}
Back to login
</GhostButton>
</div>
</template>

View File

@ -1,38 +1,24 @@
<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.AIChat'), route: 'llm-chat' },
{ title: t('navbar.404'), route: 'unknown' },
{ title: 'Mock', route: 'mock' },
{ title: 'Charts', route: 'charts' },
{ title: 'UnoCSS', route: 'unocss' },
{ title: 'Counter', route: 'counter' },
{ title: 'KeepAlive', route: 'keepalive' },
{ title: 'ScrollCache', route: 'scroll-cache' },
{ title: 'AI Chat', route: 'llm-chat' },
{ 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>
<template>
<van-cell-group :title="$t('home.settings')" :border="false" :inset="true">
<van-cell center :title="$t('home.darkMode')">
<van-cell-group title="Settings" :border="false" :inset="true">
<van-cell center title="Dark Mode">
<template #right-icon>
<van-switch
v-model="checked"
@ -41,29 +27,13 @@ function onLanguageConfirm(event: { selectedOptions: PickerColumn }) {
/>
</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">
<van-cell-group title="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">

View File

@ -8,7 +8,7 @@ const value = ref(0)
<template>
<div class="text-sm space-y-2">
<p>{{ $t('keepAlive.label') }}</p>
<p>KeepAlive Demo</p>
<van-stepper v-model="value" />
</div>
</template>

View File

@ -3,7 +3,6 @@ import type { ChatMessage } from '@/types/chat'
import { useScroll } from '@vueuse/core'
import { isDark, toggleDark } from '@/composables/dark'
const { t } = useI18n()
// Sample chat data for demonstration
const messages = ref<ChatMessage[]>([
@ -48,7 +47,8 @@ watch(messages, () => {
}, { deep: true })
function handleSend(text: string) {
if (!text.trim()) return
if (!text.trim())
return
// Add user message
const userMessage: ChatMessage = {
@ -56,7 +56,7 @@ function handleSend(text: string) {
content: text,
sender: 'user',
timestamp: new Date(),
status: 'sending',
status: 'sent',
}
messages.value.push(userMessage)
@ -87,13 +87,12 @@ function handleKeydown(e: KeyboardEvent) {
</script>
<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 -->
<van-nav-bar
:title="t('llmChat.title', 'AI Chat')"
fixed
placeholder
safe-area-inset-top
title="AI Chat"
placeholder safe-area-inset-top fixed
>
<template #right>
<van-icon
@ -107,7 +106,7 @@ function handleKeydown(e: KeyboardEvent) {
<!-- Messages container -->
<van-list
ref="messagesContainer"
class="flex-1 p-4 pt-16 pb-24"
class="p-4 pb-24 pt-16 flex-1"
:finished="true"
:loading="false"
finished-text=""
@ -130,7 +129,7 @@ function handleKeydown(e: KeyboardEvent) {
</van-list>
<!-- 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
v-model="inputText"
:disabled="isLoading"
@ -151,4 +150,4 @@ function handleKeydown(e: KeyboardEvent) {
requiresAuth: false
}
}
</route>
</route>

View File

@ -7,7 +7,6 @@ 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)
@ -28,10 +27,10 @@ const postData = reactive({
const rules = reactive({
email: [
{ required: true, message: t('login.pleaseEnterEmail') },
{ required: true, message: 'Please enter email' },
],
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"
:rules="rules.email"
name="email"
:placeholder="$t('login.email')"
placeholder="Email"
/>
</div>
@ -75,7 +74,7 @@ async function login(values: any) {
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('login.password')"
placeholder="Password"
/>
</div>
@ -86,17 +85,17 @@ async function login(values: any) {
native-type="submit"
round block
>
{{ $t('login.login') }}
Sign In
</van-button>
</div>
</van-form>
<GhostButton block to="register" :style="{ 'margin-top': vw(18) }">
{{ $t('login.signUp') }}
Click to sign up
</GhostButton>
<GhostButton block to="forgot-password" class="mt-2">
{{ $t('login.forgotPassword') }}
Forgot password?
</GhostButton>
</div>
</template>

View File

@ -13,22 +13,22 @@ function pull() {
<template>
<div class="data-label">
{{ $t('mock.fromAsyncData') }}
Data from async request
</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')" />
<VanEmpty v-else description="No data" />
</div>
<van-space class="m-2" direction="vertical" fill>
<VanButton type="primary" round block @click="pull">
{{ $t('mock.pull') }}
Pull data
</VanButton>
<VanButton type="default" round block @click="messages = ''">
{{ $t('mock.reset') }}
Reset
</VanButton>
</van-space>
</template>

View File

@ -25,18 +25,18 @@ function login() {
<template #value>
<span v-if="isLogin">{{ userInfo.name }}</span>
<span v-else>{{ $t('profile.login') }}</span>
<span v-else>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">
<van-cell title="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/">
<van-cell title="Documentation" 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>

View File

@ -5,7 +5,6 @@ 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)
@ -22,20 +21,20 @@ const validatorPassword = (val: string) => val === postData.password
const rules = reactive({
email: [
{ required: true, message: t('register.pleaseEnterEmail') },
{ required: true, message: 'Please enter email' },
],
code: [
{ required: true, message: t('register.pleaseEnterCode') },
{ required: true, message: 'Please enter code' },
],
nickname: [
{ required: true, message: t('register.pleaseEnterNickname') },
{ required: true, message: 'Please enter nickname' },
],
password: [
{ required: true, message: t('register.pleaseEnterPassword') },
{ required: true, message: 'Please enter password' },
],
confirmPassword: [
{ required: true, message: t('register.pleaseEnterConfirmPassword') },
{ required: true, validator: validatorPassword, message: t('register.passwordsDoNotMatch') },
{ required: true, message: 'Please enter password again' },
{ required: true, validator: validatorPassword, message: 'Passwords do not match' },
] as FieldRule[],
})
@ -46,7 +45,7 @@ async function register() {
const res = await userStore.register()
if (res.code === 0) {
showNotify({ type: 'success', message: t('register.registerSuccess') })
showNotify({ type: 'success', message: 'Registration succeeded' })
router.push({ name: 'Login' })
}
}
@ -58,19 +57,19 @@ async function register() {
const isGettingCode = ref(false)
const buttonText = computed(() => {
return isGettingCode.value ? t('register.gettingCode') : t('register.getCode')
return isGettingCode.value ? 'Getting code' : 'Get code'
})
async function getCode() {
if (!postData.email) {
showNotify({ type: 'warning', message: t('register.pleaseEnterEmail') })
showNotify({ type: 'warning', message: 'Please enter email' })
return
}
isGettingCode.value = true
const res = await userStore.getCode()
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
}
@ -84,7 +83,7 @@ async function getCode() {
v-model.trim="postData.email"
:rules="rules.email"
name="email"
:placeholder="$t('register.email')"
placeholder="Email"
/>
</div>
@ -93,7 +92,7 @@ async function getCode() {
v-model.trim="postData.code"
:rules="rules.code"
name="code"
:placeholder="$t('register.code')"
placeholder="Code"
>
<template #button>
<van-button size="small" type="primary" plain @click="getCode">
@ -108,7 +107,7 @@ async function getCode() {
v-model.trim="postData.nickname"
:rules="rules.nickname"
name="nickname"
:placeholder="$t('register.nickname')"
placeholder="Nickname"
/>
</div>
@ -118,7 +117,7 @@ async function getCode() {
type="password"
:rules="rules.password"
name="password"
:placeholder="$t('register.password')"
placeholder="Password"
/>
</div>
@ -128,7 +127,7 @@ async function getCode() {
type="password"
:rules="rules.confirmPassword"
name="confirmPassword"
:placeholder="$t('register.confirmPassword')"
placeholder="Password again"
/>
</div>
@ -139,13 +138,13 @@ async function getCode() {
native-type="submit"
round block
>
{{ $t('register.confirm') }}
Confirm
</van-button>
</div>
</van-form>
<GhostButton to="login" block :style="{ 'margin-top': vw(8) }">
{{ $t('register.backToLogin') }}
Back to login
</GhostButton>
</div>
</template>

View File

@ -39,8 +39,8 @@ onBeforeRouteLeave(() => {
<van-list
v-model:loading="loading"
:finished="finished"
:finished-text="$t('scrollCache.finished')"
:loading-text="$t('scrollCache.loading')"
finished-text="No more data"
loading-text="Loading..."
@load="onLoad"
>
<ul class="space-y-2">
@ -54,14 +54,14 @@ onBeforeRouteLeave(() => {
<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')}`" />
<van-text-ellipsis content="Section Title" />
</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')" />
<van-text-ellipsis rows="2" content="This is a sample section text for demonstration purposes. It can be longer and will be ellipsized." />
</p>
</div>
</li>

View File

@ -4,13 +4,12 @@ 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'),
title: 'Confirm logout?',
})
.then(() => {
userStore.logout()
@ -23,11 +22,11 @@ function Logout() {
<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" />
<van-cell v-if="userInfo.uid" title="Logout" clickable class="van-text-color" @click="Logout" />
</VanCellGroup>
<div class="text-gray mt-2">
{{ $t("settings.currentVersion") }}: v{{ version }}
Current version: v{{ version }}
</div>
</div>
</template>

View File

@ -1,14 +1,14 @@
<template>
<h1 class="text-base color-pink font-semibold">
{{ $t('unocss.title') }}
UnoCSS Demo
</h1>
<p class="text-gray-700 mt-2 dark:text-white">
{{ $t('unocss.description') }}
This is a demo of UnoCSS.
</p>
<button class="btn mt-2">
{{ $t('unocss.button') }}
Click me
</button>
</template>

View File

@ -38,7 +38,6 @@ declare global {
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
@ -50,7 +49,6 @@ declare global {
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
@ -190,7 +188,6 @@ declare global {
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

View File

@ -23,4 +23,4 @@ export interface ChatInputEmits {
(e: 'update:modelValue', value: string): void
(e: 'send', value: string): void
(e: 'keydown', event: KeyboardEvent): void
}
}

View File

@ -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
}

View File

@ -1,6 +1,5 @@
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()
window.document.title = name ? `${name} - ${appName()}` : appName()
}

View File

@ -14,8 +14,7 @@
"types": [
"node",
"unplugin-vue-router/client",
"vite-plugin-pwa/client",
"@intlify/unplugin-vue-i18n/messages"
"vite-plugin-pwa/client"
],
"allowJs": true,
"strictNullChecks": false,