feat: website pageview chart
This commit is contained in:
parent
ba0b398719
commit
81366eb106
@ -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';
|
||||
}
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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',
|
||||
label: {
|
||||
// 可手动配置 label 数据标签位置
|
||||
position: 'middle' as const,
|
||||
// 'top', 'bottom', 'middle',
|
||||
// 配置样式
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
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: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
position: 'middle' as const,
|
||||
style: {
|
||||
fill: '#FFFFFF',
|
||||
opacity: 0.6,
|
||||
},
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
type: {
|
||||
alias: '类别',
|
||||
tooltip: {
|
||||
title: (t) => formatDate(t),
|
||||
},
|
||||
sales: {
|
||||
alias: '销售额',
|
||||
xAxis: {
|
||||
label: {
|
||||
autoHide: true,
|
||||
autoRotate: false,
|
||||
formatter: (text) => formatDateWithUnit(text, props.unit),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
[props.data, props.unit]
|
||||
);
|
||||
|
||||
return <Column {...config} />;
|
||||
});
|
||||
DemoChart.displayName = 'DemoChart';
|
||||
StatsChart.displayName = 'StatsChart';
|
||||
|
@ -3,5 +3,6 @@
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
},
|
||||
"include": ["./**/*"]
|
||||
}
|
||||
|
64
src/client/utils/date.ts
Normal file
64
src/client/utils/date.ts
Normal 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);
|
||||
}
|
@ -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 });
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user