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 { setUserInfo } from '../../store/user';
|
||||||
import { getJWT, setJWT } from '../auth';
|
import { getJWT, setJWT } from '../auth';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
@ -47,3 +48,11 @@ export async function register(username: string, password: string) {
|
|||||||
setJWT(data.token);
|
setJWT(data.token);
|
||||||
setUserInfo(data.info as UserLoginInfo);
|
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 { useQuery } from '@tanstack/react-query';
|
||||||
import { queryClient } from '../cache';
|
import { queryClient } from '../cache';
|
||||||
import { request } from '../request';
|
import { request } from '../request';
|
||||||
|
import { getUserTimezone } from './user';
|
||||||
|
|
||||||
export interface WebsiteInfo {
|
export interface WebsiteInfo {
|
||||||
id: string;
|
id: string;
|
||||||
@ -103,3 +104,41 @@ export async function addWorkspaceWebsite(
|
|||||||
domain,
|
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 { Button, Tag } from 'antd';
|
||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Column } from '@ant-design/charts';
|
import { Column, ColumnConfig } from '@ant-design/charts';
|
||||||
import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons';
|
import { ArrowRightOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
import { DateFilter } from './DateFilter';
|
import { DateFilter } from './DateFilter';
|
||||||
import { HealthBar } from './HealthBar';
|
import { HealthBar } from './HealthBar';
|
||||||
import { useWorspaceWebsites, WebsiteInfo } from '../api/model/website';
|
import {
|
||||||
|
useWorkspaceWebsitePageview,
|
||||||
|
useWorspaceWebsites,
|
||||||
|
WebsiteInfo,
|
||||||
|
} from '../api/model/website';
|
||||||
import { Loading } from './Loading';
|
import { Loading } from './Loading';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import {
|
||||||
|
DateUnit,
|
||||||
|
formatDate,
|
||||||
|
formatDateWithUnit,
|
||||||
|
getDateArray,
|
||||||
|
} from '../utils/date';
|
||||||
|
|
||||||
interface WebsiteOverviewProps {
|
interface WebsiteOverviewProps {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@ -32,6 +43,25 @@ WebsiteOverview.displayName = 'WebsiteOverview';
|
|||||||
const WebsiteOverviewItem: React.FC<{
|
const WebsiteOverviewItem: React.FC<{
|
||||||
website: WebsiteInfo;
|
website: WebsiteInfo;
|
||||||
}> = React.memo((props) => {
|
}> = 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 (
|
return (
|
||||||
<div className="mb-10 pb-10 border-b">
|
<div className="mb-10 pb-10 border-b">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@ -75,7 +105,7 @@ const WebsiteOverviewItem: React.FC<{
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<DemoChart />
|
<StatsChart data={chartData} unit={unit} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -107,71 +137,36 @@ const MetricCard: React.FC<{
|
|||||||
});
|
});
|
||||||
MetricCard.displayName = 'MetricCard';
|
MetricCard.displayName = 'MetricCard';
|
||||||
|
|
||||||
export const DemoChart: React.FC = React.memo(() => {
|
export const StatsChart: React.FC<{
|
||||||
const data = [
|
data: { x: string; y: number }[];
|
||||||
{
|
unit: DateUnit;
|
||||||
type: '家具家电',
|
}> = React.memo((props) => {
|
||||||
sales: 38,
|
const config: ColumnConfig = useMemo(
|
||||||
},
|
() => ({
|
||||||
{
|
data: props.data,
|
||||||
type: '粮油副食',
|
xField: 'x',
|
||||||
sales: 52,
|
yField: 'y',
|
||||||
},
|
|
||||||
{
|
|
||||||
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: {
|
label: {
|
||||||
autoHide: true,
|
position: 'middle' as const,
|
||||||
autoRotate: false,
|
style: {
|
||||||
|
fill: '#FFFFFF',
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
tooltip: {
|
||||||
meta: {
|
title: (t) => formatDate(t),
|
||||||
type: {
|
|
||||||
alias: '类别',
|
|
||||||
},
|
},
|
||||||
sales: {
|
xAxis: {
|
||||||
alias: '销售额',
|
label: {
|
||||||
|
autoHide: true,
|
||||||
|
autoRotate: false,
|
||||||
|
formatter: (text) => formatDateWithUnit(text, props.unit),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
};
|
[props.data, props.unit]
|
||||||
|
);
|
||||||
|
|
||||||
return <Column {...config} />;
|
return <Column {...config} />;
|
||||||
});
|
});
|
||||||
DemoChart.displayName = 'DemoChart';
|
StatsChart.displayName = 'StatsChart';
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"jsx": "react-jsx"
|
"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,
|
city,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('filters', filters);
|
|
||||||
|
|
||||||
const stats = await getWorkspaceWebsitePageviewStats(
|
const stats = await getWorkspaceWebsitePageviewStats(
|
||||||
websiteId,
|
websiteId,
|
||||||
filters as QueryFilters
|
filters as QueryFilters
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('stats', stats);
|
|
||||||
|
|
||||||
res.json({ stats });
|
res.json({ stats });
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user