Merge pull request #3000 from tangly1024/release/4.7.10

Release/4.7.10
This commit is contained in:
tangly1024
2024-11-20 10:11:44 +08:00
committed by GitHub
44 changed files with 1484 additions and 265 deletions

View File

@@ -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' // 缺省版本号

View File

@@ -0,0 +1,51 @@
'use client'
import { UserProfile } from '@clerk/nextjs'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
const DashboardMenuList = dynamic(() => import('./DashboardMenuList'))
const DashboardItemMembership = dynamic(
() => import('./DashboardItemMembership')
)
const DashboardItemBalance = dynamic(() => import('./DashboardItemBalance'))
const DashboardItemHome = dynamic(() => import('./DashboardItemHome'))
const DashboardItemOrder = dynamic(() => import('./DashboardItemOrder'))
const DashboardItemAffliate = dynamic(() => import('./DashboardItemAffliate'))
/**
* 仪表盘内容主体
* 组件懒加载
* @returns
*/
export default function DashboardBody() {
const { asPath } = useRouter()
// 提取不包含查询参数的路径部分
const basePath = asPath.split('?')[0]
return (
<div className='flex flex-col md:flex-row w-full container gap-x-4 min-h-96 mx-auto mb-12 justify-center'>
<div className='side-tabs w-full md:w-72'>
<DashboardMenuList />
</div>
<div className='main-content-wrapper w-full'>
{basePath === '/dashboard' && <DashboardItemHome />}
{(basePath === '/dashboard/user-profile' ||
basePath === '/dashboard/user-profile/security') && (
<UserProfile
appearance={{
elements: {
cardBox: 'w-full',
rootBox: 'w-full'
}
}}
className='bg-blue-300'
routing='path'
path='/dashboard/user-profile'
/>
)}
{basePath === '/dashboard/balance' && <DashboardItemBalance />}
{basePath === '/dashboard/membership' && <DashboardItemMembership />}
{basePath === '/dashboard/order' && <DashboardItemOrder />}
{basePath === '/dashboard/affiliate' && <DashboardItemAffliate />}
</div>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import { siteConfig } from '@/lib/config'
import Link from 'next/link'
import { useRouter } from 'next/router'
/**
* 跳转仪表盘的按钮
* @returns
*/
export default function DashboardButton() {
const { asPath } = useRouter()
const enableDashboardButton = siteConfig('ENABLE_DASHBOARD_BUTTON', false)
if (!enableDashboardButton) {
return null
}
if (asPath?.indexOf('/dashboard') === 0) {
return null
}
return (
<button
type='button'
className='text-white bg-gray-800 hover:bg-gray-900 hover:ring-4 hover:ring-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2 me-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700'>
<Link href='/dashboard'>仪表盘</Link>
</button>
)
}

View File

@@ -0,0 +1,53 @@
import LazyImage from '@/components/LazyImage'
import { useGlobal } from '@/lib/global'
import formatDate from '@/lib/utils/formatDate'
import { SignOutButton } from '@clerk/nextjs'
import Link from 'next/link'
/**
* 仪表盘页头
* @returns
*/
export default function DashboardHeader() {
const { user } = useGlobal()
return (
<>
<div className='flex w-full container mx-auto mt-12 mb-12 justify-ends'>
{/* 头像昵称 */}
<div className='flex items-center gap-4 w-full'>
<LazyImage
className='w-10 h-10 rounded-full'
src={user?.imageUrl}
alt={user?.fullName}
/>
<div class='font-medium dark:text-white'>
<div className='flex items-center gap-x-2'>
<span>{user?.fullName}</span>
<Link href='/dashboard/membership'>
<span class='bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded dark:bg-gray-700 dark:text-gray-300'>
普通用户
</span>
</Link>
</div>
<div className='text-sm text-gray-500 gap-x-2 flex dark:text-gray-400'>
<span>{user?.username}</span>
<span>{formatDate(user?.createdAt)}</span>
</div>
</div>
</div>
{/* 登出按钮 */}
<div className='flex items-center'>
<SignOutButton redirectUrl='/'>
<button className='text-white bg-gray-800 hover:bg-gray-900 hover:ring-4 hover:ring-gray-300 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700'>
<span className='text-nowrap'>
<i className='fas fa-right-from-bracket' /> Sign Out
</span>
</button>
</SignOutButton>
</div>
</div>
</>
)
}

View File

@@ -0,0 +1,187 @@
import Link from 'next/link'
/**
* 联盟行销
* @returns
*/
export default function DashboardItemAffliate() {
const cards = [
{
title: '¥0.00',
desc: '累计佣金',
className: 'bg-blue-600 hover:bg-blue-700 text-white'
},
{
title: '¥0.00',
desc: '已提现',
className: 'bg-cyan-600 hover:bg-cyan-700 text-white'
},
{
title: '¥0.00',
desc: '提现中',
className: 'bg-pink-600 hover:bg-pink-700 text-white'
},
{
title: '¥0.00',
desc: '可提现',
className: 'bg-emerald-600 hover:bg-emerald-700 text-white'
}
]
return (
<div className='bg-white rounded-lg shadow-lg p-6 border'>
<div className='grid grid-cols-4 gap-4'>
{cards?.map((card, index) => (
<div
key={index}
className={`block max-w-sm p-6 text-center border cursor-pointer rounded-lg shadow ${card.className}`}>
<h5 className='mb-2 text-2xl font-bold tracking-tight'>
{card.title}
</h5>
<p className='font-normal'>{card.desc}</p>
</div>
))}
</div>
<form className='mt-6'>
<div className='grid gap-6 mb-6 md:grid-cols-2'>
<div>
<label
for='last_name'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
推广总数
</label>
<input
disabled
type='text'
id='last_name'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='123'
required
/>
</div>
<div>
<label
for='company'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
推广链接
</label>
<input
disabled
type='text'
id='company'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='https://tangly1024.com'
required
/>
</div>
<div>
<label
for='website'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
推广佣金提成
</label>
<input
disabled
type='url'
id='website'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='5%'
required
/>
</div>
</div>
<hr className='my-6' />
<div className='grid gap-6 mb-6 md:grid-cols-2'>
<div>
<label
for='first_name'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
提现账号
</label>
<input
type='text'
id='first_name'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='John'
required
/>
</div>
<div>
<label
for='visitors'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
提现金额
</label>
<input
type='number'
id='visitors'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder=''
required
/>
</div>
</div>
<div className='flex items-start mb-6'>
<div className='flex items-center h-5'>
<input
id='remember'
type='checkbox'
value=''
className='w-4 h-4 border border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-blue-300 dark:bg-gray-700 dark:border-gray-600 dark:focus:ring-blue-600 dark:ring-offset-gray-800'
required
/>
</div>
<label
for='remember'
className='ms-2 text-sm font-medium text-gray-900 dark:text-gray-300'>
我以阅读并同意{' '}
<Link
href='/terms-of-use'
className='text-blue-600 hover:underline dark:text-blue-500'>
服务协议
</Link>
.
</label>
</div>
<div className='flex gap-x-2'>
<button
type='submit'
className='text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800'>
提现RMB
</button>
<button
type='submit'
className='text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800'>
提现到余额
</button>
</div>
<ul className='text-gray-600 list-disc pl-6'>
<li>推广说明</li>
<li className='font-bold'>这只是一个演示页面不存在真实功能</li>
<li>
如需提现请联系网站管理员发送您的账号信息和收款码进行人工提现
</li>
<li>
如果用户是通过您的推广链接购买的资源或者开通会员则按照推广佣金比列奖励到您的佣金中
</li>
<li>
如果用户是通过您的链接新注册的用户推荐人是您该用户购买资都会给你佣金
</li>
<li>
如果用户是你的下级用户使用其他推荐人链接购买以上下级关系为准优先给注册推荐人而不是推荐链接
</li>
<li>推广奖励金额保留一位小数点四舍五入0.1之类的奖励金额不计算</li>
<li>
前台无法查看推广订单详情如需查看详情可联系管理员截图查看详细记录和时间
</li>
</ul>
</form>
</div>
)
}

View File

@@ -0,0 +1,137 @@
import { useEffect, useState } from 'react'
/**
* 余额
* @returns
*/
export default function DashboardItemBalance() {
const [selectedCard, setSelectedCard] = useState(null)
const [amount, setAmount] = useState(0)
const cards = [
{
title: '0 积分',
desc: '当前余额',
className: 'bg-blue-600 hover:bg-blue-700 text-white'
},
{
title: '0 积分',
desc: '累计消费',
className: 'bg-cyan-600 hover:bg-cyan-700 text-white'
},
{
title: '0',
desc: '累计佣金',
className: 'bg-pink-600 hover:bg-pink-700 text-white'
}
]
const cardData = [
{ points: '1积分', price: '¥1' },
{ points: '10积分', price: '¥10' },
{ points: '50积分', price: '¥50' },
{ points: '100积分', price: '¥100' },
{ points: '300积分', price: '¥300' },
{ points: '500积分', price: '¥500' }
]
const handleCardSelect = index => {
setSelectedCard(index)
}
const handleAmountChange = e => {
const value = e.target.value
setAmount(value)
}
useEffect(() => {
if (selectedCard !== null) {
// 如果用户选中了充值卡片,则自动更新支付金额
const selectedPrice = cardData[selectedCard]?.price
if (selectedPrice) {
setAmount(selectedPrice.replace('¥', ''))
}
}
}, [selectedCard])
return (
<div className='bg-white rounded-lg shadow-lg p-6 border'>
<div>
<h2 className='text-2xl font-bold mb-4'>余额充值中心</h2>
<hr className='my-2' />
</div>
{/* 余额卡片 */}
<div className='grid grid-cols-3 gap-4'>
{cards?.map((card, index) => (
<div
key={index}
className={`block max-w-sm p-6 text-center border cursor-pointer rounded-lg shadow ${card.className}`}
onClick={() => handleCardSelect(index)}>
<h5 className='mb-2 text-2xl font-bold tracking-tight'>
{card.title}
</h5>
<p className='font-normal'>{card.desc}</p>
</div>
))}
</div>
<form className='mt-6'>
<div className='py-2'>充值项目充值比例1=1积分</div>
{/* 充值选项 */}
<div className='grid gap-6 mb-6 grid-cols-4'>
{cardData?.map((item, index) => (
<div
key={index}
className={`border rounded-lg text-center bg-gray-50 py-4 cursor-pointer ${
selectedCard === index ? 'bg-blue-100' : ''
}`}
onClick={() => handleCardSelect(index)}>
<h3 className='text-yellow-500 font-bold'>{item.points}</h3>
<span>{item.price}</span>
</div>
))}
</div>
<hr className='my-6' />
<div className='grid gap-6 mb-6 md:grid-cols-2'>
<div>
<label
htmlFor='amount'
className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>
充值其它数量
</label>
<input
type='number'
id='amount'
className='bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500'
placeholder='输入数量'
value={amount}
onChange={handleAmountChange}
required
/>
</div>
</div>
<div className='flex justify-between w-full'>
<div>
支付金额<span className='text-red-500'>{amount}</span>
</div>
<button
type='submit'
className='text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800'>
在线充值
</button>
</div>
<ul className='text-gray-600 list-disc pl-6'>
<li>充值说明</li>
<li className='font-bold'>这只是一个演示页面不存在真实功能</li>
<li>充值最低额度为1积分</li>
<li>充值汇率为1元=1积分人民币和积分不能互相转换</li>
<li>余额永久有效无时间限制</li>
</ul>
</form>
</div>
)
}

View File

@@ -0,0 +1,64 @@
/**
* 首页组件
* @returns
*/
export default function DashboardItemHome() {
return (
<div className='w-full mx-auto'>
{/* 提示消息 */}
<div
className='p-4 mb-4 text-xl font-bold text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400'
role='alert'>
<span className='font-medium'>注意!</span>{' '}
整个后台都只是页面效果仅供演示查看没有对接实际功能
</div>
{/* 页面说明 */}
<div className='mb-8 text-lg text-gray-700 dark:text-gray-300'>
<p>
欢迎来到用户中心页面在这里您可以查看用户的账号信息与业务订单概况
</p>
</div>
{/* 进度条 */}
<div className='mb-8'>
<h3 className='text-xl text-gray-800 dark:text-white'>当前任务进度</h3>
<div className='bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 my-2'>
<div
className='bg-green-500 h-2.5 rounded-full'
style={{ width: '75%' }}></div>
</div>
<p className='text-sm text-gray-500 dark:text-gray-400'>
任务进度75%
</p>
</div>
{/* 背景动画块 */}
<div className='relative w-full h-64 rounded-lg bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 overflow-hidden'>
<div className='absolute inset-0 w-full h-full animate-pulse bg-black opacity-50'></div>
<div className='relative z-10 text-center text-white font-bold pt-24'>
<h3 className='text-2xl'>实时数据分析</h3>
<p className='text-lg'>监控您的系统数据查看实时变化</p>
</div>
</div>
{/* 数据卡片模块 */}
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-8'>
<div className='bg-white shadow-lg p-6 rounded-lg hover:scale-105 transition-all transform duration-300'>
<h3 className='text-xl text-gray-800 dark:text-white'>今日访问量</h3>
<p className='text-3xl text-green-600'>1,245</p>
</div>
<div className='bg-white shadow-lg p-6 rounded-lg hover:scale-105 transition-all transform duration-300'>
<h3 className='text-xl text-gray-800 dark:text-white'>用户总数</h3>
<p className='text-3xl text-blue-600'>12,300</p>
</div>
<div className='bg-white shadow-lg p-6 rounded-lg hover:scale-105 transition-all transform duration-300'>
<h3 className='text-xl text-gray-800 dark:text-white'>
系统健康状态
</h3>
<p className='text-3xl text-red-600'>正常</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,125 @@
import { useEffect, useState } from 'react'
/**
* 会员
* @returns
*/
export default function DashboardItemMembership() {
const [selectedMembership, setSelectedMembership] = useState(null)
const [amount, setAmount] = useState(0)
const memberships = [
{
title: '年度会员',
points: 98,
duration: '365天',
benefits: [
'日更5到20个热门项目',
'全站资源免费获取',
'内部会员专属交流群',
'可补差价升级',
'推广佣金高达40'
]
},
{
title: '永久会员',
points: 138,
duration: '永久',
benefits: [
'日更5到20个热门项目',
'全站资源免费获取',
'内部会员专属交流群',
'可补差价升级',
'推广佣金高达70'
]
},
{
title: '站长训练营',
points: 1998,
duration: '永久',
benefits: [
'站长学员请联系助理对接',
'一对一扶持搭建网站',
'提供独家引流技术照做就能成功',
'全站素材直接复刻到学员新站',
'软件一键同步更新',
'学员专属社群及交流群',
'设立高额福利的打卡机制(增强学员执行力)'
]
}
]
const handleMembershipSelect = index => {
setSelectedMembership(index)
setAmount(memberships[index].points)
}
const handleAmountChange = e => {
const value = e.target.value
setAmount(value)
}
useEffect(() => {
if (selectedMembership !== null) {
// 如果用户选中了会员,自动更新支付金额
const selectedPoints = memberships[selectedMembership]?.points
if (selectedPoints) {
setAmount(selectedPoints)
}
}
}, [selectedMembership])
return (
<div className='bg-white rounded-lg shadow-lg p-6 border'>
<div>
<h2 className='text-2xl font-bold mb-4'>会员注册</h2>
<hr className='my-2' />
</div>
{/* 会员卡片 */}
<div className='grid grid-cols-3 gap-4'>
{memberships.map((membership, index) => (
<div
key={index}
className={`block max-w-sm p-6 text-center border cursor-pointer rounded-lg shadow ${
selectedMembership === index ? 'bg-blue-100' : 'bg-gray-50'
}`}
onClick={() => handleMembershipSelect(index)}>
<h5 className='mb-2 text-2xl font-bold tracking-tight'>
{membership.title}
</h5>
<p className='font-normal'>所需积分{membership.points} 积分</p>
<p className='font-normal'>会员时长{membership.duration}</p>
<ul className='text-gray-600 mt-2'>
{membership.benefits.map((benefit, i) => (
<li key={i}>{benefit}</li>
))}
</ul>
</div>
))}
</div>
<form className='mt-6'>
<div className='flex justify-between w-full mb-4'>
<div>
支付金额<span className='text-red-500'>{amount}</span>
</div>
<button
type='submit'
className='text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800'>
立即开通
</button>
</div>
<ul className='text-gray-600 list-disc pl-6'>
<li>开通会员说明</li>
<li className='font-bold'>这只是一个演示页面不存在真实功能</li>
<li>本站会员账号权限为虚拟数字资源开通后不可退款</li>
<li>开通会员后可享有对应会员特权的商品折扣免费权限</li>
<li>会员特权到期后不享受特权</li>
<li>重复购买特权到期时间累计增加</li>
</ul>
</form>
</div>
)
}

View File

@@ -0,0 +1,246 @@
import { useState } from 'react'
/**
* 订单列表
*/
export default function DashboardItemOrder() {
const [currentPage, setCurrentPage] = useState(1)
const totalPages = 5
const columns = [
{ key: 'name', label: '商品名称' },
{ key: 'color', label: '颜色' },
{ key: 'category', label: '分类' },
{
key: 'accessories',
label: '配件',
render: value => (value ? '是' : '否')
},
{ key: 'available', label: '库存', render: value => (value ? '有' : '无') },
{ key: 'price', label: '价格', render: value => `¥${value}` },
{ key: 'weight', label: '重量' },
{
key: 'action',
label: '操作',
render: () => (
<div className='flex items-center space-x-3'>
<a
href='#'
className='font-medium text-blue-600 dark:text-blue-500 hover:underline'>
编辑
</a>
<a
href='#'
className='font-medium text-red-600 dark:text-red-500 hover:underline'>
删除
</a>
</div>
)
}
]
const data = [
{
name: '苹果 MacBook Pro 17"',
color: '银色',
category: '笔记本',
accessories: true,
available: true,
price: 2999,
weight: '3.0 公斤'
},
{
name: '微软 Surface Pro',
color: '白色',
category: '笔记本电脑',
accessories: false,
available: true,
price: 1999,
weight: '1.0 公斤'
},
{
name: 'Magic Mouse 2',
color: '黑色',
category: '配件',
accessories: true,
available: false,
price: 99,
weight: '0.2 公斤'
},
{
name: '苹果手表',
color: '黑色',
category: '手表',
accessories: true,
available: false,
price: 199,
weight: '0.12 公斤'
},
{
name: 'iPad Pro',
color: '金色',
category: '平板电脑',
accessories: false,
available: true,
price: 699,
weight: '1.3 公斤'
}
]
const onPageChange = page => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page)
}
}
return (
<div className='bg-white rounded-lg shadow-lg p-6 border'>
<div className='flex flex-col'>
<Table columns={columns} data={data} />
<Pagination
currentPage={currentPage}
totalPages={totalPages}
onPageChange={onPageChange}
/>
<ul className='text-gray-600 list-disc pl-6'>
<li>订单说明</li>
<li className='font-bold'>这只是一个演示页面不存在真实功能</li>
</ul>
</div>
</div>
)
}
/**
* 分页组件
*/
const Pagination = ({ currentPage, totalPages, onPageChange }) => {
const pages = Array.from({ length: totalPages }, (_, i) => i + 1)
return (
<nav
aria-label='page-navigation'
className='w-full flex mx-auto justify-center items-center py-4'>
<ul className='inline-flex -space-x-px text-sm'>
{/* 上一页 */}
<li>
<button
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
className={`flex items-center justify-center px-3 h-8 ms-0 leading-tight border border-e-0 rounded-s-lg ${
currentPage === 1
? 'text-gray-400 bg-gray-200 cursor-not-allowed'
: 'text-gray-500 bg-white hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
}`}>
上一页
</button>
</li>
{/* 页码列表 */}
{pages.map(page => (
<li key={page}>
<button
onClick={() => onPageChange(page)}
className={`flex items-center justify-center px-3 h-8 leading-tight border ${
currentPage === page
? 'text-blue-600 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white'
: 'text-gray-500 bg-white hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
}`}>
{page}
</button>
</li>
))}
{/* 下一页 */}
<li>
<button
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
className={`flex items-center justify-center px-3 h-8 leading-tight border rounded-e-lg ${
currentPage === totalPages
? 'text-gray-400 bg-gray-200 cursor-not-allowed'
: 'text-gray-500 bg-white hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white'
}`}>
下一页
</button>
</li>
</ul>
</nav>
)
}
/**
* 表格组件
*/
const Table = ({ columns, data }) => {
return (
<div className='relative overflow-x-auto shadow-md sm:rounded-lg'>
<table className='w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400'>
{/* 表头 */}
<thead className='text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400'>
<tr>
<th scope='col' className='p-4 w-4'>
<div className='flex items-center'>
<input
id='checkbox-all'
type='checkbox'
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
<label htmlFor='checkbox-all' className='sr-only'>
全选
</label>
</div>
</th>
{columns.map((column, index) => (
<th
key={index}
scope='col'
className={`${
column.key === 'name'
? 'px-6 py-3 w-[25%]'
: 'px-4 py-3 w-[10%]'
}`}>
{column.label}
</th>
))}
</tr>
</thead>
{/* 表格内容 */}
<tbody>
{data.map((item, index) => (
<tr
key={index}
className='bg-white border-b dark:bg-gray-800 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600'>
<td className='w-4 p-4'>
<div className='flex items-center'>
<input
id={`checkbox-${index}`}
type='checkbox'
className='w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 dark:focus:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600'
/>
<label htmlFor={`checkbox-${index}`} className='sr-only'>
选择
</label>
</div>
</td>
{columns.map((column, colIndex) => (
<td
key={colIndex}
className={`${
column.key === 'name'
? 'px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white w-[25%]'
: 'px-4 py-4 w-[10%]'
}`}>
{column.render
? column.render(item[column.key])
: item[column.key]}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import Link from 'next/link'
/**
* 仪表盘菜单
* @returns
*/
import { useRouter } from 'next/router'
/**
* 仪表盘菜单
* @returns
*/
export default function DashboardMenuList() {
const { asPath } = useRouter() // 获取当前路径
const dashBoardMenus = [
{ title: '仪表盘', icon: 'fas fa-gauge', href: '/dashboard' },
{ title: '基础资料', icon: 'fas fa-user', href: '/dashboard/user-profile' },
{ title: '我的余额', icon: 'fas fa-coins', href: '/dashboard/balance' },
{ title: '我的会员', icon: 'fas fa-gem', href: '/dashboard/membership' },
{
title: '我的订单',
icon: 'fas fa-cart-shopping',
href: '/dashboard/order'
},
{
title: '推广中心',
icon: 'fas fa-hand-holding-usd',
href: '/dashboard/affiliate'
}
]
return (
<ul
role='menu'
className='side-tabs-list bg-white border rounded-lg shadow-lg p-2 space-y-2 mb-6'>
{dashBoardMenus.map((item, index) => {
// 判断当前菜单是否高亮
const isActive = asPath === item.href
return (
<li
role='menuitem'
key={index}
className={`rounded-lg cursor-pointer block ${
isActive ? 'bg-blue-100 text-blue-600' : 'hover:bg-gray-100'
}`}>
<Link
href={item.href}
className='block py-2 px-4 w-full items-center justify-center'>
<i className={`${item.icon} w-6 mr-2`}></i>
<span className='whitespace-nowrap'>{item.title}</span>
</Link>
</li>
)
})}
</ul>
)
}

View File

@@ -36,6 +36,7 @@ export const siteConfig = (key, defaultVal = null, extendConfig = {}) => {
case 'POST_URL_PREFIX_MAPPING_CATEGORY':
case 'IS_TAG_COLOR_DISTINGUISHED':
case 'TAG_SORT_BY_COUNT':
case 'THEME':
case 'LINK':
return convertVal(
getValue(extendConfig[key], getValue(defaultVal, BLOG[key]))

View File

@@ -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()

View File

@@ -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

View File

@@ -220,6 +220,7 @@ const nextConfig = {
// export 静态导出时 忽略/pages/sitemap.xml.js 否则和getServerSideProps这个动态文件冲突
const pages = { ...defaultPathMap }
delete pages['/sitemap.xml']
delete pages['/auth']
return pages
},
publicRuntimeConfig: {

View File

@@ -1,6 +1,6 @@
{
"name": "notion-next",
"version": "4.7.9",
"version": "4.7.10",
"homepage": "https://github.com/tangly1024/NotionNext.git",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -9,12 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const NoFound = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps(req) {

View File

@@ -8,7 +8,7 @@ import { getPageTableOfContents } from '@/lib/notion/getPageTableOfContents'
import { getPasswordQuery } from '@/lib/password'
import { uploadDataToAlgolia } from '@/lib/plugins/algolia'
import { checkSlugHasNoSlash, getRecommendPost } from '@/lib/utils/post'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import md5 from 'js-md5'
import { useRouter } from 'next/router'
import { idToUuid } from 'notion-utils'
@@ -83,15 +83,11 @@ const Slug = props => {
}, [router, lock])
props = { ...props, lock, validPassword }
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return (
<>
{/* 文章布局 */}
<Layout {...props} />
<DynamicLayout theme={theme} router={router} {...props} />
{/* 解锁密码提示框 */}
{post?.password && post?.password !== '' && !lock && <Notification />}
{/* 导流工具 */}

View File

@@ -45,7 +45,6 @@ const MyApp = ({ Component, pageProps }) => {
// 整体布局
const GLayout = useCallback(
props => {
// 根据页面路径加载不同Layout文件
const Layout = getGlobalLayoutByTheme(queryParam)
return <Layout {...props} />
},

View File

@@ -3,17 +3,16 @@ import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { isBrowser } from '@/lib/utils'
import { formatDateFmt } from '@/lib/utils/formatDate'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
/**
* 归档首页
* @param {*} props
* @returns
*/
const ArchiveIndex = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
useEffect(() => {
if (isBrowser) {
const anchor = window.location.hash
@@ -28,7 +27,9 @@ const ArchiveIndex = props => {
}
}, [])
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ locale }) {

101
pages/auth/index.js Normal file
View File

@@ -0,0 +1,101 @@
// pages/sitemap.xml.js
import { getGlobalData } from '@/lib/db/getSiteData'
import axios from 'axios'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import Slug from '../[prefix]'
/**
* 根据notion的slug访问页面
* 解析二级目录 /article/about
* @param {*} props
* @returns
*/
const UI = props => {
const { redirect_pathname, redirect_query } = props
const router = useRouter()
useEffect(() => {
router?.push({ pathname: redirect_pathname, query: redirect_query })
}, [])
return <Slug {...props} />
}
/**
* 服务端接收参数处理
* @param {*} ctx
* @returns
*/
export const getServerSideProps = async ctx => {
const from = `auth`
const props = await getGlobalData({ from })
delete props.allPages
const code = ctx.query.code
let params = null
if (code) {
params = await fetchToken(code)
}
// 授权成功的划保存下用户的workspace信息
if (params?.status === 200) {
console.log('请求成功', params)
props.redirect_query = {
...params.data,
msg: '成功了' + JSON.stringify(params.data)
}
console.log('用户信息', JSON.stringify(params.data))
} else if (!params) {
console.log('请求异常', params)
props.redirect_query = { msg: '无效请求' }
} else {
console.log('请求失败', params)
props.redirect_query = { msg: params.statusText }
}
props.redirect_pathname = '/auth/result'
return {
props
}
}
const fetchToken = async code => {
if (!code) {
return '无效请求'
}
console.log('Auth', code)
const clientId = process.env.OAUTH_CLIENT_ID
const clientSecret = process.env.OAUTH_CLIENT_SECRET
const redirectUri = process.env.OAUTH_REDIRECT_URI
// encode in base 64
const encoded = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
try {
console.log(
`请求Code换取Token ${clientId}:${clientSecret} -- ${redirectUri}`
)
const response = await axios.post(
'https://api.notion.com/v1/oauth/token',
{
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri
},
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Basic ${encoded}`
}
}
)
console.log('Token response', response.data)
return response
} catch (error) {
console.error('Error fetching token', error)
}
}
export default UI

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,13 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ params: { category }, locale }) {

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -11,13 +11,9 @@ import { useRouter } from 'next/router'
*/
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ params: { category, page } }) {

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,13 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
export default function Category(props) {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ locale }) {

View File

@@ -0,0 +1,91 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPost, getPostBlocks } from '@/lib/db/getSiteData'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
* 根据notion的slug访问页面
* 只解析一级目录例如 /about
* @param {*} props
* @returns
*/
const Dashboard = props => {
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ locale }) {
const prefix = 'dashboard'
let fullSlug = 'dashboard'
const from = `slug-props-${fullSlug}`
const props = await getGlobalData({ from, locale })
if (siteConfig('PSEUDO_STATIC', false, props.NOTION_CONFIG)) {
if (!fullSlug.endsWith('.html')) {
fullSlug += '.html'
}
}
// 在列表内查找文章
props.post = props?.allPages?.find(p => {
return p.type.indexOf('Menu') < 0 && p.slug === fullSlug
})
// 处理非列表内文章的内信息
if (!props?.post) {
const pageId = prefix
if (pageId.length >= 32) {
const post = await getPost(pageId)
props.post = post
}
}
// 无法获取文章
if (!props?.post) {
props.post = null
return {
props,
revalidate: process.env.EXPORT
? undefined
: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
// 文章内容加载
if (!props?.post?.blockMap) {
props.post.blockMap = await getPostBlocks(props.post.id, from)
}
delete props.allPages
return {
props,
revalidate: process.env.EXPORT
? undefined
: siteConfig(
'NEXT_REVALIDATE_SECOND',
BLOG.NEXT_REVALIDATE_SECOND,
props.NOTION_CONFIG
)
}
}
export const getStaticPaths = async () => {
return {
paths: [
{ params: { index: [] } }, // 对应首页路径
{ params: { index: ['membership'] } },
{ params: { index: ['balance'] } },
{ params: { index: ['user-profile'] } },
{ params: { index: ['user-profile', 'security'] } }, // 嵌套路由,按结构传递
{ params: { index: ['order'] } },
{ params: { index: ['affiliate'] } }
],
fallback: 'blocking' // 或者 true阻塞式渲染
}
}
export default Dashboard

View File

@@ -4,7 +4,7 @@ import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { generateRobotsTxt } from '@/lib/robots.txt'
import { generateRss } from '@/lib/rss'
import { generateSitemapXml } from '@/lib/sitemap.xml'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -13,12 +13,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const Index = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
/**

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData, getPostBlocks } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,13 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const Page = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticPaths({ locale }) {

View File

@@ -2,17 +2,13 @@ import BLOG from '@/blog.config'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
/**

View File

@@ -2,19 +2,16 @@ import BLOG from '@/blog.config'
import { getDataFromCache } from '@/lib/cache/cache_manager'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
const Index = props => {
const { keyword } = props
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
props = { ...props, currentSearch: keyword }
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
/**

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -12,12 +12,6 @@ import { useRouter } from 'next/router'
const Search = props => {
const { posts } = props
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
const router = useRouter()
const keyword = router?.query?.s
@@ -37,7 +31,8 @@ const Search = props => {
props = { ...props, posts: filteredPosts }
return <Layout {...props} />
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
/**

View File

@@ -2,7 +2,7 @@ import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
// import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -11,12 +11,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const SignIn = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps(req) {

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,12 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const SignUp = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps(req) {

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,13 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const Tag = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ params: { tag }, locale }) {

View File

@@ -1,16 +1,13 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
const Tag = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps({ params: { tag, page }, locale }) {

View File

@@ -1,7 +1,7 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { DynamicLayout } from '@/themes/theme'
import { useRouter } from 'next/router'
/**
@@ -10,12 +10,9 @@ import { useRouter } from 'next/router'
* @returns
*/
const TagIndex = props => {
// 根据页面路径加载不同Layout文件
const Layout = getLayoutByTheme({
theme: siteConfig('THEME'),
router: useRouter()
})
return <Layout {...props} />
const router = useRouter()
const theme = siteConfig('THEME', BLOG.THEME, props.NOTION_CONFIG)
return <DynamicLayout theme={theme} router={router} {...props} />
}
export async function getStaticProps(req) {

View File

@@ -1,5 +1,5 @@
import MobileButtonCatalog from './MobileButtonCatalog'
import MobileButtonPageNav from './MobileButtonPageNav'
import { useGlobal } from '@/lib/global'
import { useGitBookGlobal } from '..'
/**
* 移动端底部导航
@@ -8,20 +8,43 @@ import MobileButtonPageNav from './MobileButtonPageNav'
*/
export default function BottomMenuBar({ post, className }) {
const showTocButton = post?.toc?.length > 1
const { locale } = useGlobal()
const { pageNavVisible, changePageNavVisible, tocVisible, changeTocVisible } =
useGitBookGlobal()
const togglePageNavVisible = () => {
changePageNavVisible(!pageNavVisible)
}
const toggleToc = () => {
changeTocVisible(!tocVisible)
}
return (
<>
{/* 移动端底部导航按钮 */}
<div className='pb-2 dark:bg-hexo-black-gray bottom-button-group md:hidden w-screen h-14 px-4 fixed flex items-center justify-between right-left bottom-0 z-30 bg-white border-t dark:border-gray-800'>
<div className='w-full'>
<MobileButtonPageNav />
</div>
<div className='md:hidden fixed bottom-0 left-0 z-50 w-full h-16 bg-white border-t border-gray-200 dark:bg-gray-700 dark:border-gray-600'>
<div
className={`grid h-full max-w-lg mx-auto font-medium ${showTocButton && 'grid-cols-2'}`}>
<button
type='button'
onClick={togglePageNavVisible}
className='inline-flex flex-col items-center justify-center px-5 border-gray-200 border-x hover:bg-gray-50 dark:hover:bg-gray-800 group dark:border-gray-600'>
<i className='fa-book fas w-5 h-5 mb-2 text-gray-500 dark:text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-500' />
<span class='text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-500'>
{locale.COMMON.ARTICLE_LIST}
</span>
</button>
{showTocButton && (
<div className='w-full'>
<MobileButtonCatalog />
</div>
<button
type='button'
onClick={toggleToc}
className='inline-flex flex-col items-center justify-center px-5 border-gray-200 border-x hover:bg-gray-50 dark:hover:bg-gray-800 group dark:border-gray-600'>
<i className='fa-list-ol fas w-5 h-5 mb-2 text-gray-500 dark:text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-500' />
<span class='text-sm text-gray-500 dark:text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-500'>
{locale.COMMON.TABLE_OF_CONTENTS}
</span>
</button>
)}
</div>
</>
</div>
)
}

View File

@@ -1,31 +0,0 @@
import { useGlobal } from '@/lib/global'
import { useGitBookGlobal } from '@/themes/gitbook'
/**
* 移动端目录按钮
*/
export default function MobileButtonCatalog() {
const { tocVisible, changeTocVisible } = useGitBookGlobal()
const { locale } = useGlobal()
const toggleToc = () => {
changeTocVisible(!tocVisible)
}
return (
<div
onClick={toggleToc}
className={
'text-black flex justify-center items-center dark:text-gray-200 dark:bg-hexo-black-gray py-2 px-2'
}>
<a
id='toc-button'
className={
'space-x-4 cursor-pointer hover:scale-150 transform duration-200'
}>
<i className='fa-list-ol fas' />
<span>{locale.COMMON.TABLE_OF_CONTENTS}</span>
</a>
</div>
)
}

View File

@@ -1,30 +0,0 @@
import { useGlobal } from '@/lib/global'
import { useGitBookGlobal } from '@/themes/gitbook'
/**
* 移动端文章导航按钮
*/
export default function MobileButtonPageNav() {
const { pageNavVisible, changePageNavVisible } = useGitBookGlobal()
const { locale } = useGlobal()
const togglePageNavVisible = () => {
changePageNavVisible(!pageNavVisible)
}
return (
<div
onClick={togglePageNavVisible}
className={
'text-black flex justify-center items-center dark:text-gray-200 dark:bg-hexo-black-gray py-2 px-2'
}>
<a
id='nav-button'
className={
'space-x-4 cursor-pointer hover:scale-150 transform duration-200'
}>
<i className='fa-book fas' />
<span>{locale.COMMON.ARTICLE_LIST}</span>
</a>
</div>
)
}

View File

@@ -7,6 +7,8 @@ import LoadingCover from '@/components/LoadingCover'
import NotionIcon from '@/components/NotionIcon'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import DashboardBody from '@/components/ui/dashboard/DashboardBody'
import DashboardHeader from '@/components/ui/dashboard/DashboardHeader'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
@@ -255,7 +257,9 @@ const LayoutIndex = props => {
// 重定向到指定文章
router.push(index).then(() => {
setTimeout(() => {
const article = document.querySelector('#article-wrapper #notion-article')
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
console.log(
'请检查您的Notion数据库中是否包含此slug页面 ',
@@ -309,7 +313,9 @@ const LayoutSlug = props => {
setTimeout(
() => {
if (isBrowser) {
const article = document.querySelector('#article-wrapper #notion-article')
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
@@ -539,11 +545,40 @@ const LayoutSignUp = props => {
)
}
/**
* 仪表盘
* @param {*} props
* @returns
*/
const LayoutDashboard = props => {
const { post } = props
return (
<>
<div className='container grow'>
<div className='flex flex-wrap justify-center -mx-4'>
<div id='container-inner' className='w-full p-4'>
{post && (
<div id='article-wrapper' className='mx-auto'>
<NotionPage {...props} />
</div>
)}
</div>
</div>
</div>
{/* 仪表盘 */}
<DashboardHeader />
<DashboardBody />
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutDashboard,
LayoutIndex,
LayoutPostList,
LayoutSearch,

View File

@@ -1,8 +1,9 @@
import Collapse from '@/components/Collapse'
import DarkModeButton from '@/components/DarkModeButton'
import DashboardButton from '@/components/ui/dashboard/DashboardButton'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { SignInButton, SignedOut, UserButton } from '@clerk/nextjs'
import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
import throttle from 'lodash.throttle'
import { useRouter } from 'next/router'
import { useEffect, useRef, useState } from 'react'
@@ -200,7 +201,10 @@ export default function Header(props) {
</button>
</SignInButton>
</SignedOut>
<UserButton />
<SignedIn>
<UserButton />
<DashboardButton />
</SignedIn>
</>
)}
</div>

View File

@@ -6,6 +6,8 @@ import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import WWAds from '@/components/WWAds'
import DashboardBody from '@/components/ui/dashboard/DashboardBody'
import DashboardHeader from '@/components/ui/dashboard/DashboardHeader'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { isBrowser } from '@/lib/utils'
@@ -155,7 +157,9 @@ const LayoutSlug = props => {
setTimeout(
() => {
if (isBrowser) {
const article = document.querySelector('#article-wrapper #notion-article')
const article = document.querySelector(
'#article-wrapper #notion-article'
)
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
@@ -495,11 +499,39 @@ const LayoutSignUp = props => {
)
}
/**
* 仪表盘
* @param {*} props
* @returns
*/
const LayoutDashboard = props => {
const { post } = props
return (
<>
<div className='container grow'>
<div className='flex flex-wrap justify-center -mx-4'>
<div id='container-inner' className='w-full p-4'>
{post && (
<div id='article-wrapper' className='mx-auto'>
<NotionPage {...props} />
</div>
)}
</div>
</div>
</div>
{/* 仪表盘 */}
<DashboardHeader />
<DashboardBody />
</>
)
}
export {
Layout404,
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutDashboard,
LayoutIndex,
LayoutPostList,
LayoutSearch,

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-unreachable */
import DashboardButton from '@/components/ui/dashboard/DashboardButton'
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
@@ -87,6 +88,7 @@ export const Header = props => {
</SignedOut>
<SignedIn>
<UserButton />
<DashboardButton />
</SignedIn>
</>
)}

View File

@@ -42,7 +42,7 @@ export const MenuItem = ({ link }) => {
</svg>
</a>
<div className='submenu relative left-0 top-full hidden w-[250px] rounded-sm bg-white p-4 transition-[top] duration-300 group-hover:opacity-100 dark:bg-dark-2 lg:invisible lg:absolute lg:top-[110%] lg:block lg:opacity-0 lg:shadow-lg lg:group-hover:visible lg:group-hover:top-full'>
<div className='submenu border dark:border-gray-600 relative left-0 top-full hidden w-[250px] rounded-sm bg-white p-4 transition-[top] duration-300 group-hover:opacity-100 dark:bg-dark-2 lg:invisible lg:absolute lg:top-[110%] lg:block lg:opacity-0 lg:shadow-lg lg:group-hover:visible lg:group-hover:top-full'>
{link.subMenus.map((sLink, index) => {
return (
<Link
@@ -51,7 +51,7 @@ export const MenuItem = ({ link }) => {
target={link?.target}
className='block rounded px-4 py-[10px] text-sm text-body-color hover:text-primary dark:text-dark-6 dark:hover:text-primary'>
{/* 子菜单SubMenuItem */}
<span className='text-sm ml-4 whitespace-nowrap'>
<span className='text-md ml-2 whitespace-nowrap'>
{link?.icon && (
<i className={sLink.icon + ' mr-2 my-auto'} />
)}{' '}

View File

@@ -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'
@@ -177,6 +179,39 @@ const LayoutSlug = props => {
)
}
/**
* 仪表盘
* @param {*} props
* @returns
*/
const LayoutDashboard = props => {
const { post } = props
return (
<>
<div className='container grow'>
<div className='flex flex-wrap justify-center -mx-4'>
<div id='container-inner' className='w-full p-4'>
{post && (
<div id='article-wrapper' className='mx-auto'>
<NotionPage {...props} />
</div>
)}
</div>
</div>
</div>
{/* 仪表盘 */}
<DashboardHeader />
<DashboardBody />
</>
)
}
/**
* 搜索
* @param {*} props
* @returns
*/
const LayoutSearch = props => {
const { keyword } = props
const router = useRouter()
@@ -491,6 +526,7 @@ export {
LayoutArchive,
LayoutBase,
LayoutCategoryIndex,
LayoutDashboard,
LayoutIndex,
LayoutPostList,
LayoutSearch,

View File

@@ -8,17 +8,50 @@ import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
export const { THEMES = [] } = getConfig().publicRuntimeConfig
/**
* 获取主配置
* 获取主配置
* @param {string} themeQuery - 主题查询参数(支持多个主题用逗号分隔)
* @returns {Promise<object>} 主题配置对象
*/
export const getThemeConfig = async themeQuery => {
if (themeQuery && themeQuery !== BLOG.THEME) {
const THEME_CONFIG = await import(`@/themes/${themeQuery}`).then(
m => m.THEME_CONFIG
)
return THEME_CONFIG
} else {
return ThemeComponents?.THEME_CONFIG
// 如果 themeQuery 存在且不等于默认主题,处理多主题情况
if (typeof themeQuery === 'string' && themeQuery.trim()) {
// 取 themeQuery 中第一个主题(以逗号为分隔符)
const themeName = themeQuery.split(',')[0].trim()
// 如果 themeQuery 不等于当前默认主题,则加载指定主题的配置
if (themeName !== BLOG.THEME) {
try {
// 动态导入主题配置
const THEME_CONFIG = await import(`@/themes/${themeName}`)
.then(m => m.THEME_CONFIG)
.catch(err => {
console.error(`Failed to load theme ${themeName}:`, err)
return null // 主题加载失败时返回 null 或者其他默认值
})
// 如果主题配置加载成功,返回配置
if (THEME_CONFIG) {
return THEME_CONFIG
} else {
// 如果加载失败,返回默认主题配置
console.warn(
`Loading ${themeName} failed. Falling back to default theme.`
)
return ThemeComponents?.THEME_CONFIG
}
} catch (error) {
// 如果 import 过程中出现异常,返回默认主题配置
console.error(
`Error loading theme configuration for ${themeName}:`,
error
)
return ThemeComponents?.THEME_CONFIG
}
}
}
// 如果没有 themeQuery 或 themeQuery 与默认主题相同,返回默认主题配置
return ThemeComponents?.THEME_CONFIG
}
/**
@@ -38,48 +71,46 @@ export const getGlobalLayoutByTheme = themeQuery => {
}
}
/**
* 动态获取布局
* @param {*} props
*/
export const DynamicLayout = props => {
const { router, theme } = props
const SelectedLayout = getLayoutByTheme({ router, theme })
return <SelectedLayout {...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
*/
@@ -91,8 +122,9 @@ const getLayoutNameByPath = path => {
/**
* 切换主题时的特殊处理
* 删除多余的元素
*/
const checkThemeDOM = () => {
const fixThemeDOM = () => {
if (isBrowser) {
const elements = document.querySelectorAll('[id^="theme-"]')
if (elements?.length > 1) {