feat: 添加 Vant 组件库支持,优化 toast 消息显示逻辑

This commit is contained in:
2026-01-22 17:47:15 +07:00
parent b8ac4bf1f2
commit 78c413a914
8 changed files with 82 additions and 54 deletions

2
auto-imports.d.ts vendored
View File

@@ -109,6 +109,7 @@ declare global {
const shallowReactive: typeof import('vue').shallowReactive const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef const shallowRef: typeof import('vue').shallowRef
const showToast: typeof import('vant/es').showToast
const storeToRefs: typeof import('pinia').storeToRefs const storeToRefs: typeof import('pinia').storeToRefs
const syncRef: typeof import('@vueuse/core').syncRef const syncRef: typeof import('@vueuse/core').syncRef
const syncRefs: typeof import('@vueuse/core').syncRefs const syncRefs: typeof import('@vueuse/core').syncRefs
@@ -449,6 +450,7 @@ declare module 'vue' {
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']> readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']> readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']> readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
readonly showToast: UnwrapRef<typeof import('vant/es')['showToast']>
readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']> readonly storeToRefs: UnwrapRef<typeof import('pinia')['storeToRefs']>
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']> readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']> readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>

View File

@@ -53,6 +53,7 @@
"qr-scanner-wechat": "catalog:", "qr-scanner-wechat": "catalog:",
"qrcode": "catalog:", "qrcode": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"vant": "catalog:",
"vconsole": "catalog:", "vconsole": "catalog:",
"vee-validate": "catalog:", "vee-validate": "catalog:",
"vue": "catalog:", "vue": "catalog:",
@@ -78,6 +79,7 @@
"@ionic/cli": "catalog:", "@ionic/cli": "catalog:",
"@types/lodash-es": "catalog:", "@types/lodash-es": "catalog:",
"@types/node": "catalog:", "@types/node": "catalog:",
"@vant/auto-import-resolver": "catalog:",
"@vitejs/plugin-legacy": "catalog:", "@vitejs/plugin-legacy": "catalog:",
"@vitejs/plugin-vue": "catalog:", "@vitejs/plugin-vue": "catalog:",
"@vitejs/plugin-vue-jsx": "catalog:", "@vitejs/plugin-vue-jsx": "catalog:",

43
pnpm-lock.yaml generated
View File

