feat: 增加广告配置

This commit is contained in:
fantasticit
2025-02-16 12:50:32 +08:00
parent 8b05c2d576
commit a216038f47
36 changed files with 660 additions and 87 deletions
-13
View File
@@ -1,16 +1,3 @@
# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可
dynamic:
# 输入 logo 的图片地址
logo: ''
appName: '云策文档'
appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。'
appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think'
# 版权信息,可以是 HTML
copyrightInformation: '<p>Develop by fantasticit</p>'
# 外部跳转按钮配置
externalButtonText: 'Github'
externalButtonURL: 'https://github.com/fantasticit/think'
# 开发环境配置
client:
port: 5001
-13
View File
@@ -1,16 +1,3 @@
# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可
dynamic:
# 输入 logo 的图片地址
logo: ''
appName: '云策文档'
appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。'
appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think'
# 版权信息,可以是 HTML
copyrightInformation: '<p>Develop by fantasticit</p>'
# 外部跳转按钮配置
externalButtonText: 'Github'
externalButtonURL: 'https://github.com/fantasticit/think'
# 开发环境配置
client:
port: 5001
+12
View File
@@ -0,0 +1,12 @@
# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可
dynamic:
# 输入 logo 的图片地址
logo: ''
appName: '云策文档'
appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。'
appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think'
# 版权信息,可以是 HTML
copyrightInformation: '<p>Develop by fantasticit</p>'
# 外部跳转按钮配置
externalButtonText: 'Github'
externalButtonURL: 'https://github.com/fantasticit/think'
@@ -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 (
<div>
<Image width={180} height={60} src={url}></Image>
</div>
);
},
},
{
title: '广告链接',
dataIndex: 'url',
},
];
export const Ad = () => {
const { data, loading, error, createAd, deleteAd } = useAd();
const [visible, toggleVisible] = useState(false);
const $form = useRef<FormApi>();
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 (
<DataRender
loading={loading}
error={error}
normalContent={() => (
<div style={{ marginTop: 16 }}>
<Button
htmlType="submit"
type="primary"
theme="solid"
onClick={() => {
toggleVisible(true);
}}
style={{ margin: '16px 0' }}
>
添加
</Button>
<Modal
title="添加广告"
visible={visible}
onCancel={() => {
toggleVisible(false);
}}
style={{ maxWidth: '96vw' }}
onOk={handleOk}
>
<Form
initValues={{ type: AdType.shareDocCover, cover: '', url: '' }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
>
<Form.Select field="type" label={{ text: '类型' }}>
<Option value={AdType.shareDocCover}>{AdTypeTextMap[AdType.shareDocCover]}</Option>
<Option value={AdType.shareDocAside}>{AdTypeTextMap[AdType.shareDocAside]}</Option>
</Form.Select>
<Form.Slot label="图片">
<Space align="end">
<Avatar src={currentCover} shape="square"></Avatar>
<Upload onOK={setCover} />
</Space>
</Form.Slot>
<Form.Input label="链接" field="url" style={{ width: '100%' }} placeholder="请输入跳转链接"></Form.Input>
</Form>
</Modal>
<Table
columns={[
...columns,
{
title: '操作',
dataIndex: 'id',
render: (id) => {
return (
<Button
type="danger"
onClick={() => {
deleteAd(id);
}}
>
删除
</Button>
);
},
},
]}
dataSource={data}
pagination={false}
/>
</div>
)}
/>
);
};
@@ -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<IProps> = ({ tab, onNavigate }) => {
@@ -25,6 +27,10 @@ export const SystemConfig: React.FC<IProps> = ({ tab, onNavigate }) => {
<TabPane tab={TitleMap['mail']} itemKey="mail">
<Mail />
</TabPane>
<TabPane tab={TitleMap['ad']} itemKey="ad">
<Ad />
</TabPane>
</Tabs>
);
};
@@ -35,6 +35,8 @@ export const Mail = () => {
closeIcon={null}
/>
{/*
// @ts-ignore */}
<Form initValues={data} onChange={onFormChange} onSubmit={onFinish}>
<Form.Input
field="emailServiceHost"
@@ -30,6 +30,8 @@ export const System = () => {
error={error}
normalContent={() => (
<div style={{ marginTop: 16 }}>
{/*
// @ts-ignore */}
<Form labelPosition="left" initValues={data} onChange={onFormChange} onSubmit={onFinish}>
<Form.Switch
field="isSystemLocked"
@@ -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;
@@ -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<IProps> = ({ documentId, hideLogo = false }) => {
export const DocumentPublicReader: React.FC<IProps> = ({
documentId,
hideLogo = false,
}) => {
const $form = useRef<FormApi>();
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(<Author document={data} />, target);
@@ -62,27 +81,42 @@ export const DocumentPublicReader: React.FC<IProps> = ({ 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 (
<div>
<Seo title={'输入密码后查看'} />
<Seo title={"输入密码后查看"} />
<Form
style={{ width: 320, maxWidth: 'calc(100vw - 160px)', margin: '10vh auto' }}
initValues={{ password: '' }}
style={{
width: 320,
maxWidth: "calc(100vw - 160px)",
margin: "10vh auto",
}}
initValues={{ password: "" }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
onSubmit={handleOk}
layout="horizontal"
>
<Form.Input autofocus label="密码" field="password" placeholder="请输入密码" />
<Form.Input
autofocus
label="密码"
field="password"
placeholder="请输入密码"
/>
<Button type="primary" theme="solid" htmlType="submit">
提交
</Button>
@@ -94,16 +128,16 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
return (
<div
style={{
margin: '10%',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
margin: "10%",
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center",
}}
>
<SecureDocumentIllustration />
<Text style={{ marginTop: 12 }} type="danger">
{(error && (error as Error).message) || '未知错误'}
{(error && (error as Error).message) || "未知错误"}
</Text>
</div>
);
@@ -144,9 +178,14 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
}
footer={
<Space>
{!isMobile && data && <DocumentFullscreen data={data}/>}
<Tooltip content={currentWikiId ? '独立模式' : '嵌入模式'}>
<Button theme="borderless" type="tertiary" icon={<IconRoute />} onClick={toPublicWikiOrDocumentURL} />
{!isMobile && data && <DocumentFullscreen data={data} />}
<Tooltip content={currentWikiId ? "独立模式" : "嵌入模式"}>
<Button
theme="borderless"
type="tertiary"
icon={<IconRoute />}
onClick={toPublicWikiOrDocumentURL}
/>
</Tooltip>
<DocumentStyle />
@@ -158,11 +197,20 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
<DataRender
loading={loading}
error={error}
loadingContent={<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />}
loadingContent={
<Skeleton
active
placeholder={<Skeleton.Title style={{ width: 80 }} />}
loading={true}
/>
}
normalContent={() => (
<Breadcrumb>
<Breadcrumb.Item>
<Link href="/share/wiki/[wikiId]" as={`/share/wiki/${data.wikiId}`}>
<Link
href="/share/wiki/[wikiId]"
as={`/share/wiki/${data.wikiId}`}
>
<a>{data?.wiki?.name}</a>
</Link>
</Breadcrumb.Item>
@@ -172,9 +220,38 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
/>
</Nav>
</Header>
<div className={styles.adWrap}>
{(ads || [])
.filter((ad) => ad.type === AdType.shareDocCover)
.map((ad) => {
return (
<div>
<a href={ad.url} target="_blank">
<img width={"100%"} height={"auto"} src={ad.cover} />
</a>
</div>
);
})}
</div>
<div className={styles.contentWrap} id="js-tocs-container">
{content}
</div>
<div className={styles.adAsideWrap}>
<Carousel autoPlay>
{(ads || [])
.filter((ad) => ad.type === AdType.shareDocAside)
.map((ad) => {
return (
<div>
<a href={ad.url} target="_blank">
<img width={"100%"} height={"auto"} src={ad.cover} />
</a>
</div>
);
})}
</Carousel>
</div>
</Layout>
);
};
+42
View File
@@ -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<IAd>({
method: AdApiDefinition.getAll.method,
url: AdApiDefinition.getAll.client(),
})
);
const createAd = useCallback(
async (data: Partial<IAd>) => {
const ret = await HttpClient.request<IAd>({
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<IAd>({
method: AdApiDefinition.deleteById.method,
url: AdApiDefinition.deleteById.client(id),
});
refetch();
return ret;
},
[refetch]
);
return { data, error, loading: isLoading, refresh: refetch, createAd, deleteAd };
};
@@ -169,6 +169,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<>
{(!online || status === 'disconnected') && (
<Banner
// @ts-ignore
type="warning"
description={
editable
@@ -180,6 +181,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
{/* FIXME:需要菜单栏但是无法编辑,则认为进入了编辑模式但是没有编辑权限,也许有更好的判断 */}
{!editable && menubar && (
// @ts-ignore
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)}
+2 -2
View File
@@ -20,9 +20,9 @@ function getConfig() {
exports.getConfig = getConfig;
function getRuntimeConfig() {
var env = process.env.NODE_ENV || 'development';
var filePath = path.join(__dirname, '../../../../../../config', "".concat(FILE_ENV_NAME[env], ".yaml"));
var filePath = path.join(__dirname, '../../../../../../', "dynamic.yaml");
if (!fs.existsSync(filePath)) {
filePath = path.join(__dirname, '../../../config', "".concat(FILE_ENV_NAME[env], ".yaml"));
filePath = path.join(__dirname, '../../../config', "dynamic.yaml");
}
if (!fs.existsSync(filePath)) {
throw new Error("Can not find config file: ".concat(filePath));
+4 -1
View File
@@ -21,5 +21,8 @@
],
"dependencies": {
"js-yaml": "^4.1.0"
},
"devDependencies": {
"typescript": "^4.8.4"
}
}
}
+2 -2
View File
@@ -23,10 +23,10 @@ export function getConfig() {
export function getRuntimeConfig() {
const env = process.env.NODE_ENV || 'development';
let filePath = path.join(__dirname, '../../../../../../config', `${FILE_ENV_NAME[env]}.yaml`);
let filePath = path.join(__dirname, '../../../../../../', `dynamic.yaml`);
if (!fs.existsSync(filePath)) {
filePath = path.join(__dirname, '../../../config', `${FILE_ENV_NAME[env]}.yaml`);
filePath = path.join(__dirname, '../../../config', `dynamic.yaml`);
}
if (!fs.existsSync(filePath)) {
+2 -2
View File
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
"typescript": "^4.5.5"
"typescript": "^4.8.4"
}
}
}
+23
View File
@@ -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;
};
};
+26
View File
@@ -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); }
}
};
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+6 -1
View File
@@ -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);
+5 -1
View File
@@ -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
View File
@@ -0,0 +1,10 @@
export declare enum AdType {
shareDocCover = "shareDocCover",
shareDocAside = "shareDocAside"
}
export interface IAd {
id: string;
type: AdType;
cover: string;
url: string;
}
+8
View File
@@ -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 = {}));
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+6 -1
View File
@@ -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);
+2 -2
View File
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
"typescript": "^4.5.5"
"typescript": "^4.8.4"
}
}
}
+25
View File
@@ -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}`,
},
};
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+11
View File
@@ -0,0 +1,11 @@
export enum AdType {
shareDocCover = 'shareDocCover',
shareDocAside = 'shareDocAside',
}
export interface IAd {
id: string;
type: AdType;
cover: string;
url: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+4
View File
@@ -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({
@@ -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);
}
}
+17
View File
@@ -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;
}
+37
View File
@@ -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;
}
+16
View File
@@ -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 {}
@@ -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<AdEntity>
) {}
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);
}
}
+8 -4
View File
@@ -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: