feat: basic dashboard view
This commit is contained in:
parent
bf6484c07f
commit
ca90003467
@ -8,10 +8,12 @@
|
||||
"build": "vite build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/charts": "^1.4.2",
|
||||
"@ant-design/icons": "^5.2.5",
|
||||
"antd": "^5.8.5",
|
||||
"clsx": "^2.0.0",
|
||||
"express": "^4.18.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^6.15.0",
|
||||
@ -22,6 +24,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/react": "^18.2.21",
|
||||
"@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 components;
|
||||
@tailwind utilities;
|
||||
|
@ -1,6 +1,23 @@
|
||||
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(() => {
|
||||
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';
|
||||
|
@ -9,7 +9,7 @@ export const Layout: React.FC = React.memo(() => {
|
||||
<div>
|
||||
<div className="flex items-center bg-gray-100 px-4">
|
||||
<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="/monitor" label="Monitor" />
|
||||
<NavItem to="/website" label="Website" />
|
||||
@ -31,15 +31,11 @@ export const Layout: React.FC = React.memo(() => {
|
||||
],
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
shape="circle"
|
||||
size="large"
|
||||
icon={<UserOutlined />}
|
||||
></Button>
|
||||
<Button shape="circle" size="large" icon={<UserOutlined />} />
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="max-w-7xl m-auto px-4">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,5 +4,8 @@ module.exports = {
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user