优化
This commit is contained in:
12
server/plugins/com.msgbyte.topic/.ministarrc.js
Normal file
12
server/plugins/com.msgbyte.topic/.ministarrc.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
externalDeps: [
|
||||
'react',
|
||||
'styled-components',
|
||||
'zustand',
|
||||
'zustand/middleware/immer',
|
||||
],
|
||||
pluginRoot: path.resolve(__dirname, './web'),
|
||||
outDir: path.resolve(__dirname, '../../public'),
|
||||
};
|
||||
68
server/plugins/com.msgbyte.topic/models/topic.ts
Normal file
68
server/plugins/com.msgbyte.topic/models/topic.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { db } from 'tailchat-server-sdk';
|
||||
const { getModelForClass, prop, TimeStamps, modelOptions } = db;
|
||||
import type { Types } from 'mongoose';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
class GroupTopicComment extends TimeStamps {
|
||||
@prop({
|
||||
default: () => nanoid(8),
|
||||
})
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
content: string;
|
||||
|
||||
@prop()
|
||||
author: string;
|
||||
|
||||
/**
|
||||
* 回复他人评论的id
|
||||
*/
|
||||
@prop()
|
||||
replyCommentId?: string;
|
||||
}
|
||||
|
||||
@modelOptions({
|
||||
options: {
|
||||
customName: 'p_topic',
|
||||
},
|
||||
})
|
||||
export class GroupTopic extends TimeStamps implements db.Base {
|
||||
_id: Types.ObjectId;
|
||||
id: string;
|
||||
|
||||
@prop()
|
||||
content: string;
|
||||
|
||||
@prop()
|
||||
author: string;
|
||||
|
||||
@prop()
|
||||
groupId: string;
|
||||
|
||||
/**
|
||||
* 会话面板id
|
||||
*/
|
||||
@prop()
|
||||
panelId: string;
|
||||
|
||||
@prop({
|
||||
type: () => GroupTopicComment,
|
||||
default: [],
|
||||
})
|
||||
comments: GroupTopicComment[];
|
||||
|
||||
/**
|
||||
* 话题的其他数据
|
||||
*/
|
||||
@prop()
|
||||
meta?: object;
|
||||
}
|
||||
|
||||
export type GroupTopicDocument = db.DocumentType<GroupTopic>;
|
||||
|
||||
const model = getModelForClass(GroupTopic);
|
||||
|
||||
export type GroupTopicModel = typeof model;
|
||||
|
||||
export default model;
|
||||
25
server/plugins/com.msgbyte.topic/package.json
Normal file
25
server/plugins/com.msgbyte.topic/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "tailchat-plugin-topic",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "moonrailgun",
|
||||
"description": "为群组提供话题功能",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:web": "ministar buildPlugin all",
|
||||
"build:web:watch": "ministar watchPlugin all"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "18.0.20",
|
||||
"less": "^4.1.3",
|
||||
"mini-star": "*",
|
||||
"rollup-plugin-inject-process-env": "^1.3.1",
|
||||
"rollup-plugin-less": "^1.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"nanoid": "^3.1.23",
|
||||
"tailchat-server-sdk": "*"
|
||||
}
|
||||
}
|
||||
251
server/plugins/com.msgbyte.topic/services/topic.service.ts
Normal file
251
server/plugins/com.msgbyte.topic/services/topic.service.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import _ from 'lodash';
|
||||
import { TcService, TcDbService, TcContext, call } from 'tailchat-server-sdk';
|
||||
import type { GroupTopicDocument, GroupTopicModel } from '../models/topic';
|
||||
|
||||
/**
|
||||
* 群组话题
|
||||
*/
|
||||
interface GroupTopicService
|
||||
extends TcService,
|
||||
TcDbService<GroupTopicDocument, GroupTopicModel> {}
|
||||
class GroupTopicService extends TcService {
|
||||
get serviceName(): string {
|
||||
return 'plugin:com.msgbyte.topic';
|
||||
}
|
||||
|
||||
onInit(): void {
|
||||
this.registerLocalDb(require('../models/topic').default);
|
||||
|
||||
this.registerAction('list', this.list, {
|
||||
params: {
|
||||
groupId: 'string',
|
||||
panelId: 'string',
|
||||
page: { type: 'number', optional: true },
|
||||
size: { type: 'number', optional: true },
|
||||
},
|
||||
});
|
||||
this.registerAction('create', this.create, {
|
||||
params: {
|
||||
groupId: 'string',
|
||||
panelId: 'string',
|
||||
content: 'string',
|
||||
meta: { type: 'object', optional: true },
|
||||
},
|
||||
});
|
||||
this.registerAction('createComment', this.createComment, {
|
||||
params: {
|
||||
groupId: 'string',
|
||||
panelId: 'string',
|
||||
topicId: 'string',
|
||||
content: 'string',
|
||||
replyCommentId: { type: 'string', optional: true },
|
||||
},
|
||||
});
|
||||
this.registerAction('delete', this.delete, {
|
||||
params: {
|
||||
groupId: 'string',
|
||||
panelId: 'string',
|
||||
topicId: 'string',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected onInited(): void {
|
||||
this.setPanelFeature('com.msgbyte.topic/grouppanel', ['subscribe']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有Topic
|
||||
*/
|
||||
async list(
|
||||
ctx: TcContext<{
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
page?: number;
|
||||
size?: number;
|
||||
}>
|
||||
) {
|
||||
const { groupId, panelId, page = 1, size = 20 } = ctx.params;
|
||||
const userId = ctx.meta.userId;
|
||||
const t = ctx.meta.t;
|
||||
|
||||
// 鉴权
|
||||
const group = await call(ctx).getGroupInfo(groupId);
|
||||
const isMember = group.members.some((member) => {
|
||||
return String(member.userId) === userId;
|
||||
});
|
||||
if (!isMember) {
|
||||
throw new Error(t('不是该群组成员'));
|
||||
}
|
||||
|
||||
const topic = await this.adapter.model
|
||||
.find({
|
||||
groupId,
|
||||
panelId,
|
||||
})
|
||||
.limit(size)
|
||||
.skip((page - 1) * 20)
|
||||
.sort({ _id: 'desc' })
|
||||
.exec();
|
||||
|
||||
const json = await this.transformDocuments(ctx, {}, topic);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一条Topic
|
||||
*/
|
||||
async create(
|
||||
ctx: TcContext<{
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
content: string;
|
||||
meta?: object;
|
||||
}>
|
||||
) {
|
||||
const { groupId, panelId, content, meta } = ctx.params;
|
||||
const userId = ctx.meta.userId;
|
||||
const t = ctx.meta.t;
|
||||
|
||||
// 鉴权
|
||||
const group = await call(ctx).getGroupInfo(groupId);
|
||||
const isMember = group.members.some((member) => member.userId === userId);
|
||||
if (!isMember) {
|
||||
throw new Error(t('不是该群组成员'));
|
||||
}
|
||||
|
||||
const targetPanel = group.panels.find((p) => p.id === panelId);
|
||||
|
||||
if (!targetPanel) {
|
||||
throw new Error(t('面板不存在'));
|
||||
}
|
||||
|
||||
const topic = await this.adapter.model.create({
|
||||
groupId,
|
||||
panelId,
|
||||
content,
|
||||
meta,
|
||||
author: userId,
|
||||
comment: [],
|
||||
});
|
||||
|
||||
const json = await this.transformDocuments(ctx, {}, topic);
|
||||
|
||||
this.roomcastNotify(ctx, panelId, 'create', json);
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复话题
|
||||
*/
|
||||
async createComment(
|
||||
ctx: TcContext<{
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
topicId: string;
|
||||
content: string;
|
||||
replyCommentId?: string;
|
||||
}>
|
||||
) {
|
||||
const { groupId, panelId, topicId, content, replyCommentId } = ctx.params;
|
||||
const userId = ctx.meta.userId;
|
||||
const t = ctx.meta.t;
|
||||
|
||||
// 鉴权
|
||||
const group = await call(ctx).getGroupInfo(groupId);
|
||||
const isMember = group.members.some((member) => member.userId === userId);
|
||||
if (!isMember) {
|
||||
throw new Error(t('不是该群组成员'));
|
||||
}
|
||||
|
||||
const targetPanel = group.panels.find((p) => p.id === panelId);
|
||||
|
||||
if (!targetPanel) {
|
||||
throw new Error(t('面板不存在'));
|
||||
}
|
||||
|
||||
const topic = await this.adapter.model.findOneAndUpdate(
|
||||
{
|
||||
_id: topicId,
|
||||
groupId,
|
||||
panelId,
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
comments: {
|
||||
content,
|
||||
author: userId,
|
||||
replyCommentId,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ new: true }
|
||||
);
|
||||
|
||||
const json = await this.transformDocuments(ctx, {}, topic);
|
||||
|
||||
this.roomcastNotify(ctx, panelId, 'createComment', json);
|
||||
|
||||
// 向所有参与者都添加收件箱消息
|
||||
const memberIds = _.uniq([
|
||||
topic.author,
|
||||
...topic.comments.map((c) => c.author),
|
||||
]);
|
||||
|
||||
await Promise.all(
|
||||
memberIds.map((memberId) =>
|
||||
call(ctx).appendInbox(
|
||||
'plugin:com.msgbyte.topic.comment',
|
||||
json,
|
||||
String(memberId)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除话题
|
||||
*/
|
||||
async delete(
|
||||
ctx: TcContext<{
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
topicId: string;
|
||||
}>
|
||||
) {
|
||||
const { groupId, panelId, topicId } = ctx.params;
|
||||
const userId = ctx.meta.userId;
|
||||
const t = ctx.meta.t;
|
||||
|
||||
// 鉴权
|
||||
const group = await call(ctx).getGroupInfo(groupId);
|
||||
const isMember = group.members.some((member) => member.userId === userId);
|
||||
if (!isMember) {
|
||||
throw new Error(t('不是该群组成员'));
|
||||
}
|
||||
|
||||
if (String(group.owner) !== userId) {
|
||||
throw new Error(t('仅群组所有者有权限删除话题'));
|
||||
}
|
||||
|
||||
const result = await this.adapter.model.deleteOne({
|
||||
_id: topicId,
|
||||
groupId,
|
||||
panelId,
|
||||
});
|
||||
|
||||
this.roomcastNotify(ctx, panelId, 'delete', {
|
||||
groupId,
|
||||
panelId,
|
||||
topicId,
|
||||
});
|
||||
|
||||
return result.deletedCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupTopicService;
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"label": "Topic",
|
||||
"label.zh-CN": "群组话题",
|
||||
"name": "com.msgbyte.topic",
|
||||
"url": "{BACKEND}/plugins/com.msgbyte.topic/index.js",
|
||||
"version": "0.0.0",
|
||||
"author": "moonrailgun",
|
||||
"description": "Provide topic feature for groups",
|
||||
"description.zh-CN": "为群组提供话题功能",
|
||||
"requireRestart": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@plugins/com.msgbyte.topic",
|
||||
"main": "src/index.tsx",
|
||||
"version": "0.0.0",
|
||||
"description": "为群组提供话题功能",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"sync:declaration": "tailchat declaration github"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/styled-components": "^5.1.26",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"react": "18.2.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"zustand": "^4.3.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import React, { useReducer, useState } from 'react';
|
||||
import {
|
||||
getMessageRender,
|
||||
showMessageTime,
|
||||
showSuccessToasts,
|
||||
useAsyncRequest,
|
||||
useCurrentUserInfo,
|
||||
useGroupInfo,
|
||||
} from '@capital/common';
|
||||
import {
|
||||
IconBtn,
|
||||
TextArea,
|
||||
UserName,
|
||||
UserAvatar,
|
||||
MessageAckContainer,
|
||||
Popconfirm,
|
||||
} from '@capital/component';
|
||||
import styled from 'styled-components';
|
||||
import type { GroupTopic } from '../types';
|
||||
import { Translate } from '../translate';
|
||||
import { request } from '../request';
|
||||
import { TopicComments } from './TopicComments';
|
||||
|
||||
const Root = styled.div`
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
padding: 10px;
|
||||
border-radius: 3px;
|
||||
margin: 10px;
|
||||
width: auto;
|
||||
display: flex;
|
||||
|
||||
.dark & {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.left {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 1;
|
||||
user-select: text;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
line-height: 32px;
|
||||
|
||||
.name {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.date {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
.content {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ReplyBox = styled.div`
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
background-color: transparent;
|
||||
|
||||
.dark & {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
`;
|
||||
|
||||
export const TopicCard: React.FC<{
|
||||
topic: GroupTopic;
|
||||
}> = React.memo((props) => {
|
||||
const topic: Partial<GroupTopic> = props.topic ?? {};
|
||||
const [showReply, toggleShowReply] = useReducer((state) => !state, false);
|
||||
const [comment, setComment] = useState('');
|
||||
const groupInfo = useGroupInfo(topic.groupId);
|
||||
const groupOwnerId = groupInfo?.owner;
|
||||
const userId = useCurrentUserInfo()._id;
|
||||
|
||||
const [{ loading }, handleComment] = useAsyncRequest(async () => {
|
||||
await request.post('createComment', {
|
||||
groupId: topic.groupId,
|
||||
panelId: topic.panelId,
|
||||
topicId: topic._id,
|
||||
content: comment,
|
||||
});
|
||||
|
||||
setComment('');
|
||||
toggleShowReply();
|
||||
showSuccessToasts();
|
||||
}, [topic.groupId, topic.panelId, topic._id, comment]);
|
||||
|
||||
const [, handleDeleteTopic] = useAsyncRequest(async () => {
|
||||
await request.post('delete', {
|
||||
groupId: topic.groupId,
|
||||
panelId: topic.panelId,
|
||||
topicId: topic._id,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MessageAckContainer converseId={topic.panelId} messageId={topic._id}>
|
||||
<Root>
|
||||
<div className="left">
|
||||
<UserAvatar userId={topic.author} />
|
||||
</div>
|
||||
|
||||
<div className="right">
|
||||
<div className="header">
|
||||
<div className="name">
|
||||
<UserName userId={topic.author} />
|
||||
</div>
|
||||
<div className="date">{showMessageTime(topic.createdAt)}</div>
|
||||
</div>
|
||||
|
||||
<div className="body">
|
||||
<div className="content">{getMessageRender(topic.content)}</div>
|
||||
|
||||
{Array.isArray(topic.comments) && topic.comments.length > 0 && (
|
||||
<TopicComments comments={topic.comments} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="footer">
|
||||
<IconBtn
|
||||
title={Translate.reply}
|
||||
icon="mdi:message-reply-text-outline"
|
||||
onClick={toggleShowReply}
|
||||
/>
|
||||
|
||||
{userId === groupOwnerId && (
|
||||
<Popconfirm
|
||||
title={Translate.topicDeleteConfimTip}
|
||||
onConfirm={handleDeleteTopic}
|
||||
>
|
||||
<IconBtn title={Translate.delete} icon="mdi:delete-outline" />
|
||||
</Popconfirm>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showReply && (
|
||||
<ReplyBox>
|
||||
<TextArea
|
||||
autoFocus
|
||||
placeholder={Translate.replyThisTopic}
|
||||
disabled={loading}
|
||||
value={comment}
|
||||
row={2}
|
||||
maxLength={1000}
|
||||
showCount={true}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
onPressEnter={handleComment}
|
||||
/>
|
||||
</ReplyBox>
|
||||
)}
|
||||
</div>
|
||||
</Root>
|
||||
</MessageAckContainer>
|
||||
);
|
||||
});
|
||||
TopicCard.displayName = 'TopicCard';
|
||||
@@ -0,0 +1,83 @@
|
||||
import { getMessageRender } from '@capital/common';
|
||||
import { UserAvatar, UserName } from '@capital/component';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { GroupTopicComment } from '../types';
|
||||
import _takeRight from 'lodash/takeRight';
|
||||
import { Translate } from '../translate';
|
||||
|
||||
const Root = styled.div`
|
||||
padding: 10px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 3px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
|
||||
.dark & {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.show-more {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.left {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.right {
|
||||
.username {
|
||||
font-weight: bold;
|
||||
line-height: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 话题评论
|
||||
*/
|
||||
export const TopicComments: React.FC<{
|
||||
comments: GroupTopicComment[];
|
||||
}> = React.memo((props) => {
|
||||
const [showAllComment, setShowAllComment] = useState(false);
|
||||
|
||||
const comments = showAllComment
|
||||
? props.comments
|
||||
: _takeRight(props.comments, 2);
|
||||
|
||||
return (
|
||||
<Root>
|
||||
{props.comments.length > 2 && !showAllComment && (
|
||||
<div className="show-more" onClick={() => setShowAllComment(true)}>
|
||||
{Translate.loadMore}...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{comments.map((comment) => (
|
||||
<div key={comment.id} className="comment-item">
|
||||
<div className="left">
|
||||
<UserAvatar userId={comment.author} size={24} />
|
||||
</div>
|
||||
|
||||
<div className="right">
|
||||
<div className="username">
|
||||
<UserName userId={comment.author} />
|
||||
</div>
|
||||
<div>{getMessageRender(comment.content)}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
TopicComments.displayName = 'TopicComments';
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useAsyncRequest } from '@capital/common';
|
||||
import { Button, ModalWrapper, TextArea } from '@capital/component';
|
||||
import React, { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Translate } from '../../translate';
|
||||
|
||||
const Footer = styled.div({
|
||||
textAlign: 'right',
|
||||
paddingTop: 10,
|
||||
});
|
||||
|
||||
export const TopicCreate: React.FC<{
|
||||
onCreate: (text: string) => Promise<void>;
|
||||
}> = React.memo((props) => {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const [{ loading }, handleCreate] = useAsyncRequest(async () => {
|
||||
await props.onCreate(text);
|
||||
}, [text]);
|
||||
|
||||
return (
|
||||
<ModalWrapper title={Translate.createBtn}>
|
||||
<TextArea
|
||||
autoFocus
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
/>
|
||||
|
||||
<Footer>
|
||||
<Button type="primary" loading={loading} onClick={handleCreate}>
|
||||
{Translate.createBtn}
|
||||
</Button>
|
||||
</Footer>
|
||||
</ModalWrapper>
|
||||
);
|
||||
});
|
||||
TopicCreate.displayName = 'TopicCreate';
|
||||
@@ -0,0 +1,202 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { TopicCard } from '../components/TopicCard';
|
||||
import {
|
||||
showSuccessToasts,
|
||||
useAsyncRequest,
|
||||
useGlobalSocketEvent,
|
||||
useGroupPanelContext,
|
||||
} from '@capital/common';
|
||||
import {
|
||||
Button,
|
||||
Empty,
|
||||
IconBtn,
|
||||
openModal,
|
||||
closeModal,
|
||||
LoadingOnFirst,
|
||||
} from '@capital/component';
|
||||
import { request } from '../request';
|
||||
import { Translate } from '../translate';
|
||||
import { TopicCreate } from '../components/modals/TopicCreate';
|
||||
import styled from 'styled-components';
|
||||
import { useTopicStore } from '../store';
|
||||
import type { GroupTopic } from '../types';
|
||||
|
||||
const Root = styled(LoadingOnFirst)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 10,
|
||||
|
||||
'.ant-empty': {
|
||||
paddingTop: 80,
|
||||
},
|
||||
|
||||
'.list': {
|
||||
height: '100%',
|
||||
overflow: 'auto',
|
||||
},
|
||||
|
||||
'.create-btn': {
|
||||
position: 'absolute',
|
||||
right: 20,
|
||||
bottom: 20,
|
||||
|
||||
'> .anticon': {
|
||||
fontSize: 24,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
|
||||
const GroupTopicPanelRender: React.FC = React.memo(() => {
|
||||
const panelInfo = useGroupPanelContext();
|
||||
const { panelId, groupId } = panelInfo;
|
||||
const {
|
||||
topicMap,
|
||||
addTopicPanel,
|
||||
addTopicItem,
|
||||
deleteTopicItem,
|
||||
updateTopicItem,
|
||||
resetTopicPanel,
|
||||
} = useTopicStore();
|
||||
const topicList = topicMap[panelId];
|
||||
const currentPageRef = useRef(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
|
||||
const [{ loading }, fetch] = useAsyncRequest(
|
||||
async (page = 1) => {
|
||||
if (!groupId || !panelId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { data: list } = await request.post('list', {
|
||||
groupId,
|
||||
panelId,
|
||||
page,
|
||||
size: PAGE_SIZE,
|
||||
});
|
||||
|
||||
if (Array.isArray(list)) {
|
||||
addTopicPanel(panelId, list);
|
||||
if (list.length !== PAGE_SIZE) {
|
||||
// 没有更多了
|
||||
setHasMore(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[groupId, panelId, addTopicPanel]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* 加载的时候获取列表
|
||||
*/
|
||||
fetch();
|
||||
|
||||
return () => {
|
||||
// 因为监听更新只在当前激活的面板中监听,还没添加到全局,因此为了保持面板状态需要清理面板状态
|
||||
// TODO: 增加群组级别的更新监听新增后可以移除
|
||||
resetTopicPanel(panelId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
currentPageRef.current += 1;
|
||||
fetch(currentPageRef.current);
|
||||
}, [fetch]);
|
||||
|
||||
useGlobalSocketEvent(
|
||||
'plugin:com.msgbyte.topic.create',
|
||||
(topic: GroupTopic) => {
|
||||
/**
|
||||
* 仅处理当前面板的话题更新
|
||||
*/
|
||||
if (topic.panelId === panelId) {
|
||||
addTopicItem(panelId, topic);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useGlobalSocketEvent(
|
||||
'plugin:com.msgbyte.topic.delete',
|
||||
(info: { panelId: string; topicId: string }) => {
|
||||
/**
|
||||
* 仅处理当前面板的话题更新
|
||||
*/
|
||||
if (info.panelId === panelId) {
|
||||
deleteTopicItem(panelId, info.topicId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
useGlobalSocketEvent(
|
||||
'plugin:com.msgbyte.topic.createComment',
|
||||
(topic: GroupTopic) => {
|
||||
/**
|
||||
* 仅处理当前面板的话题更新
|
||||
*/
|
||||
if (topic.panelId === panelId) {
|
||||
updateTopicItem(panelId, topic);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleCreateTopic = useCallback(() => {
|
||||
const key = openModal(
|
||||
<TopicCreate
|
||||
onCreate={async (text) => {
|
||||
await request.post('create', {
|
||||
groupId,
|
||||
panelId,
|
||||
content: text,
|
||||
});
|
||||
|
||||
showSuccessToasts();
|
||||
closeModal(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}, [panelInfo, fetch]);
|
||||
|
||||
return (
|
||||
<Root spinning={loading}>
|
||||
{Array.isArray(topicList) && topicList.length > 0 ? (
|
||||
<div className="list">
|
||||
{topicList.map((item, i) => (
|
||||
<TopicCard key={i} topic={item} />
|
||||
))}
|
||||
|
||||
{hasMore ? (
|
||||
<Button type="link" disabled={loading} onClick={handleLoadMore}>
|
||||
{loading ? Translate.loading : Translate.loadMore}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="link" disabled={true} onClick={handleLoadMore}>
|
||||
{Translate.noMore}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Empty description={Translate.noTopic}>
|
||||
<Button type="primary" onClick={handleCreateTopic}>
|
||||
{Translate.createBtn}
|
||||
</Button>
|
||||
</Empty>
|
||||
)}
|
||||
|
||||
<IconBtn
|
||||
className="create-btn"
|
||||
size="large"
|
||||
icon="mdi:plus"
|
||||
title={Translate.createBtn}
|
||||
onClick={handleCreateTopic}
|
||||
/>
|
||||
</Root>
|
||||
);
|
||||
});
|
||||
GroupTopicPanelRender.displayName = 'GroupTopicPanelRender';
|
||||
|
||||
export default GroupTopicPanelRender;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { TopicCard } from '../components/TopicCard';
|
||||
import { Problem, JumpToGroupPanelButton } from '@capital/component';
|
||||
import { Translate } from '../translate';
|
||||
|
||||
export const TopicInboxItem: React.FC<{ inboxItem: any }> = React.memo(
|
||||
(props) => {
|
||||
const payload = props.inboxItem.payload;
|
||||
if (!payload) {
|
||||
return <Problem text={Translate.topicDataError} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ height: '100%', overflow: 'auto', paddingBottom: 50 }}>
|
||||
<TopicCard topic={payload} />
|
||||
</div>
|
||||
|
||||
<JumpToGroupPanelButton
|
||||
groupId={payload.groupId}
|
||||
panelId={payload.panelId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
TopicInboxItem.displayName = 'TopicInboxItem';
|
||||
@@ -0,0 +1,30 @@
|
||||
import {
|
||||
getMessageRender,
|
||||
regGroupPanel,
|
||||
regPluginInboxItemMap,
|
||||
} from '@capital/common';
|
||||
import { Loadable } from '@capital/component';
|
||||
import { Translate } from './translate';
|
||||
|
||||
const PLUGIN_NAME = 'com.msgbyte.topic';
|
||||
|
||||
regGroupPanel({
|
||||
name: `${PLUGIN_NAME}/grouppanel`,
|
||||
label: Translate.topicpanel,
|
||||
provider: PLUGIN_NAME,
|
||||
render: Loadable(() => import('./group/GroupTopicPanelRender')),
|
||||
feature: ['subscribe', 'ack'],
|
||||
});
|
||||
|
||||
regPluginInboxItemMap('plugin:com.msgbyte.topic.comment', {
|
||||
source: Translate.topicpanel,
|
||||
getPreview: (item) => {
|
||||
return {
|
||||
title: Translate.topicpanel,
|
||||
desc: getMessageRender(item?.payload?.content ?? ''),
|
||||
};
|
||||
},
|
||||
render: Loadable(() =>
|
||||
import('./inbox/TopicInboxItem').then((module) => module.TopicInboxItem)
|
||||
),
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createPluginRequest } from '@capital/common';
|
||||
|
||||
export const request = createPluginRequest('com.msgbyte.topic');
|
||||
@@ -0,0 +1,68 @@
|
||||
import create from 'zustand';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
|
||||
import type { GroupTopic } from './types';
|
||||
|
||||
interface TopicPanelMap {
|
||||
[panelId: string]: GroupTopic[];
|
||||
}
|
||||
|
||||
interface TopicStoreState {
|
||||
topicMap: TopicPanelMap;
|
||||
addTopicPanel: (panelId: string, topicList: GroupTopic[]) => void;
|
||||
addTopicItem: (panelId: string, topic: GroupTopic) => void;
|
||||
deleteTopicItem: (panelId: string, topicId: string) => void;
|
||||
updateTopicItem: (panelId: string, topic: GroupTopic) => void;
|
||||
resetTopicPanel: (panelId: string) => void;
|
||||
}
|
||||
|
||||
export const useTopicStore = create<
|
||||
TopicStoreState,
|
||||
[['zustand/immer', never]]
|
||||
>(
|
||||
immer((set) => ({
|
||||
topicMap: {},
|
||||
addTopicPanel: (panelId, topicList) => {
|
||||
set((state) => {
|
||||
if (state.topicMap[panelId]) {
|
||||
state.topicMap[panelId].push(...topicList);
|
||||
} else {
|
||||
state.topicMap[panelId] = topicList;
|
||||
}
|
||||
});
|
||||
},
|
||||
addTopicItem: (panelId, topic) => {
|
||||
set((state) => {
|
||||
if (state.topicMap[panelId]) {
|
||||
state.topicMap[panelId].unshift(topic);
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteTopicItem: (panelId, topicId) => {
|
||||
set((state) => {
|
||||
if (state.topicMap[panelId]) {
|
||||
state.topicMap[panelId] = state.topicMap[panelId].filter(
|
||||
(item) => item._id !== topicId
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
updateTopicItem: (panelId, topic) => {
|
||||
set((state) => {
|
||||
if (state.topicMap[panelId]) {
|
||||
const findedTopicIndex = state.topicMap[panelId].findIndex(
|
||||
(t) => t._id === topic._id
|
||||
);
|
||||
if (findedTopicIndex >= 0) {
|
||||
state.topicMap[panelId][findedTopicIndex] = topic;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
resetTopicPanel: (panelId) => {
|
||||
set((state) => {
|
||||
delete state.topicMap[panelId];
|
||||
});
|
||||
},
|
||||
}))
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { localTrans } from '@capital/common';
|
||||
|
||||
export const Translate = {
|
||||
topicpanel: localTrans({ 'zh-CN': '话题面板', 'en-US': 'Topic Panel' }),
|
||||
noTopic: localTrans({ 'zh-CN': '暂无话题', 'en-US': 'No Topic' }),
|
||||
createBtn: localTrans({ 'zh-CN': '创建话题', 'en-US': 'Create Topic' }),
|
||||
reply: localTrans({ 'zh-CN': '回复', 'en-US': 'Reply' }),
|
||||
delete: localTrans({ 'zh-CN': '删除', 'en-US': 'Delete' }),
|
||||
replyThisTopic: localTrans({
|
||||
'zh-CN': '回复该话题',
|
||||
'en-US': 'Reply this topic',
|
||||
}),
|
||||
loadMore: localTrans({
|
||||
'zh-CN': '加载更多',
|
||||
'en-US': 'Load More',
|
||||
}),
|
||||
noMore: localTrans({
|
||||
'zh-CN': '没有更多了',
|
||||
'en-US': 'No More',
|
||||
}),
|
||||
loading: localTrans({
|
||||
'zh-CN': '加载中...',
|
||||
'en-US': 'Loading...',
|
||||
}),
|
||||
topicDataError: localTrans({
|
||||
'zh-CN': '话题信息异常',
|
||||
'en-US': 'Topic Data Error',
|
||||
}),
|
||||
topicDeleteConfimTip: localTrans({
|
||||
'zh-CN': '你确定要删除该话题么',
|
||||
'en-US': 'Are you sure you want to delete this topic?',
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
export interface GroupTopicComment {
|
||||
author: string;
|
||||
content: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GroupTopic {
|
||||
_id: string;
|
||||
author: string;
|
||||
comments: GroupTopicComment[];
|
||||
content: string;
|
||||
createdAt: string;
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react",
|
||||
"importsNotUsedAsValues": "error"
|
||||
}
|
||||
}
|
||||
516
server/plugins/com.msgbyte.topic/web/plugins/com.msgbyte.topic/types/tailchat.d.ts
vendored
Normal file
516
server/plugins/com.msgbyte.topic/web/plugins/com.msgbyte.topic/types/tailchat.d.ts
vendored
Normal file
@@ -0,0 +1,516 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/// <reference types="react" />
|
||||
|
||||
/**
|
||||
* 该文件由 Tailchat 自动生成
|
||||
* 用于插件的类型声明
|
||||
* 生成命令: pnpm run plugins:declaration:generate
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tailchat 通用
|
||||
*/
|
||||
declare module '@capital/common' {
|
||||
export const useGroupPanelParams: any;
|
||||
|
||||
/**
|
||||
* 打开模态框
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const openModal: (
|
||||
content: React.ReactNode,
|
||||
|
||||
props?: {
|
||||
/**
|
||||
* 是否显示右上角的关闭按钮
|
||||
* @default false
|
||||
*/
|
||||
closable?: boolean;
|
||||
|
||||
/**
|
||||
* 遮罩层是否可关闭
|
||||
*/
|
||||
maskClosable?: boolean;
|
||||
|
||||
/**
|
||||
* 关闭modal的回调
|
||||
*/
|
||||
onCloseModal?: () => void;
|
||||
}
|
||||
) => number;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const closeModal: any;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const ModalWrapper: any;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const useModalContext: any;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const openConfirmModal: any;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const openReconfirmModal: any;
|
||||
|
||||
/**
|
||||
* @deprecated 请从 @capital/component 引入
|
||||
*/
|
||||
export const Loadable: any;
|
||||
|
||||
export const getGlobalState: any;
|
||||
|
||||
export const useGlobalSocketEvent: <T>(
|
||||
eventName: string,
|
||||
callback: (data: T) => void
|
||||
) => void;
|
||||
|
||||
export const getJWTUserInfo: () => Promise<{
|
||||
_id?: string;
|
||||
nickname?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
}>;
|
||||
|
||||
export const dataUrlToFile: any;
|
||||
|
||||
export const urlSearchStringify: any;
|
||||
|
||||
export const urlSearchParse: any;
|
||||
|
||||
export const appendUrlSearch: any;
|
||||
|
||||
export const getServiceWorkerRegistration: any;
|
||||
|
||||
export const getServiceUrl: () => string;
|
||||
|
||||
export const getCachedUserInfo: (
|
||||
userId: string,
|
||||
refetch?: boolean
|
||||
) => Promise<{
|
||||
_id: string;
|
||||
email: string;
|
||||
nickname: string;
|
||||
discriminator: string;
|
||||
avatar: string | null;
|
||||
temporary: boolean;
|
||||
}>;
|
||||
|
||||
export const getCachedConverseInfo: any;
|
||||
|
||||
export const getCachedBaseGroupInfo: any;
|
||||
|
||||
export const getCachedUserSettings: any;
|
||||
|
||||
/**
|
||||
* 本地翻译
|
||||
* @example
|
||||
* localTrans({'zh-CN': '你好', 'en-US': 'Hello'});
|
||||
*
|
||||
* @param trans 翻译对象
|
||||
*/
|
||||
export const localTrans: (trans: Record<'zh-CN' | 'en-US', string>) => string;
|
||||
|
||||
export const getLanguage: any;
|
||||
|
||||
export const sharedEvent: any;
|
||||
|
||||
export const useAsync: <T extends (...args: any[]) => Promise<any>>(
|
||||
fn: T,
|
||||
deps?: React.DependencyList
|
||||
) => { loading: boolean; value?: any; error?: Error };
|
||||
|
||||
export const useAsyncFn: <T extends (...args: any[]) => Promise<any>>(
|
||||
fn: T,
|
||||
deps?: React.DependencyList
|
||||
) => [{ loading: boolean; value?: any; error?: Error }, T];
|
||||
|
||||
export const useAsyncRefresh: <T extends (...args: any[]) => Promise<any>>(
|
||||
fn: T,
|
||||
deps?: React.DependencyList
|
||||
) => { loading: boolean; value?: any; error?: Error; refresh: () => void };
|
||||
|
||||
export const useAsyncRequest: <T extends (...args: any[]) => Promise<any>>(
|
||||
fn: T,
|
||||
deps?: React.DependencyList
|
||||
) => [{ loading: boolean; value?: any }, T];
|
||||
|
||||
export const uploadFile: any;
|
||||
|
||||
export const showToasts: (
|
||||
message: string,
|
||||
type?: 'info' | 'success' | 'error' | 'warning'
|
||||
) => void;
|
||||
|
||||
export const showSuccessToasts: any;
|
||||
|
||||
export const showErrorToasts: (error: any) => void;
|
||||
|
||||
export const fetchAvailableServices: any;
|
||||
|
||||
export const isValidStr: (str: any) => str is string;
|
||||
|
||||
export const useGroupPanelInfo: any;
|
||||
|
||||
export const sendMessage: any;
|
||||
|
||||
export const showMessageTime: any;
|
||||
|
||||
export const joinArray: any;
|
||||
|
||||
export const navigate: any;
|
||||
|
||||
export const useLocation: any;
|
||||
|
||||
export const useNavigate: any;
|
||||
|
||||
/**
|
||||
* @deprecated please use createMetaFormSchema from @capital/component
|
||||
*/
|
||||
export const createFastFormSchema: any;
|
||||
|
||||
/**
|
||||
* @deprecated please use metaFormFieldSchema from @capital/component
|
||||
*/
|
||||
export const fieldSchema: any;
|
||||
|
||||
export const useCurrentUserInfo: any;
|
||||
|
||||
export const createPluginRequest: (pluginName: string) => {
|
||||
get: (actionName: string, config?: any) => Promise<any>;
|
||||
post: (actionName: string, data?: any, config?: any) => Promise<any>;
|
||||
};
|
||||
|
||||
export const postRequest: any;
|
||||
|
||||
export const pluginCustomPanel: any;
|
||||
|
||||
export const regCustomPanel: any;
|
||||
|
||||
export const pluginGroupPanel: any;
|
||||
|
||||
export const regGroupPanel: any;
|
||||
|
||||
export const messageInterpreter: {
|
||||
name?: string;
|
||||
explainMessage: (message: string) => React.ReactNode;
|
||||
}[];
|
||||
|
||||
export const regMessageInterpreter: (interpreter: {
|
||||
name?: string;
|
||||
explainMessage: (message: string) => React.ReactNode;
|
||||
}) => void;
|
||||
|
||||
export const getMessageRender: (message: string) => React.ReactNode;
|
||||
|
||||
export const regMessageRender: (
|
||||
render: (message: string) => React.ReactNode
|
||||
) => void;
|
||||
|
||||
export const getMessageTextDecorators: any;
|
||||
|
||||
export const regMessageTextDecorators: any;
|
||||
|
||||
export const ChatInputActionContextProps: any;
|
||||
|
||||
export const pluginChatInputActions: any;
|
||||
|
||||
export const regChatInputAction: any;
|
||||
|
||||
export const regSocketEventListener: (item: {
|
||||
eventName: string;
|
||||
eventFn: (...args: any[]) => void;
|
||||
}) => void;
|
||||
|
||||
export const pluginColorScheme: any;
|
||||
|
||||
export const regPluginColorScheme: any;
|
||||
|
||||
export const pluginInspectServices: any;
|
||||
|
||||
export const regInspectService: any;
|
||||
|
||||
export const pluginMessageExtraParsers: any;
|
||||
|
||||
export const regMessageExtraParser: any;
|
||||
|
||||
export const pluginRootRoute: any;
|
||||
|
||||
export const regPluginRootRoute: any;
|
||||
|
||||
export const pluginPanelActions: any;
|
||||
|
||||
export const regPluginPanelAction: (
|
||||
action:
|
||||
| {
|
||||
name: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
position: 'group';
|
||||
onClick: (ctx: { groupId: string; panelId: string }) => void;
|
||||
}
|
||||
| {
|
||||
name: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
position: 'dm';
|
||||
onClick: (ctx: { converseId: string }) => void;
|
||||
}
|
||||
) => void;
|
||||
|
||||
export const pluginPermission: any;
|
||||
|
||||
export const regPluginPermission: (permission: {
|
||||
/**
|
||||
* 权限唯一key, 用于写入数据库
|
||||
* 如果为插件则权限点应当符合命名规范, 如: plugin.com.msgbyte.github.manage
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 权限点显示名称
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
desc: string;
|
||||
/**
|
||||
* 是否默认开启
|
||||
*/
|
||||
default: boolean;
|
||||
/**
|
||||
* 是否依赖其他权限点
|
||||
*/
|
||||
required?: string[];
|
||||
}) => void;
|
||||
|
||||
export const pluginGroupPanelBadges: any;
|
||||
|
||||
export const regGroupPanelBadge: any;
|
||||
|
||||
export const pluginGroupTextPanelExtraMenus: any;
|
||||
|
||||
export const regPluginGroupTextPanelExtraMenu: any;
|
||||
|
||||
export const pluginUserExtraInfo: any;
|
||||
|
||||
export const regUserExtraInfo: any;
|
||||
|
||||
export const pluginSettings: any;
|
||||
|
||||
export const regPluginSettings: any;
|
||||
|
||||
export const pluginInboxItemMap: any;
|
||||
|
||||
export const regPluginInboxItemMap: any;
|
||||
|
||||
export const useGroupIdContext: () => string;
|
||||
|
||||
export const useGroupPanelContext: () => {
|
||||
groupId: string;
|
||||
panelId: string;
|
||||
} | null;
|
||||
|
||||
export const useSocketContext: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailchat 组件
|
||||
*/
|
||||
declare module '@capital/component' {
|
||||
export const Button: any;
|
||||
|
||||
export const Checkbox: any;
|
||||
|
||||
export const Input: any;
|
||||
|
||||
export const Divider: any;
|
||||
|
||||
export const Space: any;
|
||||
|
||||
export const Menu: any;
|
||||
|
||||
export const Table: any;
|
||||
|
||||
export const Switch: any;
|
||||
|
||||
export const Tooltip: any;
|
||||
|
||||
/**
|
||||
* @link https://ant.design/components/notification-cn/
|
||||
*/
|
||||
export const notification: any;
|
||||
|
||||
export const Empty: React.FC<
|
||||
React.PropsWithChildren<{
|
||||
prefixCls?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
imageStyle?: React.CSSProperties;
|
||||
image?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
}>
|
||||
>;
|
||||
|
||||
export const TextArea: any;
|
||||
|
||||
export const Avatar: any;
|
||||
|
||||
export const SensitiveText: React.FC<{ className?: string; text: string }>;
|
||||
|
||||
export const Icon: React.FC<{ icon: string } & React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
export const CopyableText: React.FC<{
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
config?:
|
||||
| boolean
|
||||
| {
|
||||
text?: string;
|
||||
onCopy?: (event?: React.MouseEvent<HTMLDivElement>) => void;
|
||||
icon?: React.ReactNode;
|
||||
tooltips?: boolean | React.ReactNode;
|
||||
format?: 'text/plain' | 'text/html';
|
||||
};
|
||||
}>;
|
||||
|
||||
export const WebFastForm: any;
|
||||
|
||||
export const WebMetaForm: any;
|
||||
|
||||
export const createMetaFormSchema: any;
|
||||
|
||||
export const metaFormFieldSchema: any;
|
||||
|
||||
export const Link: any;
|
||||
|
||||
export const MessageAckContainer: any;
|
||||
|
||||
export const Image: any;
|
||||
|
||||
export const IconBtn: React.FC<{
|
||||
icon: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
size?: 'small' | 'middle' | 'large';
|
||||
shape?: 'circle' | 'square';
|
||||
title?: string;
|
||||
danger?: boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLElement>;
|
||||
}>;
|
||||
|
||||
export const PillTabs: any;
|
||||
|
||||
export const PillTabPane: any;
|
||||
|
||||
export const LoadingSpinner: React.FC<{ tip?: string }>;
|
||||
|
||||
export const FullModalField: any;
|
||||
|
||||
export const DefaultFullModalInputEditorRender: any;
|
||||
|
||||
export const DefaultFullModalTextAreaEditorRender: any;
|
||||
|
||||
export const openModal: (
|
||||
content: React.ReactNode,
|
||||
|
||||
props?: {
|
||||
/**
|
||||
* 是否显示右上角的关闭按钮
|
||||
* @default false
|
||||
*/
|
||||
closable?: boolean;
|
||||
|
||||
/**
|
||||
* 遮罩层是否可关闭
|
||||
*/
|
||||
maskClosable?: boolean;
|
||||
|
||||
/**
|
||||
* 关闭modal的回调
|
||||
*/
|
||||
onCloseModal?: () => void;
|
||||
}
|
||||
) => number;
|
||||
|
||||
export const closeModal: any;
|
||||
|
||||
export const ModalWrapper: any;
|
||||
|
||||
export const useModalContext: any;
|
||||
|
||||
export const openConfirmModal: any;
|
||||
|
||||
export const openReconfirmModal: any;
|
||||
|
||||
export const Loadable: any;
|
||||
|
||||
export const Loading: React.FC<{
|
||||
spinning: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}>;
|
||||
|
||||
export const LoadingOnFirst: React.FC<{
|
||||
spinning: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
children?: React.ReactNode;
|
||||
}>;
|
||||
|
||||
export const SidebarView: any;
|
||||
|
||||
export const GroupPanelSelector: any;
|
||||
|
||||
export const Emoji: any;
|
||||
|
||||
export const PortalAdd: any;
|
||||
|
||||
export const PortalRemove: any;
|
||||
|
||||
export const ErrorBoundary: any;
|
||||
|
||||
export const UserAvatar: React.FC<{
|
||||
userId: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: 'large' | 'small' | 'default' | number;
|
||||
}>;
|
||||
|
||||
export const UserName: React.FC<{
|
||||
userId: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
}>;
|
||||
|
||||
export const Markdown: any;
|
||||
|
||||
export const Webview: any;
|
||||
|
||||
export const WebviewKeepAlive: any;
|
||||
|
||||
export const Card: any;
|
||||
|
||||
export const Problem: any;
|
||||
|
||||
export const JumpToButton: any;
|
||||
|
||||
export const JumpToGroupPanelButton: any;
|
||||
|
||||
export const JumpToConverseButton: any;
|
||||
}
|
||||
Reference in New Issue
Block a user