@@ -114,6 +114,9 @@ catalogs:
'@types/node': '@types/node':
specifier: ^24.10.2 specifier: ^24.10.2
version: 24.10.9 version: 24.10.9
'@vant/auto-import-resolver':
specifier: ^1.3.0
version: 1.3.0
'@vee-validate/zod': '@vee-validate/zod':
specifier: ^4.15.1 specifier: ^4.15.1
version: 4.15.1 version: 4.15.1
@@ -216,6 +219,9 @@ catalogs:
unplugin-vue-components: unplugin-vue-components:
specifier: ^30.0.0 specifier: ^30.0.0
version: 30.0.0 version: 30.0.0
vant:
specifier: ^4.9.22
version: 4.9.22
vconsole: vconsole:
specifier: ^3.15.1 specifier: ^3.15.1
version: 3.15.1 version: 3.15.1
@@ -365,6 +371,9 @@ importers:
tailwindcss: tailwindcss:
specifier: 'catalog:' specifier: 'catalog:'
version: 4.1.18 version: 4.1.18
vant:
specifier: 'catalog:'
version: 4.9.22(vue@3.5.26(typescript@5.9.3))
vconsole: vconsole:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.15.1 version: 3.15.1
@@ -435,6 +444,9 @@ importers:
'@types/node': '@types/node':
specifier: 'catalog:' specifier: 'catalog:'
version: 24.10.9 version: 24.10.9
'@vant/auto-import-resolver':
specifier: 'catalog:'
version: 1.3.0
'@vitejs/plugin-legacy': '@vitejs/plugin-legacy':
specifier: 'catalog:' specifier: 'catalog:'
version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2)) version: 7.2.1(terser@5.46.0)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(yaml@2.8.2))
@@ -2476,6 +2488,17 @@ packages:
resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==} resolution: {integrity: sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vant/auto-import-resolver@1.3.0':
resolution: {integrity: sha512-lJyWtCyFizR4bHZvMiNMF3w+WTFTUWAvka1eqTnPK9ticUcKTCOx6qEmHcm8JPb3g1t3GaD2W3MnHkBp/nHamw==}
'@vant/popperjs@1.3.0':
resolution: {integrity: sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw==}
'@vant/use@1.6.0':
resolution: {integrity: sha512-PHHxeAASgiOpSmMjceweIrv2AxDZIkWXyaczksMoWvKV2YAYEhoizRuk/xFnKF+emUIi46TsQ+rvlm/t2BBCfA==}
peerDependencies:
vue: ^3.0.0
'@vee-validate/zod@4.15.1': '@vee-validate/zod@4.15.1':
resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==} resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==}
peerDependencies: peerDependencies:
@@ -5709,6 +5732,11 @@ packages:
utrie@1.0.2: utrie@1.0.2:
resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==}
vant@4.9.22:
resolution: {integrity: sha512-P2PDSj3oB6l3W1OpVlQpapeLmI6bXMSvPqPdrw5rutslC0Y6tSkrVB/iSD57weD7K92GsjGkvgDK0eZlOsXGqw==}
peerDependencies:
vue: ^3.0.0
vconsole@3.15.1: vconsole@3.15.1:
resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==}
@@ -8060,6 +8088,14 @@ snapshots:
'@typescript-eslint/types': 8.53.0 '@typescript-eslint/types': 8.53.0
eslint-visitor-keys: 4.2.1 eslint-visitor-keys: 4.2.1
'@vant/auto-import-resolver@1.3.0': {}
'@vant/popperjs@1.3.0': {}
'@vant/use@1.6.0(vue@3.5.26(typescript@5.9.3))':
dependencies:
vue: 3.5.26(typescript@5.9.3)
'@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)': '@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)':
dependencies: dependencies:
type-fest: 4.41.0 type-fest: 4.41.0
@@ -11668,6 +11704,13 @@ snapshots:
dependencies: dependencies:
base64-arraybuffer: 1.0.2 base64-arraybuffer: 1.0.2
vant@4.9.22(vue@3.5.26(typescript@5.9.3)):
dependencies:
'@vant/popperjs': 1.3.0
'@vant/use': 1.6.0(vue@3.5.26(typescript@5.9.3))
'@vue/shared': 3.5.26
vue: 3.5.26(typescript@5.9.3)
vconsole@3.15.1: vconsole@3.15.1:
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6

View File

@@ -18,6 +18,7 @@ catalog:
'@capacitor/keyboard': 8.0.0 '@capacitor/keyboard': 8.0.0
'@capacitor/share': ^8.0.0 '@capacitor/share': ^8.0.0
'@capacitor/status-bar': 8.0.0 '@capacitor/status-bar': 8.0.0
'@capacitor/toast': ^8.0.0
'@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz '@capp/eden': http://192.168.1.2:9538/api/capp-eden-0.0.37.tgz
'@cloudflare/workers-types': ^4.20260113.0 '@cloudflare/workers-types': ^4.20260113.0
'@elysiajs/eden': ^1.4.6 '@elysiajs/eden': ^1.4.6
@@ -40,6 +41,7 @@ catalog:
'@types/lodash-es': ^4.17.12 '@types/lodash-es': ^4.17.12
'@types/node': ^24.10.2 '@types/node': ^24.10.2
'@types/qrcode': ^1.5.6 '@types/qrcode': ^1.5.6
'@vant/auto-import-resolver': ^1.3.0
'@vee-validate/zod': ^4.15.1 '@vee-validate/zod': ^4.15.1
'@vitejs/plugin-basic-ssl': ^2.1.3 '@vitejs/plugin-basic-ssl': ^2.1.3
'@vitejs/plugin-legacy': ^7.2.1 '@vitejs/plugin-legacy': ^7.2.1
@@ -77,6 +79,7 @@ catalog:
unplugin-auto-import: ^20.3.0 unplugin-auto-import: ^20.3.0
unplugin-icons: ^22.5.0 unplugin-icons: ^22.5.0
unplugin-vue-components: ^30.0.0 unplugin-vue-components: ^30.0.0
vant: ^4.9.22
vconsole: ^3.15.1 vconsole: ^3.15.1
vee-validate: ^4.15.1 vee-validate: ^4.15.1
vite: ^7.2.7 vite: ^7.2.7
@@ -85,6 +88,7 @@ catalog:
vue: ^3.5.25 vue: ^3.5.25
vue-i18n: ^11.2.2 vue-i18n: ^11.2.2
vue-router: ^4.6.3 vue-router: ^4.6.3
vue-toastification: ^1.7.14
vue-tsc: ^3.1.8 vue-tsc: ^3.1.8
workbox-window: ^7.4.0 workbox-window: ^7.4.0
wrangler: ^4.54.0 wrangler: ^4.54.0

View File

@@ -1,7 +1,6 @@
import type { App } from "@capp/eden"; import type { App } from "@capp/eden";
import type { WatchSource } from "vue"; import type { WatchSource } from "vue";
import { treaty } from "@elysiajs/eden"; import { treaty } from "@elysiajs/eden";
import { toastController } from "@ionic/vue";
import { i18n } from "@/locales"; import { i18n } from "@/locales";
const baseURL = import.meta.env.DEV ? window.location.origin : import.meta.env.VITE_API_URL; const baseURL = import.meta.env.DEV ? window.location.origin : import.meta.env.VITE_API_URL;
@@ -63,28 +62,19 @@ export function safeClient<T, E>(
if (res.error) { if (res.error) {
if (!options.silent && res.status === 418) { if (!options.silent && res.status === 418) {
const toast = await toastController.create({ showToast(i18n.global.t((res.error as any).value.code, {
message: i18n.global.t((res.error as any).value.code, { ...(res.error as any).value.context,
...(res.error as any).value.context, }));
}),
duration: 3000,
position: "bottom",
color: "danger",
});
await toast.present();
} }
else if (res.status === 401) { else if (res.status === 401) {
localStorage.removeItem("user-token"); setTimeout(() => {
window.location.reload(); showToast("登录状态已过期2秒后将跳转到登录页面请重新登录");
localStorage.removeItem("user-token");
window.location.reload();
}, 2000);
} }
else if (!options.silent) { else if (!options.silent) {
const toast = await toastController.create({ showToast((res.error as any).message || i18n.global.t("network_error"));
message: (res.error as any).message || i18n.global.t("network_error"),
duration: 3000,
position: "bottom",
color: "danger",
});
await toast.present();
} }
throw res.error; throw res.error;

View File

@@ -1,5 +1,4 @@
<script lang='ts' setup> <script lang='ts' setup>
import { toastController } from "@ionic/vue";
import { safeClient } from "@/api"; import { safeClient } from "@/api";
import { authClient } from "@/auth"; import { authClient } from "@/auth";
import { LoginSchema } from "./schema"; import { LoginSchema } from "./schema";
@@ -15,24 +14,15 @@ const agreed = ref(false);
const showPassword = ref(false); const showPassword = ref(false);
const isLoading = ref(false); const isLoading = ref(false);
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
const toast = await toastController.create({
message,
duration: 2000,
position: "top",
color,
});
await toast.present();
}
async function handleLogin() { async function handleLogin() {
if (!agreed.value) { if (!agreed.value) {
await showToast("请先阅读并同意服务条款和隐私政策", "warning"); showToast("请先阅读并同意服务条款和隐私政策");
return; return;
} }
const result = LoginSchema.safeParse({ ...form.value }); const result = LoginSchema.safeParse({ ...form.value });
if (!result.success) { if (!result.success) {
const first = result.error.issues[0]; const first = result.error.issues[0];
await showToast(first.message); showToast(first.message);
return; return;
} }
@@ -43,17 +33,13 @@ async function handleLogin() {
password: form.value.password, password: form.value.password,
})); }));
if (!data.value?.token) { if (!data.value?.token) {
toastController.create({ showToast("登录失败,请检查手机号或密码");
message: "登录失败,请检查手机号或密码",
duration: 2000,
color: "danger",
}).then(toast => toast.present());
} }
else { else {
const userStore = useUserStore(); const userStore = useUserStore();
userStore.setToken(data.value.token); userStore.setToken(data.value.token);
await userStore.updateProfile(); await userStore.updateProfile();
await showToast("登录成功!", "success"); showToast("登录成功!");
router.push(route.query.redirect as string || "/"); router.push(route.query.redirect as string || "/");
} }
} }
@@ -73,6 +59,11 @@ function goToTerms(type: "service" | "privacy") {
<template> <template>
<ion-page> <ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-title>登录</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="login-page"> <ion-content :fullscreen="true" class="login-page">
<!-- 渐变背景 --> <!-- 渐变背景 -->
<div class="gradient-bg" /> <div class="gradient-bg" />

View File

@@ -1,6 +1,6 @@
<script lang='ts' setup> <script lang='ts' setup>
import { faker } from "@faker-js/faker"; import { faker } from "@faker-js/faker";
import { loadingController, toastController } from "@ionic/vue"; import { toastController } from "@ionic/vue";
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { safeClient } from "@/api"; import { safeClient } from "@/api";
@@ -23,27 +23,17 @@ const showPassword = ref(false);
const showConfirmPassword = ref(false); const showConfirmPassword = ref(false);
const isLoading = ref(false); const isLoading = ref(false);
async function showToast(message: string, color: "success" | "danger" | "warning" = "danger") {
const toast = await toastController.create({
message,
duration: 2000,
position: "top",
color,
});
await toast.present();
}
async function handleSignup() { async function handleSignup() {
// 检查是否同意协议 // 检查是否同意协议
if (!agreed.value) { if (!agreed.value) {
await showToast("请先阅读并同意服务条款和隐私政策", "warning"); showToast("请先阅读并同意服务条款和隐私政策");
return; return;
} }
const result = SignupSchema.safeParse(formData.value); const result = SignupSchema.safeParse(formData.value);
if (!result.success) { if (!result.success) {
const first = result.error.issues[0]; const first = result.error.issues[0];
await showToast(first.message); showToast(first.message);
return; return;
} }
@@ -57,7 +47,7 @@ async function handleSignup() {
password: formData.value.password, password: formData.value.password,
idCard: formData.value.idCard, idCard: formData.value.idCard,
inviteCode: formData.value.inviteCode || undefined, inviteCode: formData.value.inviteCode || undefined,
})); } as any));
if (!data.value?.token) { if (!data.value?.token) {
toastController.create({ toastController.create({
@@ -67,7 +57,7 @@ async function handleSignup() {
}).then(toast => toast.present()); }).then(toast => toast.present());
} }
else { else {
await showToast("注册成功!", "success"); showToast("注册成功!");
const userStore = useUserStore(); const userStore = useUserStore();
userStore.setToken(data.value.token); userStore.setToken(data.value.token);
await userStore.updateProfile(); await userStore.updateProfile();
@@ -90,6 +80,11 @@ function goToTerms(type: "service" | "privacy") {
<template> <template>
<ion-page> <ion-page>
<ion-header class="ion-no-border">
<ion-toolbar class="ion-toolbar">
<ion-title>注册</ion-title>
</ion-toolbar>
</ion-header>
<ion-content :fullscreen="true" class="signup-page"> <ion-content :fullscreen="true" class="signup-page">
<!-- 渐变背景 --> <!-- 渐变背景 -->
<div class="gradient-bg" /> <div class="gradient-bg" />

View File

@@ -2,6 +2,7 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import process from "node:process"; import process from "node:process";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import { VantResolver } from "@vant/auto-import-resolver";
import legacy from "@vitejs/plugin-legacy"; import legacy from "@vitejs/plugin-legacy";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import jsx from "@vitejs/plugin-vue-jsx"; import jsx from "@vitejs/plugin-vue-jsx";
@@ -33,12 +34,12 @@ export default defineConfig(({ mode }) => {
autoImport({ autoImport({
dirs: ["src/composables", "src/utils", "src/store"], dirs: ["src/composables", "src/utils", "src/store"],
imports: ["vue", "vue-router", "@vueuse/core", "vue-i18n", "pinia"], imports: ["vue", "vue-router", "@vueuse/core", "vue-i18n", "pinia"],
resolvers: [IonicResolver()], resolvers: [IonicResolver(), VantResolver()],
vueTemplate: true, vueTemplate: true,
}), }),
components({ components({
directoryAsNamespace: true, directoryAsNamespace: true,
resolvers: [IonicResolver(), iconsResolver({ prefix: "i" })], resolvers: [IonicResolver(), VantResolver(), iconsResolver({ prefix: "i" })],
}), }),
VitePWA({ VitePWA({
registerType: "autoUpdate", registerType: "autoUpdate",