@xlui/xux-ui 0.3.0 → 1.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.
@@ -0,0 +1,390 @@
1
+ <template>
2
+ <div :class="containerClass" :style="containerStyle">
3
+ <canvas ref="canvasRef" :width="canvasSize" :height="canvasSize" :style="canvasStyle"></canvas>
4
+
5
+ <!-- 加载状态 -->
6
+ <div v-if="loading" class="x-qrcode-loading">
7
+ <div class="x-qrcode-spinner"></div>
8
+ <span v-if="loadingText" class="x-qrcode-loading-text">{{ loadingText }}</span>
9
+ </div>
10
+
11
+ <!-- 错误状态 -->
12
+ <div v-if="error" class="x-qrcode-error">
13
+ <div class="x-qrcode-error-icon">⚠️</div>
14
+ <span v-if="errorText" class="x-qrcode-error-text">{{ errorText }}</span>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup lang="ts">
20
+ import { ref, computed, watch, onMounted, nextTick } from 'vue'
21
+
22
+ /**
23
+ * Qrcode 二维码组件
24
+ * @displayName XQrcode
25
+ */
26
+
27
+ export interface QrcodeProps {
28
+ /**
29
+ * 二维码内容
30
+ */
31
+ value: string
32
+ /**
33
+ * 文本图标(支持emoji、文本)
34
+ */
35
+ textIcon?: string
36
+ /**
37
+ * 图片图标(支持图片URL)
38
+ */
39
+ icon?: string
40
+ /**
41
+ * 图标尺寸
42
+ */
43
+ iconSize?: number
44
+ /**
45
+ * 图标颜色(仅对文本图标有效)
46
+ */
47
+ iconColor?: string
48
+ /**
49
+ * 二维码尺寸
50
+ */
51
+ size?: number
52
+ /**
53
+ * 二维码背景色
54
+ */
55
+ backgroundColor?: string
56
+ /**
57
+ * 二维码前景色
58
+ */
59
+ foregroundColor?: string
60
+ /**
61
+ * 二维码容错级别
62
+ * @values L, M, Q, H
63
+ */
64
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'
65
+ /**
66
+ * 是否显示边框
67
+ */
68
+ bordered?: boolean
69
+ /**
70
+ * 边框颜色
71
+ */
72
+ borderColor?: string
73
+ /**
74
+ * 边框宽度
75
+ */
76
+ borderWidth?: number
77
+ /**
78
+ * 加载文本
79
+ */
80
+ loadingText?: string
81
+ /**
82
+ * 错误文本
83
+ */
84
+ errorText?: string
85
+ }
86
+
87
+ const props = withDefaults(defineProps<QrcodeProps>(), {
88
+ size: 200,
89
+ iconSize: 40,
90
+ iconColor: '#000000',
91
+ backgroundColor: '#ffffff',
92
+ foregroundColor: '#1a1a1a',
93
+ errorCorrectionLevel: 'M',
94
+ bordered: true,
95
+ borderColor: '#d9d9d9',
96
+ borderWidth: 1,
97
+ loadingText: '生成中...',
98
+ errorText: '生成失败'
99
+ })
100
+
101
+ const emit = defineEmits<{
102
+ /**
103
+ * 二维码生成成功时触发
104
+ */
105
+ (e: 'success'): void
106
+ /**
107
+ * 二维码生成失败时触发
108
+ */
109
+ (e: 'error', error: Error): void
110
+ }>()
111
+
112
+ const canvasRef = ref<HTMLCanvasElement>()
113
+ const loading = ref(false)
114
+ const error = ref(false)
115
+
116
+ const canvasSize = computed(() => props.size)
117
+ const containerSize = computed(() => props.bordered ? props.size + props.borderWidth * 2 : props.size)
118
+
119
+ const containerClass = computed(() => [
120
+ 'x-qrcode',
121
+ {
122
+ 'x-qrcode-bordered': props.bordered,
123
+ 'x-qrcode-loading': loading.value,
124
+ 'x-qrcode-error': error.value
125
+ }
126
+ ])
127
+
128
+ const containerStyle = computed(() => ({
129
+ width: `${containerSize.value}px`,
130
+ height: `${containerSize.value}px`
131
+ }))
132
+
133
+ const canvasStyle = computed(() => ({
134
+ width: `${props.size}px`,
135
+ height: `${props.size}px`,
136
+ border: props.bordered ? `${props.borderWidth}px solid ${props.borderColor}` : 'none',
137
+ borderRadius: '4px'
138
+ }))
139
+
140
+ // 使用qrcode库生成真正的二维码
141
+ async function generateQRCode(value: string, size: number): Promise<void> {
142
+ try {
143
+ // 动态导入qrcode库
144
+ const QRCode = (await import('qrcode')).default
145
+ const canvas = canvasRef.value
146
+ if (!canvas) {
147
+ throw new Error('Canvas not found')
148
+ }
149
+
150
+ // 配置二维码选项
151
+ const options = {
152
+ width: size,
153
+ margin: 1,
154
+ color: {
155
+ dark: props.foregroundColor,
156
+ light: props.backgroundColor
157
+ },
158
+ errorCorrectionLevel: props.errorCorrectionLevel
159
+ }
160
+
161
+ // 生成二维码到canvas
162
+ await QRCode.toCanvas(canvas, value, options)
163
+
164
+ // 如果有图标,在二维码中心绘制图标
165
+ if (props.textIcon || props.icon) {
166
+ await drawIconInCenter(canvas, size)
167
+ }
168
+ } catch (err) {
169
+ throw new Error(`Failed to generate QR code: ${err}`)
170
+ }
171
+ }
172
+
173
+ // 在二维码中心绘制图标
174
+ async function drawIconInCenter(canvas: HTMLCanvasElement, size: number): Promise<void> {
175
+ const ctx = canvas.getContext('2d')
176
+ if (!ctx) return
177
+
178
+ const iconSize = props.iconSize
179
+ const centerX = size / 2
180
+ const centerY = size / 2
181
+ const iconX = centerX - iconSize / 2
182
+ const iconY = centerY - iconSize / 2
183
+
184
+ // 优先处理图片图标
185
+ if (props.icon) {
186
+ // 图片图标处理
187
+ const img = new Image()
188
+ img.crossOrigin = 'anonymous'
189
+
190
+ return new Promise((resolve, reject) => {
191
+ img.onload = () => {
192
+ try {
193
+ // 绘制白色背景矩形
194
+ ctx.fillStyle = props.backgroundColor
195
+ ctx.beginPath()
196
+ ctx.rect(iconX - 4, iconY - 4, iconSize + 8, iconSize + 8)
197
+ ctx.fill()
198
+
199
+ ctx.drawImage(img, iconX, iconY, iconSize, iconSize)
200
+ resolve()
201
+ } catch (err) {
202
+ reject(err)
203
+ }
204
+ }
205
+ img.onerror = () => {
206
+ reject(new Error('Failed to load icon image'))
207
+ }
208
+ img.src = props.icon!
209
+ })
210
+ } else if (props.textIcon) {
211
+ // 文本图标处理(包括emoji)
212
+ ctx.font = `${iconSize}px Arial`
213
+ ctx.textAlign = 'center'
214
+ ctx.textBaseline = 'middle'
215
+
216
+ // 测量文本实际尺寸
217
+ const textMetrics = ctx.measureText(props.textIcon)
218
+ const textWidth = textMetrics.width
219
+ const textHeight = iconSize // 字体大小作为高度参考
220
+
221
+ // 计算背景矩形尺寸,添加适当的内边距
222
+ const padding = 8
223
+ const bgWidth = Math.max(textWidth + padding, iconSize)
224
+ const bgHeight = Math.max(textHeight + padding, iconSize)
225
+ const bgX = centerX - bgWidth / 2
226
+ const bgY = centerY - bgHeight / 2
227
+
228
+ // 绘制白色背景矩形
229
+ ctx.fillStyle = props.backgroundColor
230
+ ctx.beginPath()
231
+ ctx.rect(bgX, bgY, bgWidth, bgHeight)
232
+ ctx.fill()
233
+
234
+ // 绘制文本
235
+ ctx.fillStyle = props.iconColor
236
+ ctx.fillText(props.textIcon, centerX, centerY)
237
+ }
238
+ }
239
+
240
+ async function createQRCode() {
241
+ if (!props.value) {
242
+ error.value = true
243
+ emit('error', new Error('Value is required'))
244
+ return
245
+ }
246
+
247
+ loading.value = true
248
+ error.value = false
249
+
250
+ try {
251
+ await generateQRCode(props.value, props.size)
252
+ loading.value = false
253
+ emit('success')
254
+ } catch (err) {
255
+ loading.value = false
256
+ error.value = true
257
+ emit('error', err as Error)
258
+ }
259
+ }
260
+
261
+ // 监听属性变化
262
+ watch(
263
+ () => [props.value, props.size, props.backgroundColor, props.foregroundColor, props.textIcon, props.icon, props.iconSize, props.iconColor],
264
+ () => {
265
+ nextTick(() => {
266
+ createQRCode()
267
+ })
268
+ },
269
+ { deep: true }
270
+ )
271
+
272
+ onMounted(() => {
273
+ createQRCode()
274
+ })
275
+
276
+ // 暴露方法
277
+ defineExpose({
278
+ /**
279
+ * 重新生成二维码
280
+ */
281
+ refresh: createQRCode
282
+ })
283
+ </script>
284
+
285
+ <style scoped>
286
+ .x-qrcode {
287
+ position: relative;
288
+ display: inline-block;
289
+ background-color: var(--x-color-white, #ffffff);
290
+ border-radius: 8px;
291
+ overflow: hidden;
292
+ transition: all 0.3s ease;
293
+ }
294
+
295
+ .x-qrcode-bordered {
296
+ box-shadow: var(--x-shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));
297
+ }
298
+
299
+ .x-qrcode canvas {
300
+ display: block;
301
+ transition: opacity 0.3s ease;
302
+ }
303
+
304
+ /* 加载状态 */
305
+ .x-qrcode-loading {
306
+ position: absolute;
307
+ top: 0;
308
+ left: 0;
309
+ right: 0;
310
+ bottom: 0;
311
+ display: flex;
312
+ flex-direction: column;
313
+ align-items: center;
314
+ justify-content: center;
315
+ background-color: rgba(255, 255, 255, 0.9);
316
+ backdrop-filter: blur(2px);
317
+ }
318
+
319
+ .x-qrcode-loading canvas {
320
+ opacity: 0.3;
321
+ }
322
+
323
+ .x-qrcode-spinner {
324
+ width: 24px;
325
+ height: 24px;
326
+ border: 2px solid var(--x-color-gray-200, #e5e5e5);
327
+ border-top: 2px solid var(--x-color-primary, #ff6b35);
328
+ border-radius: 50%;
329
+ animation: spin 1s linear infinite;
330
+ margin-bottom: 8px;
331
+ }
332
+
333
+ .x-qrcode-loading-text {
334
+ font-size: 12px;
335
+ color: var(--x-color-gray-600, #666666);
336
+ text-align: center;
337
+ }
338
+
339
+ /* 错误状态 */
340
+ .x-qrcode-error {
341
+ position: absolute;
342
+ top: 0;
343
+ left: 0;
344
+ right: 0;
345
+ bottom: 0;
346
+ display: flex;
347
+ flex-direction: column;
348
+ align-items: center;
349
+ justify-content: center;
350
+ background-color: rgba(255, 255, 255, 0.9);
351
+ backdrop-filter: blur(2px);
352
+ }
353
+
354
+ .x-qrcode-error canvas {
355
+ opacity: 0.1;
356
+ }
357
+
358
+ .x-qrcode-error-icon {
359
+ font-size: 24px;
360
+ margin-bottom: 8px;
361
+ }
362
+
363
+ .x-qrcode-error-text {
364
+ font-size: 12px;
365
+ color: var(--x-color-danger, #ef4444);
366
+ text-align: center;
367
+ }
368
+
369
+ @keyframes spin {
370
+ from {
371
+ transform: rotate(0deg);
372
+ }
373
+
374
+ to {
375
+ transform: rotate(360deg);
376
+ }
377
+ }
378
+
379
+ /* 响应式 */
380
+ @media (max-width: 768px) {
381
+ .x-qrcode {
382
+ border-radius: 6px;
383
+ }
384
+
385
+ .x-qrcode-loading-text,
386
+ .x-qrcode-error-text {
387
+ font-size: 11px;
388
+ }
389
+ }
390
+ </style>
@@ -0,0 +1,196 @@
1
+ // areas.worker.ts - Web Worker 用于处理大数据量的地区数据
2
+ interface AreaNode {
3
+ value: string
4
+ label: string
5
+ children?: AreaNode[]
6
+ }
7
+
8
+ interface AreaNodeSimple {
9
+ value: string
10
+ label: string
11
+ leaf: boolean
12
+ }
13
+
14
+ interface AreaNodeWithPath {
15
+ value: string
16
+ label: string
17
+ leaf: boolean
18
+ level: number // 1-省, 2-市, 3-区
19
+ path: string[] // 完整路径的value数组
20
+ pathLabels: string[] // 完整路径的label数组
21
+ }
22
+
23
+ // 索引:父级code -> 子节点数组
24
+ let indexByParent: Record<string, AreaNodeSimple[]> = {}
25
+ let rootNodes: AreaNodeSimple[] = []
26
+ let fullData: AreaNode[] = []
27
+ // 全局索引:code -> 完整路径信息
28
+ let globalIndex: Map<string, AreaNodeWithPath> = new Map()
29
+
30
+ // 构建索引
31
+ function buildIndex(tree: AreaNode[]) {
32
+ indexByParent = {}
33
+ globalIndex = new Map()
34
+
35
+ function traverse(nodes: AreaNode[], parentValue: string = '', level: number = 1, path: string[] = [], pathLabels: string[] = []) {
36
+ nodes.forEach(node => {
37
+ const currentPath = [...path, node.value]
38
+ const currentPathLabels = [...pathLabels, node.label]
39
+
40
+ const simpleNode: AreaNodeSimple = {
41
+ value: node.value,
42
+ label: node.label,
43
+ leaf: !node.children || node.children.length === 0
44
+ }
45
+
46
+ // 构建全局索引
47
+ const nodeWithPath: AreaNodeWithPath = {
48
+ value: node.value,
49
+ label: node.label,
50
+ leaf: simpleNode.leaf,
51
+ level,
52
+ path: currentPath,
53
+ pathLabels: currentPathLabels
54
+ }
55
+ globalIndex.set(node.value, nodeWithPath)
56
+
57
+ if (parentValue === '') {
58
+ // 根节点(省份)
59
+ rootNodes.push(simpleNode)
60
+ } else {
61
+ // 子节点
62
+ if (!indexByParent[parentValue]) {
63
+ indexByParent[parentValue] = []
64
+ }
65
+ indexByParent[parentValue].push(simpleNode)
66
+ }
67
+
68
+ // 递归处理子节点
69
+ if (node.children && node.children.length > 0) {
70
+ traverse(node.children, node.value, level + 1, currentPath, currentPathLabels)
71
+ }
72
+ })
73
+ }
74
+
75
+ traverse(tree)
76
+ }
77
+
78
+ // 监听主线程消息
79
+ self.onmessage = (e: MessageEvent) => {
80
+ const { type, payload } = e.data || {}
81
+
82
+ switch (type) {
83
+ case 'init':
84
+ // 初始化:接收完整数据并建立索引
85
+ fullData = payload.root
86
+ rootNodes = []
87
+ buildIndex(fullData)
88
+ console.log('[Worker] 索引构建完成,根节点数:', rootNodes.length)
89
+ console.log('[Worker] 索引键数:', Object.keys(indexByParent).length)
90
+ self.postMessage({ type: 'ready' })
91
+ break
92
+
93
+ case 'roots':
94
+ // 获取根节点(省份列表)
95
+ self.postMessage({
96
+ type: 'roots',
97
+ list: rootNodes
98
+ })
99
+ break
100
+
101
+ case 'children':
102
+ // 获取指定父节点的子节点
103
+ const { parent } = payload
104
+ const children = indexByParent[parent] || []
105
+ self.postMessage({
106
+ type: 'children',
107
+ parent,
108
+ list: children
109
+ })
110
+ break
111
+
112
+ case 'getByCode':
113
+ // 根据code获取单个节点信息
114
+ const { code } = payload
115
+ // 先在根节点中查找
116
+ let found = rootNodes.find(n => n.value === code)
117
+ if (!found) {
118
+ // 在索引中查找
119
+ for (const children of Object.values(indexByParent)) {
120
+ found = children.find(n => n.value === code)
121
+ if (found) break
122
+ }
123
+ }
124
+ self.postMessage({
125
+ type: 'nodeInfo',
126
+ code,
127
+ node: found || null
128
+ })
129
+ break
130
+
131
+ case 'search':
132
+ // 全局搜索功能(支持智能反填)
133
+ const { keyword, parentValue } = payload
134
+ if (!keyword || keyword.trim() === '') {
135
+ self.postMessage({
136
+ type: 'searchResults',
137
+ keyword,
138
+ results: []
139
+ })
140
+ break
141
+ }
142
+
143
+ const searchKeyword = keyword.toLowerCase().trim()
144
+ const searchResults: AreaNodeWithPath[] = []
145
+
146
+ // 如果指定了父节点,只在该父节点的子节点中搜索
147
+ if (parentValue) {
148
+ const children = indexByParent[parentValue] || []
149
+ for (const node of children) {
150
+ if (node.label.toLowerCase().includes(searchKeyword) ||
151
+ node.value.includes(searchKeyword)) {
152
+ const nodeWithPath = globalIndex.get(node.value)
153
+ if (nodeWithPath) {
154
+ searchResults.push(nodeWithPath)
155
+ }
156
+ }
157
+ }
158
+ } else {
159
+ // 全局搜索:在所有节点中搜索
160
+ for (const [code, node] of globalIndex.entries()) {
161
+ if (node.label.toLowerCase().includes(searchKeyword) ||
162
+ code.includes(searchKeyword)) {
163
+ searchResults.push(node)
164
+ // 限制搜索结果数量
165
+ if (searchResults.length >= 100) break
166
+ }
167
+ }
168
+ }
169
+
170
+ // 按层级排序:省 > 市 > 区
171
+ searchResults.sort((a, b) => a.level - b.level)
172
+
173
+ self.postMessage({
174
+ type: 'searchResults',
175
+ keyword,
176
+ results: searchResults
177
+ })
178
+ break
179
+
180
+ default:
181
+ console.warn('[Worker] Unknown message type:', type)
182
+ }
183
+ }
184
+
185
+ // 错误处理
186
+ self.onerror = (error) => {
187
+ console.error('[Worker] Error:', error)
188
+ const errorMessage = error instanceof ErrorEvent ? error.message : String(error)
189
+ self.postMessage({
190
+ type: 'error',
191
+ error: errorMessage
192
+ })
193
+ }
194
+
195
+ export {}
196
+