mirror of
https://github.com/fantasticit/think.git
synced 2026-10-30 22:56:06 +08:00
feat: 增加广告配置
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -21,5 +21,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"typescript": "^4.5.5"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+23
@@ -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;
|
||||
};
|
||||
};
|
||||
@@ -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); }
|
||||
}
|
||||
};
|
||||
Vendored
+1
@@ -1,3 +1,4 @@
|
||||
export * from './ad';
|
||||
export * from './user';
|
||||
export * from './wiki';
|
||||
export * from './document';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
export declare enum AdType {
|
||||
shareDocCover = "shareDocCover",
|
||||
shareDocAside = "shareDocAside"
|
||||
}
|
||||
export interface IAd {
|
||||
id: string;
|
||||
type: AdType;
|
||||
cover: string;
|
||||
url: string;
|
||||
}
|
||||
@@ -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
@@ -1,3 +1,4 @@
|
||||
export * from './ad';
|
||||
export * from './user';
|
||||
export * from './wiki';
|
||||
export * from './document';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
"src"
|
||||
],
|
||||
"devDependencies": {
|
||||
"typescript": "^4.5.5"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
export * from './ad';
|
||||
export * from './user';
|
||||
export * from './wiki';
|
||||
export * from './document';
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export enum AdType {
|
||||
shareDocCover = 'shareDocCover',
|
||||
shareDocAside = 'shareDocAside',
|
||||
}
|
||||
|
||||
export interface IAd {
|
||||
id: string;
|
||||
type: AdType;
|
||||
cover: string;
|
||||
url: string;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './ad';
|
||||
export * from './user';
|
||||
export * from './wiki';
|
||||
export * from './document';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Generated
+8
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user