5 Commits

Author SHA1 Message Date
严浩
96728d87fd feat(src/utils/a2use.vue): 添加 SafeNFormItem 组件用于电话号码验证
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 3m28s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m17s
2025-10-31 13:16:22 +08:00
严浩
66419d22d4 refactor: 移除未使用的 RouterView 组件,优化 a2use 组件的模板和 SafeNFormItem 的实现
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 3m25s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m29s
2025-10-31 12:53:59 +08:00
严浩
18cb623730 refactor: 优化 SafeNFormItem 组件的类型定义和插槽处理
Some checks failed
CI/CD Pipeline / playwright (push) Successful in 2m38s
CI/CD Pipeline / build-and-deploy (push) Failing after 3m13s
2025-10-31 11:14:13 +08:00
严浩
4c7f052ea1 feat: 更新 useSafeNForm 组件,优化类型定义 [skip ci] 2025-10-31 10:28:12 +08:00
严浩
13b535c530 feat: 添加 useSafeNForm 组件及相关功能,更新依赖项,修复类型定义 [skip ci] 2025-10-31 01:12:55 +08:00
40 changed files with 604 additions and 1425 deletions

View File

@@ -38,9 +38,7 @@ jobs:
run: pnpm run lint run: pnpm run lint
- name: 📦 构建项目 - name: 📦 构建项目
run: | run: pnpm run build-only
export VITE_APP_BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
pnpm run build-only
env: env:
VITE_APP_BUILD_COMMIT: ${{ github.sha }} VITE_APP_BUILD_COMMIT: ${{ github.sha }}

View File

@@ -6,23 +6,22 @@
"source.fixAll.oxc": "explicit", "source.fixAll.oxc": "explicit",
"source.organizeImports": "never" "source.organizeImports": "never"
}, },
"eslint.enable": true,
"stylelint.enable": true,
"oxc.enable": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"stylelint.enable": true,
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"], "stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
"scss.lint.unknownAtRules": "ignore", "scss.lint.unknownAtRules": "ignore",
"css.lint.unknownAtRules": "ignore", "css.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore", "less.lint.unknownAtRules": "ignore",
"eslint.enable": true,
"oxc.enable": true,
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
// >>>>> // >>>>>
"i18n-ally.readonly": true,
"i18n-ally.namespace": false /* @intlify/unplugin-vue-i18n */, "i18n-ally.namespace": false /* @intlify/unplugin-vue-i18n */,
"i18n-ally.localesPaths": ["src/locales/demo", "src/locales"], "i18n-ally.localesPaths": ["src/locales/demo", "src/locales"],
// https://github.com/lokalise/i18n-ally/wiki/Path-Matcher // https://github.com/lokalise/i18n-ally/wiki/Path-Matcher

4
auto-imports.d.ts vendored
View File

@@ -111,7 +111,6 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent'] const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const routeI18nInstance: typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nInstance']
const setActivePinia: typeof import('pinia')['setActivePinia'] const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setViewportCSSVars: typeof import('utils4u/browser')['setViewportCSSVars'] const setViewportCSSVars: typeof import('utils4u/browser')['setViewportCSSVars']
@@ -157,7 +156,6 @@ declare global {
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs'] const useAttrs: typeof import('vue')['useAttrs']
const useAuthStore: typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']
const useBase64: typeof import('@vueuse/core')['useBase64'] const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery'] const useBattery: typeof import('@vueuse/core')['useBattery']
const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
@@ -456,7 +454,6 @@ declare module 'vue' {
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']> readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']> readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']> readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
readonly routeI18nInstance: UnwrapRef<typeof import('./src/locales-utils/i18n-auto-imports')['routeI18nInstance']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']> readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']> readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']> readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']>
@@ -502,7 +499,6 @@ declare module 'vue' {
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']> readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']> readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useAuthStore: UnwrapRef<typeof import('./src/stores/auth-store-auto-imports')['useAuthStore']>
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']> readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']> readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']> readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>

View File

@@ -1,17 +1,15 @@
import pluginVitest from '@vitest/eslint-plugin'; import pluginImport from 'eslint-plugin-import';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'; import { globalIgnores } from 'eslint/config';
import { import {
configureVueProject,
defineConfigWithVueTs, defineConfigWithVueTs,
vueTsConfigs, vueTsConfigs,
configureVueProject,
} from '@vue/eslint-config-typescript'; } from '@vue/eslint-config-typescript';
import pluginImport from 'eslint-plugin-import';
import pluginJsonc from 'eslint-plugin-jsonc';
import pluginOxlint from 'eslint-plugin-oxlint';
import pluginPlaywright from 'eslint-plugin-playwright';
import pluginVue from 'eslint-plugin-vue'; import pluginVue from 'eslint-plugin-vue';
import { globalIgnores } from 'eslint/config'; import pluginVitest from '@vitest/eslint-plugin';
import jsoncParser from 'jsonc-eslint-parser'; import pluginPlaywright from 'eslint-plugin-playwright';
import pluginOxlint from 'eslint-plugin-oxlint';
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
configureVueProject({ scriptLangs: ['ts', 'tsx'] }); configureVueProject({ scriptLangs: ['ts', 'tsx'] });
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup // More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
@@ -54,19 +52,6 @@ export default defineConfigWithVueTs(
}, },
}, },
{
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
files: ['src/locales/**/*.json'],
languageOptions: { parser: jsoncParser },
plugins: { jsonc: pluginJsonc },
rules: { 'jsonc/sort-keys': 'error' },
},
{ {
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',

View File

@@ -11,6 +11,40 @@
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<script>
window.addEventListener('DOMContentLoaded', function () {
window.ontouchstart = function () {};
window.ontouchend = function () {};
});
window.onloadX = function () {
// 禁止双指缩放
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
});
// 禁止双击放大
var lastTouchEnd = 0;
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
},
false,
);
// 禁止手势事件
document.addEventListener('gesturestart', function (event) {
event.preventDefault();
});
};
</script>
<meta name="color-scheme" content="light dark" /> <meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
@@ -98,40 +132,7 @@
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> --> <!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
<!-- .min.js 是 jsDelivr 的特殊处理 --> <!-- .min.js 是 jsDelivr 的特殊处理 -->
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> --> <!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
<script> </body>
window.addEventListener('DOMContentLoaded', function () {
window.ontouchstart = function () {};
window.ontouchend = function () {};
});
window.onloadX = function () {
// 禁止双指缩放
document.addEventListener('touchstart', function (event) {
if (event.touches.length > 1) {
event.preventDefault();
}
});
// 禁止双击放大
var lastTouchEnd = 0;
document.addEventListener(
'touchend',
function (event) {
var now = new Date().getTime();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
},
false,
);
// 禁止手势事件
document.addEventListener('gesturestart', function (event) {
event.preventDefault();
});
};
</script>
<script> <script>
(function (d) { (function (d) {
var config = { var config = {
@@ -162,5 +163,4 @@
s.parentNode.insertBefore(tk, s); s.parentNode.insertBefore(tk, s);
}); /* (document) */ }); /* (document) */
</script> </script>
</body>
</html> </html>

View File

@@ -103,7 +103,6 @@
"consola": "^3.4.2", "consola": "^3.4.2",
"eslint": "^9.35.0", "eslint": "^9.35.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-jsonc": "^2.21.0",
"eslint-plugin-oxlint": "~1.23.0", "eslint-plugin-oxlint": "~1.23.0",
"eslint-plugin-playwright": "^2.2.2", "eslint-plugin-playwright": "^2.2.2",
"eslint-plugin-vue": "~10.5.0", "eslint-plugin-vue": "~10.5.0",
@@ -119,7 +118,6 @@
"prettier": "3.6.2", "prettier": "3.6.2",
"rollup": "^4.52.5", "rollup": "^4.52.5",
"sass-embedded": "^1.93.2", "sass-embedded": "^1.93.2",
"sharp": "^0.34.4",
"stylelint": "^16.25.0", "stylelint": "^16.25.0",
"stylelint-config-recess-order": "^7.3.0", "stylelint-config-recess-order": "^7.3.0",
"stylelint-config-standard": "^39.0.1", "stylelint-config-standard": "^39.0.1",

1055
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { RouterView } from 'vue-router';
import AppNaiveUIProvider from './AppNaiveUIProvider.vue'; import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
import A2use from './utils/a2use.vue';
</script> </script>
<template> <template>
@@ -9,10 +9,8 @@ import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
<Toast /> <Toast />
<AppNaiveUIProvider> <AppNaiveUIProvider>
<router-view v-slot="{ Component }"> <!-- <RouterView /> -->
<transition name="fade" mode="out-in">
<component :is="Component" /> <A2use></A2use>
</transition>
</router-view>
</AppNaiveUIProvider> </AppNaiveUIProvider>
</template> </template>

View File

@@ -8,7 +8,16 @@ import IconMenuRounded from '~icons/material-symbols/menu-rounded';
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) { export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
const router = useRouter(); const router = useRouter();
const { t, te } = routeI18nInstance.global; const { t, te } = useI18n({
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: i18nRouteMessages,
});
// 获取路由表但是不包含布局路由 // 获取路由表但是不包含布局路由
const routes = createGetRoutes(router)(); const routes = createGetRoutes(router)();
@@ -17,15 +26,10 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
const selectedKey = ref(''); const selectedKey = ref('');
watch( watch(
() => router.currentRoute.value, () => router.currentRoute.value.path,
(route) => { (newPath) => {
// 优先使用 activeMenuName通过路由名称解析为路径如果没有则使用当前路径 selectedKey.value = newPath;
const activeMenuPath = route.meta.activeMenuName menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
? router.resolve({ name: route.meta.activeMenuName }).path
: route.path;
selectedKey.value = activeMenuPath;
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
}, },
{ immediate: true }, { immediate: true },
); );
@@ -109,7 +113,7 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
const pathSegments = route.path.split('/').filter(Boolean); const pathSegments = route.path.split('/').filter(Boolean);
const routeName = route.name as string; const routeName = route.name as string;
let text = te(routeName) ? t(routeName) : routeName; let text = te(routeName) ? t(routeName) : route.meta?.title || routeName;
if (import.meta.env.VITE_APP_MENU_SHOW_ORDER === 'true' && route.meta?.order) { if (import.meta.env.VITE_APP_MENU_SHOW_ORDER === 'true' && route.meta?.order) {
const order = String(route.meta.order).padStart(orderMaxLength, '0'); const order = String(route.meta.order).padStart(orderMaxLength, '0');
text = `${order}. ${text}`; text = `${order}. ${text}`;

View File

@@ -2,7 +2,6 @@
import LanguageSwitchButton from './components/LanguageSwitchButton.vue'; import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
import ThemeSwitchButton from './components/ThemeSwitchButton.vue'; import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
import ToggleSiderButton from './components/ToggleSiderButton.vue'; import ToggleSiderButton from './components/ToggleSiderButton.vue';
import UserDropdown from './components/UserDropdown.vue';
</script> </script>
<template> <template>
@@ -12,7 +11,6 @@ import UserDropdown from './components/UserDropdown.vue';
<div class="flex items-center"> <div class="flex items-center">
<LanguageSwitchButton /> <LanguageSwitchButton />
<ThemeSwitchButton /> <ThemeSwitchButton />
<UserDropdown />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -1,45 +0,0 @@
<script setup lang="tsx">
const router = useRouter();
const userStore = useAuthStore();
const dialog = useDialog();
const options = computed(() => [
{
label: userStore.userInfo?.nickname || userStore.userInfo?.username || '用户',
key: 'user',
disabled: true,
},
{
type: 'divider',
key: 'd1',
},
{
label: '退出登录',
key: 'logout',
icon: () => <icon-material-symbols-logout class="w-4 h-4" />,
},
]);
function handleSelect(key: string) {
if (key === 'logout') {
dialog.warning({
title: '退出登录',
content: '确定要退出登录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
userStore.clearToken('用户退出登录');
router.push('/login');
},
});
}
}
</script>
<template>
<NDropdown :options="options" placement="bottom-end" @select="handleSelect">
<NButton quaternary circle>
<icon-material-symbols-account-circle w-5 h-5 />
</NButton>
</NDropdown>
</template>

View File

@@ -49,4 +49,14 @@ const appStore = useAppStore();
#__SCROLL_EL_ID__ { #__SCROLL_EL_ID__ {
@include scrollbar; @include scrollbar;
} }
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style> </style>

View File

@@ -2,9 +2,7 @@
* All i18n resources specified in the plugin `include` option can be loaded * All i18n resources specified in the plugin `include` option can be loaded
* at once using the import syntax * at once using the import syntax
*/ */
import { router } from '@/plugins/00.router-plugin';
import messages from '@intlify/unplugin-vue-i18n/messages'; import messages from '@intlify/unplugin-vue-i18n/messages';
import { createGetRoutes } from 'virtual:meta-layouts';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
@@ -28,44 +26,6 @@ export const i18nInstance = createI18n({
messages, messages,
}); });
export const routeI18nInstance = createI18n({ watchEffect(() => {
legacy: false, // you must set `false`, to use Composition API
locale: locale.value,
inheritLocale: true,
useScope: 'local',
missing: (locale, key) => {
consola.warn(`菜单翻译缺失: locale=${locale}, key=${key}`);
return key;
},
fallbackRoot: true,
messages: i18nRouteMessages,
});
watchEffect(
() => {
locale.value = i18nInstance.global.locale.value; locale.value = i18nInstance.global.locale.value;
}, });
{ flush: 'sync' },
);
watch(
() => i18nInstance.global.locale.value,
() => {
routeI18nInstance.global.locale.value = i18nInstance.global.locale.value;
const routes = createGetRoutes(router)(); // 获取路由表但是不包含布局路由
routes.forEach((route) => {
const { t, te } = routeI18nInstance.global;
if (router.currentRoute.value.name) {
router.currentRoute.value.meta.title = t(router.currentRoute.value.name as string);
}
route.meta = route.meta || {};
const routeName = route.name as string;
route.meta.title = te(routeName) ? t(routeName) : routeName;
});
},
{ immediate: true, flush: 'sync' },
);

