Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center

This commit is contained in:
LinkinStars 2023-04-19 16:33:07 +08:00
commit 1d3f61e8a6
21 changed files with 226 additions and 130 deletions

View File

@ -8,6 +8,12 @@ export const CAPTCHA_CODE_STORAGE_KEY = '_a_captcha_';
export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
export const USER_AGENT_NAMES = {
SegmentFault: 'SegmentFault',
WeChat: 'WeChat',
WeCom: 'WeCom',
DingTalk: 'DingTalk',
};
export const IGNORE_PATH_LIST = [
'/users/login',

View File

@ -1,9 +1,9 @@
const pattern = {
email:
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/,
wx: /micromessenger/,
wxwork: /wxwork/,
dingtalk: /dingtalk/,
uaWeChat: /micromessenger/i,
uaWeCom: /wxwork/i,
uaDingTalk: /dingtalk/i,
};
export default pattern;

View File

@ -6,7 +6,7 @@ import type * as Type from '@/common/interface';
interface Props {
type: 'radio' | 'checkbox';
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
onChange?: (fd: Type.FormDataType) => void;
enumValues: (string | boolean | number)[];
enumNames: string[];
formData: Type.FormDataType;
@ -20,6 +20,24 @@ const Index: FC<Props> = ({
formData,
}) => {
const fieldObject = formData[fieldName];
const handleCheck = (
evt: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const { name, checked } = evt.currentTarget;
const freshVal = checked ? enumValues?.[index] : '';
const state = {
...formData,
[name]: {
...formData[name],
value: freshVal,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Stack direction="horizontal">
{enumValues?.map((item, index) => {
@ -36,7 +54,7 @@ const Index: FC<Props> = ({
feedback={fieldObject?.errorMsg}
feedbackType="invalid"
isInvalid={fieldObject?.isInvalid}
onChange={(evt) => onChange(evt, index)}
onChange={(evt) => handleCheck(evt, index)}
/>
);
})}

View File

@ -7,7 +7,7 @@ interface Props {
type: string | undefined;
placeholder: string | undefined;
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
readOnly: boolean;
}
@ -20,13 +20,28 @@ const Index: FC<Props> = ({
readOnly = false,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Control
name={fieldName}
placeholder={placeholder}
type={type}
value={fieldObject?.value || ''}
onChange={onChange}
onChange={handleChange}
readOnly={readOnly}
isInvalid={fieldObject?.isInvalid}
style={type === 'color' ? { width: '6rem' } : {}}

View File

@ -6,7 +6,7 @@ import type * as Type from '@/common/interface';
interface Props {
desc: string | undefined;
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLSelectElement>) => void;
onChange?: (fd: Type.FormDataType) => void;
enumValues: (string | boolean | number)[];
enumNames: string[];
formData: Type.FormDataType;
@ -20,12 +20,26 @@ const Index: FC<Props> = ({
formData,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Select
aria-label={desc}
name={fieldName}
value={fieldObject?.value || ''}
onChange={onChange}
onChange={handleChange}
isInvalid={fieldObject?.isInvalid}>
{enumValues?.map((item, index) => {
return (

View File

@ -7,11 +7,25 @@ interface Props {
title: string;
label: string | undefined;
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
}
const Index: FC<Props> = ({ title, fieldName, onChange, label, formData }) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value: checked,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Check
required
@ -23,7 +37,7 @@ const Index: FC<Props> = ({ title, fieldName, onChange, label, formData }) => {
feedback={fieldObject?.errorMsg}
feedbackType="invalid"
isInvalid={fieldObject.isInvalid}
onChange={onChange}
onChange={handleChange}
/>
);
};

View File

@ -10,7 +10,7 @@ interface Props {
rows: number | undefined;
className: classnames.Argument;
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLInputElement>, ...rest) => void;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
}
const Index: FC<Props> = ({
@ -22,13 +22,28 @@ const Index: FC<Props> = ({
formData,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<Form.Control
as="textarea"
name={fieldName}
placeholder={placeholder}
value={fieldObject?.value || ''}
onChange={onChange}
onChange={handleChange}
isInvalid={fieldObject?.isInvalid}
rows={rows}
className={classnames(className)}

View File

@ -5,17 +5,31 @@ import TimeZonePicker from '@/components/TimeZonePicker';
interface Props {
fieldName: string;
onChange: (evt: React.ChangeEvent<HTMLSelectElement>, ...rest) => void;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
}
const Index: FC<Props> = ({ fieldName, onChange, formData }) => {
const fieldObject = formData[fieldName];
const handleChange = (evt: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = evt.currentTarget;
const state = {
...formData,
[name]: {
...formData[name],
value,
isInvalid: false,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<TimeZonePicker
value={fieldObject?.value || ''}
isInvalid={fieldObject?.isInvalid}
name={fieldName}
onChange={onChange}
onChange={handleChange}
/>
);
};

View File

@ -8,7 +8,7 @@ interface Props {
type: Type.UploadType | undefined;
acceptType: string | undefined;
fieldName: string;
onChange: (key, val) => void;
onChange?: (fd: Type.FormDataType) => void;
formData: Type.FormDataType;
}
const Index: FC<Props> = ({
@ -19,13 +19,26 @@ const Index: FC<Props> = ({
formData,
}) => {
const fieldObject = formData[fieldName];
const handleChange = (name: string, value: string) => {
console.log('upload: ', name, value);
const state = {
...formData,
[name]: {
...formData[name],
value,
},
};
if (typeof onChange === 'function') {
onChange(state);
}
};
return (
<>
<BrandUpload
type={type}
acceptType={acceptType}
value={fieldObject?.value}
onChange={(value) => onChange(fieldName, value)}
onChange={(value) => handleChange(fieldName, value)}
/>
<Form.Control
name={fieldName}

View File

@ -148,7 +148,6 @@ interface IRef {
* TODO:
* - Normalize and document `formData[key].hidden && 'd-none'`
* - Normalize and document `hiddenSubmit`
* - `handleXXChange` methods are placed in the concrete component
* - Improving field hints for `formData`
* - Optimise form data updates
* * Automatic field type conversion
@ -210,38 +209,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
useEffect(() => {
setDefaultValueAsDomBehaviour();
}, [formData]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const handleSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
const data = {
...formData,
[name]: { ...formData[name], value: checked, isInvalid: false },
};
if (onChange instanceof Function) {
onChange(data);
}
};
const requiredValidator = () => {
const errors: string[] = [];
@ -351,32 +318,6 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
}
};
const handleUploadChange = (name: string, value: string) => {
const data = { ...formData, [name]: { ...formData[name], value } };
if (onChange instanceof Function) {
onChange(data);
}
};
const handleInputCheck = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const { name, checked } = e.currentTarget;
const freshVal = checked ? schema.properties[name]?.enum?.[index] : '';
const data = {
...formData,
[name]: {
...formData[name],
value: freshVal,
isInvalid: false,
},
};
if (onChange instanceof Function) {
onChange(data);
}
};
useImperativeHandle(ref, () => ({
validator,
}));
@ -422,7 +363,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
<Select
desc={description}
fieldName={key}
onChange={handleSelectChange}
onChange={onChange}
enumValues={enumValues}
enumNames={enumNames}
formData={formData}
@ -432,7 +373,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
<Check
type={widget}
fieldName={key}
onChange={handleInputCheck}
onChange={onChange}
enumValues={enumValues}
enumNames={enumNames}
formData={formData}
@ -443,14 +384,14 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
title={title}
label={uiOpt && 'label' in uiOpt ? uiOpt.label : ''}
fieldName={key}
onChange={handleSwitchChange}
onChange={onChange}
formData={formData}
/>
) : null}
{widget === 'timezone' ? (
<Timezone
fieldName={key}
onChange={handleSelectChange}
onChange={onChange}
formData={formData}
/>
) : null}
@ -463,7 +404,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
uiOpt && 'acceptType' in uiOpt ? uiOpt.acceptType : ''
}
fieldName={key}
onChange={handleUploadChange}
onChange={onChange}
formData={formData}
/>
) : null}
@ -475,7 +416,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
rows={uiOpt && 'rows' in uiOpt ? uiOpt.rows : 3}
className={uiOpt && 'className' in uiOpt ? uiOpt.className : ''}
fieldName={key}
onChange={handleInputChange}
onChange={onChange}
formData={formData}
/>
) : null}
@ -486,7 +427,7 @@ const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
uiOpt && 'placeholder' in uiOpt ? uiOpt.placeholder : ''
}
fieldName={key}
onChange={handleInputChange}
onChange={onChange}
formData={formData}
readOnly={readOnly}
/>

View File

@ -5,9 +5,10 @@ import { useTranslation } from 'react-i18next';
import QrCode from 'qrcode';
import { userCenterStore } from '@/stores';
import { guard, getUserAgentType } from '@/utils';
import { guard, getUaType, floppyNavigation } from '@/utils';
import { USER_AGENT_NAMES } from '@/common/constants';
import { getLoginConf, checkLoginResult } from './wecom.service';
import { getLoginConf, checkLoginResult } from './service';
let checkTimer: NodeJS.Timeout;
const Index: FC = () => {
@ -47,12 +48,14 @@ const Index: FC = () => {
return;
}
getLoginConf().then((res) => {
if (getUserAgentType() === 'wxwork') {
window.location.replace(res?.redirect_url);
if (getUaType() === USER_AGENT_NAMES.WeCom) {
floppyNavigation.navigate(res?.redirect_url, {
handler: 'replace',
});
} else {
handleQrCode(res?.redirect_url);
handleLoginResult(res?.key);
}
handleLoginResult(res?.key);
});
}, [agentName]);
useEffect(() => {
@ -60,7 +63,7 @@ const Index: FC = () => {
clearTimeout(checkTimer);
};
}, []);
if (/WeCom/i.test(agentName) && getUserAgentType() !== 'wxwork') {
if (getUaType() !== USER_AGENT_NAMES.WeCom) {
return (
<Card className="text-center">
<Card.Body>

View File

@ -0,0 +1,28 @@
import { memo } from 'react';
import { Container, Col } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { userCenterStore } from '@/stores';
import { USER_AGENT_NAMES } from '@/common/constants';
import WeComAuth from './components/WeCom';
const Index = () => {
const [searchParam] = useSearchParams();
const { agent: ucAgent } = userCenterStore();
let agentName = ucAgent?.agent_info.name || '';
if (searchParam.get('agent_name')) {
agentName = searchParam.get('agent_name') || '';
}
return (
<Container style={{ paddingTop: '5rem', paddingBottom: '5rem' }}>
<Col md={4} className="mx-auto">
{USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (
<WeComAuth />
) : null}
</Col>
</Container>
);
};
export default memo(Index);

View File

@ -3,6 +3,7 @@ import { Container } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';
import { userCenterStore } from '@/stores';
import { USER_AGENT_NAMES } from '@/common/constants';
import WeCom from './components/WeCom';
@ -13,7 +14,13 @@ const Index = () => {
if (searchParam.get('agent_name')) {
agentName = searchParam.get('agent_name') || '';
}
return <Container>{/^WeCom/i.test(agentName) ? <WeCom /> : null}</Container>;
return (
<Container>
{USER_AGENT_NAMES.WeCom.toLowerCase() === agentName.toLowerCase() ? (
<WeCom />
) : null}
</Container>
);
};
export default memo(Index);

View File

@ -17,7 +17,7 @@ import {
userCenterStore,
} from '@/stores';
import { guard, handleFormError } from '@/utils';
import { login, checkImgCode } from '@/services';
import { login, checkImgCode, UcAgent } from '@/services';
import { PicAuthCodeModal } from '@/components/Modal';
const Index: React.FC = () => {
@ -28,8 +28,12 @@ const Index: React.FC = () => {
const { user: storeUser, update: updateUser } = loggedUserInfoStore((_) => _);
const loginSetting = loginSettingStore((state) => state.login);
const ucAgent = userCenterStore().agent;
const ucLoginRedirect =
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
let ucAgentInfo: UcAgent['agent_info'] | undefined;
if (ucAgent?.enabled && ucAgent?.agent_info) {
ucAgentInfo = ucAgent.agent_info;
}
const canOriginalLogin =
!ucAgentInfo || ucAgentInfo.enabled_original_user_system;
const [formData, setFormData] = useState<FormDataType>({
e_mail: {
@ -61,7 +65,7 @@ const Index: React.FC = () => {
};
const getImgCode = () => {
if (ucLoginRedirect) {
if (!canOriginalLogin) {
return;
}
checkImgCode({
@ -171,14 +175,13 @@ const Index: React.FC = () => {
return (
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
<WelcomeTitle />
{ucLoginRedirect && step === 1 && (
<Col className="mx-auto" md={4}>
<PluginUcLogin />
</Col>
)}
{step === 1 && !ucLoginRedirect && (
{step === 1 && canOriginalLogin ? (
<Col className="mx-auto" md={3}>
<PluginOauth className="mb-5" />
{ucAgentInfo ? (
<PluginUcLogin className="mb-5" />
) : (
<PluginOauth className="mb-5" />
)}
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="email" className="mb-3">
<Form.Label>{t('email.label')}</Form.Label>
@ -250,7 +253,7 @@ const Index: React.FC = () => {
</div>
)}
</Col>
)}
) : null}
{step === 2 && <Unactivate visible={step === 2} />}

View File

@ -1,7 +1,7 @@
import { FC } from 'react';
import { Container, Row, Col, Button } from 'react-bootstrap';
import { Container, Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom';
import { useParams, useSearchParams, Link } from 'react-router-dom';
import { usePageTags } from '@/hooks';
import { Pagination, FormatTime, Empty } from '@/components';
@ -73,12 +73,11 @@ const Personal: FC = () => {
className="d-flex justify-content-start justify-content-md-end">
{isSelf && (
<div className="mb-3">
<Button
variant="outline-secondary"
href="/users/settings/profile"
className="btn">
<Link
className="btn btn-outline-secondary"
to="/users/settings/profile">
{t('edit_profile')}
</Button>
</Link>
</div>
)}
</Col>

View File

@ -2,28 +2,29 @@ import { memo, FC } from 'react';
import { Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import classnames from 'classnames';
import { SvgIcon } from '@/components';
import { userCenterStore } from '@/stores';
import WeComLogin from './WeCom';
const Index: FC = () => {
interface Props {
className?: classnames.Argument;
}
const Index: FC<Props> = ({ className }) => {
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
const ucAgent = userCenterStore().agent;
const agentName = ucAgent?.agent_info?.name || '';
const ucLoginRedirect =
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
if (/WeCom/i.test(agentName)) {
return <WeComLogin />;
}
if (ucLoginRedirect) {
return (
<Button
className="w-100"
className={classnames('w-100', className)}
variant="outline-secondary"
href={ucAgent?.agent_info.login_redirect_url}>
<SvgIcon base64={ucAgent?.agent_info.icon} />
<span>{t('connect', { auth_name: ucAgent?.agent_info.name })}</span>
<span>{t('connect', { auth_name: agentName })}</span>
</Button>
);
}

View File

@ -240,8 +240,8 @@ const routes: RouteNode[] = [
page: 'pages/Users/OauthBindEmail',
},
{
path: '/users/oauth',
page: 'pages/Users/OauthCallback',
path: '/users/auth-landing',
page: 'pages/Users/AuthCallback',
},
{
path: '/posts/:qid/timeline',
@ -362,6 +362,10 @@ const routes: RouteNode[] = [
},
],
},
{
path: '/user-center/auth',
page: 'pages/UserCenter/Auth',
},
{
path: '/user-center/auth-failed',
page: 'pages/UserCenter/AuthFailed',

View File

@ -14,6 +14,7 @@ export interface UcAgent {
login_redirect_url: string;
sign_up_redirect_url: string;
control_center: UcAgentControl[];
enabled_original_user_system: boolean;
};
}

View File

@ -1,6 +1,7 @@
import i18next from 'i18next';
import pattern from '@/common/pattern';
import { USER_AGENT_NAMES } from '@/common/constants';
const Diff = require('diff');
@ -258,18 +259,17 @@ function base64ToSvg(base64: string) {
// Determine whether the user is in WeChat or Enterprise WeChat or DingTalk, and return the corresponding type
function getUserAgentType() {
function getUaType() {
const ua = navigator.userAgent.toLowerCase();
if (pattern.wxwork.test(ua)) {
return 'wxwork';
if (pattern.uaWeCom.test(ua)) {
return USER_AGENT_NAMES.WeCom;
}
if (pattern.uaWeChat.test(ua)) {
return USER_AGENT_NAMES.WeChat;
}
if (pattern.uaDingTalk.test(ua)) {
return USER_AGENT_NAMES.DingTalk;
}
// if (pattern.wx.test(ua)) {
// return 'weixin';
// }
// if (pattern.dingtalk.test(ua)) {
// return 'dingtalk';
// }
return null;
}
@ -289,5 +289,5 @@ export {
handleFormError,
diffText,
base64ToSvg,
getUserAgentType,
getUaType,
};