mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-11-05 06:58:12 +08:00
perf(webui): 只打包 Vue 运行时、按需加载 zh-CN 语言包与布局弹窗,待审核光晕改为 opacity 动画
This commit is contained in:
@@ -200,18 +200,40 @@ const posterSrc = computed(() => resolvePosterUrl(bangumi.poster_link));
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Card glow animation when needs review - yellow
|
||||
// 待审核的黄色光晕:静态 box-shadow 放在 ::after 上,只动画 opacity(合成器线程,
|
||||
// 不触发每帧重绘)。光晕在海报外侧,所以这里取消 overflow 裁剪,由子元素继承圆角
|
||||
.card-poster--needs-review {
|
||||
animation: card-glow 2.5s ease-in-out infinite;
|
||||
overflow: visible;
|
||||
|
||||
> * {
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: 0 0 16px 4px rgba(251, 191, 36, 0.6);
|
||||
pointer-events: none;
|
||||
animation: card-glow 2.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
&::after {
|
||||
animation: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes card-glow {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: var(--shadow-md), 0 0 0 0 rgba(251, 191, 36, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
box-shadow: var(--shadow-md), 0 0 16px 4px rgba(251, 191, 36, 0.6);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import AbSearchModal from './search/ab-search-modal.vue';
|
||||
|
||||
const { showModal, provider, loading } = storeToRefs(useSearchStore());
|
||||
const { toggleModal, getProviders } = useSearchStore();
|
||||
|
||||
// 搜索弹窗首次打开时才加载
|
||||
const { comp: SearchModal } = useLazyModal(
|
||||
showModal,
|
||||
() => import('./search/ab-search-modal.vue')
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getProviders();
|
||||
});
|
||||
@@ -18,5 +22,5 @@ onMounted(() => {
|
||||
/>
|
||||
|
||||
<!-- Search Modal -->
|
||||
<AbSearchModal @close="toggleModal" />
|
||||
<component :is="SearchModal" v-if="SearchModal" @close="toggleModal" />
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,10 @@ const { isMobile } = useBreakpointQuery();
|
||||
|
||||
const showAccount = ref(false);
|
||||
const rssRule = ref(ruleTemplate);
|
||||
const { comp: AddRss, shown: addRssShown } = useLazyModal(
|
||||
showAddRSS,
|
||||
() => import('@/components/ab-add-rss.vue')
|
||||
);
|
||||
|
||||
const { start, pause, shutdown, restart, resetRule } = useProgramStore();
|
||||
const { refreshPoster } = useBangumiStore();
|
||||
@@ -138,7 +142,13 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<ab-change-account v-model:show="showAccount" />
|
||||
<ab-add-rss v-model:show="showAddRSS" v-model:rule="rssRule" />
|
||||
<component
|
||||
:is="AddRss"
|
||||
v-if="AddRss"
|
||||
v-model:rule="rssRule"
|
||||
:show="addRssShown"
|
||||
@update:show="showAddRSS = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -615,12 +615,12 @@ function handleFilterClick(
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<!-- Backdrop -->
|
||||
<transition name="overlay">
|
||||
<transition name="overlay" appear>
|
||||
<div v-if="showModal" class="modal-backdrop" />
|
||||
</transition>
|
||||
|
||||
<!-- Modal -->
|
||||
<transition name="modal">
|
||||
<transition name="modal" appear>
|
||||
<div
|
||||
v-if="showModal"
|
||||
class="modal-container"
|
||||
|
||||
28
webui/src/hooks/useLazyModal.ts
Normal file
28
webui/src/hooks/useLazyModal.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Component, Ref } from 'vue';
|
||||
|
||||
/**
|
||||
* 弹窗首次打开时才加载并挂载。
|
||||
* `comp` 就绪后才渲染,`shown` 在挂载后的下一拍再置为 true,
|
||||
* 这样 NModal 的进入过渡(appear)仍然生效。
|
||||
*/
|
||||
export function useLazyModal(
|
||||
open: Ref<boolean>,
|
||||
load: () => Promise<{ default: Component }>
|
||||
) {
|
||||
const comp = shallowRef<Component>();
|
||||
const shown = ref(false);
|
||||
|
||||
watch(
|
||||
open,
|
||||
async (val) => {
|
||||
if (val && !comp.value) {
|
||||
comp.value = (await load()).default;
|
||||
await nextTick();
|
||||
}
|
||||
shown.value = open.value;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
return { comp, shown };
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createSharedComposable, useLocalStorage } from '@vueuse/core';
|
||||
import enUS from '@/i18n/en.json';
|
||||
import zhCN from '@/i18n/zh-CN.json';
|
||||
import type { ApiSuccess } from '#/api';
|
||||
|
||||
const messages = {
|
||||
en: enUS,
|
||||
'zh-CN': zhCN,
|
||||
};
|
||||
|
||||
type Languages = keyof typeof messages;
|
||||
type Languages = 'en' | 'zh-CN';
|
||||
|
||||
function normalizeLocale(locale: string): Languages {
|
||||
if (locale.startsWith('zh')) return 'zh-CN';
|
||||
@@ -20,19 +14,39 @@ export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: normalizeLocale(navigator.language),
|
||||
fallbackLocale: 'en',
|
||||
messages,
|
||||
// zh-CN 先放空壳以固定语言类型,真正的消息由 loadLocale 载入
|
||||
messages: { en: enUS, 'zh-CN': {} },
|
||||
});
|
||||
|
||||
// 默认语言(en)静态打包;其它语言首次使用时才加载
|
||||
const loaded = new Set<Languages>(['en']);
|
||||
|
||||
export async function loadLocale(locale: Languages) {
|
||||
if (loaded.has(locale)) return;
|
||||
const { default: msgs } = await import('@/i18n/zh-CN.json');
|
||||
i18n.global.setLocaleMessage(locale, msgs);
|
||||
loaded.add(locale);
|
||||
}
|
||||
|
||||
/** 首次渲染前预载已保存的语言,避免先闪出英文 */
|
||||
export function loadSavedLocale() {
|
||||
return loadLocale(
|
||||
normalizeLocale(localStorage.getItem('lang') ?? navigator.language)
|
||||
);
|
||||
}
|
||||
|
||||
export const useMyI18n = createSharedComposable(() => {
|
||||
const lang = useLocalStorage<Languages>(
|
||||
'lang',
|
||||
normalizeLocale(navigator.language)
|
||||
);
|
||||
|
||||
i18n.global.locale.value = lang.value as unknown as Languages;
|
||||
i18n.global.locale.value = lang.value;
|
||||
|
||||
watch(lang, (val) => {
|
||||
i18n.global.locale.value = val as unknown as Languages;
|
||||
// 先加载语言包再切换,切换期间沿用旧语言
|
||||
watch(lang, async (val) => {
|
||||
await loadLocale(val);
|
||||
i18n.global.locale.value = val;
|
||||
});
|
||||
|
||||
function changeLocale() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import { router } from './router';
|
||||
import { i18n } from './hooks/useMyI18n';
|
||||
import { i18n, loadSavedLocale } from './hooks/useMyI18n';
|
||||
import App from './App.vue';
|
||||
|
||||
import '@unocss/reset/tailwind-compat.css';
|
||||
@@ -13,4 +13,5 @@ const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount('#app');
|
||||
// 已保存的语言包加载完再渲染,避免先闪出英文
|
||||
loadSavedLocale().then(() => app.mount('#app'));
|
||||
|
||||
@@ -8,6 +8,14 @@ definePage({
|
||||
});
|
||||
|
||||
const { editRule } = storeToRefs(useBangumiStore());
|
||||
const editRuleOpen = computed({
|
||||
get: () => editRule.value.show,
|
||||
set: (val) => (editRule.value.show = val),
|
||||
});
|
||||
const { comp: EditRule, shown: editRuleShown } = useLazyModal(
|
||||
editRuleOpen,
|
||||
() => import('@/components/ab-edit-rule.vue')
|
||||
);
|
||||
|
||||
// 插件页面的标题取自清单(清单到达前留空,避免闪出路由名 Plugin;清单已到达
|
||||
// 或加载失败仍无匹配页面时回退为插件 id);插件市场用翻译后的标题,其它页面沿用路由名
|
||||
@@ -74,9 +82,12 @@ function onApplyRule(rule: BangumiRule) {
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ab-edit-rule
|
||||
v-model:show="editRule.show"
|
||||
<component
|
||||
:is="EditRule"
|
||||
v-if="EditRule"
|
||||
v-model:rule="editRule.item"
|
||||
:show="editRuleShown"
|
||||
@update:show="editRuleOpen = $event"
|
||||
@enable="onEnableRule"
|
||||
@archive="onArchiveRule"
|
||||
@unarchive="onUnarchiveRule"
|
||||
|
||||
3
webui/types/dts/auto-imports.d.ts
vendored
3
webui/types/dts/auto-imports.d.ts
vendored
@@ -67,6 +67,8 @@ declare global {
|
||||
const isRef: typeof import('vue').isRef
|
||||
const isShallow: typeof import('vue').isShallow
|
||||
const isValidNexusPhpCategoryIds: typeof import('../../src/utils/nexusphp').isValidNexusPhpCategoryIds
|
||||
const loadLocale: typeof import('../../src/hooks/useMyI18n').loadLocale
|
||||
const loadSavedLocale: typeof import('../../src/hooks/useMyI18n').loadSavedLocale
|
||||
const localizeFields: typeof import('../../src/utils/plugin-text').localizeFields
|
||||
const mapActions: typeof import('pinia').mapActions
|
||||
const mapGetters: typeof import('pinia').mapGetters
|
||||
@@ -150,6 +152,7 @@ declare global {
|
||||
const useI18n: typeof import('vue-i18n').useI18n
|
||||
const useId: typeof import('vue').useId
|
||||
const useIntervalFn: typeof import('@vueuse/core').useIntervalFn
|
||||
const useLazyModal: typeof import('../../src/hooks/useLazyModal').useLazyModal
|
||||
const useLocalStorage: typeof import('@vueuse/core').useLocalStorage
|
||||
const useLogStore: typeof import('../../src/store/log').useLogStore
|
||||
const useMessage: typeof import('../../src/hooks/useMessage').useMessage
|
||||
|
||||
@@ -46,6 +46,7 @@ export default defineConfig({
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
include: resolve(import.meta.dirname, './src/i18n/**'),
|
||||
dropMessageCompiler: true,
|
||||
}),
|
||||
],
|
||||
css: {
|
||||
@@ -61,7 +62,13 @@ export default defineConfig({
|
||||
output: { minify: { compress: { dropConsole: true } } },
|
||||
},
|
||||
},
|
||||
resolve: { tsconfigPaths: true },
|
||||
resolve: {
|
||||
tsconfigPaths: true,
|
||||
// 只用运行时:模板都在构建期编译,不需要打包 @vue/compiler-*
|
||||
alias: [
|
||||
{ find: /^vue$/, replacement: 'vue/dist/vue.runtime.esm-bundler.js' },
|
||||
],
|
||||
},
|
||||
preview: { headers },
|
||||
server: {
|
||||
proxy: {
|
||||
|
||||
Reference in New Issue
Block a user