@xlui/xux-ui 0.2.0 → 0.3.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,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,234 @@
1
+ <template>
2
+ <div class="switch" :class="[`size-${size}`, { 'disabled': disabled }]" :style="colorStyle">
3
+ <input
4
+ :id="switchId"
5
+ :checked="modelValue"
6
+ @change="handleChange"
7
+ :disabled="disabled"
8
+ type="checkbox"
9
+ >
10
+ <label :for="switchId" class="slider"></label>
11
+ </div>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { computed } from 'vue'
16
+
17
+ /**
18
+ * Switch 开关组件
19
+ * @displayName XSwitch
20
+ */
21
+
22
+ interface Props {
23
+ modelValue: boolean
24
+ disabled?: boolean
25
+ size?: 'small' | 'medium' | 'large'
26
+ color?: string
27
+ shadowColor?: string
28
+ thumbColor?: string
29
+ }
30
+
31
+ const props = withDefaults(defineProps<Props>(), {
32
+ disabled: false,
33
+ size: 'medium',
34
+ color: '#0974f1',
35
+ shadowColor: '#0974f1',
36
+ thumbColor: '#ffffff'
37
+ })
38
+
39
+ const emit = defineEmits<{
40
+ (e: 'update:modelValue', value: boolean): void
41
+ }>()
42
+
43
+ // 生成唯一的 ID 避免冲突
44
+ const switchId = computed(() => `switch-${Math.random().toString(36).substr(2, 9)}`)
45
+
46
+ // 计算颜色相关的样式
47
+ const colorStyle = computed(() => {
48
+ const color = props.color || '#0974f1'
49
+ const shadowColor = props.shadowColor || color
50
+ const thumbColor = props.thumbColor || '#ffffff'
51
+
52
+ // 计算浅色背景
53
+ const lightBackgroundColor = lightenColor(color, 0.9)
54
+ // 计算激活状态的浅色版本
55
+ const activeColor = lightenColor(color, 0.2)
56
+
57
+ return {
58
+ '--switch-color': color,
59
+ '--switch-color-rgb': hexToRgb(color),
60
+ '--switch-shadow-color': shadowColor,
61
+ '--switch-shadow-color-rgb': hexToRgb(shadowColor),
62
+ '--switch-thumb-color': thumbColor,
63
+ '--switch-bg-color': lightBackgroundColor,
64
+ '--switch-active-color': activeColor
65
+ }
66
+ })
67
+
68
+ // 将十六进制颜色转换为 RGB
69
+ const hexToRgb = (hex: string) => {
70
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
71
+ if (result) {
72
+ const r = parseInt(result[1], 16)
73
+ const g = parseInt(result[2], 16)
74
+ const b = parseInt(result[3], 16)
75
+ return `${r}, ${g}, ${b}`
76
+ }
77
+ return '9, 117, 241' // 默认蓝色
78
+ }
79
+
80
+ // 将颜色变浅
81
+ const lightenColor = (hex: string, factor: number) => {
82
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
83
+ if (result) {
84
+ const r = parseInt(result[1], 16)
85
+ const g = parseInt(result[2], 16)
86
+ const b = parseInt(result[3], 16)
87
+
88
+ // 计算浅色版本
89
+ const lightR = Math.round(r + (255 - r) * factor)
90
+ const lightG = Math.round(g + (255 - g) * factor)
91
+ const lightB = Math.round(b + (255 - b) * factor)
92
+
93
+ return `#${lightR.toString(16).padStart(2, '0')}${lightG.toString(16).padStart(2, '0')}${lightB.toString(16).padStart(2, '0')}`
94
+ }
95
+ return '#e5e7eb' // 默认浅灰色
96
+ }
97
+
98
+ const handleChange = () => {
99
+ if (!props.disabled) {
100
+ emit('update:modelValue', !props.modelValue)
101
+ }
102
+ }
103
+ </script>
104
+
105
+ <style scoped>
106
+ /* The switch - the box around the slider */
107
+ .switch {
108
+ font-size: 17px;
109
+ position: relative;
110
+ display: inline-block;
111
+ width: 3.5em;
112
+ height: 2em;
113
+ }
114
+
115
+ /* Hide default HTML checkbox */
116
+ .switch input {
117
+ opacity: 0;
118
+ width: 0;
119
+ height: 0;
120
+ }
121
+
122
+ /* The slider */
123
+ .slider {
124
+ position: absolute;
125
+ cursor: pointer;
126
+ inset: 0;
127
+ border: none;
128
+ border-radius: 50px;
129
+ transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
130
+ background-color: var(--switch-bg-color);
131
+ }
132
+
133
+ .slider:before {
134
+ position: absolute;
135
+ content: "";
136
+ height: 1.4em;
137
+ width: 1.4em;
138
+ left: 0.2em;
139
+ bottom: 0.2em;
140
+ background-color: var(--switch-thumb-color);
141
+ border-radius: inherit;
142
+ transition: all 0.4s cubic-bezier(0.23, 1, 0.320, 1);
143
+ }
144
+
145
+ .switch input:checked + .slider {
146
+ box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
147
+ border: 2px solid var(--switch-active-color);
148
+ background-color: var(--switch-active-color);
149
+ }
150
+
151
+ .switch input:checked + .slider:before {
152
+ transform: translateX(1.5em);
153
+ }
154
+
155
+ /* 尺寸变体 */
156
+ .switch.size-small {
157
+ width: 2.5em;
158
+ height: 1.4em;
159
+ font-size: 14px;
160
+ }
161
+
162
+ .switch.size-small .slider:before {
163
+ height: 1em;
164
+ width: 1em;
165
+ }
166
+
167
+ .switch.size-small input:checked + .slider:before {
168
+ transform: translateX(1.1em);
169
+ }
170
+
171
+ .switch.size-medium {
172
+ width: 3.5em;
173
+ height: 2em;
174
+ font-size: 17px;
175
+ }
176
+
177
+ .switch.size-large {
178
+ width: 4.5em;
179
+ height: 2.6em;
180
+ font-size: 20px;
181
+ }
182
+
183
+ .switch.size-large .slider:before {
184
+ height: 1.8em;
185
+ width: 1.8em;
186
+ }
187
+
188
+ .switch.size-large input:checked + .slider:before {
189
+ transform: translateX(1.9em);
190
+ }
191
+
192
+ /* 禁用状态 */
193
+ .switch.disabled .slider {
194
+ cursor: not-allowed;
195
+ opacity: 0.5;
196
+ border-color: #9ca3af;
197
+ }
198
+
199
+ .switch.disabled .slider:before {
200
+ background-color: #9ca3af;
201
+ }
202
+
203
+ .switch.disabled input:checked + .slider {
204
+ border-color: #9ca3af;
205
+ box-shadow: none;
206
+ }
207
+
208
+ /* 悬停效果 */
209
+ .switch:not(.disabled):hover .slider {
210
+ box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
211
+ }
212
+
213
+ .switch:not(.disabled) input:checked + .slider:hover {
214
+ box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
215
+ }
216
+
217
+ /* 点击后移除边框 */
218
+ .switch input:active + .slider {
219
+ box-shadow: none;
220
+ }
221
+
222
+ .switch input:checked:active + .slider {
223
+ box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8);
224
+ }
225
+
226
+ /* 焦点状态 */
227
+ .switch input:focus-visible + .slider {
228
+ box-shadow: 0 0 0 3px rgba(var(--switch-shadow-color-rgb), 0.3);
229
+ }
230
+
231
+ .switch input:checked:focus-visible + .slider {
232
+ box-shadow: 0 0 20px rgba(var(--switch-shadow-color-rgb), 0.8), 0 0 0 3px rgba(var(--switch-shadow-color-rgb), 0.3);
233
+ }
234
+ </style>