样式修改

master
lzq 2026-01-16 17:31:34 +08:00
parent 29d808402d
commit 1b0f9368fd
17 changed files with 651 additions and 306 deletions

View File

@ -18,6 +18,7 @@
padding: 0;
border: 0;
font-size: var(--el-font-size-medium);
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html,

View File

@ -1,7 +1,7 @@
// Element Plus
:root {
--custom-radius: 5px;
--custom-radius: 9px;
//
--main-color: #1C6EFF;
--el-color-white: white !important;
@ -11,6 +11,11 @@
--el-button-hover-border-color: #458FFF !important;
--el-color-primary-light-3: #458FFF !important;
--el-color-danger: #CF171D !important;
--el-menu-text-color: #29343D !important;
--el-menu-hover-text-color: #29343D !important;
--el-menu-bg-color: #FFFFFF !important;
--el-menu-hover-bg-color: rgb(204, 204, 204) !important;
--el-menu-level: 0;
//
// --el-border-color: #E4E4E7 !important; // DCDFE6
//

View File

@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import Evt from '@/common/utils/evt.ts'
import Nav from '@/common/router/nav.ts'
const pageContextCache = new Map<string, AppTypes.PageContext>()
@ -12,6 +13,7 @@ initCache()
export const useAppPageStore = defineStore('AppPage', () => {
const keepAliveInclude = ref<string[]>([])
const pages = ref<AppTypes.PageContext[]>([])
const currentPage = ref<string>('')
@ -27,13 +29,68 @@ export const useAppPageStore = defineStore('AppPage', () => {
if (!keepAliveInclude.value.includes(ctx_.insId)) {
keepAliveInclude.value.push(ctx_.insId)
}
if (!pages.value.find(it => it.insId === ctx_.insId)) {
pages.value.push(ctx_)
}
}
function close(insId: string) {
function reopen(insId: string) {
if (currentPage.value === insId) {
return
}
const page = pageContextCache.get(insId)
if (page) {
currentPage.value = insId
Nav.open(page.routeName)
}
}
function closePage(insId: string) {
pageContextCache.delete(insId)
if (keepAliveInclude.value.includes(insId)) {
keepAliveInclude.value = keepAliveInclude.value.splice(keepAliveInclude.value.indexOf(insId), 1)
}
const index = pages.value.findIndex(it => it.insId === insId)
if (index !== -1) {
const oldLen = pages.value.length
pages.value = pages.value.filter(it => it.insId !== insId)
if (currentPage.value !== insId) {
return
}
if (index === 0) {
if (pages.value.length > 0) {
reopen(pages.value[0]?.insId)
} else {
Nav.open('home')
}
} else if (index === oldLen - 1) {
if (pages.value.length > 0) {
reopen(pages.value[pages.value.length - 1]?.insId)
} else {
Nav.open('home')
}
} else {
if (pages.value.length > 0) {
reopen(pages.value[index]?.insId)
} else {
Nav.open('home')
}
}
}
}
function closeCurrent() {
closePage(currentPage.value)
}
function closeOther() {
pages.value = pages.value.filter(it => it.insId === currentPage.value)
}
function closeAll() {
pages.value = []
Nav.open('home')
}
function $reset() {
@ -46,8 +103,14 @@ export const useAppPageStore = defineStore('AppPage', () => {
return {
ctx,
reopen,
open,
close,
closePage,
closeCurrent,
closeOther,
closeAll,
pages,
currentPage,
keepAliveInclude,
$reset,
}

View File

@ -11,6 +11,8 @@ declare global {
params: Record<string, any>
routeName: string
menuId: string
icon: string
breadcrumb: string[]
}
// 菜单

View File

@ -4,13 +4,32 @@ import { useAppSettingStore } from '@/common/app/app-setting-store.ts'
import { useAppUserStore } from '@/common/app/app-user-store.ts'
import Utils from '@/common/utils'
import type { R } from '@/common/utils/http-util.ts'
import { MenuCategory } from '@/common/app/constants.ts'
const home = {
'id': '-1',
'sn': 'menus',
'pid': '0',
'title': '首页',
'icon': 'menus',
'tier': 1,
'breadcrumb': [
'首页',
],
'menuCategory': MenuCategory.Page,
'freeze': null,
'sort': 0,
'routeName': 'home',
'path': '/home',
}
export const reloadUserInfo = () => {
const appSettingStore = useAppSettingStore()
const appUserStore = useAppUserStore()
return LoginApi.my()
.then(({data}) => {
const menuTree = Utils.clone(Colls.toTree(data.menus))
data.menus.unshift(home)
menuTree.unshift(home)
appSettingStore.$patch({
menus: data.menus, menuTree,
theme: data.setting?.theme ?? 'light',
@ -30,30 +49,30 @@ export const reloadUserInfo = () => {
})
}
export const loadUserInfo = () => {
const appSettingStore = useAppSettingStore()
const appUserStore = useAppUserStore()
return LoginApi.my()
.then(({data}) => {
const menuTree = Utils.clone(Colls.toTree(data.menus))
appSettingStore.$patch({
menus: data.menus, menuTree,
theme: data.setting?.theme ?? 'light',
collectedMenus: data.setting?.collectedMenus ?? [],
logo: data.setting?.logo,
language: data.setting?.language ?? 'zh',
})
appUserStore.$patch({
userId: data.id,
nickname: data.nickname,
avatar: data.avatar,
tenantId: data.tenantId,
tenantName: data.tenantName,
bizObj: data.bizObj,
roles: data.roles,
})
})
}
/* export const loadUserInfo = () => {
const appSettingStore = useAppSettingStore()
const appUserStore = useAppUserStore()
return LoginApi.my()
.then(({data}) => {
const menuTree = Utils.clone(Colls.toTree(data.menus))
appSettingStore.$patch({
menus: data.menus, menuTree,
theme: data.setting?.theme ?? 'light',
collectedMenus: data.setting?.collectedMenus ?? [],
logo: data.setting?.logo,
language: data.setting?.language ?? 'zh',
})
appUserStore.$patch({
userId: data.id,
nickname: data.nickname,
avatar: data.avatar,
tenantId: data.tenantId,
tenantName: data.tenantName,
bizObj: data.bizObj,
roles: data.roles,
})
})
} */
export function hasPermission(resSn?: string) {
const appSettingStore = useAppSettingStore()

View File

@ -9,11 +9,13 @@ import { useAppUserStore } from '@/common/app/app-user-store.ts'
import { MenuCategory } from '@/common/app/constants.ts'
import { appBaseUrl } from '@/common'
import strings from '@/common/utils/strings.ts'
import Strings from '@/common/utils/strings.ts'
import {
getRoute,
getRoutes,
} from '@/common/router/route-config.ts'
import { SpecialPage } from '@/common/router/constants.ts'
import Nav from '@/common/router/nav.ts'
function addRoutes(routNames: string[]) {
if (Colls.isEmpty(routNames)) return
@ -74,10 +76,17 @@ router.beforeEach((to, from) => {
name: SpecialPage.Home,
}
} else {
return {
replace: true,
path: to.fullPath,
let routeName = router.getRoutes().find((it) => it.path === to.path)?.name as string
console.log('reloadRouter11', routeName, Strings.isBlank(routeName))
if (Strings.isBlank(routeName)) {
routeName = SpecialPage.Home
ElMessage.error('页面不存在222')
}
console.log('reloadRouter', to, router.getRoutes(), routeName)
setTimeout(() => {
Nav.open(routeName)
})
return false
}
}
return true
@ -113,7 +122,7 @@ export function reloadRouter() {
}
Evt.on('login', (_) => {
router.replace('/')
Nav.open(SpecialPage.Home)
})
Evt.on('logout', (_) => {

View File

@ -38,6 +38,8 @@ function open(option: string | Option) {
params: {},
routeName,
menuId: menu.id,
icon: menu.icon,
breadcrumb: menu.breadcrumb,
}
ctx.insId = ctx.routeName
useAppPageStore().open(ctx)
@ -67,6 +69,8 @@ function open(option: string | Option) {
params: {},
routeName,
menuId: menu.id,
icon: menu.icon,
breadcrumb: menu.breadcrumb,
}
} else {
const routeName = option.routeName
@ -91,6 +95,8 @@ function open(option: string | Option) {
keepAlive: true,
params: option_.params ?? {},
menuId: menu.id,
icon: menu.icon,
breadcrumb: menu.breadcrumb,
}
ctx.insId = ctx.routeName
useAppPageStore().open(ctx)
@ -121,6 +127,8 @@ function open(option: string | Option) {
keepAlive: true,
params: option.params ?? {},
menuId: menu.id,
icon: menu.icon,
breadcrumb: menu.breadcrumb,
}
}
ctx.insId = ctx.routeName

View File

@ -1,4 +1,4 @@
<script generic="F extends G.PageParam,TT extends DefaultRow" lang="ts" setup>
<script generic="F extends object,TT extends DefaultRow" lang="ts" setup>
import {
elIcons,
type ElIconType,
@ -10,6 +10,7 @@ import Strings from '@/common/utils/strings.ts'
import type { R } from '@/common/utils/http-util.ts'
import type {
TableColumnCtx,
TableInstance,
TableProps,
} from 'element-plus'
import type { DefaultRow } from 'element-plus/es/components/table/src/table/defaults'
@ -44,17 +45,20 @@ export interface ActionColumnType<T> {
tableActions: TableActionType<T>[]
}
export type TablePropsType<T extends DefaultRow> = Omit<TableProps<T>, 'data' | 'headerRowClassName' | 'cellClassName' | 'context'>
export type TablePropsType<T extends DefaultRow, F extends object> = Omit<TableProps<T>, 'data' | 'headerRowClassName' | 'cellClassName' | 'context'> & {
treeLoad?: (param: F, row: T, expanded: any, resolve?: (data: T[]) => void) => void
}
export type FormPropsType = Partial<FormProps>
const props = withDefaults(
defineProps<{
defaultSearchForm?: F
paging: (param: F) => Promise<R<G.PageResult<TT>>>
paging?: (param: F) => Promise<R<G.PageResult<TT>>>
list?: (param: F) => Promise<R<TT[]>>
actionColumn?: ActionColumnType<TT>
leftTools?: ToolType[]
rightTools?: Required<Omit<ToolType, 'type' | 'label'>>[]
tableProps?: TablePropsType<TT>
tableProps?: TablePropsType<TT, F>
searchFormProps?: FormPropsType
simpleSearchFormProps?: FormPropsType
formStyle?: {
@ -100,6 +104,7 @@ const tableData = Utils.resetAble(reactive<TT[]>([])) as ResetAble<TT[]>
const totalCount = ref(0)
const loading = ref<boolean>(false)
const showSearchForm = ref<boolean>(false)
const dataTableIns = useTemplateRef<TableInstance>('dataTable')
function doReset() {
searchForm.$reset()
@ -108,15 +113,30 @@ function doReset() {
function doSearch() {
loading.value = true
props.paging(searchForm.$clone() as F)
.then((res) => {
totalCount.value = res.data?.total ?? 0
const records = res.data?.records ?? ([] as TT[])
tableData.$reset(records)
})
.finally(() => {
loading.value = false
})
if (props.paging != null) {
props.paging(searchForm.$clone() as F)
.then((res) => {
totalCount.value = res.data?.total ?? 0
const records = res.data?.records ?? ([] as TT[])
tableData.$reset(records)
})
.finally(() => {
loading.value = false
})
return
}
if (props.list != null) {
props.list(searchForm.$clone() as F)
.then((res) => {
totalCount.value = res.data?.length ?? 0
const records = res.data ?? ([] as TT[])
tableData.$reset(records)
})
.finally(() => {
loading.value = false
})
return
}
}
function showSearchFormHandle() {
@ -146,6 +166,7 @@ function rowAction(data: { row: TT, column: TableColumnCtx, $index: number }, ac
defineExpose({
doSearch,
dataTableIns,
})
onMounted(doSearch)
</script>
@ -156,7 +177,6 @@ onMounted(doSearch)
<ElScrollbar>
<ElForm :class="{'border-form':formStyle.border}" v-bind="searchFormProps" @submit.prevent="doSearch">
<slot :searchForm="searchForm" name="searchFormItem"/>
<ElFormItem class="form-action-btn">
<ElButton :icon="elIcons.Search" :loading="loading" native-type="submit" type="primary">搜索</ElButton>
<ElButton :icon="elIcons.Refresh" :loading="loading" @click="doReset"></ElButton>
@ -165,7 +185,7 @@ onMounted(doSearch)
</ElScrollbar>
</div>
<div class="data-list">
<div v-if="!Colls.isEmpty(leftTools) || !Colls.isEmpty(rightTools)" class="tool-bar">
<div class="tool-bar">
<div class="tool-bar-left">
<template v-if="!Colls.isEmpty(leftTools)">
<ElButton v-for="(tool,i) in leftTools" :key="'tool-bar-left-'+i"
@ -209,6 +229,10 @@ onMounted(doSearch)
<ElTable
v-loading="loading"
:data="tableData"
ref="dataTable"
:lazy="tableProps.treeLoad ==null?undefined:true"
:load="tableProps.treeLoad?((row, expanded, resolve)=>tableProps.treeLoad!(searchForm as F, row, expanded, resolve)):undefined"
@expand-change="tableProps.treeLoad?((row:any, expandedRows:any)=>tableProps.treeLoad!(searchForm as F, row, expandedRows, undefined)):undefined"
cell-class-name="table-cell"
class="table-list"
header-row-class-name="table-header"
@ -311,9 +335,10 @@ onMounted(doSearch)
</ElTableColumn>
</ElTable>
<ElPagination
v-if="paging!=null"
class="pagination"
v-model:current-page="searchForm.current"
v-model:page-size="searchForm.size"
v-model:current-page="(searchForm as G.PageParam).current"
v-model:page-size="(searchForm as G.PageParam).size"
:hide-on-single-page="false"
:page-sizes="[10, 20, 50, 100, 500]"
:teleported="false"
@ -329,7 +354,7 @@ onMounted(doSearch)
<style lang="stylus" scoped>
.form-page {
.search-form {
border: 1px solid #00000014;
border: 1px solid #EAEBF1;
padding: 20px;
border-radius: 8px;
background-color: white;
@ -401,7 +426,7 @@ onMounted(doSearch)
flex 1
display flex
flex-direction column
border: 1px solid #00000014;
border: 1px solid #EAEBF1;
padding: 15px 20px 20px 15px;
border-radius: 8px;
background-color: white;

View File

@ -14,7 +14,8 @@
width 100%;
overflow hidden
padding 5px
contain: layout paint;
transform: translateZ(0);
box-sizing border-box
//box-shadow: inset rgba(30, 35, 43, 0.16) 0px 0 10px 1px;
//background-color: white;

View File

@ -13,6 +13,8 @@ declare module 'vue' {
export interface GlobalComponents {
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
@ -32,8 +34,6 @@ declare module 'vue' {
ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElIconCircleClose: typeof import('@element-plus/icons-vue')['CircleClose']
ElIconCirclePlus: typeof import('@element-plus/icons-vue')['CirclePlus']
ElIconDelete: typeof import('@element-plus/icons-vue')['Delete']
ElIconPicture: typeof import('@element-plus/icons-vue')['Picture']
ElIconPlus: typeof import('@element-plus/icons-vue')['Plus']
ElImage: typeof import('element-plus/es')['ElImage']
@ -71,6 +71,8 @@ declare module 'vue' {
declare global {
const ElAside: typeof import('element-plus/es')['ElAside']
const ElAvatar: typeof import('element-plus/es')['ElAvatar']
const ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
const ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
const ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
@ -90,8 +92,6 @@ declare global {
const ElHeader: typeof import('element-plus/es')['ElHeader']
const ElIcon: typeof import('element-plus/es')['ElIcon']
const ElIconCircleClose: typeof import('@element-plus/icons-vue')['CircleClose']
const ElIconCirclePlus: typeof import('@element-plus/icons-vue')['CirclePlus']
const ElIconDelete: typeof import('@element-plus/icons-vue')['Delete']
const ElIconPicture: typeof import('@element-plus/icons-vue')['Picture']
const ElIconPlus: typeof import('@element-plus/icons-vue')['Plus']
const ElImage: typeof import('element-plus/es')['ElImage']

View File

@ -1,13 +1,11 @@
import {
ElButton,
ElIcon,
ElMenu,
ElMenuItem,
ElMenuItemGroup,
ElScrollbar,
ElSubMenu,
type MenuItemRegistered,
} from 'element-plus'
import { elIcons } from '@/common/element/element.ts'
import AIcon from '@/components/a-icon/AIcon.vue'
import type { IconName } from '@/components/a-icon/iconfont.ts'
import styles from '@/pages/a-frame/aaside.module.styl'
@ -17,6 +15,7 @@ import { MenuCategory } from '@/common/app/constants.ts'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import Nav from '@/common/router/nav.ts'
import logo from '@/assets/images/zsy.png'
export interface Menu extends G.TreeNode {
// Id
@ -44,11 +43,10 @@ export interface Menu extends G.TreeNode {
}
export default defineComponent(
() => {
(props) => {
const router = useRouter()
const appSettingStore = useAppSettingStore()
const defaultActive = ref<any>('')
const isCollapse = ref(false)
onMounted(() => {
const currentRouteName = router.currentRoute.value.name
@ -118,23 +116,27 @@ export default defineComponent(
return () => (
<>
<ElMenu default-active={defaultActive.value} collapse={isCollapse.value} class={[ styles.aMenus ]}>
{{
default: () => menuTree.value.map(renderMenu),
}}
</ElMenu>
<ElButton
class={[ styles.aCollapseBtn, {[styles.aCollapseBtnCollapse]: isCollapse.value} ]}
onClick={() => {
isCollapse.value = !isCollapse.value
}}
>
<ElIcon style={{cursor: 'pointer'}}>{isCollapse.value ? <elIcons.Fold/> : <elIcons.Expand/>}</ElIcon>
</ElButton>
<div class={[ styles.aAsideTop, {[styles.aAsideTopCollapse]: props.isCollapse} ]}>
<img alt="" src={logo}/>
<div></div>
</div>
<ElScrollbar class={styles.aScrollbar}>
<ElMenu default-active={defaultActive.value} collapse={props.isCollapse} class={[ styles.aMenus ]}>
{{
default: () => menuTree.value.map(renderMenu),
}}
</ElMenu>
</ElScrollbar>
</>
)
},
{
name: 'AAside',
props: {
isCollapse: {
type: Boolean,
default: false,
},
},
},
)

View File

@ -1,9 +1,15 @@
<script lang="ts" setup>
import AAside from '@/pages/a-frame/AAside.tsx'
import AAvatar from '@/pages/a-frame/AAvatar.vue'
import { appName } from '@/common'
import Evt from '@/common/utils/evt.ts'
import AIcon from '@/components/a-icon/AIcon.vue'
import { useAppPageStore } from '@/common/app/app-page-store.ts'
import { elIcons } from '@/common/element/element.ts'
import ATabbar from '@/pages/a-frame/ATabbar.vue'
const appPageStore = useAppPageStore()
const isCollapse = ref(false)
onMounted(() => {
Evt.emit('connect_ws')
@ -16,32 +22,35 @@ onUnmounted(() => {
<template>
<ElContainer class="a-frame">
<ElHeader>
<div>
<img alt="" src="@/assets/images/zsy.png"/>
<div>{{ appName }}</div>
</div>
<div>
<div>
<ElButton text>
<AIcon name="bell"/>
</ElButton>
<ElButton text>
<AIcon name="settings"/>
</ElButton>
</div>
<AAvatar/>
</div>
</ElHeader>
<ElAside class="a-frame-aside">
<AAside :is-collapse="isCollapse"/>
</ElAside>
<ElContainer>
<ElAside>
<ElScrollbar>
<AAside/>
</ElScrollbar>
</ElAside>
<ElMain>
<ElHeader class="a-frame-header">
<div>
<div>
<ElButton :icon="isCollapse?elIcons.Fold:elIcons.Expand" text @click="isCollapse = !isCollapse"/>
<ElBreadcrumb :separator-icon="elIcons.ArrowRight">
<ElBreadcrumbItem v-for="(item, i) in appPageStore?.ctx?.breadcrumb??[]" :key="'a-frame-header-breadcrumb'+i">{{ item }}</ElBreadcrumbItem>
</ElBreadcrumb>
</div>
<div>
<div>
<ElButton text>
<AIcon name="bell"/>
</ElButton>
<ElButton text>
<AIcon name="settings"/>
</ElButton>
</div>
<AAvatar/>
</div>
</div>
<ATabbar/>
</ElHeader>
<ElMain class="a-frame-main">
<RouterView #="{ Component }">
<Transition name="el-fade-in-linear">
<Transition name="slide-fade">
<component :is="Component"/>
</Transition>
</RouterView>
@ -55,74 +64,135 @@ onUnmounted(() => {
height 100%
width 100%;
overflow hidden
box-shadow: inset rgba(0, 0, 0, 0.12) 0px 0px 12px 0px;
box-shadow: inset #0000001F 0 0 12px 0;
background-color #F7F9FC
& > header {
display flex
justify-content space-between
border-bottom 1px solid #E5E7EB;
height 60px
.a-frame-aside {
height 100%;
width auto
position relative
border-right: solid 1px #EAEBF1;
//border-right: solid 1px var(--el-menu-border-color);
background-color white
& > div:first-child {
height: 100%;
display: flex;
align-items: center;
box-sizing border-box
img {
height 50%
margin-right 12px
}
& > div {
color: #165DFF;
font-weight: 700;
font-size: 1.5rem;
}
}
& > div:nth-child(2) {
height: 100%;
display: flex;
align-items: center;
box-sizing border-box
gap 20px
& > div:first-child {
display flex
gap 10px
& > button {
margin 0
}
}
}
box-sizing border-box
}
& > section {
width 100%;
height calc(100% - 60px)
height 100%
& > aside {
height 100%;
width auto
position relative
border-right: solid 1px var(--el-menu-border-color);
.a-frame-header {
height 100px
padding: 0 10px 0 0
background-color white
padding: 10px 0;
& > div:nth-child(1) {
height 60px
width 100%
display flex
justify-content space-between
box-sizing border-box
& > div:nth-child(1) {
height: 100%;
display: flex;
align-items: center;
box-sizing border-box
:deep(.el-breadcrumb) {
.el-breadcrumb__inner {
color #7987A1
cursor default
}
.el-breadcrumb__separator {
color #7987A1
}
}
}
& > div:nth-child(2) {
height: 100%;
display: flex;
align-items: center;
box-sizing border-box
gap 20px
& > div:first-child {
display flex
gap 10px
& > button {
margin 0
}
}
}
}
}
& > main {
height 100%
width calc(100% - 300px)
.a-frame-main {
height calc(100% - 60px)
width 100%;
padding 0
overflow auto
//background-color #F7F9FC
overflow hidden
background-color rgb(250, 251, 252)
}
}
}
/*.slide-fade-enter-active {
transition:
transform 0.4s cubic-bezier(0.25, 0.1, 0.25, 1),
opacity 0.4s ease-out;
will-change: transform, opacity;
backface-visibility: hidden;
transform-origin: 50% 50%;
}
.slide-fade-leave-active {
transition:
transform 0.3s cubic-bezier(0.5, 0, 0.2, 1),
opacity 0.3s ease-in;
will-change: transform, opacity;
backface-visibility: hidden;
transform-origin: 50% 50%;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: scaleY(0);
opacity: 0;
overflow: hidden;
}
.slide-fade-enter-to,
.slide-fade-leave-from {
transform: scaleY(1);
opacity: 1;
}*/
.slide-fade-enter-active {
//transition: transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
// opacity 0.3s ease-out;
transition: transform 0.3s ease-in-out,
opacity 0.3s ease-in-out;
will-change: transform, opacity;
}
.slide-fade-leave-active {
//transition: transform 0.3s cubic-bezier(0.5, 0, 0.2, 1),
// opacity 0.3s ease-in;
transition: transform 0.3s cubic-bezier(1, 0.5, 0.8, 1),
opacity 0.3s ease-in-out;
will-change: transform, opacity;
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateX(20px);
opacity: 0;
backface-visibility: hidden;
}
</style>

View File

@ -0,0 +1,150 @@
<template>
<div class="a-tabs">
<ElScrollbar class="a-tabs-scrollbar">
<div class="a-tabs-wrapper">
<div v-for="(item,i) in appPageStore.pages"
:key="'a-frame-header-tab'+i"
:class="{'a-tab-item-active': item.insId === appPageStore.currentPage}"
class="a-tab-item"
@click="reopen(item.insId)">
<div>
<AIcon :name="item.icon as IconName"/>
<div class="title">{{ item.title }}</div>
</div>
<ElButton :icon="elIcons.Close" text @click.stop="appPageStore.closePage(item.insId)"/>
</div>
</div>
</ElScrollbar>
<ElDropdown placement="bottom" @command="handleCommand">
<ElButton :icon="elIcons.More" text/>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="closeCurrent">关闭当前</ElDropdownItem>
<ElDropdownItem command="closeOther">关闭其他</ElDropdownItem>
<ElDropdownItem command="closeAll">关闭所有</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
</div>
</template>
<script lang="ts" setup>
import { useAppPageStore } from '@/common/app/app-page-store.ts'
import { elIcons } from '@/common/element/element.ts'
import type { IconName } from '@/components/a-icon/iconfont.ts'
import AIcon from '@/components/a-icon/AIcon.vue'
const appPageStore = useAppPageStore()
function reopen(insId: string) {
appPageStore.reopen(insId)
}
function handleCommand(command: 'closeCurrent' | 'closeOther' | 'closeAll') {
appPageStore[command]()
}
</script>
<style lang="stylus" scoped>
.a-tabs {
height 32px
width 100%;
box-sizing border-box
border-top 1px solid #EAEBF1;
border-bottom 1px solid #EAEBF1;
display flex
justify-content space-between
align-items center
color #303133
:deep(.el-dropdown) {
width: 32px;
padding: 0;
margin-right 20px
}
}
.a-tabs-scrollbar {
height 100%
width calc(100% - 52px);
box-sizing border-box
:deep(.el-scrollbar__wrap) {
height 100%
box-sizing border-box
}
:deep(.el-scrollbar__view) {
height 100%
box-sizing border-box
}
}
.a-tabs-wrapper {
height 100%
width: fit-content;
display flex
box-sizing border-box
align-items center
.a-tab-item {
height 100%
max-width 160px
padding 5px 10px
cursor pointer
display flex
justify-content space-between
gap 20px
align-items center
box-sizing border-box
position relative
border-right 1px solid #EAEBF1
transition color 0.2s ease-in-out
font-size 14px
& > div:first-child {
display flex
gap 10px
align-items center
}
& > button {
padding 0
width 14px
height 14px !important
line-height 14px
border-radius 50%
&:hover {
color var(--el-color-danger)
}
}
&::after {
content ''
height 2px
width 0
background-color var(--main-color)
position absolute
bottom 0
left: 50%;
transform: translateX(-50%);
transition width 0.2s ease-in-out
}
&.a-tab-item-active {
color var(--main-color)
&::after {
width 100%
}
}
&:hover {
color var(--main-color)
&::after {
width 100%
}
}
}
}
</style>

View File

@ -1,3 +1,55 @@
.a-aside-top {
height 60px
width 100%;
display: flex;
align-items: center;
box-sizing border-box
//padding 8px 8px 8px 18px;
padding-left 18px
//border-bottom: solid 1px var(--el-menu-border-color);
transition all 0.333s ease-in-out
gap 12px
overflow hidden
max-width 230px
//justify-content center
& > img {
height 32px
width 32px
//margin-left 18px
//transition margin-left 0.333s ease-in-out
}
& > div {
color: #252f4a;
font-weight: 700;
font-size: 1.5rem;
opacity 1
width 168px;
overflow hidden
transition all 0.333s ease-in-out
text-wrap nowrap
letter-spacing 10px
}
&.a-aside-top-collapse {
width 60px;
padding-left 14px
& > div {
opacity 0
width 0
}
}
}
.a-scrollbar {
height calc(100% - 60px)
width 100%;
padding: 0 0 10px 0;
box-sizing: border-box;
}
.a-menus {
height: 100%;
--el-menu-base-level-padding: 10px;
@ -65,7 +117,7 @@
bottom: 6px;
width: 32px;
height: 32px;
transition right 0.3s ease-in-out;
transition right 0.333s ease-in-out;
&.a-collapse-btn-collapse {
right: calc(50% - 16px);

View File

@ -4,6 +4,13 @@
<template>
<div>
<div class="home">
<div class="home-top">
<div class="home-top-title">
首页
</div>
</div>
</div>
</div>
</template>

View File

@ -10,7 +10,7 @@ import {
} from 'element-plus'
import Strings from '@/common/utils/strings.ts'
import FormUtil from '@/common/utils/formUtil.ts'
import { loadUserInfo } from '@/common/app'
import { reloadUserInfo } from '@/common/app'
const loginFormIns = useTemplateRef<FormInstance>('loginFormRef')
@ -55,7 +55,7 @@ function loginSubmitHandler() {
() => LoginApi.login(loginForm))
.then(({data}) => {
appUserStore.$patch({token: data.token})
return loadUserInfo()
return reloadUserInfo()
})
.then(() => {
Evt.emit('login')

View File

@ -1,34 +1,31 @@
<template>
<Page>
<ElForm v-show="showSearchForm" inline @submit.prevent="listAll">
<ElFormItem label-width="90" label="菜单名称">
<FormPage
ref="formPage"
:action-column="actionColumn"
:left-tools="leftTools"
:list="listAll"
:table-props="{
treeProps: { children: 'children', hasChildren: 'hasChildren' },
treeLoad: treeLoad,
}"
>
<template #searchFormItem="{ searchForm }">
<ElFormItem label="菜单名称">
<ElInput v-model="searchForm.title" placeholder="菜单名称"/>
</ElFormItem>
<ElFormItem label-width="90" label="路由名称">
<ElFormItem label="路由名称">
<ElInput v-model="searchForm.routeName" placeholder="路由名称"/>
</ElFormItem>
<ElFormItem>
<ElButton :icon="elIcons.Search" :loading="searching" native-type="submit" type="primary">搜索</ElButton>
<ElButton :icon="elIcons.Refresh" :loading="searching" @click="reset"></ElButton>
</ElFormItem>
</ElForm>
<div class="tool-bar">
<ElButton :icon="elIcons.Plus" type="primary" @click="addHandler"></ElButton>
<ElButton :icon="elIcons.Filter" type="default" @click="showSearchForm = !showSearchForm"/>
</div>
<ElTable v-loading="searching"
:data="tableData"
lazy
:load="treeLoad"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@expand-change="treeLoad"
cell-class-name="table-cell"
class="table-list"
empty-text="暂无数据"
header-row-class-name="table-header"
ref="dataTable"
row-key="id">
<!-- <ElTableColumn type="expand" width="60"/> -->
</template>
<template #columns>
<!--
:data="tableData"
lazy
:load="treeLoad"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
@expand-change="treeLoad"
-->
<ElTableColumn label="图标" prop="icon" width="100">
<template #default="scope">
<AIcon :name="scope.row.icon"/>
@ -37,7 +34,6 @@
<ElTableColumn label="类型" prop="menuCategoryTxt" width="140"/>
<ElTableColumn label="菜单名称" prop="title"/>
<ElTableColumn label="编码" prop="sn" width="180"/>
<!-- <ElTableColumn label="路径" prop="breadcrumb"/> -->
<ElTableColumn label="路由名称" prop="routeName">
<template #default="scope">
<span>
@ -52,102 +48,87 @@
</span>
</template>
</ElTableColumn>
<!-- <ElTableColumn label="排序" prop="sort" width="60"/> -->
<ElTableColumn label="操作" width="180">
<template #default="scope">
<ElPopconfirm
cancel-button-text="否"
cancel-button-type="primary"
confirm-button-text="是"
confirm-button-type="danger"
placement="top"
title="是否删除当前数据?"
width="180"
@confirm="delHandler(scope)">
<template #reference>
<ElButton :loading="deling" text type="danger">删除</ElButton>
</template>
</ElPopconfirm>
<ElButton text type="primary" @click="modifyHandler(scope)"></ElButton>
</template>
</ElTableColumn>
</ElTable>
<MenuForm ref="menuForm" @editSucc="editSuccHandler"/>
</template>
</Page>
<MenuForm ref="menuForm" @editSucc="research"/>
</FormPage>
</template>
<script lang="ts" setup>
import MenuApi from '@/pages/sys/menus/menu-api.ts'
import Page from '@/components/page/Page.vue'
import { onMounted } from 'vue'
import { elIcons } from '@/common/element/element.ts'
import MenuForm from '@/pages/sys/menus/MenuForm.vue'
import Strings from '@/common/utils/strings.ts'
import AIcon from '@/components/a-icon/AIcon.vue'
import type { TableInstance } from 'element-plus'
import FormPage, {
type ActionColumnType,
type ToolType,
} from '@/components/page/FormPage.vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
const tableData = ref<MenuTypes.SysMenu[]>([])
const searchForm = ref<MenuTypes.SearchForm>({})
const searching = ref(false)
const showSearchForm = ref(true)
const menuFormIns = useTemplateRef<InstanceType<typeof MenuForm>>('menuForm')
const dataTableIns = useTemplateRef<TableInstance>('dataTable')
const deling = ref(false)
const formPageIns = useTemplateRef<ComponentExposed<typeof FormPage>>('formPage')
function showDialog(data?: MenuTypes.MenuForm) {
menuFormIns.value?.open(data)
const actionColumn = reactive<ActionColumnType<MenuTypes.SysMenu>>({
tableActions: [
{
tooltip: '编辑',
icon: 'Edit',
action({row}) {
menuFormIns.value?.open(row as MenuTypes.MenuForm)
},
},
{
icon: 'Delete',
loading: false,
type: 'danger',
tooltip: '删除',
confirm: {
title: '是否删除当前数据',
},
action({row}) {
return MenuApi.del([ row.id! ])
.then(() => {
ElMessage.success('删除成功')
return true
})
},
},
],
})
const leftTools: ToolType[] = [
{
icon: 'Plus',
label: '新建',
action() {
menuFormIns.value?.open()
},
},
]
function research() {
formPageIns.value?.doSearch()
}
function delHandler({row}: { row: MenuTypes.MenuForm, }) {
deling.value = true
MenuApi.del([ row.id! ])
.then(() => {
ElMessage.success('删除成功')
listAll()
})
.finally(() => deling.value = false)
}
function modifyHandler({row}: { row: MenuTypes.MenuForm, }) {
showDialog(row)
}
function addHandler() {
showDialog()
}
function reset() {
searchForm.value = {}
listAll()
}
function editSuccHandler() {
listAll()
}
function listAll() {
searching.value = true
MenuApi.listAll({...searchForm.value, pid: '0'})
function listAll(param: MenuTypes.SearchForm) {
return MenuApi.listAll({...param, pid: '0'})
.then(res => {
tableData.value = []
tableData.value = res.data?.map(it => {
const data = res.data ?? []
for (let it of data) {
it.hasChildren = true
it.children = []
return it
}) ?? []
})
.finally(() => {
searching.value = false
}
return res
})
}
function treeLoad(row: MenuTypes.SysMenu, expanded: any, resolve: (data: MenuTypes.SysMenu[]) => void) {
function treeLoad(param: MenuTypes.SearchForm, row: MenuTypes.SysMenu, expanded: any, resolve: (data: MenuTypes.SysMenu[]) => void) {
if (resolve == null && !expanded) return
searching.value = true
MenuApi.listAll({...searchForm.value, pid: row.id})
MenuApi.listAll({...param, pid: row.id})
.then(res => {
if (resolve != null) {
resolve(res.data?.map(it => {
@ -155,64 +136,14 @@ function treeLoad(row: MenuTypes.SysMenu, expanded: any, resolve: (data: MenuTyp
return it
}) ?? [])
} else {
dataTableIns.value?.updateKeyChildren(row.id, res.data?.map(it => {
formPageIns.value?.dataTableIns?.updateKeyChildren(row.id, res.data?.map(it => {
it.hasChildren = true
it.children = []
return it
}) ?? [])
}
})
.finally(() => {
searching.value = false
})
}
onMounted(() => {
listAll()
})
</script>
<style lang="stylus" scoped>
.table-list {
flex 1;
width 100%
:deep(.table-header) {
color #454C59
th {
background-color #EDF1F7
font-weight 500
position relative
& > div {
display flex
gap 5px
align-items center
}
&:not(:first-child) > div::before {
position: absolute;
top: 50%;
left: 1px;
width: 1px;
background-color: #D3D7DE;
transform: translateY(-50%);
content: "";
height 50%
}
}
}
:deep(.table-cell) {
color #2F3540
}
}
.tool-bar {
display flex
justify-content space-between
margin 0 0 20px 0
}
</style>