mirror of
https://github.com/34892002/edgeKey.git
synced 2026-06-22 01:12:51 +08:00
feat: ui优化、二级菜单
This commit is contained in:
@@ -32,9 +32,10 @@
|
||||
<div class="breadcrumbs text-sm text-base-content/60 mt-0.5">
|
||||
<ul>
|
||||
<li><a href="/admin">Home</a></li>
|
||||
<li v-if="breadcrumbs?.length > 0"><a :href="breadcrumbs[0].href">{{ breadcrumbs[0].name }}</a>
|
||||
<li v-for="(crumb, index) in breadcrumbs" :key="index">
|
||||
<a v-if="crumb.href" :href="crumb.href">{{ crumb.name }}</a>
|
||||
<span v-else>{{ crumb.name }}</span>
|
||||
</li>
|
||||
<li v-if="breadcrumbs?.length > 1">{{ breadcrumbs[1].name }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,18 +96,30 @@
|
||||
<!-- Navigation -->
|
||||
<div class="p-4 flex-1 overflow-y-auto">
|
||||
<ul class="menu menu-md w-full gap-1 p-0">
|
||||
<li><a href="/admin" :class="{'active': currentPath === '/admin'}">仪表盘</a></li>
|
||||
<li><a href="/admin/categories" :class="{'active': currentPath?.startsWith('/admin/categories')}">分类管理</a></li>
|
||||
<li><a href="/admin/products" :class="{'active': currentPath?.startsWith('/admin/products')}">商品管理</a></li>
|
||||
<li><a href="/admin/cards" :class="{'active': currentPath?.startsWith('/admin/cards')}">卡密管理</a></li>
|
||||
<li><a href="/admin/orders" :class="{'active': currentPath?.startsWith('/admin/orders')}">订单管理</a></li>
|
||||
<li><a href="/admin/discount-codes" :class="{'active': currentPath?.startsWith('/admin/discount-codes')}">折扣码管理</a></li>
|
||||
<li><a href="/admin/payments" :class="{'active': currentPath?.startsWith('/admin/payments')}">支付配置</a></li>
|
||||
<li><a href="/admin/email" :class="{'active': currentPath?.startsWith('/admin/email')}">邮件管理</a></li>
|
||||
<li><a href="/admin/media" :class="{'active': currentPath?.startsWith('/admin/media')}">文件管理</a></li>
|
||||
<li><a href="/admin/settings" :class="{'active': currentPath?.startsWith('/admin/settings')}">站点设置</a></li>
|
||||
<!-- <li><a href="/admin/security" :class="{'active': currentPath?.startsWith('/admin/security')}">安全配置</a></li> -->
|
||||
<li><a href="/admin/profile" :class="{'active': currentPath?.startsWith('/admin/profile')}">个人资料</a></li>
|
||||
<li>
|
||||
<a href="/admin" :class="{'active': currentPath === '/admin'}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
仪表盘
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li v-for="group in menuGroups" :key="group.name">
|
||||
<details :open="isGroupOpen(group, currentPath)">
|
||||
<summary>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="group.icon" />
|
||||
</svg>
|
||||
{{ group.name }}
|
||||
</summary>
|
||||
<ul>
|
||||
<li v-for="item in group.items" :key="item.href">
|
||||
<a :href="item.href" :class="{'active': isItemActive(item, currentPath)}">{{ item.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -135,6 +148,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import AppButton from "../../components/AppButton.vue";
|
||||
import { usePageContext } from "vike-vue/usePageContext";
|
||||
import { menuGroups, getBreadcrumbs, isGroupOpen, isItemActive } from "./menu";
|
||||
|
||||
import logoUrl from "../../assets/logo.svg";
|
||||
|
||||
@@ -208,35 +222,7 @@ const isAdminUser = computed(() => pageContext.session?.user?.role === "admin");
|
||||
const needsLogin = computed(() => !isLoginPage.value && !isAdminUser.value);
|
||||
const siteLogo = computed(() => pageContext.site?.logo || logoUrl);
|
||||
|
||||
type Crumb = { name: string; href?: string };
|
||||
|
||||
const BREADCRUMB_ROUTES: { pattern: string; crumbs: Crumb[] }[] = [
|
||||
{ pattern: "/admin/products/new", crumbs: [{ name: "商品管理", href: "/admin/products" }, { name: "新建商品" }] },
|
||||
{ pattern: "/admin/products/:id/edit", crumbs: [{ name: "商品管理", href: "/admin/products" }, { name: "编辑商品" }] },
|
||||
{ pattern: "/admin/products", crumbs: [{ name: "商品管理" }] },
|
||||
{ pattern: "/admin/orders/:id", crumbs: [{ name: "订单管理", href: "/admin/orders" }, { name: "订单详情" }] },
|
||||
{ pattern: "/admin/orders", crumbs: [{ name: "订单管理" }] },
|
||||
{ pattern: "/admin/categories", crumbs: [{ name: "分类管理" }] },
|
||||
{ pattern: "/admin/cards", crumbs: [{ name: "卡密管理" }] },
|
||||
{ pattern: "/admin/discount-codes", crumbs: [{ name: "折扣码管理" }] },
|
||||
{ pattern: "/admin/payments", crumbs: [{ name: "支付配置" }] },
|
||||
{ pattern: "/admin/email", crumbs: [{ name: "邮件管理" }] },
|
||||
{ pattern: "/admin/media", crumbs: [{ name: "文件管理" }] },
|
||||
{ pattern: "/admin/settings", crumbs: [{ name: "站点设置" }] },
|
||||
{ pattern: "/admin/security", crumbs: [{ name: "安全配置" }] },
|
||||
{ pattern: "/admin/profile", crumbs: [{ name: "个人资料" }] },
|
||||
];
|
||||
|
||||
function matchRoute(pattern: string, path: string) {
|
||||
const re = new RegExp("^" + pattern.replace(/:[^/]+/g, "[^/]+") + "(/.*)?$");
|
||||
return re.test(path);
|
||||
}
|
||||
|
||||
const breadcrumbs = computed((): Crumb[] => {
|
||||
const path = currentPath.value ?? "";
|
||||
const route = BREADCRUMB_ROUTES.find(r => matchRoute(r.pattern, path));
|
||||
return route ? route.crumbs : [];
|
||||
});
|
||||
const breadcrumbs = computed(() => getBreadcrumbs(currentPath.value));
|
||||
|
||||
onMounted(() => {
|
||||
if (needsLogin.value) {
|
||||
|
||||
104
pages/admin/menu.ts
Normal file
104
pages/admin/menu.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 后台菜单和路由配置
|
||||
* 集中管理页面路径、菜单显示、面包屑导航
|
||||
*/
|
||||
|
||||
export interface MenuItem {
|
||||
name: string
|
||||
href: string
|
||||
}
|
||||
|
||||
export interface MenuGroup {
|
||||
icon: string // SVG path
|
||||
name: string
|
||||
items: MenuItem[]
|
||||
}
|
||||
|
||||
export interface Crumb {
|
||||
name: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
// 菜单组配置
|
||||
export const menuGroups: MenuGroup[] = [
|
||||
{
|
||||
icon: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z",
|
||||
name: "商品与订单",
|
||||
items: [
|
||||
{ name: "分类管理", href: "/admin/categories" },
|
||||
{ name: "商品管理", href: "/admin/products" },
|
||||
{ name: "卡密管理", href: "/admin/cards" },
|
||||
{ name: "订单管理", href: "/admin/orders" },
|
||||
{ name: "折扣码管理", href: "/admin/discount-codes" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065zM15 12a3 3 0 11-6 0 3 3 0 016 0z",
|
||||
name: "系统配置",
|
||||
items: [
|
||||
{ name: "支付配置", href: "/admin/payments" },
|
||||
{ name: "邮件管理", href: "/admin/email" },
|
||||
{ name: "站点设置", href: "/admin/settings" },
|
||||
{ name: "安全设置", href: "/admin/security" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z",
|
||||
name: "内容管理",
|
||||
items: [
|
||||
{ name: "文件管理", href: "/admin/media" },
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: "M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z",
|
||||
name: "账户",
|
||||
items: [
|
||||
{ name: "个人资料", href: "/admin/profile" },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// 面包屑配置(特殊路由)
|
||||
export const breadcrumbRoutes: { pattern: string; crumbs: Crumb[] }[] = [
|
||||
{ pattern: "/admin/products/new", crumbs: [{ name: "商品与订单", href: "/admin/products" }, { name: "新建商品" }] },
|
||||
{ pattern: "/admin/products/:id/edit", crumbs: [{ name: "商品与订单", href: "/admin/products" }, { name: "编辑商品" }] },
|
||||
{ pattern: "/admin/orders/:id", crumbs: [{ name: "商品与订单", href: "/admin/orders" }, { name: "订单详情" }] },
|
||||
]
|
||||
|
||||
// 根据路径获取面包屑
|
||||
export function getBreadcrumbs(path: string): Crumb[] {
|
||||
// 先匹配特殊路由
|
||||
for (const route of breadcrumbRoutes) {
|
||||
if (matchRoute(route.pattern, path)) {
|
||||
return route.crumbs
|
||||
}
|
||||
}
|
||||
|
||||
// 再匹配菜单项
|
||||
for (const group of menuGroups) {
|
||||
for (const item of group.items) {
|
||||
if (path === item.href || path.startsWith(item.href + "/")) {
|
||||
return [{ name: group.name }, { name: item.name }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
// 判断菜单组是否应该展开
|
||||
export function isGroupOpen(group: MenuGroup, currentPath: string): boolean {
|
||||
return group.items.some(item =>
|
||||
currentPath === item.href || currentPath.startsWith(item.href + "/")
|
||||
)
|
||||
}
|
||||
|
||||
// 判断菜单项是否激活
|
||||
export function isItemActive(item: MenuItem, currentPath: string): boolean {
|
||||
return currentPath === item.href || currentPath.startsWith(item.href + "/")
|
||||
}
|
||||
|
||||
function matchRoute(pattern: string, path: string): boolean {
|
||||
const re = new RegExp("^" + pattern.replace(/:[^/]+/g, "[^/]+") + "(/.*)?$")
|
||||
return re.test(path)
|
||||
}
|
||||
@@ -1,42 +1,102 @@
|
||||
<template>
|
||||
<section class="grid gap-6 lg:grid-cols-2">
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">认证密钥</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
生产环境必须配置 `AUTH_SECRET` 或 `NEXTAUTH_SECRET`。本地开发可以通过 `.env` 注入,Cloudflare 生产环境建议使用 `wrangler secret put`。
|
||||
<section class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body space-y-6">
|
||||
<div>
|
||||
<h1 class="card-title text-2xl">Cloudflare Turnstile 登录验证</h1>
|
||||
<p class="text-sm text-base-content/70 mt-2">
|
||||
在管理员登录页接入 Cloudflare Turnstile 小组件,用于拦截自动化爆破登录。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">登录限流</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
当前管理员登录已启用基础请求限流。你也应在 Cloudflare 后台为 `/api/auth/callback/credentials` 配置 Rate Limiting / WAF 规则。
|
||||
</p>
|
||||
<!-- 当前状态 -->
|
||||
<div class="alert" :class="turnstileEnabled ? 'alert-success' : 'alert-warning'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||
<span v-if="turnstileEnabled">Turnstile 已启用 - 登录页已显示人机验证</span>
|
||||
<span v-else>Turnstile 未启用 - 请按下方步骤配置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">密码安全</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
请在首次初始化完成后,立即前往"个人资料"修改默认管理员密码,并定期轮换高权限账号口令。
|
||||
</p>
|
||||
<AppButton href="/admin/profile" variant="primary" size="sm">前往个人资料</AppButton>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 配置步骤 -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-lg font-semibold">配置步骤</h2>
|
||||
|
||||
<!-- 步骤 1 -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-content flex items-center justify-center font-bold">1</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<h3 class="font-medium">创建 Turnstile 站点</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
前往
|
||||
<a href="https://dash.cloudflare.com/turnstile" target="_blank" class="link link-primary">Cloudflare Dashboard → Turnstile</a>
|
||||
创建站点,获取以下两个密钥:
|
||||
</p>
|
||||
<div class="mockup-code text-sm">
|
||||
<pre><code>TURNSTILE_SITE_KEY = 前端小组件站点 Key</code></pre>
|
||||
<pre><code>TURNSTILE_SECRET_KEY = 服务端校验 Secret Key</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 2 -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-content flex items-center justify-center font-bold">2</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<h3 class="font-medium">配置环境变量</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
使用 wrangler 命令将密钥配置到 Cloudflare Workers:
|
||||
</p>
|
||||
<div class="mockup-code text-sm">
|
||||
<pre data-prefix="$"><code>wrangler secret put TURNSTILE_SITE_KEY</code></pre>
|
||||
<pre data-prefix="$"><code>wrangler secret put TURNSTILE_SECRET_KEY</code></pre>
|
||||
</div>
|
||||
<div class="alert alert-info text-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>本地开发时,可在 <code>.env</code> 文件中直接配置这两个变量。</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤 3 -->
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-primary text-primary-content flex items-center justify-center font-bold">3</div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<h3 class="font-medium">完成</h3>
|
||||
<p class="text-sm text-base-content/70">
|
||||
配置完成后,刷新此页面查看状态变化。后台登录页会自动显示 Turnstile 小组件,并在服务端强制校验。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">支付安全</h2>
|
||||
<p class="text-sm text-base-content/70">
|
||||
支付日志会保留业务信息用于排查,但会对 `md5`、`key`、`secret`、`signature` 等敏感字段做脱敏处理。启用支付前,请先在"站点设置"配置网站地址。
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<AppButton href="/admin/settings" variant="outline" size="sm">站点设置</AppButton>
|
||||
<AppButton href="/admin/payments" variant="primary" size="sm">支付配置</AppButton>
|
||||
<div class="divider"></div>
|
||||
|
||||
<!-- 状态说明 -->
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-lg font-semibold">状态说明</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>配置状态</th>
|
||||
<th>行为</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr :class="{'bg-base-200': !turnstileEnabled}">
|
||||
<td><span class="badge" :class="turnstileEnabled ? 'badge-ghost' : 'badge-warning'">两个变量都未配置</span></td>
|
||||
<td>Turnstile 默认关闭,不影响现有登录流程</td>
|
||||
</tr>
|
||||
<tr :class="{'bg-base-200': turnstileEnabled}">
|
||||
<td><span class="badge" :class="turnstileEnabled ? 'badge-success' : 'badge-ghost'">两个变量都已配置</span></td>
|
||||
<td>登录页自动显示 Turnstile 小组件,服务端强制校验</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="badge badge-ghost">只配置了一个变量</span></td>
|
||||
<td>系统自动视为未启用,避免半配置状态</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,5 +104,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AppButton from "../../../components/AppButton.vue";
|
||||
</script>
|
||||
import { useData } from "vike-vue/useData";
|
||||
import type { Data } from "./+data";
|
||||
|
||||
const { turnstileEnabled } = useData<Data>();
|
||||
</script>
|
||||
|
||||
11
pages/admin/security/+data.ts
Normal file
11
pages/admin/security/+data.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { PrismaClient } from "../../../generated/prisma/client";
|
||||
import { getTurnstileConfig } from "../../../server/turnstile";
|
||||
|
||||
export type Data = ReturnType<typeof data>;
|
||||
|
||||
export async function data(pageContext: { prisma: PrismaClient }) {
|
||||
const config = getTurnstileConfig();
|
||||
return {
|
||||
turnstileEnabled: config.enabled,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user