mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-14 07:26:52 +00:00
Merge pull request #2183 from tangly1024/feat/theme-movie
Feat/theme movie
This commit is contained in:
11
.eslintrc.js
11
.eslintrc.js
@@ -4,11 +4,7 @@ module.exports = {
|
||||
es2021: true,
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:react/recommended',
|
||||
'plugin:@next/next/recommended',
|
||||
'standard'
|
||||
],
|
||||
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'],
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
jsx: true
|
||||
@@ -16,10 +12,7 @@ module.exports = {
|
||||
ecmaVersion: 12,
|
||||
sourceType: 'module'
|
||||
},
|
||||
plugins: [
|
||||
'react',
|
||||
'react-hooks'
|
||||
],
|
||||
plugins: ['react', 'react-hooks', 'prettier'],
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
"singleQuote": true,
|
||||
"semi": false,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true,
|
||||
"jsxSingleQuote": true,
|
||||
"jsxBracketSameLine": true
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createRef } from 'react'
|
||||
import { createRef } from 'react'
|
||||
import { init } from '@waline/client'
|
||||
import { useRouter } from 'next/router'
|
||||
import '@waline/client/dist/waline.css'
|
||||
@@ -21,7 +21,7 @@ const WalineComponent = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!waline) {
|
||||
waline = init({
|
||||
...props,
|
||||
|
||||
@@ -25,10 +25,7 @@ export { getPost } from '../notion/getNotionPost'
|
||||
* @returns
|
||||
*
|
||||
*/
|
||||
export async function getGlobalData({
|
||||
pageId = BLOG.NOTION_PAGE_ID,
|
||||
from
|
||||
}) {
|
||||
export async function getGlobalData({ pageId = BLOG.NOTION_PAGE_ID, from }) {
|
||||
// 从notion获取
|
||||
const data = await getNotionPageData({ pageId, from })
|
||||
const db = deepClone(data)
|
||||
@@ -89,7 +86,9 @@ function cleanBlock(post) {
|
||||
* @returns
|
||||
*/
|
||||
function getLatestPosts({ allPages, from, latestPostCount }) {
|
||||
const allPosts = allPages?.filter(page => page.type === 'Post' && page.status === 'Published')
|
||||
const allPosts = allPages?.filter(
|
||||
page => page.type === 'Post' && page.status === 'Published'
|
||||
)
|
||||
|
||||
const latestPosts = Object.create(allPosts).sort((a, b) => {
|
||||
const dateA = new Date(a?.lastEditedDate || a?.publishDate)
|
||||
@@ -140,7 +139,13 @@ function getCustomNav({ allPages }) {
|
||||
p.to = '/' + p.slug
|
||||
}
|
||||
}
|
||||
customNav.push({ icon: p.icon || null, name: p.title, to: p.slug, target: '_blank', show: true })
|
||||
customNav.push({
|
||||
icon: p.icon || null,
|
||||
name: p.title,
|
||||
to: p.slug,
|
||||
target: '_blank',
|
||||
show: true
|
||||
})
|
||||
})
|
||||
}
|
||||
return customNav
|
||||
@@ -152,7 +157,12 @@ function getCustomNav({ allPages }) {
|
||||
* @returns
|
||||
*/
|
||||
function getCustomMenu({ collectionData }) {
|
||||
const menuPages = collectionData.filter(post => post.status === 'Published' && (post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu || post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu))
|
||||
const menuPages = collectionData.filter(
|
||||
post =>
|
||||
post.status === 'Published' &&
|
||||
(post?.type === BLOG.NOTION_PROPERTY_NAME.type_menu ||
|
||||
post?.type === BLOG.NOTION_PROPERTY_NAME.type_sub_menu)
|
||||
)
|
||||
const menus = []
|
||||
if (menuPages && menuPages.length > 0) {
|
||||
menuPages.forEach(e => {
|
||||
@@ -190,7 +200,9 @@ function getCustomMenu({ collectionData }) {
|
||||
*/
|
||||
function getTagOptions(schema) {
|
||||
if (!schema) return {}
|
||||
const tagSchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.tags)
|
||||
const tagSchema = Object.values(schema).find(
|
||||
e => e.name === BLOG.NOTION_PROPERTY_NAME.tags
|
||||
)
|
||||
return tagSchema?.options || []
|
||||
}
|
||||
|
||||
@@ -201,7 +213,9 @@ function getTagOptions(schema) {
|
||||
*/
|
||||
function getCategoryOptions(schema) {
|
||||
if (!schema) return {}
|
||||
const categorySchema = Object.values(schema).find(e => e.name === BLOG.NOTION_PROPERTY_NAME.category)
|
||||
const categorySchema = Object.values(schema).find(
|
||||
e => e.name === BLOG.NOTION_PROPERTY_NAME.category
|
||||
)
|
||||
return categorySchema?.options || []
|
||||
}
|
||||
|
||||
@@ -213,9 +227,15 @@ function getCategoryOptions(schema) {
|
||||
*/
|
||||
function getSiteInfo({ collection, block }) {
|
||||
const title = collection?.name?.[0][0] || BLOG.TITLE
|
||||
const description = collection?.description ? Object.assign(collection).description[0][0] : BLOG.DESCRIPTION
|
||||
const pageCover = collection?.cover ? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value) : BLOG.HOME_BANNER_IMAGE
|
||||
let icon = collection?.icon ? mapImgUrl(collection?.icon, collection, 'collection') : BLOG.AVATAR
|
||||
const description = collection?.description
|
||||
? Object.assign(collection).description[0][0]
|
||||
: BLOG.DESCRIPTION
|
||||
const pageCover = collection?.cover
|
||||
? mapImgUrl(collection?.cover, block[idToUuid(BLOG.NOTION_PAGE_ID)]?.value)
|
||||
: BLOG.HOME_BANNER_IMAGE
|
||||
let icon = collection?.icon
|
||||
? mapImgUrl(collection?.icon, collection, 'collection')
|
||||
: BLOG.AVATAR
|
||||
|
||||
// 用户头像压缩一下
|
||||
icon = compressImage(icon)
|
||||
@@ -236,7 +256,13 @@ function getSiteInfo({ collection, block }) {
|
||||
*/
|
||||
export function getNavPages({ allPages }) {
|
||||
const allNavPages = allPages?.filter(post => {
|
||||
return post && post?.slug && (!post?.slug?.startsWith('http')) && post?.type === 'Post' && post?.status === 'Published'
|
||||
return (
|
||||
post &&
|
||||
post?.slug &&
|
||||
!post?.slug?.startsWith('http') &&
|
||||
post?.type === 'Post' &&
|
||||
post?.status === 'Published'
|
||||
)
|
||||
})
|
||||
|
||||
return allNavPages.map(item => ({
|
||||
@@ -248,7 +274,8 @@ export function getNavPages({ allPages }) {
|
||||
summary: item.summary || null,
|
||||
slug: item.slug,
|
||||
pageIcon: item.pageIcon || '',
|
||||
lastEditedDate: item.lastEditedDate
|
||||
lastEditedDate: item.lastEditedDate,
|
||||
publishDate: item.publishDate
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -265,19 +292,26 @@ async function getNotice(post) {
|
||||
}
|
||||
|
||||
// 没有数据时返回
|
||||
const EmptyData = (pageId) => {
|
||||
const EmptyData = pageId => {
|
||||
const empty = {
|
||||
notice: null,
|
||||
siteInfo: getSiteInfo({}),
|
||||
allPages: [{
|
||||
id: 1,
|
||||
title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`,
|
||||
summary: '访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next',
|
||||
status: 'Published',
|
||||
type: 'Post',
|
||||
slug: '13a171332816461db29d50e9f575b00d',
|
||||
date: { start_date: '2023-04-24', lastEditedDay: '2023-04-24', tagItems: [] }
|
||||
}],
|
||||
allPages: [
|
||||
{
|
||||
id: 1,
|
||||
title: `无法获取Notion数据,请检查Notion_ID: \n 当前 ${pageId}`,
|
||||
summary:
|
||||
'访问文档获取帮助→ https://tangly1024.com/article/vercel-deploy-notion-next',
|
||||
status: 'Published',
|
||||
type: 'Post',
|
||||
slug: '13a171332816461db29d50e9f575b00d',
|
||||
date: {
|
||||
start_date: '2023-04-24',
|
||||
lastEditedDay: '2023-04-24',
|
||||
tagItems: []
|
||||
}
|
||||
}
|
||||
],
|
||||
allNavPages: [],
|
||||
collection: [],
|
||||
collectionQuery: {},
|
||||
@@ -313,7 +347,8 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
const rawMetadata = block[pageId]?.value
|
||||
// Check Type Page-Database和Inline-Database
|
||||
if (
|
||||
rawMetadata?.type !== 'collection_view_page' && rawMetadata?.type !== 'collection_view'
|
||||
rawMetadata?.type !== 'collection_view_page' &&
|
||||
rawMetadata?.type !== 'collection_view'
|
||||
) {
|
||||
console.error(`pageId "${pageId}" is not a database`)
|
||||
return EmptyData(pageId)
|
||||
@@ -328,9 +363,21 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
const viewIds = rawMetadata?.view_ids
|
||||
const collectionData = []
|
||||
|
||||
const pageIds = getAllPageIds(collectionQuery, collectionId, collectionView, viewIds)
|
||||
const pageIds = getAllPageIds(
|
||||
collectionQuery,
|
||||
collectionId,
|
||||
collectionView,
|
||||
viewIds
|
||||
)
|
||||
if (pageIds?.length === 0) {
|
||||
console.error('获取到的文章列表为空,请检查notion模板', collectionQuery, collection, collectionView, viewIds, pageRecordMap)
|
||||
console.error(
|
||||
'获取到的文章列表为空,请检查notion模板',
|
||||
collectionQuery,
|
||||
collection,
|
||||
collectionView,
|
||||
viewIds,
|
||||
pageRecordMap
|
||||
)
|
||||
} else {
|
||||
// console.log('有效Page数量', pageIds?.length)
|
||||
}
|
||||
@@ -343,7 +390,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
// 如果找不到文章对应的block,说明发生了溢出,使用pageID再去请求
|
||||
const pageBlock = await getSingleBlock(id, from)
|
||||
if (pageBlock.block[id].value) {
|
||||
const properties = (await getPageProperties(id, pageBlock.block[id].value, schema, null, getTagOptions(schema))) || null
|
||||
const properties =
|
||||
(await getPageProperties(
|
||||
id,
|
||||
pageBlock.block[id].value,
|
||||
schema,
|
||||
null,
|
||||
getTagOptions(schema)
|
||||
)) || null
|
||||
if (properties) {
|
||||
collectionData.push(properties)
|
||||
}
|
||||
@@ -351,7 +405,14 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
continue
|
||||
}
|
||||
|
||||
const properties = (await getPageProperties(id, value, schema, null, getTagOptions(schema))) || null
|
||||
const properties =
|
||||
(await getPageProperties(
|
||||
id,
|
||||
value,
|
||||
schema,
|
||||
null,
|
||||
getTagOptions(schema)
|
||||
)) || null
|
||||
if (properties) {
|
||||
collectionData.push(properties)
|
||||
}
|
||||
@@ -365,13 +426,16 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
if (post?.type === 'Post' && post.status === 'Published') {
|
||||
postCount++
|
||||
}
|
||||
return post && post?.slug &&
|
||||
(!post?.slug?.startsWith('http')) &&
|
||||
return (
|
||||
post &&
|
||||
post?.slug &&
|
||||
!post?.slug?.startsWith('http') &&
|
||||
(post?.status === 'Invisible' || post?.status === 'Published')
|
||||
)
|
||||
})
|
||||
|
||||
// 站点配置优先读取配置表格,否则读取blog.config.js 文件
|
||||
const NOTION_CONFIG = await getConfigMapFromConfigPage(collectionData) || {}
|
||||
const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {}
|
||||
|
||||
// Sort by date
|
||||
if (BLOG.POSTS_SORT_BY === 'date') {
|
||||
@@ -380,13 +444,27 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) {
|
||||
})
|
||||
}
|
||||
|
||||
const notice = await getNotice(collectionData.filter(post => {
|
||||
return post && post?.type && post?.type === 'Notice' && post.status === 'Published'
|
||||
})?.[0])
|
||||
const categoryOptions = getAllCategories({ allPages, categoryOptions: getCategoryOptions(schema) })
|
||||
const notice = await getNotice(
|
||||
collectionData.filter(post => {
|
||||
return (
|
||||
post &&
|
||||
post?.type &&
|
||||
post?.type === 'Notice' &&
|
||||
post.status === 'Published'
|
||||
)
|
||||
})?.[0]
|
||||
)
|
||||
const categoryOptions = getAllCategories({
|
||||
allPages,
|
||||
categoryOptions: getCategoryOptions(schema)
|
||||
})
|
||||
const tagOptions = getAllTags({ allPages, tagOptions: getTagOptions(schema) })
|
||||
// 旧的菜单
|
||||
const customNav = getCustomNav({ allPages: collectionData.filter(post => post?.type === 'Page' && post.status === 'Published') })
|
||||
const customNav = getCustomNav({
|
||||
allPages: collectionData.filter(
|
||||
post => post?.type === 'Page' && post.status === 'Published'
|
||||
)
|
||||
})
|
||||
// 新的菜单
|
||||
const customMenu = await getCustomMenu({ collectionData })
|
||||
const latestPosts = getLatestPosts({ allPages, from, latestPostCount: 6 })
|
||||
|
||||
@@ -57,9 +57,11 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-next": "^13.1.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"eslint-plugin-import": "^2.23.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
@@ -76,4 +78,4 @@
|
||||
"url": "https://github.com/tangly/NotionNext/issues",
|
||||
"email": "tlyong1992@hotmail.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
public/videos/dplayer.html
Normal file
50
public/videos/dplayer.html
Normal file
@@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DPlayer Video Player</title>
|
||||
<!-- 引入 DPlayer 样式文件 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.css">
|
||||
<style>
|
||||
body {
|
||||
margin: 0px 0px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 创建一个容器用于放置视频播放器 -->
|
||||
<div id="dplayer-container"></div>
|
||||
<!-- 引入 Hls.js 库 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
||||
|
||||
<!-- 引入 DPlayer JavaScript 文件 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>
|
||||
|
||||
<script>
|
||||
var myParam = location.search.split('n=')[1]
|
||||
if(!myParam){
|
||||
alert('无效的视频地址')
|
||||
}
|
||||
// 创建 DPlayer 实例
|
||||
var dp = new DPlayer({
|
||||
// 定义容器
|
||||
container: document.getElementById('dplayer-container'),
|
||||
// 视频源地址
|
||||
video: {
|
||||
url: myParam
|
||||
// 如果有多个清晰度,可以在这里添加更多的清晰度选项
|
||||
// quality: [
|
||||
// { name: 'HD', url: 'https://example.com/your-video-hd.mp4', type: 'normal' },
|
||||
// { name: 'SD', url: 'https://example.com/your-video-sd.mp4', type: 'normal' }
|
||||
// ],
|
||||
},
|
||||
// 视频封面图片
|
||||
poster: 'https://example.com/your-video-poster.jpg',
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -17,105 +17,126 @@ const PaginationNumber = ({ page, totalPage }) => {
|
||||
const currentPage = +page
|
||||
const showNext = page < totalPage
|
||||
const showPrev = currentPage !== 1
|
||||
const pagePrefix = router.asPath.split('?')[0].replace(/\/page\/[1-9]\d*/, '').replace(/\/$/, '')
|
||||
const pagePrefix = router.asPath
|
||||
.split('?')[0]
|
||||
.replace(/\/page\/[1-9]\d*/, '')
|
||||
.replace(/\/$/, '')
|
||||
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
||||
|
||||
const [value, setValue] = useState('')
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
const handleInputChange = event => {
|
||||
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调到指定页
|
||||
*/
|
||||
* 调到指定页
|
||||
*/
|
||||
const jumpToPage = () => {
|
||||
if (value) {
|
||||
router.push(value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`)
|
||||
router.push(
|
||||
value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (<>
|
||||
|
||||
{/* pc端分页按钮 */}
|
||||
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: currentPage === 2 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'}`}>
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
||||
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
||||
{locale.PAGINATION.PREV}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className='flex items-center space-x-2'>
|
||||
{pages}
|
||||
|
||||
{/* 跳转页码 */}
|
||||
<div className='bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200'>
|
||||
<input value={value} className='w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg' onInput={handleInputChange}></input>
|
||||
<div onClick={jumpToPage} className='cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white'>
|
||||
<ChevronDoubleRight className={'w-4 h-4'} />
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
{/* pc端分页按钮 */}
|
||||
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'}`}
|
||||
>
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
||||
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
||||
{locale.PAGINATION.PREV}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'invisible'} `}>
|
||||
{/* 分页 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{pages}
|
||||
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
||||
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
||||
{locale.PAGINATION.NEXT}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
{/* 跳转页码 */}
|
||||
<div className="bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200">
|
||||
<input
|
||||
value={value}
|
||||
className="w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg"
|
||||
onInput={handleInputChange}
|
||||
></input>
|
||||
<div
|
||||
onClick={jumpToPage}
|
||||
className="cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white"
|
||||
>
|
||||
<ChevronDoubleRight className={'w-4 h-4'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 移动端分页 */}
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'invisible'} `}
|
||||
>
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
||||
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
||||
{locale.PAGINATION.NEXT}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='lg:hidden w-full flex flex-row'>
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: currentPage === 2 ? `${pagePrefix}/` : `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}>
|
||||
{locale.PAGINATION.PREV}
|
||||
</Link>
|
||||
{/* 移动端分页 */}
|
||||
|
||||
{showPrev && showNext && <div className='w-12'></div>}
|
||||
<div className="lg:hidden w-full flex flex-row">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||
>
|
||||
{locale.PAGINATION.PREV}
|
||||
</Link>
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}>
|
||||
{locale.PAGINATION.NEXT}
|
||||
</Link>
|
||||
</div>
|
||||
</>)
|
||||
{showPrev && showNext && <div className="w-12"></div>}
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,21 +148,23 @@ const PaginationNumber = ({ page, totalPage }) => {
|
||||
*/
|
||||
function getPageElement(page, currentPage, pagePrefix) {
|
||||
const selected = page + '' === currentPage + ''
|
||||
if (!page) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
(<Link
|
||||
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
||||
key={page}
|
||||
passHref
|
||||
className={
|
||||
(selected
|
||||
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
||||
: 'dark:bg-[#1e1e1e] bg-white') +
|
||||
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
||||
}>
|
||||
|
||||
{page}
|
||||
|
||||
</Link>)
|
||||
<Link
|
||||
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
||||
key={page}
|
||||
passHref
|
||||
className={
|
||||
(selected
|
||||
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
||||
: 'dark:bg-[#1e1e1e] bg-white') +
|
||||
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
||||
}
|
||||
>
|
||||
{page}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -171,7 +194,11 @@ function generatePages(pagePrefix, page, currentPage, totalPage) {
|
||||
startPage = totalPage - dynamicGroupCount
|
||||
}
|
||||
if (startPage > 2) {
|
||||
pages.push(<div key={-1} className='-mt-2 mx-1'>... </div>)
|
||||
pages.push(
|
||||
<div key={-1} className="-mt-2 mx-1">
|
||||
...{' '}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Link from 'next/link'
|
||||
import CONFIG from '../config'
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import CONFIG from '../config'
|
||||
|
||||
/**
|
||||
* 关联推荐文章
|
||||
@@ -13,50 +13,52 @@ import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
export default function ArticleRecommend({ recommendPosts, siteInfo }) {
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (
|
||||
!siteConfig('HEXO_ARTICLE_RECOMMEND', null, CONFIG) ||
|
||||
!recommendPosts ||
|
||||
recommendPosts.length === 0
|
||||
) {
|
||||
if (!siteConfig('HEXO_ARTICLE_RECOMMEND', null, CONFIG) || !recommendPosts || recommendPosts.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-8">
|
||||
<div className=" mb-2 px-1 flex flex-nowrap justify-between">
|
||||
<div className='dark:text-gray-300'>
|
||||
<i className="mr-2 fas fa-thumbs-up" />
|
||||
{locale.COMMON.RELATE_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{recommendPosts.map(post => {
|
||||
const headerImage = post?.pageCoverThumbnail
|
||||
? post.pageCoverThumbnail
|
||||
: siteInfo?.pageCover
|
||||
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return (
|
||||
(<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={url}
|
||||
passHref
|
||||
className="flex h-40 cursor-pointer overflow-hidden">
|
||||
|
||||
<div className="h-full w-full relative group">
|
||||
<div className="flex items-center justify-center w-full h-full duration-300 ">
|
||||
<div className="z-10 text-lg px-4 font-bold text-white text-center shadow-text select-none">
|
||||
{post.title}
|
||||
</div>
|
||||
</div>
|
||||
<LazyImage src={headerImage} className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200' />
|
||||
</div>
|
||||
|
||||
</Link>)
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className='pt-8'>
|
||||
<div className=' mb-2 px-1 flex flex-nowrap justify-between'>
|
||||
<div className='dark:text-gray-300'>
|
||||
<i className='mr-2 fas fa-thumbs-up' />
|
||||
{locale.COMMON.RELATE_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 md:grid-cols-3 gap-4'>
|
||||
{recommendPosts.map(post => {
|
||||
const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
|
||||
const url = checkContainHttp(post.slug)
|
||||
? sliceUrlFromHttp(post.slug)
|
||||
: `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={url}
|
||||
passHref
|
||||
className='flex h-40 cursor-pointer overflow-hidden'>
|
||||
<div className='h-full w-full relative group'>
|
||||
<div className='flex items-center justify-center w-full h-full duration-300 '>
|
||||
<div className='z-10 text-lg px-4 font-bold text-white text-center shadow-text select-none'>
|
||||
{post.title}
|
||||
</div>
|
||||
</div>
|
||||
<LazyImage
|
||||
src={headerImage}
|
||||
className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200'
|
||||
/>
|
||||
|
||||
{/* 卡片的阴影遮罩,为了凸显图片上的文字 */}
|
||||
<div className='h-3/4 w-full absolute left-0 bottom-0'>
|
||||
<div className='h-full w-full absolute opacity-80 group-hover:opacity-100 transition-all duration-1000 bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
22
themes/movie/components/Announcement.js
Normal file
22
themes/movie/components/Announcement.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const NotionPage = dynamic(() => import('@/components/NotionPage'))
|
||||
|
||||
const Announcement = ({ post, className }) => {
|
||||
const { locale } = useGlobal()
|
||||
if (!post || Object.keys(post).length === 0) {
|
||||
return <></>
|
||||
}
|
||||
return <aside className="rounded shadow overflow-hidden mb-6">
|
||||
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">
|
||||
<i className="mr-2 fas fa-bullhorn" />{locale.COMMON.ANNOUNCEMENT}
|
||||
</h3>
|
||||
|
||||
{post && (<div id="announcement-content">
|
||||
<NotionPage post={post} className='text-center ' />
|
||||
</div>)}
|
||||
</aside>
|
||||
}
|
||||
export default Announcement
|
||||
38
themes/movie/components/ArchiveDateList.js
Normal file
38
themes/movie/components/ArchiveDateList.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { formatDateFmt } from '@/lib/utils/formatDate'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ArchiveDateList(props) {
|
||||
const postsSortByDate = Object.create(props.allNavPages)
|
||||
const { locale } = useGlobal()
|
||||
|
||||
postsSortByDate.sort((a, b) => {
|
||||
return b?.publishDate - a?.publishDate
|
||||
})
|
||||
|
||||
let dates = []
|
||||
postsSortByDate.forEach(post => {
|
||||
const date = formatDateFmt(post.publishDate, 'yyyy-MM')
|
||||
if (!dates[date]) {
|
||||
dates.push(date)
|
||||
}
|
||||
})
|
||||
dates = dates.slice(0, 5)
|
||||
return (
|
||||
<div>
|
||||
<div className="text-2xl dark:text-white mb-2">{locale.NAV.ARCHIVE}</div>
|
||||
{dates?.map((date, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<Link
|
||||
href={`/archive#${date}`}
|
||||
className="hover:underline dark:text-green-500"
|
||||
>
|
||||
{date}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
46
themes/movie/components/ArticleInfo.js
Normal file
46
themes/movie/components/ArticleInfo.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { formatDateFmt } from '@/lib/utils/formatDate'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const ArticleInfo = props => {
|
||||
const { post } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
return (
|
||||
<section className='w-full mx-auto mb-4'>
|
||||
<h2 className='text-5xl font-semibold py-10 dark:text-white text-center'>{post?.title}</h2>
|
||||
|
||||
<div className='flex gap-3 font-semibold text-sm items-center justify-center'>
|
||||
<Link
|
||||
href={`/archive#${formatDateFmt(post?.publishDate, 'yyyy-MM')}`}
|
||||
passHref
|
||||
className='pl-1 mr-2 cursor-pointer'>
|
||||
{post?.publishDay}
|
||||
</Link>
|
||||
|
||||
{post?.type !== 'Page' && (
|
||||
<>
|
||||
<Link href={`/category/${post?.category}`} passHref className='cursor-pointer text-md mr-2 text-green-500'>
|
||||
{post?.category}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className='flex py-1 space-x-3'>
|
||||
{post?.tags?.length > 0 && (
|
||||
<>
|
||||
{locale.COMMON.TAGS} <span>:</span>
|
||||
</>
|
||||
)}
|
||||
{post?.tags?.map(tag => {
|
||||
return (
|
||||
<Link href={`/tag/${tag}`} key={tag} className='text-yellow-500 mr-2'>
|
||||
{tag}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
52
themes/movie/components/ArticleLock.js
Normal file
52
themes/movie/components/ArticleLock.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
/**
|
||||
* 加密文章校验组件
|
||||
* @param {password, validPassword} props
|
||||
* @param password 正确的密码
|
||||
* @param validPassword(bool) 回调函数,校验正确回调入参为true
|
||||
* @returns
|
||||
*/
|
||||
export const ArticleLock = props => {
|
||||
const { validPassword } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const submitPassword = () => {
|
||||
const p = document.getElementById('password')
|
||||
if (!validPassword(p?.value)) {
|
||||
const tips = document.getElementById('tips')
|
||||
if (tips) {
|
||||
tips.innerHTML = ''
|
||||
tips.innerHTML = `<div class='text-red-500 animate__shakeX animate__animated'>${locale.COMMON.PASSWORD_ERROR}</div>`
|
||||
}
|
||||
}
|
||||
}
|
||||
const passwordInputRef = useRef(null)
|
||||
useEffect(() => {
|
||||
// 选中密码输入框并将其聚焦
|
||||
passwordInputRef.current.focus()
|
||||
}, [])
|
||||
|
||||
return <div id='container' className='w-full flex justify-center items-center h-96 '>
|
||||
<div className='text-center space-y-3'>
|
||||
<div className='font-bold'>{locale.COMMON.ARTICLE_LOCK_TIPS}</div>
|
||||
<div className='flex mx-4'>
|
||||
<input id="password" type='password'
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitPassword()
|
||||
}
|
||||
}}
|
||||
ref={passwordInputRef} // 绑定ref到passwordInputRef变量
|
||||
className='outline-none w-full text-sm pl-5 rounded-l transition focus:shadow-lg font-light leading-10 text-black dark:bg-gray-500 bg-gray-50'
|
||||
></input>
|
||||
<div onClick={submitPassword} className="px-3 whitespace-nowrap cursor-pointer items-center justify-center py-2 rounded-r duration-300 bg-gray-300" >
|
||||
<i className={'duration-200 cursor-pointer fas fa-key dark:text-black'} > {locale.COMMON.SUBMIT}</i>
|
||||
</div>
|
||||
</div>
|
||||
<div id='tips'>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
38
themes/movie/components/BlogListGroupByDate.js
Normal file
38
themes/movie/components/BlogListGroupByDate.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 按照日期将文章分组
|
||||
* 归档页面用到
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlogListGroupByDate({ archiveTitle, archivePosts }) {
|
||||
return <div key={archiveTitle}>
|
||||
<div id={archiveTitle} className="pt-16 pb-4 text-3xl dark:text-gray-300" >
|
||||
{archiveTitle}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{archivePosts[archiveTitle].map(post => {
|
||||
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return <li
|
||||
key={post.id}
|
||||
className="border-l-2 p-1 text-xs md:text-base items-center hover:scale-x-105 hover:border-gray-500 dark:hover:border-gray-300 dark:border-gray-400 transform duration-500"
|
||||
>
|
||||
<div id={post?.publishDay}>
|
||||
<span className="text-gray-400">
|
||||
{post?.publishDay}
|
||||
</span>{' '}
|
||||
|
||||
<Link href={url} className="dark:text-gray-400 dark:hover:text-gray-300 overflow-x-hidden hover:underline cursor-pointer text-gray-600">
|
||||
{post.title}
|
||||
</Link>
|
||||
</div>
|
||||
</li>
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
25
themes/movie/components/BlogListPage.js
Normal file
25
themes/movie/components/BlogListPage.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import CONFIG from '../config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
import PaginationNumber from './PaginationNumber'
|
||||
|
||||
export const BlogListPage = props => {
|
||||
const { page = 1, posts, postCount } = props
|
||||
const totalPage = Math.ceil(postCount / parseInt(siteConfig('POSTS_PER_PAGE')))
|
||||
|
||||
const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG)
|
||||
|
||||
return (
|
||||
<div className={`w-full ${showPageCover ? 'md:pr-2' : 'md:pr-12'} mb-12`}>
|
||||
<div
|
||||
id='posts-wrapper'
|
||||
className='grid md:grid-cols-2 md:gap-12 lg:grid-cols-3 lg:gap-20 xl:gap-24 2xl:grid-cols-4'>
|
||||
{posts?.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PaginationNumber page={page} totalPage={totalPage} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
themes/movie/components/BlogListScroll.js
Normal file
60
themes/movie/components/BlogListScroll.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import CONFIG from '../config'
|
||||
import BlogPostCard from './BlogPostCard'
|
||||
|
||||
export const BlogListScroll = props => {
|
||||
const { posts } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
const [page, updatePage] = useState(1)
|
||||
|
||||
let hasMore = false
|
||||
const postsToShow = posts ? Object.assign(posts).slice(0, parseInt(siteConfig('POSTS_PER_PAGE')) * page) : []
|
||||
|
||||
if (posts) {
|
||||
const totalCount = posts.length
|
||||
hasMore = page * parseInt(siteConfig('POSTS_PER_PAGE')) < totalCount
|
||||
}
|
||||
const handleGetMore = () => {
|
||||
if (!hasMore) return
|
||||
updatePage(page + 1)
|
||||
}
|
||||
|
||||
const targetRef = useRef(null)
|
||||
|
||||
// 监听滚动自动分页加载
|
||||
const scrollTrigger = useCallback(
|
||||
throttle(() => {
|
||||
const scrollS = window.scrollY + window.outerHeight
|
||||
const clientHeight = targetRef ? (targetRef.current ? targetRef.current.clientHeight : 0) : 0
|
||||
if (scrollS > clientHeight + 100) {
|
||||
handleGetMore()
|
||||
}
|
||||
}, 500)
|
||||
)
|
||||
const showPageCover = siteConfig('MOVIE_POST_LIST_COVER', null, CONFIG)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('scroll', scrollTrigger)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', scrollTrigger)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div id='posts-wrapper' className={`w-full ${showPageCover ? 'md:pr-2' : 'md:pr-12'}} mb-12`} ref={targetRef}>
|
||||
{postsToShow?.map(post => (
|
||||
<BlogPostCard key={post.id} post={post} />
|
||||
))}
|
||||
|
||||
<div onClick={handleGetMore} className='w-full my-4 py-4 text-center cursor-pointer '>
|
||||
{' '}
|
||||
{hasMore ? locale.COMMON.MORE : `${locale.COMMON.NO_MORE} 😰`}{' '}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
60
themes/movie/components/BlogPostCard.js
Normal file
60
themes/movie/components/BlogPostCard.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
import NotionIcon from '@/components/NotionIcon'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import TagItemMini from './TagItemMini'
|
||||
|
||||
const BlogPostCard = ({ index, post, showSummary, siteInfo }) => {
|
||||
// 主题默认强制显示图片
|
||||
if (post && !post.pageCoverThumbnail) {
|
||||
post.pageCoverThumbnail = siteInfo?.pageCover || siteConfig('RANDOM_IMAGE_URL')
|
||||
}
|
||||
|
||||
const url = checkContainHttp(post.slug) ? sliceUrlFromHttp(post.slug) : `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return (
|
||||
<article data-wow-delay=".2s" className='wow fadeInUp w-full mb-4 cursor-pointer overflow-hidden shadow-movie dark:bg-hexo-black-gray'>
|
||||
<Link href={url} passHref legacyBehavior>
|
||||
{/* 固定高度 ,空白用图片拉升填充 */}
|
||||
<div className='group flex flex-col aspect-[2/3] justify-between relative'>
|
||||
{/* 图片 填充卡片 */}
|
||||
<div className='flex flex-grow w-full h-full relative duration-200 cursor-pointer transform overflow-hidden'>
|
||||
<LazyImage
|
||||
src={post?.pageCoverThumbnail}
|
||||
alt={post.title}
|
||||
className='h-full w-full group-hover:brightness-90 group-hover:scale-105 transform object-cover duration-500'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='absolute bottom-28 z-20'>
|
||||
{post?.tagItems && post?.tagItems.length > 0 && (
|
||||
<>
|
||||
<div className='px-6 justify-between flex p-2'>
|
||||
{post.tagItems.map(tag => (
|
||||
<TagItemMini key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{/* 阴影遮罩 */}
|
||||
<h2 className='absolute bottom-10 px-6 transition-all duration-200 text-white text-2xl font-semibold break-words shadow-text z-20'>
|
||||
<NotionIcon icon={post.pageIcon} />
|
||||
{post.title}
|
||||
</h2>
|
||||
|
||||
<p className='absolute bottom-3 z-20 line-clamp-1 text-xs mx-6'>
|
||||
{post?.summary}
|
||||
</p>
|
||||
|
||||
<div className='h-3/4 w-full absolute left-0 bottom-0 z-10'>
|
||||
<div className='h-full w-full absolute opacity-80 group-hover:opacity-100 transition-all duration-1000 bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostCard
|
||||
62
themes/movie/components/BlogRecommend.js
Normal file
62
themes/movie/components/BlogRecommend.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import LazyImage from '@/components/LazyImage'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import CONFIG from '../config'
|
||||
|
||||
/**
|
||||
* 关联推荐文章
|
||||
* @param {prev,next} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function BlogRecommend(props) {
|
||||
const { recommendPosts, siteInfo } = props
|
||||
const { locale } = useGlobal()
|
||||
if (!siteConfig('MOVIE_ARTICLE_RECOMMEND', null, CONFIG) || !recommendPosts || recommendPosts.length === 0) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='py-8'>
|
||||
<div className=' mb-2 px-1 flex flex-nowrap justify-between'>
|
||||
<div className='dark:text-gray-300'>
|
||||
<i className='mr-2 fas fa-thumbs-up' />
|
||||
{locale.COMMON.RELATE_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-nowrap gap-4'>
|
||||
{recommendPosts.map(post => {
|
||||
const headerImage = post?.pageCoverThumbnail ? post.pageCoverThumbnail : siteInfo?.pageCover
|
||||
const url = checkContainHttp(post.slug)
|
||||
? sliceUrlFromHttp(post.slug)
|
||||
: `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={url}
|
||||
passHref
|
||||
className='flex rounded-lg h-60 w-48 cursor-pointer overflow-hidden'>
|
||||
<div className='h-full w-full relative group shadow-movie'>
|
||||
<div className='absolute bottom-4 w-full z-20 duration-300 '>
|
||||
<div className='z-10 text-lg px-4 font-bold text-white shadow-text select-none'>{post.title}</div>
|
||||
</div>
|
||||
{/* 卡片的阴影遮罩,为了凸显图片上的文字 */}
|
||||
<div className='h-3/4 w-full absolute left-0 bottom-0 z-10'>
|
||||
<div className='h-full w-full absolute opacity-80 group-hover:opacity-100 transition-all duration-1000 bg-gradient-to-b from-transparent to-black'></div>
|
||||
</div>
|
||||
|
||||
<LazyImage
|
||||
src={headerImage}
|
||||
className='absolute top-0 w-full h-full object-cover object-center group-hover:scale-110 group-hover:brightness-50 transform duration-200'
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
43
themes/movie/components/CategoryGroup.js
Normal file
43
themes/movie/components/CategoryGroup.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
|
||||
const CategoryGroup = props => {
|
||||
const { currentCategory, categoryOptions } = props
|
||||
const { locale } = useGlobal()
|
||||
if (!categoryOptions || categoryOptions.length === 0) return <></>
|
||||
const categoryCount = siteConfig('PREVIEW_CATEGORY_COUNT')
|
||||
const categories = categoryOptions.slice(0, categoryCount)
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<h2 className="text-2xl dark:text-white">{locale.COMMON.CATEGORY}</h2>
|
||||
<div id="category-list" className="dark:border-gray-600 flex flex-col">
|
||||
{categories.map(category => {
|
||||
const selected = currentCategory === category.name
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
className={
|
||||
(selected
|
||||
? 'hover:text-white dark:hover:text-white bg-gray-600 text-white '
|
||||
: 'dark:text-green-400 text-gray-500 hover:text-white hover:bg-gray-500 dark:hover:text-white') +
|
||||
' w-full items-center duration-300 px-2 cursor-pointer py-1 font-light'
|
||||
}
|
||||
>
|
||||
<i
|
||||
className={`${selected ? 'text-white fa-folder-open ' : 'text-gray-500 fa-folder '} mr-2 fas`}
|
||||
/>
|
||||
{category.name}({category.count})
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CategoryGroup
|
||||
20
themes/movie/components/CategoryItem.js
Normal file
20
themes/movie/components/CategoryItem.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 文章分类
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function CategoryItem({ category }) {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<div className={'text-2xl hover:text-black dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600 px-5 cursor-pointer py-2 hover:bg-gray-100'}>
|
||||
<i className='mr-4 fas fa-folder' />{category.name}({category.count})
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
35
themes/movie/components/ExampleRecentComments.js
Normal file
35
themes/movie/components/ExampleRecentComments.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Link from 'next/link'
|
||||
import { RecentComments } from '@waline/client'
|
||||
|
||||
/**
|
||||
* @see https://waline.js.org/guide/get-started.html
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const ExampleRecentComments = (props) => {
|
||||
const [comments, updateComments] = useState([])
|
||||
const [onLoading, changeLoading] = useState(true)
|
||||
useEffect(() => {
|
||||
RecentComments({
|
||||
serverURL: siteConfig('COMMENT_WALINE_SERVER_URL'),
|
||||
count: 5
|
||||
}).then(({ comments }) => {
|
||||
changeLoading(false)
|
||||
updateComments(comments)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
{onLoading && <div>Loading...<i className='ml-2 fas fa-spinner animate-spin' /></div>}
|
||||
{!onLoading && comments && comments.length === 0 && <div>No Comments</div>}
|
||||
{!onLoading && comments && comments.length > 0 && comments.map((comment) => <div key={comment.objectId} className='pb-2'>
|
||||
<div className='dark:text-gray-300 text-gray-600 text-xs waline-recent-content wl-content' dangerouslySetInnerHTML={{ __html: comment.comment }} />
|
||||
<div className='dark:text-gray-400 text-gray-400 text-sm text-right cursor-pointer hover:text-red-500 hover:underline pt-1'><Link href={{ pathname: comment.url, hash: comment.objectId, query: { target: 'comment' } }}>--{comment.nick}</Link></div>
|
||||
</div>)}
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
export default ExampleRecentComments
|
||||
26
themes/movie/components/Footer.js
Normal file
26
themes/movie/components/Footer.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
export const Footer = (props) => {
|
||||
const d = new Date()
|
||||
const currentYear = d.getFullYear()
|
||||
const since = siteConfig('SINCE')
|
||||
const copyrightDate = parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
|
||||
|
||||
return <footer className="z-10 relative w-full bg-white px-6 border-t dark:border-hexo-black-gray dark:bg-hexo-black-gray ">
|
||||
<DarkModeButton className='text-center pt-4'/>
|
||||
|
||||
<div className="container mx-auto max-w-4xl py-6 md:flex flex-wrap md:flex-no-wrap md:justify-between items-center text-sm">
|
||||
<div className='text-center'> ©{`${copyrightDate}`} {siteConfig('AUTHOR')}. All rights reserved.</div>
|
||||
<div className="md:p-0 text-center md:text-right text-xs">
|
||||
{/* 右侧链接 */}
|
||||
{/* <a href="#" className="text-black no-underline hover:underline">Privacy Policy</a> */}
|
||||
{siteConfig('BEI_AN') && (<a href="https://beian.miit.gov.cn/" className="text-black dark:text-gray-200 no-underline hover:underline ml-4">{siteConfig('BEI_AN')} </a>)}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
}
|
||||
166
themes/movie/components/Header.js
Normal file
166
themes/movie/components/Header.js
Normal file
@@ -0,0 +1,166 @@
|
||||
import Collapse from '@/components/Collapse'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useMovieGlobal } from '..'
|
||||
import CONFIG from '../config'
|
||||
import { MenuItemCollapse } from './MenuItemCollapse'
|
||||
import { MenuItemDrop } from './MenuItemDrop'
|
||||
|
||||
/**
|
||||
* 网站顶部
|
||||
* @returns
|
||||
*/
|
||||
export const Header = props => {
|
||||
const { collapseRef, searchModal } = useMovieGlobal()
|
||||
const router = useRouter()
|
||||
const { customNav, customMenu } = props
|
||||
const { locale } = useGlobal()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [showSearch, setShowSearch] = useState(false)
|
||||
const toggleMenuOpen = () => {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
let links = [
|
||||
{
|
||||
id: 1,
|
||||
icon: 'fa-solid fa-house',
|
||||
name: locale.NAV.INDEX,
|
||||
to: '/',
|
||||
show: siteConfig('MOVIE_MENU_INDEX', null, CONFIG)
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: 'fas fa-search',
|
||||
name: locale.NAV.SEARCH,
|
||||
to: '/search',
|
||||
show: siteConfig('MOVIE_MENU_SEARCH', null, CONFIG)
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
icon: 'fas fa-archive',
|
||||
name: locale.NAV.ARCHIVE,
|
||||
to: '/archive',
|
||||
show: siteConfig('MOVIE_MENU_ARCHIVE', null, CONFIG)
|
||||
}
|
||||
// { icon: 'fas fa-folder', name: locale.COMMON.CATEGORY, to: '/category', show: siteConfig('MENU_CATEGORY', null, CONFIG) },
|
||||
// { icon: 'fas fa-tag', name: locale.COMMON.TAGS, to: '/tag', show: siteConfig('MENU_TAG', null, CONFIG) }
|
||||
]
|
||||
|
||||
if (customNav) {
|
||||
links = links.concat(customNav)
|
||||
}
|
||||
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
if (links[i].id !== i) {
|
||||
links[i].id = i
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 开启自定义菜单,则覆盖Page生成的菜单
|
||||
if (siteConfig('CUSTOM_MENU')) {
|
||||
links = customMenu
|
||||
}
|
||||
|
||||
// 展示搜索框
|
||||
const toggleShowSearchInput = () => {
|
||||
if (siteConfig('ALGOLIA_APP_ID')) {
|
||||
searchModal.current.openSearch()
|
||||
} else {
|
||||
setShowSearch(!showSearch)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (showSearch) {
|
||||
setTimeout(() => {
|
||||
document.getElementById('search').focus()
|
||||
}, 100)
|
||||
}
|
||||
}, [showSearch])
|
||||
|
||||
const onKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
const search = document.getElementById('search').value
|
||||
if (search) {
|
||||
router.push({ pathname: '/search/' + search })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
const search = document.getElementById('search').value
|
||||
if (search) {
|
||||
router.push({ pathname: '/search/' + search })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className='w-full px-8 relative z-20 flex lg:flex-row md:flex-col justify-between items-center'>
|
||||
<Link
|
||||
href='/'
|
||||
className='whitespace-nowrap py-6 text-2xl md:text-3xl font-bold text-gray-dark no-underline flex items-center'>
|
||||
{siteConfig('TITLE')}
|
||||
</Link>
|
||||
|
||||
<div className='md:w-auto text-center flex'>
|
||||
{/* 右侧菜单 */}
|
||||
<>
|
||||
<nav id='nav-mobile' className='leading-8 justify-center w-full hidden md:flex'>
|
||||
{links?.map((link, index) => link && link.show && <MenuItemDrop key={index} link={link} />)}
|
||||
</nav>
|
||||
|
||||
<div onClick={toggleShowSearchInput} className='flex items-center cursor-pointer'>
|
||||
<i className='fas fa-search dark:text-white'></i>
|
||||
</div>
|
||||
<div
|
||||
className={`${showSearch ? 'top-16 visible opacity-100' : 'top-10 invisible opacity-0'} duration-200 transition-all max-w-md absolute w-80 right-4 p-2 flex flex-col gap-2`}>
|
||||
<input
|
||||
autoFocus
|
||||
id='search'
|
||||
onClick={toggleShowSearchInput}
|
||||
onKeyUp={onKeyUp}
|
||||
className='float-left w-full outline-none h-full p-2 rounded text-black bg-gray-100'
|
||||
aria-label='Submit search'
|
||||
type='search'
|
||||
name='s'
|
||||
autoComplete='off'
|
||||
placeholder='Type then hit enter to search...'
|
||||
/>
|
||||
<button onClick={handleSearch} className='w-full bg-[#383838] rounded py-2'>
|
||||
{locale.COMMON.SEARCH} 搜索
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 移动端按钮 */}
|
||||
<div className='md:hidden'>
|
||||
<div onClick={toggleMenuOpen} className='w-8 cursor-pointer'>
|
||||
{isOpen ? <i className='fas fa-times' /> : <i className='fas fa-bars' />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Collapse collapseRef={collapseRef} type='vertical' isOpen={isOpen}>
|
||||
{/* 移动端菜单 */}
|
||||
<menu id='nav-menu-mobile' className='block md:hidden my-auto justify-start'>
|
||||
{links?.map(
|
||||
(link, index) =>
|
||||
link &&
|
||||
link.show && (
|
||||
<MenuItemCollapse
|
||||
onHeightChange={param => collapseRef.current?.updateCollapseHeight(param)}
|
||||
key={index}
|
||||
link={link}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</menu>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
18
themes/movie/components/JumpToTopButton.js
Normal file
18
themes/movie/components/JumpToTopButton.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部
|
||||
* 当屏幕下滑500像素后会出现该控件
|
||||
* @param targetRef 关联高度的目标html标签
|
||||
* @param showPercent 是否显示百分比
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JumpToTopButton = () => {
|
||||
const { locale } = useGlobal()
|
||||
return <div title={locale.POST.TOP} className='cursor-pointer p-2 text-center' onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
><i className='fas fa-angle-up text-2xl' />
|
||||
</div>
|
||||
}
|
||||
|
||||
export default JumpToTopButton
|
||||
62
themes/movie/components/LatestPostsGroup.js
Normal file
62
themes/movie/components/LatestPostsGroup.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { checkContainHttp, sliceUrlFromHttp } from '@/lib/utils'
|
||||
|
||||
/**
|
||||
* 最新文章列表
|
||||
* @param posts 所有文章数据
|
||||
* @param sliceCount 截取展示的数量 默认6
|
||||
* @constructor
|
||||
*/
|
||||
const LatestPostsGroup = ({ latestPosts }) => {
|
||||
// 获取当前路径
|
||||
const currentPath = useRouter().asPath
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (!latestPosts) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pb-1 px-2 flex flex-nowrap justify-between">
|
||||
<div className="text-2xl text-gray-600 dark:text-gray-200">
|
||||
<i className="mr-2 fas fa-history" />
|
||||
{locale.COMMON.LATEST_POSTS}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{latestPosts.map(post => {
|
||||
const selected =
|
||||
currentPath === `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
const url = checkContainHttp(post.slug)
|
||||
? sliceUrlFromHttp(post.slug)
|
||||
: `${siteConfig('SUB_PATH', '')}/${post.slug}`
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
title={post.title}
|
||||
href={url}
|
||||
passHref
|
||||
className={'my-1 flex'}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
(selected
|
||||
? 'text-white bg-gray-600 '
|
||||
: 'text-gray-500 dark:text-green-400 ') +
|
||||
' py-1 flex hover:bg-gray-500 px-2 duration-200 w-full ' +
|
||||
'hover:text-white dark:hover:text-white cursor-pointer'
|
||||
}
|
||||
>
|
||||
<li className="line-clamp-2">{post.title}</li>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default LatestPostsGroup
|
||||
8
themes/movie/components/LoadingCover.js
Normal file
8
themes/movie/components/LoadingCover.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function LoadingCover() {
|
||||
return <div id='cover-loading' className={'z-50 opacity-50 pointer-events-none transition-all duration-300'}>
|
||||
<div className='w-full h-screen flex justify-center items-center'>
|
||||
<i className="fa-solid fa-spinner text-2xl text-black dark:text-white animate-spin"> </i>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
77
themes/movie/components/MenuItemCollapse.js
Normal file
77
themes/movie/components/MenuItemCollapse.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import Collapse from '@/components/Collapse'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 折叠菜单
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export const MenuItemCollapse = props => {
|
||||
const { link } = props
|
||||
const [show, changeShow] = useState(false)
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
const [isOpen, changeIsOpen] = useState(false)
|
||||
|
||||
const toggleShow = () => {
|
||||
changeShow(!show)
|
||||
}
|
||||
|
||||
const toggleOpenSubMenu = () => {
|
||||
changeIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='select-none w-full px-6 py-2 text-left ' onClick={toggleShow}>
|
||||
{!hasSubMenu && (
|
||||
<Link
|
||||
href={link?.to}
|
||||
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||
className='flex justify-between pl-2 pr-4 dark:text-gray-200 no-underline tracking-widest'>
|
||||
<span className=' transition-all items-center duration-200'>
|
||||
{link?.icon && <i className={link.icon + ' mr-4'} />}
|
||||
{link?.name}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{hasSubMenu && (
|
||||
<div
|
||||
onClick={hasSubMenu ? toggleOpenSubMenu : null}
|
||||
className='flex items-center justify-between pl-2 pr-4 cursor-pointer dark:text-gray-200 no-underline tracking-widest'>
|
||||
<span className='transition-all items-center duration-200'>
|
||||
{link?.icon && <i className={link.icon + ' mr-4'} />}
|
||||
{link?.name}
|
||||
</span>
|
||||
<i
|
||||
className={`select-none px-2 fas fa-chevron-left transition-all duration-200 ${isOpen ? '-rotate-90' : ''} text-gray-400`}></i>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 折叠子菜单 */}
|
||||
{hasSubMenu && (
|
||||
<Collapse isOpen={isOpen} onHeightChange={props.onHeightChange} className='rounded-xl'>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='dark:text-gray-200 text-left px-3 justify-start tracking-widest transition-all duration-200 py-3 pr-6'>
|
||||
<Link href={sLink.to} target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}>
|
||||
<span className='text-sm ml-4 whitespace-nowrap'>
|
||||
{link?.icon && <i className={sLink.icon + ' mr-2'} />} {sLink.title}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</Collapse>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
67
themes/movie/components/MenuItemDrop.js
Normal file
67
themes/movie/components/MenuItemDrop.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export const MenuItemDrop = ({ link }) => {
|
||||
const [show, changeShow] = useState(false)
|
||||
const hasSubMenu = link?.subMenus?.length > 0
|
||||
|
||||
if (!link || !link.show) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseOver={() => changeShow(true)}
|
||||
onMouseOut={() => changeShow(false)}
|
||||
>
|
||||
{!hasSubMenu && (
|
||||
<Link
|
||||
href={link?.to}
|
||||
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||
className="select-none menu-link pl-2 pr-4 no-underline tracking-widest pb-1"
|
||||
>
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
{hasSubMenu && <i className="px-2 fa fa-angle-down"></i>}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{hasSubMenu && (
|
||||
<>
|
||||
<div className="cursor-pointer menu-link pl-2 pr-4 no-underline tracking-widest pb-1">
|
||||
{link?.icon && <i className={link?.icon} />} {link?.name}
|
||||
<i
|
||||
className={`px-2 fa fa-angle-down duration-300 ${show ? 'rotate-180' : 'rotate-0'}`}
|
||||
></i>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 子菜单 */}
|
||||
{hasSubMenu && (
|
||||
<ul
|
||||
style={{ backdropFilter: 'blur(3px)' }}
|
||||
className={`${show ? 'visible opacity-100 top-14' : 'invisible opacity-0 top-20'} drop-shadow-md overflow-hidden rounded-md text-black dark:text-white bg-white dark:bg-black transition-all duration-300 z-30 absolute block `}
|
||||
>
|
||||
{link.subMenus.map((sLink, index) => {
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
className="cursor-pointer text-start dark:bg-hexo-black-gray dark:hover:bg-gray-300 hover:bg-gray-300 hover:text-black tracking-widest transition-all duration-200 dark:border-gray-800 py-1 pr-6 pl-3"
|
||||
>
|
||||
<Link
|
||||
href={sLink.to}
|
||||
target={link?.to?.indexOf('http') === 0 ? '_blank' : '_self'}
|
||||
>
|
||||
<span className="text-sm">
|
||||
{link?.icon && <i className={sLink?.icon}> </i>}
|
||||
{sLink.title}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
14
themes/movie/components/NormalMenuItem.js
Normal file
14
themes/movie/components/NormalMenuItem.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 旧的普通菜单
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const NormalMenuItem = (props) => {
|
||||
const { link } = props
|
||||
return link?.show && <Link href={link.to} key={link.to}
|
||||
className="px-2 md:pl-0 md:mr-3 my-4 md:pr-3 text-gray-700 dark:text-gray-200 no-underline md:border-r border-gray-light">
|
||||
{link.name}
|
||||
</Link>
|
||||
}
|
||||
220
themes/movie/components/PaginationNumber.js
Normal file
220
themes/movie/components/PaginationNumber.js
Normal file
@@ -0,0 +1,220 @@
|
||||
import { ChevronDoubleRight } from '@/components/HeroIcons'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
|
||||
/**
|
||||
* 数字翻页插件
|
||||
* @param page 当前页码
|
||||
* @param showNext 是否有下一页
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const PaginationNumber = ({ page, totalPage }) => {
|
||||
const router = useRouter()
|
||||
const [value, setValue] = useState('')
|
||||
const { locale } = useGlobal()
|
||||
const currentPage = +page
|
||||
const showNext = page < totalPage
|
||||
const showPrev = currentPage !== 1
|
||||
const pagePrefix = router.asPath
|
||||
.split('?')[0]
|
||||
.replace(/\/page\/[1-9]\d*/, '')
|
||||
.replace(/\/$/, '')
|
||||
const pages = generatePages(pagePrefix, page, currentPage, totalPage)
|
||||
if (pages?.length <= 1) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
const handleInputChange = event => {
|
||||
const newValue = event.target.value.replace(/[^0-9]/g, '')
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
/**
|
||||
* 调到指定页
|
||||
*/
|
||||
const jumpToPage = () => {
|
||||
if (value) {
|
||||
router.push(
|
||||
value === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${value}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* pc端分页按钮 */}
|
||||
<div className="hidden lg:flex justify-between items-end mt-10 mb-5 font-medium text-black duration-500 dark:text-gray-300 py-3 space-x-2 overflow-x-auto">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'}`}
|
||||
>
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-left mr-2 transition-all duration-200 transform group-hover:-translate-x-4" />
|
||||
<div className="absolute translate-x-4 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:translate-x-0">
|
||||
{locale.PAGINATION.PREV}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* 分页 */}
|
||||
<div className="flex items-center space-x-2">
|
||||
{pages}
|
||||
|
||||
{/* 跳转页码 */}
|
||||
<div className="bg-white hover:bg-gray-100 dark:hover:bg-yellow-600 dark:bg-[#1e1e1e] h-10 border flex justify-center items-center rounded-lg group hover:border-indigo-600 transition-all duration-200">
|
||||
<input
|
||||
value={value}
|
||||
className="w-0 group-hover:w-20 group-hover:px-3 transition-all duration-200 bg-gray-100 border-none outline-none h-full rounded-lg"
|
||||
onInput={handleInputChange}
|
||||
></input>
|
||||
<div
|
||||
onClick={jumpToPage}
|
||||
className="cursor-pointer hover:bg-indigo-600 dark:bg-[#1e1e1e] dark:hover:bg-yellow-600 hover:text-white px-4 py-2 group-hover:px-2 group-hover:mx-1 group-hover:rounded bg-white"
|
||||
>
|
||||
<ChevronDoubleRight className={'w-4 h-4'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'invisible'} `}
|
||||
>
|
||||
<div className="relative w-24 h-10 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-lg cursor-pointer group">
|
||||
<i className="fas fa-angle-right mr-2 transition-all duration-200 transform group-hover:translate-x-6" />
|
||||
<div className="absolute -translate-x-10 ml-2 opacity-0 transition-all duration-200 group-hover:opacity-100 group-hover:-translate-x-2">
|
||||
{locale.PAGINATION.NEXT}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* 移动端分页 */}
|
||||
|
||||
<div className="lg:hidden w-full flex flex-row">
|
||||
{/* 上一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname:
|
||||
currentPage === 2
|
||||
? `${pagePrefix}/`
|
||||
: `${pagePrefix}/page/${currentPage - 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="prev"
|
||||
className={`${showPrev ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||
>
|
||||
{locale.PAGINATION.PREV}
|
||||
</Link>
|
||||
|
||||
{showPrev && showNext && <div className="w-12"></div>}
|
||||
|
||||
{/* 下一页 */}
|
||||
<Link
|
||||
href={{
|
||||
pathname: `${pagePrefix}/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}}
|
||||
rel="next"
|
||||
className={`${+showNext ? 'block' : 'hidden'} dark:text-white relative w-full flex-1 h-14 flex items-center transition-all duration-200 justify-center py-2 px-2 bg-white dark:bg-[#1e1e1e] border rounded-xl cursor-pointer`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 页码按钮
|
||||
* @param {*} page
|
||||
* @param {*} currentPage
|
||||
* @param {*} pagePrefix
|
||||
* @returns
|
||||
*/
|
||||
function getPageElement(page, currentPage, pagePrefix) {
|
||||
const selected = page + '' === currentPage + ''
|
||||
if (!page) {
|
||||
return <></>
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
href={page === 1 ? `${pagePrefix}/` : `${pagePrefix}/page/${page}`}
|
||||
key={page}
|
||||
passHref
|
||||
className={
|
||||
(selected
|
||||
? 'bg-indigo-600 dark:bg-yellow-600 text-white '
|
||||
: 'dark:bg-[#1e1e1e] bg-white') +
|
||||
' hover:border-indigo-600 dark:hover:bg-yellow-600 dark:border-gray-600 px-4 border py-2 rounded-lg drop-shadow-sm duration-200 transition-colors'
|
||||
}
|
||||
>
|
||||
{page}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有页码
|
||||
* @param {*} pagePrefix
|
||||
* @param {*} page
|
||||
* @param {*} currentPage
|
||||
* @param {*} totalPage
|
||||
* @returns
|
||||
*/
|
||||
function generatePages(pagePrefix, page, currentPage, totalPage) {
|
||||
const pages = []
|
||||
const groupCount = 7 // 最多显示页签数
|
||||
if (totalPage <= groupCount) {
|
||||
for (let i = 1; i <= totalPage; i++) {
|
||||
pages.push(getPageElement(i, page, pagePrefix))
|
||||
}
|
||||
} else {
|
||||
pages.push(getPageElement(1, page, pagePrefix))
|
||||
const dynamicGroupCount = groupCount - 2
|
||||
let startPage = currentPage - 2
|
||||
if (startPage <= 1) {
|
||||
startPage = 2
|
||||
}
|
||||
if (startPage + dynamicGroupCount > totalPage) {
|
||||
startPage = totalPage - dynamicGroupCount
|
||||
}
|
||||
if (startPage > 2) {
|
||||
pages.push(
|
||||
<div key={-1} className="-mt-2 mx-1">
|
||||
...{' '}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
for (let i = 0; i < dynamicGroupCount; i++) {
|
||||
if (startPage + i < totalPage) {
|
||||
pages.push(getPageElement(startPage + i, page, pagePrefix))
|
||||
}
|
||||
}
|
||||
|
||||
if (startPage + dynamicGroupCount < totalPage) {
|
||||
pages.push(<div key={-2}>... </div>)
|
||||
}
|
||||
|
||||
pages.push(getPageElement(totalPage, page, pagePrefix))
|
||||
}
|
||||
return pages
|
||||
}
|
||||
export default PaginationNumber
|
||||
87
themes/movie/components/SearchInput.js
Normal file
87
themes/movie/components/SearchInput.js
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { useImperativeHandle, useRef, useState } from 'react'
|
||||
|
||||
let lock = false
|
||||
|
||||
const SearchInput = ({ currentTag, keyword, cRef }) => {
|
||||
const { locale } = useGlobal()
|
||||
const router = useRouter()
|
||||
const searchInputRef = useRef(null)
|
||||
useImperativeHandle(cRef, () => {
|
||||
return {
|
||||
focus: () => {
|
||||
searchInputRef?.current?.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
const handleSearch = () => {
|
||||
const key = searchInputRef.current.value
|
||||
if (key && key !== '') {
|
||||
router.push({ pathname: '/search/' + key }).then(r => {
|
||||
console.log('搜索', key)
|
||||
})
|
||||
} else {
|
||||
router.push({ pathname: '/' }).then(r => {
|
||||
})
|
||||
}
|
||||
}
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) { // 回车
|
||||
handleSearch(searchInputRef.current.value)
|
||||
} else if (e.keyCode === 27) { // ESC
|
||||
cleanSearch()
|
||||
}
|
||||
}
|
||||
const cleanSearch = () => {
|
||||
searchInputRef.current.value = ''
|
||||
setShowClean(false)
|
||||
}
|
||||
function lockSearchInput () {
|
||||
lock = true
|
||||
}
|
||||
|
||||
function unLockSearchInput () {
|
||||
lock = false
|
||||
}
|
||||
const [showClean, setShowClean] = useState(false)
|
||||
const updateSearchKey = (val) => {
|
||||
if (lock) {
|
||||
return
|
||||
}
|
||||
searchInputRef.current.value = val
|
||||
if (val) {
|
||||
setShowClean(true)
|
||||
} else {
|
||||
setShowClean(false)
|
||||
}
|
||||
}
|
||||
|
||||
return <section className='flex w-full bg-gray-100'>
|
||||
<input
|
||||
ref={searchInputRef}
|
||||
type='text'
|
||||
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'outline-none w-full text-sm pl-4 transition focus:shadow-lg font-light leading-10 text-black bg-gray-100 dark:bg-gray-900 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onCompositionStart={lockSearchInput}
|
||||
onCompositionUpdate={lockSearchInput}
|
||||
onCompositionEnd={unLockSearchInput}
|
||||
onChange={e => updateSearchKey(e.target.value)}
|
||||
defaultValue={keyword || ''}
|
||||
/>
|
||||
|
||||
<div className='-ml-8 cursor-pointer float-right items-center justify-center py-2'
|
||||
onClick={handleSearch}>
|
||||
<i className={'hover:text-black transform duration-200 text-gray-500 cursor-pointer fas fa-search'} />
|
||||
</div>
|
||||
|
||||
{(showClean &&
|
||||
<div className='-ml-12 cursor-pointer dark:bg-gray-600 dark:hover:bg-gray-800 float-right items-center justify-center py-2'>
|
||||
<i className='hover:text-black transform duration-200 text-gray-400 cursor-pointer fas fa-times' onClick={cleanSearch} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
68
themes/movie/components/SideBar.js
Normal file
68
themes/movie/components/SideBar.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import Live2D from '@/components/Live2D'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import dynamic from 'next/dynamic'
|
||||
import Announcement from './Announcement'
|
||||
const ExampleRecentComments = dynamic(() => import('./ExampleRecentComments'))
|
||||
|
||||
export const SideBar = (props) => {
|
||||
const { locale } = useGlobal()
|
||||
const { latestPosts, categoryOptions, notice } = props
|
||||
return (
|
||||
<div className="w-full md:w-64 sticky top-8">
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.CATEGORY}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{categoryOptions?.map(category => {
|
||||
return (
|
||||
<Link
|
||||
key={category.name}
|
||||
href={`/category/${category.name}`}
|
||||
passHref
|
||||
legacyBehavior>
|
||||
<li> <a href={`/category/${category.name}`} className="text-gray-darkest text-sm">{category.name}({category.count})</a></li>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.LATEST_POSTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ul className="list-reset leading-normal">
|
||||
{latestPosts?.map(p => {
|
||||
return (
|
||||
<Link key={p.id} href={`/${p.slug}`} passHref legacyBehavior>
|
||||
<li> <a href={`/${p.slug}`} className="text-gray-darkest text-sm">{p.title}</a></li>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<Announcement post={notice}/>
|
||||
|
||||
{siteConfig('COMMENT_WALINE_SERVER_URL') && siteConfig('COMMENT_WALINE_RECENT') && <aside className="rounded shadow overflow-hidden mb-6">
|
||||
<h3 className="text-sm bg-gray-100 text-gray-700 dark:bg-hexo-black-gray dark:text-gray-200 py-3 px-4 dark:border-hexo-black-gray border-b">{locale.COMMON.RECENT_COMMENTS}</h3>
|
||||
|
||||
<div className="p-4">
|
||||
<ExampleRecentComments/>
|
||||
</div>
|
||||
</aside>}
|
||||
|
||||
<aside className="rounded overflow-hidden mb-6">
|
||||
<Live2D />
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
themes/movie/components/SlotBar.js
Normal file
34
themes/movie/components/SlotBar.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
|
||||
/**
|
||||
* 博客列表上方嵌入条
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function SlotBar(props) {
|
||||
const { tag, category } = props
|
||||
const { locale } = useGlobal()
|
||||
|
||||
if (tag) {
|
||||
return (
|
||||
<div className='cursor-pointer px-3 py-2 mb-2 '>
|
||||
<div className={'inline-block rounded duration-200 mr-2 px-1 text-xl whitespace-nowrap '}>
|
||||
<div className=' dark:text-white dark:hover:text-white text-5xl py-5'>
|
||||
{locale.COMMON.TAGS} : {tag}{' '}
|
||||
</div>
|
||||
</div>
|
||||
<hr className='dark:border-gray-600' />
|
||||
</div>
|
||||
)
|
||||
} else if (category) {
|
||||
return (
|
||||
<div className='cursor-pointer px-3 py-2 mb-2 '>
|
||||
<div className=' dark:text-white dark:hover:text-white text-5xl py-5'>
|
||||
{locale.COMMON.CATEGORY} : {category}
|
||||
</div>
|
||||
<hr className='dark:border-gray-600' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <></>
|
||||
}
|
||||
51
themes/movie/components/TagGroups.js
Normal file
51
themes/movie/components/TagGroups.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
/**
|
||||
* 标签组
|
||||
* @param tags
|
||||
* @param currentTag
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TagGroups = ({ tagOptions, className }) => {
|
||||
const router = useRouter()
|
||||
const { locale } = useGlobal()
|
||||
const { tag: currentTag } = router.query
|
||||
if (!tagOptions) return <></>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-2xl dark:text-white mb-2">{locale.COMMON.TAGS}</div>
|
||||
<div id="tags-group" className="dark:border-gray-700 space-y-2">
|
||||
{tagOptions.map((tag, index) => {
|
||||
const selected = currentTag === tag.name
|
||||
return (
|
||||
<Link
|
||||
passHref
|
||||
key={index}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
className={'cursor-pointer inline-block whitespace-nowrap'}
|
||||
>
|
||||
<div
|
||||
className={`${className || ''}
|
||||
${selected ? 'text-white bg-blue-600 dark:bg-yellow-600' : ''}
|
||||
flex items-center hover:bg-blue-600 dark:hover:bg-yellow-600 hover:scale-110 hover:text-white rounded-lg px-2 py-0.5 duration-150 transition-all`}
|
||||
>
|
||||
<div className="text-lg">{tag.name} </div>
|
||||
{tag.count ? (
|
||||
<sup className="relative ml-1">{tag.count}</sup>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagGroups
|
||||
23
themes/movie/components/TagItem.js
Normal file
23
themes/movie/components/TagItem.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 标签
|
||||
* @param {*} param0
|
||||
* @returns
|
||||
*/
|
||||
export default function TagItem({ tag }) {
|
||||
return (
|
||||
<div key={tag.name} className="p-2">
|
||||
<Link
|
||||
key={tag}
|
||||
href={`/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={`cursor-pointer inline-block rounded duration-200 mr-2 py-1 px-2 text-xs whitespace-nowrap`}
|
||||
>
|
||||
<div className="font-light hover:scale-105 transition-all duration-200 text-xl dark:text-green-500 hover:bg-gray-100 dark:hover:bg-hexo-black-gray p-2">
|
||||
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
19
themes/movie/components/TagItemMini.js
Normal file
19
themes/movie/components/TagItemMini.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItemMini = ({ tag, selected = false }) => {
|
||||
return (
|
||||
<Link
|
||||
key={tag}
|
||||
href={selected ? '/' : `/tag/${encodeURIComponent(tag.name)}`}
|
||||
passHref
|
||||
className={'inline-block rounded-xl py-0.5 mr-2'}
|
||||
>
|
||||
<div className="text-md font-bold text-shadow text-[#2EBF8B]">
|
||||
{selected && <i className="mr-1 fa-tag" />}{' '}
|
||||
{tag.name + (tag.count ? `(${tag.count})` : '')}{' '}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagItemMini
|
||||
20
themes/movie/components/Title.js
Normal file
20
themes/movie/components/Title.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import NotionIcon from '@/components/NotionIcon'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
|
||||
/**
|
||||
* 标题栏
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export const Title = (props) => {
|
||||
const { post } = props
|
||||
const title = post?.title || siteConfig('TITLE')
|
||||
const description = post?.description || siteConfig('AUTHOR')
|
||||
|
||||
return <div className="text-center px-6 py-12 mb-6 bg-gray-100 dark:bg-hexo-black-gray dark:border-hexo-black-gray border-b">
|
||||
<h1 className="text-xl md:text-4xl pb-4"><NotionIcon icon={post?.pageIcon} />{title}</h1>
|
||||
<p className="leading-loose text-gray-dark">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
16
themes/movie/config.js
Normal file
16
themes/movie/config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 主题配置文件
|
||||
*/
|
||||
const CONFIG = {
|
||||
// 菜单配置
|
||||
MOVIE_MENU_CATEGORY: true, // 显示分类
|
||||
MOVIE_MENU_TAG: true, // 显示标签
|
||||
MOVIE_MENU_ARCHIVE: true, // 显示归档
|
||||
MOVIE_MENU_SEARCH: true, // 显示搜索
|
||||
|
||||
MOVIE_ARTICLE_RECOMMEND: true, // 推荐关联内容在文章底部
|
||||
MOVIE_VIDEO_COMBINE: true, // 聚合视频,开启后一篇文章内的多个含caption的视频会被合并到文章开头,并展示分集按钮
|
||||
|
||||
MOVIE_POST_LIST_COVER: true // 列表显示文章封面
|
||||
}
|
||||
export default CONFIG
|
||||
438
themes/movie/index.js
Normal file
438
themes/movie/index.js
Normal file
@@ -0,0 +1,438 @@
|
||||
'use client'
|
||||
|
||||
import AlgoliaSearchModal from '@/components/AlgoliaSearchModal'
|
||||
import Comment from '@/components/Comment'
|
||||
import replaceSearchResult from '@/components/Mark'
|
||||
import NotionPage from '@/components/NotionPage'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
import { siteConfig } from '@/lib/config'
|
||||
import { useGlobal } from '@/lib/global'
|
||||
import { loadWowJS } from '@/lib/plugins/wow'
|
||||
import { isBrowser } from '@/lib/utils'
|
||||
import { Transition } from '@headlessui/react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import ArchiveDateList from './components/ArchiveDateList'
|
||||
import { ArticleInfo } from './components/ArticleInfo'
|
||||
import { ArticleLock } from './components/ArticleLock'
|
||||
import BlogListGroupByDate from './components/BlogListGroupByDate'
|
||||
import { BlogListPage } from './components/BlogListPage'
|
||||
import { BlogListScroll } from './components/BlogListScroll'
|
||||
import BlogRecommend from './components/BlogRecommend'
|
||||
import CategoryGroup from './components/CategoryGroup'
|
||||
import CategoryItem from './components/CategoryItem'
|
||||
import { Footer } from './components/Footer'
|
||||
import { Header } from './components/Header'
|
||||
import JumpToTopButton from './components/JumpToTopButton'
|
||||
import LatestPostsGroup from './components/LatestPostsGroup'
|
||||
import SlotBar from './components/SlotBar'
|
||||
import TagGroups from './components/TagGroups'
|
||||
import TagItem from './components/TagItem'
|
||||
import CONFIG from './config'
|
||||
import { Style } from './style'
|
||||
|
||||
// 主题全局状态
|
||||
const ThemeGlobalMovie = createContext()
|
||||
export const useMovieGlobal = () => useContext(ThemeGlobalMovie)
|
||||
|
||||
/**
|
||||
* 基础布局框架
|
||||
* 1.其它页面都嵌入在LayoutBase中
|
||||
* 2.采用左右两侧布局,移动端使用顶部导航栏
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const LayoutBase = props => {
|
||||
const { children, slotTop } = props
|
||||
const { onLoading, fullWidth } = useGlobal()
|
||||
const collapseRef = useRef(null)
|
||||
|
||||
const searchModal = useRef(null)
|
||||
const [expandMenu, updateExpandMenu] = useState(false)
|
||||
useEffect(() => {
|
||||
loadWowJS()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ThemeGlobalMovie.Provider value={{ searchModal, expandMenu, updateExpandMenu, collapseRef }}>
|
||||
<div
|
||||
id='theme-movie'
|
||||
className={`${siteConfig('FONT_STYLE')} dark:text-gray-300 duration-300 transition-all bg-white dark:bg-[#2A2A2A] scroll-smooth min-h-screen flex flex-col justify-between`}>
|
||||
<Style />
|
||||
|
||||
{/* 页头 */}
|
||||
<Header {...props} />
|
||||
|
||||
{/* 主体 */}
|
||||
<div id='container-inner' className='w-full relative flex-grow z-10'>
|
||||
{/* 标题栏 */}
|
||||
{/* {fullWidth ? null : <Title {...props} />} */}
|
||||
|
||||
<div
|
||||
id='container-wrapper'
|
||||
className={
|
||||
(JSON.parse(siteConfig('LAYOUT_SIDEBAR_REVERSE')) ? 'flex-row-reverse' : '') +
|
||||
'relative mx-auto justify-center md:flex items-start py-8 px-2'
|
||||
}>
|
||||
{/* 内容 */}
|
||||
<div className={`w-full ${fullWidth ? '' : ''} xl:px-32 lg:px-4`}>
|
||||
<Transition
|
||||
show={!onLoading}
|
||||
appear={true}
|
||||
enter='transition ease-in-out duration-700 transform order-first'
|
||||
enterFrom='opacity-0 translate-y-16'
|
||||
enterTo='opacity-100'
|
||||
leave='transition ease-in-out duration-300 transform'
|
||||
leaveFrom='opacity-100 translate-y-0'
|
||||
leaveTo='opacity-0 -translate-y-16'
|
||||
unmount={false}>
|
||||
{/* 嵌入模块 */}
|
||||
{slotTop}
|
||||
{children}
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 页脚 */}
|
||||
<Footer {...props} />
|
||||
|
||||
{/* 搜索框 */}
|
||||
<AlgoliaSearchModal cRef={searchModal} {...props} />
|
||||
|
||||
{/* 回顶按钮 */}
|
||||
<div className='fixed right-4 bottom-4 z-10'>
|
||||
<JumpToTopButton />
|
||||
</div>
|
||||
</div>
|
||||
</ThemeGlobalMovie.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页
|
||||
* @param {*} props
|
||||
* @returns 此主题首页就是列表
|
||||
*/
|
||||
const LayoutIndex = props => {
|
||||
return <LayoutPostList {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutPostList = props => {
|
||||
return (
|
||||
<>
|
||||
<SlotBar {...props} />
|
||||
{siteConfig('POST_LIST_STYLE') === 'page' ? <BlogListPage {...props} /> : <BlogListScroll {...props} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情页
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSlug = props => {
|
||||
const { post, lock, validPassword } = props
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
// 用js 实现将页面中的多个视频聚合为一个分集的视频
|
||||
function combineVideo() {
|
||||
// 找到 id 为 notion-article 的元素
|
||||
const notionArticle = document.getElementById('notion-article')
|
||||
if (!notionArticle) return // 如果找不到对应的元素,则退出函数
|
||||
|
||||
// 找到所有的 .notion-asset-wrapper 元素
|
||||
const assetWrappers = document.querySelectorAll('.notion-asset-wrapper')
|
||||
if (!assetWrappers || assetWrappers.length === 0) return // 如果找不到对应的元素,则退出函数
|
||||
|
||||
// 不要重复创建
|
||||
const exists = document.querySelectorAll('.video-wrapper')
|
||||
if (exists && exists.length > 0) return
|
||||
|
||||
// 创建一个新的容器元素
|
||||
const videoWrapper = document.createElement('div')
|
||||
videoWrapper.className = 'video-wrapper p-2 bg-gray-100 dark:bg-hexo-black-gray max-w-5xl mx-auto'
|
||||
|
||||
// 创建一个新的容器元素
|
||||
const carouselWrapper = document.createElement('div')
|
||||
carouselWrapper.classList.add('notion-carousel-wrapper')
|
||||
|
||||
// 创建一个用于保存 figcaption 文本内容的数组
|
||||
const figCaptionValues = []
|
||||
|
||||
// 遍历所有 .notion-asset-wrapper 元素
|
||||
assetWrappers.forEach((wrapper, index) => {
|
||||
// 检查 .notion-asset-wrapper 元素是否有子元素 figcaption
|
||||
const figCaption = wrapper.querySelector('figcaption')
|
||||
if (!figCaption) return // 如果没有子元素 figcaption,则不处理该元素
|
||||
|
||||
// 检查 .notion-asset-wrapper 元素是否有 notion-asset-wrapper-video 或 notion-asset-wrapper-embed 类
|
||||
if (
|
||||
!wrapper.classList.contains('notion-asset-wrapper-video') &&
|
||||
!wrapper.classList.contains('notion-asset-wrapper-embed')
|
||||
)
|
||||
return
|
||||
|
||||
// 获取 figcaption 的文本内容并添加到数组中
|
||||
const figCaptionValue = figCaption.textContent.trim()
|
||||
figCaptionValues.push(figCaptionValue)
|
||||
|
||||
// 创建一个新的 div 元素用于包裹当前的 .notion-asset-wrapper 元素
|
||||
const carouselItem = document.createElement('div')
|
||||
carouselItem.classList.add('notion-carousel')
|
||||
carouselItem.appendChild(wrapper)
|
||||
// 如果是第一个元素,设置为 active
|
||||
if (index === 0) {
|
||||
carouselItem.classList.add('active')
|
||||
}
|
||||
// 将元素添加到容器中
|
||||
carouselWrapper.appendChild(carouselItem)
|
||||
// 从 DOM 中移除原始的 .notion-asset-wrapper 元素
|
||||
// wrapper.parentNode.removeChild(wrapper)
|
||||
})
|
||||
|
||||
// 创建一个用于保存 figcaption 值的容器元素
|
||||
const figCaptionWrapper = document.createElement('div')
|
||||
figCaptionWrapper.className = 'notion-carousel-route py-2 max-h-36 overflow-y-auto'
|
||||
|
||||
// 遍历 figCaptionValues 数组,并将每个值添加到容器元素中
|
||||
figCaptionValues.forEach(value => {
|
||||
const div = document.createElement('div')
|
||||
div.textContent = value
|
||||
div.addEventListener('click', function () {
|
||||
// 遍历所有的 carouselItem 元素
|
||||
document.querySelectorAll('.notion-carousel').forEach(item => {
|
||||
// 判断当前元素是否包含该 figCaption 的文本内容,如果是则设置为 active,否则取消 active
|
||||
if (item.querySelector('figcaption').textContent.trim() === value) {
|
||||
item.classList.add('active')
|
||||
} else {
|
||||
item.classList.remove('active')
|
||||
// 不活跃窗口暂停播放,仅支持notion上传视频、不支持外链
|
||||
item.querySelectorAll('video')?.forEach(video => {
|
||||
video.pause()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
figCaptionWrapper.appendChild(div)
|
||||
})
|
||||
|
||||
// 条件是带有caption的视频数量大于1个,否则不处理
|
||||
if (figCaptionValues.length > 1) {
|
||||
// 将包含 figcaption 值的容器元素添加到 notion-article 的第一个子元素插入
|
||||
videoWrapper.appendChild(carouselWrapper)
|
||||
videoWrapper.appendChild(figCaptionWrapper)
|
||||
notionArticle.insertBefore(videoWrapper, notionArticle.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
combineVideo()
|
||||
}, 100)
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
return () => {
|
||||
// 获取所有 class="video-wrapper" 的元素
|
||||
const videoWrappers = document.querySelectorAll('.video-wrapper')
|
||||
|
||||
// 遍历所有匹配的元素并移除它们
|
||||
videoWrappers.forEach(wrapper => {
|
||||
wrapper.parentNode.removeChild(wrapper) // 从 DOM 中移除元素
|
||||
})
|
||||
}
|
||||
}, [post])
|
||||
|
||||
return (
|
||||
<>
|
||||
{!lock ? (
|
||||
<div id='article-wrapper' className='px-2 max-w-5xl mx-auto'>
|
||||
{/* 标题 */}
|
||||
<ArticleInfo post={post} />
|
||||
{/* 页面元素 */}
|
||||
<NotionPage post={post} />
|
||||
{/* 推荐 */}
|
||||
<BlogRecommend {...props} />
|
||||
{/* 分享栏目 */}
|
||||
<ShareBar post={post} />
|
||||
{/* 评论区 */}
|
||||
<Comment frontMatter={post} />
|
||||
</div>
|
||||
) : (
|
||||
<ArticleLock validPassword={validPassword} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 404页
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const Layout404 = props => {
|
||||
const { locale } = useGlobal()
|
||||
const { searchModal } = useMovieGlobal()
|
||||
const router = useRouter()
|
||||
// 展示搜索框
|
||||
const toggleShowSearchInput = () => {
|
||||
if (siteConfig('ALGOLIA_APP_ID')) {
|
||||
searchModal.current.openSearch()
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
const search = document.getElementById('search').value
|
||||
if (search) {
|
||||
router.push({ pathname: '/search/' + search })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='h-52'>
|
||||
<h2 className='text-4xl'>{locale.COMMON.NO_RESULTS_FOUND}</h2>
|
||||
<hr className='my-4' />
|
||||
<div className='max-w-md relative'>
|
||||
<input
|
||||
autoFocus
|
||||
id='search'
|
||||
onClick={toggleShowSearchInput}
|
||||
onKeyUp={onKeyUp}
|
||||
className='float-left w-full outline-none h-full p-2 rounded dark:bg-[#383838] bg-gray-100'
|
||||
aria-label='Submit search'
|
||||
type='search'
|
||||
name='s'
|
||||
autoComplete='off'
|
||||
placeholder='Type then hit enter to search...'
|
||||
/>
|
||||
<i className='fas fa-search absolute right-0 my-auto p-2'></i>
|
||||
</div>
|
||||
</div>
|
||||
{/* 底部导航 */}
|
||||
<div className='h-full flex-grow grid grid-cols-4 gap-4'>
|
||||
<LatestPostsGroup {...props} />
|
||||
<CategoryGroup {...props} />
|
||||
<ArchiveDateList {...props} />
|
||||
<TagGroups {...props} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索页
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutSearch = props => {
|
||||
const { keyword } = props
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (isBrowser) {
|
||||
// 高亮搜索到的结果
|
||||
const container = document.getElementById('posts-wrapper')
|
||||
if (keyword && container) {
|
||||
replaceSearchResult({
|
||||
doms: container,
|
||||
search: keyword,
|
||||
target: {
|
||||
element: 'span',
|
||||
className: 'text-red-500 border-b border-dashed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [router])
|
||||
|
||||
return <LayoutPostList {...props} />
|
||||
}
|
||||
|
||||
/**
|
||||
* 归档列表
|
||||
* @param {*} props
|
||||
* @returns 按照日期将文章分组排序
|
||||
*/
|
||||
const LayoutArchive = props => {
|
||||
const { archivePosts } = props
|
||||
return (
|
||||
<>
|
||||
<div className='mb-10 pb-20 md:py-12 p-3 min-h-screen w-full'>
|
||||
{Object.keys(archivePosts).map(archiveTitle => (
|
||||
<BlogListGroupByDate key={archiveTitle} archiveTitle={archiveTitle} archivePosts={archivePosts} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutCategoryIndex = props => {
|
||||
const { categoryOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div id='category-list' className='duration-200 flex flex-wrap'>
|
||||
{categoryOptions?.map(category => (
|
||||
<CategoryItem key={category.name} category={category} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签列表
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
const LayoutTagIndex = props => {
|
||||
const { tagOptions } = props
|
||||
return (
|
||||
<>
|
||||
<div id='tags-list' className='duration-200 flex flex-wrap'>
|
||||
{tagOptions.map(tag => (
|
||||
<TagItem key={tag.name} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Layout404,
|
||||
LayoutArchive,
|
||||
LayoutBase,
|
||||
LayoutCategoryIndex,
|
||||
LayoutIndex,
|
||||
LayoutPostList,
|
||||
LayoutSearch,
|
||||
LayoutSlug,
|
||||
LayoutTagIndex,
|
||||
CONFIG as THEME_CONFIG
|
||||
}
|
||||
47
themes/movie/style.js
Normal file
47
themes/movie/style.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
/**
|
||||
* 此处样式只对当前主题生效
|
||||
* 此处不支持tailwindCSS的 @apply 语法
|
||||
* @returns
|
||||
*/
|
||||
const Style = () => {
|
||||
return (
|
||||
<style jsx global>{`
|
||||
// 底色
|
||||
.dark body {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.shadow-movie {
|
||||
box-shadow:
|
||||
0 26px 58px 0 rgba(0, 0, 0, 0.22),
|
||||
0 5px 14px 0 rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
// 视频聚合走马灯
|
||||
.notion-carousel {
|
||||
width: 100%; /* 根据需要调整 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notion-carousel-wrapper .notion-carousel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notion-carousel-wrapper .notion-carousel.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.notion-carousel-route div {
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.notion-carousel-route div:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}</style>
|
||||
)
|
||||
}
|
||||
|
||||
export { Style }
|
||||
@@ -9,7 +9,7 @@ import { MenuList } from './MenuList'
|
||||
* @param {*} props
|
||||
* @returns
|
||||
*/
|
||||
export default function NavBar (props) {
|
||||
export default function NavBar(props) {
|
||||
const [showSearchInput, changeShowSearchInput] = useState(false)
|
||||
const router = useRouter()
|
||||
const { searchModal } = useSimpleGlobal()
|
||||
@@ -23,7 +23,7 @@ export default function NavBar (props) {
|
||||
}
|
||||
}
|
||||
|
||||
const onKeyUp = (e) => {
|
||||
const onKeyUp = e => {
|
||||
if (e.keyCode === 13) {
|
||||
const search = document.getElementById('theme-simple-search').value
|
||||
if (search) {
|
||||
@@ -33,19 +33,41 @@ export default function NavBar (props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="w-full bg-white md:pt-0 relative z-20 shadow border-t border-gray-100 dark:border-hexo-black-gray dark:bg-black">
|
||||
<div id="nav-bar-inner" className="h-12 mx-auto max-w-9/10 justify-between items-center text-sm md:text-md md:justify-start">
|
||||
{/* 左侧菜单 */}
|
||||
<div className="h-full w-full float-left text-center md:text-left flex flex-wrap items-stretch md:justify-start md:items-start space-x-4">
|
||||
{showSearchInput && <input autoFocus id="theme-simple-search" onKeyUp={onKeyUp} className='float-left w-full outline-none h-full px-4' aria-label="Submit search" type="search" name="s" autoComplete="off" placeholder="Type then hit enter to search..." />}
|
||||
{!showSearchInput && (<MenuList {...props}/>)}
|
||||
</div>
|
||||
<nav className="w-full bg-white md:pt-0 relative z-20 shadow border-t border-gray-100 dark:border-hexo-black-gray dark:bg-black">
|
||||
<div
|
||||
id="nav-bar-inner"
|
||||
className="h-12 mx-auto max-w-9/10 justify-between items-center text-sm md:text-md md:justify-start"
|
||||
>
|
||||
{/* 左侧菜单 */}
|
||||
<div className="h-full w-full float-left text-center md:text-left flex flex-wrap items-stretch md:justify-start md:items-start space-x-4">
|
||||
{showSearchInput && (
|
||||
<input
|
||||
autoFocus
|
||||
id="theme-simple-search"
|
||||
onKeyUp={onKeyUp}
|
||||
className="float-left w-full outline-none h-full px-4"
|
||||
aria-label="Submit search"
|
||||
type="search"
|
||||
name="s"
|
||||
autoComplete="off"
|
||||
placeholder="Type then hit enter to search..."
|
||||
/>
|
||||
)}
|
||||
{!showSearchInput && <MenuList {...props} />}
|
||||
</div>
|
||||
|
||||
<div className="absolute right-12 h-full text-center px-2 flex items-center text-blue-400 cursor-pointer">
|
||||
{/* <!-- extra links --> */}
|
||||
<i className={showSearchInput ? 'fa-regular fa-circle-xmark' : 'fa-solid fa-magnifying-glass' + ' align-middle'} onClick={toggleShowSearchInput}></i>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="absolute right-12 h-full text-center px-2 flex items-center text-blue-400 cursor-pointer">
|
||||
{/* <!-- extra links --> */}
|
||||
<i
|
||||
className={
|
||||
showSearchInput
|
||||
? 'fa-regular fa-circle-xmark'
|
||||
: 'fa-solid fa-magnifying-glass' + ' align-middle'
|
||||
}
|
||||
onClick={toggleShowSearchInput}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
40
yarn.lock
40
yarn.lock
@@ -869,6 +869,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||
|
||||
"@pkgr/core@^0.1.0":
|
||||
version "0.1.1"
|
||||
resolved "https://r.cnpmjs.org/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
|
||||
"@polka/url@^1.0.0-next.20", "@polka/url@^1.0.0-next.24":
|
||||
version "1.0.0-next.24"
|
||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.24.tgz#58601079e11784d20f82d0585865bb42305c4df3"
|
||||
@@ -2415,6 +2420,11 @@ eslint-config-next@^13.1.1:
|
||||
eslint-plugin-react "^7.33.2"
|
||||
eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705"
|
||||
|
||||
eslint-config-prettier@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://r.cnpmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f"
|
||||
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
|
||||
|
||||
eslint-config-standard@^16.0.2:
|
||||
version "16.0.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516"
|
||||
@@ -2514,6 +2524,14 @@ eslint-plugin-node@^11.1.0:
|
||||
resolve "^1.10.1"
|
||||
semver "^6.1.0"
|
||||
|
||||
eslint-plugin-prettier@^5.1.3:
|
||||
version "5.1.3"
|
||||
resolved "https://r.cnpmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz#17cfade9e732cef32b5f5be53bd4e07afd8e67e1"
|
||||
integrity sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==
|
||||
dependencies:
|
||||
prettier-linter-helpers "^1.0.0"
|
||||
synckit "^0.8.6"
|
||||
|
||||
eslint-plugin-promise@^5.1.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-5.2.0.tgz#a596acc32981627eb36d9d75f9666ac1a4564971"
|
||||
@@ -2685,6 +2703,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
version "1.3.0"
|
||||
resolved "https://r.cnpmjs.org/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
|
||||
fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
|
||||
@@ -4133,6 +4156,13 @@ prelude-ls@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier-linter-helpers@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://r2.cnpmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
|
||||
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
|
||||
dependencies:
|
||||
fast-diff "^1.1.2"
|
||||
|
||||
prettier@3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://r.cnpmjs.org/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368"
|
||||
@@ -4831,6 +4861,14 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
synckit@^0.8.6:
|
||||
version "0.8.8"
|
||||
resolved "https://r.cnpmjs.org/synckit/-/synckit-0.8.8.tgz#fe7fe446518e3d3d49f5e429f443cf08b6edfcd7"
|
||||
integrity sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==
|
||||
dependencies:
|
||||
"@pkgr/core" "^0.1.0"
|
||||
tslib "^2.6.2"
|
||||
|
||||
table@^6.0.9:
|
||||
version "6.8.1"
|
||||
resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf"
|
||||
@@ -4968,7 +5006,7 @@ tslib@^1.11.1:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0:
|
||||
tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.5.0, tslib@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
|
||||
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
|
||||
|
||||
Reference in New Issue
Block a user