feat: 初始化 Cool-Admin (Node.js) 后端与 Arco Design Pro (React) 后台管理框架

这个提交包含在:
wikiboss
2026-09-20 16:59:17 +08:00
父节点 cdfb198c03
当前提交 de3cf8b870
共有 394 个文件被更改,包括 50575 次插入 和 634 次删除

查看文件

@@ -0,0 +1,92 @@
import React from 'react';
import {
Chart,
Line,
Axis,
Area,
Tooltip,
Coordinate,
Legend,
} from 'bizcharts';
import CustomTooltip from './customer-tooltip';
import { Spin } from '@arco-design/web-react';
import DataSet from '@antv/data-set';
interface AreaPolarProps {
data: any[];
loading: boolean;
fields: string[];
height: number;
}
function AreaPolar(props: AreaPolarProps) {
const { data, loading, fields, height } = props;
const { DataView } = DataSet;
const dv = new DataView().source(data);
dv.transform({
type: 'fold',
fields: fields, // 展开字段集
key: 'category', // key字段
value: 'score', // value字段
});
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
height={height || 400}
padding={0}
data={dv.rows}
autoFit
scale={{
score: {
min: 0,
max: 80,
},
}}
interactions={['legend-highlight']}
className={'chart-wrapper'}
>
<Coordinate type="polar" radius={0.8} />
<Tooltip shared>
{(title, items) => {
return <CustomTooltip title={title} data={items} />;
}}
</Tooltip>
<Line
position="item*score"
size="2"
color={['category', ['#313CA9', '#21CCFF', '#249EFF']]}
/>
<Area
position="item*score"
tooltip={false}
color={[
'category',
[
'rgba(49, 60, 169, 0.4)',
'rgba(33, 204, 255, 0.4)',
'rgba(36, 158, 255, 0.4)',
],
]}
/>
<Axis name="score" label={false} />
<Legend
position="right"
marker={(_, index) => {
return {
symbol: 'circle',
style: {
r: 4,
lineWidth: 0,
fill: ['#313CA9', '#21CCFF', '#249EFF'][index],
},
};
}}
name="category"
/>
</Chart>
</Spin>
);
}
export default AreaPolar;

查看文件

@@ -0,0 +1,42 @@
import React from 'react';
import { Typography, Badge } from '@arco-design/web-react';
import styles from './style/index.module.less';
const { Text } = Typography;
interface TooltipProps {
title: string;
data: {
name: string;
value: string;
color: string;
}[];
color?: string;
name?: string;
formatter?: (value: string) => React.ReactNode;
}
function CustomTooltip(props: TooltipProps) {
const { formatter = (value) => value, color, name } = props;
return (
<div className={styles['customer-tooltip']}>
<div className={styles['customer-tooltip-title']}>
<Text bold>{props.title}</Text>
</div>
<div>
{props.data.map((item, index) => (
<div className={styles['customer-tooltip-item']} key={index}>
<div>
<Badge color={color || item.color} />
{name || item.name}
</div>
<div>
<Text bold>{formatter(item.value)}</Text>
</div>
</div>
))}
</div>
</div>
);
}
export default CustomTooltip;

查看文件

@@ -0,0 +1,67 @@
import React from 'react';
import { Chart, Legend, Facet } from 'bizcharts';
import useBizTheme from '@/utils/useChartTheme';
interface FactMultiPieProps {
data: any[];
loading: boolean;
height: number;
}
function FactMultiPie(props: FactMultiPieProps) {
return (
<Chart
theme={useBizTheme()}
forceUpdate
autoFit
data={props.data}
height={props.height || 400}
padding={[0, 0, 10, 0]}
>
<Legend visible={true} />
<Facet
fields={['category']}
type="rect"
showTitle={false}
eachView={(view, facet) => {
const data = facet.data;
view.coordinate({
type: 'theta',
cfg: {
radius: 0.8,
innerRadius: 0.7,
},
});
view
.interval()
.adjust('stack')
.position('value')
.color('type', [
'#249eff',
'#846BCE',
'#21CCFF',
' #86DF6C',
'#0E42D2',
])
.label('value', {
content: (content) => {
return `${(content.value * 100).toFixed(2)} %`;
},
}),
view.annotation().text({
position: ['50%', '46%'],
content: data[0].category,
style: {
fontSize: 14,
fontWeight: 500,
textAlign: 'center',
},
offsetY: 10,
});
view.interaction('element-single-selected');
}}
/>
</Chart>
);
}
export default FactMultiPie;

