diff --git a/package.json b/package.json index 5a24d4a..c873f73 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-core", - "version": "0.4.62", + "version": "0.4.63", "files": [ "build" ], @@ -43,6 +43,7 @@ "@kne/use-click-outside": "^0.2.1", "@kne/use-control-value": "^0.1.7", "@kne/use-ref-callback": "^0.1.2", + "@kne/use-resize": "^0.1.1", "@kne/use-simulation-blur": "^0.1.2", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", diff --git a/src/bootstrap.js b/src/bootstrap.js index af260f8..bd9a5f5 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -2,6 +2,7 @@ import {ajax} from "./preset"; import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import './index.css'; const root = ReactDOM.createRoot(document.getElementById("root")); diff --git a/src/common/components/SearchInput/index.js b/src/common/components/SearchInput/index.js index 212b646..37eebb0 100644 --- a/src/common/components/SearchInput/index.js +++ b/src/common/components/SearchInput/index.js @@ -10,6 +10,7 @@ const SearchInput = ({ }) => { const [state, setState] = useState(value); const valueRef = useRef(value); + const compositionRef = useRef(false); const debouncedFunc = useDebouncedCallback(onSearch, debounce); useEffect(() => { if (value !== valueRef.current) { @@ -31,6 +32,16 @@ const SearchInput = ({ const value = e.target.value; valueRef.current = value; setState(value); + if (!compositionRef.current) { + debouncedFunc(value.trim()); + } + }} + onCompositionStart={() => { + compositionRef.current = true; + }} + onCompositionEnd={(e) => { + compositionRef.current = false; + const value = e.target.value; debouncedFunc(value.trim()); }} onSearch={(value) => { diff --git a/src/components/FormInfo/withLocale.js b/src/components/FormInfo/withLocale.js index 09c1439..4d4b47e 100644 --- a/src/components/FormInfo/withLocale.js +++ b/src/components/FormInfo/withLocale.js @@ -5,7 +5,7 @@ import enUS from './locale/en-US'; const withLocale = createWithIntlProvider({ defaultLocale: 'zh-CN', messages: { 'zh-CN': zhCN, 'en-US': enUS - }, namespace: 'form-info' + }, namespace: 'FormInfo' }); export default withLocale; diff --git a/src/components/InfoPage/README.md b/src/components/InfoPage/README.md index f9241a8..9aa24ae 100644 --- a/src/components/InfoPage/README.md +++ b/src/components/InfoPage/README.md @@ -493,122 +493,126 @@ render(); ```jsx const { default: InfoPage, CentralContent } = _InfoPage; -const { Tag, Avatar, Space } = antd; +const { Tag, Avatar, Space, Modal, Button } = antd; +const { useState } = React; const BaseExample = () => { + const [open, setOpen] = useState(false); + const baseInfo = ( + + ( + + {value[0]} + {value} + + ), + span: 10 + }, + { + name: 'gender', + title: '性别' + }, + { + name: 'birthday', + title: '出生日期', + format: 'date' + }, + { + name: 'idCard', + title: '身份证号', + render: value => value.replace(/(\d{6})(\d{8})(\d{4})/, '$1********$3') + }, + { + name: 'maritalStatus', + title: '婚姻状况' + }, + { + name: 'education', + title: '学历' + }, + { + name: 'graduationSchool', + title: '毕业院校' + }, + { + name: 'major', + title: '专业' + }, + { + name: 'entryDate', + title: '入职日期', + format: 'date' + }, + { + name: 'workYears', + title: '工作年限', + format: 'number-suffix:年' + }, + { + name: 'phone', + title: '联系电话', + render: value => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') + }, + { + name: 'email', + title: '电子邮箱' + }, + { + name: 'address', + title: '家庭住址', + block: true + }, + { + name: 'emergencyContact', + title: '紧急联系人' + }, + { + name: 'emergencyPhone', + title: '紧急联系电话', + render: value => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') + }, + { + name: 'emergencyRelation', + title: '与本人关系' + } + ]} + /> + + ); return ( - - ( - - {value[0]} - {value} - - ), - span: 10 - }, - { - name: 'gender', - title: '性别' - }, - { - name: 'birthday', - title: '出生日期', - format: 'date' - }, - { - name: 'idCard', - title: '身份证号', - render: (value) => value.replace(/(\d{6})(\d{8})(\d{4})/, '$1********$3') - }, - { - name: 'maritalStatus', - title: '婚姻状况' - }, - { - name: 'education', - title: '学历' - }, - { - name: 'graduationSchool', - title: '毕业院校' - }, - { - name: 'major', - title: '专业' - }, - { - name: 'entryDate', - title: '入职日期', - format: 'date' - }, - { - name: 'workYears', - title: '工作年限', - format: 'number-suffix:年' - }, - { - name: 'phone', - title: '联系电话', - render: (value) => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') - }, - { - name: 'email', - title: '电子邮箱' - }, - { - name: 'address', - title: '家庭住址', - block: true - }, - { - name: 'emergencyContact', - title: '紧急联系人' - }, - { - name: 'emergencyPhone', - title: '紧急联系电话', - render: (value) => value.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3') - }, - { - name: 'emergencyRelation', - title: '与本人关系' - } - ]} - /> - - + {baseInfo} { { name: 'team', title: '所属团队' }, { name: 'workLocation', title: '工作地点' }, { name: 'office', title: '办公室位置' }, - { name: 'workStatus', title: '工作状态', render: (value) => {value} }, + { name: 'workStatus', title: '工作状态', render: value => {value} }, { name: 'contractType', title: '合同类型' }, { name: 'contractStartDate', title: '合同开始日期', format: 'date' }, { name: 'contractEndDate', title: '合同结束日期', format: 'date' }, - { name: 'probationPeriod', title: '试用期状态', render: (value) => {value} } + { name: 'probationPeriod', title: '试用期状态', render: value => {value} } ]} /> @@ -664,9 +668,9 @@ const BaseExample = () => { { name: 'baseSalary', title: '基本月薪', format: 'number-useGrouping:true-suffix:元', span: 12 }, { name: 'performanceBonus', title: '绩效奖金', format: 'number-useGrouping:true-suffix:元/月', span: 12 }, { name: 'annualBonus', title: '年终奖金', format: 'number-useGrouping:true-suffix:元', block: true }, - { name: 'socialInsurance', title: '社会保险', render: (value) => {value} }, + { name: 'socialInsurance', title: '社会保险', render: value => {value} }, { name: 'housingFund', title: '公积金', format: 'number-useGrouping:true-suffix:元/月' }, - { name: 'medicalInsurance', title: '医疗保险', render: (value) => {value} }, + { name: 'medicalInsurance', title: '医疗保险', render: value => {value} }, { name: 'mealAllowance', title: '餐补', format: 'number-useGrouping:true-suffix:元/月' }, { name: 'transportAllowance', title: '交通补贴', format: 'number-useGrouping:true-suffix:元/月' }, { name: 'stockOptions', title: '股票期权', format: 'number-useGrouping:true-suffix:股', block: true }, @@ -674,6 +678,16 @@ const BaseExample = () => { ]} /> + + + + setOpen(false)}>{baseInfo} + ); }; diff --git a/src/components/Layout/Layout.js b/src/components/Layout/Layout.js index 5f7ef4f..0ae66f0 100644 --- a/src/components/Layout/Layout.js +++ b/src/components/Layout/Layout.js @@ -1,5 +1,5 @@ import {Alert, Button, Col, Layout as AntdLayout, Row} from "antd"; -import {useCallback, useEffect, useState} from "react"; +import {useCallback, useEffect, useState, useMemo} from "react"; import {defaultProps, Provider} from "./context"; import Navigation, {navigationHeight} from "@components/Navigation"; import {getScrollEl} from "@common/utils/importantContainer"; @@ -10,6 +10,23 @@ import style from "./style.module.scss"; import HelperGuide from "@components/HelperGuide"; import {usePermissions} from "../Permissions"; +const useIsMobile = (isMobileProp) => { + const [windowWidth, setWindowWidth] = useState(window.innerWidth); + + useEffect(() => { + const handleResize = () => setWindowWidth(window.innerWidth); + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return useMemo(() => { + if (typeof isMobileProp === 'boolean') { + return isMobileProp; + } + return windowWidth < 768; + }, [isMobileProp, windowWidth]); +}; + const {Content} = AntdLayout; const ErrorComponent = () => { @@ -31,10 +48,12 @@ const ErrorBoundary = (props) => { return ; }; -const Layout = ({className, children, theme, navigation = {}}) => { +const Layout = ({className, children, theme, navigation = {}, isMobile: isMobileProp}) => { const [scrollLeft, setScrollLeft] = useState(0); const [pageProps, _setPageProps] = useState(Object.assign({}, defaultProps)); const {permissions} = usePermissions(); + const isMobile = useIsMobile(isMobileProp); + const setPageProps = useCallback((value) => { return _setPageProps((pageProps) => { return Object.assign({}, pageProps, value); @@ -52,7 +71,9 @@ const Layout = ({className, children, theme, navigation = {}}) => { }, []); return ( { )} - +
@@ -95,7 +116,7 @@ const Layout = ({className, children, theme, navigation = {}}) => { }} > - + { }, [pathModuleName]); }; -const Menu = () => { +const Menu = ({isMobile}) => { const {pageProps, setPageProps} = useContext(); const {menu, menuOpen, menuWidth, menuCloseWidth, menuFixed, menuCloseButton} = pageProps; + const [drawerVisible, setDrawerVisible] = useState(false); const location = useLocation(); const pathModuleName = location.pathname.split("/")[1]; + // 移动端渲染 + if (isMobile && menu) { + return ( + <> + + + + + } + > + + + + {isMobile ? '移动端模式' : '桌面端模式'} + + + 当窗口宽度小于768px时自动切换为移动端模式 + + + {isMobile ? '左侧菜单隐藏,显示"菜单"按钮,点击后以Drawer形式展示' : '左侧固定菜单显示'} + + + Drawer默认关闭,点击按钮后打开 + + + 可通过isMobile属性强制指定为移动端或桌面端模式 + + + 移动端模式下内容区边距和圆角会自动调整 + + + + + + + 这是移动端适配的示例内容。在实际业务中,这里会显示具体的业务数据和操作界面。 + + + + + + + + + + ); +}; + +render( + + + +); + +``` + ### API ### Layout @@ -1025,6 +1188,7 @@ Layout 组件是页面布局的容器组件,包裹所有页面内容并提供 | children | ReactNode | 是 | - | 子组件,一般放置 Page 组件 | | className | string | 否 | - | 自定义类名 | | theme | object | 否 | - | 主题样式配置 | +| isMobile | boolean | 否 | - | 强制指定是否为移动端模式。不传则自动检测(窗口宽度<768px时为移动端) | ### Page diff --git a/src/components/Layout/context.js b/src/components/Layout/context.js index 99433d4..465a3fb 100644 --- a/src/components/Layout/context.js +++ b/src/components/Layout/context.js @@ -25,6 +25,7 @@ export const defaultProps = { optionNoPadding: false, optionFixed: true, optionFooter: null, + isMobile: false, }; export const context = createContext({}); diff --git a/src/components/Layout/doc/example.json b/src/components/Layout/doc/example.json index 7ec9d9d..e3ba8c2 100644 --- a/src/components/Layout/doc/example.json +++ b/src/components/Layout/doc/example.json @@ -197,6 +197,25 @@ "packageName": "antd" } ] + }, + { + "title": "移动端适配", + "description": "展示 Layout 组件的移动端响应式适配功能,菜单在移动端以Drawer形式显示", + "code": "./mobile.js", + "scope": [ + { + "name": "layout", + "packageName": "@components/Layout" + }, + { + "name": "antd", + "packageName": "antd" + }, + { + "name": "global", + "packageName": "@components/Global" + } + ] } ] } diff --git a/src/components/Layout/doc/mobile.js b/src/components/Layout/doc/mobile.js new file mode 100644 index 0000000..9b55867 --- /dev/null +++ b/src/components/Layout/doc/mobile.js @@ -0,0 +1,147 @@ +const { default: Layout, Page, Menu } = layout; +const { Flex, Space, Button, Typography, Card, Descriptions } = antd; +const { PureGlobal } = global; +const { useState } = React; +const { Title, Paragraph } = Typography; + +const MobileExample = () => { + const [isMobile, setIsMobile] = useState(true); + + return ( + + + } + title="组织管理" + titleExtra={ + + + + + } + > + + + + {isMobile ? '移动端模式' : '桌面端模式'} + + + 当窗口宽度小于768px时自动切换为移动端模式 + + + {isMobile ? '左侧菜单隐藏,显示"菜单"按钮,点击后以Drawer形式展示' : '左侧固定菜单显示'} + + + Drawer默认关闭,点击按钮后打开 + + + 可通过isMobile属性强制指定为移动端或桌面端模式 + + + 移动端模式下内容区边距和圆角会自动调整 + + + + + + + 这是移动端适配的示例内容。在实际业务中,这里会显示具体的业务数据和操作界面。 + + + + + + + + + + ); +}; + +render( + + + +); diff --git a/src/components/Layout/doc/state-bar-page.js b/src/components/Layout/doc/state-bar-page.js index 58d9ad8..2e11655 100644 --- a/src/components/Layout/doc/state-bar-page.js +++ b/src/components/Layout/doc/state-bar-page.js @@ -6,7 +6,15 @@ const { Text } = Typography; const StateBarPageExample = () => { return ( - + [{ + value: 'order-detail-help', + content: '这是一个订单详情页面,可以查看和管理订单详情信息。', + url: 'https://example.com/help/order-detail' + }] + } + }}> ); ``` +- 移动端适配 +- 展示Navigation组件在移动端设备上的响应式布局 +- _Navigation(@components/Navigation),global(@components/Global),antd(antd) + +```jsx +const { default: Navigation } = _Navigation; +const { PureGlobal } = global; +const { Space, Switch, Divider } = antd; + +const menuList = [ + { + key: "dashboard", + title: "仪表盘", + path: "/dashboard", + icon: 📊, + }, + { + key: "products", + title: "产品管理", + path: "/products", + icon: 📦, + }, + { + key: "orders", + title: "订单管理", + path: "/orders", + icon: 📋, + }, + { + key: "customers", + title: "客户管理", + path: "/customers", + icon: 👥, + }, + { + key: "settings", + title: "系统设置", + path: "/settings", + icon: ⚙️, + }, +]; + +const MobileResponsiveExample = () => { + const [forceMobile, setForceMobile] = React.useState(undefined); + + return ( + +
+
+ +
+ 移动端模式控制: +
+ + 自动检测(根据窗口宽度) + setForceMobile(checked)} + checkedChildren="强制移动端" + unCheckedChildren="自动" + /> + +
+ 开启开关可强制切换到移动端布局,关闭则根据窗口宽度自动检测 +
+
+
+ + + +
+

1. 调整浏览器窗口宽度至小于 768px,导航将自动切换为移动端模式

+

2. 或者使用上方开关强制指定为移动端模式

+

3. 在移动端模式下,导航菜单将显示为汉堡菜单,点击后从右侧滑出

+
+
+
+ ); +}; + +render(); + +``` + +- 强制移动端模式 +- 展示如何通过isMobile属性强制指定导航栏的显示模式 +- _Navigation(@components/Navigation),global(@components/Global),antd(antd) + +```jsx +const { default: Navigation } = _Navigation; +const { PureGlobal } = global; +const { Space, Switch } = antd; +const { useState } = React; + +const menuList = [ + { + key: "dashboard", + title: "仪表盘", + path: "/dashboard", + icon: 📊, + }, + { + key: "products", + title: "产品管理", + path: "/products", + icon: 📦, + }, + { + key: "orders", + title: "订单管理", + path: "/orders", + icon: 📋, + }, + { + key: "customers", + title: "客户管理", + path: "/customers", + icon: 👥, + }, + { + key: "settings", + title: "系统设置", + path: "/settings", + icon: ⚙️, + }, +]; + +const ForceMobileExample = () => { + const [isMobile, setIsMobile] = useState(false); + + return ( + +
+
+ +
+ 强制移动端模式: +
+ +
+ 通过 isMobile 属性可以强制指定导航栏的显示模式 +
+
+
+ + + +
+

点击上方开关可以强制切换导航栏的显示模式:

+
    +
  • 关闭开关:显示桌面端水平菜单
  • +
  • 打开开关:显示移动端汉堡菜单(点击后显示下拉菜单)
  • +
+

+ 注意:当不指定 isMobile 属性时,组件会根据窗口宽度自动检测 +

+
+
+
+ ); +}; + +render(); + +``` + ### API |属性名|说明|类型|默认值| diff --git a/src/components/Navigation/doc/example.json b/src/components/Navigation/doc/example.json index 07ac1b6..301c105 100644 --- a/src/components/Navigation/doc/example.json +++ b/src/components/Navigation/doc/example.json @@ -125,6 +125,44 @@ "packageName": "antd" } ] + }, + { + "title": "移动端适配", + "description": "展示Navigation组件在移动端设备上的响应式布局", + "code": "./mobile-responsive.js", + "scope": [ + { + "name": "_Navigation", + "packageName": "@components/Navigation" + }, + { + "name": "global", + "packageName": "@components/Global" + }, + { + "name": "antd", + "packageName": "antd" + } + ] + }, + { + "title": "强制移动端模式", + "description": "展示如何通过isMobile属性强制指定导航栏的显示模式", + "code": "./force-mobile.js", + "scope": [ + { + "name": "_Navigation", + "packageName": "@components/Navigation" + }, + { + "name": "global", + "packageName": "@components/Global" + }, + { + "name": "antd", + "packageName": "antd" + } + ] } ] } diff --git a/src/components/Navigation/doc/force-mobile.js b/src/components/Navigation/doc/force-mobile.js new file mode 100644 index 0000000..d22e65e --- /dev/null +++ b/src/components/Navigation/doc/force-mobile.js @@ -0,0 +1,84 @@ +const { default: Navigation } = _Navigation; +const { PureGlobal } = global; +const { Space, Switch } = antd; +const { useState } = React; + +const menuList = [ + { + key: "dashboard", + title: "仪表盘", + path: "/dashboard", + icon: 📊, + }, + { + key: "products", + title: "产品管理", + path: "/products", + icon: 📦, + }, + { + key: "orders", + title: "订单管理", + path: "/orders", + icon: 📋, + }, + { + key: "customers", + title: "客户管理", + path: "/customers", + icon: 👥, + }, + { + key: "settings", + title: "系统设置", + path: "/settings", + icon: ⚙️, + }, +]; + +const ForceMobileExample = () => { + const [isMobile, setIsMobile] = useState(false); + + return ( + +
+
+ +
+ 强制移动端模式: +
+ +
+ 通过 isMobile 属性可以强制指定导航栏的显示模式 +
+
+
+ + + +
+

点击上方开关可以强制切换导航栏的显示模式:

+
    +
  • 关闭开关:显示桌面端水平菜单
  • +
  • 打开开关:显示移动端汉堡菜单(点击后显示下拉菜单)
  • +
+

+ 注意:当不指定 isMobile 属性时,组件会根据窗口宽度自动检测 +

+
+
+
+ ); +}; + +render(); diff --git a/src/components/Navigation/doc/mobile-responsive.js b/src/components/Navigation/doc/mobile-responsive.js new file mode 100644 index 0000000..1a2907a --- /dev/null +++ b/src/components/Navigation/doc/mobile-responsive.js @@ -0,0 +1,81 @@ +const { default: Navigation } = _Navigation; +const { PureGlobal } = global; +const { Space, Switch, Divider } = antd; + +const menuList = [ + { + key: "dashboard", + title: "仪表盘", + path: "/dashboard", + icon: 📊, + }, + { + key: "products", + title: "产品管理", + path: "/products", + icon: 📦, + }, + { + key: "orders", + title: "订单管理", + path: "/orders", + icon: 📋, + }, + { + key: "customers", + title: "客户管理", + path: "/customers", + icon: 👥, + }, + { + key: "settings", + title: "系统设置", + path: "/settings", + icon: ⚙️, + }, +]; + +const MobileResponsiveExample = () => { + const [forceMobile, setForceMobile] = React.useState(undefined); + + return ( + +
+
+ +
+ 移动端模式控制: +
+ + 自动检测(根据窗口宽度) + setForceMobile(checked)} + checkedChildren="强制移动端" + unCheckedChildren="自动" + /> + +
+ 开启开关可强制切换到移动端布局,关闭则根据窗口宽度自动检测 +
+
+
+ + + +
+

1. 调整浏览器窗口宽度至小于 768px,导航将自动切换为移动端模式

+

2. 或者使用上方开关强制指定为移动端模式

+

3. 在移动端模式下,导航菜单将显示为汉堡菜单,点击后从右侧滑出

+
+
+
+ ); +}; + +render(); diff --git a/src/components/Navigation/index.js b/src/components/Navigation/index.js index 7eae94e..53ffa39 100644 --- a/src/components/Navigation/index.js +++ b/src/components/Navigation/index.js @@ -1,19 +1,22 @@ -import {Col, Layout, Menu, Row, Space, Flex} from "antd"; +import {Col, Layout, Menu, Row, Space, Flex, Dropdown} from "antd"; +import {MenuOutlined} from "@ant-design/icons"; import {useLocation, useNavigate} from "react-router-dom"; import get from "lodash/get"; import {useEffect, useMemo, useRef, useState} from "react"; import classnames from "classnames"; import logo from "./favicon.svg"; import Image from "@components/Image"; -import importMessages from "./locale"; -import {FormattedMessage, IntlProvider} from "@components/Intl"; +import withLocale from './withLocale'; +import {useIntl} from "@kne/react-intl"; import useRefCallback from "@kne/use-ref-callback"; +import useResize from "@kne/use-resize"; import Icon from "@components/Icon"; import style from "./style.module.scss"; const {Header} = Layout; export const navigationHeight = 48; +export const mobileBreakpoint = 768; const SetTitle = ({name, mapping, defaultTitle}) => { const propsRef = useRef({ @@ -34,21 +37,23 @@ const MenuReady = ({onReady}) => { return null; }; -const Navigation = ({ - permissions = [], - list = [], - headerLogo, - rightOptions, - isFixed = true, - showIndex = true, - indexLabel, - defaultTitle, - overflowedIndicator, - base = '', - onChange, - className, - navigateTo, - }) => { +const Navigation = withLocale(({ + permissions = [], + list = [], + headerLogo, + rightOptions, + isFixed = true, + showIndex = true, + indexLabel, + defaultTitle, + overflowedIndicator, + base = '', + onChange, + className, + navigateTo, + isMobile: forceMobile, + }) => { + const {formatMessage} = useIntl(); const mapping = useMemo(() => { return new Map(list.map(({key, ...others}) => [key, others])); }, [list]); @@ -59,6 +64,21 @@ const Navigation = ({ const resizeObserverRef = useRef(null); const [nameLabel, setNameLabel] = useState("更多"); const [ready, setReady] = useState(false); + const [autoIsMobile, setAutoIsMobile] = useState(false); + const [mobileMenuVisible, setMobileMenuVisible] = useState(false); + const callback = (el) => { + const width = el ? el.getBoundingClientRect().width : window.innerWidth; + if (forceMobile === undefined) { + setAutoIsMobile(width < mobileBreakpoint); + if (!autoIsMobile && width < mobileBreakpoint) { + setMobileMenuVisible(false); + } + } + } + const windowResizeRef = useResize(callback); + useEffect(() => { + callback(windowResizeRef.current); + }, []); const pathModuleName = location.pathname .replace(new RegExp(`^${base}`), "") .split("/")[1]; @@ -67,6 +87,18 @@ const Navigation = ({ return _path.indexOf("/" + pathModuleName) !== -1; }), "[0]") : "home"; + // 是否为移动端(优先使用强制指定的值,否则使用自动检测的值) + const isMobile = forceMobile !== undefined ? forceMobile : autoIsMobile; + + // 处理移动端菜单项点击 + const handleMobileMenuClick = (path) => { + setMobileMenuVisible(false); + onChange && onChange(path); + setTimeout(() => { + navigate(path); + }, 0); + }; + useEffect(() => { const callback = () => { if (navigationRef.current) { @@ -101,9 +133,7 @@ const Navigation = ({ }; }, [name, mapping, ready]); const indexNav = showIndex ? { - label: indexLabel || (), - key: "home", - onClick: () => { + label: indexLabel || formatMessage({id: 'indexLabel'}), key: "home", onClick: () => { onChange && onChange("/"); setTimeout(() => { navigate("/"); @@ -111,93 +141,143 @@ const Navigation = ({ }, } : false; - return ( - - {(text) => ()} - -
-
- - - logo - - - { - setReady(true); - }} - /> - + return (<> + +
+
+
+ + {isMobile && ( + + (
+ {menu} + {rightOptions && (
+ {rightOptions} +
)} +
)} + menu={{ + selectedKeys: [name], items: [indexNav, ...Array.from(mapping.entries()) + .filter(([name, {permission}]) => { + if (typeof permission === "string") { + return permissions.indexOf(permission) > -1; + } + if (typeof permission === "function") { + return permission(permissions); + } + if (Array.isArray(permission)) { + for (let item of permission) { + if (permissions.indexOf(item) > -1) { + return true; + } + } + return false; + } + return true; + }) + .map(([name, {title, icon, path, permission}]) => { + const _path = typeof path === "function" ? path(permission, permissions) : path; + return { + label: icon ? + {icon} + {title} + : title, + key: name, + onClick: () => handleMobileMenuClick(_path), + }; + })], + }} + > +
+ +
+
+ )} + {!isMobile && ( + logo + )} + {!isMobile && ( + { + setReady(true); + }} + /> + - {nameLabel || ()} + {nameLabel || formatMessage({id: 'overflowedIndicator'})} - + - )} - items={[indexNav, ...Array.from(mapping.entries()) - .filter(([name, {permission}]) => { - if (typeof permission === "string") { - return permissions.indexOf(permission) > -1; - } - if (typeof permission === "function") { - return permission(permissions); - } - if (Array.isArray(permission)) { - for (let item of permission) { - if (permissions.indexOf(item) > -1) { - return true; + )} + items={[indexNav, ...Array.from(mapping.entries()) + .filter(([name, {permission}]) => { + if (typeof permission === "string") { + return permissions.indexOf(permission) > -1; + } + if (typeof permission === "function") { + return permission(permissions); + } + if (Array.isArray(permission)) { + for (let item of permission) { + if (permissions.indexOf(item) > -1) { + return true; + } } + return false; } - return false; - } - return true; - }) - .map(([name, {title, icon, path, permission}]) => { - const _path = typeof path === "function" ? path(permission, permissions) : path; - return { - label: icon ? - {icon} - {title} - : title, key: name, onClick: () => { - onChange && onChange(_path); - setTimeout(() => { - navigate(_path); - }, 0); - }, - }; - }),]} - /> - - {rightOptions} - -
+ return true; + }) + .map(([name, {title, icon, path, permission}]) => { + const _path = typeof path === "function" ? path(permission, permissions) : path; + return { + label: icon ? + {icon} + {title} + : title, key: name, onClick: () => { + onChange && onChange(_path); + setTimeout(() => { + navigate(_path); + }, 0); + }, + }; + }),]} + /> + )} + {isMobile && ( + {defaultTitle || formatMessage({id: 'defaultTitle'})} + )} + {!isMobile && {rightOptions}} + +
+
-
); -}; + ); +}); export default Navigation; diff --git a/src/components/Navigation/style.module.scss b/src/components/Navigation/style.module.scss index 9720408..6145955 100644 --- a/src/components/Navigation/style.module.scss +++ b/src/components/Navigation/style.module.scss @@ -19,14 +19,13 @@ transform: translateX(var(--scroll-left, 0px)); } - line-height: var(--nav-height) !important; - height: var(--nav-height) !important; - :global(.ant-row) { height: var(--nav-height); } - &:global(.ant-layout-header) { + :global(.ant-layout-header) { + line-height: var(--nav-height) !important; + height: var(--nav-height) !important; padding: 0 32px; background: var(--primary-color); color: #fff; @@ -161,3 +160,157 @@ display: inline-block; font-size: var(--font-size-small); } + +// 移动端适配 +.navigation-wrap { + &.is-mobile { + .navigation { + :global(.ant-layout-header) { + padding: 0 16px !important; + height: var(--nav-height); + line-height: var(--nav-height); + } + + :global(.ant-row) { + height: var(--nav-height); + } + + .navigation-logo { + display: none; + } + + .navigation-mobile-menu { + flex: 0 0 auto; + } + + .navigation-mobile-title { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 500; + color: #fff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .navigation-options { + display: none; + } + } + } +} + +.navigation-mobile-menu { + display: none !important; + + .is-mobile & { + display: flex !important; + align-items: center; + } +} + +.mobile-menu-trigger { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: #fff; + font-size: 20px; + transition: all 0.3s; + + &:hover { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; + } + + &:active { + transform: scale(0.95); + } + + :global(.anticon) { + font-size: 24px; + } +} + +.mobile-dropdown-content { + min-width: 280px; + max-width: 320px; + max-height: 70vh; + overflow-y: auto; + background: #fff; + border-radius: 4px; + box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), + 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + + :global(.ant-dropdown-menu) { + border-radius: 0; + box-shadow: none; + padding: 8px 0; + } + + :global(.ant-dropdown-menu-item) { + padding: 12px 20px; + height: auto; + line-height: 1.5; + font-size: 14px; + + &:hover { + background: #f5f5f5; + } + } + + :global(.ant-dropdown-menu-item-selected) { + background: #e6f7ff; + color: var(--primary-color); + } +} + +.mobile-dropdown-header { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 16px; + border-bottom: 1px solid #f0f0f0; + background: #fafafa; +} + +.dropdown-title { + font-size: 14px; + font-weight: 500; + color: #333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mobile-dropdown-options { + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + background: #fafafa; + + :global(.ant-space) { + width: 100%; + justify-content: flex-end; + } +} + +// 响应式断点 +@media (max-width: 767px) { + .navigation-list { + display: none; + } + + .navigation-options { + display: none; + } + + .navigation-mobile-menu { + display: flex !important; + } +} diff --git a/src/components/Navigation/withLocale.js b/src/components/Navigation/withLocale.js new file mode 100644 index 0000000..33cc376 --- /dev/null +++ b/src/components/Navigation/withLocale.js @@ -0,0 +1,11 @@ +import {createWithIntlProvider} from '@kne/react-intl'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; + +const withLocale = createWithIntlProvider({ + defaultLocale: 'zh-CN', messages: { + 'zh-CN': zhCN, 'en-US': enUS + }, namespace: 'Navigation' +}); + +export default withLocale; diff --git a/src/index.css b/src/index.css index c89e98a..288a083 100644 --- a/src/index.css +++ b/src/index.css @@ -1,30 +1,48 @@ body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; } +.mark-down-html { + overflow: auto; +} + +.example-driver-preview { + box-sizing: border-box; +} + +.example-driver-item { + min-width: unset; +} + +@media (max-width: 768px) { + .example-driver-item { + width: 100%; + } +} + .mark-down-html pre { - background: #f2f4f5; - word-wrap: break-word; - white-space: pre-wrap; - padding: 0.2em 0.4em; + background: #f2f4f5; + word-wrap: break-word; + white-space: pre-wrap; + padding: 0.2em 0.4em; } .mark-down-html pre code { - padding: 0 !important; - border: none !important; - margin: 0 !important; + padding: 0 !important; + border: none !important; + margin: 0 !important; } .example-driver-des { - white-space: pre-line; + white-space: pre-line; }