feat(核心): 添加项目源代码
- 实现 React + Arco Design Pro 基础框架 - 添加页面路由和布局组件 - 实现用户认证和权限管理 - 添加数据可视化图表组件 - 实现列表、表单等常用页面模块 - 配置 Redux 状态管理 - 添加工具函数和自定义 Hooks
1
src/assets/dark.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1599669065723" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="18411" width="32" height="32" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"></style></defs><path d="M390 250c0-52.6 10.6-102.6 29.8-148.2C237.4 146 102 310.2 102 506c0 229.6 186.4 416 416 416 195.8 0 360-135.4 404.2-317.8-45.6 19.2-95.8 29.8-148.2 29.8-212 0-384-172-384-384z" p-id="18412"></path></svg>
|
||||
|
After Width: | Height: | Size: 579 B |
39
src/assets/light.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st1{fill:none;}
|
||||
</style>
|
||||
<title>编组 5备份</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="组件页-Web端-_xD83E__xDD1F_">
|
||||
<g id="暗黑模式" transform="translate(2.500000, 2.500000)">
|
||||
<g id="编组-12">
|
||||
<g id="编组-8">
|
||||
<g id="编组-7" transform="translate(7.285714, 0.000000)">
|
||||
</g>
|
||||
|
||||
<g id="编组-7备份" transform="translate(8.500000, 8.500000) rotate(-270.000000) translate(-8.500000, -8.500000) translate(7.285714, 0.000000)">
|
||||
</g>
|
||||
|
||||
<g id="编组-7备份-2" transform="translate(8.500000, 8.500000) rotate(-225.000000) translate(-8.500000, -8.500000) translate(7.285714, 0.000000)">
|
||||
</g>
|
||||
|
||||
<g id="编组-7备份-3" transform="translate(8.500000, 8.500000) rotate(-315.000000) translate(-8.500000, -8.500000) translate(7.285714, 0.000000)">
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path id="椭圆形" class="st0" d="M8.5,11.5c1.7,0,3-1.4,3-3s-1.4-3-3-3s-3,1.4-3,3S6.8,11.5,8.5,11.5z"/>
|
||||
<rect id="矩形" x="7.3" class="st0" width="2.4" height="2.4"/>
|
||||
<rect id="矩形备份-2" x="7.3" y="14.6" class="st0" width="2.4" height="2.4"/>
|
||||
<polygon id="矩形_1_" class="st0" points="17,7.3 17,9.7 14.6,9.7 14.6,7.3 "/>
|
||||
<polygon id="矩形备份-2_1_" class="st0" points="2.4,7.3 2.4,9.7 0,9.7 0,7.3 "/>
|
||||
<polygon id="矩形_2_" class="st0" points="15.4,13.7 13.7,15.4 11.9,13.7 13.7,11.9 "/>
|
||||
<polygon id="矩形备份-2_2_" class="st0" points="5.1,3.3 3.3,5.1 1.6,3.3 3.3,1.6 "/>
|
||||
<polygon id="矩形_3_" class="st0" points="13.7,1.6 15.4,3.3 13.7,5.1 11.9,3.3 "/>
|
||||
<polygon id="矩形备份-2_3_" class="st0" points="3.3,11.9 5.1,13.7 3.3,15.4 1.6,13.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="0" class="st1" width="22" height="22"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
12
src/assets/logo.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg width="33" height="33" viewBox="0 0 33 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.37754 16.9795L12.7498 9.43027C14.7163 7.41663 17.9428 7.37837 19.9564 9.34482C19.9852 9.37297 20.0137 9.40145 20.0418 9.43027L20.1221 9.51243C22.1049 11.5429 22.1049 14.7847 20.1221 16.8152L12.7498 24.3644C10.7834 26.378 7.55686 26.4163 5.54322 24.4498C5.5144 24.4217 5.48592 24.3932 5.45777 24.3644L5.37754 24.2822C3.39468 22.2518 3.39468 19.0099 5.37754 16.9795Z" fill="#12D2AC"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0479 9.43034L27.3399 16.8974C29.3674 18.9735 29.3674 22.2883 27.3399 24.3644C25.3735 26.3781 22.147 26.4163 20.1333 24.4499C20.1045 24.4217 20.076 24.3933 20.0479 24.3644L12.7558 16.8974C10.7284 14.8213 10.7284 11.5065 12.7558 9.43034C14.7223 7.4167 17.9488 7.37844 19.9624 9.34489C19.9912 9.37304 20.0197 9.40152 20.0479 9.43034Z" fill="#307AF2"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.1321 9.52163L23.6851 13.1599L16.3931 20.627L9.10103 13.1599L12.6541 9.52163C14.6707 7.45664 17.9794 7.4174 20.0444 9.434C20.074 9.46286 20.1032 9.49207 20.1321 9.52163Z" fill="#0057FE"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="26" height="19" fill="white" transform="translate(3.5 7)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
13343
src/assets/world.json
Normal file
92
src/components/Chart/area-polar.tsx
Normal file
@ -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;
|
||||
42
src/components/Chart/customer-tooltip.tsx
Normal file
@ -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;
|
||||
67
src/components/Chart/fact-multi-pie.tsx
Normal file
@ -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;
|
||||
76
src/components/Chart/horizontal-interval.tsx
Normal file
@ -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;
|
||||
63
src/components/Chart/multi-area-line.tsx
Normal file
@ -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;
|
||||
44
src/components/Chart/multi-stack-interval.tsx
Normal file
@ -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;
|
||||
83
src/components/Chart/overview-area-line.tsx
Normal file
@ -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;
|
||||
81
src/components/Chart/period-legend-line.tsx
Normal file
@ -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;
|
||||
36
src/components/Chart/style/index.module.less
Normal file
@ -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 0 20px rgba(34, 87, 188, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/components/Footer/index.tsx
Normal file
@ -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;
|
||||
8
src/components/Footer/style/index.module.less
Normal file
@ -0,0 +1,8 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
text-align: center;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
151
src/components/MessageBox/index.tsx
Normal file
@ -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;
|
||||
126
src/components/MessageBox/list.tsx
Normal file
@ -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;
|
||||
46
src/components/MessageBox/style/index.module.less
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
21
src/components/NavBar/IconButton.tsx
Normal file
@ -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);
|
||||
217
src/components/NavBar/index.tsx
Normal file
@ -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;
|
||||
8
src/components/NavBar/style/icon-button.module.less
Normal file
@ -0,0 +1,8 @@
|
||||
.icon-button {
|
||||
font-size: 16px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
|
||||
> svg {
|
||||
vertical-align: -3px;
|
||||
}
|
||||
}
|
||||
77
src/components/NavBar/style/index.module.less
Normal file
@ -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: 0;
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
vertical-align: -4px;
|
||||
}
|
||||
}
|
||||
23
src/components/Panel/index.tsx
Normal file
@ -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;
|
||||
4
src/components/Panel/style/index.module.less
Normal file
@ -0,0 +1,4 @@
|
||||
.panel {
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
39
src/components/PermissionWrapper/index.tsx
Normal file
@ -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;
|
||||
77
src/components/Settings/block.tsx
Normal file
@ -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>
|
||||
);
|
||||
}
|
||||
72
src/components/Settings/color.tsx
Normal file
@ -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;
|
||||
72
src/components/Settings/index.tsx
Normal file
@ -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;
|
||||
16
src/components/Settings/style/block.module.less
Normal file
@ -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;
|
||||
}
|
||||
25
src/components/Settings/style/color-panel.module.less
Normal file
@ -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;
|
||||
}
|
||||
8
src/context.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const GlobalContext = createContext<{
|
||||
lang?: string;
|
||||
setLang?: (value: string) => void;
|
||||
theme?: string;
|
||||
setTheme?: (value: string) => void;
|
||||
}>({});
|
||||
27
src/declaration.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
declare module '*.svg' {
|
||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||
export default content;
|
||||
}
|
||||
|
||||
declare module '*.less' {
|
||||
const classes: { [className: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module '*/settings.json' {
|
||||
const value: {
|
||||
colorWeek: boolean;
|
||||
navbar: boolean;
|
||||
menu: boolean;
|
||||
footer: boolean;
|
||||
themeColor: string;
|
||||
menuWidth: number;
|
||||
};
|
||||
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '*.png' {
|
||||
const value: string;
|
||||
export default value;
|
||||
}
|
||||
296
src/layout.tsx
Normal file
@ -0,0 +1,296 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
import { Switch, Route, Redirect, useHistory } from 'react-router-dom';
|
||||
import { Layout, Menu, Breadcrumb, Spin } from '@arco-design/web-react';
|
||||
import cs from 'classnames';
|
||||
import {
|
||||
IconDashboard,
|
||||
IconList,
|
||||
IconMenuFold,
|
||||
IconMenuUnfold,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import { useSelector } from 'react-redux';
|
||||
import qs from 'query-string';
|
||||
import NProgress from 'nprogress';
|
||||
import Navbar from './components/NavBar';
|
||||
import Footer from './components/Footer';
|
||||
import useRoute, { IRoute } from '@/routes';
|
||||
import { isArray } from './utils/is';
|
||||
import useLocale from './utils/useLocale';
|
||||
import getUrlParams from './utils/getUrlParams';
|
||||
import lazyload from './utils/lazyload';
|
||||
import { GlobalState } from './store';
|
||||
import styles from './style/layout.module.less';
|
||||
|
||||
const MenuItem = Menu.Item;
|
||||
const SubMenu = Menu.SubMenu;
|
||||
|
||||
const Sider = Layout.Sider;
|
||||
const Content = Layout.Content;
|
||||
|
||||
// 简单的404页面组件
|
||||
function NotFound() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
minHeight: '400px',
|
||||
}}
|
||||
>
|
||||
<h1 style={{ fontSize: '72px', margin: '0', color: '#165DFF' }}>404</h1>
|
||||
<p style={{ fontSize: '20px', color: '#86909c', marginTop: '16px' }}>
|
||||
抱歉,页面不存在
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getIconFromKey(key) {
|
||||
switch (key) {
|
||||
case 'dashboard':
|
||||
return <IconDashboard className={styles.icon} />;
|
||||
case 'list':
|
||||
return <IconList className={styles.icon} />;
|
||||
default:
|
||||
return <div className={styles['icon-empty']} />;
|
||||
}
|
||||
}
|
||||
|
||||
function getFlattenRoutes(routes) {
|
||||
const mod = import.meta.glob('./pages/**/[a-z[]*.tsx');
|
||||
const res = [];
|
||||
function travel(_routes) {
|
||||
_routes.forEach((route) => {
|
||||
const visibleChildren = (route.children || []).filter(
|
||||
(child) => !child.ignore
|
||||
);
|
||||
if (route.key && (!route.children || !visibleChildren.length)) {
|
||||
try {
|
||||
route.component = lazyload(mod[`./pages/${route.key}/index.tsx`]);
|
||||
res.push(route);
|
||||
} catch (e) {
|
||||
console.log(route.key);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(route.children) && route.children.length) {
|
||||
travel(route.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
travel(routes);
|
||||
return res;
|
||||
}
|
||||
|
||||
function PageLayout() {
|
||||
const urlParams = getUrlParams();
|
||||
const history = useHistory();
|
||||
const pathname = history.location.pathname;
|
||||
const currentComponent = qs.parseUrl(pathname).url.slice(1);
|
||||
const locale = useLocale();
|
||||
const { settings, userLoading, userInfo } = useSelector(
|
||||
(state: GlobalState) => state
|
||||
);
|
||||
|
||||
const [routes, defaultRoute] = useRoute(userInfo?.permissions);
|
||||
const defaultSelectedKeys = [currentComponent || defaultRoute];
|
||||
const paths = (currentComponent || defaultRoute).split('/');
|
||||
const defaultOpenKeys = paths.slice(0, paths.length - 1);
|
||||
|
||||
const [breadcrumb, setBreadCrumb] = useState([]);
|
||||
const [collapsed, setCollapsed] = useState<boolean>(false);
|
||||
const [selectedKeys, setSelectedKeys] =
|
||||
useState<string[]>(defaultSelectedKeys);
|
||||
const [openKeys, setOpenKeys] = useState<string[]>(defaultOpenKeys);
|
||||
|
||||
const routeMap = useRef<Map<string, React.ReactNode[]>>(new Map());
|
||||
const menuMap = useRef<
|
||||
Map<string, { menuItem?: boolean; subMenu?: boolean }>
|
||||
>(new Map());
|
||||
|
||||
const navbarHeight = 60;
|
||||
const menuWidth = collapsed ? 48 : settings.menuWidth;
|
||||
|
||||
const showNavbar = settings.navbar && urlParams.navbar !== false;
|
||||
const showMenu = settings.menu && urlParams.menu !== false;
|
||||
const showFooter = settings.footer && urlParams.footer !== false;
|
||||
|
||||
const flattenRoutes = useMemo(() => getFlattenRoutes(routes) || [], [routes]);
|
||||
|
||||
function onClickMenuItem(key) {
|
||||
const currentRoute = flattenRoutes.find((r) => r.key === key);
|
||||
const component = currentRoute.component;
|
||||
const preload = component.preload();
|
||||
NProgress.start();
|
||||
preload.then(() => {
|
||||
history.push(currentRoute.path ? currentRoute.path : `/${key}`);
|
||||
NProgress.done();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleCollapse() {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
}
|
||||
|
||||
const paddingLeft = showMenu ? { paddingLeft: menuWidth } : {};
|
||||
const paddingTop = showNavbar ? { paddingTop: navbarHeight } : {};
|
||||
const paddingStyle = { ...paddingLeft, ...paddingTop };
|
||||
|
||||
function renderRoutes(locale) {
|
||||
routeMap.current.clear();
|
||||
return function travel(_routes: IRoute[], level, parentNode = []) {
|
||||
return _routes.map((route) => {
|
||||
const { breadcrumb = true, ignore } = route;
|
||||
const iconDom = getIconFromKey(route.key);
|
||||
const titleDom = (
|
||||
<>
|
||||
{iconDom} {locale[route.name] || route.name}
|
||||
</>
|
||||
);
|
||||
|
||||
routeMap.current.set(
|
||||
`/${route.key}`,
|
||||
breadcrumb ? [...parentNode, route.name] : []
|
||||
);
|
||||
|
||||
const visibleChildren = (route.children || []).filter((child) => {
|
||||
const { ignore, breadcrumb = true } = child;
|
||||
if (ignore || route.ignore) {
|
||||
routeMap.current.set(
|
||||
`/${child.key}`,
|
||||
breadcrumb ? [...parentNode, route.name, child.name] : []
|
||||
);
|
||||
}
|
||||
|
||||
return !ignore;
|
||||
});
|
||||
|
||||
if (ignore) {
|
||||
return '';
|
||||
}
|
||||
if (visibleChildren.length) {
|
||||
menuMap.current.set(route.key, { subMenu: true });
|
||||
return (
|
||||
<SubMenu key={route.key} title={titleDom}>
|
||||
{travel(visibleChildren, level + 1, [...parentNode, route.name])}
|
||||
</SubMenu>
|
||||
);
|
||||
}
|
||||
menuMap.current.set(route.key, { menuItem: true });
|
||||
return <MenuItem key={route.key}>{titleDom}</MenuItem>;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateMenuStatus() {
|
||||
const pathKeys = pathname.split('/');
|
||||
const newSelectedKeys: string[] = [];
|
||||
const newOpenKeys: string[] = [...openKeys];
|
||||
while (pathKeys.length > 0) {
|
||||
const currentRouteKey = pathKeys.join('/');
|
||||
const menuKey = currentRouteKey.replace(/^\//, '');
|
||||
const menuType = menuMap.current.get(menuKey);
|
||||
if (menuType && menuType.menuItem) {
|
||||
newSelectedKeys.push(menuKey);
|
||||
}
|
||||
if (menuType && menuType.subMenu && !openKeys.includes(menuKey)) {
|
||||
newOpenKeys.push(menuKey);
|
||||
}
|
||||
pathKeys.pop();
|
||||
}
|
||||
setSelectedKeys(newSelectedKeys);
|
||||
setOpenKeys(newOpenKeys);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const routeConfig = routeMap.current.get(pathname);
|
||||
setBreadCrumb(routeConfig || []);
|
||||
updateMenuStatus();
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<Layout className={styles.layout}>
|
||||
<div
|
||||
className={cs(styles['layout-navbar'], {
|
||||
[styles['layout-navbar-hidden']]: !showNavbar,
|
||||
})}
|
||||
>
|
||||
<Navbar show={showNavbar} />
|
||||
</div>
|
||||
{userLoading ? (
|
||||
<Spin className={styles['spin']} />
|
||||
) : (
|
||||
<Layout>
|
||||
{showMenu && (
|
||||
<Sider
|
||||
className={styles['layout-sider']}
|
||||
width={menuWidth}
|
||||
collapsed={collapsed}
|
||||
onCollapse={setCollapsed}
|
||||
trigger={null}
|
||||
collapsible
|
||||
breakpoint="xl"
|
||||
style={paddingTop}
|
||||
>
|
||||
<div className={styles['menu-wrapper']}>
|
||||
<Menu
|
||||
collapse={collapsed}
|
||||
onClickMenuItem={onClickMenuItem}
|
||||
selectedKeys={selectedKeys}
|
||||
openKeys={openKeys}
|
||||
onClickSubMenu={(_, openKeys) => {
|
||||
setOpenKeys(openKeys);
|
||||
}}
|
||||
>
|
||||
{renderRoutes(locale)(routes, 1)}
|
||||
</Menu>
|
||||
</div>
|
||||
<div className={styles['collapse-btn']} onClick={toggleCollapse}>
|
||||
{collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
|
||||
</div>
|
||||
</Sider>
|
||||
)}
|
||||
<Layout className={styles['layout-content']} style={paddingStyle}>
|
||||
<div className={styles['layout-content-wrapper']}>
|
||||
{!!breadcrumb.length && (
|
||||
<div className={styles['layout-breadcrumb']}>
|
||||
<Breadcrumb>
|
||||
{breadcrumb.map((node, index) => (
|
||||
<Breadcrumb.Item key={index}>
|
||||
{typeof node === 'string' ? locale[node] || node : node}
|
||||
</Breadcrumb.Item>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
)}
|
||||
<Content>
|
||||
<Switch>
|
||||
{flattenRoutes.map((route, index) => {
|
||||
return (
|
||||
<Route
|
||||
key={index}
|
||||
path={`/${route.key}`}
|
||||
component={route.component}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Route exact path="/">
|
||||
<Redirect to={`/${defaultRoute}`} />
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Content>
|
||||
</div>
|
||||
{showFooter && <Footer />}
|
||||
</Layout>
|
||||
</Layout>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageLayout;
|
||||
74
src/locale/index.ts
Normal file
@ -0,0 +1,74 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'menu.dashboard': 'Dashboard',
|
||||
'menu.list': 'List',
|
||||
'menu.list.searchTable': 'Search Table',
|
||||
'menu.dashboard.workplace': 'Workplace',
|
||||
'navbar.logout': 'Logout',
|
||||
'settings.title': 'Settings',
|
||||
'settings.themeColor': 'Theme Color',
|
||||
'settings.content': 'Content Setting',
|
||||
'settings.navbar': 'Navbar',
|
||||
'settings.menuWidth': 'Menu Width (px)',
|
||||
'settings.navbar.theme.toLight': 'Click to use light mode',
|
||||
'settings.navbar.theme.toDark': 'Click to use dark mode',
|
||||
'settings.menu': 'Menu',
|
||||
'settings.footer': 'Footer',
|
||||
'settings.otherSettings': 'Other Settings',
|
||||
'settings.colorWeek': 'Color Week',
|
||||
'settings.alertContent':
|
||||
'After the configuration is only temporarily effective, if you want to really affect the project, click the "Copy Settings" button below and replace the configuration in settings.json.',
|
||||
'settings.copySettings': 'Copy Settings',
|
||||
'settings.copySettings.message':
|
||||
'Copy succeeded, please paste to file src/settings.json.',
|
||||
'settings.close': 'Close',
|
||||
'settings.color.tooltip':
|
||||
'10 gradient colors generated according to the theme color',
|
||||
'message.tab.title.message': 'Message',
|
||||
'message.tab.title.notice': 'Notice',
|
||||
'message.tab.title.todo': 'ToDo',
|
||||
'message.allRead': 'All Read',
|
||||
'message.seeMore': 'SeeMore',
|
||||
'message.empty': 'Empty',
|
||||
'message.empty.tips': 'No Content',
|
||||
'message.lang.tips': 'Language switch to ',
|
||||
'navbar.search.placeholder': 'Please search',
|
||||
},
|
||||
'zh-CN': {
|
||||
'menu.dashboard': '仪表盘',
|
||||
'menu.list': '列表页',
|
||||
'menu.list.searchTable': '查询表格',
|
||||
'menu.dashboard.workplace': '工作台',
|
||||
'navbar.logout': '退出登录',
|
||||
'settings.title': '页面配置',
|
||||
'settings.themeColor': '主题色',
|
||||
'settings.content': '内容区域',
|
||||
'settings.navbar': '导航栏',
|
||||
'settings.menuWidth': '菜单宽度 (px)',
|
||||
'settings.navbar.theme.toLight': '点击切换为亮色模式',
|
||||
'settings.navbar.theme.toDark': '点击切换为暗黑模式',
|
||||
'settings.menu': '菜单栏',
|
||||
'settings.footer': '底部',
|
||||
'settings.otherSettings': '其他设置',
|
||||
'settings.colorWeek': '色弱模式',
|
||||
'settings.alertContent':
|
||||
'配置之后仅是临时生效,要想真正作用于项目,点击下方的 "复制配置" 按钮,将配置替换到 settings.json 中即可。',
|
||||
'settings.copySettings': '复制配置',
|
||||
'settings.copySettings.message':
|
||||
'复制成功,请粘贴到 src/settings.json 文件中',
|
||||
'settings.close': '关闭',
|
||||
'settings.color.tooltip':
|
||||
'根据主题颜色生成的 10 个梯度色(将配置复制到项目中,主题色才能对亮色 / 暗黑模式同时生效)',
|
||||
'message.tab.title.message': '消息',
|
||||
'message.tab.title.notice': '通知',
|
||||
'message.tab.title.todo': '待办',
|
||||
'message.allRead': '全部已读',
|
||||
'message.seeMore': '查看更多',
|
||||
'message.empty': '清空',
|
||||
'message.empty.tips': '暂无内容',
|
||||
'message.lang.tips': '语言切换至 ',
|
||||
'navbar.search.placeholder': '输入内容查询',
|
||||
},
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
98
src/main.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import './style/global.less';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createStore } from 'redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ConfigProvider } from '@arco-design/web-react';
|
||||
import zhCN from '@arco-design/web-react/es/locale/zh-CN';
|
||||
import enUS from '@arco-design/web-react/es/locale/en-US';
|
||||
import { BrowserRouter, Switch, Route } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import rootReducer from './store';
|
||||
import PageLayout from './layout';
|
||||
import { GlobalContext } from './context';
|
||||
import Login from './pages/login';
|
||||
import checkLogin from './utils/checkLogin';
|
||||
import changeTheme from './utils/changeTheme';
|
||||
import useStorage from './utils/useStorage';
|
||||
import './mock';
|
||||
|
||||
const store = createStore(rootReducer);
|
||||
|
||||
function Index() {
|
||||
const [lang, setLang] = useStorage('arco-lang', 'en-US');
|
||||
const [theme, setTheme] = useStorage('arco-theme', 'light');
|
||||
|
||||
function getArcoLocale() {
|
||||
switch (lang) {
|
||||
case 'zh-CN':
|
||||
return zhCN;
|
||||
case 'en-US':
|
||||
return enUS;
|
||||
default:
|
||||
return zhCN;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchUserInfo() {
|
||||
store.dispatch({
|
||||
type: 'update-userInfo',
|
||||
payload: { userLoading: true },
|
||||
});
|
||||
axios.get('/api/user/userInfo').then((res) => {
|
||||
store.dispatch({
|
||||
type: 'update-userInfo',
|
||||
payload: { userInfo: res.data, userLoading: false },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (checkLogin()) {
|
||||
fetchUserInfo();
|
||||
} else if (window.location.pathname.replace(/\//g, '') !== 'login') {
|
||||
window.location.pathname = '/login';
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
changeTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
const contextValue = {
|
||||
lang,
|
||||
setLang,
|
||||
theme,
|
||||
setTheme,
|
||||
};
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<ConfigProvider
|
||||
locale={getArcoLocale()}
|
||||
componentConfig={{
|
||||
Card: {
|
||||
bordered: false,
|
||||
},
|
||||
List: {
|
||||
bordered: false,
|
||||
},
|
||||
Table: {
|
||||
border: false,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Provider store={store}>
|
||||
<GlobalContext.Provider value={contextValue}>
|
||||
<Switch>
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/" component={PageLayout} />
|
||||
</Switch>
|
||||
</GlobalContext.Provider>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<Index />, document.getElementById('root'));
|
||||
11
src/mock/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import Mock from 'mockjs';
|
||||
import { isSSR } from '@/utils/is';
|
||||
|
||||
import './user';
|
||||
import './message-box';
|
||||
|
||||
if (!isSSR) {
|
||||
Mock.setup({
|
||||
timeout: '500-1500',
|
||||
});
|
||||
}
|
||||
99
src/mock/message-box.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import Mock from 'mockjs';
|
||||
import setupMock from '@/utils/setupMock';
|
||||
|
||||
const haveReadIds = [];
|
||||
const getMessageList = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
type: 'message',
|
||||
title: '郑曦月',
|
||||
subTitle: '的私信',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/8361eeb82904210b4f55fab888fe8416.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '审批请求已发送,请查收',
|
||||
time: '今天 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'message',
|
||||
title: '宁波',
|
||||
subTitle: '的回复',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content:
|
||||
'此处 bug 已经修复,如有问题请查阅文档或者继续 github 提 issue~',
|
||||
time: '今天 12:30:01',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'message',
|
||||
title: '宁波',
|
||||
subTitle: '的回复',
|
||||
avatar:
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/3ee5f13fb09879ecb5185e440cef6eb9.png~tplv-uwbnlip3yd-webp.webp',
|
||||
content: '此处 bug 已经修复',
|
||||
time: '今天 12:20:01',
|
||||
},
|
||||
|
||||
{
|
||||
id: 4,
|
||||
type: 'todo',
|
||||
title: '域名服务',
|
||||
content: '内容质检队列于 2021-12-01 19:50:23 进行变更,请重新',
|
||||
tag: {
|
||||
text: '未开始',
|
||||
color: 'gray',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'todo',
|
||||
title: '内容审批通知',
|
||||
content: '宁静提交于 2021-11-05,需要您在 2011-11-07之前审批',
|
||||
tag: {
|
||||
text: '进行中',
|
||||
color: 'arcoblue',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'notice',
|
||||
title: '质检队列变更',
|
||||
content: '您的产品使用期限即将截止,如需继续使用产品请前往购…',
|
||||
tag: {
|
||||
text: '即将到期',
|
||||
color: 'red',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: 'notice',
|
||||
title: '规则开通成功',
|
||||
subTitle: '',
|
||||
avatar: '',
|
||||
content: '内容屏蔽规则于 2021-12-01 开通成功并生效。',
|
||||
tag: {
|
||||
text: '已开通',
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
status: haveReadIds.indexOf(item.id) === -1 ? 0 : 1,
|
||||
}));
|
||||
};
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
Mock.mock(new RegExp('/api/message/list'), () => {
|
||||
return getMessageList();
|
||||
});
|
||||
|
||||
Mock.mock(new RegExp('/api/message/read'), (params) => {
|
||||
const { ids } = JSON.parse(params.body);
|
||||
haveReadIds.push(...(ids || []));
|
||||
return true;
|
||||
});
|
||||
},
|
||||
});
|
||||
62
src/mock/user.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import Mock from 'mockjs';
|
||||
import { isSSR } from '@/utils/is';
|
||||
import setupMock from '@/utils/setupMock';
|
||||
import { generatePermission } from '@/routes';
|
||||
|
||||
if (!isSSR) {
|
||||
Mock.XHR.prototype.withCredentials = true;
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
// 用户信息
|
||||
const userRole = window.localStorage.getItem('userRole') || 'admin';
|
||||
Mock.mock(new RegExp('/api/user/userInfo'), () => {
|
||||
return Mock.mock({
|
||||
name: 'admin',
|
||||
avatar:
|
||||
'https://lf1-xgcdn-tos.pstatp.com/obj/vcloud/vadmin/start.8e0e4855ee346a46ccff8ff3e24db27b.png',
|
||||
email: 'wangliqun@email.com',
|
||||
job: 'frontend',
|
||||
jobName: '前端开发工程师',
|
||||
organization: 'Frontend',
|
||||
organizationName: '前端',
|
||||
location: 'beijing',
|
||||
locationName: '北京',
|
||||
introduction: '王力群并非是一个真实存在的人。',
|
||||
personalWebsite: 'https://www.arco.design',
|
||||
verified: true,
|
||||
phoneNumber: /177[*]{6}[0-9]{2}/,
|
||||
accountId: /[a-z]{4}[-][0-9]{8}/,
|
||||
registrationTime: Mock.Random.datetime('yyyy-MM-dd HH:mm:ss'),
|
||||
permissions: generatePermission(userRole),
|
||||
});
|
||||
});
|
||||
|
||||
// 登录
|
||||
Mock.mock(new RegExp('/api/user/login'), (params) => {
|
||||
const { userName, password } = JSON.parse(params.body);
|
||||
if (!userName) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: '用户名不能为空',
|
||||
};
|
||||
}
|
||||
if (!password) {
|
||||
return {
|
||||
status: 'error',
|
||||
msg: '密码不能为空',
|
||||
};
|
||||
}
|
||||
if (userName === 'admin' && password === 'admin') {
|
||||
return {
|
||||
status: 'ok',
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'error',
|
||||
msg: '账号或者密码错误',
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
67
src/pages/dashboard/workplace/announcement.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import axios from 'axios';
|
||||
import { Link, Card, Skeleton, Tag, Typography } from '@arco-design/web-react';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/announcement.module.less';
|
||||
|
||||
function Announcement() {
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const t = useLocale(locale);
|
||||
|
||||
const fetchData = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get('/api/workplace/announcement')
|
||||
.then((res) => {
|
||||
setData(res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
function getTagColor(type) {
|
||||
switch (type) {
|
||||
case 'activity':
|
||||
return 'orangered';
|
||||
case 'info':
|
||||
return 'cyan';
|
||||
case 'notice':
|
||||
return 'arcoblue';
|
||||
default:
|
||||
return 'arcoblue';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography.Title heading={6}>
|
||||
{t['workplace.announcement']}
|
||||
</Typography.Title>
|
||||
<Link>{t['workplace.seeMore']}</Link>
|
||||
</div>
|
||||
<Skeleton loading={loading} text={{ rows: 5, width: '100%' }} animation>
|
||||
<div>
|
||||
{data.map((d) => (
|
||||
<div key={d.key} className={styles.item}>
|
||||
<Tag color={getTagColor(d.type)} size="small">
|
||||
{t[`workplace.${d.type}`]}
|
||||
</Tag>
|
||||
<span className={styles.link}>{d.content}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Skeleton>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Announcement;
|
||||
86
src/pages/dashboard/workplace/assets/calendar.svg
Normal file
@ -0,0 +1,86 @@
|
||||
<svg width="55" height="58" viewBox="0 0 55 58" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ii_1053_46645)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M34.2234 16H34.1971H19.6665C17.4612 16 15.6665 17.7937 15.6665 19.9977V23.9953V26.6605V35.9883C15.6665 38.1923 17.4612 39.986 19.6665 39.986H35.6665C37.8718 39.986 39.6665 38.1923 39.6665 35.9883V26.6605V23.9953V21.2979L34.2234 16Z" fill="#7DA2FF"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_di_1053_46645)">
|
||||
<path d="M31.6884 25.1609H20.5815C20.0751 25.1609 19.6646 25.5712 19.6646 26.0773C19.6646 26.5833 20.0751 26.9936 20.5815 26.9936H31.6884C32.1948 26.9936 32.6052 26.5833 32.6052 26.0773C32.6052 25.5712 32.1948 25.1609 31.6884 25.1609Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_di_1053_46645)">
|
||||
<path d="M27.1313 21.5852H20.5226C20.0488 21.5852 19.6646 21.9691 19.6646 22.4427C19.6646 22.9163 20.0488 23.3001 20.5226 23.3001H27.1313C27.6052 23.3001 27.9893 22.9163 27.9893 22.4427C27.9893 21.9691 27.6052 21.5852 27.1313 21.5852Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_di_1053_46645)">
|
||||
<path d="M35.6691 30.6563C35.6691 29.9208 35.1558 29.3238 34.5259 29.3238H20.8078C20.1779 29.3238 19.6646 29.9208 19.6646 30.6563V33.6234C19.6646 34.4513 20.3362 35.1225 21.1646 35.1225H34.1691C34.9975 35.1225 35.6691 34.4513 35.6691 33.6234V30.6563Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_f_1053_46645)">
|
||||
<path d="M28.1665 39.986C34.5178 39.986 39.6665 38.8674 39.6665 37.4874C39.6665 36.1075 34.5178 34.9889 28.1665 34.9889C21.8152 34.9889 16.6665 36.1075 16.6665 37.4874C16.6665 38.8674 21.8152 39.986 28.1665 39.986Z" fill="#7CA0FD"/>
|
||||
</g>
|
||||
<path d="M36.2095 21.2979H39.6669L34.2095 15.986V19.2991C34.2095 20.403 35.1049 21.2979 36.2095 21.2979Z" fill="#B9CDFA"/>
|
||||
<defs>
|
||||
<filter id="filter0_ii_1053_46645" x="15.6665" y="9.88145" width="24" height="30.1045" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-6.11855"/>
|
||||
<feGaussianBlur stdDeviation="3.82409"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0716667 0 0 0 0 0.136167 0 0 0 0 0.716667 0 0 0 0.35 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1053_46645"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.29445"/>
|
||||
<feGaussianBlur stdDeviation="1.52964"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_1053_46645" result="effect2_innerShadow_1053_46645"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_1053_46645" x="5.15213" y="18.5644" width="41.9658" height="30.8575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="7.91587"/>
|
||||
<feGaussianBlur stdDeviation="7.25621"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0450001 0 0 0 0 0.45 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1053_46645"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1053_46645" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.338819 0 0 0 0 0.521617 0 0 0 0 0.991667 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_1053_46645"/>
|
||||
</filter>
|
||||
<filter id="filter2_di_1053_46645" x="5.15213" y="14.9887" width="37.3495" height="30.7397" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="7.91587"/>
|
||||
<feGaussianBlur stdDeviation="7.25621"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0450001 0 0 0 0 0.45 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1053_46645"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1053_46645" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.5"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.338819 0 0 0 0 0.521617 0 0 0 0 0.991667 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_1053_46645"/>
|
||||
</filter>
|
||||
<filter id="filter3_di_1053_46645" x="5.15213" y="22.7273" width="45.0292" height="34.8236" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="7.91587"/>
|
||||
<feGaussianBlur stdDeviation="7.25621"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0450001 0 0 0 0 0.45 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1053_46645"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1053_46645" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2"/>
|
||||
<feGaussianBlur stdDeviation="3.29828"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.690196 0 0 0 0 0.776941 0 0 0 0 1 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_1053_46645"/>
|
||||
</filter>
|
||||
<filter id="filter4_f_1053_46645" x="4.6665" y="22.9889" width="47" height="28.9971" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feGaussianBlur stdDeviation="6" result="effect1_foregroundBlur_1053_46645"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
71
src/pages/dashboard/workplace/assets/comments.svg
Normal file
@ -0,0 +1,71 @@
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_ii_161_33236)">
|
||||
<path d="M20.4884 3.5143C21.5946 4.62323 22.4625 5.9143 23.0679 7.3527C23.692 8.84199 24.008 10.425 24.008 12.0536C24 13.6768 23.6786 15.2491 23.0464 16.7277C22.4384 18.1554 21.5679 19.4358 20.4616 20.5313C19.358 21.6268 18.0696 22.484 16.6366 23.0813C15.1714 23.6893 13.6152 24 12.0107 24H11.9544C10.9717 23.9958 10.0284 23.677 9.11446 23.2543C7.52892 22.5211 5.79636 22.1752 4.07277 22.4593L3.19059 22.6048C2.74817 22.6777 2.26797 22.7683 1.88519 22.5348C1.71473 22.4308 1.57149 22.2866 1.46868 22.1153C1.2409 21.7359 1.32941 21.2627 1.40047 20.8259L1.54851 19.9159C1.82774 18.1994 1.48269 16.4748 0.753006 14.8962C0.330661 13.9826 0.0122489 13.0404 0.00799498 12.0563C-4.07491e-05 10.4357 0.310674 8.85806 0.926747 7.37413C1.51871 5.94109 2.38121 4.65537 3.47407 3.54912C4.56961 2.44287 5.84997 1.57501 7.27765 0.964294C8.75622 0.33215 10.3285 0.0107213 11.9518 0.00268555H12.0053C13.6152 0.00268555 15.1795 0.316079 16.6527 0.93483C18.0911 1.53751 19.3821 2.40805 20.4884 3.5143Z" fill="#FDA979"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dii_161_33236)">
|
||||
<path d="M18.2016 13.5312C17.3786 13.5312 16.7141 12.8477 16.7141 12C16.7141 11.1523 17.3786 10.4688 18.2016 10.4688C19.0247 10.4688 19.6891 11.1523 19.6891 12C19.6919 12.8477 19.0247 13.5312 18.2016 13.5312ZM12.0028 13.5312C11.1797 13.5312 10.5153 12.8477 10.5153 12C10.5153 11.1523 11.1797 10.4688 12.0028 10.4688C12.8258 10.4688 13.4903 11.1523 13.4903 12C13.4876 12.8477 12.8231 13.5312 12.0028 13.5312Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dii_161_33236)">
|
||||
<path d="M4.31378 12C4.31378 12.2011 4.35226 12.4002 4.42701 12.586C4.50177 12.7718 4.61133 12.9406 4.74946 13.0828C4.88759 13.2249 5.05157 13.3377 5.23204 13.4147C5.41251 13.4916 5.60594 13.5312 5.80128 13.5312C5.99662 13.5312 6.19005 13.4916 6.37052 13.4147C6.551 13.3377 6.71498 13.2249 6.8531 13.0828C6.99123 12.9406 7.1008 12.7718 7.17555 12.586C7.25031 12.4002 7.28878 12.2011 7.28878 12C7.28878 11.5939 7.13206 11.2044 6.8531 10.9172C6.57414 10.6301 6.19579 10.4688 5.80128 10.4688C5.40677 10.4688 5.02842 10.6301 4.74946 10.9172C4.4705 11.2044 4.31378 11.5939 4.31378 12Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ii_161_33236" x="0.00784302" y="-2.95135" width="24.0002" height="28.0583" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.95403"/>
|
||||
<feGaussianBlur stdDeviation="1.84627"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 0.78 0 0 0 0 0 0 0 0 0.29 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_161_33236"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.10694"/>
|
||||
<feGaussianBlur stdDeviation="1.05495"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_161_33236" result="effect2_innerShadow_161_33236"/>
|
||||
</filter>
|
||||
<filter id="filter1_dii_161_33236" x="6.74583" y="8.75536" width="16.7127" height="10.6014" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.05607"/>
|
||||
<feGaussianBlur stdDeviation="1.88473"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.8625 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33236"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33236" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.37071"/>
|
||||
<feGaussianBlur stdDeviation="0.856696"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.925 0 0 0 0 0.320667 0 0 0 0 0.0616667 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33236"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.514017"/>
|
||||
<feGaussianBlur stdDeviation="0.342678"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33236" result="effect3_innerShadow_161_33236"/>
|
||||
</filter>
|
||||
<filter id="filter2_dii_161_33236" x="0.544321" y="8.75536" width="10.5139" height="10.6014" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.05607"/>
|
||||
<feGaussianBlur stdDeviation="1.88473"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.8625 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33236"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33236" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.37071"/>
|
||||
<feGaussianBlur stdDeviation="0.856696"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.925 0 0 0 0 0.320667 0 0 0 0 0.0616667 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33236"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.514017"/>
|
||||
<feGaussianBlur stdDeviation="0.342678"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33236" result="effect3_innerShadow_161_33236"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
168
src/pages/dashboard/workplace/assets/content.svg
Normal file
@ -0,0 +1,168 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_161_33222)">
|
||||
<rect x="0.0283203" y="3.10522" width="12.9766" height="18.5206" rx="2.03077" fill="url(#paint0_linear_161_33222)"/>
|
||||
<mask id="mask0_161_33222" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="3" y="0" width="21" height="24">
|
||||
<path d="M5.41988 0.119385L15.1885 0.119384C15.8615 0.119384 16.5092 0.376475 16.9991 0.838099L22.4105 5.93776C22.9398 6.43659 23.2399 7.13171 23.2399 7.85905L23.2399 21.8994C23.2399 22.9929 22.3534 23.8794 21.2599 23.8794L5.41988 23.8794C4.32636 23.8794 3.43988 22.9929 3.43988 21.8994L3.43988 2.09938C3.43988 1.00586 4.32636 0.119385 5.41988 0.119385Z" fill="#4D72D3"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_161_33222)">
|
||||
<g filter="url(#filter0_dii_161_33222)">
|
||||
<path d="M5.41988 0.119385L15.1885 0.119384C15.8615 0.119384 16.5092 0.376475 16.9991 0.838099L22.4105 5.93776C22.9398 6.43659 23.2399 7.13171 23.2399 7.85905L23.2399 21.8994C23.2399 22.9929 22.3534 23.8794 21.2599 23.8794L5.41988 23.8794C4.32636 23.8794 3.43988 22.9929 3.43988 21.8994L3.43988 2.09938C3.43988 1.00586 4.32636 0.119385 5.41988 0.119385Z" fill="url(#paint1_linear_161_33222)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dii_161_33222)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.2799 6.05945L23.2399 6.05945L23.2399 0.119451L17.2999 0.119452L17.2999 4.07945C17.2999 5.17297 18.1864 6.05945 19.2799 6.05945Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter2_dii_161_33222)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2546 7.37437C11.5777 7.76209 11.5253 8.33833 11.1376 8.66144L8.71308 10.6818C8.51387 10.8479 8.25351 10.9214 7.99689 10.8842C7.74026 10.847 7.51153 10.7025 7.36769 10.4867L6.55952 9.27448C6.27956 8.85454 6.39304 8.28716 6.81298 8.0072C7.23292 7.72724 7.80029 7.84072 8.08025 8.26065L8.32429 8.62671L9.96751 7.25737C10.3552 6.93426 10.9315 6.98665 11.2546 7.37437Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dii_161_33222)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.8708 9.57522C12.8708 9.07052 13.28 8.66138 13.7847 8.66138L19.4418 8.66138C19.9465 8.66138 20.3557 9.07052 20.3557 9.57522C20.3557 10.0799 19.9465 10.4891 19.4418 10.4891L13.7847 10.4891C13.28 10.4891 12.8708 10.0799 12.8708 9.57522Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dii_161_33222)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.2549 15.2324C11.2549 14.7277 11.664 14.3186 12.1687 14.3186L19.4422 14.3186C19.9469 14.3186 20.356 14.7277 20.356 15.2324C20.356 15.7372 19.9469 16.1463 19.4422 16.1463L12.1687 16.1463C11.664 16.1463 11.2549 15.7372 11.2549 15.2324Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_dii_161_33222)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21375 15.2324C7.21375 14.7277 7.60556 14.3186 8.08889 14.3186L8.16629 14.3186C8.64962 14.3186 9.04144 14.7277 9.04144 15.2324C9.04144 15.7372 8.64962 16.1463 8.16629 16.1463L8.08889 16.1463C7.60556 16.1463 7.21375 15.7372 7.21375 15.2324Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dii_161_33222" x="-5.56012" y="-4.88062" width="37.8" height="41.76" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4.5"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.697798 0 0 0 0 0.346806 0 0 0 0 0.945833 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.131148"/>
|
||||
<feGaussianBlur stdDeviation="0.0327869"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.968627 0 0 0 0 0.6 0 0 0 0 0.984314 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.131148"/>
|
||||
<feGaussianBlur stdDeviation="0.131148"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<filter id="filter1_dii_161_33222" x="11.2854" y="-2.61439" width="17.9691" height="17.9691" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.28067"/>
|
||||
<feGaussianBlur stdDeviation="3.00728"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0450001 0 0 0 0 0.45 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.18712"/>
|
||||
<feGaussianBlur stdDeviation="1.36695"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.690196 0 0 0 0 0.776941 0 0 0 0 1 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.406154"/>
|
||||
<feGaussianBlur stdDeviation="0.546779"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<filter id="filter2_dii_161_33222" x="0.391376" y="4.31164" width="17.0896" height="15.8773" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.28067"/>
|
||||
<feGaussianBlur stdDeviation="3.00728"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.00314236 0 0 0 0 0.0782449 0 0 0 0 0.754167 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.21846"/>
|
||||
<feGaussianBlur stdDeviation="1.36695"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.57 0 0 0 0 0.6308 0 0 0 0 0.95 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.820168"/>
|
||||
<feGaussianBlur stdDeviation="0.546779"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<filter id="filter3_dii_161_33222" x="6.85628" y="5.92748" width="19.514" height="13.8568" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.28067"/>
|
||||
<feGaussianBlur stdDeviation="3.00728"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.00314236 0 0 0 0 0.0782449 0 0 0 0 0.754167 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.21846"/>
|
||||
<feGaussianBlur stdDeviation="1.36695"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.57 0 0 0 0 0.6308 0 0 0 0 0.95 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.820168"/>
|
||||
<feGaussianBlur stdDeviation="0.546779"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<filter id="filter4_dii_161_33222" x="5.24031" y="11.5847" width="21.1303" height="13.8568" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.28067"/>
|
||||
<feGaussianBlur stdDeviation="3.00728"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.00314236 0 0 0 0 0.0782449 0 0 0 0 0.754167 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.21846"/>
|
||||
<feGaussianBlur stdDeviation="1.36695"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.57 0 0 0 0 0.6308 0 0 0 0 0.95 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.820168"/>
|
||||
<feGaussianBlur stdDeviation="0.546779"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<filter id="filter5_dii_161_33222" x="3.15221" y="13.1001" width="9.95078" height="10.3883" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="3.28067"/>
|
||||
<feGaussianBlur stdDeviation="2.03077"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0450001 0 0 0 0 0.45 0 0 0 0.65 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_161_33222"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_161_33222" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.21846"/>
|
||||
<feGaussianBlur stdDeviation="1.36695"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.57 0 0 0 0 0.6308 0 0 0 0 0.95 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_161_33222"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.820168"/>
|
||||
<feGaussianBlur stdDeviation="0.546779"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_161_33222" result="effect3_innerShadow_161_33222"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_161_33222" x1="1.97655" y1="3.10522" x2="1.97655" y2="16.0647" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E14BFE"/>
|
||||
<stop offset="1" stop-color="#B84FD1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_161_33222" x1="3.43988" y1="0.119385" x2="3.43988" y2="23.8794" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E982FE"/>
|
||||
<stop offset="1" stop-color="#B353FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_161_33222">
|
||||
<rect width="24" height="24" fill="white" transform="matrix(1 -8.74228e-08 -8.74228e-08 -1 0 24)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
77
src/pages/dashboard/workplace/assets/increase.svg
Normal file
@ -0,0 +1,77 @@
|
||||
<svg width="24" height="27" viewBox="0 0 24 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_178_29628" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="1" y="4" width="21" height="23">
|
||||
<path d="M20.1248 4.00061H3.87483C2.83929 4.00061 1.99983 4.84008 1.99983 5.87561V24.6256C1.99983 25.6611 2.83929 26.5006 3.87483 26.5006H20.1248C21.1604 26.5006 21.9998 25.6611 21.9998 24.6256V5.87561C21.9998 4.84008 21.1604 4.00061 20.1248 4.00061Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_178_29628)">
|
||||
<g filter="url(#filter0_ii_178_29628)">
|
||||
<path d="M20.1248 4.00061H3.87483C2.83929 4.00061 1.99983 4.84008 1.99983 5.87561V23.6256C1.99983 24.6611 2.83929 25.5006 3.87483 25.5006H20.1248C21.1604 25.5006 21.9998 24.6611 21.9998 23.6256V5.87561C21.9998 4.84008 21.1604 4.00061 20.1248 4.00061Z" fill="url(#paint0_linear_178_29628)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter1_di_178_29628)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.8747 1.87549C14.6534 1.87549 15.3213 2.35019 15.6046 3.02601C15.6294 3.08496 15.6858 3.12552 15.7497 3.12549C16.7853 3.12549 17.6247 3.96495 17.6247 5.00049C17.6247 6.03602 16.7853 6.87549 15.7497 6.87549H8.24974C7.2142 6.87549 6.37474 6.03602 6.37474 5.00049C6.37474 3.96495 7.2142 3.12549 8.24974 3.12549C8.31366 3.12552 8.37011 3.08496 8.39483 3.02601C8.67819 2.35019 9.34603 1.87549 10.1247 1.87549H13.8747Z" fill="#FFFEFE"/>
|
||||
</g>
|
||||
<path d="M17.9719 9H6.02754V20.9444H17.9719V9Z" fill="white" fill-opacity="0.01"/>
|
||||
<g filter="url(#filter2_dii_178_29628)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5335 11.6822C12.5335 11.1511 12.9641 10.7206 13.4951 10.7206H17.0843C17.6154 10.7206 18.0459 11.1511 18.0459 11.6822V15.2715C18.0459 15.8025 17.6154 16.233 17.0843 16.233C16.5533 16.233 16.1227 15.8025 16.1227 15.2715V12.6438H13.4951C12.9641 12.6438 12.5335 12.2133 12.5335 11.6822Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.7488 10.9869C18.1327 11.3539 18.1464 11.9626 17.7795 12.3465L13.3481 16.9826C13.036 17.3092 12.5385 17.3744 12.1526 17.1393L9.93329 16.233L7.35673 18.8752C7.01826 19.2844 6.41212 19.3418 6.00289 19.0033C5.59365 18.6648 5.53629 18.0587 5.87476 17.6495L8.98186 14.3659C9.28641 13.9977 9.81516 13.9089 10.2232 14.1576L12.4926 15.0943L16.3892 11.0176C16.7562 10.6337 17.3649 10.62 17.7488 10.9869Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_ii_178_29628" x="1.99983" y="1.85775" width="20" height="24.7143" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-2.14286"/>
|
||||
<feGaussianBlur stdDeviation="1.07143"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0788195 0 0 0 0 0.633708 0 0 0 0 0.945833 0 0 0 0.7 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_178_29628"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.07143"/>
|
||||
<feGaussianBlur stdDeviation="1.07143"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.7 0"/>
|
||||
<feBlend mode="normal" in2="effect1_innerShadow_178_29628" result="effect2_innerShadow_178_29628"/>
|
||||
</filter>
|
||||
<filter id="filter1_di_178_29628" x="4.23188" y="0.452631" width="15.5357" height="9.28571" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.72"/>
|
||||
<feGaussianBlur stdDeviation="1.07143"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.373715 0 0 0 0 0.67555 0 0 0 0 0.954167 0 0 0 0.6 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_178_29628"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_178_29628" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.72"/>
|
||||
<feGaussianBlur stdDeviation="1.07143"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.0178125 0 0 0 0 0.37905 0 0 0 0 0.7125 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_178_29628"/>
|
||||
</filter>
|
||||
<filter id="filter2_dii_178_29628" x="1.32699" y="9.27808" width="21.0461" height="17.1577" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2.88476"/>
|
||||
<feGaussianBlur stdDeviation="2.16357"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.355333 0 0 0 0 0.683333 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_178_29628"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_178_29628" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-1.1539"/>
|
||||
<feGaussianBlur stdDeviation="0.72119"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.58 0 0 0 0 1 0 0 0 0.4 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect2_innerShadow_178_29628"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.582535"/>
|
||||
<feGaussianBlur stdDeviation="0.388357"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.879167 0 0 0 0 1 0 0 0 0 1 0 0 0 0.75 0"/>
|
||||
<feBlend mode="normal" in2="effect2_innerShadow_178_29628" result="effect3_innerShadow_178_29628"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_178_29628" x1="31.2857" y1="-4.2138" x2="0.7475" y2="26.2897" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#24B5E3"/>
|
||||
<stop offset="0.53305" stop-color="#56CCFF"/>
|
||||
<stop offset="1" stop-color="#0BA7FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.3 KiB |
36
src/pages/dashboard/workplace/carousel.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Carousel } from '@arco-design/web-react';
|
||||
|
||||
const imageSrc = [
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/f7e8fc1e09c42e30682526252365be1c.jpg~tplv-uwbnlip3yd-webp.webp',
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/94e8dd2d6dc4efb2c8cfd82c0ff02a2c.jpg~tplv-uwbnlip3yd-webp.webp',
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/ec447228c59ae1ebe185bab6cd776ca4.jpg~tplv-uwbnlip3yd-webp.webp',
|
||||
'//p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/1d1580d2a5a1e27415ff594c756eabd8.jpg~tplv-uwbnlip3yd-webp.webp',
|
||||
];
|
||||
function C() {
|
||||
return (
|
||||
<Carousel
|
||||
indicatorType="slider"
|
||||
showArrow="never"
|
||||
autoPlay
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 160,
|
||||
}}
|
||||
>
|
||||
{imageSrc.map((src, index) => (
|
||||
<div key={index}>
|
||||
<img
|
||||
src={src}
|
||||
style={{
|
||||
width: 280,
|
||||
transform: 'translateY(-30px)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
);
|
||||
}
|
||||
|
||||
export default C;
|
||||
88
src/pages/dashboard/workplace/content-percentage.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Spin, Typography } from '@arco-design/web-react';
|
||||
import { DonutChart } from 'bizcharts';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
|
||||
function PopularContent() {
|
||||
const t = useLocale(locale);
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchData = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get('/api/workplace/content-percentage')
|
||||
.then((res) => {
|
||||
setData(res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography.Title heading={6}>
|
||||
{t['workplace.contentPercentage']}
|
||||
</Typography.Title>
|
||||
<Spin loading={loading} style={{ display: 'block' }}>
|
||||
<DonutChart
|
||||
autoFit
|
||||
height={340}
|
||||
data={data}
|
||||
radius={0.7}
|
||||
innerRadius={0.65}
|
||||
angleField="count"
|
||||
colorField="type"
|
||||
color={['#21CCFF', '#313CA9', '#249EFF']}
|
||||
interactions={[
|
||||
{
|
||||
type: 'element-single-selected',
|
||||
},
|
||||
]}
|
||||
tooltip={{ showMarkers: false }}
|
||||
label={{
|
||||
visible: true,
|
||||
type: 'spider',
|
||||
formatter: (v) => `${(v.percent * 100).toFixed(0)}%`,
|
||||
style: {
|
||||
fill: '#86909C',
|
||||
fontSize: 14,
|
||||
},
|
||||
}}
|
||||
legend={{
|
||||
position: 'bottom',
|
||||
}}
|
||||
statistic={{
|
||||
title: {
|
||||
style: {
|
||||
fontSize: '14px',
|
||||
lineHeight: 2,
|
||||
color: 'rgb(--var(color-text-1))',
|
||||
},
|
||||
formatter: () => '内容量',
|
||||
},
|
||||
content: {
|
||||
style: {
|
||||
fontSize: '16px',
|
||||
color: 'rgb(--var(color-text-1))',
|
||||
},
|
||||
formatter: (_, data) => {
|
||||
const sum = data.reduce((a, b) => a + b.count, 0);
|
||||
return Number(sum).toLocaleString();
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PopularContent;
|
||||
33
src/pages/dashboard/workplace/docs.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Link, Card, Typography } from '@arco-design/web-react';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/docs.module.less';
|
||||
|
||||
const links = {
|
||||
react: 'https://arco.design/react/docs/start',
|
||||
vue: 'https://arco.design/vue/docs/start',
|
||||
designLab: 'https://arco.design/themes',
|
||||
materialMarket: 'https://arco.design/material/',
|
||||
};
|
||||
function QuickOperation() {
|
||||
const t = useLocale(locale);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography.Title heading={6}>{t['workplace.docs']}</Typography.Title>
|
||||
<Link>{t['workplace.seeMore']}</Link>
|
||||
</div>
|
||||
<div className={styles.docs}>
|
||||
{Object.entries(links).map(([key, value]) => (
|
||||
<Link className={styles.link} key={key} href={value} target="_blank">
|
||||
{t[`workplace.${key}`]}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuickOperation;
|
||||
41
src/pages/dashboard/workplace/index.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { Grid, Space } from '@arco-design/web-react';
|
||||
import Overview from './overview';
|
||||
import PopularContents from './popular-contents';
|
||||
import ContentPercentage from './content-percentage';
|
||||
import Shortcuts from './shortcuts';
|
||||
import Announcement from './announcement';
|
||||
import Carousel from './carousel';
|
||||
import Docs from './docs';
|
||||
import styles from './style/index.module.less';
|
||||
import './mock';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
const gutter = 16;
|
||||
|
||||
function Workplace() {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Space size={16} direction="vertical" className={styles.left}>
|
||||
<Overview />
|
||||
<Row gutter={gutter}>
|
||||
<Col span={12}>
|
||||
<PopularContents />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<ContentPercentage />
|
||||
</Col>
|
||||
</Row>
|
||||
</Space>
|
||||
<Space className={styles.right} size={16} direction="vertical">
|
||||
<Shortcuts />
|
||||
<Carousel />
|
||||
<Announcement />
|
||||
<Docs />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Workplace;
|
||||
78
src/pages/dashboard/workplace/locale/index.ts
Normal file
@ -0,0 +1,78 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'workplace.welcomeBack': 'Welcome Back,',
|
||||
'workplace.totalOnlyData': 'Total online data',
|
||||
'workplace.contentInMarket': 'Content in market',
|
||||
'workplace.comments': 'Comments',
|
||||
'workplace.growth': 'Growth',
|
||||
'workplace.contentData': 'Content Data',
|
||||
'workplace.1year': 'Nearly 1 Year',
|
||||
'workplace.seeMore': 'See More',
|
||||
'workplace.popularContents': 'Popular Contents',
|
||||
'workplace.text': 'Text',
|
||||
'workplace.image': 'Image',
|
||||
'workplace.video': 'Video',
|
||||
'workplace.column.rank': 'Rank',
|
||||
'workplace.column.title': 'Title',
|
||||
'workplace.column.pv': 'PV',
|
||||
'workplace.column.increase': 'Daily Increase',
|
||||
'workplace.contentPercentage': 'Percentage of content categories',
|
||||
'workplace.shortcuts': 'Shortcuts',
|
||||
'workplace.manage': 'Manage',
|
||||
'workplace.contentMgmt': 'Management',
|
||||
'workplace.contentStatistic': 'Statistic',
|
||||
'workplace.advancedMgmt': 'Advance',
|
||||
'workplace.onlinePromotion': 'Promotion',
|
||||
'workplace.marketing': 'Marketing',
|
||||
'workplace.recent': 'Recent',
|
||||
'workplace.announcement': 'Announcement',
|
||||
'workplace.activity': 'Activity',
|
||||
'workplace.info': 'Info',
|
||||
'workplace.notice': 'Notice',
|
||||
'workplace.docs': 'Document',
|
||||
'workplace.pecs': 'pecs',
|
||||
'workplace.designLab': 'DesignLab',
|
||||
'workplace.materialMarket': 'MaterialMarket',
|
||||
'workplace.react': 'React Quick Start',
|
||||
'workplace.vue': 'Vue Quick Start',
|
||||
},
|
||||
'zh-CN': {
|
||||
'workplace.welcomeBack': '欢迎回来,',
|
||||
'workplace.totalOnlyData': '线上总数据',
|
||||
'workplace.contentInMarket': '投放中的内容',
|
||||
'workplace.comments': '日新增评论',
|
||||
'workplace.growth': '较昨日新增',
|
||||
'workplace.contentData': '内容数据',
|
||||
'workplace.1year': '近1年',
|
||||
'workplace.seeMore': '查看更多',
|
||||
'workplace.popularContents': '线上热门内容',
|
||||
'workplace.text': '文本',
|
||||
'workplace.image': '图文',
|
||||
'workplace.video': '视频',
|
||||
'workplace.column.rank': '排名',
|
||||
'workplace.column.title': '内容标题',
|
||||
'workplace.column.pv': '点击量',
|
||||
'workplace.column.increase': '日涨幅',
|
||||
'workplace.contentPercentage': '内容类别占比',
|
||||
'workplace.shortcuts': '快捷入口',
|
||||
'workplace.manage': '管理',
|
||||
'workplace.contentMgmt': '内容管理',
|
||||
'workplace.contentStatistic': '内容数据',
|
||||
'workplace.advancedMgmt': '高级管理',
|
||||
'workplace.onlinePromotion': '线上推广',
|
||||
'workplace.marketing': '内容投放',
|
||||
'workplace.recent': '最近访问',
|
||||
'workplace.announcement': '公告',
|
||||
'workplace.activity': '活动',
|
||||
'workplace.info': '消息',
|
||||
'workplace.notice': '通知',
|
||||
'workplace.docs': '文档中心',
|
||||
'workplace.pecs': '个',
|
||||
'workplace.designLab': '风格配置平台',
|
||||
'workplace.materialMarket': '物料市场',
|
||||
'workplace.react': 'React 组件库',
|
||||
'workplace.vue': 'Vue 组件库',
|
||||
},
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
117
src/pages/dashboard/workplace/mock/index.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import Mock from 'mockjs';
|
||||
import qs from 'query-string';
|
||||
import setupMock from '@/utils/setupMock';
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
Mock.mock(new RegExp('/api/workplace/overview-content'), () => {
|
||||
const year = new Date().getFullYear();
|
||||
const getLineData = () => {
|
||||
return new Array(12).fill(0).map((_item, index) => ({
|
||||
date: `${year}-${index + 1}`,
|
||||
count: Mock.Random.natural(20000, 75000),
|
||||
}));
|
||||
};
|
||||
return {
|
||||
allContents: '373.5w+',
|
||||
liveContents: '368',
|
||||
increaseComments: '8874',
|
||||
growthRate: '2.8%',
|
||||
chartData: getLineData(),
|
||||
};
|
||||
});
|
||||
|
||||
const getList = () => {
|
||||
const { list } = Mock.mock({
|
||||
'list|100': [
|
||||
{
|
||||
'rank|+1': 1,
|
||||
title: () =>
|
||||
Mock.Random.pick([
|
||||
'经济日报:财政政策要精准提升效能',
|
||||
'“双12”遇冷消费者厌倦了电商平台的促销“套路”',
|
||||
'致敬坚守战“疫”一线的社区工作者',
|
||||
'普高还是职高?家长们陷入选校难题',
|
||||
]),
|
||||
pv: function () {
|
||||
return 500000 - 3200 * this.rank;
|
||||
},
|
||||
increase: '@float(-1, 1)',
|
||||
},
|
||||
],
|
||||
});
|
||||
return list;
|
||||
};
|
||||
const listText = getList();
|
||||
const listPic = getList();
|
||||
const listVideo = getList();
|
||||
|
||||
Mock.mock(new RegExp('/api/workplace/popular-contents'), (params) => {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 5,
|
||||
category = 0,
|
||||
} = qs.parseUrl(params.url).query as unknown as {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
category?: number;
|
||||
};
|
||||
|
||||
const list = [listText, listPic, listVideo][Number(category)];
|
||||
return {
|
||||
list: list.slice((page - 1) * pageSize, page * pageSize),
|
||||
total: 100,
|
||||
};
|
||||
});
|
||||
|
||||
Mock.mock(new RegExp('/api/workplace/content-percentage'), () => {
|
||||
return [
|
||||
{
|
||||
type: '纯文本',
|
||||
count: 148564,
|
||||
percent: 0.16,
|
||||
},
|
||||
{
|
||||
type: '图文类',
|
||||
count: 334271,
|
||||
percent: 0.36,
|
||||
},
|
||||
{
|
||||
type: '视频类',
|
||||
count: 445695,
|
||||
percent: 0.48,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
Mock.mock(new RegExp('/api/workplace/announcement'), () => {
|
||||
return [
|
||||
{
|
||||
type: 'activity',
|
||||
key: '1',
|
||||
content: '内容最新优惠活动',
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
key: '2',
|
||||
content: '新增内容尚未通过审核,详情请点击查看。',
|
||||
},
|
||||
{
|
||||
type: 'notice',
|
||||
key: '3',
|
||||
content: '当前产品试用期即将结束,如需续费请点击查看。',
|
||||
},
|
||||
{
|
||||
type: 'notice',
|
||||
key: '4',
|
||||
content: '1 月新系统升级计划通知',
|
||||
},
|
||||
{
|
||||
type: 'info',
|
||||
key: '5',
|
||||
content: '新增内容已经通过审核,详情请点击查看。',
|
||||
},
|
||||
];
|
||||
});
|
||||
},
|
||||
});
|
||||
156
src/pages/dashboard/workplace/overview.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { useState, useEffect, ReactNode } from 'react';
|
||||
import {
|
||||
Grid,
|
||||
Card,
|
||||
Typography,
|
||||
Divider,
|
||||
Skeleton,
|
||||
Link,
|
||||
} from '@arco-design/web-react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { IconCaretUp } from '@arco-design/web-react/icon';
|
||||
import OverviewAreaLine from '@/components/Chart/overview-area-line';
|
||||
import axios from 'axios';
|
||||
import locale from './locale';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import styles from './style/overview.module.less';
|
||||
import IconCalendar from './assets/calendar.svg';
|
||||
import IconComments from './assets/comments.svg';
|
||||
import IconContent from './assets/content.svg';
|
||||
import IconIncrease from './assets/increase.svg';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
|
||||
type StatisticItemType = {
|
||||
icon?: ReactNode;
|
||||
title?: ReactNode;
|
||||
count?: ReactNode;
|
||||
loading?: boolean;
|
||||
unit?: ReactNode;
|
||||
};
|
||||
|
||||
function StatisticItem(props: StatisticItemType) {
|
||||
const { icon, title, count, loading, unit } = props;
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.icon}>{icon}</div>
|
||||
<div>
|
||||
<Skeleton loading={loading} text={{ rows: 2, width: 60 }} animation>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.count}>
|
||||
{count}
|
||||
<span className={styles.unit}>{unit}</span>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DataType = {
|
||||
allContents?: string;
|
||||
liveContents?: string;
|
||||
increaseComments?: string;
|
||||
growthRate?: string;
|
||||
chartData?: { count?: number; date?: string }[];
|
||||
down?: boolean;
|
||||
};
|
||||
|
||||
function Overview() {
|
||||
const [data, setData] = useState<DataType>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const t = useLocale(locale);
|
||||
|
||||
const userInfo = useSelector((state: any) => state.userInfo || {});
|
||||
|
||||
const fetchData = () => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get('/api/workplace/overview-content')
|
||||
.then((res) => {
|
||||
setData(res.data);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Typography.Title heading={5}>
|
||||
{t['workplace.welcomeBack']}
|
||||
{userInfo.name}
|
||||
</Typography.Title>
|
||||
<Divider />
|
||||
<Row>
|
||||
<Col flex={1}>
|
||||
<StatisticItem
|
||||
icon={<IconCalendar />}
|
||||
title={t['workplace.totalOnlyData']}
|
||||
count={data.allContents}
|
||||
loading={loading}
|
||||
unit={t['workplace.pecs']}
|
||||
/>
|
||||
</Col>
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Col flex={1}>
|
||||
<StatisticItem
|
||||
icon={<IconContent />}
|
||||
title={t['workplace.contentInMarket']}
|
||||
count={data.liveContents}
|
||||
loading={loading}
|
||||
unit={t['workplace.pecs']}
|
||||
/>
|
||||
</Col>
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Col flex={1}>
|
||||
<StatisticItem
|
||||
icon={<IconComments />}
|
||||
title={t['workplace.comments']}
|
||||
count={data.increaseComments}
|
||||
loading={loading}
|
||||
unit={t['workplace.pecs']}
|
||||
/>
|
||||
</Col>
|
||||
<Divider type="vertical" className={styles.divider} />
|
||||
<Col flex={1}>
|
||||
<StatisticItem
|
||||
icon={<IconIncrease />}
|
||||
title={t['workplace.growth']}
|
||||
count={
|
||||
<span>
|
||||
{data.growthRate}{' '}
|
||||
<IconCaretUp
|
||||
style={{ fontSize: 18, color: 'rgb(var(--green-6))' }}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
loading={loading}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Divider />
|
||||
<div>
|
||||
<div className={styles.ctw}>
|
||||
<Typography.Paragraph
|
||||
className={styles['chart-title']}
|
||||
style={{ marginBottom: 0 }}
|
||||
>
|
||||
{t['workplace.contentData']}
|
||||
<span className={styles['chart-sub-title']}>
|
||||
({t['workplace.1year']})
|
||||
</span>
|
||||
</Typography.Paragraph>
|
||||
<Link>{t['workplace.seeMore']}</Link>
|
||||
</div>
|
||||
<OverviewAreaLine data={data.chartData} loading={loading} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Overview;
|
||||
115
src/pages/dashboard/workplace/popular-contents.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Link, Card, Radio, Table, Typography } from '@arco-design/web-react';
|
||||
import { IconCaretDown, IconCaretUp } from '@arco-design/web-react/icon';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/popular-contents.module.less';
|
||||
|
||||
function PopularContent() {
|
||||
const t = useLocale(locale);
|
||||
const [type, setType] = useState(0);
|
||||
const [data, setData] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const fetchData = useCallback(() => {
|
||||
setLoading(true);
|
||||
axios
|
||||
.get(
|
||||
`/api/workplace/popular-contents?page=${page}&pageSize=5&category=${type}`
|
||||
)
|
||||
.then((res) => {
|
||||
setData(res.data.list);
|
||||
setTotal(res.data.total);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [page, type]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [page, fetchData]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t['workplace.column.rank'],
|
||||
dataIndex: 'rank',
|
||||
width: 65,
|
||||
},
|
||||
{
|
||||
title: t['workplace.column.title'],
|
||||
dataIndex: 'title',
|
||||
render: (x) => (
|
||||
<Typography.Paragraph style={{ margin: 0 }} ellipsis>
|
||||
{x}
|
||||
</Typography.Paragraph>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t['workplace.column.pv'],
|
||||
dataIndex: 'pv',
|
||||
width: 100,
|
||||
render: (text) => {
|
||||
return `${text / 1000}k`;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t['workplace.column.increase'],
|
||||
dataIndex: 'increase',
|
||||
sorter: (a, b) => a.increase - b.increase,
|
||||
width: 110,
|
||||
render: (text) => {
|
||||
return (
|
||||
<span>
|
||||
{`${(text * 100).toFixed(2)}%`}
|
||||
<span className={styles['symbol']}>
|
||||
{text < 0 ? (
|
||||
<IconCaretUp style={{ color: 'rgb(var(--green-6))' }} />
|
||||
) : (
|
||||
<IconCaretDown style={{ color: 'rgb(var(--red-6))' }} />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography.Title heading={6}>
|
||||
{t['workplace.popularContents']}
|
||||
</Typography.Title>
|
||||
<Link>{t['workplace.seeMore']}</Link>
|
||||
</div>
|
||||
<Radio.Group
|
||||
type="button"
|
||||
value={type}
|
||||
onChange={setType}
|
||||
options={[
|
||||
{ label: t['workplace.text'], value: 0 },
|
||||
{ label: t['workplace.image'], value: 1 },
|
||||
{ label: t['workplace.video'], value: 2 },
|
||||
]}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Table
|
||||
rowKey="rank"
|
||||
columns={columns}
|
||||
data={data}
|
||||
loading={loading}
|
||||
tableLayoutFixed
|
||||
onChange={(pagination) => {
|
||||
setPage(pagination.current);
|
||||
}}
|
||||
pagination={{ total, current: page, pageSize: 5, simple: true }}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PopularContent;
|
||||
117
src/pages/dashboard/workplace/shortcuts.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Link,
|
||||
Card,
|
||||
Divider,
|
||||
Message,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import {
|
||||
IconFile,
|
||||
IconStorage,
|
||||
IconSettings,
|
||||
IconMobile,
|
||||
IconFire,
|
||||
} from '@arco-design/web-react/icon';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/shortcuts.module.less';
|
||||
|
||||
function Shortcuts() {
|
||||
const t = useLocale(locale);
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
title: t['workplace.contentMgmt'],
|
||||
key: 'Content Management',
|
||||
icon: <IconFile />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.contentStatistic'],
|
||||
key: 'Content Statistic',
|
||||
icon: <IconStorage />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.advancedMgmt'],
|
||||
key: 'Advanced Management',
|
||||
icon: <IconSettings />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.onlinePromotion'],
|
||||
key: 'Online Promotion',
|
||||
icon: <IconMobile />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.marketing'],
|
||||
key: 'Marketing',
|
||||
icon: <IconFire />,
|
||||
},
|
||||
];
|
||||
|
||||
const recentShortcuts = [
|
||||
{
|
||||
title: t['workplace.contentStatistic'],
|
||||
key: 'Content Statistic',
|
||||
icon: <IconStorage />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.contentMgmt'],
|
||||
key: 'Content Management',
|
||||
icon: <IconFile />,
|
||||
},
|
||||
{
|
||||
title: t['workplace.advancedMgmt'],
|
||||
key: 'Advanced Management',
|
||||
icon: <IconSettings />,
|
||||
},
|
||||
];
|
||||
|
||||
function onClickShortcut(key) {
|
||||
Message.info({
|
||||
content: (
|
||||
<span>
|
||||
You clicked <b>{key}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Typography.Title heading={6}>
|
||||
{t['workplace.shortcuts']}
|
||||
</Typography.Title>
|
||||
<Link>{t['workplace.seeMore']}</Link>
|
||||
</div>
|
||||
<div className={styles.shortcuts}>
|
||||
{shortcuts.map((shortcut) => (
|
||||
<div
|
||||
className={styles.item}
|
||||
key={shortcut.key}
|
||||
onClick={() => onClickShortcut(shortcut.key)}
|
||||
>
|
||||
<div className={styles.icon}>{shortcut.icon}</div>
|
||||
<div className={styles.title}>{shortcut.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Divider />
|
||||
<div className={styles.recent}>{t['workplace.recent']}</div>
|
||||
<div className={styles.shortcuts}>
|
||||
{recentShortcuts.map((shortcut) => (
|
||||
<div
|
||||
className={styles.item}
|
||||
key={shortcut.key}
|
||||
onClick={() => onClickShortcut(shortcut.key)}
|
||||
>
|
||||
<div className={styles.icon}>{shortcut.icon}</div>
|
||||
<div className={styles.title}>{shortcut.title}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default Shortcuts;
|
||||
19
src/pages/dashboard/workplace/style/announcement.module.less
Normal file
@ -0,0 +1,19 @@
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.link {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-left: 4px;
|
||||
color: var(--color-text-2);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
15
src/pages/dashboard/workplace/style/docs.module.less
Normal file
@ -0,0 +1,15 @@
|
||||
.docs {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--color-text-2);
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&:hover {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
}
|
||||
24
src/pages/dashboard/workplace/style/index.module.less
Normal file
@ -0,0 +1,24 @@
|
||||
.banner {
|
||||
background-color: var(--color-bg-2);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left {
|
||||
width: calc(100% - 296px);
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background-color: var(--color-bg-2);
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
69
src/pages/dashboard/workplace/style/overview.module.less
Normal file
@ -0,0 +1,69 @@
|
||||
.container {
|
||||
padding: 20px;
|
||||
|
||||
:global(.arco-divider-horizontal) {
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
}
|
||||
|
||||
:global(.arco-divider-vertical) {
|
||||
border-left: 1px solid var(--color-border-1);
|
||||
}
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
background-color: var(--color-fill-2);
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
|
||||
.unit {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-2);
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.ctw {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chart-sub-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
.symbol {
|
||||
font-size: 10px;
|
||||
margin-left: 4px;
|
||||
|
||||
> svg {
|
||||
vertical-align: 0;
|
||||
}
|
||||
}
|
||||
57
src/pages/dashboard/workplace/style/shortcuts.module.less
Normal file
@ -0,0 +1,57 @@
|
||||
.shortcuts {
|
||||
display: grid;
|
||||
grid-template-columns: 33.33% 33.33% 33.33%;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
background-color: var(--color-primary-light-1);
|
||||
|
||||
svg {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
color: rgb(var(--primary-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background-color: var(--color-fill-2);
|
||||
margin-bottom: 4px;
|
||||
|
||||
svg {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.recent {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
91
src/pages/list/search-table/constants.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { Button, Typography, Badge } from '@arco-design/web-react';
|
||||
import IconText from './icons/text.svg';
|
||||
import IconHorizontalVideo from './icons/horizontal.svg';
|
||||
import IconVerticalVideo from './icons/vertical.svg';
|
||||
import dayjs from 'dayjs';
|
||||
import styles from './style/index.module.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const ContentType = ['图文', '横版短视频', '竖版短视频'];
|
||||
export const FilterType = ['规则筛选', '人工'];
|
||||
export const Status = ['未上线', '已上线'];
|
||||
|
||||
const ContentIcon = [
|
||||
<IconText key={0} />,
|
||||
<IconHorizontalVideo key={1} />,
|
||||
<IconVerticalVideo key={2} />,
|
||||
];
|
||||
|
||||
export function getColumns(
|
||||
t: any,
|
||||
callback: (record: Record<string, any>, type: string) => Promise<void>
|
||||
) {
|
||||
return [
|
||||
{
|
||||
title: t['searchTable.columns.id'],
|
||||
dataIndex: 'id',
|
||||
render: (value) => <Text copyable>{value}</Text>,
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.name'],
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.contentType'],
|
||||
dataIndex: 'contentType',
|
||||
render: (value) => (
|
||||
<div className={styles['content-type']}>
|
||||
{ContentIcon[value]}
|
||||
{ContentType[value]}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.filterType'],
|
||||
dataIndex: 'filterType',
|
||||
render: (value) => FilterType[value],
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.contentNum'],
|
||||
dataIndex: 'count',
|
||||
sorter: (a, b) => a.count - b.count,
|
||||
render(x) {
|
||||
return Number(x).toLocaleString();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.createdTime'],
|
||||
dataIndex: 'createdTime',
|
||||
render: (x) => dayjs().subtract(x, 'days').format('YYYY-MM-DD HH:mm:ss'),
|
||||
sorter: (a, b) => b.createdTime - a.createdTime,
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.status'],
|
||||
dataIndex: 'status',
|
||||
render: (x) => {
|
||||
if (x === 0) {
|
||||
return <Badge status="error" text={Status[x]}></Badge>;
|
||||
}
|
||||
return <Badge status="success" text={Status[x]}></Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t['searchTable.columns.operations'],
|
||||
dataIndex: 'operations',
|
||||
headerCellStyle: { paddingLeft: '15px' },
|
||||
render: (_, record) => (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => callback(record, 'view')}
|
||||
>
|
||||
{t['searchTable.columns.operations.view']}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export default () => ContentIcon;
|
||||
135
src/pages/list/search-table/form.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React, { useContext } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
DatePicker,
|
||||
Button,
|
||||
Grid,
|
||||
} from '@arco-design/web-react';
|
||||
import { GlobalContext } from '@/context';
|
||||
import locale from './locale';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import { IconRefresh, IconSearch } from '@arco-design/web-react/icon';
|
||||
import { ContentType, FilterType, Status } from './constants';
|
||||
import styles from './style/index.module.less';
|
||||
|
||||
const { Row, Col } = Grid;
|
||||
const { useForm } = Form;
|
||||
|
||||
function SearchForm(props: {
|
||||
onSearch: (values: Record<string, any>) => void;
|
||||
}) {
|
||||
const { lang } = useContext(GlobalContext);
|
||||
|
||||
const t = useLocale(locale);
|
||||
const [form] = useForm();
|
||||
|
||||
const handleSubmit = () => {
|
||||
const values = form.getFieldsValue();
|
||||
props.onSearch(values);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
form.resetFields();
|
||||
props.onSearch({});
|
||||
};
|
||||
|
||||
const colSpan = lang === 'zh-CN' ? 8 : 12;
|
||||
|
||||
return (
|
||||
<div className={styles['search-form-wrapper']}>
|
||||
<Form
|
||||
form={form}
|
||||
className={styles['search-form']}
|
||||
labelAlign="left"
|
||||
labelCol={{ span: 5 }}
|
||||
wrapperCol={{ span: 19 }}
|
||||
>
|
||||
<Row gutter={24}>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item label={t['searchTable.columns.id']} field="id">
|
||||
<Input placeholder={t['searchForm.id.placeholder']} allowClear />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item label={t['searchTable.columns.name']} field="name">
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={t['searchForm.name.placeholder']}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
label={t['searchTable.columns.contentType']}
|
||||
field="contentType"
|
||||
>
|
||||
<Select
|
||||
placeholder={t['searchForm.all.placeholder']}
|
||||
options={ContentType.map((item, index) => ({
|
||||
label: item,
|
||||
value: index,
|
||||
}))}
|
||||
mode="multiple"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
label={t['searchTable.columns.filterType']}
|
||||
field="filterType"
|
||||
>
|
||||
<Select
|
||||
placeholder={t['searchForm.all.placeholder']}
|
||||
options={FilterType.map((item, index) => ({
|
||||
label: item,
|
||||
value: index,
|
||||
}))}
|
||||
mode="multiple"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item
|
||||
label={t['searchTable.columns.createdTime']}
|
||||
field="createdTime"
|
||||
>
|
||||
<DatePicker.RangePicker
|
||||
allowClear
|
||||
style={{ width: '100%' }}
|
||||
disabledDate={(date) => dayjs(date).isAfter(dayjs())}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={colSpan}>
|
||||
<Form.Item label={t['searchTable.columns.status']} field="status">
|
||||
<Select
|
||||
placeholder={t['searchForm.all.placeholder']}
|
||||
options={Status.map((item, index) => ({
|
||||
label: item,
|
||||
value: index,
|
||||
}))}
|
||||
mode="multiple"
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<div className={styles['right-button']}>
|
||||
<Button type="primary" icon={<IconSearch />} onClick={handleSubmit}>
|
||||
{t['searchTable.form.search']}
|
||||
</Button>
|
||||
<Button icon={<IconRefresh />} onClick={handleReset}>
|
||||
{t['searchTable.form.reset']}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchForm;
|
||||
20
src/pages/list/search-table/icons/horizontal.svg
Normal file
@ -0,0 +1,20 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="1" width="14" height="14" rx="1.67" fill="#FFDDE0"/>
|
||||
<path d="M0.0708579 2.61134C0.0786468 1.68906 0.832619 0.94772 1.7549 0.955509L12.414 1.04553C13.3363 1.05332 14.0776 1.80729 14.0699 2.72957L13.999 11.1181L1.49778e-05 10.9999L0.0708579 2.61134Z" fill="#FF8B96"/>
|
||||
<g opacity="0.9" filter="url(#filter0_d_422_41703)">
|
||||
<path d="M5.32269 7.78472V4.65415C5.32269 4.18777 5.83148 3.8997 6.23139 4.13965L8.8402 5.70494C9.2286 5.93798 9.2286 6.50089 8.8402 6.73393L6.23139 8.29922C5.83148 8.53917 5.32269 8.2511 5.32269 7.78472Z" fill="#FFEDEF"/>
|
||||
</g>
|
||||
<rect opacity="0.6" width="10" height="1" rx="0.4" transform="matrix(1 0 0 -1 2.04199 13.5156)" fill="#FF727F"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_422_41703" x="3.6636" y="2.39413" width="7.12699" height="7.65071" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.829547"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.8625 0 0 0 0 0.280312 0 0 0 0 0.33552 0 0 0 0.8 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_422_41703"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_422_41703" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
41
src/pages/list/search-table/icons/text.svg
Normal file
@ -0,0 +1,41 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 2C0 0.89543 0.895431 0 2 0H12C13.1046 0 14 0.895431 14 2V12C14 13.1046 13.1046 14 12 14H2C0.895431 14 0 13.1046 0 12V2Z" fill="url(#paint0_linear_422_41656)"/>
|
||||
<g opacity="0.9" filter="url(#filter0_d_422_41656)">
|
||||
<path d="M4.48218 3.23096C4.81406 3.23101 5.13232 3.36289 5.36695 3.59758C5.60159 3.83228 5.73337 4.15056 5.73332 4.48241C5.73326 4.81426 5.60137 5.13249 5.36666 5.36711C5.13195 5.60172 4.81364 5.73349 4.48176 5.73344C4.14989 5.73333 3.83165 5.6014 3.59705 5.36666C3.36246 5.13193 3.23072 4.81363 3.23084 4.48178C3.23095 4.14993 3.36289 3.83172 3.59764 3.59714C3.83239 3.36257 4.15072 3.23085 4.4826 3.23096H4.48218Z" fill="white"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip0_422_41656)">
|
||||
<g opacity="0.9" filter="url(#filter1_d_422_41656)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.92035 17.5178C10.868 17.5178 12.447 15.0428 12.447 11.9896C12.447 8.93649 10.868 6.46143 8.92035 6.46143C7.69985 6.46143 6.62416 7.43332 5.99105 8.91033C5.58344 8.38402 5.03884 8.06253 4.44061 8.06253C3.17724 8.06253 2.15308 9.49636 2.15308 11.2651C2.15308 13.0338 3.17724 14.4676 4.44061 14.4676C4.87779 14.4676 5.28633 14.2959 5.6337 13.9981C6.14641 16.0582 7.42464 17.5178 8.92035 17.5178Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_422_41656" x="0.308552" y="2.25686" width="8.34704" height="8.34701" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.94819"/>
|
||||
<feGaussianBlur stdDeviation="1.46114"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.207843 0 0 0 0 0.701961 0 0 0 0 0.94902 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_422_41656"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_422_41656" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_422_41656" x="-0.5182" y="5.571" width="15.6364" height="16.3989" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.78085"/>
|
||||
<feGaussianBlur stdDeviation="1.33564"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.207843 0 0 0 0 0.701961 0 0 0 0 0.94902 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_422_41656"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_422_41656" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_422_41656" x1="0" y1="0" x2="9.36513" y2="14.6703" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B9FFF"/>
|
||||
<stop offset="0.0001" stop-color="#479AFB"/>
|
||||
<stop offset="1" stop-color="#77C6FF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_422_41656">
|
||||
<rect x="2.15375" y="6.46143" width="10.2939" height="4.95632" rx="2" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
24
src/pages/list/search-table/icons/vertical.svg
Normal file
@ -0,0 +1,24 @@
|
||||
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect opacity="0.9" width="13" height="16" rx="1.67" fill="url(#paint0_linear_422_41748)"/>
|
||||
<g opacity="0.9" filter="url(#filter0_d_422_41748)">
|
||||
<path d="M5 7.91745V5.08255C5 4.61129 5.51837 4.32398 5.918 4.57375L8.18592 5.9912C8.56192 6.2262 8.56192 6.7738 8.18592 7.0088L5.918 8.42625C5.51837 8.67602 5 8.38871 5 7.91745Z" fill="white"/>
|
||||
</g>
|
||||
<rect opacity="0.8" width="9" height="1" rx="0.315789" transform="matrix(1 0 0 -1 2 12)" fill="#FFF5E8"/>
|
||||
<rect opacity="0.8" width="6" height="1" rx="0.315789" transform="matrix(1 0 0 -1 2 14)" fill="#FFF5E8"/>
|
||||
<defs>
|
||||
<filter id="filter0_d_422_41748" x="3.73684" y="3.21853" width="5.99424" height="6.56294" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.631579"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.870833 0 0 0 0 0.554311 0 0 0 0 0.148767 0 0 0 0.8 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_422_41748"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_422_41748" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_422_41748" x1="0.5" y1="0.5" x2="12.5" y2="15.5" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF8A00"/>
|
||||
<stop offset="1" stop-color="#FFC581"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
120
src/pages/list/search-table/index.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Card,
|
||||
PaginationProps,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
} from '@arco-design/web-react';
|
||||
import PermissionWrapper from '@/components/PermissionWrapper';
|
||||
import { IconDownload, IconPlus } from '@arco-design/web-react/icon';
|
||||
import axios from 'axios';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import SearchForm from './form';
|
||||
import locale from './locale';
|
||||
import styles from './style/index.module.less';
|
||||
import './mock';
|
||||
import { getColumns } from './constants';
|
||||
|
||||
const { Title } = Typography;
|
||||
export const ContentType = ['图文', '横版短视频', '竖版短视频'];
|
||||
export const FilterType = ['规则筛选', '人工'];
|
||||
export const Status = ['已上线', '未上线'];
|
||||
|
||||
function SearchTable() {
|
||||
const t = useLocale(locale);
|
||||
|
||||
const tableCallback = async (record, type) => {
|
||||
console.log(record, type);
|
||||
};
|
||||
|
||||
const columns = useMemo(() => getColumns(t, tableCallback), [t]);
|
||||
|
||||
const [data, setData] = useState([]);
|
||||
const [pagination, setPatination] = useState<PaginationProps>({
|
||||
sizeCanChange: true,
|
||||
showTotal: true,
|
||||
pageSize: 10,
|
||||
current: 1,
|
||||
pageSizeChangeResetCurrent: true,
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [formParams, setFormParams] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
}, [pagination.current, pagination.pageSize, JSON.stringify(formParams)]);
|
||||
|
||||
function fetchData() {
|
||||
const { current, pageSize } = pagination;
|
||||
setLoading(true);
|
||||
axios
|
||||
.get('/api/list', {
|
||||
params: {
|
||||
page: current,
|
||||
pageSize,
|
||||
...formParams,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
setData(res.data.list);
|
||||
setPatination({
|
||||
...pagination,
|
||||
current,
|
||||
pageSize,
|
||||
total: res.data.total,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeTable({ current, pageSize }) {
|
||||
setPatination({
|
||||
...pagination,
|
||||
current,
|
||||
pageSize,
|
||||
});
|
||||
}
|
||||
|
||||
function handleSearch(params) {
|
||||
setPatination({ ...pagination, current: 1 });
|
||||
setFormParams(params);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Title heading={6}>{t['menu.list.searchTable']}</Title>
|
||||
<SearchForm onSearch={handleSearch} />
|
||||
<PermissionWrapper
|
||||
requiredPermissions={[
|
||||
{ resource: 'menu.list.searchTable', actions: ['write'] },
|
||||
]}
|
||||
>
|
||||
<div className={styles['button-group']}>
|
||||
<Space>
|
||||
<Button type="primary" icon={<IconPlus />}>
|
||||
{t['searchTable.operations.add']}
|
||||
</Button>
|
||||
<Button>{t['searchTable.operations.upload']}</Button>
|
||||
</Space>
|
||||
<Space>
|
||||
<Button icon={<IconDownload />}>
|
||||
{t['searchTable.operation.download']}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</PermissionWrapper>
|
||||
<Table
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
onChange={onChangeTable}
|
||||
pagination={pagination}
|
||||
columns={columns}
|
||||
data={data}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchTable;
|
||||
52
src/pages/list/search-table/locale/index.ts
Normal file
@ -0,0 +1,52 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'menu.list': 'List',
|
||||
'menu.list.searchTable': 'Search Table',
|
||||
'searchTable.form.search': 'Search',
|
||||
'searchTable.form.reset': 'Reset',
|
||||
'searchTable.columns.id': 'Collection ID',
|
||||
'searchTable.columns.name': 'Collection Name',
|
||||
'searchTable.columns.contentType': 'Content genre',
|
||||
'searchTable.columns.filterType': 'Filter method',
|
||||
'searchTable.columns.createdTime': 'Creation time',
|
||||
'searchTable.columns.status': 'Status',
|
||||
'searchTable.columns.contentNum': 'Content quantity',
|
||||
'searchTable.columns.operations': 'Operation',
|
||||
'searchTable.columns.operations.view': 'View',
|
||||
'searchTable.columns.operations.update': 'Edit',
|
||||
'searchTable.columns.operations.offline': 'Offline',
|
||||
'searchTable.columns.operations.online': 'Online',
|
||||
'searchTable.operations.add': 'New',
|
||||
'searchTable.operations.upload': 'Bulk upload',
|
||||
'searchTable.operation.download': 'Download',
|
||||
'searchForm.id.placeholder': 'Please enter the collection ID',
|
||||
'searchForm.name.placeholder': 'Please enter the collection name',
|
||||
'searchForm.all.placeholder': 'all',
|
||||
},
|
||||
'zh-CN': {
|
||||
'menu.list': '列表页',
|
||||
'menu.list.searchTable': '查询表格',
|
||||
'searchTable.form.search': '查询',
|
||||
'searchTable.form.reset': '重置',
|
||||
'searchTable.columns.id': '集合编号',
|
||||
'searchTable.columns.name': '集合名称',
|
||||
'searchTable.columns.contentType': '内容体裁',
|
||||
'searchTable.columns.filterType': '筛选方式',
|
||||
'searchTable.columns.createdTime': '创建时间',
|
||||
'searchTable.columns.status': '状态',
|
||||
'searchTable.columns.contentNum': '内容量',
|
||||
'searchTable.columns.operations': '操作',
|
||||
'searchTable.columns.operations.view': '查看',
|
||||
'searchTable.columns.operations.update': '修改',
|
||||
'searchTable.columns.operations.online': '上线',
|
||||
'searchTable.columns.operations.offline': '下线',
|
||||
'searchTable.operations.add': '新建',
|
||||
'searchTable.operations.upload': '批量导入',
|
||||
'searchTable.operation.download': '下载',
|
||||
'searchForm.id.placeholder': '请输入集合编号',
|
||||
'searchForm.name.placeholder': '请输入集合名称',
|
||||
'searchForm.all.placeholder': '全部',
|
||||
},
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
99
src/pages/list/search-table/mock/index.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import Mock from 'mockjs';
|
||||
import qs from 'query-string';
|
||||
import dayjs from 'dayjs';
|
||||
import setupMock from '@/utils/setupMock';
|
||||
|
||||
const { list } = Mock.mock({
|
||||
'list|100': [
|
||||
{
|
||||
id: /[0-9]{8}[-][0-9]{4}/,
|
||||
name: () =>
|
||||
Mock.Random.pick([
|
||||
'每日推荐视频集',
|
||||
'抖音短视频候选集',
|
||||
'国际新闻集合',
|
||||
]),
|
||||
'contentType|0-2': 0,
|
||||
'filterType|0-1': 0,
|
||||
'count|0-2000': 0,
|
||||
'createdTime|1-60': 0,
|
||||
'status|0-1': 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const filterData = (
|
||||
rest: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
'contentType[]'?: string[];
|
||||
'filterType[]'?: string[];
|
||||
'createdTime[]'?: string[];
|
||||
'status[]'?: string;
|
||||
} = {}
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
'contentType[]': contentType,
|
||||
'filterType[]': filterType,
|
||||
'createdTime[]': createdTime,
|
||||
'status[]': status,
|
||||
} = rest;
|
||||
if (id) {
|
||||
return list.filter((item) => item.id === id);
|
||||
}
|
||||
let result = [...list];
|
||||
if (name) {
|
||||
result = result.filter((item) => {
|
||||
return (item.name as string).toLowerCase().includes(name.toLowerCase());
|
||||
});
|
||||
}
|
||||
if (contentType) {
|
||||
result = result.filter((item) =>
|
||||
contentType.includes(item.contentType.toString())
|
||||
);
|
||||
}
|
||||
if (filterType) {
|
||||
result = result.filter((item) =>
|
||||
filterType.includes(item.filterType.toString())
|
||||
);
|
||||
}
|
||||
if (createdTime && createdTime.length === 2) {
|
||||
const [begin, end] = createdTime;
|
||||
result = result.filter((item) => {
|
||||
const time = dayjs()
|
||||
.subtract(item.createdTime, 'days')
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
return (
|
||||
!dayjs(time).isBefore(dayjs(begin)) && !dayjs(time).isAfter(dayjs(end))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (status && status.length) {
|
||||
result = result.filter((item) => status.includes(item.status.toString()));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
setupMock({
|
||||
setup: () => {
|
||||
Mock.mock(new RegExp('/api/list'), (params) => {
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
...rest
|
||||
} = qs.parseUrl(params.url).query;
|
||||
const p = page as number;
|
||||
const ps = pageSize as number;
|
||||
|
||||
const result = filterData(rest);
|
||||
return {
|
||||
list: result.slice((p - 1) * ps, p * ps),
|
||||
total: result.length,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
50
src/pages/list/search-table/style/index.module.less
Normal file
@ -0,0 +1,50 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.operations {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.content-type {
|
||||
display: flex;
|
||||
|
||||
> svg {
|
||||
margin-right: 8px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form-wrapper {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--color-border-1);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.right-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 1px solid var(--color-border-2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
padding-right: 20px;
|
||||
|
||||
:global(.arco-form-label-item-left) {
|
||||
> label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
src/pages/login/banner.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Carousel } from '@arco-design/web-react';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/index.module.less';
|
||||
|
||||
export default function LoginBanner() {
|
||||
const t = useLocale(locale);
|
||||
const data = [
|
||||
{
|
||||
slogan: t['login.banner.slogan1'],
|
||||
subSlogan: t['login.banner.subSlogan1'],
|
||||
image:
|
||||
'http://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/6c85f43aed61e320ebec194e6a78d6d3.png~tplv-uwbnlip3yd-png.png',
|
||||
},
|
||||
{
|
||||
slogan: t['login.banner.slogan2'],
|
||||
subSlogan: t['login.banner.subSlogan2'],
|
||||
image:
|
||||
'http://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/6c85f43aed61e320ebec194e6a78d6d3.png~tplv-uwbnlip3yd-png.png',
|
||||
},
|
||||
{
|
||||
slogan: t['login.banner.slogan3'],
|
||||
subSlogan: t['login.banner.subSlogan3'],
|
||||
image:
|
||||
'http://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/6c85f43aed61e320ebec194e6a78d6d3.png~tplv-uwbnlip3yd-png.png',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<Carousel className={styles.carousel} animation="fade">
|
||||
{data.map((item, index) => (
|
||||
<div key={`${index}`}>
|
||||
<div className={styles['carousel-item']}>
|
||||
<div className={styles['carousel-title']}>{item.slogan}</div>
|
||||
<div className={styles['carousel-sub-title']}>{item.subSlogan}</div>
|
||||
<img
|
||||
alt="banner-image"
|
||||
className={styles['carousel-image']}
|
||||
src={item.image}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Carousel>
|
||||
);
|
||||
}
|
||||
130
src/pages/login/form.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import {
|
||||
Form,
|
||||
Input,
|
||||
Checkbox,
|
||||
Link,
|
||||
Button,
|
||||
Space,
|
||||
} from '@arco-design/web-react';
|
||||
import { FormInstance } from '@arco-design/web-react/es/Form';
|
||||
import { IconLock, IconUser } from '@arco-design/web-react/icon';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import useStorage from '@/utils/useStorage';
|
||||
import useLocale from '@/utils/useLocale';
|
||||
import locale from './locale';
|
||||
import styles from './style/index.module.less';
|
||||
|
||||
export default function LoginForm() {
|
||||
const formRef = useRef<FormInstance>();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loginParams, setLoginParams, removeLoginParams] =
|
||||
useStorage('loginParams');
|
||||
|
||||
const t = useLocale(locale);
|
||||
|
||||
const [rememberPassword, setRememberPassword] = useState(!!loginParams);
|
||||
|
||||
function afterLoginSuccess(params) {
|
||||
// 记住密码
|
||||
if (rememberPassword) {
|
||||
setLoginParams(JSON.stringify(params));
|
||||
} else {
|
||||
removeLoginParams();
|
||||
}
|
||||
// 记录登录状态
|
||||
localStorage.setItem('userStatus', 'login');
|
||||
// 跳转首页
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
function login(params) {
|
||||
setErrorMessage('');
|
||||
setLoading(true);
|
||||
axios
|
||||
.post('/api/user/login', params)
|
||||
.then((res) => {
|
||||
const { status, msg } = res.data;
|
||||
if (status === 'ok') {
|
||||
afterLoginSuccess(params);
|
||||
} else {
|
||||
setErrorMessage(msg || t['login.form.login.errMsg']);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmitClick() {
|
||||
formRef.current.validate().then((values) => {
|
||||
login(values);
|
||||
});
|
||||
}
|
||||
|
||||
// 读取 localStorage,设置初始值
|
||||
useEffect(() => {
|
||||
const rememberPassword = !!loginParams;
|
||||
setRememberPassword(rememberPassword);
|
||||
if (formRef.current && rememberPassword) {
|
||||
const parseParams = JSON.parse(loginParams);
|
||||
formRef.current.setFieldsValue(parseParams);
|
||||
}
|
||||
}, [loginParams]);
|
||||
|
||||
return (
|
||||
<div className={styles['login-form-wrapper']}>
|
||||
<div className={styles['login-form-title']}>{t['login.form.title']}</div>
|
||||
<div className={styles['login-form-sub-title']}>
|
||||
{t['login.form.title']}
|
||||
</div>
|
||||
<div className={styles['login-form-error-msg']}>{errorMessage}</div>
|
||||
<Form
|
||||
className={styles['login-form']}
|
||||
layout="vertical"
|
||||
ref={formRef}
|
||||
initialValues={{ userName: 'admin', password: 'admin' }}
|
||||
>
|
||||
<Form.Item
|
||||
field="userName"
|
||||
rules={[{ required: true, message: t['login.form.userName.errMsg'] }]}
|
||||
>
|
||||
<Input
|
||||
prefix={<IconUser />}
|
||||
placeholder={t['login.form.userName.placeholder']}
|
||||
onPressEnter={onSubmitClick}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
field="password"
|
||||
rules={[{ required: true, message: t['login.form.password.errMsg'] }]}
|
||||
>
|
||||
<Input.Password
|
||||
prefix={<IconLock />}
|
||||
placeholder={t['login.form.password.placeholder']}
|
||||
onPressEnter={onSubmitClick}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space size={16} direction="vertical">
|
||||
<div className={styles['login-form-password-actions']}>
|
||||
<Checkbox checked={rememberPassword} onChange={setRememberPassword}>
|
||||
{t['login.form.rememberPassword']}
|
||||
</Checkbox>
|
||||
<Link>{t['login.form.forgetPassword']}</Link>
|
||||
</div>
|
||||
<Button type="primary" long onClick={onSubmitClick} loading={loading}>
|
||||
{t['login.form.login']}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
long
|
||||
className={styles['login-form-register-btn']}
|
||||
>
|
||||
{t['login.form.register']}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
src/pages/login/index.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Footer from '@/components/Footer';
|
||||
import Logo from '@/assets/logo.svg';
|
||||
import LoginForm from './form';
|
||||
import LoginBanner from './banner';
|
||||
import styles from './style/index.module.less';
|
||||
|
||||
function Login() {
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('arco-theme', 'light');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logo}>
|
||||
<Logo />
|
||||
<div className={styles['logo-text']}>Arco Design Pro</div>
|
||||
</div>
|
||||
<div className={styles.banner}>
|
||||
<div className={styles['banner-inner']}>
|
||||
<LoginBanner />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles['content-inner']}>
|
||||
<LoginForm />
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Login.displayName = 'LoginPage';
|
||||
|
||||
export default Login;
|
||||
42
src/pages/login/locale/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
const i18n = {
|
||||
'en-US': {
|
||||
'login.form.title': 'Login to Arco Design Pro',
|
||||
'login.form.userName.errMsg': 'Username cannot be empty',
|
||||
'login.form.password.errMsg': 'Password cannot be empty',
|
||||
'login.form.login.errMsg': 'Login error, please refresh and try again',
|
||||
'login.form.userName.placeholder': 'Username: admin',
|
||||
'login.form.password.placeholder': 'Password: admin',
|
||||
'login.form.rememberPassword': 'Remember password',
|
||||
'login.form.forgetPassword': 'Forgot password',
|
||||
'login.form.login': 'login',
|
||||
'login.form.register': 'register account',
|
||||
'login.banner.slogan1': 'Out-of-the-box high-quality template',
|
||||
'login.banner.subSlogan1':
|
||||
'Rich page templates, covering most typical business scenarios',
|
||||
'login.banner.slogan2': 'Built-in solutions to common problems',
|
||||
'login.banner.subSlogan2':
|
||||
'Internationalization, routing configuration, state management everything',
|
||||
'login.banner.slogan3': 'Access visualization enhancement tool AUX',
|
||||
'login.banner.subSlogan3': 'Realize flexible block development',
|
||||
},
|
||||
'zh-CN': {
|
||||
'login.form.title': '登录 Arco Design Pro',
|
||||
'login.form.userName.errMsg': '用户名不能为空',
|
||||
'login.form.password.errMsg': '密码不能为空',
|
||||
'login.form.login.errMsg': '登录出错,请刷新重试',
|
||||
'login.form.userName.placeholder': '用户名:admin',
|
||||
'login.form.password.placeholder': '密码:admin',
|
||||
'login.form.rememberPassword': '记住密码',
|
||||
'login.form.forgetPassword': '忘记密码',
|
||||
'login.form.login': '登录',
|
||||
'login.form.register': '注册账号',
|
||||
'login.banner.slogan1': '开箱即用的高质量模板',
|
||||
'login.banner.subSlogan1': '丰富的的页面模板,覆盖大多数典型业务场景',
|
||||
'login.banner.slogan2': '内置了常见问题的解决方案',
|
||||
'login.banner.subSlogan2': '国际化,路由配置,状态管理应有尽有',
|
||||
'login.banner.slogan3': '接入可视化增强工具AUX',
|
||||
'login.banner.subSlogan3': '实现灵活的区块式开发',
|
||||
},
|
||||
};
|
||||
|
||||
export default i18n;
|
||||
120
src/pages/login/style/index.module.less
Normal file
@ -0,0 +1,120 @@
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
|
||||
.banner {
|
||||
width: 550px;
|
||||
background: linear-gradient(163.85deg, #1d2129 0%, #00308f 100%);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
position: fixed;
|
||||
top: 24px;
|
||||
left: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
z-index: 1;
|
||||
|
||||
&-text {
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
font-size: 20px;
|
||||
color: var(--color-fill-1);
|
||||
}
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-inner {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.carousel {
|
||||
height: 100%;
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: var(--color-fill-1);
|
||||
}
|
||||
|
||||
&-sub-title {
|
||||
margin-top: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&-image {
|
||||
margin-top: 30px;
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
&-wrapper {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
&-sub-title {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&-error-msg {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
color: rgb(var(--red-6));
|
||||
}
|
||||
|
||||
&-password-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-register-btn {
|
||||
color: var(--color-text-3) !important;
|
||||
}
|
||||
}
|
||||
109
src/routes.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import auth, { AuthParams } from '@/utils/authentication';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
export type IRoute = AuthParams & {
|
||||
name: string;
|
||||
key: string;
|
||||
// 当前页是否展示面包屑
|
||||
breadcrumb?: boolean;
|
||||
children?: IRoute[];
|
||||
// 当前路由是否渲染菜单项,为 true 的话不会在菜单中显示,但可通过路由地址访问。
|
||||
ignore?: boolean;
|
||||
};
|
||||
|
||||
export const routes: IRoute[] = [
|
||||
{
|
||||
name: 'menu.dashboard',
|
||||
key: 'dashboard',
|
||||
children: [
|
||||
{
|
||||
name: 'menu.dashboard.workplace',
|
||||
key: 'dashboard/workplace',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'menu.list',
|
||||
key: 'list',
|
||||
children: [
|
||||
{
|
||||
name: 'menu.list.searchTable',
|
||||
key: 'list/search-table',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const getName = (path: string, routes) => {
|
||||
return routes.find((item) => {
|
||||
const itemPath = `/${item.key}`;
|
||||
if (path === itemPath) {
|
||||
return item.name;
|
||||
} else if (item.children) {
|
||||
return getName(path, item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const generatePermission = (role: string) => {
|
||||
const actions = role === 'admin' ? ['*'] : ['read'];
|
||||
const result = {};
|
||||
routes.forEach((item) => {
|
||||
if (item.children) {
|
||||
item.children.forEach((child) => {
|
||||
result[child.name] = actions;
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const useRoute = (userPermission): [IRoute[], string] => {
|
||||
const filterRoute = (routes: IRoute[], arr = []): IRoute[] => {
|
||||
if (!routes.length) {
|
||||
return [];
|
||||
}
|
||||
for (const route of routes) {
|
||||
const { requiredPermissions, oneOfPerm } = route;
|
||||
let visible = true;
|
||||
if (requiredPermissions) {
|
||||
visible = auth({ requiredPermissions, oneOfPerm }, userPermission);
|
||||
}
|
||||
|
||||
if (!visible) {
|
||||
continue;
|
||||
}
|
||||
if (route.children && route.children.length) {
|
||||
const newRoute = { ...route, children: [] };
|
||||
filterRoute(route.children, newRoute.children);
|
||||
if (newRoute.children.length) {
|
||||
arr.push(newRoute);
|
||||
}
|
||||
} else {
|
||||
arr.push({ ...route });
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
};
|
||||
|
||||
const [permissionRoute, setPermissionRoute] = useState(routes);
|
||||
|
||||
useEffect(() => {
|
||||
const newRoutes = filterRoute(routes);
|
||||
setPermissionRoute(newRoutes);
|
||||
}, [JSON.stringify(userPermission)]);
|
||||
|
||||
const defaultRoute = useMemo(() => {
|
||||
const first = permissionRoute[0];
|
||||
if (first) {
|
||||
const firstRoute = first?.children?.[0]?.key || first.key;
|
||||
return firstRoute;
|
||||
}
|
||||
return '';
|
||||
}, [permissionRoute]);
|
||||
|
||||
return [permissionRoute, defaultRoute];
|
||||
};
|
||||
|
||||
export default useRoute;
|
||||
8
src/settings.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"colorWeek": false,
|
||||
"navbar": true,
|
||||
"menu": true,
|
||||
"footer": true,
|
||||
"themeColor": "#006266",
|
||||
"menuWidth": 220
|
||||
}
|
||||
43
src/store/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import defaultSettings from '../settings.json';
|
||||
export interface GlobalState {
|
||||
settings?: typeof defaultSettings;
|
||||
userInfo?: {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
job?: string;
|
||||
organization?: string;
|
||||
location?: string;
|
||||
email?: string;
|
||||
permissions: Record<string, string[]>;
|
||||
};
|
||||
userLoading?: boolean;
|
||||
}
|
||||
|
||||
const initialState: GlobalState = {
|
||||
settings: defaultSettings,
|
||||
userInfo: {
|
||||
permissions: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default function store(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case 'update-settings': {
|
||||
const { settings } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
settings,
|
||||
};
|
||||
}
|
||||
case 'update-userInfo': {
|
||||
const { userInfo = initialState.userInfo, userLoading } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
userLoading,
|
||||
userInfo,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
41
src/style/global.less
Normal file
@ -0,0 +1,41 @@
|
||||
@import 'nprogress/nprogress.css';
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
background-color: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
.bizcharts-tooltip {
|
||||
background: linear-gradient(
|
||||
304.17deg,
|
||||
rgb(253 254 255 / 60%) -6.04%,
|
||||
rgb(244 247 252 / 60%) 85.2%
|
||||
) !important;
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 8px !important;
|
||||
width: 180px !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
body[arco-theme='dark'] {
|
||||
.chart-wrapper {
|
||||
.bizcharts-tooltip {
|
||||
background: linear-gradient(
|
||||
304.17deg,
|
||||
rgba(90, 92, 95, 60%) -6.04%,
|
||||
rgba(87, 87, 87, 60%) 85.2%
|
||||
) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 6px;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
138
src/style/layout.module.less
Normal file
@ -0,0 +1,138 @@
|
||||
@nav-size-height: 60px;
|
||||
@layout-max-width: 1100px;
|
||||
|
||||
.layout {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.layout-navbar {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
min-width: @layout-max-width;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: @nav-size-height;
|
||||
z-index: 100;
|
||||
|
||||
&-hidden {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
box-sizing: border-box;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 4px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 7px;
|
||||
background-color: var(--color-text-4);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-text-3);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
> :global(.arco-layout-sider-children) {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
background-color: var(--color-fill-1);
|
||||
color: var(--color-text-3);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// 位置
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrapper {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
:global(.arco-menu-item-inner > a::after),
|
||||
:global(.arco-menu-item > a::after) {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
:global(.arco-menu-inline-header) {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 18px;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.icon-empty {
|
||||
width: 12px;
|
||||
height: 18px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
background-color: var(--color-fill-2);
|
||||
min-width: @layout-max-width;
|
||||
min-height: 100vh;
|
||||
transition: padding-left 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.layout-content-wrapper {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
.layout-breadcrumb {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: calc(100vh - @nav-size-height);
|
||||
}
|
||||
59
src/utils/authentication.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* { data-analysis: ['read', 'write'] }
|
||||
*/
|
||||
|
||||
export type UserPermission = Record<string, string[]>;
|
||||
|
||||
type Auth = {
|
||||
resource: string | RegExp;
|
||||
actions?: string[];
|
||||
};
|
||||
|
||||
export interface AuthParams {
|
||||
requiredPermissions?: Array<Auth>;
|
||||
oneOfPerm?: boolean;
|
||||
}
|
||||
|
||||
const judge = (actions: string[], perm: string[]) => {
|
||||
if (!perm || !perm.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (perm.join('') === '*') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return actions.every((action) => perm.includes(action));
|
||||
};
|
||||
|
||||
const auth = (params: Auth, userPermission: UserPermission) => {
|
||||
const { resource, actions = [] } = params;
|
||||
if (resource instanceof RegExp) {
|
||||
const permKeys = Object.keys(userPermission);
|
||||
const matchPermissions = permKeys.filter((item) => item.match(resource));
|
||||
if (!matchPermissions.length) {
|
||||
return false;
|
||||
}
|
||||
return matchPermissions.every((key) => {
|
||||
const perm = userPermission[key];
|
||||
return judge(actions, perm);
|
||||
});
|
||||
}
|
||||
|
||||
const perm = userPermission[resource];
|
||||
return judge(actions, perm);
|
||||
};
|
||||
|
||||
export default (params: AuthParams, userPermission: UserPermission) => {
|
||||
const { requiredPermissions, oneOfPerm } = params;
|
||||
if (Array.isArray(requiredPermissions) && requiredPermissions.length) {
|
||||
let count = 0;
|
||||
for (const rp of requiredPermissions) {
|
||||
if (auth(rp, userPermission)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return oneOfPerm ? count > 0 : count === requiredPermissions.length;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
9
src/utils/changeTheme.ts
Normal file
@ -0,0 +1,9 @@
|
||||
function changeTheme(theme) {
|
||||
if (theme === 'dark') {
|
||||
document.body.setAttribute('arco-theme', 'dark');
|
||||
} else {
|
||||
document.body.removeAttribute('arco-theme');
|
||||
}
|
||||
}
|
||||
|
||||
export default changeTheme;
|
||||
3
src/utils/checkLogin.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function checkLogin() {
|
||||
return localStorage.getItem('userStatus') === 'login';
|
||||
}
|
||||
41
src/utils/clipboard.ts
Normal file
@ -0,0 +1,41 @@
|
||||
// https://github.com/feross/clipboard-copy/blob/master/index.js
|
||||
|
||||
export default function clipboard(text) {
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.writeText(text).catch(function (err) {
|
||||
throw err !== undefined
|
||||
? err
|
||||
: new DOMException('The request is not allowed', 'NotAllowedError');
|
||||
});
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = text;
|
||||
|
||||
span.style.whiteSpace = 'pre';
|
||||
|
||||
document.body.appendChild(span);
|
||||
|
||||
const selection = window.getSelection();
|
||||
const range = window.document.createRange();
|
||||
selection.removeAllRanges();
|
||||
range.selectNode(span);
|
||||
selection.addRange(range);
|
||||
|
||||
let success = false;
|
||||
try {
|
||||
success = window.document.execCommand('copy');
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
console.log('error', err);
|
||||
}
|
||||
|
||||
selection.removeAllRanges();
|
||||
window.document.body.removeChild(span);
|
||||
|
||||
return success
|
||||
? Promise.resolve()
|
||||
: Promise.reject(
|
||||
new DOMException('The request is not allowed', 'NotAllowedError')
|
||||
);
|
||||
}
|
||||
19
src/utils/getUrlParams.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// 仅用于线上预览,实际使用中可以将此逻辑删除
|
||||
import qs from 'query-string';
|
||||
import { isSSR } from './is';
|
||||
|
||||
export type ParamsType = Record<string, any>;
|
||||
|
||||
export default function getUrlParams(): ParamsType {
|
||||
const params = qs.parseUrl(!isSSR ? window.location.href : '').query;
|
||||
const returnParams: ParamsType = {};
|
||||
Object.keys(params).forEach((p) => {
|
||||
if (params[p] === 'true') {
|
||||
returnParams[p] = true;
|
||||
}
|
||||
if (params[p] === 'false') {
|
||||
returnParams[p] = false;
|
||||
}
|
||||
});
|
||||
return returnParams;
|
||||
}
|
||||
17
src/utils/is.ts
Normal file
@ -0,0 +1,17 @@
|
||||
export function isArray(val): boolean {
|
||||
return Object.prototype.toString.call(val) === '[object Array]';
|
||||
}
|
||||
export function isObject(val): boolean {
|
||||
return Object.prototype.toString.call(val) === '[object Object]';
|
||||
}
|
||||
export function isString(val): boolean {
|
||||
return Object.prototype.toString.call(val) === '[object String]';
|
||||
}
|
||||
|
||||
export const isSSR = (function () {
|
||||
try {
|
||||
return !(typeof window !== 'undefined' && document !== undefined);
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
})();
|
||||
38
src/utils/lazyload.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import loadable from '@loadable/component';
|
||||
import { Spin } from '@arco-design/web-react';
|
||||
import styles from '../style/layout.module.less';
|
||||
|
||||
// https://github.com/gregberge/loadable-components/pull/226
|
||||
function load(fn, options) {
|
||||
const Component = loadable(fn, options);
|
||||
|
||||
Component.preload = fn.requireAsync || fn;
|
||||
|
||||
return Component;
|
||||
}
|
||||
|
||||
function LoadingComponent(props: {
|
||||
error: boolean;
|
||||
timedOut: boolean;
|
||||
pastDelay: boolean;
|
||||
}) {
|
||||
if (props.error) {
|
||||
console.error(props.error);
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={styles.spin}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default (loader) =>
|
||||
load(loader, {
|
||||
fallback: LoadingComponent({
|
||||
pastDelay: true,
|
||||
error: false,
|
||||
timedOut: false,
|
||||
}),
|
||||
});
|
||||
5
src/utils/setupMock.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export default (config: { mock?: boolean; setup: () => void }) => {
|
||||
const { mock = process.env.NODE_ENV === 'development', setup } = config;
|
||||
if (mock === false) return;
|
||||
setup();
|
||||
};
|
||||
26
src/utils/useChartTheme.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { G2 } from 'bizcharts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
const defaultDarkTheme = G2.getTheme('dark');
|
||||
|
||||
G2.registerTheme('darkTheme', {
|
||||
...defaultDarkTheme,
|
||||
background: 'transparent',
|
||||
});
|
||||
|
||||
function useBizTheme() {
|
||||
const theme = useSelector((state: any) => state.theme);
|
||||
const themeName = theme === 'dark' ? 'darkTheme' : 'light';
|
||||
const [themeObj, setThemeObj] = useState(G2.getTheme(themeName));
|
||||
|
||||
useEffect(() => {
|
||||
const themeName = theme === 'dark' ? 'darkTheme' : 'light';
|
||||
const newTheme = G2.getTheme(themeName);
|
||||
setThemeObj(newTheme);
|
||||
}, [theme]);
|
||||
|
||||
return themeObj;
|
||||
}
|
||||
|
||||
export default useBizTheme;
|
||||
11
src/utils/useLocale.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { useContext } from 'react';
|
||||
import { GlobalContext } from '../context';
|
||||
import defaultLocale from '../locale';
|
||||
|
||||
function useLocale(locale = null) {
|
||||
const { lang } = useContext(GlobalContext);
|
||||
|
||||
return (locale || defaultLocale)[lang] || {};
|
||||
}
|
||||
|
||||
export default useLocale;
|
||||
48
src/utils/useStorage.ts
Normal file
@ -0,0 +1,48 @@
|
||||
// https://stackoverflow.com/questions/68424114/next-js-how-to-fetch-localstorage-data-before-client-side-rendering
|
||||
// 解决 nextJS 无法获取初始localstorage问题
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isSSR } from '@/utils/is';
|
||||
|
||||
const getDefaultStorage = (key) => {
|
||||
if (!isSSR) {
|
||||
return localStorage.getItem(key);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
function useStorage(
|
||||
key: string,
|
||||
defaultValue?: string
|
||||
): [string, (string) => void, () => void] {
|
||||
const [storedValue, setStoredValue] = useState(
|
||||
getDefaultStorage(key) || defaultValue
|
||||
);
|
||||
|
||||
const setStorageValue = (value: string) => {
|
||||
if (!isSSR) {
|
||||
localStorage.setItem(key, value);
|
||||
if (value !== storedValue) {
|
||||
setStoredValue(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const removeStorage = () => {
|
||||
if (!isSSR) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const storageValue = localStorage.getItem(key);
|
||||
if (storageValue) {
|
||||
setStoredValue(storageValue);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return [storedValue, setStorageValue, removeStorage];
|
||||
}
|
||||
|
||||
export default useStorage;
|
||||
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||