From 81366eb10603fddedc1af954978b808ca4deb7d6 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Sat, 16 Sep 2023 00:03:09 +0800 Subject: [PATCH] feat: website pageview chart --- src/client/api/model/user.ts | 9 ++ src/client/api/model/website.ts | 39 +++++++ src/client/components/WebsiteOverview.tsx | 125 +++++++++++----------- src/client/tsconfig.json | 3 +- src/client/utils/date.ts | 64 +++++++++++ src/server/router/workspace.ts | 4 - 6 files changed, 174 insertions(+), 70 deletions(-) create mode 100644 src/client/utils/date.ts diff --git a/src/client/api/model/user.ts b/src/client/api/model/user.ts index 36be374..fa81933 100644 --- a/src/client/api/model/user.ts +++ b/src/client/api/model/user.ts @@ -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'; +} diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts index 94a797c..072fdd0 100644 --- a/src/client/api/model/website.ts +++ b/src/client/api/model/website.ts @@ -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 +) { + 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 }; +} diff --git a/src/client/components/WebsiteOverview.tsx b/src/client/components/WebsiteOverview.tsx index 6b22e33..02e5e54 100644 --- a/src/client/components/WebsiteOverview.tsx +++ b/src/client/components/WebsiteOverview.tsx @@ -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 ; + } + return (
@@ -75,7 +105,7 @@ const WebsiteOverviewItem: React.FC<{
- +
); @@ -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 ; }); -DemoChart.displayName = 'DemoChart'; +StatsChart.displayName = 'StatsChart'; diff --git a/src/client/tsconfig.json b/src/client/tsconfig.json index 46f4260..93e1328 100644 --- a/src/client/tsconfig.json +++ b/src/client/tsconfig.json @@ -3,5 +3,6 @@ "compilerOptions": { "module": "ESNext", "jsx": "react-jsx" - } + }, + "include": ["./**/*"] } diff --git a/src/client/utils/date.ts b/src/client/utils/date.ts new file mode 100644 index 0000000..127701d --- /dev/null +++ b/src/client/utils/date.ts @@ -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); +} diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts index b3309e9..53c6ba6 100644 --- a/src/server/router/workspace.ts +++ b/src/server/router/workspace.ts @@ -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 }); } );