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 { 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>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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',
|
'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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue