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.
Files changed (50) hide show
  1. package/dist/index.js +182 -240
  2. package/dist/index.js.map +1 -1
  3. package/package.json +53 -53
  4. package/registry/components/reborn-affix.json +8 -3
  5. package/registry/components/reborn-back-top.json +9 -4
  6. package/registry/components/reborn-badge.json +11 -5
  7. package/registry/components/reborn-button.json +5 -5
  8. package/registry/components/reborn-card.json +18 -0
  9. package/registry/components/reborn-cascader.json +18 -0
  10. package/registry/components/reborn-checkbox.json +4 -4
  11. package/registry/components/reborn-chip.json +11 -5
  12. package/registry/components/reborn-collapse.json +11 -5
  13. package/registry/components/reborn-color-picker.json +50 -0
  14. package/registry/components/reborn-draggable.json +32 -0
  15. package/registry/components/reborn-drawer.json +17 -0
  16. package/registry/components/reborn-dropdown-select.json +18 -0
  17. package/registry/components/reborn-footer.json +40 -0
  18. package/registry/components/reborn-form.json +11 -6
  19. package/registry/components/reborn-image.json +10 -5
  20. package/registry/components/reborn-input-number.json +12 -6
  21. package/registry/components/reborn-input-otp.json +40 -0
  22. package/registry/components/reborn-input.json +4 -4
  23. package/registry/components/reborn-loading.json +23 -0
  24. package/registry/components/reborn-loadmore.json +23 -0
  25. package/registry/components/reborn-overlay.json +38 -0
  26. package/registry/components/reborn-page.json +18 -0
  27. package/registry/components/reborn-picker-view.json +26 -0
  28. package/registry/components/reborn-popover.json +58 -0
  29. package/registry/components/reborn-popup.json +23 -0
  30. package/registry/components/reborn-qrcode.json +45 -0
  31. package/registry/components/reborn-radio.json +45 -0
  32. package/registry/components/reborn-rate.json +40 -0
  33. package/registry/components/reborn-root-portal.json +26 -0
  34. package/registry/components/reborn-select-date.json +40 -0
  35. package/registry/components/reborn-select-trigger.json +25 -0
  36. package/registry/components/reborn-select.json +41 -0
  37. package/registry/components/reborn-slider.json +40 -0
  38. package/registry/components/reborn-sticky.json +12 -6
  39. package/registry/components/reborn-switch.json +13 -7
  40. package/registry/components/reborn-tabbar.json +38 -0
  41. package/registry/components/reborn-tabs copy.json +46 -0
  42. package/registry/components/reborn-tabs-test.json +46 -0
  43. package/registry/components/reborn-tabs.json +12 -6
  44. package/registry/components/reborn-text.json +34 -0
  45. package/registry/components/reborn-textarea.json +5 -5
  46. package/registry/components/reborn-toast.json +38 -0
  47. package/registry/components/reborn-transition.json +38 -0
  48. package/registry/components/reborn-waterfall.json +18 -0
  49. package/registry/components/scroll-island.json +2 -2
  50. package/registry/registry.json +1101 -97
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "reborn-select-date",
3
+ "dependencies": [
4
+ "clsx",
5
+ "lodash-es"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as RebornSelectDate } from \"./RebornSelectDate.vue\";\r\n"
11
+ },
12
+ {
13
+ "path": "reborn-select-date.config.ts",
14
+ "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",
15
+ "target": "web"
16
+ },
17
+ {
18
+ "path": "RebornSelectDate.vue",
19
+ "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",
20
+ "target": "web"
21
+ },
22
+ {
23
+ "path": "reborn-select-date.config.ts",
24
+ "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",
25
+ "target": "uniapp"
26
+ },
27
+ {
28
+ "path": "RebornSelectDate.vue",
29
+ "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",
30
+ "target": "uniapp"
31
+ },
32
+ {
33
+ "path": "types.ts",
34
+ "content": "export interface SelectDateShortcut {\r\n label: string\r\n value: string[]\r\n}\r\n",
35
+ "target": "uniapp"
36
+ }
37
+ ],
38
+ "fileCount": 6,
39
+ "contentHash": "60d7acf083cbedc7df60769e024b847ea67e710c"
40
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "reborn-select-trigger",
3
+ "dependencies": [
4
+ "clsx"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as RebornSelectTrigger } from './RebornSelectTrigger.vue'\r\n",
10
+ "target": "uniapp"
11
+ },
12
+ {
13
+ "path": "reborn-select-trigger.config.ts",
14
+ "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",
15
+ "target": "uniapp"
16
+ },
17
+ {
18
+ "path": "RebornSelectTrigger.vue",
19
+ "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",
20
+ "target": "uniapp"
21
+ }
22
+ ],
23
+ "fileCount": 3,
24
+ "contentHash": "db8fc4c3822d69eeb8fe7fbe887dae02b7657503"
25
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "reborn-select",
3
+ "dependencies": [
4
+ "clsx",
5
+ "lodash-es"
6
+ ],
7
+ "files": [
8
+ {
9
+ "path": "index.ts",
10
+ "content": "export { default as RebornSelect } from \"./RebornSelect.vue\";\r\n",
11
+ "target": "web"
12
+ },
13
+ {
14
+ "path": "reborn-select.config.ts",
15
+ "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",
16
+ "target": "web"
17
+ },
18
+ {
19
+ "path": "RebornSelect.vue",
20
+ "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",
21
+ "target": "web"
22
+ },
23
+ {
24
+ "path": "index.ts",
25
+ "content": "export { default as RebornSelect } from './RebornSelect.vue'\r\nexport type { SelectProps, SelectValue } from './RebornSelect.vue'\r\n",
26
+ "target": "uniapp"
27
+ },
28
+ {
29
+ "path": "reborn-select.config.ts",
30
+ "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",
31
+ "target": "uniapp"
32
+ },
33
+ {
34
+ "path": "RebornSelect.vue",
35
+ "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",
36
+ "target": "uniapp"
37
+ }
38
+ ],
39
+ "fileCount": 6,
40
+ "contentHash": "1610acf4899d0a240b9cf0bda68e0299f24eaa9d"
41
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "reborn-slider",
3
+ "dependencies": [
4
+ "clsx"
5
+ ],
6
+ "files": [
7
+ {
8
+ "path": "index.ts",
9
+ "content": "export { default as RebornSlider } from \"./RebornSlider.vue\";\r\n",
10
+ "target": "web"
11
+ },
12
+ {
13
+ "path": "reborn-slider.config.ts",
14
+ "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",
15
+ "target": "web"
16
+ },
17
+ {
18
+ "path": "RebornSlider.vue",
19
+ "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",
20
+ "target": "web"
21
+ },
22
+ {
23
+ "path": "index.ts",
24
+ "content": "export { default as RebornSlider } from './RebornSlider.vue'\r\n",
25
+ "target": "uniapp"
26
+ },
27
+ {
28
+ "path": "reborn-slider.config.ts",
29
+ "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",
30
+ "target": "uniapp"
31
+ },
32
+ {
33
+ "path": "RebornSlider.vue",
34
+ "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",
35
+ "target": "uniapp"
36
+ }
37
+ ],
38
+ "fileCount": 6,
39
+ "contentHash": "75c3f93a72efc67277cc351ff0036eacc55fa20e"
40
+ }
@@ -1,30 +1,36 @@
1
1
  {
2
2
  "name": "reborn-sticky",
3
3
  "dependencies": [
4
+ "@vueuse/core",
4
5
  "tailwind-variants"
5
6
  ],
6
7
  "files": [
7
8
  {
8
9
  "path": "reborn-sticky.config.ts",
9
- "content": "import { tv, type VariantProps } from 'tailwind-variants'\r\n\r\nexport const stickyVariants = tv({\r\n base: '',\r\n})\r\n\r\nexport type StickyVariants = VariantProps<typeof stickyVariants>\r\n\r\nexport default {\r\n base: ''\r\n}\r\n",
10
+ "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",
10
11
  "target": "web"
11
12
  },
12
13
  {
13
14
  "path": "RebornSticky.vue",
14
- "content": "<template>\r\n <div\r\n class=\"p-6 text-center border-2 border-dashed rounded-xl border-slate-200 dark:border-slate-800 text-slate-500\">\r\n RebornSticky (Web) - Under Development\r\n </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\ndefineOptions({\r\n name: \"RebornSticky\"\r\n})\r\n</script>\r\n",
15
+ "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",
15
16
  "target": "web"
16
17
  },
18
+ {
19
+ "path": "index.ts",
20
+ "content": "export { default as RebornSticky } from './RebornSticky.vue'\r\n",
21
+ "target": "uniapp"
22
+ },
17
23
  {
18
24
  "path": "reborn-sticky.config.ts",
19
- "content": "import { type VariantProps, tv } from \"tailwind-variants\";\r\n\r\nexport const stickyVariants = tv({\r\n slots: {\r\n wrapper: \"\",\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\",\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});\r\n\r\nexport type StickyVariants = VariantProps<typeof stickyVariants>;\r\n\r\nexport 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;",
25
+ "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",
20
26
  "target": "uniapp"
21
27
  },
22
28
  {
23
29
  "path": "RebornSticky.vue",
24
- "content": "<template>\r\n <view class=\"reborn-sticky-wrapper\" :class=\"ui.wrapper()\" :style=\"{\r\n height: rect.height == 0 ? 'auto' : rect.height + 'px',\r\n zIndex\r\n }\">\r\n <view :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 </view>\r\n </view>\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\nimport { computed, getCurrentInstance, onMounted, reactive, ref, watch } from \"vue\";\r\nimport { tv } from '@/lib/tv';\r\nimport theme from \"./reborn-sticky.config\";\r\n\r\ndefineOptions({\r\n name: \"reborn-sticky\"\r\n});\r\n\r\ndefineSlots<{\r\n default(props: { isSticky: boolean }): any;\r\n}>();\r\n\r\nconst props = defineProps({\r\n // 吸顶偏移量, 单位px\r\n offsetTop: {\r\n type: Number,\r\n default: 0\r\n },\r\n // 层级\r\n zIndex: {\r\n type: Number,\r\n default: 100\r\n },\r\n // 滚动位置\r\n scrollTop: {\r\n type: Number,\r\n default: 0\r\n },\r\n // 是否需要减去导航栏高度\r\n isNeedNavbarHeight: {\r\n type: Boolean,\r\n default: true\r\n },\r\n // 导航栏高度\r\n navbarHeight: {\r\n type: Number,\r\n default: 44\r\n }\r\n});\r\n\r\nconst { proxy } = getCurrentInstance()!;\r\n\r\nconst b = tv(theme);\r\n\r\n// 表示元素的位置信息\r\ntype 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 scrollTop = ref(0);\r\n\r\n// 计算属性,判断当前是否处于吸顶状态\r\nconst isSticky = computed(() => {\r\n if (rect.height == 0) return false;\r\n\r\n return scrollTop.value >= 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 } else {\r\n return 0;\r\n }\r\n});\r\n\r\nconst isHarmony = (): boolean => {\r\n // #ifdef APP-HARMONY\r\n return true;\r\n // #endif\r\n\r\n return false;\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\") // Updated selector\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 // 赋值时做空值处理,保证类型安全\r\n rect.height = node.height ?? 0;\r\n\r\n rect.width = node.width ?? 0;\r\n rect.left = node.left ?? 0;\r\n // top需要减去offsetTop并加上当前滚动距离,保证吸顶准确\r\n rect.top = (node.top ?? 0) - props.offsetTop + scrollTop.value;\r\n });\r\n };\r\n\r\n if (isHarmony()) {\r\n setTimeout(() => {\r\n next();\r\n }, 300);\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>",
30
+ "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",
25
31
  "target": "uniapp"
26
32
  }
27
33
  ],
28
- "fileCount": 4,
29
- "contentHash": "2cfb300f44d7e1149ab31eedb10d76ab0d3a7f9d"
34
+ "fileCount": 5,
35
+ "contentHash": "7943a63489a748610af9af0a2fcb51276b020135"
30
36
  }