Browse Source

ci: enable React Doctor action (#11880)

pull/11882/head
afc163 3 months ago
committed by GitHub
parent
commit
c7276df122
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 27
      .github/workflows/react-doctor.yml
  2. 46
      doctor.config.json
  3. 919
      package-lock.json
  4. 2
      package.json
  5. 21
      react-doctor.config.json
  6. 6
      src/components/AvatarList/index.style.ts
  7. 49
      src/components/AvatarList/index.tsx
  8. 42
      src/components/OfflineBanner/index.tsx
  9. 77
      src/components/RightContent/AvatarDropdown.tsx
  10. 63
      src/components/RightContent/LangDropdown.tsx
  11. 36
      src/components/RightContent/VersionDropdown.tsx
  12. 120
      src/components/RightContent/index.tsx
  13. 16
      src/components/RightContent/style.ts
  14. 28
      src/components/TagSelect/index.tsx
  15. 2
      src/pages/account/center/components/Projects/index.tsx
  16. 7
      src/pages/account/settings/components/base.tsx
  17. 70
      src/pages/account/settings/components/binding.tsx
  18. 53
      src/pages/account/settings/components/notification.tsx
  19. 110
      src/pages/account/settings/components/security.tsx
  20. 25
      src/pages/account/settings/index.tsx
  21. 11
      src/pages/dashboard/analysis/components/Charts/index.tsx
  22. 3
      src/pages/dashboard/analysis/components/IntroduceRow.tsx
  23. 10
      src/pages/dashboard/analysis/components/NumberInfo/index.tsx
  24. 38
      src/pages/dashboard/analysis/components/ProportionSales.tsx
  25. 57
      src/pages/dashboard/analysis/components/TopSearch.tsx
  26. 8
      src/pages/dashboard/analysis/index.tsx
  27. 14
      src/pages/dashboard/analysis/utils/Yuan.tsx
  28. 2
      src/pages/dashboard/analysis/utils/utils.ts
  29. 7
      src/pages/dashboard/monitor/components/ActiveChart/index.tsx
  30. 6
      src/pages/dashboard/workplace/index.tsx
  31. 13
      src/pages/form/advanced-form/index.tsx
  32. 16
      src/pages/form/basic-form/index.test.tsx
  33. 15
      src/pages/list/basic-list/index.tsx
  34. 19
      src/pages/list/card-list/index.tsx
  35. 90
      src/pages/list/search/articles/index.tsx
  36. 23
      src/pages/list/search/projects/index.tsx
  37. 6
      src/pages/profile/advanced/index.tsx
  38. 14
      src/pages/result/fail/index.tsx
  39. 4
      src/pages/result/success/index.tsx
  40. 13
      src/pages/table-list/index.tsx
  41. 48
      src/pages/user/login/index.tsx
  42. 8
      src/pages/user/register-result/index.tsx
  43. 64
      src/pages/user/register/index.tsx

27
.github/workflows/react-doctor.yml

@ -0,0 +1,27 @@
name: React Doctor
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
push:
branches: [master]
permissions:
contents: read
pull-requests: write
issues: write
statuses: write
concurrency:
group: react-doctor-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
fetch-depth: 0
persist-credentials: false
- uses: millionco/react-doctor@0b4f4f4bd248a154e64eb508a48347f71154b3f3 # v2

46
doctor.config.json

@ -0,0 +1,46 @@
{
"ignore": {
"files": [
"config/**",
"mock/**",
"public/**",
"scripts/**",
"types/**",
".worktrees/**",
"cloudflare-worker/**",
"src/.umi*/**",
"src/services/**",
"src/service-worker.js",
"**/_mock.ts",
"**/*.md",
"**/*.less",
"**/*.css"
],
"overrides": [
{
"files": [
"src/global.tsx",
"src/global.style.ts",
"src/loading.tsx",
"src/locales/**",
"src/pages/**",
"tests/__mocks__/**"
],
"rules": ["deslop/unused-file"]
},
{
"files": ["package.json"],
"rules": ["deslop/unused-dev-dependency"]
},
{
"files": [
"src/pages/form/advanced-form/index.tsx",
"src/pages/table-list/index.tsx"
],
"rules": ["react-doctor/no-giant-component"]
}
]
},
"blocking": "error",
"adoptExistingLintConfig": true
}

919
package-lock.json

File diff suppressed because it is too large

2
package.json

@ -84,7 +84,7 @@
"jsdom": "^29.1.1",
"lint-staged": "^17.0.2",
"mockjs": "^1.1.0",
"react-doctor": "^0.5.0",
"react-doctor": "^0.5.8",
"swagger-ui-dist": "^5.32.5",
"tailwindcss": "^4.3.0",
"ts-node": "^10.9.2",

21
react-doctor.config.json

@ -1,21 +0,0 @@
{
"ignore": {
"files": [
"config/**",
"mock/**",
"public/**",
"scripts/**",
"types/**",
"cloudflare-worker/**",
"src/.umi*/**",
"src/services/**",
"src/service-worker.js",
"**/_mock.ts",
"**/*.md",
"**/*.less",
"**/*.css"
]
},
"failOn": "error",
"adoptExistingLintConfig": true
}

6
src/components/AvatarList/index.style.ts

@ -35,6 +35,12 @@ const useStyles = createStyles(({ token }) => {
},
},
},
avatarButton: {
padding: 0,
border: 0,
background: 'transparent',
cursor: 'pointer',
},
};
});

49
src/components/AvatarList/index.tsx

