diff --git a/code/fms/fms-vue/index.html b/code/fms/fms-vue/index.html index fab08f0e..1e6b2e29 100644 --- a/code/fms/fms-vue/index.html +++ b/code/fms/fms-vue/index.html @@ -1,893 +1,190 @@ - - + + - + - FMS 企业管理系统 + FMS - 数字化管理系统 - - +
-
-
- -
+ +
+ +
+
+
FMS
+
+
正在加载,请稍候…
- + + diff --git a/code/fms/fms-vue/src/components/ui/button/button.vue b/code/fms/fms-vue/src/components/ui/button/button.vue index 7a73cf8d..54ad1b6c 100644 --- a/code/fms/fms-vue/src/components/ui/button/button.vue +++ b/code/fms/fms-vue/src/components/ui/button/button.vue @@ -5,7 +5,7 @@ import { Loader } from '@lucide/vue' /* ---------- props / emits / slots ---------- */ /** * 按钮视觉变体 - * @typedef {'default' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link'} ButtonType + * @typedef {'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'link'} ButtonType */ const props = defineProps({ @@ -13,9 +13,9 @@ const props = defineProps({ type: { /** @type {ButtonType} */ type: String, - default: 'default', + default: 'primary', validator: (value) => - ['default', 'secondary', 'outline', 'ghost', 'danger', 'link'].includes(value), + ['primary', 'secondary', 'outline', 'ghost', 'danger', 'link'].includes(value), }, /** 禁用态 */ disabled: { diff --git a/code/fms/fms-vue/src/components/ui/button/index.scss b/code/fms/fms-vue/src/components/ui/button/index.scss index e1cbd2e7..11e0a568 100644 --- a/code/fms/fms-vue/src/components/ui/button/index.scss +++ b/code/fms/fms-vue/src/components/ui/button/index.scss @@ -41,16 +41,16 @@ } /* 主要操作 —— 跟随主色 */ -.button-default { +.button-primary { background: var(--fms-primary); color: var(--fms-primary-text); } -.button-default:hover:not(:disabled) { +.button-primary:hover:not(:disabled) { background: var(--fms-primary-hover); } -.button-default:active { +.button-primary:active { background: var(--fms-primary-active); } diff --git a/code/fms/fms-vue/src/components/ui/demo/button.vue b/code/fms/fms-vue/src/components/ui/demo/button.vue index 3ea0c3f2..0b14d036 100644 --- a/code/fms/fms-vue/src/components/ui/demo/button.vue +++ b/code/fms/fms-vue/src/components/ui/demo/button.vue @@ -13,7 +13,7 @@ import Button from '../button/button.vue'

基础用法

- + diff --git a/code/fms/fms-vue/src/components/ui/demo/drawer.vue b/code/fms/fms-vue/src/components/ui/demo/drawer.vue index 493be487..001c1ed4 100644 --- a/code/fms/fms-vue/src/components/ui/demo/drawer.vue +++ b/code/fms/fms-vue/src/components/ui/demo/drawer.vue @@ -63,7 +63,7 @@ function openDrawer(placement) {

diff --git a/code/fms/fms-vue/src/components/ui/demo/form.vue b/code/fms/fms-vue/src/components/ui/demo/form.vue index f00d405a..c6cd29a1 100644 --- a/code/fms/fms-vue/src/components/ui/demo/form.vue +++ b/code/fms/fms-vue/src/components/ui/demo/form.vue @@ -116,7 +116,7 @@ function onPickerReset() {
- +
@@ -139,7 +139,7 @@ function onPickerReset() {
- +
@@ -166,7 +166,7 @@ function onPickerReset() {
- +
@@ -195,7 +195,7 @@ function onPickerReset() {
- +
diff --git a/code/fms/fms-vue/src/components/ui/demo/modal.vue b/code/fms/fms-vue/src/components/ui/demo/modal.vue index 9c3ca17a..09a7c0f1 100644 --- a/code/fms/fms-vue/src/components/ui/demo/modal.vue +++ b/code/fms/fms-vue/src/components/ui/demo/modal.vue @@ -38,7 +38,7 @@ function saveWithFeedback() {

基础用法

- +

垂直居中 / 无遮罩

diff --git a/code/fms/fms-vue/src/layouts/DefaultLayout.vue b/code/fms/fms-vue/src/layouts/DefaultLayout.vue index 7a5b207f..e1a61200 100644 --- a/code/fms/fms-vue/src/layouts/DefaultLayout.vue +++ b/code/fms/fms-vue/src/layouts/DefaultLayout.vue @@ -111,20 +111,22 @@ function onSplitterResize(sizes) {
- - - +
+ + + +
@@ -228,6 +230,12 @@ function onSplitterResize(sizes) { padding: 12px 16px 16px; } +/* 页面过渡外层包裹:稳定的 DOM 根,供 transition 的 out-in 模式直接管理, + * 避免 keep-alive 直接作 Transition 子节点时在 patch 阶段访问 null.parentNode 报错 */ +.fms-page-wrap { + min-height: 100%; +} + /* ---- 页面过渡 ---- */ .page-enter-active { transition: diff --git a/code/fms/fms-vue/src/layouts/components/AppTabs.vue b/code/fms/fms-vue/src/layouts/components/AppTabs.vue index f7900659..886ec7ca 100644 --- a/code/fms/fms-vue/src/layouts/components/AppTabs.vue +++ b/code/fms/fms-vue/src/layouts/components/AppTabs.vue @@ -151,11 +151,18 @@ function scrollTabs(direction) { function revealActiveTab(behavior = 'smooth') { const track = trackRef.value - if (!track) return + if (!track || !track.isConnected) return const activeElement = Array.from(track.querySelectorAll('[data-tab-key]')).find( (element) => element.dataset.tabKey === activeKey.value, ) - activeElement?.scrollIntoView({ behavior, block: 'nearest', inline: 'center' }) + // 元素未挂载(路由过渡/布局重建时可能短暂脱离 DOM,parentNode 为 null)则跳过, + // 避免 scrollIntoView 抛出未捕获异常向上冒泡成 Promise 拒绝。 + if (!activeElement || !activeElement.isConnected) return + try { + activeElement.scrollIntoView({ behavior, block: 'nearest', inline: 'center' }) + } catch { + /* 元素正处于卸载/过渡中,忽略 */ + } } function handleWheel(e) { diff --git a/code/fms/fms-vue/src/layouts/components/AppTopbar.vue b/code/fms/fms-vue/src/layouts/components/AppTopbar.vue index 075fc494..66b5ef3f 100644 --- a/code/fms/fms-vue/src/layouts/components/AppTopbar.vue +++ b/code/fms/fms-vue/src/layouts/components/AppTopbar.vue @@ -45,10 +45,10 @@ const user = computed(() => ({ account: authStore.user?.account ?? '', })) -function logout() { +async function logout() { authStore.clearSession() Message.info('已退出登录') - router.replace('/login') + await router.replace('/login') } /* 用户下拉菜单:使用组件库标准 items 渲染(图标/危险色/分隔线/键盘导航均由库负责) */ diff --git a/code/fms/fms-vue/src/main.js b/code/fms/fms-vue/src/main.js index 51dee0ad..ecebc29c 100644 --- a/code/fms/fms-vue/src/main.js +++ b/code/fms/fms-vue/src/main.js @@ -6,6 +6,8 @@ import './styles/demo.css' import App from './App.vue' import router from './router' import { initThemeSync } from './stores/app' +import { useAuthStore } from '@/stores/auth' +import { usePermissionStore } from '@/stores/permissions' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) @@ -37,7 +39,35 @@ function showBootstrapError(error) { async function bootstrap() { try { await router.isReady() + + // 登录态下,首屏需要先准备好菜单/权限数据再挂载布局, + // 否则挂载后菜单异步到达会“跳出”(对齐 cool-admin:菜单加载放在 mount 之前) + const authStore = useAuthStore() + if (authStore.isAuthenticated) { + const permissionStore = usePermissionStore() + await permissionStore.load().catch(() => {}) + } + + // 先把 boot 屏移出 #app,避免被 mount 整体替换而瞬间消失(那样没有过渡且会卡顿) + const boot = document.querySelector('#app-boot') + if (boot) document.body.appendChild(boot) + + // 数据已就绪,挂载路由首屏(布局含完整菜单将一次性渲染出来) app.mount('#app') + + // 下一帧启动 boot 屏的 opacity 淡出(合成属性,由 compositor 驱动,不被下方渲染阻塞)。 + // 此时菜单已在布局中就位,淡出过程中直接露出完整页面,不会“跳出”。 + requestAnimationFrame(() => { + if (boot) boot.classList.add('app-boot--hide') + }) + + if (boot) { + boot.addEventListener('transitionend', () => boot.remove(), { once: true }) + // 兜底:防止 transitionend 未触发导致节点残留 + setTimeout(() => { + if (boot.isConnected) boot.remove() + }, 600) + } } catch (error) { showBootstrapError(error) } diff --git a/code/fms/fms-vue/src/stores/auth.js b/code/fms/fms-vue/src/stores/auth.js index e716d9e1..0209fac7 100644 --- a/code/fms/fms-vue/src/stores/auth.js +++ b/code/fms/fms-vue/src/stores/auth.js @@ -2,6 +2,7 @@ import { computed, ref } from 'vue' import { defineStore } from 'pinia' const AUTH_STORAGE_KEY = 'fms-auth' +const LAST_LOGIN_KEY = 'fms-last-login' // 兼容旧版 sessionStorage 会话:将历史会话迁移到 localStorage try { @@ -25,10 +26,20 @@ export const useAuthStore = defineStore( token.value = session.token ?? session.access_token ?? '' orgId.value = session.orgid ?? organizationId ?? '' user.value = session.user ?? session.user_info ?? null + + // 单独保存最近登录信息,供登录页回填(与认证状态分离) + try { + const last = { orgId: orgId.value, account: user.value?.account ?? '' } + localStorage.setItem(LAST_LOGIN_KEY, JSON.stringify(last)) + } catch { + // Storage 不可用时静默忽略 + } } function clearSession() { token.value = '' + orgId.value = '' + user.value = null } return { diff --git a/code/fms/fms-vue/src/views/login/index.vue b/code/fms/fms-vue/src/views/login/index.vue index 3e7d7e49..ab146e9b 100644 --- a/code/fms/fms-vue/src/views/login/index.vue +++ b/code/fms/fms-vue/src/views/login/index.vue @@ -9,6 +9,7 @@ import Button from '@/components/ui/button/button.vue' import Message from '@/components/ui/message/message-manager.js' import { loginApi } from '@/services/api' import { useAuthStore } from '@/stores/auth' +import { usePermissionStore } from '@/stores/permissions' const router = useRouter() const route = useRoute() @@ -36,9 +37,24 @@ const formRules = { const submitting = ref(false) +const LAST_LOGIN_KEY = 'fms-last-login' + onMounted(() => { - formModel.orgId = authStore.orgId || '' - formModel.userId = authStore.user?.account || '' + // 从独立存储读取上次登录信息回填,与认证状态分离 + let lastOrgId = '' + let lastAccount = '' + try { + const raw = localStorage.getItem(LAST_LOGIN_KEY) + if (raw) { + const last = JSON.parse(raw) + lastOrgId = last.orgId || '' + lastAccount = last.account || '' + } + } catch { + // 解析失败不影响登录 + } + formModel.orgId = lastOrgId + formModel.userId = lastAccount formModel.password = '' // 聚焦机构码输入框(id 落在 Input 根元素上,需定位内部 input) document.querySelector('#orgId input')?.focus() @@ -53,6 +69,8 @@ async function onSubmit(values) { { token: res.data.token, user: { ...res.data.user, account: values.userId } }, res.data.orgid ?? values.orgId, ) + // 登录成功后、进入布局前先把菜单/权限数据拉齐,避免 /dashboard 挂载时菜单异步“跳出” + await usePermissionStore().load().catch(() => {}) Message.success('登录成功') router.replace(String(route.query.redirect || '/dashboard')) } catch (e) { @@ -127,7 +145,7 @@ function onFailed() { - diff --git a/code/test/cool-admin-vue b/code/test/cool-admin-vue new file mode 160000 index 00000000..a2d4ee9b --- /dev/null +++ b/code/test/cool-admin-vue @@ -0,0 +1 @@ +Subproject commit a2d4ee9bbfd6bfce880382f0bf6f8dd8f3397a2d