diff --git a/config/dev.yaml b/config/dev.yaml
index 41220bc8..b0e3900a 100644
--- a/config/dev.yaml
+++ b/config/dev.yaml
@@ -4,15 +4,12 @@ client:
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 +55,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/dynamic.yaml b/dynamic.yaml
new file mode 100644
index 00000000..d3d34347
--- /dev/null
+++ b/dynamic.yaml
@@ -0,0 +1,12 @@
+# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可
+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'
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/admin/system-config/ad/index.tsx b/packages/client/src/components/admin/system-config/ad/index.tsx
new file mode 100644
index 00000000..d5434bf4
--- /dev/null
+++ b/packages/client/src/components/admin/system-config/ad/index.tsx
@@ -0,0 +1,141 @@
+import React, { useCallback, useRef, useState } from 'react';
+
+import { Avatar, Button, Form, Image, Modal, Space, Table } from '@douyinfe/semi-ui';
+import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
+
+import { AdType } from '@think/domains';
+
+import { DataRender } from 'components/data-render';
+import { Upload } from 'components/upload';
+import { useAd } from 'data/ad';
+
+const { Option } = Form.Select;
+
+const AdTypeTextMap = {
+ [AdType.shareDocCover]: '分享文档主图位',
+ [AdType.shareDocAside]: '分享文档侧边位',
+};
+
+const columns = [
+ {
+ title: '类型',
+ dataIndex: 'type',
+ render: (type) => {
+ return AdTypeTextMap[type];
+ },
+ },
+ {
+ title: '广告图',
+ dataIndex: 'cover',
+ render: (url) => {
+ return (
+
+
+
+ );
+ },
+ },
+ {
+ title: '广告链接',
+ dataIndex: 'url',
+ },
+];
+
+export const Ad = () => {
+ const { data, loading, error, createAd, deleteAd } = useAd();
+ const [visible, toggleVisible] = useState(false);
+ const $form = useRef();
+ const [currentCover, setCurrentCover] = useState('');
+
+ const setCover = useCallback((url) => {
+ $form.current.setValue('cover', url);
+ setCurrentCover(url);
+ }, []);
+
+ const handleOk = useCallback(() => {
+ $form.current.validate().then((values) => {
+ if (!values.email) {
+ delete values.email;
+ }
+ createAd(values).then(() => {
+ $form.current.reset();
+ setCover('');
+ toggleVisible(false);
+ });
+ });
+ }, [createAd, setCover, toggleVisible]);
+
+ return (
+ (
+
+
+
{
+ toggleVisible(false);
+ }}
+ style={{ maxWidth: '96vw' }}
+ onOk={handleOk}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ return (
+
+ );
+ },
+ },
+ ]}
+ dataSource={data}
+ pagination={false}
+ />
+
+ )}
+ />
+ );
+};
diff --git a/packages/client/src/components/admin/system-config/index.tsx b/packages/client/src/components/admin/system-config/index.tsx
index 8cc4228b..11b7b816 100644
--- a/packages/client/src/components/admin/system-config/index.tsx
+++ b/packages/client/src/components/admin/system-config/index.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
+import { Ad } from './ad';
import { Mail } from './mail';
import { System } from './system';
@@ -13,6 +14,7 @@ interface IProps {
const TitleMap = {
base: '系统管理',
mail: '邮箱服务',
+ ad: '广告管理',
};
export const SystemConfig: React.FC = ({ tab, onNavigate }) => {
@@ -25,6 +27,10 @@ export const SystemConfig: React.FC = ({ tab, onNavigate }) => {
+
+
+
+
);
};
diff --git a/packages/client/src/components/admin/system-config/mail/index.tsx b/packages/client/src/components/admin/system-config/mail/index.tsx
index 968dae5d..dc0a9d00 100644
--- a/packages/client/src/components/admin/system-config/mail/index.tsx
+++ b/packages/client/src/components/admin/system-config/mail/index.tsx
@@ -35,6 +35,8 @@ export const Mail = () => {
closeIcon={null}
/>
+ {/*
+ // @ts-ignore */}
{
error={error}
normalContent={() => (
+ {/*
+ // @ts-ignore */}
{
+ const config = RuntimeConfig.useHook();
+
return (
-
-
- Develop by
- fantasticit
- with
-
-
+ {config.copyrightInformation ? (
+
+ ) : (
+
+
+ Develop by
+ fantasticit
+ with
+
+
+ )}
);
};
diff --git a/packages/client/src/components/document/reader/public/index.module.scss b/packages/client/src/components/document/reader/public/index.module.scss
index 8e9e9891..72a977b3 100644
--- a/packages/client/src/components/document/reader/public/index.module.scss
+++ b/packages/client/src/components/document/reader/public/index.module.scss
@@ -1,4 +1,5 @@
.wrap {
+ position: relative;
display: flex;
flex-direction: column;
height: 100%;
@@ -8,6 +9,23 @@
position: sticky;
}
+ .adWrap {
+ max-height: 180px;
+ overflow: hidden;
+ }
+
+ .adAsideWrap {
+ position: absolute;
+ bottom: 1rem;
+ left: 1rem;
+ width: 180px;
+ height: 70px;
+
+ >div {
+ height: 100%;
+ }
+ }
+
.contentWrap {
flex: 1;
overflow: hidden;
diff --git a/packages/client/src/components/document/reader/public/index.tsx b/packages/client/src/components/document/reader/public/index.tsx
index 945aa199..6ba12a55 100644
--- a/packages/client/src/components/document/reader/public/index.tsx
+++ b/packages/client/src/components/document/reader/public/index.tsx
@@ -1,28 +1,40 @@
-import { Breadcrumb, Button, Form, Layout, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
-import { IconRoute } from '@douyinfe/semi-icons';
-import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
-import { DataRender } from 'components/data-render';
-import { Divider } from 'components/divider';
-import { DocumentStyle } from 'components/document/style';
-import { LogoImage, LogoText } from 'components/logo';
-import { Seo } from 'components/seo';
-import { Theme } from 'components/theme';
-import { User } from 'components/user';
-import { usePublicDocumentDetail } from 'data/document';
-import { useMount } from 'hooks/use-mount';
-import { IsOnMobile } from 'hooks/use-on-mobile';
-import { SecureDocumentIllustration } from 'illustrations/secure-document';
-import Link from 'next/link';
-import React, { useCallback, useMemo, useRef } from 'react';
-import { createPortal } from 'react-dom';
-import { CollaborationEditor } from 'tiptap/editor';
+import {
+ Breadcrumb,
+ Button,
+ Form,
+ Layout,
+ Nav,
+ Skeleton,
+ Space,
+ Tooltip,
+ Typography,
+ Carousel,
+} from "@douyinfe/semi-ui";
+import { IconRoute } from "@douyinfe/semi-icons";
+import { FormApi } from "@douyinfe/semi-ui/lib/es/form";
+import { DataRender } from "components/data-render";
+import { Divider } from "components/divider";
+import { DocumentStyle } from "components/document/style";
+import { LogoImage, LogoText } from "components/logo";
+import { Seo } from "components/seo";
+import { Theme } from "components/theme";
+import { User } from "components/user";
+import { usePublicDocumentDetail } from "data/document";
+import { useMount } from "hooks/use-mount";
+import { IsOnMobile } from "hooks/use-on-mobile";
+import { SecureDocumentIllustration } from "illustrations/secure-document";
+import Link from "next/link";
+import React, { useCallback, useMemo, useRef } from "react";
+import { createPortal } from "react-dom";
+import { CollaborationEditor } from "tiptap/editor";
-import { Author } from '../author';
-import styles from './index.module.scss';
-import Router from 'next/router';
-import { useRouterQuery } from 'hooks/use-router-query';
-import { IDocument, IWiki } from '@think/domains';
-import { DocumentFullscreen } from 'components/document/fullscreen';
+import { Author } from "../author";
+import styles from "./index.module.scss";
+import Router from "next/router";
+import { useRouterQuery } from "hooks/use-router-query";
+import { AdType, IDocument, IWiki } from "@think/domains";
+import { DocumentFullscreen } from "components/document/fullscreen";
+import { useAd } from "data/ad";
const { Header } = Layout;
const { Text } = Typography;
@@ -32,18 +44,25 @@ interface IProps {
hideLogo?: boolean;
}
-export const DocumentPublicReader: React.FC = ({ documentId, hideLogo = false }) => {
+export const DocumentPublicReader: React.FC = ({
+ documentId,
+ hideLogo = false,
+}) => {
const $form = useRef();
const mounted = useMount();
- const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>();
+ const { wikiId: currentWikiId } = useRouterQuery<{
+ wikiId: IWiki["id"];
+ documentId: IDocument["id"];
+ }>();
const { data, loading, error, query } = usePublicDocumentDetail(documentId);
+ const { data: ads } = useAd();
const { isMobile } = IsOnMobile.useHook();
-
+
const renderAuthor = useCallback(
(element) => {
if (!document) return null;
- const target = element && element.querySelector('.ProseMirror .title');
+ const target = element && element.querySelector(".ProseMirror .title");
if (target) {
return createPortal(, target);
@@ -62,27 +81,42 @@ export const DocumentPublicReader: React.FC = ({ documentId, hideLogo =
const toPublicWikiOrDocumentURL = useCallback(() => {
Router.push({
- pathname: currentWikiId ? '/share/document/[documentId]' : '/share/wiki/[wikiId]/document/[documentId]',
- query: currentWikiId ? { documentId } : { wikiId: data.wikiId, documentId },
+ pathname: currentWikiId
+ ? "/share/document/[documentId]"
+ : "/share/wiki/[wikiId]/document/[documentId]",
+ query: currentWikiId
+ ? { documentId }
+ : { wikiId: data.wikiId, documentId },
});
}, [currentWikiId, documentId, data]);
+ console.log(ads || []);
+
const content = useMemo(() => {
if (error) {
// @ts-ignore
if (error.statusCode === 400) {
return (
-
+
+
@@ -94,16 +128,16 @@ export const DocumentPublicReader: React.FC
= ({ documentId, hideLogo =
return (
- {(error && (error as Error).message) || '未知错误'}
+ {(error && (error as Error).message) || "未知错误"}
);
@@ -144,9 +178,14 @@ export const DocumentPublicReader: React.FC = ({ documentId, hideLogo =
}
footer={
- {!isMobile && data && }
-
- } onClick={toPublicWikiOrDocumentURL} />
+ {!isMobile && data && }
+
+ }
+ onClick={toPublicWikiOrDocumentURL}
+ />
@@ -158,11 +197,20 @@ export const DocumentPublicReader: React.FC = ({ documentId, hideLogo =
} loading={true} />}
+ loadingContent={
+ }
+ loading={true}
+ />
+ }
normalContent={() => (
-
+
{data?.wiki?.name}
@@ -172,9 +220,38 @@ export const DocumentPublicReader: React.FC = ({ documentId, hideLogo =
/>
+
+ {(ads || [])
+ .filter((ad) => ad.type === AdType.shareDocCover)
+ .map((ad) => {
+ return (
+
+ );
+ })}
+
{content}
+
+
+
+ {(ads || [])
+ .filter((ad) => ad.type === AdType.shareDocAside)
+ .map((ad) => {
+ return (
+
+ );
+ })}
+
+
);
};
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/data/ad.ts b/packages/client/src/data/ad.ts
new file mode 100644
index 00000000..367dffb6
--- /dev/null
+++ b/packages/client/src/data/ad.ts
@@ -0,0 +1,42 @@
+import { useCallback } from 'react';
+import { useQuery } from 'react-query';
+
+import { AdApiDefinition, IAd } from '@think/domains';
+
+import { HttpClient } from 'services/http-client';
+
+export const useAd = () => {
+ const { data, error, isLoading, refetch } = useQuery(AdApiDefinition.getAll.client(), () =>
+ HttpClient.request({
+ method: AdApiDefinition.getAll.method,
+ url: AdApiDefinition.getAll.client(),
+ })
+ );
+
+ const createAd = useCallback(
+ async (data: Partial) => {
+ const ret = await HttpClient.request({
+ method: AdApiDefinition.create.method,
+ url: AdApiDefinition.create.client(),
+ data,
+ });
+ refetch();
+ return ret;
+ },
+ [refetch]
+ );
+
+ const deleteAd = useCallback(
+ async (id: IAd['id']) => {
+ const ret = await HttpClient.request({
+ method: AdApiDefinition.deleteById.method,
+ url: AdApiDefinition.deleteById.client(id),
+ });
+ refetch();
+ return ret;
+ },
+ [refetch]
+ );
+
+ return { data, error, loading: isLoading, refresh: refetch, createAd, deleteAd };
+};
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..903fd701 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(() => {
@@ -168,6 +169,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<>
{(!online || status === 'disconnected') && (
{
{/* FIXME:需要菜单栏但是无法编辑,则认为进入了编辑模式但是没有编辑权限,也许有更好的判断 */}
{!editable && menubar && (
+ // @ts-ignore
)}
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..20e7a847 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, '../../../../../../', "dynamic.yaml");
+ if (!fs.existsSync(filePath)) {
+ filePath = path.join(__dirname, '../../../config', "dynamic.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/package.json b/packages/config/package.json
index 9d084fe1..05d684d5 100644
--- a/packages/config/package.json
+++ b/packages/config/package.json
@@ -21,5 +21,8 @@
],
"dependencies": {
"js-yaml": "^4.1.0"
+ },
+ "devDependencies": {
+ "typescript": "^4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts
index 11016ed7..9a87668c 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, '../../../../../../', `dynamic.yaml`);
+
+ if (!fs.existsSync(filePath)) {
+ filePath = path.join(__dirname, '../../../config', `dynamic.yaml`);
+ }
+
+ if (!fs.existsSync(filePath)) {
+ throw new Error(`Can not find config file: ${filePath}`);
+ }
+
+ return (yaml.load(fs.readFileSync(filePath, 'utf8')).dynamic || {}) as Record;
+}
diff --git a/packages/constants/package.json b/packages/constants/package.json
index 1bf83a6b..4db957f6 100644
--- a/packages/constants/package.json
+++ b/packages/constants/package.json
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
- "typescript": "^4.5.5"
+ "typescript": "^4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/packages/domains/lib/api/ad.d.ts b/packages/domains/lib/api/ad.d.ts
new file mode 100644
index 00000000..f6b6c5d0
--- /dev/null
+++ b/packages/domains/lib/api/ad.d.ts
@@ -0,0 +1,23 @@
+export declare const AdApiDefinition: {
+ /**
+ * 获取用户
+ */
+ getAll: {
+ method: "get";
+ server: "";
+ client: () => string;
+ };
+ create: {
+ method: "post";
+ server: "";
+ client: () => string;
+ };
+ /**
+ * 获取组织内加星的知识库
+ */
+ deleteById: {
+ method: "delete";
+ server: ":id";
+ client: (id: any) => string;
+ };
+};
diff --git a/packages/domains/lib/api/ad.js b/packages/domains/lib/api/ad.js
new file mode 100644
index 00000000..61fefa20
--- /dev/null
+++ b/packages/domains/lib/api/ad.js
@@ -0,0 +1,26 @@
+"use strict";
+exports.__esModule = true;
+exports.AdApiDefinition = void 0;
+exports.AdApiDefinition = {
+ /**
+ * 获取用户
+ */
+ getAll: {
+ method: 'get',
+ server: '',
+ client: function () { return '/ad'; }
+ },
+ create: {
+ method: 'post',
+ server: '',
+ client: function () { return '/ad'; }
+ },
+ /**
+ * 获取组织内加星的知识库
+ */
+ deleteById: {
+ method: 'delete',
+ server: ':id',
+ client: function (id) { return "/ad/".concat(id); }
+ }
+};
diff --git a/packages/domains/lib/api/index.d.ts b/packages/domains/lib/api/index.d.ts
index 8e000d7f..3606fca8 100644
--- a/packages/domains/lib/api/index.d.ts
+++ b/packages/domains/lib/api/index.d.ts
@@ -1,3 +1,4 @@
+export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
diff --git a/packages/domains/lib/api/index.js b/packages/domains/lib/api/index.js
index 04b1be15..42cffc43 100644
--- a/packages/domains/lib/api/index.js
+++ b/packages/domains/lib/api/index.js
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
@@ -10,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
exports.__esModule = true;
+__exportStar(require("./ad"), exports);
__exportStar(require("./user"), exports);
__exportStar(require("./wiki"), exports);
__exportStar(require("./document"), exports);
diff --git a/packages/domains/lib/index.js b/packages/domains/lib/index.js
index 7cb1843b..ae75a183 100644
--- a/packages/domains/lib/index.js
+++ b/packages/domains/lib/index.js
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
diff --git a/packages/domains/lib/models/ad.d.ts b/packages/domains/lib/models/ad.d.ts
new file mode 100644
index 00000000..cf777db5
--- /dev/null
+++ b/packages/domains/lib/models/ad.d.ts
@@ -0,0 +1,10 @@
+export declare enum AdType {
+ shareDocCover = "shareDocCover",
+ shareDocAside = "shareDocAside"
+}
+export interface IAd {
+ id: string;
+ type: AdType;
+ cover: string;
+ url: string;
+}
diff --git a/packages/domains/lib/models/ad.js b/packages/domains/lib/models/ad.js
new file mode 100644
index 00000000..4b48bb4e
--- /dev/null
+++ b/packages/domains/lib/models/ad.js
@@ -0,0 +1,8 @@
+"use strict";
+exports.__esModule = true;
+exports.AdType = void 0;
+var AdType;
+(function (AdType) {
+ AdType["shareDocCover"] = "shareDocCover";
+ AdType["shareDocAside"] = "shareDocAside";
+})(AdType = exports.AdType || (exports.AdType = {}));
diff --git a/packages/domains/lib/models/index.d.ts b/packages/domains/lib/models/index.d.ts
index c2dd9441..37c705ca 100644
--- a/packages/domains/lib/models/index.d.ts
+++ b/packages/domains/lib/models/index.d.ts
@@ -1,3 +1,4 @@
+export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
diff --git a/packages/domains/lib/models/index.js b/packages/domains/lib/models/index.js
index f96093bc..93bf428a 100644
--- a/packages/domains/lib/models/index.js
+++ b/packages/domains/lib/models/index.js
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
@@ -10,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
exports.__esModule = true;
+__exportStar(require("./ad"), exports);
__exportStar(require("./user"), exports);
__exportStar(require("./wiki"), exports);
__exportStar(require("./document"), exports);
diff --git a/packages/domains/package.json b/packages/domains/package.json
index c025383c..c3468b2b 100644
--- a/packages/domains/package.json
+++ b/packages/domains/package.json
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
- "typescript": "^4.5.5"
+ "typescript": "^4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/packages/domains/src/api/ad.ts b/packages/domains/src/api/ad.ts
new file mode 100644
index 00000000..918d76f5
--- /dev/null
+++ b/packages/domains/src/api/ad.ts
@@ -0,0 +1,25 @@
+export const AdApiDefinition = {
+ /**
+ * 获取用户
+ */
+ getAll: {
+ method: 'get' as const,
+ server: '' as const,
+ client: () => '/ad',
+ },
+
+ create: {
+ method: 'post' as const,
+ server: '' as const,
+ client: () => '/ad',
+ },
+
+ /**
+ * 获取组织内加星的知识库
+ */
+ deleteById: {
+ method: 'delete' as const,
+ server: ':id' as const,
+ client: (id) => `/ad/${id}`,
+ },
+};
diff --git a/packages/domains/src/api/index.ts b/packages/domains/src/api/index.ts
index 8e000d7f..3606fca8 100644
--- a/packages/domains/src/api/index.ts
+++ b/packages/domains/src/api/index.ts
@@ -1,3 +1,4 @@
+export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
diff --git a/packages/domains/src/models/ad.ts b/packages/domains/src/models/ad.ts
new file mode 100644
index 00000000..d00a21da
--- /dev/null
+++ b/packages/domains/src/models/ad.ts
@@ -0,0 +1,11 @@
+export enum AdType {
+ shareDocCover = 'shareDocCover',
+ shareDocAside = 'shareDocAside',
+}
+
+export interface IAd {
+ id: string;
+ type: AdType;
+ cover: string;
+ url: string;
+}
diff --git a/packages/domains/src/models/index.ts b/packages/domains/src/models/index.ts
index c2dd9441..37c705ca 100644
--- a/packages/domains/src/models/index.ts
+++ b/packages/domains/src/models/index.ts
@@ -1,3 +1,4 @@
+export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts
index 05f96ec9..f77de94c 100644
--- a/packages/server/src/app.module.ts
+++ b/packages/server/src/app.module.ts
@@ -6,6 +6,7 @@ import { LoggerModule } from 'nestjs-pino';
import { getConfig } from '@think/config';
+import { AdEntity } from '@entities/ad.entity';
import { AuthEntity } from '@entities/auth.entity';
import { CommentEntity } from '@entities/comment.entity';
import { DocumentEntity } from '@entities/document.entity';
@@ -18,6 +19,7 @@ import { UserEntity } from '@entities/user.entity';
import { WikiEntity } from '@entities/wiki.entity';
import { IS_PRODUCTION } from '@helpers/env.helper';
import { getLogFileName, ONE_DAY } from '@helpers/log.helper';
+import { AdModule } from '@modules/ad.module';
import { AuthModule } from '@modules/auth.module';
import { CommentModule } from '@modules/comment.module';
import { DocumentModule } from '@modules/document.module';
@@ -46,6 +48,7 @@ const ENTITIES = [
MessageEntity,
TemplateEntity,
SystemEntity,
+ AdEntity,
];
const MODULES = [
@@ -62,6 +65,7 @@ const MODULES = [
ViewModule,
VerifyModule,
SystemModule,
+ AdModule,
];
@Module({
diff --git a/packages/server/src/controllers/ad.controller.ts b/packages/server/src/controllers/ad.controller.ts
new file mode 100644
index 00000000..cc9794a5
--- /dev/null
+++ b/packages/server/src/controllers/ad.controller.ts
@@ -0,0 +1,48 @@
+import {
+ Body,
+ ClassSerializerInterceptor,
+ Controller,
+ Delete,
+ Get,
+ HttpCode,
+ HttpStatus,
+ Param,
+ Post,
+ Request,
+ UseGuards,
+ UseInterceptors,
+} from '@nestjs/common';
+
+import { AdApiDefinition } from '@think/domains';
+
+import { AdDto } from '@dtos/ad.dto';
+import { JwtGuard } from '@guard/jwt.guard';
+import { AdService } from '@services/ad.service';
+
+@Controller('ad')
+export class AdController {
+ constructor(private readonly adService: AdService) {}
+
+ @UseInterceptors(ClassSerializerInterceptor)
+ @Post(AdApiDefinition.create.server)
+ @HttpCode(HttpStatus.CREATED)
+ @UseGuards(JwtGuard)
+ async addAd(@Request() req, @Body() data: AdDto) {
+ return await this.adService.addAd(req.user, data);
+ }
+
+ @UseInterceptors(ClassSerializerInterceptor)
+ @Get(AdApiDefinition.getAll.server)
+ @HttpCode(HttpStatus.OK)
+ async getAds() {
+ return await this.adService.getAds();
+ }
+
+ @UseInterceptors(ClassSerializerInterceptor)
+ @Delete(AdApiDefinition.deleteById.server)
+ @HttpCode(HttpStatus.OK)
+ @UseGuards(JwtGuard)
+ async deleteAd(@Request() req, @Param('id') id) {
+ return await this.adService.deleteAd(req.user, id);
+ }
+}
diff --git a/packages/server/src/dtos/ad.dto.ts b/packages/server/src/dtos/ad.dto.ts
new file mode 100644
index 00000000..0e0f4bbd
--- /dev/null
+++ b/packages/server/src/dtos/ad.dto.ts
@@ -0,0 +1,17 @@
+import { AdType } from '@think/domains';
+
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class AdDto {
+ @IsString({ message: '广告位类型错误(正确类型为:String)' })
+ @IsNotEmpty({ message: '广告位不能为空' })
+ type: AdType;
+
+ @IsString({ message: '广告封面图类型错误(正确类型为:String)' })
+ @IsNotEmpty({ message: '广告封面图不能为空' })
+ cover: string;
+
+ @IsString({ message: '广告跳转链接类型错误(正确类型为:String)' })
+ @IsNotEmpty({ message: '广告跳转链接不能为空' })
+ url: string;
+}
diff --git a/packages/server/src/entities/ad.entity.ts b/packages/server/src/entities/ad.entity.ts
new file mode 100644
index 00000000..943a7505
--- /dev/null
+++ b/packages/server/src/entities/ad.entity.ts
@@ -0,0 +1,37 @@
+import { AdType } from '@think/domains';
+
+import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad')
+export class AdEntity {
+ @PrimaryGeneratedColumn('uuid')
+ id: string;
+
+ @Column({
+ type: 'enum',
+ enum: AdType,
+ default: AdType.shareDocAside,
+ comment: '权限类型',
+ })
+ public type: AdType;
+
+ @Column({ type: 'mediumtext', comment: '广告封面图' })
+ cover: string;
+
+ @Column({ type: 'mediumtext', comment: '跳转链接', default: null, charset: 'utf8mb4' })
+ url: string;
+
+ @CreateDateColumn({
+ type: 'timestamp',
+ name: 'createdAt',
+ comment: '创建时间',
+ })
+ createdAt: Date;
+
+ @UpdateDateColumn({
+ type: 'timestamp',
+ name: 'updatedAt',
+ comment: '更新时间',
+ })
+ updatedAt: Date;
+}
diff --git a/packages/server/src/modules/ad.module.ts b/packages/server/src/modules/ad.module.ts
new file mode 100644
index 00000000..b387e73e
--- /dev/null
+++ b/packages/server/src/modules/ad.module.ts
@@ -0,0 +1,16 @@
+import { forwardRef, Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+
+import { AdController } from '@controllers/ad.controller';
+import { AdEntity } from '@entities/ad.entity';
+import { AdService } from '@services/ad.service';
+
+import { UserModule } from './user.module';
+
+@Module({
+ imports: [TypeOrmModule.forFeature([AdEntity]), forwardRef(() => UserModule)],
+ providers: [AdService],
+ exports: [AdService],
+ controllers: [AdController],
+})
+export class AdModule {}
diff --git a/packages/server/src/services/ad.service.ts b/packages/server/src/services/ad.service.ts
new file mode 100644
index 00000000..2c69f551
--- /dev/null
+++ b/packages/server/src/services/ad.service.ts
@@ -0,0 +1,47 @@
+import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+
+import { AdDto } from '@dtos/ad.dto';
+import { AdEntity } from '@entities/ad.entity';
+import { UserEntity } from '@entities/user.entity';
+import { Repository } from 'typeorm';
+
+import { UserService } from './user.service';
+
+@Injectable()
+export class AdService {
+ constructor(
+ @Inject(forwardRef(() => UserService))
+ private readonly userService: UserService,
+
+ @InjectRepository(AdEntity)
+ private readonly adRepo: Repository
+ ) {}
+
+ private async check(user: UserEntity) {
+ const currentUser = await this.userService.findById(user.id);
+
+ if (!currentUser.isSystemAdmin) {
+ throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
+ }
+ }
+
+ async addAd(user: UserEntity, dto: AdDto) {
+ await this.check(user);
+
+ const data = await this.adRepo.create(dto);
+ const res = await this.adRepo.save(data);
+ return res;
+ }
+
+ async getAds() {
+ const data = await this.adRepo.find();
+ return data;
+ }
+
+ async deleteAd(user: UserEntity, id: string) {
+ await this.check(user);
+
+ return await this.adRepo.delete(id);
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 03117fde..7829cacf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -424,18 +424,22 @@ importers:
js-yaml:
specifier: ^4.1.0
version: 4.1.0
+ devDependencies:
+ typescript:
+ specifier: ^4.8.4
+ version: 4.8.4
packages/constants:
devDependencies:
typescript:
- specifier: ^4.5.5
- version: 4.5.5
+ specifier: ^4.8.4
+ version: 4.8.4
packages/domains:
devDependencies:
typescript:
- specifier: ^4.5.5
- version: 4.5.5
+ specifier: ^4.8.4
+ version: 4.8.4
packages/server:
dependencies: