Compare commits
25 Commits
safe-form
...
renovate/l
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c79e486a2 | |||
|
|
5bbbf488fe | ||
|
|
acd7c0db13 | ||
| 1ad46a62fd | |||
| bb02b796aa | |||
|
|
a4ea7ce56e | ||
|
|
166d76d980 | ||
|
|
f9f82e4d29 | ||
|
|
b4fcde324d | ||
|
|
b669889bb0 | ||
|
|
8a3b9e03fd | ||
| ec02658ede | |||
|
|
bd9acc06a8 | ||
|
|
f790691d5a | ||
|
|
838d5cfb6e | ||
| ac544a8ff5 | |||
|
|
e7a4a7aff9 | ||
|
|
0d1a20d88d | ||
| 19aaddc4e2 | |||
| 1e24193b84 | |||
| cdec0c1a23 | |||
| 94cdecb075 | |||
|
|
7f0cf5dd8f | ||
|
|
c8b8a3caa4 | ||
|
|
48eb653f1a |
4
.github/workflows/ci-cd.yaml
vendored
4
.github/workflows/ci-cd.yaml
vendored
@@ -38,7 +38,9 @@ jobs:
|
|||||||
run: pnpm run lint
|
run: pnpm run lint
|
||||||
|
|
||||||
- name: 📦 构建项目
|
- name: 📦 构建项目
|
||||||
run: pnpm run build-only
|
run: |
|
||||||
|
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 }}
|
||||||
|
|
||||||
|
|||||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -6,22 +6,23 @@
|
|||||||
"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
4
auto-imports.d.ts
vendored
@@ -111,6 +111,7 @@ 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']
|
||||||
@@ -156,6 +157,7 @@ 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']
|
||||||
@@ -454,6 +456,7 @@ 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']>
|
||||||
@@ -499,6 +502,7 @@ 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']>
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import pluginImport from 'eslint-plugin-import';
|
import pluginVitest from '@vitest/eslint-plugin';
|
||||||
import { globalIgnores } from 'eslint/config';
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
||||||
import {
|
import {
|
||||||
|
configureVueProject,
|
||||||
defineConfigWithVueTs,
|
defineConfigWithVueTs,
|
||||||
vueTsConfigs,
|
vueTsConfigs,
|
||||||
configureVueProject,
|
|
||||||
} from '@vue/eslint-config-typescript';
|
} from '@vue/eslint-config-typescript';
|
||||||
import pluginVue from 'eslint-plugin-vue';
|
import pluginImport from 'eslint-plugin-import';
|
||||||
import pluginVitest from '@vitest/eslint-plugin';
|
import pluginJsonc from 'eslint-plugin-jsonc';
|
||||||
import pluginPlaywright from 'eslint-plugin-playwright';
|
|
||||||
import pluginOxlint from 'eslint-plugin-oxlint';
|
import pluginOxlint from 'eslint-plugin-oxlint';
|
||||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting';
|
import pluginPlaywright from 'eslint-plugin-playwright';
|
||||||
|
import pluginVue from 'eslint-plugin-vue';
|
||||||
|
import { globalIgnores } from 'eslint/config';
|
||||||
|
import jsoncParser from 'jsonc-eslint-parser';
|
||||||
|
|
||||||
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
|
||||||
@@ -52,6 +54,19 @@ 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',
|
||||||
|
|||||||
70
index.html
70
index.html
@@ -11,40 +11,6 @@
|
|||||||
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" />
|
||||||
@@ -132,7 +98,40 @@
|
|||||||
<!-- <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> -->
|
||||||
</body>
|
<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>
|
||||||
<script>
|
<script>
|
||||||
(function (d) {
|
(function (d) {
|
||||||
var config = {
|
var config = {
|
||||||
@@ -163,4 +162,5 @@
|
|||||||
s.parentNode.insertBefore(tk, s);
|
s.parentNode.insertBefore(tk, s);
|
||||||
}); /* (document) */
|
}); /* (document) */
|
||||||
</script>
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -103,7 +103,8 @@
|
|||||||
"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-oxlint": "~1.23.0",
|
"eslint-plugin-jsonc": "^2.21.0",
|
||||||
|
"eslint-plugin-oxlint": "~1.25.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",
|
||||||
"happy-dom": "^20.0.1",
|
"happy-dom": "^20.0.1",
|
||||||
@@ -113,11 +114,12 @@
|
|||||||
"lint-staged": "^16.1.6",
|
"lint-staged": "^16.1.6",
|
||||||
"npm-run-all2": "^8.0.4",
|
"npm-run-all2": "^8.0.4",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"oxlint": "~1.23.0",
|
"oxlint": "~1.25.0",
|
||||||
"postcss-html": "^1.8.0",
|
"postcss-html": "^1.8.0",
|
||||||
"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",
|
||||||
|
|||||||
1151
pnpm-lock.yaml
generated
1151
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">
|
<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,8 +9,10 @@ import A2use from './utils/a2use.vue';
|
|||||||
<Toast />
|
<Toast />
|
||||||
|
|
||||||
<AppNaiveUIProvider>
|
<AppNaiveUIProvider>
|
||||||
<!-- <RouterView /> -->
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
<A2use></A2use>
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
</AppNaiveUIProvider>
|
</AppNaiveUIProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -8,16 +8,7 @@ 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 } = useI18n({
|
const { t, te } = routeI18nInstance.global;
|
||||||
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)();
|
||||||
@@ -26,10 +17,15 @@ export function useMetaLayoutsNMenuOptions({ menuInstRef }: { menuInstRef: Ref<M
|
|||||||
const selectedKey = ref('');
|
const selectedKey = ref('');
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => router.currentRoute.value.path,
|
() => router.currentRoute.value,
|
||||||
(newPath) => {
|
(route) => {
|
||||||
selectedKey.value = newPath;
|
// 优先使用 activeMenuName(通过路由名称解析为路径),如果没有则使用当前路径
|
||||||
menuInstRef.value?.showOption(newPath); // 展开菜单,确保设定的元素被显示,如果不传入 key 会展示当前选中元素
|
const activeMenuPath = route.meta.activeMenuName
|
||||||
|
? router.resolve({ name: route.meta.activeMenuName }).path
|
||||||
|
: route.path;
|
||||||
|
|
||||||
|
selectedKey.value = activeMenuPath;
|
||||||
|
menuInstRef.value?.showOption(activeMenuPath); // 展开菜单,确保设定的元素被显示
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
@@ -113,7 +109,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) : route.meta?.title || routeName;
|
let text = te(routeName) ? t(routeName) : 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}`;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
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>
|
||||||
@@ -11,6 +12,7 @@ import ToggleSiderButton from './components/ToggleSiderButton.vue';
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<LanguageSwitchButton />
|
<LanguageSwitchButton />
|
||||||
<ThemeSwitchButton />
|
<ThemeSwitchButton />
|
||||||
|
<UserDropdown />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<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,14 +49,4 @@ 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>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
* 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';
|
||||||
|
|
||||||
@@ -26,6 +28,44 @@ export const i18nInstance = createI18n({
|
|||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
watchEffect(() => {
|
export const routeI18nInstance = createI18n({
|
||||||
locale.value = i18nInstance.global.locale.value;
|
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;
|
||||||
|
},
|
||||||
|
{ 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,13 +1,22 @@
|
|||||||
|
/*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;
|
||||||
|
|||||||
@@ -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,
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
import: 'default',
|
import: 'default',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
|
/*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;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"page": {
|
"page": {
|
||||||
"i18n-demo": {
|
"i18n-demo": {
|
||||||
"title": "Vue I18n Demo",
|
|
||||||
"current-language": "Current Language",
|
|
||||||
"change-language": "Change Language",
|
"change-language": "Change Language",
|
||||||
"hello": "Hello, {name}!"
|
"current-language": "Current Language",
|
||||||
|
"hello": "Hello, {name}!",
|
||||||
|
"title": "Vue I18n Demo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"page": {
|
"page": {
|
||||||
"i18n-demo": {
|
"i18n-demo": {
|
||||||
"title": "Vue I18n 示例",
|
|
||||||
"current-language": "当前语言",
|
|
||||||
"change-language": "切换语言",
|
"change-language": "切换语言",
|
||||||
"hello": "你好, {name}!"
|
"current-language": "当前语言",
|
||||||
|
"hello": "你好, {name}!",
|
||||||
|
"title": "Vue I18n 示例"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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 autoInstallModules = import.meta.glob('./plugins/!(index).ts', {
|
const app = createApp(App);
|
||||||
eager: true /* true 为同步,false 为异步 */,
|
setupPlugins(app);
|
||||||
});
|
|
||||||
|
|
||||||
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');
|
||||||
|
|||||||
@@ -1,5 +1,87 @@
|
|||||||
<script setup lang="ts"></script>
|
<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>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>Login</div>
|
<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>
|
||||||
</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>
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
<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="$router.back()">Back</Button>
|
<Button @click="handleBack">{{ canGoBack ? 'Back' : 'Home' }}</Button>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
12
src/pages/demos/create.page.vue
Normal file
12
src/pages/demos/create.page.vue
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
definePage({
|
||||||
|
meta: {
|
||||||
|
hideInMenu: true,
|
||||||
|
activeMenuName: 'Demos',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
@@ -21,6 +21,8 @@ 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>
|
||||||
|
|||||||
@@ -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,11 +38,10 @@ const openDialog = (type: (typeof dialogTypes)[number]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openModal = () => {
|
const openModal = () => {
|
||||||
modal.create({
|
window.$nModal!.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('点击了确定');
|
||||||
},
|
},
|
||||||
@@ -62,7 +61,9 @@ 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
|
||||||
72
src/pages/demos/naive-ui-demo/use-safe-n-form.vue
Normal file
72
src/pages/demos/naive-ui-demo/use-safe-n-form.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>Index Page</h1>
|
<n-button @click="$router.push({ name: 'DemosCreate' })">DemosCreate</n-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -10,17 +10,4 @@ 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(); */
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { RouteNamedMap } from 'vue-router/auto-routes';
|
import type { Router } from 'vue-router';
|
||||||
import { routes, handleHotUpdate } from 'vue-router/auto-routes';
|
import { handleHotUpdate, routes } from 'vue-router/auto-routes';
|
||||||
|
|
||||||
const setupLayoutsResult = setupLayouts(routes);
|
const setupLayoutsResult = setupLayouts(routes);
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -15,6 +15,10 @@ 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);
|
||||||
});
|
});
|
||||||
@@ -33,50 +37,24 @@ 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(globalThis, { stack: createStackGuard(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);
|
||||||
|
});
|
||||||
|
// <<<
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vue-router' {
|
if (__DEV__) Object.assign(window, { 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 };
|
||||||
|
|||||||
50
src/plugins/00.router-plugin.types.ts
Normal file
50
src/plugins/00.router-plugin.types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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,13 +4,18 @@
|
|||||||
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(
|
|
||||||
app: import('vue').App,
|
const autoInstallModules: AutoInstallModule = import.meta.glob(
|
||||||
modules: AutoInstallModule | Record<string, unknown>,
|
['./*.ts', '!./**/*.types.ts', '!./index.ts'],
|
||||||
) {
|
{
|
||||||
console.group('🔌 Plugins');
|
eager: true /* true 为同步,false 为异步 */,
|
||||||
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');
|
||||||
|
|||||||
28
src/plugins/router-guard/router-permission-guard.ts
Normal file
28
src/plugins/router-guard/router-permission-guard.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
45
src/stores/auth-store-auto-imports.ts
Normal file
45
src/stores/auth-store-auto-imports.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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 };
|
||||||
|
});
|
||||||
9
src/styles/css/transition.css
Normal file
9
src/styles/css/transition.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
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';
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
<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>>(
|
export function useSafeNForm<T extends Record<string, any> = Record<string, unknown>>(
|
||||||
options: UseSafeNFormOptions<T> = {},
|
options: UseSafeNFormOptions<T> = {},
|
||||||
) {
|
) {
|
||||||
const formRef = ref<FormInst | null>(null);
|
const formInst = 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) => {
|
||||||
formRef.value = inst as unknown as FormInst;
|
formInst.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: false,
|
inheritAttrs: true,
|
||||||
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,
|
||||||
formRef,
|
formInst,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
15
typed-router.d.ts
vendored
15
typed-router.d.ts
vendored
@@ -58,6 +58,13 @@ 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',
|
||||||
@@ -143,13 +150,19 @@ 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.page.vue': {
|
'src/pages/demos/naive-ui-demo/index.page.vue': {
|
||||||
routes:
|
routes:
|
||||||
| 'DemosNaiveUiDemo'
|
| 'DemosNaiveUiDemo'
|
||||||
views:
|
views:
|
||||||
|
|||||||
@@ -26,7 +26,73 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
6
worker-configuration.d.ts
vendored
6
worker-configuration.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// Generated by Wrangler by running `wrangler types` (hash: 0003c1b3cd56e3cc5549efef6b4c6d3d)
|
// Generated by Wrangler by running `wrangler types` (hash: b18d7aec4937222767b077e627f9f927)
|
||||||
// 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user