Преглед изворни кода

Add welcome apply menu item to menu configuration

- Introduced a new menu item for "欢迎语审核" with the corresponding path and permissions.
- Enhanced the menu structure to include this additional option for improved navigation.
0es пре 3 недеља
родитељ
комит
e0712daecb

+ 538 - 0
src/app/(dashboard)/operation/welcome-apply/page.tsx

@@ -0,0 +1,538 @@
+"use client";
+
+import {
+  CheckCircleOutlined,
+  CloseCircleOutlined,
+  EyeOutlined,
+  ReloadOutlined,
+  SearchOutlined,
+  UndoOutlined,
+} from "@ant-design/icons";
+import {
+  App,
+  Button,
+  DatePicker,
+  Descriptions,
+  Form,
+  Input,
+  Modal,
+  Select,
+  Space,
+  Table,
+  Tag,
+} from "antd";
+import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
+import type React from "react";
+import { useEffect, useState } from "react";
+import {
+  getPlaymateWelcomePage,
+  reviewPlaymateWelcome,
+} from "@/services/playmateWelcomeApply";
+import type {
+  PagerPlaymateWelcomeAdminDTO,
+  PlaymateWelcomeAdminDTO,
+  PlaymateWelcomeAdminQuery,
+} from "@/types/api/playmateWelcomeApply";
+import { formatTimestamp } from "@/utils/date";
+
+const PlaymateWelcomeApplyPage: React.FC = () => {
+  const { message } = App.useApp();
+  const [searchForm] = Form.useForm();
+  const [approvalForm] = Form.useForm();
+
+  const [loading, setLoading] = useState(false);
+  const [dataSource, setDataSource] = useState<PlaymateWelcomeAdminDTO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<PlaymateWelcomeAdminQuery>({
+    pageSize: 20,
+    pageIndex: 1,
+  });
+
+  const [detailModalVisible, setDetailModalVisible] = useState(false);
+  const [currentRecord, setCurrentRecord] =
+    useState<PlaymateWelcomeAdminDTO | null>(null);
+
+  const [approvalModalVisible, setApprovalModalVisible] = useState(false);
+  const [approvalLoading, setApprovalLoading] = useState(false);
+
+  const loadPageData = async () => {
+    setLoading(true);
+    try {
+      const response: PagerPlaymateWelcomeAdminDTO =
+        await getPlaymateWelcomePage(queryParams);
+      setDataSource(response.items || []);
+      setTotal(response.total || 0);
+    } catch (error) {
+      console.error("Failed to load playmate welcome apply list:", error);
+      message.error("加载欢迎语审核列表失败");
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // biome-ignore lint/correctness/useExhaustiveDependencies: queryParams drives reload
+  useEffect(() => {
+    loadPageData();
+  }, [queryParams]);
+
+  const renderStatusTag = (status?: number) => {
+    if (status === 0) return <Tag color="processing">待审核</Tag>;
+    if (status === 1) return <Tag color="success">审核通过</Tag>;
+    if (status === 2) return <Tag color="error">审核未通过</Tag>;
+    return <Tag>-</Tag>;
+  };
+
+  const renderTypeTag = (type?: number) => {
+    if (type === 0) return <Tag>文本</Tag>;
+    if (type === 1) return <Tag color="blue">语音</Tag>;
+    return <Tag>-</Tag>;
+  };
+
+  const handleSearch = () => {
+    const values = searchForm.getFieldsValue();
+    setQueryParams({
+      ...queryParams,
+      pageIndex: 1,
+      searchKeyword: values.searchKeyword?.trim() || undefined,
+      status:
+        values.status === "" ||
+        values.status === undefined ||
+        values.status === null
+          ? undefined
+          : Number(values.status),
+      type:
+        values.type === "" || values.type === undefined || values.type === null
+          ? undefined
+          : Number(values.type),
+      startDay: values.dateRange?.[0]?.format("YYYY-MM-DD") || undefined,
+      endDay: values.dateRange?.[1]?.format("YYYY-MM-DD") || undefined,
+    });
+  };
+
+  const handleReset = () => {
+    searchForm.resetFields();
+    setQueryParams({
+      pageSize: 20,
+      pageIndex: 1,
+    });
+  };
+
+  const handleRefresh = () => {
+    loadPageData();
+  };
+
+  const handleTableChange = (pagination: TablePaginationConfig) => {
+    setQueryParams({
+      ...queryParams,
+      pageIndex: pagination.current || 1,
+      pageSize: pagination.pageSize || 20,
+    });
+  };
+
+  const handleViewDetail = (record: PlaymateWelcomeAdminDTO) => {
+    setCurrentRecord(record);
+    setDetailModalVisible(true);
+  };
+
+  const handleOpenApproval = (
+    record: PlaymateWelcomeAdminDTO,
+    status: 1 | 2,
+  ) => {
+    setCurrentRecord(record);
+    approvalForm.resetFields();
+    approvalForm.setFieldsValue({
+      status,
+      reviewReason: "",
+    });
+    setApprovalModalVisible(true);
+  };
+
+  const handleCloseApprovalModal = () => {
+    setApprovalModalVisible(false);
+    approvalForm.resetFields();
+  };
+
+  const handleSubmitApproval = async () => {
+    if (!currentRecord?.id) {
+      message.error("缺少当前审核记录");
+      return;
+    }
+
+    try {
+      const values = await approvalForm.validateFields();
+      setApprovalLoading(true);
+
+      await reviewPlaymateWelcome({
+        id: currentRecord.id,
+        status: values.status,
+        reviewReason: values.reviewReason || undefined,
+      });
+
+      message.success("审核提交成功");
+      handleCloseApprovalModal();
+      loadPageData();
+    } catch (error) {
+      console.error("Failed to submit playmate welcome approval:", error);
+      if (error instanceof Error) {
+        message.error(`审核提交失败:${error.message}`);
+      } else {
+        message.error("审核提交失败");
+      }
+    } finally {
+      setApprovalLoading(false);
+    }
+  };
+
+  const columns: ColumnsType<PlaymateWelcomeAdminDTO> = [
+    {
+      title: "用户编号",
+      dataIndex: "userNo",
+      key: "userNo",
+      width: 140,
+      fixed: "left",
+      render: (v) => v || "-",
+    },
+    {
+      title: "昵称",
+      dataIndex: "nickname",
+      key: "nickname",
+      width: 140,
+      render: (v) => v || "-",
+    },
+    {
+      title: "类型",
+      dataIndex: "type",
+      key: "type",
+      width: 90,
+      render: (v?: number) => renderTypeTag(v),
+    },
+    {
+      title: "欢迎语内容",
+      key: "content",
+      width: 280,
+      render: (_: unknown, record: PlaymateWelcomeAdminDTO) => {
+        if (record.type === 1) {
+          return record.voiceUrl ? (
+            <audio controls src={record.voiceUrl} style={{ width: "100%" }}>
+              <track kind="captions" />
+              您的浏览器不支持音频播放
+            </audio>
+          ) : (
+            <span className="text-gray-400">-</span>
+          );
+        }
+        const text = record.textContent ?? "";
+        return text ? (
+          <span className="line-clamp-2" title={text}>
+            {text}
+          </span>
+        ) : (
+          "-"
+        );
+      },
+    },
+    {
+      title: "状态",
+      dataIndex: "status",
+      key: "status",
+      width: 110,
+      render: (v?: number) => renderStatusTag(v),
+    },
+    {
+      title: "审核原因",
+      dataIndex: "reviewReason",
+      key: "reviewReason",
+      width: 180,
+      ellipsis: true,
+      render: (v?: string) => v || "-",
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createdAt",
+      key: "createdAt",
+      width: 170,
+      render: (v?: number) => (v ? formatTimestamp(v) : "-"),
+    },
+    {
+      title: "操作",
+      key: "action",
+      width: 220,
+      fixed: "right",
+      render: (_, record) => (
+        <Space size="small">
+          <Button
+            type="primary"
+            size="small"
+            icon={<EyeOutlined />}
+            onClick={() => handleViewDetail(record)}
+          >
+            查看
+          </Button>
+          {record.status === 0 && (
+            <>
+              <Button
+                type="primary"
+                size="small"
+                icon={<CheckCircleOutlined />}
+                onClick={() => handleOpenApproval(record, 1)}
+              >
+                通过
+              </Button>
+              <Button
+                type="primary"
+                danger
+                size="small"
+                icon={<CloseCircleOutlined />}
+                onClick={() => handleOpenApproval(record, 2)}
+              >
+                拒绝
+              </Button>
+            </>
+          )}
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="p-6">
+      <div className="bg-white p-4 rounded-lg shadow mb-4">
+        <Form form={searchForm} layout="inline" className="gap-x-2 gap-y-4">
+          <Form.Item label="关键词" name="searchKeyword">
+            <Input
+              placeholder="用户编号/昵称"
+              allowClear
+              style={{ width: 180 }}
+            />
+          </Form.Item>
+          <Form.Item label="类型" name="type">
+            <Select placeholder="请选择类型" allowClear style={{ width: 120 }}>
+              <Select.Option value={0}>文本</Select.Option>
+              <Select.Option value={1}>语音</Select.Option>
+            </Select>
+          </Form.Item>
+          <Form.Item label="状态" name="status">
+            <Select placeholder="请选择状态" allowClear style={{ width: 140 }}>
+              <Select.Option value={0}>待审核</Select.Option>
+              <Select.Option value={1}>审核通过</Select.Option>
+              <Select.Option value={2}>审核未通过</Select.Option>
+            </Select>
+          </Form.Item>
+          <Form.Item label="创建时间" name="dateRange">
+            <DatePicker.RangePicker />
+          </Form.Item>
+          <Form.Item style={{ marginLeft: "auto" }}>
+            <Space>
+              <Button
+                type="primary"
+                icon={<SearchOutlined />}
+                onClick={handleSearch}
+              >
+                搜索
+              </Button>
+              <Button icon={<UndoOutlined />} onClick={handleReset}>
+                重置
+              </Button>
+              <Button
+                icon={<ReloadOutlined />}
+                onClick={handleRefresh}
+                loading={loading}
+              >
+                刷新
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </div>
+
+      <div className="bg-white p-4 rounded-lg shadow">
+        <Table
+          columns={columns}
+          dataSource={dataSource}
+          rowKey={(record) => record.id}
+          loading={loading}
+          pagination={{
+            current: queryParams.pageIndex,
+            pageSize: queryParams.pageSize,
+            total,
+            showSizeChanger: true,
+            showTotal: (t) => `共 ${t} 条`,
+            pageSizeOptions: ["10", "20", "30", "40", "50", "100", "200"],
+          }}
+          onChange={handleTableChange}
+          scroll={{ x: 1400 }}
+          size="small"
+          bordered
+        />
+      </div>
+
+      <Modal
+        title="欢迎语审核详情"
+        open={detailModalVisible}
+        onCancel={() => setDetailModalVisible(false)}
+        footer={[
+          <Button key="close" onClick={() => setDetailModalVisible(false)}>
+            关闭
+          </Button>,
+          ...(currentRecord?.status === 0
+            ? [
+                <Button
+                  key="approve"
+                  type="primary"
+                  icon={<CheckCircleOutlined />}
+                  onClick={() => {
+                    if (!currentRecord) return;
+                    setDetailModalVisible(false);
+                    handleOpenApproval(currentRecord, 1);
+                  }}
+                >
+                  通过
+                </Button>,
+                <Button
+                  key="reject"
+                  type="primary"
+                  danger
+                  icon={<CloseCircleOutlined />}
+                  onClick={() => {
+                    if (!currentRecord) return;
+                    setDetailModalVisible(false);
+                    handleOpenApproval(currentRecord, 2);
+                  }}
+                >
+                  拒绝
+                </Button>,
+              ]
+            : []),
+        ]}
+        width={700}
+        destroyOnHidden
+      >
+        {currentRecord && (
+          <div className="mt-4 space-y-4">
+            <Descriptions bordered column={1} size="small">
+              <Descriptions.Item label="状态">
+                {renderStatusTag(currentRecord.status)}
+              </Descriptions.Item>
+              <Descriptions.Item label="类型">
+                {renderTypeTag(currentRecord.type)}
+              </Descriptions.Item>
+              <Descriptions.Item label="用户编号">
+                {currentRecord.userNo || "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="昵称">
+                {currentRecord.nickname || "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="创建时间">
+                {currentRecord.createdAt
+                  ? formatTimestamp(currentRecord.createdAt)
+                  : "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="审核原因">
+                {currentRecord.reviewReason || "-"}
+              </Descriptions.Item>
+              {currentRecord.type === 0 && (
+                <Descriptions.Item label="文本内容">
+                  {currentRecord.textContent || "-"}
+                </Descriptions.Item>
+              )}
+              {currentRecord.type === 1 && (
+                <>
+                  <Descriptions.Item label="语音时长">
+                    {currentRecord.voiceDuration != null
+                      ? `${currentRecord.voiceDuration}秒`
+                      : "-"}
+                  </Descriptions.Item>
+                  <Descriptions.Item label="语音">
+                    {currentRecord.voiceUrl ? (
+                      <audio
+                        controls
+                        src={currentRecord.voiceUrl}
+                        style={{ width: "100%" }}
+                      >
+                        <track kind="captions" />
+                        您的浏览器不支持音频播放
+                      </audio>
+                    ) : (
+                      "-"
+                    )}
+                  </Descriptions.Item>
+                </>
+              )}
+            </Descriptions>
+          </div>
+        )}
+      </Modal>
+
+      <Modal
+        title="欢迎语审核"
+        open={approvalModalVisible}
+        onOk={handleSubmitApproval}
+        onCancel={handleCloseApprovalModal}
+        confirmLoading={approvalLoading}
+        width={520}
+        destroyOnHidden
+        maskClosable={false}
+      >
+        <Form
+          form={approvalForm}
+          layout="vertical"
+          className="mt-4"
+          initialValues={{ status: 1 }}
+        >
+          {currentRecord && (
+            <div className="mb-4 p-3 bg-gray-50 rounded text-sm text-gray-700">
+              <div>
+                <span className="font-medium">陪玩师:</span>
+                {currentRecord.nickname || "-"}({currentRecord.userNo || "-"})
+              </div>
+              <div>
+                <span className="font-medium">类型:</span>
+                {renderTypeTag(currentRecord.type)}
+              </div>
+            </div>
+          )}
+
+          <Form.Item
+            label="审核结果"
+            name="status"
+            rules={[{ required: true, message: "请选择审核结果" }]}
+          >
+            <Select placeholder="请选择审核结果">
+              <Select.Option value={1}>审核通过</Select.Option>
+              <Select.Option value={2}>审核未通过</Select.Option>
+            </Select>
+          </Form.Item>
+
+          <Form.Item shouldUpdate noStyle>
+            {({ getFieldValue }) => {
+              const status = getFieldValue("status") as number | undefined;
+              const required = status === 2;
+              return (
+                <Form.Item
+                  label="审核原因"
+                  name="reviewReason"
+                  rules={
+                    required
+                      ? [{ required: true, message: "请输入拒绝原因" }]
+                      : undefined
+                  }
+                >
+                  <Input.TextArea
+                    rows={4}
+                    placeholder={
+                      required ? "请输入拒绝原因" : "可选:请输入审核原因"
+                    }
+                    maxLength={500}
+                    showCount
+                  />
+                </Form.Item>
+              );
+            }}
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};
+
+export default PlaymateWelcomeApplyPage;

+ 7 - 0
src/config/menus.tsx

@@ -307,6 +307,13 @@ export const menuConfig: MenuItem[] = [
         icon: <AuditOutlined />,
         permission: "/content/trend-apply",
       },
+      {
+        key: "welcome-apply",
+        label: "欢迎语审核",
+        path: "/operation/welcome-apply",
+        icon: <AuditOutlined />,
+        permission: "/operation/welcome-apply",
+      },
     ],
   },
   {

+ 24 - 0
src/services/playmateWelcomeApply.ts

@@ -0,0 +1,24 @@
+/**
+ * Playmate welcome message apply (approval) API services
+ */
+
+import { post } from "@/lib/request";
+import type {
+  PagerPlaymateWelcomeAdminDTO,
+  PlaymateWelcomeAdminQuery,
+  PlaymateWelcomeReviewAdminDTO,
+} from "@/types/api/playmateWelcomeApply";
+
+/**
+ * Get playmate welcome apply page data
+ */
+export async function getPlaymateWelcomePage(query: PlaymateWelcomeAdminQuery) {
+  return post<PagerPlaymateWelcomeAdminDTO>("/playmate/welcome/page", query);
+}
+
+/**
+ * Approve or reject playmate welcome apply
+ */
+export async function reviewPlaymateWelcome(data: PlaymateWelcomeReviewAdminDTO) {
+  return post<unknown>("/playmate/welcome/review", data);
+}

+ 73 - 0
src/types/api/playmateWelcomeApply.ts

@@ -0,0 +1,73 @@
+/**
+ * Playmate welcome message apply (approval) related API type definitions.
+ * ObjectId is treated as string in frontend.
+ */
+
+import type { QuerySort } from "./common";
+
+// ============================
+// Query / Pager
+// ============================
+
+export interface PlaymateWelcomeAdminQuery {
+  pageSize?: number;
+  pageIndex?: number;
+  sort?: QuerySort;
+  searchKeyword?: string;
+  /**
+   * 0: pending, 1: approved, 2: rejected (CommonApplyStatusConsts)
+   */
+  status?: number;
+  /**
+   * 0: text, 1: voice (WelcomeMessageTypeConsts)
+   */
+  type?: number;
+  startDay?: string;
+  endDay?: string;
+  skip?: number;
+}
+
+export interface PlaymateWelcomeAdminDTO {
+  /**
+   * Record id (ObjectId as string)
+   */
+  id: string;
+  userNo?: string;
+  nickname?: string;
+  /**
+   * 0: text, 1: voice
+   */
+  type?: number;
+  textContent?: string;
+  voiceUrl?: string;
+  /**
+   * Voice duration in seconds
+   */
+  voiceDuration?: number;
+  /**
+   * 0: pending, 1: approved, 2: rejected
+   */
+  status?: number;
+  reviewReason?: string;
+  createdAt?: number;
+}
+
+export interface PagerPlaymateWelcomeAdminDTO {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: PlaymateWelcomeAdminDTO[];
+}
+
+// ============================
+// Review
+// ============================
+
+export interface PlaymateWelcomeReviewAdminDTO {
+  id: string;
+  /**
+   * 1: approved, 2: rejected
+   */
+  status: number;
+  reviewReason?: string;
+}