@aquiferre/ui-kit 0.1.6 → 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.
@@ -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
- {{ col.title }}
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,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/index.ts CHANGED
@@ -11,7 +11,8 @@ export { default as Toast } from './components/Toast.vue'
11
11
  export { default as StatusTag } from './components/StatusTag.vue'
12
12
  export { default as FileUploader } from './components/FileUploader.vue'
13
13
  export { default as UserPicker } from './components/UserPicker.vue'
14
+ export { default as SearchSelect } from './components/SearchSelect.vue'
14
15
  export { default as SideMenu } from './components/SideMenu.vue'
15
16
  export { default as TopNavbar } from './components/TopNavbar.vue'
16
17
  export { useToast } from './composables/useToast'
17
- 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@aquiferre/ui-kit",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -13,6 +13,9 @@
13
13
  "components/**/*.vue",
14
14
  "composables/**/*.ts"
15
15
  ],
16
+ "dependencies": {
17
+ "pinyin-pro": "^3.28.1"
18
+ },
16
19
  "peerDependencies": {
17
20
  "@aquiferre/theme-kit": ">=0.1.0",
18
21
  "pinia": ">=2.0.0",
package/types.ts CHANGED
@@ -3,6 +3,13 @@ export interface Column {
3
3
  title: string
4
4
  width?: string
5
5
  align?: 'left' | 'center' | 'right'
6
+ sortable?: boolean
7
+ sortField?: string
8
+ }
9
+
10
+ export interface SelectOption {
11
+ value: string
12
+ label: string
6
13
  }
7
14
 
8
15
  export interface MenuItem {