View File

@@ -1,22 +1,13 @@
/*eslint sort-keys: "error"*/
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
export default { export default {
Root: 'Index',
$Path: '$Path', $Path: '$Path',
Demos: 'Demos', Demos: 'Demos',
DemosApiDemo: 'API Demo', DemosApiDemo: 'API Demo',
DemosCounterDemo: 'Counter Demo', DemosCounterDemo: 'Counter Demo',
DemosCreate: 'Create Demo',
DemosI18nDemo: 'i18n Demo', DemosI18nDemo: 'i18n Demo',
DemosNaiveUiDemo: 'Naive UI Demo', DemosNaiveUiDemo: 'Naive UI Demo',
DemosPrimevueDemo: 'PrimeVue Demo', DemosPrimevueDemo: 'PrimeVue Demo',
DemosWebsocketDemo: 'WebSocket Demo', DemosWebsocketDemo: 'WebSocket Demo',
Home: 'Home', Home: 'Home',
Login: 'Login', Login: 'Login',
Root: 'Index',
} satisfies PageTitleLocalizations; } satisfies PageTitleLocalizations;

View File

@@ -1,7 +1,7 @@
import type { I18nOptions } from 'vue-i18n'; import type { I18nOptions } from 'vue-i18n';
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], { const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
eager: true /* true 为同步false 为异步 */, eager: true,
import: 'default', import: 'default',
}); });

View File

