seven-design-ui 0.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.
Files changed (157) hide show
  1. package/.eslintrc.json +35 -0
  2. package/.prettierrc.json +10 -0
  3. package/CONTRIBUTING.md +350 -0
  4. package/PROJECT_STRUCTURE.md +343 -0
  5. package/README.md +83 -0
  6. package/app/globals.css +125 -0
  7. package/app/layout.tsx +45 -0
  8. package/app/page.tsx +202 -0
  9. package/components/theme-provider.tsx +11 -0
  10. package/components/ui/accordion.tsx +66 -0
  11. package/components/ui/alert-dialog.tsx +157 -0
  12. package/components/ui/alert.tsx +66 -0
  13. package/components/ui/aspect-ratio.tsx +11 -0
  14. package/components/ui/avatar.tsx +53 -0
  15. package/components/ui/badge.tsx +46 -0
  16. package/components/ui/breadcrumb.tsx +109 -0
  17. package/components/ui/button-group.tsx +83 -0
  18. package/components/ui/button.tsx +60 -0
  19. package/components/ui/calendar.tsx +213 -0
  20. package/components/ui/card.tsx +92 -0
  21. package/components/ui/carousel.tsx +241 -0
  22. package/components/ui/chart.tsx +353 -0
  23. package/components/ui/checkbox.tsx +32 -0
  24. package/components/ui/collapsible.tsx +33 -0
  25. package/components/ui/command.tsx +184 -0
  26. package/components/ui/context-menu.tsx +252 -0
  27. package/components/ui/dialog.tsx +143 -0
  28. package/components/ui/drawer.tsx +135 -0
  29. package/components/ui/dropdown-menu.tsx +257 -0
  30. package/components/ui/empty.tsx +104 -0
  31. package/components/ui/field.tsx +244 -0
  32. package/components/ui/form.tsx +167 -0
  33. package/components/ui/hover-card.tsx +44 -0
  34. package/components/ui/input-group.tsx +169 -0
  35. package/components/ui/input-otp.tsx +77 -0
  36. package/components/ui/input.tsx +21 -0
  37. package/components/ui/item.tsx +193 -0
  38. package/components/ui/kbd.tsx +28 -0
  39. package/components/ui/label.tsx +24 -0
  40. package/components/ui/menubar.tsx +276 -0
  41. package/components/ui/navigation-menu.tsx +166 -0
  42. package/components/ui/pagination.tsx +127 -0
  43. package/components/ui/popover.tsx +48 -0
  44. package/components/ui/progress.tsx +31 -0
  45. package/components/ui/radio-group.tsx +45 -0
  46. package/components/ui/resizable.tsx +56 -0
  47. package/components/ui/scroll-area.tsx +58 -0
  48. package/components/ui/select.tsx +185 -0
  49. package/components/ui/separator.tsx +28 -0
  50. package/components/ui/sheet.tsx +139 -0
  51. package/components/ui/sidebar.tsx +726 -0
  52. package/components/ui/skeleton.tsx +13 -0
  53. package/components/ui/slider.tsx +63 -0
  54. package/components/ui/sonner.tsx +25 -0
  55. package/components/ui/spinner.tsx +16 -0
  56. package/components/ui/switch.tsx +31 -0
  57. package/components/ui/table.tsx +116 -0
  58. package/components/ui/tabs.tsx +66 -0
  59. package/components/ui/textarea.tsx +18 -0
  60. package/components/ui/toast.tsx +129 -0
  61. package/components/ui/toaster.tsx +35 -0
  62. package/components/ui/toggle-group.tsx +73 -0
  63. package/components/ui/toggle.tsx +47 -0
  64. package/components/ui/tooltip.tsx +61 -0
  65. package/components/ui/use-mobile.tsx +19 -0
  66. package/components/ui/use-toast.ts +191 -0
  67. package/components.json +21 -0
  68. package/docs/components/button.mdx +155 -0
  69. package/docs/components/input.mdx +157 -0
  70. package/docs/components/pagination.mdx +186 -0
  71. package/docs/components/switch.mdx +134 -0
  72. package/docs/doc_build/.nojekyll +0 -0
  73. package/docs/doc_build/404.html +15 -0
  74. package/docs/doc_build/components/button.html +39 -0
  75. package/docs/doc_build/components/input.html +39 -0
  76. package/docs/doc_build/components/pagination.html +39 -0
  77. package/docs/doc_build/components/switch.html +38 -0
  78. package/docs/doc_build/guide/introduction.html +58 -0
  79. package/docs/doc_build/guide/quick-start.html +98 -0
  80. package/docs/doc_build/guide/theme.html +139 -0
  81. package/docs/doc_build/index.html +15 -0
  82. package/docs/doc_build/logo.svg +1 -0
  83. package/docs/doc_build/static/css/styles.5a3e7113.css +1 -0
  84. package/docs/doc_build/static/js/414.04bb58dd.js +6 -0
  85. package/docs/doc_build/static/js/414.04bb58dd.js.LICENSE.txt +21 -0
  86. package/docs/doc_build/static/js/async/166.f43be01a.js +2 -0
  87. package/docs/doc_build/static/js/async/166.f43be01a.js.LICENSE.txt +19 -0
  88. package/docs/doc_build/static/js/async/218.cd780e24.js +1 -0
  89. package/docs/doc_build/static/js/async/232.11414fd7.js +1 -0
  90. package/docs/doc_build/static/js/async/416.b217df75.js +1 -0
  91. package/docs/doc_build/static/js/async/509.97958e51.js +1 -0
  92. package/docs/doc_build/static/js/async/512.9047d21e.js +1 -0
  93. package/docs/doc_build/static/js/async/531.131f5963.js +1 -0
  94. package/docs/doc_build/static/js/async/562.b402b94f.js +2 -0
  95. package/docs/doc_build/static/js/async/562.b402b94f.js.LICENSE.txt +11 -0
  96. package/docs/doc_build/static/js/async/637.cb5d76c9.js +1 -0
  97. package/docs/doc_build/static/js/async/712.558b85be.js +1 -0
  98. package/docs/doc_build/static/js/index.0991c749.js +1 -0
  99. package/docs/doc_build/static/js/lib-react.ce4199ca.js +2 -0
  100. package/docs/doc_build/static/js/lib-react.ce4199ca.js.LICENSE.txt +49 -0
  101. package/docs/doc_build/static/js/lib-router.4000fe55.js +2 -0
  102. package/docs/doc_build/static/js/lib-router.4000fe55.js.LICENSE.txt +32 -0
  103. package/docs/doc_build/static/js/styles.f2af9a40.js +1 -0
  104. package/docs/doc_build/static/search_index.72c9c372.json +1 -0
  105. package/docs/docs/public/logo.svg +1 -0
  106. package/docs/guide/introduction.md +50 -0
  107. package/docs/guide/quick-start.md +132 -0
  108. package/docs/guide/theme.md +230 -0
  109. package/docs/index.md +85 -0
  110. package/docs/package.json +22 -0
  111. package/docs/public/logo.svg +1 -0
  112. package/docs/rspress.config.ts +116 -0
  113. package/hooks/use-mobile.ts +19 -0
  114. package/hooks/use-toast.ts +191 -0
  115. package/next.config.mjs +11 -0
  116. package/package.json +75 -0
  117. package/packages/components/package.json +34 -0
  118. package/packages/components/src/button/Button.tsx +83 -0
  119. package/packages/components/src/button/button.css +256 -0
  120. package/packages/components/src/button/index.ts +2 -0
  121. package/packages/components/src/index.ts +8 -0
  122. package/packages/components/src/input/Input.tsx +230 -0
  123. package/packages/components/src/input/index.ts +2 -0
  124. package/packages/components/src/input/input.css +99 -0
  125. package/packages/components/src/pagination/Pagination.tsx +271 -0
  126. package/packages/components/src/pagination/index.ts +1 -0
  127. package/packages/components/src/pagination/pagination.css +225 -0
  128. package/packages/components/src/switch/Switch.tsx +145 -0
  129. package/packages/components/src/switch/index.ts +2 -0
  130. package/packages/components/src/switch/switch.css +119 -0
  131. package/packages/components/tsconfig.json +12 -0
  132. package/packages/components/vite.config.ts +31 -0
  133. package/packages/core/package.json +23 -0
  134. package/packages/core/src/hooks/useControllableState.ts +31 -0
  135. package/packages/core/src/hooks/useEventListener.ts +37 -0
  136. package/packages/core/src/index.ts +7 -0
  137. package/packages/core/src/utils/classnames.ts +48 -0
  138. package/packages/core/tsconfig.json +12 -0
  139. package/packages/core/vite.config.ts +24 -0
  140. package/packages/theme/package.json +20 -0
  141. package/packages/theme/src/index.css +138 -0
  142. package/packages/theme/src/index.ts +1 -0
  143. package/packages/theme/vite.config.ts +21 -0
  144. package/play/index.html +13 -0
  145. package/play/package.json +25 -0
  146. package/play/src/App.tsx +237 -0
  147. package/play/src/index.css +93 -0
  148. package/play/src/main.tsx +14 -0
  149. package/play/tsconfig.json +8 -0
  150. package/play/vite.config.ts +10 -0
  151. package/pnpm-workspace.yaml +4 -0
  152. package/postcss.config.mjs +8 -0
  153. package/public/logo.svg +1 -0
  154. package/scripts/build.sh +19 -0
  155. package/scripts/deploy-docs.js +38 -0
  156. package/styles/globals.css +125 -0
  157. package/tsconfig.json +30 -0
