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", "jsdom": "^29.1.1",
"lint-staged": "^17.0.2", "lint-staged": "^17.0.2",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"react-doctor": "^0.5.0", "react-doctor": "^0.5.8",
"swagger-ui-dist": "^5.32.5", "swagger-ui-dist": "^5.32.5",
"tailwindcss": "^4.3.0", "tailwindcss": "^4.3.0",
"ts-node": "^10.9.2", "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', [styles.avatarItemMini]: size === 'mini',
}); });
const Item: React.FC<AvatarItemProps> = ({ const Item: React.FC<AvatarItemProps> = ({ src, size, tips, onClick }) => {
src,
size,
tips,
onClick = () => {},
}) => {
const { styles } = useStyles(); const { styles } = useStyles();
const cls = avatarSizeToClassName(styles, size); 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 ( return (
<li <li className={cls}>
className={cls} {onClick ? (
onClick={onClick} <button type="button" className={styles.avatarButton} onClick={onClick}>
onKeyDown={ {avatar}
onClick </button>
? (e) => {
if (e.key === 'Enter') onClick();
}
: undefined
}
>
{tips ? (
<Tooltip title={tips}>
<Avatar
src={src}
size={size}
style={{
cursor: 'pointer',
}}
/>
</Tooltip>
) : ( ) : (
<Avatar src={src} size={size} /> avatar
)} )}
</li> </li>
); );

42
src/components/OfflineBanner/index.tsx

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

77
src/components/RightContent/AvatarDropdown.tsx

@ -14,24 +14,50 @@ type GlobalHeaderRightProps = {
children?: React.ReactNode; 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> = ({ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
children, 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 { initialState, setInitialState } = useModel('@@initialState');
const onMenuClick: MenuProps['onClick'] = (event) => { const onMenuClick: MenuProps['onClick'] = (event) => {
@ -60,27 +86,6 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
return <Spin size="small" />; 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 ( return (
<HeaderDropdown <HeaderDropdown
placement="bottomRight" 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 { import { BookOutlined } from '@ant-design/icons';
BookOutlined, import { history } from '@umijs/max';
CheckOutlined,
ForkOutlined,
GlobalOutlined,
} from '@ant-design/icons';
import { getAllLocales, getLocale, history, setLocale } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Button, Tooltip } from 'antd'; import { Button, Tooltip } from 'antd';
import { createStyles } from 'antd-style'; import React from 'react';
import React, { useMemo } from 'react'; import { LangDropdown } from './LangDropdown';
import HeaderDropdown from '../HeaderDropdown'; import useHeaderActionStyles from './style';
import { VersionDropdown } from './VersionDropdown';
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;
`,
}));
export const DocLink: React.FC = () => { export const DocLink: React.FC = () => {
const { styles } = useStyles(); const { styles } = useHeaderActionStyles();
return ( return (
<Tooltip title="使用文档"> <Tooltip title="使用文档">
<Button <Button
@ -53,78 +23,4 @@ export const DocLink: React.FC = () => {
); );
}; };
const versionItems: MenuProps['items'] = [ export { LangDropdown, VersionDropdown };
{ 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>
);
};

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

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

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

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

@ -3,47 +3,47 @@ import {
DingdingOutlined, DingdingOutlined,
TaobaoOutlined, TaobaoOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { List } from 'antd'; import { Button, List } from 'antd';
import React from 'react'; import React from 'react';
const BindingView: React.FC = () => { const bindingData = [
const getData = () => [ {
{ title: '绑定淘宝',
title: '绑定淘宝', description: '当前未绑定淘宝账号',
description: '当前未绑定淘宝账号', actions: [
actions: [ <Button key="Bind" type="link">
<a key="Bind" href="#"> 绑定
绑定 </Button>,
</a>, ],
], avatar: <TaobaoOutlined className="taobao" />,
avatar: <TaobaoOutlined className="taobao" />, },
}, {
{ title: '绑定支付宝',
title: '绑定支付宝', description: '当前未绑定支付宝账号',
description: '当前未绑定支付宝账号', actions: [
actions: [ <Button key="Bind" type="link">
<a key="Bind" href="#"> 绑定
绑定 </Button>,
</a>, ],
], avatar: <AlipayOutlined className="alipay" />,
avatar: <AlipayOutlined className="alipay" />, },
}, {
{ title: '绑定钉钉',
title: '绑定钉钉', description: '当前未绑定钉钉账号',
description: '当前未绑定钉钉账号', actions: [
actions: [ <Button key="Bind" type="link">
<a key="Bind" href="#"> 绑定
绑定 </Button>,
</a>, ],
], avatar: <DingdingOutlined className="dingding" />,
avatar: <DingdingOutlined className="dingding" />, },
}, ];
];
const BindingView: React.FC = () => {
return ( return (
<List <List
itemLayout="horizontal" itemLayout="horizontal"
dataSource={getData()} dataSource={bindingData}
renderItem={(item) => ( renderItem={(item) => (
<List.Item actions={item.actions}> <List.Item actions={item.actions}>
<List.Item.Meta <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; type Unpacked<T> = T extends (infer U)[] ? U : T;
const Action = ( const notificationAction = (key: string) => (
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked /> <Switch
key={key}
checkedChildren="开"
unCheckedChildren="关"
defaultChecked
/>
); );
const NotificationView: React.FC = () => { const notificationData = [
const getData = () => { {
return [ key: 'user-message',
{ title: '用户消息',
title: '用户消息', description: '其他用户的消息将以站内信的形式通知',
description: '其他用户的消息将以站内信的形式通知', actions: [notificationAction('user-message-switch')],
actions: [Action], },
}, {
{ key: 'system-message',
title: '系统消息', title: '系统消息',
description: '系统消息将以站内信的形式通知', description: '系统消息将以站内信的形式通知',
actions: [Action], actions: [notificationAction('system-message-switch')],
}, },
{ {
title: '待办任务', key: 'todo-task',
description: '待办任务将以站内信的形式通知', title: '待办任务',
actions: [Action], description: '待办任务将以站内信的形式通知',
}, actions: [notificationAction('todo-task-switch')],
]; },
}; ];
const data = getData(); const NotificationView: React.FC = () => {
const data = notificationData;
return ( return (
<List<Unpacked<typeof data>> <List<Unpacked<typeof data>>
rowKey="key"
itemLayout="horizontal" itemLayout="horizontal"
dataSource={data} dataSource={data}
renderItem={(item) => ( 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'; import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T; type Unpacked<T> = T extends (infer U)[] ? U : T;
@ -9,61 +9,61 @@ const passwordStrength = {
weak: <span className="weak">弱 Weak</span>, weak: <span className="weak">弱 Weak</span>,
}; };
const SecurityView: React.FC = () => { const securityData = [
const getData = () => [ {
{ title: '账户密码',
title: '账户密码', description: (
description: ( <>
<> 当前密码强度:
当前密码强度: {passwordStrength.strong}
{passwordStrength.strong} </>
</> ),
), actions: [
actions: [ <Button key="Modify" type="link">
<a key="Modify" href="#"> 修改
修改 </Button>,
</a>, ],
], },
}, {
{ title: '密保手机',
title: '密保手机', description: `已绑定手机:138****8293`,
description: `已绑定手机:138****8293`, actions: [
actions: [ <Button key="Modify" type="link">
<a key="Modify" href="#"> 修改
修改 </Button>,
</a>, ],
], },
}, {
{ title: '密保问题',
title: '密保问题', description: '未设置密保问题,密保问题可有效保护账户安全',
description: '未设置密保问题,密保问题可有效保护账户安全', actions: [
actions: [ <Button key="Set" type="link">
<a key="Set" href="#"> 设置
设置 </Button>,
</a>, ],
], },
}, {
{ title: '备用邮箱',
title: '备用邮箱', description: `已绑定邮箱:ant***sign.com`,
description: `已绑定邮箱:ant***sign.com`, actions: [
actions: [ <Button key="Modify" type="link">
<a key="Modify" href="#"> 修改
修改 </Button>,
</a>, ],
], },
}, {
{ title: 'MFA 设备',
title: 'MFA 设备', description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认', actions: [
actions: [ <Button key="bind" type="link">
<a key="bind" href="#"> 绑定
绑定 </Button>,
</a>, ],
], },
}, ];
];
const data = getData(); const SecurityView: React.FC = () => {
const data = securityData;
return ( return (
<List<Unpacked<typeof data>> <List<Unpacked<typeof data>>
itemLayout="horizontal" itemLayout="horizontal"

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

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

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

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

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

@ -7,19 +7,36 @@ import useStyles from '../style.style';
const { Text } = Typography; const { Text } = Typography;
const ProportionSales = ({ const ProportionSales = ({
dropdownGroup, renderDropdownGroup,
salesType, salesType,
loading, loading,
salesPieData, salesPieData,
handleChangeSalesType, handleChangeSalesType,
}: { }: {
loading: boolean; loading: boolean;
dropdownGroup: React.ReactNode; renderDropdownGroup: () => React.ReactNode;
salesType: 'all' | 'online' | 'stores'; salesType: 'all' | 'online' | 'stores';
salesPieData: DataItem[]; salesPieData: DataItem[];
handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void; handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void;
}) => { }) => {
const { styles } = useStyles(); 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 ( return (
<Card <Card
loading={loading} loading={loading}
@ -29,22 +46,7 @@ const ProportionSales = ({
style={{ style={{
height: '100%', height: '100%',
}} }}
extra={ extra={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>
}
> >
<Text>销售额</Text> <Text>销售额</Text>
<Pie <Pie

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

@ -7,17 +7,44 @@ import type { DataItem } from '../data.d';
import NumberInfo from './NumberInfo'; import NumberInfo from './NumberInfo';
import Trend from './Trend'; 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 = ({ const TopSearch = ({
loading, loading,
visitData2, visitData2,
searchData, searchData,
dropdownGroup, renderDropdownGroup,
}: { }: {
loading: boolean; loading: boolean;
visitData2: DataItem[]; visitData2: DataItem[];
dropdownGroup: React.ReactNode; renderDropdownGroup: () => React.ReactNode;
searchData: DataItem[]; searchData: DataItem[];
}) => { }) => {
const dropdownGroup = renderDropdownGroup();
const columns = [ const columns = [
{ {
title: '排名', title: '排名',
@ -92,18 +119,7 @@ const TopSearch = ({
}} }}
> >
<NumberInfo <NumberInfo
subTitle={ renderSubTitle={renderSearchUserSubtitle}
<span>
搜索用户数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
gap={8} gap={8}
total={formatNumber(12321)} total={formatNumber(12321)}
status="up" status="up"
@ -131,18 +147,7 @@ const TopSearch = ({
}} }}
> >
<NumberInfo <NumberInfo
subTitle={ renderSubTitle={renderAverageSearchSubtitle}
<span>
人均搜索次数
<Tooltip title="指标说明">
<InfoCircleOutlined
style={{
marginLeft: 8,
}}
/>
</Tooltip>
</span>
}
total={2.7} total={2.7}
status="down" status="down"
subTotal={26.2} 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 [salesType, setSalesType] = useState<SalesType>('all');
const [currentTabKey, setCurrentTabKey] = useState<string>(''); const [currentTabKey, setCurrentTabKey] = useState<string>('');
const [rangePickerValue, setRangePickerValue] = useState<RangePickerValue>( const [rangePickerValue, setRangePickerValue] = useState<RangePickerValue>(
getTimeDistance('year'), () => getTimeDistance('year'),
); );
const { isLoading: loading, data } = useQuery({ const { isLoading: loading, data } = useQuery({
queryKey: ['dashboard-analysis'], queryKey: ['dashboard-analysis'],
@ -71,7 +71,7 @@ const Analysis: FC<AnalysisProps> = () => {
: data?.salesTypeDataOffline; : data?.salesTypeDataOffline;
} }
const dropdownGroup = ( const renderDropdownGroup = () => (
<span className={styles.iconGroup}> <span className={styles.iconGroup}>
<Dropdown <Dropdown
menu={{ menu={{
@ -128,14 +128,14 @@ const Analysis: FC<AnalysisProps> = () => {
loading={loading} loading={loading}
visitData2={data?.visitData2 || []} visitData2={data?.visitData2 || []}
searchData={data?.searchData || []} searchData={data?.searchData || []}
dropdownGroup={dropdownGroup} renderDropdownGroup={renderDropdownGroup}
/> />
</Suspense> </Suspense>
</Col> </Col>
<Col xl={12} lg={24} md={24} sm={24} xs={24}> <Col xl={12} lg={24} md={24} sm={24} xs={24}>
<Suspense fallback={null}> <Suspense fallback={null}>
<ProportionSales <ProportionSales
dropdownGroup={dropdownGroup} renderDropdownGroup={renderDropdownGroup}
salesType={salesType} salesType={salesType}
loading={loading} loading={loading}
salesPieData={salesPieData || []} salesPieData={salesPieData || []}

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

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

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

@ -3,7 +3,7 @@ import dayjs from 'dayjs';
type RangePickerValue = RangePickerProps['value']; type RangePickerValue = RangePickerProps['value'];
export function fixedZero(val: number) { function fixedZero(val: number) {
return val * 1 < 10 ? `0${val}` : val; 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 ActiveChart = () => {
const timerRef = useRef<number | null>(null); const timerRef = useRef<number | null>(null);
const { styles } = useStyles(); const { styles } = useStyles();
const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]); const [activeData, setActiveData] = useState<{ x: string; y: number }[]>(() =>
getActiveData(),
);
useEffect(() => { useEffect(() => {
const loopData = () => { const loopData = () => {
setActiveData(getActiveData()); setActiveData(getActiveData());
timerRef.current = window.setTimeout(loopData, 2000); 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 () => { return () => {
if (timerRef.current) { if (timerRef.current) {
clearTimeout(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} />} avatar={<Avatar src={item.user.avatar} />}
title={ title={
<span> <span>
<a className={styles.username} href="#"> <a className={styles.username} href={item.user.link || '/'}>
{item.user.name} {item.user.name}
</a> </a>
&nbsp; &nbsp;
@ -281,12 +281,12 @@ const Workplace: FC = () => {
{projectNotice.map((item) => { {projectNotice.map((item) => {
return ( return (
<Col span={12} key={`members-item-${item.id}`}> <Col span={12} key={`members-item-${item.id}`}>
<a href="#"> <Link to={item.memberLink || '/'} prefetch>
<Avatar src={item.logo} size="small" /> <Avatar src={item.logo} size="small" />
<span className={styles.member}> <span className={styles.member}>
{item.member.substring(0, 3)} {item.member.substring(0, 3)}
</span> </span>
</a> </Link>
</Col> </Col>
); );
})} })}

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

@ -10,7 +10,7 @@ import {
ProFormText, ProFormText,
ProFormTimePicker, ProFormTimePicker,
} from '@ant-design/pro-components'; } 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 type { FC } from 'react';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { fakeSubmitForm } from './service'; import { fakeSubmitForm } from './service';
@ -161,16 +161,13 @@ const AdvancedForm: FC<Record<string, any>> = () => {
valueType: 'option', valueType: 'option',
render: (_, record: TableFormDateType, _index, action) => { render: (_, record: TableFormDateType, _index, action) => {
return [ return [
<a <Button
key="eidit" key="eidit"
href="#" type="link"
onClick={(e) => { onClick={() => action?.startEditable(record.key)}
e.preventDefault();
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> </div>
), ),
ProForm: ({ children, onFinish }: any) => ( ProForm: ({ children, onFinish }: any) => (
<form <div data-testid="pro-form">
data-testid="pro-form"
onSubmit={(e) => {
e.preventDefault();
onFinish?.();
}}
>
{children} {children}
<button type="submit">提交</button> <button type="button" onClick={() => onFinish?.({})}>
</form> 提交
</button>
</div>
), ),
ProFormText: ({ label, name }: any) => ( ProFormText: ({ label, name }: any) => (
<div data-testid={`form-text-${name}`}>{label}</div> <div data-testid={`form-text-${name}`}>{label}</div>
@ -145,7 +141,7 @@ describe('BasicForm', () => {
submitButton.click(); submitButton.click();
await waitFor(() => { 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 /> 更多 <DownOutlined />
</a> </Button>
</Dropdown> </Dropdown>
); );
}; };
@ -229,16 +229,13 @@ const BasicList: FC = () => {
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
actions={[ actions={[
<a <Button
key="edit" key="edit"
href="#" type="link"
onClick={(e) => { onClick={() => showEditModal(item)}
e.preventDefault();
showEditModal(item);
}}
> >
编辑 编辑
</a>, </Button>,
renderMoreBtn(item), renderMoreBtn(item),
]} ]}
> >

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

@ -7,6 +7,8 @@ import { queryFakeList } from './service';
import useStyles from './style.style'; import useStyles from './style.style';
const { Paragraph } = Typography; const { Paragraph } = Typography;
const nullData: Partial<CardListItemDataType> = {};
const CardList = () => { const CardList = () => {
const { styles } = useStyles(); const { styles } = useStyles();
const { data, isLoading: loading } = useQuery({ const { data, isLoading: loading } = useQuery({
@ -22,21 +24,21 @@ const CardList = () => {
提供跨越设计与开发的体验解决方案。 提供跨越设计与开发的体验解决方案。
</p> </p>
<div className={styles.contentLink}> <div className={styles.contentLink}>
<a href="#"> <a href="https://pro.ant.design/zh-CN/docs/getting-started">
<img <img
alt="" alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg" src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg"
/>{' '} />{' '}
快速开始 快速开始
</a> </a>
<a href="#"> <a href="https://pro.ant.design/zh-CN/docs/introduction">
<img <img
alt="" alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg" src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg"
/>{' '} />{' '}
产品简介 产品简介
</a> </a>
<a href="#"> <a href="https://pro.ant.design/zh-CN/docs/overview">
<img <img
alt="" alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg" src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg"
@ -54,7 +56,6 @@ const CardList = () => {
/> />
</div> </div>
); );
const nullData: Partial<CardListItemDataType> = {};
return ( return (
<PageContainer content={content} extraContent={extraContent}> <PageContainer content={content} extraContent={extraContent}>
<div className={styles.cardList}> <div className={styles.cardList}>
@ -79,12 +80,12 @@ const CardList = () => {
hoverable hoverable
className={styles.card} className={styles.card}
actions={[ actions={[
<a key="option1" href="#"> <Button key="option1" type="link">
操作一 操作一
</a>, </Button>,
<a key="option2" href="#"> <Button key="option2" type="link">
操作二 操作二
</a>, </Button>,
]} ]}
> >
<Card.Meta <Card.Meta
@ -95,7 +96,7 @@ const CardList = () => {
src={item.avatar} src={item.avatar}
/> />
} }
title={<a href="#">{item.title}</a>} title={<a href={item.href}>{item.title}</a>}
description={ description={
<Paragraph <Paragraph
className={styles.item} 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 FormItem = Form.Item;
const pageSize = 5; 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<{ const IconText: React.FC<{
type: string; type: string;
@ -81,42 +110,18 @@ const Articles: FC = () => {
}; };
const list = data?.list || []; const list = data?.list || [];
const listItemExtra = <div className={styles.listItemExtra} />;
const setOwner = () => { const setOwner = () => {
const owner = ['wzj'];
form.setFieldsValue({ form.setFieldsValue({
owner: ['wzj'], owner,
}); });
}; filtersRef.current = {
...filtersRef.current,
const owners = [ owner,
{ };
id: 'wzj', refetch();
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 loadMoreDom = list.length > 0 && ( const loadMoreDom = list.length > 0 && (
@ -180,22 +185,13 @@ const Articles: FC = () => {
options={ownerOptions} options={ownerOptions}
/> />
</FormItem> </FormItem>
<a <Button
type="link"
className={styles.selfTrigger} className={styles.selfTrigger}
href="#" onClick={setOwner}
onClick={(e) => {
e.preventDefault();
setOwner();
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setOwner();
}
}}
> >
只看自己的 只看自己的
</a> </Button>
</StandardFormRow> </StandardFormRow>
<StandardFormRow title="其它选项" grid last> <StandardFormRow title="其它选项" grid last>
<Row gutter={16}> <Row gutter={16}>
@ -245,7 +241,6 @@ const Articles: FC = () => {
loading={loading} loading={loading}
rowKey="id" rowKey="id"
itemLayout="vertical" itemLayout="vertical"
loadMore={loadMoreDom}
dataSource={list} dataSource={list}
renderItem={(item) => ( renderItem={(item) => (
<List.Item <List.Item
@ -255,7 +250,7 @@ const Articles: FC = () => {
<IconText key="like" type="like-o" text={item.like} />, <IconText key="like" type="like-o" text={item.like} />,
<IconText key="message" type="message" text={item.message} />, <IconText key="message" type="message" text={item.message} />,
]} ]}
extra={<div className={styles.listItemExtra} />} extra={listItemExtra}
> >
<List.Item.Meta <List.Item.Meta
title={ title={
@ -275,6 +270,7 @@ const Articles: FC = () => {
</List.Item> </List.Item>
)} )}
/> />
{loadMoreDom}
</Card> </Card>
</> </>
); );

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

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

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

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

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

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

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

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

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

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

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

@ -19,7 +19,7 @@ import {
useIntl, useIntl,
useModel, useModel,
} from '@umijs/max'; } from '@umijs/max';
import { Alert, App, Tabs } from 'antd'; import { Alert, App, Button, Tabs } from 'antd';
import { createStyles } from 'antd-style'; import { createStyles } from 'antd-style';
import React, { startTransition, useState } from 'react'; import React, { startTransition, useState } from 'react';
import { Footer } from '@/components'; 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 { getFakeCaptcha } from '@/services/ant-design-pro/login';
import Settings from '../../../../config/defaultSettings'; 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 }) => { const useStyles = createStyles(({ token }) => {
return { return {
action: { action: {
@ -117,27 +135,6 @@ const Login: React.FC = () => {
const { message } = App.useApp(); const { message } = App.useApp();
const intl = useIntl(); 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 fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.(); const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) { if (userInfo) {
@ -395,17 +392,18 @@ const Login: React.FC = () => {
defaultMessage="自动登录" defaultMessage="自动登录"
/> />
</ProFormCheckbox> </ProFormCheckbox>
<a <Button
href="#" type="link"
style={{ style={{
float: 'right', float: 'right',
padding: 0,
}} }}
> >
<FormattedMessage <FormattedMessage
id="pages.login.forgotPassword" id="pages.login.forgotPassword"
defaultMessage="忘记密码" defaultMessage="忘记密码"
/> />
</a> </Button>
</div> </div>
</LoginForm> </LoginForm>
</div> </div>

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

@ -9,11 +9,9 @@ const RegisterResult: React.FC<Record<string, unknown>> = () => {
const actions = ( const actions = (
<div className={styles.actions}> <div className={styles.actions}>
<a href="#"> <Button size="large" type="primary">
<Button size="large" type="primary"> <span>查看邮箱</span>
<span>查看邮箱</span> </Button>
</Button>
</a>
<Link to="/" prefetch> <Link to="/" prefetch>
<Button size="large">返回首页</Button> <Button size="large">返回首页</Button>
</Link> </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 { history, Link } from '@umijs/max';
import { import {
Button, Button,
@ -30,6 +30,34 @@ const passwordProgressMap: {
pass: 'normal', pass: 'normal',
poor: 'exception', 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 Register: FC = () => {
const { styles } = useStyles(); const { styles } = useStyles();
const [count, setCount]: [number, any] = useState(0); const [count, setCount]: [number, any] = useState(0);
@ -38,6 +66,7 @@ const Register: FC = () => {
const [popover, setPopover]: [boolean, any] = useState(false); const [popover, setPopover]: [boolean, any] = useState(false);
const confirmDirty = false; const confirmDirty = false;
let interval: number | undefined; let interval: number | undefined;
const queryClient = useQueryClient();
const passwordStatusMap = { const passwordStatusMap = {
ok: ( ok: (
@ -75,16 +104,6 @@ const Register: FC = () => {
} }
}, 1000); }, 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({ const { isPending: submitting, mutate: register } = useMutation({
mutationFn: (formValues: Store) => { mutationFn: (formValues: Store) => {
const payload = { const payload = {
@ -99,6 +118,7 @@ const Register: FC = () => {
}, },
onSuccess: (data, params) => { onSuccess: (data, params) => {
if (data.status === 'ok') { if (data.status === 'ok') {
queryClient.invalidateQueries({ queryKey: ['current-user'] });
message.success('注册成功!'); message.success('注册成功!');
history.push({ history.push({
pathname: `/user/register-result?account=${params.mail}`, pathname: `/user/register-result?account=${params.mail}`,
@ -139,22 +159,8 @@ const Register: FC = () => {
const changePrefix = (value: string) => { const changePrefix = (value: string) => {
setPrefix(value); setPrefix(value);
}; };
const renderPasswordProgress = () => { const password = Form.useWatch('password', form);
const value = form.getFieldValue('password'); const passwordStatus = getPasswordStatus(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;
};
return ( return (
<div className={styles.main}> <div className={styles.main}>
<h3>注册</h3> <h3>注册</h3>
@ -188,8 +194,8 @@ const Register: FC = () => {
padding: '4px 0', padding: '4px 0',
}} }}
> >
{passwordStatusMap[getPasswordStatus()]} {passwordStatusMap[passwordStatus]}
{renderPasswordProgress()} <PasswordProgress value={password} styles={styles} />
<div <div
style={{ style={{
marginTop: 10, marginTop: 10,

Loading…
Cancel
Save