@ -31,38 +31,31 @@ const avatarSizeToClassName = (styles: any, size?: SizeType | 'mini') =>
[styles.avatarItemMini]: size === 'mini',
});
const Item: React.FC<AvatarItemProps> = ({
src,
size,
tips,
onClick = () => {},
}) => {
const Item: React.FC<AvatarItemProps> = ({ src, size, tips, onClick }) => {
const { styles } = useStyles();
const cls = avatarSizeToClassName(styles, size);
const avatar = tips ? (
<Tooltip title={tips}>
<Avatar
src={src}
size={size}
style={{
cursor: onClick ? 'pointer' : undefined,
}}
/>
</Tooltip>
) : (
<Avatar src={src} size={size} />
);
return (
<li
className={cls}
onClick={onClick}
onKeyDown={
onClick
? (e) => {
if (e.key === 'Enter') onClick();
}
: undefined
}
>
{tips ? (
<Tooltip title={tips}>
<Avatar
src={src}
size={size}
style={{
cursor: 'pointer',
}}
/>
</Tooltip>
<li className={cls}>
{onClick ? (
<button type="button" className={styles.avatarButton} onClick={onClick}>
{avatar}
</button>
) : (
<Avatar src={src} size={size} />
avatar
)}
</li>
);

42
src/components/OfflineBanner/index.tsx

@ -1,31 +1,27 @@
import { getIntl } from '@umijs/max';
import { Alert } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { useSyncExternalStore } from 'react';
const OfflineBanner: React.FC = () => {
const isOnlineRef = useRef(true);
const [, forceUpdate] = useState<number>(0);
const subscribeOnlineStatus = (callback: () => void) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
};
const getOnlineStatus = () =>
typeof navigator === 'undefined' ? true : navigator.onLine;
useEffect(() => {
isOnlineRef.current = navigator.onLine;
forceUpdate((n) => n + 1);
const handleOnline = () => {
isOnlineRef.current = true;
forceUpdate((n: number) => n + 1);
};
const handleOffline = () => {
isOnlineRef.current = false;
forceUpdate((n: number) => n + 1);
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
const OfflineBanner: React.FC = () => {
const isOnline = useSyncExternalStore(
subscribeOnlineStatus,
getOnlineStatus,
() => true,
);
if (isOnlineRef.current) return null;
if (isOnline) return null;
return (
<Alert

77
src/components/RightContent/AvatarDropdown.tsx

@ -14,24 +14,50 @@ type GlobalHeaderRightProps = {
children?: React.ReactNode;
};
const menuItems: MenuProps['items'] = [
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
key: 'theme',
icon: <SkinOutlined />,
label: '主题设置',
},
{
type: 'divider' as const,
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
const loginOut = async () => {
try {
await outLogin();
} catch {
// Local logout has already cleared user state; redirect should still proceed.
}
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
const searchParams = new URLSearchParams({
redirect: pathname + search,
});
const redirect = urlParams.get('redirect');
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: searchParams.toString(),
});
}
};
export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
children,
}) => {
const loginOut = async () => {
await outLogin();
const { search, pathname } = window.location;
const urlParams = new URL(window.location.href).searchParams;
const searchParams = new URLSearchParams({
redirect: pathname + search,
});
const redirect = urlParams.get('redirect');
if (window.location.pathname !== '/user/login' && !redirect) {
history.replace({
pathname: '/user/login',
search: searchParams.toString(),
});
}
};
const { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick: MenuProps['onClick'] = (event) => {
@ -60,27 +86,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return <Spin size="small" />;
}
const menuItems: MenuProps['items'] = [
{
key: 'settings',
icon: <SettingOutlined />,
label: '个人设置',
},
{
key: 'theme',
icon: <SkinOutlined />,
label: '主题设置',
},
{
type: 'divider' as const,
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录',
},
];
return (
<HeaderDropdown
placement="bottomRight"

63
src/components/RightContent/LangDropdown.tsx

@ -0,0 +1,63 @@
import { CheckOutlined, GlobalOutlined } from '@ant-design/icons';
import { getAllLocales, getLocale, setLocale } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Button } from 'antd';
import { useMemo } from 'react';
import HeaderDropdown from '../HeaderDropdown';
import useHeaderActionStyles from './style';
const localeLabelMap: Record<string, { emoji: string; label: string }> = {
'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇭🇰', label: '繁體中文' },
'en-US': { emoji: '🇺🇸', label: 'English' },
'ja-JP': { emoji: '🇯🇵', label: '日本語' },
'pt-BR': { emoji: '🇧🇷', label: 'Português' },
'id-ID': { emoji: '🇮🇩', label: 'Bahasa Indonesia' },
'fa-IR': { emoji: '🇮🇷', label: 'فارسی' },
'bn-BD': { emoji: '🇧🇩', label: 'বাংলা' },
};
const onLangClick: MenuProps['onClick'] = ({ key }) => {
if (key.startsWith('lang-')) {
setLocale(key.replace('lang-', ''), false);
}
};
export const LangDropdown: React.FC = () => {
const { styles } = useHeaderActionStyles();
const allLocales = useMemo(() => getAllLocales(), []);
const currentLocale = getLocale();
const supportLocales = allLocales.filter((l) => l in localeLabelMap);
if (supportLocales.length <= 1) {
return null;
}
const langItems: MenuProps['items'] = supportLocales.map((locale) => ({
key: `lang-${locale}`,
icon:
locale === currentLocale ? (
<CheckOutlined style={{ color: '#52c41a' }} />
) : (
<span style={{ display: 'inline-block', width: 14 }} />
),
label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`,
}));
return (
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [`lang-${currentLocale}`],
onClick: onLangClick,
items: langItems,
style: { minWidth: 180 },
}}
>
<Button type="text" className={styles.action} aria-label="语言切换">
<GlobalOutlined />
</Button>
</HeaderDropdown>
);
};

36
src/components/RightContent/VersionDropdown.tsx

@ -0,0 +1,36 @@
import { ForkOutlined } from '@ant-design/icons';
import type { MenuProps } from 'antd';
import { Button } from 'antd';
import HeaderDropdown from '../HeaderDropdown';
import useHeaderActionStyles from './style';
const versionItems: MenuProps['items'] = [
{ key: 'https://v5.pro.ant.design', label: 'v5' },
{ key: 'https://v4.pro.ant.design', label: 'v4' },
{ key: 'https://v2.pro.ant.design', label: 'v2' },
{ key: 'https://v1.pro.ant.design', label: 'v1' },
];
const onVersionClick: MenuProps['onClick'] = ({ key }) => {
window.open(key, '_blank', 'noopener,noreferrer');
};
export const VersionDropdown: React.FC = () => {
const { styles } = useHeaderActionStyles();
return (
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [],
onClick: onVersionClick,
items: versionItems,
style: { minWidth: 100 },
}}
>
<Button type="text" className={styles.action} aria-label="历史版本">
<ForkOutlined />
</Button>
</HeaderDropdown>
);
};

120
src/components/RightContent/index.tsx

@ -1,43 +1,13 @@
import {
BookOutlined,
CheckOutlined,
ForkOutlined,
GlobalOutlined,
} from '@ant-design/icons';
import { getAllLocales, getLocale, history, setLocale } from '@umijs/max';
import type { MenuProps } from 'antd';
import { BookOutlined } from '@ant-design/icons';
import { history } from '@umijs/max';
import { Button, Tooltip } from 'antd';
import { createStyles } from 'antd-style';
import React, { useMemo } from 'react';
import HeaderDropdown from '../HeaderDropdown';
export const localeLabelMap: Record<string, { emoji: string; label: string }> =
{
'zh-CN': { emoji: '🇨🇳', label: '简体中文' },
'zh-TW': { emoji: '🇭🇰', label: '繁體中文' },
'en-US': { emoji: '🇺🇸', label: 'English' },
'ja-JP': { emoji: '🇯🇵', label: '日本語' },
'pt-BR': { emoji: '🇧🇷', label: 'Português' },
'id-ID': { emoji: '🇮🇩', label: 'Bahasa Indonesia' },
'fa-IR': { emoji: '🇮🇷', label: 'فارسی' },
'bn-BD': { emoji: '🇧🇩', label: 'বাংলা' },
};
const useStyles = createStyles(({ token, css }) => ({
action: css`
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 36px !important;
min-width: 36px;
padding-inline: 8px !important;
padding-block: 0 !important;
border-radius: ${token.borderRadius}px !important;
`,
}));
import React from 'react';
import { LangDropdown } from './LangDropdown';
import useHeaderActionStyles from './style';
import { VersionDropdown } from './VersionDropdown';
export const DocLink: React.FC = () => {
const { styles } = useStyles();
const { styles } = useHeaderActionStyles();
return (
<Tooltip title="使用文档">
<Button
@ -53,78 +23,4 @@ export const DocLink: React.FC = () => {
);
};
const versionItems: MenuProps['items'] = [
{ key: 'https://v5.pro.ant.design', label: 'v5' },
{ key: 'https://v4.pro.ant.design', label: 'v4' },
{ key: 'https://v2.pro.ant.design', label: 'v2' },
{ key: 'https://v1.pro.ant.design', label: 'v1' },
];
const onVersionClick: MenuProps['onClick'] = ({ key }) => {
window.open(key, '_blank', 'noopener,noreferrer');
};
export const VersionDropdown: React.FC = () => {
const { styles } = useStyles();
return (
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [],
onClick: onVersionClick,
items: versionItems,
style: { minWidth: 100 },
}}
>
<Button type="text" className={styles.action} aria-label="历史版本">
<ForkOutlined />
</Button>
</HeaderDropdown>
);
};
export const LangDropdown: React.FC = () => {
const { styles } = useStyles();
const allLocales = useMemo(() => getAllLocales(), []);
const currentLocale = getLocale();
const supportLocales = allLocales.filter((l) => l in localeLabelMap);
if (supportLocales.length <= 1) {
return null;
}
const langItems: MenuProps['items'] = supportLocales.map((locale) => ({
key: `lang-${locale}`,
icon:
locale === currentLocale ? (
<CheckOutlined style={{ color: '#52c41a' }} />
) : (
<span style={{ display: 'inline-block', width: 14 }} />
),
label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`,
}));
const onLangClick: MenuProps['onClick'] = ({ key }) => {
if (key.startsWith('lang-')) {
setLocale(key.replace('lang-', ''), false);
}
};
return (
<HeaderDropdown
placement="bottomRight"
arrow
menu={{
selectedKeys: [`lang-${currentLocale}`],
onClick: onLangClick,
items: langItems,
style: { minWidth: 180 },
}}
>
<Button type="text" className={styles.action} aria-label="语言切换">
<GlobalOutlined />
</Button>
</HeaderDropdown>
);
};
export { LangDropdown, VersionDropdown };

16
src/components/RightContent/style.ts

@ -0,0 +1,16 @@
import { createStyles } from 'antd-style';
const useHeaderActionStyles = createStyles(({ token, css }) => ({
action: css`
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 36px !important;
min-width: 36px;
padding-inline: 8px !important;
padding-block: 0 !important;
border-radius: ${token.borderRadius}px !important;
`,
}));
export default useHeaderActionStyles;

28
src/components/TagSelect/index.tsx

@ -1,6 +1,6 @@
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { useMergedState } from '@rc-component/util';
import { Tag } from 'antd';
import { Button, Tag } from 'antd';
import { clsx } from 'clsx';
import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
@ -32,6 +32,10 @@ type TagSelectOptionElement = React.ReactElement<
typeof TagSelectOption
>;
const isTagSelectOption = (node: TagSelectOptionElement) =>
node?.type &&
(node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption');
interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
@ -71,11 +75,6 @@ const TagSelect: FC<TagSelectProps> & {
},
);
const isTagSelectOption = (node: TagSelectOptionElement) =>
node?.type &&
(node.type.isTagSelectOption ||
node.type.displayName === 'TagSelectOption');
// Memoize all tags to avoid recalculating on every render
const allTags = useMemo(() => {
const childrenArray = React.Children.toArray(
@ -136,19 +135,10 @@ const TagSelect: FC<TagSelectProps> & {
return child;
})}
{expandable && (
<a
<Button
type="link"
className={styles.trigger}
href="#"
onClick={(e) => {
e.preventDefault();
setExpand(!expand);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setExpand(!expand);
}
}}
onClick={() => setExpand((prev) => !prev)}
>
{expand ? (
<>
@ -160,7 +150,7 @@ const TagSelect: FC<TagSelectProps> & {
<DownOutlined />
</>
)}
</a>
</Button>
)}
</div>
);

2
src/pages/account/center/components/Projects/index.tsx

@ -36,7 +36,7 @@ const Projects: React.FC = () => {
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>

7
src/pages/account/settings/components/base.tsx

@ -27,6 +27,10 @@ const validatorPhone = (
callback();
};
const handleFinish = async () => {
message.success('更新基本信息成功');
};
const BaseView: React.FC = () => {
const { styles } = useStyles();
@ -45,9 +49,6 @@ const BaseView: React.FC = () => {
}
return '';
};
const handleFinish = async () => {
message.success('更新基本信息成功');
};
return (
<div className={styles.baseView}>
{loading ? null : (

70
src/pages/account/settings/components/binding.tsx

@ -3,47 +3,47 @@ import {
DingdingOutlined,
TaobaoOutlined,
} from '@ant-design/icons';
import { List } from 'antd';
import { Button, List } from 'antd';
import React from 'react';
const BindingView: React.FC = () => {
const getData = () => [
{
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [
<a key="Bind" href="#">
绑定
</a>,
],
avatar: <TaobaoOutlined className="taobao" />,
},
{
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [
<a key="Bind" href="#">
绑定
</a>,
],
avatar: <AlipayOutlined className="alipay" />,
},
{
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [
<a key="Bind" href="#">
绑定
</a>,
],
avatar: <DingdingOutlined className="dingding" />,
},
];
const bindingData = [
{
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [
<Button key="Bind" type="link">
绑定
</Button>,
],
avatar: <TaobaoOutlined className="taobao" />,
},
{
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [
<Button key="Bind" type="link">
绑定
</Button>,
],
avatar: <AlipayOutlined className="alipay" />,
},
{
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [
<Button key="Bind" type="link">
绑定
</Button>,
],
avatar: <DingdingOutlined className="dingding" />,
},
];
const BindingView: React.FC = () => {
return (
<List
itemLayout="horizontal"
dataSource={getData()}
dataSource={bindingData}
renderItem={(item) => (
<List.Item actions={item.actions}>
<List.Item.Meta

53
src/pages/account/settings/components/notification.tsx

@ -3,34 +3,41 @@ import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
const Action = (
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked />
const notificationAction = (key: string) => (
<Switch
key={key}
checkedChildren="开"
unCheckedChildren="关"
defaultChecked
/>
);
const NotificationView: React.FC = () => {
const getData = () => {
return [
{
title: '用户消息',
description: '其他用户的消息将以站内信的形式通知',
actions: [Action],
},
{
title: '系统消息',
description: '系统消息将以站内信的形式通知',
actions: [Action],
},
{
title: '待办任务',
description: '待办任务将以站内信的形式通知',
actions: [Action],
},
];
};
const notificationData = [
{
key: 'user-message',
title: '用户消息',
description: '其他用户的消息将以站内信的形式通知',
actions: [notificationAction('user-message-switch')],
},
{
key: 'system-message',
title: '系统消息',
description: '系统消息将以站内信的形式通知',
actions: [notificationAction('system-message-switch')],
},
{
key: 'todo-task',
title: '待办任务',
description: '待办任务将以站内信的形式通知',
actions: [notificationAction('todo-task-switch')],
},
];
const data = getData();
const NotificationView: React.FC = () => {
const data = notificationData;
return (
<List<Unpacked<typeof data>>
rowKey="key"
itemLayout="horizontal"
dataSource={data}
renderItem={(item) => (

110
src/pages/account/settings/components/security.tsx

@ -1,4 +1,4 @@
import { List } from 'antd';
import { Button, List } from 'antd';
import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
@ -9,61 +9,61 @@ const passwordStrength = {
weak: <span className="weak">弱 Weak</span>,
};
const SecurityView: React.FC = () => {
const getData = () => [
{
title: '账户密码',
description: (
<>
当前密码强度:
{passwordStrength.strong}
</>
),
actions: [
<a key="Modify" href="#">
修改
</a>,
],
},
{
title: '密保手机',
description: `已绑定手机:138****8293`,
actions: [
<a key="Modify" href="#">
修改
</a>,
],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [
<a key="Set" href="#">
设置
</a>,
],
},
{
title: '备用邮箱',
description: `已绑定邮箱:ant***sign.com`,
actions: [
<a key="Modify" href="#">
修改
</a>,
],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [
<a key="bind" href="#">
绑定
</a>,
],
},
];
const securityData = [
{
title: '账户密码',
description: (
<>
当前密码强度:
{passwordStrength.strong}
</>
),
actions: [
<Button key="Modify" type="link">
修改
</Button>,
],
},
{
title: '密保手机',
description: `已绑定手机:138****8293`,
actions: [
<Button key="Modify" type="link">
修改
</Button>,
],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [
<Button key="Set" type="link">
设置
</Button>,
],
},
{
title: '备用邮箱',
description: `已绑定邮箱:ant***sign.com`,
actions: [
<Button key="Modify" type="link">
修改
</Button>,
],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [
<Button key="bind" type="link">
绑定
</Button>,
],
},
];
const data = getData();
const SecurityView: React.FC = () => {
const data = securityData;
return (
<List<Unpacked<typeof data>>
itemLayout="horizontal"

25
src/pages/account/settings/index.tsx

@ -13,6 +13,17 @@ type SettingsState = {
selectKey: SettingsStateKeys;
};
const menuMap: Record<string, React.ReactNode> = {
base: '基本设置',
security: '安全设置',
binding: '账号绑定',
notification: '新消息通知',
};
const menuItems = Object.keys(menuMap).map((item) => ({
key: item,
label: menuMap[item],
}));
const SettingsContent: React.FC<{ selectKey: SettingsStateKeys }> = ({
selectKey,
}) => {
@ -32,12 +43,6 @@ const SettingsContent: React.FC<{ selectKey: SettingsStateKeys }> = ({
const Settings: React.FC = () => {
const { styles } = useStyles();
const menuMap: Record<string, React.ReactNode> = {
base: '基本设置',
security: '安全设置',
binding: '账号绑定',
notification: '新消息通知',
};
const [initConfig, setInitConfig] = useState<SettingsState>({
mode: 'inline',
selectKey: 'base',
@ -75,12 +80,6 @@ const Settings: React.FC = () => {
window.removeEventListener('resize', handler);
};
}, []);
const getMenu = () => {
return Object.keys(menuMap).map((item) => ({
key: item,
label: menuMap[item],
}));
};
return (
<GridContent>
<div
@ -101,7 +100,7 @@ const Settings: React.FC = () => {
selectKey: key as SettingsStateKeys,
}));
}}
items={getMenu()}
items={menuItems}
/>
</div>
<div className={styles.right}>

11
src/pages/dashboard/analysis/components/Charts/index.tsx

@ -1,11 +0,0 @@
import { formatYuan } from '@/utils/format';
import ChartCard from './ChartCard';
import Field from './Field';
const Charts = {
yuan: formatYuan,
ChartCard,
Field,
};
export { ChartCard, Charts as default, Field, formatYuan as yuan };

3
src/pages/dashboard/analysis/components/IntroduceRow.tsx

@ -5,7 +5,8 @@ import { formatNumber } from '@/utils/format';
import type { DataItem } from '../data.d';
import useStyles from '../style.style';
import Yuan from '../utils/Yuan';
import { ChartCard, Field } from './Charts';
import ChartCard from './Charts/ChartCard';
import Field from './Charts/Field';
import Trend from './Trend';
const topColResponsiveProps = {

10
src/pages/dashboard/analysis/components/NumberInfo/index.tsx

@ -5,6 +5,7 @@ import useStyles from './index.style';
export type NumberInfoProps = {
title?: React.ReactNode | string;
subTitle?: React.ReactNode | string;
renderSubTitle?: () => React.ReactNode;
total?: React.ReactNode | string;
status?: 'up' | 'down';
theme?: string;
@ -17,6 +18,7 @@ const NumberInfo: React.FC<NumberInfoProps> = ({
theme,
title,
subTitle,
renderSubTitle,
total,
subTotal,
status,
@ -25,6 +27,8 @@ const NumberInfo: React.FC<NumberInfoProps> = ({
...rest
}) => {
const { styles } = useStyles();
const subTitleNode = renderSubTitle?.() ?? subTitle;
const hasSubTitle = subTitleNode !== null && subTitleNode !== undefined;
return (
<div
className={clsx({
@ -40,12 +44,12 @@ const NumberInfo: React.FC<NumberInfoProps> = ({
{title}
</div>
)}
{subTitle && (
{hasSubTitle && (
<div
className={styles.numberInfoSubTitle}
title={typeof subTitle === 'string' ? subTitle : ''}
title={typeof subTitleNode === 'string' ? subTitleNode : ''}
>
{subTitle}
{subTitleNode}
</div>
)}
<div

38
src/pages/dashboard/analysis/components/ProportionSales.tsx

@ -7,19 +7,36 @@ import useStyles from '../style.style';
const { Text } = Typography;
const ProportionSales = ({
dropdownGroup,
renderDropdownGroup,
salesType,
loading,
salesPieData,
handleChangeSalesType,
}: {
loading: boolean;
dropdownGroup: React.ReactNode;
renderDropdownGroup: () => React.ReactNode;
salesType: 'all' | 'online' | 'stores';
salesPieData: DataItem[];
handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void;
}) => {
const { styles } = useStyles();
const dropdownGroup = renderDropdownGroup();
const extra = (
<div className={styles.salesCardExtra}>
{dropdownGroup}
<Segmented
className={styles.salesTypeRadio}
value={salesType}
onChange={handleChangeSalesType}
options={[
{ label: '全部渠道', value: 'all' },
{ label: '线上', value: 'online' },
{ label: '门店', value: 'stores' },
]}
size="middle"
/>
</div>
);
return (
<Card
loading={loading}
@ -29,22 +46,7 @@ const ProportionSales = ({
style={{
height: '100%',
}}
extra={
<div className={styles.salesCardExtra}>
{dropdownGroup}
<Segmented
className={styles.salesTypeRadio}
value={salesType}
onChange={handleChangeSalesType}
options={[
{ label: '全部渠道', value: 'all' },
{ label: '线上', value: 'online' },
{ label: '门店', value: 'stores' },
]}
size="middle"
/>
</div>
}
extra={extra}
>
<Text>销售额</Text>
<Pie

57
src/pages/dashboard/analysis/components/TopSearch.tsx

@ -7,17 +7,44 @@ import type { DataItem } from '../data.d';
import NumberInfo from './NumberInfo';
import Trend from './Trend';
const renderSearchUserSubtitle = () => (
<span>
搜索用户数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
);
const renderAverageSearchSubtitle = () => (
<span>
人均搜索次数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
);
const TopSearch = ({
loading,
visitData2,
searchData,
dropdownGroup,
renderDropdownGroup,
}: {
loading: boolean;
visitData2: DataItem[];
dropdownGroup: React.ReactNode;
renderDropdownGroup: () => React.ReactNode;
searchData: DataItem[];
}) => {
const dropdownGroup = renderDropdownGroup();
const columns = [
{
title: '排名',
@ -92,18 +119,7 @@ const TopSearch = ({
}}
>
<NumberInfo
subTitle={
<span>
搜索用户数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
renderSubTitle={renderSearchUserSubtitle}
gap={8}
total={formatNumber(12321)}
status="up"
@ -131,18 +147,7 @@ const TopSearch = ({
}}
>
<NumberInfo
subTitle={
<span>
人均搜索次数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
renderSubTitle={renderAverageSearchSubtitle}
total={2.7}
status="down"
subTotal={26.2}

8
src/pages/dashboard/analysis/index.tsx

@ -29,7 +29,7 @@ const Analysis: FC<AnalysisProps> = () => {
const [salesType, setSalesType] = useState<SalesType>('all');
const [currentTabKey, setCurrentTabKey] = useState<string>('');
const [rangePickerValue, setRangePickerValue] = useState<RangePickerValue>(
getTimeDistance('year'),
() => getTimeDistance('year'),
);
const { isLoading: loading, data } = useQuery({
queryKey: ['dashboard-analysis'],
@ -71,7 +71,7 @@ const Analysis: FC<AnalysisProps> = () => {
: data?.salesTypeDataOffline;
}
const dropdownGroup = (
const renderDropdownGroup = () => (
<span className={styles.iconGroup}>
<Dropdown
menu={{
@ -128,14 +128,14 @@ const Analysis: FC<AnalysisProps> = () => {
loading={loading}
visitData2={data?.visitData2 || []}
searchData={data?.searchData || []}
dropdownGroup={dropdownGroup}
renderDropdownGroup={renderDropdownGroup}
/>
</Suspense>
</Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}>
<ProportionSales
dropdownGroup={dropdownGroup}
renderDropdownGroup={renderDropdownGroup}
salesType={salesType}
loading={loading}
salesPieData={salesPieData || []}

14
src/pages/dashboard/analysis/utils/Yuan.tsx

@ -1,17 +1,7 @@
import { useEffect, useRef } from 'react';
import { yuan } from '../components/Charts';
import { formatYuan } from '@/utils/format';
/** 减少使用 dangerouslySetInnerHTML */
const Yuan: React.FC<{ children: string | number }> = ({ children }) => {
const spanRef = useRef<HTMLSpanElement>(null);
useEffect(() => {
if (spanRef.current) {
spanRef.current.innerHTML = yuan(children);
}
}, [children]);
return <span ref={spanRef} />;
return <span>{formatYuan(children)}</span>;
};
export default Yuan;

2
src/pages/dashboard/analysis/utils/utils.ts

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
type RangePickerValue = RangePickerProps['value'];
export function fixedZero(val: number) {
function fixedZero(val: number) {
return val * 1 < 10 ? `0${val}` : val;
}

7
src/pages/dashboard/monitor/components/ActiveChart/index.tsx

@ -20,14 +20,17 @@ function getActiveData() {
const ActiveChart = () => {
const timerRef = useRef<number | null>(null);
const { styles } = useStyles();
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]);
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>(() =>
getActiveData(),
);
useEffect(() => {
const loopData = () => {
setActiveData(getActiveData());
timerRef.current = window.setTimeout(loopData, 2000);
};
loopData();
// Initial data is already created lazily; start the refresh cadence after one interval.
timerRef.current = window.setTimeout(loopData, 2000);
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);

6
src/pages/dashboard/workplace/index.tsx

@ -118,7 +118,7 @@ const Workplace: FC = () => {
avatar={<Avatar src={item.user.avatar} />}
title={
<span>
<a className={styles.username} href="#">
<a className={styles.username} href={item.user.link || '/'}>
{item.user.name}
</a>
&nbsp;
@ -281,12 +281,12 @@ const Workplace: FC = () => {
{projectNotice.map((item) => {
return (
<Col span={12} key={`members-item-${item.id}`}>
<a href="#">
<Link to={item.memberLink || '/'} prefetch>
<Avatar src={item.logo} size="small" />
<span className={styles.member}>
{item.member.substring(0, 3)}
</span>
</a>
</Link>
</Col>
);
})}

13
src/pages/form/advanced-form/index.tsx

@ -10,7 +10,7 @@ import {
ProFormText,
ProFormTimePicker,
} from '@ant-design/pro-components';
import { Card, Col, message, Popover, Row } from 'antd';
import { Button, Card, Col, message, Popover, Row } from 'antd';
import type { FC } from 'react';
import { useRef, useState } from 'react';
import { fakeSubmitForm } from './service';
@ -161,16 +161,13 @@ const AdvancedForm: FC<Record<string, any>> = () => {
valueType: 'option',
render: (_, record: TableFormDateType, _index, action) => {
return [
<a
<Button
key="eidit"
href="#"
onClick={(e) => {
e.preventDefault();
action?.startEditable(record.key);
}}
type="link"
onClick={() => action?.startEditable(record.key)}
>
编辑
</a>,
</Button>,
];
},
},

16
src/pages/form/basic-form/index.test.tsx

@ -12,16 +12,12 @@ vi.mock('@ant-design/pro-components', () => ({
</div>
),
ProForm: ({ children, onFinish }: any) => (
<form
data-testid="pro-form"
onSubmit={(e) => {
e.preventDefault();
onFinish?.();
}}
>
<div data-testid="pro-form">
{children}
<button type="submit">提交</button>
</form>
<button type="button" onClick={() => onFinish?.({})}>
提交
</button>
</div>
),
ProFormText: ({ label, name }: any) => (
<div data-testid={`form-text-${name}`}>{label}</div>
@ -145,7 +141,7 @@ describe('BasicForm', () => {
submitButton.click();
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalled();
expect(mockSubmit.mock.calls[0]?.[0]).toEqual({});
});
});

15
src/pages/list/basic-list/index.tsx

@ -171,9 +171,9 @@ const BasicList: FC = () => {
],
}}
>
<a href="#">
<Button type="link">
更多 <DownOutlined />
</a>
</Button>
</Dropdown>
);
};
@ -229,16 +229,13 @@ const BasicList: FC = () => {
renderItem={(item) => (
<List.Item
actions={[
<a
<Button
key="edit"
href="#"
onClick={(e) => {
e.preventDefault();
showEditModal(item);
}}
type="link"
onClick={() => showEditModal(item)}
>
编辑
</a>,
</Button>,
renderMoreBtn(item),
]}
>

19
src/pages/list/card-list/index.tsx

@ -7,6 +7,8 @@ import { queryFakeList } from './service';
import useStyles from './style.style';
const { Paragraph } = Typography;
const nullData: Partial<CardListItemDataType> = {};
const CardList = () => {
const { styles } = useStyles();
const { data, isLoading: loading } = useQuery({
@ -22,21 +24,21 @@ const CardList = () => {
提供跨越设计与开发的体验解决方案。
</p>
<div className={styles.contentLink}>
<a href="#">
<a href="https://pro.ant.design/zh-CN/docs/getting-started">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg"
/>{' '}
快速开始
</a>
<a href="#">
<a href="https://pro.ant.design/zh-CN/docs/introduction">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg"
/>{' '}
产品简介
</a>
<a href="#">
<a href="https://pro.ant.design/zh-CN/docs/overview">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg"
@ -54,7 +56,6 @@ const CardList = () => {
/>
</div>
);
const nullData: Partial<CardListItemDataType> = {};
return (
<PageContainer content={content} extraContent={extraContent}>
<div className={styles.cardList}>
@ -79,12 +80,12 @@ const CardList = () => {
hoverable
className={styles.card}
actions={[
<a key="option1" href="#">
<Button key="option1" type="link">
操作一
</a>,
<a key="option2" href="#">
</Button>,
<Button key="option2" type="link">
操作二
</a>,
</Button>,
]}
>
<Card.Meta
@ -95,7 +96,7 @@ const CardList = () => {
src={item.avatar}
/>
}
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={
<Paragraph
className={styles.item}

90
src/pages/list/search/articles/index.tsx

@ -18,6 +18,35 @@ import useStyles from './style.style';
const FormItem = Form.Item;
const pageSize = 5;
const owners = [
{
id: 'wzj',
name: '我自己',
},
{
id: 'wjh',
name: '吴家豪',
},
{
id: 'zxx',
name: '周星星',
},
{
id: 'zly',
name: '赵丽颖',
},
{
id: 'ym',
name: '姚明',
},
];
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 24 },
md: { span: 12 },
},
};
const IconText: React.FC<{
type: string;
@ -81,42 +110,18 @@ const Articles: FC = () => {
};
const list = data?.list || [];
const listItemExtra = <div className={styles.listItemExtra} />;
const setOwner = () => {
const owner = ['wzj'];
form.setFieldsValue({
owner: ['wzj'],
owner,
});
};
const owners = [
{
id: 'wzj',
name: '我自己',
},
{
id: 'wjh',
name: '吴家豪',
},
{
id: 'zxx',
name: '周星星',
},
{
id: 'zly',
name: '赵丽颖',
},
{
id: 'ym',
name: '姚明',
},
];
const formItemLayout = {
wrapperCol: {
xs: { span: 24 },
sm: { span: 24 },
md: { span: 12 },
},
filtersRef.current = {
...filtersRef.current,
owner,
};
refetch();
};
const loadMoreDom = list.length > 0 && (
@ -180,22 +185,13 @@ const Articles: FC = () => {
options={ownerOptions}
/>
</FormItem>
<a
<Button
type="link"
className={styles.selfTrigger}
href="#"
onClick={(e) => {
e.preventDefault();
setOwner();
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setOwner();
}
}}
onClick={setOwner}
>
只看自己的
</a>
</Button>
</StandardFormRow>
<StandardFormRow title="其它选项" grid last>
<Row gutter={16}>
@ -245,7 +241,6 @@ const Articles: FC = () => {
loading={loading}
rowKey="id"
itemLayout="vertical"
loadMore={loadMoreDom}
dataSource={list}
renderItem={(item) => (
<List.Item
@ -255,7 +250,7 @@ const Articles: FC = () => {
<IconText key="like" type="like-o" text={item.like} />,
<IconText key="message" type="message" text={item.message} />,
]}
extra={<div className={styles.listItemExtra} />}
extra={listItemExtra}
>
<List.Item.Meta
title={
@ -275,6 +270,7 @@ const Articles: FC = () => {
</List.Item>
)}
/>
{loadMoreDom}
</Card>
</>
);

23
src/pages/list/search/projects/index.tsx

@ -12,6 +12,17 @@ import useStyles from './style.style';
const FormItem = Form.Item;
const { Paragraph } = Typography;
const getKey = (id: string, index: number) => `${id}-${index}`;
const formItemLayout = {
wrapperCol: {
xs: {
span: 24,
},
sm: {
span: 16,
},
},
};
const Projects: FC = () => {
const { styles } = useStyles();
const [filters, setFilters] = useState<{
@ -50,7 +61,7 @@ const Projects: FC = () => {
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a href="#">{item.title}</a>}
title={<a href={item.href}>{item.title}</a>}
description={
<Paragraph
ellipsis={{
@ -80,16 +91,6 @@ const Projects: FC = () => {
)}
/>
);
const formItemLayout = {
wrapperCol: {
xs: {
span: 24,
},
sm: {
span: 16,
},
},
};
return (
<div className={styles.coverCardList}>
<Card variant="borderless">

6
src/pages/profile/advanced/index.tsx

@ -150,7 +150,7 @@ const descriptionItems: DescriptionsProps['items'] = [
{ key: '1', label: '创建人', children: '曲丽丽' },
{ key: '2', label: '订购产品', children: 'XX 服务' },
{ key: '3', label: '创建时间', children: '2017-07-07' },
{ key: '4', label: '关联单据', children: <a href="#">12421</a> },
{ key: '4', label: '关联单据', children: <a href="/">12421</a> },
{ key: '5', label: '生效日期', children: '2017-07-07 ~ 2017-08-08' },
{ key: '6', label: '备注', children: '请于两个工作日内确认' },
];
@ -305,7 +305,9 @@ const Advanced: FC = () => {
}}
/>
<div>
<a href="#">催一下</a>
<Button type="link" style={{ padding: 0 }}>
催一下
</Button>
</div>
</div>
);

14
src/pages/result/fail/index.tsx

@ -22,15 +22,16 @@ export default () => {
className={styles.error_icon}
/>
<span>您的账户已被冻结</span>
<a
href="#"
<Button
type="link"
style={{
marginLeft: 16,
padding: 0,
}}
>
<span>立即解冻</span>
<RightOutlined />
</a>
</Button>
</div>
<div>
<CloseCircleOutlined
@ -40,15 +41,16 @@ export default () => {
className={styles.error_icon}
/>
<span>您的账户还不具备申请资格</span>
<a
href="#"
<Button
type="link"
style={{
marginLeft: 16,
padding: 0,
}}
>
<span>立即升级</span>
<RightOutlined />
</a>
</Button>
</div>
</>
);

4
src/pages/result/success/index.tsx

@ -51,7 +51,7 @@ const Success: React.FC = () => {
}}
>
<span>周毛毛</span>
<a href="#">
<Button type="link" style={{ padding: 0 }}>
<DingdingOutlined
style={{
color: '#00A0E9',
@ -59,7 +59,7 @@ const Success: React.FC = () => {
}}
/>
<span>催一下</span>
</a>
</Button>
</div>
</div>
);

13
src/pages/table-list/index.tsx

@ -58,16 +58,15 @@ const TableList: React.FC = () => {
dataIndex: 'name',
render: (dom, entity) => {
return (
<a
href="#"
onClick={(e) => {
e.preventDefault();
<Button
type="link"
onClick={() => {
setCurrentRow(entity);
setShowDetail(true);
}}
>
{dom}
</a>
</Button>
);
},
},
@ -197,12 +196,12 @@ const TableList: React.FC = () => {
render: (_, record) => [
<UpdateForm
trigger={
<a href="#">
<Button type="link">
<FormattedMessage
id="pages.searchTable.config"
defaultMessage="Configuration"
/>
</a>
</Button>
}
key="config"
onOk={actionRef.current?.reload}

48
src/pages/user/login/index.tsx

@ -19,7 +19,7 @@ import {
useIntl,
useModel,
} from '@umijs/max';
import { Alert, App, Tabs } from 'antd';
import { Alert, App, Button, Tabs } from 'antd';
import { createStyles } from 'antd-style';
import React, { startTransition, useState } from 'react';
import { Footer } from '@/components';
@ -27,6 +27,24 @@ import { login } from '@/services/ant-design-pro/api';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
import Settings from '../../../../config/defaultSettings';
/**
* Validate redirect URL to prevent open redirect attacks.
* Only allow same-origin relative paths starting with '/'.
*/
const getSafeRedirectUrl = (redirect: string | null): string => {
if (!redirect?.startsWith('/')) return '/';
if (redirect.startsWith('//')) return '/';
try {
const parsed = new URL(redirect, window.location.origin);
if (parsed.origin !== window.location.origin) return '/';
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
} catch {
return '/';
}
};
const useStyles = createStyles(({ token }) => {
return {
action: {
@ -117,27 +135,6 @@ const Login: React.FC = () => {
const { message } = App.useApp();
const intl = useIntl();
/**
* Validate redirect URL to prevent open redirect attacks
* Only allow same-origin relative paths starting with '/'
*/
const getSafeRedirectUrl = (redirect: string | null): string => {
if (!redirect?.startsWith('/')) return '/';
// Block protocol-relative URLs (//example.com)
if (redirect.startsWith('//')) return '/';
try {
const parsed = new URL(redirect, window.location.origin);
// Only allow same-origin URLs
if (parsed.origin !== window.location.origin) return '/';
// Return the path with query and hash preserved
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
} catch {
return '/';
}
};
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
@ -395,17 +392,18 @@ const Login: React.FC = () => {
defaultMessage="自动登录"
/>
</ProFormCheckbox>
<a
href="#"
<Button
type="link"
style={{
float: 'right',
padding: 0,
}}
>
<FormattedMessage
id="pages.login.forgotPassword"
defaultMessage="忘记密码"
/>
</a>
</Button>
</div>
</LoginForm>
</div>

8
src/pages/user/register-result/index.tsx

@ -9,11 +9,9 @@ const RegisterResult: React.FC<Record<string, unknown>> = () => {
const actions = (
<div className={styles.actions}>
<a href="#">
<Button size="large" type="primary">
<span>查看邮箱</span>
</Button>
</a>
<Button size="large" type="primary">
<span>查看邮箱</span>
</Button>
<Link to="/" prefetch>
<Button size="large">返回首页</Button>
</Link>

64
src/pages/user/register/index.tsx

@ -1,4 +1,4 @@
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { history, Link } from '@umijs/max';
import {
Button,
@ -30,6 +30,34 @@ const passwordProgressMap: {
pass: 'normal',
poor: 'exception',
};
const getPasswordStatus = (value?: string) => {
if (value && value.length > 9) {
return 'ok';
}
if (value && value.length > 5) {
return 'pass';
}
return 'poor';
};
const PasswordProgress: React.FC<{
value?: string;
styles: Record<string, string>;
}> = ({ value, styles }) => {
const passwordStatus = getPasswordStatus(value);
return value?.length ? (
<div className={styles[`progress-${passwordStatus}`]}>
<Progress
status={passwordProgressMap[passwordStatus]}
size={6}
percent={value.length * 10 > 100 ? 100 : value.length * 10}
showInfo={false}
/>
</div>
) : null;
};
const Register: FC = () => {
const { styles } = useStyles();
const [count, setCount]: [number, any] = useState(0);
@ -38,6 +66,7 @@ const Register: FC = () => {
const [popover, setPopover]: [boolean, any] = useState(false);
const confirmDirty = false;
let interval: number | undefined;
const queryClient = useQueryClient();
const passwordStatusMap = {
ok: (
@ -75,16 +104,6 @@ const Register: FC = () => {
}
}, 1000);
};
const getPasswordStatus = () => {
const value = form.getFieldValue('password');
if (value && value.length > 9) {
return 'ok';
}
if (value && value.length > 5) {
return 'pass';
}
return 'poor';
};
const { isPending: submitting, mutate: register } = useMutation({
mutationFn: (formValues: Store) => {
const payload = {
@ -99,6 +118,7 @@ const Register: FC = () => {
},
onSuccess: (data, params) => {
if (data.status === 'ok') {
queryClient.invalidateQueries({ queryKey: ['current-user'] });
message.success('注册成功!');
history.push({
pathname: `/user/register-result?account=${params.mail}`,
@ -139,22 +159,8 @@ const Register: FC = () => {
const changePrefix = (value: string) => {
setPrefix(value);
};
const renderPasswordProgress = () => {
const value = form.getFieldValue('password');
const passwordStatus = getPasswordStatus();
return value?.length ? (
<div
className={styles[`progress-${passwordStatus}` as keyof typeof styles]}
>
<Progress
status={passwordProgressMap[passwordStatus]}
size={6}
percent={value.length * 10 > 100 ? 100 : value.length * 10}
showInfo={false}
/>
</div>
) : null;
};
const password = Form.useWatch('password', form);
const passwordStatus = getPasswordStatus(password);
return (
<div className={styles.main}>
<h3>注册</h3>
@ -188,8 +194,8 @@ const Register: FC = () => {
padding: '4px 0',
}}
>
{passwordStatusMap[getPasswordStatus()]}
{renderPasswordProgress()}
{passwordStatusMap[passwordStatus]}
<PasswordProgress value={password} styles={styles} />
<div
style={{
marginTop: 10,

Loading…
Cancel
Save