diff --git a/config/dev.yaml b/config/dev.yaml index 41220bc8..66572641 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -1,18 +1,28 @@ +# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可 +dynamic: + # 输入 logo 的图片地址 + logo: '' + appName: '云策文档' + appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。' + appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think' + # 版权信息,可以是 HTML + copyrightInformation: '

Develop by fantasticit

' + # 外部跳转按钮配置 + externalButtonText: 'Github' + externalButtonURL: 'https://github.com/fantasticit/think' + # 开发环境配置 client: port: 5001 assetPrefix: '/' apiUrl: 'http://localhost:5002/api' collaborationUrl: 'ws://localhost:5003' - # 以下为页面 meta 配置 - seoAppName: '云策文档' - seoDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。' - seoKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think' # 预先连接的来源,空格分割(比如图片存储服务器) dnsPrefetch: '//wipi.oss-cn-shanghai.aliyuncs.com' # 站点地址(如:https://think.codingit.cn/),一定要设置,否则会出现 cookie、跨域等问题 - siteUrl: 'http://localhost:5001' - # 一定要设置,否则会出现 cookie、跨域等问题,注意如果网页部署在 https://think.codingit.cn,那这里的值应当为 .codingit.cn(没有 think) + siteUrl: + 'http://localhost:5001' + # 一定要设置,否则会出现 cookie、跨域等问题,注意如果网页部署在 https://think.codingit.cn,那这里的值应当为 .codingit.cn(没有 think) siteDomain: '' server: @@ -58,18 +68,18 @@ oss: # 线上更改为服务端地址(如:https://api.codingit.cn) server: 'http://localhost:5002' s3: - enable: true + enable: false config: - # isp 提供商,可选 minio,s3,aliyun,tencent - cloudisp: 'minio' - accessKeyId: '' - secretAccessKey: '' - bucket: '' - region: '' - # 仅cloudisp 的值为 minio/s3 时配置,其它提供商为空 - forcePathStyle: false - # 仅cloudisp 的值为 minio 时配置,其它提供商为空 - endpoint: '' + # isp 提供商,可选 minio,s3,aliyun,tencent + cloudisp: 'minio' + accessKeyId: '' + secretAccessKey: '' + bucket: '' + region: '' + # 仅cloudisp 的值为 minio/s3 时配置,其它提供商为空 + forcePathStyle: false + # 仅cloudisp 的值为 minio 时配置,其它提供商为空 + endpoint: '' # jwt 配置 jwt: diff --git a/config/docker-prod-sample.yaml b/config/docker-prod-sample.yaml index e60c2c9b..bbe45e47 100644 --- a/config/docker-prod-sample.yaml +++ b/config/docker-prod-sample.yaml @@ -1,3 +1,16 @@ +# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可 +dynamic: + # 输入 logo 的图片地址 + logo: '' + appName: '云策文档' + appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。' + appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think' + # 版权信息,可以是 HTML + copyrightInformation: '

Develop by fantasticit

