Browse Source

Initial commit

master
mm 7 months ago
commit
6522d2b5ab
  1. 3
      .eslintrc.js
  2. 13
      .gitignore
  3. 1
      .husky/commit-msg
  4. 1
      .husky/pre-commit
  5. 8
      .idea/.gitignore
  6. 12
      .idea/chat-v1.iml
  7. 8
      .idea/modules.xml
  8. 6
      .idea/vcs.xml
  9. 17
      .lintstagedrc
  10. 2
      .npmrc
  11. 3
      .prettierignore
  12. 8
      .prettierrc
  13. 3
      .stylelintrc.js
  14. 40
      .umirc.ts
  15. 3
      README.md
  16. 20
      mock/userAPI.ts
  17. 31
      package.json
  18. 13581
      pnpm-lock.yaml
  19. 10
      src/access.ts
  20. 16
      src/app.ts
  21. 0
      src/assets/.gitkeep
  22. 4
      src/components/Guide/Guide.less
  23. 23
      src/components/Guide/Guide.tsx
  24. 2
      src/components/Guide/index.ts
  25. 1
      src/constants/index.ts
  26. 13
      src/models/global.ts
  27. 21
      src/pages/Access/index.tsx
  28. 141
      src/pages/Chat/components/ChatWindow.less
  29. 201
      src/pages/Chat/components/ChatWindow.tsx
  30. 71
      src/pages/Chat/components/ContactList.less
  31. 126
      src/pages/Chat/components/ContactList.tsx
  32. 36
      src/pages/Chat/components/InfoPanel.less
  33. 123
      src/pages/Chat/components/InfoPanel.tsx
  34. 14
      src/pages/Chat/index.less
  35. 47
      src/pages/Chat/index.tsx
  36. 239
      src/pages/Chat/wss/WebSocketClient.ts
  37. 34
      src/pages/Chat/wss/config.ts
  38. 5
      src/pages/Chat/wss/index.ts
  39. 42
      src/pages/Chat/wss/types.ts
  40. 89
      src/pages/Chat/wss/useWebSocket.ts
  41. 34
      src/pages/Chat/wss/utils.ts
  42. 3
      src/pages/Home/index.less
  43. 18
      src/pages/Home/index.tsx
  44. 26
      src/pages/Table/components/CreateForm.tsx
  45. 138
      src/pages/Table/components/UpdateForm.tsx
  46. 270
      src/pages/Table/index.tsx
  47. 96
      src/services/demo/UserController.ts
  48. 7
      src/services/demo/index.ts
  49. 68
      src/services/demo/typings.d.ts
  50. 4
      src/utils/format.ts
  51. 3
      tsconfig.json
  52. 1
      typings.d.ts

3
.eslintrc.js

@ -0,0 +1,3 @@
module.exports = {
extends: require.resolve('@umijs/max/eslint'),
};

13
.gitignore

@ -0,0 +1,13 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/.umi
/.umi-production
/.umi-test
/dist
/.mfsu
.swc

1
.husky/commit-msg

@ -0,0 +1 @@
npx --no-install max verify-commit $1

1
.husky/pre-commit

@ -0,0 +1 @@
npx --no-install lint-staged --quiet

8
.idea/.gitignore

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/chat-v1.iml

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/chat-v1.iml" filepath="$PROJECT_DIR$/.idea/chat-v1.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

17
.lintstagedrc

@ -0,0 +1,17 @@
{
"*.{md,json}": [
"prettier --cache --write"
],
"*.{js,jsx}": [
"max lint --fix --eslint-only",
"prettier --cache --write"
],
"*.{css,less}": [
"max lint --fix --stylelint-only",
"prettier --cache --write"
],
"*.ts?(x)": [
"max lint --fix --eslint-only",
"prettier --cache --parser=typescript --write"
]
}

2
.npmrc

@ -0,0 +1,2 @@
registry=https://registry.npmjs.com/

3
.prettierignore

@ -0,0 +1,3 @@
node_modules
.umi
.umi-production

8
.prettierrc

@ -0,0 +1,8 @@
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all",
"proseWrap": "never",
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
}

3
.stylelintrc.js

@ -0,0 +1,3 @@
module.exports = {
extends: require.resolve('@umijs/max/stylelint'),
};

40
.umirc.ts

@ -0,0 +1,40 @@
import { defineConfig } from '@umijs/max';
export default defineConfig({
antd: {},
access: {},
model: {},
initialState: {},
request: {},
layout: {
title: '@umijs/max',
},
routes: [
{
path: '/',
redirect: '/home',
},
{
name: '首页',
path: '/home',
component: './Home',
},
{
name: '权限演示',
path: '/access',
component: './Access',
},
{
name: ' CRUD 示例',
path: '/table',
component: './Table',
},
{
name: '客服聊天',
path: '/chat',
component: './Chat',
},
],
npmClient: 'pnpm',
});

3
README.md

@ -0,0 +1,3 @@
# README
`@umijs/max` 模板项目,更多功能参考 [Umi Max 简介](https://umijs.org/docs/max/introduce)

20
mock/userAPI.ts

@ -0,0 +1,20 @@
const users = [
{ id: 0, name: 'Umi', nickName: 'U', gender: 'MALE' },
{ id: 1, name: 'Fish', nickName: 'B', gender: 'FEMALE' },
];
export default {
'GET /api/v1/queryUserList': (req: any, res: any) => {
res.json({
success: true,
data: { list: users },
errorCode: 0,
});
},
'PUT /api/v1/user/': (req: any, res: any) => {
res.json({
success: true,
errorCode: 0,
});
},
};

31
package.json

@ -0,0 +1,31 @@
{
"private": true,
"author": "mm <RicartApache585@gmail.com>",
"scripts": {
"build": "max build",
"dev": "max dev",
"format": "prettier --cache --write .",
"postinstall": "max setup",
"prepare": "husky",
"setup": "max setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^5.0.1",
"@ant-design/pro-components": "^2.8.2",
"@umijs/max": "^4.4.1",
"antd": "^5.22.5",
"axios": "^1.7.9"
},
"devDependencies": {
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"@umijs/plugins": "^4.4.1",
"husky": "^9",
"lint-staged": "^13.2.0",
"prettier": "^2.8.7",
"prettier-plugin-organize-imports": "^3.2.2",
"prettier-plugin-packagejson": "^2.4.3",
"typescript": "^5.0.3"
}
}

13581
pnpm-lock.yaml
File diff suppressed because it is too large
View File

10
src/access.ts

@ -0,0 +1,10 @@
export default (initialState: API.UserInfo) => {
// 在这里按照初始化数据定义项目中的权限,统一管理
// 参考文档 https://umijs.org/docs/max/access
const canSeeAdmin = !!(
initialState && initialState.name !== 'dontHaveAccess'
);
return {
canSeeAdmin,
};
};

16
src/app.ts

@ -0,0 +1,16 @@
// 运行时配置
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{ name: string }> {
return { name: '@umijs/max' };
}
export const layout = () => {
return {
logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
menu: {
locale: false,
},
};
};

0
src/assets/.gitkeep

4
src/components/Guide/Guide.less

@ -0,0 +1,4 @@
.title {
margin: 0 auto;
font-weight: 200;
}

23
src/components/Guide/Guide.tsx

@ -0,0 +1,23 @@
import { Layout, Row, Typography } from 'antd';
import React from 'react';
import styles from './Guide.less';
interface Props {
name: string;
}
// 脚手架示例组件
const Guide: React.FC<Props> = (props) => {
const { name } = props;
return (
<Layout>
<Row>
<Typography.Title level={3} className={styles.title}>
使 <strong>{name}</strong>
</Typography.Title>
</Row>
</Layout>
);
};
export default Guide;

2
src/components/Guide/index.ts

@ -0,0 +1,2 @@
import Guide from './Guide';
export default Guide;

1
src/constants/index.ts

@ -0,0 +1 @@
export const DEFAULT_NAME = 'Umi Max';

13
src/models/global.ts

@ -0,0 +1,13 @@
// 全局共享数据示例
import { DEFAULT_NAME } from '@/constants';
import { useState } from 'react';
const useUser = () => {
const [name, setName] = useState<string>(DEFAULT_NAME);
return {
name,
setName,
};
};
export default useUser;

21
src/pages/Access/index.tsx

@ -0,0 +1,21 @@
import { PageContainer } from '@ant-design/pro-components';
import { Access, useAccess } from '@umijs/max';
import { Button } from 'antd';
const AccessPage: React.FC = () => {
const access = useAccess();
return (
<PageContainer
ghost
header={{
title: '权限示例',
}}
>
<Access accessible={access.canSeeAdmin}>
<Button> Admin </Button>
</Access>
</PageContainer>
);
};
export default AccessPage;

141
src/pages/Chat/components/ChatWindow.less

@ -0,0 +1,141 @@
.chatWindow {
height: 100%;
display: flex;
flex-direction: column;
background: #f5f5f5;
.chatHeader {
padding: 16px;
background: white;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.typingIndicator {
font-size: 12px;
color: #8c8c8c;
font-style: italic;
}
}
.noChat {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
color: #8c8c8c;
font-size: 16px;
}
.messageList {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.messageItem {
margin-bottom: 16px;
display: flex;
&.sent {
justify-content: flex-end;
.messageContent {
background-color: #1890ff;
color: white;
.messageTime {
color: rgba(255, 255, 255, 70%);
}
}
}
&.received {
justify-content: flex-start;
.messageContent {
background-color: white;
}
}
}
.messageContent {
max-width: 70%;
padding: 12px 16px;
border-radius: 8px;
word-wrap: break-word;
position: relative;
}
.messageTime {
font-size: 12px;
margin-top: 4px;
opacity: 0.7;
display: flex;
justify-content: flex-end;
gap: 8px;
}
.messageStatus {
font-size: 12px;
}
.inputArea {
background: white;
padding: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
flex-direction: column;
}
.toolbar {
margin-bottom: 8px;
display: flex;
gap: 16px;
}
.toolbarIcon {
font-size: 20px;
color: #8c8c8c;
cursor: pointer;
&:hover {
color: #1890ff;
}
}
.sendButton {
margin-top: 8px;
align-self: flex-end;
}
// 表情选择器样式
.emojiPicker {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 8px;
padding: 8px;
}
.emojiItem {
font-size: 24px;
cursor: pointer;
text-align: center;
padding: 4px;
border-radius: 4px;
transition: background-color 0.3s;
&:hover {
background-color: #f0f0f0;
}
}
}
:global {
.ant-popover.emojiPopover {
.ant-popover-inner-content {
padding: 8px;
}
}
}

201
src/pages/Chat/components/ChatWindow.tsx

@ -0,0 +1,201 @@
import React from 'react';
import { Input, Button, Upload, message, Popover } from 'antd';
import { SendOutlined, SmileOutlined, PaperClipOutlined } from '@ant-design/icons';
import { useWebSocket } from '../wss';
import styles from './ChatWindow.less';
// 聊天窗口组件的属性接口
interface ChatWindowProps {
userId: string; // 当前用户ID
currentChat: { // 当前聊天对象
id: string; // 聊天对象ID
name: string; // 聊天对象名称
} | null;
}
// 聊天消息接口
interface ChatMessage {
id: string; // 消息ID
sender: string; // 发送者ID
receiver: string; // 接收者ID
content: string; // 消息内容
timestamp: number; // 时间戳
status: string; // 消息状态
}
// WebSocket状态接口
interface WebSocketStatus {
status: 'CONNECTED' | 'DISCONNECTED'; // 连接状态
messages: ChatMessage[]; // 消息列表
typingUsers: string[]; // 正在输入的用户列表
sendMessage: (message: string, receiver: string) => void; // 发送消息方法
sendTyping: () => void; // 发送正在输入状态
sendRead: (messageIds: string[]) => void; // 发送已读状态
}
// 聊天窗口组件
const ChatWindow: React.FC<ChatWindowProps> = ({ userId, currentChat }) => {
const [inputValue, setInputValue] = React.useState('');
const messagesEndRef = React.useRef<HTMLDivElement>(null);
const {
messages,
typingUsers,
sendMessage,
sendTyping,
sendRead,
status
} = useWebSocket(userId) as WebSocketStatus;
// 过滤当前聊天的消息
const currentMessages = messages.filter(
msg =>
(msg.sender === userId && msg.receiver === currentChat?.id) ||
(msg.receiver === userId && msg.sender === currentChat?.id)
);
// 滚动到底部方法
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
// 消息更新时滚动到底部
React.useEffect(() => {
scrollToBottom();
}, [currentMessages]);
// 处理发送消息
const handleSend = () => {
if (!inputValue.trim() || !currentChat) return;
sendMessage(inputValue.trim(), currentChat.id);
setInputValue('');
};
// 处理输入变化
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value);
sendTyping();
};
// 处理键盘事件
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 处理表情选择
const handleEmojiSelect = (emoji: string) => {
setInputValue(prev => prev + emoji);
};
// 处理消息已读状态
React.useEffect(() => {
if (!currentChat) return;
const unreadMessages = currentMessages
.filter(msg =>
msg.sender === currentChat.id &&
msg.status !== 'read'
)
.map(msg => msg.id);
if (unreadMessages.length > 0) {
sendRead(unreadMessages);
}
}, [currentMessages, currentChat]);
const emojis = ['😊', '😂', '🤔', '👍', '❤️', '😍', '🎉', '👋', '🙏', '💪', '✨', '🌟'];
const emojiContent = (
<div className={styles.emojiPicker}>
{emojis.map((emoji) => (
<span
key={emoji}
className={styles.emojiItem}
onClick={() => handleEmojiSelect(emoji)}
>
{emoji}
</span>
))}
</div>
);
if (!currentChat) {
return (
<div className={styles.chatWindow}>
<div className={styles.noChat}>
</div>
</div>
);
}
return (
<div className={styles.chatWindow}>
<div className={styles.chatHeader}>
<span>{currentChat.name}</span>
{typingUsers.includes(currentChat.id) && (
<span className={styles.typingIndicator}>...</span>
)}
</div>
<div className={styles.messageList}>
{currentMessages.map((msg) => (
<div
key={msg.id}
className={`${styles.messageItem} ${
msg.sender === userId ? styles.sent : styles.received
}`}
>
<div className={styles.messageContent}>
{msg.content}
<div className={styles.messageTime}>
{new Date(msg.timestamp).toLocaleTimeString()}
{msg.sender === userId && (
<span className={styles.messageStatus}>{msg.status}</span>
)}
</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className={styles.inputArea}>
<div className={styles.toolbar}>
<Popover
content={emojiContent}
trigger="click"
placement="topLeft"
overlayClassName={styles.emojiPopover}
>
<SmileOutlined className={styles.toolbarIcon} />
</Popover>
<Upload>
<PaperClipOutlined className={styles.toolbarIcon} />
</Upload>
</div>
<Input.TextArea
value={inputValue}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
placeholder="输入消息..."
autoSize={{ minRows: 2, maxRows: 6 }}
disabled={status !== 'CONNECTED'}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
className={styles.sendButton}
disabled={status !== 'CONNECTED'}
/>
</div>
</div>
);
};
export default ChatWindow;

71
src/pages/Chat/components/ContactList.less

@ -0,0 +1,71 @@
.contactList {
height: 100%;
display: flex;
flex-direction: column;
.searchBar {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.list {
flex: 1;
overflow: auto;
}
.contactItem {
padding: 12px 16px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #f5f5f5;
}
&.selected {
background-color: #e6f7ff;
border-right: 3px solid #1890ff;
}
}
.contactTitle {
display: flex;
justify-content: space-between;
align-items: center;
}
.unreadBadge {
background-color: #1890ff;
color: white;
border-radius: 10px;
padding: 0 6px;
font-size: 12px;
min-width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.lastMessage {
color: #8c8c8c;
font-size: 12px;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tags {
margin-top: 4px;
:global {
.ant-tag {
margin-right: 4px;
font-size: 12px;
line-height: 16px;
padding: 0 4px;
}
}
}
}

126
src/pages/Chat/components/ContactList.tsx

@ -0,0 +1,126 @@
import React from 'react';
import { Input, List, Tag, Avatar } from 'antd';
import { SearchOutlined, UserOutlined } from '@ant-design/icons';
import { useWebSocket } from '../wss';
import styles from './ContactList.less';
interface Contact {
id: string;
name: string;
avatar?: string;
lastMessage: string;
tags: string[];
unread?: number;
}
interface ContactListProps {
currentUserId: string;
selectedContactId?: string;
onSelectContact: (contact: { id: string; name: string }) => void;
}
const ContactList: React.FC<ContactListProps> = ({
currentUserId,
selectedContactId,
onSelectContact,
}) => {
const [searchText, setSearchText] = React.useState('');
const { messages } = useWebSocket(currentUserId);
// 模拟联系人数据
const contacts: Contact[] = [
{
id: 'user_002',
name: '张三',
lastMessage: '好的,我知道了',
tags: ['售前咨询'],
unread: 2,
},
{
id: 'user_003',
name: '李四',
lastMessage: '请问产品什么时候发货?',
tags: ['售后服务'],
},
// 更多联系人...
];
// 获取每个联系人的最后一条消息
const getLastMessage = (contactId: string) => {
const contactMessages = messages.filter(
msg => msg.sender === contactId || msg.receiver === contactId
);
return contactMessages[contactMessages.length - 1]?.content || '';
};
// 获取未读消息数
const getUnreadCount = (contactId: string) => {
return messages.filter(
msg =>
msg.sender === contactId &&
msg.receiver === currentUserId &&
msg.status !== 'read'
).length;
};
const filteredContacts = contacts
.map(contact => ({
...contact,
lastMessage: getLastMessage(contact.id) || contact.lastMessage,
unread: getUnreadCount(contact.id),
}))
.filter(contact =>
contact.name.toLowerCase().includes(searchText.toLowerCase())
);
return (
<div className={styles.contactList}>
<div className={styles.searchBar}>
<Input
prefix={<SearchOutlined />}
placeholder="搜索联系人"
onChange={(e) => setSearchText(e.target.value)}
/>
</div>
<List
className={styles.list}
dataSource={filteredContacts}
renderItem={(contact) => (
<List.Item
className={`${styles.contactItem} ${
selectedContactId === contact.id ? styles.selected : ''
}`}
onClick={() => onSelectContact({ id: contact.id, name: contact.name })}
>
<List.Item.Meta
avatar={
<Avatar icon={<UserOutlined />} src={contact.avatar} />
}
title={
<div className={styles.contactTitle}>
<span>{contact.name}</span>
{contact.unread > 0 && (
<span className={styles.unreadBadge}>{contact.unread}</span>
)}
</div>
}
description={
<div>
<div className={styles.lastMessage}>{contact.lastMessage}</div>
<div className={styles.tags}>
{contact.tags.map((tag) => (
<Tag key={tag} color="blue">{tag}</Tag>
))}
</div>
</div>
}
/>
</List.Item>
)}
/>
</div>
);
};
export default ContactList;

36
src/pages/Chat/components/InfoPanel.less

@ -0,0 +1,36 @@
.infoPanel {
height: 100%;
overflow-y: auto;
padding: 16px;
:global {
.ant-card {
margin-bottom: 16px;
}
}
.customerInfo {
.infoItem {
display: flex;
align-items: center;
gap: 8px;
}
}
.ordersCard {
.orderItem {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
}
.quickReplyBtn {
width: 100%;
text-align: left;
height: auto;
white-space: normal;
word-wrap: break-word;
}
}

123
src/pages/Chat/components/InfoPanel.tsx

@ -0,0 +1,123 @@
import React from 'react';
import { Card, Typography, Button, List, Divider } from 'antd';
import { UserOutlined, PhoneOutlined, ShoppingOutlined } from '@ant-design/icons';
import styles from './InfoPanel.less';
const { Title, Text } = Typography;
interface CustomerInfo {
name: string;
phone: string;
email: string;
orders: {
id: string;
date: string;
amount: number;
}[];
}
interface QuickReply {
id: string;
content: string;
}
interface InfoPanelProps {
currentChat: {
id: string;
name: string;
} | null;
}
const InfoPanel: React.FC<InfoPanelProps> = ({ currentChat }) => {
// 模拟客户信息
const customerInfo: CustomerInfo = {
name: '张三',
phone: '13800138000',
email: 'zhangsan@example.com',
orders: [
{
id: 'ORDER001',
date: '2024-02-15',
amount: 299,
},
{
id: 'ORDER002',
date: '2024-01-20',
amount: 599,
},
],
};
// 模拟快速回复模板
const quickReplies: QuickReply[] = [
{
id: '1',
content: '您好,很高兴为您服务!',
},
{
id: '2',
content: '请问还有什么可以帮您的吗?',
},
{
id: '3',
content: '感谢您的咨询,祝您生活愉快!',
},
];
return (
<div className={styles.infoPanel}>
<Card title="客户信息" bordered={false}>
<div className={styles.customerInfo}>
<div className={styles.infoItem}>
<UserOutlined /> <Text>{customerInfo.name}</Text>
</div>
<div className={styles.infoItem}>
<PhoneOutlined /> <Text>{customerInfo.phone}</Text>
</div>
<div className={styles.infoItem}>
<Text type="secondary">{customerInfo.email}</Text>
</div>
</div>
</Card>
<Card title="历史订单" bordered={false} className={styles.ordersCard}>
<List
dataSource={customerInfo.orders}
renderItem={(order) => (
<List.Item>
<div className={styles.orderItem}>
<div>
<Text strong>{order.id}</Text>
<br />
<Text type="secondary">{order.date}</Text>
</div>
<Text type="success">{order.amount}</Text>
</div>
</List.Item>
)}
/>
</Card>
<Card title="快速回复" bordered={false}>
<List
dataSource={quickReplies}
renderItem={(reply) => (
<List.Item>
<Button
type="text"
className={styles.quickReplyBtn}
onClick={() => {
// 实现快速回复功能
}}
>
{reply.content}
</Button>
</List.Item>
)}
/>
</Card>
</div>
);
};
export default InfoPanel;

14
src/pages/Chat/index.less

@ -0,0 +1,14 @@
.chatLayout {
height: 90vh;
.sider {
border-right: 1px solid #f0f0f0;
overflow: auto;
}
.content {
padding: 0;
display: flex;
flex-direction: column;
}
}

47
src/pages/Chat/index.tsx

@ -0,0 +1,47 @@
import React from 'react';
import { Layout } from 'antd';
import ContactList from './components/ContactList';
import ChatWindow from './components/ChatWindow';
import InfoPanel from './components/InfoPanel';
import styles from './index.less';
const { Sider, Content } = Layout;
// 模拟用户ID,实际应该从登录状态获取
const CURRENT_USER_ID = 'user_001';
const ChatPage: React.FC = () => {
const [currentChat, setCurrentChat] = React.useState<{
id: string;
name: string;
} | null>(null);
const handleContactSelect = (contact: { id: string; name: string }) => {
setCurrentChat(contact);
};
return (
<Layout className={styles.chatLayout}>
<Sider width={300} theme="light" className={styles.sider}>
<ContactList
currentUserId={CURRENT_USER_ID}
selectedContactId={currentChat?.id}
onSelectContact={handleContactSelect}
/>
</Sider>
<Content className={styles.content}>
<ChatWindow
userId={CURRENT_USER_ID}
currentChat={currentChat}
/>
</Content>
<Sider width={300} theme="light" className={styles.sider}>
<InfoPanel
currentChat={currentChat}
/>
</Sider>
</Layout>
);
};
export default ChatPage;

239
src/pages/Chat/wss/WebSocketClient.ts

@ -0,0 +1,239 @@
import { WS_CONFIG } from './config';
import {
WSMessageType,
WSMessage,
ChatMessage,
WSConnectionStatus,
WSEventHandlers,
} from './types';
class WebSocketClient {
private ws: WebSocket | null = null;
private reconnectAttempts = 0;
private heartbeatInterval: NodeJS.Timeout | null = null;
private heartbeatTimeout: NodeJS.Timeout | null = null;
private messageQueue: WSMessage[] = [];
private status: WSConnectionStatus = WSConnectionStatus.DISCONNECTED;
private handlers: WSEventHandlers = {};
constructor(private userId: string) {
this.connect();
}
// 设置事件处理器
setEventHandlers(handlers: WSEventHandlers) {
this.handlers = handlers;
}
// 建立连接
private connect() {
if (this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.updateStatus(WSConnectionStatus.CONNECTING);
try {
this.ws = new WebSocket(`${WS_CONFIG.SERVER_URL}?userId=${this.userId}`);
this.setupWebSocketListeners();
} catch (error) {
this.handleError(error as Error);
}
}
// 设置WebSocket监听器
private setupWebSocketListeners() {
if (!this.ws) return;
this.ws.onopen = () => {
this.onConnectionEstablished();
};
this.ws.onmessage = (event) => {
this.handleMessage(event);
};
this.ws.onclose = () => {
this.onConnectionClosed();
};
this.ws.onerror = (error) => {
this.handleError(error as Error);
};
}
// 连接建立后的处理
private onConnectionEstablished() {
this.updateStatus(WSConnectionStatus.CONNECTED);
this.reconnectAttempts = 0;
this.startHeartbeat();
this.processMessageQueue();
}
// 连接关闭后的处理
private onConnectionClosed() {
this.updateStatus(WSConnectionStatus.DISCONNECTED);
this.stopHeartbeat();
if (WS_CONFIG.RECONNECT.ENABLED &&
this.reconnectAttempts < WS_CONFIG.RECONNECT.MAX_ATTEMPTS) {
this.scheduleReconnect();
}
}
// 安排重连
private scheduleReconnect() {
const delay = WS_CONFIG.RECONNECT.DELAY *
Math.pow(WS_CONFIG.RECONNECT.BACKOFF_FACTOR, this.reconnectAttempts);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
// 开始心跳
private startHeartbeat() {
if (!WS_CONFIG.HEARTBEAT.ENABLED) return;
this.heartbeatInterval = setInterval(() => {
this.sendHeartbeat();
}, WS_CONFIG.HEARTBEAT.INTERVAL);
}
// 停止心跳
private stopHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = null;
}
}
// 发送心跳
private sendHeartbeat() {
this.send({
type: WSMessageType.CONNECT,
payload: { type: 'ping' },
timestamp: Date.now(),
});
this.heartbeatTimeout = setTimeout(() => {
this.handleError(new Error('Heartbeat timeout'));
this.reconnect();
}, WS_CONFIG.HEARTBEAT.TIMEOUT);
}
// 重新连接
private reconnect() {
if (this.ws) {
this.ws.close();
}
this.connect();
}
// 处理消息队列
private processMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
if (message) {
this.send(message);
}
}
}
// 处理接收到的消息
private handleMessage(event: MessageEvent) {
try {
const message: WSMessage = JSON.parse(event.data);
switch (message.type) {
case WSMessageType.MESSAGE:
this.handlers.onMessage?.(message.payload as ChatMessage);
break;
case WSMessageType.TYPING:
this.handlers.onTyping?.(message.payload.userId);
break;
case WSMessageType.READ:
this.handlers.onRead?.(message.payload.messageIds);
break;
// 处理心跳响应
case WSMessageType.CONNECT:
if (this.heartbeatTimeout) {
clearTimeout(this.heartbeatTimeout);
this.heartbeatTimeout = null;
}
break;
}
} catch (error) {
this.handleError(error as Error);
}
}
// 发送消息
send(message: WSMessage) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
this.messageQueue.push(message);
}
}
// 发送聊天消息
sendMessage(message: ChatMessage) {
this.send({
type: WSMessageType.MESSAGE,
payload: message,
timestamp: Date.now(),
});
}
// 发送正在输入状态
sendTyping() {
this.send({
type: WSMessageType.TYPING,
payload: { userId: this.userId },
timestamp: Date.now(),
});
}
// 发送消息已读状态
sendRead(messageIds: string[]) {
this.send({
type: WSMessageType.READ,
payload: { messageIds },
timestamp: Date.now(),
});
}
// 更新连接状态
private updateStatus(status: WSConnectionStatus) {
this.status = status;
this.handlers.onStatusChange?.(status);
}
// 处理错误
private handleError(error: Error) {
this.updateStatus(WSConnectionStatus.ERROR);
this.handlers.onError?.(error);
}
// 关闭连接
disconnect() {
this.stopHeartbeat();
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
// 获取当前连接状态
getStatus(): WSConnectionStatus {
return this.status;
}
}
export default WebSocketClient;

34
src/pages/Chat/wss/config.ts

@ -0,0 +1,34 @@
export const WS_CONFIG = {
// WebSocket服务器地址
SERVER_URL: process.env.WS_SERVER_URL || 'ws://localhost:8080',
// 重连配置
RECONNECT: {
// 是否启用自动重连
ENABLED: true,
// 最大重连次数
MAX_ATTEMPTS: 5,
// 重连延迟(毫秒)
DELAY: 3000,
// 重连延迟增长倍数
BACKOFF_FACTOR: 1.5,
},
// 心跳配置
HEARTBEAT: {
// 是否启用心跳
ENABLED: true,
// 心跳间隔(毫秒)
INTERVAL: 30000,
// 心跳超时时间(毫秒)
TIMEOUT: 5000,
},
// 消息配置
MESSAGE: {
// 消息重发最大次数
MAX_RETRY: 3,
// 消息重发延迟(毫秒)
RETRY_DELAY: 1000,
},
};

5
src/pages/Chat/wss/index.ts

@ -0,0 +1,5 @@
export { default as WebSocketClient } from './WebSocketClient';
export { default as useWebSocket } from './useWebSocket';
export * from './types';
export * from './utils';
export { WS_CONFIG } from './config';

42
src/pages/Chat/wss/types.ts

@ -0,0 +1,42 @@
// WebSocket消息类型
export enum WSMessageType {
CONNECT = 'CONNECT',
MESSAGE = 'MESSAGE',
READ = 'READ',
TYPING = 'TYPING',
ERROR = 'ERROR',
}
// WebSocket消息接口
export interface WSMessage {
type: WSMessageType;
payload: any;
timestamp: number;
}
// 聊天消息接口
export interface ChatMessage {
id: string;
content: string;
sender: string;
receiver: string;
timestamp: number;
status: 'sending' | 'sent' | 'delivered' | 'read';
}
// WebSocket连接状态
export enum WSConnectionStatus {
CONNECTING = 'CONNECTING',
CONNECTED = 'CONNECTED',
DISCONNECTED = 'DISCONNECTED',
ERROR = 'ERROR',
}
// WebSocket事件处理器接口
export interface WSEventHandlers {
onMessage?: (message: ChatMessage) => void;
onStatusChange?: (status: WSConnectionStatus) => void;
onError?: (error: Error) => void;
onTyping?: (userId: string) => void;
onRead?: (messageIds: string[]) => void;
}

89
src/pages/Chat/wss/useWebSocket.ts

@ -0,0 +1,89 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import WebSocketClient from './WebSocketClient';
import { ChatMessage, WSConnectionStatus } from './types';
export const useWebSocket = (userId: string) => {
const wsClient = useRef<WebSocketClient | null>(null);
const [status, setStatus] = useState<WSConnectionStatus>(WSConnectionStatus.DISCONNECTED);
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());
// 初始化WebSocket客户端
useEffect(() => {
wsClient.current = new WebSocketClient(userId);
wsClient.current.setEventHandlers({
onMessage: (message: ChatMessage) => {
setMessages(prev => [...prev, message]);
},
onStatusChange: (newStatus: WSConnectionStatus) => {
setStatus(newStatus);
},
onTyping: (typingUserId: string) => {
setTypingUsers(prev => new Set(prev).add(typingUserId));
// 3秒后清除输入状态
setTimeout(() => {
setTypingUsers(prev => {
const newSet = new Set(prev);
newSet.delete(typingUserId);
return newSet;
});
}, 3000);
},
onRead: (messageIds: string[]) => {
setMessages(prev =>
prev.map(msg =>
messageIds.includes(msg.id)
? { ...msg, status: 'read' }
: msg
)
);
},
onError: (error: Error) => {
console.error('WebSocket error:', error);
},
});
return () => {
wsClient.current?.disconnect();
};
}, [userId]);
// 发送消息
const sendMessage = useCallback((content: string, receiver: string) => {
if (!wsClient.current) return;
const message: ChatMessage = {
id: Date.now().toString(), // 临时ID,实际应该由服务器生成
content,
sender: userId,
receiver,
timestamp: Date.now(),
status: 'sending',
};
setMessages(prev => [...prev, message]);
wsClient.current.sendMessage(message);
}, [userId]);
// 发送正在输入状态
const sendTyping = useCallback(() => {
wsClient.current?.sendTyping();
}, []);
// 发送消息已读状态
const sendRead = useCallback((messageIds: string[]) => {
wsClient.current?.sendRead(messageIds);
}, []);
return {
status,
messages,
typingUsers: Array.from(typingUsers),
sendMessage,
sendTyping,
sendRead,
};
};
export default useWebSocket;

34
src/pages/Chat/wss/utils.ts

@ -0,0 +1,34 @@
import { WSMessage, WSMessageType } from './types';
// 创建WebSocket消息
export const createWSMessage = (type: WSMessageType, payload: any): WSMessage => ({
type,
payload,
timestamp: Date.now(),
});
// 解析WebSocket消息
export const parseWSMessage = (data: string): WSMessage | null => {
try {
return JSON.parse(data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
return null;
}
};
// 格式化时间戳
export const formatMessageTime = (timestamp: number): string => {
const date = new Date(timestamp);
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
// 检查消息是否过期
export const isMessageExpired = (timestamp: number, expirationTime: number = 5 * 60 * 1000): boolean => {
return Date.now() - timestamp > expirationTime;
};
// 生成唯一消息ID
export const generateMessageId = (): string => {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
};

3
src/pages/Home/index.less

@ -0,0 +1,3 @@
.container {
padding-top: 80px;
}

18
src/pages/Home/index.tsx

@ -0,0 +1,18 @@
import Guide from '@/components/Guide';
import { trim } from '@/utils/format';
import { PageContainer } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
import styles from './index.less';
const HomePage: React.FC = () => {
const { name } = useModel('global');
return (
<PageContainer ghost>
<div className={styles.container}>
<Guide name={trim(name)} />
</div>
</PageContainer>
);
};
export default HomePage;

26
src/pages/Table/components/CreateForm.tsx

@ -0,0 +1,26 @@
import { Modal } from 'antd';
import React, { PropsWithChildren } from 'react';
interface CreateFormProps {
modalVisible: boolean;
onCancel: () => void;
}
const CreateForm: React.FC<PropsWithChildren<CreateFormProps>> = (props) => {
const { modalVisible, onCancel } = props;
return (
<Modal
destroyOnClose
title="新建"
width={420}
open={modalVisible}
onCancel={() => onCancel()}
footer={null}
>
{props.children}
</Modal>
);
};
export default CreateForm;

138
src/pages/Table/components/UpdateForm.tsx

@ -0,0 +1,138 @@
import {
ProFormDateTimePicker,
ProFormRadio,
ProFormSelect,
ProFormText,
ProFormTextArea,
StepsForm,
} from '@ant-design/pro-components';
import { Modal } from 'antd';
import React from 'react';
export interface FormValueType extends Partial<API.UserInfo> {
target?: string;
template?: string;
type?: string;
time?: string;
frequency?: string;
}
export interface UpdateFormProps {
onCancel: (flag?: boolean, formVals?: FormValueType) => void;
onSubmit: (values: FormValueType) => Promise<void>;
updateModalVisible: boolean;
values: Partial<API.UserInfo>;
}
const UpdateForm: React.FC<UpdateFormProps> = (props) => (
<StepsForm
stepsProps={{
size: 'small',
}}
stepsFormRender={(dom, submitter) => {
return (
<Modal
width={640}
bodyStyle={{ padding: '32px 40px 48px' }}
destroyOnClose
title="规则配置"
open={props.updateModalVisible}
footer={submitter}
onCancel={() => props.onCancel()}
>
{dom}
</Modal>
);
}}
onFinish={props.onSubmit}
>
<StepsForm.StepForm
initialValues={{
name: props.values.name,
nickName: props.values.nickName,
}}
title="基本信息"
>
<ProFormText
width="md"
name="name"
label="规则名称"
rules={[{ required: true, message: '请输入规则名称!' }]}
/>
<ProFormTextArea
name="desc"
width="md"
label="规则描述"
placeholder="请输入至少五个字符"
rules={[
{ required: true, message: '请输入至少五个字符的规则描述!', min: 5 },
]}
/>
</StepsForm.StepForm>
<StepsForm.StepForm
initialValues={{
target: '0',
template: '0',
}}
title="配置规则属性"
>
<ProFormSelect
width="md"
name="target"
label="监控对象"
valueEnum={{
0: '表一',
1: '表二',
}}
/>
<ProFormSelect
width="md"
name="template"
label="规则模板"
valueEnum={{
0: '规则模板一',
1: '规则模板二',
}}
/>
<ProFormRadio.Group
name="type"
width="md"
label="规则类型"
options={[
{
value: '0',
label: '强',
},
{
value: '1',
label: '弱',
},
]}
/>
</StepsForm.StepForm>
<StepsForm.StepForm
initialValues={{
type: '1',
frequency: 'month',
}}
title="设定调度周期"
>
<ProFormDateTimePicker
name="time"
label="开始时间"
rules={[{ required: true, message: '请选择开始时间!' }]}
/>
<ProFormSelect
name="frequency"
label="监控对象"
width="xs"
valueEnum={{
month: '月',
week: '周',
}}
/>
</StepsForm.StepForm>
</StepsForm>
);
export default UpdateForm;

270
src/pages/Table/index.tsx

@ -0,0 +1,270 @@
import services from '@/services/demo';
import {
ActionType,
FooterToolbar,
PageContainer,
ProDescriptions,
ProDescriptionsItemProps,
ProTable,
} from '@ant-design/pro-components';
import { Button, Divider, Drawer, message } from 'antd';
import React, { useRef, useState } from 'react';
import CreateForm from './components/CreateForm';
import UpdateForm, { FormValueType } from './components/UpdateForm';
const { addUser, queryUserList, deleteUser, modifyUser } =
services.UserController;
/**
*
* @param fields
*/
const handleAdd = async (fields: API.UserInfo) => {
const hide = message.loading('正在添加');
try {
await addUser({ ...fields });
hide();
message.success('添加成功');
return true;
} catch (error) {
hide();
message.error('添加失败请重试!');
return false;
}
};
/**
*
* @param fields
*/
const handleUpdate = async (fields: FormValueType) => {
const hide = message.loading('正在配置');
try {
await modifyUser(
{
userId: fields.id || '',
},
{
name: fields.name || '',
nickName: fields.nickName || '',
email: fields.email || '',
},
);
hide();
message.success('配置成功');
return true;
} catch (error) {
hide();
message.error('配置失败请重试!');
return false;
}
};
/**
*
* @param selectedRows
*/
const handleRemove = async (selectedRows: API.UserInfo[]) => {
const hide = message.loading('正在删除');
if (!selectedRows) return true;
try {
await deleteUser({
userId: selectedRows.find((row) => row.id)?.id || '',
});
hide();
message.success('删除成功,即将刷新');
return true;
} catch (error) {
hide();
message.error('删除失败,请重试');
return false;
}
};
const TableList: React.FC<unknown> = () => {
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
const [updateModalVisible, handleUpdateModalVisible] =
useState<boolean>(false);
const [stepFormValues, setStepFormValues] = useState({});
const actionRef = useRef<ActionType>();
const [row, setRow] = useState<API.UserInfo>();
const [selectedRowsState, setSelectedRows] = useState<API.UserInfo[]>([]);
const columns: ProDescriptionsItemProps<API.UserInfo>[] = [
{
title: '名称',
dataIndex: 'name',
tip: '名称是唯一的 key',
formItemProps: {
rules: [
{
required: true,
message: '名称为必填项',
},
],
},
},
{
title: '昵称',
dataIndex: 'nickName',
valueType: 'text',
},
{
title: '性别',
dataIndex: 'gender',
hideInForm: true,
valueEnum: {
0: { text: '男', status: 'MALE' },
1: { text: '女', status: 'FEMALE' },
},
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) => (
<>
<a
onClick={() => {
handleUpdateModalVisible(true);
setStepFormValues(record);
}}
>
</a>
<Divider type="vertical" />
<a href=""></a>
</>
),
},
];
return (
<PageContainer
header={{
title: 'CRUD 示例',
}}
>
<ProTable<API.UserInfo>
headerTitle="查询表格"
actionRef={actionRef}
rowKey="id"
search={{
labelWidth: 120,
}}
toolBarRender={() => [
<Button
key="1"
type="primary"
onClick={() => handleModalVisible(true)}
>
</Button>,
]}
request={async (params, sorter, filter) => {
const { data, success } = await queryUserList({
...params,
// FIXME: remove @ts-ignore
// @ts-ignore
sorter,
filter,
});
return {
data: data?.list || [],
success,
};
}}
columns={columns}
rowSelection={{
onChange: (_, selectedRows) => setSelectedRows(selectedRows),
}}
/>
{selectedRowsState?.length > 0 && (
<FooterToolbar
extra={
<div>
{' '}
<a style={{ fontWeight: 600 }}>{selectedRowsState.length}</a>{' '}
&nbsp;&nbsp;
</div>
}
>
<Button
onClick={async () => {
await handleRemove(selectedRowsState);
setSelectedRows([]);
actionRef.current?.reloadAndRest?.();
}}
>
</Button>
<Button type="primary"></Button>
</FooterToolbar>
)}
<CreateForm
onCancel={() => handleModalVisible(false)}
modalVisible={createModalVisible}
>
<ProTable<API.UserInfo, API.UserInfo>
onSubmit={async (value) => {
const success = await handleAdd(value);
if (success) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
rowKey="id"
type="form"
columns={columns}
/>
</CreateForm>
{stepFormValues && Object.keys(stepFormValues).length ? (
<UpdateForm
onSubmit={async (value) => {
const success = await handleUpdate(value);
if (success) {
handleUpdateModalVisible(false);
setStepFormValues({});
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
onCancel={() => {
handleUpdateModalVisible(false);
setStepFormValues({});
}}
updateModalVisible={updateModalVisible}
values={stepFormValues}
/>
) : null}
<Drawer
width={600}
open={!!row}
onClose={() => {
setRow(undefined);
}}
closable={false}
>
{row?.name && (
<ProDescriptions<API.UserInfo>
column={2}
title={row?.name}
request={async () => ({
data: row || {},
})}
params={{
id: row?.name,
}}
columns={columns}
/>
)}
</Drawer>
</PageContainer>
);
};
export default TableList;

96
src/services/demo/UserController.ts

@ -0,0 +1,96 @@
/* eslint-disable */
// 该文件由 OneAPI 自动生成,请勿手动修改!
import { request } from '@umijs/max';
/** 此处后端没有提供注释 GET /api/v1/queryUserList */
export async function queryUserList(
params: {
// query
/** keyword */
keyword?: string;
/** current */
current?: number;
/** pageSize */
pageSize?: number;
},
options?: { [key: string]: any },
) {
return request<API.Result_PageInfo_UserInfo__>('/api/v1/queryUserList', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}
/** 此处后端没有提供注释 POST /api/v1/user */
export async function addUser(
body?: API.UserInfoVO,
options?: { [key: string]: any },
) {
return request<API.Result_UserInfo_>('/api/v1/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 GET /api/v1/user/${param0} */
export async function getUserDetail(
params: {
// path
/** userId */
userId?: string;
},
options?: { [key: string]: any },
) {
const { userId: param0 } = params;
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
method: 'GET',
params: { ...params },
...(options || {}),
});
}
/** 此处后端没有提供注释 PUT /api/v1/user/${param0} */
export async function modifyUser(
params: {
// path
/** userId */
userId?: string;
},
body?: API.UserInfoVO,
options?: { [key: string]: any },
) {
const { userId: param0 } = params;
return request<API.Result_UserInfo_>(`/api/v1/user/${param0}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
params: { ...params },
data: body,
...(options || {}),
});
}
/** 此处后端没有提供注释 DELETE /api/v1/user/${param0} */
export async function deleteUser(
params: {
// path
/** userId */
userId?: string;
},
options?: { [key: string]: any },
) {
const { userId: param0 } = params;
return request<API.Result_string_>(`/api/v1/user/${param0}`, {
method: 'DELETE',
params: { ...params },
...(options || {}),
});
}

7
src/services/demo/index.ts

@ -0,0 +1,7 @@
/* eslint-disable */
// 该文件由 OneAPI 自动生成,请勿手动修改!
import * as UserController from './UserController';
export default {
UserController,
};

68
src/services/demo/typings.d.ts

@ -0,0 +1,68 @@
/* eslint-disable */
// 该文件由 OneAPI 自动生成,请勿手动修改!
declare namespace API {
interface PageInfo {
/**
1 */
current?: number;
pageSize?: number;
total?: number;
list?: Array<Record<string, any>>;
}
interface PageInfo_UserInfo_ {
/**
1 */
current?: number;
pageSize?: number;
total?: number;
list?: Array<UserInfo>;
}
interface Result {
success?: boolean;
errorMessage?: string;
data?: Record<string, any>;
}
interface Result_PageInfo_UserInfo__ {
success?: boolean;
errorMessage?: string;
data?: PageInfo_UserInfo_;
}
interface Result_UserInfo_ {
success?: boolean;
errorMessage?: string;
data?: UserInfo;
}
interface Result_string_ {
success?: boolean;
errorMessage?: string;
data?: string;
}
type UserGenderEnum = 'MALE' | 'FEMALE';
interface UserInfo {
id?: string;
name?: string;
/** nick */
nickName?: string;
/** email */
email?: string;
gender?: UserGenderEnum;
}
interface UserInfoVO {
name?: string;
/** nick */
nickName?: string;
/** email */
email?: string;
}
type definitions_0 = null;
}

4
src/utils/format.ts

@ -0,0 +1,4 @@
// 示例方法,没有实际意义
export function trim(str: string) {
return str.trim();
}

3
tsconfig.json

@ -0,0 +1,3 @@
{
"extends": "./src/.umi/tsconfig.json"
}

1
typings.d.ts

@ -0,0 +1 @@
import '@umijs/max/typings';
Loading…
Cancel
Save