fix(realtime): fix charts display

This commit is contained in:
oiov
2025-05-24 21:28:43 +08:00
parent a1cd74e90f
commit 24ae1bc45e
8 changed files with 241 additions and 168 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ export default async function DashboardPage() {
<TabsTrigger value="Links">Links</TabsTrigger>
<TabsTrigger value="Realtime">Realtime</TabsTrigger>
</TabsList>
<TabsContent value="Links">
<TabsContent className="space-y-3" value="Links">
<UserUrlsList
user={{
id: user.id,
+130 -47
View File
@@ -2,6 +2,18 @@
import { useEffect, useRef, useState } from "react";
import dynamic from "next/dynamic";
import {
addHours,
addMinutes,
differenceInDays,
differenceInHours,
differenceInMinutes,
format,
startOfDay,
startOfHour,
startOfMinute,
} from "date-fns";
import { create } from "lodash";
import { DAILY_DIMENSION_ENUMS } from "@/lib/enums";
import {
@@ -25,6 +37,7 @@ export interface Location {
city?: string;
country?: string;
lastUpdate?: Date;
createdAt?: Date;
updatedAt?: Date;
device?: string;
browser?: string;
@@ -43,6 +56,7 @@ interface DatabaseLocation {
country: string;
lastUpdate: Date;
updatedAt: Date;
createdAt: Date;
device?: string;
browser?: string;
userUrl?: {
@@ -89,60 +103,128 @@ export default function Realtime({ isAdmin = false }: { isAdmin?: boolean }) {
};
const processChartData = (locations: Location[]): ChartData[] => {
const countBySegment: {
[key: string]: { count: number; timestamp: number };
} = {};
const timestamps = locations
.filter((loc) => loc.updatedAt)
.map((loc) => new Date(loc.updatedAt || "").getTime());
// 过滤有效数据
const validLocations = locations.filter((loc) => loc.createdAt);
if (validLocations.length === 0) return [];
if (timestamps.length === 0) return [];
// 获取时间范围
const dates = validLocations.map((loc) => new Date(loc.createdAt!));
const minDate = new Date(Math.min(...dates.map((d) => d.getTime())));
const maxDate = new Date(Math.max(...dates.map((d) => d.getTime())));
const minTime = Math.min(...timestamps);
const maxTime = Math.max(...timestamps);
const timeRangeMinutes = (maxTime - minTime) / (1000 * 60);
console.log("[sss]", timeRangeMinutes);
// 根据时间跨度选择分组策略
const totalMinutes = differenceInMinutes(maxDate, minDate);
const totalHours = differenceInHours(maxDate, minDate);
const totalDays = differenceInDays(maxDate, minDate);
let segmentSizeMinutes: number;
if (timeRangeMinutes <= 30) segmentSizeMinutes = 0.5;
else if (timeRangeMinutes <= 60) segmentSizeMinutes = 2;
else if (timeRangeMinutes <= 360) segmentSizeMinutes = 12;
else if (timeRangeMinutes <= 720) segmentSizeMinutes = 24;
else if (timeRangeMinutes <= 1440) segmentSizeMinutes = 36;
else {
segmentSizeMinutes = Math.ceil(timeRangeMinutes / 30);
segmentSizeMinutes = Math.ceil(segmentSizeMinutes / 60) * 60;
let groupByFn: (date: Date) => Date;
let formatFn: (date: Date) => string;
let intervalFn: (date: Date, interval: number) => Date;
let interval: number;
// 30分钟内:按1分钟分组
if (totalMinutes <= 30) {
groupByFn = startOfMinute;
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 1;
} else if (totalMinutes <= 60) {
// 1小时内:按2分钟分组
groupByFn = (date) => {
const minutes = Math.floor(date.getMinutes() / 2) * 2;
const grouped = startOfMinute(date);
grouped.setMinutes(minutes);
return grouped;
};
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 2;
} else if (totalHours <= 2) {
// 2小时内:按4分钟分组
groupByFn = (date) => {
const minutes = Math.floor(date.getMinutes() / 4) * 4;
const grouped = startOfMinute(date);
grouped.setMinutes(minutes);
return grouped;
};
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 4;
} else if (totalHours <= 6) {
// 6小时内:按12分钟分组
groupByFn = (date) => {
const minutes = Math.floor(date.getMinutes() / 12) * 12;
const grouped = startOfMinute(date);
grouped.setMinutes(minutes);
return grouped;
};
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 12;
} else if (totalHours <= 12) {
// 12小时内:按24分钟分组
groupByFn = (date) => {
const minutes = Math.floor(date.getMinutes() / 24) * 24;
const grouped = startOfMinute(date);
grouped.setMinutes(minutes);
return grouped;
};
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 24;
} else if (totalHours <= 24) {
// 24小时内:按48分钟分组
groupByFn = (date) => {
const minutes = Math.floor(date.getMinutes() / 48) * 48;
const grouped = startOfMinute(date);
grouped.setMinutes(minutes);
return grouped;
};
formatFn = (date) => format(date, "MM-dd HH:mm");
intervalFn = addMinutes;
interval = 48;
} else if (totalDays <= 7) {
// 7天内:按天分组
groupByFn = startOfDay;
formatFn = (date) => format(date, "MM-dd");
intervalFn = addHours;
interval = 24;
} else {
// 更长时间:按天分组
groupByFn = startOfDay;
formatFn = (date) => format(date, "MM-dd");
intervalFn = addHours;
interval = 24;
}
locations.forEach((loc) => {
if (!loc.updatedAt) return;
const date = new Date(loc.updatedAt);
const minutesSinceStart = Math.floor(
(date.getTime() - minTime) / (1000 * 60),
);
const segmentIndex = Math.floor(minutesSinceStart / segmentSizeMinutes);
const segmentStartMinutes = segmentIndex * segmentSizeMinutes;
const segmentDate = new Date(minTime + segmentStartMinutes * 60 * 1000);
const timeKey = `${segmentDate.getHours().toString().padStart(2, "0")}:${segmentDate
.getMinutes()
.toString()
.padStart(2, "0")}`;
// 分组聚合数据
const groupedData = new Map<string, number>();
if (!countBySegment[timeKey]) {
countBySegment[timeKey] = {
count: 0,
timestamp: segmentDate.getTime(),
};
}
countBySegment[timeKey].count += loc.count;
validLocations.forEach((loc) => {
const date = new Date(loc.createdAt!);
const groupedDate = groupByFn(date);
const key = groupedDate.getTime().toString();
groupedData.set(key, (groupedData.get(key) || 0) + loc.count);
});
return Object.keys(countBySegment)
.sort((a, b) => countBySegment[a].timestamp - countBySegment[b].timestamp)
.map((time) => ({
time,
count: countBySegment[time].count,
}));
// 填充时间间隔,确保连续性
const result: ChartData[] = [];
const startGroup = groupByFn(minDate);
const endGroup = groupByFn(maxDate);
let current = startGroup;
// 过滤掉count为0 的数据
while (current <= endGroup) {
const key = current.getTime().toString();
result.push({
time: formatFn(current),
count: groupedData.get(key) || 0,
});
current = intervalFn(current, interval);
}
return result;
};
const appendLocationData = (
@@ -176,6 +258,7 @@ export default function Realtime({ isAdmin = false }: { isAdmin?: boolean }) {
browser: item.browser,
userUrl: item.userUrl,
updatedAt: item.updatedAt,
createdAt: item.createdAt,
});
}
totalNewClicks += clickCount;
@@ -216,7 +299,7 @@ export default function Realtime({ isAdmin = false }: { isAdmin?: boolean }) {
const result = await response.json();
if (result.error) {
console.error("API Error:", result.error);
// console.error("API Error:", result.error);
return;
}
@@ -29,72 +29,75 @@ export const RealtimeChart = ({
chartData,
totalClicks,
}: RealtimeChartProps) => {
const maxCount = Math.max(...chartData.map((d) => d.count), 1);
// const tickInterval =
// chartData.length <= 10 ? 0 : Math.ceil(chartData.length / 10);
const getTickInterval = (dataLength: number) => {
if (dataLength <= 6) return 0; // 显示所有刻度
if (dataLength <= 12) return 1; // 每隔1个显示
if (dataLength <= 24) return Math.ceil(dataLength / 8); // 大约8个刻度
return Math.ceil(dataLength / 6); // 大约6个刻度
if (dataLength <= 6) return 0;
if (dataLength <= 12) return 1;
if (dataLength <= 24) return Math.ceil(dataLength / 8);
return Math.ceil(dataLength / 6);
};
const tickInterval = getTickInterval(chartData.length);
// console.log("chartData", chartData);
// 过滤掉为count=0的数据,但是最后一个数据为0时不要剔除
const filteredChartData = chartData.filter((item, index) => {
return item.count !== 0 || index === chartData.length - 1;
});
const tickInterval = getTickInterval(filteredChartData.length);
return (
<div className={cn(`rounded-lg border p-3 backdrop-blur-xl`, className)}>
<div className={cn(`rounded-lg border p-3 backdrop-blur-2xl`, className)}>
<div className="mb-1 flex items-center text-base font-semibold">
<StatusDot status={1} />
<h3 className="ml-2">Realtime Visits</h3>
<Icons.mousePointerClick className="ml-auto size-4 text-muted-foreground" />
</div>
<p className="mb-2 text-lg font-semibold">{totalClicks}</p>
<ResponsiveContainer width={300} height={200}>
<BarChart
data={chartData}
margin={{ top: 10, right: 0, left: -20, bottom: 0 }}
barCategoryGap={1}
>
<XAxis
dataKey="time"
tick={{ fontSize: 12 }}
interval={tickInterval}
tickCount={Math.min(chartData.length, 10)}
axisLine={false}
tickLine={false}
type="category"
scale="point"
padding={{ left: 14, right: 20 }}
tickFormatter={(value) => value}
/>
<YAxis
// domain={[0, maxCount]}
domain={["dataMin", "dataMax"]}
tickCount={5}
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="rounded-md bg-primary-foreground/90 p-2 text-sm backdrop-blur-lg">
<p className="label">{`${label}`}</p>
<p className="label">{`Visits: ${payload[0].value}`}</p>
</div>
);
}
return null;
}}
/>
<Bar
dataKey="count"
fill="#36d399"
radius={[1, 1, 0, 0]}
maxBarSize={20}
/>
</BarChart>
</ResponsiveContainer>
{/* <ResponsiveContainer ></ResponsiveContainer> */}
<BarChart
width={300}
height={200}
data={filteredChartData}
margin={{ top: 10, right: 0, left: -20, bottom: 0 }}
barCategoryGap={1}
>
<XAxis
dataKey="time"
tick={{ fontSize: 12 }}
interval={tickInterval}
tickCount={Math.min(filteredChartData.length, 10)}
axisLine={false}
tickLine={false}
type="category"
scale="point"
padding={{ left: 14, right: 20 }}
tickFormatter={(value) => value.split(" ")[1]}
/>
<YAxis
domain={[0, "dataMax"]}
tickCount={5}
tick={{ fontSize: 12 }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="rounded-md border bg-primary-foreground py-2 text-primary backdrop-blur">
<p className="label px-2 text-base font-medium">{`${label}`}</p>
<p className="label px-2 text-sm">{`Visits: ${payload[0].value}`}</p>
</div>
);
}
return null;
}}
/>
<Bar
dataKey="count"
fill="#36d399"
radius={[1, 1, 0, 0]}
maxBarSize={20}
/>
</BarChart>
</div>
);
};
@@ -40,13 +40,8 @@ export default function RealtimeGlobe({
const [countries, setCountries] = useState<any>({});
const [currentLocation, setCurrentLocation] = useState<any>({});
const [hexAltitude, setHexAltitude] = useState(0.001);
const {
ref: wrapperRef,
width: wrapperWidth,
height: wrapperHeight,
} = useElementSize();
const { ref: wrapperRef, width: wrapperWidth } = useElementSize();
const [isLoaded, setIsLoaded] = useState(false);
const [error, setError] = useState<string | null>(null);
const highest =
locations.reduce((acc, curr) => Math.max(acc, curr.count), 0) || 1;
@@ -62,8 +57,7 @@ export default function RealtimeGlobe({
const { MeshPhongMaterial } = await import("three");
return { Globe, MeshPhongMaterial };
} catch (err) {
console.error("Failed to load Globe.gl:", err);
setError("Failed to load Globe.gl library");
// console.error("Failed to load Globe.gl:", err);
return null;
}
}, []);
@@ -128,7 +122,8 @@ export default function RealtimeGlobe({
globe = new Globe(container)
.width(wrapperWidth)
.height(wrapperWidth > 728 ? wrapperWidth * 0.8 : wrapperWidth)
.height(wrapperWidth)
.globeOffset([0, -100])
.atmosphereColor("rgba(170, 170, 200, 0.8)")
.backgroundColor("rgba(0,0,0,0)")
.globeMaterial(
@@ -188,7 +183,6 @@ export default function RealtimeGlobe({
}
setIsLoaded(true);
setError(null);
});
if (globe.controls()) {
@@ -214,14 +208,7 @@ export default function RealtimeGlobe({
}
globeInstanceRef.current = globe;
// console.log("Globe initialization complete");
} catch (err) {
// console.error("Error initializing globe:", err);
setError(
err instanceof Error ? err.message : "Failed to initialize globe",
);
}
} catch (err) {}
}, [
countries,
locations,
@@ -308,16 +295,11 @@ export default function RealtimeGlobe({
}, [cleanup]);
return (
<div ref={wrapperRef} className="relative -mt-10">
<div ref={wrapperRef} className="relative">
<div
ref={globeRef}
className="flex justify-center"
style={{
// width: `${wrapperWidth}px`,
// height:
// wrapperWidth > 728
// ? `${wrapperWidth * 0.8}px`
// : `${wrapperWidth}px`,
maxWidth: `${wrapperWidth}px`, // 比较疑惑
minHeight: "100px",
}}
+1 -1
View File
@@ -34,7 +34,7 @@ export default async function DashboardPage() {
<TabsTrigger value="Realtime">Realtime</TabsTrigger>
</TabsList>
<TabsContent value="Links">
<TabsContent className="space-y-3" value="Links">
<UserUrlsList
user={{
id: user.id,
+7 -2
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { create } from "lodash";
import { prisma } from "@/lib/db";
import { checkUserStatus } from "@/lib/dto/user";
@@ -41,7 +42,7 @@ export async function GET(request: NextRequest) {
const whereClause: any = {
...(isAdmin === "true" ? {} : { userUrl: { userId: user.id } }),
updatedAt: {
createdAt: {
gte: startDate,
lte: endDate,
},
@@ -67,6 +68,7 @@ export async function GET(request: NextRequest) {
country: true,
device: true,
browser: true,
createdAt: true,
updatedAt: true,
userUrl: {
select: {
@@ -92,6 +94,7 @@ export async function GET(request: NextRequest) {
country: string;
lastUpdate: Date;
updatedAt: Date;
createdAt: Date;
device: string;
browser: string;
userUrl: {
@@ -125,6 +128,7 @@ export async function GET(request: NextRequest) {
country: item.country || "",
lastUpdate: item.updatedAt,
updatedAt: item.updatedAt,
createdAt: item.createdAt,
device: item.device || "",
browser: item.browser || "",
userUrl: item.userUrl,
@@ -211,7 +215,7 @@ export async function POST(request: NextRequest) {
const whereClause: any = {
...(isAdmin ? {} : { userUrl: { userId: user.id } }),
updatedAt: {
createdAt: {
gt: sinceDate,
},
latitude: {
@@ -232,6 +236,7 @@ export async function POST(request: NextRequest) {
country: true,
device: true,
browser: true,
createdAt: true,
updatedAt: true,
userUrl: {
select: {
+36 -36
View File
@@ -1,39 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/pricing</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/privacy</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/terms</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/chat</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-05-23T15:05:05.510Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/robots.txt</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/authentification</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/cloudflare-email-worker</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/components</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/config-files</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/database</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/email</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/installation</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/markdown-files</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/developer/quick-start</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/dns-records</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/emails</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/cloudflare</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/other</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/vercel</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/examples/zeabur</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/icon</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/markdown</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/meta-info</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/qrcode</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/screenshot</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/open-api/text</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/plan</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/quick-start</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/short-urls</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/docs/wroom</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/pricing</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/privacy</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/terms</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/chat</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/manifest.json</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/password-prompt</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://wr.do/opengraph-image.jpg</loc><lastmod>2025-05-24T09:27:12.895Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
</urlset>
+1 -1
View File
File diff suppressed because one or more lines are too long