@@ -1,22 +1,13 @@
/*eslint sort-keys: "error"*/
/**
* 启用 sort-keys 规则以强制对象键按字母顺序排序
* 原因:
* 1. 减少多人协作时的合并冲突
* 2. 保持代码一致性,提高可维护性
*/
export default { export default {
Root: '根 (Gēn)',
$Path: '$Path', $Path: '$Path',
Demos: '示例演示', Demos: '示例演示',
DemosApiDemo: 'API 调用示例', DemosApiDemo: 'API 调用示例',
DemosCounterDemo: '点击计数器', DemosCounterDemo: '点击计数器',
DemosCreate: '创建示例',
DemosI18nDemo: '国际化示例', DemosI18nDemo: '国际化示例',
DemosNaiveUiDemo: 'Naive UI 组件示例', DemosNaiveUiDemo: 'Naive UI 组件示例',
DemosPrimevueDemo: 'PrimeVue 组件示例', DemosPrimevueDemo: 'PrimeVue 组件示例',
DemosWebsocketDemo: 'WebSocket 示例', DemosWebsocketDemo: 'WebSocket 示例',
Home: '首页', Home: '首页',
Login: '登录', Login: '登录',
Root: '根 (Gēn)',
} satisfies PageTitleLocalizations; } satisfies PageTitleLocalizations;

View File

@@ -1,10 +1,10 @@
{ {
"page": { "page": {
"i18n-demo": { "i18n-demo": {
"change-language": "Change Language", "title": "Vue I18n Demo",
"current-language": "Current Language", "current-language": "Current Language",
"hello": "Hello, {name}!", "change-language": "Change Language",
"title": "Vue I18n Demo" "hello": "Hello, {name}!"
} }
} }
} }

View File

@@ -1,10 +1,10 @@
{ {
"page": { "page": {
"i18n-demo": { "i18n-demo": {
"change-language": "切换语言", "title": "Vue I18n 示例",
"current-language": "当前语言", "current-language": "当前语言",
"hello": "你好, {name}", "change-language": "切换语言",
"title": "Vue I18n 示例" "hello": "你好, {name}"
} }
} }
} }

View File

@@ -1,13 +1,14 @@
import './styles/index.ts'; import './styles/index.ts';
import { LogLevels } from 'consola'; import { LogLevels } from 'consola';
import App from './App.vue'; import App from './App.vue';
import { setupPlugins } from './plugins'; import { setupPlugins } from './plugins';
consola.level = LogLevels.verbose; consola.level = LogLevels.verbose;
const app = createApp(App); const autoInstallModules = import.meta.glob('./plugins/!(index).ts', {
setupPlugins(app); eager: true /* true 为同步false 为异步 */,
});
const app = setupPlugins(createApp(App), autoInstallModules);
await new Promise((resolve) => setTimeout(resolve, 280)); await new Promise((resolve) => setTimeout(resolve, 280));
app.mount('#app'); app.mount('#app');

View File

@@ -1,87 +1,5 @@
<script setup lang="ts"> <script setup lang="ts"></script>
definePage({ meta: { ignoreAuth: true, layout: false } });
const router = useRouter();
const userStore = useAuthStore();
const message = useMessage();
const formValue = ref({
username: 'admin',
password: 'admin',
});
const loading = ref(false);
async function handleLogin() {
if (!formValue.value.username || !formValue.value.password) {
message.warning('请输入用户名和密码');
return;
}
loading.value = true;
try {
const result = await userStore.login(formValue.value.username, formValue.value.password);
if (result.success) {
message.success('登录成功');
router.push('/');
} else {
message.error(result.message || '登录失败');
}
} catch {
message.error('登录异常');
} finally {
loading.value = false;
}
}
</script>
<template> <template>
<div class="login-page"> <div>Login</div>
<NCard class="login-card" title="用户登录">
<NForm :model="formValue" label-placement="left" label-width="80">
<NFormItem label="用户名" path="username">
<NInput
v-model:value="formValue.username"
placeholder="请输入用户名"
@keyup.enter="handleLogin"
/>
</NFormItem>
<NFormItem label="密码" path="password">
<NInput
v-model:value="formValue.password"
type="password"
show-password-on="click"
placeholder="请输入密码"
@keyup.enter="handleLogin"
/>
</NFormItem>
<NFormItem :show-label="false">
<NButton type="primary" block :loading="loading" @click="handleLogin"> 登录 </NButton>
</NFormItem>
</NForm>
<div class="login-hint">
<NText depth="3">提示用户名和密码均为 admin</NText>
</div>
</NCard>
</div>
</template> </template>
<style scoped lang="scss">
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-card {
width: 400px;
max-width: 90%;
}
.login-hint {
margin-top: 16px;
text-align: center;
}
</style>

View File

@@ -1,29 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
defineProps<{ path: string }>(); defineProps<{ path: string }>();
declare global {
interface Window {
stack?: ReturnType<typeof createStackGuard>;
}
}
const stack = window?.stack;
const canGoBack = stack && stack.length > 1;
const router = useRouter();
function handleBack() {
if (canGoBack) {
router.back();
} else {
router.push('/');
}
}
</script> </script>
<template> <template>
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4"> <main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
<h1>Not Found</h1> <h1>Not Found</h1>
<p>{{ path }} does not exist.</p> <p>{{ path }} does not exist.</p>
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button> <Button @click="$router.back()">Back</Button>
</main> </main>
</template> </template>

View File

