Browse Source

ci: integrate react-doctor for PR health checks (#11777)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
pull/11778/head
Alex Zhu 2 weeks ago
committed by GitHub
parent
commit
dc15dfb3a4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 20
      .github/workflows/react-doctor.yml
  2. 1
      .gitignore
  3. 1503
      package-lock.json
  4. 2
      package.json
  5. 21
      react-doctor.config.json
  6. 12
      src/components/AvatarList/index.tsx
  7. 23
      src/components/OfflineBanner/index.tsx
  8. 7
      src/components/RightContent/AvatarDropdown.tsx
  9. 21
      src/components/TagSelect/index.tsx
  10. 4
      src/pages/Welcome.tsx
  11. 2
      src/pages/account/center/components/Applications/index.tsx
  12. 2
      src/pages/account/center/components/Projects/index.tsx
  13. 132
      src/pages/account/center/index.tsx
  14. 18
      src/pages/account/settings/components/binding.tsx
  15. 7
      src/pages/account/settings/components/notification.tsx
  16. 30
      src/pages/account/settings/components/security.tsx
  17. 59
      src/pages/account/settings/index.tsx
  18. 140
      src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx
  19. 10
      src/pages/dashboard/analysis/components/Trend/index.tsx
  20. 2
      src/pages/dashboard/monitor/components/ActiveChart/index.tsx
  21. 20
      src/pages/dashboard/monitor/components/Map/index.style.ts
  22. 26
      src/pages/dashboard/monitor/components/Map/index.tsx
  23. 6
      src/pages/dashboard/workplace/index.tsx
  24. 15
      src/pages/form/advanced-form/index.tsx
  25. 4
      src/pages/form/basic-form/index.tsx
  26. 11
      src/pages/list/basic-list/index.tsx
  27. 16
      src/pages/list/card-list/index.tsx
  28. 5
      src/pages/list/search/applications/index.tsx
  29. 15
      src/pages/list/search/articles/index.tsx
  30. 2
      src/pages/list/search/projects/index.tsx
  31. 4
      src/pages/profile/advanced/index.tsx
  32. 2
      src/pages/result/fail/index.tsx
  33. 20
      src/pages/result/success/index.tsx
  34. 4
      src/pages/table-list/components/CreateForm.tsx
  35. 8
      src/pages/table-list/components/UpdateForm.tsx
  36. 14
      src/pages/table-list/index.tsx
  37. 2
      src/pages/user/login/__snapshots__/login.test.tsx.snap
  38. 6
      src/pages/user/login/index.tsx
  39. 2
      src/pages/user/register-result/index.tsx

20
.github/workflows/react-doctor.yml

@ -0,0 +1,20 @@
name: 🏥 React Doctor
on: push
permissions:
contents: read
pull-requests: write
jobs:
react-doctor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v5
with:
node-version: 22
- uses: millionco/react-doctor@main
with:
diff: true
github-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore

@ -34,6 +34,7 @@ functions/*
.umi
.umi-production
.umi-test
.umi-undefined
.turbopack
# screenshot

1503
package-lock.json

File diff suppressed because it is too large

2
package.json

@ -27,6 +27,7 @@
"test": "jest",
"test:coverage": "jest --coverage",
"test:update": "jest -u",
"doctor": "react-doctor .",
"tsc": "tsc --noEmit"
},
"browserslist": [
@ -73,6 +74,7 @@
"@umijs/max-plugin-openapi": "^2.0.3",
"@umijs/request-record": "^1.1.4",
"cross-env": "^10.1.0",
"react-doctor": "^0.1.2",
"express": "^5.2.1",
"geojson": "^0.5.0",
"gh-pages": "^6.3.0",

21
react-doctor.config.json

@ -0,0 +1,21 @@
{
"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
}

12
src/components/AvatarList/index.tsx

@ -40,7 +40,17 @@ const Item: React.FC<AvatarItemProps> = ({
const { styles } = useStyles();
const cls = avatarSizeToClassName(styles, size);
return (
<li className={cls} onClick={onClick}>
<li
className={cls}
onClick={onClick}
onKeyDown={
onClick
? (e) => {
if (e.key === 'Enter') onClick();
}
: undefined
}
>
{tips ? (
<Tooltip title={tips}>
<Avatar

23
src/components/OfflineBanner/index.tsx

@ -1,15 +1,22 @@
import { getIntl } from '@umijs/max';
import { Alert } from 'antd';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
const OfflineBanner: React.FC = () => {
const [isOnline, setIsOnline] = useState(
typeof navigator !== 'undefined' ? navigator.onLine : true,
);
const isOnlineRef = useRef(true);
const [, forceUpdate] = useState<number>(0);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
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 () => {
@ -18,7 +25,7 @@ const OfflineBanner: React.FC = () => {
};
}, []);
if (isOnline) return null;
if (isOnlineRef.current) return null;
return (
<Alert
@ -30,7 +37,7 @@ const OfflineBanner: React.FC = () => {
top: 8,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 999,
zIndex: 10,
maxWidth: 480,
}}
message={getIntl().formatMessage({

7
src/components/RightContent/AvatarDropdown.tsx

@ -6,12 +6,11 @@ import {
import { history, useModel } from '@umijs/max';
import type { MenuProps } from 'antd';
import { Spin } from 'antd';
import React from 'react';
import { flushSync } from 'react-dom';
import React, { startTransition } from 'react';
import { outLogin } from '@/services/ant-design-pro/api';
import HeaderDropdown from '../HeaderDropdown';
export type GlobalHeaderRightProps = {
type GlobalHeaderRightProps = {
children?: React.ReactNode;
};
@ -38,7 +37,7 @@ export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({
const onMenuClick: MenuProps['onClick'] = (event) => {
const { key } = event;
if (key === 'logout') {
flushSync(() => {
startTransition(() => {
setInitialState((s) => ({ ...s, currentUser: undefined }));
});
loginOut();

21
src/components/TagSelect/index.tsx

@ -6,7 +6,7 @@ import React, { type FC, useMemo, useState } from 'react';
import useStyles from './index.style';
const { CheckableTag } = Tag;
export interface TagSelectOptionProps {
interface TagSelectOptionProps {
value: string | number;
style?: React.CSSProperties;
checked?: boolean;
@ -32,7 +32,7 @@ type TagSelectOptionElement = React.ReactElement<
typeof TagSelectOption
>;
export interface TagSelectProps {
interface TagSelectProps {
onChange?: (value: (string | number)[]) => void;
expandable?: boolean;
value?: (string | number)[];
@ -81,9 +81,10 @@ const TagSelect: FC<TagSelectProps> & {
const childrenArray = React.Children.toArray(
children,
) as TagSelectOptionElement[];
return childrenArray
.filter((child) => isTagSelectOption(child))
.map((child) => child.props.value);
return childrenArray.reduce<(string | number)[]>((acc, child) => {
if (isTagSelectOption(child)) acc.push(child.props.value);
return acc;
}, []);
}, [children]);
// Use Set for O(1) lookups
@ -137,9 +138,17 @@ const TagSelect: FC<TagSelectProps> & {
{expandable && (
<a
className={styles.trigger}
onClick={() => {
href="#"
onClick={(e) => {
e.preventDefault();
setExpand(!expand);
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setExpand(!expand);
}
}}
>
{expand ? (
<>

4
src/pages/Welcome.tsx

@ -23,12 +23,12 @@ const InfoCard: React.FC<InfoCardProps> = ({ title, index, desc, href }) => (
<a href={href} target="_blank" rel="noopener noreferrer" aria-label={title}>
<Card hoverable size="small">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-[#1677ff] text-base font-bold text-white">
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-[#1677ff] text-base font-bold text-white">
{index}
</div>
<div className="min-w-0 flex-1">
<h4 className="mb-1 mt-0 text-sm font-semibold">{title}</h4>
<p className="mb-0 line-clamp-2 text-xs text-gray-500">{desc}</p>
<p className="mb-0 line-clamp-2 text-xs text-zinc-500">{desc}</p>
</div>
</div>
</Card>

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

@ -12,7 +12,7 @@ import type { ListItemDataType } from '../../data.d';
import { queryFakeList } from '../../service';
import useStyles from './index.style';
export function formatWan(val: number) {
function formatWan(val: number) {
const v = val * 1;
if (!v || Number.isNaN(v)) return '';
let result: React.ReactNode = val;

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>{item.title}</a>}
title={<a href="#">{item.title}</a>}
description={item.subDescription}
/>
<div className={styles.cardItemContent}>

132
src/pages/account/center/index.tsx

@ -141,6 +141,68 @@ const TagList: React.FC<{
</div>
);
};
const UserInfo: React.FC<{ user: Partial<CurrentUser> }> = ({ user }) => {
const { styles } = useStyles();
return (
<div className={styles.detail}>
<p>
<ContactsOutlined
style={{
marginRight: 8,
}}
/>
{user.title}
</p>
<p>
<ClusterOutlined
style={{
marginRight: 8,
}}
/>
{user.group}
</p>
<p>
<HomeOutlined
style={{
marginRight: 8,
}}
/>
{
(
user.geographic || {
province: {
label: '',
},
}
).province.label
}
{
(
user.geographic || {
city: {
label: '',
},
}
).city.label
}
</p>
</div>
);
};
const TabContent: React.FC<{ tabValue: tabKeyType }> = ({ tabValue }) => {
if (tabValue === 'projects') {
return <Projects />;
}
if (tabValue === 'applications') {
return <Applications />;
}
if (tabValue === 'articles') {
return <Articles />;
}
return null;
};
const Center: React.FC = () => {
const { styles } = useStyles();
const [tabKey, setTabKey] = useState<tabKeyType>('articles');
@ -151,72 +213,6 @@ const Center: React.FC = () => {
queryFn: () => queryCurrent().then((res) => res.data),
});
// 渲染用户信息
const renderUserInfo = ({
title,
group,
geographic,
}: Partial<CurrentUser>) => {
return (
<div className={styles.detail}>
<p>
<ContactsOutlined
style={{
marginRight: 8,
}}
/>
{title}
</p>
<p>
<ClusterOutlined
style={{
marginRight: 8,
}}
/>
{group}
</p>
<p>
<HomeOutlined
style={{
marginRight: 8,
}}
/>
{
(
geographic || {
province: {
label: '',
},
}
).province.label
}
{
(
geographic || {
city: {
label: '',
},
}
).city.label
}
</p>
</div>
);
};
// 渲染tab切换
const renderChildrenByTabKey = (tabValue: tabKeyType) => {
if (tabValue === 'projects') {
return <Projects />;
}
if (tabValue === 'applications') {
return <Applications />;
}
if (tabValue === 'articles') {
return <Articles />;
}
return null;
};
return (
<GridContent>
<Row gutter={24}>
@ -235,7 +231,7 @@ const Center: React.FC = () => {
<div className={styles.name}>{currentUser.name}</div>
<div>{currentUser?.signature}</div>
</div>
{renderUserInfo(currentUser)}
<UserInfo user={currentUser} />
<Divider dashed />
<TagList tags={currentUser.tags || []} />
<Divider
@ -271,7 +267,7 @@ const Center: React.FC = () => {
setTabKey(_tabKey as tabKeyType);
}}
>
{renderChildrenByTabKey(tabKey)}
<TabContent tabValue={tabKey} />
</Card>
</Col>
</Row>

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

@ -11,19 +11,31 @@ const BindingView: React.FC = () => {
{
title: '绑定淘宝',
description: '当前未绑定淘宝账号',
actions: [<a key="Bind"></a>],
actions: [
<a key="Bind" href="#">
</a>,
],
avatar: <TaobaoOutlined className="taobao" />,
},
{
title: '绑定支付宝',
description: '当前未绑定支付宝账号',
actions: [<a key="Bind"></a>],
actions: [
<a key="Bind" href="#">
</a>,
],
avatar: <AlipayOutlined className="alipay" />,
},
{
title: '绑定钉钉',
description: '当前未绑定钉钉账号',
actions: [<a key="Bind"></a>],
actions: [
<a key="Bind" href="#">
</a>,
],
avatar: <DingdingOutlined className="dingding" />,
},
];

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

@ -3,11 +3,12 @@ import React from 'react';
type Unpacked<T> = T extends (infer U)[] ? U : T;
const Action = (
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked />
);
const NotificationView: React.FC = () => {
const getData = () => {
const Action = (
<Switch checkedChildren="开" unCheckedChildren="关" defaultChecked />
);
return [
{
title: '用户消息',

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

@ -19,27 +19,47 @@ const SecurityView: React.FC = () => {
{passwordStrength.strong}
</>
),
actions: [<a key="Modify"></a>],
actions: [
<a key="Modify" href="#">
</a>,
],
},
{
title: '密保手机',
description: `已绑定手机:138****8293`,
actions: [<a key="Modify"></a>],
actions: [
<a key="Modify" href="#">
</a>,
],
},
{
title: '密保问题',
description: '未设置密保问题,密保问题可有效保护账户安全',
actions: [<a key="Set"></a>],
actions: [
<a key="Set" href="#">
</a>,
],
},
{
title: '备用邮箱',
description: `已绑定邮箱:ant***sign.com`,
actions: [<a key="Modify"></a>],
actions: [
<a key="Modify" href="#">
</a>,
],
},
{
title: 'MFA 设备',
description: '未绑定 MFA 设备,绑定后,可以进行二次确认',
actions: [<a key="bind"></a>],
actions: [
<a key="bind" href="#">
</a>,
],
},
];

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

@ -1,6 +1,6 @@
import { GridContent } from '@ant-design/pro-components';
import { Menu } from 'antd';
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
import React, { useLayoutEffect, useRef, useState } from 'react';
import BaseView from './components/base';
import BindingView from './components/binding';
import NotificationView from './components/notification';
@ -12,6 +12,24 @@ type SettingsState = {
mode: 'inline' | 'horizontal';
selectKey: SettingsStateKeys;
};
const SettingsContent: React.FC<{ selectKey: SettingsStateKeys }> = ({
selectKey,
}) => {
switch (selectKey) {
case 'base':
return <BaseView />;
case 'security':
return <SecurityView />;
case 'binding':
return <BindingView />;
case 'notification':
return <NotificationView />;
default:
return null;
}
};
const Settings: React.FC = () => {
const { styles } = useStyles();
const menuMap: Record<string, React.ReactNode> = {
@ -26,7 +44,7 @@ const Settings: React.FC = () => {
});
const dom = useRef<HTMLDivElement>(null);
const resize = useCallback(() => {
const resize = () => {
requestAnimationFrame(() => {
if (!dom.current) {
return;
@ -44,36 +62,25 @@ const Settings: React.FC = () => {
mode: mode as SettingsState['mode'],
}));
});
}, []);
};
const resizeRef = useRef(resize);
resizeRef.current = resize;
useLayoutEffect(() => {
window.addEventListener('resize', resize);
resize();
const handler = () => resizeRef.current();
window.addEventListener('resize', handler);
handler();
return () => {
window.removeEventListener('resize', resize);
window.removeEventListener('resize', handler);
};
}, [resize]);
}, []);
const getMenu = () => {
return Object.keys(menuMap).map((item) => ({
key: item,
label: menuMap[item],
}));
};
const renderChildren = () => {
const { selectKey } = initConfig;
switch (selectKey) {
case 'base':
return <BaseView />;
case 'security':
return <SecurityView />;
case 'binding':
return <BindingView />;
case 'notification':
return <NotificationView />;
default:
return null;
}
};
return (
<GridContent>
<div
@ -89,17 +96,17 @@ const Settings: React.FC = () => {
mode={initConfig.mode}
selectedKeys={[initConfig.selectKey]}
onClick={({ key }) => {
setInitConfig({
...initConfig,
setInitConfig((prev) => ({
...prev,
selectKey: key as SettingsStateKeys,
});
}));
}}
items={getMenu()}
/>
</div>
<div className={styles.right}>
<div className={styles.title}>{menuMap[initConfig.selectKey]}</div>
{renderChildren()}
<SettingsContent selectKey={initConfig.selectKey} />
</div>
</div>
</GridContent>

140
src/pages/dashboard/analysis/components/Charts/ChartCard/index.tsx

@ -7,7 +7,7 @@ import useStyles from './index.style';
type totalType = () => React.ReactNode;
export type ChartCardProps = {
type ChartCardProps = {
title: React.ReactNode;
action?: React.ReactNode;
total?: React.ReactNode | number | (() => React.ReactNode | number);
@ -17,80 +17,76 @@ export type ChartCardProps = {
style?: React.CSSProperties;
} & CardProps;
const ChartCard: React.FC<ChartCardProps> = (props) => {
const { styles } = useStyles();
const renderTotal = (total?: number | totalType | React.ReactNode) => {
if (!total && total !== 0) {
const ChartCardTotal: React.FC<{
total?: number | totalType | React.ReactNode;
totalClassName: string;
}> = ({ total, totalClassName }) => {
if (!total && total !== 0) {
return null;
}
switch (typeof total) {
case 'undefined':
return null;
}
let totalDom: React.ReactNode | null = null;
switch (typeof total) {
case 'undefined':
totalDom = null;
break;
case 'function':
totalDom = <div className={styles.total}>{total()}</div>;
break;
default:
totalDom = <div className={styles.total}>{total}</div>;
}
return totalDom;
};
const renderContent = () => {
const {
contentHeight,
title,
avatar,
action,
total,
footer,
children,
loading,
} = props;
if (loading) {
return false;
}
return (
<div className={styles.chartCard}>
<div
className={clsx(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
<div className={styles.avatar}>{avatar}</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{renderTotal(total)}
</div>
case 'function':
return <div className={totalClassName}>{total()}</div>;
default:
return <div className={totalClassName}>{total}</div>;
}
};
const ChartCardContent: React.FC<
ChartCardProps & { styles: Record<string, string> }
> = ({
contentHeight,
title,
avatar,
action,
total,
footer,
children,
styles,
}) => (
<div className={styles.chartCard}>
<div
className={clsx(styles.chartTop, {
[styles.chartTopMargin]: !children && !footer,
})}
>
<div className={styles.avatar}>{avatar}</div>
<div className={styles.metaWrap}>
<div className={styles.meta}>
<span>{title}</span>
<span className={styles.action}>{action}</span>
</div>
{children && (
<div
className={styles.content}
style={{
height: contentHeight || 'auto',
}}
>
<div className={contentHeight ? styles.contentFixed : undefined}>
{children}
</div>
</div>
)}
{footer && (
<div
className={clsx(styles.footer, {
[styles.footerMargin]: !children,
})}
>
{footer}
</div>
)}
<ChartCardTotal total={total} totalClassName={styles.total} />
</div>
);
};
</div>
{children && (
<div
className={styles.content}
style={{
height: contentHeight || 'auto',
}}
>
<div className={contentHeight ? styles.contentFixed : undefined}>
{children}
</div>
</div>
)}
{footer && (
<div
className={clsx(styles.footer, {
[styles.footerMargin]: !children,
})}
>
{footer}
</div>
)}
</div>
);
const ChartCard: React.FC<ChartCardProps> = (props) => {
const { styles } = useStyles();
const { loading = false, ...rest } = props;
const cardProps = omit(rest, ['total', 'contentHeight', 'action']);
return (
@ -103,7 +99,7 @@ const ChartCard: React.FC<ChartCardProps> = (props) => {
}}
{...cardProps}
>
{renderContent()}
{loading ? false : <ChartCardContent {...props} styles={styles} />}
</Card>
);
};

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

@ -3,13 +3,14 @@ import { clsx } from 'clsx';
import React from 'react';
import useStyles from './index.style';
export type TrendProps = {
type TrendProps = {
colorful?: boolean;
flag: 'up' | 'down';
style?: React.CSSProperties;
reverseColor?: boolean;
className?: string;
children?: React.ReactNode;
title?: string;
};
const Trend: React.FC<TrendProps> = ({
@ -18,6 +19,7 @@ const Trend: React.FC<TrendProps> = ({
flag,
children,
className,
title = '',
...rest
}) => {
const { styles } = useStyles();
@ -30,11 +32,7 @@ const Trend: React.FC<TrendProps> = ({
className,
);
return (
<div
{...rest}
className={classString}
title={typeof children === 'string' ? children : ''}
>
<div {...rest} className={classString} title={title}>
<span>{children}</span>
{flag && (
<span className={styles[flag]}>

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

@ -38,7 +38,7 @@ const ActiveChart = () => {
// Memoize max and median to avoid double sort on every render
const { maxValue, medianValue } = useMemo(() => {
if (!activeData.length) return { maxValue: 0, medianValue: 0 };
const sorted = [...activeData].sort((a, b) => a.y - b.y);
const sorted = activeData.toSorted((a, b) => a.y - b.y);
return {
maxValue: sorted[sorted.length - 1]?.y ?? 0,
medianValue: sorted[Math.floor(sorted.length / 2)]?.y ?? 0,

20
src/pages/dashboard/monitor/components/Map/index.style.ts

@ -0,0 +1,20 @@
import { createStyles } from 'antd-style';
const useStyles = createStyles({
tooltip: {
position: 'absolute',
background: '#fff',
color: '#334155',
padding: '8px 12px',
borderRadius: '8px',
fontSize: '12px',
border: '1px solid #e2e8f0',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
pointerEvents: 'none',
opacity: 0,
transition: 'opacity 0.2s',
zIndex: 10,
},
});
export default useStyles;

26
src/pages/dashboard/monitor/components/Map/index.tsx

@ -4,6 +4,7 @@ import * as d3 from 'd3';
import type { Feature, FeatureCollection, Geometry } from 'geojson';
import { useEffect, useMemo, useRef } from 'react';
import { feature } from 'topojson-client';
import useStyles from './index.style';
const DATA_COLORS = [
'#ede9fe',
@ -103,6 +104,7 @@ function buildLandBitmap(
}
export default function MonitorMap() {
const { styles } = useStyles();
const svgRef = useRef<SVGSVGElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
@ -284,8 +286,10 @@ export default function MonitorMap() {
.on('mousemove', (event) => {
if (tooltipRef.current) {
const [x, y] = d3.pointer(event, svgRef.current);
tooltipRef.current.style.left = `${x + 12}px`;
tooltipRef.current.style.top = `${y - 12}px`;
Object.assign(tooltipRef.current.style, {
left: `${x + 12}px`,
top: `${y - 12}px`,
});
}
})
.on('mouseleave', function (_event, d) {
@ -374,23 +378,7 @@ export default function MonitorMap() {
borderRadius: 8,
}}
/>
<div
ref={tooltipRef}
style={{
position: 'absolute',
background: '#fff',
color: '#334155',
padding: '8px 12px',
borderRadius: '8px',
fontSize: '12px',
border: '1px solid #e2e8f0',
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
pointerEvents: 'none',
opacity: 0,
transition: 'opacity 0.2s',
zIndex: 10,
}}
/>
<div ref={tooltipRef} className={styles.tooltip} />
</div>
);
}

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

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

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

@ -12,7 +12,7 @@ import {
} from '@ant-design/pro-components';
import { Card, Col, message, Popover, Row } from 'antd';
import type { FC } from 'react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { fakeSubmitForm } from './service';
import useStyles from './style.style';
@ -66,6 +66,7 @@ interface ErrorField {
const AdvancedForm: FC<Record<string, any>> = () => {
const { styles } = useStyles();
const [error, setError] = useState<ErrorField[]>([]);
const keyCounter = useRef(0);
const getErrorInfo = (errors: ErrorField[]) => {
const errorCount = errors.filter((item) => item.errors.length > 0).length;
if (!errors || errorCount === 0) {
@ -89,15 +90,16 @@ const AdvancedForm: FC<Record<string, any>> = () => {
| 'dateRange'
| 'type';
return (
<li
<button
key={key}
type="button"
className={styles.errorListItem}
onClick={() => scrollToField(key)}
>
<CloseCircleOutlined className={styles.errorIcon} />
<div>{err.errors[0]}</div>
<div className={styles.errorField}>{fieldLabels[key]}</div>
</li>
</button>
);
});
return (
@ -161,7 +163,9 @@ const AdvancedForm: FC<Record<string, any>> = () => {
return [
<a
key="eidit"
onClick={() => {
href="#"
onClick={(e) => {
e.preventDefault();
action?.startEditable(record.key);
}}
>
@ -534,8 +538,9 @@ const AdvancedForm: FC<Record<string, any>> = () => {
<EditableProTable<TableFormDateType>
recordCreatorProps={{
record: () => {
keyCounter.current += 1;
return {
key: `0${Date.now()}`,
key: `new-${keyCounter.current}`,
};
},
}}

4
src/pages/form/basic-form/index.tsx

@ -9,7 +9,7 @@ import {
ProFormText,
ProFormTextArea,
} from '@ant-design/pro-components';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { Card, message } from 'antd';
import type { FC } from 'react';
import { fakeSubmitForm } from './service';
@ -17,10 +17,12 @@ import useStyles from './style.style';
const BasicForm: FC<Record<string, any>> = () => {
const { styles } = useStyles();
const queryClient = useQueryClient();
const { mutate: run } = useMutation({
mutationFn: fakeSubmitForm,
onSuccess: () => {
message.success('提交成功');
queryClient.invalidateQueries({ queryKey: ['basic-form'] });
},
});
const onFinish = async (values: Record<string, any>) => {

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

@ -1,6 +1,6 @@
import { DownOutlined, PlusOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import {
Avatar,
Button,
@ -71,7 +71,7 @@ const ListContent = ({
</div>
);
};
export const BasicList: FC = () => {
const BasicList: FC = () => {
const { styles } = useStyles();
const [done, setDone] = useState<boolean>(false);
const [open, setVisible] = useState<boolean>(false);
@ -83,6 +83,7 @@ export const BasicList: FC = () => {
queryFn: () => queryFakeList({ count: 50 }).then((res) => res.data),
});
const queryClient = useQueryClient();
const { mutate: postRun } = useMutation({
mutationFn: async ({ method, params }: { method: string; params: any }) => {
if (method === 'remove') {
@ -93,6 +94,9 @@ export const BasicList: FC = () => {
}
return addFakeList(params);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['basic-list'] });
},
});
// Wrapper to handle the original calling convention
@ -167,7 +171,7 @@ export const BasicList: FC = () => {
],
}}
>
<a>
<a href="#">
<DownOutlined />
</a>
</Dropdown>
@ -227,6 +231,7 @@ export const BasicList: FC = () => {
actions={[
<a
key="edit"
href="#"
onClick={(e) => {
e.preventDefault();
showEditModal(item);

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

@ -22,21 +22,21 @@ const CardList = () => {
</p>
<div className={styles.contentLink}>
<a>
<a href="#">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/MjEImQtenlyueSmVEfUD.svg"
/>{' '}
</a>
<a>
<a href="#">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/NbuDUAuBlIApFuDvWiND.svg"
/>{' '}
</a>
<a>
<a href="#">
<img
alt=""
src="https://gw.alipayobjects.com/zos/rmsportal/ohOEPSYdDTNnyMbGuyLb.svg"
@ -79,8 +79,12 @@ const CardList = () => {
hoverable
className={styles.card}
actions={[
<a key="option1"></a>,
<a key="option2"></a>,
<a key="option1" href="#">
</a>,
<a key="option2" href="#">
</a>,
]}
>
<Card.Meta
@ -91,7 +95,7 @@ const CardList = () => {
src={item.avatar}
/>
}
title={<a>{item.title}</a>}
title={<a href="#">{item.title}</a>}
description={
<Paragraph
className={styles.item}

5
src/pages/list/search/applications/index.tsx

@ -24,7 +24,8 @@ import { categoryOptions } from '../../mock';
import type { ListItemDataType } from './data.d';
import { queryFakeList } from './service';
import useStyles from './style.style';
export function formatWan(val: number) {
function formatWan(val: number) {
const v = val * 1;
if (!v || Number.isNaN(v)) return '';
let result: React.ReactNode = val;
@ -76,7 +77,7 @@ const CardInfo: React.FC<{
</div>
);
};
export const Applications: FC<Record<string, any>> = () => {
const Applications: FC<Record<string, any>> = () => {
const { styles } = useStyles();
const {
data,

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

@ -183,7 +183,20 @@ const Articles: FC = () => {
options={ownerOptions}
/>
</FormItem>
<a className={styles.selfTrigger} onClick={setOwner}>
<a
className={styles.selfTrigger}
href="#"
onClick={(e) => {
e.preventDefault();
setOwner();
}}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setOwner();
}
}}
>
</a>
</StandardFormRow>

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

@ -50,7 +50,7 @@ const Projects: FC = () => {
cover={<img alt={item.title} src={item.cover} />}
>
<Card.Meta
title={<a>{item.title}</a>}
title={<a href="#">{item.title}</a>}
description={
<Paragraph
ellipsis={{

4
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,7 @@ const Advanced: FC = () => {
}}
/>
<div>
<a href=""></a>
<a href="#"></a>
</div>
</div>
);

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

@ -23,6 +23,7 @@ export default () => {
/>
<span></span>
<a
href="#"
style={{
marginLeft: 16,
}}
@ -40,6 +41,7 @@ export default () => {
/>
<span></span>
<a
href="#"
style={{
marginLeft: 16,
}}

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

@ -1,6 +1,7 @@
import { DingdingOutlined } from '@ant-design/icons';
import { GridContent } from '@ant-design/pro-components';
import { Button, Card, Descriptions, Result, Steps } from 'antd';
import React from 'react';
import useStyles from './index.style';
const descriptionItems = [
@ -9,6 +10,14 @@ const descriptionItems = [
{ key: 'time', label: '生效时间', children: '2016-12-12 ~ 2017-12-12' },
];
const extra = (
<>
<Button type="primary"></Button>
<Button></Button>
<Button></Button>
</>
);
const Success: React.FC = () => {
const { styles } = useStyles();
const desc1 = (
@ -42,7 +51,7 @@ const Success: React.FC = () => {
}}
>
<span></span>
<a href="">
<a href="#">
<DingdingOutlined
style={{
color: '#00A0E9',
@ -112,20 +121,13 @@ const Success: React.FC = () => {
/>
</>
);
const extra = (
<>
<Button type="primary"></Button>
<Button></Button>
<Button></Button>
</>
);
return (
<GridContent>
<Card variant="borderless">
<Result
status="success"
title="提交成功"
subTitle="提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。 本文字区域可以展示简单的补充说明,如果有类似展示 “单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。"
subTitle='提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。 本文字区域可以展示简单的补充说明,如果有类似展示 "单据"的需求,下面这个灰色区域可以呈现比较复杂的内容。'
extra={extra}
style={{
marginBottom: 16,

4
src/pages/table-list/components/CreateForm.tsx

@ -5,7 +5,7 @@ import {
ProFormText,
ProFormTextArea,
} from '@ant-design/pro-components';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, message } from 'antd';
import type { FC } from 'react';
@ -19,6 +19,7 @@ const CreateForm: FC<CreateFormProps> = (props) => {
const { reload } = props;
const [messageApi, contextHolder] = message.useMessage();
const queryClient = useQueryClient();
/**
* @en-US International configuration
* @zh-CN
@ -29,6 +30,7 @@ const CreateForm: FC<CreateFormProps> = (props) => {
mutationFn: addRule,
onSuccess: () => {
messageApi.success('Added successfully');
queryClient.invalidateQueries({ queryKey: ['rule'] });
reload?.();
},
onError: () => {

8
src/pages/table-list/components/UpdateForm.tsx

@ -6,13 +6,13 @@ import {
ProFormTextArea,
StepsForm,
} from '@ant-design/pro-components';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Modal, message } from 'antd';
import React, { cloneElement, useCallback, useState } from 'react';
import { updateRule } from '@/services/ant-design-pro/api';
export type FormValueType = {
type FormValueType = {
target?: string;
template?: string;
type?: string;
@ -20,7 +20,7 @@ export type FormValueType = {
frequency?: string;
} & Partial<API.RuleListItem>;
export type UpdateFormProps = {
type UpdateFormProps = {
trigger?: React.ReactElement<any>;
onOk?: () => void;
values: Partial<API.RuleListItem>;
@ -30,6 +30,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
const { onOk, values, trigger } = props;
const intl = useIntl();
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@ -39,6 +40,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
mutationFn: updateRule,
onSuccess: () => {
messageApi.success('Configuration is successful');
queryClient.invalidateQueries({ queryKey: ['rule'] });
onOk?.();
},
onError: () => {

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

@ -9,7 +9,7 @@ import {
ProDescriptions,
ProTable,
} from '@ant-design/pro-components';
import { useMutation } from '@tanstack/react-query';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Button, Drawer, type FormInstance, Input, message } from 'antd';
import React, { useCallback, useRef, useState } from 'react';
@ -19,6 +19,7 @@ import UpdateForm from './components/UpdateForm';
const TableList: React.FC = () => {
const actionRef = useRef<ActionType | null>(null);
const queryClient = useQueryClient();
const [showDetail, setShowDetail] = useState<boolean>(false);
const [currentRow, setCurrentRow] = useState<API.RuleListItem>();
@ -37,6 +38,7 @@ const TableList: React.FC = () => {
onSuccess: () => {
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
queryClient.invalidateQueries({ queryKey: ['rule'] });
messageApi.success('Deleted successfully and will refresh soon');
},
@ -57,7 +59,9 @@ const TableList: React.FC = () => {
render: (dom, entity) => {
return (
<a
onClick={() => {
href="#"
onClick={(e) => {
e.preventDefault();
setCurrentRow(entity);
setShowDetail(true);
}}
@ -193,7 +197,7 @@ const TableList: React.FC = () => {
render: (_, record) => [
<UpdateForm
trigger={
<a>
<a href="#">
<FormattedMessage
id="pages.searchTable.config"
defaultMessage="Configuration"
@ -269,7 +273,9 @@ const TableList: React.FC = () => {
id="pages.searchTable.chosen"
defaultMessage="Chosen"
/>{' '}
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
<span style={{ fontWeight: 600 }}>
{selectedRowsState.length}
</span>{' '}
<FormattedMessage
id="pages.searchTable.item"
defaultMessage="项"

2
src/pages/user/login/__snapshots__/login.test.tsx.snap

@ -373,6 +373,7 @@ exports[`Login Page should login success 1`] = `
</span>
</label>
<a
href="#"
style="float: right;"
>
Forgot Password ?
@ -935,6 +936,7 @@ exports[`Login Page should show login form 1`] = `
</span>
</label>
<a
href="#"
style="float: right;"
>
Forgot Password ?

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

@ -21,8 +21,7 @@ import {
} from '@umijs/max';
import { Alert, App, Tabs } from 'antd';
import { createStyles } from 'antd-style';
import React, { useState } from 'react';
import { flushSync } from 'react-dom';
import React, { startTransition, useState } from 'react';
import { Footer } from '@/components';
import { login } from '@/services/ant-design-pro/api';
import { getFakeCaptcha } from '@/services/ant-design-pro/login';
@ -142,7 +141,7 @@ const Login: React.FC = () => {
const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
startTransition(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
@ -399,6 +398,7 @@ const Login: React.FC = () => {
/>
</ProFormCheckbox>
<a
href="#"
style={{
float: 'right',
}}

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

@ -9,7 +9,7 @@ const RegisterResult: React.FC<Record<string, unknown>> = () => {
const actions = (
<div className={styles.actions}>
<a href="">
<a href="#">
<Button size="large" type="primary">
<span></span>
</Button>

Loading…
Cancel
Save