theme-game

This commit is contained in:
tangly1024.com
2024-03-19 18:48:14 +08:00
parent ff77d30cae
commit 4d3b30b495
21 changed files with 488 additions and 342 deletions

View File

@@ -432,7 +432,8 @@ const BLOG = {
category: process.env.NEXT_PUBLIC_NOTION_PROPERTY_CATEGORY || 'category',
date: process.env.NEXT_PUBLIC_NOTION_PROPERTY_DATE || 'date',
tags: process.env.NEXT_PUBLIC_NOTION_PROPERTY_TAGS || 'tags',
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon'
icon: process.env.NEXT_PUBLIC_NOTION_PROPERTY_ICON || 'icon',
ext: process.env.NEXT_PUBLIC_NOTION_PROPERTY_EXT || 'ext' // 扩展字段存放json-string用于复杂业务
},
// RSS订阅

View File

@@ -1,10 +1,10 @@
import { useRef, useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
/**
* 可拖拽组件
*/
export const Draggable = (props) => {
const { children } = props
export const Draggable = props => {
const { children, stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
@@ -14,8 +14,10 @@ export const Draggable = (props) => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
function e(event) { // 定义事件对象标准化函数
if (!event) { // 兼容IE浏览器
function e(event) {
// 定义事件对象标准化函数
if (!event) {
// 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
@@ -40,9 +42,10 @@ export const Draggable = (props) => {
document.onmousedown = start
document.ontouchstart = start
function start (event) { // 按下鼠标时,初始化处理
function start(event) {
// 按下鼠标时,初始化处理
if (!draggableElements) return
event = e(event)// 获取标准事件对象
event = e(event) // 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
@@ -60,19 +63,20 @@ export const Draggable = (props) => {
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
document.onmousemove = move// 注册鼠标移动事件处理函数
document.onmousemove = move // 注册鼠标移动事件处理函数
document.ontouchmove = move
document.onmouseup = stop// 注册松开鼠标事件处理函数
document.onmouseup = stop // 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
function move(event) { // 鼠标移动处理函数
function move(event) {
// 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
const stop = (event) => {
const stop = event => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
@@ -80,7 +84,7 @@ export const Draggable = (props) => {
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
const updatePosition = (event) => {
const updatePosition = event => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
@@ -120,15 +124,18 @@ export const Draggable = (props) => {
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
if (offsetTop > (clientHeight - offsetHeight)) {
if (offsetTop > clientHeight - offsetHeight) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
if (offsetLeft > (clientWidth - offsetWidth)) {
if (offsetLeft > clientWidth - offsetWidth) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
if (stick === 'left') {
drag.firstElementChild.style.left = 0 + 'px'
}
}
}
@@ -142,9 +149,11 @@ export const Draggable = (props) => {
}
}, [])
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
return (
<div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
)
}
Draggable.defaultProps = { left: 0, top: 0 }

View File

@@ -1,6 +1,6 @@
import { getTextContent, getDateValue } from 'notion-utils'
import { NotionAPI } from 'notion-client'
import BLOG from '@/blog.config'
import { NotionAPI } from 'notion-client'
import { getDateValue, getTextContent } from 'notion-utils'
import formatDate from '../utils/formatDate'
// import { createHash } from 'crypto'
import md5 from 'js-md5'
@@ -49,8 +49,7 @@ export default async function getPageProperties(id, value, schema, authToken, ta
if (rawUsers[i][0][1]) {
const userId = rawUsers[i][0]
const res = await api.getUsers(userId)
const resValue =
res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const resValue = res?.recordMapWithRoles?.notion_user?.[userId[1]]?.value
const user = {
id: resValue?.id,
first_name: resValue?.given_name,
@@ -93,16 +92,17 @@ export default async function getPageProperties(id, value, schema, authToken, ta
properties.pageIcon = mapImgUrl(value?.format?.page_icon, value) ?? ''
properties.pageCover = mapImgUrl(value?.format?.page_cover, value) ?? ''
properties.pageCoverThumbnail = mapImgUrl(value?.format?.page_cover, value, 'block', 'pageCoverThumbnail') ?? ''
properties.ext = converToJSON(properties?.ext)
properties.content = value.content ?? []
properties.tagItems = properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
properties.tagItems =
properties?.tags?.map(tag => {
return { name: tag, color: tagOptions?.find(t => t.value === tag)?.color || 'gray' }
}) || []
delete properties.content
// 处理URL
if (properties.type === 'Post') {
properties.slug = (BLOG.POST_URL_PREFIX) ? generateCustomizeUrl(properties) : (properties.slug ?? properties.id)
properties.slug = BLOG.POST_URL_PREFIX ? generateCustomizeUrl(properties) : properties.slug ?? properties.id
} else if (properties.type === 'Page') {
properties.slug = properties.slug ?? properties.id
} else if (properties.type === 'Menu' || properties.type === 'SubMenu') {
@@ -122,6 +122,24 @@ export default async function getPageProperties(id, value, schema, authToken, ta
return properties
}
/**
* 字符串转json
* @param {*} str
* @returns
*/
function converToJSON(str) {
if (!str) {
return {}
}
// 使用正则表达式去除空格和换行符
try {
return JSON.parse(str.replace(/\s/g, ''))
} catch (error) {
console.warn('无效JSON', str)
return {}
}
}
/**
* 映射用户自定义表头
*/
@@ -164,7 +182,7 @@ function generateCustomizeUrl(postProperties) {
const formatPostCreatedDate = new Date(postProperties?.publishDay)
fullPrefix += String(formatPostCreatedDate.getUTCDate()).padStart(2, 0)
} else if (pattern === '%slug%') {
fullPrefix += (postProperties.slug ?? postProperties.id)
fullPrefix += postProperties.slug ?? postProperties.id
} else if (!pattern.includes('%')) {
fullPrefix += pattern
} else {
@@ -180,5 +198,5 @@ function generateCustomizeUrl(postProperties) {
if (fullPrefix.endsWith('/')) {
fullPrefix = fullPrefix.substring(0, fullPrefix.length - 1) // 去掉尾部部的"/"
}
return `${fullPrefix}/${(postProperties.slug ?? postProperties.id)}`
return `${fullPrefix}/${postProperties.slug ?? postProperties.id}`
}

View File

@@ -0,0 +1,48 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Screen iFrame</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
</style>
</head>
<body>
<!-- <div style="position: absolute;
right: 0px;
bottom: 0px;
background: white;">
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> -->
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script>
var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = myParam;
</script>
<script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</html>

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Full Screen iFrame</title>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
#myIframe {
width: 100%;
height: 100%;
border: none;
/* 可选:移除边框 */
}
</style>
</head>
<body>
<!-- <div style="display: block;background-color: red;">
<button onclick="toggleFullScreen()">Toggle Full Screen</button>
</div> -->
<iframe id="myIframe" allowfullscreen="allowfullscreen" allow="autoplay" scrolling="no"></iframe>
<!-- https://letsplay247.github.io/cz.html?n=space-wars-battleground -->
<script>
var myParam = location.search.split('n=')[1]
document.getElementById("myIframe").src = "https://games.crazygames.com/en_US/" + myParam + "/index.html";
</script>
<script src="/js/fullscreen.js" type="text/javascript"></script>
</body>
</html>

32
public/js/fullscreen.js Normal file
View File

@@ -0,0 +1,32 @@
window.toggleFullScreen = toggleFullScreen
function toggleFullScreen() {
var iframe = document.getElementById('myIframe')
if (!document.fullscreenElement) {
if (iframe.requestFullscreen) {
iframe.requestFullscreen()
} else if (iframe.mozRequestFullScreen) {
/* Firefox */
iframe.mozRequestFullScreen()
} else if (iframe.webkitRequestFullscreen) {
/* Chrome, Safari and Opera */
iframe.webkitRequestFullscreen()
} else if (iframe.msRequestFullscreen) {
/* IE/Edge */
iframe.msRequestFullscreen()
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
/* Firefox */
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
/* Chrome, Safari and Opera */
document.webkitExitFullscreen()
} else if (document.msExitFullscreen) {
/* IE/Edge */
document.msExitFullscreen()
}
}
}

View File

@@ -2,15 +2,24 @@ import dynamic from 'next/dynamic'
const NotionPage = dynamic(() => import('@/components/NotionPage'))
/**
* 公告
* @param {*} param0
* @returns
*/
const Announcement = ({ notice, className }) => {
if (notice?.blockMap) {
return <div className={className}>
<section id='announcement-wrapper' className='mb-10'>
{notice && (<div id="announcement-content">
<NotionPage post={notice} className='text-center ' />
</div>)}
</section>
</div>
return (
<div className={className}>
<section id='announcement-wrapper' className='mb-10'>
{notice && (
<div id='announcement-content'>
<NotionPage post={notice} />
</div>
)}
</section>
</div>
)
} else {
return null
}

View File

@@ -1,9 +1,8 @@
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { useRouter } from 'next/router'
import Link from 'next/link'
import BlogPost from './BlogPost'
import { useRouter } from 'next/router'
import { GameListIndexCombine } from './GameListIndexCombine'
export const BlogListPage = props => {
const { page = 1, posts, postCount } = props
@@ -14,37 +13,39 @@ export const BlogListPage = props => {
const showPrev = currentPage > 1
const showNext = currentPage < totalPage && posts?.length > 0
const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
const pagePrefix = router.asPath
.split('?')[0]
.replace(/\/page\/[1-9]\d*/, '')
.replace(/\/$/, '')
return (
<div className="w-full md:pr-12 my-6">
<div className='w-full md:pr-12 my-6'>
<div id='posts-wrapper'>
<GameListIndexCombine {...props} />
</div>
<div id="posts-wrapper">
{posts?.map(post => (
<BlogPost key={post.id} post={post}/>
))}
</div>
<div className="flex justify-between text-xs">
<Link
href={{ pathname: currentPage - 1 === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`, query: router.query.s ? { s: router.query.s } : {} }}
className={`${showPrev ? ' ' : ' invisible block pointer-events-none '}no-underline py-2 px-3 rounded`}>
<button rel="prev" className="block cursor-pointer">
{locale.PAGINATION.PREV}
</button>
</Link>
<Link
href={{ pathname: `${pagePrefix}/page/${currentPage + 1}`, query: router.query.s ? { s: router.query.s } : {} }}
className={`${showNext ? ' ' : 'invisible pointer-events-none '} no-underline py-2 px-3 rounded`}>
<button rel="next" className="block cursor-pointer">
{locale.PAGINATION.NEXT}
</button>
</Link>
</div>
</div>
<div className='flex justify-between text-xs'>
<Link
href={{
pathname: currentPage - 1 === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
className={`${showPrev ? ' ' : ' invisible block pointer-events-none '}no-underline py-2 px-3 rounded`}>
<button rel='prev' className='block cursor-pointer'>
{locale.PAGINATION.PREV}
</button>
</Link>
<Link
href={{
pathname: `${pagePrefix}/page/${currentPage + 1}`,
query: router.query.s ? { s: router.query.s } : {}
}}
className={`${showNext ? ' ' : 'invisible pointer-events-none '} no-underline py-2 px-3 rounded`}>
<button rel='next' className='block cursor-pointer'>
{locale.PAGINATION.NEXT}
</button>
</Link>
</div>
</div>
)
}

View File

@@ -1,153 +0,0 @@
import { useRef, useEffect, useState } from 'react'
/**
* 可拖拽组件
*/
export const Draggable = (props) => {
const { children,stick } = props
const draggableRef = useRef(null)
const rafRef = useRef(null)
const [moving, setMoving] = useState(false)
let currentObj, offsetX, offsetY
useEffect(() => {
const draggableElements = document.getElementsByClassName('draggable')
// 标准化鼠标事件对象
function e(event) { // 定义事件对象标准化函数
if (!event) { // 兼容IE浏览器
event = window.event
event.target = event.srcElement
event.layerX = event.offsetX
event.layerY = event.offsetY
}
// 移动端
if (event.type === 'touchstart' || event.type === 'touchmove') {
event.clientX = event.touches[0].clientX
event.clientY = event.touches[0].clientY
}
event.mx = event.pageX || event.clientX + document.body.scrollLeft
// 计算鼠标指针的x轴距离
event.my = event.pageY || event.clientY + document.body.scrollTop
// 计算鼠标指针的y轴距离
return event // 返回标准化的事件对象
}
// 定义鼠标事件处理函数
// document.pointerdown = start
document.onmousedown = start
document.ontouchstart = start
function start (event) { // 按下鼠标时,初始化处理
if (!draggableElements) return
event = e(event)// 获取标准事件对象
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
if (inDragBox(event, drag)) {
currentObj = drag.firstElementChild
}
}
if (currentObj) {
if (event.type === 'touchstart') {
event.preventDefault() // 阻止默认的滚动行为
document.documentElement.style.overflow = 'hidden' // 防止页面一起滚动
}
setMoving(true)
offsetX = event.mx - currentObj.offsetLeft
offsetY = event.my - currentObj.offsetTop
document.onmousemove = move// 注册鼠标移动事件处理函数
document.ontouchmove = move
document.onmouseup = stop// 注册松开鼠标事件处理函数
document.ontouchend = stop
}
}
function move(event) { // 鼠标移动处理函数
event = e(event)
rafRef.current = requestAnimationFrame(() => updatePosition(event))
}
const stop = (event) => {
event = e(event)
document.documentElement.style.overflow = 'auto' // 恢复默认的滚动行为
cancelAnimationFrame(rafRef.current)
setMoving(false)
currentObj = document.ontouchmove = document.ontouchend = document.onmousemove = document.onmouseup = null
}
const updatePosition = (event) => {
if (currentObj) {
const left = event.mx - offsetX
const top = event.my - offsetY
currentObj.style.left = left + 'px'
currentObj.style.top = top + 'px'
checkInWindow()
}
}
/**
* 鼠标是否在可拖拽区域内
* @param {*} event
* @returns
*/
function inDragBox(event, drag) {
const { clientX, clientY } = event // 鼠标位置
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild // 窗口位置
const horizontal = clientX > offsetLeft && clientX < offsetLeft + offsetWidth
const vertical = clientY > offsetTop && clientY < offsetTop + offsetHeight
if (horizontal && vertical) {
return true
}
return false
}
/**
* 若超出窗口则吸附。
*/
function checkInWindow() {
// 检查是否悬浮在窗口内
for (const drag of draggableElements) {
// 判断鼠标点击的区域是否是拖拽框内
const { offsetHeight, offsetWidth, offsetTop, offsetLeft } = drag.firstElementChild
const { clientHeight, clientWidth } = document.documentElement
if (offsetTop < 0) {
drag.firstElementChild.style.top = 0
}
if (offsetTop > (clientHeight - offsetHeight)) {
drag.firstElementChild.style.top = clientHeight - offsetHeight + 'px'
}
if (offsetLeft < 0) {
drag.firstElementChild.style.left = 0
}
if (offsetLeft > (clientWidth - offsetWidth)) {
drag.firstElementChild.style.left = clientWidth - offsetWidth + 'px'
}
if(stick==='left'){
drag.firstElementChild.style.left = 0 + 'px'
}
}
}
window.addEventListener('resize', checkInWindow)
return () => {
return () => {
window.removeEventListener('resize', checkInWindow)
cancelAnimationFrame(rafRef.current)
}
}
}, [])
return <div className={`draggable ${moving ? 'cursor-grabbing' : 'cursor-grab'} select-none`} ref={draggableRef}>
{children}
</div>
}
Draggable.defaultProps = { left: 0, top: 0 }

View File

@@ -1,8 +1,7 @@
import DarkModeButton from '@/components/DarkModeButton'
import Vercel from '@/components/Vercel'
import { siteConfig } from '@/lib/config'
export const Footer = (props) => {
export const Footer = props => {
const d = new Date()
const currentYear = d.getFullYear()
const { post } = props
@@ -10,20 +9,27 @@ export const Footer = (props) => {
const since = siteConfig('SINCE')
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
return <footer
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 transition-all ${
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
}`}
>
<DarkModeButton className='text-center py-4'/>
<hr className="border-gray-200 dark:border-gray-600" />
<div className="my-4 text-sm leading-6">
<div className="flex align-baseline justify-between flex-wrap">
<p>
© {siteConfig('AUTHOR')} {copyrightDate}
</p>
<Vercel />
</div>
</div>
</footer>
return (
<footer
className={`z-10 relative mt-6 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 transition-all ${
!fullWidth ? 'max-w-2xl px-4' : 'px-4 md:px-24'
}`}>
<DarkModeButton className='text-center py-4' />
<hr className='border-gray-200 dark:border-gray-600' />
<div className='my-4 text-sm leading-6'>
<div className='flex align-baseline justify-between flex-wrap'>
<span className='dark:text-gray-200 no-underline ml-4'>
Powered by
<a href='https://github.com/tangly1024/NotionNext' className=' hover:underline'>
{' '}
NotionNext {siteConfig('VERSION')}{' '}
</a>
</span>
<p>
© {siteConfig('TITLE')} {copyrightDate}
</p>
</div>
</div>
</footer>
)
}

View File

@@ -1,7 +1,5 @@
/* eslint-disable @next/next/no-img-element */
import Image from 'next/image'
/**
* 全屏按钮
* @returns
@@ -14,26 +12,20 @@ export default function FullScreen() {
block: 'end',
inline: 'nearest'
})
// console.log(document?.getElementById('game-wrapper')?.contentWindow)
document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen()
document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen &&
document?.getElementById('game-wrapper')?.contentWindow?.toggleFullScreen()
}
return (
<div
className="group text-white w-full justify-center items-center flex rounded-lg m-2 md:m-0 p-2 hover:bg-gray-700 bg-[#1F2030] md:rounded-none md:bg-none"
onClick={toggleFullScreen}
>
<Image
width={18}
height={18}
src="/svg/fullscreen-alt.svg"
alt="full screen"
title="full screen"
className="cursor-pointer group-hover:scale-125 transition-all duration-150 "
className='group text-white w-full justify-center items-center flex rounded-lg m-2 md:m-0 p-2 hover:bg-gray-700 bg-[#1F2030] md:rounded-none md:bg-none'
onClick={toggleFullScreen}>
<i
alt='full screen'
title='full screen'
className='cursor-pointer fas fa-expand group-hover:scale-125 transition-all duration-150 '
/>
<span className="h-full flex mx-2 md:hidden items-center select-none">
FullScreen
</span>
<span className='h-full flex mx-2 md:hidden items-center select-none'>FullScreen</span>
</div>
)
}

View File

@@ -1,27 +1,23 @@
/* eslint-disable @next/next/no-img-element */
import { AdSlot } from '@/components/GoogleAdsense'
import { deepClone } from '@/lib/utils'
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
import CONFIG from '../config'
/**
* 游戏列表
* @returns
*/
export const GameListIndexCombine = ({ games }) => {
const gamesClone = deepClone(games)
gamesClone?.sort((a, b) => {
const orderA = a.order || 999
const orderB = b.order || 999
return orderA - orderB
})
export const GameListIndexCombine = ({ posts }) => {
const gamesClone = deepClone(posts)
// 构造一个List<Component>
const components = []
// 根据序号随机大小;或根据game.recommend 决定
const recommend = true
const recommend = siteConfig('GAME_INDEX_EXPAND_RECOMMEND', true, CONFIG)
let index = 0
// 无限循环
@@ -40,7 +36,7 @@ export const GameListIndexCombine = ({ games }) => {
// 试图将4合一卡组塞满
while (gamesClone?.length > 0 && groupItems.length < 4) {
const item = gamesClone.shift()
if (item.recommend) {
if (item.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG))) {
components.push(<GameItem key={index} item={item} isLargeCard={true} />)
break
} else {
@@ -122,11 +118,14 @@ const GameItemGroup = ({ items }) => {
* @returns
*/
const GameItem = ({ item, isLargeCard }) => {
const { id, title, img, video } = item
const { title } = item
const img = item.pageCoverThumbnail
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const video = item?.ext?.video
return (
<a
href={`/game/${id}`}
<Link
href={`${url}`}
onMouseOver={() => {
setShowType('video')
}}
@@ -159,6 +158,6 @@ const GameItem = ({ item, isLargeCard }) => {
src={img}
alt={title}
/>
</a>
</Link>
)
}

View File

@@ -1,5 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { deepClone } from '@/lib/utils'
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
/**
@@ -38,12 +40,14 @@ export const GameListNormal = ({ games, maxCount = 18 }) => {
* @returns
*/
const GameItem = ({ item }) => {
const { id, title, img, video } = item
const { title, img } = item
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const video = item?.ext?.video
return (
<a
href={`/game/${id}`}
<Link
href={`${url}`}
onMouseOver={() => {
setShowType('video')
}}
@@ -64,6 +68,6 @@ const GameItem = ({ item }) => {
</video>
)}
<img className='w-full h-full absolute object-cover' src={img} alt={title} />
</a>
</Link>
)
}

View File

@@ -1,13 +1,15 @@
/* eslint-disable @next/next/no-img-element */
import { deepClone } from '@/lib/utils'
import { siteConfig } from '@/lib/config'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import Link from 'next/link'
import { useState } from 'react'
/**
* 游戏列表- 关联游戏,在详情页展示
* @returns
*/
export const GameListRelate = ({ games }) => {
const gamesClone = deepClone(games)
export const GameListRelate = ({ posts }) => {
const gamesClone = deepClone(posts)
// 构造一个List<Component>
const components = []
@@ -24,7 +26,7 @@ export const GameListRelate = ({ games }) => {
return (
<div className='game-list-wrapper w-full max-w-full overflow-x-auto'>
<div className='game-grid grid grid-flow-col gap-2'>
<div className='game-grid grid grid-flow-col justify-start gap-2'>
{components?.map((ItemComponent, index) => {
return ItemComponent
})}
@@ -38,13 +40,17 @@ export const GameListRelate = ({ games }) => {
* @param {*} param0
* @returns
*/
const GameItem = ({ item, isLargeCard }) => {
const { id, title, img, video } = item
const GameItem = ({ item }) => {
const { title } = item
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
return (
<a
href={`/game/${id}`}
<Link
href={`${url}`}
onMouseOver={() => {
setShowType('video')
}}
@@ -71,6 +77,6 @@ const GameItem = ({ item, isLargeCard }) => {
src={img}
alt={title}
/>
</a>
</Link>
)
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable @next/next/no-img-element */
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { deepClone } from '@/lib/utils'
import { checkContainHttp, deepClone, sliceUrlFromHttp } from '@/lib/utils'
import { useState } from 'react'
/**
@@ -48,12 +49,15 @@ export const GameListRecent = ({ maxCount = 14 }) => {
* @returns
*/
const GameItem = ({ item }) => {
const { id, title, img, video } = item || {}
const { title } = item || {}
const [showType, setShowType] = useState('img') // img or video
const url = checkContainHttp(item.slug) ? sliceUrlFromHttp(item.slug) : `${siteConfig('SUB_PATH', '')}/${item.slug}`
const img = item?.pageCoverThumbnail
const video = item?.ext?.video
return (
<a
href={`/game/${id}`}
href={`${url}`}
onMouseOver={() => {
setShowType('video')
}}

View File

@@ -1,6 +1,6 @@
import { useGlobal } from '@/lib/global'
import Image from 'next/image'
import Logo from './Logo'
import { useGlobal } from '@/lib/global'
/**
* 顶栏
@@ -9,22 +9,16 @@ import { useGlobal } from '@/lib/global'
export default function Header() {
const { setSideBarVisible } = useGlobal()
return (
<header className="z-20">
<div className="w-full h-16 rounded-md bg-[#1F2030] flex justify-between items-center px-4">
<header className='z-20'>
<div className='w-full h-16 rounded-md bg-white shadow-card dark:bg-[#1F2030] flex justify-between items-center px-4'>
<Logo />
<button
className="flex xl:hidden"
className='flex xl:hidden'
onClick={() => {
setSideBarVisible(true)
}}
>
<Image
src="/svg/search.svg"
className="mr-2"
width={20}
height={20}
/>
}}>
<Image src='/svg/search.svg' className='mr-2' width={20} height={20} />
</button>
</div>
</header>

View File

@@ -6,7 +6,7 @@ export default function Logo() {
return (
<Link passHref href='/' className='logo rounded cursor-pointer flex flex-col items-center'>
<div className='w-full'>
<h1 className='text-2xl text-white font-bold font-serif'>{siteConfig('TITLE')}</h1>
<h1 className='text-2xl dark:text-white font-bold font-serif'>{siteConfig('TITLE')}</h1>
<h2 className='text-xs text-gray-400 whitespace-nowrap'>{siteConfig('BIO')}</h2>
</div>
</Link>

View File

@@ -4,9 +4,9 @@ import { useGameGlobal } from '..'
export const MenuList = () => {
const { setSideBarVisible } = useGameGlobal()
return (
<div>
<div className='dark:text-white'>
<ul>
<li className='text-white py-4 px-2 font-bold hover:underline'>
<li className='py-4 px-2 font-bold hover:underline'>
<Link href='/' passHref>
<span className='flex items-center gap-2'>
<i className='fas fa-home' />
@@ -14,7 +14,7 @@ export const MenuList = () => {
</span>
</Link>
</li>
<li className='text-white py-4 px-2 font-bold hover:underline'>
<li className='py-4 px-2 font-bold hover:underline'>
<button
className='flex items-center gap-2'
onClick={() => {

View File

@@ -1,6 +1,9 @@
const CONFIG = {
GAME_NAV_NOTION_ICON: true, // 是否读取Notion图标作为站点头像 ; 否则默认显示黑色SVG方块
GAME_RECOMMEND_TAG: 'Recommend', // 打了此Tag被视为推荐
GAME_INDEX_EXPAND_RECOMMEND: true, // 首页列表是否将推荐游戏放大,否则随机放大。
// 特殊菜单
GAME_MENU_RANDOM_POST: true, // 是否显示随机跳转文章按钮
GAME_MENU_SEARCH_BUTTON: true, // 是否显示搜索按钮该按钮支持Algolia搜索

View File

@@ -1,21 +1,21 @@
import Comment from '@/components/Comment'
import CusdisComponent from '@/components/CusdisComponent'
import { Draggable } from '@/components/Draggable'
import { AdSlot } from '@/components/GoogleAdsense'
import replaceSearchResult from '@/components/Mark'
import NotionPage from '@/components/NotionPage'
import ShareBar from '@/components/ShareBar'
import { siteConfig } from '@/lib/config'
import { deepClone, isBrowser } from '@/lib/utils'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import Announcement from './components/Announcement'
import { ArticleFooter } from './components/ArticleFooter'
import { ArticleInfo } from './components/ArticleInfo'
import { ArticleLock } from './components/ArticleLock'
import BlogArchiveItem from './components/BlogArchiveItem'
import { BlogListPage } from './components/BlogListPage'
import { BlogListScroll } from './components/BlogListScroll'
import { Footer } from './components/Footer'
import FullScreen from './components/FullScreen'
import { GameListIndexCombine } from './components/GameListIndexCombine'
import { GameListRelate } from './components/GameListRealate'
import { GameListRecent } from './components/GameListRecent'
import Header from './components/Header'
import NavBar from './components/NavBar'
import SearchNavBar from './components/SearchNavBar'
@@ -45,7 +45,11 @@ const LayoutBase = props => {
// 在列表中进行实时过滤
const [filterKey, setFilterKey] = useState('')
const [filterGames, setFilterGames] = useState(deepClone(allNavPages?.filter(item => item.recommend)))
const [filterGames, setFilterGames] = useState(
deepClone(
allNavPages?.filter(item => item.tags?.some(t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG)))
)
)
const [recentGames, setRecentGames] = useState([])
const [sideBarVisible, setSideBarVisible] = useState(false)
@@ -68,7 +72,7 @@ const LayoutBase = props => {
}}>
<div
id='theme-game'
className={`${siteConfig('FONT_STYLE')} w-full h-full min-h-screen justify-center dark:text-gray-300 scroll-smooth`}>
className={`${siteConfig('FONT_STYLE')} w-full h-full min-h-screen justify-center bg-[#83FFE7] dark:bg-black dark:text-gray-300 scroll-smooth`}>
<Style />
{/* 左右布局 */}
<div id='wrapper' className={'relative flex justify-between w-full h-full mx-auto'}>
@@ -95,8 +99,9 @@ const LayoutBase = props => {
</div>
{/* 右侧 */}
<main className='flex-grow w-full overflow-x-scroll'>
<main className='flex-grow w-full overflow-x-auto'>
{children}
<div className='ads w-full justify-center flex p-2'>
<AdSlot type='flow' />
</div>
@@ -122,7 +127,13 @@ const LayoutBase = props => {
* @returns
*/
const LayoutIndex = props => {
return <LayoutPostList {...props} topSlot={<Announcement {...props} />} />
return (
<>
<GameListRecent />
<LayoutPostList {...props} />
<Announcement {...props} />
</>
)
}
/**
@@ -131,7 +142,7 @@ const LayoutIndex = props => {
* @returns
*/
const LayoutPostList = props => {
const { posts, topSlot, tag } = props
const { posts, tag } = props
const { filterKey } = useGameGlobal()
let filteredBlogPosts = []
if (filterKey && posts) {
@@ -146,7 +157,6 @@ const LayoutPostList = props => {
return (
<>
{topSlot}
{tag && <SearchNavBar {...props} />}
{siteConfig('POST_LIST_STYLE') === 'page' ? (
<BlogListPage {...props} posts={filteredBlogPosts} />
@@ -227,26 +237,58 @@ const LayoutArchive = props => {
* @returns
*/
const LayoutSlug = props => {
const { post, lock, validPassword } = props
const router = useRouter()
const { post, allNavPages, lock, validPassword } = props
const game = post
const [loading, setLoading] = useState(false)
// const [url, setUrl] = useState(game?.ext?.href)
// 替换成随机推荐和相关游戏
const relateGames = allNavPages
const randomGames = allNavPages
// 将当前游戏加入到最近游玩
useEffect(() => {
// 404
if (!post) {
setTimeout(
() => {
if (isBrowser) {
const article = document.getElementById('notion-article')
if (!article) {
router.push('/404').then(() => {
console.warn('找不到页面', router.asPath)
})
}
}
},
siteConfig('POST_WAITING_TIME_FOR_404') * 1000
)
// if (!url || url !== game?.ext?.href) {
// // 游戏路径
// setUrl(game?.ext?.href)
// }
// 更新最新游戏
const recentGames = localStorage.getItem('recent_games') ? JSON.parse(localStorage.getItem('recent_games')) : []
const existedIndex = recentGames.findIndex(item => item?.id === game?.id)
if (existedIndex === -1) {
recentGames.unshift(game) // 将游戏插入到数组头部
} else {
// 如果游戏已存在于数组中,将其移至数组头部
const existingGame = recentGames.splice(existedIndex, 1)[0]
recentGames.unshift(existingGame)
}
}, [post])
localStorage.setItem('recent_games', JSON.stringify(recentGames))
const iframe = document.getElementById('game-wrapper')
// 定义一个函数来处理iframe加载成功事件
function iframeLoaded() {
if (game) {
setLoading(false)
}
}
// 绑定加载事件
if (iframe.attachEvent) {
iframe.attachEvent('onload', iframeLoaded)
} else {
iframe.onload = iframeLoaded
}
// 更改iFrame的title
if (document?.getElementById('game-wrapper')?.contentDocument.querySelector('title')?.textContent) {
document.getElementById('game-wrapper').contentDocument.querySelector('title').textContent = `${
game?.title || ''
} - Play ${game?.title || ''} on ${siteConfig('TITLE')}`
}
}, [game])
return (
<>
{lock && <ArticleLock validPassword={validPassword} />}
@@ -254,11 +296,100 @@ const LayoutSlug = props => {
{!lock && (
<div id='article-wrapper' className='px-2'>
<>
<ArticleInfo post={post} />
{/* <ArticleInfo post={post} />
<NotionPage post={post} />
<ShareBar post={post} />
<Comment frontMatter={post} />
<ArticleFooter />
<ArticleFooter /> */}
{/* 游戏区域 */}
<div className='game-detail-wrapper w-full grow flex md:px-2'>
{/* 移动端返回主页按钮 */}
<Draggable stick='left'>
<div
style={{ left: '0px', top: '1rem' }}
className='fixed xl:hidden group space-x-1 flex items-center z-20 pr-3 pl-1 bg-[#202030] rounded-r-2xl shadow-lg '>
<Link href='/' className='px-1 py-3 hover:scale-125 duration-200 transition-all' passHref>
<i className='fas fa-arrow-left' />
</Link>{' '}
<span
className='text-white font-serif'
onClick={() => {
document.querySelector('.game-info').scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
})
}}>
G
</span>
</div>
</Draggable>
<div className='w-full py-1 md:py-4'>
<div className='bg-black w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md relative'>
{/* Loading遮罩 */}
{loading && (
<div className='absolute z-20 w-full xl:h-[calc(100vh-8rem)] h-screen rounded-md overflow-hidden '>
<div className='z-20 absolute bg-black bg-opacity-75 w-full h-full flex flex-col gap-4 justify-center items-center'>
<h2 className='text-3xl text-white flex gap-2'>
<i className='fas fa-spinner animate-spin'></i>
{siteConfig('TITLE')}
</h2>
<h3 className='text-xl text-white'>{siteConfig('DESCRIPTION')}</h3>
</div>
{/* 游戏封面图 */}
{/* eslint-disable-next-line @next/next/no-img-element */}
{game?.img && <img src={game?.img} className='w-full h-full blur-md absolute top-0 left-0 z-0' />}
</div>
)}
<iframe
id='game-wrapper'
className={`w-full xl:h-[calc(100vh-8rem)] h-screen md:rounded-md overflow-hidden ${game?.ext?.href ? '' : 'hidden'}`}
style={{
position: 'relative'
}}
src={game?.ext?.href}></iframe>
{/* 游戏窗口装饰器 */}
{game && !loading && (
<div className='game-decorator bg-[#0B0D14] right-0 bottom-0 flex justify-center h-12 md:w-12 z-10 md:absolute'>
{/* 加入全屏按钮 */}
<FullScreen />
</div>
)}
</div>
{/* 游戏资讯 */}
<div className='game-info dark:text-white py-4 mt-8 md:mt-0'>
<div className='w-full'>
<GameListRelate posts={relateGames} />
</div>
<h1 className='text-2xl px-2 py-2 xl:px-0'>{game?.title}</h1>
<h2 className='px-2 py-2 xl:px-0'>
Play {game?.title || ''} on {siteConfig('TITLE', '')}
</h2>
<p className='px-2 py-2 xl:px-0'>{siteConfig('DESCRIPTION')}</p>
<AdSlot />
{game && (
<div>
<div className='py-2 text-2xl dark:text-white'>Comment</div>
<CusdisComponent frontMatter={game} />
</div>
)}
</div>
</div>
</div>
<div className='xl:hidden py-2'>
<Header />
</div>
{/* 其它游戏列表 */}
<div>
<GameListIndexCombine posts={randomGames} />
</div>
</>
</div>
)}

View File

@@ -1,8 +1,8 @@
import BLOG, { LAYOUT_MAPPINGS } from '@/blog.config'
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
import dynamic from 'next/dynamic'
import getConfig from 'next/config'
import * as ThemeComponents from '@theme-components'
import getConfig from 'next/config'
import dynamic from 'next/dynamic'
import { getQueryParam, getQueryVariable, isBrowser } from '../lib/utils'
// 在next.config.js中扫描所有主题
export const { THEMES = [] } = getConfig().publicRuntimeConfig
@@ -100,7 +100,6 @@ export const initDarkMode = (updateDarkMode, defaultDarkMode) => {
// 查看localStorage中用户记录的是否深色模式
const userDarkMode = loadDarkModeFromLocalStorage()
console.log('深色模式',userDarkMode)
if (userDarkMode) {
newDarkMode = userDarkMode === 'dark' || userDarkMode === 'true'
}