查看文件

@@ -0,0 +1,76 @@
import React from 'react';
import { Chart, Tooltip, Interval, Axis, Coordinate, G2 } from 'bizcharts';
import { Spin } from '@arco-design/web-react';
import CustomTooltip from './customer-tooltip';
function HorizontalInterval({
data,
loading,
height,
}: {
data: any[];
loading: boolean;
height?: number;
}) {
G2.registerShape('interval', 'border-radius', {
draw(cfg, container) {
const points = cfg.points as unknown as { x: string; y: number };
let path = [];
path.push(['M', points[0].x, points[0].y]);
path.push(['L', points[1].x, points[1].y]);
path.push(['L', points[2].x, points[2].y]);
path.push(['L', points[3].x, points[3].y]);
path.push('Z');
path = this.parsePath(path); // 将 0 - 1 转化为画布坐标
const group = container.addGroup();
const radius = (path[1][2] - path[2][2]) / 2;
group.addShape('rect', {
attrs: {
x: path[0][1], // 矩形起始点为左上角
y: path[0][2] - radius * 2,
width: path[1][1] - path[0][1],
height: path[1][2] - path[2][2],
fill: cfg.color,
radius: radius,
},
});
return group;
},
});
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
height={height || 370}
padding="auto"
data={data}
autoFit
className={'chart-wrapper'}
>
<Coordinate transpose />
<Interval
color="#4086FF"
position="name*count"
size={10}
shape="border-radius"
/>
<Tooltip>
{(title, items) => {
return <CustomTooltip title={title} data={items} />;
}}
</Tooltip>
<Axis
name="count"
label={{
formatter(text) {
return `${Number(text) / 1000}k`;
},
}}
/>
</Chart>
</Spin>
);
}
export default HorizontalInterval;

查看文件

@@ -0,0 +1,63 @@
import React from 'react';
import { Chart, Line, Axis, Legend, Area, Tooltip } from 'bizcharts';
import { Spin } from '@arco-design/web-react';
import CustomTooltip from './customer-tooltip';
const areaColorMap = [
'l (90) 0:rgba(131, 100, 255, 0.5) 1:rgba(80, 52, 255, 0.001)',
'l (90) 0:rgba(100, 255, 236, 0.5) 1:rgba(52, 255, 243, 0.001)',
'l (90) 0:rgba(255, 211, 100, 0.5) 1:rgba(255, 235, 52, 0.001)',
'l (90) 0:rgba(100, 162, 255, 0.5) 1:rgba(52, 105, 255, 0.001)',
];
const lineColorMap = ['#722ED1', '#33D1C9', '#F77234', '#165DFF'];
function MultiAreaLine({ data, loading }: { data: any[]; loading: boolean }) {
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
height={352}
data={data}
padding={[10, 0, 30, 30]}
autoFit
scale={{ time: 'time' }}
className={'chart-wrapper'}
>
<Line
shape="smooth"
position="time*count"
color={['name', lineColorMap]}
/>
<Area
position="time*count"
shape="smooth"
color={['name', areaColorMap]}
tooltip={false}
/>
<Tooltip
crosshairs={{ type: 'x' }}
showCrosshairs
shared
showMarkers={true}
>
{(title, items) => {
return (
<CustomTooltip
title={title}
data={items.sort((a, b) => b.value - a.value)}
formatter={(value) => Number(value).toLocaleString()}
/>
);
}}
</Tooltip>
<Axis
name="count"
label={{ formatter: (value) => `${Number(value) / 100} k` }}
/>
<Legend visible={false} />
</Chart>
</Spin>
);
}
export default MultiAreaLine;

查看文件

@@ -0,0 +1,44 @@
import React from 'react';
import { Chart, Tooltip, Interval, Axis, Legend } from 'bizcharts';
import { Spin } from '@arco-design/web-react';
import CustomTooltip from './customer-tooltip';
function MultiInterval({ data, loading }: { data: any[]; loading: boolean }) {
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
height={370}
padding="auto"
data={data}
autoFit
className={'chart-wrapper'}
>
<Interval
adjust="stack"
color={['name', ['#81E2FF', '#00B2FF', '#246EFF']]}
position="time*count"
size={16}
style={{
radius: [2, 2, 0, 0],
}}
/>
<Tooltip crosshairs={{ type: 'x' }} showCrosshairs shared>
{(title, items) => {
return <CustomTooltip title={title} data={items} />;
}}
</Tooltip>
<Axis
name="count"
label={{
formatter(text) {
return `${Number(text) / 1000}k`;
},
}}
/>
<Legend name="name" marker={{ symbol: 'circle' }} />
</Chart>
</Spin>
);
}
export default MultiInterval;

