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 { Icon, UploadImg } from '@/components';
import { uploadAvatar } from '@/services';
interface Props {
type: string;
imgPath: string;
uploadCallback: (data: FormData) => Promise<any>;
deleteCallback: (type: string) => void;
type: 'logo' | 'avatar';
value: string;
onChange: (value: string) => void;
}
const Index: FC<Props> = ({
type,
imgPath,
uploadCallback,
deleteCallback,
}) => {
const Index: FC<Props> = ({ type = 'logo', value, onChange }) => {
const onUpload = (file: any) => {
return new Promise((resolve) => {
uploadAvatar(file).then((res) => {
onChange(res);
resolve(true);
});
});
};
const onRemove = () => {
onChange('');
};
return (
<div className="d-flex">
<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>
<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" />
</UploadImg>
<Button
variant="outline-secondary"
onClick={() => deleteCallback(type)}>
<Button variant="outline-secondary" onClick={onRemove}>
<Icon name="trash" />
</Button>
</ButtonGroup>

View File

@ -2,6 +2,8 @@ import { FC } from 'react';
import { Form, Button, Stack } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import BrandUpload from '../BrandUpload';
import TimeZonePicker from '../TimeZonePicker';
import type * as Type from '@/common/interface';
export interface JSONSchema {
@ -27,6 +29,8 @@ export interface UISchema {
| 'checkbox'
| 'radio'
| 'select'
| 'upload'
| 'timezone'
| 'switch';
'ui:options'?: {
rows?: number;
@ -49,6 +53,8 @@ export interface UISchema {
empty?: string;
invalid?: string;
validator?: (value) => boolean;
textRender?: () => React.ReactElement;
imageType?: 'avatar' | 'logo';
};
};
}
@ -61,6 +67,14 @@ interface IProps {
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> = ({
schema,
uiSchema = {},
@ -81,6 +95,7 @@ const SchemaForm: FC<IProps> = ({
onChange(data);
}
};
const requiredValidator = () => {
const required = schema.required || [];
const errors: string[] = [];
@ -91,6 +106,7 @@ const SchemaForm: FC<IProps> = ({
});
return errors;
};
const syncValidator = () => {
const errors: string[] = [];
keys.forEach((key) => {
@ -104,6 +120,7 @@ const SchemaForm: FC<IProps> = ({
});
return errors;
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const errors = requiredValidator();
@ -149,6 +166,14 @@ const SchemaForm: FC<IProps> = ({
}
onSubmit(e);
};
const handleUploadChange = (name: string, value: string) => {
const data = { ...formData, [name]: { ...formData[name], value } };
if (onChange instanceof Function) {
onChange(data);
}
};
return (
<Form noValidate onSubmit={handleSubmit}>
{keys.map((key) => {
@ -222,6 +247,32 @@ const SchemaForm: FC<IProps> = ({
</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';
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',
);
const imgUpload = (file: any) => {
return new Promise((resolve) => {
console.log(file);
resolve(true);
});
};
return (
<div>
<h3 className="mb-4">{t('page_title')}</h3>
<BrandUpload
type="logo"
imgPath={img}
uploadCallback={imgUpload}
deleteCallback={() => {
console.log('delete');
setImg('');
}}
/>
<BrandUpload type="logo" value={img} onChange={setImg} />
</div>
);
};

View File

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

View File

@ -1,5 +1,6 @@
import { FC, FormEvent, useEffect, useState } from 'react';
import { Form, Button, Image, Stack } from 'react-bootstrap';
/* eslint-disable react/no-unstable-nested-components */
import React, { FC, FormEvent, useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import { Trans, useTranslation } from 'react-i18next';
import { useToast } from '@/hooks';
@ -9,10 +10,9 @@ import {
AdminSettingsInterface,
} from '@/common/interface';
import { interfaceStore } from '@/stores';
import { UploadImg } from '@/components';
import { TIMEZONES, DEFAULT_TIMEZONE } from '@/common/constants';
import { JSONSchema, SchemaForm, UISchema } from '@/components';
import { DEFAULT_TIMEZONE } from '@/common/constants';
import {
uploadAvatar,
updateInterfaceSetting,
useInterfaceSetting,
useThemeOptions,
@ -33,6 +33,36 @@ const Interface: FC = () => {
const [langs, setLangs] = useState<LangsType[]>();
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>({
logo: {
value: setting?.logo || storeInterface.logo,
@ -55,6 +85,54 @@ const Interface: FC = () => {
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 res: LangsType[] = await loadLanguageOptions(true);
setLangs(res);
@ -127,34 +205,22 @@ const Interface: FC = () => {
setFormData({ ...formData });
});
};
const imgUpload = (file: any) => {
return new Promise((resolve) => {
uploadAvatar(file).then((res) => {
setFormData({
...formData,
logo: {
value: res,
isInvalid: false,
errorMsg: '',
},
});
resolve(true);
});
});
};
const onChange = (fieldName, fieldValue) => {
if (!formData[fieldName]) {
return;
}
const fieldData: FormDataType = {
[fieldName]: {
value: fieldValue,
isInvalid: false,
errorMsg: '',
},
};
setFormData({ ...formData, ...fieldData });
};
// const imgUpload = (file: any) => {
// return new Promise((resolve) => {
// uploadAvatar(file).then((res) => {
// setFormData({
// ...formData,
// logo: {
// value: res,
// isInvalid: false,
// errorMsg: '',
// },
// });
// resolve(true);
// });
// });
// };
useEffect(() => {
if (setting) {
const formMeta = {};
@ -167,10 +233,21 @@ const Interface: FC = () => {
useEffect(() => {
getLangs();
}, []);
const handleOnChange = (data) => {
setFormData(data);
};
return (
<>
<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.Label>{t('logo.label')}</Form.Label>
<Stack gap={2}>
@ -282,7 +359,7 @@ const Interface: FC = () => {
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
</Form> */}
</>
);
};

View File

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