@aquiferre/ui-kit 0.1.5 → 0.1.7
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/components/DataTable.vue +25 -2
- package/components/Modal.vue +79 -0
- package/components/SearchSelect.vue +142 -0
- package/composables/useToast.ts +22 -4
- package/index.ts +3 -1
- package/package.json +12 -4
- package/types.ts +7 -0
package/components/DataTable.vue
CHANGED
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
v-for="col in columns"
|
|
11
11
|
:key="col.key"
|
|
12
12
|
class="table-header-cell"
|
|
13
|
-
:class="col.width"
|
|
13
|
+
:class="[col.width, col.sortable ? 'cursor-pointer select-none' : '']"
|
|
14
14
|
:style="col.align ? `text-align:${col.align}` : undefined"
|
|
15
|
+
@click="col.sortable ? onSortClick(col) : undefined"
|
|
15
16
|
>
|
|
16
|
-
|
|
17
|
+
<span class="inline-flex items-center gap-1">
|
|
18
|
+
{{ col.title }}
|
|
19
|
+
<span v-if="col.sortable" class="text-[10px] leading-none">
|
|
20
|
+
<span v-if="sort && sort.field === (col.sortField || col.key)" class="text-accent-primary">
|
|
21
|
+
{{ sort.order === 'asc' ? '▲' : '▼' }}
|
|
22
|
+
</span>
|
|
23
|
+
<span v-else class="text-text-tertiary">↕</span>
|
|
24
|
+
</span>
|
|
25
|
+
</span>
|
|
17
26
|
</th>
|
|
18
27
|
</tr>
|
|
19
28
|
</thead>
|
|
@@ -67,19 +76,33 @@ const props = withDefaults(defineProps<{
|
|
|
67
76
|
selectable?: boolean
|
|
68
77
|
selected?: string[]
|
|
69
78
|
rowKey?: string
|
|
79
|
+
sort?: { field: string; order: 'asc' | 'desc' } | null
|
|
70
80
|
}>(), {
|
|
71
81
|
loading: false,
|
|
72
82
|
emptyText: '暂无数据',
|
|
73
83
|
selectable: false,
|
|
74
84
|
selected: () => [],
|
|
75
85
|
rowKey: 'id',
|
|
86
|
+
sort: null,
|
|
76
87
|
})
|
|
77
88
|
|
|
78
89
|
const emit = defineEmits<{
|
|
79
90
|
'update:selected': [ids: string[]]
|
|
80
91
|
'row-click': [row: any]
|
|
92
|
+
'sort-change': [payload: { field: string; order: 'asc' | 'desc' } | null]
|
|
81
93
|
}>()
|
|
82
94
|
|
|
95
|
+
function onSortClick(col: Column) {
|
|
96
|
+
const field = col.sortField || col.key
|
|
97
|
+
if (!props.sort || props.sort.field !== field) {
|
|
98
|
+
emit('sort-change', { field, order: 'asc' })
|
|
99
|
+
} else if (props.sort.order === 'asc') {
|
|
100
|
+
emit('sort-change', { field, order: 'desc' })
|
|
101
|
+
} else {
|
|
102
|
+
emit('sort-change', null)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
83
106
|
const colspan = computed(() => props.columns.length + (props.selectable ? 1 : 0))
|
|
84
107
|
|
|
85
108
|
const selectedSet = computed(() => new Set(props.selected))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<Transition name="fade">
|
|
4
|
+
<div v-if="visible" class="dialog-overlay">
|
|
5
|
+
<div class="fixed inset-0 bg-bg-overlay" @click="onOverlayClick" />
|
|
6
|
+
<div class="panel-glass dialog" :class="[sizeClass, customClass]">
|
|
7
|
+
<div v-if="title" class="flex items-center justify-between px-6 py-4 border-b border-border-primary">
|
|
8
|
+
<h3 class="text-lg font-medium text-text-primary">{{ title }}</h3>
|
|
9
|
+
<button
|
|
10
|
+
v-if="closable"
|
|
11
|
+
class="text-text-tertiary hover:text-text-primary text-2xl leading-none"
|
|
12
|
+
@click="close"
|
|
13
|
+
>×</button>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="px-6 py-4">
|
|
16
|
+
<slot />
|
|
17
|
+
</div>
|
|
18
|
+
<div v-if="$slots.footer" class="px-6 py-4 border-t border-border-primary bg-bg-secondary/50 rounded-b-2xl">
|
|
19
|
+
<slot name="footer" />
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</Transition>
|
|
24
|
+
</Teleport>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import { computed } from 'vue'
|
|
29
|
+
|
|
30
|
+
const props = withDefaults(defineProps<{
|
|
31
|
+
visible: boolean
|
|
32
|
+
title?: string
|
|
33
|
+
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
34
|
+
closable?: boolean
|
|
35
|
+
maskClosable?: boolean
|
|
36
|
+
customClass?: string
|
|
37
|
+
}>(), {
|
|
38
|
+
closable: true,
|
|
39
|
+
maskClosable: true,
|
|
40
|
+
size: 'md',
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
'update:visible': [value: boolean]
|
|
45
|
+
close: []
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
const sizeClass = computed(() => {
|
|
49
|
+
const map: Record<string, string> = {
|
|
50
|
+
sm: 'max-w-sm',
|
|
51
|
+
md: 'max-w-lg',
|
|
52
|
+
lg: 'max-w-2xl',
|
|
53
|
+
xl: 'max-w-4xl',
|
|
54
|
+
}
|
|
55
|
+
return map[props.size] || 'max-w-lg'
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
function close() {
|
|
59
|
+
emit('update:visible', false)
|
|
60
|
+
emit('close')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onOverlayClick() {
|
|
64
|
+
if (props.maskClosable) {
|
|
65
|
+
close()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<style scoped>
|
|
71
|
+
.fade-enter-active,
|
|
72
|
+
.fade-leave-active {
|
|
73
|
+
transition: opacity 0.2s ease;
|
|
74
|
+
}
|
|
75
|
+
.fade-enter-from,
|
|
76
|
+
.fade-leave-to {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="rootRef" class="relative w-full">
|
|
3
|
+
<div
|
|
4
|
+
class="input flex items-center w-full focus-within:border-accent-primary"
|
|
5
|
+
:class="disabled ? 'opacity-60 cursor-not-allowed' : ''"
|
|
6
|
+
>
|
|
7
|
+
<input
|
|
8
|
+
ref="inputRef"
|
|
9
|
+
:value="inputValue"
|
|
10
|
+
:placeholder="displayLabel || placeholder"
|
|
11
|
+
:disabled="disabled"
|
|
12
|
+
class="flex-1 min-w-0 bg-transparent border-none outline-none text-input-text placeholder:text-input-placeholder"
|
|
13
|
+
@focus="onFocus"
|
|
14
|
+
@input="onInput"
|
|
15
|
+
/>
|
|
16
|
+
<button
|
|
17
|
+
v-if="clearable && modelValue"
|
|
18
|
+
type="button"
|
|
19
|
+
class="px-2 text-text-tertiary hover:text-text-primary"
|
|
20
|
+
@click.stop="clear"
|
|
21
|
+
tabindex="-1"
|
|
22
|
+
>✕</button>
|
|
23
|
+
<button
|
|
24
|
+
type="button"
|
|
25
|
+
class="px-2 text-text-tertiary hover:text-text-primary"
|
|
26
|
+
@click.stop="toggle"
|
|
27
|
+
tabindex="-1"
|
|
28
|
+
>▾</button>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div
|
|
32
|
+
v-if="open"
|
|
33
|
+
class="absolute top-full left-0 mt-1 w-full max-h-60 overflow-auto card shadow-lg z-20 py-1"
|
|
34
|
+
>
|
|
35
|
+
<div v-if="filtered.length === 0" class="px-4 py-2 text-sm text-text-tertiary">无匹配项</div>
|
|
36
|
+
<div
|
|
37
|
+
v-for="opt in filtered"
|
|
38
|
+
:key="opt.value"
|
|
39
|
+
class="px-4 py-2 text-sm text-text-primary hover:bg-bg-hover cursor-pointer"
|
|
40
|
+
:class="opt.value === modelValue ? 'bg-bg-secondary' : ''"
|
|
41
|
+
@mousedown.prevent="select(opt)"
|
|
42
|
+
>
|
|
43
|
+
{{ opt.label }}
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</template>
|
|
48
|
+
|
|
49
|
+
<script setup lang="ts">
|
|
50
|
+
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
|
51
|
+
import { match } from 'pinyin-pro'
|
|
52
|
+
import type { SelectOption } from '../types'
|
|
53
|
+
|
|
54
|
+
const props = withDefaults(defineProps<{
|
|
55
|
+
modelValue: string
|
|
56
|
+
options: SelectOption[]
|
|
57
|
+
placeholder?: string
|
|
58
|
+
clearable?: boolean
|
|
59
|
+
disabled?: boolean
|
|
60
|
+
}>(), {
|
|
61
|
+
placeholder: '请选择',
|
|
62
|
+
clearable: true,
|
|
63
|
+
disabled: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
'update:modelValue': [value: string]
|
|
68
|
+
'change': [value: string]
|
|
69
|
+
}>()
|
|
70
|
+
|
|
71
|
+
const rootRef = ref<HTMLElement | null>(null)
|
|
72
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
73
|
+
const open = ref(false)
|
|
74
|
+
const query = ref('')
|
|
75
|
+
|
|
76
|
+
const displayLabel = computed(() => {
|
|
77
|
+
const opt = props.options.find(o => o.value === props.modelValue)
|
|
78
|
+
return opt ? opt.label : ''
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const inputValue = computed(() => (open.value ? query.value : displayLabel.value))
|
|
82
|
+
|
|
83
|
+
const filtered = computed(() => {
|
|
84
|
+
const q = query.value.trim()
|
|
85
|
+
if (!q) return props.options
|
|
86
|
+
const ql = q.toLowerCase()
|
|
87
|
+
return props.options.filter(o =>
|
|
88
|
+
o.label.toLowerCase().includes(ql) || match(o.label, q) !== null
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
function onInput(e: Event) {
|
|
93
|
+
query.value = (e.target as HTMLInputElement).value
|
|
94
|
+
open.value = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function onFocus() {
|
|
98
|
+
open.value = true
|
|
99
|
+
query.value = ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function select(opt: SelectOption) {
|
|
103
|
+
emit('update:modelValue', opt.value)
|
|
104
|
+
emit('change', opt.value)
|
|
105
|
+
query.value = ''
|
|
106
|
+
open.value = false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clear() {
|
|
110
|
+
emit('update:modelValue', '')
|
|
111
|
+
emit('change', '')
|
|
112
|
+
query.value = ''
|
|
113
|
+
open.value = false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function toggle() {
|
|
117
|
+
if (props.disabled) return
|
|
118
|
+
open.value = !open.value
|
|
119
|
+
if (open.value) {
|
|
120
|
+
query.value = ''
|
|
121
|
+
inputRef.value?.focus()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleOutside(e: MouseEvent) {
|
|
126
|
+
if (rootRef.value && !rootRef.value.contains(e.target as Node)) {
|
|
127
|
+
open.value = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onMounted(() => {
|
|
132
|
+
document.addEventListener('mousedown', handleOutside)
|
|
133
|
+
})
|
|
134
|
+
onUnmounted(() => {
|
|
135
|
+
document.removeEventListener('mousedown', handleOutside)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// 选项变化时(如异步加载完成),若已选中则无需调整;仅保证关闭态显示正确
|
|
139
|
+
watch(() => props.options, () => {
|
|
140
|
+
if (!open.value) query.value = ''
|
|
141
|
+
})
|
|
142
|
+
</script>
|
package/composables/useToast.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref } from 'vue'
|
|
1
|
+
import { ref, type Ref } from 'vue'
|
|
2
2
|
import type { ToastType } from '../types'
|
|
3
3
|
|
|
4
4
|
interface ToastItem {
|
|
@@ -7,20 +7,38 @@ interface ToastItem {
|
|
|
7
7
|
message: string
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// 在全局对象上存储 toast 状态,确保模块重复加载时也能共享状态
|
|
11
|
+
function getGlobalToasts(): Ref<ToastItem[]> {
|
|
12
|
+
const w = window as any
|
|
13
|
+
if (!w.__TOAST_STATE__) {
|
|
14
|
+
w.__TOAST_STATE__ = ref<ToastItem[]>([])
|
|
15
|
+
}
|
|
16
|
+
return w.__TOAST_STATE__
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getGlobalNextId(): { value: number } {
|
|
20
|
+
const w = window as any
|
|
21
|
+
if (!w.__TOAST_NEXT_ID__) {
|
|
22
|
+
w.__TOAST_NEXT_ID__ = { value: 0 }
|
|
23
|
+
}
|
|
24
|
+
return w.__TOAST_NEXT_ID__
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
function add(type: ToastType, message: string) {
|
|
14
|
-
const
|
|
28
|
+
const toasts = getGlobalToasts()
|
|
29
|
+
const nextId = getGlobalNextId()
|
|
30
|
+
const id = nextId.value++
|
|
15
31
|
toasts.value.push({ id, type, message })
|
|
16
32
|
setTimeout(() => remove(id), 3000)
|
|
17
33
|
}
|
|
18
34
|
|
|
19
35
|
function remove(id: number) {
|
|
36
|
+
const toasts = getGlobalToasts()
|
|
20
37
|
toasts.value = toasts.value.filter(t => t.id !== id)
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
export function useToast() {
|
|
41
|
+
const toasts = getGlobalToasts()
|
|
24
42
|
return {
|
|
25
43
|
toasts,
|
|
26
44
|
success: (msg: string) => add('success', msg),
|
package/index.ts
CHANGED
|
@@ -6,11 +6,13 @@ export { default as SearchInput } from './components/SearchInput.vue'
|
|
|
6
6
|
export { default as FilterDropdown } from './components/FilterDropdown.vue'
|
|
7
7
|
export { default as FormDialog } from './components/FormDialog.vue'
|
|
8
8
|
export { default as ConfirmDialog } from './components/ConfirmDialog.vue'
|
|
9
|
+
export { default as Modal } from './components/Modal.vue'
|
|
9
10
|
export { default as Toast } from './components/Toast.vue'
|
|
10
11
|
export { default as StatusTag } from './components/StatusTag.vue'
|
|
11
12
|
export { default as FileUploader } from './components/FileUploader.vue'
|
|
12
13
|
export { default as UserPicker } from './components/UserPicker.vue'
|
|
14
|
+
export { default as SearchSelect } from './components/SearchSelect.vue'
|
|
13
15
|
export { default as SideMenu } from './components/SideMenu.vue'
|
|
14
16
|
export { default as TopNavbar } from './components/TopNavbar.vue'
|
|
15
17
|
export { useToast } from './composables/useToast'
|
|
16
|
-
export type { Column, MenuItem, MenuGroup, NavModule, DropdownItem, ToastType } from './types'
|
|
18
|
+
export type { Column, SelectOption, MenuItem, MenuGroup, NavModule, DropdownItem, ToastType } from './types'
|
package/package.json
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aquiferre/ui-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": "./index.ts"
|
|
9
9
|
},
|
|
10
|
-
"files": [
|
|
10
|
+
"files": [
|
|
11
|
+
"*.ts",
|
|
12
|
+
"*.vue",
|
|
13
|
+
"components/**/*.vue",
|
|
14
|
+
"composables/**/*.ts"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"pinyin-pro": "^3.28.1"
|
|
18
|
+
},
|
|
11
19
|
"peerDependencies": {
|
|
12
|
-
"
|
|
20
|
+
"@aquiferre/theme-kit": ">=0.1.0",
|
|
13
21
|
"pinia": ">=2.0.0",
|
|
14
|
-
"
|
|
22
|
+
"vue": ">=3.0.0"
|
|
15
23
|
},
|
|
16
24
|
"publishConfig": {
|
|
17
25
|
"access": "public"
|
package/types.ts
CHANGED