Bladeren bron

Update menu configuration to include content management section and adjust sort orders

- Added a new "内容管理" (Content Management) section with a "动态管理" (Trend Management) child item.
- Introduced a "动态审核" (Trend Review) item under the user management section.
- Adjusted sort orders for existing menu items to accommodate the new entries, ensuring proper display order.
0es 1 maand geleden
bovenliggende
commit
f9e1a4384c
4 gewijzigde bestanden met toevoegingen van 844 en 7 verwijderingen
  1. 693 0
      src/app/(dashboard)/content/trend-apply/page.tsx
  2. 34 7
      src/config/menus.tsx
  3. 24 0
      src/services/trendApply.ts
  4. 93 0
      src/types/api/trendApply.ts

+ 693 - 0
src/app/(dashboard)/content/trend-apply/page.tsx

@@ -0,0 +1,693 @@
+"use client";
+
+import {
+  CheckCircleOutlined,
+  CloseCircleOutlined,
+  EyeOutlined,
+  PlayCircleOutlined,
+  ReloadOutlined,
+  SearchOutlined,
+  UndoOutlined,
+} from "@ant-design/icons";
+import {
+  App,
+  Button,
+  DatePicker,
+  Descriptions,
+  Form,
+  Image,
+  Input,
+  Modal,
+  Select,
+  Space,
+  Table,
+  Tag,
+  Tooltip,
+} from "antd";
+import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
+import type React from "react";
+import { useEffect, useState } from "react";
+import UserBrief from "@/components/UserBrief";
+import { getTrendAdminPage, reviewTrend } from "@/services/trendApply";
+import type {
+  PagerTrendAdminDTO,
+  TrendAdminDTO,
+  TrendAdminQuery,
+} from "@/types/api/trendApply";
+import { formatTimestamp } from "@/utils/date";
+
+const TREND_TYPE_MAP: Record<number, string> = {
+  0: "纯文本",
+  1: "图片",
+  2: "视频",
+  3: "图文混合",
+  4: "视频文本混合",
+};
+
+const GENDER_MAP: Record<number, string> = {
+  0: "未知",
+  1: "男",
+  2: "女",
+};
+
+const TrendApplyPage: React.FC = () => {
+  const { message } = App.useApp();
+  const [searchForm] = Form.useForm();
+  const [reviewForm] = Form.useForm();
+
+  const [loading, setLoading] = useState(false);
+  const [dataSource, setDataSource] = useState<TrendAdminDTO[]>([]);
+  const [total, setTotal] = useState(0);
+  const [queryParams, setQueryParams] = useState<TrendAdminQuery>({
+    pageSize: 20,
+    pageIndex: 1,
+  });
+
+  const [detailModalVisible, setDetailModalVisible] = useState(false);
+  const [currentRecord, setCurrentRecord] = useState<TrendAdminDTO | null>(
+    null,
+  );
+
+  const [reviewModalVisible, setReviewModalVisible] = useState(false);
+  const [reviewLoading, setReviewLoading] = useState(false);
+
+  const [videoPreviewUrl, setVideoPreviewUrl] = useState<string | null>(null);
+
+  const loadPageData = async () => {
+    setLoading(true);
+    try {
+      const response: PagerTrendAdminDTO = await getTrendAdminPage(queryParams);
+      setDataSource(response.items || []);
+      setTotal(response.total || 0);
+    } catch (error) {
+      console.error("Failed to load trend 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 renderTrendType = (type?: number) => {
+    if (type === undefined || type === null) return "-";
+    return TREND_TYPE_MAP[type] ?? String(type);
+  };
+
+  const handleSearch = () => {
+    const values = searchForm.getFieldsValue();
+    const dateRange = values.dateRange;
+    setQueryParams({
+      ...queryParams,
+      pageIndex: 1,
+      searchKeyword: values.searchKeyword || 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),
+      gender:
+        values.gender === "" ||
+        values.gender === undefined ||
+        values.gender === null
+          ? undefined
+          : Number(values.gender),
+      startDay: dateRange?.[0] ? dateRange[0].format("YYYY-MM-DD") : undefined,
+      endDay: dateRange?.[1] ? 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: TrendAdminDTO) => {
+    setCurrentRecord(record);
+    setDetailModalVisible(true);
+  };
+
+  const handleOpenReview = (record: TrendAdminDTO, status: 1 | 2) => {
+    setCurrentRecord(record);
+    reviewForm.resetFields();
+    reviewForm.setFieldsValue({ status, reviewReason: "" });
+    setReviewModalVisible(true);
+  };
+
+  const handleCloseReviewModal = () => {
+    setReviewModalVisible(false);
+    reviewForm.resetFields();
+  };
+
+  const handleSubmitReview = async () => {
+    if (!currentRecord?.id) {
+      message.error("缺少当前审核记录");
+      return;
+    }
+
+    try {
+      const values = await reviewForm.validateFields();
+      setReviewLoading(true);
+
+      await reviewTrend({
+        id: currentRecord.id,
+        status: values.status,
+        reviewReason: values.reviewReason || undefined,
+      });
+
+      message.success("审核提交成功");
+      handleCloseReviewModal();
+      loadPageData();
+    } catch (error) {
+      console.error("Failed to submit trend review:", error);
+      if (error instanceof Error) {
+        message.error(`审核提交失败:${error.message}`);
+      } else {
+        message.error("审核提交失败");
+      }
+    } finally {
+      setReviewLoading(false);
+    }
+  };
+
+  const columns: ColumnsType<TrendAdminDTO> = [
+    {
+      title: "用户",
+      key: "user",
+      width: 180,
+      fixed: "left",
+      render: (_: unknown, record: TrendAdminDTO) => (
+        <UserBrief
+          avatar={record.avatar}
+          nickname={record.nickname}
+          userNo={record.userNo}
+          size={40}
+        />
+      ),
+    },
+    {
+      title: "性别",
+      dataIndex: "gender",
+      key: "gender",
+      width: 80,
+      render: (v?: number) =>
+        v !== undefined && v !== null ? (GENDER_MAP[v] ?? "-") : "-",
+    },
+    {
+      title: "动态类型",
+      dataIndex: "type",
+      key: "type",
+      width: 120,
+      render: (v?: number) => renderTrendType(v),
+    },
+    {
+      title: "文本内容",
+      dataIndex: "textContent",
+      key: "textContent",
+      width: 220,
+      ellipsis: true,
+      render: (v?: string) =>
+        v ? (
+          <Tooltip title={v}>
+            <span>{v}</span>
+          </Tooltip>
+        ) : (
+          "-"
+        ),
+    },
+    {
+      title: "包含媒体",
+      dataIndex: "medias",
+      key: "medias",
+      width: 90,
+      render: (v?: unknown[]) => {
+        if (Array.isArray(v) && v.length > 0) {
+          return <Tag color="success">是</Tag>;
+        }
+        return <Tag>否</Tag>;
+      },
+    },
+    {
+      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 ? (
+          <Tooltip title={v}>
+            <span>{v}</span>
+          </Tooltip>
+        ) : (
+          "-"
+        ),
+    },
+    {
+      title: "审核时间",
+      dataIndex: "reviewTime",
+      key: "reviewTime",
+      width: 170,
+      render: (v?: number) => (v ? formatTimestamp(v) : "-"),
+    },
+    {
+      title: "创建时间",
+      dataIndex: "createdAt",
+      key: "createdAt",
+      width: 170,
+      render: (v?: number) => (v ? formatTimestamp(v) : "-"),
+    },
+    {
+      title: "操作",
+      key: "action",
+      width: 240,
+      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={() => handleOpenReview(record, 1)}
+              >
+                通过
+              </Button>
+              <Button
+                type="primary"
+                danger
+                size="small"
+                icon={<CloseCircleOutlined />}
+                onClick={() => handleOpenReview(record, 2)}
+              >
+                拒绝
+              </Button>
+            </>
+          )}
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div className="p-6">
+      {/* Search form */}
+      <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="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="type">
+            <Select placeholder="请选择类型" allowClear style={{ width: 150 }}>
+              <Select.Option value={0}>纯文本</Select.Option>
+              <Select.Option value={1}>图片</Select.Option>
+              <Select.Option value={2}>视频</Select.Option>
+              <Select.Option value={3}>图文混合</Select.Option>
+              <Select.Option value={4}>视频文本混合</Select.Option>
+            </Select>
+          </Form.Item>
+          <Form.Item label="性别" name="gender">
+            <Select placeholder="请选择性别" allowClear style={{ width: 110 }}>
+              <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 style={{ width: 240 }} />
+          </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>
+
+      {/* Table */}
+      <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: 1900 }}
+          size="small"
+          bordered
+        />
+      </div>
+
+      {/* Detail modal */}
+      <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);
+                    handleOpenReview(currentRecord, 1);
+                  }}
+                >
+                  通过
+                </Button>,
+                <Button
+                  key="reject"
+                  type="primary"
+                  danger
+                  icon={<CloseCircleOutlined />}
+                  onClick={() => {
+                    if (!currentRecord) return;
+                    setDetailModalVisible(false);
+                    handleOpenReview(currentRecord, 2);
+                  }}
+                >
+                  拒绝
+                </Button>,
+              ]
+            : []),
+        ]}
+        width={1000}
+        destroyOnHidden
+      >
+        {currentRecord && (
+          <div className="mt-4 space-y-4">
+            <Descriptions bordered column={2} size="small">
+              <Descriptions.Item label="状态" span={2}>
+                {renderStatusTag(currentRecord.status)}
+              </Descriptions.Item>
+              <Descriptions.Item label="用户编号">
+                {currentRecord.userNo || "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="昵称">
+                {currentRecord.nickname || "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="头像">
+                {currentRecord.avatar ? (
+                  <Image
+                    src={currentRecord.avatar}
+                    alt="avatar"
+                    width={60}
+                    height={60}
+                    style={{ borderRadius: "50%", objectFit: "cover" }}
+                    preview
+                  />
+                ) : (
+                  "-"
+                )}
+              </Descriptions.Item>
+              <Descriptions.Item label="性别">
+                {currentRecord.gender !== undefined
+                  ? (GENDER_MAP[currentRecord.gender] ?? "-")
+                  : "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="动态类型" span={2}>
+                {renderTrendType(currentRecord.type)}
+              </Descriptions.Item>
+              <Descriptions.Item label="文本内容" span={2}>
+                {currentRecord.textContent || "-"}
+              </Descriptions.Item>
+              {currentRecord.medias && currentRecord.medias.length > 0 && (
+                <Descriptions.Item label="媒体内容" span={2}>
+                  <Image.PreviewGroup>
+                    <Space wrap>
+                      {currentRecord.medias.map((media, idx) => (
+                        // biome-ignore lint/suspicious/noArrayIndexKey: stable index for media list
+                        <div key={idx} className="space-y-1">
+                          <div className="text-xs text-gray-500">
+                            {media.type === 1 ? "视频" : "图片"}
+                          </div>
+                          {media.type === 1 ? (
+                            // biome-ignore lint/a11y/useKeyWithClickEvents: video thumbnail is a visual-only interaction
+                            // biome-ignore lint/a11y/noStaticElementInteractions: video thumbnail preview
+                            <div
+                              className="relative cursor-pointer"
+                              style={{ width: 160, height: 110 }}
+                              onClick={() =>
+                                media.url && setVideoPreviewUrl(media.url)
+                              }
+                            >
+                              {media.videoCover ? (
+                                // biome-ignore lint/performance/noImgElement: antd Image breaks preview group context here
+                                <img
+                                  src={media.videoCover}
+                                  alt="video cover"
+                                  style={{
+                                    width: 160,
+                                    height: 110,
+                                    objectFit: "cover",
+                                    borderRadius: 8,
+                                    display: "block",
+                                  }}
+                                />
+                              ) : (
+                                <div
+                                  style={{
+                                    width: 160,
+                                    height: 110,
+                                    background: "#1f1f1f",
+                                    borderRadius: 8,
+                                  }}
+                                />
+                              )}
+                              <div
+                                style={{
+                                  position: "absolute",
+                                  inset: 0,
+                                  borderRadius: 8,
+                                  background: "rgba(0,0,0,0.35)",
+                                  display: "flex",
+                                  alignItems: "center",
+                                  justifyContent: "center",
+                                }}
+                              >
+                                <PlayCircleOutlined
+                                  style={{ fontSize: 36, color: "#fff" }}
+                                />
+                              </div>
+                            </div>
+                          ) : (
+                            <Image
+                              src={media.url}
+                              alt={`media-${idx}`}
+                              width={160}
+                              height={110}
+                              style={{ objectFit: "cover", borderRadius: 8 }}
+                            />
+                          )}
+                        </div>
+                      ))}
+                    </Space>
+                  </Image.PreviewGroup>
+                </Descriptions.Item>
+              )}
+              <Descriptions.Item label="审核备注">
+                {currentRecord.reviewReason || "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="审核时间">
+                {currentRecord.reviewTime
+                  ? formatTimestamp(currentRecord.reviewTime)
+                  : "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="创建时间">
+                {currentRecord.createdAt
+                  ? formatTimestamp(currentRecord.createdAt)
+                  : "-"}
+              </Descriptions.Item>
+              <Descriptions.Item label="更新时间">
+                {currentRecord.updatedAt
+                  ? formatTimestamp(currentRecord.updatedAt)
+                  : "-"}
+              </Descriptions.Item>
+            </Descriptions>
+          </div>
+        )}
+      </Modal>
+
+      {/* Review modal */}
+      <Modal
+        title="动态审核"
+        open={reviewModalVisible}
+        onOk={handleSubmitReview}
+        onCancel={handleCloseReviewModal}
+        confirmLoading={reviewLoading}
+        width={600}
+        destroyOnHidden
+        maskClosable={false}
+      >
+        <Form
+          form={reviewForm}
+          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>
+                {renderTrendType(currentRecord.type)}
+              </div>
+              {currentRecord.textContent && (
+                <div className="mt-1 line-clamp-2 text-gray-500">
+                  {currentRecord.textContent}
+                </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>
+
+      {/* Video preview modal */}
+      <Modal
+        title="视频预览"
+        open={!!videoPreviewUrl}
+        onCancel={() => setVideoPreviewUrl(null)}
+        footer={null}
+        width={720}
+        destroyOnHidden
+        centered
+      >
+        {videoPreviewUrl && (
+          // biome-ignore lint/a11y/useMediaCaption: admin review tool, captions not applicable
+          <video
+            src={videoPreviewUrl}
+            controls
+            autoPlay
+            style={{ width: "100%", maxHeight: 480, borderRadius: 8 }}
+          />
+        )}
+      </Modal>
+    </div>
+  );
+};
+
+export default TrendApplyPage;

+ 34 - 7
src/config/menus.tsx

@@ -6,6 +6,7 @@ import {
   ControlOutlined,
   DollarOutlined,
   FileOutlined,
+  FileTextOutlined,
   GlobalOutlined,
   HomeOutlined,
   KeyOutlined,
@@ -15,6 +16,7 @@ import {
   SettingOutlined,
   ShoppingOutlined,
   TeamOutlined,
+  ThunderboltOutlined,
   TransactionOutlined,
   UnorderedListOutlined,
   UserOutlined,
@@ -135,12 +137,30 @@ export const menuConfig: MenuItem[] = [
       },
     ],
   },
+  {
+    key: "content",
+    label: "内容管理",
+    path: "/content",
+    icon: <FileTextOutlined />,
+    sortOrder: 5,
+    permission: "/content",
+    children: [
+      {
+        key: "trend-manager",
+        label: "动态管理",
+        path: "/content/trend-manager",
+        icon: <ThunderboltOutlined />,
+        sortOrder: 1,
+        permission: "/content/trend-manager",
+      },
+    ],
+  },
   {
     key: "order",
     label: "订单管理",
     path: "/order",
     icon: <ShoppingOutlined />,
-    sortOrder: 5,
+    sortOrder: 6,
     permission: "/order",
     children: [
       {
@@ -166,7 +186,7 @@ export const menuConfig: MenuItem[] = [
     label: "资金管理",
     path: "/wallet",
     icon: <WalletOutlined />,
-    sortOrder: 6,
+    sortOrder: 7,
     permission: "/wallet",
     children: [
       {
@@ -218,7 +238,7 @@ export const menuConfig: MenuItem[] = [
     label: "运营管理",
     path: "/operation",
     icon: <ControlOutlined />,
-    sortOrder: 7,
+    sortOrder: 8,
     permission: "/operation",
     children: [
       {
@@ -242,7 +262,7 @@ export const menuConfig: MenuItem[] = [
     label: "审核管理",
     path: "/audit",
     icon: <AuditOutlined />,
-    sortOrder: 8,
+    sortOrder: 9,
     permission: "/audit",
     children: [
       {
@@ -280,6 +300,13 @@ export const menuConfig: MenuItem[] = [
         icon: <AuditOutlined />,
         permission: "/user/withdraw-apply",
       },
+      {
+        key: "trend-apply",
+        label: "动态审核",
+        path: "/content/trend-apply",
+        icon: <AuditOutlined />,
+        permission: "/content/trend-apply",
+      },
     ],
   },
   {
@@ -287,7 +314,7 @@ export const menuConfig: MenuItem[] = [
     label: "基础配置",
     path: "/config",
     icon: <ControlOutlined />,
-    sortOrder: 9,
+    sortOrder: 10,
     permission: "/config",
     children: [
       {
@@ -346,7 +373,7 @@ export const menuConfig: MenuItem[] = [
     label: "统计分析",
     path: "/statistics",
     icon: <BarChartOutlined />,
-    sortOrder: 10,
+    sortOrder: 11,
     permission: "/statistics",
   },
   {
@@ -354,7 +381,7 @@ export const menuConfig: MenuItem[] = [
     label: "系统管理",
     path: "/system",
     icon: <SettingOutlined />,
-    sortOrder: 10,
+    sortOrder: 12,
     permission: "/system",
     children: [
       {

+ 24 - 0
src/services/trendApply.ts

@@ -0,0 +1,24 @@
+/**
+ * Trend (dynamic) admin review API services
+ */
+
+import { post } from "@/lib/request";
+import type {
+  PagerTrendAdminDTO,
+  TrendAdminQuery,
+  TrendReviewAdminDTO,
+} from "@/types/api/trendApply";
+
+/**
+ * Get trend admin page data
+ */
+export async function getTrendAdminPage(query: TrendAdminQuery) {
+  return post<PagerTrendAdminDTO>("/trend/mananger/page", query);
+}
+
+/**
+ * Review (approve or reject) a trend
+ */
+export async function reviewTrend(data: TrendReviewAdminDTO) {
+  return post<unknown>("/trend/mananger/review", data);
+}

+ 93 - 0
src/types/api/trendApply.ts

@@ -0,0 +1,93 @@
+/**
+ * Trend (dynamic) admin review related API type definitions
+ */
+
+import type { QuerySort } from "./common";
+
+// ============================
+// Query / Pager
+// ============================
+
+export interface TrendAdminQuery {
+  pageSize?: number;
+  pageIndex?: number;
+  sort?: QuerySort;
+  /** Keyword search */
+  searchKeyword?: string;
+  /** Type: 0=text only, 1=image, 2=video, 3=image+text, 4=video+text */
+  type?: number;
+  /** Status: 0=pending, 1=approved, 2=rejected */
+  status?: number;
+  /** Whether to show */
+  show?: boolean;
+  /** Start date: yyyy-MM-dd */
+  startDay?: string;
+  /** End date: yyyy-MM-dd */
+  endDay?: string;
+  /** Gender */
+  gender?: number;
+  skip?: number;
+}
+
+export interface TrendMediaSub {
+  /** Media url */
+  url?: string;
+  /** 0=image, 1=video */
+  type?: number;
+  /** Video cover image */
+  videoCover?: string;
+}
+
+export interface TrendAdminDTO {
+  /** Record id */
+  id: string;
+  createdAt?: number;
+  updatedAt?: number;
+  userNo?: string;
+  nickname?: string;
+  avatar?: string;
+  /** Gender */
+  gender?: number;
+  /** Status: 0=pending, 1=approved, 2=rejected */
+  status?: number;
+  /** Whether to show */
+  show?: boolean;
+  /** Type: 0=text only, 1=image, 2=video, 3=image+text, 4=video+text */
+  type?: number;
+  /** Whether deleted */
+  deleted?: boolean;
+  /** Text content */
+  textContent?: string;
+  /** Media list */
+  medias?: TrendMediaSub[];
+  /** Like count */
+  likeCount?: number;
+  /** Review time */
+  reviewTime?: number;
+  /** Review reason/notes */
+  reviewReason?: string;
+  /** Report count */
+  reportCount?: number;
+  /** Unhandled report count */
+  reportNotHandlerCount?: number;
+}
+
+export interface PagerTrendAdminDTO {
+  total: number;
+  pageIndex: number;
+  pageSize: number;
+  items: TrendAdminDTO[];
+}
+
+// ============================
+// Review
+// ============================
+
+export interface TrendReviewAdminDTO {
+  /** Record id */
+  id: string;
+  /** Status: 1=approved, 2=rejected */
+  status?: number;
+  /** Review reason/notes */
+  reviewReason?: string;
+}