mirror of https://gitee.com/answerdev/answer.git
Merge branch 'feat/ui-v0.4' of git.backyard.segmentfault.com:opensource/answer into feat/ui-v0.4
This commit is contained in:
commit
399d695b83
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue