release: v0.17.0
GitHub Pages / build (push) Canceled after 0s
GitHub Pages / deploy (push) Canceled after 0s
Docker image / meta (push) Successful in 24s
Docs Docker image / meta (push) Successful in 7s
Docker image / build (amd64, linux/amd64, ubuntu-latest) (push) Failing after 59s
Docs Docker image / build (amd64, linux/amd64, ubuntu-latest) (push) Failing after 30s
Docker image / merge (push) Canceled after 0s
Docs Docker image / merge (push) Canceled after 0s
Docker image / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Docs Docker image / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
GitHub Pages / build (push) Canceled after 0s
GitHub Pages / deploy (push) Canceled after 0s
Docker image / meta (push) Successful in 24s
Docs Docker image / meta (push) Successful in 7s
Docker image / build (amd64, linux/amd64, ubuntu-latest) (push) Failing after 59s
Docs Docker image / build (amd64, linux/amd64, ubuntu-latest) (push) Failing after 30s
Docker image / merge (push) Canceled after 0s
Docs Docker image / merge (push) Canceled after 0s
Docker image / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
Docs Docker image / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
.sidePanel {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row!important;
|
||||
}
|
||||
|
||||
.sideContainer {
|
||||
flex: 1;
|
||||
min-width: 760px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.topPanel {
|
||||
min-width: 1150px;
|
||||
}
|
||||
|
||||
.mixPanel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mixMain {
|
||||
flex-direction: row!important;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mixContent {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Layout } from 'tdesign-react';
|
||||
import { ELayout } from 'modules/global';
|
||||
import Header from './Header';
|
||||
import Footer from './Footer';
|
||||
import Menu from './Menu';
|
||||
import classnames from 'classnames';
|
||||
import Content from './AppRouter';
|
||||
|
||||
import Style from './AppLayout.module.less';
|
||||
|
||||
const SideLayout = React.memo(() => (
|
||||
<Layout className={classnames(Style.sidePanel, 'narrow-scrollbar')}>
|
||||
<Menu showLogo showOperation />
|
||||
<Layout className={Style.sideContainer}>
|
||||
<Header />
|
||||
<Content />
|
||||
<Footer />
|
||||
</Layout>
|
||||
</Layout>
|
||||
));
|
||||
|
||||
const TopLayout = React.memo(() => (
|
||||
<Layout className={Style.topPanel}>
|
||||
<Header showMenu />
|
||||
<Content />
|
||||
<Footer />
|
||||
</Layout>
|
||||
));
|
||||
|
||||
const MixLayout = React.memo(() => (
|
||||
<Layout className={Style.mixPanel}>
|
||||
<Header />
|
||||
<Layout className={Style.mixMain}>
|
||||
<Menu />
|
||||
<Layout className={Style.mixContent}>
|
||||
<Content />
|
||||
<Footer />
|
||||
</Layout>
|
||||
</Layout>
|
||||
</Layout>
|
||||
));
|
||||
|
||||
const FullPageLayout = React.memo(() => <Content />);
|
||||
|
||||
export default {
|
||||
[ELayout.side]: SideLayout,
|
||||
[ELayout.top]: TopLayout,
|
||||
[ELayout.mix]: MixLayout,
|
||||
[ELayout.fullPage]: FullPageLayout,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.loading {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import React, { Suspense, memo } from 'react';
|
||||
import { Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { Layout, Loading } from 'tdesign-react';
|
||||
import routers, { IRouter } from 'router';
|
||||
import { resolve } from 'utils/path';
|
||||
import Page from './Page';
|
||||
import Style from './AppRouter.module.less';
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
type TRenderRoutes = (routes: IRouter[], parentPath?: string, breadcrumbs?: string[]) => React.ReactNode[];
|
||||
/**
|
||||
* 渲染应用路由
|
||||
* @param routes
|
||||
* @param parentPath
|
||||
* @param breadcrumb
|
||||
*/
|
||||
const renderRoutes: TRenderRoutes = (routes, parentPath = '', breadcrumb = []) =>
|
||||
routes.map((route, index: number) => {
|
||||
const { Component, children, redirect, meta } = route;
|
||||
const currentPath = resolve(parentPath, route.path);
|
||||
let currentBreadcrumb = breadcrumb;
|
||||
|
||||
if (meta?.title) {
|
||||
currentBreadcrumb = currentBreadcrumb.concat([meta?.title]);
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
// 重定向
|
||||
return <Route key={index} path={currentPath} element={<Navigate to={redirect} replace />} />;
|
||||
}
|
||||
|
||||
if (Component) {
|
||||
// 有路由菜单
|
||||
return (
|
||||
<Route
|
||||
key={index}
|
||||
path={currentPath}
|
||||
element={
|
||||
<Page isFullPage={route.isFullPage} breadcrumbs={currentBreadcrumb}>
|
||||
<Component />
|
||||
</Page>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// 无路由菜单
|
||||
return children ? renderRoutes(children, currentPath, currentBreadcrumb) : null;
|
||||
});
|
||||
|
||||
const AppRouter = () => (
|
||||
<Content>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className={Style.loading}>
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Routes>{renderRoutes(routers)}</Routes>
|
||||
</Suspense>
|
||||
</Content>
|
||||
);
|
||||
|
||||
export default memo(AppRouter);
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Layout, Row } from 'tdesign-react';
|
||||
import { useAppSelector } from 'modules/store';
|
||||
import { selectGlobal } from 'modules/global';
|
||||
|
||||
const { Footer: TFooter } = Layout;
|
||||
|
||||
const Footer = () => {
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
if (!globalState.showFooter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TFooter>
|
||||
<Row justify='center'>Copyright © 2022-{new Date().getFullYear()} Tencent. All Rights Reserved</Row>
|
||||
</TFooter>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Footer);
|
||||
@@ -0,0 +1,54 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.badge {
|
||||
:global {
|
||||
.t-badge--circle {
|
||||
top: 5px;
|
||||
right: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.menuIcon {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
}
|
||||
.dropItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
span {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.dropdown {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
}
|
||||
:global {
|
||||
.t-button--variant-text {
|
||||
padding-left: 9px;
|
||||
padding-right: 9px;
|
||||
}
|
||||
.t-button__text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.t-dropdown__item {
|
||||
max-width: none !important;
|
||||
width: 117px;
|
||||
&-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.t-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { memo } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, Popup, Badge, Dropdown, Space } from 'tdesign-react';
|
||||
import {
|
||||
Icon,
|
||||
LogoGithubIcon,
|
||||
MailIcon,
|
||||
HelpCircleIcon,
|
||||
SettingIcon,
|
||||
PoweroffIcon,
|
||||
UserCircleIcon,
|
||||
} from 'tdesign-icons-react';
|
||||
import { useAppDispatch } from 'modules/store';
|
||||
import { toggleSetting } from 'modules/global';
|
||||
import { logout } from 'modules/user';
|
||||
import Style from './HeaderIcon.module.less';
|
||||
|
||||
const { DropdownMenu, DropdownItem } = Dropdown;
|
||||
|
||||
export default memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const gotoWiki = () => {
|
||||
window.open('https://tdesign.tencent.com/react/overview');
|
||||
};
|
||||
|
||||
const gotoGitHub = () => {
|
||||
window.open('https://github.com/Tencent/tdesign-react-starter');
|
||||
};
|
||||
|
||||
const clickHandler = (data: any) => {
|
||||
if (data.value === 1) {
|
||||
navigate('/user/index');
|
||||
}
|
||||
};
|
||||
const handleLogout = async () => {
|
||||
await dispatch(logout());
|
||||
navigate('/login/index');
|
||||
};
|
||||
|
||||
return (
|
||||
<Space align='center'>
|
||||
<Badge className={Style.badge} count={6} dot={false} maxCount={99} shape='circle' showZero={false} size='medium'>
|
||||
<Button className={Style.menuIcon} shape='square' size='large' variant='text' icon={<MailIcon />} />
|
||||
</Badge>
|
||||
<Popup content='代码仓库' placement='bottom' showArrow destroyOnClose>
|
||||
<Button
|
||||
className={Style.menuIcon}
|
||||
shape='square'
|
||||
size='large'
|
||||
variant='text'
|
||||
onClick={gotoGitHub}
|
||||
icon={<LogoGithubIcon />}
|
||||
/>
|
||||
</Popup>
|
||||
<Popup content='帮助文档' placement='bottom' showArrow destroyOnClose>
|
||||
<Button
|
||||
className={Style.menuIcon}
|
||||
shape='square'
|
||||
size='large'
|
||||
variant='text'
|
||||
onClick={gotoWiki}
|
||||
icon={<HelpCircleIcon />}
|
||||
/>
|
||||
</Popup>
|
||||
<Dropdown trigger={'click'} onClick={clickHandler}>
|
||||
<Button variant='text' className={Style.dropdown}>
|
||||
<Icon name='user-circle' className={Style.icon} />
|
||||
<span className={Style.text}>Tencent</span>
|
||||
<Icon name='chevron-down' className={Style.icon} />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownItem value={1}>
|
||||
<div className={Style.dropItem}>
|
||||
<UserCircleIcon />
|
||||
<span>个人中心</span>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
<DropdownItem value={1} onClick={handleLogout}>
|
||||
<div className={Style.dropItem}>
|
||||
<PoweroffIcon />
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
<Popup content='页面设置' placement='bottom' showArrow destroyOnClose>
|
||||
<Button
|
||||
className={Style.menuIcon}
|
||||
shape='square'
|
||||
size='large'
|
||||
variant='text'
|
||||
onClick={() => dispatch(toggleSetting())}
|
||||
icon={<SettingIcon />}
|
||||
/>
|
||||
</Popup>
|
||||
</Space>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
.panel {
|
||||
:global {
|
||||
.t-input {
|
||||
border: none;
|
||||
}
|
||||
.t-input:hover {
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
transition: background var(--td-anim-duration-base) linear
|
||||
}
|
||||
.t-input--focused {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Input } from 'tdesign-react';
|
||||
import { SearchIcon } from 'tdesign-icons-react';
|
||||
import Style from './Search.module.less';
|
||||
|
||||
const Search = () => <Input className={Style.panel} prefixIcon={<SearchIcon />} placeholder='请输入搜索内容' />;
|
||||
|
||||
export default React.memo(Search);
|
||||
@@ -0,0 +1,11 @@
|
||||
.panel {
|
||||
flex-shrink: 0;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--td-component-stroke);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Layout, Button, Space } from 'tdesign-react';
|
||||
import { ViewListIcon } from 'tdesign-icons-react';
|
||||
import { useAppDispatch, useAppSelector } from 'modules/store';
|
||||
import { selectGlobal, toggleMenu } from 'modules/global';
|
||||
import HeaderIcon from './HeaderIcon';
|
||||
import { HeaderMenu } from '../Menu';
|
||||
import Search from './Search';
|
||||
import Style from './index.module.less';
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
export default memo((props: { showMenu?: boolean }) => {
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
if (!globalState.showHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let HeaderLeft;
|
||||
if (props.showMenu) {
|
||||
HeaderLeft = (
|
||||
<div>
|
||||
<HeaderMenu />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
HeaderLeft = (
|
||||
<Space align='center'>
|
||||
<Button
|
||||
shape='square'
|
||||
size='large'
|
||||
variant='text'
|
||||
onClick={() => dispatch(toggleMenu(null))}
|
||||
icon={<ViewListIcon />}
|
||||
/>
|
||||
<Search />
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Header className={Style.panel}>
|
||||
{HeaderLeft}
|
||||
<HeaderIcon />
|
||||
</Header>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
.menuPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.menuTip {
|
||||
color: var(--td-text-color-primary);
|
||||
opacity: 0.4;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menuLogo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.menuMiniLogo {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.logoFull {
|
||||
color: var(--td-text-color-primary);
|
||||
width: 184px;
|
||||
height: 32px;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Menu, MenuValue } from 'tdesign-react';
|
||||
import router, { IRouter } from 'router';
|
||||
import { resolve } from 'utils/path';
|
||||
import { useAppSelector } from 'modules/store';
|
||||
import { selectGlobal } from 'modules/global';
|
||||
import MenuLogo from './MenuLogo';
|
||||
import Style from './Menu.module.less';
|
||||
|
||||
const { SubMenu, MenuItem, HeadMenu } = Menu;
|
||||
|
||||
interface IMenuProps {
|
||||
showLogo?: boolean;
|
||||
showOperation?: boolean;
|
||||
}
|
||||
|
||||
const renderMenuItems = (menu: IRouter[], parentPath = '') => {
|
||||
const navigate = useNavigate();
|
||||
return menu.map((item) => {
|
||||
const { children, meta, path } = item;
|
||||
|
||||
if (!meta || meta?.hidden === true) {
|
||||
// 无meta信息 或 hidden == true,路由不显示为菜单
|
||||
return null;
|
||||
}
|
||||
|
||||
const { Icon, title, single } = meta;
|
||||
const routerPath = resolve(parentPath, path);
|
||||
|
||||
if (!children || children.length === 0) {
|
||||
return (
|
||||
<MenuItem
|
||||
key={routerPath}
|
||||
value={routerPath}
|
||||
icon={Icon ? <Icon /> : undefined}
|
||||
onClick={() => navigate(routerPath)}
|
||||
>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if (single && children?.length > 0) {
|
||||
const firstChild = children[0];
|
||||
if (firstChild?.meta && !firstChild?.meta?.hidden) {
|
||||
const { Icon, title } = meta;
|
||||
const singlePath = resolve(resolve(parentPath, path), firstChild.path);
|
||||
return (
|
||||
<MenuItem
|
||||
key={singlePath}
|
||||
value={singlePath}
|
||||
icon={Icon ? <Icon /> : undefined}
|
||||
onClick={() => navigate(singlePath)}
|
||||
>
|
||||
{title}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenu key={routerPath} value={routerPath} title={title} icon={Icon ? <Icon /> : undefined}>
|
||||
{renderMenuItems(children, routerPath)}
|
||||
</SubMenu>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 顶部菜单
|
||||
*/
|
||||
export const HeaderMenu = memo(() => {
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
const location = useLocation();
|
||||
const [active, setActive] = useState<MenuValue>(location.pathname); // todo
|
||||
|
||||
return (
|
||||
<HeadMenu
|
||||
expandType='popup'
|
||||
style={{ marginBottom: 20 }}
|
||||
value={active}
|
||||
theme={globalState.theme}
|
||||
onChange={(v) => setActive(v)}
|
||||
>
|
||||
{renderMenuItems(router)}
|
||||
</HeadMenu>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* 左侧菜单
|
||||
*/
|
||||
export default memo((props: IMenuProps) => {
|
||||
const location = useLocation();
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
|
||||
const { version } = globalState;
|
||||
const bottomText = globalState.collapsed ? version : `元境幻生管理台 ${version}`;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
width='232px'
|
||||
style={{ flexShrink: 0, height: '100%' }}
|
||||
className={Style.menuPanel2}
|
||||
value={location.pathname}
|
||||
theme={globalState.theme}
|
||||
collapsed={globalState.collapsed}
|
||||
operations={props.showOperation ? <div className={Style.menuTip}>{bottomText}</div> : undefined}
|
||||
logo={props.showLogo ? <MenuLogo collapsed={globalState.collapsed} /> : undefined}
|
||||
>
|
||||
{renderMenuItems(router)}
|
||||
</Menu>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { memo } from 'react';
|
||||
import Style from './Menu.module.less';
|
||||
import FullLogo from 'assets/svg/assets-logo-full.svg?component';
|
||||
import MiniLogo from 'assets/svg/assets-t-logo.svg?component';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface IProps {
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
export default memo((props: IProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Style.menuLogo} onClick={handleClick}>
|
||||
{props.collapsed ? <MiniLogo /> : <FullLogo />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
.panel {
|
||||
margin: 24px;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../modules/store';
|
||||
import { selectGlobal, switchFullPage } from '../../modules/global';
|
||||
import { Layout, Breadcrumb } from 'tdesign-react';
|
||||
import Style from './Page.module.less';
|
||||
|
||||
const { Content } = Layout;
|
||||
const { BreadcrumbItem } = Breadcrumb;
|
||||
|
||||
const Page = ({
|
||||
children,
|
||||
isFullPage,
|
||||
breadcrumbs,
|
||||
}: React.PropsWithChildren<{ isFullPage?: boolean; breadcrumbs?: string[] }>) => {
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
const dispatch = useAppDispatch();
|
||||
useEffect(() => {
|
||||
dispatch(switchFullPage(isFullPage));
|
||||
}, [isFullPage]);
|
||||
|
||||
if (isFullPage) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Content className={Style.panel}>
|
||||
{globalState.showBreadcrumbs && (
|
||||
<Breadcrumb className={Style.breadcrumb}>
|
||||
{breadcrumbs?.map((item, index) => (
|
||||
<BreadcrumbItem key={index}>{item}</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
)}
|
||||
{children}
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Page);
|
||||
@@ -0,0 +1,17 @@
|
||||
.panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.box {
|
||||
cursor: pointer;
|
||||
border: 2px solid;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.item {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defaultColor } from 'configs/color';
|
||||
import { Popup, ColorPickerPanel } from 'tdesign-react';
|
||||
import Style from './RadioColor.module.less';
|
||||
|
||||
interface IProps {
|
||||
defaultValue?: number | string;
|
||||
onChange: (color: string) => void;
|
||||
}
|
||||
|
||||
const RadioColor = (props: IProps) => {
|
||||
const [isColoPickerDisplay, onPopupVisibleChange] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={Style.panel}>
|
||||
{defaultColor.map((color, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => props?.onChange(color)}
|
||||
className={Style.box}
|
||||
style={{ borderColor: props.defaultValue === color && !isColoPickerDisplay ? color : 'transparent' }}
|
||||
>
|
||||
<div className={Style.item} style={{ backgroundColor: color }} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<Popup
|
||||
trigger='click'
|
||||
placement='bottom-right'
|
||||
expandAnimation
|
||||
visible={isColoPickerDisplay}
|
||||
onVisibleChange={(v) => onPopupVisibleChange(v)}
|
||||
overlayInnerStyle={{ padding: 0 }}
|
||||
content={
|
||||
<ColorPickerPanel
|
||||
onChange={(v) => props?.onChange(v)}
|
||||
colorModes={['monochrome']}
|
||||
format='HEX'
|
||||
swatchColors={[]}
|
||||
defaultValue={props.defaultValue as string}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div
|
||||
key='dynamic'
|
||||
className={Style.box}
|
||||
style={{
|
||||
borderColor:
|
||||
isColoPickerDisplay || defaultColor.indexOf(props.defaultValue as string) === -1
|
||||
? (props.defaultValue as string)
|
||||
: 'transparent',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={Style.item}
|
||||
style={{
|
||||
background:
|
||||
'conic-gradient(from 90deg at 50% 50%, #FF0000 -19.41deg, #FF0000 18.76deg, #FF8A00 59.32deg, #FFE600 99.87deg, #14FF00 141.65deg, #00A3FF 177.72deg, #0500FF 220.23deg, #AD00FF 260.13deg, #FF00C7 300.69deg, #FF0000 340.59deg, #FF0000 378.76deg)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RadioColor);
|
||||
@@ -0,0 +1,28 @@
|
||||
.radioRectPanel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rectItem {
|
||||
border: 2px solid #e3e6eb;
|
||||
padding: 8px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rectItemSelected {
|
||||
border-color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.rectText {
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.rectImg {
|
||||
width: 88px;
|
||||
height: 48px;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import classname from 'classnames';
|
||||
import Style from './RadioRect.module.less';
|
||||
|
||||
interface IOption {
|
||||
value?: any;
|
||||
image: JSX.Element | string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
defaultValue?: number | string;
|
||||
onChange: (value?: any) => void;
|
||||
options: IOption[];
|
||||
}
|
||||
|
||||
export default memo((props: IProps) => {
|
||||
const [selectValue, setSelectValue] = useState(props.defaultValue);
|
||||
|
||||
const handleClick = (option: IOption) => {
|
||||
setSelectValue(option.value);
|
||||
props?.onChange(option.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={Style.radioRectPanel}>
|
||||
{props.options.map((item, index) => {
|
||||
let ImageItem = item.image;
|
||||
if (typeof item.image === 'string') {
|
||||
ImageItem = <div className={Style.rectImg} style={{ backgroundImage: `url(${item.image})` }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
<div
|
||||
className={classname(Style.rectItem, { [Style.rectItemSelected]: selectValue === item.value })}
|
||||
onClick={() => handleClick(item)}
|
||||
>
|
||||
{ImageItem}
|
||||
</div>
|
||||
{item.name && <div className={Style.rectText}>{item.name}</div>}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
.settingTitle {
|
||||
margin: 32px 0 24px 0;
|
||||
color: var(--td-text-color-primary);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.settingSubTitle {
|
||||
font-size: 14px;
|
||||
line-height: 21px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { memo } from 'react';
|
||||
import { Row, Col, Switch } from 'tdesign-react';
|
||||
import { useAppDispatch, useAppSelector } from 'modules/store';
|
||||
import {
|
||||
selectGlobal,
|
||||
toggleShowHeader,
|
||||
toggleShowBreadcrumbs,
|
||||
toggleShowFooter,
|
||||
switchTheme,
|
||||
switchColor,
|
||||
switchLayout,
|
||||
openSystemTheme,
|
||||
ELayout,
|
||||
} from 'modules/global';
|
||||
import { ETheme } from 'types/index.d';
|
||||
import RadioColor from './RadioColor';
|
||||
import RadioRect from './RadioRect';
|
||||
|
||||
import Light from 'assets/svg/assets-setting-light.svg?component';
|
||||
import Dark from 'assets/svg/assets-setting-dark.svg?component';
|
||||
import System from 'assets/svg/assets-setting-auto.svg?component';
|
||||
|
||||
import Style from './index.module.less';
|
||||
|
||||
enum ESettingTheme {
|
||||
system,
|
||||
}
|
||||
|
||||
const themeList = [
|
||||
{
|
||||
value: ETheme.light,
|
||||
image: <Light />,
|
||||
name: '明亮',
|
||||
},
|
||||
{
|
||||
value: ETheme.dark,
|
||||
image: <Dark />,
|
||||
name: '黑暗',
|
||||
},
|
||||
{
|
||||
value: ESettingTheme.system,
|
||||
image: <System />,
|
||||
name: '跟随系统',
|
||||
},
|
||||
];
|
||||
|
||||
const layoutList = [
|
||||
{
|
||||
value: ELayout.side,
|
||||
image: 'https://tdesign.gtimg.com/starter/setting/side.png',
|
||||
},
|
||||
{
|
||||
value: ELayout.top,
|
||||
image: 'https://tdesign.gtimg.com/starter/setting/top.png',
|
||||
},
|
||||
{
|
||||
value: ELayout.mix,
|
||||
image: 'https://tdesign.gtimg.com/starter/setting/mix.png',
|
||||
},
|
||||
];
|
||||
|
||||
export default memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const globalState = useAppSelector(selectGlobal);
|
||||
|
||||
const handleThemeSwitch = (value: any) => {
|
||||
if (value === ESettingTheme.system) {
|
||||
dispatch(openSystemTheme());
|
||||
} else {
|
||||
dispatch(switchTheme(value));
|
||||
dispatch(switchColor(globalState.color));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={Style.settingTitle}>主题模式</div>
|
||||
<RadioRect
|
||||
defaultValue={globalState.systemTheme ? ESettingTheme.system : globalState.theme}
|
||||
onChange={handleThemeSwitch}
|
||||
options={themeList}
|
||||
/>
|
||||
|
||||
<div className={Style.settingTitle}>主题色</div>
|
||||
<RadioColor defaultValue={globalState.color} onChange={(value) => dispatch(switchColor(value))} />
|
||||
|
||||
<div className={Style.settingTitle}>导航布局</div>
|
||||
<RadioRect
|
||||
defaultValue={globalState.layout}
|
||||
onChange={(value) => dispatch(switchLayout(value))}
|
||||
options={layoutList}
|
||||
/>
|
||||
|
||||
<div className={Style.settingTitle}>元素开关</div>
|
||||
<Row justify='space-between'>
|
||||
<Col>
|
||||
<div className={Style.settingSubTitle}>显示 Header</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Switch size='large' value={globalState.showHeader} onChange={() => dispatch(toggleShowHeader())} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row justify='space-between'>
|
||||
<Col>
|
||||
<div className={Style.settingSubTitle}>显示 Breadcrumbs</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Switch size='large' value={globalState.showBreadcrumbs} onChange={() => dispatch(toggleShowBreadcrumbs())} />
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row justify='space-between'>
|
||||
<Col>
|
||||
<div className={Style.settingSubTitle}>显示 Footer</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<Switch size='large' value={globalState.showFooter} onChange={() => dispatch(toggleShowFooter())} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user