' + # 外部跳转按钮配置 + externalButtonText: 'Github' + externalButtonURL: 'https://github.com/fantasticit/think' + # 开发环境配置 client: port: 5001 diff --git a/packages/client/next.config.js b/packages/client/next.config.js index e93f21a5..ea17d882 100644 --- a/packages/client/next.config.js +++ b/packages/client/next.config.js @@ -25,9 +25,7 @@ const nextConfig = semi({ COLLABORATION_API_URL: config.client.collaborationUrl, ENABLE_OSS_S3: config.oss.s3.enable, DNS_PREFETCH: (config.client.dnsPrefetch || '').split(' '), - SEO_APPNAME: config.client.seoAppName, - SEO_DESCRIPTION: config.client.seoDescription, - SEO_KEYWORDS: config.client.seoKeywords, + SITE_URL: config.client.siteUrl, }, webpack: (config, { dev, isServer }) => { config.resolve.plugins.push(new TsconfigPathsPlugin()); diff --git a/packages/client/src/components/author.tsx b/packages/client/src/components/author.tsx index 112515d9..ba1c3fd1 100644 --- a/packages/client/src/components/author.tsx +++ b/packages/client/src/components/author.tsx @@ -1,18 +1,26 @@ import { IconLikeHeart } from '@douyinfe/semi-icons'; import { Space, Typography } from '@douyinfe/semi-ui'; +import { RuntimeConfig } from 'hooks/use-runtime-config'; + const { Text } = Typography; export const Author = () => { + const config = RuntimeConfig.useHook(); + return (
- - - Develop by - fantasticit - with - - + {config.copyrightInformation ? ( +
+ ) : ( + + + Develop by + fantasticit + with + + + )}
); }; diff --git a/packages/client/src/components/logo/index.tsx b/packages/client/src/components/logo/index.tsx index 2cf5de17..2cc1d24a 100644 --- a/packages/client/src/components/logo/index.tsx +++ b/packages/client/src/components/logo/index.tsx @@ -1,43 +1,50 @@ import { Typography } from '@douyinfe/semi-ui'; +import { RuntimeConfig } from 'hooks/use-runtime-config'; import Link from 'next/link'; import styles from './index.module.scss'; const { Text } = Typography; -export const LogoName = process.env.SEO_APPNAME; - export const LogoImage = () => { + const config = RuntimeConfig.useHook(); + return ( - - + ) : ( + - - - + + + + + )} ); }; export const LogoText = () => { + const config = RuntimeConfig.useHook(); + return ( - {LogoName} + {config?.appName} ); diff --git a/packages/client/src/components/seo.tsx b/packages/client/src/components/seo.tsx index 6db2c9fb..e36483b3 100644 --- a/packages/client/src/components/seo.tsx +++ b/packages/client/src/components/seo.tsx @@ -1,21 +1,27 @@ -import React, { useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Helmet } from 'react-helmet'; +import { RuntimeConfig } from 'hooks/use-runtime-config'; + interface IProps { title: string; needTitleSuffix?: boolean; } -const buildTitle = (title) => `${title} - 云策文档`; - export const Seo: React.FC = ({ title, needTitleSuffix = true }) => { + const config = RuntimeConfig.useHook(); + + const buildTitle = useCallback(() => { + return `${title} - ${config?.appName}`; + }, [title, config?.appName]); + useEffect(() => { - window.document.title = needTitleSuffix ? buildTitle(title) : title; - }, [title, needTitleSuffix]); + window.document.title = needTitleSuffix ? buildTitle() : title; + }, [title, needTitleSuffix, buildTitle]); return ( - {needTitleSuffix ? buildTitle(title) : title} + {needTitleSuffix ? buildTitle() : title} ); }; diff --git a/packages/client/src/hooks/use-runtime-config.tsx b/packages/client/src/hooks/use-runtime-config.tsx new file mode 100644 index 00000000..5c7a71c2 --- /dev/null +++ b/packages/client/src/hooks/use-runtime-config.tsx @@ -0,0 +1,21 @@ +import { useState } from 'react'; + +import { createGlobalHook } from './create-global-hook'; + +export type IRuntimeConfig = { + logo?: string; + appName?: string; + appDescription?: string; + appKeywords?: string; + copyrightInformation?: string; + externalButtonText?: string; + externalButtonURL?: string; +}; + +const useRuntimeConfig = (defaultConfig: IRuntimeConfig = {}) => { + const [config, setConfig] = useState(defaultConfig); + + return config; +}; + +export const RuntimeConfig = createGlobalHook(useRuntimeConfig); diff --git a/packages/client/src/pages/_app.tsx b/packages/client/src/pages/_app.tsx index b3ff8308..444df2ef 100644 --- a/packages/client/src/pages/_app.tsx +++ b/packages/client/src/pages/_app.tsx @@ -11,6 +11,7 @@ import { preloadTiptapResources } from 'tiptap/preload'; import { isMobile } from 'helpers/env'; import { DocumentVersionControl } from 'hooks/use-document-version'; import { IsOnMobile } from 'hooks/use-on-mobile'; +import { IRuntimeConfig, RuntimeConfig } from 'hooks/use-runtime-config'; import { Theme } from 'hooks/use-theme'; import App from 'next/app'; import Head from 'next/head'; @@ -19,7 +20,7 @@ import 'viewerjs/dist/viewer.css'; import 'styles/globals.scss'; import 'thirtypart/array-prototype-at'; -class MyApp extends App<{ isMobile: boolean }> { +class MyApp extends App<{ isMobile: boolean; runtimeConfig: IRuntimeConfig }> { state = { queryClient: new QueryClient({ defaultOptions: { @@ -36,13 +37,19 @@ class MyApp extends App<{ isMobile: boolean }> { static getInitialProps = async ({ Component, ctx }) => { const request = ctx?.req; const getPagePropsPromise = Component.getInitialProps ? Component.getInitialProps(ctx) : Promise.resolve({}); - const [pageProps] = await Promise.all([getPagePropsPromise]).catch((err) => { - return [{}]; + const [pageProps, runtimeConfig] = await Promise.all([ + getPagePropsPromise, + fetch(`${process.env.SITE_URL}/api/config`).then((res) => { + return res.json(); + }), + ]).catch((err) => { + return [{}, {}]; }); return { pageProps, isMobile: isMobile(request?.headers['user-agent']), + runtimeConfig, }; }; @@ -57,7 +64,7 @@ class MyApp extends App<{ isMobile: boolean }> { } render() { - const { Component, pageProps, isMobile } = this.props; + const { Component, pageProps, isMobile, runtimeConfig } = this.props; const { queryClient } = this.state; return ( @@ -68,24 +75,23 @@ class MyApp extends App<{ isMobile: boolean }> { content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover" /> - - - + + + - - + + - - - + + + - {((process.env.DNS_PREFETCH || []) as string[]).map((url) => ( ))} @@ -94,11 +100,13 @@ class MyApp extends App<{ isMobile: boolean }> { - - - - - + + + + + + + diff --git a/packages/client/src/pages/api/config.ts b/packages/client/src/pages/api/config.ts new file mode 100644 index 00000000..a281daa2 --- /dev/null +++ b/packages/client/src/pages/api/config.ts @@ -0,0 +1,7 @@ +import { getRuntimeConfig } from '@think/config'; + +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(req: NextApiRequest, res: NextApiResponse>) { + res.status(200).json(getRuntimeConfig() as Record); +} diff --git a/packages/client/src/pages/index.tsx b/packages/client/src/pages/index.tsx index 2c9fdb8e..a12e6e1c 100644 --- a/packages/client/src/pages/index.tsx +++ b/packages/client/src/pages/index.tsx @@ -4,6 +4,7 @@ import { Button, Typography } from '@douyinfe/semi-ui'; import { Seo } from 'components/seo'; import { toLogin, useUser } from 'data/user'; +import { RuntimeConfig } from 'hooks/use-runtime-config'; import { TeamWorkIllustration } from 'illustrations/team-work'; import { SingleColumnLayout } from 'layouts/single-column'; import type { NextPage } from 'next'; @@ -14,6 +15,8 @@ import styles from './index.module.scss'; const { Title, Paragraph } = Typography; const Page: NextPage = () => { + const config = RuntimeConfig.useHook(); + const { user } = useUser(); const start = useCallback(() => { @@ -24,9 +27,9 @@ const Page: NextPage = () => { } }, [user]); - const toGithub = useCallback(() => { - window.open('https://github.com/fantasticit/think'); - }, []); + const toExternal = useCallback(() => { + window.open(config.externalButtonURL); + }, [config.externalButtonURL]); return ( @@ -36,18 +39,18 @@ const Page: NextPage = () => {
- 云策文档 - - 云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。 - + {config?.appName} + {config?.appDescription}
- + {config.externalButtonText && ( + + )}
diff --git a/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx b/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx index 2b4a30f3..c8e5d5a4 100644 --- a/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx +++ b/packages/client/src/tiptap/editor/collaboration/collaboration/editor.tsx @@ -12,12 +12,12 @@ import cls from 'classnames'; import { Banner } from 'components/banner'; import { CommentEditor } from 'components/document/comments'; import { ImageViewer } from 'components/image-viewer'; -import { LogoName } from 'components/logo'; import { getRandomColor } from 'helpers/color'; import { isAndroid, isIOS } from 'helpers/env'; import { useDocumentStyle } from 'hooks/use-document-style'; import { useNetwork } from 'hooks/use-network'; import { IsOnMobile } from 'hooks/use-on-mobile'; +import { RuntimeConfig } from 'hooks/use-runtime-config'; import { useToggle } from 'hooks/use-toggle'; import { CollaborationKit } from '../kit'; @@ -47,6 +47,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => { renderInEditorPortal, onTitleUpdate, } = props; + const config = RuntimeConfig.useHook(); const $headerContainer = useRef(); const $mainContainer = useRef(); const { isMobile } = IsOnMobile.useHook(); @@ -110,7 +111,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => { const listener = (event: KeyboardEvent) => { if ((event.ctrlKey || event.metaKey) && event.keyCode == 83) { event.preventDefault(); - Toast.info(`${LogoName}会实时保存你的数据,无需手动保存。`); + Toast.info(`${config.appName}会实时保存你的数据,无需手动保存。`); return false; } }; @@ -120,7 +121,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => { return () => { window.document.removeEventListener('keydown', listener); }; - }, []); + }, [config.appName]); // 监听键盘收起、打开 useEffect(() => { diff --git a/packages/config/lib/index.d.ts b/packages/config/lib/index.d.ts index 6389de57..80ef0330 100644 --- a/packages/config/lib/index.d.ts +++ b/packages/config/lib/index.d.ts @@ -1 +1,2 @@ export declare function getConfig(): Record; +export declare function getRuntimeConfig(): Record; diff --git a/packages/config/lib/index.js b/packages/config/lib/index.js index 43a39270..2e0dec48 100644 --- a/packages/config/lib/index.js +++ b/packages/config/lib/index.js @@ -1,6 +1,6 @@ "use strict"; exports.__esModule = true; -exports.getConfig = void 0; +exports.getRuntimeConfig = exports.getConfig = void 0; var fs = require("fs-extra"); var yaml = require("js-yaml"); var path = require("path"); @@ -9,8 +9,8 @@ var FILE_ENV_NAME = { test: 'test', production: 'prod' }; -var env = process.env.NODE_ENV || 'development'; function getConfig() { + var env = process.env.NODE_ENV || 'development'; var filePath = path.join(__dirname, '../../../config', "".concat(FILE_ENV_NAME[env], ".yaml")); if (!fs.existsSync(filePath)) { throw new Error("Can not find config file: ".concat(filePath)); @@ -18,3 +18,15 @@ function getConfig() { return yaml.load(fs.readFileSync(filePath, 'utf8')); } exports.getConfig = getConfig; +function getRuntimeConfig() { + var env = process.env.NODE_ENV || 'development'; + var filePath = path.join(__dirname, '../../../../../../config', "".concat(FILE_ENV_NAME[env], ".yaml")); + if (!fs.existsSync(filePath)) { + filePath = path.join(__dirname, '../../../config', "".concat(FILE_ENV_NAME[env], ".yaml")); + } + if (!fs.existsSync(filePath)) { + throw new Error("Can not find config file: ".concat(filePath)); + } + return (yaml.load(fs.readFileSync(filePath, 'utf8')).dynamic || {}); +} +exports.getRuntimeConfig = getRuntimeConfig; diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index 11016ed7..704c23a6 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -8,9 +8,9 @@ const FILE_ENV_NAME = { production: 'prod', }; -const env = process.env.NODE_ENV || 'development'; - export function getConfig() { + const env = process.env.NODE_ENV || 'development'; + const filePath = path.join(__dirname, '../../../config', `${FILE_ENV_NAME[env]}.yaml`); if (!fs.existsSync(filePath)) { @@ -19,3 +19,19 @@ export function getConfig() { return yaml.load(fs.readFileSync(filePath, 'utf8')) as Record; } + +export function getRuntimeConfig() { + const env = process.env.NODE_ENV || 'development'; + + let filePath = path.join(__dirname, '../../../../../../config', `${FILE_ENV_NAME[env]}.yaml`); + + if (!fs.existsSync(filePath)) { + filePath = path.join(__dirname, '../../../config', `${FILE_ENV_NAME[env]}.yaml`); + } + + if (!fs.existsSync(filePath)) { + throw new Error(`Can not find config file: ${filePath}`); + } + + return (yaml.load(fs.readFileSync(filePath, 'utf8')).dynamic || {}) as Record; +}