@xlui/xux-ui 0.1.0 → 0.2.1

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,264 @@
1
+ <template>
2
+ <div class="x-score" :class="[`score-${size}`, { 'score-disabled': disabled }]">
3
+ <div class="rating">
4
+ <label
5
+ v-for="star in maxStars"
6
+ :key="star"
7
+ :class="{ 'star-filled': star <= modelValue }"
8
+ :title="getStarTitle(star)"
9
+ @click="handleChange(star)"
10
+ >
11
+ <input
12
+ type="radio"
13
+ :name="`star-radio-${uniqueId}`"
14
+ :value="star"
15
+ :disabled="disabled"
16
+ :checked="modelValue === star"
17
+ style="display: none;"
18
+ />
19
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
20
+ <path
21
+ pathLength="360"
22
+ d="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"
23
+ ></path>
24
+ </svg>
25
+ </label>
26
+ </div>
27
+
28
+ <!-- 显示评分文字 -->
29
+ <div v-if="showText" class="score-text">
30
+ {{ getScoreText() }}
31
+ </div>
32
+
33
+ <!-- 显示数值 -->
34
+ <div v-if="showValue" class="score-value">
35
+ {{ modelValue }}/{{ maxStars }}
36
+ </div>
37
+ </div>
38
+ </template>
39
+
40
+ <script setup lang="ts">
41
+ import { computed } from 'vue'
42
+
43
+ export interface ScoreProps {
44
+ /** 当前评分值 */
45
+ modelValue?: number
46
+ /** 最大星数 */
47
+ maxStars?: number
48
+ /** 组件尺寸 */
49
+ size?: 'small' | 'medium' | 'large'
50
+ /** 是否禁用 */
51
+ disabled?: boolean
52
+ /** 是否显示评分文字 */
53
+ showText?: boolean
54
+ /** 是否显示数值 */
55
+ showValue?: boolean
56
+ /** 自定义评分文字 */
57
+ texts?: string[]
58
+ /** 是否允许半星评分 */
59
+ allowHalf?: boolean
60
+ /** 自定义颜色 */
61
+ color?: string
62
+ }
63
+
64
+ const props = withDefaults(defineProps<ScoreProps>(), {
65
+ modelValue: 0,
66
+ maxStars: 5,
67
+ size: 'medium',
68
+ disabled: false,
69
+ showText: false,
70
+ showValue: false,
71
+ texts: () => ['极差', '差', '一般', '好', '极好'],
72
+ allowHalf: false,
73
+ color: '#ffc73a'
74
+ })
75
+
76
+ const emit = defineEmits<{
77
+ 'update:modelValue': [value: number]
78
+ change: [value: number]
79
+ }>()
80
+
81
+ // 生成唯一ID避免多个组件冲突
82
+ const uniqueId = computed(() => Math.random().toString(36).substr(2, 9))
83
+
84
+ // 处理评分变化
85
+ const handleChange = (value: number) => {
86
+ if (props.disabled) return
87
+
88
+ emit('update:modelValue', value)
89
+ emit('change', value)
90
+ }
91
+
92
+ // 获取星星标题
93
+ const getStarTitle = (star: number) => {
94
+ if (props.texts && props.texts[star - 1]) {
95
+ return `${star}星 - ${props.texts[star - 1]}`
96
+ }
97
+ return `${star}星`
98
+ }
99
+
100
+ // 获取评分文字
101
+ const getScoreText = () => {
102
+ if (props.modelValue === 0) return '未评分'
103
+ if (props.texts && props.texts[props.modelValue - 1]) {
104
+ return props.texts[props.modelValue - 1]
105
+ }
106
+ return `${props.modelValue}星`
107
+ }
108
+ </script>
109
+
110
+
111
+ <style scoped>
112
+ .x-score {
113
+ display: inline-flex;
114
+ flex-direction: column;
115
+ align-items: flex-start;
116
+ gap: 0.5rem;
117
+ }
118
+
119
+ .rating {
120
+ display: flex;
121
+ gap: 0.3rem;
122
+ --stroke: #666;
123
+ --fill: v-bind(color);
124
+ }
125
+
126
+ .rating label {
127
+ cursor: pointer;
128
+ transition: transform 0.2s ease;
129
+ position: relative;
130
+ }
131
+
132
+ .rating label:hover {
133
+ transform: scale(1.1);
134
+ }
135
+
136
+ .rating svg {
137
+ width: 2rem;
138
+ height: 2rem;
139
+ overflow: visible;
140
+ fill: transparent;
141
+ stroke: var(--stroke);
142
+ stroke-linejoin: bevel;
143
+ stroke-dasharray: 12;
144
+ animation: idle 4s linear infinite;
145
+ transition: stroke 0.2s, fill 0.5s;
146
+ }
147
+
148
+ @keyframes idle {
149
+ from {
150
+ stroke-dashoffset: 24;
151
+ }
152
+ }
153
+
154
+ .rating label:hover svg {
155
+ stroke: var(--fill);
156
+ }
157
+
158
+ /* 填充的星星样式 */
159
+ .rating label.star-filled svg {
160
+ transition: 0s;
161
+ animation: idle 4s linear infinite, yippee 0.75s backwards;
162
+ fill: var(--fill);
163
+ stroke: var(--fill);
164
+ stroke-opacity: 0;
165
+ stroke-dasharray: 0;
166
+ stroke-linejoin: miter;
167
+ stroke-width: 8px;
168
+ }
169
+
170
+ @keyframes yippee {
171
+ 0% {
172
+ transform: scale(1);
173
+ fill: var(--fill);
174
+ fill-opacity: 0;
175
+ stroke-opacity: 1;
176
+ stroke: var(--stroke);
177
+ stroke-dasharray: 10;
178
+ stroke-width: 1px;
179
+ stroke-linejoin: bevel;
180
+ }
181
+
182
+ 30% {
183
+ transform: scale(0);
184
+ fill: var(--fill);
185
+ fill-opacity: 0;
186
+ stroke-opacity: 1;
187
+ stroke: var(--stroke);
188
+ stroke-dasharray: 10;
189
+ stroke-width: 1px;
190
+ stroke-linejoin: bevel;
191
+ }
192
+
193
+ 30.1% {
194
+ stroke: var(--fill);
195
+ stroke-dasharray: 0;
196
+ stroke-linejoin: miter;
197
+ stroke-width: 8px;
198
+ }
199
+
200
+ 60% {
201
+ transform: scale(1.2);
202
+ fill: var(--fill);
203
+ }
204
+ }
205
+
206
+ /* 尺寸变体 */
207
+ .score-small .rating svg {
208
+ width: 1.2rem;
209
+ height: 1.2rem;
210
+ }
211
+
212
+ .score-medium .rating svg {
213
+ width: 2rem;
214
+ height: 2rem;
215
+ }
216
+
217
+ .score-large .rating svg {
218
+ width: 2.8rem;
219
+ height: 2.8rem;
220
+ }
221
+
222
+ /* 禁用状态 */
223
+ .score-disabled .rating label {
224
+ cursor: not-allowed;
225
+ opacity: 0.6;
226
+ }
227
+
228
+ .score-disabled .rating label:hover {
229
+ transform: none;
230
+ }
231
+
232
+ .score-disabled .rating label:hover svg {
233
+ stroke: var(--stroke);
234
+ }
235
+
236
+ /* 文字样式 */
237
+ .score-text {
238
+ font-size: 0.875rem;
239
+ color: #666;
240
+ font-weight: 500;
241
+ }
242
+
243
+ .score-value {
244
+ font-size: 0.75rem;
245
+ color: #999;
246
+ font-weight: 400;
247
+ }
248
+
249
+ .score-small .score-text {
250
+ font-size: 0.75rem;
251
+ }
252
+
253
+ .score-small .score-value {
254
+ font-size: 0.625rem;
255
+ }
256
+
257
+ .score-large .score-text {
258
+ font-size: 1rem;
259
+ }
260
+
261
+ .score-large .score-value {
262
+ font-size: 0.875rem;
263
+ }
264
+ </style>
@@ -0,0 +1,374 @@
1
+ <template>
2
+ <div
3
+ :class="[
4
+ variant === 'basic' ? 'x-tooltip-wrapper' : 'x-tooltip-rich-wrapper',
5
+ variant === 'rich' && `rich-${placement}`
6
+ ]"
7
+ >
8
+ <!-- 基本版本 -->
9
+ <template v-if="variant === 'basic'">
10
+ <div class="x-tooltip-trigger">
11
+ <slot></slot>
12
+ </div>
13
+ <div :class="['x-tooltip', `tooltip-${placement}`]">
14
+ {{ content }}
15
+ <div :class="['tooltip-arrow', `arrow-${placement}`]"></div>
16
+ </div>
17
+ </template>
18
+
19
+ <!-- 丰富版本 (基于 CSS Buttons) -->
20
+ <template v-else>
21
+ <span class="tooltip-label">{{ label }}</span>
22
+ <span class="tooltip-text">{{ content }}</span>
23
+ <span class="tooltip-hover-text">{{ hoverText || label }}</span>
24
+ </template>
25
+ </div>
26
+ </template>
27
+
28
+ <script setup lang="ts">
29
+ export interface TooltipsProps {
30
+ /** Tooltip 显示的内容 */
31
+ content: string
32
+ /** Tooltip 显示的位置: 'top' | 'bottom' | 'left' | 'right' */
33
+ placement?: 'top' | 'bottom' | 'left' | 'right'
34
+ /** 变体类型: 'basic' | 'rich' */
35
+ variant?: 'basic' | 'rich'
36
+ /** 丰富版本的标签文字 */
37
+ label?: string
38
+ /** 丰富版本的悬停文字 */
39
+ hoverText?: string
40
+ }
41
+
42
+ withDefaults(defineProps<TooltipsProps>(), {
43
+ placement: 'top',
44
+ variant: 'basic',
45
+ label: 'Hover',
46
+ hoverText: ''
47
+ })
48
+ </script>
49
+
50
+ <style scoped>
51
+ /* ===== 基本版本样式 ===== */
52
+ .x-tooltip-wrapper {
53
+ position: relative;
54
+ display: inline-block;
55
+ }
56
+
57
+ .x-tooltip-trigger {
58
+ cursor: pointer;
59
+ }
60
+
61
+ .x-tooltip {
62
+ position: absolute;
63
+ background-color: #1a1a1a;
64
+ color: #ffffff;
65
+ padding: 8px 12px;
66
+ border-radius: 6px;
67
+ font-size: 14px;
68
+ white-space: nowrap;
69
+ opacity: 0;
70
+ visibility: hidden;
71
+ transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
72
+ z-index: 1000;
73
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
74
+ pointer-events: none;
75
+ }
76
+
77
+ .tooltip-arrow {
78
+ position: absolute;
79
+ width: 8px;
80
+ height: 8px;
81
+ background-color: #1a1a1a;
82
+ transform: rotate(45deg);
83
+ }
84
+
85
+ /* 上方 */
86
+ .tooltip-top {
87
+ bottom: 100%;
88
+ left: 50%;
89
+ transform: translate(-50%, -8px);
90
+ margin-bottom: 8px;
91
+ }
92
+
93
+ .arrow-top {
94
+ bottom: -4px;
95
+ left: 50%;
96
+ margin-left: -4px;
97
+ }
98
+
99
+ .x-tooltip-wrapper:hover .tooltip-top {
100
+ opacity: 1;
101
+ visibility: visible;
102
+ transform: translate(-50%, 0);
103
+ }
104
+
105
+ /* 下方 */
106
+ .tooltip-bottom {
107
+ top: 100%;
108
+ left: 50%;
109
+ transform: translate(-50%, 8px);
110
+ margin-top: 8px;
111
+ }
112
+
113
+ .arrow-bottom {
114
+ top: -4px;
115
+ left: 50%;
116
+ margin-left: -4px;
117
+ }
118
+
119
+ .x-tooltip-wrapper:hover .tooltip-bottom {
120
+ opacity: 1;
121
+ visibility: visible;
122
+ transform: translate(-50%, 0);
123
+ }
124
+
125
+ /* 左侧 */
126
+ .tooltip-left {
127
+ right: 100%;
128
+ top: 50%;
129
+ transform: translate(-8px, -50%);
130
+ margin-right: 8px;
131
+ }
132
+
133
+ .arrow-left {
134
+ right: -4px;
135
+ top: 50%;
136
+ margin-top: -4px;
137
+ }
138
+
139
+ .x-tooltip-wrapper:hover .tooltip-left {
140
+ opacity: 1;
141
+ visibility: visible;
142
+ transform: translate(0, -50%);
143
+ }
144
+
145
+ /* 右侧 */
146
+ .tooltip-right {
147
+ left: 100%;
148
+ top: 50%;
149
+ transform: translate(8px, -50%);
150
+ margin-left: 8px;
151
+ }
152
+
153
+ .arrow-right {
154
+ left: -4px;
155
+ top: 50%;
156
+ margin-top: -4px;
157
+ }
158
+
159
+ .x-tooltip-wrapper:hover .tooltip-right {
160
+ opacity: 1;
161
+ visibility: visible;
162
+ transform: translate(0, -50%);
163
+ }
164
+
165
+ /* ===== 丰富版本样式 (基于 CSS Buttons) ===== */
166
+ .x-tooltip-rich-wrapper {
167
+ --background: #1a1a1a;
168
+ --color: #ffffff;
169
+ --border-color: #333333;
170
+ position: relative;
171
+ cursor: pointer;
172
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
173
+ font-size: 18px;
174
+ font-weight: 600;
175
+ color: #333333;
176
+ background-color: #ffffff;
177
+ padding: 0.7em 1.8em;
178
+ border-radius: 8px;
179
+ text-transform: uppercase;
180
+ height: 60px;
181
+ width: 180px;
182
+ display: grid;
183
+ place-items: center;
184
+ border: 2px solid var(--border-color);
185
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
186
+ }
187
+
188
+ .tooltip-text {
189
+ position: absolute;
190
+ top: 0;
191
+ left: 0;
192
+ width: 100%;
193
+ height: 100%;
194
+ display: grid;
195
+ place-items: center;
196
+ transform-origin: -100%;
197
+ transform: scale(1);
198
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
199
+ z-index: 2;
200
+ color: #333333;
201
+ }
202
+
203
+ .x-tooltip-rich-wrapper .tooltip-hover-text {
204
+ position: absolute;
205
+ top: 0%;
206
+ left: 100%;
207
+ width: 100%;
208
+ height: 100%;
209
+ border-radius: 8px;
210
+ opacity: 1;
211
+ background-color: var(--background);
212
+ color: var(--color);
213
+ z-index: 1;
214
+ border: 2px solid var(--background);
215
+ transform: scale(0);
216
+ transform-origin: 0;
217
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
218
+ display: grid;
219
+ place-items: center;
220
+ }
221
+
222
+ .tooltip-label {
223
+ position: absolute;
224
+ padding: 0.3em 0.6em;
225
+ opacity: 0;
226
+ pointer-events: none;
227
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
228
+ background: var(--background);
229
+ z-index: 10;
230
+ border-radius: 8px;
231
+ scale: 0;
232
+ text-transform: capitalize;
233
+ font-weight: 400;
234
+ font-size: 16px;
235
+ box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px;
236
+ }
237
+
238
+ .tooltip-label::before {
239
+ position: absolute;
240
+ content: "";
241
+ height: 0.6em;
242
+ width: 0.6em;
243
+ background: var(--background);
244
+ }
245
+
246
+ /* 丰富版本 - 上方 */
247
+ .rich-top .tooltip-label {
248
+ top: -0.5em;
249
+ left: 50%;
250
+ transform: translateX(-50%);
251
+ transform-origin: 0 0;
252
+ }
253
+
254
+ .rich-top .tooltip-label::before {
255
+ bottom: -0.2em;
256
+ left: 50%;
257
+ transform: translate(-50%) rotate(45deg);
258
+ }
259
+
260
+ .rich-top:hover .tooltip-label {
261
+ top: calc(-100% - 1em);
262
+ opacity: 1;
263
+ visibility: visible;
264
+ pointer-events: auto;
265
+ scale: 1;
266
+ animation: shake 0.5s ease-in-out both;
267
+ }
268
+
269
+ /* 丰富版本 - 下方 */
270
+ .rich-bottom .tooltip-label {
271
+ bottom: -0.5em;
272
+ left: 50%;
273
+ transform: translateX(-50%);
274
+ transform-origin: 50% 100%;
275
+ }
276
+
277
+ .rich-bottom .tooltip-label::before {
278
+ top: -0.2em;
279
+ left: 50%;
280
+ transform: translate(-50%) rotate(45deg);
281
+ }
282
+
283
+ .rich-bottom:hover .tooltip-label {
284
+ bottom: calc(-100% - 1em);
285
+ opacity: 1;
286
+ visibility: visible;
287
+ pointer-events: auto;
288
+ scale: 1;
289
+ animation: shake 0.5s ease-in-out both;
290
+ }
291
+
292
+ /* 丰富版本 - 左侧 */
293
+ .rich-left .tooltip-label {
294
+ top: 50%;
295
+ right: calc(100% + 0.5em);
296
+ transform: translateY(-50%);
297
+ transform-origin: 100% 50%;
298
+ }
299
+
300
+ .rich-left .tooltip-label::before {
301
+ right: -0.2em;
302
+ top: 50%;
303
+ transform: translateY(-50%) rotate(45deg);
304
+ }
305
+
306
+ .rich-left:hover .tooltip-label {
307
+ right: calc(100% + 1em);
308
+ opacity: 1;
309
+ visibility: visible;
310
+ pointer-events: auto;
311
+ scale: 1;
312
+ animation: shake 0.5s ease-in-out both;
313
+ }
314
+
315
+ /* 丰富版本 - 右侧 */
316
+ .rich-right .tooltip-label {
317
+ top: 50%;
318
+ left: calc(100% + 0.5em);
319
+ transform: translateY(-50%);
320
+ transform-origin: 0 50%;
321
+ }
322
+
323
+ .rich-right .tooltip-label::before {
324
+ left: -0.2em;
325
+ top: 50%;
326
+ transform: translateY(-50%) rotate(45deg);
327
+ }
328
+
329
+ .rich-right:hover .tooltip-label {
330
+ left: calc(100% + 1em);
331
+ opacity: 1;
332
+ visibility: visible;
333
+ pointer-events: auto;
334
+ scale: 1;
335
+ animation: shake 0.5s ease-in-out both;
336
+ }
337
+
338
+ /* 丰富版本 - 悬停效果 */
339
+ .x-tooltip-rich-wrapper:hover {
340
+ box-shadow: rgba(0, 0, 0, 0.25) 0 8px 15px;
341
+ color: white;
342
+ border-color: transparent;
343
+ }
344
+
345
+ .x-tooltip-rich-wrapper:hover .tooltip-hover-text {
346
+ transform: scale(1);
347
+ left: 0;
348
+ }
349
+
350
+ .x-tooltip-rich-wrapper:hover .tooltip-text {
351
+ opacity: 0;
352
+ top: 0%;
353
+ left: 100%;
354
+ transform: scale(0);
355
+ }
356
+
357
+ @keyframes shake {
358
+ 0% {
359
+ rotate: 0;
360
+ }
361
+ 25% {
362
+ rotate: 7deg;
363
+ }
364
+ 50% {
365
+ rotate: -7deg;
366
+ }
367
+ 75% {
368
+ rotate: 1deg;
369
+ }
370
+ 100% {
371
+ rotate: 0;
372
+ }
373
+ }
374
+ </style>