xto-fronted 0.9.3 → 0.9.5
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.
- package/dist/components/FilterBar.vue.d.ts +48 -0
- package/dist/components/TableCard.vue.d.ts +23 -0
- package/dist/{index-U6a8866w.js → index-BARaN_X9.js} +1 -1
- package/dist/{index-DkpGZDBh.js → index-Bae5aKOp.js} +1 -1
- package/dist/{index-B6OJp8lX.js → index-BuduWJdN.js} +1 -1
- package/dist/index-CoJBjzC9.js +3452 -0
- package/dist/{index-rfFkJbFI.js → index-CoJj_x19.js} +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +100 -98
- package/dist/style.css +1 -1
- package/package.json +2 -2
- package/src/components/FilterBar.vue +156 -0
- package/src/components/Layout/Sidebar.vue +5 -0
- package/src/components/Layout/SidebarMenuItem.vue +70 -37
- package/src/components/Layout/index.vue +16 -2
- package/src/components/TableCard.vue +72 -0
- package/src/index.ts +2 -0
- package/dist/index-BGjN8fL4.js +0 -3268
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FilterBar 筛选栏
|
|
4
|
+
*
|
|
5
|
+
* 统一列表页筛选区排版:inline 表单 + 内建「查询/重置」操作区。
|
|
6
|
+
* 筛选项超过 collapsedCount 时自动支持折叠,收起时只显示前 N 项,
|
|
7
|
+
* 点击「展开/收起」切换,避免筛选区占据过多纵向空间。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* <FilterBar :model="searchForm" :collapsed-count="3" @search="load" @reset="onReset">
|
|
11
|
+
* <x-form-item label="平台" name="clientId">...</x-form-item>
|
|
12
|
+
* </FilterBar>
|
|
13
|
+
*/
|
|
14
|
+
import { computed, nextTick, onMounted, onUpdated, ref, watch } from 'vue'
|
|
15
|
+
import { Icon } from '@xto/base'
|
|
16
|
+
|
|
17
|
+
defineOptions({
|
|
18
|
+
name: 'FilterBar'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const props = withDefaults(
|
|
22
|
+
defineProps<{
|
|
23
|
+
// 表单数据对象,与 x-form 的 model 一致
|
|
24
|
+
model: Record<string, any>
|
|
25
|
+
// 是否启用折叠
|
|
26
|
+
collapsible?: boolean
|
|
27
|
+
// 收起时显示的筛选项数量
|
|
28
|
+
collapsedCount?: number
|
|
29
|
+
// 初始是否收起
|
|
30
|
+
defaultCollapsed?: boolean
|
|
31
|
+
searchText?: string
|
|
32
|
+
resetText?: string
|
|
33
|
+
searchLoading?: boolean
|
|
34
|
+
}>(),
|
|
35
|
+
{
|
|
36
|
+
collapsible: true,
|
|
37
|
+
collapsedCount: 3,
|
|
38
|
+
defaultCollapsed: true,
|
|
39
|
+
searchText: '查询',
|
|
40
|
+
resetText: '重置',
|
|
41
|
+
searchLoading: false
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const emit = defineEmits<{
|
|
46
|
+
search: []
|
|
47
|
+
reset: []
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
const formRef = ref<any>(null)
|
|
51
|
+
const collapsed = ref(props.defaultCollapsed)
|
|
52
|
+
const totalCount = ref(0)
|
|
53
|
+
|
|
54
|
+
// 应用折叠:收起时隐藏第 collapsedCount 项之后的筛选项。
|
|
55
|
+
// 直接操作 slot 渲染出的 .x-form-item(排除操作区),v-if 会销毁组件丢状态,不采用
|
|
56
|
+
const refreshItems = () => {
|
|
57
|
+
const formEl: HTMLElement | undefined = formRef.value?.$el
|
|
58
|
+
if (!formEl) return
|
|
59
|
+
const items = formEl.querySelectorAll(':scope > .x-form-item:not(.filter-bar__ops)')
|
|
60
|
+
totalCount.value = items.length
|
|
61
|
+
items.forEach((el, i) => {
|
|
62
|
+
;(el as HTMLElement).style.display = collapsed.value && i >= props.collapsedCount ? 'none' : ''
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hasOverflow = computed(() => props.collapsible && totalCount.value > props.collapsedCount)
|
|
67
|
+
|
|
68
|
+
const toggleCollapsed = () => {
|
|
69
|
+
collapsed.value = !collapsed.value
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
watch(collapsed, () => nextTick(refreshItems))
|
|
73
|
+
onMounted(() => nextTick(refreshItems))
|
|
74
|
+
// 动态筛选项(v-if 切换)或折叠状态变化后重新应用
|
|
75
|
+
onUpdated(() => nextTick(refreshItems))
|
|
76
|
+
|
|
77
|
+
const handleSearch = () => emit('search')
|
|
78
|
+
const handleReset = () => emit('reset')
|
|
79
|
+
|
|
80
|
+
defineExpose({
|
|
81
|
+
/** 展开筛选项 */
|
|
82
|
+
expand: () => { collapsed.value = false },
|
|
83
|
+
/** 收起筛选项 */
|
|
84
|
+
collapse: () => { collapsed.value = true }
|
|
85
|
+
})
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<x-card class="filter-bar">
|
|
90
|
+
<x-form
|
|
91
|
+
ref="formRef"
|
|
92
|
+
:model="model"
|
|
93
|
+
layout="inline"
|
|
94
|
+
@submit="handleSearch"
|
|
95
|
+
@reset="handleReset"
|
|
96
|
+
>
|
|
97
|
+
<slot />
|
|
98
|
+
<x-form-item class="filter-bar__ops">
|
|
99
|
+
<x-button
|
|
100
|
+
type="primary"
|
|
101
|
+
size="small"
|
|
102
|
+
html-type="submit"
|
|
103
|
+
:loading="searchLoading"
|
|
104
|
+
>{{ searchText }}</x-button>
|
|
105
|
+
<x-button size="small" html-type="reset">{{ resetText }}</x-button>
|
|
106
|
+
<x-button
|
|
107
|
+
v-if="hasOverflow"
|
|
108
|
+
type="text"
|
|
109
|
+
size="small"
|
|
110
|
+
class="filter-bar__toggle"
|
|
111
|
+
@click="toggleCollapsed"
|
|
112
|
+
>
|
|
113
|
+
{{ collapsed ? '展开' : '收起' }}
|
|
114
|
+
<Icon
|
|
115
|
+
name="caret-down"
|
|
116
|
+
:size="12"
|
|
117
|
+
class="filter-bar__caret"
|
|
118
|
+
:class="{ 'is-open': !collapsed }"
|
|
119
|
+
/>
|
|
120
|
+
</x-button>
|
|
121
|
+
</x-form-item>
|
|
122
|
+
</x-form>
|
|
123
|
+
</x-card>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<style lang="scss" scoped>
|
|
127
|
+
.filter-bar {
|
|
128
|
+
display: block;
|
|
129
|
+
margin-bottom: 2px;
|
|
130
|
+
|
|
131
|
+
:deep(.x-card__body) {
|
|
132
|
+
padding: 5px 8px !important;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.filter-bar__ops {
|
|
137
|
+
:deep(.x-button + .x-button) {
|
|
138
|
+
margin-left: 8px;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.filter-bar__toggle {
|
|
143
|
+
:deep(.x-icon),
|
|
144
|
+
.filter-bar__caret {
|
|
145
|
+
margin-left: 2px;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.filter-bar__caret {
|
|
150
|
+
transition: transform var(--xto-transition-duration-fast, 150ms) var(--xto-transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
|
151
|
+
|
|
152
|
+
&.is-open {
|
|
153
|
+
transform: rotate(180deg);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -29,10 +29,70 @@ const knownIcons = new Set([
|
|
|
29
29
|
'moon', 'sun', 'theme', 'skin'
|
|
30
30
|
])
|
|
31
31
|
|
|
32
|
+
// Element UI(el-icon-*)图标名到组件库图标名的映射:
|
|
33
|
+
// sys_menu.icon 大量使用 el-icon-* 命名,不映射时只能退化为首字徽标
|
|
34
|
+
const elIconMap: Record<string, string> = {
|
|
35
|
+
'el-icon-s-platform': 'system',
|
|
36
|
+
'el-icon-platform': 'system',
|
|
37
|
+
'el-icon-s-data': 'database',
|
|
38
|
+
'el-icon-data-analysis': 'chart',
|
|
39
|
+
'el-icon-files': 'folder',
|
|
40
|
+
'el-icon-folder': 'folder',
|
|
41
|
+
'el-icon-folder-opened': 'folder-open',
|
|
42
|
+
'el-icon-s-custom': 'user',
|
|
43
|
+
'el-icon-user': 'user',
|
|
44
|
+
'el-icon-s-grid': 'grid',
|
|
45
|
+
'el-icon-menu': 'list',
|
|
46
|
+
'el-icon-setting': 'setting',
|
|
47
|
+
'el-icon-s-tools': 'setting',
|
|
48
|
+
'el-icon-s-operation': 'setting',
|
|
49
|
+
'el-icon-s-home': 'home',
|
|
50
|
+
'el-icon-house': 'home',
|
|
51
|
+
'el-icon-document': 'document',
|
|
52
|
+
'el-icon-document-copy': 'copy',
|
|
53
|
+
'el-icon-monitor': 'dashboard',
|
|
54
|
+
'el-icon-odometer': 'dashboard',
|
|
55
|
+
'el-icon-s-order': 'list',
|
|
56
|
+
'el-icon-s-check': 'check',
|
|
57
|
+
'el-icon-s-claim': 'file',
|
|
58
|
+
'el-icon-s-comment': 'message',
|
|
59
|
+
'el-icon-s-promotion': 'share',
|
|
60
|
+
'el-icon-s-marketing': 'chart',
|
|
61
|
+
'el-icon-s-finance': 'chart',
|
|
62
|
+
'el-icon-s-management': 'list',
|
|
63
|
+
'el-icon-s-cooperation': 'user-group',
|
|
64
|
+
'el-icon-s-shop': 'gift',
|
|
65
|
+
'el-icon-picture': 'image',
|
|
66
|
+
'el-icon-location': 'location',
|
|
67
|
+
'el-icon-map-location': 'map',
|
|
68
|
+
'el-icon-lock': 'lock',
|
|
69
|
+
'el-icon-unlock': 'unlock',
|
|
70
|
+
'el-icon-key': 'key',
|
|
71
|
+
'el-icon-bell': 'bell',
|
|
72
|
+
'el-icon-edit': 'edit',
|
|
73
|
+
'el-icon-delete': 'delete',
|
|
74
|
+
'el-icon-link': 'link',
|
|
75
|
+
'el-icon-connection': 'link',
|
|
76
|
+
'el-icon-upload': 'upload',
|
|
77
|
+
'el-icon-upload2': 'upload',
|
|
78
|
+
'el-icon-download': 'download',
|
|
79
|
+
'el-icon-search': 'search',
|
|
80
|
+
'el-icon-timer': 'clock',
|
|
81
|
+
'el-icon-time': 'clock',
|
|
82
|
+
'el-icon-calendar': 'calendar',
|
|
83
|
+
'el-icon-view': 'eye',
|
|
84
|
+
'el-icon-message': 'mail',
|
|
85
|
+
'el-icon-chat-dot-round': 'chat',
|
|
86
|
+
'el-icon-cpu': 'server',
|
|
87
|
+
'el-icon-cloudy': 'cloud'
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
// 获取菜单图标名称
|
|
33
91
|
const getMenuIcon = (icon?: string): string => {
|
|
34
92
|
if (!icon || icon === '') return ''
|
|
35
93
|
|
|
94
|
+
if (elIconMap[icon]) return elIconMap[icon]
|
|
95
|
+
|
|
36
96
|
if (icon.startsWith('tineco-icon-')) {
|
|
37
97
|
const iconName = icon.replace('tineco-icon-', '')
|
|
38
98
|
const tinecoIconMap: Record<string, string> = {
|
|
@@ -74,7 +134,7 @@ const getMenuIcon = (icon?: string): string => {
|
|
|
74
134
|
return iconMap[icon] || icon
|
|
75
135
|
}
|
|
76
136
|
|
|
77
|
-
//
|
|
137
|
+
// 获取菜单名称第一个字(无匹配图标时的兜底)
|
|
78
138
|
const getFirstChar = (name?: string): string => {
|
|
79
139
|
if (!name) return ''
|
|
80
140
|
return name.charAt(0)
|
|
@@ -99,15 +159,11 @@ const hasChildren = computed(() => {
|
|
|
99
159
|
<template>
|
|
100
160
|
<!-- 有子菜单 -->
|
|
101
161
|
<SubMenu v-if="hasChildren" :index="menuIndex">
|
|
102
|
-
<template #
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
<Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
|
|
106
|
-
<span v-else class="menu-item__char">{{ getFirstChar(menu.menuName) }}</span>
|
|
107
|
-
</span>
|
|
108
|
-
<span class="menu-item__text">{{ menu.menuName }}</span>
|
|
109
|
-
</span>
|
|
162
|
+
<template #icon>
|
|
163
|
+
<Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
|
|
164
|
+
<span v-else class="menu-item__char">{{ getFirstChar(menu.menuName) }}</span>
|
|
110
165
|
</template>
|
|
166
|
+
<template #title>{{ menu.menuName }}</template>
|
|
111
167
|
<!-- 递归渲染子菜单 -->
|
|
112
168
|
<template v-for="child in menu.children" :key="child.menuId || child.menuCode || child.menuUrl">
|
|
113
169
|
<SidebarMenuItem :menu="child" />
|
|
@@ -116,38 +172,15 @@ const hasChildren = computed(() => {
|
|
|
116
172
|
|
|
117
173
|
<!-- 无子菜单 -->
|
|
118
174
|
<MenuItem v-else :index="menu.menuUrl || menuIndex">
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
<span class="menu-item__text">{{ menu.menuName }}</span>
|
|
125
|
-
</span>
|
|
175
|
+
<template #icon>
|
|
176
|
+
<Icon v-if="iconExists(getMenuIcon(menu.icon))" :name="getMenuIcon(menu.icon)" :size="16" />
|
|
177
|
+
<span v-else class="menu-item__char">{{ getFirstChar(menu.menuName) }}</span>
|
|
178
|
+
</template>
|
|
179
|
+
{{ menu.menuName }}
|
|
126
180
|
</MenuItem>
|
|
127
181
|
</template>
|
|
128
182
|
|
|
129
183
|
<style lang="scss" scoped>
|
|
130
|
-
.menu-item__content {
|
|
131
|
-
display: inline-flex;
|
|
132
|
-
align-items: center;
|
|
133
|
-
line-height: 1;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
.menu-item__text {
|
|
137
|
-
flex: 1;
|
|
138
|
-
line-height: 1;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.menu-item__icon {
|
|
142
|
-
display: inline-flex;
|
|
143
|
-
align-items: center;
|
|
144
|
-
justify-content: center;
|
|
145
|
-
width: 16px;
|
|
146
|
-
height: 16px;
|
|
147
|
-
margin-right: 8px;
|
|
148
|
-
flex-shrink: 0;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
184
|
.menu-item__char {
|
|
152
185
|
display: inline-flex;
|
|
153
186
|
align-items: center;
|
|
@@ -53,7 +53,11 @@ const showMixSidebar = computed(() => {
|
|
|
53
53
|
<template>
|
|
54
54
|
<!-- sidebar模式:左侧菜单 + 顶部Header + 主内容 -->
|
|
55
55
|
<div v-if="layoutMode === 'sidebar'" class="layout layout--sidebar">
|
|
56
|
-
<aside
|
|
56
|
+
<aside
|
|
57
|
+
class="layout__aside"
|
|
58
|
+
:class="{ 'layout__aside--collapsed': appStore.isCollapsed }"
|
|
59
|
+
:style="{ width: sidebarWidth }"
|
|
60
|
+
>
|
|
57
61
|
<Sidebar :menu-list="sidebarMenuList" :show-logo="true" :show-user="true" :logo-src="resolvedLogoSrc" />
|
|
58
62
|
</aside>
|
|
59
63
|
<div class="layout__main">
|
|
@@ -86,7 +90,12 @@ const showMixSidebar = computed(() => {
|
|
|
86
90
|
</div>
|
|
87
91
|
<!-- 下方:左侧子菜单 + 主内容 -->
|
|
88
92
|
<div class="layout__mix-body">
|
|
89
|
-
<aside
|
|
93
|
+
<aside
|
|
94
|
+
v-if="showMixSidebar"
|
|
95
|
+
class="layout__aside"
|
|
96
|
+
:class="{ 'layout__aside--collapsed': appStore.isCollapsed }"
|
|
97
|
+
:style="{ width: sidebarWidth }"
|
|
98
|
+
>
|
|
90
99
|
<Sidebar :menu-list="sidebarMenuList" :show-logo="false" :show-user="false" />
|
|
91
100
|
</aside>
|
|
92
101
|
<main class="layout__content">
|
|
@@ -159,6 +168,11 @@ const showMixSidebar = computed(() => {
|
|
|
159
168
|
overflow: hidden;
|
|
160
169
|
flex-shrink: 0;
|
|
161
170
|
height: 100%;
|
|
171
|
+
|
|
172
|
+
// 折叠态:弹出子菜单浮层需要溢出侧栏显示
|
|
173
|
+
&--collapsed {
|
|
174
|
+
overflow: visible;
|
|
175
|
+
}
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
&__top-menu {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TableCard 表格卡片
|
|
4
|
+
*
|
|
5
|
+
* 统一列表页表格区排版(方案 B):操作按钮放表格卡片 header 右侧,
|
|
6
|
+
* 与卡片标题同行,取代单独占一行的工具栏卡片。
|
|
7
|
+
* 不传 title/actions/header 时与普通 x-card 完全一致。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* <TableCard title="用户列表" class="table-card">
|
|
11
|
+
* <template #actions>
|
|
12
|
+
* <x-button type="primary" size="small" @click="add">新增用户</x-button>
|
|
13
|
+
* </template>
|
|
14
|
+
* <x-table ... />
|
|
15
|
+
* </TableCard>
|
|
16
|
+
*/
|
|
17
|
+
defineOptions({
|
|
18
|
+
name: 'TableCard'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
defineProps<{
|
|
22
|
+
/** 卡片标题(header 左侧),也可用 #header 插槽自定义 */
|
|
23
|
+
title?: string
|
|
24
|
+
}>()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<x-card class="table-card-frame">
|
|
29
|
+
<template v-if="title || $slots.header || $slots.actions" #header>
|
|
30
|
+
<div class="table-card__header">
|
|
31
|
+
<div class="table-card__title">
|
|
32
|
+
<slot name="header">{{ title }}</slot>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="table-card__actions">
|
|
35
|
+
<slot name="actions" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</template>
|
|
39
|
+
<slot />
|
|
40
|
+
</x-card>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<style lang="scss" scoped>
|
|
44
|
+
.table-card-frame {
|
|
45
|
+
display: block;
|
|
46
|
+
|
|
47
|
+
:deep(.x-card__header) {
|
|
48
|
+
padding: 10px 12px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.table-card__header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
gap: 12px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.table-card__title {
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
color: var(--xto-color-text-primary, #1F2329);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.table-card__actions {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
gap: 8px;
|
|
70
|
+
margin-left: auto;
|
|
71
|
+
}
|
|
72
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -110,6 +110,8 @@ export { default as Header } from './components/Layout/Header.vue'
|
|
|
110
110
|
export { default as Sidebar } from './components/Layout/Sidebar.vue'
|
|
111
111
|
export { default as Tabs } from './components/Layout/Tabs.vue'
|
|
112
112
|
export { default as Footer } from './components/Layout/Footer.vue'
|
|
113
|
+
export { default as FilterBar } from './components/FilterBar.vue'
|
|
114
|
+
export { default as TableCard } from './components/TableCard.vue'
|
|
113
115
|
|
|
114
116
|
// 错误页面组件
|
|
115
117
|
export { default as Login } from './views/login/index.vue'
|