|
|
@@ -3,11 +3,13 @@
|
|
|
import {
|
|
|
CheckCircleOutlined,
|
|
|
CloseCircleOutlined,
|
|
|
+ EllipsisOutlined,
|
|
|
ReloadOutlined,
|
|
|
SearchOutlined,
|
|
|
} from "@ant-design/icons";
|
|
|
import {
|
|
|
App,
|
|
|
+ AutoComplete,
|
|
|
Button,
|
|
|
Card,
|
|
|
Col,
|
|
|
@@ -15,8 +17,10 @@ import {
|
|
|
Empty,
|
|
|
Form,
|
|
|
Input,
|
|
|
+ Popover,
|
|
|
Row,
|
|
|
Space,
|
|
|
+ Spin,
|
|
|
Table,
|
|
|
Tag,
|
|
|
Typography,
|
|
|
@@ -27,6 +31,7 @@ import type React from "react";
|
|
|
import { useMemo, useRef, useState } from "react";
|
|
|
import UserBrief from "@/components/UserBrief";
|
|
|
import { getChatHistory, getUserSessions } from "@/services/imTencent";
|
|
|
+import { getUserPage } from "@/services/user";
|
|
|
import type { ImChatInfoVo, UserChatSessionItemVo } from "@/types/api";
|
|
|
import dayjs from "@/utils/dayjs";
|
|
|
|
|
|
@@ -63,6 +68,14 @@ const ImRecordPage: React.FC = () => {
|
|
|
|
|
|
const [currentUserNo, setCurrentUserNo] = useState<string>("");
|
|
|
|
|
|
+ // UserNo search options (remote)
|
|
|
+ const [userSearchLoading, setUserSearchLoading] = useState(false);
|
|
|
+ const [userOptions, setUserOptions] = useState<
|
|
|
+ Array<{ value: string; label: React.ReactNode }>
|
|
|
+ >([]);
|
|
|
+ const userSearchReqId = useRef(0);
|
|
|
+ const userSearchTimer = useRef<number | null>(null);
|
|
|
+
|
|
|
// Step 1: sessions
|
|
|
const [sessionsLoading, setSessionsLoading] = useState(false);
|
|
|
const [sessionsPage, setSessionsPage] = useState(1);
|
|
|
@@ -147,6 +160,55 @@ const ImRecordPage: React.FC = () => {
|
|
|
await loadSessions(1, userNo);
|
|
|
};
|
|
|
|
|
|
+ const searchUsersByUserNo = async (keyword: string) => {
|
|
|
+ const q = keyword.trim();
|
|
|
+ if (!q) {
|
|
|
+ setUserOptions([]);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const reqId = ++userSearchReqId.current;
|
|
|
+ setUserSearchLoading(true);
|
|
|
+ try {
|
|
|
+ const res = await getUserPage({
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ userNo: q,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (reqId !== userSearchReqId.current) return;
|
|
|
+
|
|
|
+ setUserOptions(
|
|
|
+ (res.items || []).map((u) => ({
|
|
|
+ value: u.userNo,
|
|
|
+ label: (
|
|
|
+ <UserBrief
|
|
|
+ avatar={u.avatar}
|
|
|
+ nickname={u.nickname}
|
|
|
+ userNo={u.userNo}
|
|
|
+ size={28}
|
|
|
+ preview={false}
|
|
|
+ />
|
|
|
+ ),
|
|
|
+ })),
|
|
|
+ );
|
|
|
+ } catch (e) {
|
|
|
+ console.error("Failed to search users:", e);
|
|
|
+ setUserOptions([]);
|
|
|
+ } finally {
|
|
|
+ if (reqId === userSearchReqId.current) setUserSearchLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUserNoSearch = (text: string) => {
|
|
|
+ if (userSearchTimer.current) {
|
|
|
+ window.clearTimeout(userSearchTimer.current);
|
|
|
+ }
|
|
|
+ userSearchTimer.current = window.setTimeout(() => {
|
|
|
+ void searchUsersByUserNo(text);
|
|
|
+ }, 300);
|
|
|
+ };
|
|
|
+
|
|
|
const handleRefreshSessions = async () => {
|
|
|
if (!currentUserNo) return;
|
|
|
await loadSessions(sessionsPage);
|
|
|
@@ -284,15 +346,20 @@ const ImRecordPage: React.FC = () => {
|
|
|
title: "用户",
|
|
|
dataIndex: "userNo",
|
|
|
width: 240,
|
|
|
- render: (_: string, record) => (
|
|
|
- <UserBrief
|
|
|
- avatar={record.avatar}
|
|
|
- nickname={record.nickname}
|
|
|
- userNo={record.userNo}
|
|
|
- size={28}
|
|
|
- preview
|
|
|
- />
|
|
|
- ),
|
|
|
+ render: (_: string, record) =>
|
|
|
+ (() => {
|
|
|
+ const userNoNum = Number(record.userNo);
|
|
|
+ const isSystem = Number.isFinite(userNoNum) && userNoNum <= 10000;
|
|
|
+ return (
|
|
|
+ <UserBrief
|
|
|
+ avatar={isSystem ? undefined : record.avatar}
|
|
|
+ nickname={isSystem ? "系统消息" : record.nickname}
|
|
|
+ userNo={record.userNo}
|
|
|
+ size={28}
|
|
|
+ preview
|
|
|
+ />
|
|
|
+ );
|
|
|
+ })(),
|
|
|
},
|
|
|
{
|
|
|
title: "会话时间",
|
|
|
@@ -351,7 +418,21 @@ const ImRecordPage: React.FC = () => {
|
|
|
label="用户编号"
|
|
|
rules={[{ required: true, message: "请输入用户编号" }]}
|
|
|
>
|
|
|
- <Input placeholder="请输入 userNo" style={{ width: 260 }} />
|
|
|
+ <AutoComplete
|
|
|
+ style={{ width: 320 }}
|
|
|
+ placeholder="输入 userNo 搜索 / 可直接填写"
|
|
|
+ options={userOptions}
|
|
|
+ onSearch={handleUserNoSearch}
|
|
|
+ allowClear
|
|
|
+ notFoundContent={
|
|
|
+ userSearchLoading ? <Spin size="small" /> : <Empty />
|
|
|
+ }
|
|
|
+ onSelect={(value) => {
|
|
|
+ searchForm.setFieldsValue({ userNo: value });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Input />
|
|
|
+ </AutoComplete>
|
|
|
</Form.Item>
|
|
|
<Form.Item>
|
|
|
<Space>
|
|
|
@@ -458,27 +539,40 @@ const ImRecordPage: React.FC = () => {
|
|
|
<Form.Item label="To">
|
|
|
<Text>{selectedSession?.userNo || "-"}</Text>
|
|
|
</Form.Item>
|
|
|
- <Form.Item
|
|
|
- name="range"
|
|
|
- label="时间段"
|
|
|
- rules={[{ required: true, message: "请选择时间段" }]}
|
|
|
- >
|
|
|
- <RangePicker
|
|
|
- showTime
|
|
|
- style={{ width: 360 }}
|
|
|
- disabled={!selectedSession}
|
|
|
- />
|
|
|
- </Form.Item>
|
|
|
- <Form.Item>
|
|
|
- <Button
|
|
|
- type="primary"
|
|
|
- onClick={handleQueryHistory}
|
|
|
- disabled={!selectedSession}
|
|
|
- loading={historyLoading}
|
|
|
+ <Space.Compact style={{ marginLeft: "auto" }}>
|
|
|
+ <Form.Item noStyle>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ onClick={handleQueryHistory}
|
|
|
+ disabled={!selectedSession}
|
|
|
+ loading={historyLoading}
|
|
|
+ >
|
|
|
+ 刷新记录
|
|
|
+ </Button>
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="range"
|
|
|
+ rules={[{ required: true, message: "请选择时间段" }]}
|
|
|
+ noStyle
|
|
|
>
|
|
|
- 拉取聊天记录
|
|
|
- </Button>
|
|
|
- </Form.Item>
|
|
|
+ <Popover
|
|
|
+ trigger="click"
|
|
|
+ placement="bottomRight"
|
|
|
+ content={
|
|
|
+ <RangePicker
|
|
|
+ showTime
|
|
|
+ style={{ width: 360 }}
|
|
|
+ disabled={!selectedSession}
|
|
|
+ />
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ icon={<EllipsisOutlined />}
|
|
|
+ disabled={!selectedSession}
|
|
|
+ />
|
|
|
+ </Popover>
|
|
|
+ </Form.Item>
|
|
|
+ </Space.Compact>
|
|
|
</Form>
|
|
|
</Card>
|
|
|
|