first commit

main
fadeaway 4 months ago
commit 313480c3e6

@ -0,0 +1,2 @@
PORT=3000
EXTEND_ESLINT=true

@ -0,0 +1 @@
node_modules

@ -0,0 +1,34 @@
{
"root": true,
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"extends": [
"prettier",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"experimentalObjectRestSpread": true,
"jsx": true
},
"sourceType": "module"
},
"settings": {
"react": {
"version": "16.8"
}
},
"plugins": ["react", "babel", "@typescript-eslint/eslint-plugin"],
"rules": {
"react/display-name": 0,
"react/prop-types": 0
}
}

44
.gitignore vendored

@ -0,0 +1,44 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo
# eslint
.eslintcache
# stylelint
.stylelintcache

@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"jsxSingleQuote": false,
"useTabs": false,
"tabWidth": 2
}

@ -0,0 +1,4 @@
**/*.ts
**/*.tsx
**/*.jsx
**/*.js

@ -0,0 +1,17 @@
{
"extends": ["stylelint-config-standard", "stylelint-config-prettier"],
"customSyntax": "postcss-less",
"rules": {
"selector-class-pattern": null,
"no-descending-specificity": null,
"no-duplicate-selectors": null,
"color-function-notation": null,
"font-family-no-missing-generic-family-keyword": null,
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global"]
}
]
}
}

