Bläddra i källkod

Enhance ImRecordPage with user search functionality and UI improvements

- Added user search capability using AutoComplete for userNo input, improving user experience in finding users.
- Implemented loading state and debounce logic for user search requests to optimize performance.
- Updated UserBrief component to display a default avatar when no user image is available, enhancing visual consistency.
- Refined rendering logic for user information in the sessions table to differentiate system messages from regular users.
0es 1 månad sedan
förälder
incheckning
0209ff05cd
2 ändrade filer med 138 tillägg och 39 borttagningar
  1. 124 30
      src/app/(dashboard)/user/im-record/page.tsx
  2. 14 9
      src/components/UserBrief.tsx

+ 124 - 30
src/app/(dashboard)/user/im-record/page.tsx

@@ -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>
 

+ 14 - 9
src/components/UserBrief.tsx

@@ -1,4 +1,5 @@
-import { Image, Space, Typography } from "antd";
+import { UserOutlined } from "@ant-design/icons";
+import { Avatar, Image, Space, Typography } from "antd";
 import type React from "react";
 
 const { Text } = Typography;
@@ -31,14 +32,18 @@ const UserBrief: React.FC<UserBriefProps> = ({
 }) => {
   return (
     <Space>
-      <Image
-        width={size}
-        height={size}
-        src={avatar || undefined}
-        fallback=""
-        style={{ borderRadius: 9999, objectFit: "cover" }}
-        preview={preview && Boolean(avatar)}
-      />
+      {avatar ? (
+        <Image
+          width={size}
+          height={size}
+          src={avatar || undefined}
+          fallback=""
+          style={{ borderRadius: 9999, objectFit: "cover" }}
+          preview={preview && Boolean(avatar)}
+        />
+      ) : (
+        <Avatar size={size} icon={<UserOutlined />} />
+      )}
       <div>
         <div>
           <Text strong>{nickname || "-"}</Text>