@@ -1,12 +0,0 @@
<script setup lang="ts">
definePage({
meta: {
hideInMenu: true,
activeMenuName: 'Demos',
},
});
</script>
<template>
<div></div>
</template>

View File

@@ -21,8 +21,6 @@ function setLocale(newLocale: 'en-US' | 'zh-CN') {
{{ t('page.i18n-demo.hello', { name: 'Kilo' }) }} {{ t('page.i18n-demo.hello', { name: 'Kilo' }) }}
</n-p> </n-p>
<n-p> $route.meta: {{ $route.meta }} </n-p>
<n-space> <n-space>
<n-button type="primary" @click="setLocale('en-US')"> English </n-button> <n-button type="primary" @click="setLocale('en-US')"> English </n-button>
<n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button> <n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDialog, useMessage, useModal } from 'naive-ui';
import type { MessageType } from 'naive-ui'; import type { MessageType } from 'naive-ui';
import { useDialog, useMessage } from 'naive-ui';
import UseSafeNForm from './use-safe-n-form.vue';
definePage({ meta: {} }); definePage({ meta: {} });
const message = useMessage(); const message = useMessage();
const dialog = useDialog(); const dialog = useDialog();
const modal = useModal();
const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[]; const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[];
const dialogTypes = ['info', 'success', 'warning', 'error'] as const; const dialogTypes = ['info', 'success', 'warning', 'error'] as const;
@@ -38,10 +38,11 @@ const openDialog = (type: (typeof dialogTypes)[number]) => {
}; };
const openModal = () => { const openModal = () => {
window.$nModal!.create({ modal.create({
title: '命令式 Modal 示例', title: '命令式 Modal 示例',
content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。', content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。',
preset: 'dialog', preset: 'dialog',
maskClosable: false,
onPositiveClick: () => { onPositiveClick: () => {
message.success('点击了确定'); message.success('点击了确定');
}, },
@@ -61,9 +62,7 @@ const openModal = () => {
<NAlert title="信息" type="info" :bordered="false"> <NAlert title="信息" type="info" :bordered="false">
演示 Naive UI 各种组件的使用方法和功能特性 演示 Naive UI 各种组件的使用方法和功能特性
</NAlert> </NAlert>
<n-card title="SafeNForm" mt-4>
<UseSafeNForm />
</n-card>
<NCard title="Message 消息" class="mt-4"> <NCard title="Message 消息" class="mt-4">
<NSpace> <NSpace>
<NButton <NButton

View File

@@ -1,72 +0,0 @@
<script setup lang="ts">
const { formInst, formValue, SafeNForm, SafeNFormItem } = useSafeNForm({
initialFormValue: {
/* ⚠️:
如果没使用`SafeNFormItem`
这里`user`对象没有手动初始化的话,将会报错:
`can't access property "name", $setup.formValue.user is undefined`
*/
user: {
name: '',
age: 0,
},
phone: '',
},
});
function handleValidateClick() {
formInst.value?.validate((errors) => {
if (!errors) {
window.$nMessage!.success('Valid');
} else {
console.log(errors);
window.$nMessage!.error('Invalid');
}
});
}
</script>
<template>
<div border>
<pre>formValue: {{ JSON.stringify(formValue, null, 2) }}</pre>
</div>
<SafeNForm inline label-placement="left" label-width="auto" mt-4>
<n-form-item
label="姓名"
path="user.name"
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
>
<n-input v-model:value="formValue.user.name" placeholder="输入姓名" />
</n-form-item>
<SafeNFormItem
#default="{ value, setValue }"
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
label="姓名"
path="user.name"
>
<NInput :value="value" placeholder="SafeNFormItem" @update:value="setValue" />
</SafeNFormItem>
<n-form-item
label="电话号码"
path="phone"
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
>
<n-input v-model:value="formValue.phone" placeholder="电话号码" />
</n-form-item>
<SafeNFormItem
label="电话号码"
path="phone"
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
>
<!-- 如果没有提供插槽会默认渲染一个`<NInput>` -->
</SafeNFormItem>
<n-form-item>
<n-button attr-type="button" @click="handleValidateClick"> 验证 </n-button>
</n-form-item>
</SafeNForm>
</template>

View File

@@ -2,6 +2,6 @@
<template> <template>
<div> <div>
<n-button @click="$router.push({ name: 'DemosCreate' })">DemosCreate</n-button> <h1>Index Page</h1>
</div> </div>
</template> </template>

View File

@@ -10,4 +10,17 @@ export function install({ app }: { app: import('vue').App<Element> }) {
// 2. 显示全局错误提示 // 2. 显示全局错误提示
// 3. 进行错误分析和处理 // 3. 进行错误分析和处理
}; };
// if (import.meta.env.MODE === 'development' && '1' === ('2' as never)) {
// // TODO: https://github.com/hu3dao/vite-plugin-debug/
// // https://eruda.liriliri.io/zh/docs/#快速上手
// import('eruda').then(({ default: eruda }) => {
// eruda.init({
// defaults: {
// transparency: 0.9,
// },
// })
// /* eruda.show(); */
// })
// }
} }

View File

@@ -2,8 +2,8 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
import { setupLayouts } from 'virtual:meta-layouts'; import { setupLayouts } from 'virtual:meta-layouts';
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts'; // import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import type { Router } from 'vue-router'; import type { RouteNamedMap } from 'vue-router/auto-routes';
import { handleHotUpdate, routes } from 'vue-router/auto-routes'; import { routes, handleHotUpdate } from 'vue-router/auto-routes';
const setupLayoutsResult = setupLayouts(routes); const setupLayoutsResult = setupLayouts(routes);
const router = createRouter({ const router = createRouter({
@@ -15,10 +15,6 @@ const router = createRouter({
strict: true, strict: true,
}); });
router.isReady().then(() => {
console.debug('✅ [router is ready]');
});
router.onError((error) => { router.onError((error) => {
console.debug('🚨 [router error]:', error); console.debug('🚨 [router error]:', error);
}); });
@@ -37,24 +33,50 @@ export function install({ app }: { app: import('vue').App<Element> }) {
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整 // 警告:路由守卫的创建顺序会影响执行流程,请勿调整
createNProgressGuard(router); createNProgressGuard(router);
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router); if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
Object.assign(window, { stack: createStackGuard(router) }); Object.assign(globalThis, { stack: createStackGuard(router) });
// >>>
Object.values(
import.meta.glob<{
createGuard?: (router: Router) => void;
}>('./router-guard/*.ts', { eager: true /* true 为同步false 为异步 */ }),
).forEach((module) => {
module.createGuard?.(router);
});
// <<<
} }
if (__DEV__) Object.assign(window, { router }); declare module 'vue-router' {
/* definePage({ meta: { title: '示例演示' } }); */
interface RouteMeta {
/**
* @description 是否在菜单中隐藏
*/
hideInMenu?: boolean;
/**
* @description 菜单标题
* @deprecated //!⚠️请通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
*/
title?: string;
/**
* @description 使用的布局,设置为 false 则表示不使用布局
*/
layout?: string | false;
/**
* @description 菜单项是否渲染为可点击链接,默认为 true
* - true: 使用 RouterLink 包装,可点击跳转
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
*/
link?: boolean;
/**
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
*/
order?: number;
}
}
export { router, setupLayoutsResult };
declare global {
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
}
if (__DEV__) Object.assign(globalThis, { router });
// This will update routes at runtime without reloading the page // This will update routes at runtime without reloading the page
if (import.meta.hot) { if (import.meta.hot) {
handleHotUpdate(router); handleHotUpdate(router);
} }
export { router, setupLayoutsResult };

View File

@@ -1,50 +0,0 @@
import type { RouteNamedMap } from 'vue-router/auto-routes';
declare global {
type PageTitleLocalizations = Record<keyof RouteNamedMap, string>;
}
declare module 'vue-router' {
/* definePage({ meta: { title: '示例演示' } }); */
interface RouteMeta {
/**
* @description 是否在菜单中隐藏
*/
hideInMenu?: boolean;
/**
* @description 菜单标题 // !⚠️通过多语言标题方案(搜`PageTitleLocalizations`)维护标题
*/
title?: string;
/**
* @description 使用的布局,设置为 false 则表示不使用布局
*/
layout?: string | false;
/**
* @description 菜单项是否渲染为可点击链接,默认为 true
* - true: 使用 RouterLink 包装,可点击跳转
* - false: 仅渲染纯文本标签,不可点击(适用于分组标题)
*/
link?: boolean;
/**
* @description 菜单排序权重,数值越小越靠前,未设置则按路径字母顺序排序
*/
order?: number;
/**
* @description 是否忽略权限,默认为 false
*/
ignoreAuth?: boolean;
/**
* @description 当前路由激活时应该高亮的菜单项(通过路由名称指定)
* - 用于隐藏在菜单中的子页面,指定其父级菜单项应该高亮
* - 使用路由名称而非路径,提供更好的类型安全和重构友好性
* - 例如:`activeMenuName: 'Demos'` 会高亮 Demos 菜单项
*/
activeMenuName?: keyof RouteNamedMap;
}
}

View File

@@ -4,18 +4,13 @@
type UserPlugin = (ctx: UserPluginContext) => void; type UserPlugin = (ctx: UserPluginContext) => void;
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin }; type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
type UserPluginContext = { app: import('vue').App<Element> }; type UserPluginContext = { app: import('vue').App<Element> };
export function setupPlugins(
const autoInstallModules: AutoInstallModule = import.meta.glob( app: import('vue').App,
['./*.ts', '!./**/*.types.ts', '!./index.ts'], modules: AutoInstallModule | Record<string, unknown>,
{ ) {
eager: true /* true 为同步false 为异步 */, console.group('🔌 Plugins');
}, for (const path in modules) {
); const module = modules[path] as AutoInstallModule;
export function setupPlugins(app: import('vue').App) {
console.group(`🔌 Installing ${Object.keys(autoInstallModules).length} plugins`);
for (const path in autoInstallModules) {
const module = autoInstallModules[path] as AutoInstallModule;
if (module.install) { if (module.install) {
module.install({ app }); module.install({ app });
console.debug(`%c✔ ${path}`, 'color: #07a'); console.debug(`%c✔ ${path}`, 'color: #07a');

View File

@@ -1,28 +0,0 @@
import type { Router } from 'vue-router';
export function createGuard(router: Router) {
router.beforeEach(async (to /* , from */) => {
const userStore = useAuthStore();
if (to.name === 'Login') {
userStore.clearToken('User navigated to login page');
}
if (to.meta.ignoreAuth) {
return true;
}
if (!userStore.isLoggedIn) {
console.debug('🔑 [permission-guard] 用户未登录,重定向到登录页');
return { name: 'Login' };
}
});
router.beforeResolve(async (/* to, from */) => {
const userStore = useAuthStore();
if (userStore.isLoggedIn && !userStore.userInfo) {
console.debug('🔑 [permission-guard] 用户信息不存在,尝试获取用户信息');
await userStore.fetchUserInfo();
}
});
}

View File

@@ -1,45 +0,0 @@
export const useAuthStore = defineStore('auth', () => {
const token = useLocalStorage<string | null>('auth-token', null);
const userInfo = ref<Record<string, any> | null>(null);
const isLoggedIn = computed(() => !!token.value);
function clearToken(reason?: string) {
consola.info('🚮 [auth-store] clear: ', reason);
token.value = null;
userInfo.value = null;
}
async function login(username: string, password: string) {
// 模拟登录延迟
await new Promise((resolve) => setTimeout(resolve, 500));
// 模拟验证
if (username === 'admin' && password === 'admin') {
token.value = `mock-token-${Date.now()}`;
await fetchUserInfo();
return { success: true };
}
return { success: false, message: '用户名或密码错误' };
}
async function fetchUserInfo() {
if (!token.value) {
return;
}
// 模拟获取用户信息延迟
await new Promise((resolve) => setTimeout(resolve, 300));
// 模拟从服务器获取用户信息
userInfo.value = {
id: 1,
username: 'admin',
nickname: '管理员',
roles: ['admin'],
};
}
return { token, isLoggedIn, userInfo, clearToken, login, fetchUserInfo };
});

View File

@@ -1,9 +0,0 @@
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.25s ease-in-out;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@@ -1,8 +1,6 @@
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" /> import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
import 'primeicons/primeicons.css'; import 'primeicons/primeicons.css';
import './reset-primevue.css';
import './css/reset-primevue.css';
import './css/transition.css';
import 'virtual:uno.css'; import 'virtual:uno.css';

90
src/utils/a2use.vue Normal file
View File

@@ -0,0 +1,90 @@
<script setup lang="ts">
import { get, set } from 'lodash-es';
const { formValue, SafeNForm, SafeNFormItem, formRef } = useSafeNForm({
initialFormValue: {
/* ⚠️:
如果没使用`SafeNFormItem`
这里`user`对象没有手动初始化的话,将会报错:
`can't access property "name", $setup.formValue.user is undefined`
*/
user: {
name: '',
age: 0,
},
phone: '',
},
});
function handleSetUserName() {
set(formValue.value, 'user.name', 'Alice');
}
function handleValidateClick() {
formRef.value?.validate((errors) => {
if (!errors) {
window.$nMessage!.success('Valid');
} else {
console.log(errors);
window.$nMessage!.error('Invalid');
}
});
}
</script>
<template>
<div p-4>
<div border>
<div>
<pre>formValue: {{ JSON.stringify(formValue, null, 2) }}</pre>
</div>
<div>get(formValue, 'user.name'): {{ get(formValue, 'user.name') }}</div>
</div>
<n-space mt-4>
<n-button @click="handleSetUserName">Set user.name</n-button>
</n-space>
<n-space item-class="flex-1">
<n-card title="SafeForm" mt-4>
<SafeNForm label-placement="left" label-width="auto">
<n-form-item
label="姓名"
path="user.name"
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
>
<n-input v-model:value="formValue.user.name" placeholder="输入姓名" />
</n-form-item>
<SafeNFormItem
#default="{ value, setValue }"
:rule="{ required: true, message: '请输入姓名', trigger: ['input'] }"
label="姓名"
path="user.name"
>
<NInput :value="value" placeholder="SafeNFormItem" @update:value="setValue" />
</SafeNFormItem>
<n-form-item
label="电话号码"
path="phone"
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
>
<n-input v-model:value="formValue.phone" placeholder="电话号码" />
</n-form-item>
<SafeNFormItem
label="电话号码"
path="phone"
:rule="{ required: true, message: '请输入电话号码', trigger: ['blur'] }"
>
<!-- 如果没有提供插槽会默认渲染一个`<NInput>` -->
</SafeNFormItem>
<n-form-item>
<n-button attr-type="button" @click="handleValidateClick"> 验证 </n-button>
</n-form-item>
</SafeNForm>
</n-card>
</n-space>
</div>
</template>

View File

@@ -18,7 +18,7 @@ type UseSafeNFormOptions<FormValue> = {
export function useSafeNForm<T extends Record<string, any> = Record<string, unknown>>( export function useSafeNForm<T extends Record<string, any> = Record<string, unknown>>(
options: UseSafeNFormOptions<T> = {}, options: UseSafeNFormOptions<T> = {},
) { ) {
const formInst = ref<FormInst | null>(null); const formRef = ref<FormInst | null>(null);
const formValue = ref<T>(structuredClone(toRaw(options.initialFormValue)) || ({} as T)); const formValue = ref<T>(structuredClone(toRaw(options.initialFormValue)) || ({} as T));
// 创建类型安全的 Form 组件 // 创建类型安全的 Form 组件
@@ -35,7 +35,7 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
{...props} {...props}
model={formValue.value} model={formValue.value}
ref={(inst) => { ref={(inst) => {
formInst.value = inst as unknown as FormInst; formRef.value = inst as unknown as FormInst;
}} }}
> >
{ctx.slots.default?.({})} {ctx.slots.default?.({})}
@@ -44,7 +44,7 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
}, },
{ {
name: 'SafeNForm', name: 'SafeNForm',
inheritAttrs: true, inheritAttrs: false,
props: formProps, props: formProps,
}, },
); );
@@ -116,6 +116,6 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
formValue, formValue,
SafeNForm, SafeNForm,
SafeNFormItem, SafeNFormItem,
formInst, formRef,
}; };
} }

15
typed-router.d.ts vendored
View File

@@ -58,13 +58,6 @@ declare module 'vue-router/auto-routes' {
Record<never, never>, Record<never, never>,
| never | never
>, >,
'DemosCreate': RouteRecordInfo<
'DemosCreate',
'/demos/create',
Record<never, never>,
Record<never, never>,
| never
>,
'DemosI18nDemo': RouteRecordInfo< 'DemosI18nDemo': RouteRecordInfo<
'DemosI18nDemo', 'DemosI18nDemo',
'/demos/i18n-demo', '/demos/i18n-demo',
@@ -150,19 +143,13 @@ declare module 'vue-router/auto-routes' {
views: views:
| never | never
} }
'src/pages/demos/create.page.vue': {
routes:
| 'DemosCreate'
views:
| never
}
'src/pages/demos/i18n-demo.page.vue': { 'src/pages/demos/i18n-demo.page.vue': {
routes: routes:
| 'DemosI18nDemo' | 'DemosI18nDemo'
views: views:
| never | never
} }
'src/pages/demos/naive-ui-demo/index.page.vue': { 'src/pages/demos/naive-ui-demo.page.vue': {
routes: routes:
| 'DemosNaiveUiDemo' | 'DemosNaiveUiDemo'
views: views:

View File

@@ -26,73 +26,7 @@ function IndexHtmlPlugin(): PluginOption {
}; };
} }
function ___(): PluginOption {
// https://github.com/hu3dao/vite-plugin-debug/blob/2935025e8ce082b9a5aef04766bcae3e996b3e55/src/index.ts
return {
name: 'vant-touch-emulator-online',
apply: 'build',
transformIndexHtml(html) {
return {
html,
tags: [
{
tag: 'script',
attrs: {
src: 'https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js',
// 这里的 `.min.js` 是 jsDelivr 的特殊处理
// src: 'https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js',
},
injectTo: 'body',
},
// >>>>> eruda
{
tag: 'script',
attrs: {
src: 'https://testingcf.jsdelivr.net/npm/eruda/eruda.js',
},
injectTo: 'body',
},
{
tag: 'script',
children: `eruda.init();`,
injectTo: 'body',
},
// https://eruda.liriliri.io/zh/docs/#快速上手
// import('eruda').then(({ default: eruda }) => {
// eruda.init({
// defaults: {
// transparency: 0.9,
// },
// })
// /* eruda.show(); */
// })
// }
// <<<<<
// >>>>> vConsole
{
tag: 'script',
attrs: {
src: 'https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js',
},
injectTo: 'body',
},
{
tag: 'script',
children: `new window.VConsole();`,
injectTo: 'body',
},
// <<<<<
],
};
},
};
}
export function loadPlugin(_configEnv: ConfigEnv): PluginOption { export function loadPlugin(_configEnv: ConfigEnv): PluginOption {
// return [___()];
const env = loadEnv(_configEnv.mode, process.cwd()); const env = loadEnv(_configEnv.mode, process.cwd());
if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin(); if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin();
} }

View File

@@ -1,5 +1,5 @@
/* eslint-disable */ /* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: b18d7aec4937222767b077e627f9f927) // Generated by Wrangler by running `wrangler types` (hash: 0003c1b3cd56e3cc5549efef6b4c6d3d)
// Runtime types generated with workerd@1.20251008.0 2025-09-09 // Runtime types generated with workerd@1.20251008.0 2025-09-09
declare namespace Cloudflare { declare namespace Cloudflare {
interface GlobalProps { interface GlobalProps {
@@ -7,13 +7,13 @@ declare namespace Cloudflare {
} }
interface Env { interface Env {
KV: KVNamespace; KV: KVNamespace;
VITE_APP_BUILD_TIME: string;
VITE_APP_BUILD_COMMIT: string;
VITE_BUILD_SOURCE_MAP: string; VITE_BUILD_SOURCE_MAP: string;
VITE_BUILD_MINIFY: string; VITE_BUILD_MINIFY: string;
VITE_CLOUDFLARE_SERVER_ENABLED: string; VITE_CLOUDFLARE_SERVER_ENABLED: string;
VITE_APP_TITLE: string; VITE_APP_TITLE: string;
VITE_APP_BASE: string; VITE_APP_BASE: string;
VITE_APP_BUILD_COMMIT: string;
VITE_APP_BUILD_TIME: string;
VITE_APP_ENABLE_VUE_DEVTOOLS: string; VITE_APP_ENABLE_VUE_DEVTOOLS: string;
VITE_APP_MENU_SHOW_DEMOS: string; VITE_APP_MENU_SHOW_DEMOS: string;
VITE_APP_MENU_SHOW_ORDER: string; VITE_APP_MENU_SHOW_ORDER: string;