查看文件

@@ -0,0 +1,83 @@
import React from 'react';
import { Chart, Line, Axis, Area, Tooltip } from 'bizcharts';
import { Spin } from '@arco-design/web-react';
import CustomTooltip from './customer-tooltip';
function OverviewAreaLine({
data,
loading,
name = '总内容量',
color = '#4080FF',
}: {
data: any[];
loading: boolean;
name?: string;
color?: string;
}) {
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
scale={{ value: { min: 0 } }}
padding={[10, 20, 50, 40]}
autoFit
height={300}
data={data}
className={'chart-wrapper'}
>
<Axis
name="count"
title
grid={{
line: {
style: {
lineDash: [4, 4],
},
},
}}
label={{
formatter(text) {
return `${Number(text) / 1000}k`;
},
}}
/>
<Axis name="date" grid={{ line: { style: { stroke: '#E5E8EF' } } }} />
<Line
shape="smooth"
position="date*count"
size={3}
color="l (0) 0:#1EE7FF .57:#249AFF .85:#6F42FB"
/>
<Area
position="date*count"
shape="smooth"
color="l (90) 0:rgba(17, 126, 255, 0.5) 1:rgba(17, 128, 255, 0)"
/>
<Tooltip
showCrosshairs={true}
showMarkers={true}
marker={{
lineWidth: 3,
stroke: color,
fill: '#ffffff',
symbol: 'circle',
r: 8,
}}
>
{(title, items) => {
return (
<CustomTooltip
title={title}
data={items}
color={color}
name={name}
formatter={(value) => Number(value).toLocaleString()}
/>
);
}}
</Tooltip>
</Chart>
</Spin>
);
}
export default OverviewAreaLine;

查看文件

@@ -0,0 +1,81 @@
import React from 'react';
import { Chart, Line, Axis, Tooltip, Legend, Slider } from 'bizcharts';
import { Spin } from '@arco-design/web-react';
import CustomTooltip from './customer-tooltip';
import useBizTheme from '@/utils/useChartTheme';
const lineColor = ['#21CCFF', '#313CA9', '#249EFF'];
function PeriodLine({ data, loading }: { data: any[]; loading: boolean }) {
return (
<Spin loading={loading} style={{ width: '100%' }}>
<Chart
theme={useBizTheme()}
forceUpdate
height={370}
padding={[10, 20, 120, 60]}
data={data}
autoFit
scale={{ time: 'time' }}
className={'chart-wrapper'}
>
<Line shape="smooth" position="time*rate" color={['name', lineColor]} />
<Tooltip crosshairs={{ type: 'x' }} showCrosshairs shared>
{(title, items) => {
return <CustomTooltip title={title} data={items} />;
}}
</Tooltip>
<Axis
name="rate"
label={{
formatter(text) {
return `${Number(text)} %`;
},
}}
/>
<Legend
name="name"
marker={(_, index) => {
return {
symbol: 'circle',
style: {
fill: lineColor[index],
r: 4,
},
};
}}
/>
<Slider
foregroundStyle={{
borderRadius: ' 4px',
fill: 'l (180) 0:rgba(206, 224, 255, 0.9) 1:rgba(146, 186, 255, 0.8)',
opacity: 0.3,
}}
trendCfg={{
data: data.map((item) => item.rate),
isArea: true,
areaStyle: {
fill: 'rgba(4, 135, 255, 0.15)',
opacity: 1,
},
backgroundStyle: {
fill: '#F2F3F5',
},
lineStyle: {
stroke: 'rgba(36, 158, 255, 0.3)',
lineWidth: 2,
},
}}
handlerStyle={{
fill: '#ffffff',
opacity: 1,
width: 22,
height: 22,
stroke: '#165DFF',
}}
/>
</Chart>
</Spin>
);
}
export default PeriodLine;

查看文件

@@ -0,0 +1,36 @@
.customer-tooltip {
&-title {
margin-bottom: 4px;
}
&-item {
height: 32px;
line-height: 32px;
display: flex;
justify-content: space-between;
padding: 0 8px;
background: rgb(255 255 255 / 90%);
box-shadow: 6px 0 20px rgb(34 87 188 / 10%);
border-radius: 4px;
color: var(--color-text-2);
:global(.arco-badge-status-dot) {
width: 10px;
height: 10px;
margin-right: 8px;
}
}
&-item:not(:last-child) {
margin-bottom: 8px;
}
}
body[arco-theme='dark'] {
.customer-tooltip {
&-item {
background: #2a2a2b;
box-shadow: 6px 0px 20px rgba(34, 87, 188, 0.1);
}
}
}

