feat(router): 添加页面标题和滚动重置功能
- 为路由添加 meta.title 配置,在导航守卫中动态设置文档标题 - 添加 afterEach 钩子,在路由切换时重置布局内容和窗口滚动位置 - 标准化路由配置的逗号格式,增强代码一致性 - 优化主题切换 composable 的代码格式和注释翻译 - 改进访问统计页面的排序逻辑,使用 computed 替代手动重新加载 - 添加全局滚动条样式,提升视觉一致性
This commit is contained in:
@@ -6,52 +6,49 @@ const STORAGE_KEY = 'cwd_admin_theme';
|
|||||||
const theme = ref<Theme>('system');
|
const theme = ref<Theme>('system');
|
||||||
|
|
||||||
export function useTheme() {
|
export function useTheme() {
|
||||||
function applyTheme() {
|
function applyTheme() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const isDark =
|
const isDark = theme.value === 'dark' || (theme.value === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
theme.value === 'dark' ||
|
|
||||||
(theme.value === 'system' &&
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
||||||
|
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
root.classList.add('dark');
|
root.classList.add('dark');
|
||||||
} else {
|
} else {
|
||||||
root.classList.remove('dark');
|
root.classList.remove('dark');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTheme(newTheme: Theme) {
|
function setTheme(newTheme: Theme) {
|
||||||
theme.value = newTheme;
|
theme.value = newTheme;
|
||||||
localStorage.setItem(STORAGE_KEY, newTheme);
|
localStorage.setItem(STORAGE_KEY, newTheme);
|
||||||
applyTheme();
|
applyTheme();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null;
|
||||||
if (stored && ['light', 'dark', 'system'].includes(stored)) {
|
if (stored && ['light', 'dark', 'system'].includes(stored)) {
|
||||||
theme.value = stored;
|
theme.value = stored;
|
||||||
} else {
|
} else {
|
||||||
theme.value = 'system';
|
theme.value = 'system';
|
||||||
}
|
}
|
||||||
applyTheme();
|
applyTheme();
|
||||||
|
|
||||||
// Listen for system changes
|
// 监听系统变更
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
// Remove existing listener to avoid duplicates if init is called multiple times (though it shouldn't be)
|
// 移除现有的监听器,以避免 init 被多次调用时出现重复监听(尽管不应该重复监听)。
|
||||||
mediaQuery.onchange = () => {
|
mediaQuery.onchange = () => {
|
||||||
if (theme.value === 'system') {
|
if (theme.value === 'system') {
|
||||||
applyTheme();
|
applyTheme();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
setTheme,
|
setTheme,
|
||||||
initTheme
|
initTheme,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const routes: RouteRecordRaw[] = [
|
|||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: LoginView
|
component: LoginView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@@ -19,43 +19,64 @@ const routes: RouteRecordRaw[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirect: '/comments'
|
redirect: '/comments',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'comments',
|
path: 'comments',
|
||||||
name: 'comments',
|
name: 'comments',
|
||||||
component: CommentsView
|
component: CommentsView,
|
||||||
|
meta: {
|
||||||
|
title: '评论管理',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'stats',
|
path: 'stats',
|
||||||
name: 'stats',
|
name: 'stats',
|
||||||
component: StatsView
|
component: StatsView,
|
||||||
|
meta: {
|
||||||
|
title: '数据看板',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'analytics',
|
path: 'analytics',
|
||||||
name: 'analytics',
|
name: 'analytics',
|
||||||
component: AnalyticsVisitView
|
component: AnalyticsVisitView,
|
||||||
|
meta: {
|
||||||
|
title: '访问统计',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
component: SettingsView
|
component: SettingsView,
|
||||||
|
meta: {
|
||||||
|
title: '网站设置',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'data',
|
path: 'data',
|
||||||
name: 'data',
|
name: 'data',
|
||||||
component: DataView
|
component: DataView,
|
||||||
}
|
meta: {
|
||||||
]
|
title: '数据迁移',
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
routes
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
|
const defaultTitle = 'CWD 评论系统';
|
||||||
|
if (to.meta && to.meta.title) {
|
||||||
|
document.title = (to.meta.title + ' - ' + defaultTitle) as string;
|
||||||
|
} else {
|
||||||
|
document.title = defaultTitle as string;
|
||||||
|
}
|
||||||
if (to.name === 'login') {
|
if (to.name === 'login') {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
@@ -67,3 +88,13 @@ router.beforeEach((to, from, next) => {
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.afterEach((to, from) => {
|
||||||
|
if (to.name !== from.name) {
|
||||||
|
const layoutContent = document.querySelector('.layout-content');
|
||||||
|
if (layoutContent instanceof HTMLElement) {
|
||||||
|
layoutContent.scrollTop = 0;
|
||||||
|
}
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, onBeforeUnmount, ref, nextTick, watch, inject } from "vue";
|
import { onMounted, onBeforeUnmount, ref, nextTick, watch, inject, computed } from "vue";
|
||||||
import type { Ref } from "vue";
|
import type { Ref } from "vue";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
import {
|
import {
|
||||||
@@ -199,7 +199,25 @@ const overview = ref<VisitOverviewResponse>({
|
|||||||
last30Days: [],
|
last30Days: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = ref<VisitPageItem[]>([]);
|
const rawItems = ref<VisitPageItem[]>([]);
|
||||||
|
const items = computed<VisitPageItem[]>(() => {
|
||||||
|
const list = rawItems.value.slice();
|
||||||
|
list.sort((a, b) => {
|
||||||
|
const aLast = getLastVisitAtTs(a.lastVisitAt);
|
||||||
|
const bLast = getLastVisitAtTs(b.lastVisitAt);
|
||||||
|
if (visitTab.value === "latest") {
|
||||||
|
if (bLast !== aLast) {
|
||||||
|
return bLast - aLast;
|
||||||
|
}
|
||||||
|
return b.pv - a.pv;
|
||||||
|
}
|
||||||
|
if (b.pv !== a.pv) {
|
||||||
|
return b.pv - a.pv;
|
||||||
|
}
|
||||||
|
return bLast - aLast;
|
||||||
|
});
|
||||||
|
return list;
|
||||||
|
});
|
||||||
const visitTab = ref<"pv" | "latest">("pv");
|
const visitTab = ref<"pv" | "latest">("pv");
|
||||||
const visitTabStorageKey = "cwd-analytics-visit-tab";
|
const visitTabStorageKey = "cwd-analytics-visit-tab";
|
||||||
const chartRangeStorageKey = "cwd-analytics-visit-chart-range";
|
const chartRangeStorageKey = "cwd-analytics-visit-chart-range";
|
||||||
@@ -332,6 +350,16 @@ function loadChartRangeFromStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLastVisitAtTs(value: number | null | undefined): number {
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function saveChartRangeToStorage(value: "7" | "30") {
|
function saveChartRangeToStorage(value: "7" | "30") {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return;
|
return;
|
||||||
@@ -367,7 +395,7 @@ async function loadData() {
|
|||||||
const likeItemsRaw = Array.isArray(likeStatsRes.items) ? likeStatsRes.items : [];
|
const likeItemsRaw = Array.isArray(likeStatsRes.items) ? likeStatsRes.items : [];
|
||||||
likeStatsItems.value = filterLikeStatsByDomain(likeItemsRaw, domain);
|
likeStatsItems.value = filterLikeStatsByDomain(likeItemsRaw, domain);
|
||||||
const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : [];
|
const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : [];
|
||||||
items.value = pageItems;
|
rawItems.value = pageItems;
|
||||||
last30Days.value = Array.isArray(overviewRes.last30Days)
|
last30Days.value = Array.isArray(overviewRes.last30Days)
|
||||||
? overviewRes.last30Days
|
? overviewRes.last30Days
|
||||||
: [];
|
: [];
|
||||||
@@ -385,31 +413,12 @@ async function loadData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadVisitPagesOnly() {
|
|
||||||
listLoading.value = true;
|
|
||||||
error.value = "";
|
|
||||||
try {
|
|
||||||
const domain = domainFilter.value || undefined;
|
|
||||||
const order = getVisitOrderParam();
|
|
||||||
const pagesRes = await fetchVisitPages(domain, order);
|
|
||||||
const pageItems = Array.isArray(pagesRes.items) ? pagesRes.items : [];
|
|
||||||
items.value = pageItems;
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = e.message || "加载访问统计数据失败";
|
|
||||||
error.value = msg;
|
|
||||||
showToast(msg, "error");
|
|
||||||
} finally {
|
|
||||||
listLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeVisitTab(tab: "pv" | "latest") {
|
function changeVisitTab(tab: "pv" | "latest") {
|
||||||
if (visitTab.value === tab) {
|
if (visitTab.value === tab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitTab.value = tab;
|
visitTab.value = tab;
|
||||||
saveVisitTabToStorage(tab);
|
saveVisitTabToStorage(tab);
|
||||||
loadVisitPagesOnly();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChart() {
|
function renderChart() {
|
||||||
|
|||||||
@@ -31,17 +31,31 @@
|
|||||||
<a class="layout-button" href="https://github.com/anghunk/cwd" target="_blank">
|
<a class="layout-button" href="https://github.com/anghunk/cwd" target="_blank">
|
||||||
Github
|
Github
|
||||||
</a>
|
</a>
|
||||||
<button class="layout-button" @click="cycleTheme" :title="themeTitle" type="button">
|
<button
|
||||||
<svg v-if="theme === 'light'" viewBox="0 0 24 24" width="16" height="16">
|
class="layout-button"
|
||||||
<path d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z" fill="currentColor"/>
|
@click="cycleTheme"
|
||||||
</svg>
|
:title="themeTitle"
|
||||||
<svg v-else-if="theme === 'dark'" viewBox="0 0 24 24" width="16" height="16">
|
type="button"
|
||||||
<path d="M20 12.986c-.52.095-1.056.15-1.6.15-5.238 0-9.486-4.248-9.486-9.486 0-.544.055-1.08.15-1.6-5.275.986-9.264 5.615-9.264 11.123 0 6.255 5.07 11.325 11.325 11.325 5.508 0 10.137-3.989 11.123-9.264a9.66 9.66 0 0 1-2.248.752z" fill="currentColor"/>
|
>
|
||||||
</svg>
|
<svg v-if="theme === 'light'" viewBox="0 0 24 24" width="16" height="16">
|
||||||
<svg v-else viewBox="0 0 24 24" width="16" height="16">
|
<path
|
||||||
<path d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6zM6 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6zm-3 14a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-1z" fill="currentColor"/>
|
d="M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zM11 1h2v3h-2V1zm0 19h2v3h-2v-3zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05 3.515 4.93zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414-2.121-2.121zm2.121-14.85l1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414 2.121-2.121zM23 11v2h-3v-2h3zM4 11v2H1v-2h3z"
|
||||||
</svg>
|
fill="currentColor"
|
||||||
</button>
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else-if="theme === 'dark'" viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path
|
||||||
|
d="M20 12.986c-.52.095-1.056.15-1.6.15-5.238 0-9.486-4.248-9.486-9.486 0-.544.055-1.08.15-1.6-5.275.986-9.264 5.615-9.264 11.123 0 6.255 5.07 11.325 11.325 11.325 5.508 0 10.137-3.989 11.123-9.264a9.66 9.66 0 0 1-2.248.752z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else viewBox="0 0 24 24" width="16" height="16">
|
||||||
|
<path
|
||||||
|
d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6zM6 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H6zm-3 14a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-1z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button class="layout-button" @click="handleLogout">退出</button>
|
<button class="layout-button" @click="handleLogout">退出</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -150,15 +164,15 @@ const isMobileSiderOpen = ref(false);
|
|||||||
const isActionsOpen = ref(false);
|
const isActionsOpen = ref(false);
|
||||||
|
|
||||||
const themeTitle = computed(() => {
|
const themeTitle = computed(() => {
|
||||||
if (theme.value === 'light') return '明亮模式';
|
if (theme.value === "light") return "明亮模式";
|
||||||
if (theme.value === 'dark') return '暗黑模式';
|
if (theme.value === "dark") return "暗黑模式";
|
||||||
return '跟随系统';
|
return "跟随系统";
|
||||||
});
|
});
|
||||||
|
|
||||||
function cycleTheme() {
|
function cycleTheme() {
|
||||||
if (theme.value === 'system') setTheme('light');
|
if (theme.value === "system") setTheme("light");
|
||||||
else if (theme.value === 'light') setTheme('dark');
|
else if (theme.value === "light") setTheme("dark");
|
||||||
else setTheme('system');
|
else setTheme("system");
|
||||||
}
|
}
|
||||||
|
|
||||||
const storedDomain =
|
const storedDomain =
|
||||||
@@ -267,6 +281,19 @@ function handleLogoutFromActions() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-body);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--text-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.1s linear;
|
||||||
|
}
|
||||||
.layout {
|
.layout {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user