feat: website pageview chart

This commit is contained in:
moonrailgun 2023-09-16 00:03:09 +08:00
parent ba0b398719
commit 81366eb106
6 changed files with 174 additions and 70 deletions

View File

@ -1,3 +1,4 @@
import dayjs from 'dayjs';
import { setUserInfo } from '../../store/user';
import { getJWT, setJWT } from '../auth';
import { request } from '../request';
@ -47,3 +48,11 @@ export async function register(username: string, password: string) {
setJWT(data.token);
setUserInfo(data.info as UserLoginInfo);
}
/**
* Mock
* return local, or fetch remote data
*/
export function getUserTimezone(): string {
return dayjs.tz.guess() ?? 'utc';
}

View File

@ -1,6 +1,7 @@
import { useQuery } from '@tanstack/react-query';
import { queryClient } from '../cache';
import { request } from '../request';
import { getUserTimezone } from './user';
export interface WebsiteInfo {
id: string;
@ -103,3 +104,41 @@ export async function addWorkspaceWebsite(
domain,
});
}
export async function getWorkspaceWebsitePageview(
workspaceId: string,
websiteId: string,
filter: Record<string, any>
) {
const { data } = await request.get(
`/api/workspace/${workspaceId}/website/${websiteId}/pageviews`,
{
params: {
...filter,
},
}
);
return data;
}
export function useWorkspaceWebsitePageview(
workspaceId: string,
websiteId: string,
startAt: number,
endAt: number
) {
const { data, isLoading } = useQuery(
['websitePageview', { workspaceId, websiteId }],
() => {
return getWorkspaceWebsitePageview(workspaceId, websiteId, {
startAt,
endAt,
unit: 'hour',
timezone: getUserTimezone(),
});
}
);
return { stats: data?.stats ?? [], isLoading };
}

View File

@ -1,11 +1,22 @@
import { Button, Tag } from 'antd';
import React from 'react';
import { Column } from '@ant-design/charts';
import React, { useMemo } from 'react';
import { Column, ColumnConfig } from '@ant-design/charts';
import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons';
import { DateFilter } from './DateFilter';
import { HealthBar } from './HealthBar';
import { useWorspaceWebsites, WebsiteInfo } from '../api/model/website';
import {
useWorkspaceWebsitePageview,
useWorspaceWebsites,
WebsiteInfo,
} from '../api/model/website';
import { Loading } from './Loading';
import dayjs from 'dayjs';
import {
DateUnit,
formatDate,
formatDateWithUnit,
getDateArray,
} from '../utils/date';
interface WebsiteOverviewProps {
workspaceId: string;
@ -32,6 +43,25 @@ WebsiteOverview.displayName = 'WebsiteOverview';
const WebsiteOverviewItem: React.FC<{
website: WebsiteInfo;
}> = React.memo((props) => {
const unit: DateUnit = 'hour';
const startDate = dayjs().subtract(1, 'day').add(1, unit).startOf(unit);
const endDate = dayjs().endOf(unit);
const { stats, isLoading } = useWorkspaceWebsitePageview(
props.website.workspaceId,
props.website.id,
startDate.unix() * 1000,
endDate.unix() * 1000
);
const chartData = useMemo(() => {
return getDateArray(stats, startDate, endDate, unit);
}, [stats, unit]);
if (isLoading) {
return <Loading />;
}
return (
<div className="mb-10 pb-10 border-b">
<div className="flex">
@ -75,7 +105,7 @@ const WebsiteOverviewItem: React.FC<{
</div>
<div>
<DemoChart />
<StatsChart data={chartData} unit={unit} />
</div>
</div>
);
@ -107,71 +137,36 @@ const MetricCard: React.FC<{
});
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',
export const StatsChart: React.FC<{
data: { x: string; y: number }[];
unit: DateUnit;
}> = React.memo((props) => {
const config: ColumnConfig = useMemo(
() => ({
data: props.data,
xField: 'x',
yField: 'y',
label: {
// 可手动配置 label 数据标签位置
position: 'middle' as const,
// 'top', 'bottom', 'middle',
// 配置样式
style: {
fill: '#FFFFFF',
opacity: 0.6,
},
},
tooltip: {
title: (t) => formatDate(t),
},
xAxis: {
label: {
autoHide: true,
autoRotate: false,
formatter: (text) => formatDateWithUnit(text, props.unit),
},
},
meta: {
type: {
alias: '类别',
},
sales: {
alias: '销售额',
},
},
};
}),
[props.data, props.unit]
);
return <Column {...config} />;
});
DemoChart.displayName = 'DemoChart';
StatsChart.displayName = 'StatsChart';

View File

@ -3,5 +3,6 @@
"compilerOptions": {
"module": "ESNext",
"jsx": "react-jsx"
}
},
"include": ["./**/*"]
}

64
src/client/utils/date.ts Normal file
View File

@ -0,0 +1,64 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);
export type DateUnit = 'minute' | 'hour' | 'day' | 'month' | 'year';
function createDateUnitFn(unit: DateUnit) {
return {
diff: (end: dayjs.ConfigType, start: dayjs.ConfigType) =>
dayjs(end).diff(start, unit),
add: (date: dayjs.ConfigType, n: number) => dayjs(date).add(n, unit),
normalize: (date: dayjs.ConfigType) => dayjs(date).startOf(unit),
};
}
export function getDateArray(
data: { x: string; y: number }[],
startDate: dayjs.ConfigType,
endDate: dayjs.ConfigType,
unit: DateUnit
) {
const arr = [];
const { diff, add, normalize } = createDateUnitFn(unit);
const n = diff(endDate, startDate) + 1;
function findData(date: dayjs.Dayjs) {
const d = data.find(({ x }) => {
return normalize(dayjs(x)).unix() === date.unix();
});
return d?.y || 0;
}
for (let i = 0; i < n; i++) {
const t = normalize(add(startDate, i));
const y = findData(t);
arr.push({ x: formatDate(t), y });
}
return arr;
}
export function formatDate(val: dayjs.ConfigType) {
return dayjs(val).format('YYYY-MM-DD HH:mm:ss');
}
export function formatDateWithUnit(val: dayjs.ConfigType, unit: DateUnit) {
if (unit === 'minute') {
return dayjs(val).format('HH:mm');
} else if (unit === 'hour') {
return dayjs(val).format('HA');
} else if (unit === 'day') {
return dayjs(val).format('MMM DD');
} else if (unit === 'month') {
return dayjs(val).format('MMM');
} else if (unit === 'year') {
return dayjs(val).format('YYYY');
}
return formatDate(val);
}

View File

@ -186,15 +186,11 @@ workspaceRouter.get(
city,
};
console.log('filters', filters);
const stats = await getWorkspaceWebsitePageviewStats(
websiteId,
filters as QueryFilters
);
console.log('stats', stats);
res.json({ stats });
}
);