little-dizzy 2.4.0 → 2.5.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.
@@ -0,0 +1,224 @@
1
+ <template>
2
+ <div class="ld-loading" :class="[`ld-loading--${type}`, { 'ld-loading--fullscreen': fullscreen }]">
3
+ <!-- Spinner -->
4
+ <template v-if="type === 'spinner'">
5
+ <div class="spinner" :style="spinnerStyle">
6
+ <div v-for="i in 12" :key="i" class="spinner-blade" />
7
+ </div>
8
+ </template>
9
+
10
+ <!-- Dots -->
11
+ <template v-else-if="type === 'dots'">
12
+ <div class="dots">
13
+ <span v-for="i in 3" :key="i" class="dot" :style="{ background: color }" />
14
+ </div>
15
+ </template>
16
+
17
+ <!-- Ring -->
18
+ <template v-else-if="type === 'ring'">
19
+ <div class="ring" :style="ringStyle">
20
+ <div class="ring-inner" />
21
+ </div>
22
+ </template>
23
+
24
+ <!-- Pulse -->
25
+ <template v-else-if="type === 'pulse'">
26
+ <div class="pulse" :style="{ background: color, width: size, height: size }" />
27
+ </template>
28
+
29
+ <!-- Bars -->
30
+ <template v-else-if="type === 'bars'">
31
+ <div class="bars">
32
+ <span v-for="i in 5" :key="i" class="bar" :style="{ background: color }" />
33
+ </div>
34
+ </template>
35
+
36
+ <!-- 文字 -->
37
+ <div v-if="text" class="loading-text">{{ text }}</div>
38
+ </div>
39
+ </template>
40
+
41
+ <script setup>
42
+ import { computed } from 'vue'
43
+
44
+ defineOptions({
45
+ name: 'Loading'
46
+ })
47
+
48
+ const props = defineProps({
49
+ type: {
50
+ type: String,
51
+ default: 'spinner',
52
+ validator: (val) => ['spinner', 'dots', 'ring', 'pulse', 'bars'].includes(val)
53
+ },
54
+ size: {
55
+ type: String,
56
+ default: '40px'
57
+ },
58
+ color: {
59
+ type: String,
60
+ default: 'var(--ld-color-primary, #6366f1)'
61
+ },
62
+ text: {
63
+ type: String,
64
+ default: ''
65
+ },
66
+ fullscreen: {
67
+ type: Boolean,
68
+ default: false
69
+ }
70
+ })
71
+
72
+ const spinnerStyle = computed(() => ({
73
+ width: props.size,
74
+ height: props.size,
75
+ '--spinner-color': props.color
76
+ }))
77
+
78
+ const ringStyle = computed(() => ({
79
+ width: props.size,
80
+ height: props.size,
81
+ '--ring-color': props.color
82
+ }))
83
+ </script>
84
+
85
+ <style scoped>
86
+ .ld-loading {
87
+ display: inline-flex;
88
+ flex-direction: column;
89
+ align-items: center;
90
+ justify-content: center;
91
+ gap: 12px;
92
+ }
93
+
94
+ .ld-loading--fullscreen {
95
+ position: fixed;
96
+ top: 0;
97
+ left: 0;
98
+ right: 0;
99
+ bottom: 0;
100
+ background: var(--ld-loading-fullscreen-bg, rgba(255, 255, 255, 0.9));
101
+ z-index: 9999;
102
+ }
103
+
104
+ .loading-text {
105
+ color: var(--ld-color-text-secondary, #666);
106
+ font-size: 14px;
107
+ }
108
+
109
+ /* Spinner */
110
+ .spinner {
111
+ position: relative;
112
+ }
113
+
114
+ .spinner-blade {
115
+ position: absolute;
116
+ left: 50%;
117
+ top: 50%;
118
+ width: 8%;
119
+ height: 24%;
120
+ background: var(--spinner-color, #6366f1);
121
+ border-radius: 4px;
122
+ transform-origin: center bottom;
123
+ animation: spinner-fade 1s infinite linear;
124
+ }
125
+
126
+ .spinner-blade:nth-child(1) { transform: translateX(-50%) rotate(0deg) translateY(-120%); animation-delay: -0.083s; }
127
+ .spinner-blade:nth-child(2) { transform: translateX(-50%) rotate(30deg) translateY(-120%); animation-delay: -0.166s; }
128
+ .spinner-blade:nth-child(3) { transform: translateX(-50%) rotate(60deg) translateY(-120%); animation-delay: -0.25s; }
129
+ .spinner-blade:nth-child(4) { transform: translateX(-50%) rotate(90deg) translateY(-120%); animation-delay: -0.333s; }
130
+ .spinner-blade:nth-child(5) { transform: translateX(-50%) rotate(120deg) translateY(-120%); animation-delay: -0.416s; }
131
+ .spinner-blade:nth-child(6) { transform: translateX(-50%) rotate(150deg) translateY(-120%); animation-delay: -0.5s; }
132
+ .spinner-blade:nth-child(7) { transform: translateX(-50%) rotate(180deg) translateY(-120%); animation-delay: -0.583s; }
133
+ .spinner-blade:nth-child(8) { transform: translateX(-50%) rotate(210deg) translateY(-120%); animation-delay: -0.666s; }
134
+ .spinner-blade:nth-child(9) { transform: translateX(-50%) rotate(240deg) translateY(-120%); animation-delay: -0.75s; }
135
+ .spinner-blade:nth-child(10) { transform: translateX(-50%) rotate(270deg) translateY(-120%); animation-delay: -0.833s; }
136
+ .spinner-blade:nth-child(11) { transform: translateX(-50%) rotate(300deg) translateY(-120%); animation-delay: -0.916s; }
137
+ .spinner-blade:nth-child(12) { transform: translateX(-50%) rotate(330deg) translateY(-120%); animation-delay: -1s; }
138
+
139
+ @keyframes spinner-fade {
140
+ 0%, 100% { opacity: 0.2; }
141
+ 50% { opacity: 1; }
142
+ }
143
+
144
+ /* Dots */
145
+ .dots {
146
+ display: flex;
147
+ gap: 6px;
148
+ }
149
+
150
+ .dot {
151
+ width: 10px;
152
+ height: 10px;
153
+ border-radius: 50%;
154
+ animation: dots-bounce 1.4s ease-in-out infinite both;
155
+ }
156
+
157
+ .dot:nth-child(1) { animation-delay: -0.32s; }
158
+ .dot:nth-child(2) { animation-delay: -0.16s; }
159
+ .dot:nth-child(3) { animation-delay: 0s; }
160
+
161
+ @keyframes dots-bounce {
162
+ 0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
163
+ 40% { transform: scale(1); opacity: 1; }
164
+ }
165
+
166
+ /* Ring */
167
+ .ring {
168
+ position: relative;
169
+ border: 3px solid var(--ld-loading-ring-border, rgba(99, 102, 241, 0.2));
170
+ border-radius: 50%;
171
+ }
172
+
173
+ .ring-inner {
174
+ position: absolute;
175
+ top: -3px;
176
+ left: -3px;
177
+ right: -3px;
178
+ bottom: -3px;
179
+ border: 3px solid transparent;
180
+ border-top-color: var(--ring-color, #6366f1);
181
+ border-radius: 50%;
182
+ animation: ring-spin 1s linear infinite;
183
+ }
184
+
185
+ @keyframes ring-spin {
186
+ to { transform: rotate(360deg); }
187
+ }
188
+
189
+ /* Pulse */
190
+ .pulse {
191
+ border-radius: 50%;
192
+ animation: pulse-scale 1.5s ease-in-out infinite;
193
+ }
194
+
195
+ @keyframes pulse-scale {
196
+ 0%, 100% { transform: scale(0.8); opacity: 0.5; }
197
+ 50% { transform: scale(1); opacity: 1; }
198
+ }
199
+
200
+ /* Bars */
201
+ .bars {
202
+ display: flex;
203
+ align-items: flex-end;
204
+ gap: 4px;
205
+ height: 30px;
206
+ }
207
+
208
+ .bar {
209
+ width: 6px;
210
+ animation: bars-grow 1.2s ease-in-out infinite;
211
+ }
212
+
213
+ .bar:nth-child(1) { animation-delay: 0s; }
214
+ .bar:nth-child(2) { animation-delay: 0.1s; }
215
+ .bar:nth-child(3) { animation-delay: 0.2s; }
216
+ .bar:nth-child(4) { animation-delay: 0.3s; }
217
+ .bar:nth-child(5) { animation-delay: 0.4s; }
218
+
219
+ @keyframes bars-grow {
220
+ 0%, 40%, 100% { height: 40%; }
221
+ 20% { height: 100%; }
222
+ }
223
+ </style>
224
+
@@ -0,0 +1,361 @@
1
+ <template>
2
+ <div class="ld-table-wrapper" :class="{ 'ld-table--bordered': bordered, 'ld-table--striped': striped }">
3
+ <!-- Loading 遮罩层 -->
4
+ <div v-if="isLoading" class="ld-table-loading">
5
+ <div class="loading-spinner">
6
+ <svg class="circular" viewBox="0 0 50 50">
7
+ <circle class="path" cx="25" cy="25" r="20" fill="none" />
8
+ </svg>
9
+ </div>
10
+ <span v-if="loadingText" class="loading-text">{{ loadingText }}</span>
11
+ </div>
12
+
13
+ <table class="ld-table">
14
+ <thead>
15
+ <tr>
16
+ <th v-for="col in columns" :key="col.key" :style="{ width: col.width, textAlign: col.align || 'left' }"
17
+ @click="col.sortable && handleSort(col.key)">
18
+ <div class="th-content">
19
+ <span>{{ col.title }}</span>
20
+ <span v-if="col.sortable" class="sort-icon" :class="getSortClass(col.key)">
21
+ <svg viewBox="0 0 24 24" width="14" height="14">
22
+ <path fill="currentColor" d="M7 10l5-5 5 5H7zm0 4l5 5 5-5H7z" />
23
+ </svg>
24
+ </span>
25
+ </div>
26
+ </th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ <tr v-if="!sortedData.length && !isLoading">
31
+ <td :colspan="columns.length" class="empty-cell">
32
+ <slot name="empty">
33
+ <div class="empty-content">
34
+ <svg viewBox="0 0 64 41" width="64" height="41">
35
+ <g transform="translate(0 1)" fill="none" fill-rule="evenodd">
36
+ <ellipse fill="var(--ld-color-bg-secondary, #f5f5f5)" cx="32" cy="33" rx="32" ry="7" />
37
+ <g fill-rule="nonzero" stroke="var(--ld-color-border, #d9d9d9)">
38
+ <path
39
+ d="M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z" />
40
+ <path
41
+ d="M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z"
42
+ fill="var(--ld-color-bg, #fafafa)" />
43
+ </g>
44
+ </g>
45
+ </svg>
46
+ <span>暂无数据</span>
47
+ </div>
48
+ </slot>
49
+ </td>
50
+ </tr>
51
+ <tr v-for="(row, rowIndex) in sortedData" :key="rowIndex" @click="$emit('row-click', row, rowIndex)">
52
+ <td v-for="col in columns" :key="col.key" :style="{ textAlign: col.align || 'left' }">
53
+ <slot :name="col.key" :row="row" :index="rowIndex" :value="row[col.key]">
54
+ {{ row[col.key] }}
55
+ </slot>
56
+ </td>
57
+ </tr>
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ </template>
62
+
63
+ <script setup>
64
+ import { ref, computed, watch, onMounted } from 'vue'
65
+
66
+ defineOptions({
67
+ name: 'ldTable'
68
+ })
69
+
70
+ const props = defineProps({
71
+ columns: {
72
+ type: Array,
73
+ required: true
74
+ },
75
+ data: {
76
+ type: Array,
77
+ default: () => []
78
+ },
79
+ bordered: {
80
+ type: Boolean,
81
+ default: false
82
+ },
83
+ striped: {
84
+ type: Boolean,
85
+ default: false
86
+ },
87
+ loading: {
88
+ type: Boolean,
89
+ default: false
90
+ },
91
+ loadingText: {
92
+ type: String,
93
+ default: ''
94
+ },
95
+ // API 相关
96
+ api: {
97
+ type: Function,
98
+ default: null
99
+ },
100
+ autoLoad: {
101
+ type: Boolean,
102
+ default: true
103
+ },
104
+ loadParams: {
105
+ type: Object,
106
+ default: () => ({})
107
+ }
108
+ })
109
+
110
+ const emit = defineEmits(['row-click', 'load-success', 'load-error'])
111
+
112
+ // 内部状态
113
+ const internalLoading = ref(false)
114
+ const apiData = ref([])
115
+
116
+ // 是否使用 API 模式
117
+ const isApiMode = computed(() => typeof props.api === 'function')
118
+
119
+ // 实际的 loading 状态
120
+ const isLoading = computed(() => props.loading || internalLoading.value)
121
+
122
+ // 实际使用的数据
123
+ const tableData = computed(() => isApiMode.value ? apiData.value : props.data)
124
+
125
+ // 加载数据
126
+ const load = async (params = {}) => {
127
+ if (!isApiMode.value) return
128
+
129
+ internalLoading.value = true
130
+ try {
131
+ const mergedParams = { ...props.loadParams, ...params }
132
+ const result = await props.api(mergedParams)
133
+ apiData.value = Array.isArray(result) ? result : (result?.data || result?.list || [])
134
+ emit('load-success', apiData.value)
135
+ return apiData.value
136
+ } catch (error) {
137
+ console.error('[ld-table] Load error:', error)
138
+ emit('load-error', error)
139
+ throw error
140
+ } finally {
141
+ internalLoading.value = false
142
+ }
143
+ }
144
+
145
+ // 重新加载
146
+ const reload = (params = {}) => {
147
+ return load(params)
148
+ }
149
+
150
+ // 清空数据
151
+ const clear = () => {
152
+ apiData.value = []
153
+ }
154
+
155
+ // 监听 loadParams 变化
156
+ watch(() => props.loadParams, (newParams) => {
157
+ if (isApiMode.value && props.autoLoad) {
158
+ load()
159
+ }
160
+ }, { deep: true })
161
+
162
+ // 自动加载
163
+ onMounted(() => {
164
+ if (isApiMode.value && props.autoLoad) {
165
+ load()
166
+ }
167
+ })
168
+
169
+ const sortKey = ref('')
170
+ const sortOrder = ref('') // 'asc' | 'desc' | ''
171
+
172
+ const handleSort = (key) => {
173
+ if (sortKey.value === key) {
174
+ sortOrder.value = sortOrder.value === 'asc' ? 'desc' : sortOrder.value === 'desc' ? '' : 'asc'
175
+ if (!sortOrder.value) sortKey.value = ''
176
+ } else {
177
+ sortKey.value = key
178
+ sortOrder.value = 'asc'
179
+ }
180
+ }
181
+
182
+ const getSortClass = (key) => {
183
+ if (sortKey.value !== key) return ''
184
+ return sortOrder.value === 'asc' ? 'sort-asc' : sortOrder.value === 'desc' ? 'sort-desc' : ''
185
+ }
186
+
187
+ const sortedData = computed(() => {
188
+ const data = tableData.value
189
+ if (!sortKey.value || !sortOrder.value) return data
190
+
191
+ return [...data].sort((a, b) => {
192
+ const aVal = a[sortKey.value]
193
+ const bVal = b[sortKey.value]
194
+
195
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
196
+ return sortOrder.value === 'asc' ? aVal - bVal : bVal - aVal
197
+ }
198
+
199
+ const aStr = String(aVal || '')
200
+ const bStr = String(bVal || '')
201
+ return sortOrder.value === 'asc' ? aStr.localeCompare(bStr) : bStr.localeCompare(aStr)
202
+ })
203
+ })
204
+
205
+ // 暴露方法
206
+ defineExpose({
207
+ load,
208
+ reload,
209
+ clear,
210
+ getData: () => apiData.value
211
+ })
212
+ </script>
213
+
214
+ <style scoped>
215
+ .ld-table-wrapper {
216
+ position: relative;
217
+ width: 100%;
218
+ overflow-x: auto;
219
+ border-radius: 8px;
220
+ background: var(--ld-color-bg, #fff);
221
+ }
222
+
223
+ /* Loading 遮罩层 - Element Plus 风格 */
224
+ .ld-table-loading {
225
+ position: absolute;
226
+ top: 0;
227
+ left: 0;
228
+ right: 0;
229
+ bottom: 0;
230
+ z-index: 10;
231
+ display: flex;
232
+ flex-direction: column;
233
+ align-items: center;
234
+ justify-content: center;
235
+ gap: 8px;
236
+ background: var(--ld-table-loading-bg, rgba(255, 255, 255, 0.9));
237
+ transition: opacity 0.3s;
238
+ }
239
+
240
+ .loading-spinner {
241
+ width: 42px;
242
+ height: 42px;
243
+ }
244
+
245
+ .loading-spinner .circular {
246
+ width: 100%;
247
+ height: 100%;
248
+ animation: loading-rotate 2s linear infinite;
249
+ }
250
+
251
+ .loading-spinner .path {
252
+ stroke: var(--ld-color-primary, #409eff);
253
+ stroke-width: 4;
254
+ stroke-linecap: round;
255
+ animation: loading-dash 1.5s ease-in-out infinite;
256
+ }
257
+
258
+ @keyframes loading-rotate {
259
+ 100% {
260
+ transform: rotate(360deg);
261
+ }
262
+ }
263
+
264
+ @keyframes loading-dash {
265
+ 0% {
266
+ stroke-dasharray: 1, 200;
267
+ stroke-dashoffset: 0;
268
+ }
269
+
270
+ 50% {
271
+ stroke-dasharray: 90, 150;
272
+ stroke-dashoffset: -40px;
273
+ }
274
+
275
+ 100% {
276
+ stroke-dasharray: 90, 150;
277
+ stroke-dashoffset: -120px;
278
+ }
279
+ }
280
+
281
+ .loading-text {
282
+ font-size: 14px;
283
+ color: var(--ld-color-primary, #409eff);
284
+ }
285
+
286
+ .ld-table {
287
+ width: 100%;
288
+ border-collapse: collapse;
289
+ font-size: 14px;
290
+ }
291
+
292
+ .ld-table th,
293
+ .ld-table td {
294
+ padding: 12px 16px;
295
+ text-align: left;
296
+ }
297
+
298
+ .ld-table th {
299
+ background: var(--ld-color-bg-secondary, #f8f9fa);
300
+ color: var(--ld-color-text, #333);
301
+ font-weight: 600;
302
+ white-space: nowrap;
303
+ }
304
+
305
+ .ld-table td {
306
+ color: var(--ld-color-text-secondary, #666);
307
+ border-bottom: 1px solid var(--ld-color-border, #eee);
308
+ }
309
+
310
+ .ld-table tbody tr:hover {
311
+ background: var(--ld-color-bg-hover, #f5f5f5);
312
+ }
313
+
314
+ .ld-table--bordered .ld-table th,
315
+ .ld-table--bordered .ld-table td {
316
+ border: 1px solid var(--ld-color-border, #eee);
317
+ }
318
+
319
+ .ld-table--striped tbody tr:nth-child(even) {
320
+ background: var(--ld-color-bg-secondary, #f8f9fa);
321
+ }
322
+
323
+ .th-content {
324
+ display: flex;
325
+ align-items: center;
326
+ gap: 4px;
327
+ }
328
+
329
+ .sort-icon {
330
+ opacity: 0.3;
331
+ cursor: pointer;
332
+ transition: opacity 0.3s;
333
+ }
334
+
335
+ .sort-icon:hover {
336
+ opacity: 0.6;
337
+ }
338
+
339
+ .sort-icon.sort-asc,
340
+ .sort-icon.sort-desc {
341
+ opacity: 1;
342
+ color: var(--ld-color-primary, #6366f1);
343
+ }
344
+
345
+ .sort-icon.sort-desc svg {
346
+ transform: rotate(180deg);
347
+ }
348
+
349
+ .empty-cell {
350
+ text-align: center;
351
+ padding: 40px 16px;
352
+ }
353
+
354
+ .empty-content {
355
+ display: flex;
356
+ flex-direction: column;
357
+ align-items: center;
358
+ gap: 8px;
359
+ color: var(--ld-color-text-muted, #999);
360
+ }
361
+ </style>