xto-fronted 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,17 @@ import { Button } from '@xto/base'
10
10
  import { Input } from '@xto/form'
11
11
  import type { MenuItem as MenuItemType } from '@/types/api'
12
12
 
13
+ const props = withDefaults(
14
+ defineProps<{
15
+ mode?: 'vertical' | 'horizontal'
16
+ showLogo?: boolean
17
+ }>(),
18
+ {
19
+ mode: 'vertical',
20
+ showLogo: true
21
+ }
22
+ )
23
+
13
24
  const route = useRoute()
14
25
  const router = useRouter()
15
26
  const menuStore = useMenuStore()
@@ -103,18 +114,25 @@ const getMenuIcon = (icon?: string) => {
103
114
  }
104
115
  return iconMap[icon || ''] || '📄'
105
116
  }
117
+
118
+ const isVertical = computed(() => props.mode === 'vertical')
119
+ const isHorizontal = computed(() => props.mode === 'horizontal')
106
120
  </script>
107
121
 
108
122
  <template>
109
- <div class="sidebar">
110
- <!-- Logo -->
111
- <div class="sidebar__logo">
123
+ <div class="sidebar" :class="[
124
+ `sidebar--${mode}`,
125
+ { 'sidebar--collapsed': isCollapsed && isVertical },
126
+ { 'sidebar--horizontal': isHorizontal }
127
+ ]">
128
+ <!-- Logo(仅垂直模式显示) -->
129
+ <div v-if="showLogo && isVertical" class="sidebar__logo">
112
130
  <img src="/vite.svg" alt="Logo" class="sidebar__logo-img" />
113
- <span v-show="!isCollapsed" class="sidebar__logo-text">Xto Demo</span>
131
+ <span v-show="!isCollapsed" class="sidebar__logo-text">{{ appStore.appName }}</span>
114
132
  </div>
115
133
 
116
- <!-- 搜索框 -->
117
- <div v-if="!isCollapsed" class="sidebar__search">
134
+ <!-- 搜索框(仅垂直模式显示) -->
135
+ <div v-if="isVertical && !isCollapsed" class="sidebar__search">
118
136
  <Input
119
137
  v-model="searchKeyword"
120
138
  placeholder="搜索菜单..."
