best-unit 1.5.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,235 @@
1
+ import { Size, Theme, type ThemeConfig } from "@/types";
2
+ import { getInitParams } from "@/utils/business";
3
+
4
+ function createSelectPaymentThemes() {
5
+ const size = getInitParams<Size>("size");
6
+ const base = {
7
+ left: {
8
+ background: "#fff",
9
+ borderRadius: 8,
10
+ padding: 16,
11
+ border: "1px solid #E5E6EB",
12
+ },
13
+ blockTitle: {
14
+ fontSize: size === Size.SMALL ? 16 : 18,
15
+ fontWeight: 700,
16
+ marginBottom: size === Size.SMALL ? 12 : 16,
17
+ textAlign: "left",
18
+ display: "flex",
19
+ alignItems: "center",
20
+ gap: 8,
21
+ },
22
+ titleBar: {
23
+ width: 3,
24
+ height: size === Size.SMALL ? 16 : 20,
25
+ background: "#1890ff",
26
+ borderRadius: 2,
27
+ },
28
+ fieldLabel: {
29
+ fontSize: size === Size.SMALL ? 14 : 16,
30
+ marginBottom: 6,
31
+ fontWeight: 500,
32
+ textAlign: "left",
33
+ },
34
+ required: {
35
+ color: "#ff4d4f",
36
+ marginRight: 4,
37
+ },
38
+ currencyRow: {
39
+ display: "flex",
40
+ gap: 10,
41
+ flexWrap: "wrap",
42
+ // 响应式设计
43
+ "@media (max-width: 600px)": {
44
+ gap: 8,
45
+ },
46
+ },
47
+ currencyItem: (active: boolean) => ({
48
+ padding: "6px 12px",
49
+ borderRadius: 6,
50
+ border: `1px solid ${active ? "#1890ff" : "#E5E6EB"}`,
51
+ color: active ? "#1890ff" : "#222",
52
+ background: active ? "#EEF7FF" : "#fff",
53
+ cursor: "pointer",
54
+ }),
55
+ inputWrapper: { position: "relative" },
56
+ input: {
57
+ width: "100%",
58
+ padding: size === Size.SMALL ? "8px 10px" : "10px 12px",
59
+ borderRadius: 6,
60
+ outline: "none",
61
+ border: "1px solid",
62
+ boxSizing: "border-box",
63
+ paddingRight: size === Size.SMALL ? 48 : 64,
64
+ },
65
+ inputError: { border: "1px solid #ff4d4f" },
66
+ inputSuffixAbs: {
67
+ position: "absolute",
68
+ top: 0,
69
+ right: size === Size.SMALL ? 10 : 12,
70
+ height: "100%",
71
+ display: "flex",
72
+ alignItems: "center",
73
+ color: "#999",
74
+ pointerEvents: "none",
75
+ fontSize: size === Size.SMALL ? 12 : 13,
76
+ },
77
+ subTitle: {
78
+ fontSize: size === Size.SMALL ? 12 : 14,
79
+ marginTop: size === Size.SMALL ? 10 : 14,
80
+ marginBottom: size === Size.SMALL ? 8 : 10,
81
+ textAlign: "left",
82
+ },
83
+ methodList: {
84
+ display: "grid",
85
+ gridTemplateColumns: "1fr 1fr",
86
+ gap: 10,
87
+ // 响应式设计
88
+ "@media (max-width: 600px)": {
89
+ gridTemplateColumns: "1fr",
90
+ gap: 8,
91
+ },
92
+ },
93
+ methodItem: (active: boolean) => ({
94
+ padding: "12px 16px",
95
+ textAlign: "left",
96
+ borderRadius: 10,
97
+ border: `1px solid ${active ? "#1890ff" : "#F0F1F3"}`,
98
+ background: active ? "#EEF7FF" : "#F7F8FA",
99
+ cursor: "pointer",
100
+ display: "flex",
101
+ alignItems: "center",
102
+ gap: 8,
103
+ minHeight: 44,
104
+ }),
105
+ channelIcon: {
106
+ width: 22,
107
+ height: 22,
108
+ objectFit: "contain" as const,
109
+ marginRight: 8,
110
+ display: "inline-block",
111
+ },
112
+ bankList: {
113
+ display: "grid",
114
+ gridTemplateColumns: "1fr 1fr",
115
+ gap: 10,
116
+ // 响应式设计
117
+ "@media (max-width: 600px)": {
118
+ gridTemplateColumns: "1fr",
119
+ gap: 8,
120
+ },
121
+ },
122
+ bankItem: (active: boolean) => ({
123
+ padding: "12px 16px",
124
+ borderRadius: 10,
125
+ border: `1px solid ${active ? "#1890ff" : "#F0F1F3"}`,
126
+ background: active ? "#EEF7FF" : "#F7F8FA",
127
+ textAlign: "left",
128
+ cursor: "pointer",
129
+ display: "flex",
130
+ alignItems: "center",
131
+ gap: 8,
132
+ minHeight: 44,
133
+ }),
134
+ error: {
135
+ color: "#ff4d4f",
136
+ marginTop: 6,
137
+ fontSize: size === Size.SMALL ? 12 : 13,
138
+ },
139
+ } as const;
140
+
141
+ return {
142
+ white: {
143
+ ...base,
144
+ input: {
145
+ ...base.input,
146
+ border: "1px solid #E5E6EB",
147
+ background: "#fff",
148
+ color: "#222",
149
+ },
150
+ },
151
+ dark: {
152
+ ...base,
153
+ left: {
154
+ background: "#181A20",
155
+ borderRadius: 8,
156
+ padding: 16,
157
+ border: "1px solid #2B2E38",
158
+ },
159
+ titleBar: {
160
+ ...base.titleBar,
161
+ background: "#00E8C6",
162
+ },
163
+ currencyItem: (active: boolean) => ({
164
+ padding: "6px 12px",
165
+ borderRadius: 6,
166
+ border: `1px solid ${active ? "#00E8C6" : "#374151"}`,
167
+ color: active ? "#00E8C6" : "#fff",
168
+ background: active ? "#0F2824" : "#23262F",
169
+ cursor: "pointer",
170
+ }),
171
+ input: {
172
+ ...base.input,
173
+ border: "1px solid #374151",
174
+ background: "#23262F",
175
+ color: "#fff",
176
+ },
177
+ methodItem: (active: boolean) => ({
178
+ padding: "10px 12px",
179
+ textAlign: "left",
180
+ borderRadius: 8,
181
+ border: `1px solid ${active ? "#00E8C6" : "#374151"}`,
182
+ background: active ? "#0F2824" : "#23262F",
183
+ color: "#fff",
184
+ cursor: "pointer",
185
+ }),
186
+ bankItem: (active: boolean) => ({
187
+ padding: "10px 12px",
188
+ borderRadius: 8,
189
+ border: `1px solid ${active ? "#00E8C6" : "#374151"}`,
190
+ background: active ? "#0F2824" : "#23262F",
191
+ textAlign: "left",
192
+ color: "#fff",
193
+ cursor: "pointer",
194
+ }),
195
+ },
196
+ } as const;
197
+ }
198
+
199
+ export function getSelectPaymentTheme() {
200
+ const theme = getInitParams<Theme>("theme");
201
+ const themeConfig = getInitParams<ThemeConfig>("themeConfig");
202
+ const white = theme === Theme.WHITE;
203
+ const themes = createSelectPaymentThemes();
204
+ const base = white ? themes.white : themes.dark;
205
+
206
+ if (themeConfig) {
207
+ const cfg = white ? themeConfig.white : themeConfig.dark;
208
+ if (cfg?.color) {
209
+ return {
210
+ ...base,
211
+ titleBar: { ...base.titleBar, background: cfg.color },
212
+ currencyItem: (active: boolean) => ({
213
+ ...base.currencyItem(active),
214
+ border: `1px solid ${
215
+ active ? cfg.color : white ? "#E5E6EB" : "#374151"
216
+ }`,
217
+ color: active ? cfg.color : white ? "#222" : "#fff",
218
+ }),
219
+ methodItem: (active: boolean) => ({
220
+ ...base.methodItem(active),
221
+ border: `1px solid ${
222
+ active ? cfg.color : white ? "#F0F1F3" : "#374151"
223
+ }`,
224
+ }),
225
+ bankItem: (active: boolean) => ({
226
+ ...base.bankItem(active),
227
+ border: `1px solid ${
228
+ active ? cfg.color : white ? "#F0F1F3" : "#374151"
229
+ }`,
230
+ }),
231
+ } as any;
232
+ }
233
+ }
234
+ return base as any;
235
+ }
@@ -0,0 +1,338 @@
1
+ import { useState, useRef, useEffect } from "preact/hooks";
2
+ import { t } from "@/local";
3
+ import { getDatePickerTheme } from "./theme";
4
+
5
+ export interface DatePickerProps {
6
+ value?: string;
7
+ placeholder?: string;
8
+ disabled?: boolean;
9
+ size?: "small" | "middle" | "large";
10
+ onChange?: (value: string) => void;
11
+ onFocus?: () => void;
12
+ onBlur?: () => void;
13
+ className?: string;
14
+ style?: Record<string, any>;
15
+ }
16
+
17
+ export function DatePicker({
18
+ value = "",
19
+ placeholder = t("请选择日期"),
20
+ disabled = false,
21
+ size = "middle",
22
+ onChange,
23
+ onFocus,
24
+ onBlur,
25
+ className = "",
26
+ style = {},
27
+ }: DatePickerProps) {
28
+ const theme = getDatePickerTheme();
29
+ const [isOpen, setIsOpen] = useState(false);
30
+ const [selectedDate, setSelectedDate] = useState(value);
31
+ const [currentMonth, setCurrentMonth] = useState(new Date());
32
+ const [showYearPicker, setShowYearPicker] = useState(false);
33
+ const containerRef = useRef<HTMLDivElement>(null);
34
+
35
+ // 格式化日期显示
36
+ const formatDate = (dateStr: string) => {
37
+ if (!dateStr) return "";
38
+ const date = new Date(dateStr);
39
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
40
+ 2,
41
+ "0"
42
+ )}-${String(date.getDate()).padStart(2, "0")}`;
43
+ };
44
+
45
+ // 获取月份的天数
46
+ const getDaysInMonth = (year: number, month: number) => {
47
+ return new Date(year, month + 1, 0).getDate();
48
+ };
49
+
50
+ // 获取月份第一天是星期几
51
+ const getFirstDayOfMonth = (year: number, month: number) => {
52
+ return new Date(year, month, 1).getDay();
53
+ };
54
+
55
+ // 生成日历数据
56
+ const generateCalendar = () => {
57
+ const year = currentMonth.getFullYear();
58
+ const month = currentMonth.getMonth();
59
+ const daysInMonth = getDaysInMonth(year, month);
60
+ const firstDay = getFirstDayOfMonth(year, month);
61
+
62
+ const days = [];
63
+
64
+ // 添加上个月的末尾几天
65
+ const prevMonth = month === 0 ? 11 : month - 1;
66
+ const prevYear = month === 0 ? year - 1 : year;
67
+ const prevMonthDays = getDaysInMonth(prevYear, prevMonth);
68
+
69
+ for (let i = firstDay - 1; i >= 0; i--) {
70
+ days.push({
71
+ date: prevMonthDays - i,
72
+ isCurrentMonth: false,
73
+ isToday: false,
74
+ isSelected: false,
75
+ fullDate: `${prevYear}-${String(prevMonth + 1).padStart(
76
+ 2,
77
+ "0"
78
+ )}-${String(prevMonthDays - i).padStart(2, "0")}`,
79
+ });
80
+ }
81
+
82
+ // 添加当前月的天数
83
+ const today = new Date();
84
+ const selectedDateObj = selectedDate ? new Date(selectedDate) : null;
85
+
86
+ for (let day = 1; day <= daysInMonth; day++) {
87
+ const fullDate = `${year}-${String(month + 1).padStart(2, "0")}-${String(
88
+ day
89
+ ).padStart(2, "0")}`;
90
+ const dateObj = new Date(fullDate);
91
+
92
+ days.push({
93
+ date: day,
94
+ isCurrentMonth: true,
95
+ isToday: dateObj.toDateString() === today.toDateString(),
96
+ isSelected:
97
+ selectedDateObj &&
98
+ dateObj.toDateString() === selectedDateObj.toDateString(),
99
+ fullDate,
100
+ });
101
+ }
102
+
103
+ // 添加下个月的开头几天
104
+ const nextMonth = month === 11 ? 0 : month + 1;
105
+ const nextYear = month === 11 ? year + 1 : year;
106
+ const remainingDays = 42 - days.length; // 6行 x 7天 = 42
107
+
108
+ for (let day = 1; day <= remainingDays; day++) {
109
+ days.push({
110
+ date: day,
111
+ isCurrentMonth: false,
112
+ isToday: false,
113
+ isSelected: false,
114
+ fullDate: `${nextYear}-${String(nextMonth + 1).padStart(
115
+ 2,
116
+ "0"
117
+ )}-${String(day).padStart(2, "0")}`,
118
+ });
119
+ }
120
+
121
+ return days;
122
+ };
123
+
124
+ // 处理日期选择
125
+ const handleDateSelect = (fullDate: string) => {
126
+ setSelectedDate(fullDate);
127
+ setIsOpen(false);
128
+ onChange?.(fullDate);
129
+ };
130
+
131
+ // 切换月份
132
+ const changeMonth = (direction: "prev" | "next") => {
133
+ const newMonth = new Date(currentMonth);
134
+ if (direction === "prev") {
135
+ newMonth.setMonth(newMonth.getMonth() - 1);
136
+ } else {
137
+ newMonth.setMonth(newMonth.getMonth() + 1);
138
+ }
139
+ setCurrentMonth(newMonth);
140
+ };
141
+
142
+ // 切换年份
143
+ const changeYear = (direction: "prev" | "next") => {
144
+ const newMonth = new Date(currentMonth);
145
+ if (direction === "prev") {
146
+ newMonth.setFullYear(newMonth.getFullYear() - 1);
147
+ } else {
148
+ newMonth.setFullYear(newMonth.getFullYear() + 1);
149
+ }
150
+ setCurrentMonth(newMonth);
151
+ };
152
+
153
+ // 选择年份
154
+ const selectYear = (year: number) => {
155
+ const newMonth = new Date(currentMonth);
156
+ newMonth.setFullYear(year);
157
+ setCurrentMonth(newMonth);
158
+ setShowYearPicker(false);
159
+ };
160
+
161
+ // 生成年份列表
162
+ const generateYearList = () => {
163
+ const currentYear = new Date().getFullYear();
164
+ const years = [];
165
+ for (let year = currentYear - 50; year <= currentYear + 50; year++) {
166
+ years.push(year);
167
+ }
168
+ return years;
169
+ };
170
+
171
+ // 点击外部关闭
172
+ useEffect(() => {
173
+ const handleClickOutside = (event: MouseEvent) => {
174
+ if (
175
+ containerRef.current &&
176
+ !containerRef.current.contains(event.target as Node)
177
+ ) {
178
+ setIsOpen(false);
179
+ }
180
+ };
181
+
182
+ if (isOpen) {
183
+ document.addEventListener("mousedown", handleClickOutside);
184
+ return () =>
185
+ document.removeEventListener("mousedown", handleClickOutside);
186
+ }
187
+ }, [isOpen]);
188
+
189
+ const calendarDays = generateCalendar();
190
+ const monthNames = [
191
+ t("一月"),
192
+ t("二月"),
193
+ t("三月"),
194
+ t("四月"),
195
+ t("五月"),
196
+ t("六月"),
197
+ t("七月"),
198
+ t("八月"),
199
+ t("九月"),
200
+ t("十月"),
201
+ t("十一月"),
202
+ t("十二月"),
203
+ ];
204
+ const weekDays = [
205
+ t("日"),
206
+ t("一"),
207
+ t("二"),
208
+ t("三"),
209
+ t("四"),
210
+ t("五"),
211
+ t("六"),
212
+ ];
213
+
214
+ return (
215
+ <div
216
+ ref={containerRef}
217
+ className={`date-picker-container ${className}`}
218
+ style={{ ...theme.container, ...style }}
219
+ >
220
+ {/* 输入框 */}
221
+ <div
222
+ style={{
223
+ ...theme.input,
224
+ ...theme[size],
225
+ ...(disabled ? theme.disabled : {}),
226
+ }}
227
+ onClick={() => !disabled && setIsOpen(!isOpen)}
228
+ onFocus={onFocus}
229
+ onBlur={onBlur}
230
+ >
231
+ <span style={theme.inputText}>
232
+ {selectedDate ? formatDate(selectedDate) : placeholder}
233
+ </span>
234
+ <span style={theme.calendarIcon}>📅</span>
235
+ </div>
236
+
237
+ {/* 日历面板 */}
238
+ {isOpen && (
239
+ <div style={theme.panel}>
240
+ {/* 头部 */}
241
+ <div style={theme.header}>
242
+ <button
243
+ type="button"
244
+ style={theme.navButton}
245
+ onClick={() => changeYear("prev")}
246
+ >
247
+ ‹‹
248
+ </button>
249
+ <button
250
+ type="button"
251
+ style={theme.navButton}
252
+ onClick={() => changeMonth("prev")}
253
+ >
254
+
255
+ </button>
256
+ <span
257
+ style={theme.monthYear}
258
+ onClick={() => setShowYearPicker(!showYearPicker)}
259
+ >
260
+ {currentMonth.getFullYear()}年
261
+ {monthNames[currentMonth.getMonth()]}
262
+ </span>
263
+ <button
264
+ type="button"
265
+ style={theme.navButton}
266
+ onClick={() => changeMonth("next")}
267
+ >
268
+
269
+ </button>
270
+ <button
271
+ type="button"
272
+ style={theme.navButton}
273
+ onClick={() => changeYear("next")}
274
+ >
275
+ ››
276
+ </button>
277
+ </div>
278
+
279
+ {/* 年份选择器 */}
280
+ {showYearPicker && (
281
+ <div style={theme.yearPicker}>
282
+ <div style={theme.yearGrid}>
283
+ {generateYearList().map((year) => (
284
+ <button
285
+ key={year}
286
+ type="button"
287
+ style={{
288
+ ...theme.yearButton,
289
+ ...(year === currentMonth.getFullYear()
290
+ ? theme.yearSelected
291
+ : {}),
292
+ }}
293
+ onClick={() => selectYear(year)}
294
+ >
295
+ {year}
296
+ </button>
297
+ ))}
298
+ </div>
299
+ </div>
300
+ )}
301
+
302
+ {/* 星期标题 */}
303
+ <div style={theme.weekHeader}>
304
+ {weekDays.map((day) => (
305
+ <div key={day} style={theme.weekDay}>
306
+ {day}
307
+ </div>
308
+ ))}
309
+ </div>
310
+
311
+ {/* 日期网格 */}
312
+ <div style={theme.calendarGrid}>
313
+ {calendarDays.map((day, index) => (
314
+ <button
315
+ key={index}
316
+ type="button"
317
+ style={{
318
+ ...theme.dayButton,
319
+ ...(day.isCurrentMonth
320
+ ? theme.dayCurrentMonth
321
+ : theme.dayOtherMonth),
322
+ ...(day.isToday ? theme.dayToday : {}),
323
+ ...(day.isSelected ? theme.daySelected : {}),
324
+ }}
325
+ onClick={() => handleDateSelect(day.fullDate)}
326
+ disabled={disabled}
327
+ >
328
+ {day.date}
329
+ </button>
330
+ ))}
331
+ </div>
332
+ </div>
333
+ )}
334
+ </div>
335
+ );
336
+ }
337
+
338
+ export default DatePicker;