feat: basic dashboard view
This commit is contained in:
parent
bf6484c07f
commit
ca90003467
@ -8,10 +8,12 @@
|
|||||||
"build": "vite build"
|
"build": "vite build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ant-design/charts": "^1.4.2",
|
||||||
"@ant-design/icons": "^5.2.5",
|
"@ant-design/icons": "^5.2.5",
|
||||||
"antd": "^5.8.5",
|
"antd": "^5.8.5",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router": "^6.15.0",
|
"react-router": "^6.15.0",
|
||||||
@ -22,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
|
"@types/lodash-es": "^4.17.9",
|
||||||
"@types/node": "^18.17.12",
|
"@types/node": "^18.17.12",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.2.21",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
2546
pnpm-lock.yaml
2546
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
60
src/client/components/DateFilter.tsx
Normal file
60
src/client/components/DateFilter.tsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import { compact } from 'lodash-es';
|
||||||
|
|
||||||
|
export const DateFilter: React.FC<{
|
||||||
|
showAllTime?: boolean;
|
||||||
|
}> = React.memo((props) => {
|
||||||
|
const options = compact([
|
||||||
|
{ label: 'Today', value: '1day' },
|
||||||
|
{
|
||||||
|
label: 'Last 24 hours',
|
||||||
|
value: '24hour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Yesterday',
|
||||||
|
value: '-1day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'This Week',
|
||||||
|
value: '1week',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Last 7 days',
|
||||||
|
value: '7day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'This Month',
|
||||||
|
value: '1month',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Last 30 days',
|
||||||
|
value: '30day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Last 90 days',
|
||||||
|
value: '90day',
|
||||||
|
},
|
||||||
|
{ label: 'This year', value: '1year' },
|
||||||
|
props.showAllTime === true && {
|
||||||
|
label: 'All time',
|
||||||
|
value: 'all',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Custom range',
|
||||||
|
value: 'custom',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
className="min-w-[10rem]"
|
||||||
|
size="large"
|
||||||
|
options={options}
|
||||||
|
defaultValue="24hour"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
DateFilter.displayName = 'DateFilter';
|
29
src/client/components/HealthBar.tsx
Normal file
29
src/client/components/HealthBar.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
type HealthStatus = 'health' | 'error' | 'warning' | 'none';
|
||||||
|
|
||||||
|
interface HealthBarProps {
|
||||||
|
beats: { title?: string; status: HealthStatus }[];
|
||||||
|
}
|
||||||
|
export const HealthBar: React.FC<HealthBarProps> = React.memo((props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex">
|
||||||
|
{props.beats.map((beat) => (
|
||||||
|
<div
|
||||||
|
title={beat.title}
|
||||||
|
className={clsx(
|
||||||
|
'rounded-full w-1 h-4 m-0.5 hover:scale-150 transition-transform',
|
||||||
|
{
|
||||||
|
'bg-green-500': beat.status === 'health',
|
||||||
|
'bg-red-600': beat.status === 'error',
|
||||||
|
'bg-yellow-400': beat.status === 'warning',
|
||||||
|
'bg-gray-400': beat.status === 'none',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
HealthBar.displayName = 'HealthBar';
|
147
src/client/components/WebsiteOverview.tsx
Normal file
147
src/client/components/WebsiteOverview.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import { Button, Tag } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import { Column } from '@ant-design/charts';
|
||||||
|
import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
|
import { DateFilter } from './DateFilter';
|
||||||
|
import { HealthBar } from './HealthBar';
|
||||||
|
|
||||||
|
export const WebsiteOverview: React.FC = React.memo(() => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex flex-1 text-2xl font-bold items-center">
|
||||||
|
<span className="mr-2">Tianji</span>
|
||||||
|
|
||||||
|
<HealthBar
|
||||||
|
beats={Array.from({ length: 13 }).map(() => ({ status: 'health' }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="primary" size="large">
|
||||||
|
View Details <ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex mb-10 flex-wrap">
|
||||||
|
<div className="flex gap-5 flex-wrap w-full lg:w-2/3">
|
||||||
|
<MetricCard label="Views" value={20} diff={20} />
|
||||||
|
<MetricCard label="Visitors" value={20} diff={20} />
|
||||||
|
<MetricCard label="Bounce rate" value={20} diff={-20} unit="%" />
|
||||||
|
<MetricCard
|
||||||
|
label="Average visit time"
|
||||||
|
value={20}
|
||||||
|
diff={-20}
|
||||||
|
unit="s"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 justify-end w-full lg:w-1/3">
|
||||||
|
<Button size="large" icon={<SyncOutlined />} />
|
||||||
|
|
||||||
|
<DateFilter />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<DemoChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
WebsiteOverview.displayName = 'WebsiteOverview';
|
||||||
|
|
||||||
|
const MetricCard: React.FC<{
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
diff: number;
|
||||||
|
unit?: string;
|
||||||
|
}> = React.memo((props) => {
|
||||||
|
const unit = props.unit ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center min-w-[140px] min-h-[90px]">
|
||||||
|
<div className="flex items-center whitespace-nowrap font-bold text-4xl">
|
||||||
|
{String(props.value)}
|
||||||
|
{unit}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center whitespace-nowrap font-bold">
|
||||||
|
<span className="mr-2">{props.label}</span>
|
||||||
|
<Tag color={props.diff >= 0 ? 'green' : 'red'}>
|
||||||
|
{props.diff >= 0 ? `+${props.diff}${unit}` : `${props.diff}${unit}`}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MetricCard.displayName = 'MetricCard';
|
||||||
|
|
||||||
|
export const DemoChart: React.FC = React.memo(() => {
|
||||||
|
const data = [
|
||||||
|
{
|
||||||
|
type: '家具家电',
|
||||||
|
sales: 38,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '粮油副食',
|
||||||
|
sales: 52,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '生鲜水果',
|
||||||
|
sales: 61,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '美容洗护',
|
||||||
|
sales: 145,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '母婴用品',
|
||||||
|
sales: 48,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '进口食品',
|
||||||
|
sales: 38,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '食品饮料',
|
||||||
|
sales: 38,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: '家庭清洁',
|
||||||
|
sales: 38,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const config = {
|
||||||
|
data,
|
||||||
|
xField: 'type',
|
||||||
|
yField: 'sales',
|
||||||
|
label: {
|
||||||
|
// 可手动配置 label 数据标签位置
|
||||||
|
position: 'middle' as const,
|
||||||
|
// 'top', 'bottom', 'middle',
|
||||||
|
// 配置样式
|
||||||
|
style: {
|
||||||
|
fill: '#FFFFFF',
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
label: {
|
||||||
|
autoHide: true,
|
||||||
|
autoRotate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
type: {
|
||||||
|
alias: '类别',
|
||||||
|
},
|
||||||
|
sales: {
|
||||||
|
alias: '销售额',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Column {...config} />;
|
||||||
|
});
|
||||||
|
DemoChart.displayName = 'DemoChart';
|
@ -1,3 +1,24 @@
|
|||||||
|
@import 'antd/dist/reset.css';
|
||||||
|
|
||||||
|
/* fix Tailwind CSS border styles,form Tailwind CSS's preflight */
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
border-width: 0; /* 2 */
|
||||||
|
border-style: solid; /* 2 */
|
||||||
|
border-color: theme('borderColor.DEFAULT', currentColor); /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@ -1,6 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { WebsiteOverview } from '../components/WebsiteOverview';
|
||||||
|
|
||||||
export const Dashboard: React.FC = React.memo(() => {
|
export const Dashboard: React.FC = React.memo(() => {
|
||||||
return <div>Dashboard</div>;
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="h-24 flex items-center">
|
||||||
|
<div className="text-2xl flex-1">Dashboard</div>
|
||||||
|
<div>
|
||||||
|
<Button icon={<EditOutlined />} size="large">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<WebsiteOverview />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
Dashboard.displayName = 'Dashboard';
|
Dashboard.displayName = 'Dashboard';
|
||||||
|
@ -9,7 +9,7 @@ export const Layout: React.FC = React.memo(() => {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center bg-gray-100 px-4">
|
<div className="flex items-center bg-gray-100 px-4">
|
||||||
<div className="px-2 mr-10 font-bold">Tianji</div>
|
<div className="px-2 mr-10 font-bold">Tianji</div>
|
||||||
<div className="flex space-x-4">
|
<div className="flex gap-8">
|
||||||
<NavItem to="/dashboard" label="Dashboard" />
|
<NavItem to="/dashboard" label="Dashboard" />
|
||||||
<NavItem to="/monitor" label="Monitor" />
|
<NavItem to="/monitor" label="Monitor" />
|
||||||
<NavItem to="/website" label="Website" />
|
<NavItem to="/website" label="Website" />
|
||||||
@ -31,15 +31,11 @@ export const Layout: React.FC = React.memo(() => {
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button shape="circle" size="large" icon={<UserOutlined />} />
|
||||||
shape="circle"
|
|
||||||
size="large"
|
|
||||||
icon={<UserOutlined />}
|
|
||||||
></Button>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="max-w-7xl m-auto px-4">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,5 +4,8 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user