feat: 初始化 Cool-Admin (Node.js) 后端与 Arco Design Pro (React) 后台管理框架
这个提交包含在:
@@ -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;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户