@xlui/xux-ui 0.1.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/README.md +55 -0
- package/dist/index.css +1 -0
- package/dist/index.js +128 -0
- package/dist/index.mjs +4819 -0
- package/package.json +57 -0
- package/src/components/Accordion/index.vue +355 -0
- package/src/components/Button/index.vue +440 -0
- package/src/components/Card/index.vue +386 -0
- package/src/components/Checkboxes/index.vue +416 -0
- package/src/components/CountrySelect/data/countries.json +2084 -0
- package/src/components/CountrySelect/index.vue +319 -0
- package/src/components/Input/index.vue +293 -0
- package/src/components/Modal/index.vue +360 -0
- package/src/components/Select/index.vue +411 -0
- package/src/components/Skeleton/index.vue +110 -0
- package/src/components/ThumbnailContainer/index.vue +451 -0
- package/src/composables/Msg.ts +349 -0
- package/src/index.ts +28 -0
- package/src/styles/theme.css +120 -0
@@ -0,0 +1,319 @@
|
|
1
|
+
<template>
|
2
|
+
<div ref="containerRef" class="country-select-container relative">
|
3
|
+
<div
|
4
|
+
@click="toggleDropdown"
|
5
|
+
:class="[
|
6
|
+
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#1a1a1a] focus:border-[#1a1a1a] transition-colors duration-200 cursor-pointer bg-white flex items-center justify-between',
|
7
|
+
customClass,
|
8
|
+
{ 'opacity-60 cursor-not-allowed': disabled }
|
9
|
+
]"
|
10
|
+
:tabindex="disabled ? -1 : 0"
|
11
|
+
@keydown.enter="toggleDropdown"
|
12
|
+
@keydown.space="toggleDropdown"
|
13
|
+
@keydown.escape="closeDropdown"
|
14
|
+
>
|
15
|
+
<div class="flex items-center gap-2">
|
16
|
+
<img
|
17
|
+
v-if="selectedCountry"
|
18
|
+
:src="selectedCountry.flagSvg"
|
19
|
+
:alt="selectedCountry.name"
|
20
|
+
class="w-5 h-4 object-cover rounded-sm"
|
21
|
+
@error="handleFlagError"
|
22
|
+
/>
|
23
|
+
<span :class="{ 'text-gray-500': !selectedCountry && placeholder }">
|
24
|
+
{{ selectedCountry ? selectedCountry.name : (placeholder || '选择国家') }}
|
25
|
+
</span>
|
26
|
+
</div>
|
27
|
+
<svg
|
28
|
+
class="w-5 h-5 text-gray-400 transition-transform duration-200"
|
29
|
+
:class="{ 'rotate-180': isOpen }"
|
30
|
+
fill="none"
|
31
|
+
stroke="currentColor"
|
32
|
+
viewBox="0 0 24 24"
|
33
|
+
>
|
34
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
35
|
+
</svg>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<!-- Dropdown -->
|
39
|
+
<div
|
40
|
+
v-if="isOpen && !disabled"
|
41
|
+
class="absolute z-[9999] w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto"
|
42
|
+
style="position: absolute;"
|
43
|
+
>
|
44
|
+
<div
|
45
|
+
v-for="country in sortedCountries"
|
46
|
+
:key="country.code"
|
47
|
+
@click="selectCountry(country)"
|
48
|
+
class="px-3 py-2 hover:bg-gray-50 cursor-pointer flex items-center gap-2 transition-colors"
|
49
|
+
:class="{ 'bg-gray-100': selectedCountry?.code === country.code }"
|
50
|
+
>
|
51
|
+
<img
|
52
|
+
:src="country.flagSvg"
|
53
|
+
:alt="country.name"
|
54
|
+
class="w-5 h-4 object-cover rounded-sm flex-shrink-0"
|
55
|
+
@error="(e) => handleFlagError(e, country)"
|
56
|
+
/>
|
57
|
+
<span>{{ country.name }}</span>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
|
61
|
+
<!-- Backdrop -->
|
62
|
+
<div
|
63
|
+
v-if="isOpen && !disabled"
|
64
|
+
class="fixed inset-0 z-[9998]"
|
65
|
+
@click="closeDropdown"
|
66
|
+
></div>
|
67
|
+
</div>
|
68
|
+
</template>
|
69
|
+
|
70
|
+
<script setup lang="ts">
|
71
|
+
/**
|
72
|
+
* CountrySelect 国家选择器组件
|
73
|
+
* @displayName XCountrySelect
|
74
|
+
*/
|
75
|
+
import countriesData from './data/countries.json'
|
76
|
+
import { ref, computed, onMounted, onUnmounted, nextTick, readonly } from 'vue'
|
77
|
+
|
78
|
+
export interface Country {
|
79
|
+
name: string
|
80
|
+
code: string
|
81
|
+
flagEmoji: string
|
82
|
+
flagSvg: string
|
83
|
+
flagPng: string
|
84
|
+
}
|
85
|
+
|
86
|
+
export interface CountrySelectProps {
|
87
|
+
modelValue?: string
|
88
|
+
placeholder?: string
|
89
|
+
disabled?: boolean
|
90
|
+
required?: boolean
|
91
|
+
customClass?: string
|
92
|
+
autoDetectFromIp?: boolean
|
93
|
+
returnType?: 'name' | 'code' // 返回国家名称还是国家代码
|
94
|
+
priorityCountries?: string[] // 优先显示的国家代码列表
|
95
|
+
ipDetectUrl?: string // 自定义IP检测接口URL
|
96
|
+
ipDetectMethod?: 'GET' | 'POST' // 请求方法
|
97
|
+
ipDetectHeaders?: Record<string, string> // 自定义请求头
|
98
|
+
ipDetectResponseParser?: (data: any) => string // 自定义响应解析函数,返回国家代码
|
99
|
+
}
|
100
|
+
|
101
|
+
const props = withDefaults(defineProps<CountrySelectProps>(), {
|
102
|
+
modelValue: '',
|
103
|
+
placeholder: '',
|
104
|
+
disabled: false,
|
105
|
+
required: false,
|
106
|
+
customClass: '',
|
107
|
+
autoDetectFromIp: true,
|
108
|
+
returnType: 'name',
|
109
|
+
priorityCountries: () => ['US', 'CA', 'GB', 'DE', 'FR', 'JP', 'CN', 'AU'],
|
110
|
+
ipDetectUrl: 'https://ipinfo.io/json',
|
111
|
+
ipDetectMethod: 'GET'
|
112
|
+
})
|
113
|
+
|
114
|
+
const emit = defineEmits<{
|
115
|
+
'update:modelValue': [value: string]
|
116
|
+
'change': [value: string, country: Country]
|
117
|
+
'ip-detect-start': [] // IP检测开始
|
118
|
+
'ip-detect-success': [countryCode: string] // IP检测成功
|
119
|
+
'ip-detect-error': [error: Error] // IP检测失败
|
120
|
+
}>()
|
121
|
+
|
122
|
+
// 下拉框状态
|
123
|
+
const isOpen = ref(false)
|
124
|
+
|
125
|
+
// 国家数据处理
|
126
|
+
const allCountries = ref<Country[]>(countriesData as Country[])
|
127
|
+
|
128
|
+
// 当前选中的国家
|
129
|
+
const selectedCountry = computed(() => {
|
130
|
+
if (!props.modelValue) return null
|
131
|
+
return allCountries.value.find(country =>
|
132
|
+
props.returnType === 'code' ? country.code === props.modelValue : country.name === props.modelValue
|
133
|
+
)
|
134
|
+
})
|
135
|
+
|
136
|
+
// 排序后的国家列表(优先国家在前)
|
137
|
+
const sortedCountries = computed(() => {
|
138
|
+
const priority = allCountries.value.filter(country =>
|
139
|
+
props.priorityCountries.includes(country.code)
|
140
|
+
)
|
141
|
+
const others = allCountries.value.filter(country =>
|
142
|
+
!props.priorityCountries.includes(country.code)
|
143
|
+
)
|
144
|
+
|
145
|
+
// 按优先级排序,然后按字母顺序排序其他国家
|
146
|
+
const sortedPriority = priority.sort((a, b) => {
|
147
|
+
const aIndex = props.priorityCountries.indexOf(a.code)
|
148
|
+
const bIndex = props.priorityCountries.indexOf(b.code)
|
149
|
+
return aIndex - bIndex
|
150
|
+
})
|
151
|
+
|
152
|
+
const sortedOthers = others.sort((a, b) => a.name.localeCompare(b.name))
|
153
|
+
|
154
|
+
return [...sortedPriority, ...sortedOthers]
|
155
|
+
})
|
156
|
+
|
157
|
+
// 下拉框操作
|
158
|
+
const toggleDropdown = () => {
|
159
|
+
if (!props.disabled) {
|
160
|
+
isOpen.value = !isOpen.value
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
const closeDropdown = () => {
|
165
|
+
isOpen.value = false
|
166
|
+
}
|
167
|
+
|
168
|
+
const selectCountry = (country: Country | null) => {
|
169
|
+
if (country) {
|
170
|
+
const value = props.returnType === 'code' ? country.code : country.name
|
171
|
+
emit('update:modelValue', value)
|
172
|
+
emit('change', value, country)
|
173
|
+
} else {
|
174
|
+
emit('update:modelValue', '')
|
175
|
+
}
|
176
|
+
closeDropdown()
|
177
|
+
}
|
178
|
+
|
179
|
+
// 图片错误处理:SVG失败时切换到PNG
|
180
|
+
const handleFlagError = (event: Event, country?: Country) => {
|
181
|
+
const img = event.target as HTMLImageElement
|
182
|
+
if (country && img.src === country.flagSvg) {
|
183
|
+
img.src = country.flagPng
|
184
|
+
} else if (selectedCountry.value && img.src === selectedCountry.value.flagSvg) {
|
185
|
+
img.src = selectedCountry.value.flagPng
|
186
|
+
}
|
187
|
+
}
|
188
|
+
|
189
|
+
/**
|
190
|
+
* IP自动检测国家
|
191
|
+
* 支持自定义后端接口,默认使用 ipinfo.io
|
192
|
+
*
|
193
|
+
* @example 使用默认接口
|
194
|
+
* <XCountrySelect auto-detect-from-ip />
|
195
|
+
*
|
196
|
+
* @example 使用自定义后端接口
|
197
|
+
* <XCountrySelect
|
198
|
+
* auto-detect-from-ip
|
199
|
+
* ip-detect-url="https://api.example.com/ip/location"
|
200
|
+
* :ip-detect-response-parser="(data) => data.countryCode"
|
201
|
+
* />
|
202
|
+
*/
|
203
|
+
const detectCountryFromIp = async () => {
|
204
|
+
if (!props.autoDetectFromIp || props.modelValue) return
|
205
|
+
|
206
|
+
emit('ip-detect-start')
|
207
|
+
|
208
|
+
try {
|
209
|
+
// 构建请求配置
|
210
|
+
const fetchOptions: RequestInit = {
|
211
|
+
method: props.ipDetectMethod,
|
212
|
+
headers: {
|
213
|
+
'Content-Type': 'application/json',
|
214
|
+
...props.ipDetectHeaders
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
// 发起请求
|
219
|
+
const response = await fetch(props.ipDetectUrl, fetchOptions)
|
220
|
+
|
221
|
+
if (!response.ok) {
|
222
|
+
throw new Error(`HTTP error! status: ${response.status}`)
|
223
|
+
}
|
224
|
+
|
225
|
+
const data = await response.json()
|
226
|
+
|
227
|
+
// 解析国家代码
|
228
|
+
let detectedCountryCode: string
|
229
|
+
|
230
|
+
if (props.ipDetectResponseParser) {
|
231
|
+
// 使用自定义解析函数
|
232
|
+
detectedCountryCode = props.ipDetectResponseParser(data)
|
233
|
+
} else {
|
234
|
+
// 默认解析逻辑(适配 ipinfo.io 和常见后端格式)
|
235
|
+
detectedCountryCode = data.country || data.countryCode || data.country_code || ''
|
236
|
+
}
|
237
|
+
|
238
|
+
detectedCountryCode = detectedCountryCode.toUpperCase()
|
239
|
+
|
240
|
+
if (!detectedCountryCode) {
|
241
|
+
throw new Error('Country code not found in response')
|
242
|
+
}
|
243
|
+
|
244
|
+
console.log('Auto-detected country from IP:', detectedCountryCode)
|
245
|
+
emit('ip-detect-success', detectedCountryCode)
|
246
|
+
|
247
|
+
// 查找对应的国家
|
248
|
+
const detectedCountry = allCountries.value.find(country =>
|
249
|
+
country.code === detectedCountryCode
|
250
|
+
)
|
251
|
+
|
252
|
+
if (detectedCountry) {
|
253
|
+
const valueToEmit = props.returnType === 'code' ? detectedCountry.code : detectedCountry.name
|
254
|
+
emit('update:modelValue', valueToEmit)
|
255
|
+
emit('change', valueToEmit, detectedCountry)
|
256
|
+
} else {
|
257
|
+
console.warn('Detected country code not found in country list:', detectedCountryCode)
|
258
|
+
}
|
259
|
+
} catch (error) {
|
260
|
+
const err = error instanceof Error ? error : new Error(String(error))
|
261
|
+
console.warn('Failed to detect country from IP:', err)
|
262
|
+
emit('ip-detect-error', err)
|
263
|
+
|
264
|
+
// 如果检测失败且没有初始值,可选择默认国家(默认不自动设置)
|
265
|
+
// 取消注释以下代码可启用默认国家逻辑
|
266
|
+
/*
|
267
|
+
const defaultCountry = allCountries.value.find(country => country.code === 'US')
|
268
|
+
if (defaultCountry && !props.modelValue) {
|
269
|
+
const valueToEmit = props.returnType === 'code' ? defaultCountry.code : defaultCountry.name
|
270
|
+
emit('update:modelValue', valueToEmit)
|
271
|
+
emit('change', valueToEmit, defaultCountry)
|
272
|
+
}
|
273
|
+
*/
|
274
|
+
}
|
275
|
+
}
|
276
|
+
|
277
|
+
// 点击外部关闭下拉框
|
278
|
+
const containerRef = ref<HTMLElement>()
|
279
|
+
|
280
|
+
// 生命周期
|
281
|
+
onMounted(async () => {
|
282
|
+
await nextTick()
|
283
|
+
|
284
|
+
// 如果启用IP检测且没有初始值,尝试自动检测
|
285
|
+
if (props.autoDetectFromIp && !props.modelValue) {
|
286
|
+
await detectCountryFromIp()
|
287
|
+
}
|
288
|
+
|
289
|
+
// 设置点击外部关闭下拉框
|
290
|
+
const handleClickOutside = (event: Event) => {
|
291
|
+
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
|
292
|
+
closeDropdown()
|
293
|
+
}
|
294
|
+
}
|
295
|
+
|
296
|
+
document.addEventListener('click', handleClickOutside)
|
297
|
+
|
298
|
+
onUnmounted(() => {
|
299
|
+
document.removeEventListener('click', handleClickOutside)
|
300
|
+
})
|
301
|
+
})
|
302
|
+
|
303
|
+
// 暴露方法给父组件
|
304
|
+
defineExpose({
|
305
|
+
detectCountryFromIp,
|
306
|
+
allCountries: readonly(allCountries),
|
307
|
+
sortedCountries: readonly(sortedCountries)
|
308
|
+
})
|
309
|
+
</script>
|
310
|
+
|
311
|
+
<style scoped>
|
312
|
+
.country-select-container {
|
313
|
+
position: relative;
|
314
|
+
}
|
315
|
+
|
316
|
+
.country-select-container img {
|
317
|
+
flex-shrink: 0;
|
318
|
+
}
|
319
|
+
</style>
|
@@ -0,0 +1,293 @@
|
|
1
|
+
<template>
|
2
|
+
<div class="x-input-group" :style="{ width: width }">
|
3
|
+
<input
|
4
|
+
ref="inputRef"
|
5
|
+
:type="type"
|
6
|
+
:value="modelValue"
|
7
|
+
:placeholder="hasLabel ? '' : placeholder"
|
8
|
+
:disabled="disabled"
|
9
|
+
:readonly="readonly"
|
10
|
+
:required="required"
|
11
|
+
:maxlength="maxlength"
|
12
|
+
:minlength="minlength"
|
13
|
+
:autocomplete="autocomplete"
|
14
|
+
:class="[
|
15
|
+
'x-input',
|
16
|
+
{
|
17
|
+
'x-input--disabled': disabled,
|
18
|
+
'x-input--error': error,
|
19
|
+
'x-input--clearable': clearable && modelValue,
|
20
|
+
'x-input--with-label': hasLabel
|
21
|
+
}
|
22
|
+
]"
|
23
|
+
@input="handleInput"
|
24
|
+
@blur="handleBlur"
|
25
|
+
@focus="handleFocus"
|
26
|
+
@keydown="handleKeydown"
|
27
|
+
@keyup="handleKeyup"
|
28
|
+
@change="handleChange"
|
29
|
+
/>
|
30
|
+
<label
|
31
|
+
v-if="label"
|
32
|
+
:class="[
|
33
|
+
'x-input-label',
|
34
|
+
{
|
35
|
+
'x-input-label--active': isFocused || hasValue,
|
36
|
+
'x-input-label--error': error
|
37
|
+
}
|
38
|
+
]"
|
39
|
+
>
|
40
|
+
{{ label }}
|
41
|
+
</label>
|
42
|
+
|
43
|
+
<!-- 清除按钮 -->
|
44
|
+
<button
|
45
|
+
v-if="clearable && modelValue && !disabled"
|
46
|
+
type="button"
|
47
|
+
class="x-input-clear"
|
48
|
+
@click="handleClear"
|
49
|
+
@mousedown.prevent
|
50
|
+
>
|
51
|
+
<Icon icon="bi:x-circle"></Icon>
|
52
|
+
</button>
|
53
|
+
|
54
|
+
<!-- 字数统计 -->
|
55
|
+
<div v-if="showWordLimit && maxlength" class="x-input-count">
|
56
|
+
{{ currentLength }} / {{ maxlength }}
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</template>
|
60
|
+
|
61
|
+
|
62
|
+
<script setup lang="ts">
|
63
|
+
import { ref, computed } from 'vue'
|
64
|
+
import { Icon } from '@iconify/vue'
|
65
|
+
/**
|
66
|
+
* Input 输入框组件 适用于表单输入
|
67
|
+
* @displayName XInput
|
68
|
+
*/
|
69
|
+
|
70
|
+
export interface InputProps {
|
71
|
+
modelValue?: string | number
|
72
|
+
type?: string
|
73
|
+
placeholder?: string
|
74
|
+
label?: string
|
75
|
+
disabled?: boolean
|
76
|
+
readonly?: boolean
|
77
|
+
required?: boolean
|
78
|
+
clearable?: boolean
|
79
|
+
showWordLimit?: boolean
|
80
|
+
maxlength?: number
|
81
|
+
minlength?: number
|
82
|
+
autocomplete?: string
|
83
|
+
error?: boolean
|
84
|
+
width?: string
|
85
|
+
}
|
86
|
+
|
87
|
+
const props = withDefaults(defineProps<InputProps>(), {
|
88
|
+
type: 'text',
|
89
|
+
placeholder: '',
|
90
|
+
label: '',
|
91
|
+
disabled: false,
|
92
|
+
readonly: false,
|
93
|
+
required: false,
|
94
|
+
clearable: false,
|
95
|
+
showWordLimit: false,
|
96
|
+
autocomplete: 'off',
|
97
|
+
error: false,
|
98
|
+
width: '100%'
|
99
|
+
})
|
100
|
+
|
101
|
+
const emit = defineEmits<{
|
102
|
+
'update:modelValue': [value: string | number]
|
103
|
+
'input': [value: string | number, event: Event]
|
104
|
+
'change': [value: string | number, event: Event]
|
105
|
+
'blur': [event: FocusEvent]
|
106
|
+
'focus': [event: FocusEvent]
|
107
|
+
'clear': []
|
108
|
+
'keydown': [event: KeyboardEvent]
|
109
|
+
'keyup': [event: KeyboardEvent]
|
110
|
+
}>()
|
111
|
+
|
112
|
+
const inputRef = ref<HTMLInputElement>()
|
113
|
+
const isFocused = ref(false)
|
114
|
+
|
115
|
+
// 计算属性
|
116
|
+
const hasLabel = computed(() => !!props.label)
|
117
|
+
|
118
|
+
const hasValue = computed(() => {
|
119
|
+
const value = props.modelValue
|
120
|
+
return value !== undefined && value !== null && value !== ''
|
121
|
+
})
|
122
|
+
|
123
|
+
const currentLength = computed(() => {
|
124
|
+
return String(props.modelValue || '').length
|
125
|
+
})
|
126
|
+
|
127
|
+
// 事件处理
|
128
|
+
const handleInput = (event: Event) => {
|
129
|
+
const target = event.target as HTMLInputElement
|
130
|
+
const value = target.value
|
131
|
+
emit('update:modelValue', value)
|
132
|
+
emit('input', value, event)
|
133
|
+
}
|
134
|
+
|
135
|
+
const handleChange = (event: Event) => {
|
136
|
+
const target = event.target as HTMLInputElement
|
137
|
+
const value = target.value
|
138
|
+
emit('change', value, event)
|
139
|
+
}
|
140
|
+
|
141
|
+
const handleBlur = (event: FocusEvent) => {
|
142
|
+
isFocused.value = false
|
143
|
+
emit('blur', event)
|
144
|
+
}
|
145
|
+
|
146
|
+
const handleFocus = (event: FocusEvent) => {
|
147
|
+
isFocused.value = true
|
148
|
+
emit('focus', event)
|
149
|
+
}
|
150
|
+
|
151
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
152
|
+
emit('keydown', event)
|
153
|
+
}
|
154
|
+
|
155
|
+
const handleKeyup = (event: KeyboardEvent) => {
|
156
|
+
emit('keyup', event)
|
157
|
+
}
|
158
|
+
|
159
|
+
const handleClear = () => {
|
160
|
+
emit('update:modelValue', '')
|
161
|
+
emit('clear')
|
162
|
+
inputRef.value?.focus()
|
163
|
+
}
|
164
|
+
|
165
|
+
// 暴露方法
|
166
|
+
const focus = () => {
|
167
|
+
inputRef.value?.focus()
|
168
|
+
}
|
169
|
+
|
170
|
+
const blur = () => {
|
171
|
+
inputRef.value?.blur()
|
172
|
+
}
|
173
|
+
|
174
|
+
const select = () => {
|
175
|
+
inputRef.value?.select()
|
176
|
+
}
|
177
|
+
|
178
|
+
defineExpose({
|
179
|
+
focus,
|
180
|
+
blur,
|
181
|
+
select,
|
182
|
+
ref: inputRef
|
183
|
+
})
|
184
|
+
</script>
|
185
|
+
<style scoped>
|
186
|
+
.x-input-group {
|
187
|
+
font-family: var(--x-font-family, 'Segoe UI', sans-serif);
|
188
|
+
position: relative;
|
189
|
+
display: inline-block;
|
190
|
+
}
|
191
|
+
|
192
|
+
.x-input {
|
193
|
+
width: 100%;
|
194
|
+
font-size: var(--x-font-size-sm, 14px);
|
195
|
+
padding: 0.8em 1em;
|
196
|
+
outline: none;
|
197
|
+
border: 2px solid var(--x-color-gray-300, rgb(200, 200, 200));
|
198
|
+
background-color: transparent;
|
199
|
+
border-radius: var(--x-radius-lg, 12px);
|
200
|
+
transition: all var(--x-transition, 0.3s ease);
|
201
|
+
color: var(--x-color-gray-900, #1a1a1a);
|
202
|
+
}
|
203
|
+
|
204
|
+
.x-input:hover:not(:disabled) {
|
205
|
+
border-color: var(--x-color-gray-400, rgb(170, 170, 170));
|
206
|
+
}
|
207
|
+
|
208
|
+
.x-input:focus {
|
209
|
+
border-color: var(--x-color-primary, #1a1a1a);
|
210
|
+
box-shadow: 0 0 0 3px rgba(26, 26, 26, 0.1);
|
211
|
+
}
|
212
|
+
|
213
|
+
.x-input--disabled {
|
214
|
+
background-color: var(--x-color-gray-100, #f5f5f5);
|
215
|
+
cursor: not-allowed;
|
216
|
+
opacity: 0.6;
|
217
|
+
}
|
218
|
+
|
219
|
+
.x-input--error {
|
220
|
+
border-color: var(--x-color-danger, #ef4444);
|
221
|
+
}
|
222
|
+
|
223
|
+
.x-input--error:focus {
|
224
|
+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
|
225
|
+
}
|
226
|
+
|
227
|
+
.x-input--clearable {
|
228
|
+
padding-right: 2.5em;
|
229
|
+
}
|
230
|
+
|
231
|
+
.x-input-label {
|
232
|
+
position: absolute;
|
233
|
+
left: 1em;
|
234
|
+
top: 50%;
|
235
|
+
transform: translateY(-50%);
|
236
|
+
padding: 0 0.4em;
|
237
|
+
font-size: var(--x-font-size-sm, 14px);
|
238
|
+
color: var(--x-color-gray-500, rgb(100, 100, 100));
|
239
|
+
background-color: white;
|
240
|
+
pointer-events: none;
|
241
|
+
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
242
|
+
transform-origin: left center;
|
243
|
+
white-space: nowrap;
|
244
|
+
z-index: 1;
|
245
|
+
}
|
246
|
+
|
247
|
+
/* Label 浮动状态 */
|
248
|
+
.x-input-label--active {
|
249
|
+
top: 0;
|
250
|
+
transform: translateY(-50%) scale(0.85);
|
251
|
+
font-size: var(--x-font-size-xs, 12px);
|
252
|
+
color: var(--x-color-primary, #1a1a1a);
|
253
|
+
font-weight: 500;
|
254
|
+
}
|
255
|
+
|
256
|
+
/* Label 错误状态 */
|
257
|
+
.x-input-label--error.x-input-label--active {
|
258
|
+
color: var(--x-color-danger, #ef4444);
|
259
|
+
}
|
260
|
+
|
261
|
+
/* 禁用状态的 label */
|
262
|
+
.x-input--disabled ~ .x-input-label {
|
263
|
+
color: var(--x-color-gray-400, #aaa);
|
264
|
+
opacity: 0.6;
|
265
|
+
}
|
266
|
+
|
267
|
+
.x-input-clear {
|
268
|
+
position: absolute;
|
269
|
+
right: 0.8em;
|
270
|
+
top: 50%;
|
271
|
+
transform: translateY(-50%);
|
272
|
+
background: none;
|
273
|
+
border: none;
|
274
|
+
cursor: pointer;
|
275
|
+
padding: 0.2em;
|
276
|
+
color: var(--x-color-gray-400, #aaa);
|
277
|
+
transition: color 0.2s;
|
278
|
+
font-size: 1.2em;
|
279
|
+
line-height: 1;
|
280
|
+
}
|
281
|
+
|
282
|
+
.x-input-clear:hover {
|
283
|
+
color: var(--x-color-gray-600, #666);
|
284
|
+
}
|
285
|
+
|
286
|
+
.x-input-count {
|
287
|
+
position: absolute;
|
288
|
+
right: 0.8em;
|
289
|
+
bottom: -1.5em;
|
290
|
+
font-size: var(--x-font-size-xs, 12px);
|
291
|
+
color: var(--x-color-gray-500, #888);
|
292
|
+
}
|
293
|
+
</style>
|