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 服务器端口
+- 发件邮箱账号
+- 邮箱密码或授权码
## 邮件通知配置