@ -0,0 +1,13 @@
# Arco Design Pro
## 快速开始
```
// 初始化项目
npm install
// 开发模式
npm run dev
// 构建
npm run build

@ -0,0 +1,36 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const {
override,
addWebpackModuleRule,
addWebpackPlugin,
addWebpackAlias,
} = require('customize-cra');
const ArcoWebpackPlugin = require('@arco-plugins/webpack-react');
const addLessLoader = require('customize-cra-less-loader');
const setting = require('./src/settings.json');
module.exports = {
webpack: override(
addLessLoader({
lessLoaderOptions: {
lessOptions: {},
},
}),
addWebpackModuleRule({
test: /\.svg$/,
loader: '@svgr/webpack',
}),
addWebpackPlugin(
new ArcoWebpackPlugin({
theme: '@arco-themes/react-arco-pro',
modifyVars: {
'arcoblue-6': setting.themeColor,
},
})
),
addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
})
),
};

@ -0,0 +1,79 @@
{
"name": "queyue-artists",
"version": "0.0.1",
"description": "雀乐音乐人",
"scripts": {
"start": "react-app-rewired start",
"dev": "react-app-rewired start",
"build": "react-app-rewired build",
"eject": "react-scripts eject",
"eslint": "eslint src/ --ext .ts,.tsx,.js,.jsx --fix --cache",
"stylelint": "stylelint 'src/**/*.less' 'src/**/*.css' --fix --cache",
"pre-commit": "pretty-quick --staged && npm run eslint && npm run stylelint",
"prepare": "husky install && husky add .husky/pre-commit 'npm run pre-commit'"
},
"dependencies": {
"@antv/data-set": "^0.11.8",
"@arco-design/color": "^0.4.0",
"@arco-design/web-react": "^2.32.2",
"@arco-themes/react-arco-pro": "^0.0.7",
"@loadable/component": "^5.13.2",
"@turf/turf": "^6.5.0",
"arco-design-pro": "^2.8.1",
"axios": "^0.24.0",
"bizcharts": "^4.1.11",
"classnames": "^2.3.1",
"copy-to-clipboard": "^3.3.1",
"lodash": "^4.17.21",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"query-string": "^6.13.8",
"react": "^17.0.2",
"react-color": "^2.18.1",
"react-dom": "^17.0.2",
"react-redux": "^7.2.6",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"redux": "^4.1.2"
},
"devDependencies": {
"@arco-plugins/webpack-react": "^1.1.1",
"@svgr/webpack": "^5.5.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"customize-cra": "^1.0.0",
"customize-cra-less-loader": "^2.0.0",
"eslint": "^8.9.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^7.0.2",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"postcss-less": "^5.0.0",
"prettier": "^2.4.1",
"pretty-quick": "^3.1.2",
"react-app-rewired": "^2.1.8",
"react-scripts": "^5.0.0",
"stylelint": "^14.1.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^24.0.0",
"typescript": "^4.5.2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Arco Design Pro - 开箱即用的中台前端/设计解决方案</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

@ -0,0 +1 @@
/// <reference types="react-scripts" />

@ -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

@ -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

@ -0,0 +1,4 @@
<svg width="60" height="61" viewBox="0 0 60 61" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.320312" width="60" height="60" rx="13.5" fill="white" fill-opacity="0.95" style="fill:white;fill-opacity:0.95;"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.5182 19.3659C29.5182 16.556 31.1277 14.1948 33.6561 13.5164C34.1318 13.3887 34.6295 13.3209 35.1435 13.3209C37.2624 13.3209 39.1094 14.4782 40.1544 16.1983C40.1856 16.2494 41.6196 18.4174 41.6196 21.6515C41.6196 22.5636 41.5073 23.4495 41.2979 24.2939C41.2897 24.3346 41.2857 24.3762 41.2857 24.4198C41.2857 24.7661 41.5589 25.0469 41.8959 25.0469C42.0831 25.0469 42.2501 24.9606 42.3614 24.8232C42.3765 24.8067 42.3897 24.789 42.4019 24.7702C43.517 23.2707 44.1788 21.3979 44.1788 19.3659C44.1788 14.4439 40.2961 10.4539 35.5067 10.4539C35.4653 10.4539 35.4238 10.4539 35.3824 10.4559C34.2986 10.4705 33.2623 10.6904 32.3093 11.0788C29.1018 12.3862 26.8348 15.6021 26.8348 19.3659C26.8348 23.4904 29.9971 27.7703 32.2212 30.0342C32.3366 30.1508 32.452 30.2662 32.5703 30.3817C33.1046 30.9037 33.655 31.3903 34.2176 31.8416C34.3928 31.982 34.5698 32.1192 34.7469 32.2535C34.9907 32.4406 35.1476 32.739 35.1476 33.075C35.1476 33.7151 34.5926 34.1551 33.99 34.0878C31.6991 33.7582 29.5528 32.9637 27.6444 31.799C27.3247 31.6035 27.011 31.3987 26.7053 31.1813H26.7044C24.7473 29.8035 23.0807 28.02 21.8229 25.9484C20.2262 23.3195 19.2842 20.2279 19.2315 16.9127C19.2305 16.8159 19.2295 16.7182 19.2295 16.6206C19.2295 15.4932 19.3327 14.3899 19.5291 13.3209C17.8543 15.4194 16.64 17.9214 16.0481 20.6636C15.7809 21.8991 15.6403 23.1844 15.6403 24.502C15.6403 27.656 16.4457 30.6167 17.8563 33.178C15.9874 31.8249 14.3926 30.0978 13.1764 28.1011C12.729 27.368 12.3324 26.5994 11.9935 25.7988C11.9337 26.3936 11.9033 26.9978 11.9033 27.6092C11.9033 29.9127 12.3334 32.1131 13.1145 34.1304C13.8462 36.0179 14.8855 37.7442 16.1685 39.2458C15.6271 39.1792 15.0786 39.1388 14.5232 39.1252C14.3846 39.1221 14.2459 39.1199 14.1073 39.1199C12.6916 39.1199 11.3155 39.2957 10 39.6284C13.1369 40.4823 15.9205 42.2272 18.082 44.5826C21.1724 47.9135 25.7035 50.5823 31.7668 50.5823C37.4267 50.5823 47.2 47.0711 47.2 39.2947C47.2 36.3735 45.7701 33.2064 41.6325 31.2954C34.7583 28.1205 29.5182 23.671 29.5182 19.3659Z" fill="black" style="fill:black;fill-opacity:1;"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

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

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

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

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

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

@ -0,0 +1,38 @@
import React from 'react';
import cs from 'classnames';
import { Layout, Avatar } from '@arco-design/web-react';
import { IconUser } from '@arco-design/web-react/icon';
import Logo from '@/assets/logo.svg';
import styles from './style/index.module.less';
interface HeaderProps {
mode?: 'light' | 'dark';
}
const LayoutHeader = Layout.Header;
function Header({ mode }: HeaderProps) {
return (
<LayoutHeader
className={cs(
styles.header,
mode === 'dark' ? styles.dark : styles.light
)}
>
<div className={styles.cont}>
<div className={styles.brand}>
<Logo className={styles.logo} />
<h5 className={styles.brandName}></h5>
</div>
<div className={styles.avatar}>
<div className={styles.loginText}></div>
<Avatar size={32}>
<IconUser />
</Avatar>
</div>
</div>
</LayoutHeader>
);
}
export default Header;

@ -0,0 +1,66 @@
@import '@/style/variable.less';
@import '@/style/common.less';
.header {
.center();
&.light {
background-color: #fff;
}
&.dark {
background-color: #000;
}
.cont {
width: @CONTENT_WIDTH;
height: 72px;
display: flex;
justify-content: space-between;
align-items: center;
}
.brand {
height: 100%;
.center();
font-size: 14px;
.logo {
zoom: 0.5;
}
.brandName {
color: #333;
font-size: 18px;
margin-left: 6px;
font-weight: 500;
}
}
&.light .brand .brandName {
color: #333;
}
&.dark .brand .brandName {
color: #fff;
}
.avatar {
height: 100%;
cursor: pointer;
padding-left: 12px;
padding-right: 12px;
margin-right: -12px;
.center();
.loginText {
font-size: 14px;
margin-right: 6px;
}
}
&.light .avatar .loginText {
color: #333;
}
&.dark .avatar .loginText {
color: #fff;
}
}

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

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

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

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

@ -0,0 +1,208 @@
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,
} 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 = useSelector((state: GlobalState) => state.userInfo);
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.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">
<Avatar size={32} style={{ cursor: 'pointer' }}>
<img alt="avatar" src={userInfo.avatar} />
</Avatar>
</Dropdown>
</li>
)}
</ul>
</div>
);
}
export default Navbar;

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

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

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

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

@ -0,0 +1,41 @@
import React, { useEffect, useState } 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 [hasPermission, setHasPermission] = useState(false);
const userInfo = useSelector((state: GlobalState) => state.userInfo);
useEffect(() => {
const hasPermission = authentication(
{ requiredPermissions, oneOfPerm },
userInfo.permissions
);
setHasPermission(hasPermission);
}, [requiredPermissions, oneOfPerm, userInfo.permissions]);
if (hasPermission) {
return <>{convertReactElement(props.children)}</>;
}
if (backup) {
return <>{convertReactElement(backup)}</>;
}
return null;
};
function convertReactElement(node: React.ReactNode): React.ReactElement {
if (!React.isValidElement(node)) {
return <>{node}</>;
}
return node;
}
export default PermissionWrapper;

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

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

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

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

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

@ -0,0 +1,8 @@
import { createContext } from 'react';
export const GlobalContext = createContext<{
lang?: string;
setLang?: (value: string) => void;
theme?: string;
setTheme?: (value: string) => void;
}>({});

@ -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;
}

@ -0,0 +1,102 @@
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 Home from './pages/home';
import SettleIn from './pages/settle-in';
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} /> */}
<Route path="/settle-in" component={SettleIn} />
<Route path="/" component={Home} />
</Switch>
</GlobalContext.Provider>
</Provider>
</ConfigProvider>
</BrowserRouter>
);
}
ReactDOM.render(<Index />, document.getElementById('root'));

@ -0,0 +1,271 @@
import React, { useState, useRef, useMemo, 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,
IconTag,
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 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;
function getIconFromKey(key) {
switch (key) {
case 'dashboard':
return <IconDashboard className={styles.icon} />;
case 'example':
return <IconTag className={styles.icon} />;
default:
return <div className={styles['icon-empty']} />;
}
}
function getFlattenRoutes(routes) {
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(() => import(`./pages/${route.key}`));
res.push(route);
} catch (e) {
console.error(e);
}
}
if (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 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 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 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={lazyload(() => import('./pages/exception/403'))}
/>
</Switch>
</Content>
</div>
{showFooter && <Footer />}
</Layout>
</Layout>
)}
</Layout>
);
}
export default PageLayout;

@ -0,0 +1,80 @@
const i18n = {
'en-US': {
'menu.dashboard': 'Dashboard',
'menu.dashboard.workplace': 'Workplace',
'menu.user.info': 'User Info',
'menu.user.setting': 'User Setting',
'menu.user.switchRoles': 'Switch Roles',
'menu.user.role.admin': 'Admin',
'menu.user.role.user': 'General User',
'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.dashboard.workplace': '工作台',
'menu.user.info': '用户信息',
'menu.user.setting': '用户设置',
'menu.user.switchRoles': '切换角色',
'menu.user.role.admin': '管理员',
'menu.user.role.user': '普通用户',
'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;

@ -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',
});
}

@ -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;
});
},
});

@ -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: '账号或者密码错误',
};
});
},
});
}

@ -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;

@ -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

@ -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

@ -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

@ -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

@ -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;

@ -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;

@ -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;

@ -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 (
<Space size={16} align="start">
<Space size={16} direction="vertical">
<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>
</Space>
);
}
export default Workplace;

@ -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;

@ -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: '新增内容已经通过审核,详情请点击查看。',
},
];
});
},
});

@ -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;

@ -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;

@ -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;

@ -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;
}

@ -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));
}
}

@ -0,0 +1,19 @@
.banner {
background-color: var(--color-bg-2);
padding: 20px;
}
.content {
padding: 20px;
display: flex;
}
.right {
width: 280px;
}
.panel {
background-color: var(--color-bg-2);
border-radius: 4px;
overflow: auto;
}

@ -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;
}
}

@ -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;
}

@ -0,0 +1,15 @@
import React from 'react';
import { Typography, Card } from '@arco-design/web-react';
function Example() {
return (
<Card style={{ height: '80vh' }}>
<Typography.Title heading={6}>
This is a very basic and simple page
</Typography.Title>
<Typography.Text>You can add content here :)</Typography.Text>
</Card>
);
}
export default Example;

@ -0,0 +1,28 @@
import React from 'react';
import { Result, Button } from '@arco-design/web-react';
import locale from './locale';
import useLocale from '@/utils/useLocale';
import styles from './style/index.module.less';
function Exception403() {
const t = useLocale(locale);
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<Result
className={styles.result}
status="403"
subTitle={t['exception.result.403.description']}
extra={
<Button key="back" type="primary">
{t['exception.result.403.back']}
</Button>
}
/>
</div>
</div>
);
}
export default Exception403;

@ -0,0 +1,17 @@
const i18n = {
'en-US': {
'menu.exception': 'Exception page',
'menu.exception.403': '403',
'exception.result.403.description':
'Access to this resource on the server is denied.',
'exception.result.403.back': 'Back',
},
'zh-CN': {
'menu.exception': '异常页',
'menu.exception.403': '403',
'exception.result.403.description': '对不起,您没有访问该资源的权限',
'exception.result.403.back': '返回',
},
};
export default i18n;

@ -0,0 +1,11 @@
.wrapper {
position: relative;
background-color: var(--color-bg-1);
height: calc(100vh - 168px);
}
.result {
position: absolute;
top: 50%;
transform: translateY(-50%);
}

@ -0,0 +1,39 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
import { Layout, Typography, Button } from '@arco-design/web-react';
import Header from '@/components/Header';
import styles from './style/index.module.less';
const Content = Layout.Content;
const { Title, Paragraph } = Typography;
function Home() {
const history = useHistory();
const handleClick = () => {
history.push('/settle-in');
};
return (
<Layout className={styles.wrap}>
<Header mode="dark" />
<Content className={styles.cont}>
<section className={styles.contBox}>
<Paragraph className={styles.sloganTitle}></Paragraph>
<Title className={styles.slogan}> </Title>
<Button
shape="round"
type="primary"
size="large"
className={styles.btn}
onClick={handleClick}
>
</Button>
</section>
</Content>
</Layout>
);
}
export default Home;

@ -0,0 +1,44 @@
@import '@/style/variable.less';
@import '@/style/common.less';
.wrap {
min-height: 100vh;
}
.cont {
.verticalCenter();
background-color: #000;
.contBox {
.verticalCenter();
margin-top: -78px;
:global(.arco-typography) {
color: #fff;
}
.sloganTitle {
height: 28px;
line-height: 28px;
font-size: 20px;
margin-bottom: 8px;
}
.slogan {
height: 67px;
line-height: 67px;
font-weight: 600;
font-size: 48px;
margin-bottom: 38px;
}
.btn {
.center();
width: 222px;
height: 59px;
border-radius: 38px;
font-weight: 500;
font-size: 18px;
}
}
}

@ -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>
);
}

@ -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>
);
}

@ -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;

@ -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;

@ -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;
}
}

@ -0,0 +1,63 @@
import React from 'react';
import {
Layout,
Typography,
Button,
Link,
Space,
Radio,
} from '@arco-design/web-react';
import Header from '@/components/Header';
import styles from './style/index.module.less';
const Content = Layout.Content;
const Footer = Layout.Footer;
const { Title, Paragraph, Text } = Typography;
function SettleIn() {
return (
<Layout className={styles.wrap}>
<Header />
<Content className={styles.cont}>
<section className={styles.contBox}>
<Title className={styles.welcome}></Title>
<article className={styles.policy}></article>
<div className={styles.nextStep}>
<Radio></Radio>
<Button type="primary"></Button>
</div>
</section>
</Content>
<Footer className={styles.foot}>
<Paragraph>
<Link hoverable={false} href="https://www.indie.cn/" target="_blank">
</Link>
&nbsp;&nbsp;|&nbsp;&nbsp;<Link hoverable={false}></Link>
&nbsp;&nbsp;|&nbsp;&nbsp;
<Link hoverable={false}></Link>
</Paragraph>
<Space>
<Link
hoverable={false}
href="https://beian.miit.gov.cn/#/Integrated/index"
target="_blank"
>
ICP2024190175-1
</Link>
<Link
hoverable={false}
href="https://beian.mps.gov.cn/#/query/webSearch"
target="_blank"
>
44030002002777
</Link>
<Text></Text>
<Text>Shenzhen QueYue Culture Technology Co., Ltd.</Text>
</Space>
</Footer>
</Layout>
);
}
export default SettleIn;

@ -0,0 +1,51 @@
@import '@/style/variable.less';
@import '@/style/common.less';
.wrap {
min-height: 100vh;
}
.cont {
.verticalCenter();
background-color: #efefef;
.contBox {
.verticalCenter();
width: 900px;
flex: 1;
padding-top: 20px;
padding-bottom: 20px;
.welcome {
width: 100%;
}
.policy {
width: 100%;
flex: 1;
background-color: #fff;
border-radius: 16px;
}
.nextStep {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
}
}
}
.foot {
.verticalCenter();
padding-top: 32px;
padding-bottom: 40px;
background-color: #000;
color: #666;
:global(.arco-typography),
:global(.arco-link) {
color: #666;
}
}

@ -0,0 +1 @@
/// <reference types="react-scripts" />

@ -0,0 +1,103 @@
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: 'Example',
key: 'example',
},
];
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;

@ -0,0 +1,8 @@
{
"colorWeek": false,
"navbar": true,
"menu": true,
"footer": true,
"themeColor": "#165DFF",
"menuWidth": 220
}

@ -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;
}
}

@ -0,0 +1,10 @@
.center {
display: flex;
align-items: center;
justify-content: center;
}
.verticalCenter {
.center();
flex-direction: column;
}

@ -0,0 +1,41 @@
@import '@arco-themes/react-arco-pro/index.less';
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, 0.6) -6.04%,
rgba(87, 87, 87, 0.6) 85.2%
) !important;
backdrop-filter: blur(10px);
border-radius: 6px;
box-shadow: none !important;
}
}
}

@ -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 0px 20px;
}
.layout-breadcrumb {
margin-bottom: 16px;
}
.spin {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: calc(100vh - @nav-size-height);
}

@ -0,0 +1 @@
@CONTENT_WIDTH: 1200px;

@ -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;
};

@ -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;

@ -0,0 +1,3 @@
export default function checkLogin() {
return localStorage.getItem('userStatus') === 'login';
}

@ -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')
);
}

@ -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;
}

@ -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;
}
})();

@ -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,
}),
});

@ -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();
};

@ -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;

@ -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;

@ -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;

@ -0,0 +1,9 @@
{
"compilerOptions": {
// "rootDirs": ["./src", "../arco-design-pro-next/src"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}

@ -0,0 +1,27 @@
{
"extends": "./tsconfig-base.json",
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save