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} + > +
($form.current = formApi)} + labelPosition="left" + > + + + + + + + + + + + + + +
+
+ { + 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 (
- + ($form.current = formApi)} labelPosition="left" onSubmit={handleOk} layout="horizontal" > - + @@ -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 && } - - - + {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: