20260819101952

This commit is contained in:
oneao committed 2026-08-19 10:19:56 +08:00
1 parent e91fd1d8de
commit 1ffc8e1ac9
14 files changed
+258 -886

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -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: {
@@ -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);
}
@@ -13,7 +13,7 @@ import Button from '../button/button.vue'
<p class="demo-label">基础用法</p>
<div class="demo-panel">
<Button type="default">主要操作</Button>
<Button type="primary">主要操作</Button>
<Button type="secondary">次要操作</Button>
<Button type="outline">描边按钮</Button>
<Button type="ghost">低强调</Button>
@@ -63,7 +63,7 @@ function openDrawer(placement) {
</p>
<template #footer>
<Button type="outline" @click="drawerCustomOpen = false">取消</Button>
<Button type="default" @click="drawerCustomOpen = false">确定</Button>
<Button type="primary" @click="drawerCustomOpen = false">确定</Button>
</template>
</Drawer>
@@ -116,7 +116,7 @@ function onPickerReset() {
<Input v-model="basicModel.email" placeholder="you@example.com" allow-clear />
</FormItem>
<div class="form-demo-actions">
<Button type="default" html-type="submit">提交</Button>
<Button type="primary" html-type="submit">提交</Button>
<Button type="outline" html-type="reset">重置</Button>
</div>
</Form>
@@ -139,7 +139,7 @@ function onPickerReset() {
<Input v-model="verticalModel.bio" type="textarea" :rows="3" :maxlength="20" show-count />
</FormItem>
<div class="form-demo-actions">
<Button type="default" html-type="submit">提交</Button>
<Button type="primary" html-type="submit">提交</Button>
<Button type="outline" html-type="reset">重置</Button>
</div>
</Form>
@@ -166,7 +166,7 @@ function onPickerReset() {
<Input v-model="customModel.code" placeholder="6 位数字" maxlength="6" allow-clear />
</FormItem>
<div class="form-demo-actions">
<Button type="default" html-type="submit">提交</Button>
<Button type="primary" html-type="submit">提交</Button>
<Button type="outline" html-type="reset">重置</Button>
</div>
</Form>
@@ -195,7 +195,7 @@ function onPickerReset() {
<RangePicker v-model="pickerModel.period" />
</FormItem>
<div class="form-demo-actions">
<Button type="default" html-type="submit">提交</Button>
<Button type="primary" html-type="submit">提交</Button>
<Button type="outline" html-type="reset">重置</Button>
</div>
</Form>
@@ -38,7 +38,7 @@ function saveWithFeedback() {
<p class="demo-label">基础用法</p>
<div class="demo-panel">
<Button type="default" @click="modalBasicOpen = true">打开模态框</Button>
<Button type="primary" @click="modalBasicOpen = true">打开模态框</Button>
</div>
<p class="demo-label">垂直居中 / 无遮罩</p>
+22 -14
View File
@@ -111,20 +111,22 @@ function onSplitterResize(sizes) {
<div class="fms-content">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<keep-alive :include="appStore.cachedViews" :max="8">
<component
:is="
getPageWrapper(
route.meta?.cacheByKey
? cacheKeyOf(route.fullPath)
: route.meta?.componentName,
route.fullPath,
Component,
)
"
:key="route.fullPath"
/>
</keep-alive>
<div :key="route.fullPath" class="fms-page-wrap">
<keep-alive :include="appStore.cachedViews" :max="8">
<component
:is="
getPageWrapper(
route.meta?.cacheByKey
? cacheKeyOf(route.fullPath)
: route.meta?.componentName,
route.fullPath,
Component,
)
"
:key="route.fullPath"
/>
</keep-alive>
</div>
</transition>
</router-view>
</div>
@@ -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:
@@ -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) {
@@ -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 渲染(图标/危险色/分隔线/键盘导航均由库负责) */
+30
View File
@@ -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)
}
+11
View File
@@ -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 {
+21 -3
View File
@@ -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() {
</Input>
</FormItem>
<Button type="default" block :loading="submitting" html-type="submit" class="login-submit">
<Button type="primary" block :loading="submitting" html-type="submit" class="login-submit">
登录
</Button>
</Form>