@ -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
|
||||
}
|
||||
}
|
@ -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" />
|
After Width: | Height: | Size: 579 B |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 2.3 KiB |
@ -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,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;
|
After Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 13 KiB |
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,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,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>
|
||||
| <Link hoverable={false}>音乐商城</Link>
|
||||
|
|
||||
<Link hoverable={false}>联系我们</Link>
|
||||
</Paragraph>
|
||||
<Space>
|
||||
<Link
|
||||
hoverable={false}
|
||||
href="https://beian.miit.gov.cn/#/Integrated/index"
|
||||
target="_blank"
|
||||
>
|
||||
粤ICP备2024190175号-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"
|
||||
]
|
||||
}
|