From 7b0efe564753b149c9ccb2bdf75ccb2d510265c1 Mon Sep 17 00:00:00 2001 From: "tangly1024.com" Date: Tue, 19 Nov 2024 12:53:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Edashboard=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- blog.config.js | 9 ++-- components/ui/dashboard/DashboardBody.js | 41 ++++++++++++++++ components/ui/dashboard/DashboardHeader.js | 41 ++++++++++++++++ components/ui/dashboard/DashboardMenuList.js | 34 ++++++++++++++ lib/utils/formatDate.js | 29 +++++++++--- middleware.ts | 42 ++++++++++------- pages/auth/index.js | 1 + pages/dashboard/{index.js => [[...index]].js} | 47 +++++-------------- themes/starter/components/Header.js | 11 +++-- themes/starter/index.js | 35 ++++++++++++++ themes/theme.js | 47 +++++++------------ 11 files changed, 242 insertions(+), 95 deletions(-) create mode 100644 components/ui/dashboard/DashboardBody.js create mode 100644 components/ui/dashboard/DashboardHeader.js create mode 100644 components/ui/dashboard/DashboardMenuList.js rename pages/dashboard/{index.js => [[...index]].js} (60%) diff --git a/blog.config.js b/blog.config.js index ed0721cc..3b827a95 100644 --- a/blog.config.js +++ b/blog.config.js @@ -127,7 +127,8 @@ const BLOG = { '/[prefix]/[slug]/[...suffix]': 'LayoutSlug', '/auth/result': 'LayoutAuth', '/sign-in/[[...index]]': 'LayoutSignIn', - '/sign-up/[[...index]]': 'LayoutSignUp' + '/sign-up/[[...index]]': 'LayoutSignUp', + '/dashboard/[[...index]]': 'LayoutDashboard' }, CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许,如果设置为false、则全栈禁止复制内容。 @@ -252,7 +253,7 @@ const BLOG = { ], // 鼠标跟随特效 - MOUSE_FOLLOW: process.env.NEXT_PUBLIC_MOUSE_FOLLOW || true, // 开关 + MOUSE_FOLLOW: process.env.NEXT_PUBLIC_MOUSE_FOLLOW || false, // 开关 // 这两个只有在鼠标跟随特效开启时才生效 // 鼠标类型 1:路劲散点 2:下降散点 3:上升散点 4:边缘向鼠标移动散点 5:跟踪转圈散点 6:路径线条 7:聚集散点 8:聚集网格 9:移动网格 10:上升粒子 11:转圈随机颜色粒子 12:圆锥放射跟随蓝色粒子 MOUSE_FOLLOW_EFFECT_TYPE: 11, // 1-12 @@ -551,7 +552,9 @@ const BLOG = { VERSION: (() => { try { // 优先使用环境变量,否则从package.json中获取版本号 - return process.env.NEXT_PUBLIC_VERSION || require('./package.json').version + return ( + process.env.NEXT_PUBLIC_VERSION || require('./package.json').version + ) } catch (error) { console.warn('Failed to load package.json version:', error) return '1.0.0' // 缺省版本号 diff --git a/components/ui/dashboard/DashboardBody.js b/components/ui/dashboard/DashboardBody.js new file mode 100644 index 00000000..39805b27 --- /dev/null +++ b/components/ui/dashboard/DashboardBody.js @@ -0,0 +1,41 @@ +'use client' +import { UserProfile } from '@clerk/nextjs' +import { useRouter } from 'next/router' +import DashboardMenuList from './DashboardMenuList' + +/** + * 仪表盘内容主体 + * 组件懒加载 + * @returns + */ +export default function DashboardBody() { + const asPath = useRouter()?.asPath + + return ( +
+
+ +
+
+ {asPath === '/dashboard' &&
控制台首页
} + {(asPath === '/dashboard/user-profile' || + asPath === '/dashboard/user-profile/security') && ( + + )} + {asPath === '/dashboard/membership' &&
会员
} + {asPath === '/dashboard/order' &&
订单
} + {asPath === '/dashboard/favorite' &&
收藏
} +
+
+ ) +} diff --git a/components/ui/dashboard/DashboardHeader.js b/components/ui/dashboard/DashboardHeader.js new file mode 100644 index 00000000..53ba3db2 --- /dev/null +++ b/components/ui/dashboard/DashboardHeader.js @@ -0,0 +1,41 @@ +import LazyImage from '@/components/LazyImage' +import { useGlobal } from '@/lib/global' +import formatDate from '@/lib/utils/formatDate' +import { SignOutButton } from '@clerk/nextjs' +/** + * 仪表盘页头 + * @returns + */ +export default function DashboardHeader() { + const { user } = useGlobal() + + return ( +
+ {/* 头像昵称 */} +
+ +
+
{user?.fullName}
+
+ {user?.username} + {formatDate(user?.createdAt)} +
+
+
+ {/* 登出按钮 */} +
+ + + +
+
+ ) +} diff --git a/components/ui/dashboard/DashboardMenuList.js b/components/ui/dashboard/DashboardMenuList.js new file mode 100644 index 00000000..0a3e6bfe --- /dev/null +++ b/components/ui/dashboard/DashboardMenuList.js @@ -0,0 +1,34 @@ +import Link from 'next/link' + +/** + * 仪表盘菜单 + * @returns + */ +export default function DashboardMenuList() { + const dashBoardMenus = [ + { title: '控制台', href: '/dashboard' }, + { title: '基础资料', href: '/dashboard/user-profile' }, + { title: '我的会员', href: '/dashboard/membership' }, + { title: '我的订单', href: '/dashboard/order' }, + { title: '我的收藏', href: '/dashboard/favorite' } + ] + return ( + + ) +} diff --git a/lib/utils/formatDate.js b/lib/utils/formatDate.js index 29190e3b..c40a5135 100644 --- a/lib/utils/formatDate.js +++ b/lib/utils/formatDate.js @@ -1,22 +1,31 @@ +import BLOG from '@/blog.config' + /** * 格式化日期 * @param date * @param local * @returns {string} */ -export default function formatDate (date, local) { +export default function formatDate(date, local = BLOG.LANG) { if (!date || !local) return date || '' const d = new Date(date) const options = { year: 'numeric', month: 'short', day: 'numeric' } const res = d.toLocaleDateString(local, options) // 如果格式是中文日期,则转为横杆 - const format = local.slice(0, 2).toLowerCase() === 'zh' - ? res.replace('年', '-').replace('月', '-').replace('日', '') - : res + const format = + local.slice(0, 2).toLowerCase() === 'zh' + ? res.replace('年', '-').replace('月', '-').replace('日', '') + : res return format } -export function formatDateFmt (timestamp, fmt) { +/** + * 时间戳格式化 + * @param {*} timestamp + * @param {*} fmt + * @returns + */ +export function formatDateFmt(timestamp, fmt) { const date = new Date(timestamp) const o = { 'M+': date.getMonth() + 1, // 月份 @@ -28,11 +37,17 @@ export function formatDateFmt (timestamp, fmt) { S: date.getMilliseconds() // 毫秒 } if (/(y+)/.test(fmt)) { - fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length)) + fmt = fmt.replace( + RegExp.$1, + (date.getFullYear() + '').substr(4 - RegExp.$1.length) + ) } for (const k in o) { if (new RegExp('(' + k + ')').test(fmt)) { - fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length))) + fmt = fmt.replace( + RegExp.$1, + RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length) + ) } } return fmt.trim() diff --git a/middleware.ts b/middleware.ts index 0ae00a72..dc623d08 100644 --- a/middleware.ts +++ b/middleware.ts @@ -12,7 +12,9 @@ export const config = { // 限制登录访问的路由 const isTenantRoute = createRouteMatcher([ '/user/organization-selector(.*)', - '/user/orgid/(.*)' + '/user/orgid/(.*)', + '/dashboard', + '/dashboard/(.*)' ]) // 限制权限访问的路由 @@ -32,27 +34,35 @@ const noAuthMiddleware = async (req: any, ev: any) => { // 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求 return NextResponse.next() } - /** * 鉴权中间件 */ const authMiddleware = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY - ? clerkMiddleware( - (auth, req) => { - // 限制管理员路由访问权限 - if (isTenantAdminRoute(req)) { - auth().protect(has => { - return ( - has({ permission: 'org:sys_memberships:manage' }) || - has({ permission: 'org:sys_domains_manage' }) - ) - }) + ? clerkMiddleware(async (auth, req) => { + const { userId } = auth() + // 处理 /dashboard 路由的登录保护 + if (isTenantRoute(req)) { + if (!userId) { + // 用户未登录,重定向到 /sign-in + const url = new URL('/sign-in', req.url) + url.searchParams.set('redirectTo', req.url) // 保存重定向目标 + return NextResponse.redirect(url) } - // 限制组织路由访问权限 - if (isTenantRoute(req)) auth().protect() } - // { debug: process.env.npm_lifecycle_event === 'dev' } // 开发调试模式打印日志 - ) + + // 处理管理员相关权限保护 + if (isTenantAdminRoute(req)) { + auth().protect(has => { + return ( + has({ permission: 'org:sys_memberships:manage' }) || + has({ permission: 'org:sys_domains_manage' }) + ) + }) + } + + // 默认继续处理请求 + return NextResponse.next() + }) : noAuthMiddleware export default authMiddleware diff --git a/pages/auth/index.js b/pages/auth/index.js index 26aea479..cce744d9 100644 --- a/pages/auth/index.js +++ b/pages/auth/index.js @@ -97,4 +97,5 @@ const fetchToken = async code => { console.error('Error fetching token', error) } } + export default UI diff --git a/pages/dashboard/index.js b/pages/dashboard/[[...index]].js similarity index 60% rename from pages/dashboard/index.js rename to pages/dashboard/[[...index]].js index 824ad6f3..5dfca79f 100644 --- a/pages/dashboard/index.js +++ b/pages/dashboard/[[...index]].js @@ -1,9 +1,6 @@ import BLOG from '@/blog.config' import { siteConfig } from '@/lib/config' import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData' -import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents' -import { uploadDataToAlgolia } from '@/lib/plugins/algolia' -import { getRecommendPost } from '@/lib/utils/post' import { DynamicLayout } from '@/themes/theme' import { useRouter } from 'next/router' @@ -63,38 +60,6 @@ export async function getStaticProps({ locale }) { props.post.blockMap = await getPostBlocks(props.post.id, from) } - // 目录默认加载 - if (props.post?.blockMap?.block) { - props.post.content = Object.keys(props.post.blockMap.block).filter( - key => props.post.blockMap.block[key]?.value?.parent_id === props.post.id - ) - props.post.toc = getPageTableOfContents(props.post, props.post.blockMap) - } - - // 生成全文索引 && process.env.npm_lifecycle_event === 'build' && JSON.parse(BLOG.ALGOLIA_RECREATE_DATA) - if (BLOG.ALGOLIA_APP_ID) { - uploadDataToAlgolia(props?.post) - } - - // 推荐关联文章处理 - const allPosts = props.allPages?.filter( - page => page.type === 'Post' && page.status === 'Published' - ) - if (allPosts && allPosts.length > 0) { - const index = allPosts.indexOf(props.post) - props.prev = allPosts.slice(index - 1, index)[0] ?? allPosts.slice(-1)[0] - props.next = allPosts.slice(index + 1, index + 2)[0] ?? allPosts[0] - props.recommendPosts = getRecommendPost( - props.post, - allPosts, - siteConfig('POST_RECOMMEND_COUNT') - ) - } else { - props.prev = null - props.next = null - props.recommendPosts = [] - } - delete props.allPages return { props, @@ -108,4 +73,16 @@ export async function getStaticProps({ locale }) { } } +export const getStaticPaths = async () => { + return { + // 定义需要预渲染的路径 + paths: [ + { params: { index: [''] } }, // 对应 /dashboard + { params: { index: ['membership'] } }, // 对应 /dashboard/membership + { params: { index: ['order'] } }, // 对应 /dashboard/order + { params: { index: ['favorite'] } } // 对应 /dashboard/favorite + ], + fallback: 'blocking' // 或者 true,阻塞式渲染 + } +} export default Dashboard diff --git a/themes/starter/components/Header.js b/themes/starter/components/Header.js index 5e3a0ff4..8a0e898b 100644 --- a/themes/starter/components/Header.js +++ b/themes/starter/components/Header.js @@ -86,12 +86,13 @@ export const Header = props => { - - Dashboard - + + )} diff --git a/themes/starter/index.js b/themes/starter/index.js index 3ae161f6..0ba751d4 100644 --- a/themes/starter/index.js +++ b/themes/starter/index.js @@ -27,6 +27,8 @@ import { Style } from './style' import Comment from '@/components/Comment' import replaceSearchResult from '@/components/Mark' import ShareBar from '@/components/ShareBar' +import DashboardBody from '@/components/ui/dashboard/DashboardBody' +import DashboardHeader from '@/components/ui/dashboard/DashboardHeader' import { useGlobal } from '@/lib/global' import { loadWowJS } from '@/lib/plugins/wow' import { SignIn, SignUp } from '@clerk/nextjs' @@ -176,7 +178,39 @@ const LayoutSlug = props => { ) } +/** + * 仪表盘 + * @param {*} props + * @returns + */ +const LayoutDashboard = props => { + const { post } = props + return ( + <> +
+
+
+ {post && ( +
+ +
+ )} +
+
+
+ {/* 仪表盘 */} + + + + ) +} + +/** + * 搜索 + * @param {*} props + * @returns + */ const LayoutSearch = props => { const { keyword } = props const router = useRouter() @@ -491,6 +525,7 @@ export { LayoutArchive, LayoutBase, LayoutCategoryIndex, + LayoutDashboard, LayoutIndex, LayoutPostList, LayoutSearch, diff --git a/themes/theme.js b/themes/theme.js index 5ec1bf76..884a26f3 100644 --- a/themes/theme.js +++ b/themes/theme.js @@ -50,46 +50,34 @@ export const DynamicLayout = props => { /** * 加载主题文件 - * 如果是 * @param {*} router + * @param {*} theme * @returns */ export const getLayoutByTheme = ({ router, theme }) => { const themeQuery = getQueryParam(router.asPath, 'theme') || theme - if (themeQuery !== BLOG.THEME) { - return dynamic( - () => - import(`@/themes/${themeQuery}`).then(m => { - setTimeout(() => { - checkThemeDOM() - }, 500) + const layoutName = getLayoutNameByPath(router.pathname, router.asPath) + const isDefaultTheme = !themeQuery || themeQuery === BLOG.THEME - const components = - m[getLayoutNameByPath(router.pathname, router.asPath)] - if (components) { - return components - } else { - return m.LayoutSlug - } - }), + const loadThemeComponents = componentsSource => { + const components = + componentsSource[layoutName] || componentsSource.LayoutSlug + setTimeout(fixThemeDOM, isDefaultTheme ? 100 : 500) // 根据主题选择延迟时间 + return components + } + + if (isDefaultTheme) { + return loadThemeComponents(ThemeComponents) + } else { + return dynamic( + () => import(`@/themes/${themeQuery}`).then(m => loadThemeComponents(m)), { ssr: true } ) - } else { - setTimeout(() => { - checkThemeDOM() - }, 100) - const components = - ThemeComponents[getLayoutNameByPath(router.pathname, router.asPath)] - if (components) { - return components - } else { - return ThemeComponents.LayoutSlug - } } } /** - * 根据路径 获取对应的layout + * 根据路径 获取对应的layout名称 * @param {*} path * @returns */ @@ -101,8 +89,9 @@ const getLayoutNameByPath = path => { /** * 切换主题时的特殊处理 + * 删除多余的元素 */ -const checkThemeDOM = () => { +const fixThemeDOM = () => { if (isBrowser) { const elements = document.querySelectorAll('[id^="theme-"]') if (elements?.length > 1) {