@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.
- package/LICENSE +194 -0
- package/README.md +2 -0
- package/README.zip +0 -0
- package/dist/index.css +1 -1
- package/dist/index.js +31 -31
- package/dist/index.mjs +3078 -1432
- package/package.json +4 -1
- package/src/components/Card/index.vue +44 -11
- package/src/components/DateTimePicker/index.vue +919 -0
- package/src/components/Modal/index.vue +18 -12
- package/src/components/Radio/index.vue +620 -0
- package/src/components/Score/index.vue +264 -0
- package/src/components/Switch/index.vue +234 -0
- package/src/composables/DateTime.ts +391 -0
- package/src/index.ts +9 -0
@@ -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>
|