@xlui/xux-ui 0.1.0

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/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@xlui/xux-ui",
3
+ "version": "0.1.0",
4
+ "description": "VUE3 电商组件库",
5
+ "author": "leheya",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.js"
15
+ },
16
+ "./*": {
17
+ "types": "./dist/*.d.ts",
18
+ "import": "./dist/*.mjs",
19
+ "require": "./dist/*.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "dev": "vite build --watch",
29
+ "build": "vite build",
30
+ "prepublishOnly": "pnpm build"
31
+ },
32
+ "keywords": [
33
+ "vue3",
34
+ "vue3-ui",
35
+ "components",
36
+ "ui-library"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://gitee.com/leheya/xux"
41
+ },
42
+ "homepage": "https://gitee.com/leheya/xux",
43
+ "bugs": {
44
+ "url": "https://gitee.com/leheya/xux/issues"
45
+ },
46
+ "peerDependencies": {
47
+ "vue": "^3.3.0"
48
+ },
49
+ "devDependencies": {
50
+ "@vitejs/plugin-vue": "^5.0.3",
51
+ "typescript": "^5.3.3",
52
+ "vite": "^5.0.11",
53
+ "vue": "^3.4.15",
54
+ "vue-tsc": "^1.8.27"
55
+ }
56
+ }
57
+
@@ -0,0 +1,355 @@
1
+ <template>
2
+ <div
3
+ class="accordion-container"
4
+ :class="{ 'accordion-container--bordered': bordered }"
5
+ >
6
+ <div
7
+ v-for="(item, index) in items"
8
+ :key="item.id || index"
9
+ class="accordion-item"
10
+ :class="{
11
+ 'is-open': isOpen(index),
12
+ 'is-disabled': disabled || item.disabled
13
+ }"
14
+ >
15
+ <button
16
+ @click="toggle(index)"
17
+ class="accordion-header"
18
+ :disabled="disabled || item.disabled"
19
+ :aria-expanded="isOpen(index)"
20
+ :aria-controls="`accordion-content-${index}`"
21
+ >
22
+ <span class="accordion-title">{{ item.title }}</span>
23
+ <div class="accordion-icon">
24
+ <svg
25
+ class="accordion-arrow"
26
+ :class="{ 'accordion-arrow--rotate': isOpen(index) }"
27
+ fill="none"
28
+ stroke="currentColor"
29
+ viewBox="0 0 24 24"
30
+ strokeWidth="2"
31
+ >
32
+ <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
33
+ </svg>
34
+ </div>
35
+ </button>
36
+
37
+ <div
38
+ :id="`accordion-content-${index}`"
39
+ class="accordion-content"
40
+ :style="{ maxHeight: isOpen(index) ? `${contentHeights[index]}px` : '0px' }"
41
+ :ref="(el: any) => setContentRef(el as HTMLElement, index)"
42
+ >
43
+ <div class="accordion-body">
44
+ {{ item.content }}
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </template>
50
+
51
+ <script setup lang="ts">
52
+ import { withDefaults, watch, nextTick, ref } from 'vue'
53
+
54
+ /**
55
+ * Accordion 手风琴组件
56
+ * @displayName XAccordion
57
+ */
58
+
59
+ export interface AccordionItem {
60
+ id?: string | number
61
+ title: string
62
+ content: string
63
+ disabled?: boolean
64
+ }
65
+
66
+ export interface AccordionProps {
67
+ items: AccordionItem[]
68
+ allowMultiple?: boolean // 是否允许同时展开多个项目
69
+ defaultOpen?: number[] // 默认展开的项目索引
70
+ bordered?: boolean // 是否显示边框
71
+ disabled?: boolean // 是否禁用
72
+ }
73
+
74
+ const props = withDefaults(defineProps<AccordionProps>(), {
75
+ allowMultiple: false,
76
+ defaultOpen: () => [],
77
+ bordered: true,
78
+ disabled: false
79
+ })
80
+
81
+ const emit = defineEmits<{
82
+ 'change': [openItems: number[]]
83
+ 'item-click': [index: number, isOpen: boolean]
84
+ }>()
85
+
86
+ // 管理展开状态
87
+ const openItems = ref<Set<number>>(new Set(props.defaultOpen))
88
+ // 存储内容高度
89
+ const contentHeights = ref<Record<number, number>>({})
90
+ // 存储内容元素引用
91
+ const contentRefs = ref<Record<number, HTMLElement>>({})
92
+
93
+ // 设置内容元素引用
94
+ const setContentRef = (el: HTMLElement | null, index: number) => {
95
+ if (el) {
96
+ contentRefs.value[index] = el
97
+ // 计算内容高度
98
+ nextTick(() => {
99
+ const body = el.querySelector('.accordion-body') as HTMLElement
100
+ if (body) {
101
+ contentHeights.value[index] = body.scrollHeight
102
+ }
103
+ })
104
+ }
105
+ }
106
+
107
+ // 切换展开/收起状态
108
+ const toggle = (index: number) => {
109
+ if (props.disabled || props.items[index]?.disabled) {
110
+ return
111
+ }
112
+
113
+ const wasOpen = openItems.value.has(index)
114
+
115
+ if (props.allowMultiple) {
116
+ // 允许多个展开
117
+ if (wasOpen) {
118
+ openItems.value.delete(index)
119
+ } else {
120
+ openItems.value.add(index)
121
+ }
122
+ } else {
123
+ // 只允许一个展开
124
+ if (wasOpen) {
125
+ openItems.value.clear()
126
+ } else {
127
+ openItems.value.clear()
128
+ openItems.value.add(index)
129
+ }
130
+ }
131
+
132
+ // 触发事件
133
+ emit('item-click', index, !wasOpen)
134
+ emit('change', Array.from(openItems.value))
135
+ }
136
+
137
+ // 检查是否展开
138
+ const isOpen = (index: number): boolean => {
139
+ return openItems.value.has(index)
140
+ }
141
+
142
+ // 监听默认展开项的变化
143
+ watch(() => props.defaultOpen, (newDefault) => {
144
+ openItems.value = new Set(newDefault)
145
+ }, { deep: true })
146
+
147
+ // 监听items变化,重新计算高度
148
+ watch(() => props.items, () => {
149
+ nextTick(() => {
150
+ // 重新计算所有内容高度
151
+ Object.keys(contentRefs.value).forEach(key => {
152
+ const index = parseInt(key)
153
+ const el = contentRefs.value[index]
154
+ if (el) {
155
+ const body = el.querySelector('.accordion-body') as HTMLElement
156
+ if (body) {
157
+ contentHeights.value[index] = body.scrollHeight
158
+ }
159
+ }
160
+ })
161
+ })
162
+ }, { deep: true })
163
+
164
+ // 暴露的方法
165
+ const openItem = (index: number) => {
166
+ if (!props.disabled && !props.items[index]?.disabled) {
167
+ if (!openItems.value.has(index)) {
168
+ if (!props.allowMultiple) {
169
+ openItems.value.clear()
170
+ }
171
+ openItems.value.add(index)
172
+ emit('change', Array.from(openItems.value))
173
+ }
174
+ }
175
+ }
176
+
177
+ const closeItem = (index: number) => {
178
+ if (openItems.value.has(index)) {
179
+ openItems.value.delete(index)
180
+ emit('change', Array.from(openItems.value))
181
+ }
182
+ }
183
+
184
+ const openAll = () => {
185
+ if (props.allowMultiple && !props.disabled) {
186
+ props.items.forEach((item, index) => {
187
+ if (!item.disabled) {
188
+ openItems.value.add(index)
189
+ }
190
+ })
191
+ emit('change', Array.from(openItems.value))
192
+ }
193
+ }
194
+
195
+ const closeAll = () => {
196
+ openItems.value.clear()
197
+ emit('change', [])
198
+ }
199
+
200
+ defineExpose({
201
+ openItem,
202
+ closeItem,
203
+ openAll,
204
+ closeAll,
205
+ toggle
206
+ })
207
+ </script>
208
+
209
+ <style scoped>
210
+ .accordion-container {
211
+ width: 100%;
212
+ font-family: var(--x-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
213
+ }
214
+
215
+ .accordion-container--bordered {
216
+ border: 1px solid var(--x-color-gray-200, #e9ecef);
217
+ border-radius: var(--x-radius-lg, 8px);
218
+ overflow: hidden;
219
+ }
220
+
221
+ .accordion-item {
222
+ border-bottom: 1px solid var(--x-color-gray-100, #f1f5f9);
223
+ background: white;
224
+ transition: all var(--x-transition, 0.2s ease);
225
+ }
226
+
227
+ .accordion-item:last-child {
228
+ border-bottom: none;
229
+ }
230
+
231
+ .accordion-item:hover:not(.is-disabled) {
232
+ background: var(--x-color-gray-50, #fafbfc);
233
+ }
234
+
235
+ .accordion-item.is-open {
236
+ background: rgba(26, 26, 26, 0.02);
237
+ }
238
+
239
+ .accordion-item.is-disabled {
240
+ opacity: 0.6;
241
+ cursor: not-allowed;
242
+ }
243
+
244
+ .accordion-header {
245
+ width: 100%;
246
+ display: flex;
247
+ justify-content: space-between;
248
+ align-items: center;
249
+ padding: 18px 20px;
250
+ text-align: left;
251
+ border: none;
252
+ background: transparent;
253
+ cursor: pointer;
254
+ transition: all var(--x-transition, 0.2s ease);
255
+ font-size: var(--x-font-size-base, 15px);
256
+ font-weight: 500;
257
+ color: var(--x-color-gray-900, #1e293b);
258
+ outline: none;
259
+ user-select: none;
260
+ }
261
+
262
+ .accordion-header:disabled {
263
+ cursor: not-allowed;
264
+ }
265
+
266
+ .accordion-header:focus-visible {
267
+ outline: 2px solid var(--x-color-primary, #1a1a1a);
268
+ outline-offset: -2px;
269
+ }
270
+
271
+ .accordion-title {
272
+ flex: 1;
273
+ margin-right: 12px;
274
+ line-height: 1.5;
275
+ }
276
+
277
+ .accordion-icon {
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ width: 32px;
282
+ height: 32px;
283
+ border-radius: 50%;
284
+ background: var(--x-color-gray-100, #f1f5f9);
285
+ transition: all var(--x-transition, 0.3s ease);
286
+ color: var(--x-color-gray-500, #64748b);
287
+ flex-shrink: 0;
288
+ }
289
+
290
+ .accordion-item:hover:not(.is-disabled) .accordion-icon {
291
+ background: var(--x-color-gray-200, #e2e8f0);
292
+ color: var(--x-color-gray-600, #475569);
293
+ }
294
+
295
+ .accordion-item.is-open .accordion-icon {
296
+ background: rgba(26, 26, 26, 0.08);
297
+ color: var(--x-color-primary, #1a1a1a);
298
+ }
299
+
300
+ .accordion-arrow {
301
+ width: 20px;
302
+ height: 20px;
303
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
304
+ }
305
+
306
+ .accordion-arrow--rotate {
307
+ transform: rotate(180deg);
308
+ }
309
+
310
+ .accordion-content {
311
+ overflow: hidden;
312
+ transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
313
+ background: rgba(26, 26, 26, 0.01);
314
+ }
315
+
316
+ .accordion-body {
317
+ padding: 0 20px 20px 20px;
318
+ color: var(--x-color-gray-600, #64748b);
319
+ line-height: 1.6;
320
+ font-size: var(--x-font-size-sm, 14px);
321
+ }
322
+
323
+ /* 响应式设计 */
324
+ @media (max-width: 768px) {
325
+ .accordion-header {
326
+ padding: 16px 16px;
327
+ font-size: 14px;
328
+ }
329
+
330
+ .accordion-body {
331
+ padding: 0 16px 16px 16px;
332
+ font-size: 13px;
333
+ }
334
+
335
+ .accordion-icon {
336
+ width: 28px;
337
+ height: 28px;
338
+ }
339
+
340
+ .accordion-icon svg {
341
+ width: 16px;
342
+ height: 16px;
343
+ }
344
+ }
345
+
346
+ /* 动画优化 */
347
+ @media (prefers-reduced-motion: reduce) {
348
+ .accordion-content,
349
+ .accordion-header,
350
+ .accordion-icon,
351
+ .accordion-item {
352
+ transition: none;
353
+ }
354
+ }
355
+ </style>