Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4

This commit is contained in:
haitao(lj) 2022-11-10 16:37:13 +08:00
commit 399d695b83
7 changed files with 211 additions and 69 deletions

View File

@ -2,33 +2,38 @@ import { FC } from 'react';
import { ButtonGroup, Button } from 'react-bootstrap'; import { ButtonGroup, Button } from 'react-bootstrap';
import { Icon, UploadImg } from '@/components'; import { Icon, UploadImg } from '@/components';
import { uploadAvatar } from '@/services';
interface Props { interface Props {
type: string; type: 'logo' | 'avatar';
imgPath: string; value: string;
uploadCallback: (data: FormData) => Promise<any>; onChange: (value: string) => void;
deleteCallback: (type: string) => void;
} }
const Index: FC<Props> = ({ const Index: FC<Props> = ({ type = 'logo', value, onChange }) => {
type, const onUpload = (file: any) => {
imgPath, return new Promise((resolve) => {
uploadCallback, uploadAvatar(file).then((res) => {
deleteCallback, onChange(res);
}) => { resolve(true);
});
});
};
const onRemove = () => {
onChange('');
};
return ( return (
<div className="d-flex"> <div className="d-flex">
<div className="bg-gray-300 upload-img-wrap me-2 d-flex align-items-center justify-content-center"> <div className="bg-gray-300 upload-img-wrap me-2 d-flex align-items-center justify-content-center">
<img src={imgPath} alt="" height={100} /> <img src={value} alt="" height={100} />
</div> </div>
<ButtonGroup vertical className="fit-content"> <ButtonGroup vertical className="fit-content">
<UploadImg type={type} upload={uploadCallback} className="mb-0"> <UploadImg type={type} upload={onUpload} className="mb-0">
<Icon name="cloud-upload" /> <Icon name="cloud-upload" />
</UploadImg> </UploadImg>
<Button <Button variant="outline-secondary" onClick={onRemove}>
variant="outline-secondary"
onClick={() => deleteCallback(type)}>
<Icon name="trash" /> <Icon name="trash" />
</Button> </Button>
</ButtonGroup> </ButtonGroup>

View File

@ -2,6 +2,8 @@ import { FC } from 'react';
import { Form, Button, Stack } from 'react-bootstrap'; import { Form, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import BrandUpload from '../BrandUpload';
import TimeZonePicker from '../TimeZonePicker';
import type * as Type from '@/common/interface'; import type * as Type from '@/common/interface';
export interface JSONSchema { export interface JSONSchema {
@ -27,6 +29,8 @@ export interface UISchema {
| 'checkbox' | 'checkbox'
| 'radio' | 'radio'
| 'select' | 'select'
| 'upload'
| 'timezone'
| 'switch'; | 'switch';
'ui:options'?: { 'ui:options'?: {
rows?: number; rows?: number;
@ -49,6 +53,8 @@ export interface UISchema {
empty?: string; empty?: string;
invalid?: string; invalid?: string;
validator?: (value) => boolean; validator?: (value) => boolean;
textRender?: () => React.ReactElement;
imageType?: 'avatar' | 'logo';
}; };
}; };
} }
@ -61,6 +67,14 @@ interface IProps {
onSubmit: (e: React.FormEvent) => void; onSubmit: (e: React.FormEvent) => void;
} }
/**
* json schema form
* @param schema json schema
* @param uiSchema ui schema
* @param formData form data
* @param onChange change event
* @param onSubmit submit event
*/
const SchemaForm: FC<IProps> = ({ const SchemaForm: FC<IProps> = ({
schema, schema,
uiSchema = {}, uiSchema = {},
@ -81,6 +95,7 @@ const SchemaForm: FC<IProps> = ({
onChange(data); onChange(data);
} }
}; };
const requiredValidator = () => { const requiredValidator = () => {
const required = schema.required || []; const required = schema.required || [];
const errors: string[] = []; const errors: string[] = [];
@ -91,6 +106,7 @@ const SchemaForm: FC<IProps> = ({
}); });
return errors; return errors;
}; };
const syncValidator = () => { const syncValidator = () => {
const errors: string[] = []; const errors: string[] = [];
keys.forEach((key) => { keys.forEach((key) => {
@ -104,6 +120,7 @@ const SchemaForm: FC<IProps> = ({
}); });
return errors; return errors;
}; };
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const errors = requiredValidator(); const errors = requiredValidator();
@ -149,6 +166,14 @@ const SchemaForm: FC<IProps> = ({
} }
onSubmit(e); onSubmit(e);
}; };
const handleUploadChange = (name: string, value: string) => {
const data = { ...formData, [name]: { ...formData[name], value } };
if (onChange instanceof Function) {
onChange(data);
}
};
return ( return (
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
{keys.map((key) => { {keys.map((key) => {
@ -222,6 +247,32 @@ const SchemaForm: FC<IProps> = ({
</Form.Group> </Form.Group>
); );
} }
if (widget === 'timezone') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Label>{title}</Form.Label>
<TimeZonePicker
value={formData[key]?.value}
onChange={handleInputChange}
/>
<Form.Text className="text-muted">{description}</Form.Text>
</Form.Group>
);
}
if (widget === 'upload') {
return (
<Form.Group key={title} className="mb-3" controlId={key}>
<Form.Label>{title}</Form.Label>
<BrandUpload
type={options.imageType || 'avatar'}
value={formData[key]?.value}
onChange={(value) => handleUploadChange(key, value)}
/>
<Form.Text className="text-muted">{description}</Form.Text>
</Form.Group>
);
}
const as = widget === 'textarea' ? 'textarea' : 'input'; const as = widget === 'textarea' ? 'textarea' : 'input';
return ( return (

View File

@ -0,0 +1,25 @@
import { Form } from 'react-bootstrap';
import { TIMEZONES } from '@/common/constants';
const TimeZonePicker = (props) => {
return (
<Form.Select {...props}>
{TIMEZONES?.map((item) => {
return (
<optgroup label={item.label} key={item.label}>
{item.options.map((option) => {
return (
<option value={option.value} key={option.value}>
{option.label}
</option>
);
})}
</optgroup>
);
})}
</Form.Select>
);
};
export default TimeZonePicker;

View File

@ -12,24 +12,10 @@ const Index: FC = () => {
'https://image-static.segmentfault.com/405/057/4050570037-636c7b0609a49', 'https://image-static.segmentfault.com/405/057/4050570037-636c7b0609a49',
); );
const imgUpload = (file: any) => {
return new Promise((resolve) => {
console.log(file);
resolve(true);
});
};
return ( return (
<div> <div>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
<BrandUpload <BrandUpload type="logo" value={img} onChange={setImg} />
type="logo"
imgPath={img}
uploadCallback={imgUpload}
deleteCallback={() => {
console.log('delete');
setImg('');
}}
/>
</div> </div>
); );
}; };

View File

@ -18,8 +18,7 @@ const General: FC = () => {
const { data: setting } = useGeneralSetting(); const { data: setting } = useGeneralSetting();
const schema: JSONSchema = { const schema: JSONSchema = {
title: t('title'), title: t('page_title'),
description: t('description'),
required: ['name', 'site_url', 'contact_email'], required: ['name', 'site_url', 'contact_email'],
properties: { properties: {
name: { name: {

View File

@ -1,5 +1,6 @@
import { FC, FormEvent, useEffect, useState } from 'react'; /* eslint-disable react/no-unstable-nested-components */
import { Form, Button, Image, Stack } from 'react-bootstrap'; import React, { FC, FormEvent, useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import { useToast } from '@/hooks'; import { useToast } from '@/hooks';
@ -9,10 +10,9 @@ import {
AdminSettingsInterface, AdminSettingsInterface,
} from '@/common/interface'; } from '@/common/interface';
import { interfaceStore } from '@/stores'; import { interfaceStore } from '@/stores';
import { UploadImg } from '@/components'; import { JSONSchema, SchemaForm, UISchema } from '@/components';
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants'; import { DEFAULT_TIMEZONE } from '@/common/constants';
import { import {
uploadAvatar,
updateInterfaceSetting, updateInterfaceSetting,
useInterfaceSetting, useInterfaceSetting,
useThemeOptions, useThemeOptions,
@ -33,6 +33,36 @@ const Interface: FC = () => {
const [langs, setLangs] = useState<LangsType[]>(); const [langs, setLangs] = useState<LangsType[]>();
const { data: setting } = useInterfaceSetting(); const { data: setting } = useInterfaceSetting();
const schema: JSONSchema = {
title: t('page_title'),
properties: {
logo: {
type: 'string',
title: t('logo.label'),
description: t('logo.text'),
},
theme: {
type: 'string',
title: t('theme.label'),
description: t('theme.text'),
enum: themes?.map((theme) => theme.value) || [],
enumNames: themes?.map((theme) => theme.label) || [],
},
language: {
type: 'string',
title: t('language.label'),
description: t('language.text'),
enum: langs?.map((lang) => lang.value),
enumNames: langs?.map((lang) => lang.label),
},
time_zone: {
type: 'string',
title: t('time_zone.label'),
description: t('time_zone.text'),
},
},
};
const [formData, setFormData] = useState<FormDataType>({ const [formData, setFormData] = useState<FormDataType>({
logo: { logo: {
value: setting?.logo || storeInterface.logo, value: setting?.logo || storeInterface.logo,
@ -55,6 +85,54 @@ const Interface: FC = () => {
errorMsg: '', errorMsg: '',
}, },
}); });
const onChange = (fieldName, fieldValue) => {
if (!formData[fieldName]) {
return;
}
const fieldData: FormDataType = {
[fieldName]: {
value: fieldValue,
isInvalid: false,
errorMsg: '',
},
};
setFormData({ ...formData, ...fieldData });
};
const uiSchema: UISchema = {
logo: {
'ui:widget': 'upload',
'ui:options': {
textRender: () => {
return (
<Trans i18nKey="admin.interface.logo.text">
You can upload your image or
<Button
variant="link"
size="sm"
className="p-0 mx-1"
onClick={(evt) => {
evt.preventDefault();
onChange('logo', '');
}}>
reset it
</Button>
to the site title text.
</Trans>
);
},
},
},
theme: {
'ui:widget': 'select',
},
language: {
'ui:widget': 'select',
},
time_zone: {
'ui:widget': 'timezone',
},
};
const getLangs = async () => { const getLangs = async () => {
const res: LangsType[] = await loadLanguageOptions(true); const res: LangsType[] = await loadLanguageOptions(true);
setLangs(res); setLangs(res);
@ -127,34 +205,22 @@ const Interface: FC = () => {
setFormData({ ...formData }); setFormData({ ...formData });
}); });
}; };
const imgUpload = (file: any) => { // const imgUpload = (file: any) => {
return new Promise((resolve) => { // return new Promise((resolve) => {
uploadAvatar(file).then((res) => { // uploadAvatar(file).then((res) => {
setFormData({ // setFormData({
...formData, // ...formData,
logo: { // logo: {
value: res, // value: res,
isInvalid: false, // isInvalid: false,
errorMsg: '', // errorMsg: '',
}, // },
}); // });
resolve(true); // resolve(true);
}); // });
}); // });
}; // };
const onChange = (fieldName, fieldValue) => {
if (!formData[fieldName]) {
return;
}
const fieldData: FormDataType = {
[fieldName]: {
value: fieldValue,
isInvalid: false,
errorMsg: '',
},
};
setFormData({ ...formData, ...fieldData });
};
useEffect(() => { useEffect(() => {
if (setting) { if (setting) {
const formMeta = {}; const formMeta = {};
@ -167,10 +233,21 @@ const Interface: FC = () => {
useEffect(() => { useEffect(() => {
getLangs(); getLangs();
}, []); }, []);
const handleOnChange = (data) => {
setFormData(data);
};
return ( return (
<> <>
<h3 className="mb-4">{t('page_title')}</h3> <h3 className="mb-4">{t('page_title')}</h3>
<Form noValidate onSubmit={onSubmit}> <SchemaForm
schema={schema}
uiSchema={uiSchema}
formData={formData}
onSubmit={onSubmit}
onChange={handleOnChange}
/>
{/* <Form noValidate onSubmit={onSubmit}>
<Form.Group controlId="logo" className="mb-3"> <Form.Group controlId="logo" className="mb-3">
<Form.Label>{t('logo.label')}</Form.Label> <Form.Label>{t('logo.label')}</Form.Label>
<Stack gap={2}> <Stack gap={2}>
@ -282,7 +359,7 @@ const Interface: FC = () => {
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })} {t('save', { keyPrefix: 'btns' })}
</Button> </Button>
</Form> </Form> */}
</> </>
); );
}; };

View File

@ -15,8 +15,7 @@ const Smtp: FC = () => {
const Toast = useToast(); const Toast = useToast();
const { data: setting } = useSmtpSetting(); const { data: setting } = useSmtpSetting();
const schema: JSONSchema = { const schema: JSONSchema = {
title: t('title'), title: t('page_title'),
description: t('description'),
properties: { properties: {
from_email: { from_email: {
type: 'string', type: 'string',