@@ -0,0 +1,230 @@
1
+ 'use client';
2
+
3
+ import React from "react"
4
+
5
+ import { forwardRef, InputHTMLAttributes, useState, useRef, useImperativeHandle } from 'react'
6
+ import { classnames } from '@seven-design-ui/core'
7
+ import './input.css'
8
+
9
+ export type InputSize = 'large' | 'default' | 'small'
10
+
11
+ export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'onInput'> {
12
+ /** 输入框类型 */
13
+ type?: string
14
+ /** 输入框尺寸 */
15
+ size?: InputSize
16
+ /** 是否禁用 */
17
+ disabled?: boolean
18
+ /** 是否只读 */
19
+ readOnly?: boolean
20
+ /** 是否可清空 */
21
+ clearable?: boolean
22
+ /** 是否显示密码切换按钮 */
23
+ showPassword?: boolean
24
+ /** 前置图标 */
25
+ prefixIcon?: React.ReactNode
26
+ /** 后置图标 */
27
+ suffixIcon?: React.ReactNode
28
+ /** 自定义类名 */
29
+ className?: string
30
+ /** 输入框值 */
31
+ value?: string
32
+ /** 默认值 */
33
+ defaultValue?: string
34
+ /** 输入时的回调 */
35
+ onInput?: (value: string) => void
36
+ /** 值改变时的回调 */
37
+ onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
38
+ /** 清空按钮点击回调 */
39
+ onClear?: () => void
40
+ }
41
+
42
+ export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
43
+ const {
44
+ type = 'text',
45
+ size = 'default',
46
+ disabled = false,
47
+ readOnly = false,
48
+ clearable = false,
49
+ showPassword = false,
50
+ prefixIcon,
51
+ suffixIcon,
52
+ className,
53
+ value,
54
+ defaultValue,
55
+ onInput,
56
+ onChange,
57
+ onClear,
58
+ onFocus,
59
+ ...rest
60
+ } = props
61
+
62
+ const [showPwd, setShowPwd] = useState(false)
63
+ const [inputValue, setInputValue] = useState(defaultValue || '')
64
+ const internalInputRef = useRef<HTMLInputElement>(null)
65
+
66
+ // 合并外部 ref 和内部 ref
67
+ useImperativeHandle(ref, () => internalInputRef.current as HTMLInputElement, [])
68
+
69
+ const inputType = showPassword ? (showPwd ? 'text' : 'password') : type
70
+ const currentValue = value !== undefined ? value : inputValue
71
+
72
+ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
73
+ const newValue = e.target.value
74
+ if (value === undefined) {
75
+ setInputValue(newValue)
76
+ }
77
+ onChange?.(e)
78
+ onInput?.(newValue)
79
+ }
80
+
81
+ const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
82
+ // 当输入框获得焦点时,清空输入框内的文本
83
+ const inputElement = e.target
84
+
85
+ // 对于受控组件:通过回调函数通知父组件清空值
86
+ if (value !== undefined) {
87
+ onInput?.('')
88
+ // 如果提供了 onChange 回调,也触发它
89
+ if (onChange) {
90
+ const syntheticEvent = {
91
+ target: {
92
+ ...inputElement,
93
+ value: '',
94
+ },
95
+ currentTarget: inputElement,
96
+ } as React.ChangeEvent<HTMLInputElement>
97
+ onChange(syntheticEvent)
98
+ }
99
+ } else {
100
+ // 对于非受控组件:直接更新内部状态
101
+ setInputValue('')
102
+ // 触发 onChange 和 onInput 回调
103
+ if (onChange) {
104
+ const syntheticEvent = {
105
+ target: {
106
+ ...inputElement,
107
+ value: '',
108
+ },
109
+ currentTarget: inputElement,
110
+ } as React.ChangeEvent<HTMLInputElement>
111
+ onChange(syntheticEvent)
112
+ }
113
+ onInput?.('')
114
+ }
115
+
116
+ // 调用外部传入的 onFocus 回调(如果提供了)
117
+ onFocus?.(e)
118
+ }
119
+
120
+ const handleClear = (e: React.MouseEvent) => {
121
+ // 阻止事件冒泡和默认行为
122
+ e.preventDefault()
123
+ e.stopPropagation()
124
+
125
+ // 调用 onInput 回调,通知父组件清空值
126
+ // 对于受控组件,这是必需的,因为父组件需要通过 setState 更新 value
127
+ onInput?.('')
128
+
129
+ // 对于非受控组件,更新内部状态
130
+ if (value === undefined) {
131
+ setInputValue('')
132
+ }
133
+
134
+ // 创建一个合成事件对象,用于触发 onChange(如果提供了 onChange 回调)
135
+ const inputElement = internalInputRef.current
136
+ if (inputElement && onChange) {
137
+ const syntheticEvent = {
138
+ target: {
139
+ ...inputElement,
140
+ value: '',
141
+ },
142
+ currentTarget: inputElement,
143
+ } as React.ChangeEvent<HTMLInputElement>
144
+ onChange(syntheticEvent)
145
+ }
146
+
147
+ // 调用 onClear 回调
148
+ onClear?.()
149
+ }
150
+
151
+ const showClearBtn = clearable && currentValue && !disabled && !readOnly
152
+
153
+ const wrapperClasses = classnames(
154
+ 'sd-input',
155
+ `sd-input--${size}`,
156
+ {
157
+ 'is-disabled': disabled,
158
+ 'sd-input--prefix': !!prefixIcon,
159
+ 'sd-input--suffix': !!(suffixIcon || showClearBtn || showPassword),
160
+ },
161
+ className
162
+ )
163
+
164
+ return (
165
+ <div className={wrapperClasses}>
166
+ {prefixIcon && <span className="sd-input__prefix">{prefixIcon}</span>}
167
+ <input
168
+ ref={internalInputRef}
169
+ type={inputType}
170
+ className="sd-input__inner"
171
+ disabled={disabled}
172
+ readOnly={readOnly}
173
+ value={currentValue}
174
+ onChange={handleChange}
175
+ onFocus={handleFocus}
176
+ {...rest}
177
+ />
178
+ <span className="sd-input__suffix">
179
+ {showClearBtn && (
180
+ <span
181
+ className="sd-input__clear"
182
+ onClick={handleClear}
183
+ onMouseDown={(e) => {
184
+ // 防止点击清空按钮时 input 失去焦点
185
+ e.preventDefault()
186
+ }}
187
+ >
188
+ <svg
189
+ viewBox="0 0 1024 1024"
190
+ width="14"
191
+ height="14"
192
+ style={{ pointerEvents: 'none' }}
193
+ >
194
+ <path
195
+ fill="currentColor"
196
+ d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336L512 457.664z"
197
+ />
198
+ </svg>
199
+ </span>
200
+ )}
201
+ {showPassword && (
202
+ <span className="sd-input__password" onClick={() => setShowPwd(!showPwd)}>
203
+ {showPwd ? (
204
+ <svg viewBox="0 0 1024 1024" width="14" height="14">
205
+ <path
206
+ fill="currentColor"
207
+ d="M512 160c320 0 512 352 512 352S832 864 512 864 0 512 0 512s192-352 512-352zm0 64c-225.28 0-384.128 208.064-436.8 288 52.608 79.872 211.456 288 436.8 288 225.28 0 384.128-208.064 436.8-288-52.608-79.872-211.456-288-436.8-288zm0 64a224 224 0 1 1 0 448 224 224 0 0 1 0-448zm0 64a160.192 160.192 0 0 0-160 160c0 88.192 71.744 160 160 160s160-71.808 160-160-71.744-160-160-160z"
208
+ />
209
+ </svg>
210
+ ) : (
211
+ <svg viewBox="0 0 1024 1024" width="14" height="14">
212
+ <path
213
+ fill="currentColor"
214
+ d="M876.8 156.8c0-9.6-3.2-16-9.6-22.4-6.4-6.4-12.8-9.6-22.4-9.6-9.6 0-16 3.2-22.4 9.6L736 220.8c-64-32-137.6-51.2-224-60.8-160 16-288 73.6-377.6 176C44.8 438.4 0 496 0 512s48 73.6 134.4 176c22.4 25.6 44.8 48 73.6 67.2l-86.4 89.6c-6.4 6.4-9.6 12.8-9.6 22.4 0 9.6 3.2 16 9.6 22.4 6.4 6.4 12.8 9.6 22.4 9.6 9.6 0 16-3.2 22.4-9.6l704-710.4c3.2-6.4 6.4-12.8 6.4-22.4Zm-646.4 528c-76.8-70.4-128-128-153.6-172.8 28.8-48 80-105.6 153.6-172.8C304 272 400 230.4 512 224c64 3.2 124.8 19.2 176 44.8l-54.4 54.4C598.4 300.8 560 288 512 288c-64 0-115.2 22.4-160 64s-64 96-64 160c0 48 12.8 89.6 35.2 124.8L256 707.2c-9.6-6.4-19.2-16-25.6-22.4Zm140.8-96c-12.8-22.4-19.2-48-19.2-76.8 0-44.8 16-83.2 48-112 32-28.8 67.2-48 112-48 28.8 0 54.4 6.4 73.6 19.2L371.2 588.8ZM889.599 336c-12.8-16-28.8-28.8-41.6-41.6l-48 48c73.6 67.2 124.8 124.8 150.4 169.6-28.8 48-80 105.6-153.6 172.8-73.6 67.2-172.8 108.8-284.8 115.2-51.2-3.2-99.2-12.8-140.8-28.8l-48 48c57.6 22.4 118.4 38.4 188.8 44.8 160-16 288-73.6 377.6-176C979.199 585.6 1024 528 1024 512s-48.001-73.6-134.401-176Z"
215
+ />
216
+ <path
217
+ fill="currentColor"
218
+ d="M511.998 672c-12.8 0-25.6-3.2-38.4-6.4l-51.2 51.2c28.8 12.8 57.6 19.2 89.6 19.2 64 0 115.2-22.4 160-64 41.6-41.6 64-96 64-160 0-32-6.4-64-19.2-89.6l-51.2 51.2c3.2 12.8 6.4 25.6 6.4 38.4 0 44.8-16 83.2-48 112-32 28.8-67.2 48-112 48Z"
219
+ />
220
+ </svg>
221
+ )}
222
+ </span>
223
+ )}
224
+ {suffixIcon && !showClearBtn && <span className="sd-input__suffix-icon">{suffixIcon}</span>}
225
+ </span>
226
+ </div>
227
+ )
228
+ })
229
+
230
+ Input.displayName = 'Input'
@@ -0,0 +1,2 @@
1
+ export { Input } from './Input'
2
+ export type { InputProps, InputSize } from './Input'
@@ -0,0 +1,99 @@
1
+ /* Input 组件样式 */
2
+ .sd-input {
3
+ position: relative;
4
+ display: inline-flex;
5
+ width: 100%;
6
+ font-size: var(--sd-font-size-base);
7
+ line-height: var(--sd-component-size-default);
8
+ }
9
+
10
+ .sd-input__inner {
11
+ flex: 1;
12
+ width: 100%;
13
+ height: var(--sd-component-size-default);
14
+ padding: 0 15px;
15
+ font-size: inherit;
16
+ line-height: inherit;
17
+ color: var(--sd-text-color-regular);
18
+ background-color: var(--sd-fill-color-blank);
19
+ border: 1px solid var(--sd-border-color);
20
+ border-radius: var(--sd-border-radius-base);
21
+ outline: none;
22
+ transition: all var(--sd-transition-duration-fast);
23
+ }
24
+
25
+ .sd-input__inner:hover {
26
+ border-color: var(--sd-border-color-darker);
27
+ }
28
+
29
+ .sd-input__inner:focus {
30
+ border-color: var(--sd-color-primary);
31
+ }
32
+
33
+ .sd-input__inner::placeholder {
34
+ color: var(--sd-text-color-placeholder);
35
+ }
36
+
37
+ /* 前置图标 */
38
+ .sd-input--prefix .sd-input__inner {
39
+ padding-left: 30px;
40
+ }
41
+
42
+ .sd-input__prefix {
43
+ position: absolute;
44
+ left: 10px;
45
+ top: 50%;
46
+ transform: translateY(-50%);
47
+ display: inline-flex;
48
+ align-items: center;
49
+ color: var(--sd-text-color-placeholder);
50
+ pointer-events: none;
51
+ }
52
+
53
+ /* 后置图标 */
54
+ .sd-input--suffix .sd-input__inner {
55
+ padding-right: 30px;
56
+ }
57
+
58
+ .sd-input__suffix {
59
+ position: absolute;
60
+ right: 10px;
61
+ top: 50%;
62
+ transform: translateY(-50%);
63
+ display: inline-flex;
64
+ align-items: center;
65
+ gap: 4px;
66
+ color: var(--sd-text-color-placeholder);
67
+ }
68
+
69
+ .sd-input__clear,
70
+ .sd-input__password {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ cursor: pointer;
74
+ transition: color var(--sd-transition-duration-fast);
75
+ }
76
+
77
+ .sd-input__clear:hover,
78
+ .sd-input__password:hover {
79
+ color: var(--sd-text-color-regular);
80
+ }
81
+
82
+ /* 尺寸 */
83
+ .sd-input--large .sd-input__inner {
84
+ height: var(--sd-component-size-large);
85
+ font-size: var(--sd-font-size-medium);
86
+ }
87
+
88
+ .sd-input--small .sd-input__inner {
89
+ height: var(--sd-component-size-small);
90
+ font-size: var(--sd-font-size-small);
91
+ }
92
+
93
+ /* 禁用状态 */
94
+ .sd-input.is-disabled .sd-input__inner {
95
+ color: var(--sd-disabled-text-color);
96
+ background-color: var(--sd-disabled-bg-color);
97
+ border-color: var(--sd-disabled-border-color);
98
+ cursor: not-allowed;
99
+ }
@@ -0,0 +1,271 @@
1
+ import { forwardRef, useState, useCallback, useMemo } from 'react'
2
+ import { classnames } from '@seven-design-ui/core'
3
+ import './pagination.css'
4
+
5
+ export type PaginationSize = 's' | 'm'
6
+
7
+ export interface PaginationProps {
8
+ /** 总数量 */
9
+ total: number
10
+ /** 默认页码 */
11
+ defaultCurrent?: number
12
+ /** 默认每页容量 */
13
+ defaultPageSize?: number
14
+ /** 当前页码 */
15
+ current?: number
16
+ /** 每页容量 */
17
+ pageSize?: number
18
+ /** 页码改变回调 */
19
+ onChange?: (page: number, pageSize: number) => void
20
+ /** 每页容量改变回调 */
21
+ onPageSizeChange?: (pageSize: number) => void
22
+ /** 最大页码按钮数,默认7 */
23
+ pagerCount?: number
24
+ /** 分页器大小 */
25
+ size?: PaginationSize
26
+ /** 每页容量选项 */
27
+ pageSizeOptions?: number[]
28
+ /** 是否显示每页容量选择器 */
29
+ showSizeChanger?: boolean
30
+ /** 是否显示跳转输入框 */
31
+ showQuickJumper?: boolean
32
+ /** 自定义类名 */
33
+ className?: string
34
+ }
35
+
36
+ export const Pagination = forwardRef<HTMLDivElement, PaginationProps>((props, ref) => {
37
+ const {
38
+ total,
39
+ defaultCurrent = 1,
40
+ defaultPageSize = 10,
41
+ current: controlledCurrent,
42
+ pageSize: controlledPageSize,
43
+ onChange,
44
+ onPageSizeChange,
45
+ pagerCount = 7,
46
+ size = 'm',
47
+ pageSizeOptions = [10, 20, 50, 100],
48
+ showSizeChanger = true,
49
+ showQuickJumper = false,
50
+ className,
51
+ ...rest
52
+ } = props
53
+
54
+ // 使用受控状态管理当前页码
55
+ const [internalCurrent, setInternalCurrent] = useState(defaultCurrent)
56
+ const current = controlledCurrent ?? internalCurrent
57
+
58
+ // 使用受控状态管理每页容量
59
+ const [internalPageSize, setInternalPageSize] = useState(defaultPageSize)
60
+ const pageSize = controlledPageSize ?? internalPageSize
61
+
62
+ // 计算总页数
63
+ const totalPages = useMemo(() => Math.ceil(total / pageSize), [total, pageSize])
64
+
65
+ // 处理页码改变
66
+ const handlePageChange = useCallback((page: number) => {
67
+ if (page < 1 || page > totalPages || page === current) return
68
+
69
+ const newPage = Math.max(1, Math.min(page, totalPages))
70
+
71
+ if (controlledCurrent === undefined) {
72
+ setInternalCurrent(newPage)
73
+ }
74
+
75
+ onChange?.(newPage, pageSize)
76
+ }, [current, totalPages, pageSize, onChange, controlledCurrent])
77
+
78
+ // 处理每页容量改变
79
+ const handlePageSizeChange = useCallback((newPageSize: number) => {
80
+ if (controlledPageSize === undefined) {
81
+ setInternalPageSize(newPageSize)
82
+ }
83
+
84
+ onPageSizeChange?.(newPageSize)
85
+
86
+ // 容量改变后,跳转到第一页
87
+ const newPage = 1
88
+ if (controlledCurrent === undefined) {
89
+ setInternalCurrent(newPage)
90
+ }
91
+
92
+ onChange?.(newPage, newPageSize)
93
+ }, [onPageSizeChange, onChange, controlledCurrent, controlledPageSize])
94
+
95
+ // 计算页码按钮列表
96
+ const pageItems = useMemo(() => {
97
+ const items: (number | 'prev-more' | 'next-more')[] = []
98
+
99
+ if (totalPages <= pagerCount) {
100
+ // 总页数不超过最大显示数,直接显示所有页码
101
+ for (let i = 1; i <= totalPages; i++) {
102
+ items.push(i)
103
+ }
104
+ } else {
105
+ // 需要折叠的情况
106
+ const leftCount = Math.floor((pagerCount - 2) / 2) // 左侧显示的页码数
107
+ const rightCount = pagerCount - 2 - leftCount // 右侧显示的页码数
108
+
109
+ // 始终显示第一页
110
+ items.push(1)
111
+
112
+ if (current <= leftCount + 2) {
113
+ // 当前页靠近左侧
114
+ for (let i = 2; i <= Math.min(pagerCount - 2, totalPages - 1); i++) {
115
+ items.push(i)
116
+ }
117
+ if (totalPages > pagerCount - 1) {
118
+ items.push('next-more')
119
+ }
120
+ } else if (current >= totalPages - rightCount - 1) {
121
+ // 当前页靠近右侧
122
+ if (totalPages > pagerCount - 1) {
123
+ items.push('prev-more')
124
+ }
125
+ for (let i = Math.max(2, totalPages - (pagerCount - 3)); i <= totalPages - 1; i++) {
126
+ items.push(i)
127
+ }
128
+ } else {
129
+ // 当前页在中间
130
+ items.push('prev-more')
131
+ for (let i = current - leftCount; i <= current + rightCount; i++) {
132
+ items.push(i)
133
+ }
134
+ items.push('next-more')
135
+ }
136
+
137
+ // 始终显示最后一页
138
+ if (totalPages > 1) {
139
+ items.push(totalPages)
140
+ }
141
+ }
142
+
143
+ return items
144
+ }, [totalPages, pagerCount, current])
145
+
146
+ // 跳转输入框状态
147
+ const [jumpValue, setJumpValue] = useState('')
148
+
149
+ const handleJump = useCallback(() => {
150
+ const page = parseInt(jumpValue, 10)
151
+ if (!isNaN(page)) {
152
+ handlePageChange(page)
153
+ setJumpValue('')
154
+ }
155
+ }, [jumpValue, handlePageChange])
156
+
157
+ // 如果没有数据,不显示分页器
158
+ if (total === 0) {
159
+ return null
160
+ }
161
+
162
+ const classes = classnames(
163
+ 'sd-pagination',
164
+ `sd-pagination--${size}`,
165
+ className
166
+ )
167
+
168
+ return (
169
+ <div ref={ref} className={classes} {...rest}>
170
+ {/* 上一页按钮 */}
171
+ <button
172
+ type="button"
173
+ className={classnames('sd-pagination__item', 'sd-pagination__prev', {
174
+ 'is-disabled': current === 1
175
+ })}
176
+ disabled={current === 1}
177
+ onClick={() => handlePageChange(current - 1)}
178
+ aria-label="上一页"
179
+ >
180
+
181
+ </button>
182
+
183
+ {/* 页码按钮 */}
184
+ {pageItems.map((item, index) => {
185
+ if (item === 'prev-more') {
186
+ return (
187
+ <span key={`more-${index}`} className="sd-pagination__more sd-pagination__more--prev">
188
+ ...
189
+ </span>
190
+ )
191
+ }
192
+
193
+ if (item === 'next-more') {
194
+ return (
195
+ <span key={`more-${index}`} className="sd-pagination__more sd-pagination__more--next">
196
+ ...
197
+ </span>
198
+ )
199
+ }
200
+
201
+ return (
202
+ <button
203
+ key={item}
204
+ type="button"
205
+ className={classnames('sd-pagination__item', 'sd-pagination__page', {
206
+ 'is-active': item === current
207
+ })}
208
+ onClick={() => handlePageChange(item)}
209
+ >
210
+ {item}
211
+ </button>
212
+ )
213
+ })}
214
+
215
+ {/* 下一页按钮 */}
216
+ <button
217
+ type="button"
218
+ className={classnames('sd-pagination__item', 'sd-pagination__next', {
219
+ 'is-disabled': current === totalPages
220
+ })}
221
+ disabled={current === totalPages}
222
+ onClick={() => handlePageChange(current + 1)}
223
+ aria-label="下一页"
224
+ >
225
+
226
+ </button>
227
+
228
+ {/* 每页容量选择器 */}
229
+ {showSizeChanger && (
230
+ <div className="sd-pagination__size-changer">
231
+ <select
232
+ value={pageSize}
233
+ onChange={(e) => handlePageSizeChange(Number(e.target.value))}
234
+ className="sd-pagination__size-select"
235
+ >
236
+ {pageSizeOptions.map(option => (
237
+ <option key={option} value={option}>
238
+ {option} 条/页
239
+ </option>
240
+ ))}
241
+ </select>
242
+ </div>
243
+ )}
244
+
245
+ {/* 跳转输入框 */}
246
+ {showQuickJumper && (
247
+ <div className="sd-pagination__jumper">
248
+ <span className="sd-pagination__jumper-text">跳至</span>
249
+ <input
250
+ type="number"
251
+ min="1"
252
+ max={totalPages}
253
+ value={jumpValue}
254
+ onChange={(e) => setJumpValue(e.target.value)}
255
+ onKeyPress={(e) => e.key === 'Enter' && handleJump()}
256
+ className="sd-pagination__jumper-input"
257
+ placeholder={current.toString()}
258
+ />
259
+ <span className="sd-pagination__jumper-text">页</span>
260
+ </div>
261
+ )}
262
+
263
+ {/* 总数信息 */}
264
+ <div className="sd-pagination__total">
265
+ 共 {total} 条
266
+ </div>
267
+ </div>
268
+ )
269
+ })
270
+
271
+ Pagination.displayName = 'Pagination'
@@ -0,0 +1 @@
1
+ export * from './Pagination'