starter\magzine\gitbook 主题dashboard元素

This commit is contained in:
tangly1024.com
2024-11-19 19:00:57 +08:00
parent ed947d866e
commit 2988426a2f
19 changed files with 1018 additions and 81 deletions

View File

@@ -1,25 +1,34 @@
'use client'
import { UserProfile } from '@clerk/nextjs'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import DashboardMenuList from './DashboardMenuList'
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()?.asPath
const { asPath } = useRouter()
// 提取不包含查询参数的路径部分
const basePath = asPath.split('?')[0]
return (
<div className='flex w-full container gap-x-4 min-h-96 mx-auto mt-12 mb-12 justify-center'>
<div className='side-tabs w-72'>
<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 grow'>
{asPath === '/dashboard' && <div>控制台首页</div>}
{(asPath === '/dashboard/user-profile' ||
asPath === '/dashboard/user-profile/security') && (
<div className='main-content-wrapper w-full'>
{basePath === '/dashboard' && <DashboardItemHome />}
{(basePath === '/dashboard/user-profile' ||
basePath === '/dashboard/user-profile/security') && (
<UserProfile
appearance={{
elements: {
@@ -32,9 +41,10 @@ export default function DashboardBody() {
path='/dashboard/user-profile'
/>
)}
{asPath === '/dashboard/membership' && <div>会员</div>}
{asPath === '/dashboard/order' && <div>订单</div>}
{asPath === '/dashboard/favorite' && <div>收藏</div>}
{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

@@ -2,6 +2,7 @@ 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
@@ -10,32 +11,43 @@ export default function DashboardHeader() {
const { user } = useGlobal()
return (
<div className='flex w-full container mx-auto mt-24 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>{user?.fullName}</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 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>
{/* 登出按钮 */}
<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

@@ -1,34 +1,57 @@
import Link from 'next/link'
/**
* 仪表盘菜单
* @returns
*/
import { useRouter } from 'next/router'
/**
* 仪表盘菜单
* @returns
*/
export default function DashboardMenuList() {
const { asPath } = useRouter() // 获取当前路径
const dashBoardMenus = [
{ title: '控制台', href: '/dashboard' },
{ title: '基础资料', href: '/dashboard/user-profile' },
{ title: '我的会员', href: '/dashboard/membership' },
{ title: '我的订单', href: '/dashboard/order' },
{ title: '我的收藏', href: '/dashboard/favorite' }
{ 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'>
{dashBoardMenus?.map((item, index) => (
<li
role='menuitem'
key={index}
className='hover:bg-gray-100 rounded-lg cursor-pointer block'>
<Link
href={item.href}
className='block py-2 px-4 w-full' // 让 Link 填充整个 li
>
{item.title}
</Link>
</li>
))}
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

@@ -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,3 +1,4 @@
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
import { DynamicLayout } from '@/themes/theme'

View File

@@ -75,14 +75,17 @@ 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
{ 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

@@ -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,12 +88,7 @@ export const Header = props => {
</SignedOut>
<SignedIn>
<UserButton />
<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.5 me-2 mb-2 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700 dark:border-gray-700'>
<Link href='/dashboard'>Dashboard</Link>
</button>
<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

@@ -178,6 +178,7 @@ const LayoutSlug = props => {
</>
)
}
/**
* 仪表盘
* @param {*} props

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
}
/**