diff options
Diffstat (limited to 'src/FormExample.tsx')
| -rw-r--r-- | src/FormExample.tsx | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/src/FormExample.tsx b/src/FormExample.tsx new file mode 100644 index 0000000..7f38754 --- /dev/null +++ b/src/FormExample.tsx @@ -0,0 +1,306 @@ +import { useState } from 'react' +import { + Alert, + Button, + Checkbox, + Cascader, + Code, + ColorInput, + FileInput, + Group, + MultiSelect, + NumberInput, + Paper, + PasswordInput, + Radio, + Select, + SimpleGrid, + Stack, + Switch, + TagsInput, + Text, + Textarea, + TextInput, + Title, + useMatches, +} from '@mantine/core' +import { isEmail, useForm } from '@mantine/form' +import { UploadSimpleIcon } from '@phosphor-icons/react' +import classes from './FormExample.module.css' + +// 仅用于演示联动,不是完整行政区划数据。 +const regions = [ + { + name: '浙江省', + cities: [ + { name: '杭州市', districts: ['西湖区', '拱墅区'] }, + { name: '宁波市', districts: ['海曙区', '鄞州区'] }, + ], + }, + { + name: '江苏省', + cities: [ + { name: '南京市', districts: ['鼓楼区', '玄武区'] }, + { name: '苏州市', districts: ['姑苏区', '吴中区'] }, + ], + }, +] + +const regionOptions = regions.map((province) => ({ + value: province.name, + children: province.cities.map((city) => ({ + value: city.name, + children: city.districts.map((district) => ({ value: district })), + })), +})) + +type FormValues = { + name: string + email: string + role: string | null + region: string[] | null + contact: string + interests: string[] + technologies: string[] + tags: string[] + color: string + files: File[] + quantity: number | string + password: string + description: string + notifications: boolean + acceptedTerms: boolean +} + +const exampleErrors = { email: '邮箱格式不正确,例如 name@example.com' } + +export default function FormExample() { + const [submitted, setSubmitted] = useState<string | null>(null) + const withColumns = useMatches({ base: false, sm: true }) + const form = useForm<FormValues>({ + mode: 'controlled', + validateInputOnBlur: true, + initialErrors: exampleErrors, + initialValues: { + name: '', + email: 'invalid-email', + role: null, + region: null, + contact: 'email', + interests: [], + technologies: [], + tags: [], + color: '#228be6', + files: [], + quantity: 1, + password: '', + description: '', + notifications: false, + acceptedTerms: false, + }, + validate: { + name: (value) => value.trim() ? null : '请输入名称', + email: (value) => !value.trim() ? '请输入邮箱' : isEmail(exampleErrors.email)(value.trim()), + role: (value) => value ? null : '请选择角色', + quantity: (value) => typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 100 + ? null : '请输入 0–100 之间的整数', + password: (value) => value && value.length < 8 ? '密码至少需要 8 位' : null, + color: (value) => /^#[\da-f]{6}$/i.test(value) ? null : '请输入有效的六位 HEX 颜色', + acceptedTerms: (value) => value ? null : '请先阅读并同意示例协议', + }, + }) + + function submit(values: FormValues) { + setSubmitted(JSON.stringify({ + ...values, + password: values.password ? '已填写(不展示密码)' : '未填写', + files: values.files.map((file) => ({ + name: file.name, + size: file.size, + type: file.type || '未知类型', + })), + }, null, 2)) + } + + return ( + <Stack gap="lg"> + <div> + <Title order={1}>表单示例</Title> + <Text c="dimmed">体验常用表单控件。提交后在本页预览结果,数据和文件不会发送到服务器。</Text> + </div> + + <Paper withBorder p="md"> + <form + onSubmit={form.onSubmit(submit, () => setSubmitted(null))} + onReset={(event) => { + event.preventDefault() + form.reset() + form.setErrors(exampleErrors) + setSubmitted(null) + }} + noValidate + > + <Stack gap="lg"> + <Text size="sm" c="dimmed">带 * 的项目为必填项;离开输入框或提交时会校验。邮箱预填了错误值,用于演示错误提示。</Text> + <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md"> + <TextInput + label="文本输入框" + placeholder="请输入名称" + required + {...form.getInputProps('name')} + /> + <TextInput + label="邮箱(错误提示示例)" + description="修改为有效邮箱后可正常提交" + placeholder="name@example.com" + type="email" + autoComplete="email" + required + {...form.getInputProps('email')} + /> + <Select + label="选择框" + placeholder="请选择角色" + data={['开发者', '设计师', '产品经理']} + searchable + clearable + required + {...form.getInputProps('role')} + /> + <NumberInput + label="数值输入框" + description="数量,范围 0–100" + min={0} + max={100} + allowDecimal={false} + {...form.getInputProps('quantity')} + /> + <PasswordInput + label="密码输入框" + description="选填,填写时至少 8 位" + placeholder="请输入示例密码" + autoComplete="new-password" + {...form.getInputProps('password')} + /> + </SimpleGrid> + + <Cascader + label="级联选择框" + description="选择省、市、区,使用部分示例地区" + placeholder="请选择省 / 市 / 区" + data={regionOptions} + withColumns={withColumns} + searchable + clearable + closeOnSelect + clearButtonProps={{ 'aria-label': '清空地区' }} + nothingFoundMessage="没有匹配的地区" + {...form.getInputProps('region')} + /> + + <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg"> + <MultiSelect + label="下拉多选框" + description="选择多个技术方向" + placeholder="请选择技术方向" + data={['React', 'TypeScript', 'Vite', 'Mantine', 'Node.js']} + searchable + clearable + clearButtonProps={{ 'aria-label': '清空技术方向' }} + nothingFoundMessage="没有匹配的选项" + {...form.getInputProps('technologies')} + /> + <TagsInput + label="标签输入框" + description="输入后按 Enter 或逗号添加标签,也可选择推荐标签" + placeholder="输入或选择标签" + data={['学习', '工作', '个人项目', '开源']} + clearable + clearButtonProps={{ 'aria-label': '清空标签' }} + {...form.getInputProps('tags')} + /> + <Radio.Group label="单选框" description="偏好的联系方式" {...form.getInputProps('contact')}> + <Group mt="xs"> + <Radio value="email" label="邮件" /> + <Radio value="phone" label="电话" /> + <Radio value="message" label="站内消息" /> + </Group> + </Radio.Group> + <Checkbox.Group label="多选框" description="感兴趣的方向" {...form.getInputProps('interests')}> + <Group mt="xs"> + <Checkbox value="frontend" label="前端" /> + <Checkbox value="backend" label="后端" /> + <Checkbox value="design" label="设计" /> + </Group> + </Checkbox.Group> + <ColorInput + label="颜色选择框" + format="hex" + swatches={['#228be6', '#40c057', '#fab005', '#fa5252', '#7950f2']} + {...form.getInputProps('color')} + /> + <FileInput + label="文件上传框" + description="支持选择多个文件,仅展示文件信息" + placeholder="点击选择文件" + leftSection={<UploadSimpleIcon aria-hidden="true" />} + multiple + clearable + {...form.getInputProps('files')} + /> + </SimpleGrid> + + {form.values.files.length > 0 && ( + <Stack gap="xs" aria-live="polite"> + {form.values.files.map((file, index) => ( + <Text key={`${file.name}-${index}`} size="sm" className={classes.fileName}> + {file.name} · {(file.size / 1024).toFixed(1)} KB + </Text> + ))} + </Stack> + )} + + <Textarea + label="文本框" + description="多行文本,例如个人介绍或备注" + placeholder="请输入内容" + autosize + minRows={3} + maxRows={8} + {...form.getInputProps('description')} + /> + <Switch + label="开关:接收消息通知" + {...form.getInputProps('notifications', { type: 'checkbox' })} + /> + <Stack gap="xs"> + <details> + <summary>查看示例协议</summary> + <Text size="sm" c="dimmed" py="xs"> + 本页面仅用于演示表单交互。提交内容仅在当前页面预览,文件不会上传。 + 勾选同意用于演示提交前的校验流程,不代表订购或开通服务。 + </Text> + </details> + <Checkbox + label="我已阅读并同意示例协议" + required + {...form.getInputProps('acceptedTerms', { type: 'checkbox', withError: true })} + /> + </Stack> + <Group> + <Button type="submit">提交预览</Button> + <Button type="reset" variant="default">重置</Button> + </Group> + </Stack> + </form> + </Paper> + + {submitted !== null && ( + <Stack gap="xs"> + <Alert title="提交成功" color="green" role="status">以下是本次提交的示例数据。</Alert> + <Code block>{submitted}</Code> + </Stack> + )} + </Stack> + ) +} |
