diff --git a/cwd-admin/src/api/admin.ts b/cwd-admin/src/api/admin.ts index 8e649f9..f3c1396 100644 --- a/cwd-admin/src/api/admin.ts +++ b/cwd-admin/src/api/admin.ts @@ -88,8 +88,11 @@ export type VisitOverviewResponse = { totalPv: number; totalPages: number; todayPv: number; + yesterdayPv?: number; weekPv: number; + lastWeekPv?: number; monthPv: number; + lastMonthPv?: number; last30Days?: { date: string; total: number; diff --git a/cwd-admin/src/styles/components/analyticsvisit.less b/cwd-admin/src/styles/components/analyticsvisit.less index 7586fa8..02a1898 100644 --- a/cwd-admin/src/styles/components/analyticsvisit.less +++ b/cwd-admin/src/styles/components/analyticsvisit.less @@ -44,6 +44,29 @@ font-size: 26px; font-weight: 600; color: var(--text-primary); + display: flex; + align-items: baseline; +} + +.trend { + display: inline-flex; + align-items: center; + margin-left: 8px; + font-size: 14px; + font-weight: 500; + + &.up { + color: #10b981; + } + + &.down { + color: #ef4444; + } +} + +.trend-arrow { + margin-right: 2px; + font-family: sans-serif; } .domain-table-wrapper { @@ -191,4 +214,4 @@ .stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} \ No newline at end of file +} diff --git a/cwd-admin/src/views/AnalyticsVisitView/index.vue b/cwd-admin/src/views/AnalyticsVisitView/index.vue index 1d89252..9fef02f 100644 --- a/cwd-admin/src/views/AnalyticsVisitView/index.vue +++ b/cwd-admin/src/views/AnalyticsVisitView/index.vue @@ -25,15 +25,48 @@
今日访问量
-
{{ overview.todayPv }}
+
+ {{ overview.todayPv }} + + {{ percentageChange >= 0 ? '↑' : '↓' }} + {{ Math.abs(percentageChange).toFixed(1) }}% + +
本周访问量
-
{{ overview.weekPv }}
+
+ {{ overview.weekPv }} + + {{ weekPercentageChange >= 0 ? '↑' : '↓' }} + {{ Math.abs(weekPercentageChange).toFixed(1) }}% + +
本月访问量
-
{{ overview.monthPv }}
+
+ {{ overview.monthPv }} + + {{ monthPercentageChange >= 0 ? '↑' : '↓' }} + {{ Math.abs(monthPercentageChange).toFixed(1) }}% + +
有访问记录的页面数
@@ -198,11 +231,33 @@ const overview = ref({ totalPv: 0, totalPages: 0, todayPv: 0, + yesterdayPv: 0, weekPv: 0, + lastWeekPv: 0, monthPv: 0, + lastMonthPv: 0, last30Days: [], }); +function calculateChange(current: number, previous: number) { + if (previous === 0) { + return current > 0 ? 100 : 0; + } + return ((current - previous) / previous) * 100; +} + +const percentageChange = computed(() => { + return calculateChange(overview.value.todayPv, overview.value.yesterdayPv || 0); +}); + +const weekPercentageChange = computed(() => { + return calculateChange(overview.value.weekPv, overview.value.lastWeekPv || 0); +}); + +const monthPercentageChange = computed(() => { + return calculateChange(overview.value.monthPv, overview.value.lastMonthPv || 0); +}); + const rawItems = ref([]); const items = computed(() => { const list = rawItems.value.slice(); @@ -390,8 +445,11 @@ async function loadData() { totalPv: overviewRes.totalPv, totalPages: overviewRes.totalPages, todayPv: overviewRes.todayPv ?? 0, + yesterdayPv: overviewRes.yesterdayPv ?? 0, weekPv: overviewRes.weekPv ?? 0, + lastWeekPv: overviewRes.lastWeekPv ?? 0, monthPv: overviewRes.monthPv ?? 0, + lastMonthPv: overviewRes.lastMonthPv ?? 0, last30Days: Array.isArray(overviewRes.last30Days) ? overviewRes.last30Days : [], diff --git a/cwd-admin/src/views/SettingsView/index.vue b/cwd-admin/src/views/SettingsView/index.vue index 7a519d8..70bf3ec 100644 --- a/cwd-admin/src/views/SettingsView/index.vue +++ b/cwd-admin/src/views/SettingsView/index.vue @@ -305,6 +305,7 @@ @change="onProviderChange" > +
@@ -361,6 +362,10 @@ 请登录 QQ 邮箱网页版,在【设置 - 账户】中开启 POP3/SMTP 服务并生成授权码。 +
+ 注意:163 邮箱必须使用授权码,而非登录密码。
+ 请登录 163 邮箱网页版,在【设置 - POP3/SMTP/IMAP】中开启服务并生成授权码。 +
@@ -816,6 +821,10 @@ function onProviderChange() { smtpHost.value = "smtp.qq.com"; smtpPort.value = 465; smtpSecure.value = true; + } else if (smtpProvider.value === "163") { + smtpHost.value = "smtp.163.com"; + smtpPort.value = 465; + smtpSecure.value = true; } } @@ -893,6 +902,8 @@ async function load() { if (emailNotifyRes.smtp.host === "smtp.qq.com") { smtpProvider.value = "qq"; + } else if (emailNotifyRes.smtp.host === "smtp.163.com") { + smtpProvider.value = "163"; } else { smtpProvider.value = "custom"; } diff --git a/cwd-api/src/api/admin/visitAnalytics.ts b/cwd-api/src/api/admin/visitAnalytics.ts index a1e4a80..6828ed4 100644 --- a/cwd-api/src/api/admin/visitAnalytics.ts +++ b/cwd-api/src/api/admin/visitAnalytics.ts @@ -5,8 +5,11 @@ type VisitOverview = { totalPv: number; totalPages: number; todayPv: number; + yesterdayPv: number; weekPv: number; + lastWeekPv: number; monthPv: number; + lastMonthPv: number; last30Days: { date: string; total: number; @@ -101,9 +104,31 @@ export const getVisitOverview = async ( const monthStartDate = new Date(Date.UTC(year, month, 1)); const monthStartKey = toKey(monthStartDate); + // Calculate date ranges for last month and last week queries + const lastMonthStartDate = new Date(Date.UTC(year, month - 1, 1)); + const lastMonthEndDate = new Date(monthStartDate.getTime() - 24 * 60 * 60 * 1000); + + // For last week, we need the start date of last week. + // weekStartDate is the start of current week. + // So lastWeekStartDate is weekStartDate - 7 days. + const weekStartDate = (() => { + const d = new Date(Date.UTC(year, month, day)); + const weekday = d.getUTCDay(); + const offset = (weekday + 6) % 7; + return new Date(d.getTime() - offset * 24 * 60 * 60 * 1000); + })(); + + const lastWeekStartDate = new Date(weekStartDate.getTime() - 7 * 24 * 60 * 60 * 1000); + const lastWeekEndDate = new Date(weekStartDate.getTime() - 24 * 60 * 60 * 1000); + + // We need to fetch enough history for last month. + // The earliest date could be either startDate30 or lastMonthStartDate. let earliestDate = startDate30; - if (monthStartKey < earliestDate) { - earliestDate = monthStartKey; + if (toKey(lastMonthStartDate) < earliestDate) { + earliestDate = toKey(lastMonthStartDate); + } + if (toKey(lastWeekStartDate) < earliestDate) { + earliestDate = toKey(lastWeekStartDate); } let dailySql = @@ -140,28 +165,39 @@ export const getVisitOverview = async ( } const todayKey = toKey(now); + const yesterdayKey = toKey(new Date(now.getTime() - 24 * 60 * 60 * 1000)); - const weekStartDate = (() => { - const d = new Date(Date.UTC(year, month, day)); - const weekday = d.getUTCDay(); - const offset = (weekday + 6) % 7; - return new Date(d.getTime() - offset * 24 * 60 * 60 * 1000); - })(); + // weekStartDate is already calculated above const weekStartKey = toKey(weekStartDate); let todayPv = dailyMap.get(todayKey) || 0; + let yesterdayPv = dailyMap.get(yesterdayKey) || 0; let weekPv = 0; + let lastWeekPv = 0; let monthPv = 0; + let lastMonthPv = 0; + // Calculate Week PV { let cursor = new Date(weekStartDate.getTime()); while (cursor.getTime() <= now.getTime()) { const key = toKey(cursor); weekPv += dailyMap.get(key) || 0; - cursor = new Date(cursor.getTime() - 0 + 24 * 60 * 60 * 1000); + cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000); } } + // Calculate Last Week PV + { + let cursor = new Date(lastWeekStartDate.getTime()); + while (cursor.getTime() <= lastWeekEndDate.getTime()) { + const key = toKey(cursor); + lastWeekPv += dailyMap.get(key) || 0; + cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000); + } + } + + // Calculate Month PV { let cursor = new Date(monthStartDate.getTime()); while (cursor.getTime() <= now.getTime()) { @@ -171,6 +207,16 @@ export const getVisitOverview = async ( } } + // Calculate Last Month PV + { + let cursor = new Date(lastMonthStartDate.getTime()); + while (cursor.getTime() <= lastMonthEndDate.getTime()) { + const key = toKey(cursor); + lastMonthPv += dailyMap.get(key) || 0; + cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1000); + } + } + if (todayPv > totalPv) { todayPv = totalPv; } @@ -195,8 +241,11 @@ export const getVisitOverview = async ( totalPv, totalPages, todayPv, + yesterdayPv, weekPv, + lastWeekPv, monthPv, + lastMonthPv, last30Days }; diff --git a/docs/function/email-reminder.md b/docs/function/email-reminder.md index 4c144d6..32f6ff9 100644 --- a/docs/function/email-reminder.md +++ b/docs/function/email-reminder.md @@ -3,15 +3,33 @@ 目前支持进行通知的邮箱有: - QQ 邮箱 +- 163 邮箱 +- 自定义 SMTP 后续会添加其他邮箱服务,可以提交反馈。 -## QQ 邮箱 +## 邮箱服务配置 + +### QQ 邮箱 1. 登录 QQ 邮箱,进入 `设置 > 账户` 2. 开启 `POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV 服务`,并获取授权码 3. 在管理后台设置中配置 QQ 邮箱账号和授权码 +### 163 邮箱 + +1. 登录 163 邮箱,进入 `设置` +2. 开启 `POP3/SMTP/IMAP`,并获取授权码 +3. 在管理后台设置中配置 163 邮箱账号和授权码 + +### 自定义 SMTP 邮箱 + +配置自己的 SMTP 邮箱需要以下信息: + +- SMTP 服务器地址 +- SMTP 服务器端口 +- 发件邮箱账号 +- 邮箱密码或授权码 ## 邮件通知配置