From e59ce1046d661bf0f42855ce2661eb2a2e889754 Mon Sep 17 00:00:00 2001 From: afc163 Date: Mon, 22 Jun 2026 11:08:20 +0800 Subject: [PATCH] fix: resolve react doctor findings --- doctor.config.json | 25 ++++ src/components/AvatarList/index.style.ts | 6 + src/components/AvatarList/index.tsx | 49 +++---- src/components/OfflineBanner/index.tsx | 42 +++--- .../RightContent/AvatarDropdown.tsx | 73 +++++------ src/components/RightContent/LangDropdown.tsx | 63 +++++++++ .../RightContent/VersionDropdown.tsx | 36 ++++++ src/components/RightContent/index.tsx | 120 ++---------------- src/components/RightContent/style.ts | 16 +++ src/components/TagSelect/index.tsx | 28 ++-- .../center/components/Projects/index.tsx | 2 +- .../account/settings/components/base.tsx | 7 +- .../account/settings/components/binding.tsx | 70 +++++----- .../settings/components/notification.tsx | 40 +++--- .../account/settings/components/security.tsx | 110 ++++++++-------- src/pages/account/settings/index.tsx | 25 ++-- .../analysis/components/Charts/index.tsx | 11 -- .../analysis/components/IntroduceRow.tsx | 3 +- .../analysis/components/NumberInfo/index.tsx | 9 +- .../analysis/components/ProportionSales.tsx | 38 +++--- .../analysis/components/TopSearch.tsx | 57 +++++---- src/pages/dashboard/analysis/index.tsx | 8 +- src/pages/dashboard/analysis/utils/Yuan.tsx | 14 +- src/pages/dashboard/analysis/utils/utils.ts | 2 +- .../monitor/components/ActiveChart/index.tsx | 6 +- src/pages/dashboard/workplace/index.tsx | 6 +- src/pages/form/advanced-form/index.tsx | 13 +- src/pages/form/basic-form/index.test.tsx | 14 +- src/pages/list/basic-list/index.tsx | 15 +-- src/pages/list/card-list/index.tsx | 19 +-- src/pages/list/search/articles/index.tsx | 82 ++++++------ src/pages/list/search/projects/index.tsx | 23 ++-- src/pages/profile/advanced/index.tsx | 6 +- src/pages/result/fail/index.tsx | 14 +- src/pages/result/success/index.tsx | 4 +- src/pages/table-list/index.tsx | 13 +- src/pages/user/login/index.tsx | 48 ++++--- src/pages/user/register-result/index.tsx | 8 +- src/pages/user/register/index.tsx | 64 +++++----- 39 files changed, 594 insertions(+), 595 deletions(-) create mode 100644 src/components/RightContent/LangDropdown.tsx create mode 100644 src/components/RightContent/VersionDropdown.tsx create mode 100644 src/components/RightContent/style.ts delete mode 100644 src/pages/dashboard/analysis/components/Charts/index.tsx diff --git a/doctor.config.json b/doctor.config.json index a8ab17ff..f88b7552 100644 --- a/doctor.config.json +++ b/doctor.config.json @@ -6,6 +6,7 @@ "public/**", "scripts/**", "types/**", + ".worktrees/**", "cloudflare-worker/**", "src/.umi*/**", "src/services/**", @@ -14,6 +15,30 @@ "**/*.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", diff --git a/src/components/AvatarList/index.style.ts b/src/components/AvatarList/index.style.ts index d601a1d8..f471f4b8 100644 --- a/src/components/AvatarList/index.style.ts +++ b/src/components/AvatarList/index.style.ts @@ -35,6 +35,12 @@ const useStyles = createStyles(({ token }) => { }, }, }, + avatarButton: { + padding: 0, + border: 0, + background: 'transparent', + cursor: 'pointer', + }, }; }); diff --git a/src/components/AvatarList/index.tsx b/src/components/AvatarList/index.tsx index 36f3b33b..1b5819d1 100644 --- a/src/components/AvatarList/index.tsx +++ b/src/components/AvatarList/index.tsx @@ -31,38 +31,31 @@ const avatarSizeToClassName = (styles: any, size?: SizeType | 'mini') => [styles.avatarItemMini]: size === 'mini', }); -const Item: React.FC = ({ - src, - size, - tips, - onClick = () => {}, -}) => { +const Item: React.FC = ({ src, size, tips, onClick }) => { const { styles } = useStyles(); const cls = avatarSizeToClassName(styles, size); + const avatar = tips ? ( + + + + ) : ( + + ); + return ( -
  • { - if (e.key === 'Enter') onClick(); - } - : undefined - } - > - {tips ? ( - - - +
  • + {onClick ? ( + ) : ( - + avatar )}
  • ); diff --git a/src/components/OfflineBanner/index.tsx b/src/components/OfflineBanner/index.tsx index 50d8d879..e4347880 100644 --- a/src/components/OfflineBanner/index.tsx +++ b/src/components/OfflineBanner/index.tsx @@ -1,31 +1,27 @@ import { getIntl } from '@umijs/max'; import { Alert } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { useSyncExternalStore } from 'react'; -const OfflineBanner: React.FC = () => { - const isOnlineRef = useRef(true); - const [, forceUpdate] = useState(0); +const subscribeOnlineStatus = (callback: () => void) => { + window.addEventListener('online', callback); + window.addEventListener('offline', callback); + return () => { + window.removeEventListener('online', callback); + window.removeEventListener('offline', callback); + }; +}; + +const getOnlineStatus = () => + typeof navigator === 'undefined' ? true : navigator.onLine; - useEffect(() => { - isOnlineRef.current = navigator.onLine; - forceUpdate((n) => n + 1); - const handleOnline = () => { - isOnlineRef.current = true; - forceUpdate((n: number) => n + 1); - }; - const handleOffline = () => { - isOnlineRef.current = false; - forceUpdate((n: number) => n + 1); - }; - window.addEventListener('online', handleOnline); - window.addEventListener('offline', handleOffline); - return () => { - window.removeEventListener('online', handleOnline); - window.removeEventListener('offline', handleOffline); - }; - }, []); +const OfflineBanner: React.FC = () => { + const isOnline = useSyncExternalStore( + subscribeOnlineStatus, + getOnlineStatus, + () => true, + ); - if (isOnlineRef.current) return null; + if (isOnline) return null; return ( , + label: '个人设置', + }, + { + key: 'theme', + icon: , + label: '主题设置', + }, + { + type: 'divider' as const, + }, + { + key: 'logout', + icon: , + label: '退出登录', + }, +]; + +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(), + }); + } +}; + export const AvatarDropdown: React.FC = ({ children, }) => { - const loginOut = async () => { - await outLogin(); - const { search, pathname } = window.location; - const urlParams = new URL(window.location.href).searchParams; - const searchParams = new URLSearchParams({ - redirect: pathname + search, - }); - const redirect = urlParams.get('redirect'); - if (window.location.pathname !== '/user/login' && !redirect) { - history.replace({ - pathname: '/user/login', - search: searchParams.toString(), - }); - } - }; const { initialState, setInitialState } = useModel('@@initialState'); const onMenuClick: MenuProps['onClick'] = (event) => { @@ -60,27 +82,6 @@ export const AvatarDropdown: React.FC = ({ return ; } - const menuItems: MenuProps['items'] = [ - { - key: 'settings', - icon: , - label: '个人设置', - }, - { - key: 'theme', - icon: , - label: '主题设置', - }, - { - type: 'divider' as const, - }, - { - key: 'logout', - icon: , - label: '退出登录', - }, - ]; - return ( = { + '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 ? ( + + ) : ( + + ), + label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`, + })); + + return ( + + + + ); +}; diff --git a/src/components/RightContent/VersionDropdown.tsx b/src/components/RightContent/VersionDropdown.tsx new file mode 100644 index 00000000..2079c14b --- /dev/null +++ b/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 ( + + + + ); +}; diff --git a/src/components/RightContent/index.tsx b/src/components/RightContent/index.tsx index ea0b19f2..a6943678 100644 --- a/src/components/RightContent/index.tsx +++ b/src/components/RightContent/index.tsx @@ -1,43 +1,13 @@ -import { - BookOutlined, - CheckOutlined, - ForkOutlined, - GlobalOutlined, -} from '@ant-design/icons'; -import { getAllLocales, getLocale, history, setLocale } from '@umijs/max'; -import type { MenuProps } from 'antd'; +import { BookOutlined } from '@ant-design/icons'; +import { history } from '@umijs/max'; import { Button, Tooltip } from 'antd'; -import { createStyles } from 'antd-style'; -import React, { useMemo } from 'react'; -import HeaderDropdown from '../HeaderDropdown'; - -export const localeLabelMap: Record = - { - 'zh-CN': { emoji: '🇨🇳', label: '简体中文' }, - 'zh-TW': { emoji: '🇭🇰', label: '繁體中文' }, - 'en-US': { emoji: '🇺🇸', label: 'English' }, - 'ja-JP': { emoji: '🇯🇵', label: '日本語' }, - 'pt-BR': { emoji: '🇧🇷', label: 'Português' }, - 'id-ID': { emoji: '🇮🇩', label: 'Bahasa Indonesia' }, - 'fa-IR': { emoji: '🇮🇷', label: 'فارسی' }, - 'bn-BD': { emoji: '🇧🇩', label: 'বাংলা' }, - }; - -const useStyles = createStyles(({ token, css }) => ({ - action: css` - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - height: 36px !important; - min-width: 36px; - padding-inline: 8px !important; - padding-block: 0 !important; - border-radius: ${token.borderRadius}px !important; - `, -})); +import React from 'react'; +import { LangDropdown } from './LangDropdown'; +import useHeaderActionStyles from './style'; +import { VersionDropdown } from './VersionDropdown'; export const DocLink: React.FC = () => { - const { styles } = useStyles(); + const { styles } = useHeaderActionStyles(); return ( - - ); -}; - -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 ? ( - - ) : ( - - ), - label: `${localeLabelMap[locale]?.emoji ?? ''} ${localeLabelMap[locale]?.label ?? locale}`, - })); - - const onLangClick: MenuProps['onClick'] = ({ key }) => { - if (key.startsWith('lang-')) { - setLocale(key.replace('lang-', ''), false); - } - }; - - return ( - - - - ); -}; +export { LangDropdown, VersionDropdown }; diff --git a/src/components/RightContent/style.ts b/src/components/RightContent/style.ts new file mode 100644 index 00000000..fdde3a5f --- /dev/null +++ b/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; diff --git a/src/components/TagSelect/index.tsx b/src/components/TagSelect/index.tsx index c2b319e7..17c428bb 100644 --- a/src/components/TagSelect/index.tsx +++ b/src/components/TagSelect/index.tsx @@ -1,6 +1,6 @@ import { DownOutlined, UpOutlined } from '@ant-design/icons'; import { useMergedState } from '@rc-component/util'; -import { Tag } from 'antd'; +import { Button, Tag } from 'antd'; import { clsx } from 'clsx'; import React, { type FC, useMemo, useState } from 'react'; import useStyles from './index.style'; @@ -32,6 +32,10 @@ type TagSelectOptionElement = React.ReactElement< typeof TagSelectOption >; +const isTagSelectOption = (node: TagSelectOptionElement) => + node?.type && + (node.type.isTagSelectOption || node.type.displayName === 'TagSelectOption'); + interface TagSelectProps { onChange?: (value: (string | number)[]) => void; expandable?: boolean; @@ -71,11 +75,6 @@ const TagSelect: FC & { }, ); - const isTagSelectOption = (node: TagSelectOptionElement) => - node?.type && - (node.type.isTagSelectOption || - node.type.displayName === 'TagSelectOption'); - // Memoize all tags to avoid recalculating on every render const allTags = useMemo(() => { const childrenArray = React.Children.toArray( @@ -136,19 +135,10 @@ const TagSelect: FC & { return child; })} {expandable && ( - { - e.preventDefault(); - setExpand(!expand); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - setExpand(!expand); - } - }} + onClick={() => setExpand((prev) => !prev)} > {expand ? ( <> @@ -160,7 +150,7 @@ const TagSelect: FC & { )} - + )} ); diff --git a/src/pages/account/center/components/Projects/index.tsx b/src/pages/account/center/components/Projects/index.tsx index 36067b89..0c97cac8 100644 --- a/src/pages/account/center/components/Projects/index.tsx +++ b/src/pages/account/center/components/Projects/index.tsx @@ -36,7 +36,7 @@ const Projects: React.FC = () => { cover={{item.title}} > {item.title}} + title={{item.title}} description={item.subDescription} />
    diff --git a/src/pages/account/settings/components/base.tsx b/src/pages/account/settings/components/base.tsx index 51301806..9bf5e8c6 100644 --- a/src/pages/account/settings/components/base.tsx +++ b/src/pages/account/settings/components/base.tsx @@ -27,6 +27,10 @@ const validatorPhone = ( callback(); }; +const handleFinish = async () => { + message.success('更新基本信息成功'); +}; + const BaseView: React.FC = () => { const { styles } = useStyles(); @@ -45,9 +49,6 @@ const BaseView: React.FC = () => { } return ''; }; - const handleFinish = async () => { - message.success('更新基本信息成功'); - }; return (
    {loading ? null : ( diff --git a/src/pages/account/settings/components/binding.tsx b/src/pages/account/settings/components/binding.tsx index 10aad1ca..80ad7a35 100644 --- a/src/pages/account/settings/components/binding.tsx +++ b/src/pages/account/settings/components/binding.tsx @@ -3,47 +3,47 @@ import { DingdingOutlined, TaobaoOutlined, } from '@ant-design/icons'; -import { List } from 'antd'; +import { Button, List } from 'antd'; import React from 'react'; -const BindingView: React.FC = () => { - const getData = () => [ - { - title: '绑定淘宝', - description: '当前未绑定淘宝账号', - actions: [ - - 绑定 - , - ], - avatar: , - }, - { - title: '绑定支付宝', - description: '当前未绑定支付宝账号', - actions: [ - - 绑定 - , - ], - avatar: , - }, - { - title: '绑定钉钉', - description: '当前未绑定钉钉账号', - actions: [ - - 绑定 - , - ], - avatar: , - }, - ]; +const bindingData = [ + { + title: '绑定淘宝', + description: '当前未绑定淘宝账号', + actions: [ + , + ], + avatar: , + }, + { + title: '绑定支付宝', + description: '当前未绑定支付宝账号', + actions: [ + , + ], + avatar: , + }, + { + title: '绑定钉钉', + description: '当前未绑定钉钉账号', + actions: [ + , + ], + avatar: , + }, +]; +const BindingView: React.FC = () => { return ( ( ); -const NotificationView: React.FC = () => { - const getData = () => { - return [ - { - title: '用户消息', - description: '其他用户的消息将以站内信的形式通知', - actions: [Action], - }, - { - title: '系统消息', - description: '系统消息将以站内信的形式通知', - actions: [Action], - }, - { - title: '待办任务', - description: '待办任务将以站内信的形式通知', - actions: [Action], - }, - ]; - }; +const notificationData = [ + { + title: '用户消息', + description: '其他用户的消息将以站内信的形式通知', + actions: [Action], + }, + { + title: '系统消息', + description: '系统消息将以站内信的形式通知', + actions: [Action], + }, + { + title: '待办任务', + description: '待办任务将以站内信的形式通知', + actions: [Action], + }, +]; - const data = getData(); +const NotificationView: React.FC = () => { + const data = notificationData; return ( > itemLayout="horizontal" diff --git a/src/pages/account/settings/components/security.tsx b/src/pages/account/settings/components/security.tsx index d5c6398f..a7a26e14 100644 --- a/src/pages/account/settings/components/security.tsx +++ b/src/pages/account/settings/components/security.tsx @@ -1,4 +1,4 @@ -import { List } from 'antd'; +import { Button, List } from 'antd'; import React from 'react'; type Unpacked = T extends (infer U)[] ? U : T; @@ -9,61 +9,61 @@ const passwordStrength = { weak: 弱 Weak, }; -const SecurityView: React.FC = () => { - const getData = () => [ - { - title: '账户密码', - description: ( - <> - 当前密码强度: - {passwordStrength.strong} - - ), - actions: [ - - 修改 - , - ], - }, - { - title: '密保手机', - description: `已绑定手机:138****8293`, - actions: [ - - 修改 - , - ], - }, - { - title: '密保问题', - description: '未设置密保问题,密保问题可有效保护账户安全', - actions: [ - - 设置 - , - ], - }, - { - title: '备用邮箱', - description: `已绑定邮箱:ant***sign.com`, - actions: [ - - 修改 - , - ], - }, - { - title: 'MFA 设备', - description: '未绑定 MFA 设备,绑定后,可以进行二次确认', - actions: [ - - 绑定 - , - ], - }, - ]; +const securityData = [ + { + title: '账户密码', + description: ( + <> + 当前密码强度: + {passwordStrength.strong} + + ), + actions: [ + , + ], + }, + { + title: '密保手机', + description: `已绑定手机:138****8293`, + actions: [ + , + ], + }, + { + title: '密保问题', + description: '未设置密保问题,密保问题可有效保护账户安全', + actions: [ + , + ], + }, + { + title: '备用邮箱', + description: `已绑定邮箱:ant***sign.com`, + actions: [ + , + ], + }, + { + title: 'MFA 设备', + description: '未绑定 MFA 设备,绑定后,可以进行二次确认', + actions: [ + , + ], + }, +]; - const data = getData(); +const SecurityView: React.FC = () => { + const data = securityData; return ( > itemLayout="horizontal" diff --git a/src/pages/account/settings/index.tsx b/src/pages/account/settings/index.tsx index e6242bf3..8fdd6bbc 100644 --- a/src/pages/account/settings/index.tsx +++ b/src/pages/account/settings/index.tsx @@ -13,6 +13,17 @@ type SettingsState = { selectKey: SettingsStateKeys; }; +const menuMap: Record = { + base: '基本设置', + security: '安全设置', + binding: '账号绑定', + notification: '新消息通知', +}; +const menuItems = Object.keys(menuMap).map((item) => ({ + key: item, + label: menuMap[item], +})); + const SettingsContent: React.FC<{ selectKey: SettingsStateKeys }> = ({ selectKey, }) => { @@ -32,12 +43,6 @@ const SettingsContent: React.FC<{ selectKey: SettingsStateKeys }> = ({ const Settings: React.FC = () => { const { styles } = useStyles(); - const menuMap: Record = { - base: '基本设置', - security: '安全设置', - binding: '账号绑定', - notification: '新消息通知', - }; const [initConfig, setInitConfig] = useState({ mode: 'inline', selectKey: 'base', @@ -75,12 +80,6 @@ const Settings: React.FC = () => { window.removeEventListener('resize', handler); }; }, []); - const getMenu = () => { - return Object.keys(menuMap).map((item) => ({ - key: item, - label: menuMap[item], - })); - }; return (
    { selectKey: key as SettingsStateKeys, })); }} - items={getMenu()} + items={menuItems} />
    diff --git a/src/pages/dashboard/analysis/components/Charts/index.tsx b/src/pages/dashboard/analysis/components/Charts/index.tsx deleted file mode 100644 index d407c39b..00000000 --- a/src/pages/dashboard/analysis/components/Charts/index.tsx +++ /dev/null @@ -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 }; diff --git a/src/pages/dashboard/analysis/components/IntroduceRow.tsx b/src/pages/dashboard/analysis/components/IntroduceRow.tsx index fe1031d3..b278e4fb 100644 --- a/src/pages/dashboard/analysis/components/IntroduceRow.tsx +++ b/src/pages/dashboard/analysis/components/IntroduceRow.tsx @@ -5,7 +5,8 @@ import { formatNumber } from '@/utils/format'; import type { DataItem } from '../data.d'; import useStyles from '../style.style'; import Yuan from '../utils/Yuan'; -import { ChartCard, Field } from './Charts'; +import ChartCard from './Charts/ChartCard'; +import Field from './Charts/Field'; import Trend from './Trend'; const topColResponsiveProps = { diff --git a/src/pages/dashboard/analysis/components/NumberInfo/index.tsx b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx index e6d09b58..ffcc184f 100644 --- a/src/pages/dashboard/analysis/components/NumberInfo/index.tsx +++ b/src/pages/dashboard/analysis/components/NumberInfo/index.tsx @@ -5,6 +5,7 @@ import useStyles from './index.style'; export type NumberInfoProps = { title?: React.ReactNode | string; subTitle?: React.ReactNode | string; + renderSubTitle?: () => React.ReactNode; total?: React.ReactNode | string; status?: 'up' | 'down'; theme?: string; @@ -17,6 +18,7 @@ const NumberInfo: React.FC = ({ theme, title, subTitle, + renderSubTitle, total, subTotal, status, @@ -25,6 +27,7 @@ const NumberInfo: React.FC = ({ ...rest }) => { const { styles } = useStyles(); + const subTitleNode = renderSubTitle?.() ?? subTitle; return (
    = ({ {title}
    )} - {subTitle && ( + {subTitleNode && (
    - {subTitle} + {subTitleNode}
    )}
    React.ReactNode; salesType: 'all' | 'online' | 'stores'; salesPieData: DataItem[]; handleChangeSalesType?: (value: 'all' | 'online' | 'stores') => void; }) => { const { styles } = useStyles(); + const dropdownGroup = renderDropdownGroup(); + const extra = ( +
    + {dropdownGroup} + +
    + ); return ( - {dropdownGroup} - -
    - } + extra={extra} > 销售额 ( + + 搜索用户数 + + + + +); + +const renderAverageSearchSubtitle = () => ( + + 人均搜索次数 + + + + +); + const TopSearch = ({ loading, visitData2, searchData, - dropdownGroup, + renderDropdownGroup, }: { loading: boolean; visitData2: DataItem[]; - dropdownGroup: React.ReactNode; + renderDropdownGroup: () => React.ReactNode; searchData: DataItem[]; }) => { + const dropdownGroup = renderDropdownGroup(); const columns = [ { title: '排名', @@ -92,18 +119,7 @@ const TopSearch = ({ }} > - 搜索用户数 - - - - - } + renderSubTitle={renderSearchUserSubtitle} gap={8} total={formatNumber(12321)} status="up" @@ -131,18 +147,7 @@ const TopSearch = ({ }} > - 人均搜索次数 - - - - - } + renderSubTitle={renderAverageSearchSubtitle} total={2.7} status="down" subTotal={26.2} diff --git a/src/pages/dashboard/analysis/index.tsx b/src/pages/dashboard/analysis/index.tsx index b38672b5..dc97b8dd 100644 --- a/src/pages/dashboard/analysis/index.tsx +++ b/src/pages/dashboard/analysis/index.tsx @@ -29,7 +29,7 @@ const Analysis: FC = () => { const [salesType, setSalesType] = useState('all'); const [currentTabKey, setCurrentTabKey] = useState(''); const [rangePickerValue, setRangePickerValue] = useState( - getTimeDistance('year'), + () => getTimeDistance('year'), ); const { isLoading: loading, data } = useQuery({ queryKey: ['dashboard-analysis'], @@ -71,7 +71,7 @@ const Analysis: FC = () => { : data?.salesTypeDataOffline; } - const dropdownGroup = ( + const renderDropdownGroup = () => ( = () => { loading={loading} visitData2={data?.visitData2 || []} searchData={data?.searchData || []} - dropdownGroup={dropdownGroup} + renderDropdownGroup={renderDropdownGroup} /> = ({ children }) => { - const spanRef = useRef(null); - - useEffect(() => { - if (spanRef.current) { - spanRef.current.innerHTML = yuan(children); - } - }, [children]); - - return ; + return {formatYuan(children)}; }; export default Yuan; diff --git a/src/pages/dashboard/analysis/utils/utils.ts b/src/pages/dashboard/analysis/utils/utils.ts index 6a1d4d5e..114e8ea0 100644 --- a/src/pages/dashboard/analysis/utils/utils.ts +++ b/src/pages/dashboard/analysis/utils/utils.ts @@ -3,7 +3,7 @@ import dayjs from 'dayjs'; type RangePickerValue = RangePickerProps['value']; -export function fixedZero(val: number) { +function fixedZero(val: number) { return val * 1 < 10 ? `0${val}` : val; } diff --git a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx index 1f6ea808..ac689fcc 100644 --- a/src/pages/dashboard/monitor/components/ActiveChart/index.tsx +++ b/src/pages/dashboard/monitor/components/ActiveChart/index.tsx @@ -20,14 +20,16 @@ function getActiveData() { const ActiveChart = () => { const timerRef = useRef(null); const { styles } = useStyles(); - const [activeData, setActiveData] = useState<{ x: string; y: number }[]>([]); + const [activeData, setActiveData] = useState<{ x: string; y: number }[]>(() => + getActiveData(), + ); useEffect(() => { const loopData = () => { setActiveData(getActiveData()); timerRef.current = window.setTimeout(loopData, 2000); }; - loopData(); + timerRef.current = window.setTimeout(loopData, 2000); return () => { if (timerRef.current) { clearTimeout(timerRef.current); diff --git a/src/pages/dashboard/workplace/index.tsx b/src/pages/dashboard/workplace/index.tsx index 891a25ff..ab2feef8 100644 --- a/src/pages/dashboard/workplace/index.tsx +++ b/src/pages/dashboard/workplace/index.tsx @@ -118,7 +118,7 @@ const Workplace: FC = () => { avatar={} title={ - + {item.user.name}   @@ -281,12 +281,12 @@ const Workplace: FC = () => { {projectNotice.map((item) => { return ( - + {item.member.substring(0, 3)} - + ); })} diff --git a/src/pages/form/advanced-form/index.tsx b/src/pages/form/advanced-form/index.tsx index 2f761f16..6370cfda 100644 --- a/src/pages/form/advanced-form/index.tsx +++ b/src/pages/form/advanced-form/index.tsx @@ -10,7 +10,7 @@ import { ProFormText, ProFormTimePicker, } from '@ant-design/pro-components'; -import { Card, Col, message, Popover, Row } from 'antd'; +import { Button, Card, Col, message, Popover, Row } from 'antd'; import type { FC } from 'react'; import { useRef, useState } from 'react'; import { fakeSubmitForm } from './service'; @@ -161,16 +161,13 @@ const AdvancedForm: FC> = () => { valueType: 'option', render: (_, record: TableFormDateType, _index, action) => { return [ - { - e.preventDefault(); - action?.startEditable(record.key); - }} + type="link" + onClick={() => action?.startEditable(record.key)} > 编辑 - , + , ]; }, }, diff --git a/src/pages/form/basic-form/index.test.tsx b/src/pages/form/basic-form/index.test.tsx index f990e855..b89813da 100644 --- a/src/pages/form/basic-form/index.test.tsx +++ b/src/pages/form/basic-form/index.test.tsx @@ -12,16 +12,12 @@ vi.mock('@ant-design/pro-components', () => ({
    ), ProForm: ({ children, onFinish }: any) => ( -
    { - e.preventDefault(); - onFinish?.(); - }} - > +
    {children} - - + +
    ), ProFormText: ({ label, name }: any) => (
    {label}
    diff --git a/src/pages/list/basic-list/index.tsx b/src/pages/list/basic-list/index.tsx index 3416b23f..36f5c171 100644 --- a/src/pages/list/basic-list/index.tsx +++ b/src/pages/list/basic-list/index.tsx @@ -171,9 +171,9 @@ const BasicList: FC = () => { ], }} > - + ); }; @@ -229,16 +229,13 @@ const BasicList: FC = () => { renderItem={(item) => ( { - e.preventDefault(); - showEditModal(item); - }} + type="link" + onClick={() => showEditModal(item)} > 编辑 - , + , renderMoreBtn(item), ]} > diff --git a/src/pages/list/card-list/index.tsx b/src/pages/list/card-list/index.tsx index bdeae33d..62e37641 100644 --- a/src/pages/list/card-list/index.tsx +++ b/src/pages/list/card-list/index.tsx @@ -7,6 +7,8 @@ import { queryFakeList } from './service'; import useStyles from './style.style'; const { Paragraph } = Typography; +const nullData: Partial = {}; + const CardList = () => { const { styles } = useStyles(); const { data, isLoading: loading } = useQuery({ @@ -22,21 +24,21 @@ const CardList = () => { 提供跨越设计与开发的体验解决方案。

    ); - const nullData: Partial = {}; return (
    @@ -79,12 +80,12 @@ const CardList = () => { hoverable className={styles.card} actions={[ - + , + , ]} > { src={item.avatar} /> } - title={{item.title}} + title={{item.title}} description={ { }; const list = data?.list || []; + const listItemExtra =
    ; const setOwner = () => { form.setFieldsValue({ @@ -88,37 +118,6 @@ const Articles: FC = () => { }); }; - 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 loadMoreDom = list.length > 0 && (
    @@ -245,7 +235,6 @@ const Articles: FC = () => { loading={loading} rowKey="id" itemLayout="vertical" - loadMore={loadMoreDom} dataSource={list} renderItem={(item) => ( { , , ]} - extra={
    } + extra={listItemExtra} > { )} /> + {loadMoreDom} ); diff --git a/src/pages/list/search/projects/index.tsx b/src/pages/list/search/projects/index.tsx index f76f1c3b..17953223 100644 --- a/src/pages/list/search/projects/index.tsx +++ b/src/pages/list/search/projects/index.tsx @@ -12,6 +12,17 @@ import useStyles from './style.style'; const FormItem = Form.Item; const { Paragraph } = Typography; const getKey = (id: string, index: number) => `${id}-${index}`; +const formItemLayout = { + wrapperCol: { + xs: { + span: 24, + }, + sm: { + span: 16, + }, + }, +}; + const Projects: FC = () => { const { styles } = useStyles(); const [filters, setFilters] = useState<{ @@ -50,7 +61,7 @@ const Projects: FC = () => { cover={{item.title}} > {item.title}} + title={{item.title}} description={ { )} /> ); - const formItemLayout = { - wrapperCol: { - xs: { - span: 24, - }, - sm: { - span: 16, - }, - }, - }; return (
    diff --git a/src/pages/profile/advanced/index.tsx b/src/pages/profile/advanced/index.tsx index 43239156..51ae36a0 100644 --- a/src/pages/profile/advanced/index.tsx +++ b/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: 12421 }, + { key: '4', label: '关联单据', children: 12421 }, { key: '5', label: '生效日期', children: '2017-07-07 ~ 2017-08-08' }, { key: '6', label: '备注', children: '请于两个工作日内确认' }, ]; @@ -305,7 +305,9 @@ const Advanced: FC = () => { }} />
    - 催一下 +
    ); diff --git a/src/pages/result/fail/index.tsx b/src/pages/result/fail/index.tsx index 5c924358..2c97967c 100644 --- a/src/pages/result/fail/index.tsx +++ b/src/pages/result/fail/index.tsx @@ -22,15 +22,16 @@ export default () => { className={styles.error_icon} /> 您的账户已被冻结 - 立即解冻 - +
    { className={styles.error_icon} /> 您的账户还不具备申请资格 - 立即升级 - +
    ); diff --git a/src/pages/result/success/index.tsx b/src/pages/result/success/index.tsx index cc260105..387bb401 100644 --- a/src/pages/result/success/index.tsx +++ b/src/pages/result/success/index.tsx @@ -51,7 +51,7 @@ const Success: React.FC = () => { }} > 周毛毛 - +
    ); diff --git a/src/pages/table-list/index.tsx b/src/pages/table-list/index.tsx index b1459967..4ce8f49f 100644 --- a/src/pages/table-list/index.tsx +++ b/src/pages/table-list/index.tsx @@ -58,16 +58,15 @@ const TableList: React.FC = () => { dataIndex: 'name', render: (dom, entity) => { return ( - { - e.preventDefault(); + ); }, }, @@ -197,12 +196,12 @@ const TableList: React.FC = () => { render: (_, record) => [ + } key="config" onOk={actionRef.current?.reload} diff --git a/src/pages/user/login/index.tsx b/src/pages/user/login/index.tsx index e4218395..0b762c1b 100644 --- a/src/pages/user/login/index.tsx +++ b/src/pages/user/login/index.tsx @@ -19,7 +19,7 @@ import { useIntl, useModel, } from '@umijs/max'; -import { Alert, App, Tabs } from 'antd'; +import { Alert, App, Button, Tabs } from 'antd'; import { createStyles } from 'antd-style'; import React, { startTransition, useState } from 'react'; import { Footer } from '@/components'; @@ -27,6 +27,24 @@ import { login } from '@/services/ant-design-pro/api'; import { getFakeCaptcha } from '@/services/ant-design-pro/login'; import Settings from '../../../../config/defaultSettings'; +/** + * Validate redirect URL to prevent open redirect attacks. + * Only allow same-origin relative paths starting with '/'. + */ +const getSafeRedirectUrl = (redirect: string | null): string => { + if (!redirect?.startsWith('/')) return '/'; + + if (redirect.startsWith('//')) return '/'; + + try { + const parsed = new URL(redirect, window.location.origin); + if (parsed.origin !== window.location.origin) return '/'; + return `${parsed.pathname}${parsed.search}${parsed.hash}`; + } catch { + return '/'; + } +}; + const useStyles = createStyles(({ token }) => { return { action: { @@ -117,27 +135,6 @@ const Login: React.FC = () => { const { message } = App.useApp(); const intl = useIntl(); - /** - * Validate redirect URL to prevent open redirect attacks - * Only allow same-origin relative paths starting with '/' - */ - const getSafeRedirectUrl = (redirect: string | null): string => { - if (!redirect?.startsWith('/')) return '/'; - - // Block protocol-relative URLs (//example.com) - if (redirect.startsWith('//')) return '/'; - - try { - const parsed = new URL(redirect, window.location.origin); - // Only allow same-origin URLs - if (parsed.origin !== window.location.origin) return '/'; - // Return the path with query and hash preserved - return `${parsed.pathname}${parsed.search}${parsed.hash}`; - } catch { - return '/'; - } - }; - const fetchUserInfo = async () => { const userInfo = await initialState?.fetchUserInfo?.(); if (userInfo) { @@ -395,17 +392,18 @@ const Login: React.FC = () => { defaultMessage="自动登录" /> - - +
    diff --git a/src/pages/user/register-result/index.tsx b/src/pages/user/register-result/index.tsx index 33ff7d82..9ba9f160 100644 --- a/src/pages/user/register-result/index.tsx +++ b/src/pages/user/register-result/index.tsx @@ -9,11 +9,9 @@ const RegisterResult: React.FC> = () => { const actions = (
    - - - + diff --git a/src/pages/user/register/index.tsx b/src/pages/user/register/index.tsx index de1fde95..2dffe3ff 100644 --- a/src/pages/user/register/index.tsx +++ b/src/pages/user/register/index.tsx @@ -1,4 +1,4 @@ -import { useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { history, Link } from '@umijs/max'; import { Button, @@ -30,6 +30,34 @@ const passwordProgressMap: { pass: 'normal', poor: 'exception', }; + +const getPasswordStatus = (value?: string) => { + if (value && value.length > 9) { + return 'ok'; + } + if (value && value.length > 5) { + return 'pass'; + } + return 'poor'; +}; + +const PasswordProgress: React.FC<{ + value?: string; + styles: Record; +}> = ({ value, styles }) => { + const passwordStatus = getPasswordStatus(value); + return value?.length ? ( +
    + 100 ? 100 : value.length * 10} + showInfo={false} + /> +
    + ) : null; +}; + const Register: FC = () => { const { styles } = useStyles(); const [count, setCount]: [number, any] = useState(0); @@ -38,6 +66,7 @@ const Register: FC = () => { const [popover, setPopover]: [boolean, any] = useState(false); const confirmDirty = false; let interval: number | undefined; + const queryClient = useQueryClient(); const passwordStatusMap = { ok: ( @@ -75,16 +104,6 @@ const Register: FC = () => { } }, 1000); }; - const getPasswordStatus = () => { - const value = form.getFieldValue('password'); - if (value && value.length > 9) { - return 'ok'; - } - if (value && value.length > 5) { - return 'pass'; - } - return 'poor'; - }; const { isPending: submitting, mutate: register } = useMutation({ mutationFn: (formValues: Store) => { const payload = { @@ -99,6 +118,7 @@ const Register: FC = () => { }, onSuccess: (data, params) => { if (data.status === 'ok') { + queryClient.invalidateQueries({ queryKey: ['current-user'] }); message.success('注册成功!'); history.push({ pathname: `/user/register-result?account=${params.mail}`, @@ -139,22 +159,8 @@ const Register: FC = () => { const changePrefix = (value: string) => { setPrefix(value); }; - const renderPasswordProgress = () => { - const value = form.getFieldValue('password'); - const passwordStatus = getPasswordStatus(); - return value?.length ? ( -
    - 100 ? 100 : value.length * 10} - showInfo={false} - /> -
    - ) : null; - }; + const password = Form.useWatch('password', form); + const passwordStatus = getPasswordStatus(password); return (

    注册

    @@ -188,8 +194,8 @@ const Register: FC = () => { padding: '4px 0', }} > - {passwordStatusMap[getPasswordStatus()]} - {renderPasswordProgress()} + {passwordStatusMap[passwordStatus]} +