Compare commits
5 Commits
renovate/p
...
safe-form
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96728d87fd | ||
|
|
66419d22d4 | ||
|
|
18cb623730 | ||
|
|
4c7f052ea1 | ||
|
|
13b535c530 |
4
.github/workflows/ci-cd.yaml
vendored
4
.github/workflows/ci-cd.yaml
vendored
@@ -38,9 +38,7 @@ jobs:
|
||||
run: pnpm run lint
|
||||
|
||||
- name: 📦 构建项目
|
||||
run: |
|
||||
export VITE_APP_BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
|
||||
pnpm run build-only
|
||||
run: pnpm run build-only
|
||||
env:
|
||||
VITE_APP_BUILD_COMMIT: ${{ github.sha }}
|
||||
|
||||
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -6,23 +6,22 @@
|
||||
"source.fixAll.oxc": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
"eslint.enable": true,
|
||||
"stylelint.enable": true,
|
||||
"oxc.enable": true,
|
||||
"editor.formatOnSave": true,
|
||||
|
||||
"stylelint.enable": true,
|
||||
"stylelint.validate": ["css", "less", "postcss", "scss", "vue"],
|
||||
"scss.lint.unknownAtRules": "ignore",
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"less.lint.unknownAtRules": "ignore",
|
||||
|
||||
"eslint.enable": true,
|
||||
"oxc.enable": true,
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
|
||||
// >>>>>
|
||||
"i18n-ally.readonly": true,
|
||||
"i18n-ally.namespace": false /* 禁用命名空间(@intlify/unplugin-vue-i18n不支持吧?) */,
|
||||
"i18n-ally.localesPaths": ["src/locales/demo", "src/locales"],
|
||||
// https://github.com/lokalise/i18n-ally/wiki/Path-Matcher
|
||||
|
||||
4
auto-imports.d.ts
vendored
4
auto-imports.d.ts
vendored
@@ -111,7 +111,6 @@ declare global {
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
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 setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setViewportCSSVars: typeof import('utils4u/browser')['setViewportCSSVars']
|
||||
@@ -157,7 +156,6 @@ declare global {
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
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 useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
@@ -456,7 +454,6 @@ declare module 'vue' {
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
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 setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setViewportCSSVars: UnwrapRef<typeof import('utils4u/browser')['setViewportCSSVars']>
|
||||
@@ -502,7 +499,6 @@ declare module 'vue' {
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
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 useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import pluginVitest from '@vitest/eslint-plugin';
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import { globalIgnores } from 'eslint/config';
|
||||
import {
|
||||
configureVueProject,
|
||||
defineConfigWithVueTs,
|
||||
vueTsConfigs,
|
||||
configureVueProject,
|
||||
} 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 { globalIgnores } from 'eslint/config';
|
||||
import jsoncParser from 'jsonc-eslint-parser';
|
||||
import pluginVitest from '@vitest/eslint-plugin';
|
||||
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'] });
|
||||
// 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: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
|
||||
70
index.html
70
index.html
@@ -11,6 +11,40 @@
|
||||
name="viewport"
|
||||
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="format-detection" content="telephone=no" />
|
||||
@@ -98,40 +132,7 @@
|
||||
<!-- <script src="https://testingcf.jsdelivr.net/npm/@vant/touch-emulator/dist/index.min.js"></script> -->
|
||||
<!-- .min.js 是 jsDelivr 的特殊处理 -->
|
||||
<!-- <script src="https://unpkg.luckincdn.com/@vant/touch-emulator@1.4.0/dist/index.js"></script> -->
|
||||
<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>
|
||||
</body>
|
||||
<script>
|
||||
(function (d) {
|
||||
var config = {
|
||||
@@ -162,5 +163,4 @@
|
||||
s.parentNode.insertBefore(tk, s);
|
||||
}); /* (document) */
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"packageManager": "pnpm@10.20.0",
|
||||
"packageManager": "pnpm@10.18.3",
|
||||
"name": "vue-ts-example-2025",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
@@ -103,7 +103,6 @@
|
||||
"consola": "^3.4.2",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-jsonc": "^2.21.0",
|
||||
"eslint-plugin-oxlint": "~1.23.0",
|
||||
"eslint-plugin-playwright": "^2.2.2",
|
||||
"eslint-plugin-vue": "~10.5.0",
|
||||
@@ -119,7 +118,6 @@
|
||||
"prettier": "3.6.2",
|
||||
"rollup": "^4.52.5",
|
||||
"sass-embedded": "^1.93.2",
|
||||
"sharp": "^0.34.4",
|
||||
"stylelint": "^16.25.0",
|
||||
"stylelint-config-recess-order": "^7.3.0",
|
||||
"stylelint-config-standard": "^39.0.1",
|
||||
|
||||
1027
pnpm-lock.yaml
generated
1027
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
src/App.vue
10
src/App.vue
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router';
|
||||
import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
|
||||
import A2use from './utils/a2use.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -9,10 +9,8 @@ import AppNaiveUIProvider from './AppNaiveUIProvider.vue';
|
||||
<Toast />
|
||||
|
||||
<AppNaiveUIProvider>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<!-- <RouterView /> -->
|
||||
|
||||
<A2use></A2use>
|
||||
</AppNaiveUIProvider>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,16 @@ import IconMenuRounded from '~icons/material-symbols/menu-rounded';
|
||||
export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<MenuInst | null> }) {
|
||||
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)();
|
||||
@@ -17,15 +26,10 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
||||
const selectedKey = ref('');
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(route) => {
|
||||
// 优先使用 activeMenuName(通过路由名称解析为路径),如果没有则使用当前路径
|
||||
const activeMenuPath = route.meta.activeMenuName
|
||||
? router.resolve({ name: route.meta.activeMenuName }).path
|
||||
: route.path;
|
||||
|
||||
selectedKey.value = activeMenuPath;
|
||||
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
|
||||
() => router.currentRoute.value.path,
|
||||
(newPath) => {
|
||||
selectedKey.value = newPath;
|
||||
menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -109,7 +113,7 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
||||
const pathSegments = route.path.split('/').filter(Boolean);
|
||||
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) {
|
||||
const order = String(route.meta.order).padStart(orderMaxLength, '0');
|
||||
text = `${order}. ${text}`;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import LanguageSwitchButton from './components/LanguageSwitchButton.vue';
|
||||
import ThemeSwitchButton from './components/ThemeSwitchButton.vue';
|
||||
import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
||||
import UserDropdown from './components/UserDropdown.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,7 +11,6 @@ import UserDropdown from './components/UserDropdown.vue';
|
||||
<div class="flex items-center">
|
||||
<LanguageSwitchButton />
|
||||
<ThemeSwitchButton />
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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>
|
||||
@@ -49,4 +49,14 @@ const appStore = useAppStore();
|
||||
#__SCROLL_EL_ID__ {
|
||||
@include scrollbar;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* All i18n resources specified in the plugin `include` option can be loaded
|
||||
* at once using the import syntax
|
||||
*/
|
||||
import { router } from '@/plugins/00.router-plugin';
|
||||
import messages from '@intlify/unplugin-vue-i18n/messages';
|
||||
import { createGetRoutes } from 'virtual:meta-layouts';
|
||||
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
@@ -28,44 +26,6 @@ export const i18nInstance = createI18n({
|
||||
messages,
|
||||
});
|
||||
|
||||
export const routeI18nInstance = createI18n({
|
||||
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(
|
||||
() => {
|
||||
watchEffect(() => {
|
||||
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' },
|
||||
);
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
/*eslint sort-keys: "error"*/
|
||||
/**
|
||||
* 启用 sort-keys 规则以强制对象键按字母顺序排序
|
||||
* 原因:
|
||||
* 1. 减少多人协作时的合并冲突
|
||||
* 2. 保持代码一致性,提高可维护性
|
||||
*/
|
||||
|
||||
export default {
|
||||
Root: 'Index',
|
||||
$Path: '$Path',
|
||||
Demos: 'Demos',
|
||||
DemosApiDemo: 'API Demo',
|
||||
DemosCounterDemo: 'Counter Demo',
|
||||
DemosCreate: 'Create Demo',
|
||||
DemosI18nDemo: 'i18n Demo',
|
||||
DemosNaiveUiDemo: 'Naive UI Demo',
|
||||
DemosPrimevueDemo: 'PrimeVue Demo',
|
||||
DemosWebsocketDemo: 'WebSocket Demo',
|
||||
Home: 'Home',
|
||||
Login: 'Login',
|
||||
Root: 'Index',
|
||||
} satisfies PageTitleLocalizations;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { I18nOptions } from 'vue-i18n';
|
||||
|
||||
const modules = import.meta.glob(['./*.ts', '!./route-messages-auto-imports'], {
|
||||
eager: true /* true 为同步,false 为异步 */,
|
||||
eager: true,
|
||||
import: 'default',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
/*eslint sort-keys: "error"*/
|
||||
/**
|
||||
* 启用 sort-keys 规则以强制对象键按字母顺序排序
|
||||
* 原因:
|
||||
* 1. 减少多人协作时的合并冲突
|
||||
* 2. 保持代码一致性,提高可维护性
|
||||
*/
|
||||
|
||||
export default {
|
||||
Root: '根 (Gēn)',
|
||||
$Path: '$Path',
|
||||
Demos: '示例演示',
|
||||
DemosApiDemo: 'API 调用示例',
|
||||
DemosCounterDemo: '点击计数器',
|
||||
DemosCreate: '创建示例',
|
||||
DemosI18nDemo: '国际化示例',
|
||||
DemosNaiveUiDemo: 'Naive UI 组件示例',
|
||||
DemosPrimevueDemo: 'PrimeVue 组件示例',
|
||||
DemosWebsocketDemo: 'WebSocket 示例',
|
||||
Home: '首页',
|
||||
Login: '登录',
|
||||
Root: '根 (Gēn)',
|
||||
} satisfies PageTitleLocalizations;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"page": {
|
||||
"i18n-demo": {
|
||||
"change-language": "Change Language",
|
||||
"title": "Vue I18n Demo",
|
||||
"current-language": "Current Language",
|
||||
"hello": "Hello, {name}!",
|
||||
"title": "Vue I18n Demo"
|
||||
"change-language": "Change Language",
|
||||
"hello": "Hello, {name}!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"page": {
|
||||
"i18n-demo": {
|
||||
"change-language": "切换语言",
|
||||
"title": "Vue I18n 示例",
|
||||
"current-language": "当前语言",
|
||||
"hello": "你好, {name}!",
|
||||
"title": "Vue I18n 示例"
|
||||
"change-language": "切换语言",
|
||||
"hello": "你好, {name}!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import './styles/index.ts';
|
||||
|
||||
import { LogLevels } from 'consola';
|
||||
import App from './App.vue';
|
||||
import { setupPlugins } from './plugins';
|
||||
|
||||
consola.level = LogLevels.verbose;
|
||||
|
||||
const app = createApp(App);
|
||||
setupPlugins(app);
|
||||
const autoInstallModules = import.meta.glob('./plugins/!(index).ts', {
|
||||
eager: true /* true 为同步,false 为异步 */,
|
||||
});
|
||||
|
||||
const app = setupPlugins(createApp(App), autoInstallModules);
|
||||
await new Promise((resolve) => setTimeout(resolve, 280));
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,87 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
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>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<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>
|
||||
<div>Login</div>
|
||||
</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>
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<main flex-1 class="flex flex-col items-center justify-center h-full space-y-4">
|
||||
<h1>Not Found</h1>
|
||||
<p>{{ path }} does not exist.</p>
|
||||
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button>
|
||||
<Button @click="$router.back()">Back</Button>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
meta: {
|
||||
hideInMenu: true,
|
||||
activeMenuName: 'Demos',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
@@ -21,8 +21,6 @@ function setLocale(newLocale: 'en-US' | 'zh-CN') {
|
||||
{{ t('page.i18n-demo.hello', { name: 'Kilo' }) }}
|
||||
</n-p>
|
||||
|
||||
<n-p> $route.meta: {{ $route.meta }} </n-p>
|
||||
|
||||
<n-space>
|
||||
<n-button type="primary" @click="setLocale('en-US')"> English </n-button>
|
||||
<n-button type="success" @click="setLocale('zh-CN')"> 简体中文 </n-button>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { useDialog, useMessage, useModal } 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: {} });
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const modal = useModal();
|
||||
|
||||
const messageTypes = ['info', 'success', 'warning', 'error', 'loading'] satisfies MessageType[];
|
||||
const dialogTypes = ['info', 'success', 'warning', 'error'] as const;
|
||||
@@ -38,10 +38,11 @@ const openDialog = (type: (typeof dialogTypes)[number]) => {
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
window.$nModal!.create({
|
||||
modal.create({
|
||||
title: '命令式 Modal 示例',
|
||||
content: '这是一个命令式 API 创建的 Modal 示例,使用 preset="dialog"。',
|
||||
preset: 'dialog',
|
||||
maskClosable: false,
|
||||
onPositiveClick: () => {
|
||||
message.success('点击了确定');
|
||||
},
|
||||
@@ -61,9 +62,7 @@ const openModal = () => {
|
||||
<NAlert title="信息" type="info" :bordered="false">
|
||||
演示 Naive UI 各种组件的使用方法和功能特性
|
||||
</NAlert>
|
||||
<n-card title="SafeNForm" mt-4>
|
||||
<UseSafeNForm />
|
||||
</n-card>
|
||||
|
||||
<NCard title="Message 消息" class="mt-4">
|
||||
<NSpace>
|
||||
<NButton
|
||||
@@ -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>
|
||||
@@ -2,6 +2,6 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-button @click="$router.push({ name: 'DemosCreate' })">DemosCreate</n-button>
|
||||
<h1>Index Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -10,4 +10,17 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
||||
// 2. 显示全局错误提示
|
||||
// 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(); */
|
||||
// })
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import { DataLoaderPlugin } from 'unplugin-vue-router/data-loaders';
|
||||
import { setupLayouts } from 'virtual:meta-layouts';
|
||||
// import { createGetRoutes, setupLayouts } from 'virtual:generated-layouts';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import type { Router } from 'vue-router';
|
||||
import { handleHotUpdate, routes } from 'vue-router/auto-routes';
|
||||
import type { RouteNamedMap } from 'vue-router/auto-routes';
|
||||
import { routes, handleHotUpdate } from 'vue-router/auto-routes';
|
||||
|
||||
const setupLayoutsResult = setupLayouts(routes);
|
||||
const router = createRouter({
|
||||
@@ -15,10 +15,6 @@ const router = createRouter({
|
||||
strict: true,
|
||||
});
|
||||
|
||||
router.isReady().then(() => {
|
||||
console.debug('✅ [router is ready]');
|
||||
});
|
||||
|
||||
router.onError((error) => {
|
||||
console.debug('🚨 [router error]:', error);
|
||||
});
|
||||
@@ -37,24 +33,50 @@ export function install({ app }: { app: import('vue').App<Element> }) {
|
||||
// 警告:路由守卫的创建顺序会影响执行流程,请勿调整
|
||||
createNProgressGuard(router);
|
||||
if (import.meta.env.VITE_APP_ENABLE_ROUTER_LOG_GUARD === 'true') createLogGuard(router);
|
||||
Object.assign(window, { stack: createStackGuard(router) });
|
||||
|
||||
// >>>
|
||||
Object.values(
|
||||
import.meta.glob<{
|
||||
createGuard?: (router: Router) => void;
|
||||
}>('./router-guard/*.ts', { eager: true /* true 为同步,false 为异步 */ }),
|
||||
).forEach((module) => {
|
||||
module.createGuard?.(router);
|
||||
});
|
||||
// <<<
|
||||
Object.assign(globalThis, { stack: createStackGuard(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
|
||||
if (import.meta.hot) {
|
||||
handleHotUpdate(router);
|
||||
}
|
||||
|
||||
export { router, setupLayoutsResult };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,13 @@
|
||||
type UserPlugin = (ctx: UserPluginContext) => void;
|
||||
type AutoInstallModule = { [K: string]: unknown; install?: UserPlugin };
|
||||
type UserPluginContext = { app: import('vue').App<Element> };
|
||||
|
||||
const autoInstallModules: AutoInstallModule = import.meta.glob(
|
||||
['./*.ts', '!./**/*.types.ts', '!./index.ts'],
|
||||
{
|
||||
eager: true /* true 为同步,false 为异步 */,
|
||||
},
|
||||
);
|
||||
|
||||
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;
|
||||
export function setupPlugins(
|
||||
app: import('vue').App,
|
||||
modules: AutoInstallModule | Record<string, unknown>,
|
||||
) {
|
||||
console.group('🔌 Plugins');
|
||||
for (const path in modules) {
|
||||
const module = modules[path] as AutoInstallModule;
|
||||
if (module.install) {
|
||||
module.install({ app });
|
||||
console.debug(`%c✔ ${path}`, 'color: #07a');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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 };
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'nprogress/nprogress.css'; // <link rel="stylesheet" href="https://testingcf.jsdelivr.net/npm/nprogress/nprogress.css" />
|
||||
|
||||
import 'primeicons/primeicons.css';
|
||||
|
||||
import './css/reset-primevue.css';
|
||||
import './css/transition.css';
|
||||
import './reset-primevue.css';
|
||||
|
||||
import 'virtual:uno.css';
|
||||
|
||||
90
src/utils/a2use.vue
Normal file
90
src/utils/a2use.vue
Normal 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>
|
||||
@@ -18,7 +18,7 @@ type UseSafeNFormOptions<FormValue> = {
|
||||
export function useSafeNForm<T extends Record<string, any> = Record<string, unknown>>(
|
||||
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));
|
||||
|
||||
// 创建类型安全的 Form 组件
|
||||
@@ -35,7 +35,7 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
|
||||
{...props}
|
||||
model={formValue.value}
|
||||
ref={(inst) => {
|
||||
formInst.value = inst as unknown as FormInst;
|
||||
formRef.value = inst as unknown as FormInst;
|
||||
}}
|
||||
>
|
||||
{ctx.slots.default?.({})}
|
||||
@@ -44,7 +44,7 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
|
||||
},
|
||||
{
|
||||
name: 'SafeNForm',
|
||||
inheritAttrs: true,
|
||||
inheritAttrs: false,
|
||||
props: formProps,
|
||||
},
|
||||
);
|
||||
@@ -116,6 +116,6 @@ export function useSafeNForm<T extends Record<string, any> = Record<string, unkn
|
||||
formValue,
|
||||
SafeNForm,
|
||||
SafeNFormItem,
|
||||
formInst,
|
||||
formRef,
|
||||
};
|
||||
}
|
||||
|
||||
15
typed-router.d.ts
vendored
15
typed-router.d.ts
vendored
@@ -58,13 +58,6 @@ declare module 'vue-router/auto-routes' {
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'DemosCreate': RouteRecordInfo<
|
||||
'DemosCreate',
|
||||
'/demos/create',
|
||||
Record<never, never>,
|
||||
Record<never, never>,
|
||||
| never
|
||||
>,
|
||||
'DemosI18nDemo': RouteRecordInfo<
|
||||
'DemosI18nDemo',
|
||||
'/demos/i18n-demo',
|
||||
@@ -150,19 +143,13 @@ declare module 'vue-router/auto-routes' {
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/demos/create.page.vue': {
|
||||
routes:
|
||||
| 'DemosCreate'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/demos/i18n-demo.page.vue': {
|
||||
routes:
|
||||
| 'DemosI18nDemo'
|
||||
views:
|
||||
| never
|
||||
}
|
||||
'src/pages/demos/naive-ui-demo/index.page.vue': {
|
||||
'src/pages/demos/naive-ui-demo.page.vue': {
|
||||
routes:
|
||||
| 'DemosNaiveUiDemo'
|
||||
views:
|
||||
|
||||
@@ -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 {
|
||||
// return [___()];
|
||||
const env = loadEnv(_configEnv.mode, process.cwd());
|
||||
if (env.VITE_BUILD_MINIFY === 'true') return IndexHtmlPlugin();
|
||||
}
|
||||
|
||||
6
worker-configuration.d.ts
vendored
6
worker-configuration.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
/* 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
|
||||
declare namespace Cloudflare {
|
||||
interface GlobalProps {
|
||||
@@ -7,13 +7,13 @@ declare namespace Cloudflare {
|
||||
}
|
||||
interface Env {
|
||||
KV: KVNamespace;
|
||||
VITE_APP_BUILD_TIME: string;
|
||||
VITE_APP_BUILD_COMMIT: string;
|
||||
VITE_BUILD_SOURCE_MAP: string;
|
||||
VITE_BUILD_MINIFY: string;
|
||||
VITE_CLOUDFLARE_SERVER_ENABLED: string;
|
||||
VITE_APP_TITLE: string;
|
||||
VITE_APP_BASE: string;
|
||||
VITE_APP_BUILD_COMMIT: string;
|
||||
VITE_APP_BUILD_TIME: string;
|
||||
VITE_APP_ENABLE_VUE_DEVTOOLS: string;
|
||||
VITE_APP_MENU_SHOW_DEMOS: string;
|
||||
VITE_APP_MENU_SHOW_ORDER: string;
|
||||
|
||||
Reference in New Issue
Block a user