mirror of https://github.com/fantasticit/think.git
root: upload source code
commit
306d7765ec
|
@ -0,0 +1,19 @@
|
|||
node_modules
|
||||
.DS_Store
|
||||
.idea
|
||||
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
test-results
|
||||
.pnpm-store
|
||||
.npmrc
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
.env
|
||||
*.local
|
||||
*.cache
|
||||
*error.log
|
||||
*debug.log
|
||||
|
||||
packages/config/yaml/prod.yaml
|
|
@ -0,0 +1,181 @@
|
|||
# think
|
||||
|
||||
## 简介
|
||||
|
||||
Think 是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。同时支持多人协作文档。使用的技术如下:
|
||||
|
||||
- `MySQL`:数据存储
|
||||
- `next.js`:前端页面框架
|
||||
- `nest.js`:服务端框架
|
||||
- `AliyunOSS`:对象存储
|
||||
- `tiptap`:编辑器及文档协作
|
||||
|
||||
可访问[云策文档帮助中心](https://think.wipi.tech/share/wiki/4e3d0cfb-b169-4308-8037-e7d3df996af3),查看更多功能文档。
|
||||
|
||||
## 链接
|
||||
|
||||
[云策文档](https://think.wipi.tech/)已经部署上线,可前往注册使用。
|
||||
|
||||
## 项目运行
|
||||
|
||||
本项目依赖 pnpm 使用 monorepo 形式进行代码组织,分包如下:
|
||||
|
||||
- `@think/config`: 管理项目整体配置
|
||||
- `@think/share`:数据类型定义、枚举、配置等
|
||||
- `@think/server`:服务端
|
||||
- `@think/client`:客户端
|
||||
|
||||
### pnpm
|
||||
|
||||
项目依赖 pnpm,请安装后运行(`npm i -g pnpm`)。
|
||||
|
||||
### 数据库
|
||||
|
||||
首先安装 `MySQL`,推荐使用 docker 进行安装。
|
||||
|
||||
```bash
|
||||
docker image pull mysql:5.7
|
||||
docker run -d --restart=always --name think -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root mysql:5.7
|
||||
```
|
||||
|
||||
然后在 `MySQL` 中创建数据库。
|
||||
|
||||
```bash
|
||||
docker container exec -it think bash;
|
||||
mysql -u root -p;
|
||||
CREATE DATABASE `think` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
### 本地运行
|
||||
|
||||
首先,clone 项目。
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/fantasticit/think.git your-project-name
|
||||
```
|
||||
|
||||
然后,安装项目依赖。
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
- 启动项目
|
||||
|
||||
```bash
|
||||
pnpm run dev
|
||||
```
|
||||
|
||||
前台页面地址:`http://localhost:3000`。
|
||||
服务接口地址:`http://localhost:5001`。
|
||||
协作接口地址:`http://localhost:5003`。
|
||||
|
||||
如需修改配置,可在 `packages/config/yaml` 下进行配置。
|
||||
|
||||
### 配置文件
|
||||
|
||||
默认加载 `dev.yaml` 中的配置(生产环境使用 `prod.yaml` )。
|
||||
|
||||
```yaml
|
||||
# 开发环境配置
|
||||
server:
|
||||
prefix: "/api"
|
||||
port: 5001
|
||||
collaborationPort: 5003
|
||||
|
||||
client:
|
||||
assetPrefix: "/"
|
||||
apiUrl: "http://localhost:5001/api"
|
||||
collaborationUrl: "ws://localhost:5003"
|
||||
|
||||
# 数据库配置
|
||||
db:
|
||||
mysql:
|
||||
host: "127.0.0.1"
|
||||
username: "root"
|
||||
password: "root"
|
||||
database: "think"
|
||||
port: 3306
|
||||
charset: "utf8mb4"
|
||||
timezone: "+08:00"
|
||||
synchronize: true
|
||||
|
||||
# oss 文件存储服务
|
||||
oss:
|
||||
aliyun:
|
||||
accessKeyId: ""
|
||||
accessKeySecret: ""
|
||||
bucket: ""
|
||||
https: true
|
||||
region: ""
|
||||
|
||||
# jwt 配置
|
||||
jwt:
|
||||
secretkey: "zA_Think+KNOWLEDGE+WIKI+DOCUMENTS@2022"
|
||||
expiresIn: "6h"
|
||||
```
|
||||
|
||||
### 项目部署
|
||||
|
||||
生产环境部署的脚本如下:
|
||||
|
||||
```bash
|
||||
|
||||
node -v
|
||||
npm -v
|
||||
|
||||
npm config set registry http://registry.npmjs.org
|
||||
|
||||
npm i -g pm2 @nestjs/cli pnpm
|
||||
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm run pm2
|
||||
|
||||
pm2 startup
|
||||
pm2 save
|
||||
```
|
||||
|
||||
### nginx 配置
|
||||
|
||||
采用反向代理进行 `nginx` 配置,**同时设置 `proxy_set_header X-Real-IP $remote_addr;` 以便服务端获取到真实 ip 地址**。
|
||||
|
||||
```bash
|
||||
upstream wipi_client {
|
||||
server 127.0.0.1:3000;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
# http -> https 重定向
|
||||
server {
|
||||
listen 80;
|
||||
server_name 域名;
|
||||
rewrite ^(.*)$ https://$host$1 permanent;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name 域名;
|
||||
ssl_certificate 证书存放路径;
|
||||
ssl_certificate_key 证书存放路径;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Nginx-Proxy true;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_pass http://wipi_client; #反向代理
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 资料
|
||||
|
||||
- next.js 源码:https://github.com/vercel/next.js
|
||||
- next.js 文档:https://nextjs.org/
|
||||
- nest.js 源码:https://github.com/nestjs/nest
|
||||
- nest.js 文档:https://nestjs.com/
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "think",
|
||||
"private": true,
|
||||
"author": "fantasticit",
|
||||
"scripts": {
|
||||
"clean": "rimraf node_modules && rimraf ./**/node_modules",
|
||||
"dev": "concurrently \"pnpm:dev:*\"",
|
||||
"dev:server": "pnpm run --dir packages/server dev",
|
||||
"dev:client": "pnpm run --dir packages/client dev",
|
||||
"build": "pnpm build:share && pnpm build:server && pnpm build:client",
|
||||
"build:config": "pnpm run --dir packages/config build",
|
||||
"build:share": "pnpm run --dir packages/share build",
|
||||
"build:server": "pnpm run --dir packages/server build",
|
||||
"build:client": "pnpm run --dir packages/client build",
|
||||
"start": "concurrently \"pnpm:start:*\"",
|
||||
"start:server": "pnpm run --dir packages/server start",
|
||||
"start:client": "pnpm run --dir packages/client start",
|
||||
"pm2": "concurrently \"pnpm:pm2:*\"",
|
||||
"pm2:server": "pnpm run --dir packages/server pm2",
|
||||
"pm2:client": "pnpm run --dir packages/client pm2"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^10.0.0",
|
||||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.5.5"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
|
@ -0,0 +1,34 @@
|
|||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
|
@ -0,0 +1,23 @@
|
|||
const semi = require("@douyinfe/semi-next").default({});
|
||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||
const { getConfig } = require("@think/config");
|
||||
const config = getConfig().client;
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = semi({
|
||||
reactStrictMode: true,
|
||||
assetPrefix: config.assetPrefix,
|
||||
env: {
|
||||
SERVER_API_URL: config.apiUrl,
|
||||
COLLABORATION_API_URL: config.collaborationUrl,
|
||||
},
|
||||
webpack: (config, { dev, isServer }) => {
|
||||
config.resolve.plugins.push(new TsconfigPathsPlugin());
|
||||
return config;
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = nextConfig;
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"name": "@think/client",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"prebuild": "rimraf .next",
|
||||
"build": "next build",
|
||||
"start": "cross-env NODE_ENV=production next start -p 5002",
|
||||
"lint": "next lint",
|
||||
"pm2": "pm2 start npm --name @think/client -- start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@douyinfe/semi-icons": "^2.3.1",
|
||||
"@douyinfe/semi-next": "^2.3.1",
|
||||
"@douyinfe/semi-ui": "^2.3.1",
|
||||
"@hocuspocus/provider": "^1.0.0-alpha.29",
|
||||
"@think/config": "workspace:^1.0.0",
|
||||
"@think/share": "workspace:^1.0.0",
|
||||
"@tiptap/core": "^2.0.0-beta.171",
|
||||
"@tiptap/extension-blockquote": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-bold": "^2.0.0-beta.25",
|
||||
"@tiptap/extension-bullet-list": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-code": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-code-block": "^2.0.0-beta.37",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.0-beta.68",
|
||||
"@tiptap/extension-collaboration": "^2.0.0-beta.33",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.0.0-beta.34",
|
||||
"@tiptap/extension-color": "^2.0.0-beta.9",
|
||||
"@tiptap/extension-document": "^2.0.0-beta.15",
|
||||
"@tiptap/extension-dropcursor": "^2.0.0-beta.25",
|
||||
"@tiptap/extension-gapcursor": "^2.0.0-beta.34",
|
||||
"@tiptap/extension-hard-break": "^2.0.0-beta.30",
|
||||
"@tiptap/extension-heading": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-highlight": "^2.0.0-beta.33",
|
||||
"@tiptap/extension-horizontal-rule": "^2.0.0-beta.31",
|
||||
"@tiptap/extension-image": "^2.0.0-beta.25",
|
||||
"@tiptap/extension-italic": "^2.0.0-beta.25",
|
||||
"@tiptap/extension-link": "^2.0.0-beta.36",
|
||||
"@tiptap/extension-list-item": "^2.0.0-beta.20",
|
||||
"@tiptap/extension-ordered-list": "^2.0.0-beta.27",
|
||||
"@tiptap/extension-paragraph": "^2.0.0-beta.23",
|
||||
"@tiptap/extension-placeholder": "^2.0.0-beta.47",
|
||||
"@tiptap/extension-strike": "^2.0.0-beta.27",
|
||||
"@tiptap/extension-table": "^2.0.0-beta.48",
|
||||
"@tiptap/extension-table-cell": "^2.0.0-beta.20",
|
||||
"@tiptap/extension-table-header": "^2.0.0-beta.22",
|
||||
"@tiptap/extension-table-row": "^2.0.0-beta.19",
|
||||
"@tiptap/extension-task-item": "^2.0.0-beta.31",
|
||||
"@tiptap/extension-task-list": "^2.0.0-beta.26",
|
||||
"@tiptap/extension-text": "^2.0.0-beta.15",
|
||||
"@tiptap/extension-text-align": "^2.0.0-beta.29",
|
||||
"@tiptap/extension-text-style": "^2.0.0-beta.23",
|
||||
"@tiptap/extension-underline": "^2.0.0-beta.23",
|
||||
"@tiptap/react": "^2.0.0-beta.107",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"copy-to-clipboard": "^3.3.1",
|
||||
"deep-equal": "^2.0.5",
|
||||
"dompurify": "^2.3.5",
|
||||
"interactjs": "^1.10.11",
|
||||
"katex": "^0.15.2",
|
||||
"lowlight": "^2.5.0",
|
||||
"marked": "^4.0.12",
|
||||
"next": "12.0.10",
|
||||
"prosemirror-markdown": "^1.7.0",
|
||||
"prosemirror-view": "^1.23.6",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"swr": "^1.2.0",
|
||||
"tippy.js": "^6.3.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.13",
|
||||
"@types/react": "17.0.38",
|
||||
"eslint": "8.8.0",
|
||||
"eslint-config-next": "12.0.10",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "4.5.5"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,20 @@
|
|||
import { Space, Typography } from "@douyinfe/semi-ui";
|
||||
import { IconLikeHeart } from "@douyinfe/semi-icons";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const Author = () => {
|
||||
return (
|
||||
<div style={{ padding: "16px 0", textAlign: "center" }}>
|
||||
<Text>
|
||||
<Space>
|
||||
Develop by
|
||||
<Text link={{ href: "https://github.com/fantasticit/think" }}>
|
||||
fantasticit
|
||||
</Text>
|
||||
with <IconLikeHeart style={{ color: "red" }} />
|
||||
</Space>
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
import React from "react";
|
||||
import { Empty, Spin, Typography } from "@douyinfe/semi-ui";
|
||||
|
||||
type RenderProps = React.ReactNode | (() => React.ReactNode);
|
||||
|
||||
interface IProps {
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
loadingContent?: RenderProps;
|
||||
errorContent?: RenderProps;
|
||||
normalContent: RenderProps;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const defaultLoading = () => {
|
||||
return <Spin />;
|
||||
};
|
||||
|
||||
const defaultRenderError = (error) => {
|
||||
return <Text>{(error && error.message) || "未知错误"}</Text>;
|
||||
};
|
||||
|
||||
const runRender = (fn, ...args) =>
|
||||
typeof fn === "function" ? fn.apply(null, args) : fn;
|
||||
|
||||
export const DataRender: React.FC<IProps> = ({
|
||||
loading,
|
||||
error,
|
||||
loadingContent = defaultLoading,
|
||||
errorContent = defaultRenderError,
|
||||
normalContent,
|
||||
}) => {
|
||||
if (loading) {
|
||||
return runRender(loadingContent);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return runRender(errorContent, error);
|
||||
}
|
||||
|
||||
return runRender(normalContent);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
import { Button } from "@douyinfe/semi-ui";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import { useQuery } from "hooks/useQuery";
|
||||
import { DocumentCreator as DocumenCreatorForm } from "components/document/create";
|
||||
|
||||
interface IProps {
|
||||
onCreateDocument?: () => void;
|
||||
}
|
||||
|
||||
export const DocumentCreator: React.FC<IProps> = ({
|
||||
onCreateDocument,
|
||||
children,
|
||||
}) => {
|
||||
const { wikiId, docId } = useQuery<{ wikiId?: string; docId?: string }>();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{children || (
|
||||
<Button type="primary" theme="solid" onClick={toggleVisible}>
|
||||
新建文档
|
||||
</Button>
|
||||
)}
|
||||
{wikiId && (
|
||||
<DocumenCreatorForm
|
||||
wikiId={wikiId}
|
||||
parentDocumentId={docId}
|
||||
visible={visible}
|
||||
toggleVisible={toggleVisible}
|
||||
onCreate={onCreateDocument}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,112 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { Dropdown, Button, Typography, Space } from "@douyinfe/semi-ui";
|
||||
import { IconMore, IconStar, IconPlus } from "@douyinfe/semi-icons";
|
||||
import { DocumentLinkCopyer } from "components/document/link";
|
||||
import { DocumentDeletor } from "components/document/delete";
|
||||
import { DocumentCreator } from "components/document/create";
|
||||
import { DocumentStar } from "components/document/star";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
documentId: string;
|
||||
onStar?: () => void;
|
||||
onCreate?: () => void;
|
||||
onDelete?: () => void;
|
||||
onVisibleChange?: () => void;
|
||||
showCreateDocument?: boolean;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentActions: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
documentId,
|
||||
onStar,
|
||||
onCreate,
|
||||
onDelete,
|
||||
onVisibleChange,
|
||||
showCreateDocument,
|
||||
children,
|
||||
}) => {
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const prevent = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
onVisibleChange={onVisibleChange}
|
||||
render={
|
||||
<Dropdown.Menu>
|
||||
{showCreateDocument && (
|
||||
<Dropdown.Item onClick={prevent}>
|
||||
<Text onClick={toggleVisible}>
|
||||
<Space>
|
||||
<IconPlus />
|
||||
新建子文档
|
||||
</Space>
|
||||
</Text>
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
<Dropdown.Item onClick={prevent}>
|
||||
<DocumentStar
|
||||
documentId={documentId}
|
||||
render={({ star, toggleStar, text }) => (
|
||||
<Text
|
||||
onClick={() => {
|
||||
toggleStar().then(onStar);
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<IconStar
|
||||
style={{
|
||||
color: star
|
||||
? "rgba(var(--semi-amber-4), 1)"
|
||||
: "rgba(var(--semi-grey-3), 1)",
|
||||
}}
|
||||
/>
|
||||
{text}
|
||||
</Space>
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={prevent}>
|
||||
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={prevent}>
|
||||
<DocumentDeletor
|
||||
wikiId={wikiId}
|
||||
documentId={documentId}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
}
|
||||
>
|
||||
{children || (
|
||||
<Button
|
||||
onClick={prevent}
|
||||
icon={<IconMore />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
)}
|
||||
</Dropdown>
|
||||
{showCreateDocument && (
|
||||
<DocumentCreator
|
||||
wikiId={wikiId}
|
||||
parentDocumentId={documentId}
|
||||
visible={visible}
|
||||
toggleVisible={toggleVisible}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
.cardWrap {
|
||||
width: 100%;
|
||||
|
||||
> a {
|
||||
margin: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-height: 260px;
|
||||
padding: 12px 16px 16px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
cursor: pointer;
|
||||
|
||||
> header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
color: var(--semi-color-primary);
|
||||
margin-bottom: 12px;
|
||||
|
||||
.rightWrap {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--box-shadow);
|
||||
|
||||
> header .rightWrap {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
> footer {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
import { useCallback } from "react";
|
||||
import Router from "next/router";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconEdit, IconUser } from "@douyinfe/semi-icons";
|
||||
import { IDocument } from "@think/share";
|
||||
import { LocaleTime } from "components/locale-time";
|
||||
import { IconDocument } from "components/icons/IconDocument";
|
||||
import { DocumentShare } from "components/document/share";
|
||||
import { DocumentStar } from "components/document/star";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentCard: React.FC<{ document: IDocument }> = ({
|
||||
document,
|
||||
}) => {
|
||||
const gotoEdit = useCallback(() => {
|
||||
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
|
||||
}, [document]);
|
||||
|
||||
return (
|
||||
<div className={styles.cardWrap}>
|
||||
<Link
|
||||
href={{
|
||||
pathname: `/wiki/[wikiId]/document/[documentId]`,
|
||||
query: { wikiId: document.wikiId, documentId: document.id },
|
||||
}}
|
||||
>
|
||||
<a>
|
||||
<header
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<IconDocument />
|
||||
<div className={styles.rightWrap}>
|
||||
<Space>
|
||||
<DocumentShare documentId={document.id} />
|
||||
<Tooltip key="edit" content="编辑" position="bottom">
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconEdit />}
|
||||
onClick={gotoEdit}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DocumentStar documentId={document.id} />
|
||||
</Space>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>{document.title}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="tertiary" size="small">
|
||||
<Space>
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
src={document.createUser && document.createUser.avatar}
|
||||
>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
创建者:
|
||||
{document.createUser && document.createUser.name}
|
||||
</Space>
|
||||
</Text>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
更新时间:
|
||||
<LocaleTime date={document.updatedAt} />
|
||||
</Text>
|
||||
</footer>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentCardPlaceholder = () => {
|
||||
return (
|
||||
<div className={styles.cardWrap}>
|
||||
<header>
|
||||
<IconDocument />
|
||||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Skeleton.Title style={{ width: 160 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Text type="tertiary" size="small">
|
||||
<Space>
|
||||
<Avatar size="extra-extra-small">
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
创建者:
|
||||
<Skeleton.Paragraph rows={1} style={{ width: 100 }} />
|
||||
</Space>
|
||||
</Text>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
<div style={{ display: "flex" }}>
|
||||
更新时间:
|
||||
<Skeleton.Paragraph rows={1} style={{ width: 100 }} />
|
||||
</div>
|
||||
</Text>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,244 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
Spin,
|
||||
Input,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Table,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Checkbox,
|
||||
Toast,
|
||||
Popconfirm,
|
||||
AvatarGroup,
|
||||
Avatar,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconUserAdd, IconDelete } from "@douyinfe/semi-icons";
|
||||
import { useUser } from "data/user";
|
||||
import { EventEmitter } from "helpers/event-emitter";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import { useCollaborationDocument } from "data/document";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { DocumentLinkCopyer } from "components/document/link";
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Column } = Table;
|
||||
|
||||
const CollaborationEventEmitter = new EventEmitter();
|
||||
const KEY = "JOIN_USER";
|
||||
|
||||
export const joinUser = (users) => {
|
||||
CollaborationEventEmitter.emit(KEY, users);
|
||||
};
|
||||
|
||||
const renderChecked =
|
||||
(onChange, authKey: "readable" | "editable") => (checked, docAuth) => {
|
||||
const handle = (evt) => {
|
||||
const data = {
|
||||
...docAuth.auth,
|
||||
userName: docAuth.user.name,
|
||||
};
|
||||
data[authKey] = evt.target.checked;
|
||||
onChange(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
style={{ display: "inline-block" }}
|
||||
checked={checked}
|
||||
onChange={handle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DocumentCollaboration: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
documentId,
|
||||
}) => {
|
||||
const { user: currentUser } = useUser();
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const { users, loading, error, addUser, updateUser, deleteUser } =
|
||||
useCollaborationDocument(documentId);
|
||||
const [inviteUser, setInviteUser] = useState("");
|
||||
|
||||
const [collaborationUsers, setCollaborationUsers] = useState([]);
|
||||
|
||||
const handleOk = () => {
|
||||
addUser(inviteUser).then(() => {
|
||||
Toast.success("添加成功");
|
||||
setInviteUser("");
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (docAuth) => {
|
||||
const data = {
|
||||
...docAuth.auth,
|
||||
userName: docAuth.user.name,
|
||||
};
|
||||
deleteUser(data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
CollaborationEventEmitter.on(KEY, ({ states: users }) => {
|
||||
const newCollaborationUsers = users
|
||||
.filter(Boolean)
|
||||
.map((state) => ({ ...state.user, clientId: state.clientId }))
|
||||
.filter(Boolean);
|
||||
|
||||
if (
|
||||
collaborationUsers.length === newCollaborationUsers.length &&
|
||||
newCollaborationUsers.every((newUser) => {
|
||||
return collaborationUsers.find(
|
||||
(existUser) => existUser.id === newUser.id
|
||||
);
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
newCollaborationUsers.forEach((newUser) => {
|
||||
if (currentUser && newUser.name !== currentUser.name) {
|
||||
Toast.info(`${newUser.name}加入文档`);
|
||||
}
|
||||
});
|
||||
|
||||
setCollaborationUsers(newCollaborationUsers);
|
||||
});
|
||||
|
||||
return () => {
|
||||
CollaborationEventEmitter.destroy();
|
||||
};
|
||||
}, [collaborationUsers, currentUser]);
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<Tooltip content="邀请他人协作" position="bottom">
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconUserAdd />}
|
||||
></Button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AvatarGroup maxCount={5} size="extra-small">
|
||||
{collaborationUsers.map((user) => {
|
||||
return (
|
||||
<Tooltip
|
||||
key={user.id}
|
||||
content={`${user.name}-${user.clientId}`}
|
||||
position="bottom"
|
||||
>
|
||||
<Avatar src={user.avatar} size="extra-small">
|
||||
{user.name && user.name.charAt(0)}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<Tooltip content="邀请他人协作" position="bottom">
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
icon={<IconUserAdd />}
|
||||
onClick={toggleVisible}
|
||||
></Button>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
title={"文档协作"}
|
||||
okText={"邀请对方"}
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
maskClosable={false}
|
||||
style={{ maxWidth: "96vw" }}
|
||||
footer={null}
|
||||
>
|
||||
<Tabs type="line">
|
||||
<TabPane tab="添加成员" itemKey="add">
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Input
|
||||
placeholder="输入对方用户名"
|
||||
value={inviteUser}
|
||||
onChange={setInviteUser}
|
||||
></Input>
|
||||
<Paragraph style={{ marginTop: 16 }}>
|
||||
邀请成功后,请将该链接发送给对方。
|
||||
<span style={{ verticalAlign: "middle" }}>
|
||||
<DocumentLinkCopyer wikiId={wikiId} documentId={documentId} />
|
||||
</span>
|
||||
</Paragraph>
|
||||
<Button
|
||||
theme="solid"
|
||||
block
|
||||
style={{ margin: "24px 0" }}
|
||||
disabled={!inviteUser}
|
||||
onClick={handleOk}
|
||||
>
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane tab="协作成员" itemKey="list">
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingContent={<Spin />}
|
||||
normalContent={() => (
|
||||
<Table
|
||||
style={{ margin: "24px 0" }}
|
||||
dataSource={users}
|
||||
size="small"
|
||||
pagination
|
||||
>
|
||||
<Column title="用户名" dataIndex="user.name" key="name" />
|
||||
<Column
|
||||
title="是否可读"
|
||||
dataIndex="auth.readable"
|
||||
key="readable"
|
||||
render={renderChecked(updateUser, "readable")}
|
||||
align="center"
|
||||
/>
|
||||
<Column
|
||||
title="是否可编辑"
|
||||
dataIndex="auth.editable"
|
||||
key="editable"
|
||||
render={renderChecked(updateUser, "editable")}
|
||||
align="center"
|
||||
/>
|
||||
<Column
|
||||
title="操作"
|
||||
dataIndex="operate"
|
||||
key="operate"
|
||||
render={(_, document) => (
|
||||
<Popconfirm
|
||||
showArrow
|
||||
title="确认删除该成员?"
|
||||
onConfirm={() => handleDelete(document)}
|
||||
>
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconDelete />}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
/>
|
||||
</Table>
|
||||
)}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,36 @@
|
|||
.wrap {
|
||||
display: flex;
|
||||
padding: 9px 0px 9px 0;
|
||||
|
||||
+ .wrap {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.rightWrap {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
overflow: auto;
|
||||
|
||||
> main {
|
||||
margin: 10px 0;
|
||||
color: var(--semi-color-text-0);
|
||||
}
|
||||
|
||||
> footer {
|
||||
// height: 0;
|
||||
// transition: all ease-in-out 0.3s;
|
||||
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// &:hover {
|
||||
// .rightWrap {
|
||||
// > footer {
|
||||
// height: 40px;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import React from "react";
|
||||
import type { IComment, IUser } from "@think/share";
|
||||
import {
|
||||
Avatar,
|
||||
Typography,
|
||||
Space,
|
||||
Popconfirm,
|
||||
Skeleton,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconUser } from "@douyinfe/semi-icons";
|
||||
import { LocaleTime } from "components/locale-time";
|
||||
import { useUser } from "data/user";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface IProps {
|
||||
comment: IComment;
|
||||
replyComment: (comment: IComment) => void;
|
||||
editComment: (comment: IComment) => void;
|
||||
deleteComment: (comment: IComment) => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const CommentItem: React.FC<IProps> = ({
|
||||
comment,
|
||||
replyComment,
|
||||
editComment,
|
||||
deleteComment,
|
||||
}) => {
|
||||
if (!comment) return null;
|
||||
const { user } = useUser();
|
||||
const { createUser = {} } = comment;
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Avatar size="small" src={(createUser as IUser).avatar}>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className={styles.rightWrap}>
|
||||
<header>
|
||||
<Space>
|
||||
<Text strong>{(createUser as IUser).name}</Text>
|
||||
<Text type="tertiary">
|
||||
<LocaleTime date={comment.createdAt} timeago />
|
||||
</Text>
|
||||
</Space>
|
||||
</header>
|
||||
<main className="ProseMirror">
|
||||
<div dangerouslySetInnerHTML={{ __html: comment.html }}></div>
|
||||
</main>
|
||||
<footer>
|
||||
<Space>
|
||||
<Text
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={() => replyComment(comment)}
|
||||
>
|
||||
回复
|
||||
</Text>
|
||||
{user && user.id === comment.createUserId && (
|
||||
<Text
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={() => editComment(comment)}
|
||||
>
|
||||
编辑
|
||||
</Text>
|
||||
)}
|
||||
<Popconfirm
|
||||
showArrow
|
||||
title="确认删除该评论?"
|
||||
onConfirm={() => deleteComment(comment)}
|
||||
>
|
||||
<Text type="secondary" size="small">
|
||||
删除
|
||||
</Text>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommentItemPlaceholder = () => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Skeleton.Avatar size="small" />
|
||||
</div>
|
||||
<div className={styles.rightWrap}>
|
||||
<header>
|
||||
<Skeleton.Title style={{ width: 120 }} />
|
||||
</header>
|
||||
<main>
|
||||
<div>
|
||||
<Skeleton.Paragraph style={{ width: "100%" }} rows={3} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,70 @@
|
|||
import React from "react";
|
||||
import type { IComment } from "@think/share";
|
||||
import { CommentItem } from "./Item";
|
||||
|
||||
interface IProps {
|
||||
comments: Array<IComment>;
|
||||
replyComment: (comment: IComment) => void;
|
||||
editComment: (comment: IComment) => void;
|
||||
deleteComment: (comment: IComment) => void;
|
||||
}
|
||||
|
||||
const PADDING_LEFT = 32;
|
||||
|
||||
const CommentInner = ({
|
||||
data,
|
||||
depth,
|
||||
replyComment,
|
||||
editComment,
|
||||
deleteComment,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={"comment" + depth}
|
||||
style={{ paddingLeft: depth > 0 ? PADDING_LEFT : 0 }}
|
||||
>
|
||||
{(data || []).map((item) => {
|
||||
const hasChildren = item.children && item.children.length;
|
||||
return (
|
||||
<>
|
||||
<CommentItem
|
||||
key={item.id}
|
||||
comment={item}
|
||||
replyComment={replyComment}
|
||||
editComment={editComment}
|
||||
deleteComment={deleteComment}
|
||||
></CommentItem>
|
||||
{hasChildren ? (
|
||||
<CommentInner
|
||||
key={"comment-inner" + depth}
|
||||
data={item.children}
|
||||
depth={depth + 1}
|
||||
replyComment={replyComment}
|
||||
editComment={editComment}
|
||||
deleteComment={deleteComment}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Comments: React.FC<IProps> = ({
|
||||
comments,
|
||||
replyComment,
|
||||
editComment,
|
||||
deleteComment,
|
||||
}) => {
|
||||
return (
|
||||
<CommentInner
|
||||
key={"root-menu"}
|
||||
data={comments}
|
||||
depth={0}
|
||||
replyComment={replyComment}
|
||||
editComment={editComment}
|
||||
deleteComment={deleteComment}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,43 @@
|
|||
.commentsWrap {
|
||||
padding: 16px 0;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
|
||||
.paginationWrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editorOuterWrap {
|
||||
padding-top: 24px;
|
||||
display: flex;
|
||||
|
||||
.rightWrap {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
overflow: auto;
|
||||
|
||||
.placeholderWrap {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--semi-border-radius-small);
|
||||
}
|
||||
|
||||
.editorWrap {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid var(--semi-color-border);
|
||||
border-radius: var(--semi-border-radius-small);
|
||||
|
||||
.innerWrap {
|
||||
max-height: 240px;
|
||||
padding: 16px 0;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btnWrap {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
import React, { useRef, useState } from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Banner,
|
||||
Pagination,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import { useClickOutside } from "hooks/use-click-outside";
|
||||
import { DEFAULT_EXTENSION, Document, CommentMenuBar } from "components/tiptap";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { useUser } from "data/user";
|
||||
import { useComments } from "data/comment";
|
||||
import { Comments } from "./comments";
|
||||
import { CommentItemPlaceholder } from "./comments/Item";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
|
||||
export const CommentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||
const { user } = useUser();
|
||||
const {
|
||||
data: commentsData,
|
||||
loading,
|
||||
error,
|
||||
setPage,
|
||||
addComment,
|
||||
updateComment,
|
||||
deleteComment,
|
||||
} = useComments(documentId);
|
||||
const [isEdit, toggleIsEdit] = useToggle(false);
|
||||
const $container = useRef<HTMLDivElement>();
|
||||
const [replyComment, setReplyComment] = useState(null);
|
||||
const [editComment, setEditComment] = useState(null);
|
||||
|
||||
useClickOutside($container, {
|
||||
out: () => isEdit && toggleIsEdit(false),
|
||||
});
|
||||
|
||||
const editor = useEditor({
|
||||
editable: true,
|
||||
extensions: [...DEFAULT_EXTENSION, Document],
|
||||
});
|
||||
|
||||
const openEditor = () => {
|
||||
toggleIsEdit(true);
|
||||
editor.chain().focus();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setReplyComment(null);
|
||||
setEditComment(null);
|
||||
toggleIsEdit(false);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
const html = editor.getHTML();
|
||||
|
||||
if (editComment) {
|
||||
return updateComment({
|
||||
id: editComment.id,
|
||||
html,
|
||||
}).then(() => {
|
||||
editor.commands.clearNodes();
|
||||
editor.commands.clearContent();
|
||||
handleClose();
|
||||
});
|
||||
}
|
||||
|
||||
return addComment({
|
||||
html,
|
||||
parentCommentId: (replyComment && replyComment.id) || null,
|
||||
replyUserId: (replyComment && replyComment.createUserId) || null,
|
||||
}).then(() => {
|
||||
editor.commands.clearNodes();
|
||||
editor.commands.clearContent();
|
||||
handleClose();
|
||||
});
|
||||
};
|
||||
|
||||
const handleReplyComment = (comment) => {
|
||||
setReplyComment(comment);
|
||||
setEditComment(null);
|
||||
openEditor();
|
||||
};
|
||||
|
||||
const handleEditComment = (comment) => {
|
||||
setReplyComment(null);
|
||||
setEditComment(comment);
|
||||
openEditor();
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={$container}>
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingContent={
|
||||
<>
|
||||
{Array.from({ length: 5 }, (_, i) => i).map((i) => (
|
||||
<CommentItemPlaceholder key={i} />
|
||||
))}
|
||||
</>
|
||||
}
|
||||
normalContent={() => (
|
||||
<>
|
||||
{commentsData.total > 0 && (
|
||||
<Space>
|
||||
<Text strong>评论</Text>
|
||||
</Space>
|
||||
)}
|
||||
{commentsData.total > 0 && (
|
||||
<div className={styles.commentsWrap}>
|
||||
<Comments
|
||||
comments={commentsData && commentsData.data}
|
||||
replyComment={handleReplyComment}
|
||||
editComment={handleEditComment}
|
||||
deleteComment={deleteComment}
|
||||
/>
|
||||
<div className={styles.paginationWrap}>
|
||||
<Pagination
|
||||
total={commentsData.total}
|
||||
showTotal
|
||||
onPageChange={setPage}
|
||||
></Pagination>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{replyComment && replyComment.createUser && (
|
||||
<Banner
|
||||
key={replyComment.id}
|
||||
fullMode={false}
|
||||
type="info"
|
||||
icon={null}
|
||||
title={<Text>回复评论: {replyComment.createUser.name}</Text>}
|
||||
description={
|
||||
<Paragraph ellipsis={{ rows: 2 }}>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: replyComment.html }}
|
||||
></div>
|
||||
</Paragraph>
|
||||
}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
{editComment && (
|
||||
<Banner
|
||||
key={editComment.id}
|
||||
fullMode={false}
|
||||
type="info"
|
||||
icon={null}
|
||||
title={<Text>编辑评论</Text>}
|
||||
description={
|
||||
<Paragraph ellipsis={{ rows: 2 }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: editComment.html }}></div>
|
||||
</Paragraph>
|
||||
}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.editorOuterWrap}>
|
||||
<div className={styles.leftWrap}>
|
||||
{user && (
|
||||
<Avatar size="small" src={user.avatar}>
|
||||
{user.name.charAt(0)}
|
||||
</Avatar>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.rightWrap}>
|
||||
{isEdit ? (
|
||||
<>
|
||||
<div className={styles.editorWrap}>
|
||||
<div style={{ width: "100%", overflow: "auto" }}>
|
||||
<CommentMenuBar editor={editor} />
|
||||
</div>
|
||||
<div className={styles.innerWrap}>
|
||||
<EditorContent autoFocus editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.btnWrap}>
|
||||
<Space>
|
||||
<Button theme="solid" type="primary" onClick={save}>
|
||||
保存
|
||||
</Button>
|
||||
<Button
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
onClick={handleClose}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.placeholderWrap} onClick={openEditor}>
|
||||
<Text type="tertiary"> 写下评论...</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
.isActive {
|
||||
border: 1px solid var(--semi-color-link);
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import Router from "next/router";
|
||||
import { Modal, Tabs, TabPane, Checkbox } from "@douyinfe/semi-ui";
|
||||
import { useCreateDocument } from "data/document";
|
||||
import { usePublicTemplates, useOwnTemplates } from "data/template";
|
||||
import { TemplateList } from "components/template/list";
|
||||
import { TemplateCardEmpty } from "components/template/card";
|
||||
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
parentDocumentId?: string;
|
||||
visible: boolean;
|
||||
toggleVisible: Dispatch<SetStateAction<boolean>>;
|
||||
onCreate?: () => void;
|
||||
}
|
||||
|
||||
export const DocumentCreator: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
parentDocumentId,
|
||||
visible,
|
||||
toggleVisible,
|
||||
onCreate,
|
||||
}) => {
|
||||
const { loading, create } = useCreateDocument();
|
||||
const [createChildDoc, setCreateChildDoc] = useState(false);
|
||||
const [templateId, setTemplateId] = useState("");
|
||||
|
||||
const handleOk = () => {
|
||||
const data = {
|
||||
wikiId,
|
||||
parentDocumentId: createChildDoc ? parentDocumentId : null,
|
||||
templateId,
|
||||
};
|
||||
create(data).then((res) => {
|
||||
toggleVisible(false);
|
||||
onCreate && onCreate();
|
||||
setTemplateId("");
|
||||
Router.push({
|
||||
pathname: `/wiki/${wikiId}/document/${res.id}/edit`,
|
||||
});
|
||||
});
|
||||
};
|
||||
const handleCancel = useCallback(() => {
|
||||
toggleVisible(false);
|
||||
}, [toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
setCreateChildDoc(!!parentDocumentId);
|
||||
}, [parentDocumentId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="模板库"
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
okButtonProps={{ loading }}
|
||||
style={{
|
||||
maxWidth: "96vw",
|
||||
width: "calc(100vh - 120px)",
|
||||
}}
|
||||
bodyStyle={{
|
||||
maxHeight: "calc(90vh - 120px)",
|
||||
overflow: "auto",
|
||||
}}
|
||||
key={wikiId}
|
||||
>
|
||||
<Tabs
|
||||
type="button"
|
||||
tabBarExtraContent={
|
||||
parentDocumentId && (
|
||||
<Checkbox
|
||||
checked={createChildDoc}
|
||||
onChange={(e) => setCreateChildDoc(e.target.checked)}
|
||||
>
|
||||
为该文档创建子文档
|
||||
</Checkbox>
|
||||
)
|
||||
}
|
||||
>
|
||||
<TabPane tab="公开模板" itemKey="all">
|
||||
<TemplateList
|
||||
hook={usePublicTemplates}
|
||||
onClick={(id) => setTemplateId(id)}
|
||||
getClassNames={(id) => id === templateId && styles.isActive}
|
||||
firstListItem={
|
||||
<TemplateCardEmpty
|
||||
getClassNames={() => !templateId && styles.isActive}
|
||||
onClick={() => setTemplateId("")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="我创建的" itemKey="own">
|
||||
<TemplateList
|
||||
hook={useOwnTemplates}
|
||||
onClick={(id) => setTemplateId(id)}
|
||||
getClassNames={(id) => id === templateId && styles.isActive}
|
||||
firstListItem={
|
||||
<TemplateCardEmpty
|
||||
getClassNames={() => !templateId && styles.isActive}
|
||||
onClick={() => setTemplateId("")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useCallback } from "react";
|
||||
import Router from "next/router";
|
||||
import { Typography, Space, Modal } from "@douyinfe/semi-ui";
|
||||
import { IconDelete } from "@douyinfe/semi-icons";
|
||||
import { useDeleteDocument } from "data/document";
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
documentId: string;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentDeletor: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
documentId,
|
||||
onDelete,
|
||||
}) => {
|
||||
const { deleteDocument: api, loading } = useDeleteDocument(documentId);
|
||||
|
||||
const deleteAction = useCallback(() => {
|
||||
Modal.error({
|
||||
title: "确定删除吗?",
|
||||
content: "文档删除后不可恢复!",
|
||||
onOk: () => {
|
||||
api().then(() => {
|
||||
onDelete
|
||||
? onDelete()
|
||||
: Router.push({
|
||||
pathname: `/wiki/${wikiId}`,
|
||||
});
|
||||
});
|
||||
},
|
||||
okButtonProps: { loading, type: "danger" },
|
||||
style: { maxWidth: "96vw" },
|
||||
});
|
||||
}, [wikiId, documentId, api, loading, onDelete]);
|
||||
|
||||
return (
|
||||
<Text type="danger" onClick={deleteAction}>
|
||||
<Space>
|
||||
<IconDelete />
|
||||
删除
|
||||
</Space>
|
||||
</Text>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
import React, { useMemo, useEffect } from "react";
|
||||
import cls from "classnames";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { Layout, Nav, BackTop, Toast } from "@douyinfe/semi-ui";
|
||||
import { IUser, IAuthority } from "@think/share";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import {
|
||||
DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getCollaborationCursorExtension,
|
||||
getProvider,
|
||||
destoryProvider,
|
||||
MenuBar,
|
||||
Toc,
|
||||
} from "components/tiptap";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { joinUser } from "components/document/collaboration";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
|
||||
interface IProps {
|
||||
user: IUser;
|
||||
documentId: string;
|
||||
authority: IAuthority;
|
||||
className: string;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({
|
||||
user,
|
||||
documentId,
|
||||
authority,
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
if (!user) return null;
|
||||
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: documentId,
|
||||
token: user.token,
|
||||
cacheType: "EDITOR",
|
||||
user,
|
||||
docType: "document",
|
||||
events: {
|
||||
onAwarenessUpdate({ states }) {
|
||||
joinUser({ states });
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [documentId, user.token]);
|
||||
const editor = useEditor({
|
||||
editable: authority && authority.editable,
|
||||
extensions: [
|
||||
...DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
getCollaborationCursorExtension(provider, user),
|
||||
],
|
||||
});
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
|
||||
useEffect(() => {
|
||||
provider.on("synced", () => {
|
||||
toggleLoading(false);
|
||||
});
|
||||
|
||||
provider.on("status", async ({ status }) => {
|
||||
console.log("status", status);
|
||||
});
|
||||
|
||||
return () => {
|
||||
destoryProvider(provider, "EDITOR");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={null}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div className={styles.editorWrap}>
|
||||
<header className={className}>
|
||||
<div>
|
||||
<MenuBar editor={editor} />
|
||||
</div>
|
||||
</header>
|
||||
<main id="js-template-editor-container" style={style}>
|
||||
<div className={cls(styles.contentWrap, className)}>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<BackTop
|
||||
target={() =>
|
||||
document.querySelector("#js-template-editor-container")
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
.wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> header {
|
||||
height: 60px;
|
||||
> div {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
height: calc(100% - 60px);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.editorWrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
> header {
|
||||
height: 50px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
|
||||
&.isStandardWidth {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 1;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
.contentWrap {
|
||||
padding: 24px 24px 96px;
|
||||
|
||||
&.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import Router from "next/router";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import {
|
||||
Layout,
|
||||
Nav,
|
||||
Skeleton,
|
||||
Typography,
|
||||
Space,
|
||||
Button,
|
||||
Tooltip,
|
||||
Spin,
|
||||
Popover,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconChevronLeft, IconArticle } from "@douyinfe/semi-icons";
|
||||
import { useUser } from "data/user";
|
||||
import { useDocumentDetail } from "data/document";
|
||||
import { Seo } from "components/seo";
|
||||
import { Theme } from "components/theme";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { DocumentShare } from "components/document/share";
|
||||
import { DocumentStar } from "components/document/star";
|
||||
import { DocumentCollaboration } from "components/document/collaboration";
|
||||
import { DocumentStyle } from "components/document/style";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import { Editor } from "./editor";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export const DocumentEditor: React.FC<IProps> = ({ documentId }) => {
|
||||
if (!documentId) return null;
|
||||
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === "standardWidth"
|
||||
? styles.isStandardWidth
|
||||
: styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
const { user } = useUser();
|
||||
const {
|
||||
data: documentAndAuth,
|
||||
loading: docAuthLoading,
|
||||
error: docAuthError,
|
||||
} = useDocumentDetail(documentId);
|
||||
const { document, authority } = documentAndAuth || {};
|
||||
|
||||
const goback = useCallback(() => {
|
||||
Router.push({
|
||||
pathname: `/wiki/${document.wikiId}/document/${documentId}`,
|
||||
});
|
||||
}, [document]);
|
||||
|
||||
const DocumentTitle = (
|
||||
<>
|
||||
<Tooltip content="返回" position="bottom">
|
||||
<Button
|
||||
onClick={goback}
|
||||
icon={<IconChevronLeft />}
|
||||
style={{ marginRight: 16 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
error={docAuthError}
|
||||
loadingContent={
|
||||
<Skeleton
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />
|
||||
}
|
||||
loading={true}
|
||||
/>
|
||||
}
|
||||
normalContent={() => (
|
||||
<Text ellipsis={{ showTooltip: true }} style={{ width: 120 }}>
|
||||
{document.title}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<header>
|
||||
<Nav
|
||||
className={styles.headerOuterWrap}
|
||||
mode="horizontal"
|
||||
header={DocumentTitle}
|
||||
footer={
|
||||
<Space>
|
||||
{document && authority.readable && (
|
||||
<DocumentCollaboration
|
||||
key="collaboration"
|
||||
wikiId={document.wikiId}
|
||||
documentId={documentId}
|
||||
/>
|
||||
)}
|
||||
<DocumentShare key="share" documentId={documentId} />
|
||||
<DocumentStar key="star" documentId={documentId} />
|
||||
<Popover
|
||||
key="style"
|
||||
zIndex={1061}
|
||||
position="bottomLeft"
|
||||
content={<DocumentStyle />}
|
||||
>
|
||||
<Button
|
||||
icon={<IconArticle />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
</Popover>
|
||||
<Theme />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</header>
|
||||
<main className={styles.contentWrap}>
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={null}
|
||||
normalContent={() => {
|
||||
return (
|
||||
// <div style={{ fontSize }}>
|
||||
<>
|
||||
<Seo title={document.title} />
|
||||
<Editor
|
||||
key={document.id}
|
||||
user={user}
|
||||
documentId={document.id}
|
||||
authority={authority}
|
||||
className={editorWrapClassNames}
|
||||
style={{ fontSize }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { Typography, Space } from "@douyinfe/semi-ui";
|
||||
import { IconLink } from "@douyinfe/semi-icons";
|
||||
import { copy } from "helpers/copy";
|
||||
import { buildUrl } from "helpers/url";
|
||||
|
||||
interface IProps {
|
||||
wikiId: string;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentLinkCopyer: React.FC<IProps> = ({
|
||||
wikiId,
|
||||
documentId,
|
||||
}) => {
|
||||
const handle = useCallback(() => {
|
||||
copy(buildUrl(`/wiki/${wikiId}/document/${documentId}`));
|
||||
}, [wikiId, documentId]);
|
||||
|
||||
return (
|
||||
<Text onClick={handle} style={{ cursor: "pointer" }}>
|
||||
<Space>
|
||||
<IconLink />
|
||||
复制链接
|
||||
</Space>
|
||||
</Text>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
import React from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { Layout } from "@douyinfe/semi-ui";
|
||||
import { IDocument } from "@think/share";
|
||||
import { DEFAULT_EXTENSION, DocumentWithTitle } from "components/tiptap";
|
||||
import { safeJSONParse } from "helpers/json";
|
||||
|
||||
interface IProps {
|
||||
document: IDocument;
|
||||
}
|
||||
|
||||
export const DocumentContent: React.FC<IProps> = ({ document }) => {
|
||||
const c = safeJSONParse(document.content);
|
||||
let json = c.default || c;
|
||||
|
||||
if (json && json.content) {
|
||||
json = {
|
||||
type: "doc",
|
||||
content: json.content.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: [...DEFAULT_EXTENSION, DocumentWithTitle],
|
||||
content: json,
|
||||
});
|
||||
|
||||
if (!json) return null;
|
||||
|
||||
return <EditorContent editor={editor} />;
|
||||
};
|
|
@ -0,0 +1,78 @@
|
|||
import React, { useMemo, useEffect } from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { Layout } from "@douyinfe/semi-ui";
|
||||
import { IUser } from "@think/share";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import {
|
||||
DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getCollaborationCursorExtension,
|
||||
getProvider,
|
||||
destoryProvider,
|
||||
} from "components/tiptap";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { joinUser } from "components/document/collaboration";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
interface IProps {
|
||||
user: IUser;
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({ user, documentId }) => {
|
||||
if (!user) return null;
|
||||
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: documentId,
|
||||
token: user.token,
|
||||
cacheType: "READER",
|
||||
user,
|
||||
docType: "document",
|
||||
events: {
|
||||
onAwarenessUpdate({ states }) {
|
||||
joinUser({ states });
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [documentId, user.token]);
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: [
|
||||
...DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
getCollaborationCursorExtension(provider, user),
|
||||
],
|
||||
});
|
||||
const [loading, toggleLoading] = useToggle(true);
|
||||
|
||||
useEffect(() => {
|
||||
provider.on("synced", () => {
|
||||
toggleLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
destoryProvider(provider, "READER");
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={null}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
<Content className={styles.editorWrap}>
|
||||
<EditorContent editor={editor} />
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
.wrap {
|
||||
margin-top: -16px;
|
||||
height: 100%;
|
||||
|
||||
.headerWrap {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.contentWrap {
|
||||
height: 100%;
|
||||
padding: 24px 0;
|
||||
overflow: hidden;
|
||||
|
||||
.editorWrap {
|
||||
padding-bottom: 24px;
|
||||
overflow: auto;
|
||||
|
||||
&.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.commentWrap {
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import Router from "next/router";
|
||||
import React, { useCallback, useMemo } from "react";
|
||||
import cls from "classnames";
|
||||
import {
|
||||
Layout,
|
||||
Nav,
|
||||
Space,
|
||||
Button,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
Popover,
|
||||
BackTop,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconEdit, IconArticle } from "@douyinfe/semi-icons";
|
||||
import { Seo } from "components/seo";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { DocumentShare } from "components/document/share";
|
||||
import { DocumentStar } from "components/document/star";
|
||||
import { DocumentCollaboration } from "components/document/collaboration";
|
||||
import { DocumentStyle } from "components/document/style";
|
||||
import { CommentEditor } from "components/document/comments";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import { useUser } from "data/user";
|
||||
import { useDocumentDetail } from "data/document";
|
||||
import { DocumentSkeleton } from "components/tiptap";
|
||||
import { Editor } from "./editor";
|
||||
import { CreateUser } from "./user";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header } = Layout;
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
}
|
||||
|
||||
export const DocumentReader: React.FC<IProps> = ({ documentId }) => {
|
||||
if (!documentId) return null;
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === "standardWidth"
|
||||
? styles.isStandardWidth
|
||||
: styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const {
|
||||
data: documentAndAuth,
|
||||
loading: docAuthLoading,
|
||||
error: docAuthError,
|
||||
} = useDocumentDetail(documentId);
|
||||
const { document, authority } = documentAndAuth || {};
|
||||
|
||||
const gotoEdit = useCallback(() => {
|
||||
Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`);
|
||||
}, [document]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Header className={styles.headerWrap}>
|
||||
<Nav
|
||||
style={{ overflow: "auto", paddingLeft: 0, paddingRight: 0 }}
|
||||
mode="horizontal"
|
||||
header={
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
error={docAuthError}
|
||||
loadingContent={
|
||||
<Skeleton
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />
|
||||
}
|
||||
loading={true}
|
||||
/>
|
||||
}
|
||||
normalContent={() => (
|
||||
<Text
|
||||
strong
|
||||
ellipsis={{ showTooltip: true }}
|
||||
style={{ width: 120 }}
|
||||
>
|
||||
{document.title}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<Space>
|
||||
{document && authority.readable && (
|
||||
<DocumentCollaboration
|
||||
key="collaboration"
|
||||
wikiId={document.wikiId}
|
||||
documentId={documentId}
|
||||
/>
|
||||
)}
|
||||
{authority && authority.editable && (
|
||||
<Tooltip key="edit" content="编辑" position="bottom">
|
||||
<Button icon={<IconEdit />} onClick={gotoEdit} />
|
||||
</Tooltip>
|
||||
)}
|
||||
{authority && authority.readable && (
|
||||
<>
|
||||
<DocumentShare key="share" documentId={documentId} />
|
||||
<DocumentStar key="star" documentId={documentId} />
|
||||
</>
|
||||
)}
|
||||
<Popover
|
||||
key="style"
|
||||
zIndex={1061}
|
||||
position="bottomLeft"
|
||||
content={<DocumentStyle />}
|
||||
>
|
||||
<Button
|
||||
icon={<IconArticle />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
</Popover>
|
||||
</Space>
|
||||
}
|
||||
></Nav>
|
||||
</Header>
|
||||
<Layout className={styles.contentWrap}>
|
||||
<div
|
||||
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
>
|
||||
<DataRender
|
||||
loading={docAuthLoading}
|
||||
error={docAuthError}
|
||||
loadingContent={<DocumentSkeleton />}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
<Seo title={document.title} />
|
||||
<Editor
|
||||
key={document.id}
|
||||
user={user}
|
||||
documentId={document.id}
|
||||
/>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<CreateUser document={document} />
|
||||
</div>
|
||||
<div className={styles.commentWrap}>
|
||||
<CommentEditor documentId={document.id} />
|
||||
</div>
|
||||
<BackTop
|
||||
target={() => window.document.querySelector(".Pane2")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
.wrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
|
||||
.headerWrap {
|
||||
position: sticky;
|
||||
}
|
||||
|
||||
.contentWrap {
|
||||
padding: 24px 24px 48px;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
|
||||
.editorWrap {
|
||||
padding-bottom: 24px;
|
||||
|
||||
&.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
import React, { useMemo, useEffect } from "react";
|
||||
import cls from "classnames";
|
||||
import {
|
||||
Layout,
|
||||
Nav,
|
||||
Space,
|
||||
Button,
|
||||
Typography,
|
||||
Skeleton,
|
||||
Input,
|
||||
Popover,
|
||||
Modal,
|
||||
BackTop,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconArticle } from "@douyinfe/semi-icons";
|
||||
import { Seo } from "components/seo";
|
||||
import { LogoImage, LogoText } from "components/logo";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { DocumentStyle } from "components/document/style";
|
||||
import { User } from "components/user";
|
||||
import { Theme } from "components/theme";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import { usePublicDocument } from "data/document";
|
||||
import { DocumentSkeleton } from "components/tiptap";
|
||||
import { DocumentContent } from "../content";
|
||||
import { CreateUser } from "../user";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { Text, Title } = Typography;
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
hideLogo?: boolean;
|
||||
}
|
||||
|
||||
export const DocumentPublicReader: React.FC<IProps> = ({
|
||||
documentId,
|
||||
hideLogo = true,
|
||||
}) => {
|
||||
if (!documentId) return null;
|
||||
|
||||
const { data, loading, error, query } = usePublicDocument(documentId);
|
||||
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === "standardWidth"
|
||||
? styles.isStandardWidth
|
||||
: styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return;
|
||||
if (error.statusCode !== 400) return;
|
||||
Modal.confirm({
|
||||
title: "请输入密码",
|
||||
content: (
|
||||
<>
|
||||
<Seo title={"输入密码后查看"} />
|
||||
<Input
|
||||
id="js-share-document-password"
|
||||
style={{ marginTop: 24 }}
|
||||
autofocus
|
||||
mode="password"
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</>
|
||||
),
|
||||
closable: false,
|
||||
hasCancel: false,
|
||||
maskClosable: false,
|
||||
onOk() {
|
||||
const $input = document.querySelector(
|
||||
"#js-share-document-password"
|
||||
) as HTMLInputElement;
|
||||
query($input.value);
|
||||
},
|
||||
});
|
||||
}, [error, query]);
|
||||
|
||||
return (
|
||||
<Layout className={styles.wrap}>
|
||||
<Header className={styles.headerWrap}>
|
||||
<Nav
|
||||
mode="horizontal"
|
||||
header={
|
||||
!hideLogo ? (
|
||||
<>
|
||||
<LogoImage />
|
||||
<LogoText />
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
footer={
|
||||
<Space>
|
||||
<Popover
|
||||
key="style"
|
||||
zIndex={1061}
|
||||
position="bottomLeft"
|
||||
content={<DocumentStyle />}
|
||||
>
|
||||
<Button
|
||||
icon={<IconArticle />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
</Popover>
|
||||
<Theme />
|
||||
<User />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingContent={
|
||||
<Skeleton
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />
|
||||
}
|
||||
loading={true}
|
||||
/>
|
||||
}
|
||||
normalContent={() => (
|
||||
<Text
|
||||
strong
|
||||
ellipsis={{ showTooltip: true }}
|
||||
style={{ width: 120 }}
|
||||
>
|
||||
{data.title}
|
||||
</Text>
|
||||
)}
|
||||
/>
|
||||
</Nav>
|
||||
</Header>
|
||||
<Content className={styles.contentWrap}>
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingContent={
|
||||
<div
|
||||
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
>
|
||||
<DocumentSkeleton />
|
||||
</div>
|
||||
}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
<Seo title={data.title} />
|
||||
<div
|
||||
className={cls(styles.editorWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
id="js-share-document-editor-container"
|
||||
>
|
||||
<Title>{data.title}</Title>
|
||||
<div style={{ margin: "24px 0" }}>
|
||||
<CreateUser document={data} />
|
||||
</div>
|
||||
<DocumentContent document={data} />
|
||||
</div>
|
||||
<BackTop
|
||||
target={() =>
|
||||
document.querySelector(
|
||||
"#js-share-document-editor-container"
|
||||
).parentNode
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
import { Space, Typography, Avatar } from "@douyinfe/semi-ui";
|
||||
import { IconUser } from "@douyinfe/semi-icons";
|
||||
import { IDocument } from "@think/share";
|
||||
import { LocaleTime } from "components/locale-time";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const CreateUser: React.FC<{ document: IDocument }> = ({ document }) => {
|
||||
if (!document.createUser) return null;
|
||||
|
||||
return (
|
||||
<Text type="tertiary" size="small">
|
||||
<Space>
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
src={document.createUser && document.createUser.avatar}
|
||||
>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
<div>
|
||||
<p>
|
||||
创建者:
|
||||
{document.createUser && document.createUser.name}
|
||||
</p>
|
||||
<p>
|
||||
最近更新日期:
|
||||
<LocaleTime date={document.updatedAt} timeago />
|
||||
{" ⦁ "}阅读量:
|
||||
{document.views}
|
||||
</p>
|
||||
</div>
|
||||
</Space>
|
||||
</Text>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,106 @@
|
|||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { Button, Modal, Input, Typography, Toast } from "@douyinfe/semi-ui";
|
||||
import { IconLink } from "@douyinfe/semi-icons";
|
||||
import { isPublicDocument, getDocumentShareURL } from "@think/share";
|
||||
import { ShareIllustration } from "illustrations/share";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
import { useDocumentDetail } from "data/document";
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
render?: (arg: {
|
||||
isPublic: boolean;
|
||||
toggleVisible: (arg: boolean) => void;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentShare: React.FC<IProps> = ({ documentId, render }) => {
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
const { data, loading, error, toggleStatus } = useDocumentDetail(documentId);
|
||||
const [sharePassword, setSharePassword] = useState("");
|
||||
const isPublic = useMemo(
|
||||
() => data && isPublicDocument(data.document.status),
|
||||
[data]
|
||||
);
|
||||
const shareUrl = useMemo(
|
||||
() => data && getDocumentShareURL(data.document.id),
|
||||
[data]
|
||||
);
|
||||
|
||||
const handleOk = () => {
|
||||
toggleStatus({ sharePassword: isPublic ? "" : sharePassword });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || !data) return;
|
||||
setSharePassword(data.document && data.document.sharePassword);
|
||||
}, [loading, data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{render ? (
|
||||
render({ isPublic, toggleVisible })
|
||||
) : (
|
||||
<Button type="primary" theme="light" onClick={toggleVisible}>
|
||||
{isPublic ? "分享中" : "分享"}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title={isPublic ? "关闭分享" : "开启分享"}
|
||||
okText={isPublic ? "关闭分享" : "开启分享"}
|
||||
visible={visible}
|
||||
onOk={handleOk}
|
||||
onCancel={() => toggleVisible(false)}
|
||||
maskClosable={false}
|
||||
style={{ maxWidth: "96vw" }}
|
||||
>
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<ShareIllustration />
|
||||
</div>
|
||||
{isPublic ? (
|
||||
<Text
|
||||
ellipsis
|
||||
icon={<IconLink />}
|
||||
copyable={{
|
||||
onCopy: () => Toast.success({ content: "复制文本成功" }),
|
||||
}}
|
||||
style={{
|
||||
width: 320,
|
||||
}}
|
||||
>
|
||||
{shareUrl}
|
||||
</Text>
|
||||
) : (
|
||||
<Input
|
||||
autofocus
|
||||
mode="password"
|
||||
placeholder="设置访问密码"
|
||||
value={sharePassword}
|
||||
onChange={setSharePassword}
|
||||
></Input>
|
||||
)}
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Text type="tertiary">
|
||||
{isPublic
|
||||
? "分享开启后,该页面包含的所有内容均可访问,请谨慎开启"
|
||||
: " 分享关闭后,其他人将不能继续访问该页面"}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
import React from "react";
|
||||
import { Typography, Tooltip, Button } from "@douyinfe/semi-ui";
|
||||
import { IconStar } from "@douyinfe/semi-icons";
|
||||
import { useDocumentStar } from "data/document";
|
||||
|
||||
interface IProps {
|
||||
documentId: string;
|
||||
render?: (arg: {
|
||||
star: boolean;
|
||||
text: string;
|
||||
toggleStar: () => Promise<void>;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentStar: React.FC<IProps> = ({ documentId, render }) => {
|
||||
const { data, toggleStar } = useDocumentStar(documentId);
|
||||
const text = data ? "取消收藏" : "收藏文档";
|
||||
|
||||
return (
|
||||
<>
|
||||
{render ? (
|
||||
render({ star: data, toggleStar, text })
|
||||
) : (
|
||||
<Tooltip content={text} position="bottom">
|
||||
<Button
|
||||
icon={<IconStar />}
|
||||
theme="borderless"
|
||||
style={{
|
||||
color: data
|
||||
? "rgba(var(--semi-amber-4), 1)"
|
||||
: "rgba(var(--semi-grey-3), 1)",
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
toggleStar();
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
.wrap {
|
||||
padding: 16px;
|
||||
|
||||
.item {
|
||||
:global {
|
||||
.semi-slider {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.semi-slider-handle {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import React from "react";
|
||||
import { RadioGroup, Radio, Typography, Slider } from "@douyinfe/semi-ui";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const DocumentStyle = () => {
|
||||
const { width, fontSize, setWidth, setFontSize } = useDocumentStyle();
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.item}>
|
||||
<Text>正文大小</Text>
|
||||
<Text style={{ fontSize: "0.8em" }}> {fontSize}px</Text>
|
||||
<Slider
|
||||
min={12}
|
||||
max={24}
|
||||
step={1}
|
||||
tooltipVisible={false}
|
||||
value={fontSize}
|
||||
onChange={setFontSize}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.item}>
|
||||
<Text>页面尺寸</Text>
|
||||
<div>
|
||||
<RadioGroup
|
||||
type="button"
|
||||
value={width}
|
||||
onChange={(e) => setWidth(e.target.value)}
|
||||
style={{ marginTop: "0.5em" }}
|
||||
>
|
||||
<Radio value={"standardWidth"}>标宽模式</Radio>
|
||||
<Radio value={"fullWidth"}>超宽模式</Radio>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
import React from "react";
|
||||
import { Typography } from "@douyinfe/semi-ui";
|
||||
|
||||
interface IProps {
|
||||
illustration?: React.ReactNode;
|
||||
message: React.ReactNode;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const Empty: React.FC<IProps> = ({ illustration = null, message }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
margin: "16px 0",
|
||||
}}
|
||||
>
|
||||
{illustration && (
|
||||
<main style={{ textAlign: "center" }}>{illustration}</main>
|
||||
)}
|
||||
<footer style={{ textAlign: "center" }}>
|
||||
<Text type="tertiary">{message}</Text>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
import React from "react";
|
||||
import { MouseEventHandler } from "react";
|
||||
|
||||
type CellProperties = {
|
||||
active: boolean;
|
||||
hover: boolean;
|
||||
disabled: boolean;
|
||||
cellSize: number;
|
||||
onClick: MouseEventHandler<HTMLDivElement>;
|
||||
onMouseEnter: MouseEventHandler<HTMLDivElement>;
|
||||
styles: Record<string, React.CSSProperties>;
|
||||
id: string;
|
||||
};
|
||||
|
||||
const getBaseStyles = (cellSize) => ({
|
||||
cell: {
|
||||
width: cellSize,
|
||||
height: cellSize,
|
||||
background: "#fff",
|
||||
cursor: "pointer",
|
||||
borderRadius: 3,
|
||||
border: "1px solid #bababa",
|
||||
},
|
||||
active: {
|
||||
border: "1px solid #4d6cdd",
|
||||
background: "#4d6cdd",
|
||||
},
|
||||
hover: {
|
||||
border: "1px solid #fff",
|
||||
background: "#4d6cdd",
|
||||
},
|
||||
disabled: {
|
||||
filter: "brightness(0.7)",
|
||||
},
|
||||
});
|
||||
|
||||
const getMergedStyle = (baseStyles, styles, styleClass) => ({
|
||||
...baseStyles[styleClass],
|
||||
...(styles && styles[styleClass] ? styles[styleClass] : {}),
|
||||
});
|
||||
|
||||
export const GridCell = ({
|
||||
active,
|
||||
hover,
|
||||
disabled,
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
cellSize,
|
||||
styles,
|
||||
id,
|
||||
}: CellProperties) => {
|
||||
const baseStyles = getBaseStyles(cellSize);
|
||||
const cellStyles = {
|
||||
cell: getMergedStyle(baseStyles, styles, "cell"),
|
||||
active: getMergedStyle(baseStyles, styles, "active"),
|
||||
hover: getMergedStyle(baseStyles, styles, "hover"),
|
||||
disabled: getMergedStyle(baseStyles, styles, "disabled"),
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
style={{
|
||||
...cellStyles.cell,
|
||||
...(active && cellStyles.active),
|
||||
...(hover && cellStyles.hover),
|
||||
...(!active && disabled && cellStyles.disabled),
|
||||
}}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,143 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { useState, useCallback } from "react";
|
||||
import { Typography } from "@douyinfe/semi-ui";
|
||||
import { debounce } from "helpers/debounce";
|
||||
import { GridCell } from "./grid-cell";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export type RegionSelectionProps = {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
onSelect: (arg: { rows: number; cols: number }) => void;
|
||||
cellSize?: number;
|
||||
disabled?: boolean;
|
||||
styles?: {
|
||||
active?: React.CSSProperties;
|
||||
hover?: React.CSSProperties;
|
||||
cell?: React.CSSProperties;
|
||||
grid?: React.CSSProperties;
|
||||
disabled?: React.CSSProperties;
|
||||
};
|
||||
};
|
||||
|
||||
type CoordsType = {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
const getBaseStyles = (cols, cellSize) => ({
|
||||
grid: {
|
||||
position: "relative",
|
||||
display: "grid",
|
||||
color: "#444",
|
||||
margin: "8px 0",
|
||||
gridGap: "4px 6px",
|
||||
gridTemplateColumns: Array(cols).fill(`${cellSize}px`).join(" "),
|
||||
},
|
||||
});
|
||||
|
||||
export const GridSelect = ({
|
||||
onSelect,
|
||||
rows = 10,
|
||||
cols = 10,
|
||||
disabled = false,
|
||||
cellSize = 16,
|
||||
styles,
|
||||
}: RegionSelectionProps) => {
|
||||
const [activeCell, setActiveCell] = useState<CoordsType>({
|
||||
x: -1,
|
||||
y: -1,
|
||||
});
|
||||
const [hoverCell, setHoverCell] = useState<CoordsType>(null);
|
||||
|
||||
const onClick = useCallback(
|
||||
({ x, y, isCellDisabled }) => {
|
||||
// if (isCellDisabled) {
|
||||
// return null;
|
||||
// }
|
||||
// if (activeCell.x === x && activeCell.y === y) {
|
||||
// return null;
|
||||
// }
|
||||
// setActiveCell({ x, y });
|
||||
onSelect({
|
||||
rows: y + 1,
|
||||
cols: x + 1,
|
||||
});
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const onHover = useCallback(
|
||||
debounce(({ x, y, isCellDisabled }) => {
|
||||
if (isCellDisabled) {
|
||||
return setHoverCell(null);
|
||||
}
|
||||
setHoverCell({ x, y });
|
||||
}, 5),
|
||||
[disabled]
|
||||
);
|
||||
|
||||
const cells = useMemo(() => {
|
||||
const cells = [];
|
||||
for (let y = 0; y < rows; y++) {
|
||||
for (let x = 0; x < cols; x++) {
|
||||
const isActive = x <= activeCell.x && y <= activeCell.y;
|
||||
const isHover = hoverCell && x <= hoverCell.x && y <= hoverCell.y;
|
||||
const isCellDisabled = disabled;
|
||||
cells.push(
|
||||
<GridCell
|
||||
id={x + "-" + y}
|
||||
key={x + "-" + y}
|
||||
onClick={() => onClick({ x, y, isCellDisabled })}
|
||||
onMouseEnter={onHover.bind(null, { x, y, isCellDisabled })}
|
||||
active={isActive}
|
||||
hover={isHover}
|
||||
disabled={isCellDisabled}
|
||||
styles={styles}
|
||||
cellSize={cellSize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return cells;
|
||||
}, [
|
||||
rows,
|
||||
cols,
|
||||
disabled,
|
||||
activeCell.x,
|
||||
activeCell.y,
|
||||
cellSize,
|
||||
hoverCell,
|
||||
styles,
|
||||
onClick,
|
||||
onHover,
|
||||
]);
|
||||
|
||||
const baseStyles = useMemo(
|
||||
() => getBaseStyles(cols, cellSize),
|
||||
[cols, cellSize]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
...baseStyles.grid,
|
||||
...(styles && styles.grid ? styles.grid : {}),
|
||||
} as React.CSSProperties
|
||||
}
|
||||
onMouseLeave={() => setHoverCell(null)}
|
||||
>
|
||||
{cells}
|
||||
</div>
|
||||
<footer style={{ textAlign: "center" }}>
|
||||
<Text>
|
||||
{hoverCell ? `${hoverCell.y + 1} x ${hoverCell.x + 1}` : null}
|
||||
</Text>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./grid-select";
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconAddColumnAfter: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M469.333333 85.333333C516.266667 85.333333 554.666667 123.733333 554.666667 170.666667L554.666667 853.333333C554.666667 900.266667 516.266667 938.666667 469.333333 938.666667L85.333333 938.666667 85.333333 85.333333 469.333333 85.333333M170.666667 426.666667 170.666667 597.333333 469.333333 597.333333 469.333333 426.666667 170.666667 426.666667M170.666667 682.666667 170.666667 853.333333 469.333333 853.333333 469.333333 682.666667 170.666667 682.666667M170.666667 170.666667 170.666667 341.333333 469.333333 341.333333 469.333333 170.666667 170.666667 170.666667M640 469.333333 768 469.333333 768 341.333333 853.333333 341.333333 853.333333 469.333333 981.333333 469.333333 981.333333 554.666667 853.333333 554.666667 853.333333 682.666667 768 682.666667 768 554.666667 640 554.666667 640 469.333333Z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconAddColumnBefore: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M554.666667 85.333333C507.733333 85.333333 469.333333 123.733333 469.333333 170.666667L469.333333 853.333333C469.333333 900.266667 507.733333 938.666667 554.666667 938.666667L938.666667 938.666667 938.666667 85.333333 554.666667 85.333333M853.333333 426.666667 853.333333 597.333333 554.666667 597.333333 554.666667 426.666667 853.333333 426.666667M853.333333 682.666667 853.333333 853.333333 554.666667 853.333333 554.666667 682.666667 853.333333 682.666667M853.333333 170.666667 853.333333 341.333333 554.666667 341.333333 554.666667 170.666667 853.333333 170.666667M384 469.333333 256 469.333333 256 341.333333 170.666667 341.333333 170.666667 469.333333 42.666667 469.333333 42.666667 554.666667 170.666667 554.666667 170.666667 682.666667 256 682.666667 256 554.666667 384 554.666667 384 469.333333Z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconAddRowAfter: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M938.666667 426.666667a85.333333 85.333333 0 0 1-85.333334 85.333333H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333333V128h85.333334v85.333333h170.666666V128h85.333334v85.333333h170.666666V128h85.333334v85.333333h170.666666V128h85.333334v298.666667M170.666667 426.666667h170.666666V298.666667H170.666667v128m256 0h170.666666V298.666667h-170.666666v128m426.666666 0V298.666667h-170.666666v128h170.666666m-384 170.666666h85.333334v128h128v85.333334h-128v128h-85.333334v-128H341.333333v-85.333334h128v-128z"
|
||||
fill=""
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconAddRowBefore: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path
|
||||
d="M938.666667 597.333333a85.333333 85.333333 0 0 0-85.333334-85.333333H170.666667a85.333333 85.333333 0 0 0-85.333334 85.333333v298.666667h85.333334v-85.333333h170.666666v85.333333h85.333334v-85.333333h170.666666v85.333333h85.333334v-85.333333h170.666666v85.333333h85.333334v-298.666667M170.666667 597.333333h170.666666v128H170.666667v-128m256 0h170.666666v128h-170.666666v-128m426.666666 0v128h-170.666666v-128h170.666666m-384-170.666666h85.333334V298.666667h128V213.333333h-128V85.333333h-85.333334v128H341.333333v85.333334h128v128z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconAttachment: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<path
|
||||
d="M152.765 31.222c16.836-16.774 44.065-16.774 60.9 0 16.617 16.554 16.832 43.341.61 60.16l-.61.62-88.75 88.422c-10.261 10.223-26.858 10.223-37.119 0-10.139-10.101-10.27-26.446-.372-36.709l.372-.378 64.689-64.449c3.912-3.898 10.244-3.886 14.142.026 3.827 3.842 3.885 10.015.183 13.927l-.21.215-64.704 64.466a6.176 6.176 0 0 0 .016 8.734 6.296 6.296 0 0 0 8.7.178l.187-.178 88.81-88.483c8.927-8.959 8.9-23.457-.06-32.383-8.934-8.902-23.327-8.997-32.378-.284l-.29.284-99.113 98.747c-15.186 15.243-15.14 39.91.102 55.096 15.19 15.135 39.667 15.286 55.044.454l.463-.454 74.95-74.672c3.912-3.898 10.244-3.887 14.142.026 3.827 3.841 3.885 10.015.183 13.927l-.21.215-74.949 74.672c-23.149 23.064-60.59 23.064-83.74 0-22.836-22.752-23.132-59.567-.836-82.684l.837-.85 99.011-98.645Z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconCenter: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" role="presentation">
|
||||
<path
|
||||
d="M6 17h12a1 1 0 010 2H6a1 1 0 010-2zm4-8h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4a1 1 0 011-1zM6 5h12a1 1 0 010 2H6a1 1 0 110-2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconClear: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1170 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
d="M1138.249143 551.936a109.494857 109.494857 0 0 0 0-154.843429l-364.982857-364.982857a109.494857 109.494857 0 0 0-154.916572 0L34.377143 616.009143a109.494857 109.494857 0 0 0 0 154.843428l218.989714 218.989715c20.48 20.553143 48.347429 32.109714 77.385143 32.109714h812.178286a27.355429 27.355429 0 0 0 27.355428-27.428571v-91.209143a27.355429 27.355429 0 0 0-27.355428-27.355429H814.08l324.022857-324.096z m-690.468572-142.848l313.417143 313.344-153.6 153.6H345.965714L163.401143 693.394286l284.452571-284.379429z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconCodeBlock: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="currentColor" fill-rule="evenodd">
|
||||
<path
|
||||
d="M64 40c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L64 60H54a6 6 0 0 0-5.996 5.775L48 66v42.5a10 10 0 0 1-1.667 5.529l-.21.303L36.31 128l9.813 13.668a10 10 0 0 1 1.87 5.463l.007.369V190a6 6 0 0 0 5.775 5.996L54 196h10c5.523 0 10 4.477 10 10 0 5.43-4.327 9.848-9.72 9.996L64 216H54c-14.216 0-25.767-11.409-25.997-25.57L28 190v-39.281l-12.123-16.887a10 10 0 0 1-.179-11.407l.179-.257L28 105.28V66c0-14.216 11.409-25.767 25.57-25.997L54 40h10ZM192 40c-5.523 0-10 4.477-10 10 0 5.43 4.327 9.848 9.72 9.996L192 60h10a6 6 0 0 1 5.996 5.775L208 66v42.5a10 10 0 0 0 1.667 5.529l.21.303L219.69 128l-9.813 13.668a10 10 0 0 0-1.87 5.463l-.007.369V190a6 6 0 0 1-5.775 5.996L202 196h-10c-5.523 0-10 4.477-10 10 0 5.43 4.327 9.848 9.72 9.996l.28.004h10c14.216 0 25.767-11.409 25.997-25.57L228 190v-39.281l12.123-16.887a10 10 0 0 0 .179-11.407l-.179-.257L228 105.28V66c0-14.216-11.409-25.767-25.57-25.997L202 40h-10Z"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<rect x="72" y="78" width="112" height="20" rx="10"></rect>
|
||||
<path d="M82 118h36c5.523 0 10 4.477 10 10s-4.477 10-10 10H82c-5.523 0-10-4.477-10-10s4.477-10 10-10ZM82 158h92c5.523 0 10 4.477 10 10s-4.477 10-10 10H82c-5.523 0-10-4.477-10-10s4.477-10 10-10Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconDeleteColumn: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M170.666667 85.333333 469.333333 85.333333C516.266667 85.333333 554.666667 123.733333 554.666667 170.666667L554.666667 853.333333C554.666667 900.266667 516.266667 938.666667 469.333333 938.666667L170.666667 938.666667C123.733333 938.666667 85.333333 900.266667 85.333333 853.333333L85.333333 170.666667C85.333333 123.733333 123.733333 85.333333 170.666667 85.333333M170.666667 426.666667 170.666667 597.333333 469.333333 597.333333 469.333333 426.666667 170.666667 426.666667M170.666667 682.666667 170.666667 853.333333 469.333333 853.333333 469.333333 682.666667 170.666667 682.666667M170.666667 170.666667 170.666667 341.333333 469.333333 341.333333 469.333333 170.666667 170.666667 170.666667M750.506667 512 640 401.493333 700.16 341.333333 810.666667 451.84 921.173333 341.333333 981.333333 401.493333 870.826667 512 981.333333 622.506667 921.173333 682.666667 810.666667 572.16 700.16 682.666667 640 622.506667 750.506667 512Z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconDeleteRow: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M401.493333 554.666667 512 665.173333 622.506667 554.666667 682.666667 614.826667 572.16 725.333333 682.666667 835.84 622.506667 896 512 785.493333 401.493333 896 341.333333 835.84 451.84 725.333333 341.333333 614.826667 401.493333 554.666667M938.666667 384C938.666667 430.933333 900.266667 469.333333 853.333333 469.333333L170.666667 469.333333C123.733333 469.333333 85.333333 430.933333 85.333333 384L85.333333 256C85.333333 209.066667 123.733333 170.666667 170.666667 170.666667L853.333333 170.666667C900.266667 170.666667 938.666667 209.066667 938.666667 256L938.666667 384M170.666667 384 341.333333 384 341.333333 256 170.666667 256 170.666667 384M426.666667 384 597.333333 384 597.333333 256 426.666667 256 426.666667 384M682.666667 384 853.333333 384 853.333333 256 682.666667 256 682.666667 384Z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconDeleteTable: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M659.797 677.504l60.374-60.373 90.496 90.581 90.496-90.496 60.373 60.33L870.955 768l90.496 90.496-60.331 60.373-90.453-90.581-90.496 90.496-60.374-60.33L750.38 768l-90.496-90.496zM170.667 128H768a85.333 85.333 0 0 1 85.333 85.333v302.251a255.275 255.275 0 0 0-184.192 39.083H512v170.666h46.25a254.72 254.72 0 0 0-0.085 85.334H170.667a85.333 85.333 0 0 1-85.334-85.334v-512A85.333 85.333 0 0 1 170.667 128z m0 170.667v170.666h256V298.667h-256z m341.333 0v170.666h256V298.667H512z m-341.333 256v170.666h256V554.667h-256z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconDocument: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg height="16" width="16" viewBox="0 0 24 24" focusable="false">
|
||||
<g fill="currentColor" fillRule="evenodd" clipRule="evenodd">
|
||||
<path
|
||||
transform="translate(2 2)"
|
||||
d="M3.5 0C1.84315 0 0.5 1.34315 0.5 3V17C0.5 18.6569 1.84315 20 3.5 20H17.5C19.1569 20 20.5 18.6569 20.5 17V3C20.5 1.34315 19.1569 0 17.5 0H3.5ZM2.5 3C2.5 2.44772 2.94772 2 3.5 2H17.5C18.0523 2 18.5 2.44772 18.5 3V17C18.5 17.5523 18.0523 18 17.5 18H3.5C2.94772 18 2.5 17.5523 2.5 17V3ZM5.5 5C4.94772 5 4.5 5.44772 4.5 6C4.5 6.55228 4.94771 7 5.5 7H15.5C16.0523 7 16.5 6.55228 16.5 6C16.5 5.44772 16.0523 5 15.5 5H5.5ZM4.5 10C4.5 9.44771 4.94772 9 5.5 9H15.5C16.0523 9 16.5 9.44771 16.5 10C16.5 10.5523 16.0523 11 15.5 11H5.5C4.94771 11 4.5 10.5523 4.5 10ZM5.5 13C4.94772 13 4.5 13.4477 4.5 14C4.5 14.5523 4.94772 15 5.5 15H11.5C12.0523 15 12.5 14.5523 12.5 14C12.5 13.4477 12.0523 13 11.5 13H5.5Z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconDocumentFill: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M3 0h18a3 3 0 013 3v18a3 3 0 01-3 3H3a3 3 0 01-3-3V3a3 3 0 013-3zm1 18c0 .556.446 1 .995 1h8.01c.54 0 .995-.448.995-1 0-.556-.446-1-.995-1h-8.01c-.54 0-.995.448-.995 1zm0-4c0 .556.448 1 1 1h14c.555 0 1-.448 1-1 0-.556-.448-1-1-1H5c-.555 0-1 .448-1 1zm0-4c0 .556.448 1 1 1h14c.555 0 1-.448 1-1 0-.556-.448-1-1-1H5c-.555 0-1 .448-1 1zm0-4c0 .556.448 1 1 1h14c.555 0 1-.448 1-1 0-.556-.448-1-1-1H5c-.555 0-1 .448-1 1z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconEmoji: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M12 5a7 7 0 110 14 7 7 0 010-14zm0 12.5c3.033 0 5.5-2.467 5.5-5.5S15.033 6.5 12 6.5A5.506 5.506 0 006.5 12c0 3.033 2.467 5.5 5.5 5.5zm-1.5-6a1 1 0 110-2 1 1 0 010 2zm3 0a1 1 0 110-2 1 1 0 010 2zm.27 1.583a.626.626 0 01.932.834A3.63 3.63 0 0112 15.125a3.63 3.63 0 01-2.698-1.204.625.625 0 01.93-.835c.901 1.003 2.639 1.003 3.538-.003z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,25 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconFont: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
role="presentation"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14 12.5h-4l-.874 2.186A.5.5 0 0 1 8.66 15H7.273a.5.5 0 0 1-.456-.705l4.05-9A.5.5 0 0 1 11.323 5h1.354a.5.5 0 0 1 .456.295l4.05 9a.5.5 0 0 1-.456.705h-1.388a.5.5 0 0 1-.465-.314L14 12.5zm-.6-1.5L12 7.5 10.6 11h2.8z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconFull: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M18.062 11L16.5 9.914A1 1 0 1117.914 8.5l2.616 2.616c.28.167.47.5.47.884s-.19.717-.47.884L17.914 15.5a1 1 0 01-1.414-1.414L18.062 13h-3.68c-.487 0-.882-.448-.882-1s.395-1 .882-1h3.68zM3.47 12.884c-.28-.167-.47-.5-.47-.884s.19-.717.47-.884L6.086 8.5A1 1 0 017.5 9.914L5.938 11h3.68c.487 0 .882.448.882 1s-.395 1-.882 1h-3.68L7.5 14.086A1 1 0 016.086 15.5L3.47 12.884z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconHalf: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M8.062 11L6.5 9.914A1 1 0 017.914 8.5l2.616 2.616c.28.167.47.5.47.884s-.19.717-.47.884L7.914 15.5A1 1 0 116.5 14.086L8.062 13h-3.68c-.487 0-.882-.448-.882-1s.395-1 .882-1h3.68zm5.408 1.884c-.28-.167-.47-.5-.47-.884s.19-.717.47-.884L16.086 8.5A1 1 0 0117.5 9.914L15.938 11h3.68c.487 0 .882.448.882 1s-.395 1-.882 1h-3.68l1.562 1.086a1 1 0 01-1.414 1.414l-2.616-2.616z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconImage: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M197 27c17.673 0 32 14.327 32 32v138c0 17.673-14.327 32-32 32H59c-17.673 0-32-14.327-32-32V59c0-17.673 14.327-32 32-32h138Zm0 20H59c-6.525 0-11.834 5.209-11.996 11.695L47 59v138c0 6.525 5.209 11.834 11.695 11.996L59 209h138c6.525 0 11.834-5.209 11.996-11.695L209 197V59c0-6.525-5.209-11.834-11.695-11.996L197 47Z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<g fill="currentColor">
|
||||
<path
|
||||
d="M64.982 134.434c5.322-7.419 15.65-9.118 23.07-3.796a16.532 16.532 0 0 1 3.504 3.401l.292.395 23.387 32.6c3.22 4.488 2.191 10.736-2.296 13.955-4.408 3.162-10.513 2.226-13.78-2.06l-.175-.236-20.569-28.672-32.29 45.01c-3.161 4.407-9.244 5.477-13.712 2.464l-.242-.168c-4.407-3.162-5.478-9.245-2.465-13.713l.169-.242 35.107-48.938Z"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<path
|
||||
d="M143.577 105.01c5.33-7.412 15.66-9.101 23.074-3.771a16.532 16.532 0 0 1 3.48 3.38l.291.391 56.34 78.353c3.224 4.484 2.203 10.733-2.281 13.957-4.404 3.167-10.51 2.238-13.782-2.044l-.175-.237L157 120.602l-41.771 58.1c-3.167 4.403-9.25 5.467-13.715 2.45l-.242-.17c-4.404-3.166-5.468-9.25-2.45-13.714l.168-.242 44.587-62.015Z"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<path d="M99.825 104c7.732 0 14-6.268 14-14s-6.268-14-14-14-14 6.268-14 14 6.268 14 14 14Z"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconInfo: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M197 27c17.673 0 32 14.327 32 32v138c0 17.673-14.327 32-32 32H59c-17.673 0-32-14.327-32-32V59c0-17.673 14.327-32 32-32h138Zm0 20H59c-6.525 0-11.834 5.209-11.996 11.695L47 59v138c0 6.525 5.209 11.834 11.695 11.996L59 209h138c6.525 0 11.834-5.209 11.996-11.695L209 197V59c0-6.525-5.209-11.834-11.695-11.996L197 47Z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconLeft: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" role="presentation">
|
||||
<path
|
||||
d="M6 17h12a1 1 0 010 2H6a1 1 0 010-2zm0-8h4a1 1 0 011 1v4a1 1 0 01-1 1H6a1 1 0 01-1-1v-4a1 1 0 011-1zm0-4h12a1 1 0 010 2H6a1 1 0 110-2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconLink: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<path
|
||||
d="M155.639 173.9c3.839 3.84 3.904 10.023.195 13.942l-.195.2-30.783 30.783c-24.052 24.052-63.167 23.884-87.366-.315-23.957-23.957-24.362-62.533-1.03-86.64l.715-.726 30.783-30.783c3.905-3.905 10.237-3.905 14.142 0 3.839 3.84 3.904 10.023.195 13.942l-.195.2-30.783 30.783c-16.214 16.214-16.1 42.666.315 59.082 16.252 16.251 42.34 16.525 58.592.797l.49-.482 30.783-30.783c3.905-3.905 10.237-3.905 14.142 0Zm17.366-90.905c3.839 3.84 3.904 10.023.195 13.942l-.195.2-76.368 76.368c-3.905 3.905-10.237 3.905-14.142 0-3.839-3.84-3.904-10.023-.195-13.942l.195-.2 76.368-76.368c3.905-3.905 10.237-3.905 14.142 0ZM218.51 37.49c23.957 23.957 24.362 62.533 1.03 86.64l-.715.726-30.783 30.783c-3.905 3.905-10.237 3.905-14.142 0-3.839-3.84-3.904-10.023-.195-13.942l.195-.2 30.783-30.783c16.214-16.214 16.1-42.666-.315-59.082-16.252-16.251-42.34-16.525-58.592-.797l-.49.482L114.503 82.1c-3.905 3.905-10.237 3.905-14.142 0-3.839-3.84-3.904-10.023-.195-13.942l.195-.2 30.783-30.783c24.052-24.052 63.167-23.884 87.366.315Z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconMath: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<path
|
||||
d="M186 26.5c14.49 0 26.262 11.628 26.496 26.062l.004.438v23.801c0 5.8-4.701 10.5-10.5 10.5-5.704 0-10.346-4.548-10.496-10.215l-.004-.285V53a5.5 5.5 0 0 0-5.279-5.496L186 47.5H72.799a5.5 5.5 0 0 0-3.681 9.586l.197.17 61.362 50.24c11.325 9.271 12.989 25.967 3.717 37.292a26.5 26.5 0 0 1-3.212 3.293l-.505.423-61.362 50.24a5.5 5.5 0 0 0 3.224 9.75l.26.006H182a9.5 9.5 0 0 0 9.496-9.23l.004-.27v-20c0-5.799 4.701-10.5 10.5-10.5 5.704 0 10.346 4.548 10.496 10.216l.004.284v20c0 16.676-13.384 30.227-29.996 30.496l-.504.004H72.799a26.5 26.5 0 0 1-20.504-9.712c-9.18-11.211-7.64-27.687 3.38-37.012l.336-.28 61.363-50.24a5.5 5.5 0 0 0 .186-8.352l-.186-.16-61.363-50.24A26.5 26.5 0 0 1 46.298 53c0-14.49 11.63-26.262 26.063-26.496l.438-.004H186Z"
|
||||
fill="currentColor"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconMergeCell: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M213.333333 426.666667H128V170.666667h341.333333v85.333333H213.333333v170.666667m597.333334 341.333333h-256v85.333333h341.333333v-256h-85.333333v170.666667M213.333333 768v-170.666667H128v256h341.333333v-85.333333H213.333333M896 170.666667h-341.333333v85.333333h256v170.666667h85.333333V170.666667M341.333333 554.666667v85.333333l128-128-128-128v85.333333H128v85.333334h213.333333m341.333334-85.333334V384l-128 128 128 128v-85.333333h213.333333v-85.333334h-213.333333z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconMessage: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M6.485 17.669a2 2 0 002.829 0l-2.829-2.83a2 2 0 000 2.83zm4.897-12.191l-.725.725c-.782.782-2.21 1.813-3.206 2.311l-3.017 1.509c-.495.248-.584.774-.187 1.171l8.556 8.556c.398.396.922.313 1.171-.188l1.51-3.016c.494-.988 1.526-2.42 2.311-3.206l.725-.726a5.048 5.048 0 00.64-6.356 1.01 1.01 0 10-1.354-1.494c-.023.025-.046.049-.066.075a5.043 5.043 0 00-2.788-.84 5.036 5.036 0 00-3.57 1.478z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconMind: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="currentColor" fill-rule="nonzero">
|
||||
<path d="M214 35h-46c-14.36 0-26 11.64-26 26v10c0 14.36 11.64 26 26 26h46c14.36 0 26-11.64 26-26V61c0-14.36-11.64-26-26-26Zm-46 20h46a6 6 0 0 1 6 6v10a6 6 0 0 1-6 6h-46a6 6 0 0 1-6-6V61a6 6 0 0 1 6-6ZM214 159h-46c-14.36 0-26 11.64-26 26v10c0 14.36 11.64 26 26 26h46c14.36 0 26-11.64 26-26v-10c0-14.36-11.64-26-26-26Zm-46 20h46a6 6 0 0 1 6 6v10a6 6 0 0 1-6 6h-46a6 6 0 0 1-6-6v-10a6 6 0 0 1 6-6Z"></path>
|
||||
<path d="M73.55 147.305c7.858 20.517 27.486 34.415 49.774 34.69L124 182h26.207v16H124c-28.957 0-54.586-17.747-65.078-44.17l-.313-.803 14.942-5.722ZM158 58v16h-34c-22.645 0-42.63 14.068-50.511 34.857l-.235.632-15.03-5.484c9.897-27.128 35.606-45.632 64.888-46L124 58h34Z"></path>
|
||||
<path d="M88 97H42c-14.36 0-26 11.64-26 26v10c0 14.36 11.64 26 26 26h46c14.36 0 26-11.64 26-26v-10c0-14.36-11.64-26-26-26Zm-46 20h46a6 6 0 0 1 6 6v10a6 6 0 0 1-6 6H42a6 6 0 0 1-6-6v-10a6 6 0 0 1 6-6Z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconOverview: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M8.01 18c.546 0 .99-.444.99-1a1 1 0 00-.99-1H3.99A.993.993 0 003 17a1 1 0 00.99 1h4.02zM3 7c0 .552.445 1 .993 1h16.014A.994.994 0 0021 7c0-.552-.445-1-.993-1H3.993A.994.994 0 003 7zm10.998 6A.999.999 0 0015 12c0-.552-.456-1-1.002-1H4.002A.999.999 0 003 12c0 .552.456 1 1.002 1h9.996z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconRight: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" role="presentation">
|
||||
<path
|
||||
d="M6 17h12a1 1 0 010 2H6a1 1 0 010-2zm8-8h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4a1 1 0 011-1zM6 5h12a1 1 0 010 2H6a1 1 0 110-2z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconSetting: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M11.701 16.7a5.002 5.002 0 110-10.003 5.002 5.002 0 010 10.004m8.368-3.117a1.995 1.995 0 01-1.346-1.885c0-.876.563-1.613 1.345-1.885a.48.48 0 00.315-.574 8.947 8.947 0 00-.836-1.993.477.477 0 00-.598-.195 2.04 2.04 0 01-1.29.08 1.988 1.988 0 01-1.404-1.395 2.04 2.04 0 01.076-1.297.478.478 0 00-.196-.597 8.98 8.98 0 00-1.975-.826.479.479 0 00-.574.314 1.995 1.995 0 01-1.885 1.346 1.994 1.994 0 01-1.884-1.345.482.482 0 00-.575-.315c-.708.2-1.379.485-2.004.842a.47.47 0 00-.198.582A2.002 2.002 0 014.445 7.06a.478.478 0 00-.595.196 8.946 8.946 0 00-.833 1.994.48.48 0 00.308.572 1.995 1.995 0 011.323 1.877c0 .867-.552 1.599-1.324 1.877a.479.479 0 00-.308.57 8.99 8.99 0 00.723 1.79.477.477 0 00.624.194c.595-.273 1.343-.264 2.104.238.117.077.225.185.302.3.527.8.512 1.58.198 2.188a.473.473 0 00.168.628 8.946 8.946 0 002.11.897.474.474 0 00.57-.313 1.995 1.995 0 011.886-1.353c.878 0 1.618.567 1.887 1.353a.475.475 0 00.57.313 8.964 8.964 0 002.084-.883.473.473 0 00.167-.631c-.318-.608-.337-1.393.191-2.195.077-.116.185-.225.302-.302.772-.511 1.527-.513 2.125-.23a.477.477 0 00.628-.19 8.925 8.925 0 00.728-1.793.478.478 0 00-.314-.573"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
import React from "react";
|
||||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconShare: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" role="presentation">
|
||||
<g fill="currentColor" fillRule="evenodd">
|
||||
<path
|
||||
d="M6 15a3 3 0 100-6 3 3 0 000 6zm0-2a1 1 0 110-2 1 1 0 010 2zm12-4a3 3 0 100-6 3 3 0 000 6zm0-2a1 1 0 110-2 1 1 0 010 2zm0 14a3 3 0 100-6 3 3 0 000 6zm0-2a1 1 0 110-2 1 1 0 010 2z"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<path d="M7 13.562l8.66 5 1-1.732-8.66-5z"></path>
|
||||
<path d="M7 10.83l1 1.732 8.66-5-1-1.732z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconSplitCell: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M810.016 598.016h86.016v256h-768v-256h86.016v170.016h596v-170.016zM128 170.016v256h86.016V256h596v170.016h86.016v-256h-768z m342.016 300v84h-128v86.016l-128-128 128-128v86.016h128z m212 0V384l128 128-128 128v-86.016h-128v-84h128z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconStatus: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g
|
||||
transform="rotate(-45 100.071 47.645)"
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
>
|
||||
<path
|
||||
d="m44.625 4.22-47 46.951A26 26 0 0 0-10 69.566V190c0 14.36 11.64 26 26 26h94c14.36 0 26-11.64 26-26V69.566a26 26 0 0 0-7.625-18.395l-47-46.95c-10.151-10.14-26.599-10.14-36.75 0ZM67.24 18.37l47 46.95a6 6 0 0 1 1.76 4.246V190a6 6 0 0 1-6 6H16a6 6 0 0 1-6-6V69.566a6 6 0 0 1 1.76-4.245l47-46.95a6 6 0 0 1 8.48 0Z"
|
||||
fill-rule="nonzero"
|
||||
></path>
|
||||
<circle cx="63.172" cy="67.586" r="14"></circle>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconTable: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="16" height="16" viewBox="0 0 256 256" role="presentation">
|
||||
<g fill="currentColor" fill-rule="evenodd">
|
||||
<path
|
||||
d="M208 40c17.673 0 32 14.327 32 32v112c0 17.673-14.327 32-32 32H48c-17.673 0-32-14.327-32-32V72c0-17.673 14.327-32 32-32h160Zm0 20H48c-6.525 0-11.834 5.209-11.996 11.695L36 72v112c0 6.525 5.209 11.834 11.695 11.996L48 196h160c6.525 0 11.834-5.209 11.996-11.695L220 184V72c0-6.525-5.209-11.834-11.695-11.996L208 60Z"
|
||||
fillRule="nonzero"
|
||||
></path>
|
||||
<path d="M28 93h204v20H28zM28 143h204v20H28z"></path>
|
||||
<path d="M90 60v137H70V60z"></path>
|
||||
</g>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconTask: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" role="presentation">
|
||||
<path
|
||||
d="M7.5 6h9A1.5 1.5 0 0118 7.5v9a1.5 1.5 0 01-1.5 1.5h-9A1.5 1.5 0 016 16.5v-9A1.5 1.5 0 017.5 6zm3.072 8.838l.143.154a.5.5 0 00.769-.042l.13-.175 3.733-5.045a.8.8 0 00-.11-1.064.665.665 0 00-.984.118l-3.243 4.387-1.315-1.422a.663.663 0 00-.99 0 .801.801 0 000 1.07l1.867 2.019z"
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconZoomIn: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path d="M637 443H519V309c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v134H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h118v134c0 4.4 3.6 8 8 8h60c4.4 0 8-3.6 8-8V519h118c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"></path>
|
||||
<path d="M921 867L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Icon } from "@douyinfe/semi-ui";
|
||||
|
||||
export const IconZoomOut: React.FC<{ style?: React.CSSProperties }> = ({
|
||||
style = {},
|
||||
}) => {
|
||||
return (
|
||||
<Icon
|
||||
style={style}
|
||||
svg={
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
>
|
||||
<path d="M637 443H325c-4.4 0-8 3.6-8 8v60c0 4.4 3.6 8 8 8h312c4.4 0 8-3.6 8-8v-60c0-4.4-3.6-8-8-8z"></path>
|
||||
<path d="M921 867L775 721c122.1-148.9 113.6-369.5-26-509-148-148.1-388.4-148.1-537 0-148.1 148.6-148.1 389 0 537 139.5 139.6 360.1 148.1 509 26l146 146c3.2 2.8 8.3 2.8 11 0l43-43c2.8-2.7 2.8-7.8 0-11zM696 696c-118.8 118.7-311.2 118.7-430 0-118.7-118.8-118.7-311.2 0-430 118.8-118.7 311.2-118.7 430 0 118.7 118.8 118.7 311.2 0 430z"></path>
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
export * from "./IconDocument";
|
||||
export * from "./IconDocumentFill";
|
||||
export * from "./IconMessage";
|
||||
export * from "./IconOverview";
|
||||
export * from "./IconSetting";
|
||||
export * from "./IconShare";
|
||||
export * from "./IconLeft";
|
||||
export * from "./IconRight";
|
||||
export * from "./IconFull";
|
||||
export * from "./IconHalf";
|
||||
export * from "./IconCenter";
|
||||
export * from "./IconFont";
|
||||
export * from "./IconTask";
|
||||
export * from "./IconLink";
|
||||
export * from "./IconClear";
|
||||
export * from "./IconImage";
|
||||
export * from "./IconMind";
|
||||
export * from "./IconZoomIn";
|
||||
export * from "./IconZoomOut";
|
||||
export * from "./IconTable";
|
||||
export * from "./IconCodeBlock";
|
||||
export * from "./IconStatus";
|
||||
export * from "./IconInfo";
|
||||
export * from "./IconEmoji";
|
||||
export * from "./IconAddColumnBefore";
|
||||
export * from "./IconAddColumnAfter";
|
||||
export * from "./IconDeleteColumn";
|
||||
export * from "./IconAddRowBefore";
|
||||
export * from "./IconAddRowAfter";
|
||||
export * from "./IconDeleteRow";
|
||||
export * from "./IconDeleteTable";
|
||||
export * from "./IconMergeCell";
|
||||
export * from "./IconSplitCell";
|
||||
export * from "./IconAttachment";
|
||||
export * from "./IconMath";
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useRef, useState, useEffect } from "react";
|
||||
import distanceInWords from "date-fns/formatDistance";
|
||||
import dateFormat from "date-fns/format";
|
||||
import zh from "date-fns/locale/zh-CN";
|
||||
|
||||
let callbacks: Array<() => void> = [];
|
||||
|
||||
setInterval(() => {
|
||||
callbacks.forEach((cb) => cb());
|
||||
}, 1000 * 60);
|
||||
|
||||
function eachMinute(fn: () => void) {
|
||||
callbacks.push(fn);
|
||||
|
||||
return () => {
|
||||
callbacks = callbacks.filter((cb) => cb !== fn);
|
||||
};
|
||||
}
|
||||
|
||||
type Props = {
|
||||
date: string | number | Date;
|
||||
format?: string;
|
||||
timeago?: boolean;
|
||||
};
|
||||
|
||||
const getTimeago = (date: number | string | Date) => {
|
||||
let content = distanceInWords(new Date(date), new Date(), {
|
||||
addSuffix: true,
|
||||
locale: zh,
|
||||
});
|
||||
|
||||
content = content
|
||||
.replace("about", "")
|
||||
.replace("less than a minute ago", "just now")
|
||||
.replace("minute", "min");
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export const LocaleTime: React.FC<Props> = ({
|
||||
date,
|
||||
timeago,
|
||||
format = "yyyy-MM-dd HH:mm:ss",
|
||||
}) => {
|
||||
const [_, setMinutesMounted] = useState(0); // eslint-disable-line no-unused-vars
|
||||
const callback = useRef<() => void>();
|
||||
|
||||
useEffect(() => {
|
||||
callback.current = eachMinute(() => {
|
||||
setMinutesMounted((state) => ++state);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (callback.current) {
|
||||
callback.current();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const formated = dateFormat(new Date(date), format);
|
||||
|
||||
return (
|
||||
<time dateTime={formated}>{timeago ? getTimeago(date) : formated}</time>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
.wrap {
|
||||
display: inline-flex !important;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
> svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
> span {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import Link from "next/link";
|
||||
import { Typography } from "@douyinfe/semi-ui";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export const LogoImage = () => {
|
||||
return (
|
||||
<Link href={"/"} as={"/"}>
|
||||
<a style={{ width: 36, height: 36 }}>
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="36"
|
||||
height="36"
|
||||
>
|
||||
<path
|
||||
d="M512 959.948464A448.076077 448.076077 0 0 1 337.632578 99.243069a448.094728 448.094728 0 0 1 348.734844 825.553127 445.140995 445.140995 0 0 1-174.367422 35.152268z m0-876.27409c-236.180835 0-428.325626 192.144791-428.325626 428.305993s192.144791 428.335442 428.325626 428.335443 428.31581-192.144791 428.31581-428.335443S748.171019 83.674374 512 83.674374z"
|
||||
fill="var(--semi-color-primary)"
|
||||
></path>
|
||||
<path
|
||||
d="M512 910.179685a396.982093 396.982093 0 1 1 154.989992-31.294452 395.597991 395.597991 0 0 1-154.989992 31.294452z m0-786.552859c-214.152997 0-388.373174 174.220177-388.373174 388.353541s174.220177 388.38299 388.373174 388.38299 388.363357-174.220177 388.363357-388.38299-174.210361-388.353541-388.363357-388.353541z"
|
||||
fill="var(--semi-color-primary)"
|
||||
></path>
|
||||
<path
|
||||
d="M509.172898 357.196518m-70.167108 0a70.167108 70.167108 0 1 0 140.334215 0 70.167108 70.167108 0 1 0-140.334215 0Z"
|
||||
fill="var(--semi-color-primary)"
|
||||
></path>
|
||||
<path
|
||||
d="M427.559952 441.450056s34.219717 33.031941 77.411558 32.246635 83.919782-32.266268 83.919782-32.266268l99.488477 377.928602s-27.66241-20.7419-47.245983-19.200737-46.519575 41.15986-65.690863 40.001534-45.70482-54.971433-58.770351-54.156678-40.718126 57.661107-62.225699 54.186127-39.432187-48.168718-67.094597-47.118371-44.55631 22.577553-44.55631 22.577553zM509.172898 183.653668l-21.674451 68.26274h43.339085l-21.664634-68.26274zM682.715748 357.196518l-68.26274-21.664634v43.339085l68.26274-21.674451zM631.886805 234.482611l-63.590168 32.95341 30.636758 30.636758 32.95341-63.590168zM386.449174 234.482611l32.95341 63.590168 30.636758-30.636758-63.590168-32.95341zM335.620231 357.196518l68.26274 21.674451v-43.339085l-68.26274 21.664634z"
|
||||
fill="var(--semi-color-primary)"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogoText = () => {
|
||||
return (
|
||||
<Link href={"/"} as={"/"}>
|
||||
<a className={styles.wrap}>
|
||||
<Text>云策文档</Text>
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { Skeleton } from "@douyinfe/semi-ui";
|
||||
|
||||
export const Placeholder = () => {
|
||||
return (
|
||||
<Skeleton
|
||||
placeholder={
|
||||
<>
|
||||
{Array.from({ length: 6 }).fill(
|
||||
<Skeleton.Title
|
||||
style={{ width: "100%", marginBottom: 12, marginTop: 12 }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
loading={true}
|
||||
></Skeleton>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
.titleWrap {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.itemsWrap {
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.itemWrap {
|
||||
border-radius: var(--semi-border-radius-small);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--semi-color-fill-0);
|
||||
color: var(--semi-color-text-0);
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 4px 16px;
|
||||
|
||||
.leftWrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--semi-color-primary);
|
||||
|
||||
> span {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--semi-color-primary);
|
||||
color: var(--semi-color-primary);
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.paginationWrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
// padding: 16px 0 0;
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
import React, { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Typography,
|
||||
Dropdown,
|
||||
Badge,
|
||||
Button,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Pagination,
|
||||
Notification,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconMessage } from "components/icons/IconMessage";
|
||||
import {
|
||||
useAllMessages,
|
||||
useReadMessages,
|
||||
useUnreadMessages,
|
||||
} from "data/message";
|
||||
import { EmptyBoxIllustration } from "illustrations/empty-box";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { Empty } from "components/empty";
|
||||
import { Placeholder } from "./Placeholder";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Text } = Typography;
|
||||
const PAGE_SIZE = 6;
|
||||
|
||||
const MessagesRender = ({
|
||||
messageData,
|
||||
loading,
|
||||
error,
|
||||
onClick = null,
|
||||
page = 1,
|
||||
onPageChange = null,
|
||||
}) => {
|
||||
const total = (messageData && messageData.total) || 0;
|
||||
const messages = (messageData && messageData.data) || [];
|
||||
|
||||
const handleRead = (messageId) => {
|
||||
onClick && onClick(messageId);
|
||||
};
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={<Placeholder />}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div
|
||||
className={styles.itemsWrap}
|
||||
style={{ margin: "8px -16px" }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{messages.length ? (
|
||||
<>
|
||||
{messages.map((msg) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.itemWrap}
|
||||
onClick={() => handleRead(msg.id)}
|
||||
>
|
||||
<Link href={msg.url}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: msg.message },
|
||||
},
|
||||
}}
|
||||
style={{ width: 240 }}
|
||||
>
|
||||
{msg.title}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{total > PAGE_SIZE && (
|
||||
<div className={styles.paginationWrap}>
|
||||
<Pagination
|
||||
size="small"
|
||||
total={total}
|
||||
currentPage={page}
|
||||
pageSize={PAGE_SIZE}
|
||||
style={{ textAlign: "center" }}
|
||||
onPageChange={onPageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty
|
||||
illustration={<EmptyBoxIllustration />}
|
||||
message="暂无消息"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Message = () => {
|
||||
const {
|
||||
data: allMsgs,
|
||||
loading: allLoading,
|
||||
error: allError,
|
||||
page: allPage,
|
||||
setPage: allSetPage,
|
||||
} = useAllMessages();
|
||||
const {
|
||||
data: readMsgs,
|
||||
loading: readLoading,
|
||||
error: readError,
|
||||
page: readPage,
|
||||
setPage: readSetPage,
|
||||
} = useReadMessages();
|
||||
const {
|
||||
data: unreadMsgs,
|
||||
loading: unreadLoading,
|
||||
error: unreadError,
|
||||
readMessage,
|
||||
page: unreadPage,
|
||||
setPage: unreadSetPage,
|
||||
} = useUnreadMessages();
|
||||
|
||||
const clearAll = () => {
|
||||
Promise.all(
|
||||
(unreadMsgs.data || []).map((msg) => {
|
||||
return readMessage(msg.id);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!unreadMsgs || !unreadMsgs.total) return;
|
||||
|
||||
const msg = unreadMsgs.data[0];
|
||||
|
||||
Notification.info({
|
||||
title: "消息通知",
|
||||
content: (
|
||||
<Link href={msg.url}>
|
||||
<a className={styles.item}>
|
||||
<div className={styles.leftWrap}>
|
||||
<Text
|
||||
ellipsis={{
|
||||
showTooltip: {
|
||||
opts: { content: msg.message },
|
||||
},
|
||||
}}
|
||||
style={{ width: 240 }}
|
||||
>
|
||||
{msg.title}
|
||||
</Text>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
),
|
||||
|
||||
duration: 3,
|
||||
});
|
||||
}, [unreadMsgs]);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
position="bottomRight"
|
||||
trigger="click"
|
||||
content={
|
||||
<div style={{ width: 300, padding: "16px 16px 0" }}>
|
||||
<Tabs
|
||||
type="line"
|
||||
size="small"
|
||||
tabBarExtraContent={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Text
|
||||
type="quaternary"
|
||||
onClick={clearAll}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
全部已读
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<TabPane tab="未读" itemKey="unread">
|
||||
<MessagesRender
|
||||
messageData={unreadMsgs}
|
||||
loading={unreadLoading}
|
||||
error={unreadError}
|
||||
onClick={readMessage}
|
||||
page={unreadPage}
|
||||
onPageChange={unreadSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="已读" itemKey="read">
|
||||
<MessagesRender
|
||||
messageData={readMsgs}
|
||||
loading={readLoading}
|
||||
error={readError}
|
||||
page={readPage}
|
||||
onPageChange={readSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
<TabPane tab="全部" itemKey="all">
|
||||
<MessagesRender
|
||||
messageData={allMsgs}
|
||||
loading={allLoading}
|
||||
error={allError}
|
||||
page={allPage}
|
||||
onPageChange={allSetPage}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={
|
||||
unreadMsgs && unreadMsgs.total > 0 ? (
|
||||
<Badge count={unreadMsgs.total} overflowCount={99} type="danger">
|
||||
<IconMessage style={{ transform: `translateY(2px)` }} />
|
||||
</Badge>
|
||||
) : (
|
||||
<IconMessage />
|
||||
)
|
||||
}
|
||||
></Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from "./resizeable";
|
|
@ -0,0 +1,92 @@
|
|||
import React, { useRef, useEffect } from "react";
|
||||
import { useClickOutside } from "hooks/use-click-outside";
|
||||
import interact from "interactjs";
|
||||
import styles from "./style.module.scss";
|
||||
|
||||
interface IProps {
|
||||
width: number;
|
||||
height: number;
|
||||
onChange: (arg: { width: number; height: number }) => void;
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 50;
|
||||
const MIN_HEIGHT = 50;
|
||||
|
||||
export const Resizeable: React.FC<IProps> = ({
|
||||
width,
|
||||
height,
|
||||
onChange,
|
||||
children,
|
||||
}) => {
|
||||
const $container = useRef<HTMLDivElement>(null);
|
||||
const $topLeft = useRef<HTMLDivElement>(null);
|
||||
const $topRight = useRef<HTMLDivElement>(null);
|
||||
const $bottomLeft = useRef<HTMLDivElement>(null);
|
||||
const $bottomRight = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside($container, {
|
||||
in: () => $container.current.classList.add(styles.isActive),
|
||||
out: () => $container.current.classList.remove(styles.isActive),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
interact($container.current).resizable({
|
||||
edges: {
|
||||
top: true,
|
||||
right: true,
|
||||
bottom: true,
|
||||
left: true,
|
||||
},
|
||||
listeners: {
|
||||
move: function (event) {
|
||||
let { x, y } = event.target.dataset;
|
||||
x = (parseFloat(x) || 0) + event.deltaRect.left;
|
||||
y = (parseFloat(y) || 0) + event.deltaRect.top;
|
||||
|
||||
let { width, height } = event.rect;
|
||||
width = width < MIN_WIDTH ? MIN_WIDTH : width;
|
||||
height = height < MIN_HEIGHT ? MIN_HEIGHT : height;
|
||||
|
||||
Object.assign(event.target.style, {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
// transform: `translate(${x}px, ${y}px)`,
|
||||
});
|
||||
Object.assign(event.target.dataset, { x, y });
|
||||
onChange && onChange({ width, height });
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="js-resizeable-container"
|
||||
className={styles.resizable}
|
||||
ref={$container}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<span
|
||||
className={styles.resizer + " " + styles.topLeft}
|
||||
ref={$topLeft}
|
||||
data-type={"topLeft"}
|
||||
></span>
|
||||
<span
|
||||
className={styles.resizer + " " + styles.topRight}
|
||||
ref={$topRight}
|
||||
data-type={"topRight"}
|
||||
></span>
|
||||
<span
|
||||
className={styles.resizer + " " + styles.bottomLeft}
|
||||
ref={$bottomLeft}
|
||||
data-type={"bottomLeft"}
|
||||
></span>
|
||||
<span
|
||||
className={styles.resizer + " " + styles.bottomRight}
|
||||
ref={$bottomRight}
|
||||
data-type={"bottomRight"}
|
||||
></span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
.resizable {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
max-width: 100%;
|
||||
|
||||
.resizer {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
border: 3px solid #4286f4;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.resizer.topLeft {
|
||||
left: -5px;
|
||||
top: -5px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.resizer.topRight {
|
||||
right: -5px;
|
||||
top: -5px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resizer.bottomLeft {
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.resizer.bottomRight {
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
&.isActive {
|
||||
border: 1px solid #4286f4;
|
||||
|
||||
.resizer {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
needTitleSuffix?: boolean;
|
||||
}
|
||||
|
||||
const buildTitle = (title) => `${title} - 云策文档`;
|
||||
|
||||
export const Seo: React.FC<IProps> = ({ title, needTitleSuffix = true }) => {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{needTitleSuffix ? buildTitle(title) : title}</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width,initial-scale=1.0,viewport-fit=cover,maximum-scale=1"
|
||||
/>
|
||||
<meta name="keyword" content={`云策文档 协作 文档 fantasticit`} />
|
||||
<meta name="description" content={`云策文档 协作 文档 fantasticit`} />
|
||||
</Helmet>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
.cardWrap {
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
margin: 8px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 161px;
|
||||
padding: 12px 16px 16px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--semi-color-border);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
color: var(--semi-color-primary);
|
||||
margin-bottom: 12px;
|
||||
|
||||
.rightWrap {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--semi-color-shadow);
|
||||
|
||||
header .rightWrap {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
transition: all ease-in-out 0.2s;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 8px;
|
||||
border-radius: 0 0 var(--border-radius) var(--border-radius);
|
||||
background-color: var(--semi-color-fill-2);
|
||||
opacity: 0;
|
||||
|
||||
button {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
import type { ITemplate } from "@think/share";
|
||||
import { useCallback } from "react";
|
||||
import cls from "classnames";
|
||||
import Router from "next/router";
|
||||
import {
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
Modal,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconEdit, IconUser, IconPlus } from "@douyinfe/semi-icons";
|
||||
import { IconDocument } from "components/icons/IconDocument";
|
||||
import { TemplateReader } from "components/template/reader";
|
||||
import styles from "./index.module.scss";
|
||||
import { useToggle } from "hooks/useToggle";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export interface IProps {
|
||||
template: ITemplate;
|
||||
onClick?: (id: string) => void;
|
||||
getClassNames?: (id: string) => string;
|
||||
onOpenPreview?: () => void;
|
||||
onClosePreview?: () => void;
|
||||
}
|
||||
|
||||
export const TemplateCard: React.FC<IProps> = ({
|
||||
template,
|
||||
onClick,
|
||||
getClassNames = (id) => "",
|
||||
onOpenPreview,
|
||||
onClosePreview,
|
||||
}) => {
|
||||
const [visible, toggleVisible] = useToggle(false);
|
||||
|
||||
const gotoEdit = useCallback(() => {
|
||||
Router.push(`/template/${template.id}/`);
|
||||
}, [template]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title="模板预览"
|
||||
width={"calc(100vh - 120px)"}
|
||||
height={"calc(100vh - 120px)"}
|
||||
bodyStyle={{
|
||||
overflow: "auto",
|
||||
}}
|
||||
visible={visible}
|
||||
onCancel={() => {
|
||||
toggleVisible(false);
|
||||
onClosePreview && onClosePreview();
|
||||
}}
|
||||
footer={null}
|
||||
fullScreen
|
||||
>
|
||||
<TemplateReader key={template.id} templateId={template.id} />
|
||||
</Modal>
|
||||
<div className={cls(styles.cardWrap, getClassNames(template.id))}>
|
||||
<header>
|
||||
<IconDocument />
|
||||
<div className={styles.rightWrap}>
|
||||
<Space>
|
||||
<Tooltip key="edit" content="编辑" position="bottom">
|
||||
<Button
|
||||
type="tertiary"
|
||||
theme="borderless"
|
||||
icon={<IconEdit />}
|
||||
onClick={gotoEdit}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Text strong>{template.title}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Text type="tertiary" size="small">
|
||||
<Space>
|
||||
<Avatar
|
||||
size="extra-extra-small"
|
||||
src={template.createUser && template.createUser.avatar}
|
||||
>
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
创建者:
|
||||
{template.createUser && template.createUser.name}
|
||||
</Space>
|
||||
</Text>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
<div style={{ display: "flex" }}>
|
||||
已使用
|
||||
{template.usageAmount}次
|
||||
</div>
|
||||
</Text>
|
||||
</footer>
|
||||
<div className={styles.actions}>
|
||||
<Button
|
||||
theme="solid"
|
||||
type="tertiary"
|
||||
onClick={() => {
|
||||
toggleVisible(true);
|
||||
onOpenPreview && onOpenPreview();
|
||||
}}
|
||||
>
|
||||
预览
|
||||
</Button>
|
||||
{onClick && (
|
||||
<Button
|
||||
type="primary"
|
||||
theme="solid"
|
||||
onClick={() => onClick && onClick(template.id)}
|
||||
>
|
||||
使用
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateCardPlaceholder = () => {
|
||||
return (
|
||||
<div className={styles.cardWrap}>
|
||||
<header>
|
||||
<IconDocument />
|
||||
</header>
|
||||
<main>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<Skeleton.Title style={{ width: 160 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Text type="tertiary" size="small">
|
||||
<Space>
|
||||
<Avatar size="extra-extra-small">
|
||||
<IconUser />
|
||||
</Avatar>
|
||||
创建者:
|
||||
<Skeleton.Paragraph rows={1} style={{ width: 100 }} />
|
||||
</Space>
|
||||
</Text>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<Text type="tertiary" size="small">
|
||||
<div style={{ display: "flex" }}>
|
||||
更新时间:
|
||||
<Skeleton.Paragraph rows={1} style={{ width: 100 }} />
|
||||
</div>
|
||||
</Text>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateCardEmpty = ({
|
||||
getClassNames = () => "",
|
||||
onClick = () => {},
|
||||
}) => {
|
||||
return (
|
||||
<div className={cls(styles.cardWrap, getClassNames())} onClick={onClick}>
|
||||
<div
|
||||
style={{
|
||||
height: 131,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "50%",
|
||||
transform: `translate(-50%, -50%)`,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Text link style={{ textAlign: "center" }}>
|
||||
<IconPlus
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
fontSize: 36,
|
||||
margin: "0 auto 12px",
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
<Text>空白文档</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,210 @@
|
|||
import React, { useMemo, useCallback, useState, useEffect } from "react";
|
||||
import Router from "next/router";
|
||||
import cls from "classnames";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import {
|
||||
Button,
|
||||
Nav,
|
||||
Space,
|
||||
Skeleton,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Spin,
|
||||
Switch,
|
||||
Popover,
|
||||
Popconfirm,
|
||||
BackTop,
|
||||
} from "@douyinfe/semi-ui";
|
||||
import { IconChevronLeft, IconArticle } from "@douyinfe/semi-icons";
|
||||
import { IUser, ITemplate } from "@think/share";
|
||||
import { Theme } from "components/theme";
|
||||
import {
|
||||
DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension,
|
||||
getProvider,
|
||||
MenuBar,
|
||||
Toc,
|
||||
} from "components/tiptap";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { User } from "components/user";
|
||||
import { DocumentStyle } from "components/document/style";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import { safeJSONParse } from "helpers/json";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface IProps {
|
||||
user: IUser;
|
||||
data: ITemplate;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
updateTemplate: (arg) => Promise<ITemplate>;
|
||||
deleteTemplate: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({
|
||||
user,
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
updateTemplate,
|
||||
deleteTemplate,
|
||||
}) => {
|
||||
if (!user) return null;
|
||||
|
||||
const provider = useMemo(() => {
|
||||
return getProvider({
|
||||
targetId: data.id,
|
||||
token: user.token,
|
||||
cacheType: "READER",
|
||||
user,
|
||||
docType: "template",
|
||||
});
|
||||
}, [data, user.token]);
|
||||
const editor = useEditor({
|
||||
editable: true,
|
||||
extensions: [
|
||||
...DEFAULT_EXTENSION,
|
||||
DocumentWithTitle,
|
||||
getCollaborationExtension(provider),
|
||||
],
|
||||
content: safeJSONParse(data && data.content),
|
||||
});
|
||||
|
||||
const [isPublic, setPublic] = useState(false);
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === "standardWidth"
|
||||
? styles.isStandardWidth
|
||||
: styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
const goback = useCallback(() => {
|
||||
Router.back();
|
||||
}, []);
|
||||
|
||||
const handleDelte = useCallback(() => {
|
||||
deleteTemplate().then(() => {
|
||||
goback();
|
||||
});
|
||||
}, [deleteTemplate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
setPublic(data.isPublic);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<header>
|
||||
<Nav
|
||||
style={{ overflow: "auto" }}
|
||||
mode="horizontal"
|
||||
header={
|
||||
<DataRender
|
||||
loading={loading}
|
||||
error={error}
|
||||
loadingContent={
|
||||
<Skeleton
|
||||
active
|
||||
placeholder={
|
||||
<Skeleton.Title style={{ width: 80, marginBottom: 8 }} />
|
||||
}
|
||||
loading={true}
|
||||
/>
|
||||
}
|
||||
normalContent={() => (
|
||||
<>
|
||||
<Tooltip content="返回" position="bottom">
|
||||
<Button
|
||||
onClick={goback}
|
||||
icon={<IconChevronLeft />}
|
||||
style={{ marginRight: 16 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Text
|
||||
strong
|
||||
ellipsis={{ showTooltip: true }}
|
||||
style={{ width: 120 }}
|
||||
>
|
||||
{data.title}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
footer={
|
||||
<Space>
|
||||
<Popover
|
||||
key="style"
|
||||
zIndex={1061}
|
||||
position="bottomLeft"
|
||||
content={<DocumentStyle />}
|
||||
>
|
||||
<Button
|
||||
icon={<IconArticle />}
|
||||
theme="borderless"
|
||||
type="tertiary"
|
||||
/>
|
||||
</Popover>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
content={isPublic ? "公开模板" : "个人模板"}
|
||||
>
|
||||
<Switch
|
||||
onChange={(v) => updateTemplate({ isPublic: v })}
|
||||
></Switch>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
title="删除模板"
|
||||
content="模板删除后不可恢复,谨慎操作!"
|
||||
onConfirm={handleDelte}
|
||||
>
|
||||
<Button type="danger">删除</Button>
|
||||
</Popconfirm>
|
||||
<Theme />
|
||||
<User />
|
||||
</Space>
|
||||
}
|
||||
></Nav>
|
||||
</header>
|
||||
<main className={styles.contentWrap}>
|
||||
<DataRender
|
||||
loading={false}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div className={styles.editorWrap}>
|
||||
<header className={editorWrapClassNames}>
|
||||
<div>
|
||||
<MenuBar editor={editor} />
|
||||
</div>
|
||||
</header>
|
||||
<main id="js-template-editor-container">
|
||||
<div
|
||||
className={cls(styles.contentWrap, editorWrapClassNames)}
|
||||
style={{ fontSize }}
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
<BackTop
|
||||
target={() =>
|
||||
document.querySelector("#js-template-editor-container")
|
||||
}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
.wrap {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> header {
|
||||
height: 60px;
|
||||
> div {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
height: calc(100% - 60px);
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
background-color: var(--semi-color-nav-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.editorWrap {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
> header {
|
||||
height: 50px;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--semi-color-border);
|
||||
|
||||
&.isStandardWidth {
|
||||
> div {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
> div {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
> main {
|
||||
flex: 1;
|
||||
height: calc(100% - 50px);
|
||||
overflow: auto;
|
||||
|
||||
.contentWrap {
|
||||
padding: 24px 24px 96px;
|
||||
|
||||
&.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React from "react";
|
||||
import { Spin } from "@douyinfe/semi-ui";
|
||||
import { useUser } from "data/user";
|
||||
import { Seo } from "components/seo";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { useTemplate } from "data/template";
|
||||
import { Editor } from "./editor";
|
||||
|
||||
interface IProps {
|
||||
templateId: string;
|
||||
}
|
||||
|
||||
export const TemplateEditor: React.FC<IProps> = ({ templateId }) => {
|
||||
const { user } = useUser();
|
||||
const { data, loading, error, updateTemplate, deleteTemplate } =
|
||||
useTemplate(templateId);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<>
|
||||
<Seo title={data.title} />
|
||||
<Editor
|
||||
user={user}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
updateTemplate={updateTemplate}
|
||||
deleteTemplate={deleteTemplate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,104 @@
|
|||
import React, { useState, useMemo } from "react";
|
||||
import { List, Pagination } from "@douyinfe/semi-ui";
|
||||
import { DataRender } from "components/data-render";
|
||||
import {
|
||||
IProps as ITemplateCardProps,
|
||||
TemplateCardPlaceholder,
|
||||
TemplateCard,
|
||||
} from "components/template/card";
|
||||
import { Empty } from "components/empty";
|
||||
|
||||
const grid = {
|
||||
gutter: 16,
|
||||
xs: 24,
|
||||
sm: 12,
|
||||
md: 12,
|
||||
lg: 8,
|
||||
xl: 8,
|
||||
};
|
||||
|
||||
interface IProps extends Omit<ITemplateCardProps, "template"> {
|
||||
// TODO: 修复类型
|
||||
hook: any;
|
||||
firstListItem?: React.ReactNode;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export const TemplateList: React.FC<IProps> = ({
|
||||
hook,
|
||||
onClick,
|
||||
getClassNames,
|
||||
firstListItem,
|
||||
onOpenPreview,
|
||||
onClosePreview,
|
||||
pageSize = 5,
|
||||
}) => {
|
||||
const { data, loading, error } = hook();
|
||||
|
||||
const [page, onPageChange] = useState(1);
|
||||
|
||||
const arr = useMemo(() => {
|
||||
const arr = (data && data.data) || [];
|
||||
const start = (page - 1) * pageSize;
|
||||
const end = page * pageSize;
|
||||
return arr.slice(start, end);
|
||||
}, [data, page]);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={() => (
|
||||
<List
|
||||
grid={grid}
|
||||
dataSource={[1, 2, 3]}
|
||||
renderItem={() => (
|
||||
<List.Item>
|
||||
<TemplateCardPlaceholder />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
error={error}
|
||||
normalContent={() => (
|
||||
<>
|
||||
<List
|
||||
grid={grid}
|
||||
dataSource={firstListItem ? [{}, ...arr] : arr}
|
||||
renderItem={(template, idx) => {
|
||||
if (idx === 0 && firstListItem) {
|
||||
return <List.Item>{firstListItem}</List.Item>;
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item>
|
||||
<TemplateCard
|
||||
template={template}
|
||||
onClick={onClick}
|
||||
getClassNames={getClassNames}
|
||||
onOpenPreview={onOpenPreview}
|
||||
onClosePreview={onClosePreview}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
emptyContent={<Empty message={"暂无模板"} />}
|
||||
></List>
|
||||
{data.data.length > pageSize ? (
|
||||
<Pagination
|
||||
size="small"
|
||||
style={{
|
||||
width: "100%",
|
||||
flexBasis: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
pageSize={pageSize}
|
||||
total={data.data.length}
|
||||
currentPage={page}
|
||||
onChange={(cPage) => onPageChange(cPage)}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useMemo } from "react";
|
||||
import cls from "classnames";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
import { Layout, Spin, Typography } from "@douyinfe/semi-ui";
|
||||
import { IUser, ITemplate } from "@think/share";
|
||||
import { DEFAULT_EXTENSION, DocumentWithTitle } from "components/tiptap";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { useDocumentStyle } from "hooks/useDocumentStyle";
|
||||
import { safeJSONParse } from "helpers/json";
|
||||
import styles from "./index.module.scss";
|
||||
|
||||
const { Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
|
||||
interface IProps {
|
||||
user: IUser;
|
||||
data: ITemplate;
|
||||
loading: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export const Editor: React.FC<IProps> = ({ user, data, loading, error }) => {
|
||||
if (!user) return null;
|
||||
|
||||
const c = safeJSONParse(data.content);
|
||||
let json = c.default || c;
|
||||
|
||||
if (json && json.content) {
|
||||
json = {
|
||||
type: "doc",
|
||||
content: json.content.slice(1),
|
||||
};
|
||||
}
|
||||
|
||||
const editor = useEditor({
|
||||
editable: false,
|
||||
extensions: [...DEFAULT_EXTENSION, DocumentWithTitle],
|
||||
content: json,
|
||||
});
|
||||
|
||||
const { width, fontSize } = useDocumentStyle();
|
||||
const editorWrapClassNames = useMemo(() => {
|
||||
return width === "standardWidth"
|
||||
? styles.isStandardWidth
|
||||
: styles.isFullWidth;
|
||||
}, [width]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Layout className={styles.contentWrap}>
|
||||
<DataRender
|
||||
loading={false}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<Content className={cls(styles.editorWrap)}>
|
||||
<div className={editorWrapClassNames} style={{ fontSize }}>
|
||||
<Title>{data.title}</Title>
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</Content>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
.contentWrap {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.editorWrap {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isStandardWidth {
|
||||
width: 96%;
|
||||
max-width: 750px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.isFullWidth {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import React from "react";
|
||||
import { Spin } from "@douyinfe/semi-ui";
|
||||
import { useUser } from "data/user";
|
||||
import { Seo } from "components/seo";
|
||||
import { DataRender } from "components/data-render";
|
||||
import { useTemplate } from "data/template";
|
||||
import { Editor } from "./editor";
|
||||
|
||||
interface IProps {
|
||||
templateId: string;
|
||||
}
|
||||
|
||||
export const TemplateReader: React.FC<IProps> = ({ templateId }) => {
|
||||
const { user } = useUser();
|
||||
const { data, loading, error } = useTemplate(templateId);
|
||||
|
||||
return (
|
||||
<DataRender
|
||||
loading={loading}
|
||||
loadingContent={
|
||||
<div style={{ margin: 24 }}>
|
||||
<Spin></Spin>
|
||||
</div>
|
||||
}
|
||||
error={error}
|
||||
normalContent={() => {
|
||||
return (
|
||||
<div style={{ fontSize: 16 }}>
|
||||
<Seo title={data.title} />
|
||||
<Editor user={user} data={data} loading={loading} error={error} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Tooltip } from "@douyinfe/semi-ui";
|
||||
import { IconSun, IconMoon } from "@douyinfe/semi-icons";
|
||||
import { useTheme } from "hooks/useTheme";
|
||||
|
||||
export const Theme = () => {
|
||||
const { theme, toggle } = useTheme();
|
||||
const Icon = theme === "dark" ? IconSun : IconMoon;
|
||||
const text = theme === "dark" ? "切换到亮色模式" : "切换到深色模式";
|
||||
|
||||
return (
|
||||
<Tooltip content={text} position="bottom">
|
||||
<Button
|
||||
onClick={toggle}
|
||||
icon={<Icon style={{ fontSize: 20 }} />}
|
||||
theme="borderless"
|
||||
></Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue