diff --git a/components/PWA.js b/components/PWA.js new file mode 100644 index 00000000..118a7c18 --- /dev/null +++ b/components/PWA.js @@ -0,0 +1,80 @@ +import { compressImage } from '@/lib/notion/mapImage' +import { isBrowser } from '../lib/utils' + +/** + * 初始化PWA + * @see https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps + * @param {*} props + * @returns + */ +export function PWA(post, siteInfo) { + if (!isBrowser || !post) { + return + } + // 将 manifest 数据转换为 JSON 字符串 + const manifestData = { + id: post?.id, + name: post?.title + ' | ' + siteInfo.title, + short_name: post?.title, + description: post?.summary || siteInfo.description, + icons: [ + { + src: compressImage(post?.pageCoverThumbnail, 192), + type: 'image/png', + sizes: '192x192' + } + ], + form_factor: 'phone', + start_url: window.location.href, + scope: window.location.href, + display: 'standalone', + background_color: '#181818', + theme_color: '#181818' + } + + // 删除已有的 manifest link 元素(如果存在) + const existingManifest = document.querySelector('link[rel="manifest"]') + if (existingManifest) { + existingManifest.parentNode.removeChild(existingManifest) + } + + // 创建 manifest 元素 + const manifest = document.createElement('link') + manifest.rel = 'manifest' + + // 设置 manifest 的 href 为一个 Blob URL + const blobUrl = URL.createObjectURL( + new Blob([JSON.stringify(manifestData)], { + type: 'application/json' + }) + ) + // 这里会报错,因为前端收到的事一个转义了双引号的字符串,无法解析成json,不知道怎么解决 + manifest.href = blobUrl + + // 将 manifest 添加到 head 中 + document.head.appendChild(manifest) + + // 不要忘记在适当的时候释放 Blob URL,避免内存泄漏 + // 例如,在页面卸载或不再需要该 Blob URL 时 + window.addEventListener('unload', () => { + URL.revokeObjectURL(blobUrl) + }) +} + +/** + * 截去url结尾的 / , 便于和slug拼接 + * @param {*} str + * @returns + */ +// function getRootPath() { +// const protocol = window.location.protocol +// const hostname = window.location.hostname +// const port = window.location.port + +// // 如果端口号存在且不是默认的80或443,则包含端口号 +// if (port && port !== '80' && port !== '443') { +// return protocol + '//' + hostname + ':' + port +// } else { +// return protocol + '//' + hostname +// } +// } diff --git a/lib/db/getSiteData.js b/lib/db/getSiteData.js index 0abe35ef..e7771025 100755 --- a/lib/db/getSiteData.js +++ b/lib/db/getSiteData.js @@ -225,7 +225,7 @@ function getCategoryOptions(schema) { * @param from * @returns {Promise<{title,description,pageCover,icon}>} */ -function getSiteInfo({ collection, block }) { +function getSiteInfo({ collection, block, NOTION_CONFIG }) { const title = collection?.name?.[0][0] || BLOG.TITLE const description = collection?.description ? Object.assign(collection).description[0][0] @@ -233,19 +233,21 @@ function getSiteInfo({ collection, block }) { 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) + let icon = compressImage( + collection?.icon + ? mapImgUrl(collection?.icon, collection, 'collection') + : BLOG.AVATAR + ) + // 站点网址 + const link = NOTION_CONFIG?.LINK || BLOG.LINK - // 站点图标不能是emoji情 + // 站点图标不能是emoji const emojiPattern = /\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g if (!icon || emojiPattern.test(icon)) { icon = BLOG.AVATAR } - return { title, description, pageCover, icon } + return { title, description, pageCover, icon, link } } /** @@ -355,7 +357,6 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { return EmptyData(pageId) } const collection = Object.values(pageRecordMap.collection)[0]?.value || {} - const siteInfo = getSiteInfo({ collection, block }) const collectionId = rawMetadata?.collection_id const collectionQuery = pageRecordMap.collection_query const collectionView = pageRecordMap.collection_view @@ -422,6 +423,11 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { // 文章计数 let postCount = 0 + // 站点配置优先读取配置表格,否则读取blog.config.js 文件 + const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} + + const siteInfo = getSiteInfo({ collection, block }) + // 查找所有的Post和Page const allPages = collectionData.filter(post => { if (post?.type === 'Post' && post.status === 'Published') { @@ -435,9 +441,6 @@ async function getDataBaseInfoByNotionAPI({ pageId, from }) { ) }) - // 站点配置优先读取配置表格,否则读取blog.config.js 文件 - const NOTION_CONFIG = (await getConfigMapFromConfigPage(collectionData)) || {} - // Sort by date if (BLOG.POSTS_SORT_BY === 'date') { allPages.sort((a, b) => { diff --git a/lib/utils/index.js b/lib/utils/index.js index 48592fc1..b0652494 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,5 +1,5 @@ // 封装异步加载资源的方法 -import { memo } from 'react'; +import { memo } from 'react' /** * 判断是否客户端 @@ -9,18 +9,18 @@ export const isBrowser = typeof window !== 'undefined' /** * 打乱数组 - * @param {*} array - * @returns + * @param {*} array + * @returns */ -export const shuffleArray = (array) => { - if (!array) { - return [] - } - for (let i = array.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [array[i], array[j]] = [array[j], array[i]]; - } - return array; +export const shuffleArray = array => { + if (!array) { + return [] + } + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)) + ;[array[i], array[j]] = [array[j], array[i]] + } + return array } /** diff --git a/next.config.js b/next.config.js index fd0130ec..c4e9d8e4 100644 --- a/next.config.js +++ b/next.config.js @@ -96,19 +96,27 @@ module.exports = withBundleAnalyzer({ if (!isServer) { console.log('[加载主题]', path.resolve(__dirname, 'themes', THEME)) } - config.resolve.alias['@theme-components'] = path.resolve(__dirname, 'themes', THEME) + config.resolve.alias['@theme-components'] = path.resolve( + __dirname, + 'themes', + THEME + ) return config }, experimental: { scrollRestoration: true }, - exportPathMap: async function (defaultPathMap, { dev, dir, outDir, distDir, buildId }) { - // 导出时 忽略/pages/sitemap.xml.js , 否则报错getServerSideProps + exportPathMap: async function ( + defaultPathMap, + { dev, dir, outDir, distDir, buildId } + ) { + // export 静态导出时 忽略/pages/sitemap.xml.js , 否则和getServerSideProps这个动态文件冲突 const pages = { ...defaultPathMap } delete pages['/sitemap.xml'] return pages }, - publicRuntimeConfig: { // 这里的配置既可以服务端获取到,也可以在浏览器端获取到 + publicRuntimeConfig: { + // 这里的配置既可以服务端获取到,也可以在浏览器端获取到 NODE_ENV_API: process.env.NODE_ENV_API || 'prod', THEMES: themes } diff --git a/pages/index.js b/pages/index.js index 286b1b12..9f2dfb91 100644 --- a/pages/index.js +++ b/pages/index.js @@ -13,7 +13,10 @@ import { useRouter } from 'next/router' */ const Index = props => { // 根据页面路径加载不同Layout文件 - const Layout = getLayoutByTheme({ theme: siteConfig('THEME'), router: useRouter() }) + const Layout = getLayoutByTheme({ + theme: siteConfig('THEME'), + router: useRouter() + }) return } @@ -25,7 +28,9 @@ export async function getStaticProps() { const from = 'index' const props = await getGlobalData({ from }) - props.posts = props.allPages?.filter(page => page.type === 'Post' && page.status === 'Published') + props.posts = props.allPages?.filter( + page => page.type === 'Post' && page.status === 'Published' + ) // 处理分页 if (siteConfig('POST_LIST_STYLE') === 'scroll') { @@ -41,7 +46,11 @@ export async function getStaticProps() { if (post.password && post.password !== '') { continue } - post.blockMap = await getPostBlocks(post.id, 'slug', siteConfig('POST_PREVIEW_LINES')) + post.blockMap = await getPostBlocks( + post.id, + 'slug', + siteConfig('POST_PREVIEW_LINES') + ) } } diff --git a/themes/game/components/FullScreen.js b/themes/game/components/FullScreenButton.js similarity index 95% rename from themes/game/components/FullScreen.js rename to themes/game/components/FullScreenButton.js index 223f740d..d6374631 100644 --- a/themes/game/components/FullScreen.js +++ b/themes/game/components/FullScreenButton.js @@ -4,7 +4,7 @@ * 全屏按钮 * @returns */ -export default function FullScreen() { +export default function FullScreenButton() { function toggleFullScreen() { // window.scrollTo(0, 2) document?.querySelector('#game-wrapper')?.scrollIntoView({ diff --git a/themes/game/components/GameListIndexCombine.js b/themes/game/components/GameListIndexCombine.js index 030787da..897296a2 100644 --- a/themes/game/components/GameListIndexCombine.js +++ b/themes/game/components/GameListIndexCombine.js @@ -37,6 +37,7 @@ export const GameListIndexCombine = ({ posts }) => { // 试图将4合一卡组塞满 while (gamesClone?.length > 0 && groupItems.length < 4) { const item = gamesClone.shift() + index++ if ( item.tags?.some( t => t === siteConfig('GAME_RECOMMEND_TAG', 'Recommend', CONFIG) @@ -59,6 +60,7 @@ export const GameListIndexCombine = ({ posts }) => { // 剩余的4合一不满4个的给他放大卡 while (groupItems.length > 0) { const item = groupItems.shift() + index++ components.push( ) diff --git a/themes/game/index.js b/themes/game/index.js index f6e60bd5..8c6db087 100644 --- a/themes/game/index.js +++ b/themes/game/index.js @@ -4,6 +4,7 @@ import { Draggable } from '@/components/Draggable' import { AdSlot } from '@/components/GoogleAdsense' import replaceSearchResult from '@/components/Mark' import NotionPage from '@/components/NotionPage' +import { PWA as initialPWA } from '@/components/PWA' import ShareBar from '@/components/ShareBar' import { siteConfig } from '@/lib/config' import { loadWowJS } from '@/lib/plugins/wow' @@ -17,7 +18,7 @@ import { BlogListPage } from './components/BlogListPage' import { BlogListScroll } from './components/BlogListScroll' import BlogPostBar from './components/BlogPostBar' import { Footer } from './components/Footer' -import FullScreen from './components/FullScreen' +import FullScreenButton from './components/FullScreenButton' import { GameListIndexCombine } from './components/GameListIndexCombine' import { GameListRelate } from './components/GameListRealate' import { GameListRecent } from './components/GameListRecent' @@ -45,9 +46,6 @@ export const useGameGlobal = () => useContext(ThemeGlobalGame) */ const LayoutBase = props => { const { allNavPages, children } = props - - // const fullWidth = post?.fullWidth ?? false - // const { onLoading } = useGlobal() const searchModal = useRef(null) // 在列表中进行实时过滤 const [filterKey, setFilterKey] = useState('') @@ -279,13 +277,17 @@ const LayoutArchive = props => { * @returns */ const LayoutSlug = props => { - const { post, allNavPages, recommendPosts, lock, validPassword } = props + const { post, siteInfo, allNavPages, recommendPosts, lock, validPassword } = + props const game = deepClone(post) const [loading, setLoading] = useState(true) // const [url, setUrl] = useState(game?.ext?.href) const relateGames = recommendPosts const randomGames = shuffleArray(deepClone(allNavPages)) + // 初始化可安装应用 + initialPWA(game, siteInfo) + // 将当前游戏加入到最近游玩 useEffect(() => { // 更新最新游戏 @@ -403,8 +405,8 @@ const LayoutSlug = props => { {/* 游戏窗口装饰器 */} {game && !loading && (
- {/* 加入全屏按钮 */} - + {/* 全屏按钮 */} +
)}