diff --git a/app/(protected)/admin/records/page.tsx b/app/(protected)/admin/records/page.tsx index 21ee1ab..43b35f8 100644 --- a/app/(protected)/admin/records/page.tsx +++ b/app/(protected)/admin/records/page.tsx @@ -3,9 +3,9 @@ import { redirect } from "next/navigation"; import { getCurrentUser } from "@/lib/session"; import { constructMetadata } from "@/lib/utils"; import { DashboardHeader } from "@/components/dashboard/header"; +import { UserRecordStatus } from "@/components/dashboard/status-card"; import UserRecordsList from "../../dashboard/records/record-list"; -import UserRecordStatus from "../../dashboard/records/record-statu"; export const metadata = constructMetadata({ title: "DNS Records", @@ -25,16 +25,7 @@ export default async function DashboardPage() { link="/docs/dns-records" linkText="DNS records" /> - + - +
+ + + + +
); diff --git a/app/(protected)/admin/urls/page.tsx b/app/(protected)/admin/urls/page.tsx index 5ec3b30..baeabc8 100644 --- a/app/(protected)/admin/urls/page.tsx +++ b/app/(protected)/admin/urls/page.tsx @@ -24,7 +24,6 @@ export default async function DashboardPage() { link="/docs/short-urls" linkText="short urls" /> - - + ; - action: string; -} - -export default function UserRecordStatus({ action }: RecordStatusProps) { - const { data, isLoading, error } = useSWR>( - `${action}/status`, - fetcher, - ); - - if (isLoading || error) - return ( -
- - - - -
- ); - return ( - data && ( -
- - - - -
- ) - ); -} - -export function StatuInfoCard({ - title, - total, - monthTotal, -}: { - title: string; - total?: number; - monthTotal: number; -}) { - const t = useTranslations("Components"); - - const statusConfig = { - Active: { - color: "bg-green-500", - textColor: "text-green-500", - gradient: "from-green-400 to-green-600", - shadow: "shadow-green-500/25", - }, - Inactive: { - color: "bg-gray-700", - textColor: "text-gray-700", - gradient: "from-gray-400 to-gray-600", - shadow: "shadow-gray-500/25", - }, - Pending: { - color: "bg-yellow-500", - textColor: "text-yellow-500", - gradient: "from-yellow-400 to-yellow-600", - shadow: "shadow-yellow-500/25", - }, - Rejected: { - color: "bg-red-500", - textColor: "text-red-500", - gradient: "from-red-400 to-red-600", - shadow: "shadow-red-500/25", - }, - }; - - const config = statusConfig[title]; - const percentage = total && total > 0 ? (monthTotal / total) * 100 : 0; - const barHeight = Math.max(8, Math.min(100, percentage)); - - return ( - -
-
-
{t(title)}
-
- -

- / {nFormatter(total || 0)} -

-
-
- -
-
- {/* 柱状图背景 */} -
- {/* 主柱状图 */} -
- {/* 光泽效果 */} -
- {/* 顶部高光 */} - {barHeight > 15 && ( -
- )} - - {percentage.toFixed(0)}% - -
-
-
-
- - ); -} diff --git a/app/(protected)/dashboard/urls/loading.tsx b/app/(protected)/dashboard/urls/loading.tsx index 0b40922..6247767 100644 --- a/app/(protected)/dashboard/urls/loading.tsx +++ b/app/(protected)/dashboard/urls/loading.tsx @@ -8,7 +8,12 @@ export default function DashboardUrlsLoading() { heading="Manage Short URLs" text="List and manage short urls" /> - +
+ + + + +
); diff --git a/app/(protected)/dashboard/urls/url-list.tsx b/app/(protected)/dashboard/urls/url-list.tsx index 9039523..c0f6038 100644 --- a/app/(protected)/dashboard/urls/url-list.tsx +++ b/app/(protected)/dashboard/urls/url-list.tsx @@ -47,6 +47,7 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; +import { UrlStatus } from "@/components/dashboard/status-card"; import { FormType } from "@/components/forms/record-form"; import { UrlForm } from "@/components/forms/url-form"; import ApiReference from "@/components/shared/api-reference"; @@ -725,11 +726,13 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
+ {rendeSeachInputs()} {rendeList()} {rendLogs()} + {rendeSeachInputs()} {rendeGrid()} {rendLogs()} diff --git a/app/api/url/admin/status/route.ts b/app/api/url/admin/status/route.ts new file mode 100644 index 0000000..797541b --- /dev/null +++ b/app/api/url/admin/status/route.ts @@ -0,0 +1,23 @@ +import { getUrlStatusOptimized } from "@/lib/dto/short-urls"; +import { checkUserStatus } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +export async function GET(req: Request) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + if (user.role !== "ADMIN") { + return Response.json("Unauthorized", { + status: 401, + }); + } + + const status = await getUrlStatusOptimized(user.id, "ADMIN"); + + return Response.json(status); + } catch (error) { + return Response.json(error?.statusText || error, { + status: error.status || 500, + }); + } +} diff --git a/app/api/url/status/route.ts b/app/api/url/status/route.ts new file mode 100644 index 0000000..fca74a0 --- /dev/null +++ b/app/api/url/status/route.ts @@ -0,0 +1,18 @@ +import { getUrlStatusOptimized } from "@/lib/dto/short-urls"; +import { checkUserStatus } from "@/lib/dto/user"; +import { getCurrentUser } from "@/lib/session"; + +export async function GET(req: Request) { + try { + const user = checkUserStatus(await getCurrentUser()); + if (user instanceof Response) return user; + + const status = await getUrlStatusOptimized(user.id, "USER"); + + return Response.json(status); + } catch (error) { + return Response.json(error?.statusText || error, { + status: error.status || 500, + }); + } +} diff --git a/components/dashboard/count-up.tsx b/components/dashboard/count-up.tsx index 3fae8c6..a362a69 100644 --- a/components/dashboard/count-up.tsx +++ b/components/dashboard/count-up.tsx @@ -4,9 +4,16 @@ import CountUp from "react-countup"; import { nFormatter } from "@/lib/utils"; -export default function CountUpFn({ count }: { count: number }) { +export default function CountUpFn({ + count, + className, +}: { + count: number; + className?: string; +}) { return ( + ); +} + +// 短链状态组件 +export function UrlStatus({ action }: { action: string }) { + return ( + + ); +} + +export function GenericStatusDashboard({ + apiEndpoint, + statusItems, + statusConfig, + gridCols = 4, +}: { + apiEndpoint: string; + statusItems: string[]; + statusConfig: StatusConfig; + gridCols?: number; +}) { + const { data, isLoading, error } = useSWR>( + `${apiEndpoint}/status`, + fetcher, + ); + + const gridClasses = + { + 2: "grid-cols-2 sm:grid-cols-2 lg:grid-cols-2", + 3: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-3", + 4: "grid-cols-2 sm:grid-cols-4 lg:grid-cols-4", + 5: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-5", + }[gridCols] || "grid-cols-2 sm:grid-cols-4 lg:grid-cols-4"; + + if (isLoading || error) { + return ( +
+ {statusItems.map((_, index) => ( + + ))} +
+ ); + } + + return ( + data && ( +
+ {statusItems.map((status) => ( + + ))} +
+ ) + ); +} + +export function StatusInfoCard({ + title, + total, + currentTotal, + statusConfig, +}: { + title: string; + total?: number; + currentTotal: number; + statusConfig: StatusConfig; +}) { + const t = useTranslations("Components"); + + const config = statusConfig[title]; + const percentage = + total && total > 0 && currentTotal > 0 ? (currentTotal / total) * 100 : 0; + const barHeight = Math.max(8, Math.min(100, percentage)); + + return ( + +
+
+
{t(title)}
+
+ {nFormatter(currentTotal || 0)} +

+ / {nFormatter(total || 0)} +

+
+
+ +
+
+
+
+
+ {barHeight > 15 && ( +
+ )} + + {percentage.toFixed(0)}% + +
+
+
+
+ + ); +} diff --git a/lib/dto/short-urls.ts b/lib/dto/short-urls.ts index 2b4220f..45bcda1 100644 --- a/lib/dto/short-urls.ts +++ b/lib/dto/short-urls.ts @@ -2,6 +2,7 @@ import { UrlMeta, UserRole } from "@prisma/client"; import { prisma } from "@/lib/db"; +import { EXPIRATION_ENUMS } from "../enums"; import { getStartDate } from "../utils"; export interface ShortUrlFormData { @@ -154,6 +155,93 @@ export async function getUrlClicksByIds( } } +export async function getUrlStatus(userId: string, role: UserRole = "USER") { + try { + } catch (error) { + return { status: error }; + } +} + +export interface UrlStatusStats { + total: number; + actived: number; // 正常可用 + disabled: number; // 已禁用 + expired: number; // 已过期 + passwordProtected: number; // 密码保护 +} + +function isValidExpirationValue(expiration: string): boolean { + return EXPIRATION_ENUMS.some((item) => item.value === expiration); +} + +export async function getUrlStatusOptimized( + userId: string, + role: UserRole = "USER", +): Promise { + try { + const whereCondition = role === "USER" ? { userId } : {}; + + const urlRecords = await prisma.userUrl.findMany({ + where: whereCondition, + select: { + id: true, + userId: true, + active: true, + expiration: true, + password: true, + createdAt: true, + updatedAt: true, + }, + }); + + const now = Date.now(); + const stats: UrlStatusStats = { + total: urlRecords.length, + actived: 0, + disabled: 0, + expired: 0, + passwordProtected: 0, + }; + + // 遍历记录并分类 + urlRecords.forEach((record) => { + const updatedAt = new Date( + record.updatedAt || record.createdAt!, + ).getTime(); + + // 判断是否过期 + let isExpired = false; + if ( + record.expiration !== "-1" && + isValidExpirationValue(record.expiration) + ) { + const expirationSeconds = Number(record.expiration); + const expirationMilliseconds = expirationSeconds * 1000; + const expirationTime = updatedAt + expirationMilliseconds; + isExpired = now > expirationTime; + } + + const isDisabled = record.active === 0; + const hasPassword = Boolean(record.password && record.password.trim()); + + if (isExpired) { + stats.expired++; + } else if (isDisabled) { + stats.disabled++; + } else if (hasPassword) { + stats.passwordProtected++; + } else { + stats.actived++; + } + }); + + return stats; + } catch (error) { + console.error("Error getting URL status (optimized):", error); + return { status: error }; + } +} + export async function createUserShortUrl(data: ShortUrlFormData) { try { const res = await prisma.userUrl.create({ diff --git a/locales/en.json b/locales/en.json index 08cb0f5..d1e7cc5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -320,7 +320,11 @@ "retry": "Retry", "backToHome": "Back to Home", "goBack": "Go Back", - "contactSupportIfError": "If you believe this is an error, please contact technical support" + "contactSupportIfError": "If you believe this is an error, please contact technical support", + "Actived": "Active", + "Disabled": "Disabled", + "Expired": "Expired", + "PasswordProtected": "Password Protected" }, "Landing": { "settings": "Settings", diff --git a/locales/zh.json b/locales/zh.json index 5a7e0b0..d1e11df 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -320,7 +320,11 @@ "retry": "重试", "backToHome": "返回首页", "goBack": "返回上页", - "contactSupportIfError": "如果您认为这是一个错误,请联系技术支持" + "contactSupportIfError": "如果您认为这是一个错误,请联系技术支持", + "Actived": "有效", + "Disabled": "已禁用", + "Expired": "已过期", + "PasswordProtected": "密码保护" }, "Landing": { "settings": "设置", diff --git a/public/sw.js.map b/public/sw.js.map index afde4d3..8d5bb4e 100644 --- a/public/sw.js.map +++ b/public/sw.js.map @@ -1 +1 @@ -{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/982d10a57b1ab0281e8eec16db8c2713/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"sw.js","sources":["../../../../../../private/var/folders/9b/3qmyp8zd2xvdspdrp149fyg00000gn/T/c1e18fb54d3ad49ae1827e1b42c07b7f/sw.js"],"sourcesContent":["import {registerRoute as workbox_routing_registerRoute} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-routing@6.6.0/node_modules/workbox-routing/registerRoute.mjs';\nimport {NetworkFirst as workbox_strategies_NetworkFirst} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkFirst.mjs';\nimport {NetworkOnly as workbox_strategies_NetworkOnly} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-strategies@6.6.0/node_modules/workbox-strategies/NetworkOnly.mjs';\nimport {clientsClaim as workbox_core_clientsClaim} from '/Users/songjunxi/Desktop/repos/wrdo-app/wr.do/node_modules/.pnpm/workbox-core@6.6.0/node_modules/workbox-core/clientsClaim.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\nimportScripts(\n \n);\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n\nworkbox_routing_registerRoute(\"/\", new workbox_strategies_NetworkFirst({ \"cacheName\":\"start-url\", plugins: [{ cacheWillUpdate: async ({ request, response, event, state }) => { if (response && response.type === 'opaqueredirect') { return new Response(response.body, { status: 200, statusText: 'OK', headers: response.headers }) } return response } }] }), 'GET');\nworkbox_routing_registerRoute(/.*/i, new workbox_strategies_NetworkOnly({ \"cacheName\":\"dev\", plugins: [] }), 'GET');\n\n\n\n\n"],"names":["importScripts","self","skipWaiting","workbox_core_clientsClaim","workbox_routing_registerRoute","workbox_strategies_NetworkFirst","plugins","cacheWillUpdate","request","response","event","state","type","Response","body","status","statusText","headers","workbox_strategies_NetworkOnly"],"mappings":"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"} \ No newline at end of file