Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
"tooltip": "Query through UserAgent",
"confirm": "Confirm",
"error": "Please confirm that you have entered the correct content",
"dashboardTitle": "MirrorChyan x {rid} {date} Sales Dashboard",
"dashboard": "MirrorChyan x {rid} Sales Dashboard",
"export": "Export CSV file",
"application": "Applications",
"userAgent": "Checkin user agents",
Expand All @@ -229,6 +229,7 @@
"count": "Deals",
"monthlyCount": "Monthly Sales",
"monthlyAmount": "Monthly Revenue",
"refreshSuccess": "Refresh was successful",
"lineChart": {
"toggleCount": "Click to switch to sales volume",
"toggleAmount": "Click to switch to sales revenue",
Expand Down
3 changes: 2 additions & 1 deletion messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@
"tooltip": "通过请求源查询",
"confirm": "确认",
"error": "~(>_<) 怎么不对呢!是不是手指抖了一下呀?",
"dashboardTitle": "Mirror酱 x {rid} {date} 销售看板",
"dashboardTitle": "Mirror酱 x {rid} 销售看板",
"export": "导出 CSV 文件",
"application": "应用",
"userAgent": "签到源",
Expand All @@ -223,6 +223,7 @@
"count": "销量",
"monthlyCount": "月销售总量",
"monthlyAmount": "月销售总额",
"refreshSuccess": "(☆▽☆) 数据更新成功",
"lineChart": {
"toggleCount": "点击切换为销量",
"toggleAmount": "点击切换为销售额",
Expand Down
10 changes: 8 additions & 2 deletions src/app/[locale]/dashboard/LoginForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import YearMonthPicker from "@/components/YearMonthPicker";
import { RevenueResponse, RevenueType } from "@/app/[locale]/dashboard/page";

type LoginFormProps = {
onLoginSuccess: (data: RevenueType[], rid: string, date: string) => void;
onLoginSuccess: (
data: RevenueType[],
rid: string,
date: string,
token?: string,
isUa?: boolean
) => void;
};

export default function LoginForm({ onLoginSuccess }: LoginFormProps) {
Expand Down Expand Up @@ -54,7 +60,7 @@ export default function LoginForm({ onLoginSuccess }: LoginFormProps) {
return;
}

onLoginSuccess(response.data, rid, month);
onLoginSuccess(response.data, rid, month, token, isUa);
} catch (error) {
console.error("Error:", error);
closeAll();
Expand Down
57 changes: 42 additions & 15 deletions src/app/[locale]/dashboard/Revenue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@

import { useEffect, useMemo, useState } from "react";
import { useTranslations } from "next-intl";
import { Button, Card, Skeleton } from "@heroui/react";
import { Button, Card, Skeleton, Popover, PopoverTrigger, PopoverContent } from "@heroui/react";
import { debounce } from "lodash";
import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip, TooltipProps } from "recharts";
import { Props } from "recharts/types/component/DefaultLegendContent";
import { RevenueType } from "@/app/[locale]/dashboard/page";
import SalesList from "@/app/[locale]/dashboard/SalesList";
import SalesLineChart from "@/app/[locale]/dashboard/SalesLineChart";
import YearMonthPicker from "@/components/YearMonthPicker";

type RevenueProps = {
revenueData: RevenueType[];
onLogOut: () => void;
date: string;
rid: string;
month: string;
onMonthChange: (month: string) => void;
};

type ChartDataItem = {
Expand All @@ -29,7 +32,14 @@ type SalesPieChartProps = {
title: string;
};

export default function Revenue({ revenueData, onLogOut, rid, date }: RevenueProps) {
export default function Revenue({
revenueData,
onLogOut,
rid,
date,
month,
onMonthChange,
}: RevenueProps) {
const t = useTranslations("Dashboard");
const [isLoading, setIsLoading] = useState<boolean>(true);
useEffect(() => {
Expand All @@ -39,7 +49,7 @@ export default function Revenue({ revenueData, onLogOut, rid, date }: RevenuePro
setTimeout(() => {
setIsLoading(false);
}, 1000);
}, []);
}, [revenueData, onLogOut]);

// Prepare chart data
const applicationData = useMemo(() => {
Expand Down Expand Up @@ -255,21 +265,38 @@ export default function Revenue({ revenueData, onLogOut, rid, date }: RevenuePro

return (
<div className="dark:bg-gray-900">
<div className="mx-auto max-w-7xl p-6">
<div className="mx-auto max-w-7xl p-2">
{/* 标题区 */}
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
<h1 className="flex-grow text-4xl font-bold dark:text-white">
{t("dashboardTitle", { rid, date })}
<h1 className="flex-grow text-3xl font-bold dark:text-white sm:text-4xl">
<span>{t("dashboardTitle", { rid })}</span>
</h1>
{/* 导出按钮移动到标题右侧 */}
<Button
className="w-full sm:w-auto"
color="secondary"
variant="ghost"
onClick={handleExport}
>
{t("export")}
</Button>

<div className="flex w-full gap-6 px-1 sm:w-auto sm:gap-4">
<h2 className="flex items-center text-3xl font-bold dark:text-white">
<Popover placement="bottom" showArrow>
<PopoverTrigger className="transition-all hover:border-indigo-900 hover:text-indigo-900 dark:hover:border-indigo-200 dark:hover:text-indigo-200">
<span className="cursor-pointer border-b-2 border-dashed">{date}</span>
</PopoverTrigger>
<PopoverContent className="w-80 p-2">
<YearMonthPicker
onChange={onMonthChange}
initialYearMonth={month}
showArrow={true}
/>
</PopoverContent>
</Popover>
</h2>

<Button
className="flex-grow sm:flex-grow-0"
color="secondary"
variant="ghost"
onClick={handleExport}
>
{t("export")}
</Button>
</div>
</div>

<div className="mb-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
Expand Down
111 changes: 101 additions & 10 deletions src/app/[locale]/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use client";

import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { useTranslations } from "next-intl";
import { addToast, closeAll } from "@heroui/toast";
import { CLIENT_BACKEND } from "@/app/requests/misc";
import Revenue from "@/app/[locale]/dashboard/Revenue";
import LoginForm from "@/app/[locale]/dashboard/LoginForm";

Expand All @@ -21,35 +24,123 @@ export type RevenueResponse = {
};

export default function Dashboard() {
const t = useTranslations("Dashboard");

// State
const [isLogin, setIsLogin] = useState<boolean>(false);

// Revenue data
const [revenueData, setRevenueData] = useState<RevenueType[]>([]);
const [currentRid, setCurrentRid] = useState<string>("");
const [currentDate, setCurrentDate] = useState<string>("");
const [currentToken, setCurrentToken] = useState<string>("");
const [currentIsUa, setCurrentIsUa] = useState<boolean>(false);
const [month, setMonth] = useState<string>("");
const [pendingMonth, setPendingMonth] = useState<string>("");

// Debounce timer for query form
const debounceTimer = useRef<NodeJS.Timeout | null>(null);

const handleLoginSuccess = (data: RevenueType[], rid: string, date: string) => {
// Handle login success from LoginForm
const handleLoginSuccess = (
data: RevenueType[],
rid: string,
date: string,
token?: string,
isUa?: boolean
) => {
setRevenueData(data);
setCurrentRid(rid);
setCurrentDate(date);
if (token) setCurrentToken(token);
if (isUa !== undefined) setCurrentIsUa(isUa);
setMonth(date);
setPendingMonth("");
setIsLogin(true);
};

// Query effect for when month changes
useEffect(() => {
if (!pendingMonth || !currentRid || !currentToken) return;

if (debounceTimer.current) clearTimeout(debounceTimer.current);

debounceTimer.current = setTimeout(async () => {
if (month === pendingMonth) return;

try {
const response: RevenueResponse = await fetch(
`${CLIENT_BACKEND}/api/billing/revenue?rid=${currentRid}&date=${pendingMonth}&is_ua=${+currentIsUa}`,
{
headers: { Authorization: currentToken },
}
).then(res => res.json());

if (response.ec === 200) {
closeAll();
addToast({
description: t("refreshSuccess"),
color: "success",
});
setRevenueData(response.data);
setMonth(pendingMonth);
setCurrentDate(pendingMonth);
} else {
closeAll();
addToast({
description: t("error"),
color: "warning",
});
}
} catch (error) {
console.error("Error:", error);
closeAll();
addToast({
description: t("error"),
color: "warning",
});
}
}, 500);

return () => {
if (debounceTimer.current) clearTimeout(debounceTimer.current);
};
}, [pendingMonth, currentRid, currentToken, currentIsUa, t]);

const handleLogOut = () => {
setIsLogin(false);
setRevenueData([]);
setCurrentRid("");
setCurrentDate("");
setCurrentToken("");
setCurrentIsUa(false);
setMonth("");
setPendingMonth("");
};

if (isLogin) {
// If not logged in, show the standalone login form
if (!isLogin) {
return (
<Revenue
onLogOut={handleLogOut}
revenueData={revenueData}
date={currentDate}
rid={currentRid}
/>
<div className="flex min-h-screen min-w-96 flex-col items-center justify-center">
<LoginForm onLoginSuccess={handleLoginSuccess} />
</div>
);
}

return <LoginForm onLoginSuccess={handleLoginSuccess} />;
// If logged in, show dashboard view with embedded query form on the top
return (
<div className="min-w-96 dark:bg-gray-900">
<div className="max-w-8xl mx-auto p-4 sm:p-8">
{/* Revenue charts and data display */}
<Revenue
onLogOut={handleLogOut}
revenueData={revenueData}
date={currentDate}
rid={currentRid}
month={month}
onMonthChange={setPendingMonth}
/>
</div>
</div>
);
}
Loading