@@ -141,7 +159,8 @@ const getMenuIcon = (icon?: string) => {
141
159
  <!-- 菜单 -->
142
160
  <Menu
143
161
  :default-active="activeMenu"
144
- :collapse="isCollapsed"
162
+ :mode="mode"
163
+ :collapse="isCollapsed && isVertical"
145
164
  :collapse-transition="false"
146
165
  :background-color="menuBgColor"
147
166
  :text-color="menuTextColor"
@@ -173,11 +192,11 @@ const getMenuIcon = (icon?: string) => {
173
192
  </template>
174
193
  </Menu>
175
194
 
176
- <!-- 用户信息 -->
177
- <div class="sidebar__user" v-if="!isCollapsed">
195
+ <!-- 用户信息(仅垂直模式显示) -->
196
+ <div v-if="isVertical && !isCollapsed" class="sidebar__user">
178
197
  <div class="sidebar__user-info">
179
198
  <span class="sidebar__user-name">{{ userStore.nickname }}</span>
180
- <span class="sidebar__user-role">{{ userStore.roles.join(', ') }}</span>
199
+ <span class="sidebar__user-role">{{ userStore.roles?.join(', ') }}</span>
181
200
  </div>
182
201
  <Button type="text" size="small" @click="handleLogout">退出</Button>
183
202
  </div>
@@ -191,11 +210,36 @@ const getMenuIcon = (icon?: string) => {
191
210
  flex-direction: column;
192
211
  background-color: var(--bg-color);
193
212
 
213
+ // 垂直模式
214
+ &--vertical {
215
+ border-right: 1px solid var(--color-border-lighter);
216
+ }
217
+
218
+ // 水平模式
219
+ &--horizontal {
220
+ flex-direction: row;
221
+ border: none;
222
+ background-color: transparent;
223
+
224
+ .sidebar__menu {
225
+ border: none;
226
+ background-color: transparent;
227
+ }
228
+ }
229
+
230
+ // 折叠状态
231
+ &--collapsed.sidebar--vertical {
232
+ .sidebar__logo {
233
+ justify-content: center;
234
+ padding: 0;
235
+ }
236
+ }
237
+
194
238
  &__logo {
195
239
  height: 50px;
196
240
  display: flex;
197
241
  align-items: center;
198
- justify-content: center;
242
+ padding: 0 20px;
199
243
  gap: 10px;
200
244
  border-bottom: 1px solid var(--color-border-lighter);
201
245
  }
@@ -9,19 +9,73 @@ const appStore = useAppStore()
9
9
  const sidebarWidth = computed(() =>
10
10
  appStore.isCollapsed ? '64px' : '210px'
11
11
  )
12
+
13
+ const layoutMode = computed(() => appStore.layout)
14
+
15
+ const isSidebarMode = computed(() => layoutMode.value === 'sidebar')
16
+ const isTopMode = computed(() => layoutMode.value === 'top')
17
+ const isMixMode = computed(() => layoutMode.value === 'mix')
12
18
  </script>
13
19
 
14
20
  <template>
15
- <div class="layout">
16
- <aside class="layout__aside" :style="{ width: sidebarWidth }">
17
- <Sidebar />
18
- </aside>
19
- <div class="layout__main">
20
- <Header class="layout__header" />
21
+ <div class="layout" :class="`layout--${layoutMode}`">
22
+ <!-- 顶部模式:顶部导航 + 内容 -->
23
+ <template v-if="isTopMode">
24
+ <header class="layout__header-top">
25
+ <div class="layout__header-logo">
26
+ <img src="/vite.svg" alt="Logo" class="layout__logo-img" />
27
+ <span class="layout__logo-text">{{ appStore.appName }}</span>
28
+ </div>
29
+ <Sidebar class="layout__top-menu" mode="horizontal" />
30
+ <div class="layout__header-right">
31
+ <Header :show-breadcrumb="false" :show-collapse="false" />
32
+ </div>
33
+ </header>
21
34
  <main class="layout__content">
22
35
  <router-view />
23
36
  </main>
24
- </div>
37
+ </template>
38
+
39
+ <!-- 混合模式:顶部导航 + 左侧菜单 -->
40
+ <template v-else-if="isMixMode">
41
+ <header class="layout__header-mix">
42
+ <div class="layout__header-left">
43
+ <div class="layout__collapse" @click="appStore.toggleCollapse">
44
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
45
+ <path v-if="appStore.isCollapsed" d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
46
+ <path v-else d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
47
+ </svg>
48
+ </div>
49
+ <Sidebar class="layout__top-menu" mode="horizontal" />
50
+ </div>
51
+ <div class="layout__header-right">
52
+ <Header :show-breadcrumb="false" :show-collapse="false" />
53
+ </div>
54
+ </header>
55
+ <div class="layout__body">
56
+ <aside class="layout__aside" :style="{ width: sidebarWidth }">
57
+ <Sidebar mode="vertical" :show-logo="false" />
58
+ </aside>
59
+ <main class="layout__content">
60
+ <router-view />
61
+ </main>
62
+ </div>
63
+ </template>
64
+
65
+ <!-- 侧边栏模式:左侧菜单 + 顶部 -->
66
+ <template v-else-if="isSidebarMode">
67
+ <aside class="layout__aside" :style="{ width: sidebarWidth }">
68
+ <Sidebar />
69
+ </aside>
70
+ <div class="layout__main">
71
+ <header class="layout__header">
72
+ <Header />
73
+ </header>
74
+ <main class="layout__content">
75
+ <router-view />
76
+ </main>
77
+ </div>
78
+ </template>
25
79
  </div>
26
80
  </template>
27
81
 
@@ -31,6 +85,126 @@ const sidebarWidth = computed(() =>
31
85
  width: 100%;
32
86
  height: 100%;
33
87
 
88
+ // 侧边栏模式
89
+ &--sidebar {
90
+ flex-direction: row;
91
+ }
92
+
93
+ // 顶部模式
94
+ &--top {
95
+ flex-direction: column;
96
+
97
+ .layout__header-top {
98
+ height: 50px;
99
+ background-color: var(--bg-color);
100
+ border-bottom: 1px solid var(--color-border-lighter);
101
+ display: flex;
102
+ align-items: center;
103
+ padding: 0 20px;
104
+ gap: 20px;
105
+ }
106
+
107
+ .layout__header-logo {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 10px;
111
+ }
112
+
113
+ .layout__logo-img {
114
+ width: 32px;
115
+ height: 32px;
116
+ }
117
+
118
+ .layout__logo-text {
119
+ font-size: 16px;
120
+ font-weight: 600;
121
+ color: var(--color-primary);
122
+ }
123
+
124
+ .layout__top-menu {
125
+ flex: 1;
126
+ border: none;
127
+ }
128
+
129
+ .layout__header-right {
130
+ display: flex;
131
+ align-items: center;
132
+ }
133
+
134
+ .layout__content {
135
+ flex: 1;
136
+ overflow: auto;
137
+ background-color: var(--bg-color-page);
138
+ }
139
+ }
140
+
141
+ // 混合模式
142
+ &--mix {
143
+ flex-direction: column;
144
+
145
+ .layout__header-mix {
146
+ height: 50px;
147
+ background-color: var(--bg-color);
148
+ border-bottom: 1px solid var(--color-border-lighter);
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: space-between;
152
+ padding: 0 20px;
153
+ }
154
+
155
+ .layout__header-left {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 15px;
159
+ }
160
+
161
+ .layout__collapse {
162
+ width: 24px;
163
+ height: 24px;
164
+ display: flex;
165
+ align-items: center;
166
+ justify-content: center;
167
+ cursor: pointer;
168
+ color: var(--color-text-regular);
169
+ transition: color 0.2s;
170
+
171
+ &:hover {
172
+ color: var(--color-primary);
173
+ }
174
+ }
175
+
176
+ .layout__top-menu {
177
+ border: none;
178
+ }
179
+
180
+ .layout__header-right {
181
+ display: flex;
182
+ align-items: center;
183
+ }
184
+
185
+ .layout__body {
186
+ flex: 1;
187
+ display: flex;
188
+ overflow: hidden;
189
+ }
190
+
191
+ .layout__aside {
192
+ transition: width 0.3s;
193
+ overflow: hidden;
194
+ flex-shrink: 0;
195
+ height: 100%;
196
+ background-color: var(--bg-color);
197
+ border-right: 1px solid var(--color-border-lighter);
198
+ }
199
+
200
+ .layout__content {
201
+ flex: 1;
202
+ overflow: auto;
203
+ background-color: var(--bg-color-page);
204
+ }
205
+ }
206
+
207
+ // 通用样式
34
208
  &__aside {
35
209
  transition: width 0.3s;
36
210
  overflow: hidden;