little-dizzy 2.3.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,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>
@@ -1,32 +1,41 @@
1
1
  <template>
2
2
  <div class="Lztext-wrapper">
3
3
  <div class="loader-wrapper">
4
- <span class="loader-letter">G</span>
5
- <span class="loader-letter">e</span>
6
- <span class="loader-letter">n</span>
7
- <span class="loader-letter">e</span>
8
- <span class="loader-letter">r</span>
9
- <span class="loader-letter">a</span>
10
- <span class="loader-letter">t</span>
11
- <span class="loader-letter">i</span>
12
- <span class="loader-letter">n</span>
13
- <span class="loader-letter">g</span>
4
+ <span v-for="(char, index) in characters" :key="index" class="loader-letter"
5
+ :style="{ animationDelay: `${0.1 + index * 0.105}s` }">{{ char === ' ' ? '\u00A0' : char }}</span>
14
6
  <div class="loader"></div>
15
7
  </div>
16
8
  </div>
17
9
  </template>
18
10
 
19
11
  <script setup>
12
+ import { computed } from 'vue'
13
+
20
14
  /**
21
15
  * 渐变文字动画
22
16
  *
23
- * LittleDizzy Demo 自动生成
17
+ * 通过 text 属性传入文字内容
24
18
  * @component Lztext
19
+ * @example <LdLztext text="Hello World" />
25
20
  */
26
21
  defineOptions({
27
22
  name: 'Lztext'
28
23
  })
29
24
 
25
+ const props = defineProps({
26
+ /**
27
+ * 显示的文字内容
28
+ */
29
+ text: {
30
+ type: String,
31
+ default: 'Generating'
32
+ }
33
+ })
34
+
35
+ // 将文字拆分为字符数组
36
+ const characters = computed(() => {
37
+ return props.text.split('')
38
+ })
30
39
  </script>
31
40
 
32
41
  <style scoped>
@@ -57,13 +66,11 @@ defineOptions({
57
66
  z-index: 1;
58
67
 
59
68
  background-color: transparent;
60
- mask: repeating-linear-gradient(
61
- 90deg,
62
- transparent 0,
63
- transparent 6px,
64
- black 7px,
65
- black 8px
66
- );
69
+ mask: repeating-linear-gradient(90deg,
70
+ transparent 0,
71
+ transparent 6px,
72
+ black 7px,
73
+ black 8px);
67
74
  }
68
75
 
69
76
  .loader::after {
@@ -79,12 +86,10 @@ defineOptions({
79
86
  radial-gradient(circle at 55% 55%, #0ff 0%, transparent 45%),
80
87
  radial-gradient(circle at 45% 55%, #0f0 0%, transparent 45%),
81
88
  radial-gradient(circle at 55% 45%, #00f 0%, transparent 45%);
82
- mask: radial-gradient(
83
- circle at 50% 50%,
84
- transparent 0%,
85
- transparent 10%,
86
- black 25%
87
- );
89
+ mask: radial-gradient(circle at 50% 50%,
90
+ transparent 0%,
91
+ transparent 10%,
92
+ black 25%);
88
93
  animation:
89
94
  transform-animation 2s infinite alternate,
90
95
  opacity-animation 4s infinite;
@@ -95,19 +100,23 @@ defineOptions({
95
100
  0% {
96
101
  transform: translate(-55%);
97
102
  }
103
+
98
104
  100% {
99
105
  transform: translate(55%);
100
106
  }
101
107
  }
102
108
 
103
109
  @keyframes opacity-animation {
110
+
104
111
  0%,
105
112
  100% {
106
113
  opacity: 0;
107
114
  }
115
+
108
116
  15% {
109
117
  opacity: 1;
110
118
  }
119
+
111
120
  65% {
112
121
  opacity: 0;
113
122
  }
@@ -120,49 +129,21 @@ defineOptions({
120
129
  z-index: 2;
121
130
  }
122
131
 
123
- .loader-letter:nth-child(1) {
124
- animation-delay: 0.1s;
125
- }
126
- .loader-letter:nth-child(2) {
127
- animation-delay: 0.205s;
128
- }
129
- .loader-letter:nth-child(3) {
130
- animation-delay: 0.31s;
131
- }
132
- .loader-letter:nth-child(4) {
133
- animation-delay: 0.415s;
134
- }
135
- .loader-letter:nth-child(5) {
136
- animation-delay: 0.521s;
137
- }
138
- .loader-letter:nth-child(6) {
139
- animation-delay: 0.626s;
140
- }
141
- .loader-letter:nth-child(7) {
142
- animation-delay: 0.731s;
143
- }
144
- .loader-letter:nth-child(8) {
145
- animation-delay: 0.837s;
146
- }
147
- .loader-letter:nth-child(9) {
148
- animation-delay: 0.942s;
149
- }
150
- .loader-letter:nth-child(10) {
151
- animation-delay: 1.047s;
152
- }
153
-
154
132
  @keyframes loader-letter-anim {
155
133
  0% {
156
134
  opacity: 0;
157
135
  }
136
+
158
137
  5% {
159
138
  opacity: 1;
160
139
  text-shadow: 0 0 4px #fff;
161
140
  transform: scale(1.1) translateY(-2px);
162
141
  }
142
+
163
143
  20% {
164
144
  opacity: 0.2;
165
145
  }
146
+
166
147
  100% {
167
148
  opacity: 0;
168
149
  }