reborn-ui 0.1.76 → 0.1.78
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/dist/index.js +182 -240
- package/dist/index.js.map +1 -1
- package/package.json +53 -53
- package/registry/components/reborn-affix.json +8 -3
- package/registry/components/reborn-back-top.json +9 -4
- package/registry/components/reborn-badge.json +11 -5
- package/registry/components/reborn-button.json +5 -5
- package/registry/components/reborn-card.json +18 -0
- package/registry/components/reborn-cascader.json +18 -0
- package/registry/components/reborn-checkbox.json +4 -4
- package/registry/components/reborn-chip.json +11 -5
- package/registry/components/reborn-collapse.json +11 -5
- package/registry/components/reborn-color-picker.json +50 -0
- package/registry/components/reborn-draggable.json +32 -0
- package/registry/components/reborn-drawer.json +17 -0
- package/registry/components/reborn-dropdown-select.json +18 -0
- package/registry/components/reborn-footer.json +40 -0
- package/registry/components/reborn-form.json +11 -6
- package/registry/components/reborn-image.json +10 -5
- package/registry/components/reborn-input-number.json +12 -6
- package/registry/components/reborn-input-otp.json +40 -0
- package/registry/components/reborn-input.json +4 -4
- package/registry/components/reborn-loading.json +23 -0
- package/registry/components/reborn-loadmore.json +23 -0
- package/registry/components/reborn-overlay.json +38 -0
- package/registry/components/reborn-page.json +18 -0
- package/registry/components/reborn-picker-view.json +26 -0
- package/registry/components/reborn-popover.json +58 -0
- package/registry/components/reborn-popup.json +23 -0
- package/registry/components/reborn-qrcode.json +45 -0
- package/registry/components/reborn-radio.json +45 -0
- package/registry/components/reborn-rate.json +40 -0
- package/registry/components/reborn-root-portal.json +26 -0
- package/registry/components/reborn-select-date.json +40 -0
- package/registry/components/reborn-select-trigger.json +25 -0
- package/registry/components/reborn-select.json +41 -0
- package/registry/components/reborn-slider.json +40 -0
- package/registry/components/reborn-sticky.json +12 -6
- package/registry/components/reborn-switch.json +13 -7
- package/registry/components/reborn-tabbar.json +38 -0
- package/registry/components/reborn-tabs copy.json +46 -0
- package/registry/components/reborn-tabs-test.json +46 -0
- package/registry/components/reborn-tabs.json +12 -6
- package/registry/components/reborn-text.json +34 -0
- package/registry/components/reborn-textarea.json +5 -5
- package/registry/components/reborn-toast.json +38 -0
- package/registry/components/reborn-transition.json +38 -0
- package/registry/components/reborn-waterfall.json +18 -0
- package/registry/components/scroll-island.json +2 -2
- package/registry/registry.json +1101 -97
package/registry/registry.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-
|
|
3
|
+
"generatedAt": "2026-03-19T03:57:32.260Z",
|
|
4
4
|
"source": {
|
|
5
|
-
"rootDir": "D:/
|
|
5
|
+
"rootDir": "D:/demo2/Reborn-UI",
|
|
6
6
|
"componentsDir": "app/components/reborn/ui"
|
|
7
7
|
},
|
|
8
8
|
"components": [
|
|
@@ -882,9 +882,14 @@
|
|
|
882
882
|
"content": "<template>\r\n <div class=\"fixed touch-none z-[9999]\" :style=\"viewStyle\" @pointerdown=\"onPointerDown\"\r\n @pointermove.stop.prevent=\"onPointerMove\" @pointerup=\"onPointerUp\" @pointercancel=\"onPointerUp\">\r\n <slot></slot>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, nextTick, reactive, type CSSProperties, onMounted } from \"vue\";\r\nimport { useWindowSize } from '@vueuse/core'\r\n\r\ndefineOptions({\r\n name: \"RebornAffix\"\r\n});\r\n\r\nexport interface FloatViewProps {\r\n zIndex?: number;\r\n size?: number;\r\n left?: number;\r\n right?: number;\r\n top?: number;\r\n bottom?: number;\r\n gap?: number;\r\n disabled?: boolean;\r\n noSnapping?: boolean;\r\n // Web 端通常不需要 manifest 的 safeArea,但保留 API 兼容\r\n safeArea?: boolean;\r\n}\r\n\r\nconst props = withDefaults(defineProps<FloatViewProps>(), {\r\n zIndex: 500,\r\n size: 40,\r\n left: 10,\r\n bottom: 10,\r\n gap: 10,\r\n disabled: false,\r\n noSnapping: false,\r\n safeArea: true\r\n});\r\n\r\n// 使用 VueUse 获取响应式窗口大小\r\nconst { width: screenWidth, height: screenHeight } = useWindowSize()\r\n\r\n// Web 端安全区域通常为 0,除非是 PWA standalone 模式\r\n// 这里简化处理,Web 端一般默认为 0\r\nconst safeAreaBottom = 0;\r\nconst safeAreaTop = 0;\r\n\r\nconst position = reactive({\r\n x: props.right !== undefined ? (screenWidth.value - props.size - props.right) : props.left,\r\n y: props.top !== undefined\r\n ? (screenHeight.value - props.size - props.top - safeAreaTop)\r\n : (props.bottom + safeAreaBottom),\r\n isDragging: false\r\n});\r\n\r\n// 监听窗口大小变化修正位置 (防止 resize 后按钮消失)\r\nwatch([screenWidth, screenHeight], () => {\r\n performEdgeSnapping()\r\n})\r\n\r\nonMounted(() => {\r\n // 修正初始位置:如果是 right 定位,确保根据当前屏幕宽度计算\r\n // 避免 SSR/初始 width 为 0 导致被 snap 到左边\r\n if (props.right !== undefined) {\r\n position.x = screenWidth.value - props.size - props.right;\r\n }\r\n nextTick(() => {\r\n performEdgeSnapping();\r\n })\r\n})\r\n\r\nconst dragState = reactive({\r\n startX: 0,\r\n startY: 0\r\n});\r\n\r\n// 动态样式计算\r\nconst viewStyle = computed<CSSProperties>(() => {\r\n const style: CSSProperties = {\r\n left: `${position.x}px`,\r\n bottom: `${position.y}px`,\r\n zIndex: props.zIndex,\r\n width: `${props.size}px`,\r\n height: `${props.size}px`,\r\n position: \"fixed\",\r\n touchAction: \"none\", // 关键:禁用浏览器默认手势\r\n userSelect: \"none\"\r\n };\r\n\r\n if (position.isDragging) {\r\n style.transition = \"none\";\r\n } else {\r\n style.transition = \"left 300ms, bottom 300ms\";\r\n }\r\n\r\n return style;\r\n});\r\n\r\nfunction onPointerDown(e: PointerEvent) {\r\n if (props.disabled) return;\r\n if (e.isPrimary === false) return;\r\n\r\n // 捕获指针\r\n (e.target as Element).setPointerCapture(e.pointerId);\r\n\r\n dragState.startX = e.clientX;\r\n dragState.startY = e.clientY;\r\n position.isDragging = true;\r\n}\r\n\r\nfunction onPointerMove(e: PointerEvent) {\r\n if (props.disabled || !position.isDragging) return;\r\n\r\n const deltaX = e.clientX - dragState.startX;\r\n // Y轴逻辑:Web 坐标系 Y 轴向下,clientX/Y 也是左上角为原点\r\n // 但是我们的 CSS bottom 是相对于底部的。\r\n // 当鼠标 Y 变大 (向下移),e.clientY 变大,diff > 0\r\n // 此时距离底部应该变小 (position.y 变小)\r\n // 所以 deltaY = startY - clientY 没问题\r\n const deltaY = dragState.startY - e.clientY;\r\n\r\n let newX = position.x + deltaX;\r\n let newY = position.y + deltaY;\r\n\r\n // --- 水平边界限制 ---\r\n newX = Math.max(0, Math.min(screenWidth.value - props.size, newX));\r\n\r\n // --- 垂直边界限制 ---\r\n const minY = safeAreaBottom;\r\n const maxY = screenHeight.value - props.size - safeAreaTop;\r\n\r\n newY = Math.max(minY, Math.min(maxY, newY));\r\n\r\n position.x = newX;\r\n position.y = newY;\r\n\r\n dragState.startX = e.clientX;\r\n dragState.startY = e.clientY;\r\n}\r\n\r\nfunction onPointerUp(e: PointerEvent) {\r\n if (props.disabled || !position.isDragging) return;\r\n\r\n (e.target as Element).releasePointerCapture(e.pointerId);\r\n\r\n nextTick(() => {\r\n position.isDragging = false;\r\n if (!props.noSnapping) {\r\n performEdgeSnapping();\r\n }\r\n });\r\n}\r\n\r\nfunction performEdgeSnapping() {\r\n const { size, gap } = props;\r\n const edgeThreshold = 60;\r\n const centerX = screenWidth.value / 2;\r\n\r\n // 水平吸附\r\n if (position.x < edgeThreshold) {\r\n position.x = gap;\r\n } else if (position.x > screenWidth.value - size - edgeThreshold) {\r\n position.x = screenWidth.value - size - gap;\r\n } else {\r\n position.x = position.x < centerX ? gap : screenWidth.value - size - gap;\r\n }\r\n\r\n // 垂直吸附修正\r\n const maxY = screenHeight.value - size - safeAreaTop;\r\n const minSafeY = gap + safeAreaBottom;\r\n\r\n if (position.y > maxY - gap) position.y = maxY - gap;\r\n if (position.y < minSafeY) position.y = minSafeY;\r\n}\r\n</script>\r\n",
|
|
883
883
|
"target": "web"
|
|
884
884
|
},
|
|
885
|
+
{
|
|
886
|
+
"path": "index.ts",
|
|
887
|
+
"content": "export { default as RebornAffix } from './RebornAffix.vue'\r\n",
|
|
888
|
+
"target": "uniapp"
|
|
889
|
+
},
|
|
885
890
|
{
|
|
886
891
|
"path": "RebornAffix.vue",
|
|
887
|
-
"content": "<
|
|
892
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { CSSProperties } from 'vue'\r\nimport { computed, nextTick, reactive } from 'vue'\r\n\r\ndefineOptions({\r\n name: 'RebornAffix',\r\n})\r\n\r\nconst props = withDefaults(defineProps<FloatViewProps>(), {\r\n zIndex: 500,\r\n size: 40,\r\n left: 10,\r\n bottom: 10,\r\n gap: 10,\r\n disabled: false,\r\n noSnapping: false,\r\n safeArea: true,\r\n})\r\n\r\nexport interface FloatViewProps {\r\n zIndex?: number\r\n size?: number\r\n left?: number\r\n right?: number\r\n top?: number\r\n bottom?: number\r\n gap?: number\r\n disabled?: boolean\r\n noSnapping?: boolean\r\n // 新增:是否考虑安全区域(默认为 true)\r\n safeArea?: boolean\r\n}\r\n\r\n// 获取屏幕信息和安全区域\r\n// 获取屏幕信息和安全区域\r\nconst windowInfo = uni.getWindowInfo() || {}\r\nconst screenWidth = windowInfo.screenWidth || 0\r\nconst screenHeight = windowInfo.screenHeight || 0\r\n\r\n// 获取底部安全区域高度 (iPhone X+ 的底部黑条)\r\nconst safeAreaBottom = props.safeArea ? (windowInfo.safeAreaInsets?.bottom || 0) : 0\r\n// 获取顶部安全区域高度 (状态栏/刘海屏)\r\nconst safeAreaTop = props.safeArea ? (windowInfo.safeAreaInsets?.top || 0) : 0\r\n\r\nconst position = reactive({\r\n x: props.right !== undefined ? (screenWidth - props.size - props.right) : props.left,\r\n // 初始位置:\r\n // 如果指定了 top,则 y = 屏幕高度 - size - top - 顶部安全区域\r\n // 否则使用 bottom + 底部安全区域\r\n y: props.top !== undefined\r\n ? (screenHeight - props.size - props.top - safeAreaTop)\r\n : (props.bottom + safeAreaBottom),\r\n isDragging: false,\r\n})\r\n\r\nconst dragState = reactive({\r\n startX: 0,\r\n startY: 0,\r\n})\r\n\r\n// 动态样式计算\r\nconst viewStyle = computed<CSSProperties>(() => {\r\n const style: CSSProperties = {\r\n left: `${position.x}px`,\r\n bottom: `${position.y}px`, // 直接使用 position.y,逻辑在内部处理\r\n zIndex: props.zIndex,\r\n width: `${props.size}px`,\r\n height: `${props.size}px`,\r\n position: 'fixed',\r\n }\r\n\r\n if (position.isDragging) {\r\n style.transitionProperty = 'none'\r\n }\r\n else {\r\n style.transitionProperty = 'left, bottom'\r\n style.transitionDuration = '300ms'\r\n }\r\n\r\n return style\r\n})\r\n\r\nfunction onTouchStart(e: TouchEvent) {\r\n if (props.disabled) { return }\r\n if (e.touches.length > 0) {\r\n const touch = e.touches[0]\r\n dragState.startX = touch.clientX\r\n dragState.startY = touch.clientY\r\n position.isDragging = true\r\n }\r\n}\r\n\r\nfunction onTouchMove(e: TouchEvent) {\r\n if (props.disabled || !position.isDragging || e.touches.length === 0) { return }\r\n\r\n const touch = e.touches[0]\r\n const deltaX = touch.clientX - dragState.startX\r\n // Y轴逻辑:手指往上滑(clientY变小),bottom值应该变大\r\n const deltaY = dragState.startY - touch.clientY\r\n\r\n // 增加拖拽阈值判断,避免误触点击事件\r\n if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) { return }\r\n\r\n // 确认为拖拽行为,阻止默认事件\r\n if (e.cancelable) {\r\n e.preventDefault()\r\n }\r\n\r\n let newX = position.x + deltaX\r\n let newY = position.y + deltaY\r\n\r\n // --- 水平边界限制 ---\r\n newX = Math.max(0, Math.min(screenWidth - props.size, newX))\r\n\r\n // --- 垂直边界限制 ---\r\n // 最小值:如果有安全区域,则不能拖到安全区域以下\r\n const minY = safeAreaBottom\r\n // 最大值:屏幕高度减去自身高度 - 顶部安全区域\r\n const maxY = screenHeight - props.size - safeAreaTop\r\n\r\n newY = Math.max(minY, Math.min(maxY, newY))\r\n\r\n position.x = newX\r\n position.y = newY\r\n dragState.startX = touch.clientX\r\n dragState.startY = touch.clientY\r\n}\r\n\r\nfunction performEdgeSnapping() {\r\n const { size, gap } = props\r\n const edgeThreshold = 60\r\n const centerX = screenWidth / 2\r\n\r\n // 水平吸附\r\n if (position.x < edgeThreshold) {\r\n position.x = gap\r\n }\r\n else if (position.x > screenWidth - size - edgeThreshold) {\r\n position.x = screenWidth - size - gap\r\n }\r\n else {\r\n position.x = position.x < centerX ? gap : screenWidth - size - gap\r\n }\r\n\r\n // 垂直吸附修正\r\n const maxY = screenHeight - size - safeAreaTop\r\n // 底部最小值:Gap + 安全区域高度\r\n const minSafeY = gap + safeAreaBottom\r\n\r\n if (position.y > maxY - gap) { position.y = maxY - gap }\r\n if (position.y < minSafeY) { position.y = minSafeY }\r\n}\r\n\r\nfunction onTouchEnd() {\r\n if (props.disabled || !position.isDragging) { return }\r\n nextTick(() => {\r\n position.isDragging = false\r\n if (!props.noSnapping) {\r\n performEdgeSnapping()\r\n }\r\n })\r\n}\r\n</script>\r\n\r\n<template>\r\n <view\r\n class=\"fixed touch-none\" :style=\"viewStyle\" @touchstart=\"onTouchStart\" @touchmove=\"onTouchMove\"\r\n @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\"\r\n >\r\n <slot />\r\n </view>\r\n</template>\r\n\r\n<style></style>\r\n",
|
|
888
893
|
"target": "uniapp"
|
|
889
894
|
}
|
|
890
895
|
]
|
|
@@ -906,14 +911,19 @@
|
|
|
906
911
|
"content": "<template>\r\n <div :class=\"ui.wrapper()\"\r\n :style=\"{ bottom: viewBottom, opacity: visible ? 1 : 0, pointerEvents: visible ? 'auto' : 'none' }\"\r\n @click.stop=\"toTop\">\r\n <slot>\r\n <div :class=\"[\r\n ui.base(),\r\n visible ? '-translate-x-3' : 'translate-x-20'\r\n ]\">\r\n <span :class=\"ui.icon()\">↑</span>\r\n </div>\r\n </slot>\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport type { ClassValue } from \"clsx\"\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { useWindowScroll } from '@vueuse/core'\r\nimport theme, { backTopColors, backTopSizes } from \"./reborn-back-top.config\";\r\n\r\ndefineOptions({\r\n name: \"RebornBackTop\"\r\n});\r\n\r\nexport interface BackTopProps {\r\n // Web: 也可以传入自定义的 scrollTop,如果不传则自动监听 window\r\n scrollTop?: number;\r\n // 滚动多少距离后显示\r\n threshold?: number;\r\n // 底部距离 (px)\r\n bottom?: number;\r\n // 滚动动画时长 (ms) - Web 使用 CSS scroll-behavior: smooth,此参数作为兼容保留\r\n duration?: number;\r\n // 是否是 TabBar 页面 (Web 通常不需要)\r\n isTab?: boolean;\r\n // 是否适配安全区域\r\n safeArea?: boolean;\r\n color?: typeof backTopColors[number]\r\n size?: typeof backTopSizes[number]\r\n ui?: {\r\n wrapper?: ClassValue,\r\n base?: ClassValue,\r\n icon?: ClassValue\r\n }\r\n}\r\n\r\nconst props = withDefaults(defineProps<BackTopProps>(), {\r\n threshold: 300,\r\n bottom: 20,\r\n duration: 300,\r\n isTab: false,\r\n safeArea: true,\r\n color: 'primary',\r\n size: 'md'\r\n});\r\n\r\nconst b = tv(theme)\r\n\r\nconst emit = defineEmits([\"click\"]);\r\n\r\n// 自动获取 window 滚动\r\nconst { y: windowY } = useWindowScroll()\r\n\r\n// 优先使用 props.scrollTop (如果外部手动控制),否则使用 windowY\r\nconst currentScrollTop = computed(() => {\r\n return props.scrollTop !== undefined ? props.scrollTop : windowY.value\r\n})\r\n\r\n// Web 端安全区域通常为 0\r\nconst getSafeAreaBottom = () => 0\r\n\r\n// 计算最终底部距离\r\nconst viewBottom = computed(() => {\r\n let h = props.bottom;\r\n\r\n if (props.safeArea) {\r\n h += getSafeAreaBottom();\r\n }\r\n\r\n if (props.isTab) {\r\n h += 50;\r\n }\r\n\r\n return `${h}px`;\r\n});\r\n\r\n// 是否显示\r\nconst visible = computed(() => currentScrollTop.value > props.threshold);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n size: props.size,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n }\r\n})\r\n\r\n// 回到顶部\r\nfunction toTop() {\r\n window.scrollTo({\r\n top: 0,\r\n behavior: 'smooth'\r\n });\r\n emit(\"click\");\r\n}\r\n</script>\r\n",
|
|
907
912
|
"target": "web"
|
|
908
913
|
},
|
|
914
|
+
{
|
|
915
|
+
"path": "index.ts",
|
|
916
|
+
"content": "export { default as RebornBackTop } from './RebornBackTop.vue'\r\n",
|
|
917
|
+
"target": "uniapp"
|
|
918
|
+
},
|
|
909
919
|
{
|
|
910
920
|
"path": "reborn-back-top.config.ts",
|
|
911
|
-
"content": "const color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst sizes = [
|
|
921
|
+
"content": "const color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst sizes = ['sm', 'md', 'lg'] as const\r\nexport { color as backTopColors, sizes as backTopSizes }\r\nexport default {\r\n slots: {\r\n wrapper: 'fixed right-0 z-50 overflow-visible pointer-none transition-all duration-300 active:scale-95',\r\n base: 'flex flex-row items-center justify-center text-white shadow-lg rounded-full pointer-events-auto cursor-pointer transition-transform duration-300',\r\n icon: 'text-white',\r\n },\r\n variants: {\r\n color: {\r\n primary: { base: 'bg-primary' },\r\n secondary: { base: 'bg-secondary' },\r\n success: { base: 'bg-success' },\r\n info: { base: 'bg-info' },\r\n warning: { base: 'bg-warning' },\r\n error: { base: 'bg-error' },\r\n neutral: { base: 'bg-neutral' },\r\n },\r\n size: {\r\n sm: { base: 'size-8', icon: 'text-26' },\r\n md: { base: 'size-10', icon: 'text-28' },\r\n lg: { base: 'size-12', icon: 'text-30' },\r\n },\r\n },\r\n defaultVariants: {\r\n color: 'primary' as const,\r\n size: 'md' as const,\r\n },\r\n}\r\n",
|
|
912
922
|
"target": "uniapp"
|
|
913
923
|
},
|
|
914
924
|
{
|
|
915
925
|
"path": "RebornBackTop.vue",
|
|
916
|
-
"content": "<
|
|
926
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { backTopColors, backTopSizes } from './reborn-back-top.config'\r\nimport { computed } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-back-top.config'\r\n\r\ndefineOptions({\r\n name: 'RebornBackTop',\r\n})\r\n\r\nconst props = withDefaults(defineProps<BackTopProps>(), {\r\n scrollTop: 0,\r\n threshold: 300,\r\n bottom: 20,\r\n duration: 300,\r\n isTab: false,\r\n safeArea: true,\r\n color: 'primary',\r\n size: 'md',\r\n})\r\n\r\nconst emit = defineEmits(['click'])\r\n\r\nexport interface BackTopProps {\r\n // 当前页面的滚动距离\r\n scrollTop?: number\r\n // 滚动多少距离后显示\r\n threshold?: number\r\n // 底部距离 (px)\r\n bottom?: number\r\n // 滚动动画时长 (ms)\r\n duration?: number\r\n // 是否是 TabBar 页面 (如果是,会自动抬高 50px)\r\n isTab?: boolean\r\n // 是否适配安全区域\r\n safeArea?: boolean\r\n color?: typeof backTopColors[number]\r\n size?: typeof backTopSizes[number]\r\n ui?: {\r\n wrapper?: ClassValue\r\n base?: ClassValue\r\n icon?: ClassValue\r\n }\r\n}\r\n\r\nconst b = tv(theme)\r\n\r\n// 获取安全区域高度\r\nfunction getSafeAreaBottom(): number {\r\n const windowInfo = uni.getWindowInfo()\r\n return windowInfo?.safeAreaInsets?.bottom || 0\r\n}\r\n\r\n// 计算最终底部距离\r\nconst viewBottom = computed(() => {\r\n let h = props.bottom\r\n\r\n // 1. 如果开启了安全区域适配 (通常非 Tab 页需要避开底部黑条)\r\n if (props.safeArea) {\r\n h += getSafeAreaBottom()\r\n }\r\n\r\n // 2. 如果是 Tab 页,额外抬高 50px (标准 TabBar 高度)\r\n // 这样你就不用手动写 bottom=\"70\" 了\r\n if (props.isTab) {\r\n h += 50\r\n }\r\n\r\n return `${h}px`\r\n})\r\n\r\n// 是否显示\r\nconst visible = computed(() => props.scrollTop > props.threshold)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n size: props.size,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n }\r\n})\r\n\r\n// 回到顶部\r\nfunction toTop() {\r\n uni.pageScrollTo({\r\n scrollTop: 0,\r\n duration: props.duration,\r\n })\r\n emit('click')\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper()\" :style=\"{ bottom: viewBottom, opacity: visible ? 1 : 0 }\" @tap.stop=\"toTop\">\r\n <slot>\r\n <view :class=\"[\r\n ui.base(),\r\n visible ? '-translate-x-3' : 'translate-x-20',\r\n ]\">\r\n <text :class=\"ui.icon()\">↑</text>\r\n </view>\r\n </slot>\r\n </view>\r\n</template>\r\n",
|
|
917
927
|
"target": "uniapp"
|
|
918
928
|
}
|
|
919
929
|
]
|
|
@@ -930,7 +940,8 @@
|
|
|
930
940
|
},
|
|
931
941
|
{
|
|
932
942
|
"path": "index.ts",
|
|
933
|
-
"content": "export { default as Badge } from \"./RebornBadge.vue\";\r\n"
|
|
943
|
+
"content": "export { default as Badge } from \"./RebornBadge.vue\";\r\n",
|
|
944
|
+
"target": "web"
|
|
934
945
|
},
|
|
935
946
|
{
|
|
936
947
|
"path": "reborn-badge.config.ts",
|
|
@@ -942,14 +953,19 @@
|
|
|
942
953
|
"content": "<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport theme from './badge'\r\nimport { useFieldGroup } from '~/composables/useFieldGroup'\r\nimport { tv } from '~/lib/tv'\r\n\r\n// Define the variant builder\r\nconst b = tv({ extend: tv(theme) })\r\n\r\n// Derive types directly from the theme object to ensure static analysis works\r\ntype Theme = typeof theme\r\ntype BadgeColor = keyof Theme['variants']['color']\r\ntype BadgeVariant = keyof Theme['variants']['variant']\r\ntype BadgeSize = keyof Theme['variants']['size']\r\n\r\nexport interface BadgeProps {\r\n /**\r\n * The element or component this component should render as.\r\n * @defaultValue 'span'\r\n */\r\n as?: any\r\n label?: string | number\r\n /**\r\n * @defaultValue 'primary'\r\n */\r\n color?: BadgeColor | (string & {})\r\n /**\r\n * @defaultValue 'solid'\r\n */\r\n variant?: BadgeVariant | (string & {})\r\n /**\r\n * @defaultValue 'md'\r\n */\r\n size?: BadgeSize | (string & {})\r\n /** Render the badge with equal padding on all sides. */\r\n square?: boolean\r\n /** Whether the badge is closable. */\r\n closable?: boolean\r\n /** The icon for the close button. */\r\n closeIcon?: string\r\n class?: any\r\n ui?: any\r\n}\r\n\r\nexport interface BadgeSlots {\r\n leading(props: { ui: any }): any\r\n default(props: { ui: any }): any\r\n trailing(props: { ui: any }): any\r\n close(props: { ui: any; close: (e: MouseEvent) => void }): any\r\n}\r\n\r\nconst props = withDefaults(defineProps<BadgeProps>(), {\r\n as: 'span',\r\n color: 'primary',\r\n variant: 'solid',\r\n size: 'md',\r\n closeIcon: 'i-lucide-x'\r\n})\r\n\r\nconst slots = defineSlots<BadgeSlots>()\r\n\r\nconst emit = defineEmits<{\r\n close: [payload: MouseEvent]\r\n}>()\r\n\r\nconst show = defineModel<boolean>('show', { default: true })\r\n\r\nconst { orientation, size: fieldGroupSize } = useFieldGroup(props)\r\n\r\nconst ui = computed(() => b({\r\n color: props.color as BadgeColor,\r\n variant: props.variant as BadgeVariant,\r\n size: (fieldGroupSize.value || props.size) as BadgeSize,\r\n square: props.square || (!slots.default && !props.label),\r\n fieldGroup: orientation.value\r\n}))\r\n\r\nconst handleClose = (e: MouseEvent) => {\r\n show.value = false\r\n emit('close', e)\r\n}\r\n</script>\r\n\r\n<template>\r\n <component :is=\"props.as\" v-if=\"show\" :class=\"ui.base({ class: props.class })\">\r\n <slot name=\"leading\" :ui=\"ui\" />\r\n\r\n <slot :ui=\"ui\">\r\n <span v-if=\"label\" :class=\"ui.label()\">\r\n {{ label }}\r\n </span>\r\n </slot>\r\n\r\n <slot name=\"trailing\" :ui=\"ui\" />\r\n\r\n <span v-if=\"closable\" @click.stop=\"handleClose\" :class=\"ui.closeButton()\">\r\n <slot name=\"close\" :ui=\"ui\" :close=\"handleClose\">\r\n <Icon :name=\"closeIcon\" class=\"size-3\" />\r\n </slot>\r\n </span>\r\n </component>\r\n</template>\r\n",
|
|
943
954
|
"target": "web"
|
|
944
955
|
},
|
|
956
|
+
{
|
|
957
|
+
"path": "index.ts",
|
|
958
|
+
"content": "export { default as RebornBadge } from './RebornBadge.vue'\r\n",
|
|
959
|
+
"target": "uniapp"
|
|
960
|
+
},
|
|
945
961
|
{
|
|
946
962
|
"path": "reborn-badge.config.ts",
|
|
947
|
-
"content": "const
|
|
963
|
+
"content": "const badgeSizes = ['sm', 'md', 'lg'] as const\r\nconst badgeColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst badgeVariants = ['solid', 'outline', 'soft', 'subtle'] as const\r\n\r\nexport { badgeColors, badgeSizes, badgeVariants }\r\n\r\nexport type BadgeColor = (typeof badgeColors)[number]\r\nexport type BadgeSize = (typeof badgeSizes)[number]\r\nexport type BadgeVariant = (typeof badgeVariants)[number]\r\n\r\nconst config = {\r\n slots: {\r\n base: 'font-medium inline-flex items-center justify-center p-1 gap-1 transition-all duration-200',\r\n label: 'truncate',\r\n leadingIcon: 'shrink-0',\r\n trailingIcon: 'shrink-0',\r\n closeButton: 'ml-1 inline-flex items-center justify-center rounded-full transition-colors hover:bg-black/10 dark:hover:bg-white/10 cursor-pointer',\r\n closeIcon: 'shrink-0',\r\n },\r\n variants: {\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n variant: {\r\n solid: '',\r\n outline: '',\r\n soft: '',\r\n subtle: '',\r\n },\r\n size: {\r\n sm: {\r\n base: 'h-[28rpx] min-w-[28rpx] text-20 rounded-[6rpx] px-[8rpx]',\r\n leadingIcon: 'size-2',\r\n trailingIcon: 'size-2',\r\n closeIcon: 'size-2',\r\n },\r\n md: {\r\n base: 'h-[36rpx] min-w-[36rpx] leading-1.5 text-22 rounded-[8rpx] px-[12rpx]',\r\n leadingIcon: 'size-3',\r\n trailingIcon: 'size-3',\r\n closeIcon: 'size-3',\r\n },\r\n lg: {\r\n base: 'h-[44rpx] min-w-[44rpx] leading-1.5 text-24 rounded-[12rpx] px-[16rpx]',\r\n leadingIcon: 'size-4',\r\n trailingIcon: 'size-4',\r\n closeIcon: 'size-4',\r\n },\r\n },\r\n square: {\r\n true: {\r\n base: 'px-0 aspect-square',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n // solid\r\n { color: 'primary' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-primary text-white' },\r\n { color: 'secondary' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-secondary text-secondary-foreground' },\r\n { color: 'success' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-success text-white' },\r\n { color: 'info' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-info text-white' },\r\n { color: 'warning' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-warning text-white' },\r\n { color: 'error' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-error text-white' },\r\n { color: 'neutral' as BadgeColor, variant: 'solid' as BadgeVariant, class: 'bg-neutral text-white' },\r\n // outline\r\n { color: 'primary' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-primary border border-primary/50' },\r\n { color: 'secondary' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-secondary border border-secondary/50' },\r\n { color: 'success' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-success border border-success/50' },\r\n { color: 'info' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-info border border-info/50' },\r\n { color: 'warning' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-warning border border-warning/50' },\r\n { color: 'error' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-error border border-error/50' },\r\n { color: 'neutral' as BadgeColor, variant: 'outline' as BadgeVariant, class: 'text-neutral border border-neutral/50' },\r\n // soft\r\n { color: 'primary' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-primary/10 text-primary' },\r\n { color: 'secondary' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-secondary/10 text-secondary' },\r\n { color: 'success' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-success/10 text-success' },\r\n { color: 'info' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-info/10 text-info' },\r\n { color: 'warning' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-warning/10 text-warning' },\r\n { color: 'error' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-error/10 text-error' },\r\n { color: 'neutral' as BadgeColor, variant: 'soft' as BadgeVariant, class: 'bg-neutral/10 text-neutral' },\r\n // subtle\r\n { color: 'primary' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-primary/10 border border-primary/50 text-primary' },\r\n { color: 'secondary' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-secondary/10 border border-secondary/50 text-secondary' },\r\n { color: 'success' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-success/10 border border-success/50 text-success' },\r\n { color: 'info' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-info/10 border border-info/50 text-info' },\r\n { color: 'warning' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-warning/10 border border-warning/50 text-warning' },\r\n { color: 'error' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-error/10 border border-error/50 text-error' },\r\n { color: 'neutral' as BadgeColor, variant: 'subtle' as BadgeVariant, class: 'bg-neutral/10 border border-neutral/50 text-neutral' },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as BadgeColor,\r\n variant: 'solid' as BadgeVariant,\r\n size: 'md' as BadgeSize,\r\n },\r\n}\r\n\r\nexport default config\r\n",
|
|
948
964
|
"target": "uniapp"
|
|
949
965
|
},
|
|
950
966
|
{
|
|
951
967
|
"path": "RebornBadge.vue",
|
|
952
|
-
"content": "<script setup lang=\"ts\">\r\nimport {
|
|
968
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { badgeColors, badgeSizes, badgeVariants } from './reborn-badge.config'\r\nimport { computed, ref, useSlots } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-badge.config'\r\n\r\ndefineOptions({\r\n name: 'RebornBadge',\r\n inheritAttrs: false,\r\n})\r\n\r\nimport type { BadgeColor, BadgeSize, BadgeVariant } from './reborn-badge.config'\r\n\r\nexport interface BadgeProps {\r\n label?: string | number\r\n color?: BadgeColor\r\n variant?: BadgeVariant\r\n size?: BadgeSize\r\n icon?: string\r\n square?: boolean\r\n closable?: boolean\r\n closeIcon?: string\r\n customClass?: any\r\n ui?: {\r\n base?: string\r\n label?: string\r\n leadingIcon?: string\r\n trailingIcon?: string\r\n closeButton?: string\r\n closeIcon?: string\r\n }\r\n}\r\n\r\nconst props = withDefaults(defineProps<BadgeProps>(), {\r\n color: 'primary',\r\n variant: 'solid',\r\n size: 'md',\r\n closeIcon: 'i-mdi-close-circle',\r\n})\r\n\r\nconst emit = defineEmits(['close', 'click'])\r\n\r\nconst b = tv(theme)\r\n\r\nconst slots = useSlots()\r\nconst isClosing = ref(false)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n variant: props.variant,\r\n size: props.size,\r\n square: props.square || (!props.label && !props.icon && !slots.default),\r\n })\r\n\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n leadingIcon: (opts?: { class?: any }) => styles.leadingIcon({ class: cn(opts?.class, uiOverrides.value.leadingIcon) }),\r\n trailingIcon: (opts?: { class?: any }) => styles.trailingIcon({ class: cn(opts?.class, uiOverrides.value.trailingIcon) }),\r\n closeButton: (opts?: { class?: any }) => styles.closeButton({ class: cn(opts?.class, uiOverrides.value.closeButton) }),\r\n closeIcon: (opts?: { class?: any }) => styles.closeIcon({ class: cn(opts?.class, uiOverrides.value.closeIcon) }),\r\n }\r\n})\r\n\r\nfunction onClick(e: any) {\r\n emit('click', e)\r\n}\r\n\r\nfunction handleClose(e: any) {\r\n isClosing.value = true\r\n nextTick(() => {\r\n emit('close', e)\r\n })\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.base({\r\n class: cn(props.customClass, `\r\n transition-all duration-200 ease-in-out\r\n `, isClosing && 'scale-90 opacity-0')\r\n })\" @tap=\"onClick\">\r\n <slot name=\"leading\">\r\n <view v-if=\"props.icon\" :class=\"cn(props.icon, ui.leadingIcon())\" />\r\n </slot>\r\n\r\n <slot>\r\n <text v-if=\"props.label\" :class=\"ui.label()\">{{ props.label }}</text>\r\n </slot>\r\n\r\n <slot name=\"trailing\" />\r\n\r\n <view v-if=\"props.closable\" :class=\"ui.closeButton()\" @tap.stop=\"handleClose\">\r\n <slot name=\"close\">\r\n <view :class=\"cn(props.closeIcon, ui.closeIcon())\" />\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped></style>\r\n",
|
|
953
969
|
"target": "uniapp"
|
|
954
970
|
}
|
|
955
971
|
]
|
|
@@ -970,22 +986,54 @@
|
|
|
970
986
|
},
|
|
971
987
|
{
|
|
972
988
|
"path": "RebornButton.vue",
|
|
973
|
-
"content": "<script setup lang=\"ts\">\r\nimport { computed, toRef } from 'vue'\r\nimport theme, { buttonColors, buttonVariants, buttonSizes } from './reborn-button.config'\r\nimport { useFieldGroup } from '~/composables/useFieldGroup'\r\nimport { tv } from '~/lib/tv'\r\
|
|
989
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, toRef } from 'vue'\r\nimport theme, { buttonColors, buttonVariants, buttonSizes } from './reborn-button.config'\r\nimport { useFieldGroup } from '~/composables/useFieldGroup'\r\nimport { tv } from '~/lib/tv'\r\nimport { cn } from '~/lib/utils'\r\n\r\n\r\nexport interface ButtonProps {\r\n label?: string\r\n color?: typeof buttonColors[number]\r\n variant?: typeof buttonVariants[number]\r\n size?: typeof buttonSizes[number]\r\n loading?: boolean\r\n disabled?: boolean\r\n square?: boolean\r\n class?: any\r\n ui?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<ButtonProps>(), {\r\n color: 'primary',\r\n variant: 'solid',\r\n size: 'md',\r\n loading: false,\r\n disabled: false,\r\n square: false\r\n})\r\n\r\nconst slots = defineSlots<{\r\n leading(props: { ui: any }): any\r\n default(props: { ui: any }): any\r\n trailing(props: { ui: any }): any\r\n}>()\r\n\r\nconst { orientation, size: fieldGroupSize } = useFieldGroup(props)\r\n\r\nconst isDisabled = computed(() => props.disabled || props.loading)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst square = toRef(props, 'square')\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: (fieldGroupSize.value || size.value) as any,\r\n square: square.value,\r\n fieldGroup: orientation.value,\r\n })\r\n\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n leadingIcon: (opts?: { class?: any }) => styles.leadingIcon({ class: cn(opts?.class, uiOverrides.value.leadingIcon) }),\r\n leadingAvatar: (opts?: { class?: any }) => styles.leadingAvatar({ class: cn(opts?.class, uiOverrides.value.leadingAvatar) }),\r\n leadingAvatarSize: (opts?: { class?: any }) => styles.leadingAvatarSize({ class: cn(opts?.class, uiOverrides.value.leadingAvatarSize) }),\r\n trailingIcon: (opts?: { class?: any }) => styles.trailingIcon({ class: cn(opts?.class, uiOverrides.value.trailingIcon) }),\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <button :disabled=\"isDisabled\" :class=\"ui.base({ class: props.class })\" v-bind=\"$attrs\">\r\n <slot name=\"leading\" :ui=\"ui\">\r\n <Icon name=\"svg-spinners:270-ring\" v-if=\"props.loading\" :class=\"ui.leadingIcon()\" />\r\n </slot>\r\n\r\n <slot :ui=\"ui\">\r\n <span v-if=\"label\" :class=\"ui.label()\">\r\n {{ label }}\r\n </span>\r\n <slot v-else :ui=\"ui\" />\r\n </slot>\r\n\r\n\r\n <slot name=\"trailing\" :ui=\"ui.trailingIcon()\" />\r\n </button>\r\n</template>\r\n",
|
|
974
990
|
"target": "web"
|
|
975
991
|
},
|
|
976
992
|
{
|
|
977
993
|
"path": "index.ts",
|
|
978
|
-
"content": "export { default as RebornButton } from
|
|
994
|
+
"content": "export { default as RebornButton } from './RebornButton.vue'\r\n",
|
|
979
995
|
"target": "uniapp"
|
|
980
996
|
},
|
|
981
997
|
{
|
|
982
998
|
"path": "reborn-button.config.ts",
|
|
983
|
-
"content": "export const buttonColors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\nexport const buttonVariants = [\"solid\", \"outline\", \"soft\", \"subtle\"] as const;\r\n\r\nexport const buttonSizes = [\r\n \"xs\",\r\n \"sm\",\r\n \"default\", // Mapped to md\r\n \"md\",\r\n \"lg\",\r\n \"xl\",\r\n \"2xl\",\r\n \"icon-xs\",\r\n \"icon-sm\",\r\n \"icon\", // Mapped to icon-md\r\n \"icon-md\",\r\n \"icon-lg\",\r\n \"icon-xl\",\r\n \"icon-2xl\",\r\n] as const;\r\n\r\nexport default {\r\n slots: {\r\n base: \"inline-flex items-center justify-center gap-2 whitespace-nowrap transition-all cursor-pointer disabled:cursor-not-allowed disabled:opacity-70 disabled:bg-gdray-4 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive\",\r\n label: \"truncate\",\r\n leadingIcon: \"shrink-0\",\r\n leadingAvatar: \"shrink-0\",\r\n leadingAvatarSize: \"\",\r\n trailingIcon: \"shrink-0\",\r\n },\r\n variants: {\r\n fieldGroup: {\r\n horizontal:\r\n \"not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-visible:z-[1]\",\r\n vertical:\r\n \"not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-visible:z-[1]\",\r\n },\r\n color: {\r\n primary: \"\",\r\n secondary: \"\",\r\n success: \"\",\r\n info: \"\",\r\n warning: \"\",\r\n error: \"\",\r\n neutral: \"\",\r\n },\r\n variant: {\r\n solid: \"\",\r\n outline: \"\",\r\n soft: \"\",\r\n subtle: \"\",\r\n },\r\n size: {\r\n xs: {\r\n base: \"h-[var(--button-xs-height)] text-24 gap-1.5 px-3 has-[>svg]:px-2.5\",\r\n },\r\n sm: {\r\n base: \"h-[var(--button-sm-height)] text-24 gap-1.5 px-3 has-[>svg]:px-2.5\",\r\n },\r\n default: {\r\n base: \"h-[var(--button-base-height)] text-26 px-4 py-4 has-[>svg]:px-3\",\r\n },\r\n md: { base: \"h-[var(--button-base-height)] text-26 px-4 py-4 has-[>svg]:px-3\" },\r\n lg: { base: \"h-[var(--button-lg-height)] text-28 px-6 has-[>svg]:px-4\" },\r\n xl: { base: \"h-[var(--button-xl-height)] text-30 px-6 has-[>svg]:px-4\" },\r\n \"2xl\": { base: \"h-[var(--button-2xl-height)] text-32 px-6 has-[>svg]:px-4\" },\r\n \"icon-xs\": { base: \"\" },\r\n \"icon-sm\": { base: \"size-[var(--button-sm-height)] p-1\" },\r\n icon: { base: \"size-[var(--button-base-height)] p-2\" },\r\n \"icon-md\": { base: \"size-[var(--button-base-height)] p-2\" },\r\n \"icon-lg\": { base: \"size-[var(--button-lg-height)] p-2.5\" },\r\n \"icon-xl\": { base: \"size-[var(--button-xl-height)] p-2.5\" },\r\n \"icon-2xl\": { base: \"size-[var(--button-2xl-height)] p-2.5\" },\r\n },\r\n square: {\r\n true: { base: \"p-0\" },\r\n },\r\n },\r\n compoundVariants: [\r\n // Solid Variants\r\n {\r\n color: \"primary\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-primary text-white hover:bg-primary/75\",\r\n },\r\n {\r\n color: \"secondary\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-secondary text-white hover:bg-secondary/75\",\r\n },\r\n {\r\n color: \"success\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-success text-white hover:bg-success/75\",\r\n },\r\n {\r\n color: \"info\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-info text-white hover:bg-info/75\",\r\n },\r\n {\r\n color: \"warning\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-warning text-white hover:bg-warning/75\",\r\n },\r\n {\r\n color: \"error\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-error text-white hover:bg-error/75\",\r\n },\r\n {\r\n color: \"neutral\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n class: \"bg-neutral text-white hover:bg-neutral/75\",\r\n },\r\n\r\n // Outline Variants\r\n {\r\n color: \"primary\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-primary border border-primary hover:bg-primary/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"secondary\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-secondary border border-secondary hover:bg-secondary/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"success\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-success border border-success hover:bg-success/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"info\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-info border border-info hover:bg-info/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"warning\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-warning border border-warning hover:bg-warning/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"error\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-error border border-error hover:bg-error/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n {\r\n color: \"neutral\" as (typeof buttonColors)[number],\r\n variant: \"outline\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-transparent text-neutral border border-neutral hover:bg-neutral/10 disabled:bg-gray-2 disabled:border-gray-4 disabled:text-gray-6\",\r\n },\r\n\r\n // Soft Variants\r\n {\r\n color: \"primary\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-primary/10 text-primary hover:bg-primary/20\",\r\n },\r\n {\r\n color: \"secondary\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-secondary/10 text-secondary hover:bg-secondary/20\",\r\n },\r\n {\r\n color: \"success\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-success/10 text-success hover:bg-success/20\",\r\n },\r\n {\r\n color: \"info\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-info/10 text-info hover:bg-info/20\",\r\n },\r\n {\r\n color: \"warning\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-warning/10 text-warning hover:bg-warning/20\",\r\n },\r\n {\r\n color: \"error\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-error/10 text-error hover:bg-error/20\",\r\n },\r\n {\r\n color: \"neutral\" as (typeof buttonColors)[number],\r\n variant: \"soft\" as (typeof buttonVariants)[number],\r\n class: \"bg-neutral/10 text-neutral hover:bg-neutral/20\",\r\n },\r\n\r\n // Subtle Variants\r\n {\r\n color: \"primary\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-primary/10 border border-primary text-primary hover:bg-primary/20\",\r\n },\r\n {\r\n color: \"secondary\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class:\r\n \"bg-secondary/10 border border-secondary text-secondary hover:bg-secondary/20\",\r\n },\r\n {\r\n color: \"success\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-success/10 border border-success text-success hover:bg-success/20\",\r\n },\r\n {\r\n color: \"info\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-info/10 border border-info text-info hover:bg-info/20\",\r\n },\r\n {\r\n color: \"warning\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-warning/10 border border-warning text-warning hover:bg-warning/20\",\r\n },\r\n {\r\n color: \"error\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-error/10 border border-error text-error hover:bg-error/20\",\r\n },\r\n {\r\n color: \"neutral\" as (typeof buttonColors)[number],\r\n variant: \"subtle\" as (typeof buttonVariants)[number],\r\n class: \"bg-neutral/10 border border-neutral text-neutral hover:bg-neutral/20\",\r\n },\r\n ],\r\n defaultVariants: {\r\n color: \"primary\" as (typeof buttonColors)[number],\r\n variant: \"solid\" as (typeof buttonVariants)[number],\r\n size: \"md\" as (typeof buttonSizes)[number],\r\n },\r\n};\r\n",
|
|
999
|
+
"content": "export const buttonColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const buttonVariants = ['solid', 'outline', 'soft', 'subtle'] as const\r\n\r\nexport const buttonSizes = [\r\n 'xs',\r\n 'sm',\r\n 'md',\r\n 'lg',\r\n 'xl',\r\n '2xl',\r\n] as const\r\n\r\nexport default {\r\n slots: {\r\n base: 'reborn-button flex flex-row items-center justify-center relative box-border border-transparent border border-solid transition-[background-color,border-color,opacity] duration-300 overflow-visible',\r\n inner: 'reborn-button-clicker absolute inset-0 z-10 m-0 size-full p-0 opacity-0',\r\n label: 'truncate',\r\n loading: 'border-2 border-current border-t-transparent rounded-full animate-spin',\r\n leadingIcon: 'shrink-0',\r\n leadingAvatar: 'shrink-0',\r\n leadingAvatarSize: '',\r\n trailingIcon: 'shrink-0',\r\n },\r\n variants: {\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n block: {\r\n true: {\r\n base: 'flex'\r\n },\r\n false: {\r\n base: 'inline-flex',\r\n }\r\n },\r\n variant: {\r\n solid: '',\r\n outline: '',\r\n soft: '',\r\n subtle: '',\r\n },\r\n disabled: {\r\n true: {\r\n base: '!bg-opacity-60 !border-opacity-60 !text-opacity-60',\r\n },\r\n false: '',\r\n },\r\n size: {\r\n 'xs': {\r\n base: 'h-button-xs px-3 text-24 gap-1.5 rounded-[6px]',\r\n loading: 'size-3',\r\n },\r\n 'sm': {\r\n base: 'h-button-sm px-3 text-24 gap-1.5 rounded-[6px]',\r\n loading: 'size-3.5',\r\n },\r\n 'md': {\r\n base: 'h-button-md px-4 text-26 gap-1.5 rounded-[8px]',\r\n loading: 'size-4',\r\n },\r\n 'lg': {\r\n base: 'h-button-lg px-6 text-28 gap-1.5 rounded-[10px]',\r\n loading: 'size-5',\r\n },\r\n 'xl': {\r\n base: 'h-button-xl px-6 text-30 gap-2 rounded-[12px]',\r\n loading: 'size-6',\r\n },\r\n '2xl': {\r\n base: 'h-button-2xl px-6 text-32 gap-2 rounded-[14px]',\r\n loading: 'size-7',\r\n },\r\n },\r\n gap: {\r\n true: {\r\n base: '[.reborn-button_+_&]:ml-2',\r\n },\r\n false: '',\r\n },\r\n },\r\n compoundVariants: [\r\n // Solid Variants\r\n {\r\n color: 'primary' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-primary border-primary text-white hover:bg-primary/90',\r\n },\r\n {\r\n color: 'secondary' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-secondary border-secondary text-secondary-foreground hover:bg-secondary/90',\r\n },\r\n {\r\n color: 'success' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-success border-success text-white hover:bg-success/90',\r\n },\r\n {\r\n color: 'info' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-info border-info text-white hover:bg-info/90',\r\n },\r\n {\r\n color: 'warning' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-warning border-warning text-white hover:bg-warning/90',\r\n },\r\n {\r\n color: 'error' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-error border-error text-white hover:bg-error/90',\r\n },\r\n {\r\n color: 'neutral' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n class: 'bg-neutral border-neutral text-white hover:bg-neutral/90',\r\n },\r\n\r\n // Outline Variants\r\n {\r\n color: 'primary' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-primary border-primary hover:bg-primary-50',\r\n },\r\n {\r\n color: 'secondary' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-secondary border-secondary hover:bg-secondary-50',\r\n },\r\n {\r\n color: 'success' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-success border-success hover:bg-success-50',\r\n },\r\n {\r\n color: 'info' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-info border-info hover:bg-info-50',\r\n },\r\n {\r\n color: 'warning' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-warning border-warning hover:bg-warning-50',\r\n },\r\n {\r\n color: 'error' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-error border-error hover:bg-error-50',\r\n },\r\n {\r\n color: 'neutral' as (typeof buttonColors)[number],\r\n variant: 'outline' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-transparent text-neutral border-neutral hover:bg-neutral-50',\r\n },\r\n\r\n // Soft Variants\r\n {\r\n color: 'primary' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-primary/10 border-transparent text-primary hover:bg-primary/20',\r\n },\r\n {\r\n color: 'secondary' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-secondary/10 border-transparent text-secondary hover:bg-secondary/20',\r\n },\r\n {\r\n color: 'success' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-success/10 border-transparent text-success hover:bg-success/20',\r\n },\r\n {\r\n color: 'info' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-info/10 border-transparent text-info hover:bg-info/20',\r\n },\r\n {\r\n color: 'warning' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-warning/10 border-transparent text-warning hover:bg-warning/20',\r\n },\r\n {\r\n color: 'error' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-error/10 border-transparent text-error hover:bg-error/20',\r\n },\r\n {\r\n color: 'neutral' as (typeof buttonColors)[number],\r\n variant: 'soft' as (typeof buttonVariants)[number],\r\n class: 'bg-neutral/10 border-transparent text-neutral hover:bg-neutral/20',\r\n },\r\n\r\n // Subtle Variants\r\n {\r\n color: 'primary' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-primary/10 border-primary text-primary hover:bg-primary/20',\r\n },\r\n {\r\n color: 'secondary' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class:\r\n 'bg-secondary/10 border-secondary text-secondary hover:bg-secondary/20',\r\n },\r\n {\r\n color: 'success' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-success/10 border-success text-success hover:bg-success/20',\r\n },\r\n {\r\n color: 'info' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-info/10 border-info text-info hover:bg-info/20',\r\n },\r\n {\r\n color: 'warning' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-warning/10 border-warning text-warning hover:bg-warning/20',\r\n },\r\n {\r\n color: 'error' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-error/10 border-error text-error hover:bg-error/20',\r\n },\r\n {\r\n color: 'neutral' as (typeof buttonColors)[number],\r\n variant: 'subtle' as (typeof buttonVariants)[number],\r\n class: 'bg-neutral/10 border-neutral text-neutral hover:bg-neutral/20',\r\n },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as (typeof buttonColors)[number],\r\n variant: 'solid' as (typeof buttonVariants)[number],\r\n size: 'md' as (typeof buttonSizes)[number],\r\n block: false\r\n },\r\n}\r\n",
|
|
984
1000
|
"target": "uniapp"
|
|
985
1001
|
},
|
|
986
1002
|
{
|
|
987
1003
|
"path": "RebornButton.vue",
|
|
988
|
-
"content": "<script lang=\"ts\">\r\nexport interface ButtonProps {\r\n
|
|
1004
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { buttonColors, buttonSizes, buttonVariants } from './reborn-button.config'\r\nimport { computed, ref, toRef } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-button.config'\r\n\r\nexport interface ButtonProps {\r\n label?: string\r\n color?: typeof buttonColors[number]\r\n variant?: typeof buttonVariants[number]\r\n size?: typeof buttonSizes[number]\r\n loading?: boolean\r\n disabled?: boolean\r\n fluid?: boolean // 是否为 flex-1 布局\r\n gap?: boolean // 是否间隔按钮\r\n block?: boolean // 是否块级元素\r\n customClass?: any\r\n ui?: any\r\n hoverClass?: string // 按钮点击态样式类\r\n hoverStopPropagation?: boolean // 是否阻止点击态冒泡\r\n hoverStartTime?: number // 按钮点击态持续时间\r\n hoverStayTime?: number // 按钮点击态持续时间\r\n formType?: string // 表单提交类型\r\n openType?: string // 开放能力类型\r\n lang?: string // 语言\r\n sessionFrom?: string // 会话来源\r\n sendMessageTitle?: string // 会话标题\r\n sendMessagePath?: string // 会话路径\r\n sendMessageImg?: string // 会话图片\r\n showMessageCard?: boolean // 显示会话卡片\r\n appParameter?: string // 打开 APP 时,向 APP 传递的参数\r\n groupId?: string // 群ID\r\n guildId?: string // 公会ID\r\n publicId?: string // 公众号ID\r\n phoneNumberNoQuotaToast?: boolean // 手机号获取失败时是否弹出错误提示\r\n createliveactivity?: boolean // 是否创建直播活动\r\n}\r\n\r\nconst props = withDefaults(defineProps<ButtonProps>(), {\r\n color: 'primary',\r\n variant: 'solid',\r\n size: 'md',\r\n loading: false,\r\n disabled: false,\r\n fluid: false,\r\n gap: false,\r\n hoverStartTime: 20,\r\n hoverStayTime: 70,\r\n block: false\r\n})\r\n\r\n// 事件定义\r\nconst emit = defineEmits([\r\n 'click',\r\n 'tap',\r\n 'getuserinfo',\r\n 'contact',\r\n 'getphonenumber',\r\n 'error',\r\n 'opensetting',\r\n 'launchapp',\r\n 'chooseavatar',\r\n 'chooseaddress',\r\n 'chooseinvoicetitle',\r\n 'addgroupapp',\r\n 'subscribe',\r\n 'login',\r\n 'getrealtimephonenumber',\r\n 'agreeprivacyauthorization',\r\n])\r\n\r\nconst slots = defineSlots<{\r\n leading: (props: { ui: any, loading: boolean }) => any\r\n default: (props: { ui: any }) => any\r\n trailing: (props: { ui: any }) => any\r\n}>()\r\n\r\nconst b = tv(theme)\r\n\r\ninterface UniEvent {\r\n bubbles: boolean\r\n cancelable: boolean\r\n type: string\r\n target: any\r\n currentTarget: any\r\n timeStamp: number\r\n [key: string]: any\r\n}\r\nconst { size: fieldGroupSize, disabled: fieldGroupDisabled } = useFormInject(props)\r\n\r\nconst isDisabled = computed(() => fieldGroupDisabled.value || props.loading)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: (fieldGroupSize.value || size.value) as any,\r\n disabled: isDisabled.value,\r\n gap: props.gap,\r\n })\r\n\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n loading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n }\r\n})\r\n\r\n// 点击事件处理\r\nfunction onTap(e: UniEvent) {\r\n if (isDisabled.value) { return }\r\n\r\n emit('click', e)\r\n emit('tap', e)\r\n}\r\n\r\n// 点击态状态\r\nconst isHover = ref(false)\r\n\r\nconst onGetUserInfo = (e: any) => emit('getuserinfo', e)\r\nconst onContact = (e: any) => emit('contact', e)\r\nconst onGetPhoneNumber = (e: any) => emit('getphonenumber', e)\r\nconst onError = (e: any) => emit('error', e)\r\nconst onOpenSetting = (e: any) => emit('opensetting', e)\r\nconst onLaunchApp = (e: any) => emit('launchapp', e)\r\nconst onChooseAvatar = (e: any) => emit('chooseavatar', e)\r\nconst onChooseAddress = (e: any) => emit('chooseaddress', e)\r\nconst onChooseInvoiceTitle = (e: any) => emit('chooseinvoicetitle', e)\r\nconst onAddGroupApp = (e: any) => emit('addgroupapp', e)\r\nconst onSubscribe = (e: any) => emit('subscribe', e)\r\nconst onLogin = (e: any) => emit('login', e)\r\nconst onGetRealtimePhoneNumber = (e: any) => emit('getrealtimephonenumber', e)\r\nconst onAgreePrivacyAuthorization = (e: any) => emit('agreeprivacyauthorization', e)\r\n\r\n// 触摸开始事件处理\r\nfunction onTouchStart() {\r\n if (!isDisabled.value) {\r\n isHover.value = true\r\n }\r\n}\r\n\r\n// 触摸结束事件处理\r\nfunction onTouchEnd() {\r\n isHover.value = false\r\n}\r\n\r\n// 触摸取消事件处理\r\nfunction onTouchCancel() {\r\n isHover.value = false\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"[\r\n ui.base({ class: props.customClass }),\r\n isHover && hoverClass ? hoverClass : ''\r\n ]\" @tap=\"onTap\" @touchstart=\"onTouchStart\" @touchend=\"onTouchEnd\" @touchcancel=\"onTouchCancel\">\r\n <button :class=\"ui.inner()\" :disabled=\"isDisabled\" :hover-class=\"hoverClass\"\r\n :hover-stop-propagation=\"hoverStopPropagation\" :hover-start-time=\"hoverStartTime\" :hover-stay-time=\"hoverStayTime\"\r\n :form-type=\"formType\" :open-type=\"openType\" :lang=\"lang\" :session-from=\"sessionFrom\"\r\n :send-message-title=\"sendMessageTitle\" :send-message-path=\"sendMessagePath\" :send-message-img=\"sendMessageImg\"\r\n :show-message-card=\"showMessageCard\" :app-parameter=\"appParameter\" :group-id=\"groupId\" :guild-id=\"guildId\"\r\n :public-id=\"publicId\" :phone-number-no-quota-toast=\"phoneNumberNoQuotaToast\"\r\n :createliveactivity=\"createliveactivity\" @getuserinfo=\"onGetUserInfo\" @contact=\"onContact\"\r\n @getphonenumber=\"onGetPhoneNumber\" @error=\"onError\" @opensetting=\"onOpenSetting\" @launchapp=\"onLaunchApp\"\r\n @chooseavatar=\"onChooseAvatar\" @chooseaddress=\"onChooseAddress\" @chooseinvoicetitle=\"onChooseInvoiceTitle\"\r\n @addgroupapp=\"onAddGroupApp\" @subscribe=\"onSubscribe\" @login=\"onLogin\"\r\n @getrealtimephonenumber=\"onGetRealtimePhoneNumber\" @agreeprivacyauthorization=\"onAgreePrivacyAuthorization\" />\r\n\r\n <slot name=\"leading\" :loading=\"props.loading\" :ui=\"ui\">\r\n <view v-if=\"props.loading\" :class=\"ui.loading?.()\" />\r\n </slot>\r\n\r\n <slot :ui=\"ui\">\r\n <text v-if=\"label\" :class=\"ui.label()\">\r\n {{ label }}\r\n </text>\r\n <slot v-else :ui=\"ui\" :class=\"ui.label()\" />\r\n </slot>\r\n\r\n <slot name=\"trailing\" :ui=\"ui\" />\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n.reborn-button-clicker {\r\n border: none;\r\n}\r\n\r\n.reborn-button-clicker::after {\r\n border: none;\r\n}\r\n</style>\r\n",
|
|
1005
|
+
"target": "uniapp"
|
|
1006
|
+
}
|
|
1007
|
+
]
|
|
1008
|
+
},
|
|
1009
|
+
{
|
|
1010
|
+
"name": "reborn-card",
|
|
1011
|
+
"dependencies": [],
|
|
1012
|
+
"files": [
|
|
1013
|
+
{
|
|
1014
|
+
"path": "index.ts",
|
|
1015
|
+
"content": "export { default as RebornCard } from './RebornCard.vue'\r\n",
|
|
1016
|
+
"target": "uniapp"
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
"path": "RebornCard.vue",
|
|
1020
|
+
"content": "<script setup lang=\"ts\">\r\nwithDefaults(defineProps<{\r\n title?: string\r\n padding?: boolean\r\n customClass?: string\r\n overflowVisible?: boolean\r\n}>(), {\r\n padding: true,\r\n customClass: '',\r\n overflowVisible: false,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"flex flex-col gap-3\">\r\n <slot name=\"title\">\r\n <view v-if=\"title\" class=\"\r\n text-sm font-medium uppercase tracking-wider text-slate-500\r\n dark:text-slate-200\r\n \">\r\n {{ title }}\r\n </view>\r\n </slot>\r\n <view class=\"\r\n rounded-xl border border-slate-200 bg-white shadow-sm\r\n dark:border-gray-700 dark:bg-gray-800 flex flex-col gap-3\r\n \" :class=\"[padding ? 'p-4' : '', customClass, overflowVisible ? '' : 'overflow-hidden']\">\r\n <slot />\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1021
|
+
"target": "uniapp"
|
|
1022
|
+
}
|
|
1023
|
+
]
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
"name": "reborn-cascader",
|
|
1027
|
+
"dependencies": [],
|
|
1028
|
+
"files": [
|
|
1029
|
+
{
|
|
1030
|
+
"path": "reborn-cascader.config.ts",
|
|
1031
|
+
"content": "\r\nexport const CascaderSizes = ['sm', 'md', 'lg'] as const\r\nexport const CascaderColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport type CascaderSize = (typeof CascaderSizes)[number]\r\nexport type CascaderColor = (typeof CascaderColors)[number]\r\n\r\nexport interface CascaderUI {\r\n root?: string\r\n popup?: string\r\n tabs?: string\r\n tab?: string\r\n tabActive?: string\r\n list?: string\r\n item?: string\r\n itemActive?: string\r\n itemText?: string\r\n itemTextActive?: string\r\n footer?: string\r\n footerText?: string\r\n loading?: string\r\n loadingText?: string\r\n}\r\n\r\n// 选项接口定义\r\nexport interface CascaderOption {\r\n label: string\r\n value: string | number\r\n children?: CascaderOption[]\r\n [key: string]: any\r\n}\r\n\r\nexport interface CascaderProps {\r\n /** 绑定值 */\r\n modelValue?: (string | number)[] | (string | number)[][]\r\n /** 选项数据 */\r\n options?: CascaderOption[]\r\n /** 标题 */\r\n title?: string\r\n /** 占位提示 */\r\n placeholder?: string\r\n /** 是否显示触发器 */\r\n showTrigger?: boolean\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 标签字段 */\r\n labelKey?: string\r\n /** 值字段 */\r\n valueKey?: string\r\n /** 分隔符 */\r\n textSeparator?: string\r\n /** 列表高度 (rpx) */\r\n height?: number | string\r\n /** 尺寸 */\r\n size?: CascaderSize\r\n /** 颜色 */\r\n color?: CascaderColor\r\n /** 自定义类名 */\r\n customClass?: string\r\n /** 自定义样式 */\r\n customStyle?: string\r\n /** UI 覆盖 */\r\n ui?: CascaderUI\r\n /** 是否动态加载子节点 */\r\n lazy?: boolean\r\n /** 加载动态数据的方法 */\r\n lazyLoad?: (node: CascaderOption, resolve: (children: CascaderOption[]) => void, reject: () => void) => void\r\n /** 指定选项的子选项字段 */\r\n childrenKey?: string\r\n /** 查询次级菜单层级;为0时候查询所有 */\r\n leafLevel?: number\r\n /** 是否多选 */\r\n multiple?: boolean\r\n /** 是否显示省略号 */\r\n ellipsis?: boolean\r\n /** 省略号行数 */\r\n lines?: number\r\n}\r\n\r\nconst config = {\r\n slots: {\r\n root: 'w-full',\r\n popup: 'bg-white dark:bg-slate-900 rounded-t-[32rpx] overflow-hidden',\r\n tabs: 'flex flex-row items-center border-b border-gray-100 dark:border-slate-800 p-2 gap-1.5 overflow-x-auto no-scrollbar',\r\n tab: 'shrink-0',\r\n tabActive: '',\r\n list: 'h-[600rpx]',\r\n item: 'flex flex-row items-center justify-between px-[32rpx] py-[24rpx] active:bg-gray-50 dark:active:bg-slate-800 transition-colors',\r\n itemActive: 'bg-primary/5 dark:bg-primary/10',\r\n itemText: 'text-[28rpx]',\r\n itemTextActive: 'text-primary font-medium',\r\n footer: 'flex flex-row items-center justify-between px-[32rpx] py-[24rpx] border-t border-gray-100 dark:border-slate-800',\r\n footerText: 'text-[24rpx] text-gray-500',\r\n loading: 'flex flex-col items-center justify-center h-full w-full',\r\n loadingText: 'mt-2 text-[24rpx] text-gray-400',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n item: 'px-[24rpx] py-[16rpx]',\r\n itemText: 'text-[24rpx]',\r\n },\r\n md: {\r\n item: 'px-[32rpx] py-[24rpx]',\r\n itemText: 'text-[28rpx]',\r\n },\r\n lg: {\r\n item: 'px-[40rpx] py-[32rpx]',\r\n itemText: 'text-[32rpx]',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n itemTextActive: 'text-primary',\r\n itemActive: 'bg-primary/5',\r\n },\r\n secondary: {\r\n itemTextActive: 'text-secondary',\r\n itemActive: 'bg-secondary/5',\r\n },\r\n success: {\r\n itemTextActive: 'text-success',\r\n itemActive: 'bg-success/5',\r\n },\r\n info: {\r\n itemTextActive: 'text-info',\r\n itemActive: 'bg-info/5',\r\n },\r\n warning: {\r\n itemTextActive: 'text-warning',\r\n itemActive: 'bg-warning/5',\r\n },\r\n error: {\r\n itemTextActive: 'text-error',\r\n itemActive: 'bg-error/5',\r\n },\r\n neutral: {\r\n itemTextActive: 'text-slate-900 dark:text-white',\r\n itemActive: 'bg-slate-100 dark:bg-slate-800',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n color: 'primary',\r\n size: 'md',\r\n },\r\n} as const\r\n\r\nexport default config\r\n",
|
|
1032
|
+
"target": "uniapp"
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
"path": "RebornCascader.vue",
|
|
1036
|
+
"content": "<script setup lang=\"ts\">\r\n/**\r\n * RebornCascader 级联选择器\r\n * 用于多层级的数据选择,如省市区、分类层级等。\r\n */\r\nimport { ref, computed, nextTick, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-cascader.config'\r\nimport type { CascaderUI, CascaderColor, CascaderSize, CascaderOption, CascaderProps } from './reborn-cascader.config'\r\nimport RebornSelectTrigger from '../reborn-select-trigger/RebornSelectTrigger.vue'\r\nimport RebornText from '../reborn-text/RebornText.vue'\r\nimport RebornPopup from '../reborn-popup/RebornPopup.vue'\r\nimport RebornBadge from '../reborn-badge/RebornBadge.vue'\r\nimport RebornButton from '../reborn-button/RebornButton.vue'\r\nimport RebornCheckbox from '../reborn-checkbox/RebornCheckbox.vue'\r\nimport RebornLoading from '../reborn-loading/RebornLoading.vue'\r\n\r\ndefineOptions({\r\n name: 'RebornCascader',\r\n})\r\n\r\n\r\n\r\nconst props = withDefaults(defineProps<CascaderProps>(), {\r\n modelValue: () => [],\r\n options: () => [],\r\n title: '请选择',\r\n placeholder: '请选择',\r\n showTrigger: true,\r\n disabled: false,\r\n labelKey: 'label',\r\n valueKey: 'value',\r\n textSeparator: ' / ',\r\n height: 600,\r\n size: 'md',\r\n color: 'primary',\r\n customClass: '',\r\n customStyle: '',\r\n ui: () => ({}),\r\n lazy: false,\r\n childrenKey: 'children',\r\n leafLevel: 0,\r\n multiple: false,\r\n ellipsis: true,\r\n lines: 1,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: (string | number)[] | (string | number)[][]): void\r\n (e: 'change', value: (string | number)[] | (string | number)[][]): void\r\n}>()\r\n\r\n// 样式系统\r\nconst b = tv(theme)\r\n\r\nconst styles = computed(() => b({ color: props.color, size: props.size }))\r\n\r\nconst ui = computed(() => {\r\n const s = styles.value\r\n const u = props.ui || {}\r\n return {\r\n root: (opts?: { class?: any }) => s.root({ class: cn(opts?.class, props.customClass, u.root) }),\r\n popup: (opts?: { class?: any }) => s.popup({ class: cn(opts?.class, u.popup) }),\r\n tabs: (opts?: { class?: any }) => s.tabs({ class: cn(opts?.class, u.tabs) }),\r\n tab: (opts?: { class?: any }) => s.tab({ class: cn(opts?.class, u.tab) }),\r\n list: (opts?: { class?: any }) => s.list({ class: cn(opts?.class, u.list) }),\r\n item: (opts?: { class?: any }) => s.item({ class: cn(opts?.class, u.item) }),\r\n itemActive: (opts?: { class?: any }) => s.itemActive({ class: cn(opts?.class, u.itemActive) }),\r\n itemText: (opts?: { class?: any }) => s.itemText({ class: cn(opts?.class, u.itemText) }),\r\n itemTextActive: (opts?: { class?: any }) => s.itemTextActive({ class: cn(opts?.class, u.itemTextActive) }),\r\n footer: (opts?: { class?: any }) => s.footer({ class: cn(opts?.class, u.footer) }),\r\n footerText: (opts?: { class?: any }) => s.footerText({ class: cn(opts?.class, u.footerText) }),\r\n loading: (opts?: { class?: any }) => s.loading({ class: cn(opts?.class, u.loading) }),\r\n loadingText: (opts?: { class?: any }) => s.loadingText({ class: cn(opts?.class, u.loadingText) }),\r\n }\r\n})\r\n\r\n// 状态\r\nconst visible = ref(false)\r\nconst isRootLoading = ref(false)\r\nconst current = ref(0) // 当前操作的层级索引\r\nconst activePath = ref<(string | number)[]>([]) // 当前显示的路径\r\nconst selectedPaths = ref<(string | number)[][]>([]) // 多选选中的路径集合\r\nconst scrollIntoViewId = ref('') // 水平滚动的目标ID\r\nconst loadingNodes = ref<Record<string, boolean>>({}) // 记录加载状态\r\nconst internalOptions = ref<CascaderOption[]>([])\r\n\r\nwatch(() => props.options, (val) => {\r\n internalOptions.value = JSON.parse(JSON.stringify(val || []))\r\n}, { immediate: true, deep: true })\r\n\r\n// 监听外界 modelValue 变化\r\nwatch(() => props.modelValue, (val) => {\r\n if (props.multiple) {\r\n selectedPaths.value = JSON.parse(JSON.stringify(val || []))\r\n activePath.value = selectedPaths.value[0] ? [...selectedPaths.value[0]] : []\r\n } else {\r\n activePath.value = [...(val as (string | number)[] || [])]\r\n }\r\n}, { immediate: true })\r\n\r\n// 计算属性:各级选项列表\r\nconst lists = computed<CascaderOption[][]>(() => {\r\n let data = internalOptions.value\r\n const result: CascaderOption[][] = [data]\r\n\r\n for (let i = 0; i < activePath.value.length; i++) {\r\n // 如果达到限定层级,不再增加下一级列表\r\n if (props.leafLevel > 0 && i + 1 >= props.leafLevel) break\r\n\r\n const val = activePath.value[i]\r\n const found = data.find(item => item[props.valueKey] === val)\r\n const children = found?.[props.childrenKey]\r\n if (found && children && children.length > 0) {\r\n data = children\r\n result.push(data)\r\n } else {\r\n break\r\n }\r\n }\r\n return result\r\n})\r\n\r\nconst columnWidthClass = computed(() => {\r\n const len = lists.value.filter(l => l && l.length > 0).length\r\n if (len === 1) return 'w-full'\r\n if (len === 2) return 'w-1/2'\r\n return 'w-1/3'\r\n})\r\n\r\n// 计算属性:当前页面的标签\r\nconst labels = computed(() => {\r\n const result: string[] = []\r\n for (let i = 0; i < activePath.value.length; i++) {\r\n const val = activePath.value[i]\r\n const found = lists.value[i]?.find(item => item[props.valueKey] === val)\r\n if (found) {\r\n result.push(found[props.labelKey])\r\n } else {\r\n break\r\n }\r\n }\r\n\r\n // 如果最后一级选完后还有下一级,或者刚开始没选,增加一个“请选择”标签\r\n const lastIndex = result.length - 1\r\n const lastVal = activePath.value[lastIndex]\r\n const lastListData = lists.value[lastIndex]\r\n const lastFound = lastListData?.find(item => item[props.valueKey] === lastVal)\r\n const children = lastFound?.[props.childrenKey]\r\n\r\n // 层级限制判断\r\n const reachedLeafLevel = props.leafLevel > 0 && result.length >= props.leafLevel\r\n\r\n if (!reachedLeafLevel && (!lastVal || (lastFound && ((children && children.length > 0) || props.lazy)))) {\r\n if (result.length < lists.value.length || !lastVal) {\r\n result.push('请选择')\r\n }\r\n }\r\n return result\r\n})\r\n\r\n// 触发器显示的文本\r\nconst triggerText = computed(() => {\r\n if (props.multiple) {\r\n if (selectedPaths.value.length === 0) return ''\r\n return `已选 ${selectedPaths.value.length} 项`\r\n }\r\n\r\n const texts: string[] = []\r\n let data = internalOptions.value\r\n\r\n for (const val of (props.modelValue as (string | number)[])) {\r\n const found = data.find(item => item[props.valueKey] === val)\r\n if (found) {\r\n texts.push(found[props.labelKey])\r\n const children = found[props.childrenKey]\r\n if (children) {\r\n data = children\r\n } else {\r\n break\r\n }\r\n }\r\n }\r\n return texts.join(props.textSeparator)\r\n})\r\n\r\n// 方法\r\nconst open = async () => {\r\n if (props.disabled) return\r\n visible.value = true\r\n\r\n // 懒加载初始化\r\n if (props.lazy && internalOptions.value.length === 0) {\r\n isRootLoading.value = true\r\n try {\r\n const children = await new Promise<CascaderOption[]>((resolve, reject) => {\r\n const rootNode = {\r\n [props.labelKey]: 'root',\r\n [props.valueKey]: 'root',\r\n isRoot: true\r\n } as unknown as CascaderOption\r\n props.lazyLoad?.(rootNode, resolve, reject)\r\n })\r\n internalOptions.value = children\r\n } catch (e) {\r\n console.error('Initial lazy load failed', e)\r\n } finally {\r\n isRootLoading.value = false\r\n }\r\n }\r\n\r\n // 定位到最后一级\r\n if (activePath.value.length > 0) {\r\n current.value = Math.min(activePath.value.length, lists.value.length - 1)\r\n } else {\r\n current.value = 0\r\n }\r\n console.log(activePath.value)\r\n console.log(labels.value)\r\n}\r\n\r\nconst close = () => {\r\n visible.value = false\r\n}\r\n\r\nconst onLabelTap = (index: number) => {\r\n // 点击标签,回到该级并清除后续选择\r\n activePath.value = activePath.value.slice(0, index)\r\n current.value = index\r\n scrollIntoViewId.value = `column-${index}`\r\n}\r\n\r\nconst toggleMultiSelection = (node: CascaderOption, path: (string | number)[], isCheck: boolean) => {\r\n const leafPaths = getLeafPaths(node, path.slice(0, -1))\r\n leafPaths.forEach(p => {\r\n const pStr = JSON.stringify(p)\r\n const index = selectedPaths.value.findIndex(sp => JSON.stringify(sp) === pStr)\r\n if (isCheck && index === -1) {\r\n selectedPaths.value.push(p)\r\n } else if (!isCheck && index > -1) {\r\n selectedPaths.value.splice(index, 1)\r\n }\r\n })\r\n}\r\n\r\nconst getLeafPaths = (node: CascaderOption, parentPath: (string | number)[] = []): (string | number)[][] => {\r\n const currentPath = [...parentPath, node[props.valueKey]]\r\n const children = node[props.childrenKey]\r\n if (!children || children.length === 0) {\r\n return [currentPath]\r\n }\r\n return children.flatMap((child: CascaderOption) => getLeafPaths(child, currentPath))\r\n}\r\n\r\nconst isLeafNode = (item: CascaderOption, listIndex: number) => {\r\n const childrenKey = props.childrenKey\r\n return item.leaf === true ||\r\n (!props.lazy && (!item[childrenKey] || item[childrenKey].length === 0)) ||\r\n (props.leafLevel > 0 && listIndex + 1 >= props.leafLevel)\r\n}\r\n\r\nconst onItemTap = async (item: CascaderOption, listIndex: number, fromCheckbox = false) => {\r\n if (props.disabled) return\r\n const val = item[props.valueKey]\r\n const childrenKey = props.childrenKey\r\n const isLeaf = isLeafNode(item, listIndex)\r\n\r\n // 更新当前路径\r\n const newPath = activePath.value.slice(0, listIndex)\r\n newPath.push(val)\r\n activePath.value = newPath\r\n current.value = listIndex\r\n\r\n if (props.multiple && fromCheckbox) {\r\n // 多选模式下点击复选框:父子全选逻辑\r\n const currentlyChecked = isItemSelected(item, listIndex)\r\n toggleMultiSelection(item, newPath, !currentlyChecked)\r\n return\r\n }\r\n\r\n if (isLeaf) {\r\n if (props.multiple) {\r\n // 多选点击叶子:切换选中状态\r\n toggleMultiSelection(item, newPath, !isItemSelected(item, listIndex))\r\n } else {\r\n // 单选:直接提交\r\n emit('update:modelValue', activePath.value)\r\n emit('change', activePath.value)\r\n close()\r\n }\r\n } else {\r\n // 需要加载子节点或者显示下一级\r\n if (props.lazy && !item.leaf && (!item[childrenKey] || item[childrenKey].length === 0)) {\r\n const nodeId = String(val)\r\n loadingNodes.value[nodeId] = true\r\n try {\r\n const children = await new Promise<CascaderOption[]>((resolve, reject) => {\r\n props.lazyLoad?.(item, resolve, reject)\r\n })\r\n item[childrenKey] = children\r\n } catch (e) {\r\n console.error('Lazy load failed', e)\r\n } finally {\r\n loadingNodes.value[nodeId] = false\r\n }\r\n }\r\n // 自动滚动到新的一级\r\n nextTick(() => {\r\n scrollIntoViewId.value = `column-${listIndex + 1}`\r\n })\r\n }\r\n}\r\n\r\n\r\nconst onConfirm = () => {\r\n emit('update:modelValue', selectedPaths.value)\r\n emit('change', selectedPaths.value)\r\n close()\r\n}\r\n\r\nconst clear = () => {\r\n activePath.value = []\r\n selectedPaths.value = []\r\n current.value = 0\r\n emit('update:modelValue', props.multiple ? [] : [])\r\n emit('change', props.multiple ? [] : [])\r\n}\r\n\r\nconst isItemSelected = (item: CascaderOption, listIndex: number) => {\r\n const path = [...activePath.value.slice(0, listIndex), item[props.valueKey]]\r\n if (props.multiple) {\r\n const leaves = getLeafPaths(item, activePath.value.slice(0, listIndex))\r\n return leaves.every(p => {\r\n const pStr = JSON.stringify(p)\r\n return selectedPaths.value.some(sp => JSON.stringify(sp) === pStr)\r\n })\r\n }\r\n return activePath.value[listIndex] === item[props.valueKey]\r\n}\r\n\r\nconst isItemIndeterminate = (item: CascaderOption, listIndex: number) => {\r\n if (!props.multiple) return false\r\n const leaves = getLeafPaths(item, activePath.value.slice(0, listIndex))\r\n if (leaves.length <= 1) return false\r\n\r\n const selectedCount = leaves.filter(p => {\r\n const pStr = JSON.stringify(p)\r\n return selectedPaths.value.some(sp => JSON.stringify(sp) === pStr)\r\n }).length\r\n\r\n return selectedCount > 0 && selectedCount < leaves.length\r\n}\r\n\r\ndefineExpose({ open, close, clear })\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root()\" :style=\"customStyle\">\r\n <RebornSelectTrigger v-if=\"showTrigger\" :text=\"triggerText\" :placeholder=\"placeholder\" :disabled=\"disabled\"\r\n :focus=\"visible\" :color=\"color\" :size=\"size\" @open=\"open\" @clear=\"clear\" />\r\n\r\n <RebornPopup v-model=\"visible\" :title=\"title\" position=\"bottom\" round @close=\"close\" :color=\"color\" rootPortal>\r\n <view :class=\"ui.popup()\">\r\n <!-- 顶部标签页 -->\r\n <view v-if=\"!multiple\" :class=\"ui.tabs()\">\r\n <view v-for=\"(label, index) in labels\" :key=\"index\" :class=\"ui.tab()\" @tap=\"onLabelTap(index)\">\r\n <slot name=\"tabs\" :label=\"label\" :index=\"index\" :current=\"current\">\r\n <RebornBadge variant=\"subtle\" :color=\"index === current ? color : 'neutral'\" :size=\"size\">\r\n <template #default>\r\n {{ label }}\r\n </template>\r\n </RebornBadge>\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <!-- 列表内容 -->\r\n <view :class=\"ui.list()\" :style=\"{ height: typeof height === 'number' ? height + 'rpx' : height }\">\r\n <view v-if=\"isRootLoading\" :class=\"ui.loading()\">\r\n <RebornLoading type=\"spinner\" :color=\"color\" size=\"64rpx\" />\r\n <text :class=\"ui.loadingText()\">正在加载数据...</text>\r\n </view>\r\n <scroll-view v-else scroll-x class=\"w-full h-full no-scrollbar\" :scroll-into-view=\"scrollIntoViewId\"\r\n scroll-with-animation>\r\n <view class=\"flex flex-row h-full min-w-full\">\r\n <view v-for=\"(listData, listIndex) in lists\" :key=\"listIndex\" :id=\"`column-${listIndex}`\"\r\n :class=\"[\r\n columnWidthClass,\r\n 'shrink-0 h-full border-r border-gray-100 dark:border-slate-800 last:border-r-0'\r\n ]\">\r\n <scroll-view scroll-y class=\"h-full\">\r\n <view v-for=\"(item, itemIndex) in listData\" :key=\"`${listIndex}-${item[valueKey]}`\"\r\n :class=\"[\r\n ui.item(),\r\n activePath[listIndex] === item[valueKey] ? ui.itemActive() : ''\r\n ]\" @tap=\"onItemTap(item, listIndex)\">\r\n <view class=\"flex flex-row items-center\">\r\n <view v-if=\"props.multiple\" class=\"mr-2\"\r\n @tap.stop=\"onItemTap(item, listIndex, true)\">\r\n <RebornCheckbox :modelValue=\"isItemSelected(item, listIndex)\"\r\n :indeterminate=\"isItemIndeterminate(item, listIndex)\" />\r\n </view>\r\n <slot name=\"item\" :item=\"item\" :listIndex=\"listIndex\"\r\n :active=\"activePath[listIndex] === item[valueKey]\">\r\n <RebornText\r\n :custom-class=\"activePath[listIndex] === item[valueKey] ? ui.itemTextActive() : ui.itemText()\"\r\n :ellipsis=\"ellipsis\" :lines=\"lines\">\r\n {{ item[labelKey] }}\r\n </RebornText>\r\n </slot>\r\n </view>\r\n <view v-if=\"loadingNodes[String(item[valueKey])]\"\r\n class=\"ml-auto h-[32rpx] w-[32rpx]\">\r\n <RebornLoading type=\"outline\" :color=\"color\" size=\"32rpx\" />\r\n </view>\r\n <view v-else-if=\"!isLeafNode(item, listIndex)\"\r\n class=\"i-lucide-chevron-right text-[32rpx] text-gray-300 ml-auto\" />\r\n </view>\r\n </scroll-view>\r\n </view>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n\r\n <!-- 底部操作栏 (多选模式) -->\r\n <view v-if=\"props.multiple\" :class=\"ui.footer()\">\r\n <text :class=\"ui.footerText()\">已选 {{ selectedPaths.length }} 项</text>\r\n <view class=\"flex flex-row gap-2\">\r\n <RebornButton size=\"sm\" variant=\"outline\" @tap=\"clear\">清空</RebornButton>\r\n <RebornButton size=\"sm\" @tap=\"onConfirm\">确认</RebornButton>\r\n </view>\r\n </view>\r\n </view>\r\n </RebornPopup>\r\n </view>\r\n</template>\r\n",
|
|
989
1037
|
"target": "uniapp"
|
|
990
1038
|
}
|
|
991
1039
|
]
|
|
@@ -1013,17 +1061,17 @@
|
|
|
1013
1061
|
},
|
|
1014
1062
|
{
|
|
1015
1063
|
"path": "index.ts",
|
|
1016
|
-
"content": "export { default as RebornCheckbox } from
|
|
1064
|
+
"content": "export { default as RebornCheckbox } from './RebornCheckbox.vue'\r\n",
|
|
1017
1065
|
"target": "uniapp"
|
|
1018
1066
|
},
|
|
1019
1067
|
{
|
|
1020
1068
|
"path": "reborn-checkbox.config.ts",
|
|
1021
|
-
"content": "const size = [
|
|
1069
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as checkboxColors, size as checkboxSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper: 'group inline-flex items-center gap-3 cursor-pointer select-none',\r\n input: 'sr-only',\r\n control:\r\n 'flex items-center justify-center rounded-md border border-gray-4 bg-white text-white transition-colors ring-1 ring-transparent group-[.is-disabled]:cursor-not-allowed group-[.is-disabled]:bg-gray-2 group-[.is-disabled]:border-gray-3',\r\n icon: 'size-4 opacity-0 scale-75 transition-all group-[.is-checked]:opacity-100 group-[.is-checked]:scale-100',\r\n label: 'text-gray-8 dark:text-gray-2',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n control: 'size-4',\r\n label: 'text-[length:var(--text-size-24)]',\r\n },\r\n md: {\r\n control: 'size-5',\r\n label: 'text-[length:var(--text-size-26)]',\r\n },\r\n lg: {\r\n control: 'size-6',\r\n label: 'text-[length:var(--text-size-28)]',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n control: 'group-[.is-checked]:bg-primary group-[.is-checked]:border-primary',\r\n icon: 'text-white',\r\n },\r\n secondary: {\r\n control: 'group-[.is-checked]:bg-secondary group-[.is-checked]:border-secondary',\r\n icon: 'text-white',\r\n },\r\n success: {\r\n control: 'group-[.is-checked]:bg-success group-[.is-checked]:border-success',\r\n icon: 'text-white',\r\n },\r\n info: {\r\n control: 'group-[.is-checked]:bg-info group-[.is-checked]:border-info',\r\n icon: 'text-white',\r\n },\r\n warning: {\r\n control: 'group-[.is-checked]:bg-warning group-[.is-checked]:border-warning',\r\n icon: 'text-white',\r\n },\r\n error: {\r\n control: 'group-[.is-checked]:bg-error group-[.is-checked]:border-error',\r\n icon: 'text-white',\r\n },\r\n neutral: {\r\n control: 'group-[.is-checked]:bg-neutral group-[.is-checked]:border-neutral',\r\n icon: 'text-white',\r\n },\r\n },\r\n error: {\r\n true: {\r\n control: 'border-error',\r\n label: 'text-error',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md' as (typeof size)[number],\r\n color: 'primary' as (typeof color)[number],\r\n },\r\n}\r\n",
|
|
1022
1070
|
"target": "uniapp"
|
|
1023
1071
|
},
|
|
1024
1072
|
{
|
|
1025
1073
|
"path": "RebornCheckbox.vue",
|
|
1026
|
-
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, useAttrs, watch } from
|
|
1074
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { checkboxColors, checkboxSizes } from './reborn-checkbox.config'\r\nimport { computed, ref, useAttrs, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-checkbox.config'\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n})\r\n\r\nconst props = withDefaults(defineProps<CheckboxProps>(), {\r\n disabled: false,\r\n size: 'md',\r\n color: 'primary',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: boolean | CheckboxValue[]): void\r\n}>()\r\n\r\nconst b = tv(theme)\r\n\r\nexport type CheckboxValue = string | number\r\n\r\nexport interface CheckboxProps {\r\n modelValue?: boolean | CheckboxValue[]\r\n defaultValue?: boolean | CheckboxValue[]\r\n value?: CheckboxValue\r\n label?: string\r\n disabled?: boolean\r\n size?: typeof checkboxSizes[number]\r\n color?: typeof checkboxColors[number]\r\n customClass?: any\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n input: ClassValue\r\n control: ClassValue\r\n icon: ClassValue\r\n label: ClassValue\r\n }>\r\n}\r\n\r\nconst { size: fieldGroupSize, disabled: fieldGroupDisabled, isError, validate } = useFormInject(props)\r\n\r\nconst localValue = ref<boolean | CheckboxValue[]>(props.defaultValue ?? false)\r\nconst currentValue = computed(() => (props.modelValue !== undefined ? props.modelValue : localValue.value))\r\nconst optionValue = computed<CheckboxValue>(() => props.value ?? props.label ?? '')\r\n\r\nconst isChecked = computed(() => {\r\n if (Array.isArray(currentValue.value)) {\r\n return currentValue.value.includes(optionValue.value)\r\n }\r\n\r\n return Boolean(currentValue.value)\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size || fieldGroupSize.value,\r\n color: props.color,\r\n error: isError.value,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(uiOverrides.value.wrapper, opts?.class) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n control: (opts?: { class?: any }) => styles.control({ class: cn(opts?.class, uiOverrides.value.control) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n }\r\n})\r\n\r\nfunction updateValue(nextValue: boolean | CheckboxValue[]) {\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue\r\n }\r\n emit('update:modelValue', nextValue)\r\n if (validate) { validate('change') }\r\n}\r\n\r\nfunction toggle() {\r\n if (props.disabled) { return }\r\n\r\n if (Array.isArray(currentValue.value)) {\r\n const next = new Set(currentValue.value)\r\n if (next.has(optionValue.value)) {\r\n next.delete(optionValue.value)\r\n }\r\n else {\r\n next.add(optionValue.value)\r\n }\r\n updateValue(Array.from(next))\r\n }\r\n else {\r\n updateValue(!isChecked.value)\r\n }\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value) => {\r\n if (value !== undefined) {\r\n localValue.value = value\r\n }\r\n },\r\n)\r\n</script>\r\n\r\n<template>\r\n <view :class=\"cn(ui.wrapper({ class: customClass }), isChecked && 'is-checked', fieldGroupDisabled && `\r\n is-disabled\r\n `)\" :data-disabled=\"fieldGroupDisabled\" hover-class=\"none\" style=\"-webkit-tap-highlight-color: transparent;\"\r\n @tap=\"toggle\">\r\n <view :class=\"ui.control()\">\r\n <slot name=\"icon\" :checked=\"isChecked\">\r\n <view class=\"i-lucide-check\" :class=\"ui.icon()\" />\r\n </slot>\r\n </view>\r\n\r\n <view v-if=\"props.label || $slots.default\" :class=\"ui.label()\">\r\n <slot>{{ props.label }}</slot>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1027
1075
|
"target": "uniapp"
|
|
1028
1076
|
}
|
|
1029
1077
|
]
|
|
@@ -1034,7 +1082,8 @@
|
|
|
1034
1082
|
"files": [
|
|
1035
1083
|
{
|
|
1036
1084
|
"path": "index.ts",
|
|
1037
|
-
"content": "export { default as RebornChip } from './RebornChip.vue'\r\n"
|
|
1085
|
+
"content": "export { default as RebornChip } from './RebornChip.vue'\r\n",
|
|
1086
|
+
"target": "web"
|
|
1038
1087
|
},
|
|
1039
1088
|
{
|
|
1040
1089
|
"path": "reborn-chip.config.ts",
|
|
@@ -1046,14 +1095,19 @@
|
|
|
1046
1095
|
"content": "<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport theme from './reborn-chip.config'\r\nimport { tv } from '~/lib/tv'\r\n\r\nconst b = tv(theme)\r\n\r\ntype Theme = typeof theme\r\n\r\ntype ChipColor = keyof Theme['variants']['color']\r\ntype ChipSize = keyof Theme['variants']['size']\r\ntype ChipPosition = keyof Theme['variants']['position']\r\n\r\nexport interface ChipProps {\r\n color?: ChipColor | (string & {})\r\n size?: ChipSize | (string & {})\r\n text?: string | number\r\n position?: ChipPosition | (string & {})\r\n show?: boolean\r\n inset?: boolean\r\n standalone?: boolean\r\n class?: any\r\n ui?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<ChipProps>(), {\r\n color: 'primary',\r\n size: 'md',\r\n position: 'top-right',\r\n show: true,\r\n inset: false,\r\n standalone: false\r\n})\r\n\r\nconst emit = defineEmits<{\r\n 'update:show': [value: boolean]\r\n}>()\r\n\r\nconst show = computed({\r\n get: () => props.show,\r\n set: (value) => emit('update:show', value)\r\n})\r\n\r\nconst ui = computed(() => b({\r\n color: props.color as ChipColor,\r\n size: props.size as ChipSize,\r\n position: props.position as ChipPosition,\r\n inset: props.inset,\r\n standalone: props.standalone\r\n}))\r\n</script>\r\n\r\n<template>\r\n <span :class=\"ui.root({ class: props.class })\">\r\n <slot />\r\n <div v-if=\"show\" :class=\"ui.base()\">\r\n <div v-if=\"props.text\" :class=\"ui.label()\">\r\n {{ props.text }}\r\n </div>\r\n </div>\r\n </span>\r\n</template>\r\n",
|
|
1047
1096
|
"target": "web"
|
|
1048
1097
|
},
|
|
1098
|
+
{
|
|
1099
|
+
"path": "index.ts",
|
|
1100
|
+
"content": "export { default as RebornChip } from './RebornChip.vue'\r\n",
|
|
1101
|
+
"target": "uniapp"
|
|
1102
|
+
},
|
|
1049
1103
|
{
|
|
1050
1104
|
"path": "reborn-chip.config.ts",
|
|
1051
|
-
"content": "const size = ['3xs', '2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst position = ['top-right', 'bottom-right', 'top-left', 'bottom-left'] as const\r\n\r\nexport {
|
|
1105
|
+
"content": "const size = ['3xs', '2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl', '3xl'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst position = ['top-right', 'bottom-right', 'top-left', 'bottom-left'] as const\r\n\r\nexport { color as chipColors, position as chipPositions, size as chipSizes }\r\n\r\nexport default {\r\n slots: {\r\n root: 'relative inline-flex items-center justify-center shrink-0 h-fit leading-none',\r\n base: 'absolute rounded-full flex items-center justify-center font-medium whitespace-nowrap ring-1',\r\n label: 'text-white',\r\n },\r\n variants: {\r\n color: {\r\n primary: { base: 'bg-primary ring-primary' },\r\n secondary: { base: 'bg-secondary ring-secondary' },\r\n success: { base: 'bg-success ring-success' },\r\n info: { base: 'bg-info ring-info' },\r\n warning: { base: 'bg-warning ring-warning' },\r\n error: { base: 'bg-error ring-error' },\r\n neutral: { base: 'bg-neutral ring-neutral' },\r\n },\r\n size: {\r\n '3xs': { base: 'h-[8rpx] min-w-[8rpx]', label: 'text-[length:8rpx]' },\r\n '2xs': { base: 'h-[10rpx] min-w-[10rpx]', label: 'text-[length:10rpx]' },\r\n 'xs': { base: 'h-[12rpx] min-w-[12rpx]', label: 'text-[length:12rpx]' },\r\n 'sm': { base: 'h-[14rpx] min-w-[14rpx]', label: 'text-[length:14rpx]' },\r\n 'md': { base: 'h-[16rpx] min-w-[16rpx]', label: 'text-[length:16rpx]' },\r\n 'lg': { base: 'h-[18rpx] min-w-[18rpx]', label: 'text-[length:18rpx]' },\r\n 'xl': { base: 'h-[20rpx] min-w-[20rpx]', label: 'text-[length:20rpx]' },\r\n '2xl': { base: 'h-[22rpx] min-w-[22rpx]', label: 'text-[length:22rpx]' },\r\n '3xl': { base: 'h-[24rpx] min-w-[24rpx]', label: 'text-[length:24rpx]' },\r\n },\r\n position: {\r\n 'top-right': { base: 'top-0 right-0' },\r\n 'bottom-right': { base: 'bottom-0 right-0' },\r\n 'top-left': { base: 'top-0 left-0' },\r\n 'bottom-left': { base: 'bottom-0 left-0' },\r\n },\r\n inset: {\r\n true: { base: '' },\r\n false: { base: '' },\r\n },\r\n standalone: {\r\n true: { base: 'absolute' },\r\n false: { base: '' },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n inset: false,\r\n position: 'top-right' as const,\r\n class: { base: '-translate-y-1/2 translate-x-1/2 transform' },\r\n },\r\n {\r\n inset: false,\r\n position: 'bottom-right' as const,\r\n class: { base: 'translate-y-1/2 translate-x-1/2 transform' },\r\n },\r\n {\r\n inset: false,\r\n position: 'top-left' as const,\r\n class: { base: '-translate-y-1/2 -translate-x-1/2 transform' },\r\n },\r\n {\r\n inset: false,\r\n position: 'bottom-left' as const,\r\n class: { base: 'translate-y-1/2 -translate-x-1/2 transform' },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as const,\r\n color: 'primary' as const,\r\n position: 'top-right' as const,\r\n inset: false,\r\n standalone: false,\r\n },\r\n}\r\n",
|
|
1052
1106
|
"target": "uniapp"
|
|
1053
1107
|
},
|
|
1054
1108
|
{
|
|
1055
1109
|
"path": "RebornChip.vue",
|
|
1056
|
-
"content": "<script setup lang=\"ts\">\r\nimport {
|
|
1110
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { chipColors, chipPositions, chipSizes } from './reborn-chip.config'\r\nimport { computed } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-chip.config'\r\n\r\ndefineOptions({\r\n name: 'RebornChip',\r\n inheritAttrs: false,\r\n})\r\n\r\nconst props = withDefaults(defineProps<ChipProps>(), {\r\n color: 'primary',\r\n size: 'md',\r\n position: 'top-right',\r\n show: true,\r\n inset: false,\r\n standalone: false,\r\n})\r\n\r\nconst emit = defineEmits(['update:show'])\r\n\r\nconst b = tv(theme)\r\n\r\ntype Theme = typeof theme\r\ntype ChipColor = keyof Theme['variants']['color']\r\ntype ChipSize = keyof Theme['variants']['size']\r\ntype ChipPosition = keyof Theme['variants']['position']\r\n\r\nexport interface ChipProps {\r\n color?: typeof chipColors[number]\r\n size?: typeof chipSizes[number]\r\n text?: string | number\r\n position?: typeof chipPositions[number]\r\n show?: boolean\r\n inset?: boolean\r\n standalone?: boolean\r\n customClass?: any\r\n}\r\n\r\nconst show = computed({\r\n get: () => props.show,\r\n set: (value: boolean) => emit('update:show', value),\r\n})\r\n\r\nconst ui = computed(() => b({\r\n color: props.color as ChipColor,\r\n size: props.size as ChipSize,\r\n position: props.position as ChipPosition,\r\n inset: props.inset,\r\n standalone: props.standalone,\r\n}))\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(props.customClass) })\">\r\n <slot />\r\n <view v-if=\"show\" :class=\"ui.base()\">\r\n <text v-if=\"props.text\" :class=\"ui.label()\">{{ props.text }}</text>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped></style>\r\n",
|
|
1057
1111
|
"target": "uniapp"
|
|
1058
1112
|
}
|
|
1059
1113
|
]
|
|
@@ -1064,7 +1118,8 @@
|
|
|
1064
1118
|
"files": [
|
|
1065
1119
|
{
|
|
1066
1120
|
"path": "index.ts",
|
|
1067
|
-
"content": "export { default as RebornCollapse } from './RebornCollapse.vue'\r\nexport { default as rebornCollapseConfig } from './reborn-collapse.config'\r\n"
|
|
1121
|
+
"content": "export { default as RebornCollapse } from './RebornCollapse.vue'\r\nexport { default as rebornCollapseConfig } from './reborn-collapse.config'\r\n",
|
|
1122
|
+
"target": "web"
|
|
1068
1123
|
},
|
|
1069
1124
|
{
|
|
1070
1125
|
"path": "reborn-collapse.config.ts",
|
|
@@ -1076,14 +1131,166 @@
|
|
|
1076
1131
|
"content": "<script lang=\"ts\">\r\nexport interface RebornCollapseProps {\r\n customClass?: any\r\n ui?: {\r\n root?: any\r\n trigger?: any\r\n content?: any\r\n }\r\n}\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, ref, watch, onMounted, nextTick } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\nimport { tv } from '@/lib/tv'\r\nimport theme from './reborn-collapse.config'\r\n\r\nconst props = withDefaults(defineProps<RebornCollapseProps>(), {\r\n customClass: '',\r\n ui: () => ({})\r\n})\r\n\r\nconst collapse = defineModel(\"modelValue\", {\r\n type: Boolean,\r\n default: false\r\n})\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => b())\r\nconst overrides = computed(() => props.ui || {})\r\n\r\nconst contentRef = ref<HTMLElement | null>(null)\r\nconst height = ref(0)\r\nconst isOpened = ref(false)\r\n\r\nfunction updateHeight() {\r\n if (contentRef.value) {\r\n height.value = contentRef.value.scrollHeight\r\n }\r\n}\r\n\r\nfunction show() {\r\n isOpened.value = true\r\n // Wait for display:block (if inherent) or just ensuring Ref is ready\r\n nextTick(() => {\r\n updateHeight()\r\n })\r\n}\r\n\r\nfunction hide() {\r\n isOpened.value = false\r\n height.value = 0\r\n}\r\n\r\nfunction toggle() {\r\n if (isOpened.value) {\r\n hide()\r\n } else {\r\n show()\r\n }\r\n}\r\n\r\nwatch(\r\n () => collapse.value,\r\n (val) => {\r\n if (val) show()\r\n else hide()\r\n },\r\n { immediate: true }\r\n)\r\n\r\n// Optional: ResizeObserver to update height if content changes while open\r\nonMounted(() => {\r\n if (contentRef.value) {\r\n const resizeObserver = new ResizeObserver(() => {\r\n if (isOpened.value) {\r\n updateHeight()\r\n }\r\n })\r\n resizeObserver.observe(contentRef.value)\r\n }\r\n})\r\n\r\ndefineExpose({\r\n show,\r\n hide,\r\n toggle,\r\n resize: updateHeight\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: cn(props.customClass, overrides?.root) })\">\r\n <div @click=\"toggle\">\r\n <slot :open=\"isOpened\" />\r\n </div>\r\n <div :class=\"ui.trigger({ class: overrides?.trigger })\" :style=\"{ height: isOpened ? height + 'px' : '0px' }\">\r\n <div ref=\"contentRef\" class=\"reborn-collapse__content\" :class=\"ui.content({ class: overrides?.content })\">\r\n <slot name=\"content\"></slot>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n",
|
|
1077
1132
|
"target": "web"
|
|
1078
1133
|
},
|
|
1134
|
+
{
|
|
1135
|
+
"path": "index.ts",
|
|
1136
|
+
"content": "export { default as RebornCollapse } from './RebornCollapse.vue'\r\n",
|
|
1137
|
+
"target": "uniapp"
|
|
1138
|
+
},
|
|
1079
1139
|
{
|
|
1080
1140
|
"path": "reborn-collapse.config.ts",
|
|
1081
|
-
"content": "export default {\r\n
|
|
1141
|
+
"content": "export default {\r\n slots: {\r\n root: '',\r\n trigger: 'relative overflow-hidden transition-[height] duration-300 ease-in-out',\r\n content: 'absolute top-0 left-0 w-full',\r\n },\r\n}\r\n",
|
|
1082
1142
|
"target": "uniapp"
|
|
1083
1143
|
},
|
|
1084
1144
|
{
|
|
1085
1145
|
"path": "RebornCollapse.vue",
|
|
1086
|
-
"content": "<script lang=\"ts\">\r\
|
|
1146
|
+
"content": "<script lang=\"ts\">\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-collapse.config'\r\n\r\nexport interface RebornCollapseProps {\r\n customClass?: any\r\n ui?: {\r\n root?: any\r\n trigger?: any\r\n content?: any\r\n }\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornCollapseProps>(), {\r\n customClass: '',\r\n ui: () => ({}),\r\n})\r\n\r\nconst collapse = defineModel('modelValue', {\r\n type: Boolean,\r\n default: false,\r\n})\r\n\r\n// 获取组件实例\r\nconst { proxy } = getCurrentInstance()!\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => b())\r\nconst overrides = computed(() => props.ui || {})\r\n\r\n// 折叠展开状态\r\nconst isOpened = ref(false)\r\n// 内容高度\r\nconst height = ref(0)\r\n\r\n/**\r\n * 显示折叠内容\r\n */\r\nfunction show() {\r\n isOpened.value = true\r\n updateHeight()\r\n}\r\n\r\n/**\r\n * 隐藏折叠内容\r\n */\r\nfunction hide() {\r\n isOpened.value = false\r\n height.value = 0\r\n}\r\n\r\n/**\r\n * 更新高度\r\n */\r\nfunction updateHeight() {\r\n // 使用 nextTick 确保 DOM 更新后再获取\r\n // Use setTimeout as simple generic nextTick for uni-app sometimes nextTick is not enough for layout\r\n setTimeout(() => {\r\n const query = uni.createSelectorQuery().in(proxy)\r\n query.select('.reborn-collapse__content')\r\n .boundingClientRect((data) => {\r\n const node = data as UniApp.NodeInfo\r\n if (node && node.height !== undefined) {\r\n height.value = node.height\r\n }\r\n })\r\n .exec()\r\n }, 50)\r\n}\r\n\r\n/**\r\n * 切换折叠状态\r\n */\r\nfunction toggle() {\r\n if (isOpened.value) {\r\n hide()\r\n } else {\r\n show()\r\n }\r\n}\r\n\r\n// 监听折叠状态变化\r\nwatch(\r\n () => collapse.value,\r\n (val: boolean) => {\r\n if (val) {\r\n show()\r\n } else {\r\n hide()\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\ndefineExpose({\r\n show,\r\n hide,\r\n toggle,\r\n resize: updateHeight,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(props.customClass, overrides?.root) })\">\r\n <view @click=\"toggle\">\r\n <slot :open=\"isOpened\" />\r\n </view>\r\n <view :class=\"ui.trigger({ class: overrides?.trigger })\" :style=\"{ height: isOpened ? `${height}px` : '0px' }\">\r\n <view class=\"reborn-collapse__content\" :class=\"ui.content({ class: overrides?.content })\">\r\n <slot name=\"content\" />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1147
|
+
"target": "uniapp"
|
|
1148
|
+
}
|
|
1149
|
+
]
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
"name": "reborn-color-picker",
|
|
1153
|
+
"dependencies": [
|
|
1154
|
+
"@uiw/color-convert"
|
|
1155
|
+
],
|
|
1156
|
+
"files": [
|
|
1157
|
+
{
|
|
1158
|
+
"path": "reborn-color-picker-panel.config.ts",
|
|
1159
|
+
"content": "export default {\r\n slots: {\r\n root: \"w-64 space-y-4 p-3 bg-white dark:bg-gray-900 rounded-xl border border-gray-100 dark:border-gray-800 shadow-sm\",\r\n saturation: \"relative w-full aspect-video rounded-lg cursor-crosshair overflow-hidden ring-1 ring-inset ring-black/5\",\r\n saturationCursor: \"absolute w-4 h-4 -ml-2 -mt-2 rounded-full border-2 border-white shadow-sm pointer-events-none\",\r\n controls: \"flex gap-3 items-center\",\r\n preview: \"size-10 rounded-lg shadow-inner ring-1 ring-inset ring-black/5 shrink-0\",\r\n sliders: \"flex-1 space-y-2\",\r\n hueSlider: \"relative h-3 w-full rounded-full cursor-pointer ring-1 ring-inset ring-black/5\",\r\n hueCursor: \"absolute h-4 w-4 -mt-0.5 -ml-2 bg-white rounded-full shadow-md border border-gray-100 pointer-events-none\",\r\n alphaSlider: \"relative h-3 w-full rounded-full cursor-pointer ring-1 ring-inset ring-black/5\",\r\n alphaCursor: \"absolute h-4 w-4 -mt-0.5 -ml-2 bg-white rounded-full shadow-md border border-gray-100 pointer-events-none\",\r\n inputs: \"space-y-2\",\r\n formatToggles: \"flex gap-1\",\r\n input: \"w-full text-xs font-mono\",\r\n presets: \"pt-3 border-t border-gray-100 dark:border-gray-800\",\r\n presetTitle: \"text-[10px] text-gray-400 font-bold mb-2 uppercase tracking-tight\",\r\n presetGrid: \"grid grid-cols-10 gap-1.5\",\r\n presetSwatch: \"aspect-square ring-1 ring-black/5 hover:scale-110 transition-transform p-0!\"\r\n }\r\n}\r\n",
|
|
1160
|
+
"target": "web"
|
|
1161
|
+
},
|
|
1162
|
+
{
|
|
1163
|
+
"path": "reborn-color-picker.config.ts",
|
|
1164
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\n\r\nexport { size as colorPickerSizes }\r\nexport default {\r\n slots: {\r\n root: \"ring ring-1 ring-gray-5 p-1 rounded-md cursor-pointer select-none hover:scale-105 transition-all\",\r\n base: \"rounded-md ring ring-1 ring-gray-5 w-full h-full flex justify-center items-center\",\r\n icon: \"text-white transition-transform duration-200\",\r\n },\r\n variants: {\r\n disabled: {\r\n true: {\r\n root: \"cursor-not-allowed opacity-50\",\r\n },\r\n },\r\n open: {\r\n true: {\r\n icon: \"rotate-180\",\r\n },\r\n },\r\n size: {\r\n sm: {\r\n root: \"h-[var(--button-sm-height)] w-[var(--button-sm-height)]\",\r\n },\r\n md: {\r\n root: \"h-[var(--button-base-height)] w-[var(--button-base-height)]\",\r\n },\r\n lg: {\r\n root: \"h-[var(--button-lg-height)] w-[var(--button-lg-height)]\",\r\n },\r\n },\r\n },\r\n}\r\n",
|
|
1165
|
+
"target": "web"
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
"path": "RebornColorPicker.vue",
|
|
1169
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed } from 'vue'\r\nimport theme from './reborn-color-picker.config'\r\nimport { tv } from '~/lib/tv'\r\nimport { cn } from '~/lib/utils'\r\nimport RebornButton from '../reborn-button/RebornButton.vue'\r\nimport RebornColorPickerPanel from './RebornColorPickerPanel.vue'\r\n\r\ninterface Props {\r\n modelValue?: string\r\n disabled?: boolean\r\n size?: 'xs' | 'sm' | 'md' | 'lg',\r\n ui?: {\r\n root?: string\r\n base?: string\r\n icon?: string\r\n }\r\n}\r\n\r\nconst open = ref(false)\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n modelValue: '#000000',\r\n disabled: false,\r\n size: 'md',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: string): void\r\n}>()\r\n\r\n// --- 颜色值计算属性 ---\r\nconst colorValue = computed({\r\n get: () => props.modelValue!,\r\n set: (val: string) => emit('update:modelValue', val),\r\n})\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n disabled: props.disabled,\r\n open: open.value,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <RebornPopover v-model:open=\"open\" :disabled=\"disabled\" arrow>\r\n <!-- 触发器插槽 -->\r\n <slot>\r\n <div :class=\"ui.root()\">\r\n <div :class=\"ui.base()\" :style=\"{ backgroundColor: colorValue }\">\r\n <Icon name=\"lucide:chevron-down\" :class=\"ui.icon()\" />\r\n </div>\r\n </div>\r\n </slot>\r\n\r\n <!-- 颜色选择面板 -->\r\n <template #content>\r\n <RebornColorPickerPanel v-model=\"colorValue\" />\r\n </template>\r\n </RebornPopover>\r\n</template>\r\n",
|
|
1170
|
+
"target": "web"
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
"path": "RebornColorPickerPanel.vue",
|
|
1174
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, watch } from \"vue\";\r\nimport {\r\n hexToHsva,\r\n hsvaToHex,\r\n hsvaToRgba,\r\n rgbaToHsva,\r\n} from \"@uiw/color-convert\";\r\nimport type { HsvaColor } from \"@uiw/color-convert\";\r\nimport { tv } from \"~/lib/tv\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme from \"./reborn-color-picker-panel.config\";\r\nimport RebornButton from \"../reborn-button/RebornButton.vue\";\r\nimport RebornInput from \"../reborn-input/RebornInput.vue\";\r\n\r\ninterface Props {\r\n modelValue?: string;\r\n class?: any;\r\n ui?: {\r\n root?: string\r\n saturation?: string\r\n saturationCursor?: string\r\n controls?: string\r\n preview?: string\r\n sliders?: string\r\n hueSlider?: string\r\n hueCursor?: string\r\n alphaSlider?: string\r\n alphaCursor?: string\r\n inputs?: string\r\n formatToggles?: string\r\n input?: string\r\n presets?: string\r\n presetTitle?: string\r\n presetGrid?: string\r\n presetSwatch?: string\r\n };\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n modelValue: \"#000000\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string): void;\r\n}>();\r\n\r\n// --- 样式配置 ---\r\nconst b = tv(theme);\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b() as any;\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n saturation: (opts?: { class?: any }) => styles.saturation({ class: cn(opts?.class, uiOverrides.value.saturation) }),\r\n saturationCursor: (opts?: { class?: any }) => styles.saturationCursor({ class: cn(opts?.class, uiOverrides.value.saturationCursor) }),\r\n controls: (opts?: { class?: any }) => styles.controls({ class: cn(opts?.class, uiOverrides.value.controls) }),\r\n preview: (opts?: { class?: any }) => styles.preview({ class: cn(opts?.class, uiOverrides.value.preview) }),\r\n sliders: (opts?: { class?: any }) => styles.sliders({ class: cn(opts?.class, uiOverrides.value.sliders) }),\r\n hueSlider: (opts?: { class?: any }) => styles.hueSlider({ class: cn(opts?.class, uiOverrides.value.hueSlider) }),\r\n hueCursor: (opts?: { class?: any }) => styles.hueCursor({ class: cn(opts?.class, uiOverrides.value.hueCursor) }),\r\n alphaSlider: (opts?: { class?: any }) => styles.alphaSlider({ class: cn(opts?.class, uiOverrides.value.alphaSlider) }),\r\n alphaCursor: (opts?: { class?: any }) => styles.alphaCursor({ class: cn(opts?.class, uiOverrides.value.alphaCursor) }),\r\n inputs: (opts?: { class?: any }) => styles.inputs({ class: cn(opts?.class, uiOverrides.value.inputs) }),\r\n formatToggles: (opts?: { class?: any }) => styles.formatToggles({ class: cn(opts?.class, uiOverrides.value.formatToggles) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n presets: (opts?: { class?: any }) => styles.presets({ class: cn(opts?.class, uiOverrides.value.presets) }),\r\n presetTitle: (opts?: { class?: any }) => styles.presetTitle({ class: cn(opts?.class, uiOverrides.value.presetTitle) }),\r\n presetGrid: (opts?: { class?: any }) => styles.presetGrid({ class: cn(opts?.class, uiOverrides.value.presetGrid) }),\r\n presetSwatch: (opts?: { class?: any }) => styles.presetSwatch({ class: cn(opts?.class, uiOverrides.value.presetSwatch) }),\r\n }\r\n});\r\n\r\n// --- 状态 ---\r\nconst colorHsv = ref<HsvaColor>(hexToHsva(props.modelValue || \"#000000\"));\r\nconst format = ref<\"hex\" | \"rgb\" | \"rgba\">(\"hex\");\r\n\r\n// --- 来自 base.css 的预设颜色 ---\r\nconst presets = [\r\n // 红色系\r\n \"#ffebee\", \"#ffe0e4\", \"#ffb1bc\", \"#ff8b9b\", \"#ff6675\", \"#ff3d58\", \"#d92946\", \"#b31938\", \"#8c0d2a\", \"#660821\",\r\n // 橙色系\r\n \"#fff7df\", \"#ffe9c9\", \"#ffd5a0\", \"#ffc370\", \"#ffb03b\", \"#ff9711\", \"#bf7c2a\", \"#995c1a\", \"#733d0e\", \"#522601\",\r\n // 绿色系\r\n \"#f1faf8\", \"#e7f6f3\", \"#a2dfcf\", \"#5fcfad\", \"#3ac29e\", \"#16ae88\", \"#0b876c\", \"#036150\", \"#003b32\", \"#001412\",\r\n // 蓝色系\r\n \"#ecf9ff\", \"#dff4ff\", \"#9ed6f5\", \"#61ccff\", \"#35b6f2\", \"#0d99e5\", \"#0277bf\", \"#005999\", \"#003f73\", \"#00284d\",\r\n // 灰色系\r\n \"#ffffff\", \"#f5f5f5\", \"#eeeeee\", \"#cccccc\", \"#aaaaaa\", \"#999999\", \"#666666\", \"#333333\",\r\n];\r\n\r\n// --- 计算属性 ---\r\nconst hexValue = computed(() => hsvaToHex(colorHsv.value));\r\nconst rgbaValue = computed(() => hsvaToRgba(colorHsv.value));\r\n\r\nconst displayValue = computed({\r\n get: () => {\r\n if (format.value === \"hex\") return hexValue.value.toUpperCase();\r\n if (format.value === \"rgb\") return `rgb(${rgbaValue.value.r}, ${rgbaValue.value.g}, ${rgbaValue.value.b})`;\r\n return `rgba(${rgbaValue.value.r}, ${rgbaValue.value.g}, ${rgbaValue.value.b}, ${rgbaValue.value.a.toFixed(2)})`;\r\n },\r\n set: (val: string) => {\r\n try {\r\n if (val.startsWith(\"#\")) {\r\n colorHsv.value = hexToHsva(val);\r\n } else if (val.startsWith(\"rgb\")) {\r\n const matches = val.match(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*([\\d.]+))?\\)/);\r\n if (matches) {\r\n const [_, r, g, b, a] = matches;\r\n colorHsv.value = rgbaToHsva({\r\n r: parseInt(r || \"0\"),\r\n g: parseInt(g || \"0\"),\r\n b: parseInt(b || \"0\"),\r\n a: a ? parseFloat(a) : 1,\r\n });\r\n }\r\n }\r\n } catch (e) {\r\n // 忽略无效输入\r\n }\r\n },\r\n});\r\n\r\n// --- 监听器 ---\r\nwatch(() => props.modelValue, (val: string | undefined) => {\r\n if (val && val !== hexValue.value) {\r\n colorHsv.value = hexToHsva(val);\r\n }\r\n});\r\n\r\nwatch(hexValue, (val: string) => {\r\n emit(\"update:modelValue\", val);\r\n});\r\n\r\n// --- 交互逻辑 ---\r\nconst saturationRef = ref<HTMLElement | undefined>();\r\nconst isDraggingSaturation = ref(false);\r\n\r\nfunction handleSaturationDrag(event: MouseEvent | TouchEvent) {\r\n if (!saturationRef.value) return;\r\n const rect = saturationRef.value.getBoundingClientRect();\r\n const isTouch = \"touches\" in event;\r\n const clientX = isTouch ? (event as TouchEvent).touches[0]?.clientX : (event as MouseEvent).clientX;\r\n const clientY = isTouch ? (event as TouchEvent).touches[0]?.clientY : (event as MouseEvent).clientY;\r\n\r\n if (clientX === undefined || clientY === undefined) return;\r\n\r\n const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\r\n const y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));\r\n\r\n colorHsv.value = {\r\n ...colorHsv.value,\r\n s: x * 100,\r\n v: (1 - y) * 100,\r\n };\r\n}\r\n\r\nfunction startSaturationDrag(event: MouseEvent | TouchEvent) {\r\n isDraggingSaturation.value = true;\r\n handleSaturationDrag(event);\r\n const up = () => {\r\n isDraggingSaturation.value = false;\r\n window.removeEventListener(\"mousemove\", move);\r\n window.removeEventListener(\"mouseup\", up);\r\n window.removeEventListener(\"touchmove\", move);\r\n window.removeEventListener(\"touchend\", up);\r\n };\r\n const move = (e: MouseEvent | TouchEvent) => handleSaturationDrag(e);\r\n window.addEventListener(\"mousemove\", move);\r\n window.addEventListener(\"mouseup\", up);\r\n window.addEventListener(\"touchmove\", move);\r\n window.addEventListener(\"touchend\", up);\r\n}\r\n\r\nconst hueRef = ref<HTMLElement | undefined>();\r\nfunction handleHueDrag(event: MouseEvent | TouchEvent) {\r\n if (!hueRef.value) return;\r\n const rect = hueRef.value.getBoundingClientRect();\r\n const isTouch = \"touches\" in event;\r\n const clientX = isTouch ? (event as TouchEvent).touches[0]?.clientX : (event as MouseEvent).clientX;\r\n\r\n if (clientX === undefined) return;\r\n\r\n const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\r\n colorHsv.value = { ...colorHsv.value, h: x * 360 };\r\n}\r\n\r\nfunction startHueDrag(event: MouseEvent | TouchEvent) {\r\n handleHueDrag(event);\r\n const up = () => {\r\n window.removeEventListener(\"mousemove\", move);\r\n window.removeEventListener(\"mouseup\", up);\r\n window.removeEventListener(\"touchmove\", move);\r\n window.removeEventListener(\"touchend\", up);\r\n };\r\n const move = (e: MouseEvent | TouchEvent) => handleHueDrag(e);\r\n window.addEventListener(\"mousemove\", move);\r\n window.addEventListener(\"mouseup\", up);\r\n window.addEventListener(\"touchmove\", move);\r\n window.addEventListener(\"touchend\", up);\r\n}\r\n\r\nconst alphaRef = ref<HTMLElement | undefined>();\r\nfunction handleAlphaDrag(event: MouseEvent | TouchEvent) {\r\n if (!alphaRef.value) return;\r\n const rect = alphaRef.value.getBoundingClientRect();\r\n const isTouch = \"touches\" in event;\r\n const clientX = isTouch ? (event as TouchEvent).touches[0]?.clientX : (event as MouseEvent).clientX;\r\n\r\n if (clientX === undefined) return;\r\n\r\n const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\r\n colorHsv.value = { ...colorHsv.value, a: x };\r\n}\r\n\r\nfunction startAlphaDrag(event: MouseEvent | TouchEvent) {\r\n handleAlphaDrag(event);\r\n const up = () => {\r\n window.removeEventListener(\"mousemove\", move);\r\n window.removeEventListener(\"mouseup\", up);\r\n window.removeEventListener(\"touchmove\", move);\r\n window.removeEventListener(\"touchend\", up);\r\n };\r\n const move = (e: MouseEvent | TouchEvent) => handleAlphaDrag(e);\r\n window.addEventListener(\"mousemove\", move);\r\n window.addEventListener(\"mouseup\", up);\r\n window.addEventListener(\"touchmove\", move);\r\n window.addEventListener(\"touchend\", up);\r\n}\r\n\r\nfunction selectPreset(color: string) {\r\n colorHsv.value = hexToHsva(color);\r\n}\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: props.class })\">\r\n <!-- 饱和度/明度 画布 -->\r\n <div ref=\"saturationRef\" :class=\"ui.saturation()\"\r\n :style=\"{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${colorHsv.h}, 100%, 50%))` }\"\r\n @mousedown=\"startSaturationDrag\" @touchstart=\"startSaturationDrag\">\r\n <div :class=\"ui.saturationCursor()\" :style=\"{ left: `${colorHsv.s}%`, top: `${100 - colorHsv.v}%` }\" />\r\n </div>\r\n\r\n <div :class=\"ui.controls()\">\r\n <!-- 预览 -->\r\n <div :class=\"ui.preview()\" :style=\"{ backgroundColor: hexValue }\" />\r\n\r\n <div :class=\"ui.sliders()\">\r\n <!-- 色相滑块 -->\r\n <div ref=\"hueRef\" :class=\"ui.hueSlider()\"\r\n style=\"background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)\"\r\n @mousedown=\"startHueDrag\" @touchstart=\"startHueDrag\">\r\n <div :class=\"ui.hueCursor()\" :style=\"{ left: `${(colorHsv.h / 360) * 100}%` }\" />\r\n </div>\r\n\r\n <!-- 透明度滑块 -->\r\n <div ref=\"alphaRef\" :class=\"ui.alphaSlider()\" :style=\"{\r\n background: `linear-gradient(to right, transparent, ${hexValue}), url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAAXNSR0IArs4c6QAAACBIREFUGF5jYmBgYGBgYGBgYGBgYGBgYGBgYGBgYGBgYGAEAAAgAAB9vNcCAAAAAElFTkSuQmCC')`\r\n }\" @mousedown=\"startAlphaDrag\" @touchstart=\"startAlphaDrag\">\r\n <div :class=\"ui.alphaCursor()\" :style=\"{ left: `${colorHsv.a * 100}%` }\" />\r\n </div>\r\n </div>\r\n </div>\r\n\r\n <!-- 输入框与格式 -->\r\n <div :class=\"ui.inputs()\">\r\n <div :class=\"ui.formatToggles()\">\r\n <RebornButton v-for=\"f in (['hex', 'rgb', 'rgba'] as const)\" :key=\"f\" size=\"xs\"\r\n :variant=\"format === f ? 'solid' : 'ghost'\" :color=\"format === f ? 'primary' : 'neutral'\"\r\n class=\"px-2 py-1 text-[10px] uppercase font-bold rounded transition-colors\" @click=\"format = f\">\r\n {{ f }}\r\n </RebornButton>\r\n </div>\r\n <RebornInput v-model=\"displayValue\" size=\"sm\" :class=\"ui.input()\" spellcheck=\"false\" />\r\n </div>\r\n\r\n <!-- 预设颜色 -->\r\n <div :class=\"ui.presets()\">\r\n <div :class=\"ui.presetTitle()\">主题预设</div>\r\n <div :class=\"ui.presetGrid()\">\r\n <RebornButton v-for=\"color in presets\" :key=\"color\" size=\"xs\" square :class=\"ui.presetSwatch()\"\r\n :style=\"{ backgroundColor: color }\" @click=\"selectPreset(color)\" />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n",
|
|
1175
|
+
"target": "web"
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
"path": "reborn-color-picker-panel.config.ts",
|
|
1179
|
+
"content": "const theme = {\r\n slots: {\r\n root: 'flex flex-col w-[260px] bg-white dark:bg-gray-900 rounded-xl shadow-xl border border-gray-100 dark:border-gray-800 p-4 select-none',\r\n saturation: 'relative w-full h-40 rounded-lg overflow-hidden cursor-crosshair mb-4',\r\n saturationCursor: 'absolute w-4 h-4 rounded-full border-2 border-white shadow-md -translate-x-2 -translate-y-2 pointer-events-none transition-all duration-75',\r\n controls: 'flex items-center gap-4 mb-4',\r\n preview: 'w-10 h-10 rounded-full border border-gray-100 dark:border-gray-800 shadow-sm shrink-0',\r\n sliders: 'flex-1 flex flex-col gap-2',\r\n hueSlider: 'relative h-3 rounded-full cursor-pointer',\r\n hueCursor: 'absolute w-4 h-4 rounded-full bg-white border border-gray-200 shadow-sm top-1/2 -translate-y-1/2 -translate-x-2 pointer-events-none',\r\n alphaSlider: 'relative h-3 rounded-full cursor-pointer',\r\n alphaCursor: 'absolute w-4 h-4 rounded-full bg-white border border-gray-200 shadow-sm top-1/2 -translate-y-1/2 -translate-x-2 pointer-events-none',\r\n inputs: 'flex flex-col gap-3',\r\n formatToggles: 'flex gap-1',\r\n input: 'flex-1',\r\n presets: 'mt-4 pt-4 border-t border-gray-100 dark:border-gray-800',\r\n presetTitle: 'text-[10px] font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider mb-2',\r\n presetGrid: 'grid grid-cols-8 gap-1.5',\r\n presetSwatch: 'w-full h-5 rounded-sm hover:scale-110 active:scale-95 transition-transform'\r\n }\r\n} as const\r\nexport default theme\r\n",
|
|
1180
|
+
"target": "uniapp"
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
"path": "reborn-color-picker.config.ts",
|
|
1184
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\n\r\nexport { size as colorPickerSizes }\r\nexport default {\r\n slots: {\r\n root: \"ring ring-1 ring-gray-5 p-1 rounded-md cursor-pointer select-none hover:scale-105 transition-all\",\r\n base: \"rounded-md ring ring-1 ring-gray-5 w-full h-full flex justify-center items-center relative overflow-hidden\",\r\n icon: \"text-white transition-transform duration-200\",\r\n },\r\n variants: {\r\n disabled: {\r\n true: {\r\n root: \"cursor-not-allowed opacity-50\",\r\n },\r\n },\r\n open: {\r\n true: {\r\n icon: \"rotate-180\",\r\n },\r\n },\r\n size: {\r\n sm: {\r\n root: \"h-[var(--button-sm-height)] w-[var(--button-sm-height)]\",\r\n },\r\n md: {\r\n root: \"h-[var(--button-base-height)] w-[var(--button-base-height)]\",\r\n },\r\n lg: {\r\n root: \"h-[var(--button-lg-height)] w-[var(--button-lg-height)]\",\r\n },\r\n },\r\n },\r\n}\r\n",
|
|
1185
|
+
"target": "uniapp"
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
"path": "RebornColorPicker.vue",
|
|
1189
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-color-picker',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref, type PropType, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport theme from './reborn-color-picker.config'\r\nimport RebornPopover from '../reborn-popover/RebornPopover.vue'\r\nimport RebornColorPickerPanel from './RebornColorPickerPanel.vue'\r\nimport { colorStringToHsva, detectColorFormat, hsvaToColorString, type ColorFormat } from '@/lib/color-utils'\r\n\r\nconst props = defineProps({\r\n modelValue: { type: String, default: '#000000' },\r\n disabled: { type: Boolean, default: false },\r\n size: { type: String as PropType<'sm' | 'md' | 'lg'>, default: 'md' },\r\n defaultFormat: { type: String as PropType<ColorFormat | undefined>, default: undefined },\r\n format: { type: String as PropType<ColorFormat | undefined>, default: undefined },\r\n /** Popover content config */\r\n content: { type: Object as PropType<any>, default: () => ({ side: 'right', align: 'center', sideOffset: 8 }) },\r\n /** Whether to show arrow */\r\n arrow: { type: Boolean, default: true },\r\n ui: { type: Object as PropType<any>, default: () => ({}) }\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'onChange'])\r\nconst b = tv(theme)\r\nconst showPicker = ref(false)\r\nconst resolvedDefaultFormat = computed<ColorFormat | undefined>(() => props.defaultFormat ?? props.format)\r\nconst selectedFormat = ref<ColorFormat>(resolvedDefaultFormat.value ?? detectColorFormat(props.modelValue))\r\n\r\n// Internal color state in HSVA to avoid precision loss\r\nconst internalHsva = ref(colorStringToHsva(props.modelValue))\r\n\r\nwatch(() => resolvedDefaultFormat.value, (val) => {\r\n if (val) {\r\n selectedFormat.value = val\r\n }\r\n}, { immediate: true })\r\n\r\nwatch(() => props.modelValue, (val) => {\r\n const nextHsva = colorStringToHsva(val)\r\n // 与当前内部状态一致时跳过,避免 Panel 选色后产生多余回写和重渲染导致卡顿\r\n const same =\r\n internalHsva.value.h === nextHsva.h &&\r\n internalHsva.value.s === nextHsva.s &&\r\n internalHsva.value.v === nextHsva.v &&\r\n internalHsva.value.a === nextHsva.a\r\n if (!same) {\r\n internalHsva.value = nextHsva\r\n }\r\n\r\n if (!resolvedDefaultFormat.value) {\r\n selectedFormat.value = detectColorFormat(val)\r\n }\r\n}, { immediate: true })\r\n\r\nfunction getFormattedColor(hsva: any) {\r\n return hsvaToColorString(hsva, selectedFormat.value)\r\n}\r\n\r\nconst colorValue = computed({\r\n get: () => getFormattedColor(internalHsva.value),\r\n set: (val: string) => emit('update:modelValue', val)\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst uiClasses = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n disabled: props.disabled\r\n })\r\n return {\r\n root: styles.root({ class: uiOverrides.value.root }),\r\n base: styles.base({ class: uiOverrides.value.base }),\r\n icon: styles.icon({ class: uiOverrides.value.icon }),\r\n }\r\n})\r\n\r\nfunction onPanelChange(value: string) {\r\n const hsva = colorStringToHsva(value)\r\n internalHsva.value = hsva\r\n emit('update:modelValue', getFormattedColor(hsva))\r\n emit('onChange', value)\r\n}\r\n\r\nfunction togglePicker() {\r\n if (props.disabled) return\r\n showPicker.value = !showPicker.value\r\n}\r\n</script>\r\n\r\n<template>\r\n <RebornPopover v-model=\"showPicker\" :content=\"content\" :arrow=\"arrow\" :disabled=\"disabled\">\r\n <view :class=\"uiClasses.root\">\r\n <view :class=\"uiClasses.base\" :style=\"{ backgroundColor: colorValue }\" @tap=\"togglePicker\">\r\n <view class=\"i-lucide-chevron-down text-white transition-transform duration-200\"\r\n :class=\"[uiClasses.icon, showPicker ? 'rotate-180' : '']\">\r\n </view>\r\n </view>\r\n </view>\r\n <template #content>\r\n <RebornColorPickerPanel :model-value=\"colorValue\" v-model:format=\"selectedFormat\"\r\n @update:modelValue=\"onPanelChange\" />\r\n </template>\r\n </RebornPopover>\r\n</template>\r\n",
|
|
1190
|
+
"target": "uniapp"
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
"path": "RebornColorPickerPanel.vue",
|
|
1194
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, watch, getCurrentInstance, onMounted } from \"vue\";\r\nimport {\r\n colorStringToHsva,\r\n detectColorFormat,\r\n hsvaToColorString,\r\n hsvaToHex,\r\n hsvaToRgba,\r\n rgbaToHsva,\r\n rgbaToHex,\r\n} from \"../../lib/color-utils\";\r\nimport type { ColorFormat, HsvaColor } from \"../../lib/color-utils\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport theme from \"./reborn-color-picker-panel.config\";\r\n\r\nimport RebornButton from \"../reborn-button/RebornButton.vue\";\r\nimport RebornInput from \"../reborn-input/RebornInput.vue\";\r\n\r\ninterface Props {\r\n modelValue?: string;\r\n format?: ColorFormat;\r\n class?: any;\r\n ui?: {\r\n root?: string\r\n saturation?: string\r\n saturationCursor?: string\r\n controls?: string\r\n preview?: string\r\n sliders?: string\r\n hueSlider?: string\r\n hueCursor?: string\r\n alphaSlider?: string\r\n alphaCursor?: string\r\n inputs?: string\r\n formatToggles?: string\r\n input?: string\r\n presets?: string\r\n presetTitle?: string\r\n presetGrid?: string\r\n presetSwatch?: string\r\n };\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n modelValue: \"#000000\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string): void;\r\n (e: \"update:format\", value: ColorFormat): void;\r\n}>();\r\n\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\n// --- 样式配置 ---\r\nconst b = tv(theme);\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b();\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n saturation: (opts?: { class?: any }) => styles.saturation({ class: cn(opts?.class, uiOverrides.value.saturation) }),\r\n saturationCursor: (opts?: { class?: any }) => styles.saturationCursor({ class: cn(opts?.class, uiOverrides.value.saturationCursor) }),\r\n controls: (opts?: { class?: any }) => styles.controls({ class: cn(opts?.class, uiOverrides.value.controls) }),\r\n preview: (opts?: { class?: any }) => styles.preview({ class: cn(opts?.class, uiOverrides.value.preview) }),\r\n sliders: (opts?: { class?: any }) => styles.sliders({ class: cn(opts?.class, uiOverrides.value.sliders) }),\r\n hueSlider: (opts?: { class?: any }) => styles.hueSlider({ class: cn(opts?.class, uiOverrides.value.hueSlider) }),\r\n hueCursor: (opts?: { class?: any }) => styles.hueCursor({ class: cn(opts?.class, uiOverrides.value.hueCursor) }),\r\n alphaSlider: (opts?: { class?: any }) => styles.alphaSlider({ class: cn(opts?.class, uiOverrides.value.alphaSlider) }),\r\n alphaCursor: (opts?: { class?: any }) => styles.alphaCursor({ class: cn(opts?.class, uiOverrides.value.alphaCursor) }),\r\n inputs: (opts?: { class?: any }) => styles.inputs({ class: cn(opts?.class, uiOverrides.value.inputs) }),\r\n formatToggles: (opts?: { class?: any }) => styles.formatToggles({ class: cn(opts?.class, uiOverrides.value.formatToggles) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n presets: (opts?: { class?: any }) => styles.presets({ class: cn(opts?.class, uiOverrides.value.presets) }),\r\n presetTitle: (opts?: { class?: any }) => styles.presetTitle({ class: cn(opts?.class, uiOverrides.value.presetTitle) }),\r\n presetGrid: (opts?: { class?: any }) => styles.presetGrid({ class: cn(opts?.class, uiOverrides.value.presetGrid) }),\r\n presetSwatch: (opts?: { class?: any }) => styles.presetSwatch({ class: cn(opts?.class, uiOverrides.value.presetSwatch) }),\r\n }\r\n});\r\n\r\n// --- 状态 ---\r\nconst colorHsv = ref<HsvaColor>(colorStringToHsva(props.modelValue || \"#000000\"));\r\nconst format = ref<ColorFormat>(props.format || detectColorFormat(props.modelValue));\r\n\r\n// 画布/滑块区域 rect 缓存,touchmove 时用缓存同步计算位置,避免每次异步 query 导致不跟手\r\ntype Rect = { left: number; top: number; width: number; height: number };\r\nconst saturationRect = ref<Rect | null>(null);\r\nconst hueRect = ref<Rect | null>(null);\r\nconst alphaRect = ref<Rect | null>(null);\r\n\r\n// --- 来自 base.css 的预设颜色 ---\r\nconst presets = [\r\n \"#ffebee\", \"#ff3d58\", \"#ff9711\", \"#16ae88\", \"#0d99e5\", \"#ffffff\", \"#eeeeee\", \"#333333\",\r\n];\r\n\r\n// --- 计算属性 ---\r\nconst hexValue = computed(() => hsvaToHex(colorHsv.value));\r\nconst rgbaValue = computed(() => hsvaToRgba(colorHsv.value));\r\n\r\nconst displayValue = computed({\r\n get: () => {\r\n const value = hsvaToColorString(colorHsv.value, format.value);\r\n return format.value === \"hex\" ? value.toUpperCase() : value;\r\n },\r\n set: (val: string) => {\r\n try {\r\n colorHsv.value = colorStringToHsva(val);\r\n\r\n if (!props.format) {\r\n format.value = detectColorFormat(val);\r\n }\r\n } catch (e) {\r\n // 忽略无效输入\r\n }\r\n },\r\n});\r\n\r\n// --- 监听器 ---\r\nwatch(() => props.modelValue, (val: string | undefined) => {\r\n if (!val) return;\r\n\r\n if (props.format) {\r\n format.value = props.format;\r\n } else {\r\n format.value = detectColorFormat(val);\r\n }\r\n\r\n const hsv = colorStringToHsva(val);\r\n const cur = colorHsv.value;\r\n if (\r\n cur.h !== hsv.h || cur.s !== hsv.s || cur.v !== hsv.v || cur.a !== hsv.a\r\n ) {\r\n colorHsv.value = hsv;\r\n }\r\n});\r\n\r\nwatch(() => props.format, (val) => {\r\n if (val) {\r\n format.value = val;\r\n }\r\n}, { immediate: true });\r\n\r\nwatch(displayValue, (val: string) => {\r\n emit(\"update:modelValue\", val);\r\n});\r\n\r\nwatch(format, (val) => {\r\n emit(\"update:format\", val);\r\n});\r\n\r\n// --- 交互逻辑:touchstart 预热 rect 缓存,touchmove 用缓存同步计算,避免异步 query 导致不跟手 ---\r\nfunction updateSaturationFromRect(rect: Rect, clientX: number, clientY: number) {\r\n const x = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\r\n const y = Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));\r\n colorHsv.value = {\r\n ...colorHsv.value,\r\n s: x * 100,\r\n v: (1 - y) * 100,\r\n };\r\n}\r\n\r\nfunction handleSaturationTouch(e: any) {\r\n const touch = e.touches[0];\r\n if (!touch) return;\r\n if (e.type === \"touchstart\") saturationRect.value = null;\r\n const rect = saturationRect.value;\r\n if (rect) {\r\n updateSaturationFromRect(rect, touch.clientX, touch.clientY);\r\n return;\r\n }\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select(\".reborn-saturation\")\r\n .boundingClientRect((r: any) => {\r\n if (!r) return;\r\n saturationRect.value = { left: r.left, top: r.top, width: r.width, height: r.height };\r\n updateSaturationFromRect(saturationRect.value, touch.clientX, touch.clientY);\r\n })\r\n .exec();\r\n}\r\n\r\nfunction handleHueTouch(e: any) {\r\n const touch = e.touches[0];\r\n if (!touch) return;\r\n if (e.type === \"touchstart\") hueRect.value = null;\r\n const rect = hueRect.value;\r\n if (rect) {\r\n const x = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));\r\n colorHsv.value = { ...colorHsv.value, h: x * 360 };\r\n return;\r\n }\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select(\".reborn-hue-slider\")\r\n .boundingClientRect((r: any) => {\r\n if (!r) return;\r\n hueRect.value = { left: r.left, top: r.top, width: r.width, height: r.height };\r\n const x = Math.max(0, Math.min(1, (touch.clientX - r.left) / r.width));\r\n colorHsv.value = { ...colorHsv.value, h: x * 360 };\r\n })\r\n .exec();\r\n}\r\n\r\nfunction handleAlphaTouch(e: any) {\r\n const touch = e.touches[0];\r\n if (!touch) return;\r\n if (e.type === \"touchstart\") alphaRect.value = null;\r\n const rect = alphaRect.value;\r\n if (rect) {\r\n const x = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));\r\n colorHsv.value = { ...colorHsv.value, a: x };\r\n return;\r\n }\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select(\".reborn-alpha-slider\")\r\n .boundingClientRect((r: any) => {\r\n if (!r) return;\r\n alphaRect.value = { left: r.left, top: r.top, width: r.width, height: r.height };\r\n const x = Math.max(0, Math.min(1, (touch.clientX - r.left) / r.width));\r\n colorHsv.value = { ...colorHsv.value, a: x };\r\n })\r\n .exec();\r\n}\r\n\r\nfunction selectPreset(color: string) {\r\n colorHsv.value = colorStringToHsva(color);\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.class })\" @touchmove.stop.prevent=\"\">\r\n <!-- 饱和度/明度 画布 -->\r\n <view :class=\"[ui.saturation(), 'reborn-saturation']\"\r\n :style=\"{ background: `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${colorHsv.h}, 100%, 50%))` }\"\r\n @touchstart=\"handleSaturationTouch\" @touchmove=\"handleSaturationTouch\">\r\n <view :class=\"ui.saturationCursor()\" :style=\"{ left: `${colorHsv.s}%`, top: `${100 - colorHsv.v}%` }\" />\r\n </view>\r\n\r\n <view :class=\"ui.controls()\">\r\n <!-- 预览 -->\r\n <view :class=\"ui.preview()\" :style=\"{ backgroundColor: hexValue }\" />\r\n\r\n <view :class=\"ui.sliders()\">\r\n <!-- 色相滑块 -->\r\n <view :class=\"[ui.hueSlider(), 'reborn-hue-slider']\"\r\n style=\"background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)\"\r\n @touchstart=\"handleHueTouch\" @touchmove=\"handleHueTouch\">\r\n <view :class=\"ui.hueCursor()\" :style=\"{ left: `${(colorHsv.h / 360) * 100}%` }\" />\r\n </view>\r\n\r\n <!-- 透明度滑块 -->\r\n <view :class=\"[ui.alphaSlider(), 'reborn-alpha-slider']\" :style=\"{\r\n background: `linear-gradient(to right, transparent, ${hexValue})`\r\n }\" @touchstart=\"handleAlphaTouch\" @touchmove=\"handleAlphaTouch\">\r\n <view :class=\"ui.alphaCursor()\" :style=\"{ left: `${colorHsv.a * 100}%` }\" />\r\n </view>\r\n </view>\r\n </view>\r\n\r\n <!-- 输入框与格式 -->\r\n <view :class=\"ui.inputs()\">\r\n <view :class=\"ui.formatToggles()\">\r\n <RebornButton v-for=\"f in (['hex', 'rgb', 'rgba'] as const)\" :key=\"f\" size=\"xs\"\r\n :variant=\"format === f ? 'solid' : 'soft'\" :color=\"format === f ? 'primary' : 'neutral'\"\r\n class=\"px-2 py-1 text-[10px] uppercase font-bold rounded transition-colors\"\r\n @tap=\"format = f as ColorFormat\">\r\n {{ f }}\r\n </RebornButton>\r\n </view>\r\n <RebornInput v-model=\"displayValue\" size=\"sm\" :class=\"ui.input()\" spellcheck=\"false\" />\r\n </view>\r\n\r\n <!-- 预设颜色 -->\r\n <view :class=\"ui.presets()\">\r\n <view :class=\"ui.presetTitle()\">主题预设</view>\r\n <view :class=\"ui.presetGrid()\">\r\n <view v-for=\"color in presets\" :key=\"color\" :class=\"ui.presetSwatch()\"\r\n :style=\"{ backgroundColor: color }\" @tap=\"selectPreset(color)\" />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1195
|
+
"target": "uniapp"
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
"name": "reborn-draggable",
|
|
1201
|
+
"dependencies": [],
|
|
1202
|
+
"files": [
|
|
1203
|
+
{
|
|
1204
|
+
"path": "index.ts",
|
|
1205
|
+
"content": "export { default as RebornDraggable } from './RebornDraggable.vue';\r\n"
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
"path": "reborn-draggable.config.ts",
|
|
1209
|
+
"content": "export default { root: 'grid gap-3', item: 'relative' } as const;\r\n",
|
|
1210
|
+
"target": "web"
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
"path": "RebornDraggable.vue",
|
|
1214
|
+
"content": "<script setup lang=\"ts\" generic=\"T extends Record<string, any> | string | number\">\r\nimport { ref, watch } from 'vue';\r\nimport theme from './reborn-draggable.config';\r\n\r\nconst props = withDefaults(defineProps<{ modelValue?: T[]; disabled?: boolean; className?: string; }>(), { modelValue: () => [], disabled: false, className: '' });\r\nconst emit = defineEmits(['update:modelValue', 'change', 'start', 'end']);\r\nconst list = ref<T[]>([...props.modelValue]);\r\nconst dragIndex = ref(-1);\r\nconst isDragging = ref(false);\r\n\r\nwatch(() => props.modelValue, (v) => {\r\n if (!isDragging.value) {\r\n list.value = [...v];\r\n }\r\n}, { deep: true });\r\n\r\nconst isItemDisabled = (item: any) => props.disabled || (item && typeof item === 'object' && item.disabled === true);\r\n\r\nconst onDragStart = (e: DragEvent, index: number, item: any) => {\r\n if (isItemDisabled(item)) return e.preventDefault();\r\n isDragging.value = true;\r\n dragIndex.value = index;\r\n if (e.dataTransfer) {\r\n e.dataTransfer.effectAllowed = 'move';\r\n }\r\n emit('start', { index, item });\r\n};\r\n\r\nconst onDragEnter = (e: DragEvent, index: number, item: any) => {\r\n e.preventDefault();\r\n if (dragIndex.value < 0 || dragIndex.value === index) return;\r\n\r\n const next = [...list.value];\r\n const [draggedItem] = next.splice(dragIndex.value, 1);\r\n if (draggedItem !== undefined) {\r\n next.splice(index, 0, draggedItem as any);\r\n }\r\n list.value = next;\r\n dragIndex.value = index;\r\n};\r\n\r\nconst onDrop = (index: number) => {\r\n emit('update:modelValue', list.value);\r\n emit('change', list.value);\r\n};\r\n\r\nconst onDragEnd = () => {\r\n dragIndex.value = -1;\r\n isDragging.value = false;\r\n emit('end');\r\n};\r\n\r\nconst getItemKey = (item: any, index: number) => {\r\n return (item && typeof item === 'object' && (item.id || item.key || item.name)) || item || index;\r\n};\r\n</script>\r\n\r\n<template>\r\n <TransitionGroup tag=\"div\" :class=\"`${theme.root} ${props.className}`\" name=\"reborn-drag\">\r\n <div v-for=\"(item, index) in list\" :key=\"getItemKey(item, index)\" :class=\"[\r\n theme.item,\r\n isItemDisabled(item) ? 'cursor-not-allowed opacity-50' : 'cursor-move',\r\n dragIndex === index ? 'opacity-90 scale-105 shadow-md relative z-10' : ''\r\n ]\" :draggable=\"!isItemDisabled(item)\" @dragstart=\"onDragStart($event, index, item)\"\r\n @dragenter=\"onDragEnter($event, index, item)\" @dragover.prevent @drop=\"onDrop(index)\" @dragend=\"onDragEnd\">\r\n <slot name=\"item\" :item=\"item\" :index=\"index\" :dragging=\"dragIndex === index\">{{ item }}</slot>\r\n </div>\r\n </TransitionGroup>\r\n</template>\r\n\r\n<style scoped>\r\n.reborn-drag-move {\r\n transition: transform 0.3s ease;\r\n}\r\n\r\n.reborn-drag-enter-active,\r\n.reborn-drag-leave-active {\r\n transition: all 0.3s ease;\r\n}\r\n\r\n.reborn-drag-enter-from,\r\n.reborn-drag-leave-to {\r\n opacity: 0;\r\n transform: scale(0.9);\r\n}\r\n</style>\r\n",
|
|
1215
|
+
"target": "web"
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"path": "reborn-draggable.config.ts",
|
|
1219
|
+
"content": "const theme = {\r\n slots: {\r\n root: 'flex flex-col relative overflow-visible',\r\n // #ifdef APP-IOS\r\n item: 'relative z-10 transition-none opacity-100',\r\n // #endif\r\n // #ifndef APP-IOS\r\n // @ts-ignore\r\n item: 'relative z-10',\r\n // #endif\r\n },\r\n variants: {\r\n columns: {\r\n true: { root: 'flex-row flex-wrap' },\r\n },\r\n disabled: {\r\n true: { item: 'opacity-60' },\r\n },\r\n },\r\n defaultVariants: {\r\n columns: false,\r\n disabled: false,\r\n },\r\n}\r\n\r\nexport default theme\r\n",
|
|
1220
|
+
"target": "uniapp"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"path": "RebornDraggable.vue",
|
|
1224
|
+
"content": "<template>\r\n <view class=\"reborn-draggable\" :class=\"ui.root({ class: props.className })\" :data-dragging=\"dragging\"\r\n @touchmove=\"dragWxs.touchmove\">\r\n <!-- @vue-ignore -->\r\n <view v-for=\"(item, index) in list\" :key=\"getItemKey(item, index)\" class=\"reborn-draggable__item\" :class=\"[\r\n ui.item(),\r\n {\r\n 'reborn-draggable__item--disabled': disabled,\r\n 'reborn-draggable__item--dragging': dragging && dragIndex == index,\r\n 'reborn-draggable__item--animating': dragging && dragIndex != index\r\n }\r\n ]\" :style=\"getItemStyle(index)\" @touchstart=\"\r\n (event: UniTouchEvent) => {\r\n onTouchStart(event, index, 'touch');\r\n }\r\n \" @longpress=\"\r\n (event: UniTouchEvent) => {\r\n onTouchStart(event, index, 'longpress');\r\n }\r\n \" @touchmove=\"onTouchMove\" @touchend=\"onTouchEnd\">\r\n <slot name=\"item\" :item=\"item\" :index=\"index\" :dragging=\"dragging\" :dragIndex=\"dragIndex\"\r\n :insertIndex=\"insertIndex\">\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref, getCurrentInstance, type PropType, watch, onMounted, onUnmounted } from \"vue\";\r\nimport theme from \"./reborn-draggable.config\";\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { uuid } from \"@/lib/file\";\r\n\r\ndefineOptions({\r\n name: \"reborn-draggable\"\r\n});\r\n\r\ndefineSlots<{\r\n item(props: {\r\n item: any;\r\n index: number;\r\n dragging: boolean;\r\n dragIndex: number;\r\n insertIndex: number;\r\n }): any;\r\n}>();\r\n\r\n// 项目位置信息类型定义\r\ntype ItemPosition = {\r\n top: number;\r\n left: number;\r\n width: number;\r\n height: number;\r\n};\r\n\r\n// 解决 TS 类型检查报错:为 WXS 模块提供声明\r\nconst dragWxs = { touchmove: (e: any, ins: any) => true };\r\n\r\n// 位移偏移量类型定义\r\ntype TranslateOffset = {\r\n x: number;\r\n y: number;\r\n};\r\n\r\nconst props = defineProps({\r\n /** PassThrough 样式配置 */\r\n className: {\r\n type: String,\r\n default: \"\"\r\n },\r\n /** UI 配置 */\r\n ui: {\r\n type: Object as PropType<any>,\r\n default: () => ({})\r\n },\r\n /** 数据数组,支持双向绑定 */\r\n modelValue: {\r\n type: Array as PropType<any[]>,\r\n default: () => []\r\n },\r\n /** 是否禁用拖拽功能 */\r\n disabled: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 列数:1为单列纵向布局,>1为多列网格布局 */\r\n columns: {\r\n type: Number,\r\n default: 1\r\n },\r\n // 是否需要长按触发\r\n longPress: {\r\n type: Boolean,\r\n default: true\r\n }\r\n});\r\n\r\nconst emit = defineEmits([\"update:modelValue\", \"change\", \"start\", \"end\"]);\r\n\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\n/** 数据列表 */\r\nconst list = ref<any[]>([]);\r\n\r\n/** 是否正在拖拽 */\r\nconst dragging = ref(false);\r\n/** 当前拖拽元素的原始索引 */\r\nconst dragIndex = ref(-1);\r\n/** 预期插入的目标索引 */\r\nconst insertIndex = ref(-1);\r\n/** 触摸开始时的Y坐标 */\r\nconst startY = ref(0);\r\n/** 触摸开始时的X坐标 */\r\nconst startX = ref(0);\r\n/** Y轴偏移量 */\r\nconst offsetY = ref(0);\r\n/** X轴偏移量 */\r\nconst offsetX = ref(0);\r\n/** 当前拖拽的数据项 */\r\nconst dragItem = ref<any>({});\r\n/** 所有项目的位置信息缓存 */\r\nconst itemPositions = ref<ItemPosition[]>([]);\r\n/** 是否处于放下动画状态 */\r\nconst dropping = ref(false);\r\n/** 动态计算的项目高度 */\r\nconst itemHeight = ref(0);\r\n/** 动态计算的项目宽度 */\r\nconst itemWidth = ref(0);\r\n/** 是否已开始排序模拟(防止误触) */\r\nconst sortingStarted = ref(false);\r\n\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst ui = computed(() => {\r\n const styles = b({\r\n disabled: props.disabled,\r\n columns: props.columns > 1,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n item: (opts?: { class?: any }) => styles.item({ class: cn(opts?.class, uiOverrides.value.base) }), // alias to base\r\n }\r\n})\r\n\r\nfunction isNull(value?: any | null): boolean {\r\n // #ifdef APP\r\n return value == null;\r\n // #endif\r\n\r\n // #ifndef APP\r\n return value == null || value == undefined;\r\n // #endif\r\n}\r\n/**\r\n * 重置所有拖拽相关的状态\r\n * 在拖拽结束后调用,确保组件回到初始状态\r\n */\r\nfunction reset() {\r\n dragging.value = false; // 拖拽状态\r\n dropping.value = false; // 放下动画状态\r\n dragIndex.value = -1; // 拖拽元素索引\r\n insertIndex.value = -1; // 插入位置索引\r\n offsetX.value = 0; // X轴偏移\r\n offsetY.value = 0; // Y轴偏移\r\n dragItem.value = {}; // 拖拽的数据项\r\n itemPositions.value = []; // 位置信息缓存\r\n itemHeight.value = 0; // 动态计算的高度\r\n itemWidth.value = 0; // 动态计算的宽度\r\n sortingStarted.value = false; // 排序模拟状态\r\n}\r\n\r\n/**\r\n * 计算网格布局中元素的位移偏移\r\n * @param index 当前元素索引\r\n * @param dragIdx 拖拽元素索引\r\n * @param insertIdx 插入位置索引\r\n * @returns 包含 x 和 y 坐标偏移的对象\r\n */\r\nfunction calculateGridOffset(index: number, dragIdx: number, insertIdx: number): TranslateOffset {\r\n const cols = props.columns;\r\n\r\n // 计算当前元素在网格中的行列位置\r\n const currentRow = Math.floor(index / cols);\r\n const currentCol = index % cols;\r\n\r\n // 计算元素在拖拽后的新位置索引\r\n let newIndex = index;\r\n\r\n if (dragIdx < insertIdx) {\r\n // 向后拖拽:dragIdx+1 到 insertIdx 之间的元素需要向前移动一位\r\n if (index > dragIdx && index <= insertIdx) {\r\n newIndex = index - 1;\r\n }\r\n } else if (dragIdx > insertIdx) {\r\n // 向前拖拽:insertIdx 到 dragIdx-1 之间的元素需要向后移动一位\r\n if (index >= insertIdx && index < dragIdx) {\r\n newIndex = index + 1;\r\n }\r\n }\r\n\r\n // 计算新位置的行列坐标\r\n const newRow = Math.floor(newIndex / cols);\r\n const newCol = newIndex % cols;\r\n\r\n // 使用动态计算的网格尺寸\r\n const cellWidth = itemWidth.value;\r\n const cellHeight = itemHeight.value;\r\n\r\n // 计算实际的像素位移\r\n const offsetX = (newCol - currentCol) * cellWidth;\r\n const offsetY = (newRow - currentRow) * cellHeight;\r\n\r\n return { x: offsetX, y: offsetY };\r\n}\r\n\r\n/**\r\n * 计算网格布局的插入位置\r\n * @param dragCenterX 拖拽元素中心点X坐标\r\n * @param dragCenterY 拖拽元素中心点Y坐标\r\n * @returns 最佳插入位置索引\r\n */\r\nfunction calculateGridInsertIndex(dragCenterX: number, dragCenterY: number): number {\r\n if (itemPositions.value.length == 0) {\r\n return dragIndex.value;\r\n }\r\n\r\n let closestIndex = dragIndex.value;\r\n let minDistance = Infinity;\r\n\r\n // 使用欧几里得距离找到最近的网格位置(包括原位置)\r\n for (let i = 0; i < itemPositions.value.length; i++) {\r\n const position = itemPositions.value[i];\r\n\r\n // 计算到元素中心点的距离\r\n const centerX = position.left + position.width / 2;\r\n const centerY = position.top + position.height / 2;\r\n\r\n // 使用欧几里得距离公式\r\n const distance = Math.sqrt(\r\n Math.pow(dragCenterX - centerX, 2) + Math.pow(dragCenterY - centerY, 2)\r\n );\r\n\r\n // 更新最近的位置\r\n if (distance < minDistance) {\r\n minDistance = distance;\r\n closestIndex = i;\r\n }\r\n }\r\n\r\n return closestIndex;\r\n}\r\n\r\n/**\r\n * 计算单列布局的插入位置\r\n * @param clientY Y坐标\r\n * @returns 最佳插入位置索引\r\n */\r\nfunction calculateSingleColumnInsertIndex(clientY: number): number {\r\n let closestIndex = dragIndex.value;\r\n let minDistance = Infinity;\r\n\r\n // 遍历所有元素,找到距离最近的元素中心\r\n for (let i = 0; i < itemPositions.value.length; i++) {\r\n const position = itemPositions.value[i];\r\n\r\n // 计算到元素中心点的距离\r\n const itemCenter = position.top + position.height / 2;\r\n const distance = Math.abs(clientY - itemCenter);\r\n\r\n if (distance < minDistance) {\r\n minDistance = distance;\r\n closestIndex = i;\r\n }\r\n }\r\n\r\n return closestIndex;\r\n}\r\n\r\n/**\r\n * 计算拖拽元素的最佳插入位置\r\n * @param clientPosition 在主轴上的坐标(仅用于单列布局的Y轴坐标)\r\n * @returns 最佳插入位置的索引\r\n */\r\nfunction calculateInsertIndex(clientPosition: number): number {\r\n // 如果没有位置信息,保持原位置\r\n if (itemPositions.value.length == 0) {\r\n return dragIndex.value;\r\n }\r\n\r\n // 根据布局类型选择计算方式\r\n if (props.columns > 1) {\r\n // 多列网格布局:计算拖拽元素的中心点坐标,使用2D坐标计算最近位置\r\n const dragPos = itemPositions.value[dragIndex.value];\r\n const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;\r\n const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;\r\n return calculateGridInsertIndex(dragCenterX, dragCenterY);\r\n } else {\r\n // 单列布局:基于Y轴距离计算最近的元素中心\r\n return calculateSingleColumnInsertIndex(clientPosition);\r\n }\r\n}\r\n\r\n/**\r\n * 计算单列布局的位移偏移\r\n * @param index 元素索引\r\n * @param dragIdx 拖拽元素索引\r\n * @param insertIdx 插入位置索引\r\n * @returns 位移偏移对象\r\n */\r\nfunction calculateSingleColumnOffset(\r\n index: number,\r\n dragIdx: number,\r\n insertIdx: number\r\n): TranslateOffset {\r\n if (dragIdx < insertIdx) {\r\n // 向下拖拽:dragIdx+1 到 insertIdx 之间的元素向上移动\r\n if (index > dragIdx && index <= insertIdx) {\r\n return { x: 0, y: -itemHeight.value };\r\n }\r\n } else if (dragIdx > insertIdx) {\r\n // 向上拖拽:insertIdx 到 dragIdx-1 之间的元素向下移动\r\n if (index >= insertIdx && index < dragIdx) {\r\n return { x: 0, y: itemHeight.value };\r\n }\r\n }\r\n\r\n return { x: 0, y: 0 };\r\n}\r\n\r\n/**\r\n * 计算非拖拽元素的位移偏移量\r\n * @param index 元素索引\r\n * @returns 包含 x 和 y 坐标偏移的对象\r\n */\r\nfunction getItemTranslateOffset(index: number): TranslateOffset {\r\n // 只在满足所有条件时才计算位移:拖拽中、非放下状态、已开始排序\r\n if (!dragging.value || dropping.value || !sortingStarted.value) {\r\n return { x: 0, y: 0 };\r\n }\r\n\r\n const dragIdx = dragIndex.value;\r\n const insertIdx = insertIndex.value;\r\n\r\n // 跳过正在拖拽的元素(拖拽元素由位置控制)\r\n if (index == dragIdx) {\r\n return { x: 0, y: 0 };\r\n }\r\n\r\n // 没有位置变化时不需要位移(拖回原位置)\r\n if (dragIdx == insertIdx) {\r\n return { x: 0, y: 0 };\r\n }\r\n\r\n // 根据布局类型计算位移\r\n if (props.columns > 1) {\r\n // 多列网格布局:使用2D位移计算\r\n return calculateGridOffset(index, dragIdx, insertIdx);\r\n } else {\r\n // 单列布局:使用简单的纵向位移\r\n return calculateSingleColumnOffset(index, dragIdx, insertIdx);\r\n }\r\n}\r\n\r\n/**\r\n * 计算项目的完整样式对象\r\n * @param index 项目索引\r\n * @returns 样式对象\r\n */\r\nfunction getItemStyle(index: number) {\r\n const style: Record<string, string> = {};\r\n const isCurrent = dragIndex.value == index;\r\n\r\n // 多列布局时设置等宽分布\r\n if (props.columns > 1) {\r\n const widthPercent = 100 / props.columns;\r\n style[\"flex-basis\"] = `${widthPercent}%`;\r\n style[\"width\"] = `${widthPercent}%`;\r\n style[\"box-sizing\"] = \"border-box\";\r\n }\r\n\r\n // 放下动画期间,只保留基础样式\r\n if (dropping.value) {\r\n return style;\r\n }\r\n\r\n // 拖拽状态下的样式处理\r\n if (dragging.value) {\r\n if (isCurrent) {\r\n // 拖拽元素:跟随移动\r\n style[\"transform\"] = `translate(${offsetX.value}px, ${offsetY.value}px)`;\r\n style[\"z-index\"] = \"100\";\r\n } else {\r\n // 其他元素:显示排序预览位移\r\n const translateOffset = getItemTranslateOffset(index);\r\n style[\"transform\"] = `translate(${translateOffset.x}px, ${translateOffset.y}px)`;\r\n }\r\n }\r\n\r\n return style;\r\n}\r\n\r\n/**\r\n * 获取所有项目的位置信息\r\n */\r\nasync function getItemPosition(): Promise<void> {\r\n return new Promise((resolve) => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select(\".reborn-draggable\")\r\n .boundingClientRect()\r\n .exec((res) => {\r\n const box = res[0] as any;\r\n\r\n itemWidth.value = (box.width ?? 0) / props.columns;\r\n\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .selectAll(\".reborn-draggable__item\")\r\n .boundingClientRect()\r\n .exec((res) => {\r\n const rects = res[0] as any[];\r\n const positions: ItemPosition[] = [];\r\n\r\n for (let i = 0; i < rects.length; i++) {\r\n const rect = rects[i];\r\n\r\n if (i == 0) {\r\n itemHeight.value = rect.height ?? 0;\r\n }\r\n\r\n positions.push({\r\n top: rect.top ?? 0,\r\n left: rect.left ?? 0,\r\n width: itemWidth.value,\r\n height: itemHeight.value\r\n });\r\n }\r\n\r\n itemPositions.value = positions;\r\n\r\n resolve();\r\n });\r\n });\r\n });\r\n}\r\n\r\n/**\r\n * 获取项目是否禁用\r\n * @param index 项目索引\r\n * @returns 是否禁用\r\n */\r\nfunction getItemDisabled(index: number): boolean {\r\n return !isNull(list.value[index][\"disabled\"]) && (list.value[index][\"disabled\"] as boolean);\r\n}\r\n\r\n/**\r\n * 检查拖拽元素的中心点是否移动到其他元素区域\r\n */\r\nfunction checkMovedToOtherElement(): boolean {\r\n // 如果没有位置信息,默认未移出\r\n if (itemPositions.value.length == 0) return false;\r\n\r\n const dragIdx = dragIndex.value;\r\n const dragPosition = itemPositions.value[dragIdx];\r\n\r\n // 计算拖拽元素当前的中心点位置(考虑拖拽偏移)\r\n const dragCenterX = dragPosition.left + dragPosition.width / 2 + offsetX.value;\r\n const dragCenterY = dragPosition.top + dragPosition.height / 2 + offsetY.value;\r\n\r\n // 根据布局类型采用不同的判断策略\r\n if (props.columns > 1) {\r\n // 多列网格布局:检查中心点是否与其他元素区域重叠\r\n for (let i = 0; i < itemPositions.value.length; i++) {\r\n if (i == dragIdx) continue;\r\n\r\n const otherPosition = itemPositions.value[i];\r\n const isOverlapping =\r\n dragCenterX >= otherPosition.left &&\r\n dragCenterX <= otherPosition.left + otherPosition.width &&\r\n dragCenterY >= otherPosition.top &&\r\n dragCenterY <= otherPosition.top + otherPosition.height;\r\n\r\n if (isOverlapping) {\r\n return true;\r\n }\r\n }\r\n } else {\r\n // 检查是否向上移动超过上一个元素的中线\r\n if (dragIdx > 0) {\r\n const prevPosition = itemPositions.value[dragIdx - 1];\r\n const prevCenterY = prevPosition.top + prevPosition.height / 2;\r\n if (dragCenterY <= prevCenterY) {\r\n return true;\r\n }\r\n }\r\n\r\n // 检查是否向下移动超过下一个元素的中线\r\n if (dragIdx < itemPositions.value.length - 1) {\r\n const nextPosition = itemPositions.value[dragIdx + 1];\r\n const nextCenterY = nextPosition.top + nextPosition.height / 2;\r\n if (dragCenterY >= nextCenterY) {\r\n return true;\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * 触摸开始事件处理\r\n * @param event 触摸事件对象\r\n * @param index 触摸的项目索引\r\n */\r\nasync function onTouchStart(event: any, index: number, type: string) {\r\n // 如果是长按触发,但未开启长按功能,则直接返回\r\n if (type == \"longpress\" && !props.longPress) return;\r\n // 如果是普通触摸触发,但已开启长按功能,则直接返回\r\n if (type == \"touch\" && props.longPress) return;\r\n\r\n // 检查是否禁用或索引无效\r\n if (props.disabled) return;\r\n if (getItemDisabled(index)) return;\r\n if (index < 0 || index >= list.value.length) return;\r\n\r\n // 获取触摸点\r\n const touch = event.touches[0];\r\n\r\n // 初始化拖拽状态\r\n dragging.value = true;\r\n\r\n // 初始化拖拽索引\r\n dragIndex.value = index;\r\n insertIndex.value = index; // 初始插入位置为原位置\r\n startX.value = touch.clientX;\r\n startY.value = touch.clientY;\r\n offsetX.value = 0;\r\n offsetY.value = 0;\r\n // 初始化拖拽数据项\r\n dragItem.value = list.value[index];\r\n\r\n // 先获取所有项目的位置信息,为后续计算做准备\r\n await getItemPosition();\r\n\r\n // 触发开始事件\r\n emit(\"start\", index);\r\n\r\n // 震动\r\n uni.$emit(\"cool.vibrate\");\r\n\r\n // 阻止事件冒泡\r\n if (event.stopPropagation) {\r\n event.stopPropagation();\r\n }\r\n // 阻止默认行为\r\n if (event.cancelable !== false && event.preventDefault) {\r\n event.preventDefault();\r\n }\r\n}\r\n\r\n/**\r\n * 触摸移动事件处理\r\n * @param event 触摸事件对象\r\n */\r\nfunction onTouchMove(event: TouchEvent): void {\r\n if (!dragging.value) return;\r\n\r\n const touch = event.touches[0];\r\n\r\n // 更新拖拽偏移量\r\n offsetX.value = touch.clientX - startX.value;\r\n offsetY.value = touch.clientY - startY.value;\r\n\r\n // 智能启动排序模拟:只有移出原元素区域才开始\r\n if (!sortingStarted.value) {\r\n if (checkMovedToOtherElement()) {\r\n sortingStarted.value = true;\r\n }\r\n }\r\n\r\n // 只有开始排序模拟后才计算插入位置\r\n if (sortingStarted.value) {\r\n // 计算拖拽元素当前的中心点坐标\r\n const dragPos = itemPositions.value[dragIndex.value];\r\n const dragCenterX = dragPos.left + dragPos.width / 2 + offsetX.value;\r\n const dragCenterY = dragPos.top + dragPos.height / 2 + offsetY.value;\r\n\r\n // 根据布局类型选择坐标轴:网格布局使用X坐标,单列布局使用Y坐标\r\n const dragCenter = props.columns > 1 ? dragCenterX : dragCenterY;\r\n\r\n // 计算最佳插入位置\r\n const newIndex = calculateInsertIndex(dragCenter);\r\n if (newIndex != insertIndex.value) {\r\n insertIndex.value = newIndex;\r\n }\r\n }\r\n\r\n // 阻止默认行为\r\n if (event.cancelable !== false && event.preventDefault) {\r\n event.preventDefault();\r\n }\r\n}\r\n\r\n/**\r\n * 触摸结束事件处理\r\n */\r\nfunction onTouchEnd(): void {\r\n if (!dragging.value) return;\r\n\r\n // 旧索引\r\n const oldIndex = dragIndex.value;\r\n\r\n // 新索引\r\n const newIndex = insertIndex.value;\r\n\r\n // 如果位置发生变化,立即更新数组\r\n if (oldIndex != newIndex && newIndex >= 0) {\r\n const newList = [...list.value];\r\n const item = newList.splice(oldIndex, 1)[0];\r\n newList.splice(newIndex, 0, item);\r\n list.value = newList;\r\n\r\n // 触发变化事件\r\n emit(\"update:modelValue\", list.value);\r\n emit(\"change\", list.value);\r\n }\r\n\r\n // 开始放下动画\r\n dropping.value = true;\r\n dragging.value = false;\r\n\r\n // 重置所有状态\r\n reset();\r\n\r\n // 等待放下动画完成后重置所有状态\r\n emit(\"end\", newIndex >= 0 ? newIndex : oldIndex);\r\n}\r\n\r\n/**\r\n * 根据平台选择合适的key\r\n * @param item 数据项\r\n * @param index 索引\r\n * @returns 合适的key\r\n */\r\nfunction getItemKey(item: any, index: number): string {\r\n // #ifdef MP\r\n // 小程序环境使用 index 作为 key,避免数据错乱\r\n return `${index}`;\r\n // #endif\r\n\r\n // #ifndef MP\r\n // 其他平台使用 uid,提供更好的性能\r\n return item[\"uid\"] as string;\r\n // #endif\r\n}\r\n\r\nwatch(\r\n computed(() => props.modelValue),\r\n (val: any[]) => {\r\n list.value = val.map((e) => {\r\n return {\r\n uid: e[\"uid\"] ?? uuid(),\r\n ...e\r\n };\r\n });\r\n },\r\n {\r\n immediate: true\r\n }\r\n);\r\n</script>\r\n\r\n<script module=\"dragWxs\" lang=\"wxs\">\r\nmodule.exports = {\r\n touchmove: function (e, ins) {\r\n if (e.currentTarget && e.currentTarget.dataset) {\r\n var dragging = e.currentTarget.dataset.dragging;\r\n // 判断是否在拖拽中(兼容各端 boolean 和 string)\r\n if (dragging === true || dragging === 'true') {\r\n return false; // 阻止默认的页面滚动行为\r\n }\r\n }\r\n return true;\r\n }\r\n}\r\n</script>\r\n\r\n<style lang=\"scss\" scoped>\r\n.reborn-draggable {\r\n &__item {\r\n &--dragging {\r\n @apply opacity-95 z-20 scale-105 shadow-md;\r\n }\r\n\r\n &--disabled {\r\n @apply opacity-60;\r\n }\r\n\r\n &--animating {\r\n @apply duration-200;\r\n transition-property: transform;\r\n }\r\n }\r\n}\r\n</style>\r\n",
|
|
1225
|
+
"target": "uniapp"
|
|
1226
|
+
}
|
|
1227
|
+
]
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
"name": "reborn-drawer",
|
|
1231
|
+
"dependencies": [],
|
|
1232
|
+
"files": [
|
|
1233
|
+
{
|
|
1234
|
+
"path": "index.ts",
|
|
1235
|
+
"content": "export { default as RebornDrawer } from './RebornDrawer.vue';\r\n"
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
"path": "RebornDrawer.vue",
|
|
1239
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed } from 'vue';\r\nimport RebornOverlay from '../reborn-overlay/RebornOverlay.vue';\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue';\r\n\r\nconst props = withDefaults(defineProps<{ modelValue?: boolean; placement?: 'left'|'right'|'top'|'bottom'; size?: string; zIndex?: number; closeOnClickOverlay?: boolean; lockScroll?: boolean; }>(), { modelValue: false, placement: 'right', size: '320px', zIndex: 20, closeOnClickOverlay: true, lockScroll: true });\r\nconst emit = defineEmits(['update:modelValue', 'close', 'open']);\r\n\r\nconst transitionName = computed(() => ({ left:'slide-right', right:'slide-right', top:'fade-down', bottom:'slide-up' }[props.placement] as any));\r\nconst panelStyle = computed(() => {\r\n if (props.placement === 'left' || props.placement === 'right') return `${props.placement}:0;top:0;height:100%;width:${props.size};`;\r\n return `${props.placement}:0;left:0;width:100%;height:${props.size};`;\r\n});\r\n</script>\r\n<template>\r\n <RebornOverlay :model-value=\"props.modelValue\" :z-index=\"props.zIndex\" :close-on-click-overlay=\"props.closeOnClickOverlay\" :lock-scroll=\"props.lockScroll\" @update:model-value=\"emit('update:modelValue', $event)\">\r\n <RebornTransition :show=\"props.modelValue\" :name=\"transitionName\" custom-class=\"pointer-events-auto fixed bg-white dark:bg-gray-900 shadow-xl\" :custom-style=\"`z-index:${props.zIndex + 1};${panelStyle}`\" @after-enter=\"emit('open')\" @after-leave=\"emit('close')\">\r\n <slot />\r\n </RebornTransition>\r\n </RebornOverlay>\r\n</template>\r\n",
|
|
1240
|
+
"target": "web"
|
|
1241
|
+
}
|
|
1242
|
+
]
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
"name": "reborn-dropdown-select",
|
|
1246
|
+
"dependencies": [],
|
|
1247
|
+
"files": [
|
|
1248
|
+
{
|
|
1249
|
+
"path": "reborn-dropdown-select.config.ts",
|
|
1250
|
+
"content": "const colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst sizes = ['sm', 'md', 'lg'] as const\r\n\r\nconst config = {\r\n slots: {\r\n wrapper: 'relative w-full',\r\n trigger: 'w-full',\r\n content: 'absolute top-full left-0 right-0 bg-white dark:bg-gray-8 border border-gray-2 dark:border-gray-7 rounded-md shadow-lg z-[999] max-h-[400rpx] overflow-y-auto mt-1',\r\n item: 'py-2 px-3 text-sm text-gray-8 dark:text-gray-2 border-b border-gray-100 dark:border-gray-7 last:border-b-0 active:bg-gray-50 dark:active:bg-gray-7 flex items-center justify-between',\r\n itemText: 'flex-1 truncate',\r\n itemIcon: 'w-4 h-4 text-blue-6 dark:text-blue-400',\r\n empty: 'py-3 text-center text-gray-400 text-sm',\r\n mask: 'fixed inset-0 z-[998]',\r\n },\r\n variants: {\r\n color: {\r\n primary: {\r\n item: 'active:bg-primary/10',\r\n itemIcon: 'text-primary',\r\n },\r\n secondary: {\r\n item: 'active:bg-secondary/10',\r\n itemIcon: 'text-secondary',\r\n },\r\n success: {\r\n item: 'active:bg-success/10',\r\n itemIcon: 'text-success',\r\n },\r\n info: {\r\n item: 'active:bg-info/10',\r\n itemIcon: 'text-info',\r\n },\r\n warning: {\r\n item: 'active:bg-warning/10',\r\n itemIcon: 'text-warning',\r\n },\r\n error: {\r\n item: 'active:bg-error/10',\r\n itemIcon: 'text-error',\r\n },\r\n neutral: {\r\n item: 'active:bg-neutral/10',\r\n itemIcon: 'text-neutral',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n item: 'py-1.5 px-2 text-xs',\r\n },\r\n md: {\r\n item: 'py-2 px-3 text-sm',\r\n },\r\n lg: {\r\n item: 'py-3 px-4 text-base',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: 'opacity-50 pointer-events-none',\r\n },\r\n },\r\n selected: {\r\n true: {\r\n itemText: 'font-medium',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: 'primary' as (typeof colors)[number], selected: true as const, class: { item: 'bg-primary/10 text-primary' } },\r\n { color: 'secondary' as (typeof colors)[number], selected: true as const, class: { item: 'bg-secondary/10 text-secondary' } },\r\n { color: 'success' as (typeof colors)[number], selected: true as const, class: { item: 'bg-success/10 text-success' } },\r\n { color: 'info' as (typeof colors)[number], selected: true as const, class: { item: 'bg-info/10 text-info' } },\r\n { color: 'warning' as (typeof colors)[number], selected: true as const, class: { item: 'bg-warning/10 text-warning' } },\r\n { color: 'error' as (typeof colors)[number], selected: true as const, class: { item: 'bg-error/10 text-error' } },\r\n { color: 'neutral' as (typeof colors)[number], selected: true as const, class: { item: 'bg-neutral/10 text-neutral' } },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as (typeof colors)[number],\r\n size: 'md' as (typeof sizes)[number],\r\n disabled: false,\r\n selected: false,\r\n },\r\n}\r\n\r\nexport { colors as dropdownSelectColors, sizes as dropdownSelectSizes }\r\nexport default config\r\n",
|
|
1251
|
+
"target": "uniapp"
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
"path": "RebornDropdownSelect.vue",
|
|
1255
|
+
"content": "<script setup lang=\"ts\">\r\nimport { ref, computed } from 'vue'\r\nimport RebornSelectTrigger from '@/components/reborn-select-trigger/RebornSelectTrigger.vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type dropdownSelectColors, type dropdownSelectSizes } from './reborn-dropdown-select.config'\r\n\r\ndefineOptions({\r\n name: 'RebornDropdownSelect',\r\n})\r\n\r\ninterface Option {\r\n label: string\r\n value: any\r\n}\r\n\r\ninterface Props {\r\n modelValue?: any\r\n options?: Option[]\r\n placeholder?: string\r\n disabled?: boolean\r\n size?: typeof dropdownSelectSizes[number]\r\n color?: typeof dropdownSelectColors[number]\r\n clearable?: boolean\r\n ui?: Partial<{\r\n wrapper?: string\r\n trigger?: string\r\n content?: string\r\n item?: string\r\n itemText?: string\r\n itemIcon?: string\r\n empty?: string\r\n mask?: string\r\n }>\r\n customClass?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n options: () => [],\r\n placeholder: '请选择',\r\n disabled: false,\r\n size: 'md',\r\n color: 'primary',\r\n clearable: false,\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\nconst isOpen = ref(false)\r\n\r\nconst selectedLabel = computed(() => {\r\n const option = props.options.find(opt => opt.value === props.modelValue)\r\n return option ? option.label : ''\r\n})\r\n\r\n// ui 样式系统\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n size: props.size,\r\n disabled: props.disabled,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, props.ui?.wrapper) }),\r\n trigger: (opts?: { class?: any }) => styles.trigger({ class: cn(opts?.class, props.ui?.trigger) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, props.ui?.content) }),\r\n item: (opts?: { class?: any; selected?: boolean }) => styles.item({ selected: opts?.selected, class: cn(opts?.class, props.ui?.item) }),\r\n itemText: (opts?: { class?: any; selected?: boolean }) => styles.itemText({ selected: opts?.selected, class: cn(opts?.class, props.ui?.itemText) }),\r\n itemIcon: (opts?: { class?: any }) => styles.itemIcon({ class: cn(opts?.class, props.ui?.itemIcon) }),\r\n empty: (opts?: { class?: any }) => styles.empty({ class: cn(opts?.class, props.ui?.empty) }),\r\n mask: (opts?: { class?: any }) => styles.mask({ class: cn(opts?.class, props.ui?.mask) }),\r\n }\r\n})\r\n\r\nfunction toggleDropdown() {\r\n if (props.disabled) return\r\n isOpen.value = !isOpen.value\r\n}\r\n\r\nfunction closeDropdown() {\r\n isOpen.value = false\r\n}\r\n\r\nfunction selectOption(option: Option) {\r\n emit('update:modelValue', option.value)\r\n emit('change', option.value)\r\n closeDropdown()\r\n}\r\n\r\nfunction onClear() {\r\n emit('update:modelValue', null)\r\n emit('change', null)\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\">\r\n <RebornSelectTrigger :customClass=\"ui.trigger()\" :text=\"selectedLabel\" :placeholder=\"placeholder\"\r\n :disabled=\"disabled\" :size=\"size\" :color=\"color\" :focus=\"isOpen\" :clearable=\"clearable\" @open=\"toggleDropdown\"\r\n @clear=\"onClear\" />\r\n\r\n <!-- Mask to close dropdown -->\r\n <view v-if=\"isOpen\" :class=\"ui.mask()\" @tap=\"closeDropdown\" />\r\n\r\n <view v-if=\"isOpen\" :class=\"ui.content()\">\r\n <view v-for=\"(item, index) in options\" :key=\"index\" :class=\"ui.item({ selected: item.value === modelValue })\"\r\n @tap.stop=\"selectOption(item)\">\r\n <text :class=\"ui.itemText({ selected: item.value === modelValue })\">{{ item.label }}</text>\r\n <text v-if=\"item.value === modelValue\" class=\"i-lucide-check\" :class=\"ui.itemIcon()\" />\r\n </view>\r\n <view v-if=\"options.length === 0\" :class=\"ui.empty()\">\r\n 无数据\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1256
|
+
"target": "uniapp"
|
|
1257
|
+
}
|
|
1258
|
+
]
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
"name": "reborn-footer",
|
|
1262
|
+
"dependencies": [
|
|
1263
|
+
"clsx"
|
|
1264
|
+
],
|
|
1265
|
+
"files": [
|
|
1266
|
+
{
|
|
1267
|
+
"path": "index.ts",
|
|
1268
|
+
"content": "export { default as RebornFooter } from './RebornFooter.vue'\r\n",
|
|
1269
|
+
"target": "web"
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
"path": "RebornFooter.vue",
|
|
1273
|
+
"content": "<template>\r\n <div class=\"rounded-lg border border-dashed border-gray-300 p-4 text-sm text-gray-500 dark:border-gray-700 dark:text-gray-300\">\r\n web端暂未开发\r\n </div>\r\n</template>\r\n",
|
|
1274
|
+
"target": "web"
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
"path": "index.ts",
|
|
1278
|
+
"content": "export { rebornFooterOffset } from './offset'\r\nexport { default as RebornFooter } from './RebornFooter.vue'\r\n",
|
|
1279
|
+
"target": "uniapp"
|
|
1280
|
+
},
|
|
1281
|
+
{
|
|
1282
|
+
"path": "offset.ts",
|
|
1283
|
+
"content": "import { ref } from 'vue'\r\n\r\nconst height = ref(0)\r\n\r\nexport const rebornFooterOffset = {\r\n height,\r\n set(val: number) {\r\n height.value = val\r\n },\r\n}\r\n",
|
|
1284
|
+
"target": "uniapp"
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
"path": "reborn-footer.config.ts",
|
|
1288
|
+
"content": "export default {\r\n slots: {\r\n placeholder: 'w-full',\r\n wrapper: 'fixed bottom-0 left-0 z-70 w-full overflow-visible',\r\n base: 'overflow-visible pb-[env(safe-area-inset-bottom)] bg-white dark:bg-gray-8',\r\n content: 'overflow-visible px-3 py-3',\r\n },\r\n} as const\r\n",
|
|
1289
|
+
"target": "uniapp"
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
"path": "RebornFooter.vue",
|
|
1293
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'\r\nimport { isHarmony } from '@/lib/device'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { rebornFooterOffset } from './offset'\r\nimport theme from './reborn-footer.config'\r\n\r\nexport interface RebornFooterProps {\r\n // 最小高度,小于该高度时,不显示\r\n minHeight?: number\r\n // 监听值,触发更新\r\n vt?: number\r\n // 内容高度\r\n height?: number | null\r\n ui?: {\r\n placeholder?: ClassValue\r\n wrapper?: ClassValue\r\n base?: ClassValue\r\n content?: ClassValue\r\n }\r\n}\r\n\r\ndefineOptions({\r\n name: 'RebornFooter',\r\n})\r\nconst props = withDefaults(defineProps<RebornFooterProps>(), {\r\n minHeight: 30,\r\n vt: 0,\r\n height: null,\r\n})\r\n\r\nconst { proxy } = getCurrentInstance()!\r\n\r\nconst b = tv(theme)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b()\r\n\r\n return {\r\n placeholder: (opts?: { class?: any }) => styles.placeholder({ class: cn(opts?.class, uiOverrides.value.placeholder) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n }\r\n})\r\n\r\nconst placeholderHeight = ref(0)\r\nconst visible = ref(true)\r\n\r\nconst contentStyle = computed(() => {\r\n const style: Record<string, string> = {}\r\n\r\n if (props.height != null) {\r\n style.height = `${props.height}px`\r\n }\r\n\r\n return style\r\n})\r\n\r\nfunction getSafeAreaHeight(type: 'top' | 'bottom') {\r\n const { safeAreaInsets } = uni.getWindowInfo()\r\n\r\n if (type === 'top') {\r\n return safeAreaInsets.top\r\n }\r\n\r\n let h = safeAreaInsets.bottom\r\n\r\n // #ifdef APP-ANDROID\r\n if (h === 0) {\r\n h = 16\r\n }\r\n // #endif\r\n\r\n return h\r\n}\r\n\r\nfunction setHeight(val: number) {\r\n placeholderHeight.value = val + 5\r\n visible.value = val > props.minHeight + getSafeAreaHeight('bottom')\r\n rebornFooterOffset.set(visible.value ? val : 0)\r\n}\r\n\r\nfunction getHeight() {\r\n if (props.height != null) {\r\n setHeight(props.height + getSafeAreaHeight('bottom'))\r\n return\r\n }\r\n\r\n nextTick(() => {\r\n setTimeout(() => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select('#reborn-footer-node')\r\n .boundingClientRect((res) => {\r\n setHeight(Math.floor((res as NodeInfo).height ?? 0))\r\n })\r\n .exec()\r\n }, isHarmony() ? 50 : 0)\r\n })\r\n}\r\n\r\nonMounted(() => {\r\n watch(\r\n () => props.vt,\r\n () => {\r\n visible.value = true\r\n getHeight()\r\n },\r\n { immediate: true },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view v-if=\"visible\" :class=\"ui.placeholder()\" :style=\"{ height: `${placeholderHeight}px` }\" />\r\n\r\n <view :class=\"ui.wrapper()\">\r\n <view v-if=\"visible\" id=\"reborn-footer-node\" :class=\"ui.base()\">\r\n <view :class=\"ui.content()\" :style=\"contentStyle\">\r\n <slot />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1087
1294
|
"target": "uniapp"
|
|
1088
1295
|
}
|
|
1089
1296
|
]
|
|
@@ -1116,24 +1323,29 @@
|
|
|
1116
1323
|
"content": "<template>\r\n <div class=\"mb-4\">\r\n RebornFormItem (Web) - Under Development\r\n <slot />\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ndefineOptions({\r\n name: \"RebornFormItem\"\r\n})\r\n</script>\r\n",
|
|
1117
1324
|
"target": "web"
|
|
1118
1325
|
},
|
|
1326
|
+
{
|
|
1327
|
+
"path": "index.ts",
|
|
1328
|
+
"content": "export { default as RebornForm } from './RebornForm.vue'\r\nexport { default as RebornFormItem } from './RebornFormItem.vue'\r\n",
|
|
1329
|
+
"target": "uniapp"
|
|
1330
|
+
},
|
|
1119
1331
|
{
|
|
1120
1332
|
"path": "reborn-form-item.config.ts",
|
|
1121
|
-
"content": "const size = [
|
|
1333
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst labelPositions = ['left', 'top', 'right'] as const\r\nexport default {\r\n slots: {\r\n root: 'flex gap-2 mb-4',\r\n wrapper: 'flex-1',\r\n label: 'text-gray-8 dark:text-gray-1 font-medium flex items-center shrink-0',\r\n content: 'relative w-full flex-1',\r\n error: 'text-xs text-red-500 mt-1 animate-in slide-in-from-top-1 fade-in duration-200',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n label: 'text-26',\r\n },\r\n md: {\r\n label: 'text-28',\r\n },\r\n lg: {\r\n label: 'text-30',\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: 're-form-item--error',\r\n },\r\n },\r\n labelPosition: {\r\n left: {\r\n root: 'flex-row',\r\n label: 'justify-start text-left',\r\n },\r\n right: {\r\n root: 'flex-row',\r\n label: 'justify-end text-right',\r\n },\r\n top: {\r\n root: 'flex-col items-stretch',\r\n label: 'justify-start text-left w-full',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm',\r\n error: false,\r\n labelPosition: 'left',\r\n },\r\n} as const\r\n\r\nexport { size as formItemLabelSize, labelPositions as formItemLabelPositions }\r\n",
|
|
1122
1334
|
"target": "uniapp"
|
|
1123
1335
|
},
|
|
1124
1336
|
{
|
|
1125
1337
|
"path": "reborn-form.config.ts",
|
|
1126
|
-
"content": "const labelPositions = [
|
|
1338
|
+
"content": "const labelPositions = ['left', 'top', 'right'] as const\r\n\r\nexport { labelPositions as formLabelPositions }\r\n\r\nexport default {\r\n slots: {\r\n root: 'w-full',\r\n },\r\n}\r\n",
|
|
1127
1339
|
"target": "uniapp"
|
|
1128
1340
|
},
|
|
1129
1341
|
{
|
|
1130
1342
|
"path": "RebornForm.vue",
|
|
1131
|
-
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from
|
|
1343
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type * as z from 'zod'\r\nimport type { formLabelPositions } from './reborn-form.config'\r\nimport type { FormValidateError } from '@/composables/useFieldGroup'\r\nimport { computed, onMounted, provide, ref, watch } from 'vue'\r\nimport { useFieldGroup } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-form.config'\r\n\r\nexport type { FormValidateError }\r\n\r\nexport interface FormRule {\r\n required?: boolean\r\n message?: string\r\n validator: (value: any) => boolean | string\r\n trigger?: string\r\n}\r\n\r\nexport interface FromProps {\r\n customClass?: ClassValue\r\n modelValue: any\r\n rules?: z.ZodObject<{ [key: string]: any }, any>\r\n labelPosition?: typeof formLabelPositions[number] // 标签位置\r\n labelWidth?: string | number // 标签宽度\r\n hideRequiredAsterisk?: boolean // 是否隐藏必填符号\r\n requireAsteriskPosition?: 'left' | 'right' // 必填符号位置\r\n showMessage?: boolean // 是否显示错误信息\r\n inlineMessage?: boolean // 是否内联显示错误信息\r\n statusIcon?: boolean // 是否在输入框中显示校验结果反馈图标\r\n validateOnRuleChange?: boolean // 是否在规则改变时重新验证\r\n size?: '' | 'sm' | 'md' | 'lg' // 表单大小\r\n disabled?: boolean // 是否禁用\r\n scrollToError?: boolean // 是否滚动到错误信息\r\n trigger?: 'blur' | 'change' | 'none' | Array<'blur' | 'change'> // 触发验证\r\n ui?: Partial<{\r\n root: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<FromProps>(), {\r\n modelValue: () => ({}),\r\n labelPosition: 'left',\r\n labelWidth: '140rpx',\r\n hideRequiredAsterisk: false,\r\n requireAsteriskPosition: 'left',\r\n showMessage: true,\r\n inlineMessage: false,\r\n statusIcon: false,\r\n validateOnRuleChange: true,\r\n size: '',\r\n disabled: false,\r\n scrollToError: true,\r\n trigger: 'none',\r\n})\r\n\r\nconst b = tv(theme)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b()\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n }\r\n})\r\n\r\n// 使用 Composable获取状态管理能力\r\nconst {\r\n fields,\r\n fieldInstances,\r\n errors,\r\n addField,\r\n removeField,\r\n setError,\r\n removeError,\r\n getError,\r\n getErrors,\r\n clearValidate,\r\n} = useFieldGroup()\r\n\r\nconst initialModel = ref<any>({})\r\nconst data = ref({} as any)// 滚动距离\r\nconst scrollTop = ref(0)\r\n\r\n// 滚动到指定位置\r\nfunction scrollTo(top: number) {\r\n // #ifdef H5\r\n window.scrollTo({ top, behavior: 'smooth' })\r\n // #endif\r\n\r\n // #ifdef MP\r\n uni.pageScrollTo({\r\n scrollTop: top,\r\n duration: 300,\r\n })\r\n // #endif\r\n\r\n // #ifdef APP\r\n uni.pageScrollTo({\r\n scrollTop: top,\r\n duration: 0,\r\n })\r\n // #endif\r\n}\r\n\r\n// 回到顶部\r\nfunction scrollToTop() {\r\n scrollTo(0 + Math.random() / 1000)\r\n}\r\n\r\nfunction parseToObject<T>(val: T) {\r\n return JSON.parse(JSON.stringify(val || {}))\r\n}\r\n\r\n// 设置初始值\r\nfunction setInitialValues(values: any) {\r\n initialModel.value = parseToObject(values)\r\n}\r\n\r\n// 验证单个字段\r\nasync function validateField(prop: string): Promise<string | null> {\r\n let error = null as string | null\r\n\r\n if (prop != '') {\r\n // Zod check\r\n const parts = prop.split('-')\r\n\r\n // Nested logic: contacts-0-name\r\n if (parts.length >= 3 && !isNaN(Number(parts[1]))) {\r\n const [key, indexStr, fieldName] = parts\r\n const index = Number(indexStr)\r\n\r\n if (fieldName && props.rules && props.rules.shape[key]) {\r\n const itemSchema = props.rules.shape[key].element\r\n const schema = itemSchema?.pick({ [fieldName]: true })\r\n\r\n const list = data.value[key]\r\n if (Array.isArray(list) && list[index]) {\r\n const result = await schema.safeParseAsync(list[index])\r\n if (!result.success) {\r\n const issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === fieldName)\r\n if (issue) { error = issue.message }\r\n }\r\n }\r\n }\r\n }\r\n else {\r\n if (props.rules) {\r\n const schema = props.rules.pick({ [prop]: true })\r\n if (schema) {\r\n const result = await schema.safeParseAsync(data.value)\r\n if (!result.success) {\r\n const issue = result.error.issues.find((i: any) => i.path.length === 1 && i.path[0] === prop)\r\n if (issue) { error = issue.message }\r\n }\r\n }\r\n }\r\n }\r\n\r\n removeError(prop)\r\n }\r\n\r\n if (error != null) {\r\n setError(prop, error!)\r\n }\r\n\r\n return error\r\n}\r\n\r\n// 滚动到字段\r\nfunction scrollToField(prop: string) {\r\n const field = fieldInstances.value.find(f => f.prop === prop)\r\n if (field && field.getBoundingClientRect) {\r\n field.getBoundingClientRect((res: any) => {\r\n if (res) {\r\n scrollTo(res.top + (fields.value.size > 0 ? 0 : 0) + scrollTop.value)\r\n }\r\n })\r\n }\r\n}\r\n\r\n// 验证整个表单\r\nfunction validate(callback?: (valid: boolean, errors: FormValidateError[]) => void): Promise<boolean> {\r\n return new Promise(async (resolve) => {\r\n const promises = Array.from(fields.value).map(prop => validateField(prop))\r\n await Promise.all(promises)\r\n\r\n const currentErrors = await getErrors()\r\n\r\n if (currentErrors.length > 0 && props.scrollToError) {\r\n const errorInstances = fieldInstances.value.filter(f => errors.value[f.prop] !== undefined)\r\n if (errorInstances.length > 0) {\r\n scrollToField(errorInstances[0].prop)\r\n }\r\n }\r\n\r\n const isValid = currentErrors.length === 0\r\n if (callback) { callback(isValid, currentErrors) }\r\n resolve(isValid)\r\n })\r\n}\r\n\r\n// 重置表单\r\nfunction resetFields() {\r\n if (!props.modelValue) { return }\r\n clearValidate()\r\n const initData = initialModel.value\r\n\r\n Object.keys(props.modelValue).forEach((key) => {\r\n if (key in initData) {\r\n props.modelValue[key] = initData[key]\r\n }\r\n })\r\n}\r\n\r\n// 监听数据变化\r\nwatch(() => props.modelValue, (val) => {\r\n data.value = val\r\n}, { immediate: true, deep: true })\r\n\r\nonMounted(() => {\r\n if (props.modelValue) {\r\n setInitialValues(props.modelValue)\r\n }\r\n})\r\n\r\n// 提供 Context 给子组件 (包括 Props)\r\nprovide('rebornForm', {\r\n props,\r\n addField: (f: any) => {\r\n addField(f)\r\n },\r\n removeField,\r\n getError,\r\n validateField,\r\n})\r\n\r\nfunction getField(prop: string) {\r\n return fieldInstances.value.find(f => f.prop === prop)\r\n}\r\n\r\ndefineExpose({\r\n validate,\r\n validateField,\r\n clearValidate,\r\n resetFields,\r\n scrollToField,\r\n fields,\r\n getField,\r\n setInitialValues,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
1132
1344
|
"target": "uniapp"
|
|
1133
1345
|
},
|
|
1134
1346
|
{
|
|
1135
1347
|
"path": "RebornFormItem.vue",
|
|
1136
|
-
"content": "<
|
|
1348
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { formItemLabelPositions } from './reborn-form-item.config'\r\nimport { computed, provide } from 'vue'\r\nimport { useFieldGroupItem } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-form-item.config'\r\n\r\ndefineOptions({\r\n name: 'ReFormItem',\r\n})\r\n\r\nconst props = withDefaults(defineProps<FormItemProps>(), {\r\n prop: '',\r\n label: '',\r\n customClass: '',\r\n required: false,\r\n requireAsteriskPosition: 'right',\r\n})\r\n\r\nexport interface FormItemProps {\r\n prop?: string\r\n label?: string\r\n required?: boolean\r\n requireAsteriskPosition?: 'left' | 'right' // 标签位置\r\n labelWidth?: string | number // 标签宽度\r\n labelPosition?: typeof formItemLabelPositions[number] // 标签位置\r\n trigger?: 'blur' | 'change' | 'none' | Array<'blur' | 'change'> // 触发验证\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n label: ClassValue\r\n wrapper: ClassValue\r\n content: ClassValue\r\n error: ClassValue\r\n }>\r\n}\r\n\r\nconst {\r\n error,\r\n labelPosition,\r\n labelWidth,\r\n size,\r\n getBoundingClientRect,\r\n validate,\r\n} = useFieldGroupItem({\r\n prop: props.prop,\r\n labelPosition: props.labelPosition,\r\n labelWidth: props.labelWidth,\r\n trigger: props.trigger,\r\n})\r\n\r\nprovide('rebornFormItem', {\r\n isError: computed(() => !!error.value),\r\n validate,\r\n})\r\n\r\nconst labelStyle = computed(() => {\r\n return {\r\n width: labelWidth.value,\r\n }\r\n})\r\n\r\n// Configure Component Styles\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n error: !!error.value,\r\n labelPosition: labelPosition.value as any, // Cast to match config variants\r\n size: size.value as any,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n content: (opts?: { class?: any }) => styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n error: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n }\r\n})\r\n\r\ndefineExpose({\r\n prop: props.prop,\r\n getBoundingClientRect,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"re-form-item\" :class=\"ui.root({ class: customClass })\">\r\n <view v-if=\"label || $slots.label\" :class=\"ui.label()\" :style=\"labelStyle\">\r\n <slot name=\"label\">\r\n <text\r\n v-if=\"required && requireAsteriskPosition === 'left'\" class=\"\r\n mr-1 text-red-500\r\n \"\r\n >\r\n *\r\n </text>\r\n {{ label }}\r\n <text\r\n v-if=\"required && requireAsteriskPosition === 'right'\" class=\"\r\n ml-1 text-red-500\r\n \"\r\n >\r\n *\r\n </text>\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.wrapper()\">\r\n <view :class=\"ui.content()\">\r\n <slot />\r\n </view>\r\n\r\n <view v-if=\"error\" :class=\"ui.error()\">\r\n {{ error }}\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1137
1349
|
"target": "uniapp"
|
|
1138
1350
|
}
|
|
1139
1351
|
]
|
|
@@ -1148,7 +1360,7 @@
|
|
|
1148
1360
|
"files": [
|
|
1149
1361
|
{
|
|
1150
1362
|
"path": "reborn-image.config.ts",
|
|
1151
|
-
"content": "const mode = [\"scaleToFill\", \"aspectFit\", \"aspectFill\", \"widthFix\", \"heightFix\", \"top\", \"bottom\", \"center\", \"left\", \"right\", \"top left\", \"top right\", \"bottom left\", \"bottom right\"]\r\nexport default {\r\n slots: {\r\n root: \"relative flex flex-row items-center justify-center rounded-xl overflow-hidden\",\r\n error: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n errorIcon: \"text-gray-4 size-8 icon-[lucide--image-off]\",\r\n loading: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n loadingIcon: \"text-
|
|
1363
|
+
"content": "const mode = [\"scaleToFill\", \"aspectFit\", \"aspectFill\", \"widthFix\", \"heightFix\", \"top\", \"bottom\", \"center\", \"left\", \"right\", \"top left\", \"top right\", \"bottom left\", \"bottom right\"]\r\nexport default {\r\n slots: {\r\n root: \"relative flex flex-row items-center justify-center rounded-xl overflow-hidden\",\r\n error: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n errorIcon: \"text-gray-4 size-8 icon-[lucide--image-off]\",\r\n loading: \"absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center\",\r\n loadingIcon: \"text-gray-4 size-8 border-2 border-gray-3 border-t-blue-500 rounded-full animate-spin\",\r\n inner: \"w-full h-full\",\r\n },\r\n}\r\n\r\nexport { mode as imageMode }",
|
|
1152
1364
|
"target": "web"
|
|
1153
1365
|
},
|
|
1154
1366
|
{
|
|
@@ -1160,14 +1372,19 @@
|
|
|
1160
1372
|
"path": "RefactorPlan.md",
|
|
1161
1373
|
"content": "# RebornImage State Handling Refactor\r\n\r\n## Problem\r\n`vue-lazyload` manages image loading internally. When an image fails, it sets the `lazy=\"error\"` attribute on the `img` element but does not necessarily bubble a standard `@error` event to the Vue component, causing `isError` state to remain `false`.\r\n\r\n## Solution\r\nUse CSS-based state control via Tailwind's `peer` utility for `v-lazy`, while maintaining JS-based control for standard loading.\r\n\r\n### Changes\r\n1. **Reorder DOM**: Move `<img>` to the beginning of the container so it can be a `peer` to the overlays.\r\n2. **Add `peer` class**: Add `peer` to both standard and lazy `img` tags.\r\n3. **Update Overlays**:\r\n - Change `v-if` to `v-show` or class binding to keep elements in DOM (required for peer).\r\n - Add Tailwind peer modifiers:\r\n - Error Overlay: `peer-[lazy=error]:flex` (and ensure it's hidden by default/controlled by `isError`).\r\n - Loading Overlay: `peer-[lazy=loading]:flex` (and controlled by `isLoading`).\r\n4. **Compatibility**: Ensure `isError`/`isLoading` refs still work for non-lazy mode.\r\n\r\n## Implementation Details\r\n- `img` tag needs `z-0` or similar? Default stacking: later siblings on top. So `img` first is perfect for `absolute` overlays on top.\r\n- Error Overlay Class: `hidden peer-[lazy=error]:flex` (if relying purely on CSS for lazy) + `flex` if `isError` is true.\r\n - Combined: `cn(..., { 'flex': isError, 'hidden': !isError && !lazyLoad, 'peer-[lazy=error]:flex': lazyLoad })`\r\n - Simplest: Always render, default hidden. Show if `isError` OR `peer-lazy=error`.\r\n - Class: `absolute ... hidden peer-[lazy=error]:flex` ?\r\n - Vue logic: `:class=\"{ '!flex': isError }\"` (using `!` to override hidden).\r\n\r\n### Plan\r\n1. Move `img` tags to top of `div`.\r\n2. Add `peer` to `img`.\r\n3. Update `error` and `loading` divs to use peer classes.\r\n"
|
|
1162
1374
|
},
|
|
1375
|
+
{
|
|
1376
|
+
"path": "index.ts",
|
|
1377
|
+
"content": "export { default as RebornImage } from './RebornImage.vue'\r\n",
|
|
1378
|
+
"target": "uniapp"
|
|
1379
|
+
},
|
|
1163
1380
|
{
|
|
1164
1381
|
"path": "reborn-image.config.ts",
|
|
1165
|
-
"content": "const mode = [
|
|
1382
|
+
"content": "const mode = ['scaleToFill', 'aspectFit', 'aspectFill', 'widthFix', 'heightFix', 'top', 'bottom', 'center', 'left', 'right', 'top left', 'top right', 'bottom left', 'bottom right']\r\nexport default {\r\n slots: {\r\n root: 'relative flex flex-row items-center justify-center overflow-hidden',\r\n error: 'absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center',\r\n errorIcon: 'text-gray-4',\r\n loading: 'absolute h-full w-full bg-gray-2 dark:bg-gray-7 flex flex-col items-center justify-center',\r\n loadingIcon: 'text-gray-4',\r\n inner: 'w-full h-full',\r\n },\r\n variants: {\r\n round: {\r\n true: {\r\n root: 'rounded-xl',\r\n },\r\n },\r\n },\r\n}\r\n\r\nexport { mode as imageMode }\r\n",
|
|
1166
1383
|
"target": "uniapp"
|
|
1167
1384
|
},
|
|
1168
1385
|
{
|
|
1169
1386
|
"path": "RebornImage.vue",
|
|
1170
|
-
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from
|
|
1387
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { imageMode } from './reborn-image.config'\r\nimport { computed, ref } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-image.config'\r\n\r\nexport interface ImageProps {\r\n customClass?: ClassValue\r\n src: string\r\n mode?: typeof imageMode[number] // 图片裁剪、缩放的模式\r\n preview?: boolean // 是否显示边框\r\n previewList?: string[]\r\n height?: string | number\r\n width?: string | number\r\n showLoading?: boolean // 是否显示加载状态\r\n lazyLoad?: boolean // 是否懒加载\r\n fadeShow?: boolean // 图片显示动画效果\r\n webp?: boolean // 是否解码webp格式\r\n showMenuByLongpress?: boolean // 是否长按显示菜单\r\n round?: boolean // 是否显示圆角\r\n ui?: Partial<{\r\n root: ClassValue\r\n error: ClassValue\r\n errorIcon: ClassValue\r\n loading: ClassValue\r\n loadingIcon: ClassValue\r\n inner: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<ImageProps>(), {\r\n mode: 'aspectFill',\r\n preview: false,\r\n height: 120,\r\n width: 120,\r\n showLoading: true,\r\n lazyLoad: false,\r\n webp: false,\r\n showMenuByLongpress: false,\r\n round: false,\r\n})\r\n\r\n// 事件定义\r\nconst emit = defineEmits(['load', 'error'])\r\n\r\nconst b = tv(theme)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n round: props.round,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n error: (opts?: { class?: any }) => styles.error({ class: cn(opts?.class, uiOverrides.value.error) }),\r\n errorIcon: (opts?: { class?: any }) => styles.errorIcon({ class: cn(opts?.class, uiOverrides.value.errorIcon) }),\r\n loading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n loadingIcon: (opts?: { class?: any }) => styles.loadingIcon({ class: cn(opts?.class, uiOverrides.value.loadingIcon) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n }\r\n})\r\n\r\n// 加载状态\r\nconst isLoading = ref(true)\r\n// 加载失败状态\r\nconst isError = ref(false)\r\n\r\nfunction getUnit(val: string | number | undefined | null): string | undefined {\r\n if (val == null || val === '') { return undefined }\r\n\r\n if (typeof val === 'string') {\r\n // 如果包含单位则直接返回,否则补 rpx\r\n const hasUnit = /px|rpx|%|vw|vh$/.test(val)\r\n return hasUnit ? val : `${val}rpx`\r\n }\r\n return `${val}rpx`\r\n}\r\n\r\n// 图片加载成功\r\nfunction onLoad(e: any) {\r\n isLoading.value = false\r\n isError.value = false\r\n emit('load', e)\r\n}\r\n\r\n// 图片加载失败\r\nfunction onError(e: any) {\r\n isLoading.value = false\r\n isError.value = true\r\n emit('error', e)\r\n}\r\n\r\n// 图片点击\r\nfunction onTap() {\r\n if (props.preview) {\r\n // 修正逻辑:优先使用 previewList,如果没有则用当前 src 组成数组\r\n const urls = (props.previewList && props.previewList.length > 0)\r\n ? props.previewList\r\n : [props.src]\r\n\r\n uni.previewImage({\r\n urls, // 此时 urls 必定是 string[]\r\n current: props.src,\r\n })\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" :style=\"{\r\n width: getUnit(width),\r\n height: getUnit(height),\r\n }\">\r\n <view v-if=\"isError\" :class=\"ui.error()\">\r\n <slot name=\"error\">\r\n <!-- <view :class=\"absolute w-[2px] h-full bg-current rotate-45\"></view>\r\n\t\t\t\t<view :class=\"absolute w-[2px] h-full bg-current -rotate-45\"></view> -->\r\n <view :class=\"ui.errorIcon()\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\">\r\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"1.5\">\r\n <path\r\n d=\"m3 16l7-3l4 1.818M16 10a2 2 0 1 1 0-4a2 2 0 0 1 0 4m.879 11.121L19 19m2.121-2.121L19 19m0 0l-2.121-2.121M19 19l2.121 2.121\" />\r\n <path d=\"M13 21H3.6a.6.6 0 0 1-.6-.6V3.6a.6.6 0 0 1 .6-.6h16.8a.6.6 0 0 1 .6.6V13\" />\r\n </g>\r\n </svg>\r\n </view>\r\n </slot>\r\n </view>\r\n <view v-else-if=\"isLoading && showLoading\" :class=\"ui.loading()\">\r\n <slot name=\"loading\">\r\n <view :class=\"ui.loadingIcon()\" class=\"\r\n size-6 animate-spin rounded-full border-2 border-gray-3\r\n border-t-blue-500\r\n \" />\r\n </slot>\r\n </view>\r\n <image :class=\"ui.inner()\" :src=\"src\" :mode=\"mode\" :lazy-load=\"lazyLoad\" :webp=\"webp\"\r\n :show-menu-by-longpress=\"showMenuByLongpress\" @load=\"onLoad\" @error=\"onError\" @tap=\"onTap\" />\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
1171
1388
|
"target": "uniapp"
|
|
1172
1389
|
}
|
|
1173
1390
|
]
|
|
@@ -1193,17 +1410,17 @@
|
|
|
1193
1410
|
},
|
|
1194
1411
|
{
|
|
1195
1412
|
"path": "index.ts",
|
|
1196
|
-
"content": "export { default as RebornInput } from
|
|
1413
|
+
"content": "export { default as RebornInput } from './RebornInput.vue'\r\n",
|
|
1197
1414
|
"target": "uniapp"
|
|
1198
1415
|
},
|
|
1199
1416
|
{
|
|
1200
1417
|
"path": "reborn-input.config.ts",
|
|
1201
|
-
"content": "const sizes = [
|
|
1418
|
+
"content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n wrapper: 'relative flex w-full items-center',\r\n input:\r\n 'flex h-10 w-full rounded-md border border-gray-2 dark:border-gray-7 bg-gray-2 dark:bg-gray-8 px-3 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',\r\n leading: 'absolute left-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground',\r\n trailing: 'absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground',\r\n // Styles for internal icons like clear and password toggle\r\n icon: 'absolute right-3 top-0 bottom-0 flex items-center justify-center text-muted-foreground transition-opacity hover:opacity-80 cursor-pointer z-10',\r\n iconBox: 'absolute bottom-0 right-3 top-0 z-20 flex items-center gap-2',\r\n iconSection: 'flex cursor-pointer items-center justify-center p-1 text-muted-foreground transition-opacity hover:opacity-80',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n input: 'h-input-sm px-3',\r\n },\r\n md: {\r\n input: 'h-input-md px-3',\r\n },\r\n lg: {\r\n input: 'h-input-lg px-8 rounded-md',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-primary',\r\n },\r\n secondary: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-secondary',\r\n },\r\n success: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-success',\r\n },\r\n info: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-info',\r\n },\r\n warning: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-warning',\r\n },\r\n error: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-error',\r\n },\r\n neutral: {\r\n input: 'focus-within:ring-[1.5px] focus-within:ring-gray-4',\r\n },\r\n },\r\n multiline: {\r\n true: {\r\n input: 'h-auto',\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal: {\r\n wrapper: 'first:rounded-r-none last:rounded-l-none',\r\n input: 'first:rounded-r-none last:rounded-l-none focus:z-10',\r\n },\r\n vertical: {\r\n wrapper: 'first:rounded-b-none last:rounded-t-none',\r\n input: 'first:rounded-b-none last:rounded-t-none focus:z-10',\r\n },\r\n },\r\n hasLeading: {\r\n true: {\r\n input: 'pl-9',\r\n },\r\n },\r\n hasTrailing: {\r\n true: {\r\n input: 'pr-9',\r\n },\r\n },\r\n rounded: {\r\n true: {\r\n input: 'rounded-full',\r\n },\r\n false: {\r\n input: 'rounded-md',\r\n },\r\n },\r\n error: {\r\n true: {\r\n input: 'border-error text-error placeholder:text-error/50 focus-visible:ring-error focus-within:ring-error focus-within:border-error ring-error',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'neutral',\r\n rounded: true,\r\n },\r\n} as const\r\n\r\nexport { colors as inputColors, sizes as inputSizes }\r\nexport default config\r\n",
|
|
1202
1419
|
"target": "uniapp"
|
|
1203
1420
|
},
|
|
1204
1421
|
{
|
|
1205
1422
|
"path": "RebornInput.vue",
|
|
1206
|
-
"content": "<script lang=\"ts\">\r\nimport { inputSizes,
|
|
1423
|
+
"content": "<script lang=\"ts\">\r\nimport type { inputColors, inputSizes } from './reborn-input.config'\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, toRef, useSlots, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-input.config'\r\n\r\nexport type InputType\r\n = | 'text'\r\n | 'number'\r\n | 'idcard'\r\n | 'digit'\r\n | 'tel'\r\n | 'safe-password'\r\n | 'nickname'\r\n | 'none'\r\n | 'decimal'\r\n | 'numeric'\r\n | 'search'\r\n | 'email'\r\n | 'url'\r\n\r\nexport type InputUI = {\r\n wrapper?: string\r\n input?: string\r\n leading?: string\r\n trailing?: string\r\n iconBox?: string\r\n clear?: string\r\n password?: string\r\n}\r\n\r\nexport interface InputProps {\r\n modelValue?: string | number\r\n defaultValue?: string | number\r\n placeholder?: string\r\n disabled?: boolean\r\n readonly?: boolean\r\n type?: InputType\r\n size?: typeof inputSizes[number]\r\n rows?: number\r\n customClass?: any\r\n password?: boolean\r\n clearable?: boolean\r\n focus?: boolean\r\n maxlength?: number\r\n cursorSpacing?: number\r\n confirmHold?: boolean\r\n confirmType?: string\r\n adjustPosition?: boolean\r\n holdKeyboard?: boolean\r\n placeholderClass?: string\r\n autofocus?: boolean\r\n rounded?: boolean\r\n color?: typeof inputColors[number]\r\n ui?: InputUI\r\n}\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n})\r\nconst props = withDefaults(defineProps<InputProps>(), {\r\n disabled: false,\r\n readonly: false,\r\n type: 'text',\r\n size: 'md',\r\n rows: 4,\r\n focus: false,\r\n password: false,\r\n maxlength: 140,\r\n cursorSpacing: 5,\r\n confirmHold: false,\r\n confirmType: 'done',\r\n adjustPosition: true,\r\n holdKeyboard: false,\r\n placeholderClass: '',\r\n autofocus: false,\r\n clearable: false,\r\n rounded: true,\r\n color: 'primary',\r\n})\r\nconst emit = defineEmits([\r\n 'update:modelValue',\r\n 'input',\r\n 'change',\r\n 'focus',\r\n 'blur',\r\n 'confirm',\r\n 'clear',\r\n 'keyboardheightchange',\r\n])\r\nconst slots = useSlots()\r\n\r\nconst inputRef = ref<HTMLInputElement | null>(null)\r\nconst localValue = ref(props.defaultValue ?? '')\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus)\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus)\r\n\r\n// 是否显示密码\r\nconst isPassword = ref(props.password)\r\n\r\nconst inputValue = computed(() =>\r\n props.modelValue !== undefined ? props.modelValue : localValue.value,\r\n)\r\n\r\n// 是否显示清除按钮\r\nconst showClear = computed(() => {\r\n return props.clearable && !fieldGroupDisabled.value && !props.readonly && `${inputValue.value}` !== ''\r\n})\r\n\r\nconst isFilled = computed(() => `${inputValue.value ?? ''}`.length > 0)\r\n\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled, isError, validate } = useFormInject(props)\r\n\r\nconst size = toRef(props, 'size')\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n fieldGroup: orientation.value,\r\n hasLeading: !!slots.leading,\r\n hasTrailing: !!slots.trailing || showClear.value || props.password,\r\n rounded: props.rounded,\r\n color: props.color,\r\n error: isError.value,\r\n })\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) =>\r\n styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n leading: (opts?: { class?: any }) =>\r\n styles.leading({ class: cn(opts?.class, uiOverrides.value.leading) }),\r\n trailing: (opts?: { class?: any }) =>\r\n styles.trailing({ class: cn(opts?.class, uiOverrides.value.trailing) }),\r\n iconBox: (opts?: { class?: any }) =>\r\n styles.iconBox({ class: cn(opts?.class, uiOverrides.value.iconBox) }),\r\n clear: (opts?: { class?: any }) =>\r\n styles.iconSection({ class: cn(opts?.class, uiOverrides.value.clear) }),\r\n password: (opts?: { class?: any }) =>\r\n styles.iconSection({ class: cn(opts?.class, uiOverrides.value.password) }),\r\n }\r\n}\r\n)\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value) => {\r\n if (value !== undefined) {\r\n localValue.value = value\r\n }\r\n },\r\n)\r\n\r\n// 输入事件\r\nfunction onInput(e: any) {\r\n const v1 = e.detail.value\r\n localValue.value = v1 // Update local value for uncontrolled usage\r\n emit('update:modelValue', v1)\r\n emit('input', v1)\r\n emit('change', v1)\r\n if (validate) { validate('change') }\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n emit('confirm', e)\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n emit('keyboardheightchange', e)\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n if (fieldGroupDisabled.value || props.readonly) {\r\n isFocusing.value = false\r\n return\r\n };\r\n\r\n setTimeout(() => {\r\n isFocusing.value = false\r\n\r\n nextTick(() => {\r\n isFocusing.value = true\r\n })\r\n }, 0)\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n if (fieldGroupDisabled.value || props.readonly) { return }\r\n isFocus.value = true\r\n emit('focus', e)\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n isFocus.value = false\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n}\r\n// 切换密码显示状态\r\nfunction showPassword() {\r\n if (fieldGroupDisabled.value || props.readonly) { return }\r\n isPassword.value = !isPassword.value\r\n nextTick(() => focus())\r\n}\r\n// 清除方法\r\nfunction clear() {\r\n localValue.value = ''\r\n emit('update:modelValue', '')\r\n emit('change', '')\r\n emit('clear')\r\n\r\n // #ifdef H5\r\n focus()\r\n // #endif\r\n}\r\n\r\ndefineExpose({\r\n isFocus,\r\n focus,\r\n clear,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\" :data-filled=\"isFilled\"\r\n @click=\"focus\">\r\n <view v-if=\"$slots.leading\" :class=\"ui.leading()\">\r\n <slot name=\"leading\" :ui=\"ui\" />\r\n </view>\r\n\r\n <input ref=\"inputRef\" :type=\"props.type\" :disabled=\"fieldGroupDisabled || props.readonly\" :readonly=\"props.readonly\"\r\n :placeholder=\"props.placeholder\" :value=\"inputValue\" :class=\"ui.input()\" :password=\"isPassword\"\r\n :focus=\"isFocusing && !fieldGroupDisabled && !props.readonly\"\r\n :placeholder-class=\"`text-gart-4 ${props.placeholderClass}`\" :maxlength=\"props.maxlength\"\r\n :cursor-spacing=\"props.cursorSpacing\" :confirm-type=\"props.confirmType\" :confirm-hold=\"props.confirmHold\"\r\n :adjust-position=\"props.adjustPosition\" :hold-keyboard=\"props.holdKeyboard\" @input=\"onInput\" @focus=\"onFocus\"\r\n @blur=\"onBlur\" @confirm=\"onConfirm\" @keyboardheightchange=\"onKeyboardheightchange\">\r\n\r\n <view v-if=\"$slots.trailing\" :class=\"ui.trailing()\">\r\n <slot name=\"trailing\" :ui=\"ui\" />\r\n </view>\r\n\r\n <!-- Icons Section -->\r\n <view :class=\"ui.iconBox()\" @tap.stop>\r\n <view v-if=\"showClear\" :class=\"ui.clear()\" @tap.stop=\"clear\">\r\n <view class=\"i-lucide-x-circle size-4\" />\r\n </view>\r\n\r\n <view v-if=\"password\" :class=\"ui.password()\" @tap.stop=\"showPassword\">\r\n <view class=\"size-4\" :class=\"[isPassword ? 'i-lucide-eye' : `\r\n i-lucide-eye-off\r\n `]\" />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1207
1424
|
"target": "uniapp"
|
|
1208
1425
|
}
|
|
1209
1426
|
]
|
|
@@ -1216,7 +1433,8 @@
|
|
|
1216
1433
|
"files": [
|
|
1217
1434
|
{
|
|
1218
1435
|
"path": "index.ts",
|
|
1219
|
-
"content": "export { default as RebornInputNumber } from \"./RebornInputNumber.vue\";\r\n"
|
|
1436
|
+
"content": "export { default as RebornInputNumber } from \"./RebornInputNumber.vue\";\r\n",
|
|
1437
|
+
"target": "web"
|
|
1220
1438
|
},
|
|
1221
1439
|
{
|
|
1222
1440
|
"path": "reborn-input-number.config.ts",
|
|
@@ -1228,163 +1446,949 @@
|
|
|
1228
1446
|
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, toRef, useAttrs, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { inputNumberColors, inputNumberSizes } from \"./reborn-input-number.config\";\r\nimport { useFieldGroup } from \"~/composables/useFieldGroup\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\nexport interface InputNumberProps {\r\n modelValue?: number;\r\n defaultValue?: number;\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n size?: typeof inputNumberSizes[number];\r\n color?: typeof inputNumberColors[number];\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n button: ClassValue;\r\n input: ClassValue;\r\n divider: ClassValue;\r\n icon: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<InputNumberProps>(), {\r\n disabled: false,\r\n step: 1,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: number): void;\r\n}>();\r\n\r\nconst attrs = useAttrs();\r\n\r\nconst localValue = ref(props.defaultValue ?? props.min ?? 0);\r\nconst currentValue = computed(() => (props.modelValue !== undefined ? props.modelValue : localValue.value));\r\n\r\nconst { orientation, size: fieldGroupSize } = useFieldGroup(props);\r\n\r\nconst size = toRef(props, \"size\");\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n color: props.color,\r\n fieldGroup: orientation.value,\r\n });\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n button: (opts?: { class?: any }) => styles.button({ class: cn(opts?.class, uiOverrides.value.button) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, uiOverrides.value.divider) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n };\r\n});\r\n\r\nconst inputAttrs = computed(() => {\r\n const { class: _class, ...rest } = attrs;\r\n return rest;\r\n});\r\n\r\nconst isDecrementDisabled = computed(() =>\r\n props.disabled || (props.min !== undefined && currentValue.value <= props.min),\r\n);\r\n\r\nconst isIncrementDisabled = computed(() =>\r\n props.disabled || (props.max !== undefined && currentValue.value >= props.max),\r\n);\r\n\r\nfunction clampValue(value: number) {\r\n let nextValue = value;\r\n if (props.min !== undefined) {\r\n nextValue = Math.max(nextValue, props.min);\r\n }\r\n if (props.max !== undefined) {\r\n nextValue = Math.min(nextValue, props.max);\r\n }\r\n return nextValue;\r\n}\r\n\r\nfunction updateValue(value: number) {\r\n const nextValue = clampValue(value);\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue;\r\n }\r\n emit(\"update:modelValue\", nextValue);\r\n}\r\n\r\nfunction handleInput(event: Event) {\r\n const target = event.target as HTMLInputElement;\r\n const parsed = Number(target.value);\r\n\r\n if (Number.isNaN(parsed)) {\r\n return;\r\n }\r\n\r\n updateValue(parsed);\r\n}\r\n\r\nfunction increase() {\r\n if (isIncrementDisabled.value) return;\r\n updateValue(currentValue.value + props.step);\r\n}\r\n\r\nfunction decrease() {\r\n if (isDecrementDisabled.value) return;\r\n updateValue(currentValue.value - props.step);\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value) => {\r\n if (value !== undefined) {\r\n localValue.value = value;\r\n }\r\n },\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.wrapper({ class: props.class })\" :data-disabled=\"props.disabled\">\r\n <button type=\"button\" :class=\"ui.button()\" :disabled=\"isDecrementDisabled\" @click=\"decrease\">\r\n <slot name=\"decrement\" :icon-class=\"ui.icon()\">\r\n <Icon name=\"lucide:minus\" :class=\"ui.icon()\" />\r\n </slot>\r\n </button>\r\n\r\n <span :class=\"ui.divider()\" aria-hidden=\"true\" />\r\n\r\n <input v-bind=\"inputAttrs\" type=\"number\" inputmode=\"decimal\" :min=\"props.min\" :max=\"props.max\" :step=\"props.step\"\r\n :value=\"currentValue\" :disabled=\"props.disabled\" :class=\"ui.input()\" @input=\"handleInput\" />\r\n\r\n <span :class=\"ui.divider()\" aria-hidden=\"true\" />\r\n\r\n <button type=\"button\" :class=\"ui.button()\" :disabled=\"isIncrementDisabled\" @click=\"increase\">\r\n <slot name=\"increment\" :icon-class=\"ui.icon()\">\r\n <Icon name=\"lucide:plus\" :class=\"ui.icon()\" />\r\n </slot>\r\n </button>\r\n </div>\r\n</template>\r\n",
|
|
1229
1447
|
"target": "web"
|
|
1230
1448
|
},
|
|
1449
|
+
{
|
|
1450
|
+
"path": "index.ts",
|
|
1451
|
+
"content": "export { default as RebornInputNumber } from './RebornInputNumber.vue'\r\n",
|
|
1452
|
+
"target": "uniapp"
|
|
1453
|
+
},
|
|
1231
1454
|
{
|
|
1232
1455
|
"path": "long-press.ts",
|
|
1233
|
-
"content": "import { onUnmounted, ref
|
|
1456
|
+
"content": "import type { Ref } from 'vue'\r\nimport { onUnmounted, ref } from 'vue'\r\n\r\n// 长按触发延迟时间,单位毫秒\r\nconst DELAY = 500\r\n// 长按重复执行间隔时间,单位毫秒\r\nconst REPEAT = 100\r\n\r\n/**\r\n * 长按操作钩子函数返回类型\r\n */\r\ninterface UseLongPress {\r\n // 开始长按\r\n start: (cb: () => void) => void\r\n // 停止长按\r\n stop: () => void\r\n // 清除定时器\r\n clear: () => void\r\n // 是否正在长按中\r\n isPressing: Ref<boolean>\r\n}\r\n\r\n/**\r\n * 长按操作钩子函数\r\n * 支持长按持续触发,可用于数字输入框等需要连续操作的场景\r\n */\r\nexport function useLongPress(): UseLongPress {\r\n // 是否正在长按中\r\n const isPressing = ref(false)\r\n // 长按延迟定时器\r\n let pressTimer: number = 0\r\n // 重复执行定时器\r\n let repeatTimer: number = 0\r\n\r\n /**\r\n * 清除所有定时器\r\n * 重置长按状态\r\n */\r\n const clear = () => {\r\n // 清除长按延迟定时器\r\n if (pressTimer != 0) {\r\n clearTimeout(pressTimer)\r\n pressTimer = 0\r\n }\r\n // 清除重复执行定时器\r\n if (repeatTimer != 0) {\r\n clearInterval(repeatTimer)\r\n repeatTimer = 0\r\n }\r\n // 重置长按状态\r\n isPressing.value = false\r\n }\r\n\r\n /**\r\n * 开始长按操作\r\n * @param cb 长按时重复执行的回调函数\r\n */\r\n const start = (cb: () => void) => {\r\n // 清除已有定时器\r\n clear()\r\n\r\n // 立即执行一次回调\r\n cb()\r\n\r\n // 延迟500ms后开始长按\r\n // @ts-ignore\r\n pressTimer = setTimeout(() => {\r\n // 设置长按状态\r\n isPressing.value = true\r\n // 每100ms重复执行回调\r\n // @ts-ignore\r\n repeatTimer = setInterval(() => {\r\n cb()\r\n }, REPEAT)\r\n }, DELAY)\r\n }\r\n\r\n /**\r\n * 停止长按操作\r\n * 清除定时器并重置状态\r\n */\r\n const stop = () => {\r\n clear()\r\n }\r\n\r\n // 组件卸载时清理定时器\r\n onUnmounted(() => {\r\n clear()\r\n })\r\n\r\n return {\r\n start,\r\n stop,\r\n clear,\r\n isPressing,\r\n }\r\n}\r\n",
|
|
1234
1457
|
"target": "uniapp"
|
|
1235
1458
|
},
|
|
1236
1459
|
{
|
|
1237
1460
|
"path": "reborn-input-number.config.ts",
|
|
1238
|
-
"content": "const size = [
|
|
1461
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst shape = ['circle', 'square'] as const\r\n\r\nexport { color as inputNumberColors, shape as inputNumberShapes, size as inputNumberSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper:\r\n 'group relative inline-flex items-center overflow-hidden bg-white text-gray-8 ring-1 ring-gray-4 transition-colors focus-within:ring-2 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:bg-gray-1 data-[disabled=true]:text-gray-4 dark:bg-gray-800 dark:text-gray-200 dark:data-[disabled=true]:bg-gray-900 dark:data-[disabled=true]:text-gray-600',\r\n button:\r\n 'flex h-full items-center justify-center text-gray-8 transition-colors disabled:cursor-not-allowed disabled:text-gray-4 dark:text-gray-4 dark:hover:text-gray-2 dark:disabled:text-gray-6',\r\n input:\r\n 'min-w-0 flex-1 bg-transparent text-center text-gray-8 outline-none placeholder:text-gray-4 [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none dark:text-gray-200 dark:placeholder:text-gray-500',\r\n divider: 'h-full w-[1px] bg-gray-4',\r\n icon: 'shrink-0',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n wrapper: 'h-[calc(var(--text-size-26)*2)] text-[length:var(--text-size-26)]',\r\n input: 'w-[calc(var(--text-size-26)*2.5)] text-[length:var(--text-size-26)]',\r\n button: 'p-1.5',\r\n icon: 'size-3.5',\r\n },\r\n md: {\r\n wrapper: 'h-[calc(var(--text-size-28)*2)] text-[length:var(--text-size-28)]',\r\n input: 'w-[calc(var(--text-size-28)*2.5)] text-[length:var(--text-size-28)]',\r\n button: 'p-2',\r\n icon: 'size-4',\r\n },\r\n lg: {\r\n wrapper: 'h-[calc(var(--text-size-32)*2)] text-[length:var(--text-size-32)]',\r\n input: 'w-[calc(var(--text-size-32)*5)] text-[length:var(--text-size-32)]',\r\n button: 'p-2',\r\n icon: 'size-5',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n wrapper: 'focus-within:ring-primary',\r\n button: 'hover:text-primary',\r\n divider: 'group-focus-within:bg-primary',\r\n },\r\n secondary: {\r\n wrapper: 'focus-within:ring-secondary',\r\n button: 'hover:text-secondary',\r\n divider: 'group-focus-within:bg-secondary',\r\n },\r\n success: {\r\n wrapper: 'focus-within:ring-success',\r\n button: 'hover:text-success',\r\n divider: 'group-focus-within:bg-success',\r\n },\r\n info: {\r\n wrapper: 'focus-within:ring-info',\r\n button: 'hover:text-info',\r\n divider: 'group-focus-within:bg-info',\r\n },\r\n warning: {\r\n wrapper: 'focus-within:ring-warning',\r\n button: 'hover:text-warning',\r\n divider: 'group-focus-within:bg-warning',\r\n },\r\n error: {\r\n wrapper: 'focus-within:ring-error',\r\n button: 'hover:text-error',\r\n divider: 'group-focus-within:bg-error',\r\n },\r\n neutral: {\r\n wrapper: 'focus-within:ring-gray-4',\r\n button: 'hover:text-neutral',\r\n divider: 'group-focus-within:bg-gray-4',\r\n },\r\n },\r\n shape: {\r\n circle: {\r\n wrapper: 'rounded-full',\r\n },\r\n square: {\r\n wrapper: 'rounded-md',\r\n },\r\n },\r\n error: {\r\n true: {\r\n wrapper: 'ring-error',\r\n divider: 'bg-error',\r\n },\r\n },\r\n fieldGroup: {\r\n horizontal:\r\n 'not-only:first:rounded-e-none not-only:last:rounded-s-none not-last:not-first:rounded-none focus-within:z-[1]',\r\n vertical:\r\n 'not-only:first:rounded-b-none not-only:last:rounded-t-none not-last:not-first:rounded-none focus-within:z-[1]',\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'sm' as (typeof size)[number],\r\n color: 'neutral' as (typeof color)[number],\r\n shape: 'circle' as (typeof shape)[number],\r\n },\r\n}\r\n",
|
|
1239
1462
|
"target": "uniapp"
|
|
1240
1463
|
},
|
|
1241
1464
|
{
|
|
1242
1465
|
"path": "RebornInputNumber.vue",
|
|
1243
|
-
"content": "<
|
|
1466
|
+
"content": "<script lang=\"ts\" setup>\r\nimport type { ClassValue } from 'clsx'\r\nimport type { inputNumberColors, inputNumberShapes, inputNumberSizes } from './reborn-input-number.config'\r\nimport { computed, nextTick, ref, toRef, watch } from 'vue'\r\n\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { useLongPress } from './long-press'\r\nimport theme from './reborn-input-number.config'\r\n\r\ndefineOptions({\r\n name: 'ReInputNumber',\r\n inheritAttrs: false,\r\n})\r\n\r\nconst props = withDefaults(defineProps<InputNumberProps>(), {\r\n modelValue: 0,\r\n defaultValue: 0,\r\n min: 0,\r\n max: 200,\r\n step: 1,\r\n disabled: false,\r\n ui: () => ({}),\r\n size: 'md',\r\n color: 'primary',\r\n shape: 'square',\r\n placeholder: '',\r\n inputType: 'number',\r\n readonly: true,\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change', 'input', 'blur', 'focus'])\r\n\r\nexport interface InputNumberProps {\r\n modelValue?: number\r\n defaultValue?: number\r\n min?: number\r\n max?: number\r\n step?: number\r\n disabled?: boolean\r\n size?: typeof inputNumberSizes[number]\r\n color?: typeof inputNumberColors[number]\r\n shape?: typeof inputNumberShapes[number]\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n button: ClassValue\r\n input: ClassValue\r\n divider: ClassValue\r\n icon: ClassValue\r\n }>\r\n readonly?: boolean\r\n placeholder?: string\r\n inputType?: 'digit' | 'number'\r\n customClass?: any\r\n}\r\n\r\nconst longPress = useLongPress()\r\nconst { orientation, size: fieldGroupSize, disabled: fieldGroupDisabled, isError, validate } = useFormInject(props)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\nconst size = toRef(props, 'size')\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (fieldGroupSize.value || size.value) as any,\r\n color: props.color,\r\n shape: props.shape,\r\n fieldGroup: orientation.value,\r\n error: isError.value,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n button: (opts?: { class?: any }) => styles.button({ class: cn(opts?.class, uiOverrides.value.button) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, uiOverrides.value.divider) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n }\r\n})\r\n\r\nconst value = ref(props.modelValue)\r\n\r\nconst isPlus = computed(() => !fieldGroupDisabled.value && value.value < props.max)\r\nconst isMinus = computed(() => !fieldGroupDisabled.value && value.value > props.min)\r\n\r\nfunction update() {\r\n nextTick(() => {\r\n let val = value.value\r\n\r\n if (val < props.min) { val = props.min }\r\n if (val > props.max) { val = props.max }\r\n if (props.min > props.max) { val = props.max }\r\n\r\n if (props.inputType == 'digit') {\r\n val = Number.parseFloat(val.toFixed(2))\r\n }\r\n\r\n value.value = val\r\n\r\n if (val != props.modelValue) {\r\n emit('update:modelValue', val)\r\n emit('change', val)\r\n if (validate) { validate('change') }\r\n }\r\n })\r\n}\r\n\r\nfunction onInput(e: any) {\r\n const val = Number.parseFloat(e.detail.value)\r\n if (!isNaN(val)) {\r\n value.value = val\r\n }\r\n emit('input', e)\r\n}\r\n\r\nfunction onPlus() {\r\n if (fieldGroupDisabled.value || !isPlus.value) { return }\r\n\r\n longPress.start(() => {\r\n if (isPlus.value) {\r\n const val = props.max - value.value\r\n value.value += val > props.step ? props.step : val\r\n update()\r\n }\r\n })\r\n}\r\n\r\nfunction onMinus() {\r\n if (fieldGroupDisabled.value || !isMinus.value) { return }\r\n\r\n longPress.start(() => {\r\n if (isMinus.value) {\r\n const val = value.value - props.min\r\n value.value -= val > props.step ? props.step : val\r\n update()\r\n }\r\n })\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n if (e.detail.value == '') {\r\n value.value = props.min || 0\r\n }\r\n else {\r\n value.value = Number.parseFloat(e.detail.value)\r\n }\r\n update()\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== undefined && val !== value.value) {\r\n value.value = val\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\nwatch(() => props.max, update)\r\nwatch(() => props.min, update)\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" :data-disabled=\"fieldGroupDisabled\">\r\n <view :class=\"ui.button()\" @touchstart=\"onMinus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"decrease-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-minus\" />\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <input :class=\"ui.input()\" :type=\"inputType\" :value=\"value\" :disabled=\"fieldGroupDisabled\" :readonly=\"!readonly\"\r\n :placeholder=\"placeholder\" @input=\"onInput\" @blur=\"onBlur\">\r\n\r\n <view :class=\"ui.divider()\" />\r\n\r\n <view :class=\"ui.button()\" @touchstart=\"onPlus\" @touchend=\"longPress.stop\" @touchcancel=\"longPress.stop\">\r\n <slot name=\"increase-icon\">\r\n <view :class=\"ui.icon()\" class=\"i-lucide-plus\" />\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1244
1467
|
"target": "uniapp"
|
|
1245
1468
|
}
|
|
1246
1469
|
]
|
|
1247
1470
|
},
|
|
1248
1471
|
{
|
|
1249
|
-
"name": "reborn-
|
|
1472
|
+
"name": "reborn-input-otp",
|
|
1250
1473
|
"dependencies": [
|
|
1251
|
-
"
|
|
1474
|
+
"clsx"
|
|
1252
1475
|
],
|
|
1253
1476
|
"files": [
|
|
1254
1477
|
{
|
|
1255
|
-
"path": "
|
|
1256
|
-
"content": "
|
|
1478
|
+
"path": "index.ts",
|
|
1479
|
+
"content": "export { default as RebornInputOtp } from \"./RebornInputOtp.vue\";\r\n",
|
|
1257
1480
|
"target": "web"
|
|
1258
1481
|
},
|
|
1259
1482
|
{
|
|
1260
|
-
"path": "
|
|
1261
|
-
"content": "
|
|
1483
|
+
"path": "reborn-input-otp.config.ts",
|
|
1484
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as inputOtpSizes, colors as inputOtpColors };\r\n\r\nexport default {\r\n slots: {\r\n root: \"relative inline-flex items-center\",\r\n inner: \"absolute top-0 h-full z-10 opacity-0 w-full left-0\",\r\n list: \"flex flex-row relative gap-1\",\r\n item: \"flex flex-row items-center justify-center duration-100 border border-solid border-gray-3 dark:border-gray-6 rounded-lg bg-gray-1 dark:bg-gray-8\",\r\n value: \"text-inherit font-medium\",\r\n cursor: \"absolute w-[1px] h-[60%]\",\r\n },\r\n variants: {\r\n size: {\r\n sm: { item: \"h-8 w-8 text-xs\" },\r\n md: { item: \"h-10 w-10 text-sm\" },\r\n lg: { item: \"h-12 w-12 text-base\" },\r\n },\r\n color: {\r\n primary: {\r\n item: \"data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary/20 data-[active=true]:text-primary\",\r\n cursor: \"bg-primary\",\r\n },\r\n secondary: {\r\n item: \"data-[active=true]:border-secondary data-[active=true]:ring-2 data-[active=true]:ring-secondary/20 data-[active=true]:text-secondary\",\r\n cursor: \"bg-secondary\",\r\n },\r\n success: {\r\n item: \"data-[active=true]:border-success data-[active=true]:ring-2 data-[active=true]:ring-success/20 data-[active=true]:text-success\",\r\n cursor: \"bg-success\",\r\n },\r\n info: {\r\n item: \"data-[active=true]:border-info data-[active=true]:ring-2 data-[active=true]:ring-info/20 data-[active=true]:text-info\",\r\n cursor: \"bg-info\",\r\n },\r\n warning: {\r\n item: \"data-[active=true]:border-warning data-[active=true]:ring-2 data-[active=true]:ring-warning/20 data-[active=true]:text-warning\",\r\n cursor: \"bg-warning\",\r\n },\r\n error: {\r\n item: \"data-[active=true]:border-error data-[active=true]:ring-2 data-[active=true]:ring-error/20 data-[active=true]:text-error\",\r\n cursor: \"bg-error\",\r\n },\r\n neutral: {\r\n item: \"data-[active=true]:border-neutral data-[active=true]:ring-2 data-[active=true]:ring-neutral/20 data-[active=true]:text-neutral\",\r\n cursor: \"bg-neutral\",\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: \"opacity-50 pointer-events-none\",\r\n item: \"bg-gray-100 dark:bg-gray-700\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
1262
1485
|
"target": "web"
|
|
1263
1486
|
},
|
|
1264
1487
|
{
|
|
1265
|
-
"path": "
|
|
1266
|
-
"content": "
|
|
1488
|
+
"path": "RebornInputOtp.vue",
|
|
1489
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { inputOtpColors, inputOtpSizes } from \"./reborn-input-otp.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface InputOtpProps {\r\n modelValue?: string;\r\n autofocus?: boolean;\r\n length?: number;\r\n disabled?: boolean;\r\n inputType?: \"text\" | \"number\";\r\n size?: (typeof inputOtpSizes)[number];\r\n color?: (typeof inputOtpColors)[number];\r\n class?: any;\r\n ui?: Partial<{\r\n root: ClassValue;\r\n inner: ClassValue;\r\n list: ClassValue;\r\n item: ClassValue;\r\n value: ClassValue;\r\n cursor: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<InputOtpProps>(), {\r\n modelValue: \"\",\r\n autofocus: false,\r\n length: 4,\r\n disabled: false,\r\n inputType: \"number\",\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string): void;\r\n (e: \"done\", value: string): void;\r\n (e: \"focus\", event: FocusEvent): void;\r\n (e: \"blur\", event: FocusEvent): void;\r\n}>();\r\n\r\nconst inputRef = ref<HTMLInputElement | null>(null);\r\nconst isFocus = ref(false);\r\nconst value = ref(props.modelValue);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n size: props.size,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n list: (opts?: { class?: any }) => styles.list({ class: cn(opts?.class, uiOverrides.value.list) }),\r\n item: (opts?: { class?: any }) => styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n value: (opts?: { class?: any }) => styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n cursor: (opts?: { class?: any }) => styles.cursor({ class: cn(opts?.class, uiOverrides.value.cursor) }),\r\n };\r\n});\r\n\r\nconst list = computed<string[]>(() => {\r\n const arr: string[] = [];\r\n for (let i = 0; i < props.length; i++) {\r\n arr.push(value.value.charAt(i));\r\n }\r\n return arr;\r\n});\r\n\r\nfunction onInput(e: Event) {\r\n const target = e.target as HTMLInputElement;\r\n let val = target.value;\r\n if (props.inputType === \"number\") {\r\n val = val.replace(/\\D/g, \"\");\r\n }\r\n val = val.slice(0, props.length);\r\n value.value = val;\r\n target.value = val;\r\n emit(\"update:modelValue\", val);\r\n if (val.length === props.length) {\r\n emit(\"done\", val);\r\n inputRef.value?.blur();\r\n }\r\n}\r\n\r\nfunction onFocus(e: FocusEvent) {\r\n isFocus.value = true;\r\n emit(\"focus\", e);\r\n}\r\n\r\nfunction onBlur(e: FocusEvent) {\r\n isFocus.value = false;\r\n emit(\"blur\", e);\r\n}\r\n\r\nfunction onClick() {\r\n inputRef.value?.focus();\r\n}\r\n\r\nonMounted(() => {\r\n if (props.autofocus) {\r\n nextTick(() => inputRef.value?.focus());\r\n }\r\n});\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n value.value = val;\r\n },\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: props.class })\" @click=\"onClick\">\r\n <div :class=\"ui.inner()\">\r\n <input ref=\"inputRef\" :value=\"value\" :type=\"inputType === 'number' ? 'tel' : 'text'\" :maxlength=\"length\"\r\n :disabled=\"disabled\" :autofocus=\"autofocus\" autocomplete=\"one-time-code\" inputmode=\"numeric\"\r\n class=\"h-full w-full opacity-0\" @input=\"onInput\" @focus=\"onFocus\" @blur=\"onBlur\" />\r\n </div>\r\n <div :class=\"ui.list()\">\r\n <div v-for=\"(item, index) in list\" :key=\"index\" :class=\"ui.item()\"\r\n :data-active=\"value.length === index && isFocus\" :data-disabled=\"disabled\" @click=\"onClick\">\r\n <span :class=\"ui.value()\">{{ item }}</span>\r\n <span v-if=\"value.length === index && isFocus && item === ''\" class=\"otp-cursor\" :class=\"ui.cursor()\" />\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<style scoped>\r\n.otp-cursor {\r\n animation: blink 1s infinite;\r\n}\r\n\r\n@keyframes blink {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0;\r\n }\r\n}\r\n</style>\r\n",
|
|
1490
|
+
"target": "web"
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
"path": "index.ts",
|
|
1494
|
+
"content": "export { default as RebornInputOtp } from './RebornInputOtp.vue'\r\n",
|
|
1267
1495
|
"target": "uniapp"
|
|
1268
1496
|
},
|
|
1269
1497
|
{
|
|
1270
|
-
"path": "
|
|
1271
|
-
"content": "
|
|
1498
|
+
"path": "reborn-input-otp.config.ts",
|
|
1499
|
+
"content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'relative inline-flex items-center',\r\n inner: 'absolute top-0 h-full z-10 opacity-0 w-[200%] -left-full',\r\n list: 'flex flex-row relative gap-1',\r\n item: 'flex flex-row items-center justify-center duration-100 border border-solid border-gray-4 rounded-lg bg-gray-1 dark:bg-gray-8 ',\r\n value: 'text-inherit font-medium',\r\n cursor: 'absolute w-[1px] h-[60%]',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n item: 'h-8 w-8 text-xs',\r\n },\r\n md: {\r\n item: 'h-10 w-10 text-sm',\r\n },\r\n lg: {\r\n item: 'h-12 w-12 text-base',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n item: 'data-[active=true]:border-primary data-[active=true]:ring-2 data-[active=true]:ring-primary/20 data-[active=true]:text-primary',\r\n cursor: 'bg-primary',\r\n },\r\n secondary: {\r\n item: 'data-[active=true]:border-secondary data-[active=true]:ring-2 data-[active=true]:ring-secondary/20 data-[active=true]:text-secondary',\r\n cursor: 'bg-secondary',\r\n },\r\n success: {\r\n item: 'data-[active=true]:border-success data-[active=true]:ring-2 data-[active=true]:ring-success/20 data-[active=true]:text-success',\r\n cursor: 'bg-success',\r\n },\r\n info: {\r\n item: 'data-[active=true]:border-info data-[active=true]:ring-2 data-[active=true]:ring-info/20 data-[active=true]:text-info',\r\n cursor: 'bg-info',\r\n },\r\n warning: {\r\n item: 'data-[active=true]:border-warning data-[active=true]:ring-2 data-[active=true]:ring-warning/20 data-[active=true]:text-warning',\r\n cursor: 'bg-warning',\r\n },\r\n error: {\r\n item: 'data-[active=true]:border-error data-[active=true]:ring-2 data-[active=true]:ring-error/20 data-[active=true]:text-error',\r\n cursor: 'bg-error',\r\n },\r\n neutral: {\r\n item: 'data-[active=true]:border-neutral data-[active=true]:ring-2 data-[active=true]:ring-neutral/20 data-[active=true]:text-neutral',\r\n cursor: 'bg-neutral',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none',\r\n item: 'bg-muted',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'primary',\r\n },\r\n} as const\r\n\r\nexport { colors as inputOtpColors, sizes as inputOtpSizes }\r\n",
|
|
1500
|
+
"target": "uniapp"
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
"path": "RebornInputOtp.vue",
|
|
1504
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { inputOtpColors, inputOtpSizes } from './reborn-input-otp.config'\r\nimport type { AnimationEngine } from '@/lib/animation'\r\nimport { computed, nextTick, onMounted, ref, watch } from 'vue'\r\nimport RebornInput from '@/components/reborn-input/RebornInput.vue'\r\nimport { createAnimation } from '@/lib/animation'\r\nimport { tv } from '@/lib/tv'\r\n\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-input-otp.config'\r\n\r\ndefineOptions({\r\n name: 'RebornInputOtp',\r\n})\r\n\r\nconst props = withDefaults(defineProps<{\r\n ui?: any\r\n customClass?: any\r\n modelValue?: string\r\n autofocus?: boolean\r\n length?: number\r\n disabled?: boolean\r\n inputType?: 'text' | 'number' | 'digit'\r\n size?: typeof inputOtpSizes[number]\r\n color?: typeof inputOtpColors[number]\r\n}>(), {\r\n modelValue: '',\r\n autofocus: false,\r\n length: 4,\r\n disabled: false,\r\n inputType: 'number',\r\n size: 'md',\r\n color: 'primary',\r\n ui: () => ({}),\r\n})\r\nconst emit = defineEmits(['update:modelValue', 'done', 'focus', 'blur'])\r\nconst b = tv(theme)\r\nconst inputRef = ref<InstanceType<typeof RebornInput> | null>(null)\r\n\r\nconst cursorRef = ref<any[]>([])\r\n\r\nconst value = ref(props.modelValue)\r\n\r\nconst ui = computed(() => {\r\n const style = b({\r\n color: props.color,\r\n size: props.size,\r\n disabled: props.disabled,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => style.root({ class: cn(opts?.class, props.ui?.root) }),\r\n inner: (opts?: { class?: any }) => style.inner({ class: cn(opts?.class, props.ui?.inner) }),\r\n list: (opts?: { class?: any }) => style.list({ class: cn(opts?.class, props.ui?.list) }),\r\n item: (opts?: { class?: any }) => style.item({ class: cn(opts?.class, props.ui?.item) }),\r\n value: (opts?: { class?: any }) => style.value({ class: cn(opts?.class, props.ui?.value) }),\r\n cursor: (opts?: { class?: any }) => style.cursor({ class: cn(opts?.class, props.ui?.cursor) }),\r\n }\r\n})\r\n\r\nconst isFocus = ref(false)\r\n\r\nconst list = computed<string[]>(() => {\r\n const arr = [] as string[]\r\n for (let i = 0; i < props.length; i++) {\r\n arr.push(value.value.charAt(i))\r\n }\r\n return arr\r\n})\r\n\r\nlet animationEngine: AnimationEngine | null = null\r\n\r\nfunction last<T>(array: T[]): T | null {\r\n return Array.isArray(array) && array.length > 0 ? array[array.length - 1] : null\r\n}\r\n\r\nasync function onCursor() {\r\n await nextTick()\r\n\r\n if (!cursorRef.value) {\r\n return\r\n }\r\n\r\n // #ifdef APP\r\n if (animationEngine != null) {\r\n animationEngine.stop()\r\n }\r\n\r\n const target = last(cursorRef.value)\r\n if (target) {\r\n animationEngine = createAnimation(target, {\r\n duration: 600,\r\n loop: -1,\r\n alternate: true,\r\n })\r\n .opacity('0', '1')\r\n .play()\r\n }\r\n // #endif\r\n}\r\n\r\nfunction onChange(val: string) {\r\n emit('update:modelValue', val)\r\n\r\n // 输入完成时触发done事件\r\n if (val.length == props.length) {\r\n uni.hideKeyboard()\r\n emit('done', val)\r\n }\r\n\r\n // 更新光标动画\r\n onCursor()\r\n}\r\n\r\nfunction onFocus(e: any) {\r\n isFocus.value = true\r\n emit('focus', e)\r\n onCursor()\r\n}\r\n\r\nfunction onBlur(e: any) {\r\n isFocus.value = false\r\n emit('blur', e)\r\n if (animationEngine) {\r\n animationEngine.stop()\r\n }\r\n}\r\n\r\nfunction onTap() {\r\n if (inputRef.value) {\r\n inputRef.value.focus()\r\n }\r\n onCursor()\r\n}\r\n\r\nonMounted(() => {\r\n if (props.autofocus) {\r\n isFocus.value = true\r\n nextTick(() => {\r\n onTap()\r\n })\r\n }\r\n\r\n watch(\r\n () => props.modelValue,\r\n (val: string) => {\r\n value.value = val\r\n if (val && isFocus.value) {\r\n onCursor()\r\n }\r\n },\r\n {\r\n immediate: true,\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" @tap=\"onTap()\">\r\n <view :class=\"ui.inner()\">\r\n <RebornInput\r\n ref=\"inputRef\" v-model=\"value\" :type=\"inputType\" :maxlength=\"length\" :disabled=\"disabled\"\r\n :autofocus=\"autofocus\" :hold-keyboard=\"false\" :clearable=\"false\" customClass=\"!h-full\" @input=\"onChange\"\r\n @focus=\"onFocus\" @blur=\"onBlur\"\r\n />\r\n </view>\r\n <view :class=\"ui.list()\">\r\n <view\r\n v-for=\"(item, index) in list\" :key=\"index\" :class=\"ui.item()\"\r\n :data-active=\"value.length >= index && isFocus\" :data-disabled=\"disabled\" @tap=\"onTap\"\r\n >\r\n <text :class=\"ui.value()\" :style=\"{ color: value.length >= index && isFocus ? props.color : '' }\">\r\n {{ item }}\r\n </text>\r\n <view\r\n v-if=\"value.length == index && isFocus && item == ''\" ref=\"cursorRef\" class=\"\r\n cursor\r\n \"\r\n :class=\"ui.cursor()\"\r\n />\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style lang=\"scss\" scoped>\r\n.cursor {\r\n // #ifndef APP\r\n animation: blink 1s infinite;\r\n\r\n @keyframes blink {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n opacity: 0;\r\n }\r\n }\r\n\r\n // #endif\r\n}\r\n</style>\r\n",
|
|
1272
1505
|
"target": "uniapp"
|
|
1273
1506
|
}
|
|
1274
1507
|
]
|
|
1275
1508
|
},
|
|
1276
1509
|
{
|
|
1277
|
-
"name": "reborn-
|
|
1278
|
-
"dependencies": [
|
|
1279
|
-
"clsx"
|
|
1280
|
-
],
|
|
1510
|
+
"name": "reborn-loading",
|
|
1511
|
+
"dependencies": [],
|
|
1281
1512
|
"files": [
|
|
1282
1513
|
{
|
|
1283
1514
|
"path": "index.ts",
|
|
1284
|
-
"content": "
|
|
1515
|
+
"content": "// @ts-ignore\r\nimport RebornLoading from './reborn-loading.vue'\r\n\r\nexport default RebornLoading\r\nexport { RebornLoading }\r\nexport type { LoadingType } from './reborn-loading.vue'\r\n",
|
|
1516
|
+
"target": "uniapp"
|
|
1285
1517
|
},
|
|
1286
1518
|
{
|
|
1287
|
-
"path": "reborn-
|
|
1288
|
-
"content": "const
|
|
1289
|
-
"target": "
|
|
1519
|
+
"path": "reborn-loading.config.ts",
|
|
1520
|
+
"content": "export const LoadingTypes = ['outline', 'ring', 'spinner', 'bars-scale', 'blocks-shuffle', 'blocks-wave', 'gooey-balls'] as const\r\nexport const LoadingColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'inline-block align-middle rb-loading',\r\n container: 'w-full h-full relative',\r\n indicator: 'w-full h-full rb-loading-indicator',\r\n outlineTrack: 'absolute inset-0 rounded-full border-[3px] opacity-20',\r\n spinnerItem: 'absolute top-0 left-[46%] w-[8%] h-[25%] bg-current rounded-sm origin-[50%_200%] rb-loading-spinnerItem',\r\n barItem: 'w-[15%] h-[60%] bg-current rounded-sm rb-loading-barItem',\r\n blockItem: 'absolute w-[40%] h-[40%] bg-current rounded-sm',\r\n waveItem: 'bg-current rounded-[1px] rb-loading-waveItem',\r\n gooeyItem: 'absolute w-[40%] h-[40%] bg-current rounded-full',\r\n },\r\n variants: {\r\n type: {\r\n ring: {\r\n container: 'flex items-center justify-center',\r\n indicator: 'rounded-full border-[3px] border-solid'\r\n },\r\n outline: {\r\n container: 'flex items-center justify-center',\r\n indicator: 'absolute inset-0 rounded-full border-[3px]'\r\n },\r\n spinner: {\r\n container: 'flex items-center justify-center'\r\n },\r\n 'bars-scale': {\r\n container: 'flex justify-between items-center'\r\n },\r\n 'blocks-shuffle': {\r\n container: 'block'\r\n },\r\n 'blocks-wave': {\r\n container: 'grid grid-cols-3 grid-rows-3 gap-[10%]'\r\n },\r\n 'gooey-balls': {\r\n container: 'flex items-center justify-center'\r\n }\r\n },\r\n color: {\r\n primary: { container: 'text-primary' },\r\n secondary: { container: 'text-secondary' },\r\n success: { container: 'text-success' },\r\n info: { container: 'text-info' },\r\n warning: { container: 'text-warning' },\r\n error: { container: 'text-error' },\r\n neutral: { container: 'text-neutral' }\r\n }\r\n },\r\n defaultVariants: {\r\n color: 'primary',\r\n type: 'ring'\r\n }\r\n} as const\r\n\r\nexport type LoadingUI = {\r\n root?: string\r\n container?: string\r\n indicator?: string\r\n outlineTrack?: string\r\n spinnerItem?: string\r\n barItem?: string\r\n blockItem?: string\r\n waveItem?: string\r\n gooeyItem?: string\r\n}\r\n\r\nexport default config",
|
|
1521
|
+
"target": "uniapp"
|
|
1290
1522
|
},
|
|
1291
1523
|
{
|
|
1292
|
-
"path": "
|
|
1293
|
-
"content": "<
|
|
1294
|
-
"target": "
|
|
1524
|
+
"path": "RebornLoading.vue",
|
|
1525
|
+
"content": "<template>\r\n <view :class=\"ui.root()\" :style=\"rootStyle\">\r\n <view v-if=\"props.type === 'ring' || props.type === 'outline'\" :key=\"props.type\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-if=\"props.type === 'outline'\" :class=\"ui.outlineTrack()\" />\r\n <view :class=\"ui.indicator()\" :style=\"ringIndicatorStyle\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'spinner'\" key=\"loading-spinner\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 12\" :key=\"i\" :class=\"ui.spinnerItem()\" :style=\"{ '--i': i }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'bars-scale'\" key=\"loading-bars-scale\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 5\" :key=\"i\" :class=\"ui.barItem()\" :style=\"{ '--i': i }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'blocks-shuffle'\" key=\"loading-blocks-shuffle\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view :class=\"ui.blockItem()\" class=\"rb-shuffle-1\" />\r\n <view :class=\"ui.blockItem()\" class=\"rb-shuffle-2\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'blocks-wave'\" key=\"loading-blocks-wave\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view v-for=\"i in 9\" :key=\"i\" :class=\"ui.waveItem()\" :style=\"{ '--d': getWaveDelay(i) }\" />\r\n </view>\r\n\r\n <view v-else-if=\"props.type === 'gooey-balls'\" key=\"loading-gooey-balls\" :class=\"ui.container()\"\r\n :style=\"containerStyle\">\r\n <view :class=\"ui.gooeyItem()\" class=\"rb-gooey-1\" />\r\n <view :class=\"ui.gooeyItem()\" class=\"rb-gooey-2\" />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-loading',\r\n options: { virtualHost: true, addGlobalClass: true, styleIsolation: 'shared' }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, watch, ref } from 'vue'\r\nimport { addUnit, isDef, objToStyle } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type LoadingUI, LoadingColors, LoadingTypes } from './reborn-loading.config'\r\n\r\nexport type RebornLoadingProps = {\r\n ui?: LoadingUI\r\n type?: typeof LoadingTypes[number]\r\n color?: typeof LoadingColors[number] | string\r\n size?: string | number\r\n customClass?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornLoadingProps>(), {\r\n ui: () => ({}),\r\n type: 'ring',\r\n color: 'primary',\r\n size: '30px'\r\n})\r\n\r\nconst iconSize = ref<string>('30px')\r\nwatch(() => props.size, (val) => { iconSize.value = addUnit(val) }, { immediate: true })\r\n\r\nconst isPresetColor = computed(() => LoadingColors.includes(props.color as typeof LoadingColors[number]))\r\n\r\n// bars-scale / blocks-wave 用百分比布局,尺寸过小时子元素会接近 0 不显示,故设最小宽高\r\nconst MIN_SIZE_FOR_GRID = '32px'\r\nconst rootStyle = computed(() => {\r\n const style: Record<string, string> = {\r\n width: iconSize.value,\r\n height: iconSize.value,\r\n }\r\n if (props.type === 'bars-scale' || props.type === 'blocks-wave') {\r\n style.minWidth = MIN_SIZE_FOR_GRID\r\n style.minHeight = MIN_SIZE_FOR_GRID\r\n }\r\n return objToStyle(style)\r\n})\r\nconst containerStyle = computed(() => {\r\n const style: Record<string, string> = {}\r\n if (props.color && !isPresetColor.value) {\r\n style.color = props.color\r\n }\r\n\r\n return objToStyle(style)\r\n})\r\n\r\n// ring 在自定义颜色时直接给 indicator 设边框色,避免 currentColor 在小程序等环境不继承导致显示成 U 形\r\nconst ringIndicatorStyle = computed(() => {\r\n if (props.type !== 'ring' || isPresetColor.value) return {}\r\n const c = props.color as string\r\n if (!c) return {}\r\n return {\r\n borderColor: c,\r\n borderTopColor: 'transparent',\r\n }\r\n})\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: isPresetColor.value ? props.color as typeof LoadingColors[number] : undefined,\r\n type: props.type\r\n })\r\n // 映射所有 slots\r\n const slots = ['root', 'container', 'indicator', 'outlineTrack', 'spinnerItem', 'barItem', 'blockItem', 'waveItem', 'gooeyItem'] as const\r\n const res: any = {}\r\n slots.forEach(slot => {\r\n res[slot] = (opts?: { class?: any }) => styles[slot]({ class: cn(opts?.class, slot === 'root' ? props.customClass : undefined, (props.ui as any)?.[slot]) })\r\n })\r\n return res as Record<keyof LoadingUI, (opts?: { class?: any }) => string>\r\n})\r\n\r\n// 仅保留复杂的延迟算法在 JS 中,其余交给 CSS\r\nfunction getWaveDelay(index: number) {\r\n return ((index - 1) % 3 + Math.floor((index - 1) / 3)) * 0.12 + 's'\r\n}\r\n</script>\r\n\r\n<style>\r\n/* 1. 性能基础设置 */\r\n.rb-loading view {\r\n box-sizing: border-box;\r\n backface-visibility: hidden;\r\n transform: translateZ(0);\r\n /* 开启硬件加速 */\r\n}\r\n\r\n/* 2. 动画定义 */\r\n@keyframes rb-rotate {\r\n from {\r\n transform: rotate(0deg);\r\n }\r\n\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n@keyframes rb-spinner {\r\n 0% {\r\n opacity: 1;\r\n }\r\n\r\n 100% {\r\n opacity: 0.15;\r\n }\r\n}\r\n\r\n@keyframes rb-bars-scale {\r\n\r\n 0%,\r\n 100% {\r\n transform: scaleY(0.5);\r\n opacity: 0.5;\r\n }\r\n\r\n 50% {\r\n transform: scaleY(1.2);\r\n opacity: 1;\r\n }\r\n}\r\n\r\n@keyframes rb-blocks-shuffle {\r\n 0% {\r\n transform: translate(0, 0);\r\n }\r\n\r\n 25% {\r\n transform: translate(120%, 0);\r\n }\r\n\r\n 50% {\r\n transform: translate(120%, 120%);\r\n }\r\n\r\n 75% {\r\n transform: translate(0, 120%);\r\n }\r\n\r\n 100% {\r\n transform: translate(0, 0);\r\n }\r\n}\r\n\r\n@keyframes rb-blocks-wave {\r\n\r\n 0%,\r\n 100% {\r\n transform: scale(1);\r\n opacity: 1;\r\n }\r\n\r\n 50% {\r\n transform: scale(0.3);\r\n opacity: 0.3;\r\n }\r\n}\r\n\r\n@keyframes rb-gooey-1 {\r\n\r\n 0%,\r\n 100% {\r\n transform: translateX(-50%) scale(1);\r\n }\r\n\r\n 50% {\r\n transform: translateX(20%) scale(1.5);\r\n }\r\n}\r\n\r\n@keyframes rb-gooey-2 {\r\n\r\n 0%,\r\n 100% {\r\n transform: translateX(50%) scale(1.5);\r\n }\r\n\r\n 50% {\r\n transform: translateX(-20%) scale(1);\r\n }\r\n}\r\n\r\n/* 3. 动画逻辑应用 (核心优化) */\r\n/* Ring & Outline */\r\n.rb-loading .rb-loading-indicator {\r\n border-color: currentColor;\r\n border-top-color: transparent !important;\r\n animation: rb-rotate 0.8s linear infinite;\r\n will-change: transform;\r\n}\r\n\r\n/* Spinner */\r\n.rb-loading .rb-loading-spinnerItem {\r\n transform: rotate(calc((var(--i) - 1) * 30deg));\r\n animation: rb-spinner 1s linear infinite;\r\n animation-delay: calc((var(--i) - 1) * 0.08s);\r\n will-change: opacity;\r\n}\r\n\r\n/* Bars Scale */\r\n.rb-loading .rb-loading-barItem {\r\n animation: rb-bars-scale 1s ease-in-out infinite;\r\n animation-delay: calc((var(--i) - 1) * 0.12s);\r\n will-change: transform, opacity;\r\n}\r\n\r\n/* Blocks Shuffle */\r\n.rb-shuffle-1 {\r\n animation: rb-blocks-shuffle 1s linear infinite;\r\n}\r\n\r\n.rb-shuffle-2 {\r\n animation: rb-blocks-shuffle 1s linear infinite -0.5s;\r\n}\r\n\r\n/* Blocks Wave */\r\n.rb-loading .rb-loading-waveItem {\r\n animation: rb-blocks-wave 1s ease-in-out infinite;\r\n animation-delay: var(--d);\r\n will-change: transform, opacity;\r\n}\r\n\r\n/* Gooey */\r\n.rb-gooey-1 {\r\n animation: rb-gooey-1 0.75s ease-in-out infinite;\r\n}\r\n\r\n.rb-gooey-2 {\r\n animation: rb-gooey-2 0.75s ease-in-out infinite;\r\n}\r\n</style>",
|
|
1526
|
+
"target": "uniapp"
|
|
1527
|
+
}
|
|
1528
|
+
]
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
"name": "reborn-loadmore",
|
|
1532
|
+
"dependencies": [],
|
|
1533
|
+
"files": [
|
|
1534
|
+
{
|
|
1535
|
+
"path": "index.ts",
|
|
1536
|
+
"content": "// @ts-ignore\r\nimport RebornLoadmore from './reborn-loadmore.vue'\r\n\r\nexport default RebornLoadmore\r\nexport { RebornLoadmore }\r\nexport type { LoadMoreState } from './reborn-loadmore.vue'\r\n",
|
|
1537
|
+
"target": "uniapp"
|
|
1295
1538
|
},
|
|
1296
1539
|
{
|
|
1297
|
-
"path": "reborn-
|
|
1298
|
-
"content": "
|
|
1540
|
+
"path": "reborn-loadmore.config.ts",
|
|
1541
|
+
"content": "export const loadMoreColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const LoadMoreState = ['loading', 'error', 'finished'] as const\r\n\r\nexport type LoadMoreUI = {\r\n root?: string\r\n divider?: string\r\n line?: string\r\n text?: string\r\n errorText?: string\r\n refresh?: string\r\n}\r\n\r\nconst config = {\r\n slots: {\r\n root: 'w-full h-[48px] leading-[48px] text-center text-[#999999] bg-transparent',\r\n divider: 'flex items-center justify-center w-[80%] mx-auto',\r\n line: 'h-[1px] bg-[#e8e8e8] flex-1',\r\n text: 'inline-block text-[14px] align-middle',\r\n errorText: 'inline-block text-[14px] align-middle px-[6px] cursor-pointer',\r\n refresh: 'inline-block align-middle text-[16px] cursor-pointer ml-1',\r\n },\r\n variants: {\r\n color: {\r\n primary: { text: 'text-primary', errorText: 'text-primary', refresh: 'text-primary' },\r\n secondary: { text: 'text-secondary', errorText: 'text-secondary', refresh: 'text-secondary' },\r\n success: { text: 'text-success', errorText: 'text-success', refresh: 'text-success' },\r\n info: { text: 'text-info', errorText: 'text-info', refresh: 'text-info' },\r\n warning: { text: 'text-warning', errorText: 'text-warning', refresh: 'text-warning' },\r\n error: { text: 'text-error', errorText: 'text-error', refresh: 'text-error' },\r\n neutral: { text: 'text-neutral', errorText: 'text-neutral', refresh: 'text-neutral' },\r\n },\r\n state: {\r\n finished: { text: 'px-2', },\r\n error: {},\r\n loading: {},\r\n }\r\n },\r\n defaultVariants: {\r\n color: 'neutral',\r\n },\r\n} as const\r\n\r\nexport default config\r\n",
|
|
1299
1542
|
"target": "uniapp"
|
|
1300
1543
|
},
|
|
1301
1544
|
{
|
|
1302
|
-
"path": "
|
|
1303
|
-
"content": "<script
|
|
1545
|
+
"path": "RebornLoadmore.vue",
|
|
1546
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-loadmore',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref } from 'vue'\r\nimport RebornLoading, { type RebornLoadingProps } from '@/components/reborn-loading/RebornLoading.vue'\r\nimport { isDef, isUndefined, omitBy } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type LoadMoreUI, loadMoreColors, LoadMoreState } from './reborn-loadmore.config'\r\n\r\n\r\nexport interface LoadMoreProps {\r\n customClass?: string\r\n customStyle?: string\r\n state: typeof LoadMoreState[number]\r\n loadingText?: string\r\n finishedText?: string\r\n errorText?: string\r\n color?: typeof loadMoreColors[number]\r\n ui?: LoadMoreUI\r\n loadingProps?: RebornLoadingProps\r\n}\r\n\r\nconst props = withDefaults(defineProps<LoadMoreProps>(), {\r\n customClass: '',\r\n customStyle: '',\r\n state: 'loading',\r\n color: 'neutral',\r\n ui: () => ({}),\r\n loadingProps: () => ({\r\n size: '40rpx'\r\n })\r\n})\r\n\r\nconst emit = defineEmits(['reload'])\r\n\r\nconst currentState = ref<typeof LoadMoreState[number] | null>(null)\r\n\r\nfunction reload() {\r\n if (props.state !== 'error') return\r\n currentState.value = 'loading'\r\n emit('reload')\r\n}\r\n\r\nconst b = tv(theme)\r\n\r\nconst customLoadingProps = computed(() => {\r\n const loadingProps = isDef(props.loadingProps) ? omitBy(props.loadingProps, isUndefined) : {}\r\n loadingProps.customClass = cn('inline-block align-middle mr-2 w-4 h-4', loadingProps.customClass)\r\n if (!loadingProps.color) {\r\n loadingProps.color = props.color\r\n }\r\n return loadingProps\r\n})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n state: props.state\r\n })\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, props.customClass, props.ui?.root) }),\r\n divider: (opts?: { class?: any }) => styles.divider({ class: cn(opts?.class, props.ui?.divider) }),\r\n line: (opts?: { class?: any }) => styles.line({ class: cn(opts?.class, props.ui?.line) }),\r\n text: (opts?: { class?: any }) => styles.text({ class: cn(opts?.class, props.ui?.text) }),\r\n errorText: (opts?: { class?: any }) => styles.errorText({ class: cn(opts?.class, props.ui?.errorText) }),\r\n refresh: (opts?: { class?: any }) => styles.refresh({ class: cn(opts?.class, props.ui?.refresh) }),\r\n }\r\n})\r\n</script>\r\n<template>\r\n <view :class=\"ui.root()\" :style=\"customStyle\" @click=\"reload\">\r\n <view v-if=\"state === 'finished'\" :class=\"ui.divider()\">\r\n <view :class=\"ui.line()\"></view>\r\n <text :class=\"ui.text()\">{{ finishedText || '没有更多了' }}</text>\r\n <view :class=\"ui.line()\"></view>\r\n </view>\r\n <block v-else-if=\"state === 'error'\">\r\n <text :class=\"ui.text()\">{{ errorText || '加载失败' }}</text>\r\n <text :class=\"ui.errorText()\">{{ '点击重试' }}</text>\r\n <svg :class=\"ui.refresh()\" viewBox=\"0 0 1024 1024\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\"\r\n height=\"1em\" fill=\"currentColor\">\r\n <path\r\n d=\"M512 1024C229.2 1024 0 794.8 0 512S229.2 0 512 0s512 229.2 512 512-229.2 512-512 512zM512 66.8C266.3 66.8 66.8 266.3 66.8 512S266.3 957.2 512 957.2 957.2 757.7 957.2 512 757.7 66.8 512 66.8z\">\r\n </path>\r\n <path\r\n d=\"M512 795.1c-156.1 0-283.1-127-283.1-283.1S355.9 228.9 512 228.9c35.6 0 70 6.6 102.1 19.5 17.6 7 26.1 27.1 19 44.7s-27.1 26.1-44.7 19c-24.3-9.7-50.2-14.7-76.4-14.7-119.3 0-216.3 97-216.3 216.3s97 216.3 216.3 216.3c103.5 0 193.3-73.4 212.4-175 3.4-18.7 21.4-31 40-27.6s31 21.4 27.6 40c-25.2 133.5-143.1 229.2-279.7 229.2z\">\r\n </path>\r\n <path\r\n d=\"M478.6 478.6c13.1 13.1 34.3 13.1 47.3 0L764 240.4c13.1-13.1 13.1-34.3 0-47.3-13.1-13.1-34.3-13.1-47.3 0L478.6 431.2c-13.1 13.1-13.1 34.4 0 47.4z\">\r\n </path>\r\n <path\r\n d=\"M856 507.2c-15.3 0-28.7-10.4-32.3-25.7-4.3-18-9.6-35.8-15.8-53.1-6.1-17.6-25.4-26.8-43-20.7s-26.8 25.4-20.7 43c4.8 13.3 8.9 27.1 12.2 41 4.2 18 22.3 29.3 40.3 25 1.7-.4 3.4-.9 5-.1 13.9 6.2 30 0 36.2-13.9.7-1.6 1.4-3.3 2.1-5s-.2-4.1-.2-4.1-1.3-4.8-3.8-6.4z\">\r\n </path>\r\n </svg>\r\n </block>\r\n <block v-else-if=\"state === 'loading'\">\r\n <reborn-loading v-bind=\"customLoadingProps\" />\r\n <text :class=\"ui.text()\">{{ loadingText || '加载中...' }}</text>\r\n </block>\r\n </view>\r\n</template>\r\n",
|
|
1304
1547
|
"target": "uniapp"
|
|
1305
1548
|
}
|
|
1306
1549
|
]
|
|
1307
1550
|
},
|
|
1308
1551
|
{
|
|
1309
|
-
"name": "reborn-
|
|
1310
|
-
"dependencies": [
|
|
1311
|
-
"@vueuse/core",
|
|
1312
|
-
"clsx"
|
|
1313
|
-
],
|
|
1552
|
+
"name": "reborn-overlay",
|
|
1553
|
+
"dependencies": [],
|
|
1314
1554
|
"files": [
|
|
1315
1555
|
{
|
|
1316
1556
|
"path": "index.ts",
|
|
1317
|
-
"content": "export { default as
|
|
1557
|
+
"content": "export { default as RebornOverlay } from './RebornOverlay.vue';\r\n",
|
|
1558
|
+
"target": "web"
|
|
1318
1559
|
},
|
|
1319
1560
|
{
|
|
1320
|
-
"path": "reborn-
|
|
1321
|
-
"content": "export
|
|
1561
|
+
"path": "reborn-overlay.config.ts",
|
|
1562
|
+
"content": "export default { base: 'inset-0 bg-gray-8/70 dark:bg-gray-2/70' } as const;\r\n",
|
|
1322
1563
|
"target": "web"
|
|
1323
1564
|
},
|
|
1324
1565
|
{
|
|
1325
|
-
"path": "
|
|
1326
|
-
"content": "<script setup lang=\"ts\">\r\nimport {
|
|
1566
|
+
"path": "RebornOverlay.vue",
|
|
1567
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, watch } from 'vue';\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue';\r\nimport theme from './reborn-overlay.config';\r\n\r\ninterface Props {\r\n modelValue?: boolean;\r\n duration?: number;\r\n lockScroll?: boolean;\r\n zIndex?: number;\r\n closeOnClickOverlay?: boolean;\r\n absolute?: boolean;\r\n customClass?: string;\r\n customStyle?: string;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n modelValue: false,\r\n duration: 300,\r\n lockScroll: true,\r\n zIndex: 100,\r\n closeOnClickOverlay: true,\r\n absolute: false,\r\n customClass: '',\r\n customStyle: ''\r\n});\r\nconst emit = defineEmits(['update:modelValue', 'close']);\r\n\r\nwatch(() => props.modelValue && props.lockScroll, (locked) => {\r\n if (typeof document !== 'undefined') document.body.style.overflow = locked ? 'hidden' : '';\r\n}, { immediate: true });\r\n\r\nconst overlayClass = computed(() => `${props.absolute ? 'absolute' : 'fixed'} ${theme.base} ${props.customClass}`);\r\nconst overlayStyle = computed(() => `z-index:${props.zIndex};${props.customStyle}`);\r\nconst onClick = () => { if (props.closeOnClickOverlay) { emit('update:modelValue', false); emit('close'); } };\r\n</script>\r\n<template>\r\n <RebornTransition :show=\"props.modelValue\" name=\"fade\" :duration=\"props.duration\" :custom-class=\"overlayClass\"\r\n :custom-style=\"overlayStyle\" :disable-touch-move=\"props.lockScroll\" @click=\"onClick\">\r\n <slot />\r\n </RebornTransition>\r\n</template>\r\n",
|
|
1327
1568
|
"target": "web"
|
|
1328
1569
|
},
|
|
1329
1570
|
{
|
|
1330
|
-
"path": "
|
|
1331
|
-
"content": "
|
|
1332
|
-
"target": "
|
|
1571
|
+
"path": "index.ts",
|
|
1572
|
+
"content": "// @ts-ignore\r\nimport RebornOverlay from './reborn-overlay.vue'\r\n\r\nexport default RebornOverlay\r\nexport { RebornOverlay }\r\n",
|
|
1573
|
+
"target": "uniapp"
|
|
1333
1574
|
},
|
|
1334
1575
|
{
|
|
1335
|
-
"path": "
|
|
1336
|
-
"content": "
|
|
1337
|
-
"target": "
|
|
1576
|
+
"path": "reborn-overlay.config.ts",
|
|
1577
|
+
"content": "const config = {\r\n base: 'inset-0 bg-gray-8/70 dark:bg-gray-2/70',\r\n variants: {\r\n absolute: {\r\n true: 'absolute',\r\n false: 'fixed'\r\n }\r\n },\r\n defaultVariants: {\r\n absolute: false\r\n }\r\n} as const\r\n\r\nexport default config\r\n",
|
|
1578
|
+
"target": "uniapp"
|
|
1338
1579
|
},
|
|
1339
1580
|
{
|
|
1340
|
-
"path": "
|
|
1341
|
-
"content": "<script
|
|
1342
|
-
"target": "
|
|
1581
|
+
"path": "RebornOverlay.vue",
|
|
1582
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-overlay',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, type PropType } from 'vue'\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue'\r\nimport { tv } from '@/lib/tv'\r\nimport theme from './reborn-overlay.config'\r\n// #ifdef H5\r\nimport { useLockScroll } from '@/composables/useLockScroll'\r\n// #endif\r\n\r\nconst props = defineProps({\r\n customClass: { type: String, default: '' },\r\n customStyle: { type: String, default: '' },\r\n modelValue: { type: Boolean, default: false },\r\n duration: {\r\n type: [Object, Number, Boolean] as PropType<Record<string, number> | number | boolean>,\r\n default: 300\r\n },\r\n lockScroll: { type: Boolean, default: true },\r\n zIndex: { type: Number, default: 10 },\r\n closeOnClickOverlay: { type: Boolean, default: true },\r\n absolute: { type: Boolean, default: false },\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'close'])\r\n\r\nconst b = tv(theme)\r\n\r\nconst overlayClass = computed(() => {\r\n return `${b({ absolute: props.absolute })} ${props.customClass}`\r\n})\r\n\r\nfunction handleClick() {\r\n if (props.closeOnClickOverlay) {\r\n emit('update:modelValue', false)\r\n emit('close')\r\n }\r\n}\r\n\r\n// #ifdef H5\r\nuseLockScroll(() => props.modelValue && props.lockScroll)\r\n// #endif\r\n</script>\r\n\r\n<template>\r\n <reborn-transition :show=\"modelValue\" name=\"fade\" :custom-class=\"overlayClass\" :duration=\"duration\"\r\n :custom-style=\"`z-index: ${zIndex}; ${customStyle}`\" :disable-touch-move=\"lockScroll\" @click=\"handleClick\">\r\n <slot></slot>\r\n </reborn-transition>\r\n</template>\r\n",
|
|
1583
|
+
"target": "uniapp"
|
|
1584
|
+
}
|
|
1585
|
+
]
|
|
1586
|
+
},
|
|
1587
|
+
{
|
|
1588
|
+
"name": "reborn-page",
|
|
1589
|
+
"dependencies": [],
|
|
1590
|
+
"files": [
|
|
1591
|
+
{
|
|
1592
|
+
"path": "reborn-page.config.ts",
|
|
1593
|
+
"content": "const config = {\r\n slots: {\r\n root: 'min-h-screen w-full flex flex-col gap-4 bg-gray-2 transition-colors duration-300',\r\n header: 'flex flex-col gap-2 p-4',\r\n title: 'text-xl font-bold text-gray-800 dark:text-white',\r\n description: 'text-sm text-gray-5 dark:text-gray-3',\r\n body: 'p-4',\r\n },\r\n} as const\r\n\r\nexport type PageUI = {\r\n root?: string\r\n header?: string\r\n title?: string\r\n description?: string\r\n body?: string\r\n}\r\n\r\nexport default config\r\n",
|
|
1594
|
+
"target": "uniapp"
|
|
1343
1595
|
},
|
|
1344
1596
|
{
|
|
1345
|
-
"path": "
|
|
1346
|
-
"content": "
|
|
1597
|
+
"path": "RebornPage.vue",
|
|
1598
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, type PropType } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { type PageUI } from './reborn-page.config'\r\nimport RebornToast from '@/components/reborn-toast/RebornToast.vue'\r\n\r\nconst props = defineProps({\r\n title: { type: String, default: '' },\r\n description: { type: String, default: '' },\r\n customClass: { type: String, default: '' },\r\n ui: {\r\n type: Object as PropType<PageUI>,\r\n default: () => ({}),\r\n },\r\n})\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b()\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n header: (opts?: { class?: any }) => styles.header({ class: cn(opts?.class, uiOverrides.value.header) }),\r\n title: (opts?: { class?: any }) => styles.title({ class: cn(opts?.class, uiOverrides.value.title) }),\r\n description: (opts?: { class?: any }) => styles.description({ class: cn(opts?.class, uiOverrides.value.description) }),\r\n body: (opts?: { class?: any }) => styles.body({ class: cn(opts?.class, uiOverrides.value.body) }),\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <view v-if=\"title || description || $slots.header\" :class=\"ui.header()\">\r\n <slot name=\"header\">\r\n <view v-if=\"title\" :class=\"ui.title()\">\r\n {{ title }}\r\n </view>\r\n <view v-if=\"description\" :class=\"ui.description()\">\r\n {{ description }}\r\n </view>\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.body()\">\r\n <slot />\r\n </view>\r\n\r\n <RebornToast />\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-page',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared',\r\n },\r\n}\r\n</script>\r\n",
|
|
1599
|
+
"target": "uniapp"
|
|
1600
|
+
}
|
|
1601
|
+
]
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
"name": "reborn-picker-view",
|
|
1605
|
+
"dependencies": [
|
|
1606
|
+
"clsx",
|
|
1607
|
+
"lodash-es"
|
|
1608
|
+
],
|
|
1609
|
+
"files": [
|
|
1610
|
+
{
|
|
1611
|
+
"path": "index.ts",
|
|
1612
|
+
"content": "export { default as RebornPickerView } from './RebornPickerView.vue'\r\nexport type { PickerViewProps, SelectOption } from './RebornPickerView.vue'\r\n",
|
|
1347
1613
|
"target": "uniapp"
|
|
1348
1614
|
},
|
|
1349
1615
|
{
|
|
1350
|
-
"path": "reborn-
|
|
1351
|
-
"content": "const
|
|
1616
|
+
"path": "reborn-picker-view.config.ts",
|
|
1617
|
+
"content": "const color = ['primary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst config = {\r\n slots: {\r\n wrapper: 'w-full h-full',\r\n header: 'flex flex-row items-center py-4',\r\n headerText: 'flex-1 text-center font-medium text-gray-8 dark:text-gray-2',\r\n pickerContainer: 'px-1',\r\n item: 'flex flex-row items-center justify-center w-full',\r\n itemText: 'transition-colors duration-200',\r\n indicator: 'bg-primary/10 before:content-[\"\"] before:absolute before:top-0 before:border-none after:content-[\"\"] after:absolute after:bottom-0 after:border-none',\r\n },\r\n variants: {\r\n color: {\r\n primary: {\r\n indicator: 'bg-primary/10',\r\n },\r\n success: {\r\n indicator: 'bg-success/10',\r\n },\r\n info: {\r\n indicator: 'bg-info/10',\r\n },\r\n warning: {\r\n indicator: 'bg-warning/10',\r\n },\r\n error: {\r\n indicator: 'bg-error/10',\r\n },\r\n neutral: {\r\n indicator: 'bg-neutral/10',\r\n },\r\n },\r\n active: {\r\n false: {\r\n itemText: 'text-gray-8 dark:text-gray-6',\r\n },\r\n true: {\r\n itemText: 'font-bold',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: 'primary' as (typeof color)[number], active: true as const, class: { itemText: 'text-primary dark:text-primary' } },\r\n { color: 'success' as (typeof color)[number], active: true as const, class: { itemText: 'text-success dark:text-success' } },\r\n { color: 'info' as (typeof color)[number], active: true as const, class: { itemText: 'text-info dark:text-info' } },\r\n { color: 'warning' as (typeof color)[number], active: true as const, class: { itemText: 'text-warning dark:text-warning' } },\r\n { color: 'error' as (typeof color)[number], active: true as const, class: { itemText: 'text-error dark:text-error' } },\r\n { color: 'neutral' as (typeof color)[number], active: true as const, class: { itemText: 'text-gray-9 dark:text-white' } },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as (typeof color)[number],\r\n active: false,\r\n },\r\n}\r\n\r\nexport { color as pickerColors }\r\n\r\nexport default config\r\n",
|
|
1352
1618
|
"target": "uniapp"
|
|
1353
1619
|
},
|
|
1354
1620
|
{
|
|
1355
|
-
"path": "
|
|
1356
|
-
"content": "<script lang=\"ts\">\r\n// Tabs组件的单项类型\r\nexport interface TabsItem {\r\n label: string; // 标签文本\r\n value: string | number; // 对应值\r\n disabled?: boolean; // 是否禁用\r\n};\r\n\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { TabsColors, TabsSizes, TabsVariants } from \"./reborn-tabs.config\";\r\n\r\n\r\ninterface Item extends TabsItem {\r\n isActive: boolean;\r\n}\r\n\r\ninterface Props {\r\n customClass?: string;\r\n // modelValue: string | number;\r\n list: TabsItem[];\r\n fill?: boolean; // 是否填充标签\r\n color?: typeof TabsColors[number];\r\n variant?: typeof TabsVariants[number]; // 标签类型\r\n size?: typeof TabsSizes[number]; // 标签大小\r\n disabled?: boolean; // 是否禁用\r\n justify?: 'start' | 'center' | 'end'; // 对齐方式\r\n ui?: {\r\n tabs?: string;\r\n scrollbar?: string;\r\n inner?: string;\r\n item?: string;\r\n text?: string;\r\n line?: string;\r\n slider?: string;\r\n active?: string;\r\n }\r\n}\r\n\r\ndefineSlots<{\r\n item(props: { item: Item; active: boolean }): any;\r\n}>();\r\n\r\n// 定义事件发射器\r\nconst emit = defineEmits([\"update:modelValue\", \"change\"]);\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n list: () => [],\r\n fill: false,\r\n color: TabsColors[0],\r\n variant: TabsVariants[0],\r\n size: TabsSizes[0],\r\n disabled: false,\r\n justify: 'start'\r\n});\r\nconst active = defineModel<string | number>('modelValue')\r\n\r\n// 获取当前组件实例的proxy对象\r\nconst { proxy } = getCurrentInstance()!;\r\nconst b = tv(theme)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst ui = computed(() => b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: size.value,\r\n fill: props.fill,\r\n justify: props.justify\r\n}))\r\n\r\n// 当前选中的标签值\r\n// const active = ref(props.modelValue);\r\n\r\n// 计算标签列表,增加isActive和disabled属性,便于渲染和状态判断\r\nconst list = computed(() =>\r\n props.list.map((e) => {\r\n return {\r\n label: e.label,\r\n value: e.value,\r\n // 如果未传disabled则默认为false\r\n disabled: e.disabled ?? false,\r\n // 判断当前标签是否为激活状态\r\n isActive: e.value == active.value\r\n } as Item;\r\n })\r\n);\r\n// 切换标签时触发,参数为索引\r\nasync function change(index: number) {\r\n // 如果整个Tabs被禁用,则不响应点击\r\n if (props.disabled) {\r\n return false;\r\n }\r\n\r\n // 获取当前点击标签的值\r\n const { value, disabled } = list.value[index];\r\n\r\n // 如果标签被禁用,则不响应点击\r\n if (disabled) {\r\n return false;\r\n }\r\n // 触发v-model的更新\r\n emit(\"update:modelValue\", value, list.value[index], index);\r\n // 触发change事件\r\n emit(\"change\", value, list.value[index], index);\r\n}\r\n\r\n// tab区域宽度\r\nconst tabWidth = ref(0);\r\n// tab区域左侧偏移\r\nconst tabLeft = ref(0);\r\n// 下划线左侧偏移\r\nconst lineLeft = ref(0);\r\n// 下划线宽度\r\nconst lineWidth = ref(0);\r\n// 滑块左侧偏移\r\nconst sliderLeft = ref(0);\r\n// 滑块宽度\r\nconst sliderWidth = ref(0);\r\n// 滚动条左侧偏移\r\nconst scrollLeft = ref(0);\r\n\r\n// 单个标签的位置信息类型,包含left和width\r\ntype ItemRect = {\r\n left: number;\r\n width: number;\r\n};\r\n\r\n// 所有标签的位置信息,响应式数组\r\nconst itemRects = ref<ItemRect[]>([]);\r\nconst textRects = ref<ItemRect[]>([]);\r\n\r\n// 获取当前选中标签的下标,未找到则返回0\r\nfunction getIndex() {\r\n const index = list.value.findIndex((e) => e.isActive);\r\n return index == -1 ? 0 : index;\r\n}\r\n\r\n// 更新下划线、滑块、滚动条等位置\r\n// 更新下划线、滑块、滚动条等位置\r\nfunction updatePosition() {\r\n nextTick(() => {\r\n if (itemRects.value?.length) {\r\n const index = getIndex();\r\n // 获取当前选中标签的位置信息\r\n const item = itemRects.value[index];\r\n const text = textRects.value?.[index];\r\n\r\n if (!!item) {\r\n // 计算滚动条偏移,使选中项居中\r\n // x = Content_Center - Container_Half_Width\r\n // 由于 item.left 已经标准化(相对于内容区域),公式简化为:\r\n let x = item.left - (tabWidth.value - item.width) / 2;\r\n // 防止滚动条偏移为负\r\n if (x < 0) {\r\n x = 0;\r\n }\r\n // 设置滚动条偏移\r\n scrollLeft.value = x;\r\n\r\n // 设置下划线偏移 (Text based)\r\n if (text) {\r\n // text.left 已经是 relative to content\r\n lineLeft.value = text.left;\r\n lineWidth.value = text.width;\r\n } else {\r\n // Fallback\r\n lineLeft.value = item.left + item.width / 2 - 20 / 2;\r\n lineWidth.value = 20;\r\n }\r\n\r\n // 设置滑块左侧偏移\r\n sliderLeft.value = item.left;\r\n // 设置滑块宽度\r\n sliderWidth.value = item.width;\r\n }\r\n }\r\n });\r\n}\r\n\r\n// 获取所有标签的位置信息,便于后续计算\r\nfunction getRects() {\r\n // 创建选择器查询\r\n const query = uni.createSelectorQuery().in(proxy);\r\n\r\n query.selectAll(\".reborn-tabs__item\").boundingClientRect();\r\n query.selectAll(\".reborn-tabs__text\").boundingClientRect();\r\n query.select(\".reborn-tabs__scroll-view\").scrollOffset(() => { });\r\n\r\n query.exec((nodes: any) => {\r\n const scrollNode = nodes?.[2];\r\n const currentScrollLeft = scrollNode?.scrollLeft ?? 0;\r\n\r\n // 解析查询结果, 转换为相对于内容区域的坐标\r\n // Relative_Left = Viewport_Left - Tab_Viewport_Left + Current_Scroll\r\n itemRects.value = nodes?.[0]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0\r\n } as ItemRect)) ?? [];\r\n\r\n textRects.value = nodes?.[1]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0\r\n } as ItemRect)) ?? [];\r\n\r\n // 更新下划线、滑块等位置\r\n updatePosition();\r\n });\r\n}\r\n\r\n// 刷新tab区域的宽度和位置信息\r\nfunction refresh() {\r\n // 使用 setTimeout 替代 nextTick,确保在视图层渲染完成后再获取布局信息\r\n // 尤其是在修改 size 等导致几何变化的属性时,需要一定延迟\r\n setTimeout(() => {\r\n // 创建选择器查询\r\n uni.createSelectorQuery()\r\n // 作用域限定为当前组件\r\n .in(proxy)\r\n // 选择tab容器\r\n .select(\".reborn-tabs\")\r\n // 获取容器的left和width\r\n .boundingClientRect((node: any) => {\r\n // 设置tab左侧偏移\r\n tabLeft.value = node?.left ?? 0;\r\n // 设置tab宽度\r\n tabWidth.value = node?.width ?? 0;\r\n\r\n // 获取所有标签的位置信息\r\n getRects();\r\n })\r\n .exec();\r\n }, 50);\r\n}\r\n\r\n\r\n\r\nonMounted(() => {\r\n watch(\r\n () => active.value,\r\n () => {\r\n // 更新下划线、滑块等位置\r\n updatePosition();\r\n },\r\n {\r\n // 立即执行一次\r\n immediate: true\r\n }\r\n );\r\n\r\n // 监听标签列表变化,刷新布局\r\n watch(\r\n computed(() => props.list),\r\n () => {\r\n refresh();\r\n },\r\n {\r\n immediate: true,\r\n deep: true\r\n }\r\n );\r\n\r\n // 监听其他影响布局的属性的变化\r\n watch(\r\n [\r\n computed(() => props.size),\r\n computed(() => props.variant),\r\n computed(() => props.fill),\r\n computed(() => props.justify)\r\n ],\r\n () => {\r\n refresh();\r\n }\r\n );\r\n});\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabs\" :class=\"ui.tabs({ class: cn(props.customClass, uiOverrides.tabs) })\">\r\n <scroll-view class=\"reborn-tabs__scroll-view\" :class=\"ui.scrollbar({ class: uiOverrides.scrollbar })\"\r\n :scroll-with-animation=\"true\" :scroll-x=\"true\" direction=\"horizontal\" :scroll-left=\"scrollLeft\"\r\n :show-scrollbar=\"false\">\r\n <view :class=\"ui.inner({ class: uiOverrides.inner })\">\r\n <view class=\"reborn-tabs__item\" :class=\"ui.item({ class: uiOverrides.item })\"\r\n :data-state=\"item.isActive ? 'active' : 'inactive'\"\r\n :data-disabled=\"item.disabled ? 'true' : undefined\" v-for=\"(item, index) in list\" :key=\"index\"\r\n @tap=\"change(index)\">\r\n <slot name=\"item\" :item=\"item\" :active=\"item.isActive\">\r\n <text v-if=\"item.isActive\" class=\"reborn-tabs__text\"\r\n :class=\"ui.active({ class: cn(uiOverrides.active, item.disabled || props.disabled ? 'text-gray-5 cursor-not-allowed' : '') })\">\r\n {{ item.label }}\r\n </text>\r\n <text v-else class=\"reborn-tabs__text\"\r\n :class=\"ui.text({ class: cn(uiOverrides.text, item.disabled || props.disabled ? 'text-gray-5 cursor-not-allowed' : '') })\">\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <template v-if=\"lineLeft > 0\">\r\n <view :class=\"ui.line({ class: uiOverrides.line })\"\r\n :style=\"{ transform: `translateX(${lineLeft}px)`, width: `${lineWidth}px` }\"\r\n v-if=\"variant == 'line'\"></view>\r\n <view :class=\"ui.slider({ class: uiOverrides.slider })\"\r\n :style=\"{ transform: `translateX(${sliderLeft}px)`, width: `${sliderWidth}px` }\"\r\n v-if=\"variant == 'card'\"></view>\r\n </template>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n</template>",
|
|
1621
|
+
"path": "RebornPickerView.vue",
|
|
1622
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { pickerColors } from './reborn-picker-view.config'\r\nimport { isEqual, isNull } from 'lodash-es'\r\nimport { computed, nextTick, onMounted, ref, shallowRef, watch } from 'vue'\r\nimport { initTheme, isAppAndroid, isAppIOS } from '@/lib/device'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-picker-view.config'\r\n\r\ndefineOptions({\r\n name: 'RebornPickerView',\r\n})\r\n\r\nconst props = withDefaults(defineProps<PickerViewProps>(), {\r\n color: 'primary',\r\n headers: () => [],\r\n value: () => [],\r\n columns: () => [],\r\n itemHeight: isAppIOS() ? 50 : 42,\r\n height: 300,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'change-value', values: any[]): void\r\n (e: 'change-index', indexes: number[]): void\r\n (e: 'change-item', item: any): void\r\n}>()\r\n\r\nexport interface SelectOption {\r\n label: string\r\n value: any\r\n children?: SelectOption[]\r\n [key: string]: any\r\n}\r\n\r\nexport interface PickerViewProps {\r\n /** 颜色 */\r\n color?: typeof pickerColors[number]\r\n /** 表头 */\r\n headers?: string[]\r\n /** 当前选中索引 */\r\n value?: number[]\r\n /** 列数据 */\r\n columns?: SelectOption[][]\r\n /** 每项高度 */\r\n itemHeight?: number\r\n /** 整体高度 */\r\n height?: number\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n header: ClassValue\r\n headerText: ClassValue\r\n pickerContainer: ClassValue\r\n item: ClassValue\r\n itemText: ClassValue\r\n indicator: ClassValue\r\n }>\r\n}\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n header: (opts?: { class?: any }) =>\r\n styles.header({ class: cn(opts?.class, uiOverrides.value.header) }),\r\n headerText: (opts?: { class?: any }) =>\r\n styles.headerText({ class: cn(opts?.class, uiOverrides.value.headerText) }),\r\n pickerContainer: (opts?: { class?: any }) =>\r\n styles.pickerContainer({ class: cn(opts?.class, uiOverrides.value.pickerContainer) }),\r\n item: (opts?: { class?: any }) =>\r\n styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n itemText: (opts?: { class?: any, active?: boolean, color?: any }) =>\r\n styles.itemText({ active: opts?.active, color: opts?.color, class: cn(opts?.class, uiOverrides.value.itemText) }),\r\n indicator: (opts?: { class?: any }) =>\r\n styles.indicator({ class: cn(opts?.class, uiOverrides.value.indicator) }),\r\n }\r\n})\r\n\r\n// 获取窗口宽度,用于计算选择器列宽\r\nconst { windowWidth } = uni.getWindowInfo()\r\nconst isDark = ref(false)\r\n\r\n// 顶部显示表头\r\nconst computedHeaders = computed(() => {\r\n return props.headers.slice(0, props.columns.length)\r\n})\r\n\r\n// 监听选择器值改变事件\r\nfunction onChange(e: any) {\r\n const indexs = e.detail.value\r\n\r\n // 处理快速滑动导致的索引越界\r\n indexs.forEach((v: number, i: number, arr: number[]) => {\r\n if (i < props.columns.length) {\r\n const n = props.columns[i].length\r\n if (v >= n) {\r\n arr[i] = n - 1\r\n }\r\n }\r\n })\r\n\r\n // 相同值不触发事件\r\n if (isEqual(indexs, props.value)) {\r\n return\r\n }\r\n\r\n // 获取所有列的值\r\n const values = props.columns.map((c, i) => {\r\n return c?.[indexs[i]]?.value ?? 0\r\n })\r\n const select = props.columns.map((c, i) => {\r\n return c?.[indexs[i]] ?? null\r\n })\r\n emit('change-value', values)\r\n emit('change-index', indexs)\r\n emit('change-item', select)\r\n}\r\n\r\n// === Android Canvas 渲染 ===\r\nconst columnItemRef = shallowRef<any[]>([])\r\n\r\nfunction renderColumnItem() {\r\n const fontSize = 14\r\n const color = isDark.value ? 'white' : '#666666'\r\n\r\n for (let i = 0; i < columnItemRef.value.length; i++) {\r\n const column = props.columns[i]\r\n const dom = columnItemRef.value[i]\r\n if (!dom) { continue }\r\n const rect = dom.getBoundingClientRect()\r\n const ctx = dom.getDrawableContext()\r\n if (!ctx) { continue }\r\n\r\n ctx.reset()\r\n ctx.textAlign = 'center'\r\n\r\n const x = rect.width / 2\r\n\r\n for (let j = 0; j < column.length; j++) {\r\n ctx.fillStyle = color\r\n ctx.font = `${fontSize}px`\r\n const y = 12 + (props.itemHeight - fontSize) / 2 + props.itemHeight * j\r\n ctx.fillText(column[j].label, x, y)\r\n }\r\n ctx.update()\r\n }\r\n}\r\n\r\n// 遮罩层样式\r\nconst maskStyle = computed(() => {\r\n if (isDark.value) {\r\n return `background-image: linear-gradient(180deg, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0))`\r\n }\r\n return ''\r\n})\r\n\r\n// 选择器指示器样式\r\nconst indicatorStyle = computed(() => {\r\n let str = ''\r\n const columnsCount = props.columns.length || 1\r\n const width = Math.ceil((windowWidth - 8) / columnsCount - 10)\r\n\r\n const style: Record<string, string> = {\r\n 'height': `${props.itemHeight}px`,\r\n 'width': 'calc(100% - 4px)',\r\n 'left': '2px',\r\n 'border-radius': '10px',\r\n 'box-sizing': 'border-box',\r\n 'border': 'none',\r\n }\r\n\r\n if (isAppIOS()) {\r\n if (isDark.value) {\r\n style['box-shadow'] = 'none'\r\n style.width = `${width - 3}px`\r\n }\r\n else {\r\n style.width = `${width + 2}px`\r\n }\r\n }\r\n\r\n if (isAppAndroid()) {\r\n style.width = `${width + 1}px`\r\n }\r\n\r\n const objKeys = Object.keys(style)\r\n for (let i = 0; i < objKeys.length; i++) {\r\n const key = objKeys[i]\r\n str += `${key}: ${style[key]};`\r\n }\r\n return str\r\n})\r\n\r\nfunction render() {\r\n renderColumnItem()\r\n}\r\n\r\nonMounted(() => {\r\n isDark.value = initTheme() === 'dark'\r\n nextTick(() => {\r\n render()\r\n })\r\n\r\n watch(\r\n computed(() => [props.columns, props.itemHeight]),\r\n () => {\r\n render()\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-picker-view\" :class=\"ui.wrapper()\">\r\n <view v-if=\"computedHeaders.length > 0\" :class=\"ui.header()\" @touchstart.stop @touchmove.stop @touchend.stop\r\n @touchcancel.stop>\r\n <text v-for=\"(label, index) in computedHeaders\" :key=\"index\" :class=\"ui.headerText()\">\r\n {{ label }}\r\n </text>\r\n </view>\r\n\r\n <view :class=\"ui.pickerContainer()\" :style=\"{ height: `${height}px` }\" @touchstart.stop @touchmove.stop\r\n @touchend.stop @touchcancel.stop>\r\n <picker-view class=\"h-full\" :value=\"value\" :mask-style=\"maskStyle\" :mask-top-style=\"maskStyle\"\r\n :indicator-class=\"ui.indicator()\" :mask-bottom-style=\"maskStyle\" :immediate-change=\"true\"\r\n :indicator-style=\"indicatorStyle\" @change=\"onChange\">\r\n <picker-view-column v-for=\"(column, columnIndex) in columns\" :key=\"columnIndex\">\r\n <!-- #ifdef APP-ANDROID -->\r\n <view ref=\"columnItemRef\" :style=\"{ height: `${itemHeight * column.length}px` }\" />\r\n <!-- #endif -->\r\n\r\n <!-- #ifndef APP-ANDROID -->\r\n <view v-for=\"(item, index) in column\" :key=\"index\" :class=\"ui.item()\" :style=\"{ height: `${itemHeight}px` }\">\r\n <slot :item=\"item\" :index=\"index\">\r\n <text :class=\"ui.itemText({ active: index == value[columnIndex], color })\">\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <!-- #endif -->\r\n </picker-view-column>\r\n </picker-view>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style lang=\"scss\" scoped>\r\n.reborn-picker-view {\r\n .uni-picker-view-indicator {\r\n\r\n // #ifdef H5\r\n &::after,\r\n &::before {\r\n display: none;\r\n }\r\n\r\n // #endif\r\n }\r\n}\r\n</style>\r\n",
|
|
1357
1623
|
"target": "uniapp"
|
|
1358
1624
|
}
|
|
1359
1625
|
]
|
|
1360
1626
|
},
|
|
1361
1627
|
{
|
|
1362
|
-
"name": "reborn-
|
|
1628
|
+
"name": "reborn-popover",
|
|
1363
1629
|
"dependencies": [],
|
|
1364
1630
|
"files": [
|
|
1365
1631
|
{
|
|
1366
|
-
"path": "
|
|
1367
|
-
"content": "
|
|
1632
|
+
"path": "index.ts",
|
|
1633
|
+
"content": "export { default as RebornPopover } from './RebornPopover.vue';\r\n",
|
|
1368
1634
|
"target": "web"
|
|
1369
1635
|
},
|
|
1370
1636
|
{
|
|
1371
|
-
"path": "
|
|
1372
|
-
"content": "
|
|
1637
|
+
"path": "reborn-popover.config.ts",
|
|
1638
|
+
"content": "export const popoverAnimations = {\r\n base: {\r\n enterActiveClass: \"transition ease-out duration-200\",\r\n enterToClass: \"opacity-100 scale-100 translate-x-0 translate-y-0\",\r\n leaveActiveClass: \"transition ease-in duration-150\",\r\n leaveFromClass: \"opacity-100 scale-100 translate-x-0 translate-y-0\",\r\n },\r\n top: {\r\n enterFromClass: \"opacity-0 translate-y-2 scale-95\",\r\n leaveToClass: \"opacity-0 translate-y-2 scale-95\",\r\n },\r\n bottom: {\r\n enterFromClass: \"opacity-0 -translate-y-2 scale-95\",\r\n leaveToClass: \"opacity-0 -translate-y-2 scale-95\",\r\n },\r\n left: {\r\n enterFromClass: \"opacity-0 translate-x-2 scale-95\",\r\n leaveToClass: \"opacity-0 translate-x-2 scale-95\",\r\n },\r\n right: {\r\n enterFromClass: \"opacity-0 -translate-x-2 scale-95\",\r\n leaveToClass: \"opacity-0 -translate-x-2 scale-95\",\r\n }\r\n} as const;\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"relative inline-block\",\r\n trigger: \"inline-block\",\r\n contentWrapper: \"fixed top-0 left-0 z-[9999]\",\r\n content: \"relative bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 shadow-xl rounded-xl p-3\",\r\n arrow: \"absolute w-3 h-3 border dark:border-gray-800 border-gray-200 bg-white dark:bg-gray-900\",\r\n bridge: \"absolute inset-0 z-[-1]\",\r\n mask: \"fixed inset-0 bg-black/30 z-[9998]\",\r\n },\r\n variants: {\r\n side: {\r\n top: {\r\n content: \"origin-bottom\"\r\n },\r\n bottom: {\r\n content: \"origin-top\"\r\n },\r\n left: {\r\n content: \"origin-right\"\r\n },\r\n right: {\r\n content: \"origin-left\"\r\n },\r\n },\r\n align: {\r\n start: {},\r\n center: {},\r\n end: {},\r\n },\r\n },\r\n defaultVariants: {\r\n side: \"bottom\",\r\n align: \"center\",\r\n },\r\n} as const;\r\n",
|
|
1373
1639
|
"target": "web"
|
|
1374
1640
|
},
|
|
1375
1641
|
{
|
|
1376
|
-
"path": "
|
|
1377
|
-
"content": "export { default as RebornTextarea } from \"./RebornTextarea.vue\";\r\n",
|
|
1378
|
-
"target": "
|
|
1642
|
+
"path": "RebornPopover.vue",
|
|
1643
|
+
"content": "<script setup lang=\"ts\">\r\nimport { ref, computed, watch, nextTick, onMounted, onUnmounted, toRef } from \"vue\"\r\nimport { tv } from \"~/lib/tv\"\r\nimport theme, { popoverAnimations } from \"./reborn-popover.config\"\r\nimport { cn } from \"~/lib/utils\"\r\n\r\ndefineOptions({\r\n name: \"RebornPopover\"\r\n})\r\n\r\n/**\r\n * Popover 内容配置\r\n */\r\nexport interface PopoverContentProps {\r\n /** Popover 相对于触发器的显示位置 */\r\n side?: \"top\" | \"right\" | \"bottom\" | \"left\"\r\n /** Popover 沿触发器轴线的对齐方式 */\r\n align?: \"start\" | \"center\" | \"end\"\r\n /** Popover 与触发器之间的间距 */\r\n sideOffset?: number\r\n}\r\n\r\n/**\r\n * RebornPopover 组件属性\r\n */\r\nexport interface PopoverProps {\r\n /** 触发模式:'click' (默认) 或 'hover' */\r\n mode?: \"click\" | \"hover\"\r\n /** 内容位置与偏移配置 */\r\n content?: PopoverContentProps\r\n /** 是否显示箭头 */\r\n arrow?: boolean\r\n /** 是否将 Popover 渲染到指定的 DOM 节点 (通常为 'body') */\r\n portal?: boolean | string\r\n /** 点击外部时是否关闭 Popover */\r\n dismissible?: boolean\r\n /** 受控显示状态 */\r\n open?: boolean\r\n /** 非受控默认显示状态 */\r\n defaultOpen?: boolean\r\n /** 是否显示遮罩层并捕获焦点 */\r\n modal?: boolean\r\n /** 延迟打开时间 (ms) - 适用于 hover 模式 */\r\n openDelay?: number\r\n /** 延迟关闭时间 (ms) - 适用于 hover 模式,防止意外关闭 */\r\n closeDelay?: number\r\n /** 额外的类名 */\r\n class?: any\r\n /** UI 覆盖配置 */\r\n ui?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<PopoverProps>(), {\r\n mode: \"click\",\r\n portal: true,\r\n arrow: false,\r\n dismissible: true,\r\n modal: false,\r\n openDelay: 0,\r\n closeDelay: 120,\r\n content: () => ({\r\n side: \"bottom\",\r\n align: \"center\",\r\n sideOffset: 8\r\n })\r\n})\r\n\r\nconst emit = defineEmits<{\r\n /** 当显示状态发生变化时触发 */\r\n (e: \"update:open\", v: boolean): void\r\n}>()\r\n\r\n/* ---------------- 显示状态 ---------------- */\r\n\r\n/** 内部状态,与 props.defaultOpen 或 props.open 同步 */\r\nconst internalOpen = ref(props.defaultOpen ?? props.open ?? false)\r\n\r\nwatch(() => props.open, v => {\r\n if (v !== undefined) internalOpen.value = v\r\n})\r\n\r\n/** 计算后的显示状态,处理 v-model 同步 */\r\nconst open = computed({\r\n get: () => internalOpen.value,\r\n set: v => {\r\n internalOpen.value = v\r\n emit(\"update:open\", v)\r\n }\r\n})\r\n\r\n/* ---------------- refs ---------------- */\r\n\r\nconst wrapperRef = ref<HTMLElement>()\r\nconst triggerRef = ref<HTMLElement>()\r\nconst contentRef = ref<HTMLElement>()\r\n\r\n/* ---------------- 悬停逻辑 ---------------- */\r\n\r\nlet hoverCount = 0\r\nlet hoverTimer: any = null\r\n\r\n/** 处理 hover 模式下的鼠标移入,支持延迟打开 */\r\nconst onMouseEnter = () => {\r\n if (props.mode !== \"hover\") return\r\n\r\n hoverCount++\r\n clearTimeout(hoverTimer)\r\n\r\n hoverTimer = setTimeout(() => {\r\n open.value = true\r\n }, props.openDelay)\r\n}\r\n\r\n/** 处理 hover 模式下的鼠标移出,支持延迟关闭并检查连续性 */\r\nconst onMouseLeave = () => {\r\n if (props.mode !== \"hover\") return\r\n\r\n hoverCount--\r\n\r\n hoverTimer = setTimeout(() => {\r\n if (hoverCount <= 0) open.value = false\r\n }, props.closeDelay)\r\n}\r\n\r\n/* ---------------- click trigger ---------------- */\r\n\r\nconst onClickTrigger = () => {\r\n if (props.mode === \"click\") {\r\n open.value = !open.value\r\n }\r\n}\r\n\r\n/* ---------------- 点击外部关闭 ---------------- */\r\n\r\nconst onClickOutside = (e: MouseEvent) => {\r\n if (!open.value || !props.dismissible) return\r\n\r\n const target = e.target as Node\r\n\r\n if (\r\n wrapperRef.value?.contains(target) ||\r\n contentRef.value?.contains(target)\r\n ) {\r\n return\r\n }\r\n\r\n open.value = false\r\n}\r\n\r\n/* ---------------- 位置计算 ---------------- */\r\n\r\nconst style = ref<Record<string, string>>({\r\n transform: \"translate3d(0,0,0)\"\r\n})\r\n\r\n/**\r\n * 根据触发器的边界动态计算 Popover 的位置。\r\n * 使用 offsetWidth/Height 以排除绝对定位箭头的溢出干扰。\r\n */\r\nconst calculatePosition = () => {\r\n if (!triggerRef.value || !contentRef.value) return\r\n\r\n const rect = triggerRef.value.getBoundingClientRect()\r\n // 不直接测量 contentRef 的 bounds,因为它包含了绝对定位出的箭头,\r\n // 我们需要测量定义的视觉边界内容盒子。\r\n const contentBox = contentRef.value.firstElementChild as HTMLElement\r\n const cWidth = contentBox.offsetWidth\r\n const cHeight = contentBox.offsetHeight\r\n\r\n const side = props.content?.side || \"bottom\"\r\n const align = props.content?.align || \"center\"\r\n const offset = props.content?.sideOffset ?? 8\r\n\r\n let x = rect.left\r\n let y = rect.bottom + offset\r\n\r\n // 基于 'side' 属性的坐标计算\r\n if (side === \"top\") {\r\n y = rect.top - cHeight - offset\r\n }\r\n\r\n if (side === \"left\") {\r\n x = rect.left - cWidth - offset\r\n y = rect.top + rect.height / 2 - cHeight / 2\r\n }\r\n\r\n if (side === \"right\") {\r\n x = rect.right + offset\r\n y = rect.top + rect.height / 2 - cHeight / 2\r\n }\r\n\r\n // side 轴线上的对齐逻辑\r\n if (side === \"bottom\" || side === \"top\") {\r\n if (align === \"center\") {\r\n x = rect.left + rect.width / 2 - cWidth / 2\r\n }\r\n\r\n if (align === \"end\") {\r\n x = rect.right - cWidth\r\n }\r\n }\r\n\r\n if (side === \"left\" || side === \"right\") {\r\n if (align === \"start\") {\r\n y = rect.top\r\n }\r\n\r\n if (align === \"end\") {\r\n y = rect.bottom - cHeight\r\n }\r\n }\r\n\r\n // 屏幕碰撞排查逻辑(防止 Popover 超出视口)\r\n const vWidth = window.innerWidth\r\n const vHeight = document.documentElement.clientHeight\r\n\r\n if (x < 8) x = 8\r\n if (x + cWidth > vWidth - 8) x = vWidth - cWidth - 8\r\n if (y < 8) y = 8\r\n if (y + cHeight > vHeight - 8) y = vHeight - cHeight - 8\r\n\r\n style.value = {\r\n transform: `translate3d(${x}px, ${y}px, 0)`\r\n }\r\n}\r\n\r\n\r\nlet frame: number | null = null\r\n\r\nconst updatePosition = () => {\r\n if (frame) return\r\n\r\n frame = requestAnimationFrame(() => {\r\n calculatePosition()\r\n frame = null\r\n })\r\n}\r\n\r\n\r\nlet resizeObserver: ResizeObserver | null = null;\r\n\r\n/** 当内容元素被检测到或尺寸变化时自动更新位置 */\r\nwatch(contentRef, (el) => {\r\n if (resizeObserver) {\r\n resizeObserver.disconnect()\r\n resizeObserver = null\r\n }\r\n if (el) {\r\n resizeObserver = new ResizeObserver(() => {\r\n if (open.value) updatePosition()\r\n })\r\n resizeObserver.observe(el)\r\n\r\n if (open.value) {\r\n // 使用双重 requestAnimationFrame 确保在显示状态切换(display:block)后的布局重计算已完成\r\n requestAnimationFrame(() => {\r\n requestAnimationFrame(() => calculatePosition())\r\n })\r\n }\r\n }\r\n}, { immediate: true })\r\n\r\nonMounted(() => {\r\n document.addEventListener(\"mousedown\", onClickOutside)\r\n window.addEventListener(\"resize\", updatePosition)\r\n window.addEventListener(\"scroll\", updatePosition, true)\r\n})\r\n\r\nonUnmounted(() => {\r\n document.removeEventListener(\"mousedown\", onClickOutside)\r\n window.removeEventListener(\"resize\", updatePosition)\r\n window.removeEventListener(\"scroll\", updatePosition, true)\r\n if (resizeObserver) resizeObserver.disconnect()\r\n})\r\n\r\nwatch(open, v => {\r\n if (v) {\r\n requestAnimationFrame(() => {\r\n requestAnimationFrame(() => {\r\n calculatePosition()\r\n })\r\n })\r\n }\r\n})\r\n\r\n\r\n/**\r\n * 计算指示箭头的精确样式。\r\n * 使用 clip-path 渲染一个真正的三角形,并隐藏旋转正方形中不需要的边缘。\r\n */\r\nconst arrowStyle = computed(() => {\r\n const side = props.content?.side || \"bottom\"\r\n const offsetScale = \"-6px\"\r\n\r\n // 为了形成完美的三角形并防止重叠,我们在旋转后的正方形上使用 clip-path 裁剪。\r\n if (side === \"bottom\")\r\n return {\r\n top: offsetScale, left: \"50%\", transform: \"translateX(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(0 0, 100% 0, 0 100%)\",\r\n borderBottomWidth: \"0\", borderRightWidth: \"0\"\r\n }\r\n\r\n if (side === \"top\")\r\n return {\r\n bottom: offsetScale, left: \"50%\", transform: \"translateX(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(100% 100%, 100% 0, 0 100%)\",\r\n borderTopWidth: \"0\", borderLeftWidth: \"0\"\r\n }\r\n\r\n if (side === \"left\")\r\n return {\r\n right: offsetScale, top: \"50%\", transform: \"translateY(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(100% 0, 0 0, 100% 100%)\",\r\n borderBottomWidth: \"0\", borderLeftWidth: \"0\"\r\n }\r\n\r\n if (side === \"right\")\r\n return {\r\n left: offsetScale, top: \"50%\", transform: \"translateY(-50%) rotate(45deg)\",\r\n clipPath: \"polygon(0 100%, 0 0, 100% 100%)\",\r\n borderTopWidth: \"0\", borderRightWidth: \"0\"\r\n }\r\n})\r\n\r\n/* ---------------- styles ---------------- */\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n return b({\r\n side: props.content?.side,\r\n align: props.content?.align\r\n })\r\n})\r\n\r\n/**\r\n * 计算动画类名,根据 side 动态选择方向性动画偏移\r\n */\r\nconst ani = computed(() => {\r\n const side = props.content?.side || \"bottom\"\r\n const specific = popoverAnimations[side]\r\n return {\r\n ...popoverAnimations.base,\r\n enterFromClass: cn(popoverAnimations.base.enterActiveClass && \"\", specific.enterFromClass),\r\n leaveToClass: cn(popoverAnimations.base.leaveActiveClass && \"\", specific.leaveToClass)\r\n }\r\n})\r\n\r\ndefineExpose({\r\n close: () => (open.value = false)\r\n})\r\n</script>\r\n\r\n<template>\r\n <div ref=\"wrapperRef\" :class=\"ui.wrapper({ class: props.class })\" @mouseenter=\"onMouseEnter\"\r\n @mouseleave=\"onMouseLeave\">\r\n <div ref=\"triggerRef\" :class=\"ui.trigger()\" @click=\"onClickTrigger\">\r\n <slot :open=\"open\" />\r\n </div>\r\n\r\n <Teleport :to=\"typeof portal === 'string' ? portal : 'body'\" :disabled=\"!portal\">\r\n\r\n <!-- mask -->\r\n <div v-if=\"open && modal\" :class=\"ui.mask()\" @click=\"props.dismissible && (open = false)\" />\r\n\r\n <Transition :enter-active-class=\"ani.enterActiveClass\" :enter-from-class=\"ani.enterFromClass\"\r\n :enter-to-class=\"ani.enterToClass\" :leave-active-class=\"ani.leaveActiveClass\"\r\n :leave-from-class=\"ani.leaveFromClass\" :leave-to-class=\"ani.leaveToClass\">\r\n <div v-show=\"open\" ref=\"contentRef\" :class=\"ui.contentWrapper()\" :style=\"style\"\r\n @mouseenter=\"onMouseEnter\" @mouseleave=\"onMouseLeave\">\r\n <div :class=\"ui.content()\">\r\n <slot name=\"content\" />\r\n\r\n <!-- 隐形的悬停桥接层,用于通过间隙时保持鼠标连续性 -->\r\n <div v-if=\"props.mode === 'hover'\" :class=\"ui.bridge()\"\r\n :style=\"{ margin: `-${props.content?.sideOffset ?? 8}px` }\" />\r\n\r\n <div v-if=\"arrow\" :class=\"ui.arrow()\" :style=\"arrowStyle\" />\r\n </div>\r\n </div>\r\n </Transition>\r\n\r\n </Teleport>\r\n </div>\r\n</template>",
|
|
1644
|
+
"target": "web"
|
|
1379
1645
|
},
|
|
1380
1646
|
{
|
|
1381
|
-
"path": "
|
|
1382
|
-
"content": "
|
|
1647
|
+
"path": "composables/clickoutside.ts",
|
|
1648
|
+
"content": "let queue: any[] = []\r\n\r\nexport function pushToQueue(comp: any) {\r\n queue.push(comp)\r\n}\r\n\r\nexport function removeFromQueue(comp: any) {\r\n queue = queue.filter((item) => {\r\n return item.$.uid !== comp.$.uid\r\n })\r\n}\r\n\r\nexport function closeOther(comp: any) {\r\n queue.forEach((item) => {\r\n if (item.$.uid !== comp.$.uid) {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n }\r\n })\r\n}\r\n\r\nexport function closeOutside() {\r\n queue.forEach((item) => {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n })\r\n}\r\n",
|
|
1649
|
+
"target": "uniapp"
|
|
1650
|
+
},
|
|
1651
|
+
{
|
|
1652
|
+
"path": "composables/usePopover.ts",
|
|
1653
|
+
"content": "import { getCurrentInstance, ref } from 'vue'\r\n\r\nexport function getRect(selector: string, all: boolean = false, context?: any) {\r\n return new Promise<any>((resolve) => {\r\n let query = uni.createSelectorQuery()\r\n if (context) {\r\n query = query.in(context)\r\n }\r\n query[all ? 'selectAll' : 'select'](selector)\r\n .boundingClientRect((rect) => {\r\n if (all && Array.isArray(rect) && rect.length) {\r\n resolve(rect)\r\n } else if (!all && rect) {\r\n resolve(rect)\r\n } else {\r\n resolve(null)\r\n }\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\nexport function usePopover() {\r\n const { proxy } = getCurrentInstance() as any\r\n const popStyle = ref<string>('')\r\n const arrowStyle = ref<string>('')\r\n const showStyle = ref<string>('')\r\n\r\n const arrowSide = ref<'top' | 'bottom' | 'left' | 'right' | 'none'>('none')\r\n\r\n const popWidth = ref<number>(0)\r\n const popHeight = ref<number>(0)\r\n const left = ref<number>(0)\r\n const bottom = ref<number>(0)\r\n const width = ref<number>(0)\r\n const height = ref<number>(0)\r\n const top = ref<number>(0)\r\n\r\n function noop() { }\r\n\r\n function init(\r\n side: 'top' | 'right' | 'bottom' | 'left',\r\n align: 'start' | 'center' | 'end',\r\n visibleArrow: boolean,\r\n ) {\r\n if (visibleArrow) {\r\n if (side === 'top') arrowSide.value = 'bottom'\r\n else if (side === 'bottom') arrowSide.value = 'top'\r\n else if (side === 'left') arrowSide.value = 'right'\r\n else if (side === 'right') arrowSide.value = 'left'\r\n } else {\r\n arrowSide.value = 'none'\r\n }\r\n\r\n getRect('#target', false, proxy).then((rect) => {\r\n if (!rect) return\r\n left.value = rect.left as number\r\n bottom.value = rect.bottom as number\r\n width.value = rect.width as number\r\n height.value = rect.height as number\r\n top.value = rect.top as number\r\n })\r\n\r\n getRect('#pos', false, proxy).then((rect) => {\r\n if (!rect) return\r\n popWidth.value = rect.width as number\r\n popHeight.value = rect.height as number\r\n })\r\n }\r\n\r\n function control(\r\n side: 'top' | 'right' | 'bottom' | 'left',\r\n align: 'start' | 'center' | 'end',\r\n offset: number,\r\n visibleArrow: boolean\r\n ) {\r\n const arrowSize = visibleArrow ? 9 : 0\r\n let placement = side as string\r\n if (align === 'start') placement += '-start'\r\n else if (align === 'end') placement += '-end'\r\n\r\n const verticalX = width.value / 2\r\n const verticalY = arrowSize + height.value + offset + 5\r\n const horizontalX = width.value + arrowSize + offset + 5\r\n const horizontalY = height.value / 2\r\n\r\n const offsetX = (verticalX - 17 > 0 ? 0 : verticalX - 25)\r\n const offsetY = (horizontalY - 17 > 0 ? 0 : horizontalY - 25)\r\n\r\n const placements = new Map([\r\n ['top', [`left: ${verticalX}px; bottom: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%; margin-left: -4.5px;']],\r\n ['top-start', [`left: ${offsetX}px; bottom: ${verticalY}px;`, 'left: 16px;']],\r\n ['top-end', [`right: ${offsetX}px; bottom: ${verticalY}px;`, 'right: 16px;']],\r\n\r\n ['bottom', [`left: ${verticalX}px; top: ${verticalY}px; transform: translateX(-50%);`, 'left: 50%; margin-left: -4.5px;']],\r\n ['bottom-start', [`left: ${offsetX}px; top: ${verticalY}px;`, 'left: 16px;']],\r\n ['bottom-end', [`right: ${offsetX}px; top: ${verticalY}px;`, 'right: 16px;']],\r\n\r\n ['left', [`right: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%; margin-top: -4.5px;']],\r\n ['left-start', [`right: ${horizontalX}px; top: ${offsetY}px;`, 'top: 16px;']],\r\n ['left-end', [`right: ${horizontalX}px; bottom: ${offsetY}px;`, 'bottom: 16px;']],\r\n\r\n ['right', [`left: ${horizontalX}px; top: ${horizontalY}px; transform: translateY(-50%);`, 'top: 50%; margin-top: -4.5px;']],\r\n ['right-start', [`left: ${horizontalX}px; top: ${offsetY}px;`, 'top: 16px;']],\r\n ['right-end', [`left: ${horizontalX}px; bottom: ${offsetY}px;`, 'bottom: 16px;']]\r\n ])\r\n\r\n popStyle.value = placements.get(placement)?.[0] || placements.get('bottom')![0]\r\n arrowStyle.value = placements.get(placement)?.[1] || placements.get('bottom')![1]\r\n }\r\n\r\n return { popStyle, arrowStyle, showStyle, arrowSide, init, control, noop }\r\n}\r\n",
|
|
1654
|
+
"target": "uniapp"
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
"path": "composables/useQueue.ts",
|
|
1658
|
+
"content": "import { type Ref, provide, ref } from 'vue'\r\n\r\nexport const queueKey = '__QUEUE_KEY__'\r\n\r\nexport interface Queue {\r\n queue: Ref<any[]>\r\n pushToQueue: (comp: any) => void\r\n removeFromQueue: (comp: any) => void\r\n closeOther: (comp: any) => void\r\n closeOutside: () => void\r\n}\r\n\r\nexport function useQueue() {\r\n const queue = ref<any[]>([])\r\n\r\n function pushToQueue(comp: any) {\r\n queue.value.push(comp)\r\n }\r\n\r\n function removeFromQueue(comp: any) {\r\n queue.value = queue.value.filter((item) => {\r\n return item.$.uid !== comp.$.uid\r\n })\r\n }\r\n\r\n function closeOther(comp: any) {\r\n queue.value.forEach((item) => {\r\n if (item.$.uid !== comp.$.uid) {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n }\r\n })\r\n }\r\n\r\n function closeOutside() {\r\n queue.value.forEach((item) => {\r\n if (item.$.exposed && item.$.exposed.close) {\r\n item.$.exposed.close()\r\n }\r\n })\r\n }\r\n\r\n provide(queueKey, {\r\n queue,\r\n pushToQueue,\r\n removeFromQueue,\r\n closeOther,\r\n closeOutside\r\n })\r\n\r\n return {\r\n closeOther,\r\n closeOutside\r\n }\r\n}\r\n",
|
|
1659
|
+
"target": "uniapp"
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
"path": "index.ts",
|
|
1663
|
+
"content": "import RebornPopover from './RebornPopover.vue'\r\n\r\nexport * from './types'\r\nexport { RebornPopover }\r\nexport default RebornPopover\r\n",
|
|
1664
|
+
"target": "uniapp"
|
|
1665
|
+
},
|
|
1666
|
+
{
|
|
1667
|
+
"path": "reborn-popover.config.ts",
|
|
1668
|
+
"content": "export default {\r\n slots: {\r\n base: 'relative inline-block',\r\n target: 'inline-block',\r\n pos: 'absolute box-border min-h-[36px] z-[500] transition-opacity duration-200 rounded-md bg-white',\r\n hidden: 'left-[-100vw] invisible',\r\n container: 'relative text-sm leading-normal shadow-[0_2px_15px_0_rgba(0,0,0,0.1)]',\r\n inner: 'relative whitespace-nowrap p-3 rounded-md bg-white dark:bg-gray-8',\r\n arrow: 'absolute w-[9px] h-[9px] bg-white dark:bg-zinc-800 pointer-events-none', // requires rotation in style\r\n closeIcon: 'absolute text-[12px] right-[-8px] top-[-10px] scale-50 p-2.5',\r\n menu: 'inline-block px-3 whitespace-nowrap relative rounded-md bg-white dark:bg-zinc-800 z-[500]',\r\n menuInner: 'relative py-3 flex items-center border-t border-solid border-gray-200 dark:border-gray-700 first:border-0',\r\n },\r\n variants: {\r\n // Arrow directions\r\n arrowSide: {\r\n top: 'top-[-4.5px] rotate-45 border-t border-l border-gray-200 dark:border-gray-700',\r\n bottom: 'bottom-[-4.5px] rotate-45 border-b border-r border-gray-200 dark:border-gray-700',\r\n left: 'left-[-4.5px] rotate-45 border-b border-l border-gray-200 dark:border-gray-700',\r\n right: 'right-[-4.5px] rotate-45 border-t border-r border-gray-200 dark:border-gray-700',\r\n none: '',\r\n }\r\n },\r\n defaultVariants: {\r\n arrowSide: 'none' as 'none' | 'top' | 'bottom' | 'left' | 'right',\r\n }\r\n}\r\n",
|
|
1669
|
+
"target": "uniapp"
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
"path": "RebornPopover.vue",
|
|
1673
|
+
"content": "<template>\r\n <view :class=\"[ui.base({ class: customClass })]\" id=\"popover\" @click.stop=\"popover.noop\">\r\n <!-- 使用插槽时无法获取正确宽高 -->\r\n <view :class=\"cn(ui.pos(), ui.hidden())\" id=\"pos\">\r\n <view :class=\"ui.container()\">\r\n <view v-if=\"!useContentSlot && displayMode === 'normal'\" :class=\"ui.inner()\">\r\n {{ title }}\r\n </view>\r\n <view v-if=\"!useContentSlot && displayMode === 'menu' && typeof title === 'object'\" :class=\"ui.menu()\">\r\n <view v-for=\"(item, index) in title\" :key=\"index\" :class=\"ui.menuInner()\" @click=\"menuClick(index)\">\r\n <text>{{ item.content || item.title }}</text>\r\n </view>\r\n </view>\r\n </view>\r\n </view>\r\n\r\n <RebornTransition :custom-class=\"ui.pos()\" :custom-style=\"popover.popStyle.value\" :show=\"showPopover\"\r\n name=\"fade\" :duration=\"200\">\r\n <view :class=\"ui.container()\">\r\n <view v-if=\"props.arrow\" :class=\"[ui.arrow(), theme.variants.arrowSide[popover.arrowSide.value]]\"\r\n :style=\"popover.arrowStyle.value\" />\r\n\r\n <!-- 普通模式 -->\r\n <view v-if=\"!useContentSlot && displayMode === 'normal'\" :class=\"ui.inner()\">\r\n {{ title }}\r\n </view>\r\n\r\n <!-- 列表模式 -->\r\n <view v-if=\"!useContentSlot && displayMode === 'menu'\" :class=\"ui.menu()\">\r\n <view v-for=\"(item, index) in title\" :key=\"index\" :class=\"ui.menuInner()\" @click=\"menuClick(index)\">\r\n <view style=\"display: inline-block\">{{ typeof item === 'object' && (item.content || item.title)\r\n ? (item.content || item.title) : '' }}</view>\r\n </view>\r\n </view>\r\n\r\n <!-- 用户自定义样式 -->\r\n <view v-if=\"useContentSlot\">\r\n <slot name=\"content\" />\r\n </view>\r\n </view>\r\n\r\n </RebornTransition>\r\n\r\n <!-- Click-away mask to handle closing -->\r\n <view v-if=\"showPopover && dismissible\" class=\"fixed inset-0 z-490 bg-transparent\" @click.stop=\"close\"\r\n @touchmove.stop.prevent=\"() => { }\"></view>\r\n\r\n <view @click.stop=\"toggle\" :class=\"ui.target()\" id=\"target\">\r\n <slot />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nexport default {\r\n name: 'RebornPopover',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, getCurrentInstance, inject, onBeforeMount, onBeforeUnmount, onMounted, ref, watch, useSlots } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { popoverProps, type PopoverExpose } from './types'\r\nimport { usePopover } from './composables/usePopover'\r\nimport { closeOther, pushToQueue, removeFromQueue } from './composables/clickoutside'\r\nimport { type Queue, queueKey } from './composables/useQueue'\r\nimport theme from './reborn-popover.config'\r\n\r\nimport RebornTransition from '@/components/reborn-transition/RebornTransition.vue'\r\n\r\nconst props = defineProps(popoverProps)\r\nconst emit = defineEmits(['update:modelValue', 'update:open', 'menuclick', 'change', 'open', 'close'])\r\nconst slots = useSlots()\r\nconst useContentSlot = computed(() => !!slots.content)\r\n\r\nconst b = tv(theme)\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({})\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n target: (opts?: { class?: any }) => styles.target({ class: cn(opts?.class, uiOverrides.value.target) }),\r\n pos: (opts?: { class?: any }) => styles.pos({ class: cn(opts?.class, uiOverrides.value.pos) }),\r\n hidden: (opts?: { class?: any }) => styles.hidden({ class: cn(opts?.class, uiOverrides.value.hidden) }),\r\n container: (opts?: { class?: any }) => styles.container({ class: cn(opts?.class, uiOverrides.value.container) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n arrow: (opts?: { class?: any }) => styles.arrow({ class: cn(opts?.class, uiOverrides.value.arrow) }),\r\n closeIcon: (opts?: { class?: any }) => styles.closeIcon({ class: cn(opts?.class, uiOverrides.value.closeIcon) }),\r\n menu: (opts?: { class?: any }) => styles.menu({ class: cn(opts?.class, uiOverrides.value.menu) }),\r\n menuInner: (opts?: { class?: any }) => styles.menuInner({ class: cn(opts?.class, uiOverrides.value.menuInner) })\r\n }\r\n})\r\n\r\nconst queue = inject<Queue | null>(queueKey, null)\r\nconst { proxy } = getCurrentInstance() as any\r\nconst popover = usePopover()\r\n\r\nconst showPopover = ref<boolean>(props.open || props.defaultOpen)\r\n\r\nconst side = computed(() => props.content?.side || 'bottom')\r\nconst align = computed(() => props.content?.align || 'center')\r\nconst sideOffset = computed(() => props.content?.sideOffset || 0)\r\n\r\nwatch(\r\n () => props.title,\r\n (newVal) => {\r\n const mode = props.displayMode\r\n if (mode === 'normal' && typeof newVal !== 'string') {\r\n console.error('The title type must be a string type in normal mode')\r\n } else if (mode === 'menu' && !isArray(newVal)) {\r\n console.error('The title type must be an Array type in menu mode')\r\n }\r\n }\r\n)\r\n\r\nwatch([side, align, () => props.arrow], () => {\r\n popover.init(side.value, align.value, props.arrow)\r\n})\r\n\r\nwatch(\r\n () => props.open,\r\n (newValue) => {\r\n showPopover.value = newValue\r\n }\r\n)\r\n\r\nwatch(\r\n () => showPopover.value,\r\n (newValue) => {\r\n if (newValue) {\r\n popover.control(side.value, align.value, sideOffset.value, props.arrow)\r\n if (queue && queue.closeOther) {\r\n queue.closeOther(proxy)\r\n } else {\r\n closeOther(proxy)\r\n }\r\n }\r\n popover.showStyle.value = newValue ? 'display: inline-block;' : 'display: none;'\r\n emit('change', { show: newValue })\r\n emit(newValue ? 'open' : 'close')\r\n }\r\n)\r\n\r\nfunction isArray(value: any): value is Array<any> {\r\n if (typeof Array.isArray === 'function') {\r\n return Array.isArray(value)\r\n }\r\n return Object.prototype.toString.call(value) === '[object Array]'\r\n}\r\n\r\nonMounted(() => {\r\n popover.init(side.value, align.value, props.arrow)\r\n})\r\n\r\nonBeforeMount(() => {\r\n if (queue && queue.pushToQueue) {\r\n queue.pushToQueue(proxy)\r\n } else {\r\n pushToQueue(proxy)\r\n }\r\n popover.showStyle.value = showPopover.value ? 'opacity: 1;' : 'opacity: 0;'\r\n})\r\n\r\nonBeforeUnmount(() => {\r\n if (queue && queue.removeFromQueue) {\r\n queue.removeFromQueue(proxy)\r\n } else {\r\n removeFromQueue(proxy)\r\n }\r\n})\r\n\r\nfunction menuClick(index: number) {\r\n updateModelValue(false)\r\n if (isArray(props.title)) {\r\n emit('menuclick', {\r\n item: props.title[index],\r\n index\r\n })\r\n }\r\n}\r\n\r\nfunction toggle() {\r\n if (props.disabled) return\r\n updateModelValue(!showPopover.value)\r\n}\r\n\r\nfunction open() {\r\n updateModelValue(true)\r\n}\r\n\r\nfunction close() {\r\n updateModelValue(false)\r\n}\r\n\r\nfunction updateModelValue(value: boolean) {\r\n showPopover.value = value\r\n emit('update:modelValue', value)\r\n emit('update:open', value)\r\n}\r\n\r\ndefineExpose<PopoverExpose>({\r\n open,\r\n close\r\n})\r\n</script>\r\n",
|
|
1674
|
+
"target": "uniapp"
|
|
1675
|
+
},
|
|
1676
|
+
{
|
|
1677
|
+
"path": "types.ts",
|
|
1678
|
+
"content": "import type { ExtractPropTypes, PropType } from 'vue'\r\n\r\nexport interface PopoverContentProps {\r\n /** Popover 相对于触发器的显示位置 */\r\n side?: 'top' | 'right' | 'bottom' | 'left'\r\n /** Popover 沿触发器轴线的对齐方式 */\r\n align?: 'start' | 'center' | 'end'\r\n /** Popover 与触发器之间的间距 */\r\n sideOffset?: number\r\n}\r\n\r\nexport const popoverProps = {\r\n /** 内容位置与偏移配置 */\r\n content: {\r\n type: Object as PropType<PopoverContentProps>,\r\n default: () => ({ side: 'bottom', align: 'center', sideOffset: 0 })\r\n },\r\n /** 是否显示箭头 */\r\n arrow: {\r\n type: Boolean,\r\n default: true\r\n },\r\n /** 是否将 Popover 渲染到指定的 DOM 节点 (对于 UniApp 意义不大,保留作 API 一致性) */\r\n portal: {\r\n type: [Boolean, String] as PropType<boolean | string>,\r\n default: false\r\n },\r\n /** 点击外部时是否关闭 Popover */\r\n dismissible: {\r\n type: Boolean,\r\n default: true\r\n },\r\n /** 受控显示状态 */\r\n open: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 非受控默认显示状态 */\r\n defaultOpen: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 是否显示遮罩层并捕获焦点 */\r\n modal: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /** 延迟打开时间 (ms) - 适用于 hover 模式 */\r\n openDelay: {\r\n type: Number,\r\n default: 0\r\n },\r\n /** 延迟关闭时间 (ms) - 适用于 hover 模式,防止意外关闭 */\r\n closeDelay: {\r\n type: Number,\r\n default: 0\r\n },\r\n /** 额外的类名 */\r\n customClass: {\r\n type: [String, Object, Array] as PropType<any>,\r\n default: ''\r\n },\r\n /** UI 覆盖配置 */\r\n ui: {\r\n type: Object as PropType<any>,\r\n default: () => ({})\r\n },\r\n // 以下为针对 uni-app 扩展的特有属性\r\n /**\r\n * 是否禁用 popover\r\n */\r\n disabled: {\r\n type: Boolean,\r\n default: false\r\n },\r\n /**\r\n * 显示的内容,可以通过 prop 也可以通过 slot 传入\r\n */\r\n title: {\r\n type: [String, Object] as PropType<string | Record<string, any>[]>,\r\n default: ''\r\n },\r\n useContentSlot: {\r\n type: Boolean,\r\n default: true\r\n },\r\n displayMode: {\r\n type: String as PropType<'normal' | 'menu'>,\r\n default: 'normal'\r\n }\r\n}\r\n\r\nexport type PopoverProps = ExtractPropTypes<typeof popoverProps>\r\n\r\nexport interface PopoverExpose {\r\n open: () => void\r\n close: () => void\r\n}\r\n",
|
|
1679
|
+
"target": "uniapp"
|
|
1680
|
+
}
|
|
1681
|
+
]
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
"name": "reborn-popup",
|
|
1685
|
+
"dependencies": [],
|
|
1686
|
+
"files": [
|
|
1687
|
+
{
|
|
1688
|
+
"path": "index.ts",
|
|
1689
|
+
"content": "",
|
|
1690
|
+
"target": "uniapp"
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
"path": "reborn-popup.config.ts",
|
|
1694
|
+
"content": "const PopupDirection = ['center', 'top', 'right', 'bottom', 'left'] as const\nconst PopupColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\nexport type PopupPosition = (typeof PopupDirection)[number]\nexport type PopupColor = (typeof PopupColors)[number]\n\nexport default {\n slots: {\n base: 'fixed bg-white',\n inner: 'relative',\n draw: 'mx-auto mt-2 h-1 w-10 rounded-full bg-gray-3',\n header: 'flex items-center justify-between px-4 py-2',\n title: 'text-30 font-medium text-gray-9',\n closeIcon: 'text-gray-5 cursor-pointer',\n },\n variants: {\n position: {\n center: {\n base: 'top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',\n },\n top: {\n base: 'top-0 left-0 right-0',\n },\n bottom: {\n base: 'bottom-0 left-0 right-0',\n },\n left: {\n base: 'top-0 left-0 bottom-0',\n },\n right: {\n base: 'top-0 right-0 bottom-0',\n },\n },\n color: {\n primary: {\n draw: 'bg-primary/50',\n },\n secondary: {\n draw: 'bg-secondary/50',\n },\n success: {\n draw: 'bg-success/50',\n },\n info: {\n draw: 'bg-info/50',\n },\n warning: {\n draw: 'bg-warning/50',\n },\n error: {\n draw: 'bg-error/50',\n },\n neutral: {\n draw: 'bg-neutral/50',\n },\n },\n round: {\n true: {\n base: '',\n },\n false: {\n base: '',\n },\n },\n },\n compoundVariants: [\n {\n position: 'center' as const,\n round: true,\n class: { base: 'rounded-lg' },\n },\n {\n position: 'top' as const,\n round: true,\n class: { base: 'rounded-b-2xl' },\n },\n {\n position: 'bottom' as const,\n round: true,\n class: { base: 'rounded-t-2xl' },\n },\n {\n position: 'left' as const,\n round: true,\n class: { base: 'rounded-r-2xl' },\n },\n {\n position: 'right' as const,\n round: true,\n class: { base: 'rounded-l-2xl' },\n },\n ],\n defaultVariants: {\n position: 'center' as const,\n color: 'neutral' as const,\n round: true,\n },\n}\n",
|
|
1695
|
+
"target": "uniapp"
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
"path": "RebornPopup.vue",
|
|
1699
|
+
"content": "<script lang=\"ts\">\nexport default {\n name: 'reborn-popup',\n options: {\n virtualHost: true,\n addGlobalClass: true,\n styleIsolation: 'shared'\n }\n}\n</script>\n\n<script lang=\"ts\" setup>\nimport { computed, onBeforeMount, ref, reactive, type PropType } from 'vue'\nimport RebornOverlay from '../reborn-overlay/RebornOverlay.vue'\nimport RebornTransition from '../reborn-transition/RebornTransition.vue'\nimport RebornRootPortal from '../reborn-root-portal/RebornRootPortal.vue'\nimport type { TransitionName } from '../reborn-transition/RebornTransition.vue'\nimport { tv } from '@/lib/tv'\nimport { getSystemInfo } from '@/lib/device'\nimport theme, { PopupPosition, PopupColor } from './reborn-popup.config'\n\ninterface PopupProps {\n customClass?: string\n customStyle?: string\n modelValue?: boolean\n position?: PopupPosition\n direction?: PopupPosition\n transition?: TransitionName\n closeOnClickModal?: boolean\n maskClosable?: boolean\n duration?: number | boolean\n modal?: boolean\n showMask?: boolean\n zIndex?: number\n overlayZIndex?: number\n modalStyle?: string\n safeAreaInsetBottom?: boolean\n safeAreaInsetTop?: boolean\n lazyRender?: boolean\n lockScroll?: boolean\n title?: string\n showHeader?: boolean\n showClose?: boolean\n swipeClose?: boolean\n swipeCloseThreshold?: number\n ui?: any\n rootPortal?: boolean\n enablePortal?: boolean\n color?: PopupColor\n round?: boolean\n}\n\nconst props = withDefaults(defineProps<PopupProps>(), {\n customClass: '',\n customStyle: '',\n modelValue: false,\n position: 'bottom',\n direction: 'bottom',\n closeOnClickModal: true,\n maskClosable: true,\n duration: 300,\n modal: true,\n showMask: true,\n zIndex: 10,\n overlayZIndex: 10,\n modalStyle: '',\n safeAreaInsetBottom: true,\n safeAreaInsetTop: true,\n lazyRender: true,\n lockScroll: true,\n title: '',\n showHeader: true,\n showClose: true,\n swipeClose: true,\n swipeCloseThreshold: 150,\n rootPortal: false,\n enablePortal: false,\n color: 'neutral',\n round: true,\n})\n\nconst emit = defineEmits([\n 'update:modelValue',\n 'before-enter',\n 'enter',\n 'before-leave',\n 'leave',\n 'after-leave',\n 'after-enter',\n 'click-modal',\n 'close'\n])\n\n// 兼容旧参数\nconst actualPosition = computed(() => props.direction || props.position)\nconst actualModal = computed(() => props.showMask ?? props.modal)\nconst actualCloseOnClick = computed(() => props.maskClosable ?? props.closeOnClickModal)\nconst actualZIndex = computed(() => props.overlayZIndex || props.zIndex)\nconst actualRootPortal = computed(() => props.enablePortal || props.rootPortal)\n\nconst transitionName = computed<TransitionName | TransitionName[]>(() => {\n if (props.transition) return props.transition\n if (actualPosition.value === 'center') return ['zoom-in', 'fade']\n if (actualPosition.value === 'left') return 'slide-left'\n if (actualPosition.value === 'right') return 'slide-right'\n if (actualPosition.value === 'bottom') return 'slide-up'\n if (actualPosition.value === 'top') return 'slide-down'\n return 'slide-up'\n})\n\nconst safeTop = ref<number>(0)\nconst safeBottom = ref<number>(0)\n\nconst swipe = reactive({\n isTouch: false,\n startY: 0,\n offsetY: 0,\n})\n\nconst isSwipeClose = computed(() => actualPosition.value === 'bottom' && props.swipeClose)\n\nconst style = computed(() => {\n let transform = ''\n if (swipe.isTouch && swipe.offsetY > 0) {\n transform = `transform: translateY(${swipe.offsetY}px);`\n }\n return `z-index:${actualZIndex.value}; padding-top: ${safeTop.value}px; padding-bottom: ${safeBottom.value}px; ${transform} ${props.customStyle}`\n})\n\nconst b = tv(theme)\nconst rootClass = computed(() => {\n return b({ position: actualPosition.value, color: props.color, class: props.customClass, round: props.round })\n})\n\nonBeforeMount(() => {\n const { safeArea, screenHeight, safeAreaInsets } = getSystemInfo()\n if (props.safeAreaInsetTop && safeArea && actualPosition.value === 'top') {\n // #ifdef MP-WEIXIN\n safeTop.value = safeArea.top || 44\n // #endif\n // #ifndef MP-WEIXIN\n safeTop.value = safeAreaInsets?.top || 44\n // #endif\n }\n if (props.safeAreaInsetBottom && safeArea && actualPosition.value === 'bottom') {\n // #ifdef MP-WEIXIN\n safeBottom.value = screenHeight - (safeArea.bottom || 0)\n // #endif\n // #ifndef MP-WEIXIN\n safeBottom.value = safeAreaInsets ? safeAreaInsets.bottom : 0\n // #endif\n }\n})\n\nfunction handleClickModal() {\n emit('click-modal')\n if (actualCloseOnClick.value) {\n close()\n }\n}\n\nfunction close() {\n emit('close')\n emit('update:modelValue', false)\n}\n\nfunction onTouchStart(e: any) {\n if (isSwipeClose.value) {\n swipe.isTouch = true\n swipe.startY = e.touches[0].clientY\n }\n}\n\nfunction onTouchMove(e: any) {\n if (swipe.isTouch) {\n const offsetY = e.touches[0].clientY - swipe.startY\n if (offsetY > 0) {\n swipe.offsetY = offsetY\n }\n }\n}\n\nfunction onTouchEnd() {\n if (swipe.isTouch) {\n swipe.isTouch = false\n if (swipe.offsetY > props.swipeCloseThreshold) {\n close()\n }\n swipe.offsetY = 0\n }\n}\n</script>\n\n<template>\n <reborn-root-portal v-if=\"actualRootPortal\">\n <view class=\"rb-popup-wrapper\">\n <reborn-overlay v-if=\"actualModal\" :model-value=\"modelValue\" :z-index=\"actualZIndex\" :lock-scroll=\"lockScroll\"\n :duration=\"duration\" :custom-style=\"modalStyle\" @click=\"handleClickModal\" />\n <reborn-transition :lazy-render=\"lazyRender\" :custom-class=\"rootClass.base()\" :custom-style=\"style\"\n :duration=\"duration\" :show=\"modelValue\" :name=\"transitionName\" @before-enter=\"emit('before-enter')\"\n @enter=\"emit('enter')\" @after-enter=\"emit('after-enter')\" @before-leave=\"emit('before-leave')\"\n @leave=\"emit('leave')\" @after-leave=\"emit('after-leave')\" @touchstart=\"onTouchStart\" @touchmove=\"onTouchMove\"\n @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\">\n <view :class=\"rootClass.inner()\">\n <view v-if=\"isSwipeClose\" :class=\"rootClass.draw()\" />\n <view v-if=\"showHeader\" :class=\"rootClass.header()\">\n <slot name=\"header\">\n <text :class=\"rootClass.title()\">{{ title }}</text>\n </slot>\n <text v-if=\"showClose\" :class=\"rootClass.closeIcon()\" class=\"i-lucide-x\" @click=\"close\" />\n </view>\n <slot />\n </view>\n </reborn-transition>\n </view>\n </reborn-root-portal>\n\n <view v-else class=\"rb-popup-wrapper\">\n <reborn-overlay v-if=\"actualModal\" :model-value=\"modelValue\" :z-index=\"actualZIndex\" :lock-scroll=\"lockScroll\"\n :duration=\"duration\" :custom-style=\"modalStyle\" @click=\"handleClickModal\" />\n <reborn-transition :lazy-render=\"lazyRender\" :custom-class=\"rootClass.base()\" :custom-style=\"style\"\n :duration=\"duration\" :show=\"modelValue\" :name=\"transitionName\" @before-enter=\"emit('before-enter')\"\n @enter=\"emit('enter')\" @after-enter=\"emit('after-enter')\" @before-leave=\"emit('before-leave')\"\n @leave=\"emit('leave')\" @after-leave=\"emit('after-leave')\" @touchstart=\"onTouchStart\" @touchmove=\"onTouchMove\"\n @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\">\n <view :class=\"rootClass.inner()\">\n <view v-if=\"isSwipeClose\" :class=\"rootClass.draw()\" />\n <view v-if=\"showHeader\" :class=\"rootClass.header()\">\n <slot name=\"header\">\n <text :class=\"rootClass.title()\">{{ title }}</text>\n </slot>\n <text v-if=\"showClose\" :class=\"rootClass.closeIcon()\" class=\"i-lucide-x\" @click=\"close\" />\n </view>\n <slot />\n </view>\n </reborn-transition>\n </view>\n</template>\n",
|
|
1700
|
+
"target": "uniapp"
|
|
1701
|
+
}
|
|
1702
|
+
]
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
"name": "reborn-qrcode",
|
|
1706
|
+
"dependencies": [],
|
|
1707
|
+
"files": [
|
|
1708
|
+
{
|
|
1709
|
+
"path": "index.ts",
|
|
1710
|
+
"content": "export { qrcodeMode, default as rebornQrcodeConfig } from './reborn-qrcode.config'\r\nexport { default as RebornQrcode } from './RebornQrcode.vue'\r\nexport { type ClQrcodeMode, eccLevel } from './types'\r\n"
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
"path": "qrcode.ts",
|
|
1714
|
+
"content": "export type GenerateFrameResult = {\r\n\tframeBuffer: Uint8Array;\r\n\twidth: number;\r\n};\r\n\r\n/**\r\n * 二维码生成器\r\n * @description 纯 UTS 实现的二维码生成算法,支持多平台,兼容 uni-app x。核心算法参考 QR Code 标准,支持自定义纠错级别、自动适配内容长度。\r\n * @version 1.0.0\r\n * @平台兼容性 App、H5、微信小程序、UTS\r\n * @注意事项\r\n * - 仅支持 8bit 字符串内容,不支持数字/字母/汉字等模式优化\r\n * - 生成结果为二维码点阵数据和宽度,需配合 canvas 绘制\r\n * - 纠错级别支持 'L'/'M'/'Q'/'H',默认 'L'\r\n */\r\n\r\n// 对齐块间距表 - 不同版本二维码的对齐块分布位置\r\nconst ALIGNMENT_DELTA = [\r\n\t0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,\r\n\t26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28\r\n] as number[];\r\n\r\n// 纠错块参数表 - 每个版本包含4个参数:块数、数据宽度、纠错宽度\r\nconst ECC_BLOCKS = [\r\n\t1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22,\r\n\t1, 0, 16, 28, 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32,\r\n\t18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, 2, 0,\r\n\t68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,\r\n\t1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22,\r\n\t4, 4, 16, 20, 4, 4, 12, 24, 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81,\r\n\t20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4,\r\n\t14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,\r\n\t11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98,\r\n\t24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2,\r\n\t17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, 3, 4, 113, 28, 3, 11, 44,\r\n\t26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,\r\n\t4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16,\r\n\t24, 30, 34, 0, 13, 24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117,\r\n\t30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30,\r\n\t22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8, 4, 122, 30, 22,\r\n\t3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31,\r\n\t15, 30, 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47,\r\n\t28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15,\r\n\t30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1, 115, 30, 14, 21, 46,\r\n\t28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16,\r\n\t30, 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47,\r\n\t28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15,\r\n\t30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20, 4, 117, 30, 40, 7, 47,\r\n\t28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15,\r\n\t30\r\n] as number[];\r\n\r\n// 纠错级别映射表 - 将人类可读的纠错级别映射为内部数值\r\nconst ECC_LEVELS = new Map<string, number>([\r\n\t[\"L\", 1],\r\n\t[\"M\", 2],\r\n\t[\"Q\", 3],\r\n\t[\"H\", 4]\r\n]);\r\n\r\n// 最终格式信息掩码表 - 用于格式信息区域的掩码计算(level << 3 | mask)\r\nconst FINAL_FORMAT = [\r\n\t0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976 /* L */, 0x5412, 0x5125, 0x5e7c,\r\n\t0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0 /* M */, 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183,\r\n\t0x2eda, 0x2bed /* Q */, 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b /* H */\r\n];\r\n\r\n// Galois域指数表 - 用于纠错码计算的查找表\r\nconst GALOIS_EXPONENT = [\r\n\t0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,\r\n\t0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,\r\n\t0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,\r\n\t0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,\r\n\t0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,\r\n\t0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,\r\n\t0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,\r\n\t0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,\r\n\t0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,\r\n\t0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,\r\n\t0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,\r\n\t0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,\r\n\t0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,\r\n\t0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,\r\n\t0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,\r\n\t0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00\r\n];\r\n\r\n// Galois域对数表 - 用于纠错码计算的反向查找表\r\nconst GALOIS_LOG = [\r\n\t0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,\r\n\t0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,\r\n\t0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,\r\n\t0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,\r\n\t0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,\r\n\t0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,\r\n\t0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,\r\n\t0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,\r\n\t0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,\r\n\t0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,\r\n\t0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,\r\n\t0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,\r\n\t0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,\r\n\t0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,\r\n\t0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,\r\n\t0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf\r\n];\r\n\r\n// 二维码质量评估系数 - 用于计算最佳掩码模式\r\n// N1: 连续5个及以上同色模块的惩罚分数\r\nconst N1 = 3;\r\n// N2: 2x2同色模块区域的惩罚分数\r\nconst N2 = 3;\r\n// N3: 类似定位图形的图案(1:1:3:1:1)的惩罚分数\r\nconst N3 = 40;\r\n// N4: 黑白模块比例不均衡的惩罚分数\r\nconst N4 = 10;\r\n\r\n// 版本信息掩码表 - 用于在二维码中嵌入版本信息\r\nconst VERSION_BLOCK = [\r\n\t0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532,\r\n\t0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5,\r\n\t0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69\r\n];\r\n\r\n/**\r\n * 生成二维码点阵\r\n * @param _str 输入字符串,支持任意文本内容,默认 null 表示空字符串\r\n * @param ecc 纠错级别,可选 'L' | 'M' | 'Q' | 'H',默认 'L'\r\n * @returns {GenerateFrameResult} 返回二维码点阵数据和宽度\r\n */\r\nexport function generateFrame(\r\n\t_str: string | null = null,\r\n\tecc: string | null = null\r\n): GenerateFrameResult {\r\n\t// 变量声明区,所有临时变量、缓冲区\r\n\tlet i: number;\r\n\tlet t: number;\r\n\tlet j: number;\r\n\tlet k: number;\r\n\tlet m: number;\r\n\tlet v: number;\r\n\tlet x: number;\r\n\tlet y: number;\r\n\tlet version: number;\r\n\tlet str = _str == null ? \"\" : _str;\r\n\tlet width = 0;\r\n\t// 获取纠错级别数值\r\n\tlet eccLevel = ECC_LEVELS.get(ecc == null ? \"L\" : ecc)!;\r\n\r\n\t// Data block\r\n\t// 数据块、纠错块、块数\r\n\tlet dataBlock: number;\r\n\tlet eccBlock: number;\r\n\tlet neccBlock1: number;\r\n\tlet neccBlock2: number;\r\n\r\n\t// ECC buffer.\r\n\t// 纠错码缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet eccBuffer: Uint8Array;\r\n\r\n\t// Image buffer.\r\n\t// 二维码点阵缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet frameBuffer = new Uint8Array(0);\r\n\r\n\t// Fixed part of the image.\r\n\t// 点阵掩码缓冲区(标记不可变区域) - 先初始化为空数组,后面会重新赋值\r\n\tlet frameMask = new Uint8Array(0);\r\n\r\n\t// Generator polynomial.\r\n\t// 生成多项式缓冲区(纠错码计算用) - 先初始化为空数组,后面会重新赋值\r\n\tlet polynomial = new Uint8Array(0);\r\n\r\n\t// Data input buffer.\r\n\t// 数据输入缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet stringBuffer = new Uint8Array(0);\r\n\r\n\t/**\r\n\t * 设置掩码位,表示该点为不可变区域(对称处理)\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction setMask(_x: number, _y: number) {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit *= y;\r\n\t\tbit += y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\r\n\t\tframeMask[bit] = 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 添加对齐块,设置对应点阵和掩码\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction addAlignment(_x: number, _y: number) {\r\n\t\tlet i: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tframeBuffer[x + width * y] = 1;\r\n\r\n\t\tfor (i = -2; i < 2; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i + 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i)] = 1;\r\n\t\t\tframeBuffer[x + i + 1 + width * (y + 2)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < 2; i++) {\r\n\t\t\tsetMask(x - 1, y + i);\r\n\t\t\tsetMask(x + 1, y - i);\r\n\t\t\tsetMask(x - i, y - 1);\r\n\t\t\tsetMask(x + i, y + 1);\r\n\t\t}\r\n\r\n\t\tfor (i = 2; i < 4; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i - 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i - 2)] = 1;\r\n\t\t\tframeBuffer[x - 1 + width * (y + i - 2)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Galois 域取模运算\r\n\t * @param _x 输入数值\r\n\t * @returns {number} 取模结果\r\n\t */\r\n\tfunction modN(_x: number): number {\r\n\t\tvar x = _x;\r\n\t\twhile (x >= 255) {\r\n\t\t\tx -= 255;\r\n\t\t\tx = (x >> 8) + (x & 255);\r\n\t\t}\r\n\r\n\t\treturn x;\r\n\t}\r\n\r\n\t/**\r\n\t * 计算并追加纠错码到数据块\r\n\t * @param _data 数据起始索引\r\n\t * @param _dataLength 数据长度\r\n\t * @param _ecc 纠错码起始索引\r\n\t * @param _eccLength 纠错码长度\r\n\t */\r\n\tfunction appendData(_data: number, _dataLength: number, _ecc: number, _eccLength: number) {\r\n\t\tlet bit: number;\r\n\t\tlet i: number;\r\n\t\tlet j: number;\r\n\t\tlet data = _data;\r\n\t\tlet dataLength = _dataLength;\r\n\t\tlet ecc = _ecc;\r\n\t\tlet eccLength = _eccLength;\r\n\r\n\t\tfor (i = 0; i < eccLength; i++) {\r\n\t\t\tstringBuffer[ecc + i] = 0;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < dataLength; i++) {\r\n\t\t\tbit = GALOIS_LOG[stringBuffer[data + i] ^ stringBuffer[ecc]];\r\n\r\n\t\t\tif (bit != 255) {\r\n\t\t\t\tfor (j = 1; j < eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[ecc + j - 1] =\r\n\t\t\t\t\t\tstringBuffer[ecc + j] ^\r\n\t\t\t\t\t\tGALOIS_EXPONENT[modN(bit + polynomial[eccLength - j])];\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tfor (j = ecc; j < ecc + eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[j] = stringBuffer[j + 1];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstringBuffer[ecc + eccLength - 1] =\r\n\t\t\t\tbit == 255 ? 0 : GALOIS_EXPONENT[modN(bit + polynomial[0])];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 判断某点是否为掩码区域\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t * @returns {boolean} 是否为掩码\r\n\t */\r\n\tfunction isMasked(_x: number, _y: number): boolean {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit += y * y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\t\treturn frameMask[bit] == 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 根据 QR Code 标准,应用指定的掩码 pattern\r\n\t * @param mask 掩码编号 (0-7)\r\n\t */\r\n\tfunction applyMask(mask: number) {\r\n\t\tfor (let y = 0; y < width; y++) {\r\n\t\t\tfor (let x = 0; x < width; x++) {\r\n\t\t\t\tif (!isMasked(x, y)) {\r\n\t\t\t\t\tlet shouldInvert = false;\r\n\t\t\t\t\tswitch (mask) {\r\n\t\t\t\t\t\tcase 0:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 1:\r\n\t\t\t\t\t\t\tshouldInvert = y % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 2:\r\n\t\t\t\t\t\t\tshouldInvert = x % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 3:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 4:\r\n\t\t\t\t\t\t\tshouldInvert = (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 5:\r\n\t\t\t\t\t\t\tshouldInvert = ((x * y) % 2) + ((x * y) % 3) == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 6:\r\n\t\t\t\t\t\t\tshouldInvert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 7:\r\n\t\t\t\t\t\t\tshouldInvert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (shouldInvert) {\r\n\t\t\t\t\t\tframeBuffer[x + y * width] ^= 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 计算连续同色块的\"坏度\"分数\r\n\t * @param runLengths\r\n\t * @param length 块长度\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction getBadRuns(runLengths: number[], length: number): number {\r\n\t\tlet badRuns = 0;\r\n\t\tlet i: number;\r\n\r\n\t\tfor (i = 0; i <= length; i++) {\r\n\t\t\tif (i < runLengths.length && runLengths[i] >= 5) {\r\n\t\t\t\tbadRuns += N1 + runLengths[i] - 5;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// FBFFFBF as in finder.\r\n\t\tfor (i = 3; i < length - 1; i += 2) {\r\n\t\t\t// 检查数组索引是否越界\r\n\t\t\tif (i + 2 >= runLengths.length || i - 3 < 0) {\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (\r\n\t\t\t\trunLengths[i - 2] == runLengths[i + 2] &&\r\n\t\t\t\trunLengths[i + 2] == runLengths[i - 1] &&\r\n\t\t\t\trunLengths[i - 1] == runLengths[i + 1] &&\r\n\t\t\t\trunLengths[i - 1] * 3 == runLengths[i] &&\r\n\t\t\t\t// Background around the foreground pattern? Not part of the specs.\r\n\t\t\t\t(runLengths[i - 3] == 0 ||\r\n\t\t\t\t\ti + 3 > length ||\r\n\t\t\t\t\trunLengths[i - 3] * 3 >= runLengths[i] * 4 ||\r\n\t\t\t\t\trunLengths[i + 3] * 3 >= runLengths[i] * 4)\r\n\t\t\t) {\r\n\t\t\t\tbadRuns += N3;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn badRuns;\r\n\t}\r\n\r\n\t/**\r\n\t * 评估当前二维码点阵的整体\"坏度\"\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction checkBadness(): number {\r\n\t\tlet b: number;\r\n\t\tlet b1: number;\r\n\t\tlet bad = 0;\r\n\t\tlet big: number;\r\n\t\tlet bw = 0;\r\n\t\tlet count = 0;\r\n\t\tlet h: number;\r\n\t\tlet x: number;\r\n\t\tlet y: number;\r\n\t\t// 优化:在函数内创建badBuffer,避免外部变量的内存泄漏风险\r\n\t\tlet badBuffer = new Array<number>(width);\r\n\r\n\t\t// Blocks of same colour.\r\n\t\tfor (y = 0; y < width - 1; y++) {\r\n\t\t\tfor (x = 0; x < width - 1; x++) {\r\n\t\t\t\t// All foreground colour.\r\n\t\t\t\tif (\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 1) ||\r\n\t\t\t\t\t// All background colour.\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 0)\r\n\t\t\t\t) {\r\n\t\t\t\t\tbad += N2;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// X runs\r\n\t\tfor (y = 0; y < width; y++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (x = 0; x < width; x++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t\tbw += b > 0 ? 1 : -1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\tif (bw < 0) bw = -bw;\r\n\r\n\t\tbig = bw;\r\n\t\tbig += big << 2;\r\n\t\tbig <<= 1;\r\n\r\n\t\twhile (big > width * width) {\r\n\t\t\tbig -= width * width;\r\n\t\t\tcount++;\r\n\t\t}\r\n\r\n\t\tbad += count * N4;\r\n\r\n\t\t// Y runs.\r\n\t\tfor (x = 0; x < width; x++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (y = 0; y < width; y++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\treturn bad;\r\n\t}\r\n\r\n\t/**\r\n\t * 将字符串转为 UTF-8 编码,兼容多平台\r\n\t * @param str 输入字符串\r\n\t * @returns {string} UTF-8 编码字符串\r\n\t */\r\n\tfunction toUtf8(str: string): string {\r\n\t\tlet out = \"\";\r\n\t\tlet i: number;\r\n\t\tlet len: number;\r\n\t\tlet c: number;\r\n\t\tlen = str.length;\r\n\t\tfor (i = 0; i < len; i++) {\r\n\t\t\tc = str.charCodeAt(i)!;\r\n\t\t\tif (c >= 0x0001 && c <= 0x007f) {\r\n\t\t\t\tout += str.charAt(i);\r\n\t\t\t} else if (c > 0x07ff) {\r\n\t\t\t\tout += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t} else {\r\n\t\t\t\tout += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\t//end functions\r\n\r\n\t// Find the smallest version that fits the string.\r\n\t// 1. 字符串转 UTF-8,计算长度\r\n\tstr = toUtf8(str);\r\n\tt = str.length;\r\n\r\n\t// 2. 自动选择最小可用版本\r\n\tversion = 0;\r\n\tdo {\r\n\t\tversion++;\r\n\t\tk = (eccLevel - 1) * 4 + (version - 1) * 16;\r\n\t\tneccBlock1 = ECC_BLOCKS[k++];\r\n\t\tneccBlock2 = ECC_BLOCKS[k++];\r\n\t\tdataBlock = ECC_BLOCKS[k++];\r\n\t\teccBlock = ECC_BLOCKS[k];\r\n\r\n\t\tk = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2 - 3 + (version <= 9 ? 1 : 0);\r\n\r\n\t\tif (t <= k) break;\r\n\t} while (version < 40);\r\n\r\n\t// FIXME: Ensure that it fits insted of being truncated.\r\n\t// 3. 计算二维码宽度\r\n\twidth = 17 + 4 * version;\r\n\r\n\t// Allocate, clear and setup data structures.\r\n\t// 4. 分配缓冲区, 使用定长的 Uint8Array 优化内存\r\n\tv = dataBlock + (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\teccBuffer = new Uint8Array(v);\r\n\tstringBuffer = new Uint8Array(v);\r\n\r\n\t// 5. 预分配点阵、掩码缓冲区\r\n\tframeBuffer = new Uint8Array(width * width);\r\n\tframeMask = new Uint8Array(Math.floor((width * (width + 1) + 1) / 2));\r\n\r\n\t// Insert finders: Foreground colour to frame and background to mask.\r\n\t// 插入定位点: 前景色为二维码,背景色为掩码\r\n\tfor (t = 0; t < 3; t++) {\r\n\t\tk = 0;\r\n\t\ty = 0;\r\n\t\tif (t == 1) k = width - 7;\r\n\t\tif (t == 2) y = width - 7;\r\n\r\n\t\tframeBuffer[y + 3 + width * (k + 3)] = 1;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tframeBuffer[y + x + width * k] = 1;\r\n\t\t\tframeBuffer[y + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 6 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 6)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (x = 1; x < 5; x++) {\r\n\t\t\tsetMask(y + x, k + 1);\r\n\t\t\tsetMask(y + 1, k + x + 1);\r\n\t\t\tsetMask(y + 5, k + x);\r\n\t\t\tsetMask(y + x + 1, k + 5);\r\n\t\t}\r\n\r\n\t\tfor (x = 2; x < 4; x++) {\r\n\t\t\tframeBuffer[y + x + width * (k + 2)] = 1;\r\n\t\t\tframeBuffer[y + 2 + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 4 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 4)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Alignment blocks.\r\n\t// 插入对齐点: 前景色为二维码,背景色为掩码\r\n\tif (version > 1) {\r\n\t\tt = ALIGNMENT_DELTA[version];\r\n\t\ty = width - 7;\r\n\r\n\t\tfor (; ;) {\r\n\t\t\tx = width - 7;\r\n\r\n\t\t\twhile (x > t - 3) {\r\n\t\t\t\taddAlignment(x, y);\r\n\r\n\t\t\t\tif (x < t) break;\r\n\r\n\t\t\t\tx -= t;\r\n\t\t\t}\r\n\r\n\t\t\tif (y <= t + 9) break;\r\n\r\n\t\t\ty -= t;\r\n\r\n\t\t\taddAlignment(6, y);\r\n\t\t\taddAlignment(y, 6);\r\n\t\t}\r\n\t}\r\n\r\n\t// Single foreground cell.\r\n\t// 插入单个前景色单元格: 前景色为二维码,背景色为掩码\r\n\tframeBuffer[8 + width * (width - 8)] = 1;\r\n\r\n\t// Timing gap (mask only).\r\n\t// 插入时间间隔: 掩码\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(7, y);\r\n\t\tsetMask(width - 8, y);\r\n\t\tsetMask(7, y + width - 7);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x, 7);\r\n\t\tsetMask(x + width - 8, 7);\r\n\t\tsetMask(x, width - 8);\r\n\t}\r\n\r\n\t// Reserve mask, format area.\r\n\t// 保留掩码,格式化区域\r\n\tfor (x = 0; x < 9; x++) {\r\n\t\tsetMask(x, 8);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x + width - 8, 8);\r\n\t\tsetMask(8, x);\r\n\t}\r\n\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(8, y + width - 7);\r\n\t}\r\n\r\n\t// Timing row/column.\r\n\t// 插入时间间隔行/列: 掩码\r\n\tfor (x = 0; x < width - 14; x++) {\r\n\t\tif ((x & 1) > 0) {\r\n\t\t\tsetMask(8 + x, 6);\r\n\t\t\tsetMask(6, 8 + x);\r\n\t\t} else {\r\n\t\t\tframeBuffer[8 + x + width * 6] = 1;\r\n\t\t\tframeBuffer[6 + width * (8 + x)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Version block.\r\n\tif (version > 6) {\r\n\t\tt = VERSION_BLOCK[version - 7];\r\n\t\tk = 17;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tfor (y = 0; y < 3; y++) {\r\n\t\t\t\tif ((1 & (k > 11 ? version >> (k - 12) : t >> k)) > 0) {\r\n\t\t\t\t\tframeBuffer[5 - x + width * (2 - y + width - 11)] = 1;\r\n\t\t\t\t\tframeBuffer[2 - y + width - 11 + width * (5 - x)] = 1;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsetMask(5 - x, 2 - y + width - 11);\r\n\t\t\t\t\tsetMask(2 - y + width - 11, 5 - x);\r\n\t\t\t\t}\r\n\t\t\t\tk--;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Sync mask bits. Only set above for background cells, so now add the foreground.\r\n\t// 同步掩码位。只有上方的背景单元格需要设置,现在添加前景色。\r\n\tfor (y = 0; y < width; y++) {\r\n\t\tfor (x = 0; x <= y; x++) {\r\n\t\t\tif (frameBuffer[x + width * y] > 0) {\r\n\t\t\t\tsetMask(x, y);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Convert string to bit stream. 8-bit data to QR-coded 8-bit data (numeric, alphanum, or kanji\r\n\t// not supported).\r\n\t// 将字符串转换为位流。8位数据转换为QR编码的8位数据(不支持数字、字母或汉字)。\r\n\tv = str.length;\r\n\r\n\t// String to array.\r\n\tfor (i = 0; i < v; i++) {\r\n\t\teccBuffer[i] = str.charCodeAt(i)!;\r\n\t}\r\n\r\n\t//++++++++++++++++++++==============\r\n\tstringBuffer.set(eccBuffer.subarray(0, v));\r\n\r\n\t// Calculate max string length.\r\n\tx = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tif (v >= x - 2) {\r\n\t\tv = x - 2;\r\n\r\n\t\tif (version > 9) v--;\r\n\t}\r\n\r\n\t// Shift and re-pack to insert length prefix.\r\n\t// 移位并重新打包以插入长度前缀。\r\n\ti = v;\r\n\r\n\tif (version > 9) {\r\n\t\tstringBuffer[i + 2] = 0;\r\n\t\tstringBuffer[i + 3] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 3] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 2] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[2] |= 255 & (v << 4);\r\n\t\tstringBuffer[1] = v >> 4;\r\n\t\tstringBuffer[0] = 0x40 | (v >> 12);\r\n\t} else {\r\n\t\tstringBuffer[i + 1] = 0;\r\n\t\tstringBuffer[i + 2] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 2] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 1] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[1] |= 255 & (v << 4);\r\n\t\tstringBuffer[0] = 0x40 | (v >> 4);\r\n\t}\r\n\r\n\t// Fill to end with pad pattern.\r\n\t// 用填充模式填充到结束。\r\n\ti = v + 3 - (version < 10 ? 1 : 0);\r\n\r\n\twhile (i < x) {\r\n\t\tstringBuffer[i++] = 0xec;\r\n\t\tstringBuffer[i++] = 0x11;\r\n\t}\r\n\r\n\t// Calculate generator polynomial.\r\n\t// 计算生成多项式。\r\n\tpolynomial = new Uint8Array(eccBlock + 1);\r\n\tpolynomial[0] = 1;\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i + 1] = 1;\r\n\r\n\t\tfor (j = i; j > 0; j--) {\r\n\t\t\tpolynomial[j] =\r\n\t\t\t\tpolynomial[j] > 0\r\n\t\t\t\t\t? polynomial[j - 1] ^ GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[j]] + i)]\r\n\t\t\t\t\t: polynomial[j - 1];\r\n\t\t}\r\n\r\n\t\tpolynomial[0] = GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[0]] + i)];\r\n\t}\r\n\r\n\t// Use logs for generator polynomial to save calculation step.\r\n\t// 使用对数计算生成多项式以节省计算步骤。\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i] = GALOIS_LOG[polynomial[i]];\r\n\t}\r\n\r\n\t// Append ECC to data buffer.\r\n\t// 将ECC附加到数据缓冲区。\r\n\tk = x;\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < neccBlock1; i++) {\r\n\t\tappendData(y, dataBlock, k, eccBlock);\r\n\r\n\t\ty += dataBlock;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\tfor (i = 0; i < neccBlock2; i++) {\r\n\t\tappendData(y, dataBlock + 1, k, eccBlock);\r\n\r\n\t\ty += dataBlock + 1;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\t// Interleave blocks.\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < dataBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[i + j * dataBlock];\r\n\t\t}\r\n\r\n\t\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t\t}\r\n\t}\r\n\r\n\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t}\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1 + neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[x + i + j * eccBlock];\r\n\t\t}\r\n\t}\r\n\r\n\tstringBuffer.set(eccBuffer);\r\n\r\n\t// Pack bits into frame avoiding masked area.\r\n\t// 将位流打包到帧中,避免掩码区域。\r\n\tx = width - 1;\r\n\ty = width - 1;\r\n\tk = 1;\r\n\tv = 1;\r\n\r\n\t// inteleaved data and ECC codes.\r\n\t// 交错数据和ECC代码。\r\n\tm = (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tfor (i = 0; i < m; i++) {\r\n\t\tt = stringBuffer[i];\r\n\r\n\t\tfor (j = 0; j < 8; j++) {\r\n\t\t\tif ((0x80 & t) > 0) {\r\n\t\t\t\tframeBuffer[x + width * y] = 1;\r\n\t\t\t}\r\n\r\n\t\t\t// Find next fill position.\r\n\t\t\t// 找到下一个填充位置。\r\n\t\t\tdo {\r\n\t\t\t\tif (v > 0) {\r\n\t\t\t\t\tx--;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tx++;\r\n\r\n\t\t\t\t\tif (k > 0) {\r\n\t\t\t\t\t\tif (y != 0) {\r\n\t\t\t\t\t\t\ty--;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty = 9;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (y != width - 1) {\r\n\t\t\t\t\t\t\ty++;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty -= 8;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tv = v == 0 ? 1 : 0;\r\n\t\t\t} while (isMasked(x, y));\r\n\t\t\tt <<= 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Save pre-mask copy of frame.\r\n\tconst frameBufferCopy = frameBuffer.slice(0);\r\n\r\n\tt = 0;\r\n\ty = 30000;\r\n\r\n\t// Using `for` instead of `while` since in original Arduino code if an early mask was *good\r\n\t// enough* it wouldn't try for a better one since they get more complex and take longer.\r\n\t// 使用`for`而不是`while`,因为在原始Arduino代码中,如果早期掩码足够好,它不会尝试更好的掩码,因为它们变得更复杂并需要更长的时间。\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\t// Returns foreground-background imbalance.\r\n\t\t// 返回前景色和背景色的不平衡。\r\n\t\tapplyMask(k);\r\n\r\n\t\tx = checkBadness();\r\n\r\n\t\t// Is current mask better than previous best?\r\n\t\t// 当前掩码是否比之前的最佳掩码更好?\r\n\t\tif (x < y) {\r\n\t\t\ty = x;\r\n\t\t\tt = k;\r\n\t\t}\r\n\r\n\t\t// Don't increment `i` to a void redoing mask.\r\n\t\t// 不要增加`i`以避免重新做掩码。\r\n\t\tif (t == 7) break;\r\n\r\n\t\t// Reset for next pass.\r\n\t\t// 重置下一个循环。\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t}\r\n\r\n\t// Redo best mask as none were *good enough* (i.e. last wasn't `t`).\r\n\t// 重做最佳掩码,因为没有一个掩码足够好(即最后一个不是`t`)。\r\n\tif (t != k) {\r\n\t\t// Reset buffer to pre-mask state before applying the best one\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t\tapplyMask(t);\r\n\t}\r\n\r\n\t// Add in final mask/ECC level bytes.\r\n\t// 添加最终的掩码/ECC级别字节。\r\n\ty = FINAL_FORMAT[t + ((eccLevel - 1) << 3)];\r\n\r\n\t// Low byte.\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[width - 1 - k + width * 8] = 1;\r\n\r\n\t\t\tif (k < 6) {\r\n\t\t\t\tframeBuffer[8 + width * k] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[8 + width * (k + 1)] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// High byte.\r\n\tfor (k = 0; k < 7; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[8 + width * (width - 7 + k)] = 1;\r\n\r\n\t\t\tif (k > 0) {\r\n\t\t\t\tframeBuffer[6 - k + width * 8] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[7 + width * 8] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// Finally, return the image data.\r\n\treturn {\r\n\t\tframeBuffer: frameBuffer,\r\n\t\twidth: width\r\n\t} as GenerateFrameResult;\r\n}\r\n",
|
|
1715
|
+
"target": "web"
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
"path": "reborn-qrcode.config.ts",
|
|
1719
|
+
"content": "export default {\r\n slots: {\r\n root: 'relative',\r\n },\r\n}\r\n\r\nexport const qrcodeMode = ['rect', 'circular', 'line', 'rectSmall']\r\n"
|
|
1720
|
+
},
|
|
1721
|
+
{
|
|
1722
|
+
"path": "RebornQrcode.vue",
|
|
1723
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClQrcodeMode } from './types'\r\nimport { computed } from 'vue'\r\nimport { generateFrame } from './qrcode'\r\nimport { eccLevel } from './types'\r\n\r\ninterface RebornQrcodeOptions {\r\n size?: number\r\n foreground?: string\r\n background?: string\r\n pdColor?: string | null\r\n pdRadius?: number\r\n text?: string\r\n logo?: string\r\n logoSize?: number\r\n padding?: number\r\n logoMargin?: number\r\n logoHideBackgroundDots?: boolean\r\n logoShadow?: boolean\r\n mode?: ClQrcodeMode\r\n ecc?: eccLevel\r\n pdOuterRadius?: number\r\n pdInnerRadius?: number\r\n dotsGradient?: any\r\n dotsImage?: string | null\r\n backgroundGradient?: any\r\n backgroundTransparent?: boolean\r\n logoOptions?: any\r\n cornersSquareGradient?: any\r\n cornersDotGradient?: any\r\n cornersSquareOptions?: any\r\n cornersDotOptions?: any\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornQrcodeOptions>(), {\r\n size: 200,\r\n foreground: '#131313',\r\n background: '#FFFFFF',\r\n pdColor: null,\r\n pdRadius: 10,\r\n text: 'https://cool-js.com/',\r\n logo: '',\r\n logoSize: 40,\r\n padding: 5,\r\n logoMargin: 4,\r\n logoHideBackgroundDots: false,\r\n logoShadow: false,\r\n mode: 'circular',\r\n ecc: eccLevel.H,\r\n pdOuterRadius: undefined,\r\n pdInnerRadius: undefined,\r\n})\r\n\r\nconst frameData = computed(() => generateFrame(props.text, props.ecc))\r\nconst width = computed(() => frameData.value.width)\r\nconst cellSize = computed(() => (props.size - props.padding * 2) / width.value)\r\n\r\ninterface EyeRect {\r\n x: number\r\n y: number\r\n size: number\r\n}\r\n\r\nconst eyeRects = computed((): EyeRect[] => {\r\n const w = width.value\r\n const s = 7\r\n return [\r\n { x: 0, y: 0, size: s },\r\n { x: w - s, y: 0, size: s },\r\n { x: 0, y: w - s, size: s },\r\n ]\r\n})\r\n\r\nfunction isInEye(x: number, y: number) {\r\n return eyeRects.value.some((eye: EyeRect) => x >= eye.x && x < eye.x + eye.size && y >= eye.y && y < eye.y + eye.size)\r\n}\r\n\r\nfunction isInLogo(x: number, y: number) {\r\n if (!props.logo || !props.logoHideBackgroundDots) return false\r\n\r\n const logoPx = {\r\n x1: (props.size - props.logoSize) / 2 - props.logoMargin,\r\n y1: (props.size - props.logoSize) / 2 - props.logoMargin,\r\n x2: (props.size + props.logoSize) / 2 + props.logoMargin,\r\n y2: (props.size + props.logoSize) / 2 + props.logoMargin,\r\n }\r\n\r\n const dotPx = {\r\n x1: props.padding + x * cellSize.value,\r\n y1: props.padding + y * cellSize.value,\r\n x2: props.padding + (x + 1) * cellSize.value,\r\n y2: props.padding + (y + 1) * cellSize.value,\r\n }\r\n\r\n return (\r\n dotPx.x1 < logoPx.x2 &&\r\n dotPx.x2 > logoPx.x1 &&\r\n dotPx.y1 < logoPx.y2 &&\r\n dotPx.y2 > logoPx.y1\r\n )\r\n}\r\n\r\nconst dots = computed(() => {\r\n const arr: Array<{ x: number, y: number }> = []\r\n const frame = frameData.value.frameBuffer\r\n const w = width.value\r\n for (let y = 0; y < w; y++) {\r\n for (let x = 0; x < w; x++) {\r\n const idx = y * w + x\r\n if (frame[idx] && !isInEye(x, y) && !isInLogo(x, y))\r\n arr.push({ x, y })\r\n }\r\n }\r\n return arr\r\n})\r\n\r\nconst eyeCenter = (eye: EyeRect) => {\r\n return { x: eye.x + 2, y: eye.y + 2 }\r\n}\r\n\r\nconst qrcodeId = computed(() => `qr-${Math.random().toString(36).slice(2, 9)}`)\r\n\r\nconst getGradientStops = (gradient: any) => {\r\n if (!gradient || !gradient.colorStops) return []\r\n return gradient.colorStops\r\n}\r\n\r\nconst getGradientCoords = (gradient: any) => {\r\n if (!gradient || !gradient.direction) return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n switch (gradient.direction) {\r\n case 'horizontal': return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n case 'vertical': return { x1: \"0%\", y1: \"0%\", x2: \"0%\", y2: \"100%\" }\r\n case 'diagonal': return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"100%\" }\r\n default: return { x1: \"0%\", y1: \"0%\", x2: \"100%\", y2: \"0%\" }\r\n }\r\n}\r\n\r\nconst dotsFill = computed(() => {\r\n if (props.dotsImage) return `url(#${qrcodeId.value}-dots-img)`\r\n if (props.dotsGradient) return `url(#${qrcodeId.value}-dots-grad)`\r\n return props.foreground\r\n})\r\n\r\nconst bgFill = computed(() => {\r\n if (props.backgroundTransparent) return 'transparent'\r\n if (props.backgroundGradient) return `url(#${qrcodeId.value}-bg-grad)`\r\n return props.background\r\n})\r\n\r\nconst cornerSquareFill = computed(() => {\r\n if (props.cornersSquareGradient) return `url(#${qrcodeId.value}-cs-grad)`\r\n if (props.cornersSquareOptions?.color) return props.cornersSquareOptions.color\r\n if (props.pdColor) return props.pdColor\r\n return props.foreground\r\n})\r\n\r\nconst cornerDotFill = computed(() => {\r\n if (props.cornersDotGradient) return `url(#${qrcodeId.value}-cd-grad)`\r\n if (props.cornersDotOptions?.color) return props.cornersDotOptions.color\r\n if (props.pdColor) return props.pdColor\r\n return props.foreground\r\n})\r\n\r\nconst getRoundRectPath = (x: number, y: number, w: number, h: number, r: number) => {\r\n if (r <= 0) return `M ${x} ${y} h ${w} v ${h} h ${-w} Z`\r\n const radius = Math.min(r, w / 2, h / 2)\r\n return `M ${x + radius} ${y} \r\n h ${w - 2 * radius} \r\n a ${radius} ${radius} 0 0 1 ${radius} ${radius} \r\n v ${h - 2 * radius} \r\n a ${radius} ${radius} 0 0 1 ${-radius} ${radius} \r\n h ${-(w - 2 * radius)} \r\n a ${radius} ${radius} 0 0 1 ${-radius} ${-radius} \r\n v ${-(h - 2 * radius)} \r\n a ${radius} ${radius} 0 0 1 ${radius} ${-radius} Z`\r\n}\r\n</script>\r\n\r\n<template>\r\n <div :style=\"{ width: `${size}px`, height: `${size}px` }\" class=\"relative\">\r\n <svg :width=\"size\" :height=\"size\" :viewBox=\"`0 0 ${size} ${size}`\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <defs>\r\n <!-- Dots Gradient -->\r\n <linearGradient v-if=\"dotsGradient\" :id=\"`${qrcodeId}-dots-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(dotsGradient)\">\r\n <stop v-for=\"stop in getGradientStops(dotsGradient)\" :key=\"stop.offset\" :offset=\"`${stop.offset * 100}%`\"\r\n :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Background Gradient -->\r\n <linearGradient v-if=\"backgroundGradient\" :id=\"`${qrcodeId}-bg-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(backgroundGradient)\">\r\n <stop v-for=\"stop in getGradientStops(backgroundGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Corner Square Gradient -->\r\n <linearGradient v-if=\"cornersSquareGradient\" :id=\"`${qrcodeId}-cs-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(cornersSquareGradient)\">\r\n <stop v-for=\"stop in getGradientStops(cornersSquareGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Corner Dot Gradient -->\r\n <linearGradient v-if=\"cornersDotGradient\" :id=\"`${qrcodeId}-cd-grad`\" gradientUnits=\"userSpaceOnUse\"\r\n v-bind=\"getGradientCoords(cornersDotGradient)\">\r\n <stop v-for=\"stop in getGradientStops(cornersDotGradient)\" :key=\"stop.offset\"\r\n :offset=\"`${stop.offset * 100}%`\" :stop-color=\"stop.color\" />\r\n </linearGradient>\r\n\r\n <!-- Dots Image Pattern -->\r\n <pattern v-if=\"dotsImage\" :id=\"`${qrcodeId}-dots-img`\" patternUnits=\"userSpaceOnUse\" width=\"100%\" height=\"100%\">\r\n <image :href=\"dotsImage\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" preserveAspectRatio=\"xMidYMid slice\" />\r\n </pattern>\r\n </defs>\r\n\r\n <rect :width=\"size\" :height=\"size\" :fill=\"bgFill\" />\r\n\r\n <template v-for=\"dot in dots\" :key=\"`${dot.x}-${dot.y}`\">\r\n <circle v-if=\"mode === 'circular'\" :cx=\"padding + (dot.x + 0.5) * cellSize\"\r\n :cy=\"padding + (dot.y + 0.5) * cellSize\" :r=\"cellSize * 0.42\" :fill=\"dotsFill\" />\r\n <rect v-else-if=\"mode === 'rectSmall'\" :x=\"padding + dot.x * cellSize + cellSize * 0.2\"\r\n :y=\"padding + dot.y * cellSize + cellSize * 0.2\" :width=\"cellSize * 0.6\" :height=\"cellSize * 0.6\" rx=\"1\"\r\n :fill=\"dotsFill\" />\r\n <rect v-else-if=\"mode === 'line'\" :x=\"padding + dot.x * cellSize\"\r\n :y=\"padding + dot.y * cellSize + cellSize * 0.15\" :width=\"cellSize\" :height=\"cellSize * 0.7\"\r\n :fill=\"dotsFill\" />\r\n <rect v-else :x=\"padding + dot.x * cellSize\" :y=\"padding + dot.y * cellSize\" :width=\"cellSize\"\r\n :height=\"cellSize\" :fill=\"dotsFill\" />\r\n </template>\r\n\r\n <template v-for=\"eye in eyeRects\" :key=\"`${eye.x}-${eye.y}`\">\r\n <!-- Position Ring (Outer 7x7 - Inner 5x5) -->\r\n <path :d=\"`\r\n ${getRoundRectPath(padding + eye.x * cellSize, padding + eye.y * cellSize, eye.size * cellSize, eye.size * cellSize, pdOuterRadius ?? pdRadius)}\r\n ${getRoundRectPath(padding + (eye.x + 1) * cellSize, padding + (eye.y + 1) * cellSize, 5 * cellSize, 5 * cellSize, Math.max(0, (pdOuterRadius ?? pdRadius) - cellSize))}\r\n `\" fill-rule=\"evenodd\" :fill=\"cornerSquareFill\" />\r\n <!-- Inner Center Dot (3x3) -->\r\n <path\r\n :d=\"getRoundRectPath(padding + eyeCenter(eye).x * cellSize, padding + eyeCenter(eye).y * cellSize, 3 * cellSize, 3 * cellSize, pdInnerRadius ?? pdRadius / 2)\"\r\n :fill=\"cornerDotFill\" />\r\n </template>\r\n </svg>\r\n\r\n <img v-if=\"logo\" :src=\"logo\" alt=\"logo\" class=\"absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded\"\r\n :style=\"{\r\n width: `${logoSize}px`,\r\n height: `${logoSize}px`,\r\n filter: logoShadow ? 'drop-shadow(0 2px 4px rgba(0,0,0,0.15))' : 'none'\r\n }\">\r\n </div>\r\n</template>\r\n",
|
|
1724
|
+
"target": "web"
|
|
1725
|
+
},
|
|
1726
|
+
{
|
|
1727
|
+
"path": "types.ts",
|
|
1728
|
+
"content": "export type ClQrcodeMode = 'rect' | 'circular' | 'line' | 'rectSmall'\r\n\r\nexport enum eccLevel {\r\n L = 'L',\r\n M = 'M',\r\n Q = 'Q',\r\n H = 'H',\r\n}\r\n"
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
"path": "draw.ts",
|
|
1732
|
+
"content": "/**\r\n * 导入所需的工具函数和依赖\r\n */\r\nimport { generateFrame } from \"./qrcode\";\r\n\r\nexport type ClQrcodeMode = \"rect\" | \"circular\" | \"line\" | \"rectSmall\";\r\n\r\nexport enum eccLevel {\r\n\tL = \"L\",\r\n\tM = \"M\",\r\n\tQ = \"Q\",\r\n\tH = \"H\"\r\n}\r\n\r\nexport type QrcodeOptions = {\r\n\tecc: eccLevel; // 纠错级别\r\n\ttext: string; // 二维码内容\r\n\tsize: number; // 二维码尺寸,单位px\r\n\tforeground: string; // 前景色\r\n\tbackground: string; // 背景色\r\n\tpadding: number; // 内边距\r\n\tlogo: string; // logo图片地址\r\n\tlogoSize: number; // logo尺寸\r\n\tmode: ClQrcodeMode; // 二维码样式模式\r\n\tpdColor: string | null; // 定位点颜色\r\n\tpdRadius: number; // 定位图案圆角半径(兼容旧版)\r\n\tpdOuterRadius?: number; // 码眼外框圆角半径\r\n\tpdInnerRadius?: number; // 码眼内点圆角半径\r\n\tdotsGradient?: any;\r\n\tdotsImage?: string | null;\r\n\tbackgroundGradient?: any;\r\n\tbackgroundTransparent?: boolean;\r\n\tlogoOptions?: any;\r\n\tcornersSquareGradient?: any;\r\n\tcornersDotGradient?: any;\r\n\tcornersSquareOptions?: any;\r\n\tcornersDotOptions?: any;\r\n};\r\n\r\n/**\r\n * 绘制圆角矩形\r\n */\r\nfunction drawRoundedRect(\r\n\tctx: any,\r\n\tx: number,\r\n\ty: number,\r\n\twidth: number,\r\n\theight: number,\r\n\tradius: number\r\n) {\r\n\tif (radius <= 0) {\r\n\t\tctx.fillRect(x, y, width, height);\r\n\t\treturn;\r\n\t}\r\n\r\n\tconst maxRadius = Math.min(width, height) / 2;\r\n\tconst r = Math.min(radius, maxRadius);\r\n\r\n\tctx.beginPath();\r\n\tctx.moveTo(x + r, y);\r\n\tctx.lineTo(x + width - r, y);\r\n\tctx.arcTo(x + width, y, x + width, y + r, r);\r\n\tctx.lineTo(x + width, y + height - r);\r\n\tctx.arcTo(x + width, y + height, x + width - r, y + height, r);\r\n\tctx.lineTo(x + r, y + height);\r\n\tctx.arcTo(x, y + height, x, y + height - r, r);\r\n\tctx.lineTo(x, y + r);\r\n\tctx.arcTo(x, y, x + r, y, r);\r\n\tctx.closePath();\r\n\tctx.fill();\r\n}\r\n\r\nfunction createFillStyle(ctx: any, styleConfig: any, size: number) {\r\n\tif (!styleConfig) return null;\r\n\ttry {\r\n\t\tif (styleConfig.type === 'linear') {\r\n\t\t\tlet x1 = 0, y1 = 0, x2 = 0, y2 = 0;\r\n\t\t\tswitch (styleConfig.direction) {\r\n\t\t\t\tcase 'horizontal': x2 = size; break;\r\n\t\t\t\tcase 'vertical': y2 = size; break;\r\n\t\t\t\tcase 'diagonal': x2 = size; y2 = size; break;\r\n\t\t\t\tcase 'center': x1 = size / 2; y1 = size / 2; x2 = size; y2 = size; break;\r\n\t\t\t\tdefault: x2 = size; break;\r\n\t\t\t}\r\n\t\t\tconst grd = ctx.createLinearGradient(x1, y1, x2, y2);\r\n\t\t\tif (styleConfig.colorStops) {\r\n\t\t\t\tstyleConfig.colorStops.forEach((stop: any) => {\r\n\t\t\t\t\tgrd.addColorStop(stop.offset, stop.color);\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t\treturn grd;\r\n\t\t} else if (styleConfig.type === 'radial') {\r\n\t\t\tconst grd = ctx.createCircularGradient ? ctx.createCircularGradient(size / 2, size / 2, size / 2) : ctx.createLinearGradient(0, 0, size, size);\r\n\t\t\tif (styleConfig.colorStops) {\r\n\t\t\t\tstyleConfig.colorStops.forEach((stop: any) => {\r\n\t\t\t\t\tgrd.addColorStop(stop.offset, stop.color);\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t\treturn grd;\r\n\t\t}\r\n\t} catch (e) {\r\n\t\tconsole.warn('创建渐变失败', e);\r\n\t}\r\n\treturn null;\r\n}\r\n\r\nfunction setFillStyle(ctx: any, fillStyle: any) {\r\n\tif (typeof ctx.setFillStyle === 'function') {\r\n\t\tctx.setFillStyle(fillStyle);\r\n\t\treturn;\r\n\t}\r\n\tctx.fillStyle = fillStyle;\r\n}\r\n\r\nfunction flushDraw(ctx: any) {\r\n\tif (typeof ctx.draw === 'function') {\r\n\t\tctx.draw(false);\r\n\t}\r\n}\r\n\r\nasync function loadImageSource(src: string, canvasNode?: any) {\r\n\tif (canvasNode?.createImage) {\r\n\t\tconst image = canvasNode.createImage();\r\n\t\tawait new Promise<void>((resolve, reject) => {\r\n\t\t\timage.onload = () => resolve();\r\n\t\t\timage.onerror = (err: any) => reject(err);\r\n\t\t\timage.src = src;\r\n\t\t});\r\n\t\treturn image;\r\n\t}\r\n\r\n\tconst imageInfo = await new Promise<any>((resolve, reject) => {\r\n\t\tuni.getImageInfo({\r\n\t\t\tsrc,\r\n\t\t\tsuccess: (res) => {\r\n\t\t\t\tif (\r\n\t\t\t\t\t!res.path.startsWith('http://') &&\r\n\t\t\t\t\t!res.path.startsWith('https://') &&\r\n\t\t\t\t\t!res.path.startsWith('/') &&\r\n\t\t\t\t\t!res.path.startsWith('data:') &&\r\n\t\t\t\t\t!res.path.startsWith('wxfile://')\r\n\t\t\t\t) {\r\n\t\t\t\t\tres.path = '/' + res.path;\r\n\t\t\t\t}\r\n\t\t\t\tresolve(res);\r\n\t\t\t},\r\n\t\t\tfail: reject\r\n\t\t});\r\n\t});\r\n\r\n\treturn imageInfo.path;\r\n}\r\n\r\n/**\r\n * 绘制定位图案(码眼)\r\n */\r\nfunction drawPositionPattern(\r\n\tctx: any,\r\n\tstartX: number,\r\n\tstartY: number,\r\n\tpx: number,\r\n\tpdSquareFillStyle: any,\r\n\tpdDotFillStyle: any,\r\n\tbackground: string,\r\n\touterRadius: number,\r\n\tcenterRadius: number\r\n) {\r\n\tconst patternSize = px * 7;\r\n\r\n\tconst safeOuterRadius = Math.max(0, outerRadius);\r\n\tconst safeCenterRadius = Math.max(0, centerRadius);\r\n\r\n\tsetFillStyle(ctx, pdSquareFillStyle);\r\n\tdrawRoundedRect(ctx, startX, startY, patternSize, patternSize, safeOuterRadius);\r\n\r\n\tif (background !== 'transparent') {\r\n\t\tsetFillStyle(ctx, background);\r\n\t\tconst innerStartX = startX + px;\r\n\t\tconst innerStartY = startY + px;\r\n\t\tconst innerSize = px * 5;\r\n\t\tconst innerRadius = Math.max(0, safeOuterRadius - px);\r\n\t\tdrawRoundedRect(ctx, innerStartX, innerStartY, innerSize, innerSize, innerRadius);\r\n\t} else {\r\n\t\t// 在透明背景下,如果只用上面的方法,内圈会实心。所以我们改用 clip+填充 的方式,或者绘制两层来避免遮挡:\r\n\t\t// 实际上透明背景应该清空内圈,由于 Canvas 是叠加的,这里比较难直接在图案上抠洞,\r\n\t\t// 通常做法是用 clip 或 composite 模式。为了简单处理,透明背景不填充中间层\r\n\t\t// 由于第一步已经填充了实心外框,如果背景透明,中间会是一整块。\r\n\t\t// 这里我们用 clearRect 加上 clear 的圆角实现不太容易。\r\n\t\t// 采用最保险的方式:用 path 绘制一个带有内孔的圆角矩形环\r\n\t}\r\n\r\n\t// 我们修改 drawPositionPattern 的实现,让它支持带洞的圆环和中心的点\r\n\tctx.beginPath();\r\n\r\n\t// 绘制外环(顺时针)\r\n\tconst r = Math.min(safeOuterRadius, patternSize / 2);\r\n\tif (r <= 0) {\r\n\t\tctx.moveTo(startX, startY);\r\n\t\tctx.lineTo(startX + patternSize, startY);\r\n\t\tctx.lineTo(startX + patternSize, startY + patternSize);\r\n\t\tctx.lineTo(startX, startY + patternSize);\r\n\t\tctx.lineTo(startX, startY);\r\n\t} else {\r\n\t\tctx.moveTo(startX + r, startY);\r\n\t\tctx.lineTo(startX + patternSize - r, startY);\r\n\t\tctx.arcTo(startX + patternSize, startY, startX + patternSize, startY + r, r);\r\n\t\tctx.lineTo(startX + patternSize, startY + patternSize - r);\r\n\t\tctx.arcTo(startX + patternSize, startY + patternSize, startX + patternSize - r, startY + patternSize, r);\r\n\t\tctx.lineTo(startX + r, startY + patternSize);\r\n\t\tctx.arcTo(startX, startY + patternSize, startX, startY + patternSize - r, r);\r\n\t\tctx.lineTo(startX, startY + r);\r\n\t\tctx.arcTo(startX, startY, startX + r, startY, r);\r\n\t}\r\n\r\n\t// 绘制内环(逆时针)以便留空\r\n\tconst innerStartX = startX + px;\r\n\tconst innerStartY = startY + px;\r\n\tconst innerSize = px * 5;\r\n\tconst innerR = Math.max(0, r - px);\r\n\r\n\tif (innerR <= 0) {\r\n\t\tctx.moveTo(innerStartX, innerStartY);\r\n\t\tctx.lineTo(innerStartX, innerStartY + innerSize);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY + innerSize);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY);\r\n\t\tctx.lineTo(innerStartX, innerStartY);\r\n\t} else {\r\n\t\tctx.moveTo(innerStartX + innerR, innerStartY);\r\n\t\tctx.arcTo(innerStartX, innerStartY, innerStartX, innerStartY + innerR, innerR);\r\n\t\tctx.lineTo(innerStartX, innerStartY + innerSize - innerR);\r\n\t\tctx.arcTo(innerStartX, innerStartY + innerSize, innerStartX + innerR, innerStartY + innerSize, innerR);\r\n\t\tctx.lineTo(innerStartX + innerSize - innerR, innerStartY + innerSize);\r\n\t\tctx.arcTo(innerStartX + innerSize, innerStartY + innerSize, innerStartX + innerSize, innerStartY + innerSize - innerR, innerR);\r\n\t\tctx.lineTo(innerStartX + innerSize, innerStartY + innerR);\r\n\t\tctx.arcTo(innerStartX + innerSize, innerStartY, innerStartX + innerSize - innerR, innerStartY, innerR);\r\n\t}\r\n\tctx.closePath();\r\n\r\n\tsetFillStyle(ctx, pdSquareFillStyle);\r\n\tctx.fill('evenodd'); // 用 evenodd 填充带洞的多边形\r\n\r\n\t// 绘制中心点\r\n\tsetFillStyle(ctx, pdDotFillStyle);\r\n\tconst centerStartX = startX + px * 2;\r\n\tconst centerStartY = startY + px * 2;\r\n\tconst centerSize = px * 3;\r\n\tdrawRoundedRect(ctx, centerStartX, centerStartY, centerSize, centerSize, safeCenterRadius);\r\n}\r\n\r\n/**\r\n * 在二维码中心绘制Logo\r\n */\r\nfunction drawLogo(ctx: any, options: QrcodeOptions, imageSource: any) {\r\n\tctx.save();\r\n\r\n\tconst contentSize = options.size - options.padding * 2;\r\n\tconst contentCenterX = options.padding + contentSize / 2;\r\n\tconst contentCenterY = options.padding + contentSize / 2;\r\n\r\n\tlet logoSize = options.logoSize;\r\n\tif (options.logoOptions && options.logoOptions.size) {\r\n\t\tswitch (options.logoOptions.size) {\r\n\t\t\tcase 'small': logoSize = 30; break;\r\n\t\t\tcase 'large': logoSize = 60; break;\r\n\t\t\tdefault: logoSize = 45; break; // medium\r\n\t\t}\r\n\t}\r\n\r\n\tlet backgroundPadding = 3;\r\n\tif (options.logoOptions && options.logoOptions.margin) {\r\n\t\tswitch (options.logoOptions.margin) {\r\n\t\t\tcase 'none': backgroundPadding = 0; break;\r\n\t\t\tcase 'small': backgroundPadding = 3; break;\r\n\t\t\tcase 'medium': backgroundPadding = 6; break;\r\n\t\t\tcase 'large': backgroundPadding = 10; break;\r\n\t\t}\r\n\t}\r\n\r\n\tconst backgroundSize = logoSize + backgroundPadding * 2;\r\n\tconst backgroundX = contentCenterX - backgroundSize / 2;\r\n\tconst backgroundY = contentCenterY - backgroundSize / 2;\r\n\r\n\tlet cornerRadius = Math.min(backgroundSize * 0.1, 6);\r\n\tif (options.logoOptions && options.logoOptions.shape) {\r\n\t\tswitch (options.logoOptions.shape) {\r\n\t\t\tcase 'rectangle': cornerRadius = 0; break;\r\n\t\t\tcase 'circle': cornerRadius = backgroundSize / 2; break;\r\n\t\t\tcase 'rounded-rectangle': cornerRadius = Math.min(backgroundSize * 0.2, 12); break;\r\n\t\t}\r\n\t}\r\n\r\n\tif (options.logoOptions && options.logoOptions.shadow) {\r\n\t\tctx.shadowColor = 'rgba(0, 0, 0, 0.2)';\r\n\t\tctx.shadowBlur = 10;\r\n\t\tctx.shadowOffsetX = 0;\r\n\t\tctx.shadowOffsetY = 4;\r\n\t}\r\n\r\n\t// 绘制Logo背景\r\n\tif (!options.logoOptions || options.logoOptions.hideBackgroundDots !== false) {\r\n\t\tsetFillStyle(ctx, options.backgroundTransparent ? '#ffffff' : options.background);\r\n\t\tdrawRoundedRect(ctx, backgroundX, backgroundY, backgroundSize, backgroundSize, cornerRadius);\r\n\t}\r\n\r\n\t// 重置阴影,避免影响图片\r\n\tctx.shadowColor = 'transparent';\r\n\tctx.shadowBlur = 0;\r\n\tctx.shadowOffsetX = 0;\r\n\tctx.shadowOffsetY = 0;\r\n\r\n\t// 绘制图片(如果需要裁剪的话可以加clip)\r\n\tconst logoX = contentCenterX - logoSize / 2;\r\n\tconst logoY = contentCenterY - logoSize / 2;\r\n\r\n\tif (cornerRadius > 0) {\r\n\t\tctx.save();\r\n\t\tctx.beginPath();\r\n\t\tconst innerRadius = Math.max(0, cornerRadius - backgroundPadding);\r\n\t\tctx.moveTo(logoX + innerRadius, logoY);\r\n\t\tctx.lineTo(logoX + logoSize - innerRadius, logoY);\r\n\t\tctx.arcTo(logoX + logoSize, logoY, logoX + logoSize, logoY + innerRadius, innerRadius);\r\n\t\tctx.lineTo(logoX + logoSize, logoY + logoSize - innerRadius);\r\n\t\tctx.arcTo(logoX + logoSize, logoY + logoSize, logoX + logoSize - innerRadius, logoY + logoSize, innerRadius);\r\n\t\tctx.lineTo(logoX + innerRadius, logoY + logoSize);\r\n\t\tctx.arcTo(logoX, logoY + logoSize, logoX, logoY + logoSize - innerRadius, innerRadius);\r\n\t\tctx.lineTo(logoX, logoY + innerRadius);\r\n\t\tctx.arcTo(logoX, logoY, logoX + innerRadius, logoY, innerRadius);\r\n\t\tctx.closePath();\r\n\t\tctx.clip();\r\n\t}\r\n\r\n\tctx.drawImage(imageSource, logoX, logoY, logoSize, logoSize);\r\n\r\n\tif (cornerRadius > 0) {\r\n\t\tctx.restore();\r\n\t}\r\n\r\n\tctx.restore();\r\n}\r\n\r\n/**\r\n * 绘制二维码到Canvas上下文\r\n */\r\nexport async function drawQrcode(ctx: any, options: QrcodeOptions, canvasNode?: any) {\r\n\tif (!ctx) return;\r\n\r\n\t// 生成二维码数据矩阵\r\n\tconst frame = generateFrame(options.text, options.ecc);\r\n\tconst points = frame.frameBuffer; // 点阵数据\r\n\tconst width = frame.width; // 矩阵宽度\r\n\r\n\t// 计算二维码内容区域大小\r\n\tconst contentSize = options.size - options.padding * 2;\r\n\tconst px = contentSize / width;\r\n\tconst offsetX = options.padding;\r\n\tconst offsetY = options.padding;\r\n\r\n\t// 绘制整个画布背景\r\n\tif (options.backgroundTransparent) {\r\n\t\tctx.clearRect(0, 0, options.size, options.size);\r\n\t} else {\r\n\t\tconst bgGradient = createFillStyle(ctx, options.backgroundGradient, options.size);\r\n\t\tsetFillStyle(ctx, bgGradient || options.background);\r\n\t\tctx.fillRect(0, 0, options.size, options.size);\r\n\t}\r\n\r\n\tfunction isPositionDetectionPattern(i: number, j: number, width: number): boolean {\r\n\t\tif (i < 7 && j < 7) return true; // 左上角\r\n\t\tif (i > width - 8 && j < 7) return true; // 右上角\r\n\t\tif (i < 7 && j > width - 8) return true; // 左下角\r\n\t\treturn false;\r\n\t}\r\n\r\n\tfunction isInLogoArea(\r\n\t\ti: number,\r\n\t\tj: number,\r\n\t\twidth: number,\r\n\t\tbaseLogoSize: number,\r\n\t\tpx: number\r\n\t): boolean {\r\n\t\tlet logoSize = baseLogoSize;\r\n\t\tif (options.logoOptions && options.logoOptions.size) {\r\n\t\t\tswitch (options.logoOptions.size) {\r\n\t\t\t\tcase 'small': logoSize = 30; break;\r\n\t\t\t\tcase 'large': logoSize = 60; break;\r\n\t\t\t\tdefault: logoSize = 45; break; // medium\r\n\t\t\t}\r\n\t\t}\r\n\t\tlet backgroundPadding = 3;\r\n\t\tif (options.logoOptions && options.logoOptions.margin) {\r\n\t\t\tswitch (options.logoOptions.margin) {\r\n\t\t\t\tcase 'none': backgroundPadding = 0; break;\r\n\t\t\t\tcase 'small': backgroundPadding = 3; break;\r\n\t\t\t\tcase 'medium': backgroundPadding = 6; break;\r\n\t\t\t\tcase 'large': backgroundPadding = 10; break;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (logoSize <= 0) return false;\r\n\r\n\t\tconst maxLogoRatio = 0.3;\r\n\t\tconst maxLogoPoints = Math.floor(width * maxLogoRatio);\r\n\t\tconst logoPoints = Math.min(Math.ceil((logoSize + backgroundPadding * 2) / px), maxLogoPoints);\r\n\r\n\t\tconst buffer = logoPoints > width * 0.1 ? 1 : 0;\r\n\t\tconst totalLogoPoints = logoPoints + buffer * 2;\r\n\r\n\t\tconst centerI = Math.floor(width / 2);\r\n\t\tconst centerJ = Math.floor(width / 2);\r\n\r\n\t\tconst halfSize = Math.floor(totalLogoPoints / 2);\r\n\t\tconst minI = centerI - halfSize;\r\n\t\tconst maxI = centerI + halfSize;\r\n\t\tconst minJ = centerJ - halfSize;\r\n\t\tconst maxJ = centerJ + halfSize;\r\n\r\n\t\treturn i >= minI && i <= maxI && j >= minJ && j <= maxJ;\r\n\t}\r\n\r\n\tconst pdColor = options.pdColor ?? options.foreground;\r\n\r\n\tconst baseRadius = options.pdRadius;\r\n\tconst outerRadius = options.pdOuterRadius !== undefined ? options.pdOuterRadius : baseRadius;\r\n\tconst centerRadius = options.pdInnerRadius !== undefined ? options.pdInnerRadius : Math.max(0, outerRadius - px * 2);\r\n\r\n\tconst fgGradient = createFillStyle(ctx, options.dotsGradient, options.size);\r\n\tconst fgStyle = fgGradient || options.foreground;\r\n\r\n\tconst pdSquareGradient = options.cornersSquareGradient || options.dotsGradient;\r\n\tconst pdDotGradient = options.cornersDotGradient || options.dotsGradient;\r\n\r\n\tconst pdSquareFillStyle = createFillStyle(ctx, pdSquareGradient, options.size) || (options.cornersSquareOptions && options.cornersSquareOptions.color) || ((options.pdColor && options.pdColor !== options.foreground) ? options.pdColor : fgStyle);\r\n\tconst pdDotFillStyle = createFillStyle(ctx, pdDotGradient, options.size) || (options.cornersDotOptions && options.cornersDotOptions.color) || ((options.pdColor && options.pdColor !== options.foreground) ? options.pdColor : fgStyle);\r\n\r\n\tdrawPositionPattern(ctx, offsetX, offsetY, px, pdSquareFillStyle, pdDotFillStyle, options.backgroundTransparent ? 'transparent' : options.background, outerRadius, centerRadius);\r\n\tdrawPositionPattern(\r\n\t\tctx,\r\n\t\toffsetX + (width - 7) * px,\r\n\t\toffsetY,\r\n\t\tpx,\r\n\t\tpdSquareFillStyle,\r\n\t\tpdDotFillStyle,\r\n\t\toptions.backgroundTransparent ? 'transparent' : options.background,\r\n\t\touterRadius,\r\n\t\tcenterRadius\r\n\t);\r\n\tdrawPositionPattern(\r\n\t\tctx,\r\n\t\toffsetX,\r\n\t\toffsetY + (width - 7) * px,\r\n\t\tpx,\r\n\t\tpdSquareFillStyle,\r\n\t\tpdDotFillStyle,\r\n\t\toptions.backgroundTransparent ? 'transparent' : options.background,\r\n\t\touterRadius,\r\n\t\tcenterRadius\r\n\t);\r\n\r\n\tconst dot = px * 0.1;\r\n\r\n\t// 加载前景图\r\n\tlet fgImageSource: any = null;\r\n\tif (options.dotsImage) {\r\n\t\ttry {\r\n\t\t\tlet src = options.dotsImage;\r\n\t\t\t// #ifdef MP-WEIXIN\r\n\t\t\t// 对于 http/https 或以 /、data: 开头的路径,直接使用原路径\r\n\t\t\tif (\r\n\t\t\t\t!src.startsWith('http://') &&\r\n\t\t\t\t!src.startsWith('https://') &&\r\n\t\t\t\t!src.startsWith('/') &&\r\n\t\t\t\t!src.startsWith('data:')\r\n\t\t\t) {\r\n\t\t\t\t// 微信小程序中 uni.getImageInfo 需要的是绝对路径,如果是 static/xx 则补上 /\r\n\t\t\t\tsrc = '/' + src;\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\t\t\tfgImageSource = await loadImageSource(src, canvasNode);\r\n\t\t} catch (e) {\r\n\t\t\tconsole.error(\"加载前景图失败\", e);\r\n\t\t}\r\n\t}\r\n\r\n\tif (fgImageSource) {\r\n\t\tctx.save();\r\n\t\tctx.beginPath();\r\n\t}\r\n\r\n\tfor (let i = 0; i < width; i++) {\r\n\t\tfor (let j = 0; j < width; j++) {\r\n\t\t\tif (points[j * width + i] > 0) {\r\n\t\t\t\tif (isPositionDetectionPattern(i, j, width)) {\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (options.logo != \"\" && isInLogoArea(i, j, width, options.logoSize, px)) {\r\n\t\t\t\t\tif (!options.logoOptions || options.logoOptions.hideBackgroundDots !== false) {\r\n\t\t\t\t\t\tcontinue;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (!fgImageSource) {\r\n\t\t\t\t\tsetFillStyle(ctx, fgStyle);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tconst x = offsetX + px * i;\r\n\t\t\t\tconst y = offsetY + px * j;\r\n\r\n\t\t\t\tswitch (options.mode) {\r\n\t\t\t\t\tcase \"line\":\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x, y, px, px / 2);\r\n\t\t\t\t\t\telse ctx.fillRect(x, y, px, px / 2);\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase \"circular\":\r\n\t\t\t\t\t\tif (!fgImageSource) ctx.beginPath();\r\n\t\t\t\t\t\tconst rx = x + px / 2 - dot;\r\n\t\t\t\t\t\tconst ry = y + px / 2 - dot;\r\n\t\t\t\t\t\tif (fgImageSource) ctx.moveTo(rx + px / 2 - dot, ry);\r\n\t\t\t\t\t\tctx.arc(rx, ry, px / 2 - dot, 0, 2 * Math.PI);\r\n\t\t\t\t\t\tif (!fgImageSource) {\r\n\t\t\t\t\t\t\tctx.fill();\r\n\t\t\t\t\t\t\tctx.closePath();\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tcase \"rectSmall\":\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x + dot, y + dot, px - dot * 2, px - dot * 2);\r\n\t\t\t\t\t\telse ctx.fillRect(x + dot, y + dot, px - dot * 2, px - dot * 2);\r\n\t\t\t\t\t\tbreak;\r\n\r\n\t\t\t\t\tdefault:\r\n\t\t\t\t\t\tif (fgImageSource) ctx.rect(x, y, px, px);\r\n\t\t\t\t\t\telse ctx.fillRect(x, y, px, px);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tif (fgImageSource) {\r\n\t\tctx.clip();\r\n\t\tctx.drawImage(fgImageSource, 0, 0, options.size, options.size);\r\n\t\tctx.restore();\r\n\t}\r\n\r\n\tif (options.logo != \"\") {\r\n\t\ttry {\r\n\t\t\tlet logoSrc = options.logo;\r\n\r\n\t\t\t// #ifdef H5\r\n\t\t\tif (logoSrc.startsWith(\"http\")) {\r\n\t\t\t\ttry {\r\n\t\t\t\t\tlogoSrc = await new Promise<string>((resolve, reject) => {\r\n\t\t\t\t\t\tconst img = new Image();\r\n\t\t\t\t\t\timg.crossOrigin = \"Anonymous\";\r\n\t\t\t\t\t\timg.onload = () => {\r\n\t\t\t\t\t\t\tconst canvas = document.createElement(\"canvas\");\r\n\t\t\t\t\t\t\tcanvas.width = img.width;\r\n\t\t\t\t\t\t\tcanvas.height = img.height;\r\n\t\t\t\t\t\t\tconst context = canvas.getContext(\"2d\");\r\n\t\t\t\t\t\t\tcontext?.drawImage(img, 0, 0);\r\n\t\t\t\t\t\t\ttry {\r\n\t\t\t\t\t\t\t\tconst dataUrl = canvas.toDataURL(\"image/png\");\r\n\t\t\t\t\t\t\t\tresolve(dataUrl);\r\n\t\t\t\t\t\t\t} catch (err) {\r\n\t\t\t\t\t\t\t\treject(err);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t};\r\n\t\t\t\t\t\timg.onerror = reject;\r\n\t\t\t\t\t\timg.src = logoSrc;\r\n\t\t\t\t\t});\r\n\t\t\t\t} catch (e) {\r\n\t\t\t\t\tconsole.warn(\"Logo CORS load failed, using original URL. Canvas export may fail.\", e);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\r\n\t\t\t// #ifdef MP-WEIXIN\r\n\t\t\t// 对于 http/https 或以 /、data: 开头的路径,直接使用原路径\r\n\t\t\tif (\r\n\t\t\t\t!logoSrc.startsWith('http://') &&\r\n\t\t\t\t!logoSrc.startsWith('https://') &&\r\n\t\t\t\t!logoSrc.startsWith('/') &&\r\n\t\t\t\t!logoSrc.startsWith('data:')\r\n\t\t\t) {\r\n\t\t\t\t// 微信小程序中 uni.getImageInfo 需要的是绝对路径,如果是 static/xx 则补上 /\r\n\t\t\t\tlogoSrc = '/' + logoSrc;\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\r\n\t\t\tconst logoImageSource = await loadImageSource(logoSrc, canvasNode);\r\n\t\t\tdrawLogo(ctx, options, logoImageSource);\r\n\t\t} catch (err) {\r\n\t\t\tconsole.error(\"二维码 Logo 加载失败\", err);\r\n\t\t}\r\n\t}\r\n\r\n\t// 执行绘制\r\n\tflushDraw(ctx);\r\n}\r\n",
|
|
1733
|
+
"target": "uniapp"
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
"path": "qrcode.ts",
|
|
1737
|
+
"content": "export type GenerateFrameResult = {\r\n\tframeBuffer: Uint8Array;\r\n\twidth: number;\r\n};\r\n\r\n/**\r\n * 二维码生成器\r\n * @description 纯 UTS 实现的二维码生成算法,支持多平台,兼容 uni-app x。核心算法参考 QR Code 标准,支持自定义纠错级别、自动适配内容长度。\r\n * @version 1.0.0\r\n * @平台兼容性 App、H5、微信小程序、UTS\r\n * @注意事项\r\n * - 仅支持 8bit 字符串内容,不支持数字/字母/汉字等模式优化\r\n * - 生成结果为二维码点阵数据和宽度,需配合 canvas 绘制\r\n * - 纠错级别支持 'L'/'M'/'Q'/'H',默认 'L'\r\n */\r\n\r\n// 对齐块间距表 - 不同版本二维码的对齐块分布位置\r\nconst ALIGNMENT_DELTA = [\r\n\t0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24,\r\n\t26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28\r\n] as number[];\r\n\r\n// 纠错块参数表 - 每个版本包含4个参数:块数、数据宽度、纠错宽度\r\nconst ECC_BLOCKS = [\r\n\t1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22,\r\n\t1, 0, 16, 28, 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32,\r\n\t18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, 2, 0,\r\n\t68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,\r\n\t1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22,\r\n\t4, 4, 16, 20, 4, 4, 12, 24, 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81,\r\n\t20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4,\r\n\t14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,\r\n\t11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98,\r\n\t24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2,\r\n\t17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, 3, 4, 113, 28, 3, 11, 44,\r\n\t26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28,\r\n\t4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16,\r\n\t24, 30, 34, 0, 13, 24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117,\r\n\t30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30,\r\n\t22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8, 4, 122, 30, 22,\r\n\t3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31,\r\n\t15, 30, 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47,\r\n\t28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15,\r\n\t30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1, 115, 30, 14, 21, 46,\r\n\t28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16,\r\n\t30, 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47,\r\n\t28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15,\r\n\t30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20, 4, 117, 30, 40, 7, 47,\r\n\t28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15,\r\n\t30\r\n] as number[];\r\n\r\n// 纠错级别映射表 - 将人类可读的纠错级别映射为内部数值\r\nconst ECC_LEVELS = new Map<string, number>([\r\n\t[\"L\", 1],\r\n\t[\"M\", 2],\r\n\t[\"Q\", 3],\r\n\t[\"H\", 4]\r\n]);\r\n\r\n// 最终格式信息掩码表 - 用于格式信息区域的掩码计算(level << 3 | mask)\r\nconst FINAL_FORMAT = [\r\n\t0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976 /* L */, 0x5412, 0x5125, 0x5e7c,\r\n\t0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0 /* M */, 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183,\r\n\t0x2eda, 0x2bed /* Q */, 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b /* H */\r\n];\r\n\r\n// Galois域指数表 - 用于纠错码计算的查找表\r\nconst GALOIS_EXPONENT = [\r\n\t0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26,\r\n\t0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0,\r\n\t0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23,\r\n\t0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1,\r\n\t0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0,\r\n\t0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2,\r\n\t0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce,\r\n\t0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc,\r\n\t0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54,\r\n\t0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73,\r\n\t0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff,\r\n\t0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41,\r\n\t0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6,\r\n\t0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09,\r\n\t0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16,\r\n\t0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00\r\n];\r\n\r\n// Galois域对数表 - 用于纠错码计算的反向查找表\r\nconst GALOIS_LOG = [\r\n\t0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b,\r\n\t0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71,\r\n\t0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45,\r\n\t0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6,\r\n\t0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88,\r\n\t0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40,\r\n\t0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d,\r\n\t0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57,\r\n\t0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18,\r\n\t0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e,\r\n\t0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61,\r\n\t0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2,\r\n\t0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6,\r\n\t0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a,\r\n\t0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7,\r\n\t0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf\r\n];\r\n\r\n// 二维码质量评估系数 - 用于计算最佳掩码模式\r\n// N1: 连续5个及以上同色模块的惩罚分数\r\nconst N1 = 3;\r\n// N2: 2x2同色模块区域的惩罚分数\r\nconst N2 = 3;\r\n// N3: 类似定位图形的图案(1:1:3:1:1)的惩罚分数\r\nconst N3 = 40;\r\n// N4: 黑白模块比例不均衡的惩罚分数\r\nconst N4 = 10;\r\n\r\n// 版本信息掩码表 - 用于在二维码中嵌入版本信息\r\nconst VERSION_BLOCK = [\r\n\t0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532,\r\n\t0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5,\r\n\t0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69\r\n];\r\n\r\n/**\r\n * 生成二维码点阵\r\n * @param _str 输入字符串,支持任意文本内容,默认 null 表示空字符串\r\n * @param ecc 纠错级别,可选 'L' | 'M' | 'Q' | 'H',默认 'L'\r\n * @returns {GenerateFrameResult} 返回二维码点阵数据和宽度\r\n */\r\nexport function generateFrame(\r\n\t_str: string | null = null,\r\n\tecc: string | null = null\r\n): GenerateFrameResult {\r\n\t// 变量声明区,所有临时变量、缓冲区\r\n\tlet i: number;\r\n\tlet t: number;\r\n\tlet j: number;\r\n\tlet k: number;\r\n\tlet m: number;\r\n\tlet v: number;\r\n\tlet x: number;\r\n\tlet y: number;\r\n\tlet version: number;\r\n\tlet str = _str == null ? \"\" : _str;\r\n\tlet width = 0;\r\n\t// 获取纠错级别数值\r\n\tlet eccLevel = ECC_LEVELS.get(ecc == null ? \"L\" : ecc)!;\r\n\r\n\t// Data block\r\n\t// 数据块、纠错块、块数\r\n\tlet dataBlock: number;\r\n\tlet eccBlock: number;\r\n\tlet neccBlock1: number;\r\n\tlet neccBlock2: number;\r\n\r\n\t// ECC buffer.\r\n\t// 纠错码缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet eccBuffer: Uint8Array;\r\n\r\n\t// Image buffer.\r\n\t// 二维码点阵缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet frameBuffer = new Uint8Array(0);\r\n\r\n\t// Fixed part of the image.\r\n\t// 点阵掩码缓冲区(标记不可变区域) - 先初始化为空数组,后面会重新赋值\r\n\tlet frameMask = new Uint8Array(0);\r\n\r\n\t// Generator polynomial.\r\n\t// 生成多项式缓冲区(纠错码计算用) - 先初始化为空数组,后面会重新赋值\r\n\tlet polynomial = new Uint8Array(0);\r\n\r\n\t// Data input buffer.\r\n\t// 数据输入缓冲区 - 先初始化为空数组,后面会重新赋值\r\n\tlet stringBuffer = new Uint8Array(0);\r\n\r\n\t/**\r\n\t * 设置掩码位,表示该点为不可变区域(对称处理)\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction setMask(_x: number, _y: number) {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit *= y;\r\n\t\tbit += y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\r\n\t\tframeMask[bit] = 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 添加对齐块,设置对应点阵和掩码\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t */\r\n\tfunction addAlignment(_x: number, _y: number) {\r\n\t\tlet i: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tframeBuffer[x + width * y] = 1;\r\n\r\n\t\tfor (i = -2; i < 2; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i + 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i)] = 1;\r\n\t\t\tframeBuffer[x + i + 1 + width * (y + 2)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < 2; i++) {\r\n\t\t\tsetMask(x - 1, y + i);\r\n\t\t\tsetMask(x + 1, y - i);\r\n\t\t\tsetMask(x - i, y - 1);\r\n\t\t\tsetMask(x + i, y + 1);\r\n\t\t}\r\n\r\n\t\tfor (i = 2; i < 4; i++) {\r\n\t\t\tframeBuffer[x + i + width * (y - 2)] = 1;\r\n\t\t\tframeBuffer[x - 2 + width * (y + i - 1)] = 1;\r\n\t\t\tframeBuffer[x + 2 + width * (y + i - 2)] = 1;\r\n\t\t\tframeBuffer[x - 1 + width * (y + i - 2)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Galois 域取模运算\r\n\t * @param _x 输入数值\r\n\t * @returns {number} 取模结果\r\n\t */\r\n\tfunction modN(_x: number): number {\r\n\t\tvar x = _x;\r\n\t\twhile (x >= 255) {\r\n\t\t\tx -= 255;\r\n\t\t\tx = (x >> 8) + (x & 255);\r\n\t\t}\r\n\r\n\t\treturn x;\r\n\t}\r\n\r\n\t/**\r\n\t * 计算并追加纠错码到数据块\r\n\t * @param _data 数据起始索引\r\n\t * @param _dataLength 数据长度\r\n\t * @param _ecc 纠错码起始索引\r\n\t * @param _eccLength 纠错码长度\r\n\t */\r\n\tfunction appendData(_data: number, _dataLength: number, _ecc: number, _eccLength: number) {\r\n\t\tlet bit: number;\r\n\t\tlet i: number;\r\n\t\tlet j: number;\r\n\t\tlet data = _data;\r\n\t\tlet dataLength = _dataLength;\r\n\t\tlet ecc = _ecc;\r\n\t\tlet eccLength = _eccLength;\r\n\r\n\t\tfor (i = 0; i < eccLength; i++) {\r\n\t\t\tstringBuffer[ecc + i] = 0;\r\n\t\t}\r\n\r\n\t\tfor (i = 0; i < dataLength; i++) {\r\n\t\t\tbit = GALOIS_LOG[stringBuffer[data + i] ^ stringBuffer[ecc]];\r\n\r\n\t\t\tif (bit != 255) {\r\n\t\t\t\tfor (j = 1; j < eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[ecc + j - 1] =\r\n\t\t\t\t\t\tstringBuffer[ecc + j] ^\r\n\t\t\t\t\t\tGALOIS_EXPONENT[modN(bit + polynomial[eccLength - j])];\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tfor (j = ecc; j < ecc + eccLength; j++) {\r\n\t\t\t\t\tstringBuffer[j] = stringBuffer[j + 1];\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\tstringBuffer[ecc + eccLength - 1] =\r\n\t\t\t\tbit == 255 ? 0 : GALOIS_EXPONENT[modN(bit + polynomial[0])];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 判断某点是否为掩码区域\r\n\t * @param _x 横坐标\r\n\t * @param _y 纵坐标\r\n\t * @returns {boolean} 是否为掩码\r\n\t */\r\n\tfunction isMasked(_x: number, _y: number): boolean {\r\n\t\tlet bit: number;\r\n\t\tlet x = _x;\r\n\t\tlet y = _y;\r\n\r\n\t\tif (x > y) {\r\n\t\t\tbit = x;\r\n\t\t\tx = y;\r\n\t\t\ty = bit;\r\n\t\t}\r\n\r\n\t\tbit = y;\r\n\t\tbit += y * y;\r\n\t\tbit >>= 1;\r\n\t\tbit += x;\r\n\t\treturn frameMask[bit] == 1;\r\n\t}\r\n\r\n\t/**\r\n\t * 根据 QR Code 标准,应用指定的掩码 pattern\r\n\t * @param mask 掩码编号 (0-7)\r\n\t */\r\n\tfunction applyMask(mask: number) {\r\n\t\tfor (let y = 0; y < width; y++) {\r\n\t\t\tfor (let x = 0; x < width; x++) {\r\n\t\t\t\tif (!isMasked(x, y)) {\r\n\t\t\t\t\tlet shouldInvert = false;\r\n\t\t\t\t\tswitch (mask) {\r\n\t\t\t\t\t\tcase 0:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 1:\r\n\t\t\t\t\t\t\tshouldInvert = y % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 2:\r\n\t\t\t\t\t\t\tshouldInvert = x % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 3:\r\n\t\t\t\t\t\t\tshouldInvert = (x + y) % 3 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 4:\r\n\t\t\t\t\t\t\tshouldInvert = (Math.floor(y / 2) + Math.floor(x / 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 5:\r\n\t\t\t\t\t\t\tshouldInvert = ((x * y) % 2) + ((x * y) % 3) == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 6:\r\n\t\t\t\t\t\t\tshouldInvert = (((x * y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t\tcase 7:\r\n\t\t\t\t\t\t\tshouldInvert = (((x + y) % 2) + ((x * y) % 3)) % 2 == 0;\r\n\t\t\t\t\t\t\tbreak;\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tif (shouldInvert) {\r\n\t\t\t\t\t\tframeBuffer[x + y * width] ^= 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * 计算连续同色块的\"坏度\"分数\r\n\t * @param runLengths\r\n\t * @param length 块长度\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction getBadRuns(runLengths: number[], length: number): number {\r\n\t\tlet badRuns = 0;\r\n\t\tlet i: number;\r\n\r\n\t\tfor (i = 0; i <= length; i++) {\r\n\t\t\tif (i < runLengths.length && runLengths[i] >= 5) {\r\n\t\t\t\tbadRuns += N1 + runLengths[i] - 5;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// FBFFFBF as in finder.\r\n\t\tfor (i = 3; i < length - 1; i += 2) {\r\n\t\t\t// 检查数组索引是否越界\r\n\t\t\tif (i + 2 >= runLengths.length || i - 3 < 0) {\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif (\r\n\t\t\t\trunLengths[i - 2] == runLengths[i + 2] &&\r\n\t\t\t\trunLengths[i + 2] == runLengths[i - 1] &&\r\n\t\t\t\trunLengths[i - 1] == runLengths[i + 1] &&\r\n\t\t\t\trunLengths[i - 1] * 3 == runLengths[i] &&\r\n\t\t\t\t// Background around the foreground pattern? Not part of the specs.\r\n\t\t\t\t(runLengths[i - 3] == 0 ||\r\n\t\t\t\t\ti + 3 > length ||\r\n\t\t\t\t\trunLengths[i - 3] * 3 >= runLengths[i] * 4 ||\r\n\t\t\t\t\trunLengths[i + 3] * 3 >= runLengths[i] * 4)\r\n\t\t\t) {\r\n\t\t\t\tbadRuns += N3;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn badRuns;\r\n\t}\r\n\r\n\t/**\r\n\t * 评估当前二维码点阵的整体\"坏度\"\r\n\t * @returns {number} 坏度分数\r\n\t */\r\n\tfunction checkBadness(): number {\r\n\t\tlet b: number;\r\n\t\tlet b1: number;\r\n\t\tlet bad = 0;\r\n\t\tlet big: number;\r\n\t\tlet bw = 0;\r\n\t\tlet count = 0;\r\n\t\tlet h: number;\r\n\t\tlet x: number;\r\n\t\tlet y: number;\r\n\t\t// 优化:在函数内创建badBuffer,避免外部变量的内存泄漏风险\r\n\t\tlet badBuffer = new Array<number>(width);\r\n\r\n\t\t// Blocks of same colour.\r\n\t\tfor (y = 0; y < width - 1; y++) {\r\n\t\t\tfor (x = 0; x < width - 1; x++) {\r\n\t\t\t\t// All foreground colour.\r\n\t\t\t\tif (\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 1 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 1) ||\r\n\t\t\t\t\t// All background colour.\r\n\t\t\t\t\t(frameBuffer[x + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * y] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + width * (y + 1)] == 0 &&\r\n\t\t\t\t\t\tframeBuffer[x + 1 + width * (y + 1)] == 0)\r\n\t\t\t\t) {\r\n\t\t\t\t\tbad += N2;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t// X runs\r\n\t\tfor (y = 0; y < width; y++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (x = 0; x < width; x++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t\tbw += b > 0 ? 1 : -1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\tif (bw < 0) bw = -bw;\r\n\r\n\t\tbig = bw;\r\n\t\tbig += big << 2;\r\n\t\tbig <<= 1;\r\n\r\n\t\twhile (big > width * width) {\r\n\t\t\tbig -= width * width;\r\n\t\t\tcount++;\r\n\t\t}\r\n\r\n\t\tbad += count * N4;\r\n\r\n\t\t// Y runs.\r\n\t\tfor (x = 0; x < width; x++) {\r\n\t\t\th = 0;\r\n\t\t\tbadBuffer[h] = 0;\r\n\t\t\tb = 0;\r\n\t\t\tfor (y = 0; y < width; y++) {\r\n\t\t\t\tb1 = frameBuffer[x + width * y];\r\n\t\t\t\tif (b1 == b) {\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h]++;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\th++;\r\n\t\t\t\t\tif (h < badBuffer.length) {\r\n\t\t\t\t\t\tbadBuffer[h] = 1;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tb = b1;\r\n\t\t\t}\r\n\r\n\t\t\tbad += getBadRuns(badBuffer, h);\r\n\t\t}\r\n\r\n\t\treturn bad;\r\n\t}\r\n\r\n\t/**\r\n\t * 将字符串转为 UTF-8 编码,兼容多平台\r\n\t * @param str 输入字符串\r\n\t * @returns {string} UTF-8 编码字符串\r\n\t */\r\n\tfunction toUtf8(str: string): string {\r\n\t\tlet out = \"\";\r\n\t\tlet i: number;\r\n\t\tlet len: number;\r\n\t\tlet c: number;\r\n\t\tlen = str.length;\r\n\t\tfor (i = 0; i < len; i++) {\r\n\t\t\tc = str.charCodeAt(i)!;\r\n\t\t\tif (c >= 0x0001 && c <= 0x007f) {\r\n\t\t\t\tout += str.charAt(i);\r\n\t\t\t} else if (c > 0x07ff) {\r\n\t\t\t\tout += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t} else {\r\n\t\t\t\tout += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));\r\n\t\t\t\tout += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn out;\r\n\t}\r\n\t//end functions\r\n\r\n\t// Find the smallest version that fits the string.\r\n\t// 1. 字符串转 UTF-8,计算长度\r\n\tstr = toUtf8(str);\r\n\tt = str.length;\r\n\r\n\t// 2. 自动选择最小可用版本\r\n\tversion = 0;\r\n\tdo {\r\n\t\tversion++;\r\n\t\tk = (eccLevel - 1) * 4 + (version - 1) * 16;\r\n\t\tneccBlock1 = ECC_BLOCKS[k++];\r\n\t\tneccBlock2 = ECC_BLOCKS[k++];\r\n\t\tdataBlock = ECC_BLOCKS[k++];\r\n\t\teccBlock = ECC_BLOCKS[k];\r\n\r\n\t\tk = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2 - 3 + (version <= 9 ? 1 : 0);\r\n\r\n\t\tif (t <= k) break;\r\n\t} while (version < 40);\r\n\r\n\t// FIXME: Ensure that it fits insted of being truncated.\r\n\t// 3. 计算二维码宽度\r\n\twidth = 17 + 4 * version;\r\n\r\n\t// Allocate, clear and setup data structures.\r\n\t// 4. 分配缓冲区, 使用定长的 Uint8Array 优化内存\r\n\tv = dataBlock + (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\teccBuffer = new Uint8Array(v);\r\n\tstringBuffer = new Uint8Array(v);\r\n\r\n\t// 5. 预分配点阵、掩码缓冲区\r\n\tframeBuffer = new Uint8Array(width * width);\r\n\tframeMask = new Uint8Array(Math.floor((width * (width + 1) + 1) / 2));\r\n\r\n\t// Insert finders: Foreground colour to frame and background to mask.\r\n\t// 插入定位点: 前景色为二维码,背景色为掩码\r\n\tfor (t = 0; t < 3; t++) {\r\n\t\tk = 0;\r\n\t\ty = 0;\r\n\t\tif (t == 1) k = width - 7;\r\n\t\tif (t == 2) y = width - 7;\r\n\r\n\t\tframeBuffer[y + 3 + width * (k + 3)] = 1;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tframeBuffer[y + x + width * k] = 1;\r\n\t\t\tframeBuffer[y + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 6 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 6)] = 1;\r\n\t\t}\r\n\r\n\t\tfor (x = 1; x < 5; x++) {\r\n\t\t\tsetMask(y + x, k + 1);\r\n\t\t\tsetMask(y + 1, k + x + 1);\r\n\t\t\tsetMask(y + 5, k + x);\r\n\t\t\tsetMask(y + x + 1, k + 5);\r\n\t\t}\r\n\r\n\t\tfor (x = 2; x < 4; x++) {\r\n\t\t\tframeBuffer[y + x + width * (k + 2)] = 1;\r\n\t\t\tframeBuffer[y + 2 + width * (k + x + 1)] = 1;\r\n\t\t\tframeBuffer[y + 4 + width * (k + x)] = 1;\r\n\t\t\tframeBuffer[y + x + 1 + width * (k + 4)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Alignment blocks.\r\n\t// 插入对齐点: 前景色为二维码,背景色为掩码\r\n\tif (version > 1) {\r\n\t\tt = ALIGNMENT_DELTA[version];\r\n\t\ty = width - 7;\r\n\r\n\t\tfor (;;) {\r\n\t\t\tx = width - 7;\r\n\r\n\t\t\twhile (x > t - 3) {\r\n\t\t\t\taddAlignment(x, y);\r\n\r\n\t\t\t\tif (x < t) break;\r\n\r\n\t\t\t\tx -= t;\r\n\t\t\t}\r\n\r\n\t\t\tif (y <= t + 9) break;\r\n\r\n\t\t\ty -= t;\r\n\r\n\t\t\taddAlignment(6, y);\r\n\t\t\taddAlignment(y, 6);\r\n\t\t}\r\n\t}\r\n\r\n\t// Single foreground cell.\r\n\t// 插入单个前景色单元格: 前景色为二维码,背景色为掩码\r\n\tframeBuffer[8 + width * (width - 8)] = 1;\r\n\r\n\t// Timing gap (mask only).\r\n\t// 插入时间间隔: 掩码\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(7, y);\r\n\t\tsetMask(width - 8, y);\r\n\t\tsetMask(7, y + width - 7);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x, 7);\r\n\t\tsetMask(x + width - 8, 7);\r\n\t\tsetMask(x, width - 8);\r\n\t}\r\n\r\n\t// Reserve mask, format area.\r\n\t// 保留掩码,格式化区域\r\n\tfor (x = 0; x < 9; x++) {\r\n\t\tsetMask(x, 8);\r\n\t}\r\n\r\n\tfor (x = 0; x < 8; x++) {\r\n\t\tsetMask(x + width - 8, 8);\r\n\t\tsetMask(8, x);\r\n\t}\r\n\r\n\tfor (y = 0; y < 7; y++) {\r\n\t\tsetMask(8, y + width - 7);\r\n\t}\r\n\r\n\t// Timing row/column.\r\n\t// 插入时间间隔行/列: 掩码\r\n\tfor (x = 0; x < width - 14; x++) {\r\n\t\tif ((x & 1) > 0) {\r\n\t\t\tsetMask(8 + x, 6);\r\n\t\t\tsetMask(6, 8 + x);\r\n\t\t} else {\r\n\t\t\tframeBuffer[8 + x + width * 6] = 1;\r\n\t\t\tframeBuffer[6 + width * (8 + x)] = 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Version block.\r\n\tif (version > 6) {\r\n\t\tt = VERSION_BLOCK[version - 7];\r\n\t\tk = 17;\r\n\r\n\t\tfor (x = 0; x < 6; x++) {\r\n\t\t\tfor (y = 0; y < 3; y++) {\r\n\t\t\t\tif ((1 & (k > 11 ? version >> (k - 12) : t >> k)) > 0) {\r\n\t\t\t\t\tframeBuffer[5 - x + width * (2 - y + width - 11)] = 1;\r\n\t\t\t\t\tframeBuffer[2 - y + width - 11 + width * (5 - x)] = 1;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tsetMask(5 - x, 2 - y + width - 11);\r\n\t\t\t\t\tsetMask(2 - y + width - 11, 5 - x);\r\n\t\t\t\t}\r\n\t\t\t\tk--;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Sync mask bits. Only set above for background cells, so now add the foreground.\r\n\t// 同步掩码位。只有上方的背景单元格需要设置,现在添加前景色。\r\n\tfor (y = 0; y < width; y++) {\r\n\t\tfor (x = 0; x <= y; x++) {\r\n\t\t\tif (frameBuffer[x + width * y] > 0) {\r\n\t\t\t\tsetMask(x, y);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Convert string to bit stream. 8-bit data to QR-coded 8-bit data (numeric, alphanum, or kanji\r\n\t// not supported).\r\n\t// 将字符串转换为位流。8位数据转换为QR编码的8位数据(不支持数字、字母或汉字)。\r\n\tv = str.length;\r\n\r\n\t// String to array.\r\n\tfor (i = 0; i < v; i++) {\r\n\t\t// #ifdef APP-ANDROID\r\n\t\t// @ts-ignore\r\n\t\teccBuffer[i.toInt()] = str.charCodeAt(i)!;\r\n\t\t// #endif\r\n\t\t// #ifndef APP-ANDROID\r\n\t\teccBuffer[i] = str.charCodeAt(i)!;\r\n\t\t// #endif\r\n\t}\r\n\r\n\t//++++++++++++++++++++==============\r\n\tstringBuffer.set(eccBuffer.subarray(0, v));\r\n\r\n\t// Calculate max string length.\r\n\tx = dataBlock * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tif (v >= x - 2) {\r\n\t\tv = x - 2;\r\n\r\n\t\tif (version > 9) v--;\r\n\t}\r\n\r\n\t// Shift and re-pack to insert length prefix.\r\n\t// 移位并重新打包以插入长度前缀。\r\n\ti = v;\r\n\r\n\tif (version > 9) {\r\n\t\tstringBuffer[i + 2] = 0;\r\n\t\tstringBuffer[i + 3] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 3] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 2] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[2] |= 255 & (v << 4);\r\n\t\tstringBuffer[1] = v >> 4;\r\n\t\tstringBuffer[0] = 0x40 | (v >> 12);\r\n\t} else {\r\n\t\tstringBuffer[i + 1] = 0;\r\n\t\tstringBuffer[i + 2] = 0;\r\n\r\n\t\twhile (i-- > 0) {\r\n\t\t\tt = stringBuffer[i];\r\n\r\n\t\t\tstringBuffer[i + 2] |= 255 & (t << 4);\r\n\t\t\tstringBuffer[i + 1] = t >> 4;\r\n\t\t}\r\n\r\n\t\tstringBuffer[1] |= 255 & (v << 4);\r\n\t\tstringBuffer[0] = 0x40 | (v >> 4);\r\n\t}\r\n\r\n\t// Fill to end with pad pattern.\r\n\t// 用填充模式填充到结束。\r\n\ti = v + 3 - (version < 10 ? 1 : 0);\r\n\r\n\twhile (i < x) {\r\n\t\tstringBuffer[i++] = 0xec;\r\n\t\tstringBuffer[i++] = 0x11;\r\n\t}\r\n\r\n\t// Calculate generator polynomial.\r\n\t// 计算生成多项式。\r\n\tpolynomial = new Uint8Array(eccBlock + 1);\r\n\tpolynomial[0] = 1;\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i + 1] = 1;\r\n\r\n\t\tfor (j = i; j > 0; j--) {\r\n\t\t\tpolynomial[j] =\r\n\t\t\t\tpolynomial[j] > 0\r\n\t\t\t\t\t? polynomial[j - 1] ^ GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[j]] + i)]\r\n\t\t\t\t\t: polynomial[j - 1];\r\n\t\t}\r\n\r\n\t\tpolynomial[0] = GALOIS_EXPONENT[modN(GALOIS_LOG[polynomial[0]] + i)];\r\n\t}\r\n\r\n\t// Use logs for generator polynomial to save calculation step.\r\n\t// 使用对数计算生成多项式以节省计算步骤。\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tpolynomial[i] = GALOIS_LOG[polynomial[i]];\r\n\t}\r\n\r\n\t// Append ECC to data buffer.\r\n\t// 将ECC附加到数据缓冲区。\r\n\tk = x;\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < neccBlock1; i++) {\r\n\t\tappendData(y, dataBlock, k, eccBlock);\r\n\r\n\t\ty += dataBlock;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\tfor (i = 0; i < neccBlock2; i++) {\r\n\t\tappendData(y, dataBlock + 1, k, eccBlock);\r\n\r\n\t\ty += dataBlock + 1;\r\n\t\tk += eccBlock;\r\n\t}\r\n\r\n\t// Interleave blocks.\r\n\ty = 0;\r\n\r\n\tfor (i = 0; i < dataBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[i + j * dataBlock];\r\n\t\t}\r\n\r\n\t\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t\t}\r\n\t}\r\n\r\n\tfor (j = 0; j < neccBlock2; j++) {\r\n\t\teccBuffer[y++] = stringBuffer[neccBlock1 * dataBlock + i + j * (dataBlock + 1)];\r\n\t}\r\n\r\n\tfor (i = 0; i < eccBlock; i++) {\r\n\t\tfor (j = 0; j < neccBlock1 + neccBlock2; j++) {\r\n\t\t\teccBuffer[y++] = stringBuffer[x + i + j * eccBlock];\r\n\t\t}\r\n\t}\r\n\r\n\tstringBuffer.set(eccBuffer);\r\n\r\n\t// Pack bits into frame avoiding masked area.\r\n\t// 将位流打包到帧中,避免掩码区域。\r\n\tx = width - 1;\r\n\ty = width - 1;\r\n\tk = 1;\r\n\tv = 1;\r\n\r\n\t// inteleaved data and ECC codes.\r\n\t// 交错数据和ECC代码。\r\n\tm = (dataBlock + eccBlock) * (neccBlock1 + neccBlock2) + neccBlock2;\r\n\r\n\tfor (i = 0; i < m; i++) {\r\n\t\tt = stringBuffer[i];\r\n\r\n\t\tfor (j = 0; j < 8; j++) {\r\n\t\t\tif ((0x80 & t) > 0) {\r\n\t\t\t\tframeBuffer[x + width * y] = 1;\r\n\t\t\t}\r\n\r\n\t\t\t// Find next fill position.\r\n\t\t\t// 找到下一个填充位置。\r\n\t\t\tdo {\r\n\t\t\t\tif (v > 0) {\r\n\t\t\t\t\tx--;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tx++;\r\n\r\n\t\t\t\t\tif (k > 0) {\r\n\t\t\t\t\t\tif (y != 0) {\r\n\t\t\t\t\t\t\ty--;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty = 9;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (y != width - 1) {\r\n\t\t\t\t\t\t\ty++;\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tx -= 2;\r\n\t\t\t\t\t\t\tk = k == 0 ? 1 : 0;\r\n\r\n\t\t\t\t\t\t\tif (x == 6) {\r\n\t\t\t\t\t\t\t\tx--;\r\n\t\t\t\t\t\t\t\ty -= 8;\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tv = v == 0 ? 1 : 0;\r\n\t\t\t} while (isMasked(x, y));\r\n\t\t\tt <<= 1;\r\n\t\t}\r\n\t}\r\n\r\n\t// Save pre-mask copy of frame.\r\n\tconst frameBufferCopy = frameBuffer.slice(0);\r\n\r\n\tt = 0;\r\n\ty = 30000;\r\n\r\n\t// Using `for` instead of `while` since in original Arduino code if an early mask was *good\r\n\t// enough* it wouldn't try for a better one since they get more complex and take longer.\r\n\t// 使用`for`而不是`while`,因为在原始Arduino代码中,如果早期掩码足够好,它不会尝试更好的掩码,因为它们变得更复杂并需要更长的时间。\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\t// Returns foreground-background imbalance.\r\n\t\t// 返回前景色和背景色的不平衡。\r\n\t\tapplyMask(k);\r\n\r\n\t\tx = checkBadness();\r\n\r\n\t\t// Is current mask better than previous best?\r\n\t\t// 当前掩码是否比之前的最佳掩码更好?\r\n\t\tif (x < y) {\r\n\t\t\ty = x;\r\n\t\t\tt = k;\r\n\t\t}\r\n\r\n\t\t// Don't increment `i` to a void redoing mask.\r\n\t\t// 不要增加`i`以避免重新做掩码。\r\n\t\tif (t == 7) break;\r\n\r\n\t\t// Reset for next pass.\r\n\t\t// 重置下一个循环。\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t}\r\n\r\n\t// Redo best mask as none were *good enough* (i.e. last wasn't `t`).\r\n\t// 重做最佳掩码,因为没有一个掩码足够好(即最后一个不是`t`)。\r\n\tif (t != k) {\r\n\t\t// Reset buffer to pre-mask state before applying the best one\r\n\t\tframeBuffer.set(frameBufferCopy);\r\n\t\tapplyMask(t);\r\n\t}\r\n\r\n\t// Add in final mask/ECC level bytes.\r\n\t// 添加最终的掩码/ECC级别字节。\r\n\ty = FINAL_FORMAT[t + ((eccLevel - 1) << 3)];\r\n\r\n\t// Low byte.\r\n\tfor (k = 0; k < 8; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[width - 1 - k + width * 8] = 1;\r\n\r\n\t\t\tif (k < 6) {\r\n\t\t\t\tframeBuffer[8 + width * k] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[8 + width * (k + 1)] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// High byte.\r\n\tfor (k = 0; k < 7; k++) {\r\n\t\tif ((y & 1) > 0) {\r\n\t\t\tframeBuffer[8 + width * (width - 7 + k)] = 1;\r\n\r\n\t\t\tif (k > 0) {\r\n\t\t\t\tframeBuffer[6 - k + width * 8] = 1;\r\n\t\t\t} else {\r\n\t\t\t\tframeBuffer[7 + width * 8] = 1;\r\n\t\t\t}\r\n\t\t}\r\n\t\ty >>= 1;\r\n\t}\r\n\r\n\t// Finally, return the image data.\r\n\treturn {\r\n\t\tframeBuffer: frameBuffer,\r\n\t\twidth: width\r\n\t} as GenerateFrameResult;\r\n}\r\n",
|
|
1738
|
+
"target": "uniapp"
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
"path": "RebornQrcode.vue",
|
|
1742
|
+
"content": "<template>\r\n <view :style=\"{ width: size + 'px', height: size + 'px' }\">\r\n <!-- #ifdef MP-WEIXIN -->\r\n <canvas class=\"relative z-1\" type=\"2d\" :id=\"qrcodeId\"\r\n :style=\"{ width: size + 'px', height: size + 'px' }\"></canvas>\r\n <!-- #endif -->\r\n <!-- #ifndef MP-WEIXIN -->\r\n <canvas :canvas-id=\"qrcodeId\" :id=\"qrcodeId\" :style=\"{ width: size + 'px', height: size + 'px' }\"></canvas>\r\n <!-- #endif -->\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport {\r\n ref,\r\n watch,\r\n onMounted,\r\n getCurrentInstance,\r\n nextTick,\r\n computed,\r\n onUnmounted,\r\n} from \"vue\";\r\n\r\nimport { drawQrcode, eccLevel, type ClQrcodeMode } from \"./draw\";\r\nimport { canvasToPng, uuid } from \"../../lib/file\";\r\nimport { isAppIOS, isHarmony } from \"../../lib/device\";\r\n\r\ndefineOptions({\r\n name: \"reborn-qrcode\",\r\n inheritAttrs: false\r\n});\r\n\r\ninterface RebornQrcodeOptions {\r\n size?: number;\r\n foreground?: string;\r\n background?: string;\r\n pdColor?: string | null;\r\n pdRadius?: number;\r\n text?: string;\r\n logo?: string;\r\n logoSize?: number;\r\n padding?: number;\r\n mode?: ClQrcodeMode;\r\n ecc?: eccLevel;\r\n pdOuterRadius?: number;\r\n pdInnerRadius?: number;\r\n dotsGradient?: any;\r\n dotsImage?: string | null;\r\n backgroundGradient?: any;\r\n backgroundTransparent?: boolean;\r\n logoOptions?: any;\r\n cornersSquareGradient?: any;\r\n cornersDotGradient?: any;\r\n cornersSquareOptions?: any;\r\n cornersDotOptions?: any;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornQrcodeOptions>(), {\r\n size: 200,\r\n foreground: \"#131313\",\r\n background: \"#FFFFFF\",\r\n pdColor: null,\r\n pdRadius: 10,\r\n text: \"https://cool-js.com/\",\r\n logo: \"\",\r\n logoSize: 40,\r\n padding: 5,\r\n mode: \"circular\",\r\n ecc: eccLevel.H,\r\n pdOuterRadius: undefined,\r\n pdInnerRadius: undefined\r\n});\r\n\r\n\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\n// 二维码组件id\r\nconst qrcodeId = ref<string>(\"cl-qrcode-\" + uuid());\r\n\r\n/**\r\n * 主绘制方法,根据当前 props 生成二维码并绘制到 canvas。\r\n * 支持多平台(APP、H5、微信小程序),自动适配高分屏。\r\n * 内部调用 drawQrcode 进行二维码点阵绘制。\r\n */\r\nfunction drawer() {\r\n const data = {\r\n ecc: props.ecc,\r\n text: props.text,\r\n size: props.size,\r\n foreground: props.foreground,\r\n background: props.background,\r\n padding: props.padding,\r\n logo: props.logo,\r\n logoSize: props.logoSize,\r\n mode: props.mode,\r\n pdColor: props.pdColor,\r\n pdRadius: props.pdRadius,\r\n pdOuterRadius: props.pdOuterRadius,\r\n pdInnerRadius: props.pdInnerRadius,\r\n dotsGradient: props.dotsGradient,\r\n dotsImage: props.dotsImage,\r\n backgroundGradient: props.backgroundGradient,\r\n backgroundTransparent: props.backgroundTransparent,\r\n logoOptions: props.logoOptions,\r\n cornersSquareGradient: props.cornersSquareGradient, // 优先使用外框的渐变\r\n cornersDotGradient: props.cornersDotGradient, // 优先使用内点的渐变\r\n cornersSquareOptions: props.cornersSquareOptions,\r\n cornersDotOptions: props.cornersDotOptions,\r\n };\r\n\r\n nextTick(() => {\r\n // #ifdef MP-WEIXIN\r\n const query = uni.createSelectorQuery().in(proxy as any);\r\n (query.select('#' + qrcodeId.value) as any)\r\n .fields({ node: true, size: true }, (res: any) => {\r\n if (res?.node) {\r\n const canvas = res.node as any;\r\n const ctx = canvas.getContext('2d') as any;\r\n const dpr = uni.getSystemInfoSync().pixelRatio;\r\n canvas.width = props.size * dpr;\r\n canvas.height = props.size * dpr;\r\n ctx.scale(dpr, dpr);\r\n drawQrcode(ctx, data, canvas);\r\n }\r\n })\r\n .exec();\r\n // #endif\r\n\r\n // #ifndef MP-WEIXIN\r\n const context = uni.createCanvasContext(qrcodeId.value, proxy);\r\n drawQrcode(context, data);\r\n // #endif\r\n });\r\n}\r\n/**\r\n * 获取当前二维码图片的临时文件地址\r\n * @param call 回调函数,返回图片路径,失败返回空字符串\r\n */\r\nfunction toPng(): Promise<string> {\r\n return new Promise((resolve, reject) => {\r\n setTimeout(() => {\r\n // #ifdef MP-WEIXIN\r\n const query = uni.createSelectorQuery().in(proxy as any);\r\n (query.select('#' + qrcodeId.value) as any)\r\n .fields({ node: true }, (res: any) => {\r\n if (res?.node) {\r\n const canvas = res.node as any;\r\n (uni.canvasToTempFilePath as any)({\r\n canvas,\r\n destWidth: props.size * 3,\r\n destHeight: props.size * 3,\r\n success: (result: any) => {\r\n resolve(result.tempFilePath);\r\n },\r\n fail: (err: any) => {\r\n reject(err);\r\n }\r\n });\r\n } else {\r\n reject(new Error('未找到 canvas 节点'));\r\n }\r\n })\r\n .exec();\r\n // #endif\r\n\r\n // #ifndef MP-WEIXIN\r\n uni.canvasToTempFilePath({\r\n canvasId: qrcodeId.value,\r\n destWidth: props.size * 3,\r\n destHeight: props.size * 3,\r\n success: (res) => {\r\n resolve(res.tempFilePath);\r\n },\r\n fail: (err) => {\r\n reject(err);\r\n }\r\n }, proxy);\r\n // #endif\r\n }, 100);\r\n });\r\n}\r\n\r\n// 自动重绘\r\nconst stopWatch = watch(\r\n computed(() => [\r\n props.pdColor,\r\n props.pdRadius,\r\n props.pdOuterRadius,\r\n props.pdInnerRadius,\r\n props.dotsGradient,\r\n props.dotsImage,\r\n props.backgroundGradient,\r\n props.backgroundTransparent,\r\n props.logoOptions,\r\n props.cornersSquareGradient,\r\n props.cornersDotGradient,\r\n props.cornersSquareOptions,\r\n props.cornersDotOptions,\r\n props.foreground,\r\n props.background,\r\n props.text,\r\n props.logo,\r\n props.logoSize,\r\n props.mode,\r\n props.padding,\r\n props.ecc\r\n ]),\r\n () => {\r\n drawer();\r\n }\r\n);\r\n\r\nonMounted(() => {\r\n setTimeout(\r\n () => {\r\n drawer();\r\n },\r\n isHarmony() || isAppIOS() ? 50 : 0\r\n );\r\n});\r\n\r\nonUnmounted(() => {\r\n stopWatch();\r\n});\r\n\r\ndefineExpose({\r\n toPng\r\n});\r\n</script>\r\n",
|
|
1743
|
+
"target": "uniapp"
|
|
1744
|
+
}
|
|
1745
|
+
]
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
"name": "reborn-radio",
|
|
1749
|
+
"dependencies": [
|
|
1750
|
+
"clsx"
|
|
1751
|
+
],
|
|
1752
|
+
"files": [
|
|
1753
|
+
{
|
|
1754
|
+
"path": "index.ts",
|
|
1755
|
+
"content": "export { default as RebornRadio } from \"./RebornRadio.vue\";\r\n",
|
|
1756
|
+
"target": "web"
|
|
1757
|
+
},
|
|
1758
|
+
{
|
|
1759
|
+
"path": "reborn-radio.config.ts",
|
|
1760
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as radioSizes, colors as radioColors };\r\n\r\nexport default {\r\n slots: {\r\n root: \"inline-flex cursor-pointer select-none\",\r\n wrapper: \"inline-flex items-center gap-2\",\r\n activeIcon:\r\n \"inline-flex items-center justify-center rounded-full border-2 border-transparent text-white transition-all duration-200\",\r\n inactiveIcon:\r\n \"inline-flex items-center justify-center rounded-full border-2 border-gray-4 dark:border-gray-6 transition-all duration-200\",\r\n label: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n activeIcon: \"size-4 text-[10px]\",\r\n inactiveIcon: \"size-4\",\r\n label: \"text-xs\",\r\n },\r\n md: {\r\n activeIcon: \"size-5 text-xs\",\r\n inactiveIcon: \"size-5\",\r\n label: \"text-sm\",\r\n },\r\n lg: {\r\n activeIcon: \"size-6 text-sm\",\r\n inactiveIcon: \"size-6\",\r\n label: \"text-base\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n activeIcon: \"bg-primary border-primary\",\r\n },\r\n secondary: {\r\n activeIcon: \"bg-secondary border-secondary\",\r\n },\r\n success: {\r\n activeIcon: \"bg-success border-success\",\r\n },\r\n info: {\r\n activeIcon: \"bg-info border-info\",\r\n },\r\n warning: {\r\n activeIcon: \"bg-warning border-warning\",\r\n },\r\n error: {\r\n activeIcon: \"bg-error border-error\",\r\n },\r\n neutral: {\r\n activeIcon: \"bg-neutral border-neutral\",\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: \"opacity-50 pointer-events-none\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
1761
|
+
"target": "web"
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
"path": "RebornRadio.vue",
|
|
1765
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, useAttrs, useSlots } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { radioColors, radioSizes } from \"./reborn-radio.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface RadioProps {\r\n modelValue?: any;\r\n value?: any;\r\n label?: string;\r\n disabled?: boolean;\r\n size?: (typeof radioSizes)[number];\r\n color?: (typeof radioColors)[number];\r\n activeIcon?: string;\r\n inactiveIcon?: string;\r\n showIcon?: boolean;\r\n class?: any;\r\n ui?: Partial<{\r\n root: ClassValue;\r\n wrapper: ClassValue;\r\n activeIcon: ClassValue;\r\n inactiveIcon: ClassValue;\r\n label: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RadioProps>(), {\r\n disabled: false,\r\n size: \"md\",\r\n color: \"primary\",\r\n activeIcon: \"i-lucide-check\",\r\n inactiveIcon: \"\",\r\n showIcon: true,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: any): void;\r\n (e: \"change\", value: any): void;\r\n}>();\r\n\r\nconst slots = useSlots();\r\nconst attrs = useAttrs();\r\n\r\nconst isChecked = computed(() => props.modelValue === props.value);\r\nconst showLabel = computed(() => !!props.label || !!slots.default);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, uiOverrides.value.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, uiOverrides.value.inactiveIcon) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, uiOverrides.value.label) }),\r\n };\r\n});\r\n\r\nfunction onTap() {\r\n if (!props.disabled && !isChecked.value) {\r\n emit(\"update:modelValue\", props.value);\r\n emit(\"change\", props.value);\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: props.class })\" :data-disabled=\"disabled\" :data-checked=\"isChecked\" @click=\"onTap\">\r\n <div :class=\"ui.wrapper()\">\r\n <template v-if=\"showIcon\">\r\n <slot v-if=\"isChecked\" name=\"active-icon\">\r\n <div :class=\"ui.activeIcon()\">\r\n <Icon v-if=\"activeIcon\" :name=\"activeIcon\" class=\"size-full\" />\r\n </div>\r\n </slot>\r\n <slot v-else name=\"inactive-icon\">\r\n <div :class=\"ui.inactiveIcon()\">\r\n <Icon v-if=\"inactiveIcon\" :name=\"inactiveIcon\" class=\"size-full\" />\r\n </div>\r\n </slot>\r\n </template>\r\n\r\n <div v-if=\"showLabel\" :class=\"ui.label()\" :data-checked=\"isChecked\">\r\n <slot>{{ label }}</slot>\r\n </div>\r\n </div>\r\n </div>\r\n</template>\r\n",
|
|
1766
|
+
"target": "web"
|
|
1767
|
+
},
|
|
1768
|
+
{
|
|
1769
|
+
"path": "index.ts",
|
|
1770
|
+
"content": "export { default as RebornRadio } from './RebornRadio.vue'\r\nexport { default as RebornRadioGroup } from './RebornRadioGroup.vue'\r\n",
|
|
1771
|
+
"target": "uniapp"
|
|
1772
|
+
},
|
|
1773
|
+
{
|
|
1774
|
+
"path": "reborn-radio.config.ts",
|
|
1775
|
+
"content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-row items-center',\r\n wrapper: 'inline-flex items-center gap-2', // wrapper to align icon and label\r\n activeIcon: 'flex justify-center items-center border-2 border-gray-4 dark:border-gray-600 transition-all duration-200',\r\n inactiveIcon: 'border-2 border-gray-4 dark:border-gray-1 transition-all duration-200',\r\n label: 'text-sm text-gray-7 dark:text-gray-2',\r\n control: 'flex items-center justify-center rounded-full border border-gray-300 dark:border-gray-600 transition-colors',\r\n },\r\n variants: {\r\n color: {\r\n primary: {\r\n activeIcon: 'bg-gradient-to-br from-primary/80 to-primary border-transparent',\r\n label: 'data-[checked=true]:text-primary',\r\n },\r\n secondary: {\r\n activeIcon: 'bg-gradient-to-br from-secondary/80 to-secondary border-transparent',\r\n label: 'data-[checked=true]:text-secondary',\r\n },\r\n success: {\r\n activeIcon: 'bg-gradient-to-br from-success/80 to-success border-transparent',\r\n label: 'data-[checked=true]:text-success',\r\n },\r\n info: {\r\n activeIcon: 'bg-gradient-to-br from-info/80 to-info border-transparent',\r\n label: 'data-[checked=true]:text-info',\r\n },\r\n warning: {\r\n activeIcon: 'bg-gradient-to-br from-warning/80 to-warning border-transparent',\r\n label: 'data-[checked=true]:text-warning',\r\n },\r\n error: {\r\n activeIcon: 'bg-gradient-to-br from-error/80 to-error border-transparent',\r\n label: 'data-[checked=true]:text-error',\r\n },\r\n neutral: {\r\n activeIcon: 'bg-gradient-to-br from-neutral/80 to-neutral border-transparent',\r\n label: 'data-[checked=true]:text-neutral',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n activeIcon: 'w-[32rpx] h-[32rpx]',\r\n inactiveIcon: 'w-[32rpx] h-[32rpx]',\r\n label: 'text-24',\r\n },\r\n md: {\r\n activeIcon: 'w-[36rpx] h-[36rpx]',\r\n inactiveIcon: 'w-[36rpx] h-[36rpx]',\r\n label: 'text-26',\r\n },\r\n lg: {\r\n activeIcon: 'w-[40rpx] h-[40rpx]',\r\n inactiveIcon: 'w-[40rpx] h-[40rpx]',\r\n label: 'text-28',\r\n },\r\n },\r\n isRound: {\r\n true: {\r\n activeIcon: 'rounded-full text-white',\r\n inactiveIcon: 'rounded-full text-white',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none cursor-not-allowed',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n color: 'primary',\r\n isRound: true,\r\n },\r\n} as const\r\n\r\nexport { colors as radioColors, sizes as radioSizes }\r\n",
|
|
1776
|
+
"target": "uniapp"
|
|
1777
|
+
},
|
|
1778
|
+
{
|
|
1779
|
+
"path": "RebornRadio.vue",
|
|
1780
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { radioColors, radioSizes } from './reborn-radio.config'\r\nimport { computed, useSlots, inject } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-radio.config'\r\n\r\ndefineOptions({\r\n name: 'RebornRadio',\r\n})\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n disabled: false,\r\n size: 'md',\r\n color: 'primary',\r\n activeIcon: 'i-lucide-check',\r\n inactiveIcon: '',\r\n showIcon: true,\r\n isRound: true,\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\ninterface Props {\r\n modelValue?: any\r\n value?: any\r\n label?: string\r\n disabled?: boolean\r\n size?: typeof radioSizes[number]\r\n color?: typeof radioColors[number]\r\n activeIcon?: string\r\n inactiveIcon?: string\r\n showIcon?: boolean\r\n isRound?: boolean\r\n ui?: {\r\n root?: string\r\n wrapper?: string\r\n activeIcon?: string\r\n inactiveIcon?: string\r\n label?: string\r\n }\r\n customClass?: any\r\n}\r\n\r\nconst slots = useSlots()\r\n\r\nconst { disabled: formDisabled, size: formSize } = useFormInject(props)\r\nconst radioGroup = inject('RebornRadioGroup', null) as any\r\n\r\nconst isGroup = computed(() => !!radioGroup)\r\n\r\nconst modelValue = computed({\r\n get() {\r\n return isGroup.value ? radioGroup.modelValue.value : props.modelValue\r\n },\r\n set(val) {\r\n if (isGroup.value) {\r\n radioGroup.updateValue(val)\r\n } else {\r\n emit('update:modelValue', val)\r\n }\r\n }\r\n})\r\n\r\nconst isDisabled = computed(() => {\r\n return isGroup.value ? (radioGroup.disabled.value || props.disabled) : (props.disabled || formDisabled.value)\r\n})\r\n\r\nconst resolvedSize = computed(() => {\r\n return isGroup.value ? (radioGroup.size.value || props.size) : (props.size || formSize.value)\r\n})\r\n\r\nconst resolvedColor = computed(() => {\r\n return isGroup.value ? (radioGroup.color.value || props.color) : props.color\r\n})\r\n\r\nconst isChecked = computed(() => modelValue.value === props.value)\r\nconst showLabel = computed(() => !!props.label || !!slots.default)\r\n\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: resolvedSize.value,\r\n color: resolvedColor.value,\r\n disabled: isDisabled.value,\r\n isRound: props.isRound,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, props.ui?.root) }),\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, props.ui?.wrapper) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, props.ui?.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, props.ui?.inactiveIcon) }),\r\n label: (opts?: { class?: any }) => styles.label({ class: cn(opts?.class, props.ui?.label) }),\r\n }\r\n})\r\n\r\nfunction onTap() {\r\n if (!isDisabled.value && !isChecked.value) {\r\n if (isGroup.value) {\r\n radioGroup.updateValue(props.value)\r\n } else {\r\n emit('update:modelValue', props.value)\r\n emit('change', props.value)\r\n }\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: props.customClass })\" :data-disabled=\"isDisabled\" :data-checked=\"isChecked\"\r\n @tap=\"onTap\">\r\n <view :class=\"ui.wrapper()\">\r\n <template v-if=\"showIcon\">\r\n <slot v-if=\"isChecked\" name=\"active-icon\">\r\n <view :class=\"ui.activeIcon()\">\r\n <view :class=\"activeIcon\" />\r\n </view>\r\n </slot>\r\n <slot v-else name=\"inactive-icon\">\r\n <view :class=\"ui.inactiveIcon()\">\r\n <view :class=\"inactiveIcon\" />\r\n </view>\r\n </slot>\r\n </template>\r\n\r\n <view v-if=\"showLabel\" :class=\"ui.label()\" :data-checked=\"isChecked\">\r\n <slot :isChecked=\"isChecked\">{{ label }}</slot>\r\n </view>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1781
|
+
"target": "uniapp"
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
"path": "RebornRadioGroup.vue",
|
|
1785
|
+
"content": "<script setup lang=\"ts\">\r\nimport { provide, toRef } from 'vue'\r\nimport type { radioColors, radioSizes } from './reborn-radio.config'\r\n\r\ninterface Props {\r\n modelValue?: any\r\n disabled?: boolean\r\n size?: typeof radioSizes[number]\r\n color?: typeof radioColors[number]\r\n fill?: string // Support fill as an alias for color or custom color if needed, but for now map to color logic if it matches\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n disabled: false,\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\nconst updateValue = (value: any) => {\r\n emit('update:modelValue', value)\r\n emit('change', value)\r\n}\r\n\r\nprovide('RebornRadioGroup', {\r\n modelValue: toRef(props, 'modelValue'),\r\n disabled: toRef(props, 'disabled'),\r\n size: toRef(props, 'size'),\r\n color: toRef(props, 'color'),\r\n updateValue\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-radio-group flex flex-wrap gap-4\">\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
1786
|
+
"target": "uniapp"
|
|
1787
|
+
}
|
|
1788
|
+
]
|
|
1789
|
+
},
|
|
1790
|
+
{
|
|
1791
|
+
"name": "reborn-rate",
|
|
1792
|
+
"dependencies": [
|
|
1793
|
+
"clsx"
|
|
1794
|
+
],
|
|
1795
|
+
"files": [
|
|
1796
|
+
{
|
|
1797
|
+
"path": "index.ts",
|
|
1798
|
+
"content": "export { default as RebornRate } from \"./RebornRate.vue\";\r\n",
|
|
1799
|
+
"target": "web"
|
|
1800
|
+
},
|
|
1801
|
+
{
|
|
1802
|
+
"path": "reborn-rate.config.ts",
|
|
1803
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { color as rateColors, size as rateSizes };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex flex-row items-center gap-1\",\r\n star: \"relative cursor-pointer transition-all duration-200 ease-out\",\r\n icon: \"transition-all duration-200 ease-out dark:text-gray-2\",\r\n iconActive: \"transition-all duration-200 ease-out\",\r\n value: \"ml-1 font-medium tabular-nums dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n icon: \"size-4\",\r\n iconActive: \"size-4\",\r\n value: \"text-[length:var(--text-size-24)]\",\r\n },\r\n md: {\r\n icon: \"size-5\",\r\n iconActive: \"size-5\",\r\n value: \"text-[length:var(--text-size-28)]\",\r\n },\r\n lg: {\r\n icon: \"size-7\",\r\n iconActive: \"size-7\",\r\n value: \"text-[length:var(--text-size-32)]\",\r\n },\r\n },\r\n color: {\r\n primary: { iconActive: \"text-primary\" },\r\n secondary: { iconActive: \"text-secondary\" },\r\n success: { iconActive: \"text-success\" },\r\n info: { iconActive: \"text-info\" },\r\n warning: { iconActive: \"text-warning\" },\r\n error: { iconActive: \"text-error\" },\r\n neutral: { iconActive: \"text-neutral\" },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: \"opacity-50 pointer-events-none\",\r\n },\r\n },\r\n readonly: {\r\n true: {\r\n star: \"cursor-default active:scale-100\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"warning\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
1804
|
+
"target": "web"
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
"path": "RebornRate.vue",
|
|
1808
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from \"clsx\";\r\nimport type { rateColors, rateSizes } from \"./reborn-rate.config\";\r\nimport { computed, ref, watch } from \"vue\";\r\n\r\nimport { tv } from \"~/lib/tv\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme from \"./reborn-rate.config\";\r\n\r\ndefineOptions({\r\n name: \"RebornRate\",\r\n});\r\n\r\nconst props = withDefaults(defineProps<RateProps>(), {\r\n modelValue: 0,\r\n count: 5,\r\n allowHalf: false,\r\n showValue: false,\r\n disabled: false,\r\n readonly: false,\r\n icon: \"lucide:star\",\r\n activeIcon: \"lucide:star\",\r\n size: \"md\",\r\n color: \"warning\",\r\n ui: () => ({}),\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: number): void;\r\n (e: \"change\", value: number): void;\r\n}>();\r\n\r\nexport interface RateProps {\r\n /** 当前评分 */\r\n modelValue?: number;\r\n /** 星星总数 */\r\n count?: number;\r\n /** 允许半星 */\r\n allowHalf?: boolean;\r\n /** 显示分数 */\r\n showValue?: boolean;\r\n /** 是否禁用 */\r\n disabled?: boolean;\r\n /** 是否只读 */\r\n readonly?: boolean;\r\n /** 未选中图标 (Nuxt Icon name) */\r\n icon?: string;\r\n /** 选中图标 (Nuxt Icon name) */\r\n activeIcon?: string;\r\n /** 半星选中图标 (Nuxt Icon name) */\r\n halfIcon?: string;\r\n /** 尺寸 */\r\n size?: (typeof rateSizes)[number];\r\n /** 颜色 */\r\n color?: (typeof rateColors)[number];\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n star: ClassValue;\r\n icon: ClassValue;\r\n iconActive: ClassValue;\r\n value: ClassValue;\r\n }>;\r\n /** 自定义 class */\r\n class?: any;\r\n}\r\n\r\nconst isInteractive = computed(() => !props.disabled && !props.readonly);\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst b = tv(theme);\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size as any,\r\n color: props.color,\r\n disabled: props.disabled,\r\n readonly: props.readonly,\r\n });\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n star: (opts?: { class?: any }) =>\r\n styles.star({ class: cn(opts?.class, uiOverrides.value.star) }),\r\n icon: (opts?: { class?: any }) =>\r\n styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n iconActive: (opts?: { class?: any }) =>\r\n styles.iconActive({ class: cn(opts?.class, uiOverrides.value.iconActive) }),\r\n value: (opts?: { class?: any }) =>\r\n styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n };\r\n});\r\n\r\nfunction getActiveIcon(index: number) {\r\n if (isHalf(index)) {\r\n return props.halfIcon ?? props.activeIcon;\r\n }\r\n return props.activeIcon;\r\n}\r\n\r\n// 当前评分\r\nconst currentValue = ref<number>(props.modelValue);\r\n\r\n// 判断当前星星是否为半星状态\r\nfunction isHalf(index: number): boolean {\r\n return props.allowHalf && currentValue.value >= index - 0.5 && currentValue.value < index;\r\n}\r\n\r\n// 点击事件(Web 版:用鼠标位置判断半星)\r\nfunction onClick(e: MouseEvent, index: number) {\r\n if (!isInteractive.value) {\r\n return;\r\n }\r\n\r\n if (props.allowHalf) {\r\n const target = e.currentTarget as HTMLElement;\r\n const rect = target.getBoundingClientRect();\r\n const midX = rect.left + rect.width / 2;\r\n const newValue = e.clientX < midX ? index - 0.5 : index;\r\n\r\n if (currentValue.value === newValue) {\r\n updateValue(0);\r\n } else {\r\n updateValue(newValue);\r\n }\r\n } else {\r\n // 点击同一个星星则清零\r\n if (currentValue.value === index) {\r\n updateValue(0);\r\n } else {\r\n updateValue(index);\r\n }\r\n }\r\n}\r\n\r\n// Web 增强:鼠标移动 hover 预览\r\nconst hoverValue = ref(-1);\r\nlet moveRaf = 0;\r\n\r\nconst displayValue = computed(() =>\r\n hoverValue.value >= 0 ? hoverValue.value : currentValue.value,\r\n);\r\n\r\nfunction isHalfDisplay(index: number): boolean {\r\n return props.allowHalf && displayValue.value >= index - 0.5 && displayValue.value < index;\r\n}\r\n\r\nfunction isActiveDisplay(index: number): boolean {\r\n return (\r\n displayValue.value >= index - (props.allowHalf ? 0.5 : 0) && displayValue.value >= index - 0.5\r\n );\r\n}\r\n\r\nfunction onMouseMove(e: MouseEvent) {\r\n if (!isInteractive.value) {\r\n return;\r\n }\r\n if (moveRaf) cancelAnimationFrame(moveRaf);\r\n\r\n const x = e.clientX;\r\n const wrapper = e.currentTarget as HTMLElement;\r\n\r\n moveRaf = requestAnimationFrame(() => {\r\n const stars = wrapper.querySelectorAll(\".reborn-rate__star\");\r\n for (let i = stars.length - 1; i >= 0; i--) {\r\n const rect = (stars[i] as HTMLElement).getBoundingClientRect();\r\n if (x >= rect.left) {\r\n if (props.allowHalf) {\r\n const midX = rect.left + rect.width / 2;\r\n hoverValue.value = x < midX ? i + 0.5 : i + 1;\r\n } else {\r\n hoverValue.value = i + 1;\r\n }\r\n return;\r\n }\r\n }\r\n hoverValue.value = 0;\r\n });\r\n}\r\n\r\nfunction onMouseLeave() {\r\n if (moveRaf) cancelAnimationFrame(moveRaf);\r\n hoverValue.value = -1;\r\n}\r\n\r\n// 更新值\r\nfunction updateValue(newValue: number) {\r\n if (currentValue.value !== newValue) {\r\n currentValue.value = newValue;\r\n emit(\"update:modelValue\", newValue);\r\n emit(\"change\", newValue);\r\n }\r\n}\r\n\r\n// 同步外部值\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== currentValue.value) {\r\n currentValue.value = Math.max(0, Math.min(props.count, val));\r\n }\r\n },\r\n { immediate: true },\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.wrapper({ class: props.class })\" @mousemove=\"onMouseMove\" @mouseleave=\"onMouseLeave\">\r\n <div v-for=\"index in count\" :key=\"index\" class=\"reborn-rate__star\" :class=\"ui.star()\"\r\n @click=\"onClick($event, index)\">\r\n <!-- 未激活图标 -->\r\n <div :class=\"ui.icon()\" class=\"opacity-30\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"false\">\r\n <Icon :name=\"props.icon\" class=\"size-full\" />\r\n </slot>\r\n </div>\r\n\r\n <!-- 激活图标(整星 / 半星) -->\r\n <div v-if=\"isActiveDisplay(index)\" class=\"absolute inset-0\" :class=\"ui.iconActive()\"\r\n :style=\"isHalfDisplay(index) ? { clipPath: 'polygon(0 0, 50% 0, 50% 100%, 0 100%)' } : {}\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"true\">\r\n <Icon :name=\"getActiveIcon(index)\" class=\"size-full\" />\r\n </slot>\r\n </div>\r\n </div>\r\n\r\n <!-- 分数显示 -->\r\n <slot name=\"value\" :value=\"currentValue\">\r\n <span v-if=\"showValue\" :class=\"ui.value()\">{{ currentValue }}</span>\r\n </slot>\r\n </div>\r\n</template>\r\n",
|
|
1809
|
+
"target": "web"
|
|
1810
|
+
},
|
|
1811
|
+
{
|
|
1812
|
+
"path": "index.ts",
|
|
1813
|
+
"content": "export { default as RebornRate } from './RebornRate.vue'\r\n",
|
|
1814
|
+
"target": "uniapp"
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
"path": "reborn-rate.config.ts",
|
|
1818
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as rateColors, size as rateSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper: 'inline-flex flex-row items-center gap-1',\r\n star: 'relative cursor-pointer transition-transform active:scale-90',\r\n icon: 'transition-colors duration-150 dark:text-gray-2',\r\n iconActive: 'transition-colors duration-150',\r\n value: 'ml-1 font-medium tabular-nums dark:text-gray-1',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n icon: 'size-4',\r\n iconActive: 'size-4',\r\n value: 'text-24',\r\n },\r\n md: {\r\n icon: 'size-5',\r\n iconActive: 'size-5',\r\n value: 'text-28',\r\n },\r\n lg: {\r\n icon: 'size-7',\r\n iconActive: 'size-7',\r\n value: 'text-32',\r\n },\r\n },\r\n color: {\r\n primary: { iconActive: 'text-primary' },\r\n secondary: { iconActive: 'text-secondary' },\r\n success: { iconActive: 'text-success' },\r\n info: { iconActive: 'text-info' },\r\n warning: { iconActive: 'text-warning' },\r\n error: { iconActive: 'text-error' },\r\n neutral: { iconActive: 'text-neutral' },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: 'opacity-50 pointer-events-none',\r\n },\r\n },\r\n readonly: {\r\n true: {\r\n star: 'cursor-default active:scale-100',\r\n },\r\n },\r\n allowHalf: {\r\n true: {},\r\n false: {},\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n size: 'sm',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[30rpx]',\r\n },\r\n },\r\n {\r\n size: 'md',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[40rpx]',\r\n },\r\n },\r\n {\r\n size: 'lg',\r\n allowHalf: true,\r\n class: {\r\n value: '!w-[40rpx]',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as (typeof size)[number],\r\n color: 'warning' as (typeof color)[number],\r\n allowHalf: false,\r\n },\r\n}\r\n",
|
|
1819
|
+
"target": "uniapp"
|
|
1820
|
+
},
|
|
1821
|
+
{
|
|
1822
|
+
"path": "RebornRate.vue",
|
|
1823
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { rateColors, rateSizes } from './reborn-rate.config'\r\nimport { computed, getCurrentInstance, ref, watch } from 'vue'\r\n\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-rate.config'\r\n\r\ndefineOptions({\r\n name: 'RebornRate',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RateProps>(), {\r\n modelValue: 0,\r\n count: 5,\r\n allowHalf: false,\r\n showValue: false,\r\n disabled: false,\r\n readonly: false,\r\n icon: 'i-lucide-star',\r\n activeIcon: 'i-lucide-star',\r\n size: 'md',\r\n color: 'warning',\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: number): void\r\n (e: 'change', value: number): void\r\n}>()\r\n\r\nexport interface RateProps {\r\n /** 当前评分 */\r\n modelValue?: number\r\n /** 星星总数 */\r\n count?: number\r\n /** 允许半星 */\r\n allowHalf?: boolean\r\n /** 显示分数 */\r\n showValue?: boolean\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 是否只读 */\r\n readonly?: boolean\r\n /** 未选中图标 class */\r\n icon?: string\r\n /** 选中图标 class */\r\n activeIcon?: string\r\n /** 半星选中图标 class */\r\n halfIcon?: string\r\n /** 尺寸 */\r\n size?: (typeof rateSizes)[number]\r\n /** 颜色 */\r\n color?: (typeof rateColors)[number]\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n star: ClassValue\r\n icon: ClassValue\r\n iconActive: ClassValue\r\n value: ClassValue\r\n }>\r\n /** 自定义 class */\r\n customClass?: any\r\n}\r\n\r\nconst { proxy } = getCurrentInstance()!\r\n\r\n// reborn-form 上下文\r\nconst { disabled, size } = useFormInject(props)\r\n\r\nconst isInteractive = computed(\r\n () => !disabled.value && !props.readonly,\r\n)\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (size.value || props.size) as any,\r\n color: props.color,\r\n disabled: disabled.value,\r\n readonly: props.readonly,\r\n allowHalf: props.allowHalf,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n star: (opts?: { class?: any }) =>\r\n styles.star({ class: cn(opts?.class, uiOverrides.value.star) }),\r\n icon: (opts?: { class?: any }) =>\r\n styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n iconActive: (opts?: { class?: any }) =>\r\n styles.iconActive({ class: cn(opts?.class, uiOverrides.value.iconActive) }),\r\n value: (opts?: { class?: any }) =>\r\n styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n }\r\n})\r\n\r\nfunction activeIcon(index: number) {\r\n if (isHalf(index)) {\r\n return props.halfIcon ?? props.activeIcon\r\n }\r\n return props.activeIcon\r\n}\r\n\r\n// 当前评分\r\nconst currentValue = ref<number>(props.modelValue)\r\n\r\n// 判断当前星星是否为半星状态\r\nfunction isHalf(index: number): boolean {\r\n return props.allowHalf && currentValue.value >= index - 0.5 && currentValue.value < index\r\n}\r\n\r\n// 防止 touchstart 和 tap 重复触发\r\nlet touchHandled = false\r\n\r\n// 点击事件\r\nfunction onTap(index: number) {\r\n if (!isInteractive.value) { return }\r\n\r\n // 如果 touchStart 已处理(半星模式),跳过 tap\r\n if (touchHandled) {\r\n touchHandled = false\r\n return\r\n }\r\n\r\n let newValue = index\r\n\r\n if (props.allowHalf) {\r\n if (currentValue.value === index) {\r\n newValue = index - 0.5\r\n }\r\n else if (currentValue.value === index - 0.5) {\r\n newValue = 0\r\n }\r\n }\r\n else {\r\n // 点击同一个星星则清零\r\n if (currentValue.value === index) {\r\n newValue = 0\r\n }\r\n }\r\n\r\n updateValue(newValue)\r\n}\r\n\r\n// 触摸开始 — 用于半星判断\r\nfunction onTouchStart(e: TouchEvent, index: number) {\r\n if (!isInteractive.value || !props.allowHalf) { return }\r\n\r\n touchHandled = true\r\n\r\n const touch = e.touches[0]\r\n if (!touch) { return }\r\n\r\n // 获取当前星星的位置进行左/右判断\r\n getStarRect(index - 1).then((rect) => {\r\n if (!rect) { return }\r\n const midX = rect.left + rect.width / 2\r\n const newValue = touch.clientX < midX ? index - 0.5 : index\r\n updateValue(newValue)\r\n })\r\n}\r\n\r\n// 触摸移动 — 拖动评分\r\nfunction onTouchMove(e: TouchEvent, index: number) {\r\n if (!isInteractive.value) { return }\r\n\r\n const touch = e.touches[0]\r\n if (!touch) { return }\r\n\r\n // 通过触摸位置计算哪个星星\r\n getAllStarRects().then((rects) => {\r\n if (!rects || rects.length === 0) { return }\r\n\r\n for (let i = rects.length - 1; i >= 0; i--) {\r\n const rect = rects[i]\r\n if (touch.clientX >= rect.left) {\r\n if (props.allowHalf) {\r\n const midX = rect.left + rect.width / 2\r\n const newValue = touch.clientX < midX ? i + 0.5 : i + 1\r\n updateValue(newValue)\r\n }\r\n else {\r\n updateValue(i + 1)\r\n }\r\n return\r\n }\r\n }\r\n\r\n // 在所有星星左边\r\n updateValue(0)\r\n })\r\n}\r\n\r\n// 更新值\r\nfunction updateValue(newValue: number) {\r\n if (currentValue.value !== newValue) {\r\n currentValue.value = newValue\r\n emit('update:modelValue', newValue)\r\n emit('change', newValue)\r\n }\r\n}\r\n\r\n// 获取单个星星的矩形信息\r\nfunction getStarRect(index: number): Promise<any> {\r\n return new Promise((resolve) => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .selectAll('.reborn-rate__star')\r\n .boundingClientRect((nodes: any) => {\r\n resolve(nodes?.[index] ?? null)\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\n// 获取所有星星的矩形信息\r\nfunction getAllStarRects(): Promise<any[]> {\r\n return new Promise((resolve) => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .selectAll('.reborn-rate__star')\r\n .boundingClientRect((nodes: any) => {\r\n resolve(nodes ?? [])\r\n })\r\n .exec()\r\n })\r\n}\r\n\r\n// 同步外部值\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== currentValue.value) {\r\n currentValue.value = Math.max(0, Math.min(props.count, val))\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\">\r\n <view v-for=\"index in count\" :key=\"index\" class=\"reborn-rate__star\" :class=\"ui.star()\" @tap=\"onTap(index)\"\r\n @touchstart=\"onTouchStart($event, index)\" @touchmove=\"onTouchMove($event, index)\">\r\n <!-- 未激活图标 -->\r\n <view :class=\"ui.icon()\" class=\"opacity-30\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"false\">\r\n <view :class=\"props.icon\" class=\"size-full\" />\r\n </slot>\r\n </view>\r\n\r\n <!-- 激活图标(整星 / 半星) -->\r\n <view v-if=\"currentValue >= index - (allowHalf ? 0.5 : 0) && currentValue >= index - 0.5\" class=\"absolute inset-0\"\r\n :class=\"[ui.iconActive(), isHalf(index) && `\r\n w-1/2 overflow-hidden\r\n `]\">\r\n <slot name=\"icon\" :index=\"index\" :active=\"true\" :style=\"isHalf(index) ? { width: '200%' } : {}\">\r\n <view :class=\"activeIcon(index)\" class=\"size-full\" :style=\"isHalf(index) ? { width: '200%' } : {}\" />\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <!-- 分数显示 -->\r\n <slot name=\"value\" :value=\"currentValue\">\r\n <view v-if=\"showValue\" :class=\"ui.value()\">{{ currentValue }}</view>\r\n </slot>\r\n </view>\r\n</template>\r\n",
|
|
1824
|
+
"target": "uniapp"
|
|
1825
|
+
}
|
|
1826
|
+
]
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
"name": "reborn-root-portal",
|
|
1830
|
+
"dependencies": [],
|
|
1831
|
+
"files": [
|
|
1832
|
+
{
|
|
1833
|
+
"path": "index.ts",
|
|
1834
|
+
"content": "import RebornRootPortal from './RebornRootPortal.vue'\r\nimport config from './reborn-root-portal.config'\r\n\r\nexport { RebornRootPortal, config }\r\nexport default RebornRootPortal\r\n"
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
"path": "reborn-root-portal.config.ts",
|
|
1838
|
+
"content": "const config = {\r\n slots: {\r\n base: 'fixed inset-0 pointer-events-none z-[999]',\r\n content: 'pointer-events-auto'\r\n }\r\n} as const\r\n\r\nexport default config\r\n"
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
"path": "RebornRootPortal.vue",
|
|
1842
|
+
"content": "<template>\r\n <teleport to=\"body\">\r\n <slot />\r\n </teleport>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\n// RebornRootPortal Web Implementation\r\n</script>\r\n",
|
|
1843
|
+
"target": "web"
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
"path": "RebornRootPortal.vue",
|
|
1847
|
+
"content": "<script lang=\"ts\">\nexport default {\n name: 'reborn-root-portal',\n options: {\n virtualHost: true,\n addGlobalClass: true,\n styleIsolation: 'shared'\n }\n}\n</script>\n\n<script lang=\"ts\" setup>\n</script>\n\n<template>\n <!-- #ifdef H5 -->\n <!-- H5端使用 teleport -->\n <teleport to=\"body\">\n <!-- #endif -->\n <!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n <!-- #ifndef MP-DINGTALK -->\n <!-- 小程序使用 root-portal -->\n <root-portal>\n <!-- #endif -->\n <!-- #endif -->\n <view>\n <slot />\n </view>\n <!-- #ifdef MP-WEIXIN || MP-ALIPAY -->\n <!-- #ifndef MP-DINGTALK -->\n </root-portal>\n <!-- #endif -->\n <!-- #endif -->\n <!-- #ifdef H5 -->\n </teleport>\n <!-- #endif -->\n</template>\n\n<!-- #ifdef APP-PLUS -->\n<script module=\"render\" lang=\"renderjs\">\nexport default {\n mounted() {\n if (this.$ownerInstance.$el) {\n (document.querySelector('uni-app') || document.body).appendChild(this.$ownerInstance.$el)\n }\n },\n beforeDestroy() {\n if (this.$ownerInstance.$el) {\n (document.querySelector('uni-app') || document.body).removeChild(this.$ownerInstance.$el)\n }\n }\n}\n</script>\n<!-- #endif -->\n",
|
|
1848
|
+
"target": "uniapp"
|
|
1849
|
+
}
|
|
1850
|
+
]
|
|
1851
|
+
},
|
|
1852
|
+
{
|
|
1853
|
+
"name": "reborn-select",
|
|
1854
|
+
"dependencies": [
|
|
1855
|
+
"clsx",
|
|
1856
|
+
"lodash-es"
|
|
1857
|
+
],
|
|
1858
|
+
"files": [
|
|
1859
|
+
{
|
|
1860
|
+
"path": "index.ts",
|
|
1861
|
+
"content": "export { default as RebornSelect } from \"./RebornSelect.vue\";\r\n",
|
|
1862
|
+
"target": "web"
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
"path": "reborn-select.config.ts",
|
|
1866
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as selectSizes, colors as selectColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"relative inline-flex w-full group outline-none\",\r\n trigger:\r\n \"flex w-full items-center justify-between rounded-lg border border-gray-3 dark:border-gray-6 bg-gray-1 dark:bg-gray-8 transition-colors cursor-pointer select-none outline-none\",\r\n triggerText: \"truncate text-gray-8 dark:text-gray-1\",\r\n placeholder: \"text-gray-4 dark:text-gray-5\",\r\n arrow: \"transition-transform duration-200 text-gray-4 shrink-0\",\r\n dropdown:\r\n \"absolute z-50 mt-1 w-full rounded-lg border border-gray-2 dark:border-gray-7 bg-white dark:bg-gray-8 shadow-lg overflow-auto\",\r\n option:\r\n \"flex items-center cursor-pointer transition-colors text-gray-7 dark:text-gray-2\",\r\n optionActive: \"\",\r\n clearBtn: \"shrink-0 text-gray-4 hover:text-gray-6 dark:hover:text-gray-3 cursor-pointer\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n trigger: \"h-8 px-2 text-xs gap-1\",\r\n option: \"px-2 py-1.5 text-xs\",\r\n arrow: \"size-3\",\r\n clearBtn: \"size-3\",\r\n },\r\n md: {\r\n trigger: \"h-10 px-3 text-sm gap-2\",\r\n option: \"px-3 py-2 text-sm\",\r\n arrow: \"size-4\",\r\n clearBtn: \"size-4\",\r\n },\r\n lg: {\r\n trigger: \"h-12 px-4 text-base gap-2\",\r\n option: \"px-4 py-2.5 text-base\",\r\n arrow: \"size-5\",\r\n clearBtn: \"size-5\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n trigger: \"group-focus:border-primary group-focus:ring-2 group-focus:ring-primary/20 data-[state=open]:border-primary data-[state=open]:ring-2 data-[state=open]:ring-primary/20\",\r\n optionActive: \"bg-primary/10 text-primary\",\r\n },\r\n secondary: {\r\n trigger: \"group-focus:border-secondary group-focus:ring-2 group-focus:ring-secondary/20 data-[state=open]:border-secondary data-[state=open]:ring-2 data-[state=open]:ring-secondary/20\",\r\n optionActive: \"bg-secondary/10 text-secondary\",\r\n },\r\n success: {\r\n trigger: \"group-focus:border-success group-focus:ring-2 group-focus:ring-success/20 data-[state=open]:border-success data-[state=open]:ring-2 data-[state=open]:ring-success/20\",\r\n optionActive: \"bg-success/10 text-success\",\r\n },\r\n info: {\r\n trigger: \"group-focus:border-info group-focus:ring-2 group-focus:ring-info/20 data-[state=open]:border-info data-[state=open]:ring-2 data-[state=open]:ring-info/20\",\r\n optionActive: \"bg-info/10 text-info\",\r\n },\r\n warning: {\r\n trigger: \"group-focus:border-warning group-focus:ring-2 group-focus:ring-warning/20 data-[state=open]:border-warning data-[state=open]:ring-2 data-[state=open]:ring-warning/20\",\r\n optionActive: \"bg-warning/10 text-warning\",\r\n },\r\n error: {\r\n trigger: \"group-focus:border-error group-focus:ring-2 group-focus:ring-error/20 data-[state=open]:border-error data-[state=open]:ring-2 data-[state=open]:ring-error/20\",\r\n optionActive: \"bg-error/10 text-error\",\r\n },\r\n neutral: {\r\n trigger: \"group-focus:border-neutral group-focus:ring-2 group-focus:ring-neutral/20 data-[state=open]:border-neutral data-[state=open]:ring-2 data-[state=open]:ring-neutral/20\",\r\n optionActive: \"bg-neutral/10 text-neutral\",\r\n },\r\n },\r\n open: {\r\n true: { arrow: \"rotate-180\" },\r\n },\r\n disabled: {\r\n true: {\r\n trigger: \"opacity-50 pointer-events-none cursor-not-allowed bg-gray-50 dark:bg-gray-900\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
1867
|
+
"target": "web"
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
"path": "RebornSelect.vue",
|
|
1871
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { selectColors, selectSizes } from \"./reborn-select.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface SelectOption {\r\n label: string;\r\n value: any;\r\n disabled?: boolean;\r\n [key: string]: any;\r\n}\r\n\r\nexport interface SelectProps {\r\n modelValue?: any;\r\n options?: SelectOption[];\r\n placeholder?: string;\r\n disabled?: boolean;\r\n clearable?: boolean;\r\n size?: (typeof selectSizes)[number];\r\n color?: (typeof selectColors)[number];\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n trigger: ClassValue;\r\n triggerText: ClassValue;\r\n placeholder: ClassValue;\r\n arrow: ClassValue;\r\n dropdown: ClassValue;\r\n option: ClassValue;\r\n optionActive: ClassValue;\r\n clearBtn: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<SelectProps>(), {\r\n modelValue: null,\r\n options: () => [],\r\n placeholder: \"请选择\",\r\n disabled: false,\r\n clearable: true,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: any): void;\r\n (e: \"change\", value: any): void;\r\n}>();\r\n\r\nconst isOpen = ref(false);\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\nconst dropdownRef = ref<HTMLElement | null>(null);\r\nconst highlightIndex = ref(-1);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n open: isOpen.value,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n trigger: (opts?: { class?: any }) => styles.trigger({ class: cn(opts?.class, uiOverrides.value.trigger) }),\r\n triggerText: (opts?: { class?: any }) => styles.triggerText({ class: cn(opts?.class, uiOverrides.value.triggerText) }),\r\n placeholder: (opts?: { class?: any }) => styles.placeholder({ class: cn(opts?.class, uiOverrides.value.placeholder) }),\r\n arrow: (opts?: { class?: any }) => styles.arrow({ class: cn(opts?.class, uiOverrides.value.arrow) }),\r\n dropdown: (opts?: { class?: any }) => styles.dropdown({ class: cn(opts?.class, uiOverrides.value.dropdown) }),\r\n option: (opts?: { class?: any }) => styles.option({ class: cn(opts?.class, uiOverrides.value.option) }),\r\n optionActive: (opts?: { class?: any }) => styles.optionActive({ class: cn(opts?.class, uiOverrides.value.optionActive) }),\r\n clearBtn: (opts?: { class?: any }) => styles.clearBtn({ class: cn(opts?.class, uiOverrides.value.clearBtn) }),\r\n };\r\n});\r\n\r\nconst selectedOption = computed(() =>\r\n props.options.find((o) => o.value === props.modelValue) ?? null,\r\n);\r\n\r\nconst displayText = computed(() => selectedOption.value?.label ?? \"\");\r\n\r\nfunction toggle() {\r\n if (props.disabled) return;\r\n isOpen.value = !isOpen.value;\r\n if (isOpen.value) {\r\n highlightIndex.value = props.options.findIndex((o) => o.value === props.modelValue);\r\n nextTick(() => scrollToActive());\r\n }\r\n}\r\n\r\nfunction selectOption(option: SelectOption) {\r\n if (option.disabled) return;\r\n emit(\"update:modelValue\", option.value);\r\n emit(\"change\", option.value);\r\n isOpen.value = false;\r\n}\r\n\r\nfunction clear(e: Event) {\r\n e.stopPropagation();\r\n emit(\"update:modelValue\", null);\r\n emit(\"change\", null);\r\n}\r\n\r\nfunction onClickOutside(e: MouseEvent) {\r\n if (wrapperRef.value && !wrapperRef.value.contains(e.target as Node)) {\r\n isOpen.value = false;\r\n }\r\n}\r\n\r\nfunction onKeydown(e: KeyboardEvent) {\r\n if (!isOpen.value) {\r\n if (e.key === \"ArrowDown\" || e.key === \"Enter\" || e.key === \" \") {\r\n e.preventDefault();\r\n toggle();\r\n }\r\n return;\r\n }\r\n\r\n switch (e.key) {\r\n case \"ArrowDown\":\r\n e.preventDefault();\r\n highlightIndex.value = Math.min(highlightIndex.value + 1, props.options.length - 1);\r\n break;\r\n case \"ArrowUp\":\r\n e.preventDefault();\r\n highlightIndex.value = Math.max(highlightIndex.value - 1, 0);\r\n break;\r\n case \"Enter\":\r\n case \" \":\r\n e.preventDefault();\r\n if (highlightIndex.value >= 0 && highlightIndex.value < props.options.length) {\r\n const opt = props.options[highlightIndex.value];\r\n if (opt) selectOption(opt);\r\n }\r\n break;\r\n case \"Escape\":\r\n e.preventDefault();\r\n isOpen.value = false;\r\n break;\r\n }\r\n}\r\n\r\nfunction scrollToActive() {\r\n if (dropdownRef.value && highlightIndex.value >= 0) {\r\n const el = dropdownRef.value.children[highlightIndex.value] as HTMLElement;\r\n el?.scrollIntoView?.({ block: \"nearest\" });\r\n }\r\n}\r\n\r\nwatch(highlightIndex, () => nextTick(scrollToActive));\r\n\r\nonMounted(() => document.addEventListener(\"click\", onClickOutside));\r\nonBeforeUnmount(() => document.removeEventListener(\"click\", onClickOutside));\r\n</script>\r\n\r\n<template>\r\n <div ref=\"wrapperRef\" :class=\"ui.wrapper({ class: props.class })\" @keydown=\"onKeydown\" tabindex=\"0\">\r\n <div :class=\"ui.trigger()\" @click=\"toggle\" :data-state=\"isOpen ? 'open' : 'closed'\">\r\n <span v-if=\"displayText\" :class=\"ui.triggerText()\">{{ displayText }}</span>\r\n <span v-else :class=\"ui.placeholder()\">{{ placeholder }}</span>\r\n\r\n <div class=\"flex items-center gap-1\">\r\n <span v-if=\"clearable && modelValue != null\" :class=\"ui.clearBtn()\" @click=\"clear\">\r\n <Icon name=\"lucide:x\" class=\"size-full\" />\r\n </span>\r\n <Icon v-else name=\"lucide:chevron-down\" :class=\"ui.arrow()\" />\r\n </div>\r\n </div>\r\n\r\n <Transition enter-active-class=\"transition duration-150 ease-out\" enter-from-class=\"opacity-0 -translate-y-1\"\r\n enter-to-class=\"opacity-100 translate-y-0\" leave-active-class=\"transition duration-100 ease-in\"\r\n leave-from-class=\"opacity-100 translate-y-0\" leave-to-class=\"opacity-0 -translate-y-1\">\r\n <div v-if=\"isOpen\" ref=\"dropdownRef\" :class=\"ui.dropdown()\" style=\"max-height: 240px; top: 100%\">\r\n <div v-for=\"(option, index) in options\" :key=\"index\" :class=\"[\r\n ui.option(),\r\n option.value === modelValue ? ui.optionActive() : '',\r\n highlightIndex === index ? 'bg-gray-100 dark:bg-gray-700/50' : '',\r\n option.disabled ? 'opacity-50 pointer-events-none' : 'hover:bg-gray-50 dark:hover:bg-gray-700/30',\r\n ]\" @click=\"selectOption(option)\" @mouseenter=\"highlightIndex = index\">\r\n <slot name=\"option\" :option=\"option\" :active=\"option.value === modelValue\">\r\n {{ option.label }}\r\n </slot>\r\n </div>\r\n\r\n <div v-if=\"options.length === 0\" class=\"flex items-center justify-center py-6 text-sm text-gray-400\">\r\n 暂无数据\r\n </div>\r\n </div>\r\n </Transition>\r\n </div>\r\n</template>\r\n",
|
|
1872
|
+
"target": "web"
|
|
1873
|
+
},
|
|
1874
|
+
{
|
|
1875
|
+
"path": "index.ts",
|
|
1876
|
+
"content": "export { default as RebornSelect } from './RebornSelect.vue'\r\nexport type { SelectProps, SelectValue } from './RebornSelect.vue'\r\n",
|
|
1877
|
+
"target": "uniapp"
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
"path": "reborn-select.config.ts",
|
|
1881
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as selectColors, size as selectSizes }\r\n\r\nconst config = {\r\n slots: {\r\n empty: 'py-3 text-center text-gray-400 text-sm',\r\n emptyText: 'text-gray-400 text-sm',\r\n buttons: 'flex flex-row items-center justify-center gap-2 p-3',\r\n cancel: 'flex-1 ',\r\n cancelButton: 'w-full',\r\n confirm: 'flex-1 ',\r\n confirmButton: 'w-full',\r\n },\r\n variants: {\r\n hideButtons: {\r\n true: {\r\n buttons: 'hidden',\r\n },\r\n },\r\n },\r\n}\r\n\r\nexport default config",
|
|
1882
|
+
"target": "uniapp"
|
|
1883
|
+
},
|
|
1884
|
+
{
|
|
1885
|
+
"path": "RebornSelect.vue",
|
|
1886
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { SelectOption } from '../reborn-picker-view/RebornPickerView.vue'\r\nimport theme, { type selectColors, type selectSizes } from './reborn-select.config'\r\n\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\n\r\nimport { isEmpty, isNull } from 'lodash-es'\r\nimport { computed, onMounted, ref, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\n\r\nimport RebornButton from '../reborn-button/RebornButton.vue'\r\nimport RebornPickerView from '../reborn-picker-view/RebornPickerView.vue'\r\nimport RebornPopup from '../reborn-popup/RebornPopup.vue'\r\nimport RebornSelectTrigger from '../reborn-select-trigger/RebornSelectTrigger.vue'\r\n\r\ndefineOptions({\r\n name: 'RebornSelect',\r\n})\r\n\r\nconst props = withDefaults(defineProps<SelectProps>(), {\r\n modelValue: null,\r\n title: '请选择',\r\n placeholder: '请选择',\r\n options: () => [],\r\n showTrigger: true,\r\n disabled: false,\r\n columnCount: 1,\r\n splitor: ' - ',\r\n confirmText: '确定',\r\n showConfirm: true,\r\n cancelText: '取消',\r\n showCancel: true,\r\n clearable: true,\r\n color: 'primary',\r\n size: 'lg',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: SelectValue): void\r\n (e: 'change', value: SelectValue, select: any): void\r\n (e: 'changing', value: SelectValue): void\r\n}>()\r\n\r\ndefineSlots<{\r\n tag: (props: { selectItem: any[] }) => any\r\n prepend: () => any\r\n append: () => any\r\n option: (props: { item: SelectOption, index: number }) => any\r\n empty: () => any\r\n}>()\r\n\r\nexport type SelectValue = string | number | (string | number)[] | null\r\n\r\nexport interface SelectProps {\r\n /** 选择器的值 */\r\n modelValue?: SelectValue\r\n /** 标题 */\r\n title?: string\r\n /** 占位符 */\r\n placeholder?: string\r\n /** 选项数据 */\r\n options?: SelectOption[]\r\n /** 是否显示触发器 */\r\n showTrigger?: boolean\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 列数 */\r\n columnCount?: number\r\n /** 分隔符 */\r\n splitor?: string\r\n /** 确认按钮文本 */\r\n confirmText?: string\r\n /** 是否显示确认按钮 */\r\n showConfirm?: boolean\r\n /** 取消按钮文本 */\r\n cancelText?: string\r\n /** 是否显示取消按钮 */\r\n showCancel?: boolean\r\n /** 是否显示清空按钮 */\r\n clearable?: boolean\r\n /** 颜色 */\r\n color?: typeof selectColors[number]\r\n /** 尺寸 */\r\n size?: typeof selectSizes[number]\r\n ui?: Partial<{\r\n empty: ClassValue\r\n buttons: ClassValue\r\n emptyText: ClassValue\r\n cancel: ClassValue\r\n cancelButton: ClassValue\r\n confirm: ClassValue\r\n confirmButton: ClassValue\r\n }>,\r\n /** 样式覆盖 */\r\n triggerUi?: Partial<{\r\n wrapper: ClassValue\r\n content: ClassValue\r\n text: ClassValue\r\n placeholder: ClassValue\r\n iconWrapper: ClassValue\r\n clearIcon: ClassValue\r\n arrowIcon: ClassValue\r\n }>\r\n popupUi?: Partial<{\r\n wrapper: ClassValue\r\n mask: ClassValue\r\n popup: ClassValue\r\n inner: ClassValue\r\n draw: ClassValue\r\n header: ClassValue\r\n title: ClassValue\r\n container: ClassValue\r\n }>\r\n pickerUi?: Partial<{\r\n wrapper: ClassValue\r\n header: ClassValue\r\n headerText: ClassValue\r\n pickerContainer: ClassValue\r\n item: ClassValue\r\n itemText: ClassValue\r\n indicator: ClassValue\r\n }>\r\n}\r\n\r\n// reborn-form 上下文\r\nconst { disabled, validate } = useFormInject(props)\r\nconst isDisabled = computed(() => disabled.value || props.disabled)\r\n\r\n// 弹出层引用\r\nconst popupRef = ref<any>(null)\r\n\r\n// 是否为空选项\r\nconst noOptions = computed(() => {\r\n return isEmpty(props.options)\r\n})\r\n\r\n// ui 样式系统\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n hideButtons: !props.showCancel && !props.showConfirm,\r\n })\r\n\r\n return {\r\n buttons: (opts?: { class?: any }) => styles.buttons({ class: cn(opts?.class, props.ui?.buttons) }),\r\n empty: (opts?: { class?: any }) => styles.empty({ class: cn(opts?.class, props.ui?.empty) }),\r\n emptyText: (opts?: { class?: any }) => styles.emptyText({ class: cn(opts?.class, props.ui?.emptyText) }),\r\n cancel: (opts?: { class?: any }) => styles.cancel({ class: cn(opts?.class, props.ui?.cancel) }),\r\n cancelButton: (opts?: { class?: any }) => styles.cancelButton({ class: cn(opts?.class, props.ui?.cancelButton) }),\r\n confirm: (opts?: { class?: any }) => styles.confirm({ class: cn(opts?.class, props.ui?.confirm) }),\r\n confirmButton: (opts?: { class?: any }) => styles.confirmButton({ class: cn(opts?.class, props.ui?.confirmButton) }),\r\n }\r\n})\r\n\r\n// 当前选中的值\r\nconst value = ref<any[]>([])\r\n\r\n// 当前选中项的索引\r\nconst indexes = ref<number[]>([])\r\n\r\nconst selectItem = ref<any[]>([])\r\n\r\n// 计算选择器列表数据\r\nconst columns = computed<SelectOption[][]>(() => {\r\n let options = props.options || []\r\n const cols: SelectOption[][] = []\r\n\r\n for (let i = 0; i < props.columnCount; i++) {\r\n const column = [...options]\r\n const val = i >= value.value.length ? null : value.value[i]\r\n\r\n let item = options?.find(item => item.value == val)\r\n if (item == null && !isEmpty(options)) {\r\n item = options[0]\r\n }\r\n\r\n if (item?.children != null) {\r\n options = item.children\r\n }\r\n\r\n cols.push(column)\r\n }\r\n\r\n return cols\r\n})\r\n\r\n// 显示文本\r\nconst text = ref('')\r\n\r\nfunction updateText() {\r\n const val = props.modelValue\r\n if (val == null || val == undefined) {\r\n text.value = ''\r\n }\r\n else {\r\n let arr: any[]\r\n if (props.columnCount == 1) {\r\n arr = [val]\r\n }\r\n else {\r\n arr = val as any[]\r\n }\r\n\r\n text.value = arr\r\n .map((e, i) => columns.value[i]?.find(a => a.value == e)?.label ?? '')\r\n .join(props.splitor)\r\n }\r\n}\r\n\r\nfunction getValue() {\r\n return props.columnCount == 1 ? value.value[0] : value.value\r\n}\r\n\r\nfunction getSelectItem(a: number[]): any[] {\r\n return columns.value.map((c, i) => {\r\n return isNull(c[a[i]]) ? 0 : c[a[i]]\r\n })\r\n}\r\n\r\nfunction setValue(val: SelectValue) {\r\n let _value: any[]\r\n\r\n if (val == null) {\r\n _value = []\r\n }\r\n else if (Array.isArray(val)) {\r\n _value = [...val]\r\n }\r\n else {\r\n _value = [val]\r\n }\r\n\r\n const _indexes: number[] = []\r\n\r\n for (let i = 0; i < props.columnCount; i++) {\r\n const column = columns.value[i]\r\n\r\n if (i >= _value.length) {\r\n _indexes.push(0)\r\n if (!isNull(column) && column.length > 0 && !isNull(column[0])) {\r\n _value.push(column[0].value)\r\n }\r\n }\r\n else {\r\n let index = column.findIndex(e => e.value == _value[i])\r\n if (index < 0) { index = 0 }\r\n _indexes.push(index)\r\n }\r\n }\r\n\r\n value.value = _value\r\n indexes.value = _indexes\r\n\r\n selectItem.value = getSelectItem(indexes.value)\r\n updateText()\r\n}\r\n\r\nfunction onChange(a: number[]) {\r\n const b = [...indexes.value]\r\n let changed = false\r\n\r\n for (let i = 0; i < a.length; i++) {\r\n if (changed) {\r\n b[i] = 0\r\n }\r\n else if (b[i] != a[i]) {\r\n b[i] = a[i]\r\n changed = true\r\n }\r\n }\r\n\r\n indexes.value = b\r\n value.value = b.map((e, i) => (isNull(columns.value[i][e]) ? 0 : columns.value[i][e].value))\r\n emit('changing', getValue())\r\n}\r\n\r\nconst visible = ref(false)\r\nlet callback: ((value: SelectValue) => void) | null = null\r\n\r\nfunction open(cb: ((value: SelectValue) => void) | null = null) {\r\n visible.value = true\r\n setValue(props.modelValue)\r\n callback = cb\r\n}\r\n\r\nfunction close() {\r\n visible.value = false\r\n}\r\n\r\nfunction clear() {\r\n text.value = ''\r\n if (props.columnCount == 1) {\r\n emit('update:modelValue', null)\r\n emit('change', null, null)\r\n }\r\n else {\r\n emit('update:modelValue', [])\r\n emit('change', [], [])\r\n }\r\n if (validate) { validate('change') }\r\n}\r\n\r\nfunction confirm() {\r\n onChange(indexes.value)\r\n const val = getValue()\r\n\r\n selectItem.value = getSelectItem(indexes.value)\r\n\r\n emit('update:modelValue', val)\r\n emit('change', val, selectItem.value)\r\n if (validate) { validate('change') }\r\n if (callback != null) {\r\n callback(val)\r\n }\r\n close()\r\n}\r\n\r\nonMounted(() => {\r\n watch(\r\n () => props.modelValue,\r\n (val) => {\r\n setValue(val)\r\n },\r\n { immediate: true },\r\n )\r\n\r\n watch(\r\n () => props.options,\r\n () => {\r\n updateText()\r\n },\r\n )\r\n})\r\n\r\ndefineExpose({\r\n open,\r\n close,\r\n})\r\n</script>\r\n\r\n<template>\r\n <RebornSelectTrigger v-if=\"showTrigger\" :placeholder=\"placeholder\" :disabled=\"isDisabled\" :focus=\"popupRef?.isOpen\"\r\n :text=\"text\" :clearable=\"clearable\" :color=\"color\" :size=\"size\" :ui=\"triggerUi\" @open=\"open()\" @clear=\"clear\">\r\n\r\n <template #default=\"{ showText, text, placeholder, ui }\">\r\n <!-- #ifndef MP-WEIXIN -->\r\n <slot name=\"tag\" :selectItem=\"selectItem\" />\r\n <!-- #endif -->\r\n <!-- #ifdef MP-WEIXIN -->\r\n <slot v-if=\"$slots.tag\" name=\"tag\" :selectItem=\"selectItem\" />\r\n <text v-else-if=\"showText\" :class=\"ui.text()\">{{ text }}</text>\r\n <text v-else :class=\"ui.placeholder()\">{{ placeholder }}</text>\r\n <!-- #endif -->\r\n </template>\r\n </RebornSelectTrigger>\r\n <RebornPopup ref=\"popupRef\" v-model=\"visible\" :title=\"title\" :ui=\"popupUi\">\r\n <view @touchmove.stop>\r\n <slot name=\"prepend\" />\r\n\r\n <view>\r\n <RebornPickerView v-if=\"!noOptions\" :color=\"color\" :value=\"indexes\" :columns=\"columns\" :ui=\"pickerUi\"\r\n @change-index=\"onChange\">\r\n <!-- #ifndef MP-WEIXIN -->\r\n <template #default=\"{ item, index }\">\r\n <slot name=\"option\" :item=\"item\" :index=\"index\" />\r\n </template>\r\n <!-- #endif -->\r\n </RebornPickerView>\r\n\r\n <view v-else :class=\"ui.empty()\">\r\n <slot name=\"empty\">\r\n <text :class=\"ui.emptyText()\">暂无数据</text>\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <slot name=\"append\" />\r\n\r\n <view :class=\"ui.buttons()\">\r\n <view :class=\"ui.cancel()\">\r\n <RebornButton v-if=\"showCancel\" :size=\"size\" variant=\"outline\" :color=\"color\"\r\n :ui=\"{ base: ui.cancelButton() }\" block @tap.stop=\"close\">\r\n {{ cancelText }}\r\n </RebornButton>\r\n </view>\r\n <view :class=\"ui.confirm()\">\r\n <RebornButton v-if=\"showConfirm && !noOptions\" :size=\"size\" variant=\"solid\" :color=\"color\"\r\n :ui=\"{ base: ui.confirmButton() }\" block @tap.stop=\"confirm\">\r\n {{ confirmText }}\r\n </RebornButton>\r\n </view>\r\n </view>\r\n </view>\r\n </RebornPopup>\r\n</template>\r\n",
|
|
1887
|
+
"target": "uniapp"
|
|
1888
|
+
}
|
|
1889
|
+
]
|
|
1890
|
+
},
|
|
1891
|
+
{
|
|
1892
|
+
"name": "reborn-select-date",
|
|
1893
|
+
"dependencies": [
|
|
1894
|
+
"clsx",
|
|
1895
|
+
"lodash-es"
|
|
1896
|
+
],
|
|
1897
|
+
"files": [
|
|
1898
|
+
{
|
|
1899
|
+
"path": "index.ts",
|
|
1900
|
+
"content": "export { default as RebornSelectDate } from \"./RebornSelectDate.vue\";\r\n"
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
"path": "reborn-select-date.config.ts",
|
|
1904
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as selectDateSizes, colors as selectDateColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"relative inline-flex w-full group outline-none\",\r\n trigger:\r\n \"flex w-full items-center justify-between rounded-lg border border-gray-3 dark:border-gray-6 bg-gray-1 dark:bg-gray-8 transition-colors cursor-pointer select-none outline-none\",\r\n triggerText: \"truncate text-gray-8 dark:text-gray-1\",\r\n placeholder: \"text-gray-4 dark:text-gray-5\",\r\n arrow: \"transition-transform duration-200 text-gray-4 shrink-0\",\r\n dropdown:\r\n \"absolute z-50 mt-1 w-full rounded-lg border border-gray-2 dark:border-gray-7 bg-white dark:bg-gray-8 shadow-lg p-3\",\r\n calHeader: \"flex items-center justify-between mb-2\",\r\n calNavBtn: \"p-1 rounded-md hover:bg-gray-2 dark:hover:bg-gray-7 transition-colors cursor-pointer text-gray-6 dark:text-gray-3\",\r\n calTitle: \"text-sm font-medium text-gray-8 dark:text-gray-1 cursor-pointer hover:text-primary transition-colors\",\r\n calWeekdays: \"grid grid-cols-7 gap-0 text-center text-xs text-gray-4 dark:text-gray-5 mb-1\",\r\n calDays: \"grid grid-cols-7 gap-0\",\r\n calDay:\r\n \"flex items-center justify-center rounded-md text-sm cursor-pointer transition-colors text-gray-7 dark:text-gray-2 hover:bg-gray-2 dark:hover:bg-gray-7\",\r\n calDayActive: \"\",\r\n calDayDisabled: \"text-gray-4 dark:text-gray-5 opacity-40 pointer-events-none\",\r\n calDayToday: \"font-bold\",\r\n clearBtn: \"shrink-0 text-gray-4 hover:text-gray-6 dark:hover:text-gray-3 cursor-pointer\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n trigger: \"h-8 px-2 text-xs gap-1\",\r\n arrow: \"size-3\",\r\n clearBtn: \"size-3\",\r\n calDays: \"gap-y-3\",\r\n calDay: \"text-xs\",\r\n },\r\n md: {\r\n trigger: \"h-10 px-3 text-sm gap-2\",\r\n arrow: \"size-4\",\r\n clearBtn: \"size-4\",\r\n calDays: \"gap-y-4\",\r\n calDay: \"text-sm\",\r\n },\r\n lg: {\r\n trigger: \"h-12 px-4 text-base gap-2\",\r\n arrow: \"size-5\",\r\n clearBtn: \"size-5\",\r\n calDays: \"gap-y-5\",\r\n calDay: \"text-base\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n trigger: \"group-focus:border-primary group-focus:ring-2 group-focus:ring-primary/20 data-[state=open]:border-primary data-[state=open]:ring-2 data-[state=open]:ring-primary/20\",\r\n calDayActive: \"bg-primary text-white hover:bg-primary/90\",\r\n },\r\n secondary: {\r\n trigger: \"group-focus:border-secondary group-focus:ring-2 group-focus:ring-secondary/20 data-[state=open]:border-secondary data-[state=open]:ring-2 data-[state=open]:ring-secondary/20\",\r\n calDayActive: \"bg-secondary text-white hover:bg-secondary/90\",\r\n },\r\n success: {\r\n trigger: \"group-focus:border-success group-focus:ring-2 group-focus:ring-success/20 data-[state=open]:border-success data-[state=open]:ring-2 data-[state=open]:ring-success/20\",\r\n calDayActive: \"bg-success text-white hover:bg-success/90\",\r\n },\r\n info: {\r\n trigger: \"group-focus:border-info group-focus:ring-2 group-focus:ring-info/20 data-[state=open]:border-info data-[state=open]:ring-2 data-[state=open]:ring-info/20\",\r\n calDayActive: \"bg-info text-white hover:bg-info/90\",\r\n },\r\n warning: {\r\n trigger: \"group-focus:border-warning group-focus:ring-2 group-focus:ring-warning/20 data-[state=open]:border-warning data-[state=open]:ring-2 data-[state=open]:ring-warning/20\",\r\n calDayActive: \"bg-warning text-white hover:bg-warning/90\",\r\n },\r\n error: {\r\n trigger: \"group-focus:border-error group-focus:ring-2 group-focus:ring-error/20 data-[state=open]:border-error data-[state=open]:ring-2 data-[state=open]:ring-error/20\",\r\n calDayActive: \"bg-error text-white hover:bg-error/90\",\r\n },\r\n neutral: {\r\n trigger: \"group-focus:border-neutral group-focus:ring-2 group-focus:ring-neutral/20 data-[state=open]:border-neutral data-[state=open]:ring-2 data-[state=open]:ring-neutral/20\",\r\n calDayActive: \"bg-neutral text-white hover:bg-neutral/90\",\r\n },\r\n },\r\n open: {\r\n true: { arrow: \"rotate-180\" },\r\n },\r\n disabled: {\r\n true: {\r\n trigger: \"opacity-50 pointer-events-none cursor-not-allowed bg-gray-50 dark:bg-gray-900\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
1905
|
+
"target": "web"
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
"path": "RebornSelectDate.vue",
|
|
1909
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, onBeforeUnmount, onMounted, ref } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { selectDateColors, selectDateSizes } from \"./reborn-select-date.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface SelectDateProps {\r\n modelValue?: string | string[];\r\n type?: \"year\" | \"month\" | \"date\";\r\n placeholder?: string;\r\n disabled?: boolean;\r\n clearable?: boolean;\r\n rangeable?: boolean;\r\n start?: string;\r\n end?: string;\r\n labelFormat?: string;\r\n valueFormat?: string;\r\n size?: (typeof selectDateSizes)[number];\r\n color?: (typeof selectDateColors)[number];\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n trigger: ClassValue;\r\n triggerText: ClassValue;\r\n placeholder: ClassValue;\r\n arrow: ClassValue;\r\n dropdown: ClassValue;\r\n calHeader: ClassValue;\r\n calNavBtn: ClassValue;\r\n calTitle: ClassValue;\r\n calWeekdays: ClassValue;\r\n calDays: ClassValue;\r\n calDay: ClassValue;\r\n calDayActive: ClassValue;\r\n calDayDisabled: ClassValue;\r\n calDayToday: ClassValue;\r\n clearBtn: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<SelectDateProps>(), {\r\n modelValue: \"\",\r\n type: \"date\",\r\n placeholder: \"请选择日期\",\r\n disabled: false,\r\n clearable: true,\r\n rangeable: false,\r\n start: \"1970-01-01\",\r\n end: \"2099-12-31\",\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string | string[]): void;\r\n (e: \"change\", value: string | string[]): void;\r\n}>();\r\n\r\nconst isOpen = ref(false);\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\n\r\n// Calendar state\r\nconst viewYear = ref(new Date().getFullYear());\r\nconst viewMonth = ref(new Date().getMonth()); // 0-indexed\r\n\r\nconst selectedDate = ref<Date | null>(null);\r\nconst rangeStart = ref<Date | null>(null);\r\nconst rangeEnd = ref<Date | null>(null);\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n open: isOpen.value,\r\n disabled: props.disabled,\r\n });\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n trigger: (opts?: { class?: any }) => styles.trigger({ class: cn(opts?.class, uiOverrides.value.trigger) }),\r\n triggerText: (opts?: { class?: any }) => styles.triggerText({ class: cn(opts?.class, uiOverrides.value.triggerText) }),\r\n placeholder: (opts?: { class?: any }) => styles.placeholder({ class: cn(opts?.class, uiOverrides.value.placeholder) }),\r\n arrow: (opts?: { class?: any }) => styles.arrow({ class: cn(opts?.class, uiOverrides.value.arrow) }),\r\n dropdown: (opts?: { class?: any }) => styles.dropdown({ class: cn(opts?.class, uiOverrides.value.dropdown) }),\r\n calHeader: (opts?: { class?: any }) => styles.calHeader({ class: cn(opts?.class, uiOverrides.value.calHeader) }),\r\n calNavBtn: (opts?: { class?: any }) => styles.calNavBtn({ class: cn(opts?.class, uiOverrides.value.calNavBtn) }),\r\n calTitle: (opts?: { class?: any }) => styles.calTitle({ class: cn(opts?.class, uiOverrides.value.calTitle) }),\r\n calWeekdays: (opts?: { class?: any }) => styles.calWeekdays({ class: cn(opts?.class, uiOverrides.value.calWeekdays) }),\r\n calDays: (opts?: { class?: any }) => styles.calDays({ class: cn(opts?.class, uiOverrides.value.calDays) }),\r\n calDay: (opts?: { class?: any }) => styles.calDay({ class: cn(opts?.class, uiOverrides.value.calDay) }),\r\n calDayActive: (opts?: { class?: any }) => styles.calDayActive({ class: cn(opts?.class, uiOverrides.value.calDayActive) }),\r\n calDayDisabled: (opts?: { class?: any }) => styles.calDayDisabled({ class: cn(opts?.class, uiOverrides.value.calDayDisabled) }),\r\n calDayToday: (opts?: { class?: any }) => styles.calDayToday({ class: cn(opts?.class, uiOverrides.value.calDayToday) }),\r\n clearBtn: (opts?: { class?: any }) => styles.clearBtn({ class: cn(opts?.class, uiOverrides.value.clearBtn) }),\r\n };\r\n});\r\n\r\nconst weekdays = [\"日\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\"];\r\nconst today = new Date();\r\n\r\nfunction parseValue(v: string): Date | null {\r\n if (!v) return null;\r\n const d = new Date(v);\r\n return isNaN(d.getTime()) ? null : d;\r\n}\r\n\r\nfunction formatDate(d: Date): string {\r\n const y = d.getFullYear();\r\n const m = String(d.getMonth() + 1).padStart(2, \"0\");\r\n const day = String(d.getDate()).padStart(2, \"0\");\r\n\r\n if (props.valueFormat) {\r\n return props.valueFormat\r\n .replace(\"YYYY\", String(y))\r\n .replace(\"MM\", m)\r\n .replace(\"DD\", day);\r\n }\r\n\r\n if (props.type === \"year\") return `${y}`;\r\n if (props.type === \"month\") return `${y}-${m}`;\r\n return `${y}-${m}-${day}`;\r\n}\r\n\r\nfunction formatDisplay(d: Date): string {\r\n if (props.labelFormat) {\r\n return props.labelFormat\r\n .replace(\"YYYY\", String(d.getFullYear()))\r\n .replace(\"MM\", String(d.getMonth() + 1).padStart(2, \"0\"))\r\n .replace(\"DD\", String(d.getDate()).padStart(2, \"0\"));\r\n }\r\n return formatDate(d);\r\n}\r\n\r\nconst displayText = computed(() => {\r\n if (props.rangeable) {\r\n if (rangeStart.value && rangeEnd.value) {\r\n return `${formatDisplay(rangeStart.value)} ~ ${formatDisplay(rangeEnd.value)}`;\r\n }\r\n if (rangeStart.value) {\r\n return formatDisplay(rangeStart.value);\r\n }\r\n return \"\";\r\n }\r\n if (selectedDate.value) return formatDisplay(selectedDate.value);\r\n return \"\";\r\n});\r\n\r\ninterface CalDay {\r\n date: Date;\r\n day: number;\r\n isCurrentMonth: boolean;\r\n isToday: boolean;\r\n isSelected: boolean;\r\n isDisabled: boolean;\r\n isInRange: boolean;\r\n isRangeStart: boolean;\r\n isRangeEnd: boolean;\r\n}\r\n\r\nconst calendarDays = computed<CalDay[]>(() => {\r\n const firstDayOfMonth = new Date(viewYear.value, viewMonth.value, 1);\r\n const startWeekday = firstDayOfMonth.getDay();\r\n const daysInMonth = new Date(viewYear.value, viewMonth.value + 1, 0).getDate();\r\n\r\n const startDate = new Date(firstDayOfMonth);\r\n startDate.setDate(startDate.getDate() - startWeekday);\r\n\r\n const totalCells = Math.ceil((startWeekday + daysInMonth) / 7) * 7;\r\n const days: CalDay[] = [];\r\n\r\n const startLimit = props.start ? new Date(props.start) : null;\r\n const endLimit = props.end ? new Date(props.end) : null;\r\n\r\n for (let i = 0; i < totalCells; i++) {\r\n const d = new Date(startDate);\r\n d.setDate(startDate.getDate() + i);\r\n\r\n const isCurrentMonth = d.getMonth() === viewMonth.value && d.getFullYear() === viewYear.value;\r\n const isToday = d.toDateString() === today.toDateString();\r\n const isSelected = selectedDate.value ? d.toDateString() === selectedDate.value.toDateString() : false;\r\n\r\n let isDisabled = !isCurrentMonth;\r\n if (startLimit && d < startLimit) isDisabled = true;\r\n if (endLimit && d > endLimit) isDisabled = true;\r\n\r\n const isRangeStart = rangeStart.value ? d.toDateString() === rangeStart.value.toDateString() : false;\r\n const isRangeEnd = rangeEnd.value ? d.toDateString() === rangeEnd.value.toDateString() : false;\r\n const isInRange = rangeStart.value && rangeEnd.value && d >= rangeStart.value && d <= rangeEnd.value;\r\n\r\n days.push({ date: d, day: d.getDate(), isCurrentMonth, isToday, isSelected, isDisabled, isInRange, isRangeStart, isRangeEnd });\r\n }\r\n\r\n return days;\r\n});\r\n\r\nconst currentYearDecade = computed(() => {\r\n return Math.floor(viewYear.value / 10) * 10;\r\n});\r\n\r\nconst viewYearPageStart = ref(currentYearDecade.value);\r\n\r\nconst yearList = computed(() => {\r\n const start = props.start ? new Date(props.start).getFullYear() : 1970;\r\n const end = props.end ? new Date(props.end).getFullYear() : 2099;\r\n const years: { year: number, isDisabled: boolean }[] = [];\r\n\r\n for (let i = -1; i <= 10; i++) {\r\n const y = viewYearPageStart.value + i;\r\n years.push({\r\n year: y,\r\n isDisabled: y < start || y > end\r\n });\r\n }\r\n return years;\r\n});\r\n\r\nconst monthList = computed(() => {\r\n return Array.from({ length: 12 }, (_, i) => i + 1);\r\n});\r\n\r\nconst currentView = ref<\"year\" | \"month\" | \"date\">(props.type);\r\n\r\nwatch(() => props.type, (newType) => {\r\n currentView.value = newType;\r\n});\r\n\r\nconst headerTitle = computed(() => {\r\n if (currentView.value === \"year\") return `${viewYearPageStart.value} - ${viewYearPageStart.value + 9}`;\r\n if (currentView.value === \"month\") return `${viewYear.value}年`;\r\n return `${viewYear.value}年${viewMonth.value + 1}月`;\r\n});\r\n\r\nfunction prevPage() {\r\n if (currentView.value === \"year\") {\r\n viewYearPageStart.value -= 10;\r\n return;\r\n }\r\n if (currentView.value === \"month\") {\r\n viewYear.value--;\r\n return;\r\n }\r\n if (viewMonth.value === 0) {\r\n viewMonth.value = 11;\r\n viewYear.value--;\r\n } else {\r\n viewMonth.value--;\r\n }\r\n}\r\n\r\nfunction nextPage() {\r\n if (currentView.value === \"year\") {\r\n viewYearPageStart.value += 10;\r\n return;\r\n }\r\n if (currentView.value === \"month\") {\r\n viewYear.value++;\r\n return;\r\n }\r\n if (viewMonth.value === 11) {\r\n viewMonth.value = 0;\r\n viewYear.value++;\r\n } else {\r\n viewMonth.value++;\r\n }\r\n}\r\n\r\nfunction selectDay(day: CalDay) {\r\n if (day.isDisabled) return;\r\n\r\n if (props.rangeable) {\r\n if (!rangeStart.value || (rangeStart.value && rangeEnd.value)) {\r\n rangeStart.value = day.date;\r\n rangeEnd.value = null;\r\n } else {\r\n if (day.date < rangeStart.value) {\r\n rangeEnd.value = rangeStart.value;\r\n rangeStart.value = day.date;\r\n } else {\r\n rangeEnd.value = day.date;\r\n }\r\n const val = [formatDate(rangeStart.value), formatDate(rangeEnd.value)];\r\n emit(\"update:modelValue\", val);\r\n emit(\"change\", val);\r\n isOpen.value = false;\r\n }\r\n } else {\r\n selectedDate.value = day.date;\r\n const val = formatDate(day.date);\r\n emit(\"update:modelValue\", val);\r\n emit(\"change\", val);\r\n isOpen.value = false;\r\n }\r\n}\r\n\r\nfunction selectYear(year: number) {\r\n viewYear.value = year;\r\n viewYearPageStart.value = Math.floor(year / 10) * 10;\r\n if (props.type === \"year\") {\r\n selectedDate.value = new Date(year, 0, 1);\r\n const val = String(year);\r\n emit(\"update:modelValue\", val);\r\n emit(\"change\", val);\r\n isOpen.value = false;\r\n } else {\r\n currentView.value = \"month\";\r\n }\r\n}\r\n\r\nfunction selectMonth(month: number) {\r\n viewMonth.value = month - 1;\r\n if (props.type === \"month\") {\r\n selectedDate.value = new Date(viewYear.value, month - 1, 1);\r\n const val = `${viewYear.value}-${String(month).padStart(2, \"0\")}`;\r\n emit(\"update:modelValue\", val);\r\n emit(\"change\", val);\r\n isOpen.value = false;\r\n } else {\r\n currentView.value = \"date\";\r\n }\r\n}\r\n\r\nfunction toggle() {\r\n if (props.disabled) return;\r\n isOpen.value = !isOpen.value;\r\n if (isOpen.value) {\r\n currentView.value = props.type;\r\n if (selectedDate.value) {\r\n viewYear.value = selectedDate.value.getFullYear();\r\n viewMonth.value = selectedDate.value.getMonth();\r\n viewYearPageStart.value = Math.floor(viewYear.value / 10) * 10;\r\n }\r\n }\r\n}\r\n\r\nfunction clear(e: Event) {\r\n e.stopPropagation();\r\n selectedDate.value = null;\r\n rangeStart.value = null;\r\n rangeEnd.value = null;\r\n emit(\"update:modelValue\", props.rangeable ? [] : \"\");\r\n emit(\"change\", props.rangeable ? [] : \"\");\r\n}\r\n\r\nfunction onClickOutside(e: MouseEvent) {\r\n if (wrapperRef.value && !wrapperRef.value.contains(e.target as Node)) {\r\n isOpen.value = false;\r\n }\r\n}\r\n\r\nif (props.modelValue) {\r\n if (props.rangeable && Array.isArray(props.modelValue)) {\r\n const [start, end] = props.modelValue;\r\n if (start) {\r\n const d1 = parseValue(start);\r\n if (d1) {\r\n rangeStart.value = d1;\r\n viewYear.value = d1.getFullYear();\r\n viewMonth.value = d1.getMonth();\r\n }\r\n }\r\n if (end) {\r\n const d2 = parseValue(end);\r\n if (d2) rangeEnd.value = d2;\r\n }\r\n } else if (!props.rangeable && typeof props.modelValue === 'string') {\r\n const d = parseValue(props.modelValue);\r\n if (d) {\r\n selectedDate.value = d;\r\n viewYear.value = d.getFullYear();\r\n viewMonth.value = d.getMonth();\r\n }\r\n }\r\n}\r\n\r\nonMounted(() => document.addEventListener(\"click\", onClickOutside));\r\nonBeforeUnmount(() => document.removeEventListener(\"click\", onClickOutside));\r\n</script>\r\n\r\n<template>\r\n <div ref=\"wrapperRef\" :class=\"ui.wrapper({ class: props.class })\">\r\n <div :class=\"ui.trigger()\" @click.stop=\"toggle\" :data-state=\"isOpen ? 'open' : 'closed'\">\r\n <span v-if=\"displayText\" :class=\"ui.triggerText()\">{{ displayText }}</span>\r\n <span v-else :class=\"ui.placeholder()\">{{ placeholder }}</span>\r\n\r\n <div class=\"flex items-center gap-1\">\r\n <span v-if=\"clearable && modelValue\" :class=\"ui.clearBtn()\" @click.stop=\"clear\">\r\n <Icon name=\"lucide:x\" class=\"size-full\" />\r\n </span>\r\n <Icon name=\"lucide:calendar\" :class=\"ui.arrow()\" />\r\n </div>\r\n </div>\r\n\r\n <Transition enter-active-class=\"transition duration-150 ease-out\" enter-from-class=\"opacity-0 -translate-y-1\"\r\n enter-to-class=\"opacity-100 translate-y-0\" leave-active-class=\"transition duration-100 ease-in\"\r\n leave-from-class=\"opacity-100 translate-y-0\" leave-to-class=\"opacity-0 -translate-y-1\">\r\n <div v-if=\"isOpen\" :class=\"ui.dropdown()\" style=\"top: 100%; min-width: 280px\">\r\n <template v-if=\"currentView === 'year'\">\r\n <div :class=\"ui.calHeader()\">\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"prevPage\">\r\n <Icon name=\"lucide:chevron-left\" class=\"size-4\" />\r\n </span>\r\n <span :class=\"ui.calTitle()\">{{ headerTitle }}</span>\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"nextPage\">\r\n <Icon name=\"lucide:chevron-right\" class=\"size-4\" />\r\n </span>\r\n </div>\r\n <div class=\"grid grid-cols-4 gap-1 max-h-[240px] overflow-auto\">\r\n <div v-for=\"item in yearList\" :key=\"item.year\" :class=\"[\r\n ui.calDay(),\r\n selectedDate && selectedDate.getFullYear() === item.year ? ui.calDayActive() : '',\r\n item.year === today.getFullYear() && (!selectedDate || selectedDate.getFullYear() !== item.year) ? ui.calDayToday() : '',\r\n item.isDisabled ? ui.calDayDisabled() : '',\r\n item.year < viewYearPageStart || item.year >= viewYearPageStart + 10 ? 'text-gray-4 dark:text-gray-5' : ''\r\n ]\" class=\"h-9\" @click.stop=\"selectYear(item.year)\">\r\n {{ item.year }}\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else-if=\"currentView === 'month'\">\r\n <div :class=\"ui.calHeader()\">\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"prevPage\">\r\n <Icon name=\"lucide:chevron-left\" class=\"size-4\" />\r\n </span>\r\n <span :class=\"ui.calTitle()\" @click.stop=\"currentView = 'year'\">{{ headerTitle }}</span>\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"nextPage\">\r\n <Icon name=\"lucide:chevron-right\" class=\"size-4\" />\r\n </span>\r\n </div>\r\n <div class=\"grid grid-cols-4 gap-1\">\r\n <div v-for=\"m in monthList\" :key=\"m\" :class=\"[\r\n ui.calDay(),\r\n selectedDate && selectedDate.getFullYear() === viewYear && selectedDate.getMonth() === m - 1\r\n ? ui.calDayActive()\r\n : '',\r\n ]\" class=\"h-9\" @click.stop=\"selectMonth(m)\">\r\n {{ m }}月\r\n </div>\r\n </div>\r\n </template>\r\n\r\n <template v-else>\r\n <div :class=\"ui.calHeader()\">\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"prevPage\">\r\n <Icon name=\"lucide:chevron-left\" class=\"size-4\" />\r\n </span>\r\n <span :class=\"ui.calTitle()\" @click.stop=\"currentView = 'month'\">{{ headerTitle }}</span>\r\n <span :class=\"ui.calNavBtn()\" @click.stop=\"nextPage\">\r\n <Icon name=\"lucide:chevron-right\" class=\"size-4\" />\r\n </span>\r\n </div>\r\n\r\n <div :class=\"ui.calWeekdays()\">\r\n <span v-for=\"w in weekdays\" :key=\"w\">{{ w }}</span>\r\n </div>\r\n\r\n <div :class=\"ui.calDays()\">\r\n <div v-for=\"(day, idx) in calendarDays\" :key=\"idx\" :class=\"[\r\n ui.calDay(),\r\n day.isSelected || day.isRangeStart || day.isRangeEnd ? ui.calDayActive() : '',\r\n day.isDisabled ? ui.calDayDisabled() : '',\r\n day.isToday && !day.isSelected && !day.isRangeStart && !day.isRangeEnd ? ui.calDayToday() : '',\r\n day.isInRange && !day.isRangeStart && !day.isRangeEnd ? 'bg-primary/10 dark:bg-primary/20' : '',\r\n ]\" @click.stop=\"selectDay(day)\">\r\n {{ day.day }}\r\n </div>\r\n </div>\r\n </template>\r\n </div>\r\n </Transition>\r\n </div>\r\n</template>\r\n",
|
|
1910
|
+
"target": "web"
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
"path": "reborn-select-date.config.ts",
|
|
1914
|
+
"content": "export const selectDateSizes = ['sm', 'md', 'lg'] as const\r\nexport const selectDateColors = ['primary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n wrapper: 'w-full',\r\n popupOp: 'flex flex-row items-center justify-center gap-2 p-3',\r\n rangeBox: 'px-3 pb-5 pt-2',\r\n rangeValues: 'flex flex-row items-center justify-center',\r\n rangeStart: 'flex-1 rounded-xl border border-solid p-2 text-center transition-colors',\r\n rangeEnd: 'flex-1 rounded-xl border border-solid p-2 text-center transition-colors',\r\n shortcuts: 'mb-4 flex flex-row flex-wrap items-center gap-2',\r\n shortcutItem: 'flex cursor-pointer items-center gap-1 rounded-md border border-solid px-2 py-1 text-xs transition-colors',\r\n separator: 'text-gray-5 mx-3 text-sm',\r\n rangeValueText: 'text-center block w-full',\r\n rangePlaceholder: 'text-surface-400 block w-full text-center',\r\n footer: 'flex flex-row items-center justify-center gap-2 px-3 pt-3 pb-[calc(0.75rem+var(--window-bottom))]',\r\n cancel: 'flex-1 ',\r\n cancelButton: 'w-full',\r\n confirm: 'flex-1 ',\r\n confirmButton: 'w-full',\r\n popup: 'bg-white',\r\n draw: 'bg-gray-4',\r\n header: 'bg-white',\r\n title: 'text-gray-9',\r\n },\r\n variants: {\r\n shortcutActive: {\r\n true: {},\r\n false: {\r\n shortcutItem: `\r\n border-gray-2\r\n dark:border-gray-7\r\n text-gray-6\r\n dark:text-gray-4\r\n bg-transparent\r\n `,\r\n },\r\n },\r\n color: {\r\n primary: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n success: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n info: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n warning: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n error: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n neutral: {\r\n rangeStart: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n rangeEnd: `\r\n border-gray-2\r\n dark:border-gray-6\r\n bg-gray-1\r\n dark:bg-gray-8\r\n `,\r\n },\r\n },\r\n rangeActive: {\r\n start: {\r\n rangeStart: `\r\n border-primary bg-transparent\r\n dark:border-primary dark:bg-transparent\r\n `,\r\n },\r\n end: {\r\n rangeEnd: `\r\n border-primary bg-transparent\r\n dark:border-primary dark:bg-transparent\r\n `,\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n color: 'success' as const,\r\n rangeActive: 'start' as const,\r\n class: {\r\n rangeStart: `\r\n border-success bg-transparent\r\n dark:border-success dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'success' as const,\r\n rangeActive: 'end' as const,\r\n class: {\r\n rangeEnd: `\r\n border-success bg-transparent\r\n dark:border-success dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'info' as const,\r\n rangeActive: 'start' as const,\r\n class: {\r\n rangeStart: `\r\n border-info bg-transparent\r\n dark:border-info dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'info' as const,\r\n rangeActive: 'end' as const,\r\n class: {\r\n rangeEnd: `\r\n border-info bg-transparent\r\n dark:border-info dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'warning' as const,\r\n rangeActive: 'start' as const,\r\n class: {\r\n rangeStart: `\r\n border-warning bg-transparent\r\n dark:border-warning dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'warning' as const,\r\n rangeActive: 'end' as const,\r\n class: {\r\n rangeEnd: `\r\n border-warning bg-transparent\r\n dark:border-warning dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'error' as const,\r\n rangeActive: 'start' as const,\r\n class: {\r\n rangeStart: `\r\n border-error bg-transparent\r\n dark:border-error dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'error' as const,\r\n rangeActive: 'end' as const,\r\n class: {\r\n rangeEnd: `\r\n border-error bg-transparent\r\n dark:border-error dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'neutral' as const,\r\n rangeActive: 'start' as const,\r\n class: {\r\n rangeStart: `\r\n border-neutral bg-transparent\r\n dark:border-neutral dark:bg-transparent\r\n `,\r\n },\r\n },\r\n {\r\n color: 'neutral' as const,\r\n rangeActive: 'end' as const,\r\n class: {\r\n rangeEnd: `\r\n border-neutral bg-transparent\r\n dark:border-neutral dark:bg-transparent\r\n `,\r\n },\r\n },\r\n // 快捷选项选中态(按 color)\r\n { shortcutActive: true as const, color: 'primary' as const, class: { shortcutItem: 'border-primary text-primary bg-primary-50 dark:bg-primary-900/20' } },\r\n { shortcutActive: true as const, color: 'success' as const, class: { shortcutItem: 'border-success text-success bg-success-50 dark:bg-success-900/20' } },\r\n { shortcutActive: true as const, color: 'info' as const, class: { shortcutItem: 'border-info text-info bg-info-50 dark:bg-info-900/20' } },\r\n { shortcutActive: true as const, color: 'warning' as const, class: { shortcutItem: 'border-warning text-warning bg-warning-50 dark:bg-warning-900/20' } },\r\n { shortcutActive: true as const, color: 'error' as const, class: { shortcutItem: 'border-error text-error bg-error-50 dark:bg-error-900/20' } },\r\n { shortcutActive: true as const, color: 'neutral' as const, class: { shortcutItem: 'border-neutral text-neutral bg-neutral-50 dark:bg-neutral-900/20' } },\r\n ],\r\n defaultVariants: {\r\n color: 'primary' as const,\r\n shortcutActive: false as const,\r\n },\r\n}\r\n",
|
|
1915
|
+
"target": "uniapp"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
"path": "RebornSelectDate.vue",
|
|
1919
|
+
"content": "<script setup lang=\"ts\">\nimport type { ClassValue } from 'clsx'\nimport type { SelectOption } from '../reborn-picker-view/RebornPickerView.vue'\nimport type { selectDateColors, selectDateSizes } from './reborn-select-date.config'\nimport type { SelectDateShortcut } from './types'\nimport { isEmpty, isNull } from 'lodash-es'\nimport { computed, nextTick, ref, watch } from 'vue'\n\nimport { useFormInject } from '@/composables/useFieldGroup'\nimport { dayUts } from '@/lib/dayUts'\nimport { tv } from '@/lib/tv'\n\nimport RebornButton from '../reborn-button/RebornButton.vue'\nimport RebornPickerView from '../reborn-picker-view/RebornPickerView.vue'\nimport RebornPopup from '../reborn-popup/RebornPopup.vue'\nimport RebornSelectTrigger from '../reborn-select-trigger/RebornSelectTrigger.vue'\nimport RebornText from '../reborn-text/RebornText.vue'\nimport theme from './reborn-select-date.config'\n\ndefineOptions({\n name: 'RebornSelectDate',\n})\n\nexport type SelectDateType = 'year' | 'month' | 'date' | 'hour' | 'minute' | 'second'\n\nexport interface TriggerUiShape {\n wrapper?: ClassValue\n content?: ClassValue\n text?: ClassValue\n placeholder?: ClassValue\n iconWrapper?: ClassValue\n clearIcon?: ClassValue\n arrowIcon?: ClassValue\n}\n\nexport interface PopupUiShape {\n wrapper?: ClassValue\n mask?: ClassValue\n popup?: ClassValue\n inner?: ClassValue\n draw?: ClassValue\n header?: ClassValue\n title?: ClassValue\n container?: ClassValue\n}\n\nexport interface PickerUiShape {\n wrapper?: ClassValue\n header?: ClassValue\n headerText?: ClassValue\n pickerContainer?: ClassValue\n item?: ClassValue\n itemText?: ClassValue\n indicator?: ClassValue\n}\n\nexport interface SelectDateUiShape {\n wrapper?: ClassValue\n popupOp?: ClassValue\n rangeBox?: ClassValue\n rangeValues?: ClassValue\n rangeStart?: ClassValue\n rangeEnd?: ClassValue\n shortcuts?: ClassValue\n shortcutItem?: ClassValue\n separator?: ClassValue\n rangeValueText?: ClassValue\n rangePlaceholder?: ClassValue\n footer?: ClassValue\n cancel?: ClassValue\n cancelButton?: ClassValue\n confirm?: ClassValue\n confirmButton?: ClassValue\n}\n\nexport interface SelectDateProps {\n modelValue?: string\n values?: string[]\n title?: string\n headers?: string[]\n placeholder?: string\n showTrigger?: boolean\n disabled?: boolean\n confirmText?: string\n showConfirm?: boolean\n cancelText?: string\n showCancel?: boolean\n labelFormat?: string\n valueFormat?: string\n start?: string\n end?: string\n type?: SelectDateType\n rangeable?: boolean\n startPlaceholder?: string\n endPlaceholder?: string\n rangeSeparator?: string\n showShortcuts?: boolean\n shortcuts?: SelectDateShortcut[]\n clearable?: boolean\n size?: typeof selectDateSizes[number]\n color?: typeof selectDateColors[number]\n triggerUi?: Partial<TriggerUiShape>\n popupUi?: Partial<PopupUiShape>\n pickerUi?: Partial<PickerUiShape>\n ui?: Partial<SelectDateUiShape>\n}\n\nconst props = withDefaults(\n defineProps<SelectDateProps>(),\n {\n modelValue: '',\n values: () => [],\n title: '请选择',\n headers: () => ['年', '月', '日', '时', '分', '秒'],\n placeholder: '请选择',\n showTrigger: true,\n disabled: false,\n confirmText: '确定',\n showConfirm: true,\n cancelText: '取消',\n showCancel: true,\n labelFormat: '',\n valueFormat: '',\n start: '1970-01-01 00:00:00',\n end: '2099-12-31 23:59:59',\n type: 'second',\n rangeable: false,\n startPlaceholder: '开始日期',\n endPlaceholder: '结束日期',\n rangeSeparator: '至',\n showShortcuts: true,\n shortcuts: () => [],\n clearable: true,\n size: 'md',\n color: 'primary',\n triggerUi: () => ({}),\n popupUi: () => ({}),\n pickerUi: () => ({}),\n ui: () => ({}),\n },\n)\n\nconst emit = defineEmits(['update:modelValue', 'change', 'update:values', 'range-change'])\n\nconst popupRef = ref<any>(null)\n\n// Form integration\nconst { disabled: formDisabled, validate } = useFormInject(props as any)\nconst isDisabled = computed(() => formDisabled.value || props.disabled)\n\nconst b = tv(theme)\nconst ui = computed(() => {\n const styles = b({ color: props.color })\n return {\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: [opts?.class, props.ui?.wrapper] }),\n popupOp: (opts?: { class?: any }) => styles.popupOp({ class: [opts?.class, props.ui?.popupOp] }),\n rangeBox: (opts?: { class?: any }) => styles.rangeBox({ class: [opts?.class, props.ui?.rangeBox] }),\n rangeValues: (opts?: { class?: any }) => styles.rangeValues({ class: [opts?.class, props.ui?.rangeValues] }),\n rangeStart: (opts?: { class?: any }) => styles.rangeStart({ class: [opts?.class, props.ui?.rangeStart] }),\n rangeEnd: (opts?: { class?: any }) => styles.rangeEnd({ class: [opts?.class, props.ui?.rangeEnd] }),\n shortcuts: (opts?: { class?: any }) => styles.shortcuts({ class: [opts?.class, props.ui?.shortcuts] }),\n shortcutItem: (opts?: { class?: any, active?: boolean }) => b({ color: props.color, shortcutActive: opts?.active === true }).shortcutItem({ class: [opts?.class, props.ui?.shortcutItem] }),\n separator: (opts?: { class?: any }) => styles.separator({ class: [opts?.class, props.ui?.separator] }),\n rangeValueText: (opts?: { class?: any }) => styles.rangeValueText({ class: [opts?.class, props.ui?.rangeValueText] }),\n rangePlaceholder: (opts?: { class?: any }) => styles.rangePlaceholder({ class: [opts?.class, props.ui?.rangePlaceholder] }),\n footer: (opts?: { class?: any }) => styles.footer({ class: [opts?.class, props.ui?.footer] }),\n cancel: (opts?: { class?: any }) => styles.cancel({ class: [opts?.class, props.ui?.cancel] }),\n cancelButton: (opts?: { class?: any }) => styles.cancelButton({ class: [opts?.class, props.ui?.cancelButton] }),\n confirm: (opts?: { class?: any }) => styles.confirm({ class: [opts?.class, props.ui?.confirm] }),\n confirmButton: (opts?: { class?: any }) => styles.confirmButton({ class: [opts?.class, props.ui?.confirmButton] }),\n }\n})\n\nconst rangeActiveStyle = computed(() => {\n const s = b({ color: props.color, rangeActive: 'start' }).rangeStart()\n const e = b({ color: props.color, rangeActive: 'end' }).rangeEnd()\n return { start: s, end: e }\n})\n\n// 格式化类型\nconst formatType = computed(() => {\n switch (props.type) {\n case 'year':\n return 'YYYY'\n case 'month':\n return 'YYYY-MM'\n case 'date':\n return 'YYYY-MM-DD'\n case 'hour':\n case 'minute':\n case 'second':\n return 'YYYY-MM-DD HH:mm:ss'\n default:\n return 'YYYY-MM-DD HH:mm:ss'\n }\n})\n\nconst labelFormat = computed(() => {\n if (isNull(props.labelFormat) || isEmpty(props.labelFormat)) {\n return formatType.value\n }\n return props.labelFormat\n})\n\nconst valueFormat = computed(() => {\n if (isNull(props.valueFormat) || isEmpty(props.valueFormat)) {\n return formatType.value\n }\n return props.valueFormat\n})\n\nconst shortcutsIndex = ref<number>(-1)\n\nconst shortcuts = computed<SelectDateShortcut[]>(() => {\n if (!isEmpty(props.shortcuts)) {\n return props.shortcuts\n }\n\n return [\n {\n label: '今天',\n value: [dayUts().format(valueFormat.value), dayUts().format(valueFormat.value)],\n },\n {\n label: '近7天',\n value: [\n dayUts().subtract(7, 'day').format(valueFormat.value),\n dayUts().format(valueFormat.value),\n ],\n },\n {\n label: '近30天',\n value: [\n dayUts().subtract(30, 'day').format(valueFormat.value),\n dayUts().format(valueFormat.value),\n ],\n },\n {\n label: '近90天',\n value: [\n dayUts().subtract(90, 'day').format(valueFormat.value),\n dayUts().format(valueFormat.value),\n ],\n },\n {\n label: '近一年',\n value: [\n dayUts().subtract(1, 'year').format(valueFormat.value),\n dayUts().format(valueFormat.value),\n ],\n },\n ]\n})\n\nconst rangeIndex = ref<number>(0)\nconst values = ref<string[]>(['', ''])\nconst value = ref<number[]>([])\n\nconst start = computed(() => {\n if (props.rangeable) {\n if (rangeIndex.value == 0) {\n return props.start\n }\n else {\n // 结束日期模式下,必须大于等于已选的开始日期;若未选开始日期则使用全局开始日期\n return values.value[0] || props.start\n }\n }\n else {\n return props.start\n }\n})\n\n// 时间选择器列表,动态生成每一列的选项\nconst list = computed(() => {\n const [startYear, startMonth, startDate, startHour, startMinute, startSecond] = dayUts(start.value).toArray()\n const [endYear, endMonth, endDate, endHour, endMinute, endSecond] = dayUts(props.end).toArray()\n const arr = [[], [], [], [], [], []] as SelectOption[][]\n\n if (isEmpty(value.value)) {\n return arr\n }\n\n // 获取当前选中的各个分量,用于判断后续列的边界\n const [year, month, date, hour, minute] = value.value\n\n // 1. 年份列\n for (let y = startYear; y <= endYear; y++) {\n arr[0].push({ label: y.toString(), value: y })\n }\n\n // 2. 月份列\n const sM = (year === startYear) ? startMonth : 1\n const eM = (year === endYear) ? endMonth : 12\n for (let m = sM; m <= eM; m++) {\n arr[1].push({ label: m.toString().padStart(2, '0'), value: m })\n }\n\n // 3. 日期列\n const isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0\n const daysInMonth = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] || 31\n const sD = (year === startYear && month === startMonth) ? startDate : 1\n const eD = (year === endYear && month === endMonth) ? endDate : daysInMonth\n for (let d = sD; d <= eD; d++) {\n arr[2].push({ label: d.toString().padStart(2, '0'), value: d })\n }\n\n // 4. 小时列\n const sH = (year === startYear && month === startMonth && date === startDate) ? startHour : 0\n const eH = (year === endYear && month === endMonth && date === endDate) ? endHour : 23\n for (let h = sH; h <= eH; h++) {\n arr[3].push({ label: h.toString().padStart(2, '0'), value: h })\n }\n\n // 5. 分钟列\n const smm = (year === startYear && month === startMonth && date === startDate && hour === sH) ? startMinute : 0\n const emm = (year === endYear && month === endMonth && date === endDate && hour === eH) ? endMinute : 59\n for (let m = smm; m <= emm; m++) {\n arr[4].push({ label: m.toString().padStart(2, '0'), value: m })\n }\n\n // 6. 秒钟列\n const ss = (year === startYear && month === startMonth && date === startDate && hour === sH && minute === smm) ? startSecond : 0\n const es = (year === endYear && month === endMonth && date === endDate && hour === eH && minute === emm) ? endSecond : 59\n for (let s = ss; s <= es; s++) {\n arr[5].push({ label: s.toString().padStart(2, '0'), value: s })\n }\n\n return arr\n})\n\nconst columnNum = computed(() => {\n return (['year', 'month', 'date', 'hour', 'minute', 'second'].findIndex(e => e == props.type) + 1)\n})\n\nconst columns = computed(() => {\n return list.value.slice(0, columnNum.value)\n})\n\nconst indexes = computed(() => {\n if (isEmpty(value.value)) {\n return []\n }\n\n return value.value.map((e, i) => {\n let index = list.value[i].findIndex(a => a.value == e) as number\n if (index == -1) { index = list.value[i].length - 1 }\n if (index < 0) { index = 0 }\n return index\n })\n})\n\nfunction toDate() {\n const parts: string[] = []\n const units = ['', '-', '-', ' ', ':', ':']\n const defaultValue = [2000, 1, 1, 0, 0, 0]\n\n units.forEach((key, i) => {\n let val = value.value[i]\n if (i >= columnNum.value) { val = defaultValue[i] }\n parts.push(key + val.toString().padStart(2, '0'))\n })\n return parts.join('')\n}\n\nfunction checkDate(values: number[]): number[] {\n if (values.length == 0) { return values }\n\n const checkedValues = [...values]\n const defaultValues = [2000, 1, 1, 0, 0, 0]\n for (let i = checkedValues.length; i < 6; i++) {\n checkedValues.push(defaultValues[i])\n }\n\n // 修复可能出现的 NaN 情况\n for (let i = 0; i < 6; i++) {\n if (isNaN(checkedValues[i])) {\n checkedValues[i] = defaultValues[i]\n }\n }\n\n let [year, month, date, hour, minute, second] = checkedValues\n const isLeapYear = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0\n const daysInMonth = [31, isLeapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\n const maxDay = daysInMonth[month > 0 ? month - 1 : 0] || 31\n\n if (month < 1) { month = 1 }\n else if (month > 12) { month = 12 }\n\n if (date < 1) { date = 1 }\n else if (date > maxDay) { date = maxDay }\n\n if (hour < 0) { hour = 0 }\n else if (hour > 23) { hour = 23 }\n if (minute < 0) { minute = 0 }\n else if (minute > 59) { minute = 59 }\n if (second < 0) { second = 0 }\n else if (second > 59) { second = 59 }\n\n return [year, month, date, hour, minute, second]\n}\n\nconst text = ref('')\n\nfunction updateText() {\n if (props.rangeable) {\n text.value = values.value\n .filter(e => e)\n .map(e => dayUts(e).format(labelFormat.value))\n .join(` ${props.rangeSeparator} `)\n }\n else {\n if (!props.modelValue) {\n text.value = ''\n }\n else { text.value = dayUts(toDate()).format(labelFormat.value) }\n }\n}\n\nasync function onChange(data: number[]) {\n value.value = checkDate([...data])\n\n if (dayUts(toDate()).isAfter(dayUts(props.end))) { value.value = dayUts(props.end).toArray() }\n if (dayUts(toDate()).isBefore(dayUts(props.start))) { value.value = dayUts(props.start).toArray() }\n\n if (props.rangeable) {\n values.value[rangeIndex.value] = dayUts(toDate()).format(valueFormat.value)\n\n if (dayUts(values.value[0]).isAfter(dayUts(values.value[1])) && values.value[1] != '') {\n values.value[1] = values.value[0]\n }\n\n shortcutsIndex.value = -1\n }\n}\n\nfunction setValue(val: string) {\n if (isNull(val) || isEmpty(val)) {\n value.value = checkDate(dayUts().toArray())\n text.value = ''\n }\n else {\n value.value = checkDate(dayUts(val).toArray())\n updateText()\n }\n}\n\nfunction setValues(val: string[]) {\n if (isEmpty(val)) {\n values.value = ['', '']\n text.value = ''\n }\n else {\n values.value = [...val]\n updateText()\n }\n}\n\nfunction setRange(index: number) {\n rangeIndex.value = index\n setValue(values.value[index])\n}\n\nfunction setRangeValue(val: string[], index: number) {\n shortcutsIndex.value = index\n values.value = [...val] as string[]\n setValue(val[rangeIndex.value])\n pickerKey.value += 1\n}\n\nconst visible = ref(false)\nconst pickerKey = ref(0)\nlet callback: ((value: string | string[]) => void) | null = null\n\nfunction open(cb: ((value: string | string[]) => void) | null = null) {\n if (isDisabled.value) { return }\n\n callback = cb\n // 先同步写好选中值,再打开弹窗,避免首帧用空 value 渲染\n nextTick(() => {\n if (props.rangeable) {\n rangeIndex.value = 0\n setValues(props.values)\n setValue(values.value[0])\n // 打开时若无已选范围,滚轮会显示当前时间,把当前展示同步到 values 以便直接点确定即可选中\n const currentStr = dayUts(toDate()).format(valueFormat.value)\n if (!values.value[0])\n values.value[0] = currentStr\n if (!values.value[1])\n values.value[1] = values.value[0]\n }\n else {\n setValue(props.modelValue)\n }\n visible.value = true\n pickerKey.value += 1\n })\n}\n\nfunction close() {\n visible.value = false\n}\n\nfunction clear() {\n text.value = ''\n\n if (props.rangeable) {\n emit('update:values', [] as string[])\n emit('range-change', [] as string[])\n }\n else {\n emit('update:modelValue', '')\n emit('change', '')\n }\n if (validate) { validate('change') }\n}\n\nfunction confirm() {\n if (props.rangeable) {\n const [a, b] = values.value\n\n if (a == '' || b == '') {\n uni.showToast({ title: '请选择完整时间范围', icon: 'none' })\n if (a != '') { rangeIndex.value = 1 }\n return\n }\n\n if (dayUts(a).isAfter(dayUts(b))) {\n uni.showToast({ title: '开始日期不能大于结束日期', icon: 'none' })\n return\n }\n\n emit('update:values', values.value)\n emit('range-change', values.value)\n if (validate) { validate('change') }\n\n if (callback != null) { callback!(values.value as string[]) }\n }\n else {\n // 打开时若无初始值,内部已用当前时间作为选中;确认时若 value 仍为空则兜底为当前时间,确保能赋上值\n if (isEmpty(value.value)) {\n value.value = checkDate(dayUts().toArray())\n }\n const val = dayUts(toDate()).format(valueFormat.value)\n\n emit('update:modelValue', val)\n emit('change', val)\n if (validate) { validate('change') }\n\n if (callback != null) { callback!(val) }\n }\n\n updateText()\n // 非范围模式:确认后用当前选中值更新展示(props 未同步时避免文案被清空)\n if (!props.rangeable && !isEmpty(value.value)) {\n text.value = dayUts(toDate()).format(labelFormat.value)\n }\n close()\n}\n\nwatch(\n computed(() => props.modelValue),\n (val: string) => {\n if (!props.rangeable) {\n setValue(val)\n }\n },\n { immediate: true },\n)\n\nwatch(\n computed(() => props.values),\n (val: string[]) => {\n if (props.rangeable) {\n setValues(val)\n }\n },\n { immediate: true },\n)\n\nwatch(\n computed(() => props.labelFormat),\n () => {\n updateText()\n },\n)\n\ndefineExpose({\n open,\n close,\n clear,\n confirm,\n setValue,\n setValues,\n setRange,\n})\n</script>\n\n<template>\n <view>\n <RebornSelectTrigger v-if=\"showTrigger\" :placeholder=\"placeholder\" :disabled=\"isDisabled\" :focus=\"popupRef?.isOpen\"\n :text=\"text\" arrow-icon=\"i-lucide-calendar\" :ui=\"triggerUi\" :color=\"color\" :size=\"size\" :clearable=\"clearable\"\n @open=\"open()\" @clear=\"clear\">\n\n <template #default=\"{ showText, text, placeholder, ui }\">\n <!-- #ifndef MP-WEIXIN -->\n <slot name=\"tag\" />\n <!-- #endif -->\n <!-- #ifdef MP-WEIXIN -->\n <slot v-if=\"$slots.tag\" name=\"tag\" />\n <text v-else-if=\"showText\" :class=\"ui.text()\">{{ text }}</text>\n <text v-else :class=\"ui.placeholder()\">{{ placeholder }}</text>\n <!-- #endif -->\n </template>\n </RebornSelectTrigger>\n\n <RebornPopup ref=\"popupRef\" v-model=\"visible\" :title=\"title\" :ui=\"popupUi\" position=\"bottom\" :color=\"color\">\n <view @touchmove.stop class=\"bg-white\">\n <view v-if=\"rangeable\" :class=\"ui.rangeBox()\">\n <view v-if=\"showShortcuts\" :class=\"ui.shortcuts()\">\n <!-- #ifdef H5 -->\n <view v-for=\"(item, index) in shortcuts\" :key=\"index\"\n :class=\"ui.shortcutItem({ active: shortcutsIndex === index })\"\n @tap.stop=\"setRangeValue(item.value, index)\" @touchstart.stop @touchmove.stop @touchend.stop\n @touchcancel.stop>\n <text class=\"i-lucide-zap\" />\n <text>{{ item.label }}</text>\n </view>\n <!-- #endif -->\n <!-- #ifndef H5 -->\n <view v-for=\"(item, index) in shortcuts\" :key=\"index\"\n :class=\"ui.shortcutItem({ active: shortcutsIndex === index })\"\n @tap.stop=\"setRangeValue(item.value, index)\">\n <text class=\"i-lucide-zap\" />\n <text>{{ item.label }}</text>\n </view>\n <!-- #endif -->\n </view>\n\n <view :class=\"ui.rangeValues()\">\n <!-- #ifdef H5 -->\n <view :class=\"ui.rangeStart({\n class: rangeIndex == 0 ? rangeActiveStyle.start : '',\n })\" @tap.stop=\"setRange(0)\" @touchstart.stop @touchmove.stop @touchend.stop @touchcancel.stop>\n <RebornText v-if=\"values.length > 0 && values[0] != ''\" :ui=\"{ base: ui.rangeValueText() }\"\n :color=\"rangeIndex == 0 ? color : 'neutral'\">\n {{\n values[0] }}\n </RebornText>\n <RebornText v-else :ui=\"{ base: ui.rangePlaceholder() }\">\n {{ startPlaceholder\n }}\n </RebornText>\n </view>\n <!-- #endif -->\n <!-- #ifndef H5 -->\n <view :class=\"ui.rangeStart({\n class: rangeIndex == 0 ? rangeActiveStyle.start : '',\n })\" @tap.stop=\"setRange(0)\">\n <RebornText v-if=\"values.length > 0 && values[0] != ''\" :ui=\"{ base: ui.rangeValueText() }\"\n :color=\"rangeIndex == 0 ? color : 'neutral'\">\n {{\n values[0] }}\n </RebornText>\n <RebornText v-else :ui=\"{ base: ui.rangePlaceholder() }\">\n {{ startPlaceholder\n }}\n </RebornText>\n </view>\n <!-- #endif -->\n\n <RebornText :ui=\"{ base: ui.separator() }\">\n {{ rangeSeparator }}\n </RebornText>\n\n <!-- #ifdef H5 -->\n <view :class=\"ui.rangeEnd({\n class: rangeIndex == 1 ? rangeActiveStyle.end : '',\n })\" @tap.stop=\"setRange(1)\" @touchstart.stop @touchmove.stop @touchend.stop @touchcancel.stop>\n <RebornText v-if=\"values.length > 1 && values[1] != ''\" :ui=\"{ base: ui.rangeValueText() }\"\n :color=\"rangeIndex == 1 ? color : 'neutral'\">\n {{\n values[1] }}\n </RebornText>\n <RebornText v-else :ui=\"{ base: ui.rangePlaceholder() }\">\n {{ endPlaceholder\n }}\n </RebornText>\n </view>\n <!-- #endif -->\n <!-- #ifndef H5 -->\n <view :class=\"ui.rangeEnd({\n class: rangeIndex == 1 ? rangeActiveStyle.end : '',\n })\" @tap.stop=\"setRange(1)\">\n <RebornText v-if=\"values.length > 1 && values[1] != ''\" :ui=\"{ base: ui.rangeValueText() }\"\n :color=\"rangeIndex == 1 ? color : 'neutral'\">\n {{\n values[1] }}\n </RebornText>\n <RebornText v-else :ui=\"{ base: ui.rangePlaceholder() }\">\n {{ endPlaceholder\n }}\n </RebornText>\n </view>\n <!-- #endif -->\n </view>\n </view>\n\n <view>\n <!-- rangeable 时仅在 pickerKey>0 后渲染,避免首帧用空 indexes 导致 1970/1/1;非 rangeable 始终渲染 -->\n <RebornPickerView v-if=\"!rangeable || pickerKey > 0\" :key=\"pickerKey\" :headers=\"headers\" :value=\"indexes\"\n :columns=\"columns\" :ui=\"pickerUi\" :color=\"color\" @change-value=\"onChange\" />\n </view>\n <view :class=\"ui.footer()\">\n <view :class=\"ui.cancel()\">\n <RebornButton v-if=\"showCancel\" :size=\"size\" variant=\"outline\" :color=\"color\"\n :ui=\"{ base: ui.cancelButton() }\" block @tap=\"close\">\n {{ cancelText }}\n </RebornButton>\n </view>\n <view :class=\"ui.confirm()\">\n <RebornButton v-if=\"showConfirm\" :size=\"size\" variant=\"solid\" :color=\"color\"\n :ui=\"{ base: ui.confirmButton() }\" block @click=\"confirm\">\n {{ confirmText }}\n </RebornButton>\n </view>\n </view>\n </view>\n </RebornPopup>\n </view>\n</template>\n\n<style scoped></style>\n",
|
|
1920
|
+
"target": "uniapp"
|
|
1921
|
+
},
|
|
1922
|
+
{
|
|
1923
|
+
"path": "types.ts",
|
|
1924
|
+
"content": "export interface SelectDateShortcut {\r\n label: string\r\n value: string[]\r\n}\r\n",
|
|
1925
|
+
"target": "uniapp"
|
|
1926
|
+
}
|
|
1927
|
+
]
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
"name": "reborn-select-trigger",
|
|
1931
|
+
"dependencies": [
|
|
1932
|
+
"clsx"
|
|
1933
|
+
],
|
|
1934
|
+
"files": [
|
|
1935
|
+
{
|
|
1936
|
+
"path": "index.ts",
|
|
1937
|
+
"content": "export { default as RebornSelectTrigger } from './RebornSelectTrigger.vue'\r\n",
|
|
1938
|
+
"target": "uniapp"
|
|
1939
|
+
},
|
|
1940
|
+
{
|
|
1941
|
+
"path": "reborn-select-trigger.config.ts",
|
|
1942
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as selectTriggerColors, size as selectTriggerSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper:\r\n 'flex flex-row items-center w-full box-border rounded-lg bg-white dark:bg-gray-8 border border-solid border-gray-3 dark:border-gray-7 transition-[border-color] duration-200 px-2.5',\r\n content: 'flex-1 truncate text-gray-8 dark:text-gray-1',\r\n text: 'text-28 overflow-hidden text-ellipsis whitespace-nowrap',\r\n placeholder: 'text-28 text-gray-4',\r\n iconWrapper: 'flex flex-row items-center justify-center pl-2.5',\r\n clearIcon: 'text-gray-4 size-4',\r\n arrowIcon: 'text-gray-4 size-4 transition-transform duration-200 origin-center',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n wrapper: 'h-input-sm',\r\n text: 'text-24',\r\n placeholder: 'text-24',\r\n },\r\n md: {\r\n wrapper: 'h-input-md',\r\n text: 'text-28',\r\n placeholder: 'text-28',\r\n },\r\n lg: {\r\n wrapper: 'h-input-lg',\r\n text: 'text-32',\r\n placeholder: 'text-32',\r\n },\r\n },\r\n color: {\r\n primary: {},\r\n secondary: {},\r\n success: {},\r\n info: {},\r\n warning: {},\r\n error: {},\r\n neutral: {},\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: 'opacity-70 bg-gray-1 dark:bg-gray-7 pointer-events-none',\r\n text: 'text-gray-4',\r\n },\r\n },\r\n focus: {\r\n true: {\r\n arrowIcon: 'rotate-180',\r\n },\r\n },\r\n error: {\r\n true: {\r\n wrapper: 'border-error dark:border-error',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: 'primary' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-primary dark:border-primary' } },\r\n { color: 'secondary' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-secondary dark:border-secondary' } },\r\n { color: 'success' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-success dark:border-success' } },\r\n { color: 'info' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-info dark:border-info' } },\r\n { color: 'warning' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-warning dark:border-warning' } },\r\n { color: 'error' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-error dark:border-error' } },\r\n { color: 'neutral' as (typeof color)[number], focus: true as const, class: { wrapper: 'border-neutral dark:border-neutral' } },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as (typeof size)[number],\r\n color: 'primary' as (typeof color)[number],\r\n },\r\n}\r\n",
|
|
1943
|
+
"target": "uniapp"
|
|
1944
|
+
},
|
|
1945
|
+
{
|
|
1946
|
+
"path": "RebornSelectTrigger.vue",
|
|
1947
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { selectTriggerColors, selectTriggerSizes } from './reborn-select-trigger.config'\r\nimport { computed } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-select-trigger.config'\r\n\r\ndefineOptions({\r\n name: 'RebornSelectTrigger',\r\n})\r\n\r\nconst props = withDefaults(defineProps<SelectTriggerProps>(), {\r\n text: '',\r\n placeholder: '请选择',\r\n disabled: false,\r\n focus: false,\r\n size: 'md',\r\n color: 'primary',\r\n clearable: true,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'open'): void\r\n (e: 'clear'): void\r\n}>()\r\n\r\nexport interface SelectTriggerProps {\r\n /** 显示文本 */\r\n text?: string\r\n /** 占位符 */\r\n placeholder?: string\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 是否聚焦 */\r\n focus?: boolean\r\n /** 尺寸 */\r\n size?: (typeof selectTriggerSizes)[number]\r\n /** 颜色 */\r\n color?: (typeof selectTriggerColors)[number]\r\n clearable?: boolean\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n content: ClassValue\r\n text: ClassValue\r\n placeholder: ClassValue\r\n iconWrapper: ClassValue\r\n clearIcon: ClassValue\r\n arrowIcon: ClassValue\r\n }>\r\n /** 自定义 class */\r\n customClass?: any\r\n}\r\n\r\n// reborn-form 上下文\r\nconst { disabled, size, isError } = useFormInject(props)\r\n\r\nconst isDisabled = computed(() => disabled.value || props.disabled)\r\n\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: size.value,\r\n color: props.color,\r\n disabled: isDisabled.value,\r\n focus: props.focus,\r\n error: isError.value,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n content: (opts?: { class?: any }) =>\r\n styles.content({ class: cn(opts?.class, uiOverrides.value.content) }),\r\n text: (opts?: { class?: any }) =>\r\n styles.text({ class: cn(opts?.class, uiOverrides.value.text) }),\r\n placeholder: (opts?: { class?: any }) =>\r\n styles.placeholder({ class: cn(opts?.class, uiOverrides.value.placeholder) }),\r\n iconWrapper: (opts?: { class?: any }) =>\r\n styles.iconWrapper({ class: cn(opts?.class, uiOverrides.value.iconWrapper) }),\r\n clearIcon: (opts?: { class?: any }) =>\r\n styles.clearIcon({ class: cn(opts?.class, uiOverrides.value.clearIcon) }),\r\n arrowIcon: (opts?: { class?: any }) =>\r\n styles.arrowIcon({ class: cn(opts?.class, uiOverrides.value.arrowIcon) }),\r\n }\r\n})\r\n\r\n// 是否显示文本\r\nconst showText = computed(() => props.text !== '')\r\n\r\n// 清空\r\nfunction clear() {\r\n emit('clear')\r\n}\r\n\r\nconst slot = useSlots()\r\n// 打开\r\nfunction open() {\r\n if (isDisabled.value) { return }\r\n emit('open')\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ class: props.customClass })\" @tap=\"open\">\r\n <view :class=\"ui.content()\">\r\n\r\n <!-- #ifndef MP-WEIXIN -->\r\n <slot>\r\n <text v-if=\"showText\" :class=\"ui.text()\">{{ text }}</text>\r\n <text v-else :class=\"ui.placeholder()\">{{ placeholder }}</text>\r\n </slot>\r\n <!-- #endif -->\r\n <!-- #ifdef MP-WEIXIN -->\r\n <slot :showText=\"showText\" :text=\"text\" :placeholder=\"placeholder\" :ui=\"ui\">\r\n <text v-if=\"showText\" :class=\"ui.text()\">{{ text }}</text>\r\n <text v-else :class=\"ui.placeholder()\">{{ placeholder }}</text>\r\n </slot>\r\n <!-- #endif -->\r\n </view>\r\n\r\n <!-- 清空按钮 -->\r\n <view v-if=\"showText && !isDisabled && clearable\" :class=\"ui.iconWrapper()\" @tap.stop=\"clear\">\r\n <slot name=\"clear-icon\" :ui=\"ui.clearIcon()\">\r\n <view class=\"i-lucide-x-circle\" :class=\"ui.clearIcon()\" />\r\n </slot>\r\n </view>\r\n\r\n <!-- 箭头图标 -->\r\n <view v-if=\"!isDisabled && !showText\" :class=\"ui.iconWrapper()\">\r\n <slot name=\"arrow-icon\" :ui=\"ui.arrowIcon()\">\r\n <view class=\"i-lucide-chevron-down\" :class=\"ui.arrowIcon()\" />\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
1948
|
+
"target": "uniapp"
|
|
1949
|
+
}
|
|
1950
|
+
]
|
|
1951
|
+
},
|
|
1952
|
+
{
|
|
1953
|
+
"name": "reborn-slider",
|
|
1954
|
+
"dependencies": [
|
|
1955
|
+
"clsx"
|
|
1956
|
+
],
|
|
1957
|
+
"files": [
|
|
1958
|
+
{
|
|
1959
|
+
"path": "index.ts",
|
|
1960
|
+
"content": "export { default as RebornSlider } from \"./RebornSlider.vue\";\r\n",
|
|
1961
|
+
"target": "web"
|
|
1962
|
+
},
|
|
1963
|
+
{
|
|
1964
|
+
"path": "reborn-slider.config.ts",
|
|
1965
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { sizes as sliderSizes, colors as sliderColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"flex items-center w-full\",\r\n inner: \"flex-1 relative h-full flex items-center\",\r\n track: \"relative w-full rounded-full bg-gray-200 dark:bg-gray-700\",\r\n progress: \"absolute top-0 h-full rounded-full\",\r\n thumb:\r\n \"absolute rounded-full border-2 border-solid border-white pointer-events-none z-[1] shadow-[0_0_1px_1px_rgba(100,100,100,0.1)]\",\r\n thumbActive: \"z-[2]\",\r\n value: \"text-center w-[50px] text-gray-700 dark:text-gray-200\",\r\n },\r\n variants: {\r\n size: {\r\n sm: { track: \"h-1\", value: \"text-xs\" },\r\n md: { track: \"h-1.5\", value: \"text-sm\" },\r\n lg: { track: \"h-2\", value: \"text-base\" },\r\n },\r\n color: {\r\n primary: { progress: \"bg-primary\", thumb: \"bg-primary\" },\r\n secondary: { progress: \"bg-secondary\", thumb: \"bg-secondary\" },\r\n success: { progress: \"bg-success\", thumb: \"bg-success\" },\r\n info: { progress: \"bg-info\", thumb: \"bg-info\" },\r\n warning: { progress: \"bg-warning\", thumb: \"bg-warning\" },\r\n error: { progress: \"bg-error\", thumb: \"bg-error\" },\r\n neutral: { progress: \"bg-neutral\", thumb: \"bg-neutral\" },\r\n },\r\n disabled: {\r\n true: { wrapper: \"opacity-50 pointer-events-none\" },\r\n },\r\n },\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n",
|
|
1966
|
+
"target": "web"
|
|
1967
|
+
},
|
|
1968
|
+
{
|
|
1969
|
+
"path": "RebornSlider.vue",
|
|
1970
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, onMounted, ref, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { sliderColors, sliderSizes } from \"./reborn-slider.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\nexport interface SliderProps {\r\n modelValue?: number;\r\n values?: number[];\r\n min?: number;\r\n max?: number;\r\n step?: number;\r\n disabled?: boolean;\r\n showValue?: boolean;\r\n range?: boolean;\r\n size?: (typeof sliderSizes)[number];\r\n color?: (typeof sliderColors)[number];\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n inner: ClassValue;\r\n track: ClassValue;\r\n progress: ClassValue;\r\n thumb: ClassValue;\r\n thumbActive: ClassValue;\r\n value: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<SliderProps>(), {\r\n modelValue: 0,\r\n values: () => [0, 0],\r\n min: 0,\r\n max: 100,\r\n step: 1,\r\n disabled: false,\r\n showValue: false,\r\n range: false,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: number): void;\r\n (e: \"update:values\", value: number[]): void;\r\n (e: \"change\", value: number | number[]): void;\r\n (e: \"changing\", value: number | number[]): void;\r\n}>();\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\nconst ui = computed(() => {\r\n const styles = b({ size: props.size, color: props.color, disabled: props.disabled });\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n inner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n track: (opts?: { class?: any }) => styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n progress: (opts?: { class?: any }) => styles.progress({ class: cn(opts?.class, uiOverrides.value.progress) }),\r\n thumb: (opts?: { class?: any }) => styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n thumbActive: (opts?: { class?: any }) => styles.thumbActive({ class: cn(opts?.class, uiOverrides.value.thumbActive) }),\r\n value: (opts?: { class?: any }) => styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n };\r\n});\r\n\r\nconst value = ref(props.modelValue);\r\nconst rangeValue = ref([...props.values]);\r\nconst trackRef = ref<HTMLElement | null>(null);\r\nconst activeThumbIndex = ref(0);\r\n\r\nconst blockSize = computed(() => {\r\n switch (props.size) {\r\n case \"sm\": return 16;\r\n case \"lg\": return 24;\r\n default: return 20;\r\n }\r\n});\r\n\r\nconst percentage = computed(() => {\r\n if (props.range) return 0;\r\n return ((value.value - props.min) / (props.max - props.min)) * 100;\r\n});\r\n\r\nconst rangePercentage = computed(() => {\r\n if (!props.range) return { min: 0, max: 0 };\r\n const range = props.max - props.min;\r\n return {\r\n min: ((rangeValue.value[0] - props.min) / range) * 100,\r\n max: ((rangeValue.value[1] - props.min) / range) * 100,\r\n };\r\n});\r\n\r\nconst progressStyle = computed(() => {\r\n if (props.range) {\r\n return {\r\n left: `${rangePercentage.value.min}%`,\r\n width: `${rangePercentage.value.max - rangePercentage.value.min}%`,\r\n };\r\n }\r\n return { left: \"0%\", width: `${percentage.value}%` };\r\n});\r\n\r\nfunction thumbStyle(pct: number) {\r\n return {\r\n left: `calc(${pct}% - ${blockSize.value / 2}px)`,\r\n width: `${blockSize.value}px`,\r\n height: `${blockSize.value}px`,\r\n };\r\n}\r\n\r\nconst singleThumbStyle = computed(() => thumbStyle(percentage.value));\r\nconst minThumbStyle = computed(() => thumbStyle(rangePercentage.value.min));\r\nconst maxThumbStyle = computed(() => thumbStyle(rangePercentage.value.max));\r\n\r\nconst displayValue = computed(() => {\r\n if (props.range) return `${rangeValue.value[0]} - ${rangeValue.value[1]}`;\r\n return `${value.value}`;\r\n});\r\n\r\nfunction calculateValue(clientX: number): number {\r\n if (!trackRef.value) return props.min;\r\n const rect = trackRef.value.getBoundingClientRect();\r\n const pct = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\r\n let val = props.min + pct * (props.max - props.min);\r\n if (props.step > 0) {\r\n val = Math.round((val - props.min) / props.step) * props.step + props.min;\r\n }\r\n return Math.max(props.min, Math.min(props.max, val));\r\n}\r\n\r\nfunction determineActiveThumb(clientX: number): number {\r\n if (!props.range) return 0;\r\n const touchValue = calculateValue(clientX);\r\n const d0 = Math.abs(touchValue - rangeValue.value[0]);\r\n const d1 = Math.abs(touchValue - rangeValue.value[1]);\r\n return d0 <= d1 ? 0 : 1;\r\n}\r\n\r\nfunction updateValue(newValue: number | number[]) {\r\n if (props.range) {\r\n const arr = newValue as number[];\r\n const sorted = [Math.min(arr[0], arr[1]), Math.max(arr[0], arr[1])];\r\n rangeValue.value = sorted;\r\n emit(\"update:values\", sorted);\r\n emit(\"changing\", sorted);\r\n } else {\r\n const n = newValue as number;\r\n if (value.value !== n) {\r\n value.value = n;\r\n emit(\"update:modelValue\", n);\r\n emit(\"changing\", n);\r\n }\r\n }\r\n}\r\n\r\nfunction onPointerDown(e: PointerEvent) {\r\n if (props.disabled) return;\r\n e.preventDefault();\r\n (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);\r\n\r\n const val = calculateValue(e.clientX);\r\n if (props.range) {\r\n activeThumbIndex.value = determineActiveThumb(e.clientX);\r\n const updated = [...rangeValue.value];\r\n updated[activeThumbIndex.value] = val;\r\n updateValue(updated);\r\n } else {\r\n updateValue(val);\r\n }\r\n}\r\n\r\nfunction onPointerMove(e: PointerEvent) {\r\n if (props.disabled || !e.buttons) return;\r\n const val = calculateValue(e.clientX);\r\n if (props.range) {\r\n const updated = [...rangeValue.value];\r\n updated[activeThumbIndex.value] = val;\r\n updateValue(updated);\r\n } else {\r\n updateValue(val);\r\n }\r\n}\r\n\r\nfunction onPointerUp() {\r\n if (props.disabled) return;\r\n emit(\"change\", props.range ? rangeValue.value : value.value);\r\n}\r\n\r\nwatch(() => props.modelValue, (v) => { if (v !== value.value) value.value = Math.max(props.min, Math.min(props.max, v)); }, { immediate: true });\r\nwatch(() => props.values, (v) => { rangeValue.value = v.map(n => Math.max(props.min, Math.min(props.max, n))); }, { immediate: true });\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.wrapper({ class: props.class })\">\r\n <div :class=\"ui.inner()\" :style=\"{ height: `${blockSize + 4}px` }\">\r\n <div ref=\"trackRef\" :class=\"ui.track()\" @pointerdown=\"onPointerDown\" @pointermove=\"onPointerMove\"\r\n @pointerup=\"onPointerUp\" @pointercancel=\"onPointerUp\">\r\n <div :class=\"ui.progress()\" :style=\"progressStyle\" />\r\n </div>\r\n\r\n <!-- Single thumb -->\r\n <template v-if=\"!range\">\r\n <slot name=\"thumb\" :value=\"{ value: displayValue, style: singleThumbStyle }\">\r\n <div :class=\"ui.thumb()\" :style=\"singleThumbStyle\" />\r\n </slot>\r\n </template>\r\n\r\n <!-- Range thumbs -->\r\n <template v-if=\"range\">\r\n <div :class=\"[ui.thumb(), ui.thumbActive()]\" :style=\"minThumbStyle\" />\r\n <div :class=\"[ui.thumb(), ui.thumbActive()]\" :style=\"maxThumbStyle\" />\r\n </template>\r\n </div>\r\n\r\n <slot name=\"value\" :value=\"displayValue\">\r\n <span v-if=\"showValue\" :class=\"ui.value()\">{{ displayValue }}</span>\r\n </slot>\r\n </div>\r\n</template>\r\n",
|
|
1971
|
+
"target": "web"
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
"path": "index.ts",
|
|
1975
|
+
"content": "export { default as RebornSlider } from './RebornSlider.vue'\r\n",
|
|
1976
|
+
"target": "uniapp"
|
|
1977
|
+
},
|
|
1978
|
+
{
|
|
1979
|
+
"path": "reborn-slider.config.ts",
|
|
1980
|
+
"content": "const size = ['sm', 'md', 'lg'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport { color as sliderColors, size as sliderSizes }\r\n\r\nexport default {\r\n slots: {\r\n wrapper: 'flex flex-row items-center w-full overflow-visible',\r\n inner: 'flex-1 relative h-full flex flex-row items-center overflow-visible',\r\n picker: 'absolute left-0 w-full',\r\n track: 'relative w-full rounded-full overflow-visible bg-gray-3',\r\n progress: 'absolute top-0 h-full rounded-full',\r\n thumb:\r\n 'absolute rounded-full border-2 border-solid border-white pointer-events-none z-[1] shadow-[0_0_1px_1px_rgba(100,100,100,0.1)]',\r\n thumbActive: 'z-[2]',\r\n value: 'text-center w-[50px] dark:text-gray-1',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n track: 'h-1',\r\n value: 'text-[length:var(--text-size-24)]',\r\n },\r\n md: {\r\n track: 'h-1.5',\r\n value: 'text-[length:var(--text-size-28)]',\r\n },\r\n lg: {\r\n track: 'h-2',\r\n value: 'text-[length:var(--text-size-32)]',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n progress: 'bg-primary',\r\n thumb: 'bg-primary',\r\n },\r\n secondary: {\r\n progress: 'bg-secondary',\r\n thumb: 'bg-secondary',\r\n },\r\n success: {\r\n progress: 'bg-success',\r\n thumb: 'bg-success',\r\n },\r\n info: {\r\n progress: 'bg-info',\r\n thumb: 'bg-info',\r\n },\r\n warning: {\r\n progress: 'bg-warning',\r\n thumb: 'bg-warning',\r\n },\r\n error: {\r\n progress: 'bg-error',\r\n thumb: 'bg-error',\r\n },\r\n neutral: {\r\n progress: 'bg-neutral',\r\n thumb: 'bg-neutral',\r\n },\r\n },\r\n disabled: {\r\n true: {\r\n wrapper: 'opacity-50',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md' as (typeof size)[number],\r\n color: 'primary' as (typeof color)[number],\r\n },\r\n}\r\n",
|
|
1981
|
+
"target": "uniapp"
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
"path": "RebornSlider.vue",
|
|
1985
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { sliderColors, sliderSizes } from './reborn-slider.config'\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'\r\n\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-slider.config'\r\n\r\ndefineOptions({\r\n name: 'RebornSlider',\r\n})\r\n\r\nconst props = withDefaults(defineProps<SliderProps>(), {\r\n modelValue: 0,\r\n values: () => [0, 0],\r\n min: 0,\r\n max: 100,\r\n step: 1,\r\n disabled: false,\r\n // blockSize: 20,\r\n trackHeight: 4,\r\n showValue: false,\r\n range: false,\r\n size: 'md',\r\n color: 'primary',\r\n ui: () => ({}),\r\n})\r\n\r\nconst emit = defineEmits(['update:modelValue', 'update:values', 'change', 'changing'])\r\n\r\nexport interface SliderProps {\r\n /** v-model 绑定的值,单值模式使用 */\r\n modelValue?: number\r\n /** v-model:values 绑定的值,范围模式使用 */\r\n values?: number[]\r\n /** 最小值 */\r\n min?: number\r\n /** 最大值 */\r\n max?: number\r\n /** 步长 */\r\n step?: number\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 滑块的大小 */\r\n // blockSize?: number;\r\n /** 线的高度 */\r\n trackHeight?: number\r\n /** 是否显示当前值 */\r\n showValue?: boolean\r\n /** 是否启用范围选择 */\r\n range?: boolean\r\n /** 尺寸 */\r\n size?: typeof sliderSizes[number]\r\n /** 颜色 */\r\n color?: typeof sliderColors[number]\r\n /** 样式覆盖 */\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n inner: ClassValue\r\n picker: ClassValue\r\n track: ClassValue\r\n progress: ClassValue\r\n thumb: ClassValue\r\n thumbActive: ClassValue\r\n value: ClassValue\r\n }>\r\n /** 自定义 class */\r\n customClass?: any\r\n}\r\n\r\nconst { proxy } = getCurrentInstance()!\r\n\r\nconst blockSize = ref(20)\r\n// ui 样式系统\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: (size.value || props.size) as any,\r\n color: props.color,\r\n disabled: disabled.value,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any, disabled?: boolean }) =>\r\n styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n inner: (opts?: { class?: any }) =>\r\n styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n picker: (opts?: { class?: any }) =>\r\n styles.picker({ class: cn(opts?.class, uiOverrides.value.picker) }),\r\n track: (opts?: { class?: any }) =>\r\n styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n progress: (opts?: { class?: any }) =>\r\n styles.progress({ class: cn(opts?.class, uiOverrides.value.progress) }),\r\n thumb: (opts?: { class?: any }) =>\r\n styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n thumbActive: (opts?: { class?: any }) =>\r\n styles.thumbActive({ class: cn(opts?.class, uiOverrides.value.thumbActive) }),\r\n value: (opts?: { class?: any }) =>\r\n styles.value({ class: cn(opts?.class, uiOverrides.value.value) }),\r\n }\r\n})\r\n\r\n// reborn-form 上下文\r\nconst { disabled, size, validate } = useFormInject(props)\r\n\r\n// 当前滑块的值,单值模式\r\nconst value = ref<number>(props.modelValue)\r\n\r\n// 当前范围值,范围模式\r\nconst rangeValue = ref<number[]>([...props.values])\r\n\r\n// 轨道宽度(像素)\r\nconst trackWidth = ref<number>(0)\r\n\r\n// 轨道高度(像素)\r\nconst trackHeight = ref<number>(0)\r\n\r\n// 轨道左侧距离屏幕的距离(像素)\r\nconst trackLeft = ref<number>(0)\r\n\r\n// 当前活动的滑块索引(0: min, 1: max),仅在范围模式下使用\r\nconst activeThumbIndex = ref<number>(0)\r\n\r\n// 计算当前值在滑块轨道上的百分比位置(单值模式专用)\r\nconst percentage = computed(() => {\r\n if (props.range) { return 0 }\r\n return ((value.value - props.min) / (props.max - props.min)) * 100\r\n})\r\n\r\n// 计算范围模式下两个滑块的百分比位置\r\ninterface RangePercentage {\r\n min: number\r\n max: number\r\n}\r\n\r\nconst rangePercentage = computed<RangePercentage>(() => {\r\n if (!props.range) { return { min: 0, max: 0 } }\r\n\r\n const currentValues = rangeValue.value\r\n const valueRange = props.max - props.min\r\n\r\n const minPercent = ((currentValues[0] - props.min) / valueRange) * 100\r\n const maxPercent = ((currentValues[1] - props.min) / valueRange) * 100\r\n\r\n return { min: minPercent, max: maxPercent }\r\n})\r\n\r\n// 计算进度条的样式属性\r\nconst progressStyle = computed(() => {\r\n const style: any = {}\r\n const halfBlock = blockSize.value / 2\r\n\r\n if (props.range) {\r\n // 范围模式:从左滑块中心到右滑块中心\r\n const minPos = (rangePercentage.value.min / 100) * (trackWidth.value - blockSize.value) + halfBlock\r\n const maxPos = (rangePercentage.value.max / 100) * (trackWidth.value - blockSize.value) + halfBlock\r\n style.left = `${minPos}px`\r\n style.width = `${maxPos - minPos}px`\r\n }\r\n else {\r\n // 单值模式:从轨道起点到滑块中心\r\n // 这里的计算公式要和 createThumbStyle 保持一致\r\n const thumbLeft = (percentage.value / 100) * (trackWidth.value - blockSize.value)\r\n style.left = `0px`\r\n style.width = `${thumbLeft + halfBlock}px`\r\n }\r\n\r\n return style\r\n})\r\n\r\n// 创建滑块的定位样式(通用函数)\r\nfunction createThumbStyle(percentPosition: number) {\r\n const style: any = {}\r\n const effectiveTrackWidth = trackWidth.value - blockSize.value\r\n const leftPosition = (percentPosition / 100) * effectiveTrackWidth\r\n\r\n // 使用 Math.max/min 防止越界\r\n const finalLeft = Math.max(0, Math.min(effectiveTrackWidth, leftPosition))\r\n\r\n style.left = `${finalLeft}px`\r\n style.width = `${blockSize.value}px`\r\n style.height = `${blockSize.value}px`\r\n style.top = `${(blockSize.value / 2 - trackHeight.value) / 2}px`\r\n style.position = 'absolute'\r\n // 移除 flex,因为样式已经由 ui.thumb() 控制\r\n return style\r\n}\r\n\r\n// 单值模式滑块的样式\r\nconst singleThumbStyle = computed(() => {\r\n return createThumbStyle(percentage.value)\r\n})\r\n\r\n// 范围模式最小值滑块的样式\r\nconst minThumbStyle = computed(() => {\r\n return createThumbStyle(rangePercentage.value.min)\r\n})\r\n\r\n// 范围模式最大值滑块的样式\r\nconst maxThumbStyle = computed(() => {\r\n return createThumbStyle(rangePercentage.value.max)\r\n})\r\n\r\n// 计算要显示的数值文本\r\nconst displayValue = computed<string>(() => {\r\n if (props.range) {\r\n const currentValues = rangeValue.value\r\n return `${currentValues[0]} - ${currentValues[1]}`\r\n }\r\n return `${value.value}`\r\n})\r\n\r\n// 获取滑块轨道的位置和尺寸信息\r\nfunction getTrackInfo(): Promise<void> {\r\n return new Promise((resolve) => {\r\n const query = uni.createSelectorQuery().in(proxy)\r\n\r\n // 同时选择轨道和滑块节点\r\n query.select('.reborn-slider__track').boundingClientRect()\r\n query.select('.reborn-slider__thumb-node').boundingClientRect()\r\n\r\n query.exec((res) => {\r\n const [trackNode, thumbNode] = res\r\n\r\n if (trackNode) {\r\n trackWidth.value = trackNode.width ?? 0\r\n trackHeight.value = trackNode.height ?? 0\r\n trackLeft.value = trackNode.left ?? 0\r\n }\r\n\r\n // 自动识别 slot 或 默认滑块的高度\r\n if (thumbNode && thumbNode.height > 0) {\r\n blockSize.value = thumbNode.height\r\n }\r\n\r\n resolve()\r\n })\r\n })\r\n}\r\n\r\n// 根据触摸点的横坐标计算对应的滑块数值\r\nfunction calculateValue(clientX: number): number {\r\n if (trackWidth.value == 0) { return props.min }\r\n\r\n const touchOffset = clientX - trackLeft.value\r\n const progressPercentage = Math.max(0, Math.min(1, touchOffset / trackWidth.value))\r\n const valueRange = props.max - props.min\r\n let calculatedValue = props.min + progressPercentage * valueRange\r\n\r\n if (props.step > 0) {\r\n calculatedValue\r\n = Math.round((calculatedValue - props.min) / props.step) * props.step + props.min\r\n }\r\n\r\n return Math.max(props.min, Math.min(props.max, calculatedValue))\r\n}\r\n\r\n// 在范围模式下,根据触摸点离哪个滑块更近来确定应该移动哪个滑块\r\nfunction determineActiveThumb(clientX: number): number {\r\n if (!props.range) { return 0 }\r\n\r\n const currentValues = rangeValue.value\r\n const touchValue = calculateValue(clientX)\r\n\r\n const distanceToMinThumb = Math.abs(touchValue - currentValues[0])\r\n const distanceToMaxThumb = Math.abs(touchValue - currentValues[1])\r\n\r\n return distanceToMinThumb <= distanceToMaxThumb ? 0 : 1\r\n}\r\n\r\n// 更新滑块的值,并触发相应的事件\r\nfunction updateValue(newValue: number | number[]) {\r\n if (props.range) {\r\n const newRangeValues = newValue as number[]\r\n const currentRangeValues = rangeValue.value\r\n\r\n // 移除交叉时切换activeThumbIndex的逻辑,保持用户选择的滑块不变\r\n const sortedValues = [\r\n Math.min(newRangeValues[0], newRangeValues[1]),\r\n Math.max(newRangeValues[0], newRangeValues[1]),\r\n ]\r\n\r\n if (JSON.stringify(currentRangeValues) !== JSON.stringify(sortedValues)) {\r\n rangeValue.value = sortedValues\r\n emit('update:values', sortedValues)\r\n emit('changing', sortedValues)\r\n }\r\n }\r\n else {\r\n const newSingleValue = newValue as number\r\n const currentSingleValue = value.value\r\n\r\n if (currentSingleValue !== newSingleValue) {\r\n value.value = newSingleValue\r\n emit('update:modelValue', newSingleValue)\r\n emit('changing', newSingleValue)\r\n }\r\n }\r\n}\r\n\r\n// 触摸开始事件\r\nasync function onTouchStart(e: TouchEvent) {\r\n if (disabled.value) { return }\r\n\r\n await getTrackInfo()\r\n\r\n nextTick(() => {\r\n const clientX = e.touches[0].clientX\r\n const calculatedValue = calculateValue(clientX)\r\n\r\n if (props.range) {\r\n activeThumbIndex.value = determineActiveThumb(clientX)\r\n const updatedValues = [...rangeValue.value]\r\n updatedValues[activeThumbIndex.value] = calculatedValue\r\n updateValue(updatedValues)\r\n }\r\n else {\r\n updateValue(calculatedValue)\r\n }\r\n })\r\n}\r\n\r\n// 触摸移动事件\r\nfunction onTouchMove(e: TouchEvent) {\r\n if (disabled.value) { return }\r\n\r\n const clientX = e.touches[0].clientX\r\n const calculatedValue = calculateValue(clientX)\r\n\r\n if (props.range) {\r\n const updatedValues = [...rangeValue.value]\r\n updatedValues[activeThumbIndex.value] = calculatedValue\r\n updateValue(updatedValues)\r\n }\r\n else {\r\n updateValue(calculatedValue)\r\n }\r\n}\r\n\r\n// 触摸结束事件\r\nfunction onTouchEnd() {\r\n if (disabled.value) { return }\r\n\r\n if (props.range) {\r\n emit('change', rangeValue.value)\r\n }\r\n else {\r\n emit('change', value.value)\r\n }\r\n if (validate) { validate('change') }\r\n}\r\n\r\nfunction setBlockSize() {\r\n if (props.size === 'sm') {\r\n blockSize.value = 16\r\n }\r\n else if (props.size === 'md') {\r\n blockSize.value = 20\r\n }\r\n else if (props.size === 'lg') {\r\n blockSize.value = 24\r\n }\r\n}\r\n\r\n// 监听外部传入的 modelValue 变化\r\nwatch(\r\n () => props.modelValue,\r\n (newModelValue: number) => {\r\n if (newModelValue !== value.value) {\r\n value.value = Math.max(props.min, Math.min(props.max, newModelValue))\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\n// 监听外部传入的 values 变化\r\nwatch(\r\n () => props.values,\r\n (newValues: number[]) => {\r\n rangeValue.value = newValues.map((singleValue) => {\r\n return Math.max(props.min, Math.min(props.max, singleValue))\r\n })\r\n },\r\n { immediate: true },\r\n)\r\n\r\n// 监听最大值变化\r\nwatch(\r\n () => props.max,\r\n (newMaxValue: number) => {\r\n if (props.range) {\r\n const currentRangeValues = rangeValue.value\r\n if (currentRangeValues[0] > newMaxValue || currentRangeValues[1] > newMaxValue) {\r\n updateValue([\r\n Math.min(currentRangeValues[0], newMaxValue),\r\n Math.min(currentRangeValues[1], newMaxValue),\r\n ])\r\n }\r\n }\r\n else {\r\n if (value.value > newMaxValue) {\r\n updateValue(newMaxValue)\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\n// 监听最小值变化\r\nwatch(\r\n () => props.min,\r\n (newMinValue: number) => {\r\n if (props.range) {\r\n const currentRangeValues = rangeValue.value\r\n if (currentRangeValues[0] < newMinValue || currentRangeValues[1] < newMinValue) {\r\n updateValue([\r\n Math.max(currentRangeValues[0], newMinValue),\r\n Math.max(currentRangeValues[1], newMinValue),\r\n ])\r\n }\r\n }\r\n else {\r\n if (value.value < newMinValue) {\r\n updateValue(newMinValue)\r\n }\r\n }\r\n },\r\n { immediate: true },\r\n)\r\n\r\nonMounted(() => {\r\n getTrackInfo()\r\n watch(\r\n () => [props.range, props.size, props.showValue],\r\n () => {\r\n nextTick(() => {\r\n setBlockSize()\r\n getTrackInfo()\r\n })\r\n },\r\n { deep: true },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.wrapper({ disabled, class: props.customClass })\">\r\n <view :class=\"ui.inner()\" :style=\"{ height: `${blockSize + 4}px` }\">\r\n <view class=\"reborn-slider__track\" :class=\"ui.track()\">\r\n <view :class=\"ui.progress()\" :style=\"progressStyle\" />\r\n </view>\r\n\r\n <!-- 单滑块模式 -->\r\n <template v-if=\"!range\">\r\n <slot name=\"thumb\" :value=\"{ value: displayValue, style: singleThumbStyle }\">\r\n <view class=\"reborn-slider__thumb-measure\" :class=\"ui.thumb()\" :style=\"singleThumbStyle\" />\r\n </slot>\r\n </template>\r\n\r\n <!-- 双滑块模式 -->\r\n <template v-if=\"range\">\r\n <view class=\"reborn-slider__thumb-measure\" :class=\"[ui.thumb(), ui.thumbActive()]\" :style=\"minThumbStyle\" />\r\n <view class=\"reborn-slider__thumb-measure\" :class=\"[ui.thumb(), ui.thumbActive()]\" :style=\"maxThumbStyle\" />\r\n </template>\r\n\r\n <view :class=\"ui.picker()\" :style=\"{ height: `${blockSize * 1.5}px` }\" @touchstart.prevent=\"onTouchStart\"\r\n @touchmove.prevent=\"onTouchMove\" @touchend=\"onTouchEnd\" @touchcancel=\"onTouchEnd\" />\r\n </view>\r\n\r\n <slot name=\"value\" :value=\"displayValue\">\r\n <text v-if=\"showValue\" :class=\"ui.value()\">\r\n {{ displayValue }}\r\n </text>\r\n </slot>\r\n </view>\r\n</template>\r\n",
|
|
1986
|
+
"target": "uniapp"
|
|
1987
|
+
}
|
|
1988
|
+
]
|
|
1989
|
+
},
|
|
1990
|
+
{
|
|
1991
|
+
"name": "reborn-sticky",
|
|
1992
|
+
"dependencies": [
|
|
1993
|
+
"@vueuse/core",
|
|
1994
|
+
"tailwind-variants"
|
|
1995
|
+
],
|
|
1996
|
+
"files": [
|
|
1997
|
+
{
|
|
1998
|
+
"path": "reborn-sticky.config.ts",
|
|
1999
|
+
"content": "export default {\r\n slots: {\r\n wrapper: \"reborn-sticky-wrapper relative\",\r\n content: \"w-full relative transition-[top] duration-200\",\r\n },\r\n variants: {\r\n sticky: {\r\n true: {\r\n content: \"fixed w-full z-50\",\r\n },\r\n false: {\r\n content: \"relative w-full\",\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n sticky: false,\r\n },\r\n} as const;\r\n",
|
|
2000
|
+
"target": "web"
|
|
2001
|
+
},
|
|
2002
|
+
{
|
|
2003
|
+
"path": "RebornSticky.vue",
|
|
2004
|
+
"content": "<template>\r\n <div ref=\"wrapperRef\" class=\"reborn-sticky-wrapper\" :class=\"ui.wrapper()\" :style=\"{\r\n height: isSticky ? rect.height + 'px' : 'auto',\r\n zIndex\r\n }\">\r\n <div ref=\"contentRef\" :class=\"ui.content()\" :style=\"{\r\n width: isSticky ? rect.width + 'px' : '100%',\r\n left: isSticky ? rect.left + 'px' : 0,\r\n top: stickyTop + 'px'\r\n }\">\r\n <slot :is-sticky=\"isSticky\"></slot>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref, reactive, watch } from 'vue';\r\nimport { tv } from 'tailwind-variants';\r\nimport { useWindowScroll, useElementBounding } from '@vueuse/core';\r\nimport theme from './reborn-sticky.config';\r\n\r\ndefineOptions({\r\n name: \"RebornSticky\"\r\n});\r\n\r\ndefineSlots<{\r\n default(props: { isSticky: boolean }): any;\r\n}>();\r\n\r\nexport interface RebornStickyProps {\r\n // 吸顶偏移量, 单位px\r\n offsetTop?: number;\r\n // 层级\r\n zIndex?: number;\r\n // 是否需要减去导航栏高度\r\n isNeedNavbarHeight?: boolean;\r\n // 导航栏高度\r\n navbarHeight?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornStickyProps>(), {\r\n offsetTop: 0,\r\n zIndex: 100,\r\n isNeedNavbarHeight: true,\r\n navbarHeight: 0 // Default for web might be 0 unless there's a fixed header\r\n});\r\n\r\nconst b = tv(theme);\r\n\r\nconst wrapperRef = ref<HTMLElement | null>(null);\r\n\r\n// Use VueUse for scroll and bounding\r\nconst { y: scrollTop } = useWindowScroll();\r\nconst { top: wrapperTop, height: wrapperHeight, width: wrapperWidth, left: wrapperLeft, update: updateRect } = useElementBounding(wrapperRef);\r\n\r\n// Reactive rect to store wrapper dimensions when sticky starts\r\nconst rect = reactive({\r\n height: 0,\r\n width: 0,\r\n left: 0,\r\n top: 0\r\n});\r\n\r\n// Threshold for becoming sticky\r\nconst stickyThreshold = computed(() => {\r\n let offset = props.offsetTop;\r\n if (props.isNeedNavbarHeight) {\r\n offset += props.navbarHeight;\r\n }\r\n return offset;\r\n});\r\n\r\n// Determine if sticky\r\nconst isSticky = computed(() => {\r\n if (!wrapperRef.value) return false;\r\n return wrapperTop.value <= stickyThreshold.value;\r\n});\r\n\r\nwatch(isSticky, (newValue) => {\r\n if (newValue) {\r\n rect.height = wrapperHeight.value;\r\n rect.width = wrapperWidth.value;\r\n rect.left = wrapperLeft.value;\r\n rect.top = wrapperTop.value + scrollTop.value;\r\n }\r\n});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n sticky: isSticky.value\r\n });\r\n return {\r\n wrapper: () => styles.wrapper(),\r\n content: () => styles.content(),\r\n };\r\n});\r\n\r\nconst stickyTop = computed(() => {\r\n return isSticky.value ? stickyThreshold.value : 0;\r\n});\r\n\r\n</script>\r\n",
|
|
2005
|
+
"target": "web"
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
"path": "index.ts",
|
|
2009
|
+
"content": "export { default as RebornSticky } from './RebornSticky.vue'\r\n",
|
|
2010
|
+
"target": "uniapp"
|
|
2011
|
+
},
|
|
2012
|
+
{
|
|
2013
|
+
"path": "reborn-sticky.config.ts",
|
|
2014
|
+
"content": "export default {\r\n slots: {\r\n wrapper: 'reborn-sticky-wrapper relative',\r\n content: 'w-full relative transition-[top] duration-200',\r\n },\r\n variants: {\r\n sticky: {\r\n true: {\r\n content: 'fixed w-full z-50',\r\n },\r\n false: {\r\n content: 'relative w-full',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n sticky: false,\r\n },\r\n} as const\r\n",
|
|
2015
|
+
"target": "uniapp"
|
|
2016
|
+
},
|
|
2017
|
+
{
|
|
2018
|
+
"path": "RebornSticky.vue",
|
|
2019
|
+
"content": "<script lang=\"ts\" setup>\r\nimport { computed, getCurrentInstance, onMounted, reactive, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { isHarmony } from '@/lib/device'\r\nimport theme from './reborn-sticky.config'\r\n\r\ndefineOptions({\r\n name: 'RebornSticky',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RebornStickyProps>(), {\r\n offsetTop: 0,\r\n zIndex: 100,\r\n scrollTop: 0,\r\n isNeedNavbarHeight: true,\r\n navbarHeight: 44,\r\n})\r\n\r\ndefineSlots<{\r\n default: (props: { isSticky: boolean }) => any\r\n}>()\r\n\r\nexport interface RebornStickyProps {\r\n // 吸顶偏移量, 单位px\r\n offsetTop?: number\r\n // 层级\r\n zIndex?: number\r\n // 滚动位置\r\n scrollTop?: number\r\n // 是否需要减去导航栏高度\r\n isNeedNavbarHeight?: boolean\r\n // 导航栏高度\r\n navbarHeight?: number\r\n}\r\n\r\nconst { proxy } = getCurrentInstance()!\r\n\r\nconst b = tv(theme)\r\n\r\n// 表示元素的位置信息\r\ninterface Rect {\r\n height: number // 高度\r\n width: number // 宽度\r\n left: number // 距离页面左侧的距离\r\n top: number // 距离页面顶部的距离\r\n}\r\n\r\n// 存储当前sticky元素的位置信息\r\nconst rect = reactive<Rect>({\r\n height: 0,\r\n width: 0,\r\n left: 0,\r\n top: 0,\r\n})\r\n\r\n// 记录初始位置是否已获取\r\nconst isInitialized = ref(false)\r\n\r\n// 当前页面滚动的距离\r\nconst scrollTop = ref(0)\r\n\r\n// 计算属性,判断当前是否处于吸顶状态\r\nconst isSticky = computed(() => {\r\n if (rect.height == 0) { return false }\r\n\r\n // 添加1px的容差,避免微信小程序滚动精度问题\r\n return scrollTop.value + 1 >= rect.top\r\n})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n sticky: isSticky.value,\r\n })\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: opts?.class }),\r\n content: (opts?: { class?: any }) => styles.content({ class: opts?.class }),\r\n }\r\n})\r\n\r\n// 计算属性,返回sticky元素的top值(吸顶时的偏移量)\r\nconst stickyTop = computed(() => {\r\n if (isSticky.value) {\r\n let v = 0\r\n\r\n // #ifdef H5\r\n // H5端默认导航栏高度为44\r\n if (props.isNeedNavbarHeight) {\r\n v = props.navbarHeight\r\n }\r\n // #endif\r\n\r\n return v + props.offsetTop\r\n }\r\n else {\r\n return 0\r\n }\r\n})\r\n\r\n\r\n// 获取安全区域高度\r\nfunction getSafeAreaTop(): number {\r\n const windowInfo = uni.getWindowInfo()\r\n return windowInfo?.safeAreaInsets?.top || 0\r\n}\r\n\r\nfunction isEmpty(value: any): boolean {\r\n if (Array.isArray(value)) {\r\n return (value as any[]).length == 0\r\n }\r\n\r\n if (typeof value === 'string') {\r\n return value == ''\r\n }\r\n\r\n if (typeof value === 'object' && value !== null) {\r\n return Object.keys(value).length == 0\r\n }\r\n\r\n return false\r\n}\r\n\r\n// 获取sticky元素的位置信息,并更新rect\r\nfunction getRect() {\r\n const next = () => {\r\n uni.createSelectorQuery()\r\n .in(proxy)\r\n .select('.reborn-sticky-wrapper')\r\n .boundingClientRect()\r\n .exec((nodes) => {\r\n if (isEmpty(nodes)) {\r\n return\r\n }\r\n\r\n const node = nodes[0] as UniApp.NodeInfo\r\n\r\n rect.height = node.height ?? 0\r\n rect.width = node.width ?? 0\r\n\r\n // 只在初始化时记录位置\r\n if (!isInitialized.value) {\r\n rect.left = node.left ?? 0\r\n // 记录元素距离页面顶部的绝对位置(不减去offsetTop)\r\n rect.top = (node.top ?? 0) + scrollTop.value\r\n isInitialized.value = true\r\n }\r\n })\r\n }\r\n\r\n if (isHarmony()) {\r\n setTimeout(() => {\r\n next()\r\n }, 300)\r\n }\r\n else {\r\n next()\r\n }\r\n}\r\n\r\nonMounted(() => {\r\n // 获取元素位置信息\r\n getRect()\r\n\r\n // 监听参数变化\r\n watch(\r\n () => props.scrollTop,\r\n (top: number) => {\r\n scrollTop.value = top\r\n },\r\n {\r\n immediate: true,\r\n },\r\n )\r\n})\r\n\r\ndefineExpose({\r\n getRect,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-sticky-wrapper\" :class=\"ui.wrapper()\" :style=\"{\r\n height: rect.height > 0 ? `${rect.height}px` : 'auto',\r\n zIndex,\r\n }\">\r\n <view :class=\"ui.content()\" :style=\"{\r\n width: isSticky ? `${rect.width}px` : 'auto',\r\n left: isSticky ? `${rect.left}px` : 'auto',\r\n top: isSticky ? `${stickyTop}px` : 'auto',\r\n }\">\r\n <slot :is-sticky=\"isSticky\" />\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
2020
|
+
"target": "uniapp"
|
|
2021
|
+
}
|
|
2022
|
+
]
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
"name": "reborn-switch",
|
|
2026
|
+
"dependencies": [
|
|
2027
|
+
"clsx"
|
|
2028
|
+
],
|
|
2029
|
+
"files": [
|
|
2030
|
+
{
|
|
2031
|
+
"path": "index.ts",
|
|
2032
|
+
"content": "export { default as RebornSwitch } from \"./RebornSwitch.vue\";\r\n",
|
|
2033
|
+
"target": "web"
|
|
2034
|
+
},
|
|
2035
|
+
{
|
|
2036
|
+
"path": "reborn-switch.config.ts",
|
|
2037
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { size as switchSizes, color as switchColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex items-center gap-3 cursor-pointer select-none\",\r\n input: \"peer sr-only\",\r\n track:\r\n \"relative inline-flex items-center rounded-full bg-gray-3 transition-colors ring-1 ring-transparent peer-focus-visible:ring-2 peer-focus-visible:ring-primary/40 peer-disabled:cursor-not-allowed peer-disabled:bg-gray-2 data-[loading=true]:cursor-wait data-[loading=true]:opacity-80\",\r\n thumb:\r\n \"absolute left-0.5 top-0.5 flex items-center justify-center rounded-full bg-white shadow transition-transform duration-200\",\r\n activeLabel: \"text-gray-8 dark:text-gray-1\",\r\n inactiveLabel: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n track: \"h-5 w-9 peer-checked:[&>span]:translate-x-4\",\r\n thumb: \"size-4\",\r\n activeLabel: \"text-[length:var(--text-size-24)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-24)]\",\r\n },\r\n md: {\r\n track: \"h-6 w-11 peer-checked:[&>span]:translate-x-5\",\r\n thumb: \"size-5\",\r\n activeLabel: \"text-[length:var(--text-size-26)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-26)]\",\r\n },\r\n lg: {\r\n track: \"h-7 w-14 peer-checked:[&>span]:translate-x-7\",\r\n thumb: \"size-6\",\r\n activeLabel: \"text-[length:var(--text-size-28)]\",\r\n inactiveLabel: \"text-[length:var(--text-size-28)]\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n track: \"peer-checked:bg-primary\",\r\n },\r\n secondary: {\r\n track: \"peer-checked:bg-secondary\",\r\n },\r\n success: {\r\n track: \"peer-checked:bg-success\",\r\n },\r\n info: {\r\n track: \"peer-checked:bg-info\",\r\n },\r\n warning: {\r\n track: \"peer-checked:bg-warning\",\r\n },\r\n error: {\r\n track: \"peer-checked:bg-error\",\r\n },\r\n neutral: {\r\n track: \"peer-checked:bg-neutral\",\r\n },\r\n },\r\n active: {\r\n true: {\r\n activeLabel: \"font-medium\",\r\n inactiveLabel: \"text-gray-4 dark:text-gray-6\",\r\n },\r\n false: {\r\n activeLabel: \"text-gray-4 dark:text-gray-6\",\r\n inactiveLabel: \"text-gray-9 dark:text-gray-1 font-medium\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n { color: \"primary\" as (typeof color)[number], active: true, class: { activeLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: true, class: { activeLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: true, class: { activeLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: true, class: { activeLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: true, class: { activeLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: true, class: { activeLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: true, class: { activeLabel: \"text-neutral\" } },\r\n // Inactive states\r\n { color: \"primary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-neutral\" } },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"primary\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
2038
|
+
"target": "web"
|
|
2039
|
+
},
|
|
2040
|
+
{
|
|
2041
|
+
"path": "RebornSwitch.vue",
|
|
2042
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, ref, useAttrs, watch } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { switchColors, switchSizes } from \"./reborn-switch.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({\r\n inheritAttrs: false,\r\n});\r\n\r\nexport interface SwitchProps {\r\n modelValue?: any;\r\n defaultValue?: any;\r\n activeValue?: any;\r\n inactiveValue?: any;\r\n activeLabel?: string;\r\n inactiveLabel?: string;\r\n disabled?: boolean;\r\n loading?: boolean;\r\n size?: typeof switchSizes[number];\r\n color?: typeof switchColors[number];\r\n beforeChange?: () => boolean | Promise<boolean>;\r\n class?: any;\r\n ui?: Partial<{\r\n wrapper: ClassValue;\r\n input: ClassValue;\r\n track: ClassValue;\r\n thumb: ClassValue;\r\n activeLabel: ClassValue;\r\n inactiveLabel: ClassValue;\r\n }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<SwitchProps>(), {\r\n activeValue: true,\r\n inactiveValue: false,\r\n disabled: false,\r\n loading: false,\r\n size: \"md\",\r\n color: \"primary\",\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: any): void;\r\n (e: \"change\", value: any): void;\r\n}>();\r\n\r\nconst attrs = useAttrs();\r\nconst inputRef = ref<HTMLInputElement>();\r\n\r\nconst localValue = ref(props.defaultValue ?? props.inactiveValue);\r\nconst isChecked = computed(() => {\r\n const val = props.modelValue !== undefined ? props.modelValue : localValue.value;\r\n return val === props.activeValue;\r\n});\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n color: props.color,\r\n active: isChecked.value,\r\n });\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n track: (opts?: { class?: any }) => styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n thumb: (opts?: { class?: any }) => styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n activeLabel: (opts?: { class?: any }) => styles.activeLabel({ class: cn(opts?.class, uiOverrides.value.activeLabel) }),\r\n inactiveLabel: (opts?: { class?: any }) => styles.inactiveLabel({ class: cn(opts?.class, uiOverrides.value.inactiveLabel) }),\r\n };\r\n});\r\n\r\nconst inputAttrs = computed(() => {\r\n const { class: _class, ...rest } = attrs;\r\n return rest;\r\n});\r\n\r\nfunction updateValue(checked: boolean) {\r\n const nextValue = checked ? props.activeValue : props.inactiveValue;\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue;\r\n }\r\n emit(\"update:modelValue\", nextValue);\r\n emit(\"change\", nextValue);\r\n}\r\n\r\nasync function handleClick(event: Event) {\r\n if (props.disabled || props.loading) return;\r\n\r\n // 阻止默认行为(防止 input 自动切换状态)\r\n event.preventDefault();\r\n\r\n const newChecked = !isChecked.value;\r\n\r\n if (props.beforeChange) {\r\n try {\r\n const result = await props.beforeChange();\r\n if (result === false) return;\r\n } catch (e) {\r\n return;\r\n }\r\n }\r\n\r\n updateValue(newChecked);\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value: any) => {\r\n if (value !== undefined) {\r\n localValue.value = value;\r\n }\r\n },\r\n);\r\n\r\ndefineExpose({\r\n focus: () => inputRef.value?.focus(),\r\n});\r\n</script>\r\n\r\n<template>\r\n <label :class=\"ui.wrapper({ class: props.class })\" :data-disabled=\"props.disabled || props.loading\"\r\n @click=\"handleClick\">\r\n\r\n <span v-if=\"props.inactiveLabel || $slots.inactiveLabel\" :class=\"ui.inactiveLabel()\">\r\n <slot name=\"inactiveLabel\">{{ props.inactiveLabel }}</slot>\r\n </span>\r\n\r\n <!-- 增加 @click.stop 防止事件冒泡造成的双重触发 -->\r\n <input ref=\"inputRef\" v-bind=\"inputAttrs\" type=\"checkbox\" :checked=\"isChecked\"\r\n :disabled=\"props.disabled || props.loading\" :class=\"ui.input()\" @click.stop />\r\n\r\n <span :class=\"ui.track()\" :data-loading=\"props.loading\">\r\n <span :class=\"ui.thumb()\">\r\n <slot name=\"thumb\" :checked=\"isChecked\" :loading=\"props.loading\">\r\n <Icon v-if=\"props.loading\" name=\"lucide:loader-2\" class=\"size-full p-0.5 animate-spin text-gray-400\" />\r\n </slot>\r\n </span>\r\n </span>\r\n\r\n <span v-if=\"props.activeLabel || $slots.activeLabel\" :class=\"ui.activeLabel()\">\r\n <slot name=\"activeLabel\">{{ props.activeLabel }}</slot>\r\n </span>\r\n </label>\r\n</template>\r\n",
|
|
2043
|
+
"target": "web"
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
"path": "index.ts",
|
|
2047
|
+
"content": "export { default as RebornSwitch } from './RebornSwitch.vue'\r\n",
|
|
2048
|
+
"target": "uniapp"
|
|
2049
|
+
},
|
|
2050
|
+
{
|
|
2051
|
+
"path": "reborn-switch.config.ts",
|
|
2052
|
+
"content": "const size = [\"sm\", \"md\", \"lg\"] as const;\r\nconst color = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { size as switchSizes, color as switchColors };\r\n\r\nexport default {\r\n slots: {\r\n wrapper: \"inline-flex items-center gap-3 cursor-pointer select-none\",\r\n input: \"sr-only\",\r\n track:\r\n \"relative inline-flex items-center rounded-full bg-gray-3 transition-colors ring-1 ring-transparent\",\r\n thumb:\r\n \"absolute left-0.5 top-0.5 flex items-center justify-center rounded-full bg-white shadow transition-transform duration-200\",\r\n loading: \"size-full p-0.5 animate-spin text-gray-400 border-2 border-current border-t-transparent rounded-full\",\r\n activeLabel: \"text-gray-8 dark:text-gray-1\",\r\n inactiveLabel: \"text-gray-8 dark:text-gray-1\",\r\n },\r\n variants: {\r\n active: {\r\n true: {\r\n activeLabel: \"font-medium\",\r\n inactiveLabel: \"text-gray-4 dark:text-gray-6\",\r\n },\r\n false: {\r\n activeLabel: \"text-gray-4 dark:text-gray-6\",\r\n inactiveLabel: \"text-gray-9 dark:text-gray-1 font-medium\",\r\n },\r\n },\r\n size: {\r\n sm: {\r\n track: \"h-5 w-9\",\r\n thumb: \"size-4\",\r\n activeLabel: \"text-24\",\r\n inactiveLabel: \"text-24\",\r\n },\r\n md: {\r\n track: \"h-6 w-11\",\r\n thumb: \"size-5\",\r\n activeLabel: \"text-26\",\r\n inactiveLabel: \"text-26\",\r\n },\r\n lg: {\r\n track: \"h-7 w-14\",\r\n thumb: \"size-6\",\r\n activeLabel: \"text-28\",\r\n inactiveLabel: \"text-28\",\r\n },\r\n },\r\n color: {\r\n primary: {},\r\n secondary: {},\r\n success: {},\r\n info: {},\r\n warning: {},\r\n error: {},\r\n neutral: {},\r\n },\r\n },\r\n compoundVariants: [\r\n // 开启状态下的轨道颜色\r\n { color: \"primary\" as (typeof color)[number], active: true, class: { track: \"bg-primary\", activeLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: true, class: { track: \"bg-secondary\", activeLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: true, class: { track: \"bg-success\", activeLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: true, class: { track: \"bg-info\", activeLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: true, class: { track: \"bg-warning\", activeLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: true, class: { track: \"bg-error\", activeLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: true, class: { track: \"bg-neutral\", activeLabel: \"text-neutral\" } },\r\n\r\n // 关闭状态下的标签颜色\r\n { color: \"primary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-primary\" } },\r\n { color: \"secondary\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-secondary\" } },\r\n { color: \"success\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-success\" } },\r\n { color: \"info\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-info\" } },\r\n { color: \"warning\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-warning\" } },\r\n { color: \"error\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-error\" } },\r\n { color: \"neutral\" as (typeof color)[number], active: false, class: { inactiveLabel: \"text-neutral\" } },\r\n\r\n // 开启状态下根据尺寸进行的滑块位移\r\n { size: \"sm\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-4\" } },\r\n { size: \"md\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-5\" } },\r\n { size: \"lg\" as (typeof size)[number], active: true, class: { thumb: \"translate-x-7\" } },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof size)[number],\r\n color: \"primary\" as (typeof color)[number],\r\n },\r\n};\r\n",
|
|
2053
|
+
"target": "uniapp"
|
|
2054
|
+
},
|
|
2055
|
+
{
|
|
2056
|
+
"path": "RebornSwitch.vue",
|
|
2057
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from 'clsx'\r\nimport type { switchColors, switchSizes } from './reborn-switch.config'\r\nimport { computed, ref, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-switch.config'\r\n\r\ndefineOptions({\r\n name: 'RebornSwitch',\r\n inheritAttrs: false,\r\n})\r\n\r\nexport interface SwitchProps {\r\n modelValue?: any\r\n defaultValue?: any\r\n activeValue?: any\r\n inactiveValue?: any\r\n activeLabel?: string\r\n inactiveLabel?: string\r\n disabled?: boolean\r\n loading?: boolean\r\n size?: typeof switchSizes[number]\r\n color?: typeof switchColors[number]\r\n beforeChange?: () => boolean | Promise<boolean>\r\n customClass?: any\r\n ui?: Partial<{\r\n wrapper: ClassValue\r\n input: ClassValue\r\n track: ClassValue\r\n thumb: ClassValue\r\n loading: ClassValue\r\n activeLabel: ClassValue\r\n inactiveLabel: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<SwitchProps>(), {\r\n activeValue: true,\r\n inactiveValue: false,\r\n disabled: false,\r\n loading: false,\r\n size: 'md',\r\n color: 'primary',\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:modelValue', value: any): void\r\n (e: 'change', value: any): void\r\n}>()\r\n\r\nconst b = tv(theme as any)\r\n\r\nconst { disabled: formDisabled, size: formSize, isError, validate } = useFormInject(props)\r\n\r\nconst isDisabled = computed(() => formDisabled.value || props.disabled || props.loading)\r\n\r\nconst localValue = ref(props.defaultValue ?? props.inactiveValue)\r\nconst isChecked = computed(() => {\r\n const val = props.modelValue !== undefined ? props.modelValue : localValue.value\r\n return val === props.activeValue\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = (b as any)({\r\n size: formSize.value || props.size,\r\n color: props.color,\r\n active: isChecked.value,\r\n error: isError.value,\r\n loading: props.loading,\r\n })\r\n\r\n return {\r\n wrapper: (opts?: { class?: any }) => styles.wrapper({ class: cn(opts?.class, uiOverrides.value.wrapper) }),\r\n input: (opts?: { class?: any }) => styles.input({ class: cn(opts?.class, uiOverrides.value.input) }),\r\n track: (opts?: { class?: any }) => styles.track({ class: cn(opts?.class, uiOverrides.value.track) }),\r\n thumb: (opts?: { class?: any }) => styles.thumb({ class: cn(opts?.class, uiOverrides.value.thumb) }),\r\n loading: (opts?: { class?: any }) => styles.loading({ class: cn(opts?.class, uiOverrides.value.loading) }),\r\n activeLabel: (opts?: { class?: any }) => styles.activeLabel({ class: cn(opts?.class, uiOverrides.value.activeLabel) }),\r\n inactiveLabel: (opts?: { class?: any }) => styles.inactiveLabel({ class: cn(opts?.class, uiOverrides.value.inactiveLabel) }),\r\n }\r\n})\r\n\r\nfunction updateValue(checked: boolean) {\r\n const nextValue = checked ? props.activeValue : props.inactiveValue\r\n if (!props.disabled && !props.loading) {\r\n if (props.modelValue === undefined) {\r\n localValue.value = nextValue\r\n }\r\n emit('update:modelValue', nextValue)\r\n emit('change', nextValue)\r\n if (validate) { validate('change') }\r\n }\r\n}\r\n\r\nasync function onTap() {\r\n if (props.disabled || props.loading) return\r\n\r\n const originalChecked = isChecked.value\r\n const newChecked = !originalChecked\r\n\r\n if (props.beforeChange) {\r\n try {\r\n const result = await props.beforeChange()\r\n if (result === false) return\r\n } catch (e) {\r\n return\r\n }\r\n }\r\n\r\n updateValue(newChecked)\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (value) => {\r\n if (value !== undefined) {\r\n localValue.value = value\r\n }\r\n },\r\n)\r\n\r\nconst isFocused = ref(false)\r\ndefineExpose({\r\n focus: () => {\r\n isFocused.value = true\r\n },\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"group\" :class=\"[ui.wrapper({ class: props.customClass }), isChecked && `\r\n is-checked\r\n `, isFocused && 'is-focused']\" :data-disabled=\"isDisabled\" style=\"-webkit-tap-highlight-color: transparent;\"\r\n @tap=\"onTap\">\r\n <view v-if=\"props.inactiveLabel || $slots.inactiveLabel\" :class=\"ui.inactiveLabel()\">\r\n <slot name=\"inactiveLabel\">\r\n {{ props.inactiveLabel }}\r\n </slot>\r\n </view>\r\n\r\n <view :class=\"ui.track()\" :data-loading=\"props.loading\">\r\n <view :class=\"ui.thumb()\">\r\n <slot name=\"thumb\" :checked=\"isChecked\" :loading=\"props.loading\">\r\n <view v-if=\"props.loading\" :class=\"ui.loading()\" />\r\n </slot>\r\n </view>\r\n </view>\r\n\r\n <view v-if=\"props.activeLabel || $slots.activeLabel\" :class=\"ui.activeLabel()\">\r\n <slot name=\"activeLabel\">\r\n {{ props.activeLabel }}\r\n </slot>\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped></style>\r\n",
|
|
2058
|
+
"target": "uniapp"
|
|
2059
|
+
}
|
|
2060
|
+
]
|
|
2061
|
+
},
|
|
2062
|
+
{
|
|
2063
|
+
"name": "reborn-tabbar",
|
|
2064
|
+
"dependencies": [],
|
|
2065
|
+
"files": [
|
|
2066
|
+
{
|
|
2067
|
+
"path": "index.ts",
|
|
2068
|
+
"content": "export { default as RebornTabbar } from './RebornTabbar.vue'\r\nexport type { TabbarProps } from './RebornTabbar.vue'\r\n\r\nexport { default as RebornTabbarTrigger } from './RebornTabbarTrigger.vue'\r\nexport type { TabbarTriggerProps } from './RebornTabbarTrigger.vue'\r\n\r\nexport { TABBAR_KEY } from './types'\r\nexport type { TabbarItem, TabbarProvide } from './types'",
|
|
2069
|
+
"target": "uniapp"
|
|
2070
|
+
},
|
|
2071
|
+
{
|
|
2072
|
+
"path": "reborn-tabbar-trigger.config.ts",
|
|
2073
|
+
"content": "export const triggerColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'relative z-[2] flex-1 text-center no-underline h-full flex justify-center items-center cursor-pointer',\r\n body: 'relative z-[2] flex items-center flex-col gap-y-1 leading-none p-0',\r\n icon: 'relative grid place-items-center overflow-y-hidden transition-transform duration-300 ease-out',\r\n activeIcon: 'transition-all duration-300 ease-in-out col-start-1 row-start-1 flex items-center justify-center',\r\n inactiveIcon: 'transition-all duration-300 ease-in-out col-start-1 row-start-1 flex items-center justify-center',\r\n iconInner: 'text-40',\r\n title: 'text-24 transition-all duration-300 ease-in-out',\r\n glowLayer: 'absolute z-[1] w-full h-full rounded-[14rpx] transition-all duration-400 ease-in-out opacity-0',\r\n bodyGlowLayer: '[display:contents]',\r\n },\r\n variants: {\r\n active: {\r\n true: {\r\n title: 'opacity-100',\r\n },\r\n false: {\r\n title: 'opacity-100',\r\n },\r\n },\r\n shape: {\r\n normal: {\r\n body: 'flex-col',\r\n title: 'leading-[34rpx] mt-[4rpx]',\r\n },\r\n round: {\r\n root: 'transition-all duration-300 ease-in-out',\r\n body: 'flex-row rounded-full transition-all duration-300 ease-in-out px-[24rpx] h-[64rpx] flex items-center justify-center',\r\n title: 'transition-all duration-300 overflow-hidden whitespace-nowrap',\r\n }\r\n },\r\n animation: {\r\n fade: {},\r\n flip: {\r\n icon: '[perspective:1000px]',\r\n },\r\n reveal: {\r\n inactiveIcon: 'z-0',\r\n activeIcon: 'z-10',\r\n },\r\n creative: {},\r\n glass: {\r\n icon: 'w-[64rpx] h-[64rpx] grid !overflow-visible',\r\n activeIcon: 'absolute overflow-hidden z-[2] bottom-0 left-0 w-full h-full rounded-[12rpx] transition-all duration-400 ease-in-out box-border border border-solid border-transparent',\r\n inactiveIcon: 'absolute overflow-hidden z-[2] bottom-0 left-0 w-full h-full rounded-[12rpx] transition-all duration-400 ease-in-out bg-[#f5f3f7] box-border border border-solid border-transparent',\r\n },\r\n drop: {\r\n icon: '!overflow-visible',\r\n },\r\n 'fly-balls': {},\r\n },\r\n disabled: {\r\n true: {\r\n root: 'opacity-50 pointer-events-none',\r\n },\r\n false: '',\r\n },\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n },\r\n compoundVariants: [\r\n // 基础动画 (Animations)\r\n { animation: 'fade' as const, active: true, class: { activeIcon: 'opacity-100 scale-110', inactiveIcon: 'opacity-0 scale-90' } },\r\n { animation: 'fade' as const, active: false, class: { activeIcon: 'opacity-0 scale-90', inactiveIcon: 'opacity-100 scale-100' } },\r\n\r\n { animation: 'flip' as const, active: true, class: { activeIcon: 'opacity-100 [transform:perspective(1000px)_rotateY(0deg)]', inactiveIcon: 'opacity-0 [transform:perspective(1000px)_rotateY(-180deg)]' } },\r\n { animation: 'flip' as const, active: false, class: { activeIcon: 'opacity-0 [transform:perspective(1000px)_rotateY(180deg)]', inactiveIcon: 'opacity-100 [transform:perspective(1000px)_rotateY(0deg)]' } },\r\n\r\n { animation: 'reveal' as const, active: true, class: { inactiveIcon: 'opacity-0 z-10 [clip-path:inset(0_0_0_100%)] duration-1000', activeIcon: 'opacity-100 z-0 ' } },\r\n { animation: 'reveal' as const, active: false, class: { inactiveIcon: 'opacity-100 z-10 [clip-path:inset(0_0_0_0)]', activeIcon: 'opacity-0 z-0 duration-1000' } },\r\n\r\n { animation: 'creative' as const, active: true, class: { activeIcon: 'opacity-100 translate-y-0 scale-100', inactiveIcon: 'opacity-0 translate-y-full scale-50' } },\r\n { animation: 'creative' as const, active: false, class: { activeIcon: 'opacity-0 -translate-y-full scale-50', inactiveIcon: 'opacity-100 translate-y-0 scale-100' } },\r\n\r\n // fly-balls 动画: 激活时延迟 500ms 执行渐变,未激活时立刻执行渐变复原\r\n { animation: 'fly-balls' as const, active: true, class: { activeIcon: 'opacity-100 transition-all duration-300 delay-500', inactiveIcon: 'opacity-0 transition-all duration-300 delay-500' } },\r\n { animation: 'fly-balls' as const, active: false, class: { activeIcon: 'opacity-0 transition-all duration-300', inactiveIcon: 'opacity-100 transition-all duration-300' } },\r\n\r\n // Drop 动画: 只移动 icon,不带动标题一起上浮\r\n { animation: 'drop' as const, shape: 'normal' as const, active: true, class: { icon: '-translate-y-[18px]', activeIcon: 'opacity-100 scale-100 transition-all duration-300', inactiveIcon: 'opacity-0 scale-75 transition-all duration-200' } },\r\n { animation: 'drop' as const, shape: 'normal' as const, active: false, class: { icon: 'translate-y-0', activeIcon: 'opacity-0 scale-75 transition-all duration-200', inactiveIcon: 'opacity-100 scale-100 transition-all duration-300' } },\r\n { animation: 'drop' as const, shape: 'round' as const, active: true, class: { icon: 'translate-y-0', activeIcon: 'opacity-100 scale-100 transition-all duration-300', inactiveIcon: 'opacity-0 scale-75 transition-all duration-200' } },\r\n { animation: 'drop' as const, shape: 'round' as const, active: false, class: { icon: 'translate-y-0', activeIcon: 'opacity-0 scale-75 transition-all duration-200', inactiveIcon: 'opacity-100 scale-100 transition-all duration-300' } },\r\n\r\n // Glass 动画: 激活状态 — 前景层向左下方偏移,背景产生毛玻璃效果\r\n {\r\n animation: 'glass' as const,\r\n active: true,\r\n class: {\r\n activeIcon: 'opacity-100 !bottom-[-2px] !left-[-2px] bg-white/20 [backdrop-filter:blur(3px)]',\r\n inactiveIcon: 'opacity-0 !bottom-[-2px] !left-[-2px] bg-white/20 [backdrop-filter:blur(3px)]',\r\n glowLayer: 'opacity-100 relative [transform:scale(0.96)] [transform-origin:right_top] !top-[-2px] !right-[-2px] duration-1000',\r\n },\r\n },\r\n // Glass 动画: 未激活状态\r\n {\r\n animation: 'glass' as const,\r\n active: false,\r\n class: {\r\n activeIcon: 'opacity-0',\r\n inactiveIcon: 'opacity-100 bg-[#f5f3f7]',\r\n glowLayer: 'opacity-0',\r\n },\r\n },\r\n\r\n // Glass 动画 + 激活颜色特定样式: 特定颜色边框和渐变发光层 (使用 theme.css 中的 color-4 → color-1 渐变)\r\n {\r\n animation: 'glass' as const, active: true, color: 'primary' as const, class: {\r\n activeIcon: 'border-b-primary/10 border-l-primary/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'secondary' as const, class: {\r\n activeIcon: 'border-b-secondary/10 border-l-secondary/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-8),var(--color-gray-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'success' as const, class: {\r\n activeIcon: 'border-b-success/60 border-l-success/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-green-8),var(--color-green-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'info' as const, class: {\r\n activeIcon: 'border-b-info/30 border-l-info/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-blue-8),var(--color-blue-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'warning' as const, class: {\r\n activeIcon: 'border-b-warning/10 border-l-warning/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-orange-8),var(--color-orange-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'error' as const, class: {\r\n activeIcon: 'border-b-error/30 border-l-error/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]',\r\n }\r\n },\r\n {\r\n animation: 'glass' as const, active: true, color: 'neutral' as const, class: {\r\n activeIcon: 'border-b-neutral/30 border-l-neutral/10 border-t-white/20 border-r-white/20',\r\n glowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-6),var(--color-gray-3))]',\r\n }\r\n },\r\n\r\n // Round 形状: 基础布局 (展开与收缩表现)\r\n { shape: 'round' as const, active: true, class: { root: 'flex-[1.5]', title: 'ml-[8rpx] max-w-[200rpx] opacity-100 w-auto' } },\r\n { shape: 'round' as const, active: false, class: { root: 'flex-1', title: 'ml-0 max-w-0 opacity-0 !mx-0 w-0' } },\r\n\r\n // Round 形状: 激活状态背景颜色 (非 Glass 动画)\r\n { shape: 'round' as const, active: true, color: 'primary' as const, class: { body: 'bg-primary/20' } },\r\n { shape: 'round' as const, active: true, color: 'secondary' as const, class: { body: 'bg-secondary/10' } },\r\n { shape: 'round' as const, active: true, color: 'success' as const, class: { body: 'bg-success/10' } },\r\n { shape: 'round' as const, active: true, color: 'info' as const, class: { body: 'bg-info/10' } },\r\n { shape: 'round' as const, active: true, color: 'warning' as const, class: { body: 'bg-warning/10' } },\r\n { shape: 'round' as const, active: true, color: 'error' as const, class: { body: 'bg-error/10' } },\r\n { shape: 'round' as const, active: true, color: 'neutral' as const, class: { body: 'bg-neutral/40' } },\r\n\r\n // Drop + round: 小球动画结束后再显示 body 背景\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, class: { body: 'overflow-visible transition-colors duration-200 delay-[480ms]' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: false, class: { body: 'overflow-visible' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'primary' as const, class: { body: '!bg-primary/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'secondary' as const, class: { body: '!bg-secondary/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'success' as const, class: { body: '!bg-success/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'info' as const, class: { body: '!bg-info/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'warning' as const, class: { body: '!bg-warning/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'error' as const, class: { body: '!bg-error/30' } },\r\n { shape: 'round' as const, animation: 'drop' as const, active: true, color: 'neutral' as const, class: { body: '!bg-neutral/30' } },\r\n\r\n // Round 形状 + Glass 动画: bodyGlowLayer 作为外层彩色背景,body 本身产生毛玻璃和悬浮效果\r\n {\r\n shape: 'round' as const, animation: 'glass' as const, active: true, class: {\r\n bodyGlowLayer: '![display:block] rounded-full transition-all duration-400 ease-in-out',\r\n body: 'relative z-[2] bg-white/45 [backdrop-filter:blur(3px)] border border-solid border-transparent rounded-full transition-all duration-400 ease-in-out [transform:translate(-4rpx,4rpx)]',\r\n icon: 'w-auto h-auto grid overflow-y-hidden',\r\n activeIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-100 scale-110 [backdrop-filter:none]',\r\n inactiveIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-0 scale-90 [backdrop-filter:none]',\r\n glowLayer: '!hidden',\r\n },\r\n },\r\n {\r\n shape: 'round' as const, animation: 'glass' as const, active: false, class: {\r\n bodyGlowLayer: '![display:block] rounded-full transition-all duration-400 ease-in-out bg-transparent',\r\n body: 'relative z-[2] bg-transparent [backdrop-filter:none] border border-solid border-transparent rounded-full transition-all duration-400 ease-in-out [transform:translate(0,0)]',\r\n icon: 'w-auto h-auto grid overflow-y-hidden',\r\n activeIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none bg-transparent opacity-0 scale-90 [backdrop-filter:none]',\r\n inactiveIcon: 'relative overflow-visible z-auto bottom-auto left-auto w-auto h-auto rounded-none transition-all duration-300 ease-in-out border-none !bg-transparent opacity-100 scale-100 [backdrop-filter:none]',\r\n glowLayer: '!hidden',\r\n },\r\n },\r\n\r\n // Round 形状 + Glass 动画 + 激活颜色特定样式: bodyGlowLayer 应用相同的渐变背景,body 应用同色系边框\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'primary' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]', body: 'border-b-primary/10 border-l-primary/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'secondary' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-8),var(--color-gray-4))]', body: 'border-b-secondary/10 border-l-secondary/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'success' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-green-8),var(--color-green-4))]', body: 'border-b-success/10 border-l-success/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'info' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-blue-8),var(--color-blue-4))]', body: 'border-b-info/10 border-l-info/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'warning' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-orange-8),var(--color-orange-4))]', body: 'border-b-warning/10 border-l-warning/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'error' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-red-8),var(--color-red-4))]', body: 'border-b-error/10 border-l-error/10 border-t-white/20 border-r-white/20' } },\r\n { shape: 'round' as const, animation: 'glass' as const, active: true, color: 'neutral' as const, class: { bodyGlowLayer: '[background-image:linear-gradient(100deg,var(--color-gray-6),var(--color-gray-4))]', body: 'border-b-neutral/10 border-l-neutral/10 border-t-white/20 border-r-white/20' } },\r\n\r\n // 激活状态文字与图标颜色\r\n { active: true, color: 'primary' as const, class: { title: 'text-primary', activeIcon: 'text-primary' } },\r\n { active: true, color: 'secondary' as const, class: { title: 'text-secondary', activeIcon: 'text-secondary' } },\r\n { active: true, color: 'success' as const, class: { title: 'text-success', activeIcon: 'text-success' } },\r\n { active: true, color: 'info' as const, class: { title: 'text-info', activeIcon: 'text-info' } },\r\n { active: true, color: 'warning' as const, class: { title: 'text-warning', activeIcon: 'text-warning' } },\r\n { active: true, color: 'error' as const, class: { title: 'text-error', activeIcon: 'text-error' } },\r\n { active: true, color: 'neutral' as const, class: { title: 'text-neutral', activeIcon: 'text-neutral' } },\r\n\r\n // 未激活状态文字与图标颜色\r\n { active: false, class: { title: 'text-gray-5', inactiveIcon: 'text-gray-5' } },\r\n ],\r\n defaultVariants: {\r\n active: false,\r\n disabled: false,\r\n color: 'primary' as const,\r\n shape: 'normal' as const,\r\n animation: 'fade' as const,\r\n },\r\n}\r\n",
|
|
2074
|
+
"target": "uniapp"
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
"path": "reborn-tabbar.config.ts",
|
|
2078
|
+
"content": "export const tabbarColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const tabbarShapes = ['normal', 'round'] as const\r\nexport const tabbarAnimations = ['reveal', 'flip', 'creative', 'glass', 'fly-balls', 'drop'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'box-content',\r\n base: 'flex items-center flex-nowrap relative bg-white dark:bg-black h-[110rpx] box-border overflow-visible',\r\n dropBall: 'absolute z-1 top-[-16px] w-[44px] h-[44px] rounded-full pointer-events-none transform-gpu will-change-transform [backface-visibility:hidden] shadow-none',\r\n flyBallsContainer: 'absolute left-0 top-0 w-full h-full pointer-events-none z-10',\r\n flyBallItem: 'absolute pointer-events-none z-10',\r\n },\r\n variants: {\r\n shape: {\r\n normal: '',\r\n round: {\r\n base: 'mx-[32rpx] rounded-full shadow-[0_6px_30px_5px_rgba(0,0,0,0.05),0_16px_24px_2px_rgba(0,0,0,0.04),0_8px_10px_-5px_rgba(0,0,0,0.08)] dark:shadow-[0_6px_30px_5px_rgba(0,0,0,0.5),0_16px_24px_2px_rgba(0,0,0,0.4),0_8px_10px_-5px_rgba(0,0,0,0.6)]',\r\n },\r\n },\r\n pureIcon: {\r\n true: '',\r\n false: '',\r\n },\r\n fixed: {\r\n true: {\r\n base: 'fixed left-0 bottom-0 right-0 !z-[150]',\r\n },\r\n false: '',\r\n },\r\n bordered: {\r\n true: '',\r\n false: '',\r\n },\r\n safeAreaInsetBottom: {\r\n true: '',\r\n false: '',\r\n },\r\n animation: {\r\n reveal: '',\r\n flip: '',\r\n creative: '',\r\n glass: '',\r\n 'fly-balls': '',\r\n drop: '',\r\n },\r\n color: {\r\n primary: '',\r\n secondary: '',\r\n success: '',\r\n info: '',\r\n warning: '',\r\n error: '',\r\n neutral: '',\r\n },\r\n },\r\n compoundVariants: [\r\n // 默认形状 + 边框\r\n {\r\n shape: 'normal' as const,\r\n bordered: true,\r\n class: {\r\n base: 'border-t border-t-gray-3 border-solid',\r\n },\r\n },\r\n // 固定在底部 + 安全区适配 (适应 normal 和 round 的基础逻辑)\r\n {\r\n fixed: true,\r\n safeAreaInsetBottom: true,\r\n class: {\r\n base: 'box-content bottom-[env(safe-area-inset-bottom)]',\r\n },\r\n },\r\n // round 形状特有的安全区 padding (用于撑起空间)\r\n {\r\n shape: 'round' as const,\r\n fixed: true,\r\n safeAreaInsetBottom: true,\r\n class: {\r\n root: 'pb-[env(safe-area-inset-bottom)]',\r\n },\r\n },\r\n // normal 形状下使用 glass 动画增加默认高度\r\n {\r\n shape: 'normal' as const,\r\n animation: 'glass' as const,\r\n class: {\r\n base: 'h-[130rpx]',\r\n },\r\n },\r\n {\r\n pureIcon: true,\r\n class: {\r\n base: 'h-[90rpx]',\r\n },\r\n },\r\n\r\n // Drop 动画: 球体背景色跟随 color\r\n { animation: 'drop' as const, color: 'primary' as const, class: { dropBall: 'bg-primary/30' } },\r\n { animation: 'drop' as const, color: 'secondary' as const, class: { dropBall: 'bg-secondary/30' } },\r\n { animation: 'drop' as const, color: 'success' as const, class: { dropBall: 'bg-success/30' } },\r\n { animation: 'drop' as const, color: 'info' as const, class: { dropBall: 'bg-info/30' } },\r\n { animation: 'drop' as const, color: 'warning' as const, class: { dropBall: 'bg-warning/30' } },\r\n { animation: 'drop' as const, color: 'error' as const, class: { dropBall: 'bg-error/30' } },\r\n { animation: 'drop' as const, color: 'neutral' as const, class: { dropBall: 'bg-neutral/30' } },\r\n\r\n // round 模式下使用更小的共享指示球,避免顶部白层闪烁\r\n { shape: 'round' as const, animation: 'drop' as const, class: { dropBall: '!w-8 !h-8 !top-[-10px] shadow-none' } },\r\n ],\r\n defaultVariants: {\r\n shape: 'normal' as const,\r\n fixed: false,\r\n bordered: true,\r\n safeAreaInsetBottom: false,\r\n color: 'primary' as const,\r\n },\r\n}\r\n",
|
|
2079
|
+
"target": "uniapp"
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
"path": "RebornTabbar.vue",
|
|
2083
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-tabbar',\r\n options: {\r\n virtualHost: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch, type CSSProperties } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { tabbarAnimations, tabbarColors, tabbarShapes } from './reborn-tabbar.config'\r\nimport { useChildren } from '@/composables/useChildren'\r\nimport { TABBAR_KEY } from './types'\r\nimport type { TabbarItem } from './types'\r\n\r\nexport interface TabbarProps {\r\n modelValue?: number | string\r\n fixed?: boolean\r\n bordered?: boolean\r\n safeAreaInsetBottom?: boolean\r\n pureIcon?: boolean\r\n shape?: (typeof tabbarShapes)[number]\r\n animation?: (typeof tabbarAnimations)[number] | null\r\n ballColors?: string[]\r\n activeColor?: string\r\n inactiveColor?: string\r\n placeholder?: boolean\r\n zIndex?: number\r\n color?: (typeof tabbarColors)[number]\r\n customClass?: any\r\n customStyle?: string\r\n ui?: Partial<Record<'root' | 'base' | 'dropBall' | 'flyBallsContainer' | 'flyBallItem', string>>\r\n ballShiftY?: number\r\n beforeChange?: (params: { name: string | number }, done: (shouldProceed?: boolean) => void) => boolean | Promise<boolean> | void\r\n}\r\n\r\nconst props = withDefaults(defineProps<TabbarProps>(), {\r\n modelValue: 0,\r\n fixed: false,\r\n bordered: true,\r\n safeAreaInsetBottom: false,\r\n pureIcon: false,\r\n shape: 'normal',\r\n animation: null,\r\n placeholder: true,\r\n zIndex: 99,\r\n color: 'primary',\r\n customStyle: '',\r\n ballColors: () => ['#ff6675', '#ffb03b', '#35b6f2', '#3ac29e']\r\n})\r\n\r\nconst emit = defineEmits(['change', 'update:modelValue'])\r\n\r\nconst b = tv(theme)\r\nconst { proxy } = getCurrentInstance() as any\r\nconst { linkChildren, children } = useChildren(TABBAR_KEY)\r\n\r\nconst height = ref<number | string>('')\r\nconst locked = ref(false)\r\nconst isTransition = ref(false)\r\nconst oldIndexRef = ref(0)\r\nconst newIndexRef = ref(0)\r\nconst dropBallLeft = ref(0)\r\nconst dropBallStartLeft = ref(0)\r\nconst dropBallTop = ref(-16)\r\nconst dropBallSize = ref(44)\r\nconst dropBallReady = ref(false)\r\nconst dropBallAnimating = ref(false)\r\nconst pendingDropAnimation = ref(false)\r\nconst dropBallVisible = computed(() => {\r\n if (props.animation !== 'drop' || !dropBallReady.value) {\r\n return false\r\n }\r\n if (props.shape === 'round') {\r\n return dropBallAnimating.value\r\n }\r\n return true\r\n})\r\n\r\nlet transitionTimer: ReturnType<typeof setTimeout> | null = null\r\nlet layoutTimer: ReturnType<typeof setTimeout> | null = null\r\nlet unlockTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\nconst DROP_ANIMATION_DURATION = 480\r\n\r\nlinkChildren({\r\n props,\r\n setChange,\r\n locked,\r\n})\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n shape: props.shape as any,\r\n pureIcon: props.pureIcon,\r\n fixed: props.fixed,\r\n bordered: props.bordered,\r\n animation: props.animation as any,\r\n safeAreaInsetBottom: props.safeAreaInsetBottom,\r\n color: props.color as any,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n dropBall: (opts?: { class?: any }) => styles.dropBall({ class: cn(opts?.class, uiOverrides.value.dropBall) }),\r\n flyBallsContainer: (opts?: { class?: any }) => styles.flyBallsContainer({ class: cn(opts?.class, uiOverrides.value.flyBallsContainer) }),\r\n flyBallItem: (opts?: { class?: any }) => styles.flyBallItem({ class: cn(opts?.class, uiOverrides.value.flyBallItem) }),\r\n }\r\n})\r\n\r\nconst rootStyle = computed(() => {\r\n const style: CSSProperties = {}\r\n if (props.zIndex !== undefined) {\r\n style['z-index'] = props.zIndex\r\n }\r\n return style\r\n})\r\n\r\nconst placeholderStyle = computed(() => {\r\n if (props.fixed && props.placeholder && height.value) {\r\n return { height: `${height.value}px` }\r\n }\r\n return {}\r\n})\r\n\r\nconst presets = computed(() => {\r\n return props.ballColors.map((color, index) => ({\r\n top: 35 - index * 2,\r\n width: 12,\r\n height: 12,\r\n offsetXStart: index,\r\n shiftY: props.ballShiftY || -5,\r\n backgroundColor: color,\r\n }))\r\n})\r\n\r\nconst ballCount = computed(() => props.ballColors.length || 3)\r\n\r\nconst activeIndex = computed(() => {\r\n const index = children.findIndex((child: any, idx: number) => getChildName(child, idx) === props.modelValue)\r\n return index >= 0 ? index : 0\r\n})\r\n\r\nfunction getChildName(child: any, index: number) {\r\n const childProps = child?.$props || {}\r\n return childProps.name !== undefined ? childProps.name : index\r\n}\r\n\r\nfunction getDropMetrics(itemRect: any, baseRect: any) {\r\n const size = props.shape === 'round' ? 32 : 44\r\n const top = props.shape === 'round' ? -10 : -22\r\n return {\r\n left: itemRect.left - baseRect.left + itemRect.width / 2 - size / 2,\r\n top,\r\n size,\r\n }\r\n}\r\n\r\nfunction getFallbackRect(baseRect: any, index: number, count: number) {\r\n const safeCount = Math.max(count, 1)\r\n const width = baseRect.width / safeCount\r\n return {\r\n left: baseRect.left + width * index,\r\n width,\r\n height: baseRect.height,\r\n }\r\n}\r\n\r\nfunction syncDropIndicator(animate = false) {\r\n if (props.animation !== 'drop') {\r\n dropBallReady.value = false\r\n dropBallAnimating.value = false\r\n pendingDropAnimation.value = false\r\n return\r\n }\r\n\r\n const query = uni.createSelectorQuery().in(proxy)\r\n query.select('.reborn-tabbar-base').boundingClientRect()\r\n query.selectAll('.reborn-tabbar-trigger').boundingClientRect()\r\n query.exec((res: any[]) => {\r\n const baseRect = res?.[0]\r\n const rects = (res?.[1] || []).filter(Boolean)\r\n if (!baseRect) return\r\n\r\n const targetRect = rects[activeIndex.value] || getFallbackRect(baseRect, activeIndex.value, children.length || 1)\r\n const metrics = getDropMetrics(targetRect, baseRect)\r\n\r\n dropBallTop.value = metrics.top\r\n dropBallSize.value = metrics.size\r\n\r\n if (!dropBallReady.value) {\r\n dropBallReady.value = true\r\n dropBallStartLeft.value = metrics.left\r\n dropBallLeft.value = metrics.left\r\n return\r\n }\r\n\r\n if (!animate) {\r\n dropBallAnimating.value = false\r\n dropBallStartLeft.value = metrics.left\r\n dropBallLeft.value = metrics.left\r\n return\r\n }\r\n\r\n dropBallStartLeft.value = dropBallLeft.value\r\n dropBallAnimating.value = false\r\n setTimeout(() => {\r\n dropBallLeft.value = metrics.left\r\n dropBallAnimating.value = true\r\n }, 16)\r\n })\r\n}\r\n\r\nfunction scheduleLayoutSync(animate = false, delay = 0) {\r\n if (layoutTimer) clearTimeout(layoutTimer)\r\n layoutTimer = setTimeout(() => {\r\n nextTick(() => {\r\n syncDropIndicator(animate)\r\n setPlaceholderHeight()\r\n })\r\n }, delay)\r\n}\r\n\r\nwatch(\r\n () => [props.fixed, props.placeholder, props.shape, props.safeAreaInsetBottom, props.animation, props.bordered, props.pureIcon],\r\n () => {\r\n scheduleLayoutSync(false, 16)\r\n }\r\n)\r\n\r\nwatch(\r\n () => children.length,\r\n () => {\r\n scheduleLayoutSync(false, 16)\r\n }\r\n)\r\n\r\nwatch(\r\n () => props.modelValue,\r\n () => {\r\n const shouldAnimate = props.animation === 'drop' && pendingDropAnimation.value\r\n scheduleLayoutSync(shouldAnimate, shouldAnimate ? 16 : 0)\r\n pendingDropAnimation.value = false\r\n\r\n if (shouldAnimate) {\r\n if (unlockTimer) clearTimeout(unlockTimer)\r\n unlockTimer = setTimeout(() => {\r\n dropBallAnimating.value = false\r\n locked.value = false\r\n }, DROP_ANIMATION_DURATION)\r\n }\r\n }\r\n)\r\n\r\nonMounted(() => {\r\n scheduleLayoutSync(false, 32)\r\n})\r\n\r\nonUnmounted(() => {\r\n if (transitionTimer) clearTimeout(transitionTimer)\r\n if (layoutTimer) clearTimeout(layoutTimer)\r\n if (unlockTimer) clearTimeout(unlockTimer)\r\n})\r\n\r\nfunction setChange(child: TabbarItem) {\r\n const active = child.name\r\n\r\n if (active === props.modelValue) return\r\n if (locked.value) return\r\n\r\n const done = () => {\r\n const currentIndex = children.findIndex((c: any, i: number) => getChildName(c, i) === props.modelValue)\r\n oldIndexRef.value = currentIndex >= 0 ? currentIndex : 0\r\n\r\n emit('update:modelValue', active)\r\n emit('change', { value: active })\r\n\r\n if (props.animation === 'fly-balls') {\r\n const nextIdx = children.findIndex((c: any, i: number) => getChildName(c, i) === active)\r\n newIndexRef.value = nextIdx >= 0 ? nextIdx : 0\r\n\r\n locked.value = true\r\n isTransition.value = false\r\n setTimeout(() => {\r\n isTransition.value = true\r\n if (transitionTimer) clearTimeout(transitionTimer)\r\n transitionTimer = setTimeout(() => {\r\n isTransition.value = false\r\n locked.value = false\r\n }, 600)\r\n }, 20)\r\n } else if (props.animation === 'drop') {\r\n locked.value = true\r\n pendingDropAnimation.value = true\r\n } else {\r\n locked.value = false\r\n }\r\n }\r\n\r\n if (props.beforeChange && typeof props.beforeChange === 'function') {\r\n locked.value = true\r\n const wrappedDone = (shouldProceed: boolean = true) => {\r\n if (shouldProceed) {\r\n done()\r\n } else {\r\n locked.value = false\r\n }\r\n }\r\n const result = props.beforeChange({ name: active }, wrappedDone)\r\n if (result === true) {\r\n wrappedDone()\r\n } else if (result && typeof (result as Promise<any>).then === 'function') {\r\n ; (result as Promise<any>).then((isPass: boolean | void) => {\r\n if (isPass !== false) {\r\n wrappedDone()\r\n } else {\r\n locked.value = false\r\n }\r\n }).catch(() => {\r\n locked.value = false\r\n })\r\n } else if (result === false) {\r\n locked.value = false\r\n }\r\n } else {\r\n done()\r\n }\r\n}\r\n\r\nconst getBallStyle = (ballIndex: number) => {\r\n const count = children.length || 1\r\n const renderOldIndex = oldIndexRef.value\r\n const renderNewIndex = newIndexRef.value\r\n const startLeftPercent = ((renderOldIndex + 0.5) / count) * 100\r\n const endLeftPercent = ((renderNewIndex + 0.5) / count) * 100\r\n const p = presets.value[ballIndex % presets.value.length]\r\n const duration = ballCount.value * 0.2 + 0.1\r\n\r\n return {\r\n '--fly-ball-left': `calc(${endLeftPercent}%)`,\r\n '--fly-ball-start-left': `calc(${startLeftPercent}% + ${p.offsetXStart}px)`,\r\n '--fly-ball-top': `${p.top}rpx`,\r\n '--fly-ball-shift': `${p.shiftY}rpx`,\r\n '--fly-ball-jump': `-${40 + (ballIndex % 3) * 5}px`,\r\n '--fly-ball-duration': `${duration - ballIndex * 0.15}s`,\r\n left: 'var(--fly-ball-start-left)',\r\n top: 'var(--fly-ball-top)',\r\n width: `${p.width}rpx`,\r\n height: `${p.height}rpx`,\r\n borderRadius: '50%',\r\n backgroundColor: props.ballColors?.[ballIndex % count] || '#000',\r\n opacity: 0,\r\n }\r\n}\r\n\r\nfunction setPlaceholderHeight() {\r\n if (!props.fixed || !props.placeholder) {\r\n height.value = ''\r\n return\r\n }\r\n\r\n const query = uni.createSelectorQuery().in(proxy)\r\n query.select('.reborn-tabbar-base').boundingClientRect((res: any) => {\r\n if (res) {\r\n const extra = props.animation === 'drop' ? Math.max(-dropBallTop.value, 0) : 0\r\n height.value = Number(res.height) + extra\r\n }\r\n }).exec()\r\n}\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(customClass) })\" :style=\"placeholderStyle\">\r\n <view class=\"reborn-tabbar-base\" :class=\"ui.base()\" :style=\"[rootStyle, customStyle]\">\r\n <view v-if=\"dropBallVisible\" :class=\"[ui.dropBall(), { 'drop-ball-bounce': dropBallAnimating }]\" :style=\"{\r\n left: '0px',\r\n top: `${dropBallTop}px`,\r\n width: `${dropBallSize}px`,\r\n height: `${dropBallSize}px`,\r\n transform: `translate3d(${dropBallLeft}px, 0, 0)`,\r\n opacity: props.shape === 'round' ? (dropBallAnimating ? 1 : 0) : 1,\r\n '--drop-ball-start-x': `${dropBallStartLeft}px`,\r\n '--drop-ball-end-x': `${dropBallLeft}px`,\r\n '--drop-ball-peak': props.shape === 'round' ? '-26px' : '-34px'\r\n }\">\r\n </view>\r\n\r\n <view v-if=\"animation === 'fly-balls'\" :class=\"ui.flyBallsContainer()\">\r\n <view v-for=\"(color, index) in ballColors\" :key=\"index\"\r\n :class=\"[ui.flyBallItem(), isTransition ? 'fly-ball-anim-dynamic' : '']\"\r\n :style=\"getBallStyle(index)\">\r\n </view>\r\n </view>\r\n\r\n <slot />\r\n </view>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n@keyframes flyBallsJump {\r\n 0% {\r\n transform: translateY(0) scale(1) translateX(0);\r\n opacity: 0;\r\n left: var(--fly-ball-start-left);\r\n }\r\n\r\n 10% {\r\n opacity: 1;\r\n left: var(--fly-ball-start-left);\r\n }\r\n\r\n 50% {\r\n transform: translateY(var(--fly-ball-jump)) scale(1.1);\r\n opacity: 1;\r\n left: calc(var(--fly-ball-start-left) / 2 + var(--fly-ball-left) / 2);\r\n }\r\n\r\n 80% {\r\n transform: translateY(var(--fly-ball-shift)) scale(1.2) translateX(0);\r\n opacity: 1;\r\n left: var(--fly-ball-left);\r\n }\r\n\r\n 100% {\r\n transform: translateY(calc(var(--fly-ball-shift) + 15px)) scale(0) translateX(0);\r\n opacity: 0;\r\n left: var(--fly-ball-left);\r\n }\r\n}\r\n\r\n.fly-ball-anim-dynamic {\r\n animation: flyBallsJump var(--fly-ball-duration) ease-in-out forwards;\r\n}\r\n\r\n.drop-ball-bounce {\r\n animation: dropBallParabola 0.58s cubic-bezier(0.25, 0.9, 0.3, 1) both;\r\n}\r\n\r\n@keyframes dropBallParabola {\r\n 0% {\r\n transform: translate3d(var(--drop-ball-start-x), 0, 0);\r\n }\r\n\r\n 50% {\r\n transform: translate3d(calc((var(--drop-ball-start-x) + var(--drop-ball-end-x)) / 2), var(--drop-ball-peak), 0);\r\n }\r\n\r\n 100% {\r\n transform: translate3d(var(--drop-ball-end-x), 0, 0);\r\n }\r\n}\r\n</style>\r\n",
|
|
2084
|
+
"target": "uniapp"
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
2087
|
+
"path": "RebornTabbarTrigger.vue",
|
|
2088
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-tabbar-trigger',\r\n options: {\r\n virtualHost: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-tabbar-trigger.config'\r\nimport RebornImage from '@/components/reborn-image/RebornImage.vue'\r\nimport { useParent } from '@/composables/useChildren'\r\nimport { TABBAR_KEY } from '@/components/reborn-tabbar/types'\r\nimport type { tabbarShapes } from '@/components/reborn-tabbar/reborn-tabbar.config'\r\n\r\nexport interface TabbarTriggerProps {\r\n /** 标签页标题 */\r\n title?: string\r\n /** 唯一标识符 */\r\n name?: number | string\r\n /** 图标名称或图片链接 */\r\n icon?: string\r\n /** 未选中时的图标名称或图片链接 */\r\n inactive?: string\r\n /** 是否禁用 */\r\n disabled?: boolean\r\n /** 自定义样式类 */\r\n customClass?: any\r\n /** 自定义内联样式 */\r\n customStyle?: string\r\n /** 颜色 */\r\n color?: string\r\n /** 图片大小 */\r\n imageSize?: number\r\n /** UI 样式覆盖 */\r\n ui?: Partial<Record<'root' | 'body' | 'icon' | 'activeIcon' | 'inactiveIcon' | 'iconInner' | 'title' | 'glowLayer' | 'bodyGlowLayer', string>>\r\n}\r\n\r\nconst props = withDefaults(defineProps<TabbarTriggerProps>(), {\r\n disabled: false,\r\n customStyle: '',\r\n})\r\n\r\nconst { parent: tabbar, index } = useParent(TABBAR_KEY)\r\n\r\nconst active = computed(() => {\r\n const itemName = props.name !== undefined ? props.name : index.value\r\n if (tabbar) {\r\n return tabbar.props.modelValue === itemName\r\n }\r\n return false\r\n})\r\n\r\nconst parentColor = computed(() => {\r\n if (props.color) {\r\n return props.color\r\n }\r\n if (tabbar && tabbar.props.color) {\r\n return tabbar.props.color\r\n }\r\n return 'primary'\r\n})\r\n\r\nconst parentShape = computed(() => {\r\n if (tabbar && tabbar.props.shape) {\r\n return tabbar.props.shape as (typeof tabbarShapes)[number]\r\n }\r\n return 'normal'\r\n})\r\n\r\nconst parentAnimation = computed(() => {\r\n if (tabbar && tabbar.props.animation) {\r\n return tabbar.props.animation as 'fade' | 'flip' | 'reveal' | 'creative' | 'glass' | 'fly-balls' | 'drop'\r\n }\r\n return 'fade'\r\n})\r\n\r\nconst pureIcon = computed(() => Boolean(tabbar?.props.pureIcon))\r\nconst hasTitle = computed(() => Boolean(props.title))\r\nconst isNormalDrop = computed(() => parentShape.value === 'normal' && parentAnimation.value === 'drop')\r\nconst isRoundDropPureIcon = computed(() =>\r\n parentShape.value === 'round' && parentAnimation.value === 'drop' && pureIcon.value\r\n)\r\n\r\nconst rootExtraClass = computed(() => {\r\n if (parentShape.value === 'round' && pureIcon.value) {\r\n return '!flex-1'\r\n }\r\n return ''\r\n})\r\n\r\nconst bodyExtraClass = computed(() => {\r\n const classes: string[] = []\r\n if (parentShape.value === 'round' && pureIcon.value) {\r\n classes.push('!px-0 !w-[64rpx] !min-w-[64rpx]')\r\n }\r\n if (isRoundDropPureIcon.value) {\r\n if (active.value) {\r\n classes.push('shadow-[0_8px_20px_rgba(15,23,42,0.08)]')\r\n } else {\r\n classes.push('!bg-transparent shadow-none')\r\n }\r\n }\r\n return classes.join(' ')\r\n})\r\n\r\nconst iconExtraClass = computed(() => {\r\n if (!active.value) return ''\r\n if (isNormalDrop.value) {\r\n return pureIcon.value ? '!-translate-y-[22px]' : '!-translate-y-[18px]'\r\n }\r\n return ''\r\n})\r\n\r\nconst b = tv(theme)\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n active: active.value,\r\n disabled: props.disabled,\r\n color: parentColor.value as any,\r\n shape: parentShape.value as any,\r\n animation: parentAnimation.value as any,\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n body: (opts?: { class?: any }) => styles.body({ class: cn(opts?.class, uiOverrides.value.body) }),\r\n icon: (opts?: { class?: any }) => styles.icon({ class: cn(opts?.class, uiOverrides.value.icon) }),\r\n activeIcon: (opts?: { class?: any }) => styles.activeIcon({ class: cn(opts?.class, uiOverrides.value.activeIcon) }),\r\n inactiveIcon: (opts?: { class?: any }) => styles.inactiveIcon({ class: cn(opts?.class, uiOverrides.value.inactiveIcon) }),\r\n iconInner: (opts?: { class?: any }) => styles.iconInner({ class: cn(opts?.class, uiOverrides.value.iconInner) }),\r\n title: (opts?: { class?: any }) => styles.title({ class: cn(opts?.class, uiOverrides.value.title) }),\r\n glowLayer: (opts?: { class?: any }) => styles.glowLayer({ class: cn(opts?.class, uiOverrides.value.glowLayer) }),\r\n bodyGlowLayer: (opts?: { class?: any }) => styles.bodyGlowLayer({ class: cn(opts?.class, uiOverrides.value.bodyGlowLayer) }),\r\n }\r\n})\r\n\r\nconst textStyle = computed(() => {\r\n if (tabbar) {\r\n if (active.value && tabbar.props.activeColor) {\r\n return { color: tabbar.props.activeColor }\r\n }\r\n if (!active.value && tabbar.props.inactiveColor) {\r\n return { color: tabbar.props.inactiveColor }\r\n }\r\n }\r\n return {}\r\n})\r\n\r\nconst isImage = (name?: string) => {\r\n if (!name) return false\r\n return name.includes('/') || name.includes('.') || name.startsWith('http') || name.startsWith('data:image')\r\n}\r\n\r\nconst isShaking = ref(false)\r\nlet shakeTimer: any = null\r\n\r\nconst isJelly = ref(false)\r\n\r\nwatch(() => active.value, (newVal) => {\r\n if (parentAnimation.value === 'fly-balls') {\r\n if (newVal) {\r\n isJelly.value = true\r\n setTimeout(() => {\r\n isJelly.value = false\r\n }, 500)\r\n }\r\n }\r\n}, { immediate: true })\r\n\r\n/**\r\n * 点击 tabbar 选项\r\n */\r\nfunction handleClick() {\r\n if (props.disabled) return\r\n if (tabbar && tabbar.locked.value) return\r\n const itemName: string | number = props.name !== undefined ? props.name : index.value\r\n\r\n if (active.value) {\r\n if (shakeTimer) clearTimeout(shakeTimer)\r\n isShaking.value = false\r\n setTimeout(() => {\r\n isShaking.value = true\r\n shakeTimer = setTimeout(() => {\r\n isShaking.value = false\r\n }, 300)\r\n }, 10)\r\n return\r\n }\r\n\r\n if (tabbar) {\r\n tabbar.setChange({ name: itemName })\r\n }\r\n}\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabbar-trigger\" :class=\"ui.root({ class: cn(customClass, rootExtraClass) })\" :style=\"customStyle\"\r\n @click=\"handleClick\">\r\n <slot :active=\"active\" :ui=\"ui\">\r\n <!-- round + glass: bodyGlowLayer 包裹 body,提供颜色背景;默认 display:contents 不影响布局 -->\r\n <view :class=\"ui.bodyGlowLayer()\">\r\n <view :class=\"ui.body({ class: bodyExtraClass })\">\r\n <!-- Icon area with animation -->\r\n <view v-if=\"$slots.icon || icon\"\r\n :class=\"[ui.icon({ class: iconExtraClass }), isShaking ? 'animate-[shake_0.3s_ease-in-out]' : '', isJelly ? 'fly-balls-jelly' : '']\">\r\n <slot name=\"icon\" :active=\"active\" :ui=\"ui\">\r\n <!-- 选中时 -->\r\n <view :class=\"ui.activeIcon()\" :style=\"textStyle\">\r\n <RebornImage v-if=\"isImage(icon)\" :width=\"imageSize || 40\" :height=\"imageSize || 40\"\r\n :src=\"icon!\" mode=\"scaleToFill\" />\r\n <view v-else :class=\"['text-40', icon]\" />\r\n </view>\r\n <!-- 未选中时 -->\r\n <view :class=\"ui.inactiveIcon()\" :style=\"textStyle\">\r\n <RebornImage v-if=\"isImage(inactive || icon)\" :width=\"imageSize || 40\"\r\n :height=\"imageSize || 40\" :src=\"inactive! || icon!\" mode=\"scaleToFill\" />\r\n <view v-else :class=\"['text-40', inactive || icon]\" />\r\n </view>\r\n </slot>\r\n <!-- Glass 动画: icon 级别渐变光影背景层 (normal + glass) -->\r\n <view :class=\"ui.glowLayer()\" />\r\n </view>\r\n\r\n <!-- 标题 -->\r\n <view v-if=\"title && !pureIcon\" :class=\"ui.title()\" :style=\"textStyle\">\r\n {{ title }}\r\n </view>\r\n </view>\r\n </view>\r\n </slot>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n.fly-balls-jelly {\r\n animation: jelly 0.5s ease-in-out;\r\n}\r\n\r\n@keyframes jelly {\r\n\r\n 0%,\r\n 100% {\r\n transform: scale(1, 1);\r\n }\r\n\r\n 25% {\r\n transform: scale(1, 0.5);\r\n }\r\n\r\n 50% {\r\n transform: scale(0.75, 1);\r\n }\r\n\r\n 75% {\r\n transform: scale(1, 0.75);\r\n }\r\n}\r\n</style>\r\n",
|
|
2089
|
+
"target": "uniapp"
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
"path": "types.ts",
|
|
2093
|
+
"content": "import type { InjectionKey } from 'vue'\r\nimport type { tabbarColors, tabbarShapes } from './reborn-tabbar.config'\r\n\r\n/**\r\n * TabbarItem 子项数据结构\r\n */\r\nexport interface TabbarItem {\r\n name: string | number\r\n}\r\n\r\n/**\r\n * TabbarProvide - 提供给子组件的数据\r\n */\r\nexport type TabbarProvide = {\r\n props: {\r\n modelValue?: number | string\r\n fixed?: boolean\r\n safeAreaInsetBottom?: boolean\r\n bordered?: boolean\r\n pureIcon?: boolean\r\n shape?: (typeof tabbarShapes)[number] | null\r\n animation?: 'fade' | 'flip' | 'reveal' | 'creative' | 'glass' | 'fly-balls' | 'drop' | null\r\n activeColor?: string\r\n inactiveColor?: string\r\n placeholder?: boolean\r\n zIndex?: number\r\n color?: (typeof tabbarColors)[number]\r\n }\r\n setChange: (child: TabbarItem) => void\r\n locked: import('vue').Ref<boolean>\r\n}\r\n\r\nexport const TABBAR_KEY: InjectionKey<TabbarProvide> = Symbol('reborn-tabbar')\r\n",
|
|
2094
|
+
"target": "uniapp"
|
|
2095
|
+
}
|
|
2096
|
+
]
|
|
2097
|
+
},
|
|
2098
|
+
{
|
|
2099
|
+
"name": "reborn-tabs",
|
|
2100
|
+
"dependencies": [
|
|
2101
|
+
"@vueuse/core",
|
|
2102
|
+
"clsx"
|
|
2103
|
+
],
|
|
2104
|
+
"files": [
|
|
2105
|
+
{
|
|
2106
|
+
"path": "index.ts",
|
|
2107
|
+
"content": "export { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\nexport { default as TabsContent } from './TabsContent.vue'\r\n",
|
|
2108
|
+
"target": "web"
|
|
2109
|
+
},
|
|
2110
|
+
{
|
|
2111
|
+
"path": "reborn-tabs.config.ts",
|
|
2112
|
+
"content": "export const tabsTypes = [\"line\", \"card\"] as const;\r\nexport const tabsVariants = [\"primary\", \"info\", \"success\", \"warning\", \"neutral\"] as const;\r\nexport const tabsSizes = [\"sm\", \"md\", \"lg\"] as const;\r\nexport const tabsOrientations = [\"horizontal\", \"vertical\"] as const;\r\n\r\nexport default {\r\n slots: {\r\n root: \"flex flex-col gap-2 min-w-0\",\r\n list: \"relative flex max-w-full box-border gap-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]\",\r\n trigger:\r\n \"relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap px-3 py-2 text-32 font-medium text-gray-7 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 flex-1\",\r\n leadingIcon: \"flex items-center text-28\",\r\n leadingAvatar: \"flex items-center overflow-hidden rounded-full\",\r\n leadingAvatarSize: \"h-6 w-6\",\r\n label: \"relative z-10\",\r\n trailingBadge: \"flex items-center rounded-full bg-gray-2 px-2 py-0.5 text-24 text-gray-7\",\r\n trailingBadgeSize: \"text-24\",\r\n content:\r\n \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 scroll-mt-24\",\r\n indicator:\r\n \"absolute bottom-0 left-0 h-1.5 w-0 rounded-full transition-all duration-300 ease-in-out z-0 opacity-90 -translate-y-1\",\r\n },\r\n variants: {\r\n type: {\r\n line: {\r\n list: \"border-b border-gray-2\",\r\n trigger: \"bg-transparent shadow-none rounded-none hover:text-gray-8\",\r\n indicator: \"block\",\r\n },\r\n card: {\r\n list: \"inline-flex items-center justify-start rounded-[var(--radius-ui-base)] bg-gray-2/70 p-1\",\r\n trigger: \"rounded-[var(--radius-ui-sm)] px-3 py-2 shadow-none\",\r\n indicator: \"hidden\",\r\n },\r\n },\r\n variant: {\r\n primary: {\r\n trigger: \"data-[state=active]:text-primary\",\r\n indicator:\r\n \"bg-[linear-gradient(90deg,var(--color-red-6),var(--color-red-3),var(--color-blue-1),var(--color-orange-1))] bg-[length:200%]\",\r\n },\r\n info: {\r\n trigger: \"data-[state=active]:text-info\",\r\n indicator: \"bg-info\",\r\n },\r\n success: {\r\n trigger: \"data-[state=active]:text-success\",\r\n indicator: \"bg-success\",\r\n },\r\n warning: {\r\n trigger: \"data-[state=active]:text-warning\",\r\n indicator: \"bg-warning\",\r\n },\r\n neutral: {\r\n trigger: \"data-[state=active]:text-gray-8\",\r\n indicator: \"bg-gray-8\",\r\n },\r\n },\r\n orientation: {\r\n horizontal: {\r\n root: \"flex-col w-full h-full min-w-0 max-w-full relative overflow-hidden\",\r\n list: \"flex-row w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden shrink-0\",\r\n trigger: \"shrink-0\",\r\n indicator:\r\n \"h-1.5 w-[var(--radix-tabs-indicator-width)] translate-x-[var(--radix-tabs-indicator-position)] bottom-2\",\r\n content: \"flex-1 w-full min-h-0 overflow-y-auto\",\r\n },\r\n vertical: {\r\n root: \"flex-row items-start gap-4 h-full [&>*:not([role=tablist])]:flex-1 [&>*:not([role=tablist])]:w-full [&>*:not([role=tablist])]:h-full relative overflow-hidden\",\r\n list: \"flex-col w-auto h-full overflow-y-auto overflow-x-hidden border-b-0 border-r border-gray-2\",\r\n trigger: \"flex-initial w-full justify-start border-b-0 border-r-0 rounded-none\",\r\n indicator: \"hidden\",\r\n content:\r\n \"flex-1 w-full h-full overflow-y-auto mt-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]\",\r\n },\r\n },\r\n size: {\r\n sm: {\r\n trigger: \"text-28 px-2.5 py-1.5\",\r\n leadingAvatarSize: \"h-5 w-5\",\r\n trailingBadgeSize: \"text-20\",\r\n },\r\n md: {\r\n trigger: \"text-32 px-3 py-2\",\r\n leadingAvatarSize: \"h-6 w-6\",\r\n trailingBadgeSize: \"text-22\",\r\n },\r\n lg: {\r\n trigger: \"text-36 px-4 py-2.5\",\r\n leadingAvatarSize: \"h-7 w-7\",\r\n trailingBadgeSize: \"text-24\",\r\n },\r\n },\r\n sticky: {\r\n true: {\r\n root: \"h-auto block overflow-visible\",\r\n list: \"sticky top-0 z-40 bg-background/95 backdrop-blur\",\r\n content: \"h-auto block overflow-visible\",\r\n },\r\n },\r\n shrink: {\r\n true: {\r\n list: \"justify-start\",\r\n trigger: \"flex-none\",\r\n },\r\n },\r\n scrollspy: {\r\n true: {\r\n // Base scrollspy styles\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n orientation: \"horizontal\" as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: \"block h-auto overflow-visible relative\",\r\n list: \"sticky top-0 z-10 bg-background shadow-sm\",\r\n content: \"h-auto block\",\r\n },\r\n },\r\n {\r\n orientation: \"vertical\" as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: \"flex flex-row items-start gap-0 h-full min-h-0 overflow-hidden\",\r\n list: \"h-full min-h-0 overflow-y-auto overflow-x-hidden border-r border-gray-2 shrink-0 w-auto\",\r\n content: \"flex-1 min-w-0 w-full\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n variant: \"primary\" as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: \"data-[state=active]:bg-primary data-[state=active]:text-white\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n variant: \"info\" as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: \"data-[state=active]:bg-info data-[state=active]:text-white\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n variant: \"success\" as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: \"data-[state=active]:bg-success data-[state=active]:text-white\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n variant: \"warning\" as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: \"data-[state=active]:bg-warning data-[state=active]:text-white\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n variant: \"neutral\" as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: \"data-[state=active]:bg-gray-8 data-[state=active]:text-white\",\r\n },\r\n },\r\n {\r\n type: \"card\" as (typeof tabsTypes)[number],\r\n orientation: \"vertical\" as (typeof tabsOrientations)[number],\r\n class: {\r\n list: \"border-r-0\",\r\n trigger: \"border-r-0\",\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n type: \"line\" as (typeof tabsTypes)[number],\r\n variant: \"primary\" as (typeof tabsVariants)[number],\r\n orientation: \"horizontal\" as (typeof tabsOrientations)[number],\r\n size: \"md\" as (typeof tabsSizes)[number],\r\n sticky: false,\r\n shrink: false,\r\n },\r\n};\r\n",
|
|
2113
|
+
"target": "web"
|
|
2114
|
+
},
|
|
2115
|
+
{
|
|
2116
|
+
"path": "TabsContent.vue",
|
|
2117
|
+
"content": "<script setup lang=\"ts\">\r\nimport { inject, computed, nextTick, onBeforeUnmount, ref, watch } from \"vue\";\r\nimport { useIntersectionObserver } from \"@vueuse/core\";\r\nimport { cn } from \"~/lib/utils\";\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n class?: any\r\n}>();\r\n\r\nconst context = inject(\"TabsContext\") as any;\r\nconst localIndex = ref<number>(context.registerContent(props.index));\r\nconst contentRef = ref<HTMLElement | null>(null);\r\n\r\n// Direction logic:\r\n// Horizontal: Next -> Slide Left (Enter from Right)\r\n// Vertical: Next -> Slide Up (Enter from Bottom)\r\n\r\nconst transitionName = computed(() => {\r\n if (isScrollspy.value) return undefined;\r\n\r\n const isVertical = context.orientation.value === 'vertical';\r\n const dir = context.direction.value;\r\n\r\n if (isVertical) {\r\n return dir === 'next' ? 'tabs-slide-up' : 'tabs-slide-down';\r\n }\r\n return dir === 'next' ? 'tabs-slide-left' : 'tabs-slide-right';\r\n});\r\n\r\n// Fix overflow flashing by ensuring the parent container clips content.\r\n// Ideally, this should be on the TabsRoot, but we can try to enforce it here or suggest it.\r\n// Since we can't easily change the parent, we'll try to rely on the global config change for overflow.\r\n// However, ensuring the leaving element is absolute is key.\r\n\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value);\r\nconst isScrollspy = computed(() => context.scrollspy.value);\r\nconst stopObserver = ref<null | (() => void)>(null);\r\n\r\nimport { usePointerSwipe } from \"@vueuse/core\";\r\n\r\nconst { distanceX, distanceY } = usePointerSwipe(contentRef, {\r\n disableTextSelect: true,\r\n onSwipeEnd(e: PointerEvent, direction) {\r\n if (!context.swipeable.value) return;\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal';\r\n const isVertical = context.orientation.value === 'vertical';\r\n const maxIndex = context.contentCounter.value;\r\n const currentIndex = context.activeIndex.value;\r\n\r\n // Define threshold for swipe (e.g., 50px)\r\n const threshold = 50;\r\n\r\n if (isHorizontal) {\r\n if (direction === 'left' && Math.abs(distanceX.value) > threshold) {\r\n // Swiping Left -> Next Tab\r\n if (currentIndex < maxIndex - 1) {\r\n context.setActiveIndex(currentIndex + 1);\r\n }\r\n } else if (direction === 'right' && Math.abs(distanceX.value) > threshold) {\r\n // Swiping Right -> Prev Tab\r\n if (currentIndex > 0) {\r\n context.setActiveIndex(currentIndex - 1);\r\n }\r\n }\r\n } else if (isVertical) {\r\n if (direction === 'up' && Math.abs(distanceY.value) > threshold) {\r\n // Swiping Up -> Next Tab\r\n if (currentIndex < maxIndex - 1) {\r\n context.setActiveIndex(currentIndex + 1);\r\n }\r\n } else if (direction === 'down' && Math.abs(distanceY.value) > threshold) {\r\n // Swiping Down -> Prev Tab\r\n if (currentIndex > 0) {\r\n context.setActiveIndex(currentIndex - 1);\r\n }\r\n }\r\n }\r\n },\r\n});\r\n\r\n// Helper to find scroll parent\r\nfunction getScrollParent(node: HTMLElement | null): HTMLElement | Document | null {\r\n if (typeof window === 'undefined' || !node) return null;\r\n let parent = node.parentElement;\r\n while (parent) {\r\n const style = window.getComputedStyle(parent);\r\n const overflowY = style.overflowY;\r\n if (overflowY === 'auto' || overflowY === 'scroll') {\r\n return parent;\r\n }\r\n parent = parent.parentElement;\r\n }\r\n return document; // Use document/viewport if no scroll parent\r\n}\r\n\r\nwatch(\r\n isScrollspy,\r\n async (enabled) => {\r\n if (!enabled) {\r\n stopObserver.value?.();\r\n stopObserver.value = null;\r\n return;\r\n }\r\n\r\n await nextTick();\r\n if (!contentRef.value) return;\r\n\r\n // Find the scroll container (the wrapper div)\r\n const scrollParent = getScrollParent(contentRef.value);\r\n\r\n const { stop } = useIntersectionObserver(\r\n contentRef,\r\n (entries) => {\r\n const entry = entries[0];\r\n if (entry?.isIntersecting) {\r\n context.setActiveIndex(localIndex.value);\r\n }\r\n },\r\n {\r\n root: scrollParent === document ? null : scrollParent as HTMLElement,\r\n threshold: [0, 0.1, 0.2, 1], // Better threshold as requested\r\n rootMargin: \"-40% 0px -55% 0px\" // Adjusted margin as requested\r\n }\r\n );\r\n\r\n stopObserver.value = stop;\r\n },\r\n { immediate: true }\r\n);\r\n\r\nwatch(contentRef, (el) => {\r\n if (localIndex.value !== undefined) {\r\n if (el) {\r\n context.registerContentRef(localIndex.value, el);\r\n } else {\r\n context.unregisterContentRef(localIndex.value);\r\n }\r\n }\r\n}, { immediate: true });\r\n\r\nonBeforeUnmount(() => {\r\n stopObserver.value?.();\r\n if (localIndex.value !== undefined) {\r\n context.unregisterContentRef(localIndex.value);\r\n }\r\n});\r\n</script>\r\n\r\n<template>\r\n <div ref=\"contentRef\" v-show=\"isScrollspy || isActive\" role=\"tabpanel\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-index=\"localIndex\"\r\n :class=\"context.ui.value.content({ class: cn(props.class, context.uiOverrides.value?.content) })\">\r\n <slot></slot>\r\n </div>\r\n</template>",
|
|
2118
|
+
"target": "web"
|
|
2119
|
+
},
|
|
2120
|
+
{
|
|
2121
|
+
"path": "TabsList.vue",
|
|
2122
|
+
"content": "<script setup lang=\"ts\">\r\nimport { inject, ref, onMounted, watch, nextTick } from \"vue\";\r\nimport { useResizeObserver } from \"@vueuse/core\";\r\nimport { cn } from \"~/lib/utils\";\r\n\r\nconst context = inject(\"TabsContext\") as any;\r\n\r\nconst props = defineProps<{\r\n class?: any\r\n}>();\r\n\r\nconst listRef = ref<HTMLElement | null>(null);\r\nconst indicatorStyle = ref({});\r\n\r\n// --- Indicator logic ---\r\nfunction updateIndicator() {\r\n if (!listRef.value) return;\r\n\r\n const activeTab = listRef.value.querySelector('[data-state=\"active\"]') as HTMLElement;\r\n if (!activeTab) {\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': '0px',\r\n '--radix-tabs-indicator-width': '0px',\r\n '--radix-tabs-indicator-height': '0px',\r\n opacity: 0\r\n };\r\n return;\r\n }\r\n\r\n const listRect = listRef.value.getBoundingClientRect();\r\n const tabRect = activeTab.getBoundingClientRect();\r\n\r\n // Try to get the text span for precise width measurement\r\n const textSpan = activeTab.querySelector('[data-tab-label]') as HTMLElement;\r\n const textRect = textSpan?.getBoundingClientRect() || tabRect;\r\n\r\n const isHorizontal = context.orientation.value === \"horizontal\";\r\n\r\n if (isHorizontal) {\r\n // Calculate position based on text span, centered under the text\r\n const left = textRect.left - listRect.left + listRef.value.scrollLeft;\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${left}px`,\r\n '--radix-tabs-indicator-width': `${textRect.width}px`,\r\n opacity: 1\r\n };\r\n } else {\r\n const top = textRect.top - listRect.top + listRef.value.scrollTop\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${tabRect.height}px`,\r\n opacity: 1\r\n };\r\n }\r\n}\r\n\r\n// --- Scrolling Logic ---\r\nfunction scrollToActiveTab() {\r\n if (!listRef.value) return;\r\n const activeTab = listRef.value.querySelector('[data-state=\"active\"]') as HTMLElement;\r\n if (!activeTab) return;\r\n\r\n const isHorizontal = context.orientation.value === \"horizontal\";\r\n const container = listRef.value;\r\n\r\n const listRect = listRef.value.getBoundingClientRect();\r\n const tabRect = activeTab.getBoundingClientRect();\r\n\r\n if (isHorizontal) {\r\n // Horizontal: Calculate target scrollLeft to center the tab\r\n // relativeLeft = distance from left edge of container visible area\r\n const relativeLeft = tabRect.left - listRect.left;\r\n const centerOffset = (listRect.width - tabRect.width) / 2;\r\n const diff = relativeLeft - centerOffset;\r\n\r\n container.scrollTo({\r\n left: container.scrollLeft + diff,\r\n behavior: \"smooth\"\r\n });\r\n } else {\r\n // Vertical: Calculate target scrollTop to center the tab\r\n // relativeTop = distance from top edge of container visible area\r\n const relativeTop = tabRect.top - listRect.top;\r\n const centerOffset = (listRect.height - tabRect.height) / 2;\r\n const diff = relativeTop - centerOffset;\r\n\r\n container.scrollTo({\r\n top: container.scrollTop + diff,\r\n behavior: \"smooth\"\r\n });\r\n }\r\n}\r\n\r\n// Watchers\r\nwatch(() => context.activeIndex.value, async () => {\r\n await nextTick();\r\n scrollToActiveTab();\r\n updateIndicator();\r\n});\r\n\r\nuseResizeObserver(listRef, () => {\r\n updateIndicator();\r\n});\r\n\r\nonMounted(async () => {\r\n await nextTick();\r\n scrollToActiveTab();\r\n updateIndicator();\r\n // Initial scroll might not be needed if default is handled, but good to ensure\r\n});\r\n\r\n</script>\r\n\r\n<template>\r\n <div ref=\"listRef\" role=\"tablist\"\r\n :data-sticky-tabs=\"context.sticky.value || context.scrollspy.value ? 'true' : undefined\"\r\n :class=\"context.ui.value.list({ class: cn(props.class, context.uiOverrides.value?.list) })\"\r\n :style=\"indicatorStyle\">\r\n <slot></slot>\r\n <slot name=\"indicator\" :style=\"indicatorStyle\"\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\">\r\n <span :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n aria-hidden=\"true\"></span>\r\n </slot>\r\n </div>\r\n</template>\r\n",
|
|
2123
|
+
"target": "web"
|
|
2124
|
+
},
|
|
2125
|
+
{
|
|
2126
|
+
"path": "TabsRoot.vue",
|
|
2127
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ClassValue } from \"clsx\";\r\nimport { computed, provide, ref, toRef } from \"vue\";\r\nimport { useVModel } from \"@vueuse/core\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport { tv } from \"~/lib/tv\";\r\nimport theme, { tabsTypes, tabsVariants, tabsSizes, tabsOrientations } from \"./reborn-tabs.config\";\r\n\r\nexport interface TabsProps {\r\n active?: number\r\n defaultActive?: number\r\n type?: typeof tabsTypes[number]\r\n variant?: typeof tabsVariants[number]\r\n size?: typeof tabsSizes[number]\r\n orientation?: typeof tabsOrientations[number]\r\n sticky?: boolean\r\n swipeable?: boolean\r\n shrink?: boolean\r\n scrollspy?: boolean\r\n activationMode?: \"automatic\" | \"manual\"\r\n class?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n list: ClassValue\r\n indicator: ClassValue\r\n trigger: ClassValue\r\n leadingIcon: ClassValue\r\n leadingAvatar: ClassValue\r\n leadingAvatarSize: ClassValue\r\n label: ClassValue\r\n trailingBadge: ClassValue\r\n trailingBadgeSize: ClassValue\r\n content: ClassValue\r\n }>\r\n}\r\n\r\nconst props = withDefaults(defineProps<TabsProps>(), {\r\n defaultActive: 0,\r\n type: \"line\",\r\n variant: \"primary\",\r\n size: \"md\",\r\n orientation: \"horizontal\",\r\n sticky: false,\r\n swipeable: false,\r\n shrink: false,\r\n scrollspy: false,\r\n activationMode: \"automatic\"\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:active\", value: number): void\r\n (e: \"click-tab\", value: number, event: MouseEvent): void\r\n}>();\r\n\r\nconst activeIndex = useVModel(props, \"active\", emit, {\r\n defaultValue: props.defaultActive,\r\n passive: true,\r\n});\r\n\r\nconst b = tv(theme);\r\nconst type = toRef(props, \"type\");\r\nconst variant = toRef(props, \"variant\");\r\nconst size = toRef(props, \"size\");\r\nconst orientation = toRef(props, \"orientation\");\r\nconst sticky = toRef(props, \"sticky\");\r\nconst swipeable = toRef(props, \"swipeable\");\r\nconst shrink = toRef(props, \"shrink\");\r\nconst scrollspy = toRef(props, \"scrollspy\");\r\nconst uiOverrides = computed(() => props.ui ?? {});\r\n\r\nconst ui = computed(() => b({\r\n type: type.value,\r\n variant: variant.value,\r\n size: size.value,\r\n orientation: orientation.value,\r\n sticky: sticky.value,\r\n shrink: shrink.value,\r\n scrollspy: scrollspy.value\r\n}));\r\n\r\nconst triggerCounter = ref(0);\r\nconst contentCounter = ref(0);\r\n\r\nfunction registerTrigger(index?: number) {\r\n if (typeof index === \"number\") {\r\n triggerCounter.value = Math.max(triggerCounter.value, index + 1);\r\n return index;\r\n }\r\n const value = triggerCounter.value;\r\n triggerCounter.value += 1;\r\n return value;\r\n}\r\n\r\nfunction registerContent(index?: number) {\r\n if (typeof index === \"number\") {\r\n contentCounter.value = Math.max(contentCounter.value, index + 1);\r\n return index;\r\n }\r\n const value = contentCounter.value;\r\n contentCounter.value += 1;\r\n return value;\r\n}\r\n\r\nconst contentRefs = ref<Record<number, HTMLElement | null>>({});\r\n\r\nfunction registerContentRef(index: number, el: HTMLElement | null) {\r\n contentRefs.value[index] = el;\r\n}\r\n\r\nfunction unregisterContentRef(index: number) {\r\n if (contentRefs.value[index]) {\r\n delete contentRefs.value[index];\r\n }\r\n}\r\n\r\n\r\n\r\nfunction getScrollParent(node: HTMLElement | null): HTMLElement | Window {\r\n if (typeof window === 'undefined' || !node) return { scrollTo: () => { } } as any;\r\n let parent = node.parentElement;\r\n while (parent) {\r\n const style = window.getComputedStyle(parent);\r\n const overflowY = style.overflowY;\r\n if (overflowY === 'auto' || overflowY === 'scroll') {\r\n return parent;\r\n }\r\n parent = parent.parentElement;\r\n }\r\n return window;\r\n}\r\n\r\nfunction scrollToContent(index: number) {\r\n if (typeof window === 'undefined') return;\r\n const el = contentRefs.value[index];\r\n if (!el) return;\r\n\r\n const scrollParent = getScrollParent(el);\r\n\r\n // Manual calculation:\r\n const elRect = el.getBoundingClientRect();\r\n let currentScrollTop = 0;\r\n let parentRectTop = 0;\r\n\r\n if (scrollParent === window) {\r\n currentScrollTop = window.scrollY;\r\n parentRectTop = 0;\r\n } else {\r\n const p = scrollParent as HTMLElement;\r\n currentScrollTop = p.scrollTop;\r\n if (p.getBoundingClientRect) {\r\n parentRectTop = p.getBoundingClientRect().top;\r\n }\r\n }\r\n\r\n // Target position relative to the scroll parent's top\r\n const relativeTop = elRect.top - parentRectTop + currentScrollTop;\r\n\r\n // Read scroll-margin-top from element style (defined in config)\r\n const style = window.getComputedStyle(el);\r\n const scrollMarginTop = parseFloat(style.scrollMarginTop) || 0;\r\n\r\n const target = relativeTop - scrollMarginTop;\r\n\r\n scrollParent.scrollTo({\r\n top: target,\r\n behavior: 'smooth'\r\n });\r\n}\r\n\r\nconst direction = ref<'next' | 'prev'>('next');\r\n\r\nfunction setActiveIndex(value: number) {\r\n const currentDefault = activeIndex.value ?? 0;\r\n if (value > currentDefault) {\r\n direction.value = 'next';\r\n } else if (value < currentDefault) {\r\n direction.value = 'prev';\r\n }\r\n activeIndex.value = value;\r\n}\r\n\r\nfunction onTabClick(value: number, event: MouseEvent) {\r\n if (props.activationMode === \"manual\") {\r\n // Manual mode handled by Trigger\r\n }\r\n emit(\"click-tab\", value, event);\r\n}\r\n\r\n\r\nprovide('TabsContext', {\r\n activeIndex,\r\n type,\r\n variant,\r\n size,\r\n orientation,\r\n swipeable,\r\n contentCounter,\r\n activationMode: toRef(props, \"activationMode\"),\r\n sticky,\r\n scrollspy,\r\n registerTrigger,\r\n registerContent,\r\n registerContentRef,\r\n unregisterContentRef,\r\n scrollToContent,\r\n setActiveIndex,\r\n onTabClick,\r\n ui,\r\n uiOverrides,\r\n direction // Provide direction to context\r\n});\r\n\r\ndefineExpose({\r\n activeIndex\r\n})\r\n</script>\r\n\r\n<template>\r\n <div :class=\"cn(ui.root(), uiOverrides.root, props.class)\">\r\n <slot :ui=\"ui\" :ui-overrides=\"uiOverrides\"></slot>\r\n </div>\r\n</template>\r\n",
|
|
2128
|
+
"target": "web"
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
"path": "TabsTrigger.vue",
|
|
2132
|
+
"content": "<script setup lang=\"ts\">\r\nimport { inject, computed, ref } from \"vue\";\r\nimport { cn } from \"~/lib/utils\";\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n disabled?: boolean\r\n class?: any\r\n}>();\r\n\r\nconst context = inject(\"TabsContext\") as any;\r\nconst localIndex = ref<number>(context.registerTrigger(props.index));\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value);\r\n\r\nfunction handleClick(event: MouseEvent) {\r\n if (props.disabled) return;\r\n\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(localIndex.value);\r\n // We still set active index, but scroll handling might trigger intersection observer\r\n // which sets active index again. That's fine.\r\n context.setActiveIndex(localIndex.value);\r\n } else {\r\n context.setActiveIndex(localIndex.value);\r\n }\r\n context.onTabClick(localIndex.value, event);\r\n\r\n // native scrollIntoView causes page to jump, handled in TabsList instead\r\n}\r\n\r\nfunction handleKeydown(event: KeyboardEvent) {\r\n if (props.disabled) return\r\n\r\n // Basic keyboard support could normally go here (Arrow keys), \r\n // but typically requires managing focus across the list.\r\n // Given the constraints and usage, we might rely on default browser behavior + click.\r\n // For Reka parity, focusing next/prev ref in list would be needed.\r\n // For now, implementing basic focus triggering for 'automatic' mode if we were managing focus.\r\n}\r\n</script>\r\n\r\n<template>\r\n <button type=\"button\" role=\"tab\" :aria-selected=\"isActive\" :tabindex=\"isActive ? 0 : -1\" :disabled=\"props.disabled\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-orientation=\"context.orientation.value\"\r\n :data-index=\"localIndex\"\r\n :class=\"context.ui.value.trigger({ class: cn(props.class, context.uiOverrides.value?.trigger) })\"\r\n @click=\"handleClick\" @keydown=\"handleKeydown\">\r\n <span v-if=\"$slots['leading-icon']\"\r\n :class=\"context.ui.value.leadingIcon({ class: context.uiOverrides.value?.leadingIcon })\">\r\n <slot name=\"leading-icon\"></slot>\r\n </span>\r\n <span v-if=\"$slots['leading-avatar']\"\r\n :class=\"context.ui.value.leadingAvatar({ class: context.uiOverrides.value?.leadingAvatar })\">\r\n <span :class=\"context.ui.value.leadingAvatarSize({ class: context.uiOverrides.value?.leadingAvatarSize })\">\r\n <slot name=\"leading-avatar\"></slot>\r\n </span>\r\n </span>\r\n <span data-tab-label :class=\"context.ui.value.label({ class: context.uiOverrides.value?.label })\">\r\n <slot name=\"label\">\r\n <slot></slot>\r\n </slot>\r\n </span>\r\n <span v-if=\"$slots['trailing-badge']\"\r\n :class=\"context.ui.value.trailingBadge({ class: context.uiOverrides.value?.trailingBadge })\">\r\n <span :class=\"context.ui.value.trailingBadgeSize({ class: context.uiOverrides.value?.trailingBadgeSize })\">\r\n <slot name=\"trailing-badge\"></slot>\r\n </span>\r\n </span>\r\n </button>\r\n</template>\r\n",
|
|
2133
|
+
"target": "web"
|
|
2134
|
+
},
|
|
2135
|
+
{
|
|
2136
|
+
"path": "context.ts",
|
|
2137
|
+
"content": "import type { InjectionKey, Ref } from 'vue'\r\n\r\nexport interface TabsContext {\r\n activeValue: Ref<string | number>\r\n upateValue: (value: string | number) => void\r\n}\r\n\r\nexport const TABS_INJECTION_KEY: InjectionKey<TabsContext> = Symbol('RebornTabs')\r\n",
|
|
2138
|
+
"target": "uniapp"
|
|
2139
|
+
},
|
|
2140
|
+
{
|
|
2141
|
+
"path": "index.ts",
|
|
2142
|
+
"content": "export { default as RebornText } from './RebornText.vue'\r\n",
|
|
2143
|
+
"target": "uniapp"
|
|
2144
|
+
},
|
|
2145
|
+
{
|
|
2146
|
+
"path": "reborn-tabs.config.ts",
|
|
2147
|
+
"content": "const size = ['xs', 'sm', 'md', 'lg', 'xl'] as const\r\nconst color = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nconst variant = ['line', 'card'] as const\r\n\r\nexport { color as TabsColors, size as TabsSizes, variant as TabsVariants }\r\n\r\nexport default {\r\n slots: {\r\n tabs: '',\r\n scrollbar: 'flex flex-row w-full h-full overflow-x-scroll',\r\n inner: 'flex flex-row relative',\r\n item: 'flex flex-row items-center justify-center relative z-10 transition-colors group',\r\n text: 'font-light whitespace-nowrap',\r\n line: 'absolute left-0 bottom-0 h-[4px] rounded-full bg-[length:200%] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform',\r\n slider: 'absolute top-0 left-0 h-full w-full bg-primary text-white rounded-md transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)] will-change-transform z-0',\r\n active: 'font-medium whitespace-nowrap',\r\n },\r\n variants: {\r\n variant: {\r\n line: {\r\n inner: '',\r\n item: 'hover:text-gray-900 dark:hover:text-gray-100', // Removed static border-b-2\r\n text: 'text-gray-7',\r\n active: 'font-semibold text-gray-8',\r\n },\r\n card: {\r\n inner: '',\r\n item: 'rounded-md',\r\n text: 'text-gray-7',\r\n active: 'text-white',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n // item: \"data-[state=active]:text-primary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-primary',\r\n },\r\n secondary: {\r\n // item: \"data-[state=active]:text-secondary\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-secondary',\r\n },\r\n success: {\r\n // item: \"data-[state=active]:text-success\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-green-4),var(--color-green-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-success',\r\n },\r\n info: {\r\n // item: \"data-[state=active]:text-info\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-blue-4),var(--color-blue-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-info',\r\n },\r\n warning: {\r\n // item: \"data-[state=active]:text-warning\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-orange-4),var(--color-orange-2),var(--color-red-3),var(--color-red-1))]',\r\n slider: 'bg-warning',\r\n },\r\n error: {\r\n // item: \"data-[state=active]:text-error\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-red-4),var(--color-red-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-error',\r\n },\r\n neutral: {\r\n // item: \"data-[state=active]:text-neutral\",\r\n line: 'bg-[linear-gradient(90deg,var(--color-gray-4),var(--color-gray-2),var(--color-orange-3),var(--color-orange-1))]',\r\n slider: 'bg-neutral',\r\n },\r\n },\r\n size: {\r\n xs: {\r\n scrollbar: 'py-1.5',\r\n item: 'px-2 text-xs',\r\n text: 'text-xs',\r\n },\r\n sm: {\r\n scrollbar: 'py-2',\r\n item: 'px-3 text-sm',\r\n text: 'text-sm',\r\n },\r\n md: {\r\n scrollbar: 'py-2.5',\r\n item: 'px-4 text-base',\r\n text: 'text-base',\r\n },\r\n lg: {\r\n scrollbar: 'py-3',\r\n item: 'px-5 text-lg',\r\n text: 'text-lg',\r\n },\r\n xl: {\r\n scrollbar: 'py-3.5',\r\n item: 'px-6 text-xl',\r\n text: 'text-xl',\r\n },\r\n },\r\n fill: {\r\n true: {\r\n inner: 'w-full',\r\n item: 'flex-1',\r\n },\r\n },\r\n justify: {\r\n start: {\r\n inner: 'justify-start',\r\n },\r\n center: {\r\n inner: 'justify-center',\r\n },\r\n end: {\r\n inner: 'justify-end',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'line' as (typeof variant)[number],\r\n color: 'primary' as (typeof color)[number],\r\n size: 'xs' as (typeof size)[number],\r\n fill: false,\r\n justify: 'start' as 'start' | 'center' | 'end',\r\n },\r\n}\r\n",
|
|
2148
|
+
"target": "uniapp"
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
"path": "RebornTabs.vue",
|
|
2152
|
+
"content": "<script lang=\"ts\">\r\n// Tabs组件的单项类型\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme, { TabsColors, TabsSizes, TabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsItem {\r\n label: string // 标签文本\r\n value: string | number // 对应值\r\n disabled?: boolean // 是否禁用\r\n};\r\n\r\ninterface Item extends TabsItem {\r\n isActive: boolean\r\n}\r\n\r\ninterface Props {\r\n customClass?: string\r\n // modelValue: string | number;\r\n list: TabsItem[]\r\n fill?: boolean // 是否填充标签\r\n color?: typeof TabsColors[number]\r\n variant?: typeof TabsVariants[number] // 标签类型\r\n size?: typeof TabsSizes[number] // 标签大小\r\n disabled?: boolean // 是否禁用\r\n justify?: 'start' | 'center' | 'end' // 对齐方式\r\n ui?: {\r\n tabs?: string\r\n scrollbar?: string\r\n inner?: string\r\n item?: string\r\n text?: string\r\n line?: string\r\n slider?: string\r\n active?: string\r\n }\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n list: () => [],\r\n fill: false,\r\n color: TabsColors[0],\r\n variant: TabsVariants[0],\r\n size: TabsSizes[0],\r\n disabled: false,\r\n justify: 'start',\r\n})\r\n\r\n// 定义事件发射器\r\nconst emit = defineEmits(['update:modelValue', 'change'])\r\n\r\ndefineSlots<{\r\n item: (props: { item: Item, active: boolean }) => any\r\n}>()\r\n\r\nconst active = defineModel<string | number>('modelValue')\r\n\r\n// 获取当前组件实例的proxy对象\r\nconst { proxy } = getCurrentInstance()!\r\nconst b = tv(theme)\r\n\r\nconst color = toRef(props, 'color')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst ui = computed(() => b({\r\n color: color.value,\r\n variant: variant.value,\r\n size: size.value,\r\n fill: props.fill,\r\n justify: props.justify,\r\n}))\r\n\r\n// 当前选中的标签值\r\n// const active = ref(props.modelValue);\r\n\r\n// 计算标签列表,增加isActive和disabled属性,便于渲染和状态判断\r\nconst list = computed(() =>\r\n props.list.map((e) => {\r\n return {\r\n label: e.label,\r\n value: e.value,\r\n // 如果未传disabled则默认为false\r\n disabled: e.disabled ?? false,\r\n // 判断当前标签是否为激活状态\r\n isActive: e.value == active.value,\r\n } as Item\r\n }),\r\n)\r\n// 切换标签时触发,参数为索引\r\nasync function change(index: number) {\r\n // 如果整个Tabs被禁用,则不响应点击\r\n if (props.disabled) {\r\n return false\r\n }\r\n\r\n // 获取当前点击标签的值\r\n const { value, disabled } = list.value[index]\r\n\r\n // 如果标签被禁用,则不响应点击\r\n if (disabled) {\r\n return false\r\n }\r\n // 触发v-model的更新\r\n emit('update:modelValue', value, list.value[index], index)\r\n // 触发change事件\r\n emit('change', value, list.value[index], index)\r\n}\r\n\r\n// tab区域宽度\r\nconst tabWidth = ref(0)\r\n// tab区域左侧偏移\r\nconst tabLeft = ref(0)\r\n// 下划线左侧偏移\r\nconst lineLeft = ref(0)\r\n// 下划线宽度\r\nconst lineWidth = ref(0)\r\n// 滑块左侧偏移\r\nconst sliderLeft = ref(0)\r\n// 滑块宽度\r\nconst sliderWidth = ref(0)\r\n// 滚动条左侧偏移\r\nconst scrollLeft = ref(0)\r\n\r\n// 单个标签的位置信息类型,包含left和width\r\ninterface ItemRect {\r\n left: number\r\n width: number\r\n}\r\n\r\n// 所有标签的位置信息,响应式数组\r\nconst itemRects = ref<ItemRect[]>([])\r\nconst textRects = ref<ItemRect[]>([])\r\n\r\n// 获取当前选中标签的下标,未找到则返回0\r\nfunction getIndex() {\r\n const index = list.value.findIndex(e => e.isActive)\r\n return index == -1 ? 0 : index\r\n}\r\n\r\n// 更新下划线、滑块、滚动条等位置\r\n// 更新下划线、滑块、滚动条等位置\r\nfunction updatePosition() {\r\n nextTick(() => {\r\n if (itemRects.value?.length) {\r\n const index = getIndex()\r\n // 获取当前选中标签的位置信息\r\n const item = itemRects.value[index]\r\n const text = textRects.value?.[index]\r\n\r\n if (item) {\r\n // 计算滚动条偏移,使选中项居中\r\n // x = Content_Center - Container_Half_Width\r\n // 由于 item.left 已经标准化(相对于内容区域),公式简化为:\r\n let x = item.left - (tabWidth.value - item.width) / 2\r\n // 防止滚动条偏移为负\r\n if (x < 0) {\r\n x = 0\r\n }\r\n // 设置滚动条偏移\r\n scrollLeft.value = x\r\n\r\n // 设置下划线偏移 (Text based)\r\n if (text) {\r\n // text.left 已经是 relative to content\r\n lineLeft.value = text.left\r\n lineWidth.value = text.width\r\n }\r\n else {\r\n // Fallback\r\n lineLeft.value = item.left + item.width / 2 - 20 / 2\r\n lineWidth.value = 20\r\n }\r\n\r\n // 设置滑块左侧偏移\r\n sliderLeft.value = item.left\r\n // 设置滑块宽度\r\n sliderWidth.value = item.width\r\n }\r\n }\r\n })\r\n}\r\n\r\n// 获取所有标签的位置信息,便于后续计算\r\nfunction getRects() {\r\n // 创建选择器查询\r\n const query = uni.createSelectorQuery().in(proxy)\r\n\r\n query.selectAll('.reborn-tabs__item').boundingClientRect()\r\n query.selectAll('.reborn-tabs__text').boundingClientRect()\r\n query.select('.reborn-tabs__scroll-view').scrollOffset(() => { })\r\n\r\n query.exec((nodes: any) => {\r\n const scrollNode = nodes?.[2]\r\n const currentScrollLeft = scrollNode?.scrollLeft ?? 0\r\n\r\n // 解析查询结果, 转换为相对于内容区域的坐标\r\n // Relative_Left = Viewport_Left - Tab_Viewport_Left + Current_Scroll\r\n itemRects.value = nodes?.[0]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0,\r\n } as ItemRect)) ?? []\r\n\r\n textRects.value = nodes?.[1]?.map((e: any) => ({\r\n left: (e.left ?? 0) - tabLeft.value + currentScrollLeft,\r\n width: e.width ?? 0,\r\n } as ItemRect)) ?? []\r\n\r\n // 更新下划线、滑块等位置\r\n updatePosition()\r\n })\r\n}\r\n\r\n// 刷新tab区域的宽度和位置信息\r\nfunction refresh() {\r\n // 使用 setTimeout 替代 nextTick,确保在视图层渲染完成后再获取布局信息\r\n // 尤其是在修改 size 等导致几何变化的属性时,需要一定延迟\r\n setTimeout(() => {\r\n // 创建选择器查询\r\n uni.createSelectorQuery()\r\n // 作用域限定为当前组件\r\n .in(proxy)\r\n // 选择tab容器\r\n .select('.reborn-tabs')\r\n // 获取容器的left和width\r\n .boundingClientRect((node: any) => {\r\n // 设置tab左侧偏移\r\n tabLeft.value = node?.left ?? 0\r\n // 设置tab宽度\r\n tabWidth.value = node?.width ?? 0\r\n\r\n // 获取所有标签的位置信息\r\n getRects()\r\n })\r\n .exec()\r\n }, 50)\r\n}\r\n\r\nonMounted(() => {\r\n watch(\r\n () => active.value,\r\n () => {\r\n // 更新下划线、滑块等位置\r\n updatePosition()\r\n },\r\n {\r\n // 立即执行一次\r\n immediate: true,\r\n },\r\n )\r\n\r\n // 监听标签列表变化,刷新布局\r\n watch(\r\n computed(() => props.list),\r\n () => {\r\n refresh()\r\n },\r\n {\r\n immediate: true,\r\n deep: true,\r\n },\r\n )\r\n\r\n // 监听其他影响布局的属性的变化\r\n watch(\r\n [\r\n computed(() => props.size),\r\n computed(() => props.variant),\r\n computed(() => props.fill),\r\n computed(() => props.justify),\r\n ],\r\n () => {\r\n refresh()\r\n },\r\n )\r\n})\r\n</script>\r\n\r\n<template>\r\n <view class=\"reborn-tabs\" :class=\"ui.tabs({ class: cn(props.customClass, uiOverrides.tabs) })\">\r\n <scroll-view\r\n class=\"reborn-tabs__scroll-view\" :class=\"ui.scrollbar({ class: uiOverrides.scrollbar })\"\r\n :scroll-with-animation=\"true\" :scroll-x=\"true\" direction=\"horizontal\" :scroll-left=\"scrollLeft\"\r\n :show-scrollbar=\"false\"\r\n >\r\n <view :class=\"ui.inner({ class: uiOverrides.inner })\">\r\n <view\r\n v-for=\"(item, index) in list\" :key=\"index\"\r\n class=\"reborn-tabs__item\"\r\n :class=\"ui.item({ class: uiOverrides.item })\" :data-state=\"item.isActive ? 'active' : 'inactive'\" :data-disabled=\"item.disabled ? 'true' : undefined\"\r\n @tap=\"change(index)\"\r\n >\r\n <slot name=\"item\" :item=\"item\" :active=\"item.isActive\">\r\n <text\r\n v-if=\"item.isActive\" class=\"reborn-tabs__text\"\r\n :class=\"ui.active({ class: cn(uiOverrides.active, item.disabled || props.disabled ? `\r\n cursor-not-allowed text-gray-5\r\n ` : '') })\"\r\n >\r\n {{ item.label }}\r\n </text>\r\n <text\r\n v-else class=\"reborn-tabs__text\"\r\n :class=\"ui.text({ class: cn(uiOverrides.text, item.disabled || props.disabled ? `\r\n cursor-not-allowed text-gray-5\r\n ` : '') })\"\r\n >\r\n {{ item.label }}\r\n </text>\r\n </slot>\r\n </view>\r\n <template v-if=\"lineLeft > 0\">\r\n <view\r\n v-if=\"variant == 'line'\"\r\n :class=\"ui.line({ class: uiOverrides.line })\"\r\n :style=\"{ transform: `translateX(${lineLeft}px)`, width: `${lineWidth}px` }\"\r\n />\r\n <view\r\n v-if=\"variant == 'card'\"\r\n :class=\"ui.slider({ class: uiOverrides.slider })\"\r\n :style=\"{ transform: `translateX(${sliderLeft}px)`, width: `${sliderWidth}px` }\"\r\n />\r\n </template>\r\n </view>\r\n </scroll-view>\r\n </view>\r\n</template>\r\n",
|
|
2153
|
+
"target": "uniapp"
|
|
2154
|
+
}
|
|
2155
|
+
]
|
|
2156
|
+
},
|
|
2157
|
+
{
|
|
2158
|
+
"name": "reborn-tabs copy",
|
|
2159
|
+
"dependencies": [
|
|
2160
|
+
"@vueuse/core",
|
|
2161
|
+
"clsx"
|
|
2162
|
+
],
|
|
2163
|
+
"files": [
|
|
2164
|
+
{
|
|
2165
|
+
"path": "index.ts",
|
|
2166
|
+
"content": "export { default as TabsContent } from './TabsContent.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\n",
|
|
2167
|
+
"target": "uniapp"
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
"path": "reborn-tabs.config.ts",
|
|
2171
|
+
"content": "export const tabsTypes = ['line', 'card'] as const\r\nexport const tabsVariants = ['primary', 'info', 'success', 'warning', 'neutral'] as const\r\nexport const tabsSizes = ['sm', 'md', 'lg'] as const\r\nexport const tabsOrientations = ['horizontal', 'vertical'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-col gap-2 min-w-0',\r\n list: 'relative flex max-w-full box-border gap-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n trigger:\r\n 'relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap px-3 py-2 text-32 font-medium text-gray-7 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-20 flex-1',\r\n leadingIcon: 'flex items-center text-28',\r\n leadingAvatar: 'flex items-center overflow-hidden rounded-full',\r\n leadingAvatarSize: 'h-6 w-6',\r\n label: 'relative z-10',\r\n trailingBadge: 'flex items-center rounded-full bg-gray-2 px-2 py-0.5 text-24 text-gray-7',\r\n trailingBadgeSize: 'text-24',\r\n content:\r\n 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 scroll-mt-24',\r\n indicator:\r\n 'absolute bottom-1 left-0 h-1.5 w-0 rounded-full transition-all duration-300 ease-in-out z-0 opacity-90 -translate-y-1',\r\n },\r\n variants: {\r\n type: {\r\n line: {\r\n list: 'border-b border-gray-2',\r\n trigger: 'bg-transparent shadow-none rounded-none !text-gray-7 hover:text-gray-8 data-[state=active]:font-bold data-[state=active]:!text-gray-8',\r\n indicator: 'block',\r\n },\r\n card: {\r\n list: 'inline-flex items-center justify-start rounded-[var(--radius-ui-base)] bg-gray-2/70 p-1',\r\n trigger: 'rounded-[var(--radius-ui-sm)] px-3 py-2 shadow-none !text-gray-7 data-[state=active]:!text-white',\r\n indicator: 'hidden',\r\n },\r\n },\r\n variant: {\r\n primary: {\r\n trigger: 'data-[state=active]:text-primary',\r\n indicator:\r\n 'bg-[linear-gradient(90deg,var(--color-red-6),var(--color-red-3),var(--color-blue-1),var(--color-orange-1))] bg-[length:200%]',\r\n },\r\n info: {\r\n trigger: 'data-[state=active]:text-info',\r\n indicator: 'bg-info',\r\n },\r\n success: {\r\n trigger: 'data-[state=active]:text-success',\r\n indicator: 'bg-success',\r\n },\r\n warning: {\r\n trigger: 'data-[state=active]:text-warning',\r\n indicator: 'bg-warning',\r\n },\r\n neutral: {\r\n trigger: 'data-[state=active]:text-gray-8',\r\n indicator: 'bg-gray-8',\r\n },\r\n },\r\n orientation: {\r\n horizontal: {\r\n root: 'flex-col w-full h-full min-w-0 max-w-full relative overflow-hidden',\r\n list: 'flex-row flex-nowrap w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden shrink-0',\r\n trigger: 'shrink-0',\r\n indicator:\r\n 'h-1.5 w-[var(--radix-tabs-indicator-width)] translate-x-[var(--radix-tabs-indicator-position)] bottom-1',\r\n content: 'flex-1 w-full min-h-0 overflow-y-auto',\r\n },\r\n vertical: {\r\n root: 'flex-row items-start gap-4 h-full [&>*:not([role=tablist])]:flex-1 [&>*:not([role=tablist])]:w-full [&>*:not([role=tablist])]:h-full relative overflow-hidden',\r\n list: 'flex-col w-auto h-full overflow-y-auto overflow-x-hidden border-b-0 border-r border-gray-2',\r\n trigger: 'flex-initial w-full justify-start border-b-0 border-r-0 rounded-none',\r\n indicator: 'hidden',\r\n content:\r\n 'flex-1 w-full h-full overflow-y-auto mt-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n trigger: 'text-28 px-2.5 py-1.5',\r\n leadingAvatarSize: 'h-5 w-5',\r\n trailingBadgeSize: 'text-20',\r\n },\r\n md: {\r\n trigger: 'text-32 px-3 py-2',\r\n leadingAvatarSize: 'h-6 w-6',\r\n trailingBadgeSize: 'text-22',\r\n },\r\n lg: {\r\n trigger: 'text-36 px-4 py-2.5',\r\n leadingAvatarSize: 'h-7 w-7',\r\n trailingBadgeSize: 'text-24',\r\n },\r\n },\r\n sticky: {\r\n true: {\r\n root: 'h-auto block overflow-visible',\r\n list: 'sticky top-0 z-40 bg-background/95 backdrop-blur',\r\n content: 'h-auto block overflow-visible',\r\n },\r\n },\r\n shrink: {\r\n true: {\r\n list: 'justify-start',\r\n trigger: 'flex-none',\r\n },\r\n },\r\n scrollspy: {\r\n true: {\r\n // Base scrollspy styles\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'block h-auto overflow-visible relative',\r\n list: 'sticky top-0 z-10 bg-background shadow-sm',\r\n content: 'h-auto block',\r\n },\r\n },\r\n {\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'flex flex-row items-start gap-0 h-full min-h-0 overflow-hidden',\r\n list: 'h-full min-h-0 overflow-y-auto overflow-x-hidden border-r border-gray-2 shrink-0 w-auto',\r\n content: 'flex-1 min-w-0 w-full',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-primary data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'info' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-info data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'success' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-success data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'warning' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-warning data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'neutral' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: 'data-[state=active]:bg-gray-8 data-[state=active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n class: {\r\n list: 'border-r-0',\r\n trigger: 'border-r-0',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n type: 'line' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n size: 'md' as (typeof tabsSizes)[number],\r\n sticky: false,\r\n shrink: false,\r\n },\r\n}\r\n",
|
|
2172
|
+
"target": "uniapp"
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
"path": "TabsContent.vue",
|
|
2176
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerContent(props.index))\r\nconst instance = getCurrentInstance()\r\n\r\n// Register ID for scroll handling (if page scroll logic is used)\r\nconst contentId = computed(() => `${context.rootId}-content-${localIndex.value}`)\r\nwatch(contentId, (id) => {\r\n if (localIndex.value !== undefined) {\r\n context.registerContentId(localIndex.value, id)\r\n }\r\n}, { immediate: true })\r\n\r\n// Direction logic\r\nconst transitionName = computed(() => {\r\n if (isScrollspy.value) { return undefined }\r\n\r\n const isVertical = context.orientation.value === 'vertical'\r\n const dir = context.direction.value\r\n\r\n if (isVertical) {\r\n return dir === 'next' ? 'tabs-slide-up' : 'tabs-slide-down'\r\n }\r\n return dir === 'next' ? 'tabs-slide-left' : 'tabs-slide-right'\r\n})\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\nconst isScrollspy = computed(() => context.scrollspy.value)\r\nlet observer: UniApp.IntersectionObserver | null = null\r\n\r\n// --- Swipe Logic ---\r\nlet startX = 0\r\nlet startY = 0\r\n\r\nfunction onTouchStart(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n startX = e.changedTouches[0].clientX\r\n startY = e.changedTouches[0].clientY\r\n }\r\n}\r\n\r\nfunction onTouchEnd(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n const endX = e.changedTouches[0].clientX\r\n const endY = e.changedTouches[0].clientY\r\n const dx = endX - startX\r\n const dy = endY - startY\r\n const threshold = 50\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const isVertical = context.orientation.value === 'vertical'\r\n const maxIndex = context.contentCounter.value\r\n const currentIndex = context.activeIndex.value\r\n\r\n if (isHorizontal) {\r\n if (Math.abs(dx) > threshold && Math.abs(dy) < threshold) { // horizontal swipe\r\n if (dx < 0) {\r\n // Swipe Left -> Next\r\n if (currentIndex < maxIndex - 1) { context.setActiveIndex(currentIndex + 1) }\r\n }\r\n else {\r\n // Swipe Right -> Prev\r\n if (currentIndex > 0) { context.setActiveIndex(currentIndex - 1) }\r\n }\r\n }\r\n }\r\n else if (isVertical) {\r\n if (Math.abs(dy) > threshold && Math.abs(dx) < threshold) {\r\n if (dy < 0) {\r\n // Swipe Up -> Next\r\n if (currentIndex < maxIndex - 1) { context.setActiveIndex(currentIndex + 1) }\r\n }\r\n else {\r\n // Swipe Down -> Prev\r\n if (currentIndex > 0) { context.setActiveIndex(currentIndex - 1) }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\nwatch(\r\n isScrollspy,\r\n async (enabled) => {\r\n if (!enabled) {\r\n if (observer) {\r\n observer.disconnect()\r\n observer = null\r\n }\r\n return\r\n }\r\n\r\n await nextTick()\r\n\r\n // In UniApp, we create observer from the instance\r\n if (observer) { observer.disconnect() }\r\n\r\n observer = uni.createIntersectionObserver(instance)\r\n\r\n // We observe relative to viewport (or a scroll view if we knew it).\r\n // Since we don't know the scroll parent, observing relative to viewport is safest default for scrollspy.\r\n observer.relativeToViewport({ bottom: -100 }) // Adjust margins as needed\r\n observer.observe(`#${contentId.value}`, (res) => {\r\n if (res.intersectionRatio > 0) {\r\n // intersection\r\n // If we have multiple contents intersecting, the last one usually wins or the first one?\r\n // Simple logic: if intersecting, set active.\r\n // Debounce might be needed if scrolling fast.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n })\r\n },\r\n { immediate: true },\r\n)\r\n\r\nonBeforeUnmount(() => {\r\n if (observer) { observer.disconnect() }\r\n if (localIndex.value !== undefined) {\r\n context.unregisterContentId(localIndex.value)\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view\r\n v-show=\"isScrollspy || isActive\" :id=\"contentId\" role=\"tabpanel\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-index=\"localIndex\"\r\n :class=\"context.ui.value.content({ class: cn(props.customClass, context.uiOverrides.value?.content) })\"\r\n @touchstart=\"onTouchStart\" @touchend=\"onTouchEnd\"\r\n >\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
2177
|
+
"target": "uniapp"
|
|
2178
|
+
},
|
|
2179
|
+
{
|
|
2180
|
+
"path": "TabsList.vue",
|
|
2181
|
+
"content": "<script setup lang=\"ts\">\r\nimport { getCurrentInstance, inject, nextTick, onMounted, ref, watch } from 'vue'\r\n// import { useResizeObserver } from \"@vueuse/core\"; // Removed non-uniapp compatible observer\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\n\r\nconst instance = getCurrentInstance()\r\nconst indicatorStyle = ref({})\r\nconst scrollIntoId = ref('')\r\n\r\n// --- Indicator logic ---\r\nfunction updateIndicator() {\r\n // In UniApp, we must use createSelectorQuery\r\n // We need to measure the List relative to the active Trigger.\r\n // Or rather the Active Trigger relative to the List.\r\n const activeIdx = context.activeIndex.value\r\n const triggerId = `#${context.rootId}-trigger-${activeIdx}`\r\n\r\n // We assume the root of TabsList has a specific class or ID we can target if needed,\r\n // but better to target the trigger directly and the list container.\r\n // However, in UniApp customized component, selecting 'in(instance)' is best.\r\n\r\n const query = uni.createSelectorQuery().in(instance)\r\n\r\n // Select the List container (this component's root or the scroll-view)\r\n // Select the List container (this component's root or the scroll-view)\r\n // query.select(`#${context.rootId}-list`).boundingClientRect(); // legacy check\r\n\r\n // Select the active trigger (inside the slot)\r\n // Note: Since Trigger is in a SLOT, selection might be tricky if it's not part of this component's shadow DOM.\r\n // But in Vue/UniApp MP, slots are part of the page/parent usually.\r\n // Wait, if TabsList is a component, and Trigger is passed in slot,\r\n // `query.in(instance)` might NOT find the trigger if it is not in `TabsList`'s template?\r\n // Actually, in default slot, it IS children.\r\n // If it fails, we might need to query from the Page context or ask `TabsRoot` to query?\r\n // Let's try `in(instance)` first, if that fails, we might need a workaround.\r\n // Another approach: coordinate via TabsRoot. Note that Refs don't work across slots comfortably in MP.\r\n // Trying global selection (without .in(instance)) might work if IDs are unique.\r\n\r\n const globalQuery = uni.createSelectorQuery() // Try global first or in root?\r\n // actually `uni.createSelectorQuery()` in a component is scoped to component? No.\r\n // Without `in`, it is global in H5, but page-scoped in MP.\r\n\r\n // Select the active trigger content wrapper if available, otherwise fallback to trigger.\r\n // In UniApp, we can try selecting the descendant.\r\n // If we select `#id .class`, it should work.\r\n\r\n // Changing logic: Query BOTH the trigger and the inner content.\r\n // If inner content exists, use it. If not, use trigger.\r\n // But `exec` returns array order.\r\n\r\n // Let's select the inner content primarily.\r\n globalQuery.select(`${triggerId} .rb-tabs__trigger-inner`).boundingClientRect()\r\n globalQuery.select(triggerId).boundingClientRect() // Fallback\r\n globalQuery.select(`#${context.rootId}-list`).boundingClientRect()\r\n\r\n globalQuery.exec((res) => {\r\n if (!res || res.length < 3) {\r\n // If array length is small, maybe inner query failed or elements not ready.\r\n // We expect [innerRect, triggerRect, listRect].\r\n // If innerRect is null, we use triggerRect.\r\n return\r\n }\r\n\r\n const innerRect = res[0] as UniApp.NodeInfo\r\n const fallbackTriggerRect = res[1] as UniApp.NodeInfo // Was triggerRect\r\n const listRect = res[2] as UniApp.NodeInfo\r\n\r\n if (!fallbackTriggerRect || !listRect) { return } // Critical elements missing\r\n\r\n // Prefer innerRect if valid (width > 0), else use triggerRect\r\n const triggerRect = (innerRect && (innerRect.width ?? 0) > 0) ? innerRect : fallbackTriggerRect\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n\r\n if (isHorizontal) {\r\n // Relative left: trigger.left - list.left\r\n // However, if list is scrolled, `list.left` is visible window.\r\n // `trigger.left` is also visible window.\r\n // So `trigger.left - list.left` is the visual offset from left edge.\r\n // But for `indicator` (which is likely inside the scroll-view?),\r\n // we want position relative to the CONTENT start.\r\n // If indicator is `absolute` inside `relative` list, it moves with scroll.\r\n // So we need `visual offset + current scroll-left`.\r\n\r\n // To get current scroll-left? We can listen to scroll event or query it.\r\n // Let's query scroll offset of the list.\r\n\r\n uni.createSelectorQuery().in(instance).select(`#${context.rootId}-list`).scrollOffset((scrollRes) => {\r\n const res = scrollRes as any\r\n const scrollLeft = res.scrollLeft || 0\r\n const scrollTop = res.scrollTop || 0\r\n\r\n const left = (triggerRect.left || 0) - (listRect.left || 0) + scrollLeft\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n\r\n if (isHorizontal) {\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${left}px`,\r\n '--radix-tabs-indicator-width': `${triggerRect.width}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n else {\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n }).exec()\r\n }\r\n else {\r\n // Vertical logic similar\r\n uni.createSelectorQuery().in(instance).select(`#${context.rootId}-list`).scrollOffset((scrollRes) => {\r\n const res = scrollRes as any\r\n const scrollTop = res.scrollTop || 0\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }).exec()\r\n }\r\n })\r\n}\r\n\r\n// --- Scrolling Logic ---\r\nfunction scrollToActiveTab() {\r\n // Use scroll-into-view\r\n // The ID to scroll to is the trigger ID\r\n const activeIdx = context.activeIndex.value\r\n scrollIntoId.value = `${context.rootId}-trigger-${activeIdx}`\r\n}\r\n\r\n// Watchers\r\nwatch(() => context.activeIndex.value, async () => {\r\n // Wait for DOM update\r\n await nextTick()\r\n // In MP, sometimes need double tick or slight delay\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 50)\r\n})\r\n\r\nonMounted(async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 100)\r\n})\r\n\r\n// Since we removed ResizeObserver, we might want to listen to window resize?\r\n// Or just rely on updates.\r\n</script>\r\n\r\n<template>\r\n <scroll-view\r\n :id=\"`${context.rootId}-list`\" :scroll-x=\"context.orientation.value === 'horizontal'\"\r\n :scroll-y=\"context.orientation.value === 'vertical'\" :scroll-into-view=\"scrollIntoId\" scroll-with-animation\r\n role=\"tablist\" :enable-flex=\"true\"\r\n :data-sticky-tabs=\"context.sticky.value || context.scrollspy.value ? 'true' : undefined\"\r\n :class=\"context.ui.value.list({ class: cn(props.customClass, context.uiOverrides.value?.list) })\"\r\n :style=\"indicatorStyle\"\r\n >\r\n <slot />\r\n <slot\r\n name=\"indicator\" :style=\"indicatorStyle\"\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n aria-hidden=\"true\" :style=\"indicatorStyle\"\r\n />\r\n </slot>\r\n </scroll-view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(::-webkit-scrollbar) {\r\n display: none;\r\n width: 0;\r\n height: 0;\r\n}\r\n\r\n/* Ensure scroll view content container behaves correctly for flex items */\r\n:deep(.uni-scroll-view-content) {\r\n display: flex;\r\n}\r\n</style>\r\n",
|
|
2182
|
+
"target": "uniapp"
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
"path": "TabsRoot.vue",
|
|
2186
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { TabsProps } from './types'\r\nimport { useVModel } from '@vueuse/core'\r\nimport { computed, provide, ref, toRef } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\n\r\nimport theme from './reborn-tabs.config'\r\n\r\nexport type { TabsProps }\r\n\r\nconst props = withDefaults(defineProps<TabsProps>(), {\r\n defaultActive: 0,\r\n type: 'line',\r\n variant: 'primary',\r\n size: 'md',\r\n orientation: 'horizontal',\r\n sticky: false,\r\n swipeable: false,\r\n shrink: false,\r\n scrollspy: false,\r\n activationMode: 'automatic',\r\n ignorePageScroll: false,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:active', value: number): void\r\n (e: 'click-tab', value: number, event: any): void\r\n}>()\r\n\r\nconst activeIndex = useVModel(props, 'active', emit, {\r\n defaultValue: props.defaultActive,\r\n passive: true,\r\n})\r\n\r\nconst b = tv(theme)\r\nconst type = toRef(props, 'type')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst orientation = toRef(props, 'orientation')\r\nconst sticky = toRef(props, 'sticky')\r\nconst swipeable = toRef(props, 'swipeable')\r\nconst shrink = toRef(props, 'shrink')\r\nconst scrollspy = toRef(props, 'scrollspy')\r\nconst uiOverrides = computed(() => props.ui ?? {})\r\n\r\nconst ui = computed(() => b({\r\n type: type.value,\r\n variant: variant.value,\r\n size: size.value,\r\n orientation: orientation.value,\r\n sticky: sticky.value,\r\n shrink: shrink.value,\r\n scrollspy: scrollspy.value,\r\n}))\r\n\r\nconst triggerCounter = ref(0)\r\nconst contentCounter = ref(0)\r\n\r\n// Simple ID generator for coordination\r\nconst rootId = `tabs-${Math.random().toString(36).substring(2, 9)}`\r\n\r\nfunction registerTrigger(index?: number) {\r\n if (typeof index === 'number') {\r\n triggerCounter.value = Math.max(triggerCounter.value, index + 1)\r\n return index\r\n }\r\n const value = triggerCounter.value\r\n triggerCounter.value += 1\r\n return value\r\n}\r\n\r\nfunction registerContent(index?: number) {\r\n if (typeof index === 'number') {\r\n contentCounter.value = Math.max(contentCounter.value, index + 1)\r\n return index\r\n }\r\n const value = contentCounter.value\r\n contentCounter.value += 1\r\n return value\r\n}\r\n\r\n// Map of index -> unique ID string (needed for scroll-into-view or selector queries)\r\nconst contentIds = ref<Record<number, string>>({})\r\n\r\nfunction registerContentId(index: number, id: string) {\r\n contentIds.value[index] = id\r\n}\r\n\r\nfunction unregisterContentId(index: number) {\r\n delete contentIds.value[index]\r\n}\r\n\r\nconst currentScrollToId = ref('')\r\n\r\nfunction scrollToContent(index: number) {\r\n const id = contentIds.value[index]\r\n if (id) {\r\n currentScrollToId.value = id\r\n\r\n if (!props.ignorePageScroll) {\r\n // In UniApp, we can try to scroll the page to the content\r\n uni.pageScrollTo({\r\n selector: `#${id}`,\r\n duration: 300,\r\n fail: () => {\r\n // If ID not found or in a scroll-view, this might fail or do nothing.\r\n // We could emit an event for manual handling\r\n // console.warn(`[TabsRoot] Could not scroll to content #${id}. If content is inside a scroll-view, please handle scrolling manually.`);\r\n },\r\n })\r\n }\r\n }\r\n}\r\n\r\nconst direction = ref<'next' | 'prev'>('next')\r\n\r\nfunction setActiveIndex(value: number) {\r\n const currentDefault = activeIndex.value ?? 0\r\n if (value > currentDefault) {\r\n direction.value = 'next'\r\n }\r\n else if (value < currentDefault) {\r\n direction.value = 'prev'\r\n }\r\n activeIndex.value = value\r\n}\r\n\r\nfunction onTabClick(value: number, event: any) {\r\n if (props.activationMode === 'manual') {\r\n // Manual mode handled by Trigger\r\n }\r\n emit('click-tab', value, event)\r\n}\r\n\r\nprovide('TabsContext', {\r\n rootId,\r\n activeIndex,\r\n type,\r\n variant,\r\n size,\r\n orientation,\r\n swipeable,\r\n contentCounter,\r\n activationMode: toRef(props, 'activationMode'),\r\n sticky,\r\n scrollspy,\r\n registerTrigger,\r\n registerContent,\r\n registerContentId,\r\n unregisterContentId,\r\n scrollToContent,\r\n setActiveIndex,\r\n onTabClick,\r\n ui,\r\n uiOverrides,\r\n direction, // Provide direction to context\r\n})\r\n\r\ndefineExpose({\r\n activeIndex,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"cn(ui.root(), uiOverrides.root, props.customClass)\">\r\n <slot :ui=\"ui\" :ui-overrides=\"uiOverrides\" :current-scroll-to-id=\"currentScrollToId\" />\r\n </view>\r\n</template>\r\n",
|
|
2187
|
+
"target": "uniapp"
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
"path": "TabsTrigger.vue",
|
|
2191
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, inject, ref } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n disabled?: boolean\r\n label?: string\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerTrigger(props.index))\r\n\r\n// Generate unique ID for this trigger to be selected by TabsList\r\nconst triggerId = computed(() => `${context.rootId}-trigger-${localIndex.value}`)\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\n\r\nfunction handleClick(event: any) {\r\n if (props.disabled) { return }\r\n\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(localIndex.value)\r\n // We still set active index, but scroll handling might trigger intersection observer\r\n // which sets active index again. That's fine.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n else {\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n context.onTabClick(localIndex.value, event)\r\n}\r\n</script>\r\n\r\n<template>\r\n <view\r\n :id=\"triggerId\" role=\"tab\" :aria-selected=\"isActive\"\r\n :class=\"context.ui.value.trigger({ class: cn(props.customClass, context.uiOverrides.value?.trigger) })\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-orientation=\"context.orientation.value\"\r\n :data-disabled=\"props.disabled ? '' : undefined\" :data-index=\"localIndex\" @tap=\"handleClick\"\r\n >\r\n <view\r\n class=\"\r\n rb-tabs__trigger-inner inline-flex items-center justify-center gap-2\r\n \"\r\n >\r\n <text\r\n v-if=\"$slots['leading-icon']\"\r\n :class=\"context.ui.value.leadingIcon({ class: context.uiOverrides.value?.leadingIcon })\"\r\n >\r\n <slot name=\"leading-icon\" />\r\n </text>\r\n <text\r\n v-if=\"$slots['leading-avatar']\"\r\n :class=\"context.ui.value.leadingAvatar({ class: context.uiOverrides.value?.leadingAvatar })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.leadingAvatarSize({ class: context.uiOverrides.value?.leadingAvatarSize })\"\r\n >\r\n <slot name=\"leading-avatar\" />\r\n </text>\r\n </text>\r\n <view\r\n data-tab-label :class=\"context.ui.value.label({ class: context.uiOverrides.value?.label })\"\r\n class=\"relative inline-flex items-center justify-center\"\r\n >\r\n <!-- Hidden bold text to reserve space -->\r\n <text\r\n class=\"\r\n pointer-events-none invisible select-none overflow-hidden\r\n whitespace-pre font-bold opacity-0\r\n \"\r\n >\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n <!-- Visible text -->\r\n <text\r\n class=\"\r\n absolute left-1/2 top-1/2 z-10 -translate-x-1/2 -translate-y-1/2\r\n whitespace-pre\r\n \"\r\n >\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n </view>\r\n <text\r\n v-if=\"$slots['trailing-badge']\"\r\n :class=\"context.ui.value.trailingBadge({ class: context.uiOverrides.value?.trailingBadge })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.trailingBadgeSize({ class: context.uiOverrides.value?.trailingBadgeSize })\"\r\n >\r\n <slot name=\"trailing-badge\" />\r\n </text>\r\n </text>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
2192
|
+
"target": "uniapp"
|
|
2193
|
+
},
|
|
2194
|
+
{
|
|
2195
|
+
"path": "types.ts",
|
|
2196
|
+
"content": "import type { ClassValue } from 'clsx'\r\nimport type { tabsOrientations, tabsSizes, tabsTypes, tabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsProps {\r\n active?: number\r\n defaultActive?: number\r\n type?: typeof tabsTypes[number]\r\n variant?: typeof tabsVariants[number]\r\n size?: typeof tabsSizes[number]\r\n orientation?: typeof tabsOrientations[number]\r\n sticky?: boolean\r\n swipeable?: boolean\r\n shrink?: boolean\r\n scrollspy?: boolean\r\n ignorePageScroll?: boolean\r\n activationMode?: 'automatic' | 'manual'\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n list: ClassValue\r\n indicator: ClassValue\r\n trigger: ClassValue\r\n leadingIcon: ClassValue\r\n leadingAvatar: ClassValue\r\n leadingAvatarSize: ClassValue\r\n label: ClassValue\r\n trailingBadge: ClassValue\r\n trailingBadgeSize: ClassValue\r\n content: ClassValue\r\n }>\r\n}\r\n",
|
|
2197
|
+
"target": "uniapp"
|
|
2198
|
+
}
|
|
2199
|
+
]
|
|
2200
|
+
},
|
|
2201
|
+
{
|
|
2202
|
+
"name": "reborn-tabs-test",
|
|
2203
|
+
"dependencies": [
|
|
2204
|
+
"@vueuse/core",
|
|
2205
|
+
"clsx"
|
|
2206
|
+
],
|
|
2207
|
+
"files": [
|
|
2208
|
+
{
|
|
2209
|
+
"path": "index.ts",
|
|
2210
|
+
"content": "export { default as TabsContent } from './TabsContent.vue'\r\nexport { default as TabsList } from './TabsList.vue'\r\nexport { default as TabsRoot } from './TabsRoot.vue'\r\nexport { default as TabsTrigger } from './TabsTrigger.vue'\r\n",
|
|
2211
|
+
"target": "uniapp"
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
"path": "reborn-tabs.config.ts",
|
|
2215
|
+
"content": "export const tabsTypes = ['line', 'card'] as const\r\nexport const tabsVariants = ['primary', 'info', 'success', 'warning', 'neutral'] as const\r\nexport const tabsSizes = ['sm', 'md', 'lg'] as const\r\nexport const tabsOrientations = ['horizontal', 'vertical'] as const\r\n\r\nexport default {\r\n slots: {\r\n root: 'flex flex-col gap-2 min-w-0',\r\n list: 'relative flex max-w-full box-border gap-2 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n trigger:\r\n 'relative z-10 inline-flex items-center justify-center gap-2 whitespace-nowrap px-3 py-2 text-32 font-medium text-gray-7 ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none data-[disabled=true]:pointer-events-none data-[disabled=true]:!text-gray-4 flex-1',\r\n leadingIcon: 'flex items-center text-28',\r\n leadingAvatar: 'flex items-center overflow-hidden rounded-full',\r\n leadingAvatarSize: 'h-6 w-6',\r\n label: 'relative z-10',\r\n trailingBadge: 'flex items-center rounded-full bg-gray-2 px-2 py-0.5 text-24 text-gray-7',\r\n trailingBadgeSize: 'text-24',\r\n content:\r\n 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 scroll-mt-24',\r\n indicator:\r\n 'absolute bottom-1 left-0 h-1.5 w-0 rounded-full transition-all duration-300 ease-in-out z-0 opacity-90 -translate-y-1 pointer-events-none',\r\n },\r\n variants: {\r\n type: {\r\n line: {\r\n list: 'border-b border-gray-2',\r\n trigger: 'bg-transparent shadow-none rounded-none !text-gray-7 hover:text-gray-8 [&.active]:font-bold [&.active]:!text-gray-8',\r\n indicator: 'block',\r\n },\r\n card: {\r\n list: 'inline-flex items-center justify-start rounded-[var(--radius-ui-base)] bg-gray-2/70 p-1',\r\n trigger: 'rounded-[var(--radius-ui-sm)] px-3 py-2 shadow-none !text-gray-7 [&.active]:!text-white',\r\n indicator: 'hidden',\r\n },\r\n },\r\n variant: {\r\n primary: {\r\n trigger: '[&.active]:text-primary',\r\n indicator:\r\n 'bg-[linear-gradient(90deg,var(--color-red-6),var(--color-red-3),var(--color-blue-1),var(--color-orange-1))] bg-[length:200%]',\r\n },\r\n info: {\r\n trigger: '[&.active]:text-info',\r\n indicator: 'bg-info',\r\n },\r\n success: {\r\n trigger: '[&.active]:text-success',\r\n indicator: 'bg-success',\r\n },\r\n warning: {\r\n trigger: '[&.active]:text-warning',\r\n indicator: 'bg-warning',\r\n },\r\n neutral: {\r\n trigger: '[&.active]:text-gray-8',\r\n indicator: 'bg-gray-8',\r\n },\r\n },\r\n orientation: {\r\n horizontal: {\r\n root: 'flex-col w-full h-full min-w-0 max-w-full relative overflow-hidden',\r\n list: 'flex-row flex-nowrap w-full min-w-0 max-w-full overflow-x-auto overflow-y-hidden shrink-0',\r\n trigger: 'shrink-0',\r\n indicator:\r\n 'h-1.5 w-[var(--radix-tabs-indicator-width)] translate-x-[var(--radix-tabs-indicator-position)] bottom-1',\r\n content: 'flex-1 w-full min-h-0 overflow-y-auto',\r\n },\r\n vertical: {\r\n root: 'flex-row items-start gap-4 h-full [&>*:not([role=tablist])]:flex-1 [&>*:not([role=tablist])]:w-full [&>*:not([role=tablist])]:h-full relative overflow-hidden',\r\n list: 'flex-col w-auto h-full overflow-y-auto overflow-x-hidden border-b-0 border-r border-gray-2',\r\n trigger: 'flex-initial w-full justify-start border-b-0 border-r-0 rounded-none',\r\n indicator: 'hidden',\r\n content:\r\n 'flex-1 w-full h-full overflow-y-auto mt-0 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]',\r\n },\r\n },\r\n size: {\r\n sm: {\r\n trigger: 'text-28 px-2.5 py-1.5',\r\n leadingAvatarSize: 'h-5 w-5',\r\n trailingBadgeSize: 'text-20',\r\n },\r\n md: {\r\n trigger: 'text-32 px-3 py-2',\r\n leadingAvatarSize: 'h-6 w-6',\r\n trailingBadgeSize: 'text-22',\r\n },\r\n lg: {\r\n trigger: 'text-36 px-4 py-2.5',\r\n leadingAvatarSize: 'h-7 w-7',\r\n trailingBadgeSize: 'text-24',\r\n },\r\n },\r\n sticky: {\r\n true: {\r\n root: 'h-auto block overflow-visible',\r\n list: 'sticky top-0 z-40 bg-background/95 backdrop-blur',\r\n content: 'h-auto block overflow-visible',\r\n },\r\n },\r\n shrink: {\r\n true: {\r\n list: 'justify-start',\r\n trigger: 'flex-none',\r\n },\r\n },\r\n scrollspy: {\r\n true: {\r\n // Base scrollspy styles\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'block h-auto overflow-visible relative',\r\n list: 'sticky top-0 z-10 bg-background shadow-sm',\r\n content: 'h-auto block',\r\n },\r\n },\r\n {\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n scrollspy: true,\r\n class: {\r\n root: 'flex flex-row items-start gap-0 h-full min-h-0 overflow-hidden',\r\n list: 'h-full min-h-0 overflow-y-auto overflow-x-hidden border-r border-gray-2 shrink-0 w-auto',\r\n content: 'flex-1 min-w-0 w-full',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-primary [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'info' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-info [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'success' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-success [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'warning' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-warning [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n variant: 'neutral' as (typeof tabsVariants)[number],\r\n class: {\r\n trigger: '[&.active]:bg-gray-8 [&.active]:text-white',\r\n },\r\n },\r\n {\r\n type: 'card' as (typeof tabsTypes)[number],\r\n orientation: 'vertical' as (typeof tabsOrientations)[number],\r\n class: {\r\n list: 'border-r-0',\r\n trigger: 'border-r-0',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n type: 'line' as (typeof tabsTypes)[number],\r\n variant: 'primary' as (typeof tabsVariants)[number],\r\n orientation: 'horizontal' as (typeof tabsOrientations)[number],\r\n size: 'md' as (typeof tabsSizes)[number],\r\n sticky: false,\r\n shrink: false,\r\n },\r\n}\r\n",
|
|
2216
|
+
"target": "uniapp"
|
|
2217
|
+
},
|
|
2218
|
+
{
|
|
2219
|
+
"path": "TabsContent.vue",
|
|
2220
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, inject, nextTick, onBeforeUnmount, ref, watch } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerContent(props.index))\r\nconst instance = getCurrentInstance()\r\n\r\n// Register ID for scroll handling (if page scroll logic is used)\r\nconst contentId = computed(() => `${context.rootId}-content-${localIndex.value}`)\r\nwatch(contentId, (id) => {\r\n if (localIndex.value !== undefined) {\r\n context.registerContentId(localIndex.value, id)\r\n }\r\n}, { immediate: true })\r\n\r\n// Direction logic\r\nconst transitionName = computed(() => {\r\n if (isScrollspy.value) { return undefined }\r\n\r\n const isVertical = context.orientation.value === 'vertical'\r\n const dir = context.direction.value\r\n\r\n if (isVertical) {\r\n return dir === 'next' ? 'tabs-slide-up' : 'tabs-slide-down'\r\n }\r\n return dir === 'next' ? 'tabs-slide-left' : 'tabs-slide-right'\r\n})\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\nconst isScrollspy = computed(() => context.scrollspy.value)\r\nlet observer: UniApp.IntersectionObserver | null = null\r\n\r\n// --- Swipe Logic ---\r\nlet startX = 0\r\nlet startY = 0\r\n\r\nfunction onTouchStart(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n startX = e.changedTouches[0].clientX\r\n startY = e.changedTouches[0].clientY\r\n }\r\n}\r\n\r\nfunction onTouchEnd(e: any) {\r\n if (!context.swipeable.value) { return }\r\n if (e.changedTouches && e.changedTouches.length > 0) {\r\n const endX = e.changedTouches[0].clientX\r\n const endY = e.changedTouches[0].clientY\r\n const dx = endX - startX\r\n const dy = endY - startY\r\n const threshold = 50\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const isVertical = context.orientation.value === 'vertical'\r\n const maxIndex = context.contentCounter.value\r\n const currentIndex = context.activeIndex.value\r\n\r\n if (isHorizontal) {\r\n if (Math.abs(dx) > threshold && Math.abs(dy) < threshold) { // horizontal swipe\r\n if (dx < 0) {\r\n // Swipe Left -> Next\r\n if (currentIndex < maxIndex - 1) {\r\n const nextIndex = currentIndex + 1\r\n context.setActiveIndex(nextIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(nextIndex)\r\n }\r\n }\r\n }\r\n else {\r\n // Swipe Right -> Prev\r\n if (currentIndex > 0) {\r\n const prevIndex = currentIndex - 1\r\n context.setActiveIndex(prevIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(prevIndex)\r\n }\r\n }\r\n }\r\n }\r\n }\r\n else if (isVertical) {\r\n if (Math.abs(dy) > threshold && Math.abs(dx) < threshold) {\r\n if (dy < 0) {\r\n // Swipe Up -> Next\r\n if (currentIndex < maxIndex - 1) {\r\n const nextIndex = currentIndex + 1\r\n context.setActiveIndex(nextIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(nextIndex)\r\n }\r\n }\r\n }\r\n else {\r\n // Swipe Down -> Prev\r\n if (currentIndex > 0) {\r\n const prevIndex = currentIndex - 1\r\n context.setActiveIndex(prevIndex)\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(prevIndex)\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n}\r\n\r\nwatch(\r\n isScrollspy,\r\n async (enabled) => {\r\n if (!enabled) {\r\n if (observer) {\r\n observer.disconnect()\r\n observer = null\r\n }\r\n return\r\n }\r\n\r\n await nextTick()\r\n\r\n // In UniApp, we create observer from the instance\r\n if (observer) { observer.disconnect() }\r\n\r\n observer = uni.createIntersectionObserver(instance)\r\n\r\n // We observe relative to viewport (or a scroll view if we knew it).\r\n // Since we don't know the scroll parent, observing relative to viewport is safest default for scrollspy.\r\n observer.relativeToViewport({ bottom: -100 }) // Adjust margins as needed\r\n observer.observe(`#${contentId.value}`, (res) => {\r\n if (res.intersectionRatio > 0) {\r\n // intersection\r\n // If we have multiple contents intersecting, the last one usually wins or the first one?\r\n // Simple logic: if intersecting, set active.\r\n // Debounce might be needed if scrolling fast.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n })\r\n },\r\n { immediate: true },\r\n)\r\n\r\nonBeforeUnmount(() => {\r\n if (observer) { observer.disconnect() }\r\n if (localIndex.value !== undefined) {\r\n context.unregisterContentId(localIndex.value)\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <view\r\n v-if=\"isScrollspy || isActive\" :id=\"contentId\" role=\"tabpanel\" :data-state=\"isActive ? 'active' : 'inactive'\"\r\n :data-index=\"localIndex\"\r\n :class=\"context.ui.value.content({ class: cn(props.customClass, context.uiOverrides.value?.content) })\"\r\n @touchstart=\"onTouchStart\" @touchend=\"onTouchEnd\"\r\n >\r\n <slot />\r\n </view>\r\n</template>\r\n",
|
|
2221
|
+
"target": "uniapp"
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
"path": "TabsList.vue",
|
|
2225
|
+
"content": "<script setup lang=\"ts\">\r\nimport { getCurrentInstance, inject, nextTick, onMounted, ref, watch } from 'vue'\r\n// import { useResizeObserver } from \"@vueuse/core\"; // Removed non-uniapp compatible observer\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n customClass?: any\r\n}>()\r\n\r\nconst context = inject('TabsContext') as any\r\n\r\nconst instance = getCurrentInstance()\r\nconst indicatorStyle = ref({})\r\nconst scrollIntoId = ref('')\r\n\r\n// --- Indicator logic ---\r\nfunction updateIndicator() {\r\n const activeIdx = context.activeIndex.value\r\n const triggerId = `#${context.rootId}-trigger-${activeIdx}`\r\n const query = uni.createSelectorQuery().in(instance)\r\n query.select(`${triggerId} .rb-tabs__trigger-inner`).boundingClientRect()\r\n query.select(triggerId).boundingClientRect() // Fallback\r\n query.select(`#${context.rootId}-list`).boundingClientRect()\r\n query.select(`#${context.rootId}-list`).scrollOffset()\r\n\r\n query.exec((res) => {\r\n if (!res || res.length < 4) { return }\r\n\r\n const innerRect = res[0] as UniApp.NodeInfo\r\n const fallbackTriggerRect = res[1] as UniApp.NodeInfo\r\n const listRect = res[2] as UniApp.NodeInfo\r\n const scrollRes = res[3] as any\r\n\r\n if (!fallbackTriggerRect || !listRect) { return }\r\n const triggerRect = (innerRect && (innerRect.width ?? 0) > 0) ? innerRect : fallbackTriggerRect\r\n\r\n const isHorizontal = context.orientation.value === 'horizontal'\r\n const scrollLeft = scrollRes?.scrollLeft || 0\r\n const scrollTop = scrollRes?.scrollTop || 0\r\n\r\n if (isHorizontal) {\r\n const left = (triggerRect.left || 0) - (listRect.left || 0) + scrollLeft\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${left}px`,\r\n '--radix-tabs-indicator-width': `${triggerRect.width}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n else {\r\n const top = (triggerRect.top || 0) - (listRect.top || 0) + scrollTop\r\n indicatorStyle.value = {\r\n '--radix-tabs-indicator-position': `${top}px`,\r\n '--radix-tabs-indicator-height': `${triggerRect.height}px`,\r\n 'opacity': 1,\r\n }\r\n }\r\n })\r\n}\r\n\r\n// --- Scrolling Logic ---\r\nfunction scrollToActiveTab() {\r\n const activeIdx = context.activeIndex.value\r\n const nextId = `${context.rootId}-trigger-${activeIdx}`\r\n if (scrollIntoId.value === nextId) {\r\n scrollIntoId.value = ''\r\n nextTick(() => {\r\n scrollIntoId.value = nextId\r\n })\r\n return\r\n }\r\n scrollIntoId.value = nextId\r\n}\r\n\r\n// Watchers\r\nwatch(() => context.activeIndex.value, async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 50)\r\n})\r\n\r\nonMounted(async () => {\r\n await nextTick()\r\n setTimeout(() => {\r\n scrollToActiveTab()\r\n updateIndicator()\r\n }, 100)\r\n})\r\n</script>\r\n\r\n<template>\r\n <scroll-view\r\n :id=\"`${context.rootId}-list`\" :scroll-x=\"context.orientation.value === 'horizontal'\"\r\n :scroll-y=\"context.orientation.value === 'vertical'\" :scroll-into-view=\"scrollIntoId\" scroll-with-animation\r\n role=\"tablist\" :enable-flex=\"true\"\r\n :data-sticky-tabs=\"context.sticky.value || context.scrollspy.value ? 'true' : undefined\"\r\n :class=\"context.ui.value.list({ class: cn(props.customClass, context.uiOverrides.value?.list) })\"\r\n :style=\"indicatorStyle\"\r\n >\r\n <slot />\r\n <slot\r\n name=\"indicator\" :style=\"indicatorStyle\"\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n >\r\n <view\r\n :class=\"context.ui.value.indicator({ class: context.uiOverrides.value?.indicator })\"\r\n aria-hidden=\"true\" :style=\"indicatorStyle\"\r\n />\r\n </slot>\r\n </scroll-view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(::-webkit-scrollbar) {\r\n display: none;\r\n width: 0;\r\n height: 0;\r\n}\r\n\r\n:deep(.uni-scroll-view-content) {\r\n display: flex;\r\n}\r\n</style>\r\n",
|
|
2226
|
+
"target": "uniapp"
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
"path": "TabsRoot.vue",
|
|
2230
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { TabsProps } from './types'\r\nimport { useVModel } from '@vueuse/core'\r\nimport { computed, provide, ref, toRef } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\n\r\nimport theme from './reborn-tabs.config'\r\n\r\nexport type { TabsProps }\r\n\r\nconst props = withDefaults(defineProps<TabsProps>(), {\r\n defaultActive: 0,\r\n type: 'line',\r\n variant: 'primary',\r\n size: 'md',\r\n orientation: 'horizontal',\r\n sticky: false,\r\n swipeable: false,\r\n shrink: false,\r\n scrollspy: false,\r\n activationMode: 'automatic',\r\n ignorePageScroll: false,\r\n})\r\n\r\nconst emit = defineEmits<{\r\n (e: 'update:active', value: number): void\r\n (e: 'click-tab', value: number, event: any): void\r\n}>()\r\n\r\nconst activeIndex = useVModel(props, 'active', emit, {\r\n defaultValue: props.defaultActive,\r\n passive: true,\r\n})\r\n\r\nconst b = tv(theme)\r\nconst type = toRef(props, 'type')\r\nconst variant = toRef(props, 'variant')\r\nconst size = toRef(props, 'size')\r\nconst orientation = toRef(props, 'orientation')\r\nconst sticky = toRef(props, 'sticky')\r\nconst swipeable = toRef(props, 'swipeable')\r\nconst shrink = toRef(props, 'shrink')\r\nconst scrollspy = toRef(props, 'scrollspy')\r\nconst uiOverrides = computed(() => props.ui ?? {})\r\n\r\nconst ui = computed(() => b({\r\n type: type.value,\r\n variant: variant.value,\r\n size: size.value,\r\n orientation: orientation.value,\r\n sticky: sticky.value,\r\n shrink: shrink.value,\r\n scrollspy: scrollspy.value,\r\n}))\r\n\r\nconst triggerCounter = ref(0)\r\nconst contentCounter = ref(0)\r\n\r\n// Simple ID generator for coordination\r\nconst rootId = `tabs-${Math.random().toString(36).substring(2, 9)}`\r\n\r\nfunction registerTrigger(index?: number) {\r\n if (typeof index === 'number') {\r\n triggerCounter.value = Math.max(triggerCounter.value, index + 1)\r\n return index\r\n }\r\n const value = triggerCounter.value\r\n triggerCounter.value += 1\r\n return value\r\n}\r\n\r\nfunction registerContent(index?: number) {\r\n if (typeof index === 'number') {\r\n contentCounter.value = Math.max(contentCounter.value, index + 1)\r\n return index\r\n }\r\n const value = contentCounter.value\r\n contentCounter.value += 1\r\n return value\r\n}\r\n\r\n// Map of index -> unique ID string (needed for scroll-into-view or selector queries)\r\nconst contentIds = ref<Record<number, string>>({})\r\n\r\nfunction registerContentId(index: number, id: string) {\r\n contentIds.value[index] = id\r\n}\r\n\r\nfunction unregisterContentId(index: number) {\r\n delete contentIds.value[index]\r\n}\r\n\r\nconst currentScrollToId = ref('')\r\n\r\nfunction scrollToContent(index: number) {\r\n const id = contentIds.value[index]\r\n if (id) {\r\n currentScrollToId.value = id\r\n\r\n if (!props.ignorePageScroll) {\r\n // In UniApp, we can try to scroll the page to the content\r\n uni.pageScrollTo({\r\n selector: `#${id}`,\r\n duration: 300,\r\n fail: () => {\r\n // If ID not found or in a scroll-view, this might fail or do nothing.\r\n // We could emit an event for manual handling\r\n // console.warn(`[TabsRoot] Could not scroll to content #${id}. If content is inside a scroll-view, please handle scrolling manually.`);\r\n },\r\n })\r\n }\r\n }\r\n}\r\n\r\nconst direction = ref<'next' | 'prev'>('next')\r\n\r\nfunction setActiveIndex(value: number) {\r\n const currentDefault = activeIndex.value ?? 0\r\n if (value > currentDefault) {\r\n direction.value = 'next'\r\n }\r\n else if (value < currentDefault) {\r\n direction.value = 'prev'\r\n }\r\n activeIndex.value = value\r\n}\r\n\r\nfunction onTabClick(value: number, event: any) {\r\n if (props.activationMode === 'manual') {\r\n // Manual mode handled by Trigger\r\n }\r\n emit('click-tab', value, event)\r\n}\r\n\r\nprovide('TabsContext', {\r\n rootId,\r\n activeIndex,\r\n type,\r\n variant,\r\n size,\r\n orientation,\r\n swipeable,\r\n contentCounter,\r\n activationMode: toRef(props, 'activationMode'),\r\n sticky,\r\n scrollspy,\r\n registerTrigger,\r\n registerContent,\r\n registerContentId,\r\n unregisterContentId,\r\n scrollToContent,\r\n setActiveIndex,\r\n onTabClick,\r\n ui,\r\n uiOverrides,\r\n direction, // Provide direction to context\r\n})\r\n\r\ndefineExpose({\r\n activeIndex,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"cn(ui.root(), uiOverrides.root, props.customClass)\">\r\n <slot :ui=\"ui\" :ui-overrides=\"uiOverrides\" :current-scroll-to-id=\"currentScrollToId\" :root-id=\"rootId\" />\r\n </view>\r\n</template>\r\n",
|
|
2231
|
+
"target": "uniapp"
|
|
2232
|
+
},
|
|
2233
|
+
{
|
|
2234
|
+
"path": "TabsTrigger.vue",
|
|
2235
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed, inject, ref, useSlots } from 'vue'\r\nimport { cn } from '@/lib/utils'\r\n\r\nconst props = defineProps<{\r\n index?: number\r\n disabled?: boolean\r\n label?: string\r\n customClass?: any\r\n}>()\r\n\r\nconst slots = useSlots()\r\nconst hasLabelSlot = computed(() => !!slots.label)\r\n\r\nconst context = inject('TabsContext') as any\r\nconst localIndex = ref<number>(context.registerTrigger(props.index))\r\n\r\n// Generate unique ID for this trigger to be selected by TabsList\r\nconst triggerId = computed(() => `${context.rootId}-trigger-${localIndex.value}`)\r\n\r\nconst isActive = computed(() => context.activeIndex.value === localIndex.value)\r\n\r\nfunction handleClick(event: any) {\r\n if (props.disabled) { return }\r\n // console.log('[TabsTrigger] Clicked:', localIndex.value, 'Current Active:', context.activeIndex.value);\r\n\r\n if (context.scrollspy.value) {\r\n context.scrollToContent(localIndex.value)\r\n // We still set active index, but scroll handling might trigger intersection observer\r\n // which sets active index again. That's fine.\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n else {\r\n context.setActiveIndex(localIndex.value)\r\n }\r\n context.onTabClick(localIndex.value, event)\r\n}\r\n</script>\r\n\r\n<template>\r\n <view\r\n :id=\"triggerId\" role=\"tab\" :aria-selected=\"isActive\"\r\n :class=\"[context.ui.value.trigger({ class: cn(props.customClass, context.uiOverrides.value?.trigger) }), { active: isActive }]\"\r\n :data-state=\"isActive ? 'active' : 'inactive'\" :data-orientation=\"context.orientation.value\"\r\n :data-disabled=\"props.disabled ? 'true' : 'false'\" :data-index=\"localIndex\" @tap=\"handleClick\"\r\n >\r\n <view\r\n class=\"\r\n rb-tabs__trigger-inner inline-flex items-center justify-center gap-2\r\n \"\r\n >\r\n <text\r\n v-if=\"$slots['leading-icon']\"\r\n :class=\"context.ui.value.leadingIcon({ class: context.uiOverrides.value?.leadingIcon })\"\r\n >\r\n <slot name=\"leading-icon\" />\r\n </text>\r\n <text\r\n v-if=\"$slots['leading-avatar']\"\r\n :class=\"context.ui.value.leadingAvatar({ class: context.uiOverrides.value?.leadingAvatar })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.leadingAvatarSize({ class: context.uiOverrides.value?.leadingAvatarSize })\"\r\n >\r\n <slot name=\"leading-avatar\" />\r\n </text>\r\n </text>\r\n <view\r\n data-tab-label :class=\"context.ui.value.label({ class: context.uiOverrides.value?.label })\"\r\n class=\"relative inline-flex items-center justify-center\"\r\n >\r\n <!-- Direct render for debugging MP click issues -->\r\n <text class=\"whitespace-pre\">\r\n <slot name=\"label\">\r\n {{ props.label }}\r\n <slot />\r\n </slot>\r\n </text>\r\n </view>\r\n <text\r\n v-if=\"$slots['trailing-badge']\"\r\n :class=\"context.ui.value.trailingBadge({ class: context.uiOverrides.value?.trailingBadge })\"\r\n >\r\n <text\r\n :class=\"context.ui.value.trailingBadgeSize({ class: context.uiOverrides.value?.trailingBadgeSize })\"\r\n >\r\n <slot name=\"trailing-badge\" />\r\n </text>\r\n </text>\r\n </view>\r\n </view>\r\n</template>\r\n",
|
|
2236
|
+
"target": "uniapp"
|
|
2237
|
+
},
|
|
2238
|
+
{
|
|
2239
|
+
"path": "types.ts",
|
|
2240
|
+
"content": "import type { ClassValue } from 'clsx'\r\nimport type { tabsOrientations, tabsSizes, tabsTypes, tabsVariants } from './reborn-tabs.config'\r\n\r\nexport interface TabsProps {\r\n active?: number\r\n defaultActive?: number\r\n type?: typeof tabsTypes[number]\r\n variant?: typeof tabsVariants[number]\r\n size?: typeof tabsSizes[number]\r\n orientation?: typeof tabsOrientations[number]\r\n sticky?: boolean\r\n swipeable?: boolean\r\n shrink?: boolean\r\n scrollspy?: boolean\r\n ignorePageScroll?: boolean\r\n activationMode?: 'automatic' | 'manual'\r\n customClass?: ClassValue\r\n ui?: Partial<{\r\n root: ClassValue\r\n list: ClassValue\r\n indicator: ClassValue\r\n trigger: ClassValue\r\n leadingIcon: ClassValue\r\n leadingAvatar: ClassValue\r\n leadingAvatarSize: ClassValue\r\n label: ClassValue\r\n trailingBadge: ClassValue\r\n trailingBadgeSize: ClassValue\r\n content: ClassValue\r\n }>\r\n}\r\n",
|
|
2241
|
+
"target": "uniapp"
|
|
2242
|
+
}
|
|
2243
|
+
]
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
"name": "reborn-text",
|
|
2247
|
+
"dependencies": [
|
|
2248
|
+
"clsx"
|
|
2249
|
+
],
|
|
2250
|
+
"files": [
|
|
2251
|
+
{
|
|
2252
|
+
"path": "index.ts",
|
|
2253
|
+
"content": "export { default as RebornText } from \"./RebornText.vue\";\r\n"
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
"path": "reborn-text.config.ts",
|
|
2257
|
+
"content": "const colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nexport { colors as textColors };\r\n\r\nexport default {\r\n slots: {\r\n base: \"inline [flex-shrink:unset]\",\r\n },\r\n variants: {\r\n preWrap: {\r\n true: {\r\n base: \"whitespace-pre-wrap\",\r\n },\r\n },\r\n color: {\r\n primary: { base: \"text-primary\" },\r\n secondary: { base: \"text-secondary\" },\r\n success: { base: \"text-success\" },\r\n info: { base: \"text-info\" },\r\n warning: { base: \"text-warning\" },\r\n error: { base: \"text-error\" },\r\n neutral: { base: \"text-neutral\" },\r\n },\r\n ellipsis: {\r\n true: {\r\n base: \"text-ellipsis overflow-hidden [display:-webkit-box] [-webkit-box-orient:vertical] [line-break:anywhere]\",\r\n },\r\n },\r\n },\r\n};\r\n",
|
|
2258
|
+
"target": "web"
|
|
2259
|
+
},
|
|
2260
|
+
{
|
|
2261
|
+
"path": "RebornText.vue",
|
|
2262
|
+
"content": "<script setup lang=\"ts\">\r\nimport { computed } from \"vue\";\r\nimport type { ClassValue } from \"clsx\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { textColors } from \"./reborn-text.config\";\r\nimport { tv } from \"~/lib/tv\";\r\n\r\nconst b = tv(theme);\r\n\r\ndefineOptions({ inheritAttrs: false });\r\n\r\nexport interface RebornTextProps {\r\n /** 文本颜色 */\r\n color?: (typeof textColors)[number];\r\n /** 字体大小 px */\r\n size?: number;\r\n /** 显示的值 */\r\n value?: string | number | null;\r\n /** 文本类型: default | phone | name | amount | card | email */\r\n type?: string;\r\n /** 是否开启脱敏 */\r\n mask?: boolean;\r\n /** 金额货币符号 */\r\n currency?: string;\r\n /** 金额小数位数 */\r\n precision?: number;\r\n /** 脱敏起始位置 */\r\n maskStart?: number;\r\n /** 脱敏结束位置 */\r\n maskEnd?: number;\r\n /** 脱敏替换字符 */\r\n maskChar?: string;\r\n /** 是否省略号 */\r\n ellipsis?: boolean;\r\n /** 最大行数 */\r\n lines?: number;\r\n /** 是否保留空白 */\r\n preWrap?: boolean;\r\n class?: any;\r\n ui?: Partial<{ base: ClassValue }>;\r\n}\r\n\r\nconst props = withDefaults(defineProps<RebornTextProps>(), {\r\n value: null,\r\n type: \"default\",\r\n mask: false,\r\n currency: \"¥\",\r\n precision: 2,\r\n maskStart: 3,\r\n maskEnd: 4,\r\n maskChar: \"*\",\r\n ellipsis: false,\r\n lines: 1,\r\n preWrap: false,\r\n});\r\n\r\nconst uiOverrides = computed(() => props.ui || {});\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n preWrap: props.preWrap,\r\n ellipsis: props.ellipsis,\r\n });\r\n return {\r\n base: (opts?: { class?: any }) =>\r\n styles.base({ class: cn(opts?.class, uiOverrides.value.base) }),\r\n };\r\n});\r\n\r\nconst textStyle = computed(() => {\r\n const style: Record<string, any> = {};\r\n if (props.ellipsis) {\r\n style[\"-webkit-line-clamp\"] = props.lines;\r\n style[\"line-clamp\"] = props.lines;\r\n style[\"-webkit-box-orient\"] = \"vertical\";\r\n style[\"display\"] = \"-webkit-box\";\r\n style[\"overflow\"] = \"hidden\";\r\n style[\"word-break\"] = \"break-all\";\r\n }\r\n if (props.size) {\r\n style.fontSize = `${props.size}px`;\r\n }\r\n return style;\r\n});\r\n\r\nfunction formatPhone(phone: string): string {\r\n if (phone.length !== 11 || !props.mask) return phone;\r\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, `$1${props.maskChar.repeat(4)}$2`);\r\n}\r\n\r\nfunction formatName(name: string): string {\r\n if (name.length <= 1 || !props.mask) return name;\r\n if (name.length === 2) return name[0] + props.maskChar;\r\n return name[0] + props.maskChar.repeat(name.length - 2) + name[name.length - 1];\r\n}\r\n\r\nfunction formatAmount(amount: string | number): string {\r\n const num = typeof amount === \"number\" ? amount : parseFloat(amount);\r\n const formatted = num.toFixed(props.precision);\r\n const parts = formatted.split(\".\");\r\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n return props.currency + parts.join(\".\");\r\n}\r\n\r\nfunction formatCard(card: string): string {\r\n if (card.length < 8 || !props.mask) return card;\r\n const start = card.substring(0, props.maskStart);\r\n const end = card.substring(card.length - props.maskEnd);\r\n const middle = props.maskChar.repeat(card.length - props.maskStart - props.maskEnd);\r\n return start + middle + end;\r\n}\r\n\r\nfunction formatEmail(email: string): string {\r\n if (!props.mask) return email;\r\n const atIndex = email.indexOf(\"@\");\r\n if (atIndex === -1) return email;\r\n const username = email.substring(0, atIndex);\r\n const domain = email.substring(atIndex);\r\n if (username.length <= 2) return email;\r\n return username[0] + props.maskChar.repeat(username.length - 2) + username[username.length - 1] + domain;\r\n}\r\n\r\nconst content = computed(() => {\r\n const val = props.value ?? \"\";\r\n switch (props.type) {\r\n case \"phone\": return formatPhone(val as string);\r\n case \"name\": return formatName(val as string);\r\n case \"amount\": return formatAmount(val as number);\r\n case \"card\": return formatCard(val as string);\r\n case \"email\": return formatEmail(val as string);\r\n default: return val;\r\n }\r\n});\r\n</script>\r\n\r\n<template>\r\n <span :class=\"ui.base({ class: props.class })\" :style=\"textStyle\">\r\n <slot>{{ content }}</slot>\r\n </span>\r\n</template>\r\n",
|
|
2263
|
+
"target": "web"
|
|
2264
|
+
},
|
|
2265
|
+
{
|
|
2266
|
+
"path": "reborn-text.config.ts",
|
|
2267
|
+
"content": "export const textColors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nexport default {\r\n slots: {\r\n // #ifndef APP\r\n base: 'reborn-text [flex-shrink:unset] text-28',\r\n // #endif\r\n // #ifdef APP\r\n // @ts-ignore\r\n base: 'reborn-text text-28',\r\n // #endif\r\n },\r\n variants: {\r\n preWrap: {\r\n true: {\r\n // #ifndef APP\r\n base: 'whitespace-pre-wrap',\r\n // #endif\r\n },\r\n },\r\n color: {\r\n primary: {\r\n base: 'text-primary',\r\n },\r\n secondary: {\r\n base: 'text-secondary',\r\n },\r\n success: {\r\n base: 'text-success',\r\n },\r\n info: {\r\n base: 'text-info',\r\n },\r\n warning: {\r\n base: 'text-warning',\r\n },\r\n error: {\r\n base: 'text-error',\r\n },\r\n neutral: {\r\n base: 'text-neutral',\r\n },\r\n },\r\n ellipsis: {\r\n true: {\r\n base: [\r\n 'text-ellipsis overflow-hidden [display:-webkit-box] [-webkit-box-orient:vertical] [line-break:anywhere]',\r\n ],\r\n },\r\n },\r\n },\r\n}\r\n",
|
|
2268
|
+
"target": "uniapp"
|
|
2269
|
+
},
|
|
2270
|
+
{
|
|
2271
|
+
"path": "RebornText.vue",
|
|
2272
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { textColors } from './reborn-text.config'\r\nimport { computed } from 'vue'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-text.config'\r\n\r\ndefineOptions({\r\n name: 'RebornText',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RebornTextProps>(), {\r\n ui: () => ({\r\n base: '',\r\n }),\r\n customClass: '',\r\n value: null,\r\n type: 'default',\r\n mask: false,\r\n currency: '¥',\r\n precision: 2,\r\n maskStart: 3,\r\n maskEnd: 4,\r\n maskChar: '*',\r\n ellipsis: false,\r\n lines: 1,\r\n selectable: false,\r\n space: '',\r\n decode: false,\r\n preWrap: false,\r\n})\r\nexport interface RebornTextProps {\r\n // 透传样式\r\n ui?: {\r\n base: string\r\n }\r\n customClass?: string\r\n // 文本颜色\r\n color?: typeof textColors[number]\r\n // 字体大小\r\n size?: number\r\n // 显示的值\r\n value?: string | number | null\r\n // 文本类型\r\n type?: string\r\n // 是否开启脱敏/加密\r\n mask?: boolean\r\n // 金额货币符号\r\n currency?: string\r\n // 金额小数位数\r\n precision?: number\r\n // 脱敏起始位置\r\n maskStart?: number\r\n // 脱敏结束位置\r\n maskEnd?: number\r\n // 脱敏替换字符\r\n maskChar?: string\r\n // 是否省略号\r\n ellipsis?: boolean\r\n // 最大行数,仅在ellipsis时生效\r\n lines?: number\r\n // 是否可选择\r\n selectable?: boolean\r\n // 显示连续空格\r\n space?: string\r\n // 是否解码 (app平台如需解析字符实体,需要配置为 true)\r\n decode?: boolean\r\n // 是否保留单词\r\n preWrap?: boolean\r\n}\r\nconst cache = { key: 1 }\r\ninterface PassThrough { className?: string }\r\n\r\n// 样式生成\r\nconst b = tv(theme)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n color: props.color,\r\n preWrap: props.preWrap,\r\n ellipsis: props.ellipsis,\r\n })\r\n\r\n return {\r\n base: (opts?: { class?: any }) => styles.base({ class: cn(opts?.class, props.ui?.base) }),\r\n }\r\n})\r\n\r\n// 文本样式\r\nconst textStyle = computed(() => {\r\n const style: Record<string, any> = {}\r\n\r\n // 省略号\r\n if (props.ellipsis) {\r\n style['-webkit-line-clamp'] = props.lines\r\n style['line-clamp'] = props.lines\r\n }\r\n\r\n // 字号\r\n if (props.size) {\r\n style.fontSize = `${props.size}rpx`\r\n }\r\n\r\n return style\r\n})\r\n\r\n/**\r\n * 手机号脱敏处理\r\n * 保留前3位和后4位,中间4位替换为掩码\r\n */\r\nfunction formatPhone(phone: string): string {\r\n if (phone.length != 11 || !props.mask) { return phone }\r\n return phone.replace(/(\\d{3})\\d{4}(\\d{4})/, `$1${props.maskChar.repeat(4)}$2`)\r\n}\r\n\r\n/**\r\n * 姓名脱敏处理\r\n * 2个字时保留第1个字\r\n * 大于2个字时保留首尾字\r\n */\r\nfunction formatName(name: string): string {\r\n if (name.length <= 1 || !props.mask) { return name }\r\n if (name.length == 2) {\r\n return name[0] + props.maskChar\r\n }\r\n return name[0] + props.maskChar.repeat(name.length - 2) + name[name.length - 1]\r\n}\r\n\r\n/**\r\n * 金额格式化\r\n * 1. 处理小数位数\r\n * 2. 添加千分位分隔符\r\n * 3. 添加货币符号\r\n */\r\nfunction formatAmount(amount: string | number): string {\r\n let num: number\r\n\r\n if (typeof amount == 'number') {\r\n num = amount\r\n }\r\n else {\r\n num = Number.parseFloat(amount)\r\n }\r\n\r\n const formatted = num.toFixed(props.precision)\r\n const parts = formatted.split('.')\r\n parts[0] = parts[0].replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')\r\n\r\n return props.currency + parts.join('.')\r\n}\r\n\r\n/**\r\n * 银行卡号脱敏\r\n * 保留开头和结尾指定位数,中间用掩码替换\r\n */\r\nfunction formatCard(card: string): string {\r\n if (card.length < 8 || !props.mask) { return card }\r\n\r\n const start = card.substring(0, props.maskStart)\r\n const end = card.substring(card.length - props.maskEnd)\r\n const middle = props.maskChar.repeat(card.length - props.maskStart - props.maskEnd)\r\n return start + middle + end\r\n}\r\n\r\n/**\r\n * 邮箱脱敏处理\r\n * 保留用户名首尾字符和完整域名\r\n */\r\nfunction formatEmail(email: string): string {\r\n if (!props.mask) { return email }\r\n\r\n const atIndex = email.indexOf('@')\r\n if (atIndex == -1) { return email }\r\n\r\n const username = email.substring(0, atIndex)\r\n const domain = email.substring(atIndex)\r\n\r\n if (username.length <= 2) { return email }\r\n\r\n const maskedUsername\r\n = username[0] + props.maskChar.repeat(username.length - 2) + username[username.length - 1]\r\n return maskedUsername + domain\r\n}\r\n\r\n/**\r\n * 根据不同类型格式化显示\r\n */\r\nconst content = computed(() => {\r\n const val = props.value ?? ''\r\n\r\n switch (props.type) {\r\n case 'phone':\r\n return formatPhone(val as string)\r\n case 'name':\r\n return formatName(val as string)\r\n case 'amount':\r\n return formatAmount(val as number)\r\n case 'card':\r\n return formatCard(val as string)\r\n case 'email':\r\n return formatEmail(val as string)\r\n default:\r\n return val\r\n }\r\n})\r\n</script>\r\n\r\n<template>\r\n <!-- #ifdef MP -->\r\n <view :key=\"cache.key\" :class=\"ui.base({ class: customClass })\" :style=\"textStyle\" :selectable=\"selectable\"\r\n :space=\"space\" :decode=\"decode\">\r\n <slot>{{ content }}</slot>\r\n </view>\r\n <!-- #endif -->\r\n\r\n <!-- #ifndef MP -->\r\n <text :key=\"cache.key\" :class=\"ui.base({ class: customClass })\" :style=\"textStyle\" :selectable=\"selectable\"\r\n :space=\"space\" :decode=\"decode\">\r\n <slot>{{ content }}</slot>\r\n </text>\r\n <!-- #endif -->\r\n</template>\r\n\r\n<style lang=\"scss\" scoped></style>\r\n",
|
|
2273
|
+
"target": "uniapp"
|
|
2274
|
+
}
|
|
2275
|
+
]
|
|
2276
|
+
},
|
|
2277
|
+
{
|
|
2278
|
+
"name": "reborn-textarea",
|
|
2279
|
+
"dependencies": [],
|
|
2280
|
+
"files": [
|
|
2281
|
+
{
|
|
2282
|
+
"path": "reborn-textarea.config.ts",
|
|
2283
|
+
"content": "const sizes = [\"sm\", \"md\", \"lg\"] as const;\r\nconst colors = [\"primary\", \"secondary\", \"success\", \"info\", \"warning\", \"error\", \"neutral\"] as const;\r\n\r\nconst config = {\r\n slots: {\r\n root: \"relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200\",\r\n inner: \"h-full flex-1 text-sm text-gray-7 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 outline-none border-none\",\r\n text: \"absolute right-4 bottom-2 text-xs text-gray-500 pointer-events-none\",\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: \"text-xs\",\r\n },\r\n md: {\r\n inner: \"text-sm\",\r\n },\r\n lg: {\r\n inner: \"text-base\",\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: \"ring-1 ring-gray-4 dark:ring-gray-1\",\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: \"bg-gray-3 text-gray-5\",\r\n inner: \"cursor-not-allowed\",\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: \"border-red-500 ring-red-500\",\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: \"pb-6\",\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: \"\",\r\n },\r\n secondary: {\r\n root: \"\",\r\n },\r\n success: {\r\n root: \"\",\r\n },\r\n info: {\r\n root: \"\",\r\n },\r\n warning: {\r\n root: \"\",\r\n },\r\n error: {\r\n root: \"\",\r\n },\r\n neutral: {\r\n root: \"\",\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-primary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"secondary\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-secondary\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"success\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-success\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"info\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-info\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"warning\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-warning\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"error\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-error\",\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: \"neutral\" as (typeof colors)[number],\r\n class: {\r\n root: \"ring-2 ring-neutral\",\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: \"md\" as (typeof sizes)[number],\r\n border: true,\r\n color: \"primary\" as (typeof colors)[number],\r\n },\r\n};\r\n\r\nexport { sizes as textareaSizes, colors as textareaColors };\r\nexport default config;\r\n",
|
|
2284
|
+
"target": "web"
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
"path": "RebornTextarea.vue",
|
|
2288
|
+
"content": "<script lang=\"ts\">\r\ninterface RebornTextareaProps {\r\n color?: (typeof textareaColors)[number];\r\n customClass?: any;\r\n ui?: Record<string, any>;\r\n modelValue?: string;\r\n size?: (typeof textareaSizes)[number];\r\n border?: boolean;\r\n disabled?: boolean;\r\n readonly?: boolean;\r\n showWordLimit?: boolean;\r\n name?: string;\r\n placeholder?: string;\r\n maxlength?: number;\r\n autofocus?: boolean;\r\n rows?: number;\r\n autoHeight?: boolean;\r\n}\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, watch } from \"vue\";\r\nimport { tv } from \"~/lib/tv\";\r\nimport { cn } from \"~/lib/utils\";\r\nimport theme, { textareaSizes, textareaColors } from \"./reborn-textarea.config\";\r\nimport { useFieldGroup } from \"~/composables/useFieldGroup\";\r\n\r\ndefineOptions({\r\n name: \"RebornTextarea\",\r\n inheritAttrs: false,\r\n});\r\n\r\nconst props = withDefaults(defineProps<RebornTextareaProps>(), {\r\n color: 'primary',\r\n size: 'md',\r\n border: true,\r\n disabled: false,\r\n readonly: false,\r\n showWordLimit: false,\r\n name: '',\r\n placeholder: '',\r\n maxlength: 100,\r\n autofocus: false,\r\n rows: 4,\r\n autoHeight: false,\r\n});\r\n\r\nconst emit = defineEmits<{\r\n (e: \"update:modelValue\", value: string): void;\r\n (e: \"change\", value: string): void;\r\n (e: \"focus\", event: FocusEvent): void;\r\n (e: \"blur\", event: FocusEvent): void;\r\n}>();\r\n\r\nconst { disabled, isError } = useFieldGroup(props);\r\n\r\nconst isDisabled = computed(() => {\r\n return disabled.value || props.disabled;\r\n});\r\n\r\nconst value = ref(props.modelValue ?? \"\");\r\nconst isFocus = ref<boolean>(props.autofocus ?? false);\r\nconst textareaRef = ref<HTMLTextAreaElement | null>(null);\r\n\r\nconst b = tv(theme);\r\n\r\nconst ui = computed(() =>\r\n b({\r\n size: props.size ?? 'md',\r\n border: props.border ?? true,\r\n focused: isFocus.value,\r\n disabled: isDisabled.value,\r\n error: isError.value,\r\n hasCount: props.showWordLimit ?? true,\r\n color: props.color ?? 'primary',\r\n })\r\n);\r\n\r\nfunction handleInput(event: Event) {\r\n const target = event.target as HTMLTextAreaElement;\r\n value.value = target.value;\r\n emit(\"update:modelValue\", target.value);\r\n\r\n if (props.autoHeight) {\r\n adjustHeight(target);\r\n }\r\n}\r\n\r\nfunction adjustHeight(target: HTMLTextAreaElement) {\r\n target.style.height = 'auto'; // Reset height to calculate scrollHeight\r\n target.style.height = `${target.scrollHeight}px`;\r\n}\r\n\r\nfunction onFocus(e: FocusEvent) {\r\n isFocus.value = true;\r\n emit(\"focus\", e);\r\n}\r\n\r\nfunction onBlur(e: FocusEvent) {\r\n isFocus.value = false;\r\n emit(\"blur\", e);\r\n}\r\n\r\nwatch(\r\n () => props.modelValue,\r\n (val) => {\r\n if (val !== undefined) {\r\n value.value = val;\r\n if (props.autoHeight && textareaRef.value) {\r\n nextTick(() => adjustHeight(textareaRef.value!));\r\n }\r\n }\r\n }\r\n);\r\n</script>\r\n\r\n<template>\r\n <div :class=\"ui.root({ class: cn(props.customClass, props.ui?.root) })\">\r\n <textarea ref=\"textareaRef\" :class=\"ui.inner({ class: props.ui?.inner })\" v-bind=\"$attrs\" :value=\"value\"\r\n :disabled=\"readonly || isDisabled\" :readonly=\"readonly\" :placeholder=\"placeholder\" :maxlength=\"maxlength\"\r\n :autofocus=\"autofocus\" :rows=\"rows ?? 4\" :name=\"name\" @input=\"handleInput\" @focus=\"onFocus\"\r\n @blur=\"onBlur\" />\r\n\r\n <slot v-if=\"showWordLimit\" name=\"limit\" :length=\"value.length\" :max=\"maxlength\">\r\n <span :class=\"ui.text({ class: props.ui?.text })\">{{ value.length }} / {{ maxlength }}</span>\r\n </slot>\r\n </div>\r\n</template>\r\n",
|
|
2289
|
+
"target": "web"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
"path": "index.ts",
|
|
2293
|
+
"content": "export { default as RebornTextarea } from './RebornTextarea.vue'\r\n",
|
|
2294
|
+
"target": "uniapp"
|
|
2295
|
+
},
|
|
2296
|
+
{
|
|
2297
|
+
"path": "reborn-textarea.config.ts",
|
|
2298
|
+
"content": "const sizes = ['sm', 'md', 'lg'] as const\r\nconst colors = ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'relative box-border shrink-0 grow-0 basis-auto min-h-0 min-w-0 flex flex-row items-center bg-white rounded-lg p-2 transition-all duration-200',\r\n inner: 'h-full min-h-0 flex-1 text-28 text-gray-7 bg-transparent disabled:cursor-not-allowed disabled:opacity-50 w-full',\r\n text: 'absolute right-2 bottom-2 text-xs text-gray-5 pointer-events-none',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n inner: 'text-26',\r\n },\r\n md: {\r\n inner: 'text-28',\r\n },\r\n lg: {\r\n inner: 'text-32',\r\n },\r\n },\r\n border: {\r\n true: {\r\n root: 'ring-1 ring-gray-4 dark:ring-gray-1',\r\n },\r\n },\r\n focused: {\r\n true: {},\r\n },\r\n disabled: {\r\n true: {\r\n root: 'bg-gray-3 text-gray-5',\r\n inner: 'cursor-not-allowed',\r\n },\r\n },\r\n error: {\r\n true: {\r\n root: 'ring-error text-error',\r\n inner: 'placeholder:text-error/50',\r\n },\r\n },\r\n hasCount: {\r\n true: {\r\n inner: 'pb-6',\r\n },\r\n },\r\n color: {\r\n primary: {\r\n root: '',\r\n },\r\n secondary: {\r\n root: '',\r\n },\r\n success: {\r\n root: '',\r\n },\r\n info: {\r\n root: '',\r\n },\r\n warning: {\r\n root: '',\r\n },\r\n error: {\r\n root: '',\r\n },\r\n neutral: {\r\n root: '',\r\n },\r\n },\r\n },\r\n compoundVariants: [\r\n {\r\n focused: true,\r\n color: 'primary' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-primary',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'secondary' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-secondary',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'success' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-success',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'info' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-info',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'warning' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-warning',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'error' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-error',\r\n },\r\n },\r\n {\r\n focused: true,\r\n color: 'neutral' as (typeof colors)[number],\r\n class: {\r\n root: 'ring-2 ring-neutral',\r\n },\r\n },\r\n ],\r\n defaultVariants: {\r\n size: 'md' as (typeof sizes)[number],\r\n border: true,\r\n color: 'primary' as (typeof colors)[number],\r\n },\r\n}\r\n\r\nexport { colors as textareaColors, sizes as textareaSizes }\r\nexport default config\r\n",
|
|
1383
2299
|
"target": "uniapp"
|
|
1384
2300
|
},
|
|
1385
2301
|
{
|
|
1386
2302
|
"path": "RebornTextarea.vue",
|
|
1387
|
-
"content": "<script setup lang=\"ts\">\r\nimport { computed, nextTick, ref, watch
|
|
2303
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { textareaColors, textareaSizes } from './reborn-textarea.config'\r\nimport { computed, nextTick, ref, watch } from 'vue'\r\nimport { useFormInject } from '@/composables/useFieldGroup'\r\n\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport theme from './reborn-textarea.config'\r\n\r\ndefineOptions({\r\n name: 'ClTextarea',\r\n})\r\n\r\nconst props = withDefaults(defineProps<RebornTextareaProps>(), {\r\n color: 'primary',\r\n customClass: '',\r\n ui: () => ({}),\r\n modelValue: '',\r\n size: 'md',\r\n border: true,\r\n disabled: false,\r\n readonly: false,\r\n showWordLimit: true,\r\n name: '',\r\n placeholder: () => '请输入',\r\n placeholderClass: '',\r\n placeholderStyle: '',\r\n maxlength: 100,\r\n autofocus: false,\r\n confirmType: 'done',\r\n cursor: 0,\r\n confirmHold: false,\r\n height: 140,\r\n autoHeight: false,\r\n fixed: false,\r\n cursorSpacing: 5,\r\n cursorColor: '',\r\n showConfirmBar: true,\r\n selectionStart: -1,\r\n selectionEnd: -1,\r\n adjustPosition: true,\r\n inputmode: 'text',\r\n holdKeyboard: false,\r\n disableDefaultPadding: true,\r\n adjustKeyboardTo: 'cursor',\r\n})\r\n// 事件定义\r\nconst emit = defineEmits([\r\n 'update:modelValue',\r\n 'input',\r\n 'change',\r\n 'focus',\r\n 'blur',\r\n 'confirm',\r\n 'linechange',\r\n 'keyboardheightchange',\r\n])\r\nexport interface RebornTextareaProps {\r\n // 颜色\r\n color?: typeof textareaColors[number]\r\n // 自定义样式类\r\n customClass?: string\r\n // UI 覆盖\r\n ui?: Record<string, any>\r\n // 绑定值\r\n modelValue: string\r\n // 尺寸\r\n size?: typeof textareaSizes[number]\r\n // 是否显示边框\r\n border?: boolean\r\n // 是否禁用\r\n disabled?: boolean\r\n // 是否只读\r\n readonly?: boolean\r\n // 是否显示字数统计\r\n showWordLimit?: boolean\r\n // 名称\r\n name?: string\r\n // 占位符\r\n placeholder?: string\r\n // 占位符样式类\r\n placeholderClass?: string\r\n // 占位符样式\r\n placeholderStyle?: string\r\n // 最大输入长度\r\n maxlength?: number\r\n // 是否自动聚焦\r\n autofocus?: boolean\r\n // 设置键盘右下角按钮的文字\r\n confirmType?: string\r\n // 指定focus时的光标位置\r\n cursor?: number\r\n // 点击键盘确认按钮时是否保持键盘不收起\r\n confirmHold?: boolean\r\n // 高度\r\n height?: number | string\r\n // 是否自动增高\r\n autoHeight?: boolean\r\n // 如果 textarea 是在一个 position:fixed 的区域,需要显示指定属性 fixed 为 true\r\n fixed?: boolean\r\n // 光标与键盘的距离\r\n cursorSpacing?: number\r\n // 指定光标颜色\r\n cursorColor?: string\r\n // 是否显示键盘上方带有”完成“按钮那一栏\r\n showConfirmBar?: boolean\r\n // 光标起始位置\r\n selectionStart?: number\r\n // 光标结束位置\r\n selectionEnd?: number\r\n // 盘弹起时,是否自动上推页面\r\n adjustPosition?: boolean\r\n // 它提供了用户在编辑元素或其内容时可能输入的数据类型的提示。\r\n inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'tel' | 'search' | 'email' | 'url'\r\n // focus时,点击页面的时候不收起键盘\r\n holdKeyboard?: boolean\r\n // 是否禁用默认内边距\r\n disableDefaultPadding?: boolean\r\n // 键盘对齐位置\r\n adjustKeyboardTo?: string\r\n}\r\nconst { disabled: isDisabled, isError, validate } = useFormInject(props)\r\n\r\n// 绑定值\r\nconst value = ref(props.modelValue)\r\n\r\n// 是否聚焦(样式作用)\r\nconst isFocus = ref<boolean>(props.autofocus)\r\n\r\n// 是否聚焦(输入框作用)\r\nconst isFocusing = ref<boolean>(props.autofocus)\r\n\r\nconst b = tv(theme)\r\n\r\nconst ui = computed(() => {\r\n const styles = b({\r\n size: props.size,\r\n border: props.border,\r\n focused: isFocus.value,\r\n disabled: isDisabled.value,\r\n error: isError.value,\r\n hasCount: props.showWordLimit,\r\n color: props.color,\r\n })\r\n return {\r\n root: (opts?: { class?: any }) =>\r\n styles.root({ class: cn(opts?.class, props.ui?.root) }),\r\n inner: (opts?: { class?: any }) =>\r\n styles.inner({ class: cn(opts?.class, props.ui?.inner) }),\r\n text: (opts?: { class?: any }) =>\r\n styles.text({ class: cn(opts?.class, props.ui?.text) }),\r\n }\r\n})\r\n\r\n// 文本框样式\r\nconst textareaStyle = computed(() => {\r\n const style: any = {}\r\n\r\n if (!props.autoHeight) {\r\n style.height = typeof props.height === 'number' ? `${props.height}rpx` : props.height\r\n }\r\n else {\r\n style.minHeight = '48rpx'\r\n }\r\n\r\n return style\r\n})\r\n\r\nconst placeholderStyle = computed(() => {\r\n return `${props.placeholderStyle}`\r\n})\r\n\r\n// 点击事件\r\nfunction onTap() {\r\n if (!isFocus.value) {\r\n focus()\r\n }\r\n}\r\n\r\n// 获取焦点事件\r\nfunction onFocus(e: any) {\r\n isFocus.value = true\r\n emit('focus', e)\r\n}\r\n\r\n// 失去焦点事件\r\nfunction onBlur(e: any) {\r\n emit('blur', e)\r\n if (validate) { validate('blur') }\r\n\r\n setTimeout(() => {\r\n isFocus.value = false\r\n }, 0)\r\n}\r\n\r\n// 输入事件\r\nfunction onInput(e: any) {\r\n const v1 = e.detail.value\r\n const v2 = value.value\r\n\r\n value.value = v1\r\n\r\n emit('update:modelValue', v1)\r\n emit('input', e)\r\n\r\n if (v1 != v2) {\r\n emit('change', v1)\r\n if (validate) { validate('change') }\r\n }\r\n}\r\n\r\n// 点击确认按钮事件\r\nfunction onConfirm(e: any) {\r\n emit('confirm', e)\r\n}\r\n\r\n// 键盘高度变化事件\r\nfunction onKeyboardheightchange(e: any) {\r\n emit('keyboardheightchange', e)\r\n}\r\n\r\n// 行数变化事件\r\nfunction onLineChange(e: any) {\r\n emit('linechange', e)\r\n}\r\n\r\n// 聚焦方法\r\nfunction focus() {\r\n setTimeout(() => {\r\n isFocusing.value = false\r\n\r\n nextTick(() => {\r\n isFocusing.value = true\r\n })\r\n }, 0)\r\n}\r\n\r\nwatch(\r\n computed(() => props.modelValue),\r\n (val: string) => {\r\n value.value = val\r\n },\r\n)\r\n\r\ndefineExpose({\r\n isFocus,\r\n focus,\r\n})\r\n</script>\r\n\r\n<template>\r\n <view :class=\"ui.root({ class: cn(props.customClass) })\" @tap=\"onTap\">\r\n <textarea :class=\"ui.inner()\" :style=\"textareaStyle\" :value=\"value\" :name=\"name\" :disabled=\"readonly || isDisabled\"\r\n :placeholder=\"placeholder\" :placeholder-class=\"`text-gray-4 ${placeholderClass}`\"\r\n :placeholder-style=\"placeholderStyle\" :maxlength=\"maxlength\" :focus=\"isFocusing\" :cursor=\"cursor\"\r\n :cursor-spacing=\"cursorSpacing\" :cursor-color=\"cursorColor\" :show-confirm-bar=\"showConfirmBar\"\r\n :confirm-hold=\"confirmHold\" :auto-height=\"autoHeight\" :fixed=\"fixed\" :adjust-position=\"adjustPosition\"\r\n :hold-keyboard=\"holdKeyboard\" :inputmode=\"inputmode\" :disable-default-padding=\"disableDefaultPadding\"\r\n :adjust-keyboard-to=\"adjustKeyboardTo\" @confirm=\"onConfirm\" @input=\"onInput\" @linechange=\"onLineChange\"\r\n @blur=\"onBlur\" @focus=\"onFocus\" @keyboardheightchange=\"onKeyboardheightchange\" />\r\n\r\n <slot v-if=\"showWordLimit\" name=\"limit\" :length=\"value.length\" :max=\"maxlength\">\r\n <text :size=\"12\" :class=\"ui.text()\">\r\n {{\r\n value.length }} / {{ maxlength }}\r\n </text>\r\n </slot>\r\n </view>\r\n</template>\r\n\r\n<style scoped>\r\n:deep(.uni-textarea-compute) {\r\n opacity: 0;\r\n}\r\n</style>\r\n",
|
|
2304
|
+
"target": "uniapp"
|
|
2305
|
+
}
|
|
2306
|
+
]
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
"name": "reborn-toast",
|
|
2310
|
+
"dependencies": [],
|
|
2311
|
+
"files": [
|
|
2312
|
+
{
|
|
2313
|
+
"path": "index.ts",
|
|
2314
|
+
"content": "import type { Ref } from 'vue';\r\nimport { createVNode, getCurrentInstance, inject, provide, ref, render } from 'vue';\r\nimport RebornToast from './RebornToast.vue';\r\n\r\nexport interface ToastOptions {\r\n msg?: string;\r\n duration?: number;\r\n iconName?: 'success' | 'error' | 'warning' | 'loading' | 'info';\r\n position?: 'top' | 'middle-top' | 'middle' | 'bottom';\r\n show?: boolean;\r\n zIndex?: number;\r\n cover?: boolean;\r\n color?: 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral' | '';\r\n direction?: 'horizontal' | 'vertical';\r\n}\r\n\r\nconst toastDefaultOptionKey = '__REBORN_TOAST_OPTION__';\r\nexport const defaultOptions: ToastOptions = { duration: 2000, show: false };\r\nexport const getToastOptionKey = (selector = '') => (selector ? `${toastDefaultOptionKey}${selector}` : toastDefaultOptionKey);\r\n\r\nlet toastContainer: HTMLElement | null = null;\r\nlet globalTimer: ReturnType<typeof setTimeout> | null = null;\r\nconst globalOptionRef = ref<ToastOptions>({ ...defaultOptions });\r\n\r\nexport function useToast(selector = '') {\r\n const key = getToastOptionKey(selector);\r\n const instance = getCurrentInstance();\r\n const optionRef = inject<Ref<ToastOptions>>(key, globalOptionRef);\r\n\r\n if (typeof document !== 'undefined' && !toastContainer) {\r\n toastContainer = document.createElement('div');\r\n document.body.appendChild(toastContainer);\r\n\r\n const vnode = createVNode(RebornToast);\r\n if (instance) {\r\n vnode.appContext = instance.appContext;\r\n }\r\n if (vnode.appContext) {\r\n vnode.appContext.provides[key] = optionRef;\r\n }\r\n render(vnode, toastContainer);\r\n }\r\n\r\n provide(key, optionRef);\r\n\r\n const close = () => {\r\n if (globalTimer) clearTimeout(globalTimer);\r\n optionRef.value = { ...optionRef.value, show: false };\r\n };\r\n\r\n const show = (option: ToastOptions | string) => {\r\n const next = typeof option === 'string' ? { msg: option } : option;\r\n optionRef.value = { ...defaultOptions, ...next, show: true };\r\n\r\n if (globalTimer) clearTimeout(globalTimer);\r\n if ((optionRef.value.duration || 0) > 0) {\r\n globalTimer = setTimeout(close, optionRef.value.duration);\r\n }\r\n };\r\n\r\n const build = (base: ToastOptions) => (options: ToastOptions | string) => show({ ...base, ...(typeof options === 'string' ? { msg: options } : options) });\r\n\r\n return {\r\n show,\r\n close,\r\n loading: build({ iconName: 'loading', duration: 0, cover: true }),\r\n success: build({ iconName: 'success', duration: 1500 }),\r\n error: build({ iconName: 'error' }),\r\n warning: build({ iconName: 'warning' }),\r\n info: build({ iconName: 'info' }),\r\n };\r\n}\r\n\r\nexport { RebornToast };\r\n",
|
|
2315
|
+
"target": "web"
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
"path": "reborn-toast.config.ts",
|
|
2319
|
+
"content": "export const toastTheme = {\r\n root: 'inline-flex max-w-[70%] items-center justify-center rounded-xl bg-black/80 px-4 py-3 text-white shadow-lg transition-all duration-300',\r\n positions: { top: '-translate-y-[40vh]', 'middle-top': '-translate-y-[18vh]', middle: '', bottom: 'translate-y-[40vh]' },\r\n colors: {\r\n primary: 'bg-primary text-primary-foreground',\r\n secondary: 'bg-secondary text-secondary-foreground',\r\n success: 'bg-green-500 text-white',\r\n info: 'bg-blue-500 text-white',\r\n warning: 'bg-orange-500 text-white',\r\n error: 'bg-red-500 text-white',\r\n neutral: 'bg-gray-500 text-white',\r\n },\r\n msg: 'text-sm break-all text-center',\r\n};\r\n",
|
|
2320
|
+
"target": "web"
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
"path": "RebornToast.vue",
|
|
2324
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { ToastOptions } from './index';\r\nimport { computed, inject, ref, watch } from 'vue';\r\nimport RebornOverlay from '../reborn-overlay/RebornOverlay.vue';\r\nimport RebornTransition from '../reborn-transition/RebornTransition.vue';\r\nimport { defaultOptions, getToastOptionKey } from './index';\r\nimport { toastTheme } from './reborn-toast.config';\r\n\r\nconst props = withDefaults(defineProps<{ selector?: string }>(), { selector: '' });\r\nconst key = getToastOptionKey(props.selector);\r\nconst optionRef = inject(key, ref<ToastOptions>(defaultOptions));\r\nconst state = ref<ToastOptions>({ ...defaultOptions });\r\nwatch(() => optionRef.value, (v) => { state.value = { ...v }; }, { immediate: true, deep: true });\r\n\r\nconst rootClass = computed(() => {\r\n const base = toastTheme.root;\r\n const pos = toastTheme.positions[state.value.position || 'top'];\r\n const customConfigColor = state.value.color ? toastTheme.colors[state.value.color as keyof typeof toastTheme.colors] : '';\r\n const finalBase = customConfigColor ? base.replace('bg-black/80', '').replace('text-white', '') + ` ${customConfigColor}` : base;\r\n return `${finalBase} ${pos}`;\r\n});\r\n\r\nconst getIconName = (name: string) => {\r\n if (name === 'loading') return 'lucide:loader-2';\r\n if (name === 'success') return 'lucide:circle-check';\r\n if (name === 'error') return 'lucide:circle-x';\r\n if (name === 'warning') return 'lucide:triangle-alert';\r\n if (name === 'info') return 'lucide:info';\r\n return name;\r\n};\r\n</script>\r\n<template>\r\n <RebornOverlay v-if=\"state.cover\" :model-value=\"!!state.show\" :z-index=\"state.zIndex || 100\"\r\n custom-style=\"background-color:transparent;pointer-events:auto;\" />\r\n <RebornTransition name=\"fade\" :show=\"!!state.show\"\r\n :custom-style=\"`z-index:${state.zIndex || 100};position:fixed;left:0;top:50%;width:100%;transform:translateY(-50%);text-align:center;pointer-events:none;`\">\r\n <div :class=\"rootClass\">\r\n <Transition name=\"fade-content\" mode=\"out-in\">\r\n <div :key=\"state.msg + (state.iconName || '') + (state.color || '')\"\r\n class=\"flex items-center justify-center transition-all duration-300\"\r\n :class=\"{ 'flex-col': state.direction === 'vertical' }\">\r\n <Icon v-if=\"state.iconName\" :name=\"getIconName(state.iconName)\" :class=\"[\r\n state.iconName === 'loading' ? 'animate-spin' : '',\r\n state.iconName === 'success' ? 'text-success' : '',\r\n state.iconName === 'error' ? 'text-error' : '',\r\n state.iconName === 'warning' ? 'text-warning' : '',\r\n state.iconName === 'info' ? 'text-info' : '',\r\n state.direction === 'vertical' ? 'mb-2 text-3xl' : 'mr-2 text-xl'\r\n ]\" />\r\n <span :class=\"toastTheme.msg\">{{ state.msg }}</span>\r\n </div>\r\n </Transition>\r\n </div>\r\n </RebornTransition>\r\n</template>\r\n\r\n<style scoped>\r\n.fade-content-enter-active,\r\n.fade-content-leave-active {\r\n transition: opacity 0.2s ease, transform 0.2s ease;\r\n}\r\n\r\n.fade-content-enter-from,\r\n.fade-content-leave-to {\r\n opacity: 0;\r\n transform: scale(0.95);\r\n}\r\n</style>\r\n",
|
|
2325
|
+
"target": "web"
|
|
2326
|
+
},
|
|
2327
|
+
{
|
|
2328
|
+
"path": "index.ts",
|
|
2329
|
+
"content": "import { inject, provide, ref, render, createVNode, getCurrentInstance } from 'vue'\r\nimport { deepMerge } from '@/lib/util'\r\n\r\nexport type ToastIconType = 'success' | 'error' | 'warning' | 'loading' | 'info'\r\nexport type ToastPositionType = 'top' | 'middle-top' | 'middle' | 'bottom'\r\nexport type ToastDirection = 'vertical' | 'horizontal'\r\nexport type ToastLoadingType = 'outline' | 'ring' | 'spinner' | 'bars-scale' | 'blocks-shuffle' | 'blocks-wave' | 'gooey-balls'\r\n\r\nexport type ToastColorType = 'primary' | 'secondary' | 'success' | 'info' | 'warning' | 'error' | 'neutral'\r\n\r\nexport type ToastOptions = {\r\n msg?: string\r\n duration?: number\r\n direction?: ToastDirection\r\n iconName?: ToastIconType\r\n iconSize?: number\r\n loadingType?: ToastLoadingType\r\n loadingColor?: string\r\n loadingSize?: number\r\n position?: ToastPositionType\r\n show?: boolean\r\n zIndex?: number\r\n cover?: boolean\r\n iconClass?: string\r\n classPrefix?: string\r\n color?: ToastColorType\r\n opened?: () => void\r\n closed?: () => void\r\n}\r\n\r\nconst toastDefaultOptionKey = '__REBORN_TOAST_OPTION__'\r\nexport const defaultOptions: ToastOptions = { duration: 2000, show: false }\r\n\r\nexport function getToastOptionKey(selector: string) {\r\n return selector ? `${toastDefaultOptionKey}${selector}` : toastDefaultOptionKey\r\n}\r\n\r\nimport RebornToast from './RebornToast.vue'\r\n\r\n// 全局唯一的组件挂载节点与实例状态\r\nlet toastContainer: HTMLElement | null = null\r\nlet globalTimer: ReturnType<typeof setTimeout> | null = null\r\n\r\n// 全局唯一的配置,保证应用中同一时刻只有一个 Toast 实际在工作\r\nexport const globalOptionRef = ref<ToastOptions>({ ...defaultOptions })\r\n\r\nexport function useToast(selector = '') {\r\n const key = getToastOptionKey(selector)\r\n const instance = getCurrentInstance()\r\n\r\n // 尝试在当前上下文中挂载或获取,以兼容不同端\r\n let optionRef = inject(key, globalOptionRef)\r\n\r\n // #ifdef H5\r\n if (!toastContainer) {\r\n toastContainer = document.createElement('div')\r\n toastContainer.id = 'reborn-toast-container'\r\n document.body.appendChild(toastContainer)\r\n\r\n const vnode = createVNode(RebornToast)\r\n\r\n // 桥接当前上下文,以支持 router, pinia 等\r\n if (instance) {\r\n vnode.appContext = instance.appContext\r\n } else if (!vnode.appContext) {\r\n vnode.appContext = { provides: {} } as any\r\n }\r\n\r\n // 极重要:在这里 provide key 使得动态渲染的 RebornToast 能 inject 到这个 Ref\r\n if (vnode.appContext) {\r\n if (!vnode.appContext.provides) vnode.appContext.provides = {}\r\n vnode.appContext.provides[key] = optionRef\r\n }\r\n\r\n render(vnode, toastContainer)\r\n }\r\n // #endif\r\n\r\n // 向下提供引用给可能写在页面里的 <RebornToast />,保证内外使用的 optionRef 永远映射到同一个实例\r\n provide(key, optionRef)\r\n\r\n const close = () => {\r\n if (globalTimer) {\r\n clearTimeout(globalTimer)\r\n globalTimer = null\r\n }\r\n optionRef.value = { ...optionRef.value, show: false }\r\n }\r\n\r\n const show = (option: ToastOptions | string) => {\r\n const next = deepMerge(defaultOptions, typeof option === 'string' ? { msg: option } : option)\r\n\r\n // 保证每次 show 时都是立刻覆盖当前唯一的提示\r\n optionRef.value = deepMerge(next, { show: true })\r\n\r\n if (globalTimer) {\r\n clearTimeout(globalTimer)\r\n globalTimer = null\r\n }\r\n\r\n if (next.duration !== undefined && next.duration > 0) {\r\n globalTimer = setTimeout(close, next.duration)\r\n }\r\n }\r\n\r\n const build = (base: ToastOptions) => (options: ToastOptions | string) => show(deepMerge(base, typeof options === 'string' ? { msg: options } : options))\r\n return {\r\n show,\r\n close,\r\n loading: build({ iconName: 'loading', duration: 0, cover: true }),\r\n success: build({ iconName: 'success', duration: 1500 }),\r\n error: build({ iconName: 'error' }),\r\n warning: build({ iconName: 'warning' }),\r\n info: build({ iconName: 'info' }),\r\n }\r\n}\r\n\r\nexport const toastIcon = {\r\n success() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#34D19D\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#34D19D\"/><path d=\"M19 24l4 4 8-8\" stroke=\"#FFF\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" fill=\"none\"/></svg>'\r\n },\r\n warning() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#F0883A\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#F0883A\"/><rect x=\"22.5\" y=\"14\" width=\"3\" height=\"12\" fill=\"#FFF\" rx=\"1.5\"/><circle cx=\"24\" cy=\"30\" r=\"2\" fill=\"#FFF\"/></svg>'\r\n },\r\n info() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#909CB7\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#909CB7\"/><circle cx=\"24\" cy=\"18\" r=\"2\" fill=\"#FFF\"/><rect x=\"22.5\" y=\"22\" width=\"3\" height=\"12\" fill=\"#FFF\" rx=\"1.5\"/></svg>'\r\n },\r\n error() {\r\n return '<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"2 2 44 44\" width=\"48\" height=\"48\"><circle cx=\"24\" cy=\"26\" r=\"22\" fill=\"#000\" opacity=\".1\"/><circle cx=\"24\" cy=\"24\" r=\"20\" fill=\"#fa4350\" opacity=\".4\"/><circle cx=\"24\" cy=\"24\" r=\"16\" fill=\"#fa4350\"/><path d=\"M18 18l12 12M30 18L18 30\" stroke=\"#FFF\" stroke-width=\"2.5\" stroke-linecap=\"round\"/></svg>'\r\n }\r\n}\r\nexport { RebornToast }\r\n",
|
|
2330
|
+
"target": "uniapp"
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
"path": "reborn-toast.config.ts",
|
|
2334
|
+
"content": "export const toastColors = ['default', 'primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'] as const\r\nexport const toastPositions = ['top', 'middle-top', 'middle', 'bottom'] as const\r\nexport const toastDirections = ['horizontal', 'vertical'] as const\r\n\r\nconst config = {\r\n slots: {\r\n root: 'rb-toast inline-block max-w-[70%] rounded-xl px-4 py-3 shadow-lg transition-all duration-300',\r\n msg: 'text-left text-24 break-all',\r\n },\r\n variants: {\r\n position: {\r\n top: { root: '-translate-y-[40vh]' },\r\n 'middle-top': { root: '-translate-y-[18.8vh]' },\r\n middle: { root: '' },\r\n bottom: { root: 'translate-y-[40vh]' },\r\n },\r\n direction: {\r\n horizontal: { root: '' },\r\n vertical: { root: 'flex-col' },\r\n },\r\n withIcon: {\r\n true: { root: 'inline-flex items-center' },\r\n false: { root: '' },\r\n },\r\n color: {\r\n default: { root: 'bg-black/80 text-white' },\r\n primary: { root: 'bg-primary text-primary-foreground' },\r\n secondary: { root: 'bg-secondary text-secondary-foreground' },\r\n success: { root: 'bg-success text-success-foreground' },\r\n info: { root: 'bg-info text-info-foreground' },\r\n warning: { root: 'bg-warning text-warning-foreground' },\r\n error: { root: 'bg-error text-error-foreground' },\r\n neutral: { root: 'bg-neutral text-neutral-foreground' },\r\n },\r\n },\r\n defaultVariants: {\r\n color: 'default',\r\n position: 'middle-top',\r\n direction: 'horizontal',\r\n withIcon: false,\r\n },\r\n} as const\r\n\r\nexport default config\r\n",
|
|
2335
|
+
"target": "uniapp"
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
"path": "RebornToast.vue",
|
|
2339
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-toast',\r\n options: {\r\n virtualHost: true,\r\n addGlobalClass: true,\r\n styleIsolation: 'shared',\r\n },\r\n}\r\n</script>\r\n\r\n<script setup lang=\"ts\">\r\nimport { computed, inject, ref, watch } from 'vue'\r\nimport RebornLoading from '@/components/reborn-loading/RebornLoading.vue'\r\nimport RebornOverlay from '@/components/reborn-overlay/RebornOverlay.vue'\r\nimport RebornTransition from '@/components/reborn-transition/RebornTransition.vue'\r\nimport base64 from '@/lib/base64'\r\nimport { addUnit, isDef, isFunction } from '@/lib/util'\r\nimport { tv } from '@/lib/tv'\r\nimport { cn } from '@/lib/utils'\r\nimport { globalOptionRef, getToastOptionKey, toastIcon, type ToastDirection, type ToastOptions } from './index'\r\nimport theme, { toastColors, toastDirections, toastPositions } from './reborn-toast.config'\r\nimport type { ToastLoadingType, ToastIconType, ToastPositionType } from './index'\r\n\r\ninterface ToastProps {\r\n selector?: string\r\n msg?: string\r\n direction?: ToastDirection\r\n iconName?: ToastIconType | ''\r\n iconSize?: number\r\n loadingType?: ToastLoadingType\r\n loadingColor?: string\r\n loadingSize?: number\r\n position?: ToastPositionType\r\n zIndex?: number\r\n cover?: boolean\r\n iconClass?: string\r\n classPrefix?: string\r\n color?: Exclude<typeof toastColors[number], 'default'> | ''\r\n opened?: () => void\r\n closed?: () => void\r\n customClass?: string\r\n}\r\n\r\nconst props = withDefaults(defineProps<ToastProps>(), {\r\n selector: '',\r\n msg: '',\r\n direction: 'horizontal',\r\n iconName: '',\r\n loadingType: 'outline',\r\n loadingColor: '#4D80F0',\r\n position: 'middle-top',\r\n zIndex: 100,\r\n cover: false,\r\n iconClass: '',\r\n classPrefix: 'rb-icon',\r\n customClass: '',\r\n color: '',\r\n loadingSize: 40,\r\n iconSize: 40,\r\n})\r\nconst show = ref(false)\r\nconst msg = ref('')\r\nconst iconName = ref<ToastProps['iconName']>('')\r\nconst svgStr = ref('')\r\nconst cover = ref(false)\r\nconst position = ref(props.position)\r\nconst zIndex = ref(props.zIndex)\r\nconst direction = ref(props.direction)\r\nconst loadingType = ref(props.loadingType)\r\nconst loadingColor = ref(props.loadingColor)\r\nconst loadingSize = ref<string | undefined>(undefined)\r\nconst iconSize = ref<string | undefined>(undefined)\r\nconst color = ref<ToastProps['color']>(props.color)\r\nlet opened: (() => void) | null = null\r\nlet closed: (() => void) | null = null\r\n\r\nconst key = getToastOptionKey(props.selector)\r\nconst toastOption = inject(key, globalOptionRef)\r\nwatch(() => toastOption.value, (val) => reset(val), { immediate: true, deep: true })\r\nwatch(() => iconName.value, buildSvg, { immediate: true })\r\n\r\nconst b = tv(theme)\r\nconst transitionStyle = computed(() => `z-index:${zIndex.value};position:fixed;left:0;top:50%;width:100%;transform:translate(0,-50%);text-align:center;pointer-events:none;`)\r\nconst ui = computed(() => {\r\n const styles = b({\r\n position: position.value as typeof toastPositions[number],\r\n direction: direction.value as typeof toastDirections[number],\r\n withIcon: Boolean(iconName.value && (iconName.value !== 'loading' || msg.value)),\r\n color: (color.value || 'default') as typeof toastColors[number],\r\n })\r\n\r\n return {\r\n root: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class) }),\r\n msg: (opts?: { class?: any }) => styles.msg({ class: cn(opts?.class) }),\r\n }\r\n})\r\nconst svgStyle = computed(() => {\r\n const size = iconSize.value || '30rpx'\r\n return `background-image:url(${svgStr.value});width:${size};height:${size};`\r\n})\r\n\r\nconst onAfterEnter = () => isFunction(opened) && opened()\r\nconst onAfterLeave = () => isFunction(closed) && closed()\r\n\r\nfunction buildSvg() {\r\n if (!iconName.value || iconName.value === 'loading') return\r\n svgStr.value = `data:image/svg+xml;base64,${base64(toastIcon[iconName.value as keyof typeof toastIcon]())}`\r\n}\r\n\r\nfunction reset(option: ToastOptions) {\r\n show.value = !!option.show\r\n if (!show.value) return\r\n iconName.value = option.iconName ?? props.iconName\r\n msg.value = option.msg ?? props.msg\r\n position.value = option.position ?? props.position\r\n zIndex.value = option.zIndex ?? props.zIndex\r\n cover.value = option.cover ?? props.cover\r\n direction.value = option.direction ?? props.direction\r\n loadingType.value = option.loadingType ?? props.loadingType\r\n loadingColor.value = option.loadingColor ?? props.loadingColor\r\n color.value = option.color ?? props.color\r\n iconSize.value = isDef(option.iconSize) ? addUnit(option.iconSize) : isDef(props.iconSize) ? addUnit(props.iconSize) : undefined\r\n loadingSize.value = isDef(option.loadingSize) ? addUnit(option.loadingSize) : isDef(props.loadingSize) ? addUnit(props.loadingSize) : undefined\r\n closed = option.closed ?? props.closed ?? null\r\n opened = option.opened ?? props.opened ?? null\r\n}\r\n</script>\r\n\r\n<template>\r\n <RebornOverlay v-if=\"cover\" :show=\"show\" :z-index=\"zIndex\" lock-scroll\r\n custom-style=\"background-color:transparent;pointer-events:auto;\" />\r\n <RebornTransition name=\"fade\" :show=\"show\" :custom-style=\"transitionStyle\" @after-enter=\"onAfterEnter\"\r\n @after-leave=\"onAfterLeave\">\r\n <view :class=\"ui.root({ class: props.customClass })\">\r\n <RebornLoading v-if=\"iconName === 'loading'\" :type=\"loadingType\" :color=\"loadingColor as any\"\r\n :size=\"loadingSize\" :custom-class=\"direction === 'vertical' ? 'mb-2' : 'mr-2'\" />\r\n <view v-else-if=\"iconName\"\r\n :class=\"direction === 'vertical' ? 'mb-2 mx-auto inline-block bg-cover bg-no-repeat' : 'mr-2 inline-block bg-cover bg-no-repeat'\"\r\n :style=\"svgStyle\" />\r\n <view v-if=\"msg\" :class=\"ui.msg()\">{{ msg }}</view>\r\n </view>\r\n </RebornTransition>\r\n</template>\r\n",
|
|
2340
|
+
"target": "uniapp"
|
|
2341
|
+
}
|
|
2342
|
+
]
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
"name": "reborn-transition",
|
|
2346
|
+
"dependencies": [],
|
|
2347
|
+
"files": [
|
|
2348
|
+
{
|
|
2349
|
+
"path": "index.ts",
|
|
2350
|
+
"content": "export { default as RebornTransition } from './RebornTransition.vue';\r\n",
|
|
2351
|
+
"target": "web"
|
|
2352
|
+
},
|
|
2353
|
+
{
|
|
2354
|
+
"path": "reborn-transition.config.ts",
|
|
2355
|
+
"content": "export const transitionStyles: Record<string, Record<string, string>> = {\r\n fade: {\r\n enter: 'opacity-0',\r\n 'enter-active': 'transition-opacity',\r\n 'enter-to': 'opacity-100',\r\n leave: 'opacity-100',\r\n 'leave-active': 'transition-opacity',\r\n 'leave-to': 'opacity-0',\r\n },\r\n 'fade-up': {\r\n enter: 'translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-y-full opacity-0',\r\n },\r\n 'fade-down': {\r\n enter: '-translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-y-full opacity-0',\r\n },\r\n 'fade-left': {\r\n enter: 'translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-x-full opacity-0',\r\n },\r\n 'fade-right': {\r\n enter: '-translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-x-full opacity-0',\r\n },\r\n 'slide-up': {\r\n enter: 'translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-y-full',\r\n },\r\n 'slide-right': {\r\n enter: 'translate-x-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-x-[100%]',\r\n },\r\n 'slide-down': {\r\n enter: '-translate-y-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-y-[100%]',\r\n },\r\n 'slide-left': {\r\n enter: '-translate-x-[100%]',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-x-[100%]',\r\n },\r\n 'zoom-in': {\r\n enter: 'opacity-0 scale-[0.8]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[0.8]',\r\n },\r\n 'zoom-out': {\r\n enter: 'opacity-0 scale-[1.2]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[1.2]',\r\n },\r\n};\r\n\r\nexport type TransitionName = keyof typeof transitionStyles;\r\n",
|
|
2356
|
+
"target": "web"
|
|
2357
|
+
},
|
|
2358
|
+
{
|
|
2359
|
+
"path": "RebornTransition.vue",
|
|
2360
|
+
"content": "<script setup lang=\"ts\">\r\nimport type { TransitionName } from './reborn-transition.config';\r\nimport { computed, ref, watch } from 'vue';\r\nimport { transitionStyles } from './reborn-transition.config';\r\n\r\nconst props = withDefaults(defineProps<{\r\n show?: boolean;\r\n duration?: number | { enter?: number; leave?: number } | boolean;\r\n lazyRender?: boolean;\r\n name?: TransitionName | TransitionName[];\r\n destroy?: boolean;\r\n customClass?: string;\r\n customStyle?: string;\r\n disableTouchMove?: boolean;\r\n}>(), {\r\n show: false,\r\n duration: 300,\r\n lazyRender: false,\r\n destroy: true,\r\n customClass: '',\r\n customStyle: '',\r\n disableTouchMove: false,\r\n});\r\n\r\nconst emit = defineEmits(['click', 'beforeEnter', 'enter', 'beforeLeave', 'afterLeave', 'leave', 'afterEnter']);\r\n\r\nconst durationOf = (type: 'enter' | 'leave') => (typeof props.duration === 'object'\r\n ? (props.duration[type] ?? 300)\r\n : (props.duration === false ? 0 : Number(props.duration)));\r\n\r\nconst style = computed(() => `${props.customStyle}`);\r\n\r\nfunction getClassNames(name = props.name ?? \"fade\") {\r\n const names = Array.isArray(name) ? name : [name];\r\n const picked = names.map(n => transitionStyles[n]).filter((it): it is Record<string, string> => !!it);\r\n const join = (key: string) => picked.map(it => it[key]).join(' ');\r\n return {\r\n enterFrom: join('enter'),\r\n enterActive: join('enter-active'),\r\n enterTo: join('enter-to'),\r\n leaveFrom: join('leave'),\r\n leaveActive: join('leave-active'),\r\n leaveTo: join('leave-to'),\r\n };\r\n}\r\n\r\nconst transitionClasses = computed(() => getClassNames());\r\nconst renderReady = ref(!props.lazyRender);\r\n\r\nwatch(() => props.show, (val) => {\r\n if (val && props.lazyRender) renderReady.value = true;\r\n}, { immediate: true });\r\n</script>\r\n\r\n<template>\r\n <Transition :enter-from-class=\"transitionClasses.enterFrom\" :enter-active-class=\"transitionClasses.enterActive\"\r\n :enter-to-class=\"transitionClasses.enterTo\" :leave-from-class=\"transitionClasses.leaveFrom\"\r\n :leave-active-class=\"transitionClasses.leaveActive\" :leave-to-class=\"transitionClasses.leaveTo\"\r\n @before-enter=\"emit('beforeEnter')\" @enter=\"emit('enter')\" @after-enter=\"emit('afterEnter')\"\r\n @before-leave=\"emit('beforeLeave')\" @leave=\"emit('leave')\" @after-leave=\"emit('afterLeave')\">\r\n <div v-if=\"renderReady && (props.destroy ? props.show : true)\" v-show=\"props.destroy ? true : props.show\"\r\n :class=\"`rb-transition ease-in-out ${props.customClass}`\"\r\n :style=\"`transition-duration: ${durationOf(props.show ? 'enter' : 'leave')}ms; ${style}`\" @click=\"emit('click')\">\r\n <slot />\r\n </div>\r\n </Transition>\r\n</template>\r\n",
|
|
2361
|
+
"target": "web"
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
"path": "index.ts",
|
|
2365
|
+
"content": "// @ts-ignore\r\nimport RebornTransition from './RebornTransition.vue'\r\n\r\nexport default RebornTransition\r\nexport { RebornTransition }\r\nexport type { TransitionName } from './RebornTransition.vue'\r\n",
|
|
2366
|
+
"target": "uniapp"
|
|
2367
|
+
},
|
|
2368
|
+
{
|
|
2369
|
+
"path": "reborn-transition.config.ts",
|
|
2370
|
+
"content": "export const transitionStyles: Record<string, Record<string, string>> = {\r\n fade: {\r\n enter: 'opacity-0',\r\n 'enter-active': 'transition-opacity',\r\n 'enter-to': 'opacity-100',\r\n leave: 'opacity-100',\r\n 'leave-active': 'transition-opacity',\r\n 'leave-to': 'opacity-0'\r\n },\r\n 'fade-up': {\r\n enter: 'translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-y-full opacity-0'\r\n },\r\n 'fade-down': {\r\n enter: '-translate-y-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-y-0 opacity-100',\r\n leave: 'translate-y-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-y-full opacity-0'\r\n },\r\n 'fade-left': {\r\n enter: '-translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': '-translate-x-full opacity-0'\r\n },\r\n 'fade-right': {\r\n enter: 'translate-x-full opacity-0',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'translate-x-0 opacity-100',\r\n leave: 'translate-x-0 opacity-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'translate-x-full opacity-0'\r\n },\r\n 'slide-up': {\r\n enter: 'translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-y-full'\r\n },\r\n 'slide-down': {\r\n enter: '-translate-y-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-y-0',\r\n leave: 'translate-y-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-y-full'\r\n },\r\n 'slide-left': {\r\n enter: '-translate-x-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': '-translate-x-full'\r\n },\r\n 'slide-right': {\r\n enter: 'translate-x-full',\r\n 'enter-active': 'transition-transform',\r\n 'enter-to': 'translate-x-0',\r\n leave: 'translate-x-0',\r\n 'leave-active': 'transition-transform',\r\n 'leave-to': 'translate-x-full'\r\n },\r\n 'zoom-in': {\r\n enter: 'opacity-0 scale-[0.8]',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-[0.8]'\r\n },\r\n 'zoom-out': {\r\n enter: 'opacity-0 scale-125',\r\n 'enter-active': 'transition-[opacity,transform]',\r\n 'enter-to': 'opacity-100 scale-100',\r\n leave: 'opacity-100 scale-100',\r\n 'leave-active': 'transition-[opacity,transform]',\r\n 'leave-to': 'opacity-0 scale-125'\r\n }\r\n}\r\n",
|
|
2371
|
+
"target": "uniapp"
|
|
2372
|
+
},
|
|
2373
|
+
{
|
|
2374
|
+
"path": "RebornTransition.vue",
|
|
2375
|
+
"content": "<script lang=\"ts\">\r\nexport default {\r\n name: 'reborn-transition',\r\n options: {\r\n addGlobalClass: true,\r\n virtualHost: true,\r\n styleIsolation: 'shared'\r\n }\r\n}\r\n</script>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, onBeforeMount, ref, watch, type PropType } from 'vue'\r\nimport { isObj, isPromise, pause } from '../../lib/util'\r\nimport { AbortablePromise } from '../../lib/AbortablePromise'\r\nimport { transitionStyles } from './reborn-transition.config'\r\n\r\nexport type TransitionName =\r\n | 'fade'\r\n | 'fade-down'\r\n | 'fade-left'\r\n | 'fade-right'\r\n | 'fade-up'\r\n | 'slide-down'\r\n | 'slide-left'\r\n | 'slide-right'\r\n | 'slide-up'\r\n | 'zoom-in'\r\n | 'zoom-out'\r\n\r\nconst props = defineProps({\r\n customClass: { type: String, default: '' },\r\n customStyle: { type: String, default: '' },\r\n show: { type: Boolean, default: false },\r\n duration: { type: [Object, Number, Boolean] as PropType<Record<string, number> | number | boolean>, default: 300 },\r\n lazyRender: { type: Boolean, default: false },\r\n name: { type: [String, Array] as PropType<TransitionName | TransitionName[]>, default: 'fade' },\r\n destroy: { type: Boolean, default: true },\r\n enterClass: { type: String, default: '' },\r\n enterActiveClass: { type: String, default: '' },\r\n enterToClass: { type: String, default: '' },\r\n leaveClass: { type: String, default: '' },\r\n leaveActiveClass: { type: String, default: '' },\r\n leaveToClass: { type: String, default: '' },\r\n disableTouchMove: { type: Boolean, default: false }\r\n})\r\n\r\nconst emit = defineEmits(['click', 'before-enter', 'enter', 'before-leave', 'leave', 'after-leave', 'after-enter'])\r\n\r\nconst inited = ref<boolean>(false)\r\nconst display = ref<boolean>(false)\r\nconst status = ref<string>('')\r\nconst transitionEnded = ref<boolean>(false)\r\nconst currentDuration = ref<number>(300)\r\nconst classes = ref<string>('')\r\nconst enterPromise = ref<AbortablePromise<void> | null>(null)\r\nconst enterLifeCyclePromises = ref<AbortablePromise<unknown> | null>(null)\r\nconst leaveLifeCyclePromises = ref<AbortablePromise<unknown> | null>(null)\r\n\r\nconst getClassNames = (name?: TransitionName | TransitionName[]) => {\r\n let enter: string = `${props.enterClass} ${props.enterActiveClass}`\r\n let enterTo: string = `${props.enterToClass} ${props.enterActiveClass}`\r\n let leave: string = `${props.leaveClass} ${props.leaveActiveClass}`\r\n let leaveTo: string = `${props.leaveToClass} ${props.leaveActiveClass}`\r\n\r\n const addClass = (n: string) => {\r\n const config = transitionStyles[n]\r\n if (config) {\r\n enter = `rb-transition-${n}-enter ${config.enter} rb-transition-${n}-enter-active ${config['enter-active']} ${enter}`\r\n enterTo = `rb-transition-${n}-enter-to ${config['enter-to']} rb-transition-${n}-enter-active ${config['enter-active']} ${enterTo}`\r\n leave = `rb-transition-${n}-leave ${config.leave} rb-transition-${n}-leave-active ${config['leave-active']} ${leave}`\r\n leaveTo = `rb-transition-${n}-leave-to ${config['leave-to']} rb-transition-${n}-leave-active ${config['leave-active']} ${leaveTo}`\r\n } else {\r\n enter = `rb-${n}-enter rb-${n}-enter-active ${enter}`\r\n enterTo = `rb-${n}-enter-to rb-${n}-enter-active ${enterTo}`\r\n leave = `rb-${n}-leave rb-${n}-leave-active ${leave}`\r\n leaveTo = `rb-${n}-leave-to rb-${n}-leave-active ${leaveTo}`\r\n }\r\n }\r\n\r\n if (Array.isArray(name)) {\r\n name.forEach(n => addClass(n))\r\n } else if (name) {\r\n addClass(name as string)\r\n }\r\n\r\n return { enter, 'enter-to': enterTo, leave, 'leave-to': leaveTo }\r\n}\r\n\r\nconst style = computed(() => {\r\n return `-webkit-transition-duration:${currentDuration.value}ms;transition-duration:${currentDuration.value}ms;${display.value || !props.destroy ? '' : 'display: none;'\r\n }${props.customStyle}`\r\n})\r\n\r\nconst rootClass = computed(() => `rb-transition ease-in-out ${props.customClass} ${classes.value}`)\r\n\r\nconst isShow = computed(() => !props.lazyRender || inited.value)\r\n\r\nonBeforeMount(() => { if (props.show) enter() })\r\nwatch(() => props.show, (newVal) => handleShow(newVal), { deep: true })\r\nfunction handleClick() { emit('click') }\r\nfunction handleShow(value: boolean) {\r\n if (value) { handleAbortPromise(); enter() } else leave()\r\n}\r\nfunction handleAbortPromise() {\r\n isPromise(enterPromise.value) && enterPromise.value.abort()\r\n isPromise(enterLifeCyclePromises.value) && enterLifeCyclePromises.value.abort()\r\n isPromise(leaveLifeCyclePromises.value) && leaveLifeCyclePromises.value.abort()\r\n enterPromise.value = null\r\n enterLifeCyclePromises.value = null\r\n leaveLifeCyclePromises.value = null\r\n}\r\nfunction enter() {\r\n enterPromise.value = new AbortablePromise(async (resolve) => {\r\n try {\r\n const classNames = getClassNames(props.name)\r\n const duration = isObj(props.duration) ? (props.duration as any).enter : props.duration\r\n status.value = 'enter'\r\n emit('before-enter')\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n emit('enter')\r\n classes.value = classNames.enter\r\n currentDuration.value = duration\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n inited.value = true\r\n display.value = true\r\n enterLifeCyclePromises.value = pause()\r\n await enterLifeCyclePromises.value\r\n enterLifeCyclePromises.value = null\r\n transitionEnded.value = false\r\n classes.value = classNames['enter-to']\r\n resolve()\r\n } catch (error) { }\r\n })\r\n}\r\nasync function leave() {\r\n if (!enterPromise.value) {\r\n transitionEnded.value = false\r\n return onTransitionEnd()\r\n }\r\n try {\r\n await enterPromise.value\r\n if (!display.value) return\r\n const classNames = getClassNames(props.name)\r\n const duration = isObj(props.duration) ? (props.duration as any).leave : props.duration\r\n status.value = 'leave'\r\n emit('before-leave')\r\n currentDuration.value = duration\r\n leaveLifeCyclePromises.value = pause()\r\n await leaveLifeCyclePromises.value\r\n emit('leave')\r\n classes.value = classNames.leave\r\n leaveLifeCyclePromises.value = pause()\r\n await leaveLifeCyclePromises.value\r\n transitionEnded.value = false\r\n classes.value = classNames['leave-to']\r\n leaveLifeCyclePromises.value = setPromise(currentDuration.value)\r\n await leaveLifeCyclePromises.value\r\n leaveLifeCyclePromises.value = null\r\n onTransitionEnd()\r\n enterPromise.value = null\r\n } catch (error) { }\r\n}\r\nfunction setPromise(duration: number) {\r\n return new AbortablePromise<void>((resolve) => {\r\n const timer = setTimeout(() => { clearTimeout(timer); resolve() }, duration)\r\n })\r\n}\r\nfunction onTransitionEnd() {\r\n if (transitionEnded.value) return\r\n transitionEnded.value = true\r\n if (status.value === 'leave') {\r\n emit('after-leave')\r\n } else if (status.value === 'enter') { emit('after-enter') }\r\n if (!props.show && display.value) display.value = false\r\n}\r\nfunction noop() { }\r\n</script>\r\n\r\n<template>\r\n <view :class=\"rootClass\" :style=\"style\" @transitionend=\"onTransitionEnd\" @click=\"handleClick\"\r\n @touchmove.stop.prevent=\"noop\" v-if=\"isShow && disableTouchMove\">\r\n <slot />\r\n </view>\r\n <view :class=\"rootClass\" :style=\"style\" @transitionend=\"onTransitionEnd\" @click=\"handleClick\"\r\n v-else-if=\"isShow && !disableTouchMove\">\r\n <slot />\r\n </view>\r\n</template>",
|
|
2376
|
+
"target": "uniapp"
|
|
2377
|
+
}
|
|
2378
|
+
]
|
|
2379
|
+
},
|
|
2380
|
+
{
|
|
2381
|
+
"name": "reborn-waterfall",
|
|
2382
|
+
"dependencies": [],
|
|
2383
|
+
"files": [
|
|
2384
|
+
{
|
|
2385
|
+
"path": "reborn-waterfall.config.ts",
|
|
2386
|
+
"content": "export default {\r\n slots: {\r\n root: 'flex flex-row w-full relative',\r\n column: 'flex-1',\r\n item: '',\r\n inner: '',\r\n virtual: 'absolute top-0 w-full -left-full opacity-0',\r\n },\r\n} as const\r\n",
|
|
2387
|
+
"target": "uniapp"
|
|
2388
|
+
},
|
|
2389
|
+
{
|
|
2390
|
+
"path": "RebornWaterfall.vue",
|
|
2391
|
+
"content": "<script lang=\"ts\">\r\nexport type Props = {\r\n\tui?: any;\r\n\tcustomClass?: string;\r\n\tcolumn?: number; // 瀑布流列数,默认为2列\r\n\tgutter?: number; // 列间距,单位为px\r\n\tnodeKey?: string; // 数据项的唯一标识字段名,默认为\"id\"\r\n};\r\n</script>\r\n<script setup lang=\"ts\">\r\nimport { computed, getCurrentInstance, nextTick, onMounted, ref, watch } from \"vue\";\r\nimport { tv } from \"@/lib/tv\";\r\nimport { cn } from \"@/lib/utils\";\r\nimport theme from \"./reborn-waterfall.config\";\r\n\r\ndefineOptions({\r\n\tname: \"reborn-waterfall\"\r\n});\r\n\r\n\r\n// 定义插槽类型,item插槽接收item和index参数\r\ndefineSlots<{\r\n\titem(props: { item: any; index: number }): any;\r\n}>();\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n\tcolumn: 2,\r\n\tgutter: 16,\r\n\tnodeKey: \"id\",\r\n\tui: () => ({}),\r\n});\r\n\r\n// 获取当前组件实例的代理对象\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\nconst b = tv(theme)\r\n\r\n// 存储每列的当前高度,用于计算最短列\r\nconst heights = ref<number[]>([]);\r\n\r\n// 存储瀑布流数据,二维数组,每个子数组代表一列\r\nconst columns = ref<any[][]>([]);\r\n\r\nconst uiOverrides = computed(() => props.ui || {})\r\nconst uiClasses = computed(() => {\r\n\tconst styles = b({\r\n\t\tcolumn: props.column,\r\n\t\tgutter: props.gutter,\r\n\t\tnodeKey: props.nodeKey\r\n\t})\r\n\treturn {\r\n\t\troot: (opts?: { class?: any }) => styles.root({ class: cn(opts?.class, uiOverrides.value.root) }),\r\n\t\tcolumn: (opts?: { class?: any }) => styles.column({ class: cn(opts?.class, uiOverrides.value.column) }),\r\n\t\titem: (opts?: { class?: any }) => styles.item({ class: cn(opts?.class, uiOverrides.value.item) }),\r\n\t\tinner: (opts?: { class?: any }) => styles.inner({ class: cn(opts?.class, uiOverrides.value.inner) }),\r\n\t\tvirtual: (opts?: { class?: any }) => styles.virtual({ class: cn(opts?.class, uiOverrides.value.virtual) })\r\n\t}\r\n})\r\n/**\r\n * 获取各列的当前高度\r\n * 通过uni.createSelectorQuery查询DOM元素的实际高度\r\n * @returns Promise<> 返回Promise对象\r\n */\r\nasync function getHeight(): Promise<void> {\r\n\t// 等待DOM更新完成\r\n\tawait nextTick();\r\n\r\n\treturn new Promise((resolve) => {\r\n\t\t// 创建选择器查询,获取所有列容器的边界信息\r\n\t\tuni.createSelectorQuery()\r\n\t\t\t.in(proxy)\r\n\t\t\t.selectAll(\".cl-waterfall__column-inner\")\r\n\t\t\t.boundingClientRect()\r\n\t\t\t.exec((rect) => {\r\n\t\t\t\tconst nodes = rect[0] as any[];\r\n\r\n\t\t\t\tif (!(nodes == null || nodes == undefined)) {\r\n\t\t\t\t\t// 提取每列的高度信息,如果获取失败则默认为0\r\n\t\t\t\t\theights.value = nodes.map((e) => e.height ?? 0);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tresolve();\r\n\t\t\t});\r\n\t});\r\n}\r\n\r\n/**\r\n * 向瀑布流添加新数据\r\n * 使用虚拟定位技术计算每个项目的高度,然后分配到最短的列\r\n * @param data 要添加的数据数组\r\n */\r\nasync function append(data: any[]) {\r\n\t// 首先获取当前各列高度\r\n\tawait getHeight();\r\n\r\n\t// 将新数据作为虚拟项目添加到第一列,用于计算高度\r\n\tcolumns.value[0].push(\r\n\t\t...data.map((e) => {\r\n\t\t\treturn {\r\n\t\t\t\t...e,\r\n\t\t\t\tisVirtual: true // 标记为虚拟项目,会在CSS中隐藏\r\n\t\t\t} as any;\r\n\t\t})\r\n\t);\r\n\r\n\t// 等待DOM更新\r\n\tawait nextTick();\r\n\r\n\t// 延迟300ms后计算虚拟项目的高度并重新分配\r\n\tsetTimeout(() => {\r\n\t\tuni.createSelectorQuery()\r\n\t\t\t.in(proxy)\r\n\t\t\t.selectAll(\".is-virtual\")\r\n\t\t\t.boundingClientRect()\r\n\t\t\t.exec((rect) => {\r\n\t\t\t\t// 遍历每个虚拟项目\r\n\t\t\t\t(rect[0] as any[]).forEach((e, i) => {\r\n\t\t\t\t\t// 找到当前高度最小的列\r\n\t\t\t\t\tconst min = Math.min(...heights.value);\r\n\t\t\t\t\tconst index = heights.value.indexOf(min);\r\n\r\n\t\t\t\t\t// 将实际数据添加到最短列\r\n\t\t\t\t\tcolumns.value[index].push(data[i]);\r\n\r\n\t\t\t\t\t// 更新该列的高度\r\n\t\t\t\t\theights.value[index] += e.height ?? 0;\r\n\r\n\t\t\t\t\t// 清除第一列中的虚拟项目(临时用于计算高度的项目)\r\n\t\t\t\t\tcolumns.value[0] = columns.value[0].filter((e) => e.isVirtual != true);\r\n\t\t\t\t});\r\n\t\t\t});\r\n\t}, 300);\r\n}\r\n/**\r\n * 根据ID移除指定项目\r\n * @param id 要移除的项目ID\r\n */\r\nfunction remove(id: string | number) {\r\n\tcolumns.value.forEach((column, columnIndex) => {\r\n\t\t// 过滤掉指定ID的项目\r\n\t\tcolumns.value[columnIndex] = column.filter((e) => e[props.nodeKey] != id);\r\n\t});\r\n}\r\n\r\n/**\r\n * 根据ID更新指定项目的数据\r\n * @param id 要更新的项目ID\r\n * @param data 新的数据对象\r\n */\r\nfunction update(id: string | number, data: any) {\r\n\tcolumns.value.forEach((column) => {\r\n\t\tcolumn.forEach((e) => {\r\n\t\t\t// 找到指定ID的项目并更新数据\r\n\t\t\tif (e[props.nodeKey] == id) {\r\n\t\t\t\tObject.assign(e, data);\r\n\t\t\t}\r\n\t\t});\r\n\t});\r\n}\r\n/**\r\n * 清空瀑布流数据\r\n * 重新初始化列数组\r\n */\r\nfunction clear() {\r\n\tcolumns.value = [];\r\n\r\n\t// 根据列数创建空的列数组\r\n\tfor (let i = 0; i < props.column; i++) {\r\n\t\tcolumns.value.push([]);\r\n\t}\r\n}\r\n\r\n// 组件挂载时的初始化逻辑\r\nonMounted(() => {\r\n\t// 监听列数变化,当列数改变时重新初始化\r\n\twatch(\r\n\t\tcomputed(() => props.column),\r\n\t\t() => {\r\n\t\t\tclear(); // 清空现有数据\r\n\t\t\tgetHeight(); // 重新获取高度\r\n\t\t},\r\n\t\t{\r\n\t\t\timmediate: true // 立即执行一次\r\n\t\t}\r\n\t);\r\n});\r\n\r\ndefineExpose({\r\n\tappend,\r\n\tremove,\r\n\tupdate,\r\n\tclear\r\n});\r\n</script>\r\n<template>\r\n\t<view :class=\"uiClasses.root({ class: customClass })\" :style=\"{\r\n\t\tpadding: `0 ${props.gutter / 2}px`\r\n\t}\">\r\n\t\t<view :class=\"uiClasses.column()\" v-for=\"(column, columnIndex) in columns\" :key=\"columnIndex\" :style=\"{\r\n\t\t\tmargin: `0 ${props.gutter / 2}px`\r\n\t\t}\">\r\n\t\t\t<view :class=\"[uiClasses.inner(), 'cl-waterfall__column-inner']\">\r\n\t\t\t\t<view v-for=\"(item, index) in column\" :key=\"`${columnIndex}-${index}-${item[props.nodeKey]}`\"\r\n\t\t\t\t\t:class=\"[uiClasses.item({ class: item.isVirtual ? uiClasses.virtual() : '' }), item.isVirtual ? 'is-virtual' : '']\">\r\n\t\t\t\t\t<slot name=\"item\" :item=\"item\" :index=\"index\"></slot>\r\n\t\t\t\t</view>\r\n\t\t\t</view>\r\n\t\t</view>\r\n\t</view>\r\n</template>",
|
|
1388
2392
|
"target": "uniapp"
|
|
1389
2393
|
}
|
|
1390
2394
|
]
|
|
@@ -1475,7 +2479,7 @@
|
|
|
1475
2479
|
},
|
|
1476
2480
|
{
|
|
1477
2481
|
"path": "ScrollIsland.vue",
|
|
1478
|
-
"content": "<script lang=\"ts\" setup>\r\nimport NumberFlow from \"@number-flow/vue\";\r\nimport { useColorMode } from \"@vueuse/core\";\r\nimport { motion, MotionConfig } from \"motion-v\";\r\nimport { computed, onMounted, onUnmounted, ref, useSlots } from \"vue\";\r\n\r\ninterface Props {\r\n class?: string;\r\n title?: string;\r\n height?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n class: \"\",\r\n title: \"Progress\",\r\n height: 44,\r\n});\r\n\r\nconst open = ref(false);\r\nconst slots = useSlots();\r\n\r\nconst scrollPercentage = ref(0);\r\n\r\nconst isDark = computed(() => useColorMode().value == \"dark\");\r\nconst isSlotAvailable = computed(() => !!slots.default);\r\nconst borderRadius = computed(() => `${props.height / 2}px`);\r\n\r\nonMounted(() => {\r\n if (window === undefined) return;\r\n\r\n window.addEventListener(\"scroll\", updatePageScroll);\r\n updatePageScroll();\r\n});\r\n\r\nfunction updatePageScroll() {\r\n scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);\r\n}\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"scroll\", updatePageScroll);\r\n});\r\n</script>\r\n\r\n<template>\r\n <MotionConfig
|
|
2482
|
+
"content": "<script lang=\"ts\" setup>\r\nimport NumberFlow from \"@number-flow/vue\";\r\nimport { useColorMode } from \"@vueuse/core\";\r\nimport { motion, MotionConfig } from \"motion-v\";\r\nimport { computed, onMounted, onUnmounted, ref, useSlots } from \"vue\";\r\n\r\ninterface Props {\r\n class?: string;\r\n title?: string;\r\n height?: number;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n class: \"\",\r\n title: \"Progress\",\r\n height: 44,\r\n});\r\n\r\nconst open = ref(false);\r\nconst slots = useSlots();\r\n\r\nconst scrollPercentage = ref(0);\r\n\r\nconst isDark = computed(() => useColorMode().value == \"dark\");\r\nconst isSlotAvailable = computed(() => !!slots.default);\r\nconst borderRadius = computed(() => `${props.height / 2}px`);\r\n\r\nonMounted(() => {\r\n if (window === undefined) return;\r\n\r\n window.addEventListener(\"scroll\", updatePageScroll);\r\n updatePageScroll();\r\n});\r\n\r\nfunction updatePageScroll() {\r\n scrollPercentage.value = window.scrollY / (document.body.scrollHeight - window.innerHeight);\r\n}\r\n\r\nonUnmounted(() => {\r\n window.removeEventListener(\"scroll\", updatePageScroll);\r\n});\r\n</script>\r\n\r\n<template>\r\n <MotionConfig :transition=\"{\r\n duration: 0.7,\r\n type: 'spring',\r\n bounce: 0.5,\r\n }\">\r\n <div class=\"bg-primary/90 border-radius fixed top-12 left-1/2 z-[999] -translate-x-1/2 backdrop-blur-lg\"\r\n :class=\"[$props.class]\" @click=\"() => (open = !open)\">\r\n <motion.div id=\"motion-id\" layout :initial=\"{\r\n height: props.height,\r\n width: 0,\r\n }\" :animate=\"{\r\n height: open && isSlotAvailable ? 'auto' : props.height,\r\n width: open && isSlotAvailable ? 320 : 260,\r\n }\" class=\"bg-natural-900 text-secondary relative cursor-pointer overflow-hidden\">\r\n <header class=\"gray- flex h-11 cursor-pointer items-center gap-2 px-4\">\r\n <AnimatedCircularProgressBar :value=\"scrollPercentage * 100\" :min=\"0\" :max=\"100\" :circle-stroke-width=\"10\"\r\n class=\"w-6\" :show-percentage=\"false\" :duration=\"0.3\"\r\n :gauge-secondary-color=\"isDark ? '#6b728055' : '#6b728099'\"\r\n :gauge-primary-color=\"isDark ? 'black' : 'white'\" />\r\n <h1 class=\"grow text-center font-bold text-white\">{{ title }}</h1>\r\n <NumberFlow :value=\"scrollPercentage\" :format=\"{ style: 'percent' }\" locales=\"en-US\" />\r\n </header>\r\n <motion.div v-if=\"isSlotAvailable\"\r\n class=\"mb-2 flex h-full max-h-60 flex-col gap-1 overflow-y-auto px-4 text-sm\">\r\n <slot />\r\n </motion.div>\r\n </motion.div>\r\n </div>\r\n </MotionConfig>\r\n</template>\r\n\r\n<style scoped>\r\n.border-radius {\r\n border-radius: v-bind(borderRadius);\r\n}\r\n</style>\r\n",
|
|
1479
2483
|
"target": "web"
|
|
1480
2484
|
}
|
|
1481
2485
|
]
|