查看文件

@@ -0,0 +1,16 @@
import React from 'react';
import { Layout } from '@arco-design/web-react';
import { FooterProps } from '@arco-design/web-react/es/Layout/interface';
import cs from 'classnames';
import styles from './style/index.module.less';
function Footer(props: FooterProps = {}) {
const { className, ...restProps } = props;
return (
<Layout.Footer className={cs(styles.footer, className)} {...restProps}>
Arco Design Pro
</Layout.Footer>
);
}
export default Footer;

查看文件

@@ -0,0 +1,8 @@
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 40px;
text-align: center;
color: var(--color-text-2);
}

查看文件

@@ -0,0 +1,151 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import groupBy from 'lodash/groupBy';
import {
Trigger,
Badge,
Tabs,
Avatar,
Spin,
Button,
} from '@arco-design/web-react';
import {
IconMessage,
IconCustomerService,
IconFile,
IconDesktop,
} from '@arco-design/web-react/icon';
import useLocale from '../../utils/useLocale';
import MessageList, { MessageListType } from './list';
import styles from './style/index.module.less';
function DropContent() {
const t = useLocale();
const [loading, setLoading] = useState(false);
const [groupData, setGroupData] = useState<{
[key: string]: MessageListType;
}>({});
const [sourceData, setSourceData] = useState<MessageListType>([]);
function fetchSourceData(showLoading = true) {
showLoading && setLoading(true);
axios
.get('/api/message/list')
.then((res) => {
setSourceData(res.data);
})
.finally(() => {
showLoading && setLoading(false);
});
}
function readMessage(data: MessageListType) {
const ids = data.map((item) => item.id);
axios
.post('/api/message/read', {
ids,
})
.then(() => {
fetchSourceData();
});
}
useEffect(() => {
fetchSourceData();
}, []);
useEffect(() => {
const groupData: { [key: string]: MessageListType } = groupBy(
sourceData,
'type'
);
setGroupData(groupData);
}, [sourceData]);
const tabList = [
{
key: 'message',
title: t['message.tab.title.message'],
titleIcon: <IconMessage />,
},
{
key: 'notice',
title: t['message.tab.title.notice'],
titleIcon: <IconCustomerService />,
},
{
key: 'todo',
title: t['message.tab.title.todo'],
titleIcon: <IconFile />,
avatar: (
<Avatar style={{ backgroundColor: '#0FC6C2' }}>
<IconDesktop />
</Avatar>
),
},
];
return (
<div className={styles['message-box']}>
<Spin loading={loading} style={{ display: 'block' }}>
<Tabs
overflow="dropdown"
type="rounded"
defaultActiveTab="message"
destroyOnHide
extra={
<Button type="text" onClick={() => setSourceData([])}>
{t['message.empty']}
</Button>
}
>
{tabList.map((item) => {
const { key, title, avatar } = item;
const data = groupData[key] || [];
const unReadData = data.filter((item) => !item.status);
return (
<Tabs.TabPane
key={key}
title={
<span>
{title}
{unReadData.length ? `(${unReadData.length})` : ''}
</span>
}
>
<MessageList
data={data}
unReadData={unReadData}
onItemClick={(item) => {
readMessage([item]);
}}
onAllBtnClick={(unReadData) => {
readMessage(unReadData);
}}
/>
</Tabs.TabPane>
);
})}
</Tabs>
</Spin>
</div>
);
}
function MessageBox({ children }) {
return (
<Trigger
trigger="hover"
popup={() => <DropContent />}
position="br"
unmountOnExit={false}
popupAlign={{ bottom: 4 }}
>
<Badge count={9} dot>
{children}
</Badge>
</Trigger>
);
}
export default MessageBox;

查看文件

@@ -0,0 +1,126 @@
import React from 'react';
import {
List,
Avatar,
Typography,
Button,
Space,
Result,
Tag,
} from '@arco-design/web-react';
import useLocale from '../../utils/useLocale';
import styles from './style/index.module.less';
export interface MessageItemData {
id: string;
title: string;
subTitle?: string;
avatar?: string;
content: string;
time?: string;
status: number;
tag?: {
text?: string;
color?: string;
};
}
export type MessageListType = MessageItemData[];
interface MessageListProps {
data: MessageItemData[];
unReadData: MessageItemData[];
onItemClick?: (item: MessageItemData, index: number) => void;
onAllBtnClick?: (
unReadData: MessageItemData[],
data: MessageItemData[]
) => void;
}
function MessageList(props: MessageListProps) {
const t = useLocale();
const { data, unReadData } = props;
function onItemClick(item: MessageItemData, index: number) {
if (item.status) return;
props.onItemClick && props.onItemClick(item, index);
}
function onAllBtnClick() {
props.onAllBtnClick && props.onAllBtnClick(unReadData, data);
}
return (
<List
noDataElement={<Result status="404" subTitle={t['message.empty.tips']} />}
footer={
<div className={styles.footer}>
<div className={styles['footer-item']}>
<Button type="text" size="small" onClick={onAllBtnClick}>
{t['message.allRead']}
</Button>
</div>
<div className={styles['footer-item']}>
<Button type="text" size="small">
{t['message.seeMore']}
</Button>
</div>
</div>
}
>
{data.map((item, index) => (
<List.Item
key={item.id}
actionLayout="vertical"
style={{
opacity: item.status ? 0.5 : 1,
}}
>
<div
style={{
cursor: 'pointer',
}}
onClick={() => {
onItemClick(item, index);
}}
>
<List.Item.Meta
avatar={
item.avatar && (
<Avatar shape="circle" size={36}>
<img src={item.avatar} />
</Avatar>
)
}
title={
<div className={styles['message-title']}>
<Space size={4}>
<span>{item.title}</span>
<Typography.Text type="secondary">
{item.subTitle}
</Typography.Text>
</Space>
{item.tag && item.tag.text ? (
<Tag color={item.tag.color}>{item.tag.text}</Tag>
) : null}
</div>
}
description={
<div>
<Typography.Paragraph style={{ marginBottom: 0 }} ellipsis>
{item.content}
</Typography.Paragraph>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{item.time}
</Typography.Text>
</div>
}
/>
</div>
</List.Item>
))}
</List>
);
}
export default MessageList;

查看文件

@@ -0,0 +1,46 @@
@import '@arco-themes/react-arco-pro/variables.less';
.message-box {
width: 400px;
max-height: 800px;
background-color: var(--color-bg-popup);
border: 1px solid var(--color-border-2);
box-shadow: @shadow2-down;
border-radius: @border-radius-medium;
:global(.arco-tabs-header-nav) {
padding: 8px 16px;
border-bottom: 1px solid var(--color-border-2);
}
:global(.arco-list-item-meta) {
align-items: flex-start;
}
:global(.arco-list-item-meta-content) {
width: 100%;
}
:global(.arco-tabs-content) {
padding-top: 0;
}
}
.message-title {
display: flex;
justify-content: space-between;
}
.footer {
display: flex;
}
.footer-item {
display: flex;
justify-content: center;
width: 50%;
&:first-child {
border-right: 1px solid var(--color-border-2);
}
}

查看文件

@@ -0,0 +1,21 @@
import React, { forwardRef } from 'react';
import { Button } from '@arco-design/web-react';
import styles from './style/icon-button.module.less';
import cs from 'classnames';
function IconButton(props, ref) {
const { icon, className, ...rest } = props;
return (
<Button
ref={ref}
icon={icon}
shape="circle"
type="secondary"
className={cs(styles['icon-button'], className)}
{...rest}
/>
);
}
export default forwardRef(IconButton);

查看文件

@@ -0,0 +1,217 @@
import React, { useContext, useEffect } from 'react';
import {
Tooltip,
Input,
Avatar,
Select,
Dropdown,
Menu,
Divider,
Message,
Button,
} from '@arco-design/web-react';
import {
IconLanguage,
IconNotification,
IconSunFill,
IconMoonFill,
IconUser,
IconSettings,
IconPoweroff,
IconExperiment,
IconDashboard,
IconInteraction,
IconTag,
IconLoading,
} from '@arco-design/web-react/icon';
import { useSelector, useDispatch } from 'react-redux';
import { GlobalState } from '@/store';
import { GlobalContext } from '@/context';
import useLocale from '@/utils/useLocale';
import Logo from '@/assets/logo.svg';
import MessageBox from '@/components/MessageBox';
import IconButton from './IconButton';
import Settings from '../Settings';
import styles from './style/index.module.less';
import defaultLocale from '@/locale';
import useStorage from '@/utils/useStorage';
import { generatePermission } from '@/routes';
function Navbar({ show }: { show: boolean }) {
const t = useLocale();
const { userInfo, userLoading } = useSelector((state: GlobalState) => state);
const dispatch = useDispatch();
const [_, setUserStatus] = useStorage('userStatus');
const [role, setRole] = useStorage('userRole', 'admin');
const { setLang, lang, theme, setTheme } = useContext(GlobalContext);
function logout() {
setUserStatus('logout');
window.location.href = '/login';
}
function onMenuItemClick(key) {
if (key === 'logout') {
logout();
} else {
Message.info(`You clicked ${key}`);
}
}
useEffect(() => {
dispatch({
type: 'update-userInfo',
payload: {
userInfo: {
...userInfo,
permissions: generatePermission(role),
},
},
});
}, [role]);
if (!show) {
return (
<div className={styles['fixed-settings']}>
<Settings
trigger={
<Button icon={<IconSettings />} type="primary" size="large" />
}
/>
</div>
);
}
const handleChangeRole = () => {
const newRole = role === 'admin' ? 'user' : 'admin';
setRole(newRole);
};
const droplist = (
<Menu onClickMenuItem={onMenuItemClick}>
<Menu.SubMenu
key="role"
title={
<>
<IconUser className={styles['dropdown-icon']} />
<span className={styles['user-role']}>
{role === 'admin'
? t['menu.user.role.admin']
: t['menu.user.role.user']}
</span>
</>
}
>
<Menu.Item onClick={handleChangeRole} key="switch role">
<IconTag className={styles['dropdown-icon']} />
{t['menu.user.switchRoles']}
</Menu.Item>
</Menu.SubMenu>
<Menu.Item key="setting">
<IconSettings className={styles['dropdown-icon']} />
{t['menu.user.setting']}
</Menu.Item>
<Menu.SubMenu
key="more"
title={
<div style={{ width: 80 }}>
<IconExperiment className={styles['dropdown-icon']} />
{t['message.seeMore']}
</div>
}
>
<Menu.Item key="workplace">
<IconDashboard className={styles['dropdown-icon']} />
{t['menu.dashboard.workplace']}
</Menu.Item>
<Menu.Item key="card list">
<IconInteraction className={styles['dropdown-icon']} />
{t['menu.list.cardList']}
</Menu.Item>
</Menu.SubMenu>
<Divider style={{ margin: '4px 0' }} />
<Menu.Item key="logout">
<IconPoweroff className={styles['dropdown-icon']} />
{t['navbar.logout']}
</Menu.Item>
</Menu>
);
return (
<div className={styles.navbar}>
<div className={styles.left}>
<div className={styles.logo}>
<Logo />
<div className={styles['logo-name']}>Arco Pro</div>
</div>
</div>
<ul className={styles.right}>
<li>
<Input.Search
className={styles.round}
placeholder={t['navbar.search.placeholder']}
/>
</li>
<li>
<Select
triggerElement={<IconButton icon={<IconLanguage />} />}
options={[
{ label: '中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
]}
value={lang}
triggerProps={{
autoAlignPopupWidth: false,
autoAlignPopupMinWidth: true,
position: 'br',
}}
trigger="hover"
onChange={(value) => {
setLang(value);
const nextLang = defaultLocale[value];
Message.info(`${nextLang['message.lang.tips']}${value}`);
}}
/>
</li>
<li>
<MessageBox>
<IconButton icon={<IconNotification />} />
</MessageBox>
</li>
<li>
<Tooltip
content={
theme === 'light'
? t['settings.navbar.theme.toDark']
: t['settings.navbar.theme.toLight']
}
>
<IconButton
icon={theme !== 'dark' ? <IconMoonFill /> : <IconSunFill />}
onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
/>
</Tooltip>
</li>
<Settings />
{userInfo && (
<li>
<Dropdown droplist={droplist} position="br" disabled={userLoading}>
<Avatar size={32} style={{ cursor: 'pointer' }}>
{userLoading ? (
<IconLoading />
) : (
<img alt="avatar" src={userInfo.avatar} />
)}
</Avatar>
</Dropdown>
</li>
)}
</ul>
</div>
);
}
export default Navbar;

查看文件

@@ -0,0 +1,8 @@
.icon-button {
font-size: 16px;
border: 1px solid var(--color-border-2);
> svg {
vertical-align: -3px;
}
}

查看文件

@@ -0,0 +1,77 @@
.navbar {
display: flex;
justify-content: space-between;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
background-color: var(--color-bg-2);
height: 100%;
}
.left {
display: flex;
align-items: center;
}
.logo {
display: flex;
align-items: center;
width: 200px;
padding-left: 20px;
box-sizing: border-box;
}
.logo-name {
color: var(--color-text-1);
font-weight: 500;
font-size: 20px;
margin-left: 10px;
font-family: 'PingFang SC';
}
.right {
display: flex;
list-style: none;
padding-right: 20px;
li {
padding: 0 8px;
display: flex;
align-items: center;
}
a {
text-decoration: none;
color: var(--color-text-1);
}
}
.username {
cursor: pointer;
}
.round {
:global(.arco-input-inner-wrapper) {
border-radius: 16px;
}
svg {
font-size: 16px;
}
}
.dropdown-icon {
margin-right: 8px;
font-size: 16px;
vertical-align: text-bottom;
}
.fixed-settings {
position: fixed;
top: 280px;
right: 0px;
svg {
font-size: 18px;
vertical-align: -4px;
}
}

查看文件

@@ -0,0 +1,23 @@
import React, { CSSProperties, ReactNode } from 'react';
import { Typography } from '@arco-design/web-react';
import cs from 'classnames';
import styles from './style/index.module.less';
interface PanelProps {
className?: string;
style?: CSSProperties;
title?: ReactNode;
children?: ReactNode;
}
function Panel(props: PanelProps) {
const { className, style, title, children } = props;
return (
<div className={cs(styles.panel, className)} style={style}>
<Typography.Title>{title}</Typography.Title>
{children}
</div>
);
}
export default Panel;

查看文件

@@ -0,0 +1,4 @@
.panel {
background-color: var(--color-bg-2);
border-radius: 4px;
}

查看文件

@@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import { GlobalState } from '@/store';
import { useSelector } from 'react-redux';
import authentication, { AuthParams } from '@/utils/authentication';
type PermissionWrapperProps = AuthParams & {
backup?: React.ReactNode;
};
const PermissionWrapper = (
props: React.PropsWithChildren<PermissionWrapperProps>
) => {
const { backup, requiredPermissions, oneOfPerm } = props;
const userInfo = useSelector((state: GlobalState) => state.userInfo);
const hasPermission = useMemo(() => {
return authentication(
{requiredPermissions, oneOfPerm},
userInfo.permissions
);
},[oneOfPerm, requiredPermissions, userInfo.permissions]);
if (hasPermission) {
return <>{convertReactElement(props.children)}</>;
}
if (backup) {
return <>{convertReactElement(backup)}</>;
}
return null;
};
function convertReactElement(node: React.ReactNode): React.ReactElement {
if (!React.isValidElement(node)) {
return <>{node}</>;
}
return node;
}
export default PermissionWrapper;

查看文件

@@ -0,0 +1,77 @@
import React, { ReactNode } from 'react';
import { Switch, Divider, InputNumber } from '@arco-design/web-react';
import { useSelector, useDispatch } from 'react-redux';
import { GlobalState } from '../../store';
import useLocale from '../../utils/useLocale';
import styles from './style/block.module.less';
export interface BlockProps {
title?: ReactNode;
options?: { name: string; value: string; type?: 'switch' | 'number' }[];
children?: ReactNode;
}
export default function Block(props: BlockProps) {
const { title, options, children } = props;
const locale = useLocale();
const settings = useSelector((state: GlobalState) => state.settings);
const dispatch = useDispatch();
return (
<div className={styles.block}>
<h5 className={styles.title}>{title}</h5>
{options &&
options.map((option) => {
const type = option.type || 'switch';
return (
<div className={styles['switch-wrapper']} key={option.value}>
<span>{locale[option.name]}</span>
{type === 'switch' && (
<Switch
size="small"
checked={!!settings[option.value]}
onChange={(checked) => {
const newSetting = {
...settings,
[option.value]: checked,
};
dispatch({
type: 'update-settings',
payload: { settings: newSetting },
});
// set color week
if (checked && option.value === 'colorWeek') {
document.body.style.filter = 'invert(80%)';
}
if (!checked && option.value === 'colorWeek') {
document.body.style.filter = 'none';
}
}}
/>
)}
{type === 'number' && (
<InputNumber
style={{ width: 80 }}
size="small"
value={settings.menuWidth}
onChange={(value) => {
const newSetting = {
...settings,
[option.value]: value,
};
dispatch({
type: 'update-settings',
payload: { settings: newSetting },
});
}}
/>
)}
</div>
);
})}
{children}
<Divider />
</div>
);
}

查看文件

@@ -0,0 +1,72 @@
import React from 'react';
import { Trigger, Typography } from '@arco-design/web-react';
import { SketchPicker } from 'react-color';
import { generate, getRgbStr } from '@arco-design/color';
import { useSelector, useDispatch } from 'react-redux';
import { GlobalState } from '../../store';
import useLocale from '@/utils/useLocale';
import styles from './style/color-panel.module.less';
function ColorPanel() {
const theme =
document.querySelector('body').getAttribute('arco-theme') || 'light';
const settings = useSelector((state: GlobalState) => state.settings);
const locale = useLocale();
const themeColor = settings.themeColor;
const list = generate(themeColor, { list: true });
const dispatch = useDispatch();
return (
<div>
<Trigger
trigger="hover"
position="bl"
popup={() => (
<SketchPicker
color={themeColor}
onChangeComplete={(color) => {
const newColor = color.hex;
dispatch({
type: 'update-settings',
payload: { settings: { ...settings, themeColor: newColor } },
});
const newList = generate(newColor, {
list: true,
dark: theme === 'dark',
});
newList.forEach((l, index) => {
const rgbStr = getRgbStr(l);
document.body.style.setProperty(
`--arcoblue-${index + 1}`,
rgbStr
);
});
}}
/>
)}
>
<div className={styles.input}>
<div
className={styles.color}
style={{ backgroundColor: themeColor }}
/>
<span>{themeColor}</span>
</div>
</Trigger>
<ul className={styles.ul}>
{list.map((item, index) => (
<li
key={index}
className={styles.li}
style={{ backgroundColor: item }}
/>
))}
</ul>
<Typography.Paragraph style={{ fontSize: 12 }}>
{locale['settings.color.tooltip']}
</Typography.Paragraph>
</div>
);
}
export default ColorPanel;

查看文件

@@ -0,0 +1,72 @@
import React, { useState } from 'react';
import { Drawer, Alert, Message } from '@arco-design/web-react';
import { IconSettings } from '@arco-design/web-react/icon';
import copy from 'copy-to-clipboard';
import { useSelector } from 'react-redux';
import { GlobalState } from '../../store';
import Block from './block';
import ColorPanel from './color';
import IconButton from '../NavBar/IconButton';
import useLocale from '@/utils/useLocale';
interface SettingProps {
trigger?: React.ReactElement;
}
function Setting(props: SettingProps) {
const { trigger } = props;
const [visible, setVisible] = useState(false);
const locale = useLocale();
const settings = useSelector((state: GlobalState) => state.settings);
function onCopySettings() {
copy(JSON.stringify(settings, null, 2));
Message.success(locale['settings.copySettings.message']);
}
return (
<>
{trigger ? (
React.cloneElement(trigger as React.ReactElement, {
onClick: () => setVisible(true),
})
) : (
<IconButton icon={<IconSettings />} onClick={() => setVisible(true)} />
)}
<Drawer
width={300}
title={
<>
<IconSettings />
{locale['settings.title']}
</>
}
visible={visible}
okText={locale['settings.copySettings']}
cancelText={locale['settings.close']}
onOk={onCopySettings}
onCancel={() => setVisible(false)}
>
<Block title={locale['settings.themeColor']}>
<ColorPanel />
</Block>
<Block
title={locale['settings.content']}
options={[
{ name: 'settings.navbar', value: 'navbar' },
{ name: 'settings.menu', value: 'menu' },
{ name: 'settings.footer', value: 'footer' },
{ name: 'settings.menuWidth', value: 'menuWidth', type: 'number' },
]}
/>
<Block
title={locale['settings.otherSettings']}
options={[{ name: 'settings.colorWeek', value: 'colorWeek' }]}
/>
<Alert content={locale['settings.alertContent']} />
</Drawer>
</>
);
}
export default Setting;

查看文件

@@ -0,0 +1,16 @@
.block {
margin-bottom: 24px;
}
.title {
font-size: 14px;
padding: 0;
margin: 10px 0;
}
.switch-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
height: 32px;
}

查看文件

@@ -0,0 +1,25 @@
.input {
display: flex;
width: 100%;
height: 32px;
border: 1px solid var(--color-border);
padding: 3px;
box-sizing: border-box;
}
.color {
width: 100px;
height: 24px;
margin-right: 10px;
}
.ul {
list-style: none;
display: flex;
padding: 0;
}
.li {
width: 10%;
height: 26px;
}