|
|
@@ -0,0 +1,564 @@
|
|
|
+"use client";
|
|
|
+
|
|
|
+import {
|
|
|
+ DeleteOutlined,
|
|
|
+ EditOutlined,
|
|
|
+ PlusOutlined,
|
|
|
+ ReloadOutlined,
|
|
|
+ SearchOutlined,
|
|
|
+ UndoOutlined,
|
|
|
+} from "@ant-design/icons";
|
|
|
+import {
|
|
|
+ App,
|
|
|
+ Button,
|
|
|
+ DatePicker,
|
|
|
+ Form,
|
|
|
+ Image,
|
|
|
+ Input,
|
|
|
+ InputNumber,
|
|
|
+ Modal,
|
|
|
+ Popconfirm,
|
|
|
+ Select,
|
|
|
+ Space,
|
|
|
+ Switch,
|
|
|
+ Table,
|
|
|
+} from "antd";
|
|
|
+import type { ColumnsType, TablePaginationConfig } from "antd/es/table";
|
|
|
+import type React from "react";
|
|
|
+import { useEffect, useMemo, useState } from "react";
|
|
|
+import ImageUpload from "@/components/ImageUpload";
|
|
|
+import {
|
|
|
+ createBannerConfig,
|
|
|
+ deleteBannerConfigs,
|
|
|
+ getBannerConfigPage,
|
|
|
+ updateBannerConfig,
|
|
|
+} from "@/services/bannerConfig";
|
|
|
+import type { BannerConfigAdminDTO, BannerConfigAdminQuery } from "@/types/api";
|
|
|
+import { formatTimestamp } from "@/utils/date";
|
|
|
+import dayjs from "@/utils/dayjs";
|
|
|
+
|
|
|
+function normalizeId(id: unknown): string | undefined {
|
|
|
+ if (typeof id === "string") return id;
|
|
|
+ if (id && typeof id === "object") {
|
|
|
+ const maybe = id as Record<string, unknown>;
|
|
|
+ if (typeof maybe.$oid === "string") return maybe.$oid;
|
|
|
+ if (typeof maybe.oid === "string") return maybe.oid;
|
|
|
+ if (typeof maybe._id === "string") return maybe._id;
|
|
|
+ if (typeof maybe.id === "string") return maybe.id;
|
|
|
+ if (typeof maybe.date === "string") return maybe.date;
|
|
|
+ if (typeof maybe.timestamp === "number") return String(maybe.timestamp);
|
|
|
+ try {
|
|
|
+ return JSON.stringify(id);
|
|
|
+ } catch {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return undefined;
|
|
|
+}
|
|
|
+
|
|
|
+type TableRow = BannerConfigAdminDTO & {
|
|
|
+ _rawId?: unknown;
|
|
|
+ _idStr?: string;
|
|
|
+};
|
|
|
+
|
|
|
+const platformOptions = [
|
|
|
+ { label: "全部", value: 0 },
|
|
|
+ { label: "安卓", value: 1 },
|
|
|
+ { label: "苹果", value: 2 },
|
|
|
+ { label: "浏览器", value: 3 },
|
|
|
+ { label: "电脑应用", value: 4 },
|
|
|
+];
|
|
|
+
|
|
|
+const BannerConfigPage: React.FC = () => {
|
|
|
+ const { message } = App.useApp();
|
|
|
+ const [searchForm] = Form.useForm();
|
|
|
+ const [editForm] = Form.useForm();
|
|
|
+
|
|
|
+ // List state
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
+ const [dataSource, setDataSource] = useState<TableRow[]>([]);
|
|
|
+ const [total, setTotal] = useState(0);
|
|
|
+ const [queryParams, setQueryParams] = useState<BannerConfigAdminQuery>({
|
|
|
+ pageSize: 20,
|
|
|
+ pageIndex: 1,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Modal state
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
+ const [editMode, setEditMode] = useState(false);
|
|
|
+ const [editLoading, setEditLoading] = useState(false);
|
|
|
+ const [currentRecord, setCurrentRecord] = useState<TableRow | null>(null);
|
|
|
+
|
|
|
+ const platformLabel = useMemo(() => {
|
|
|
+ const map = new Map<number, string>();
|
|
|
+ for (const it of platformOptions) map.set(it.value, it.label);
|
|
|
+ return (v?: number) => (typeof v === "number" ? (map.get(v) ?? "-") : "-");
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const loadPageData = async () => {
|
|
|
+ setLoading(true);
|
|
|
+ try {
|
|
|
+ const response = await getBannerConfigPage(queryParams);
|
|
|
+ const items: TableRow[] = (response.items || []).map((it) => {
|
|
|
+ const idStr = normalizeId(it.id);
|
|
|
+ return {
|
|
|
+ ...it,
|
|
|
+ _rawId: it.id,
|
|
|
+ _idStr: idStr,
|
|
|
+ id: (idStr ?? it.id) as unknown as typeof it.id,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ setDataSource(items);
|
|
|
+ setTotal(response.total || 0);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to load banner config:", error);
|
|
|
+ message.error("加载 Banner 配置失败");
|
|
|
+ } finally {
|
|
|
+ setLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // biome-ignore lint/correctness/useExhaustiveDependencies: loadPageData is stable and doesn't need to be in dependencies
|
|
|
+ useEffect(() => {
|
|
|
+ loadPageData();
|
|
|
+ }, [queryParams]);
|
|
|
+
|
|
|
+ const handleSearch = () => {
|
|
|
+ const values = searchForm.getFieldsValue();
|
|
|
+ setQueryParams({
|
|
|
+ ...queryParams,
|
|
|
+ pageIndex: 1,
|
|
|
+ title: values.title?.trim() || undefined,
|
|
|
+ platform:
|
|
|
+ typeof values.platform === "number" ||
|
|
|
+ typeof values.platform === "string"
|
|
|
+ ? Number(values.platform)
|
|
|
+ : 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 openModal = (mode: "create" | "edit", record?: TableRow) => {
|
|
|
+ const isEdit = mode === "edit";
|
|
|
+ setEditMode(isEdit);
|
|
|
+ setCurrentRecord(record ?? null);
|
|
|
+ editForm.resetFields();
|
|
|
+
|
|
|
+ if (isEdit && record) {
|
|
|
+ editForm.setFieldsValue({
|
|
|
+ title: record.title,
|
|
|
+ subTitle: record.subTitle,
|
|
|
+ imageUrl: record.imageUrl,
|
|
|
+ platform:
|
|
|
+ typeof record.platform === "number" ? record.platform : undefined,
|
|
|
+ adSlots: (record.adSlots ?? []).map((n) => String(n)),
|
|
|
+ allAdSlotEffective: record.allAdSlotEffective ?? false,
|
|
|
+ jumpTarget: record.jumpTarget,
|
|
|
+ jumpLink: record.jumpLink,
|
|
|
+ priority: record.priority,
|
|
|
+ startTime: record.startTime ? dayjs(record.startTime) : undefined,
|
|
|
+ endTime: record.endTime ? dayjs(record.endTime) : undefined,
|
|
|
+ minVersion: record.minVersion,
|
|
|
+ maxVersion: record.maxVersion,
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ editForm.setFieldsValue({
|
|
|
+ platform: 0,
|
|
|
+ allAdSlotEffective: false,
|
|
|
+ adSlots: [],
|
|
|
+ priority: 0,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ setModalVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleAdd = () => openModal("create");
|
|
|
+ const handleEdit = (record: TableRow) => openModal("edit", record);
|
|
|
+
|
|
|
+ const parseAdSlots = (raw: unknown): number[] | undefined => {
|
|
|
+ if (!Array.isArray(raw)) return undefined;
|
|
|
+ const nums = raw
|
|
|
+ .map((it) => {
|
|
|
+ if (typeof it === "number") return it;
|
|
|
+ if (typeof it === "string" && it.trim() !== "") return Number(it);
|
|
|
+ return NaN;
|
|
|
+ })
|
|
|
+ .filter((n) => Number.isFinite(n));
|
|
|
+ return nums.length > 0 ? nums : [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async () => {
|
|
|
+ try {
|
|
|
+ const values = await editForm.validateFields();
|
|
|
+ setEditLoading(true);
|
|
|
+
|
|
|
+ const submitData: BannerConfigAdminDTO = {
|
|
|
+ id: (currentRecord?._rawId ?? currentRecord?.id) as string,
|
|
|
+ title: values.title,
|
|
|
+ subTitle: values.subTitle,
|
|
|
+ imageUrl: values.imageUrl,
|
|
|
+ platform:
|
|
|
+ typeof values.platform === "number" ||
|
|
|
+ typeof values.platform === "string"
|
|
|
+ ? Number(values.platform)
|
|
|
+ : undefined,
|
|
|
+ adSlots: parseAdSlots(values.adSlots),
|
|
|
+ allAdSlotEffective: Boolean(values.allAdSlotEffective),
|
|
|
+ jumpTarget:
|
|
|
+ typeof values.jumpTarget === "number" ||
|
|
|
+ typeof values.jumpTarget === "string"
|
|
|
+ ? Number(values.jumpTarget)
|
|
|
+ : undefined,
|
|
|
+ jumpLink: values.jumpLink?.trim() || undefined,
|
|
|
+ priority:
|
|
|
+ typeof values.priority === "number" ||
|
|
|
+ typeof values.priority === "string"
|
|
|
+ ? Number(values.priority)
|
|
|
+ : undefined,
|
|
|
+ startTime: values.startTime
|
|
|
+ ? dayjs(values.startTime).valueOf()
|
|
|
+ : undefined,
|
|
|
+ endTime: values.endTime ? dayjs(values.endTime).valueOf() : undefined,
|
|
|
+ minVersion: values.minVersion?.trim() || undefined,
|
|
|
+ maxVersion: values.maxVersion?.trim() || undefined,
|
|
|
+ createdAt: currentRecord?.createdAt,
|
|
|
+ updatedAt: currentRecord?.updatedAt,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (editMode && (currentRecord?._rawId ?? currentRecord?.id)) {
|
|
|
+ await updateBannerConfig(submitData);
|
|
|
+ message.success("更新成功");
|
|
|
+ } else {
|
|
|
+ delete submitData.id;
|
|
|
+ delete submitData.createdAt;
|
|
|
+ delete submitData.updatedAt;
|
|
|
+ await createBannerConfig(submitData);
|
|
|
+ message.success("创建成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ setModalVisible(false);
|
|
|
+ loadPageData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to submit banner config:", error);
|
|
|
+ } finally {
|
|
|
+ setEditLoading(false);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDelete = async (record: TableRow) => {
|
|
|
+ const rawId = record._rawId ?? record.id;
|
|
|
+ if (!rawId) {
|
|
|
+ message.warning("缺少记录 ID,无法删除");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await deleteBannerConfigs({ ids: [rawId as string] });
|
|
|
+ message.success("删除成功");
|
|
|
+ loadPageData();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Failed to delete banner config:", error);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const columns: ColumnsType<TableRow> = [
|
|
|
+ { title: "标题", dataIndex: "title", key: "title", width: 220 },
|
|
|
+ {
|
|
|
+ title: "副标题",
|
|
|
+ dataIndex: "subTitle",
|
|
|
+ key: "subTitle",
|
|
|
+ width: 220,
|
|
|
+ render: (v?: string) => v || "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "图片",
|
|
|
+ dataIndex: "imageUrl",
|
|
|
+ key: "imageUrl",
|
|
|
+ width: 120,
|
|
|
+ render: (url?: string) =>
|
|
|
+ url ? <Image src={url} alt="banner" width={60} height={60} /> : "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "平台",
|
|
|
+ dataIndex: "platform",
|
|
|
+ key: "platform",
|
|
|
+ width: 110,
|
|
|
+ render: (v?: number) => platformLabel(v),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "广告位",
|
|
|
+ dataIndex: "adSlots",
|
|
|
+ key: "adSlots",
|
|
|
+ width: 160,
|
|
|
+ render: (v?: number[]) => (v ? (v.length ? v.join(", ") : "-") : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "广告位是否有效",
|
|
|
+ dataIndex: "allAdSlotEffective",
|
|
|
+ key: "allAdSlotEffective",
|
|
|
+ width: 140,
|
|
|
+ render: (v?: boolean) => (v ? "是" : "否"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "跳转目标",
|
|
|
+ dataIndex: "jumpTarget",
|
|
|
+ key: "jumpTarget",
|
|
|
+ width: 110,
|
|
|
+ render: (v?: number) => (typeof v === "number" ? v : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "跳转链接",
|
|
|
+ dataIndex: "jumpLink",
|
|
|
+ key: "jumpLink",
|
|
|
+ width: 260,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (v?: string) => v || "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "优先级",
|
|
|
+ dataIndex: "priority",
|
|
|
+ key: "priority",
|
|
|
+ width: 90,
|
|
|
+ render: (v?: number) => (typeof v === "number" ? v : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "生效时间",
|
|
|
+ dataIndex: "startTime",
|
|
|
+ key: "startTime",
|
|
|
+ width: 180,
|
|
|
+ render: (ts: number | undefined) => (ts ? formatTimestamp(ts) : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "失效时间",
|
|
|
+ dataIndex: "endTime",
|
|
|
+ key: "endTime",
|
|
|
+ width: 180,
|
|
|
+ render: (ts: number | undefined) => (ts ? formatTimestamp(ts) : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "最低版本",
|
|
|
+ dataIndex: "minVersion",
|
|
|
+ key: "minVersion",
|
|
|
+ width: 120,
|
|
|
+ render: (v?: string) => v || "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "最高版本",
|
|
|
+ dataIndex: "maxVersion",
|
|
|
+ key: "maxVersion",
|
|
|
+ width: 120,
|
|
|
+ render: (v?: string) => v || "-",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "创建时间",
|
|
|
+ dataIndex: "createdAt",
|
|
|
+ key: "createdAt",
|
|
|
+ width: 180,
|
|
|
+ render: (ts: number | undefined) => (ts ? formatTimestamp(ts) : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "更新时间",
|
|
|
+ dataIndex: "updatedAt",
|
|
|
+ key: "updatedAt",
|
|
|
+ width: 180,
|
|
|
+ render: (ts: number | undefined) => (ts ? formatTimestamp(ts) : "-"),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "操作",
|
|
|
+ key: "action",
|
|
|
+ width: 160,
|
|
|
+ fixed: "right",
|
|
|
+ render: (_, record) => (
|
|
|
+ <Space size="small">
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ size="small"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => handleEdit(record)}
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </Button>
|
|
|
+ <Popconfirm
|
|
|
+ title="确定删除该 Banner 吗?"
|
|
|
+ okText="确定"
|
|
|
+ cancelText="取消"
|
|
|
+ onConfirm={() => handleDelete(record)}
|
|
|
+ >
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ danger
|
|
|
+ size="small"
|
|
|
+ icon={<DeleteOutlined />}
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </Button>
|
|
|
+ </Popconfirm>
|
|
|
+ </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="title">
|
|
|
+ <Input placeholder="请输入" allowClear style={{ width: 220 }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="平台" name="platform">
|
|
|
+ <Select
|
|
|
+ placeholder="请选择"
|
|
|
+ allowClear
|
|
|
+ style={{ width: 180 }}
|
|
|
+ options={platformOptions}
|
|
|
+ />
|
|
|
+ </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}>
|
|
|
+ 刷新
|
|
|
+ </Button>
|
|
|
+ <Button
|
|
|
+ type="primary"
|
|
|
+ icon={<PlusOutlined />}
|
|
|
+ onClick={handleAdd}
|
|
|
+ style={{ backgroundColor: "#52c41a" }}
|
|
|
+ >
|
|
|
+ 新增
|
|
|
+ </Button>
|
|
|
+ </Space>
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="bg-white p-4 rounded-lg shadow">
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={dataSource}
|
|
|
+ rowKey={(record) =>
|
|
|
+ record._idStr ||
|
|
|
+ normalizeId(record.id) ||
|
|
|
+ `${record.title ?? ""}-${record.createdAt ?? ""}`
|
|
|
+ }
|
|
|
+ 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: 2100 }}
|
|
|
+ size="small"
|
|
|
+ bordered
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ title={editMode ? "编辑 Banner 配置" : "新增 Banner 配置"}
|
|
|
+ open={modalVisible}
|
|
|
+ onOk={handleSubmit}
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
+ confirmLoading={editLoading}
|
|
|
+ width={720}
|
|
|
+ destroyOnHidden
|
|
|
+ forceRender
|
|
|
+ >
|
|
|
+ <Form form={editForm} layout="vertical" style={{ marginTop: 16 }}>
|
|
|
+ <Form.Item
|
|
|
+ label="标题"
|
|
|
+ name="title"
|
|
|
+ rules={[{ required: true, message: "请输入标题" }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入标题" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="副标题" name="subTitle">
|
|
|
+ <Input placeholder="请输入副标题" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="图片"
|
|
|
+ name="imageUrl"
|
|
|
+ rules={[{ required: true, message: "请上传图片" }]}
|
|
|
+ >
|
|
|
+ <ImageUpload dir="banner" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="平台" name="platform">
|
|
|
+ <Select placeholder="请选择" options={platformOptions} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="广告位列表"
|
|
|
+ name="adSlots"
|
|
|
+ tooltip="支持输入多个数字,回车分隔"
|
|
|
+ >
|
|
|
+ <Select
|
|
|
+ mode="tags"
|
|
|
+ tokenSeparators={[",", " ", "\n", "\t"]}
|
|
|
+ placeholder="例如:1,2,3"
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ label="是否所有广告位有效"
|
|
|
+ name="allAdSlotEffective"
|
|
|
+ valuePropName="checked"
|
|
|
+ >
|
|
|
+ <Switch />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="跳转目标(jumpTarget)" name="jumpTarget">
|
|
|
+ <InputNumber style={{ width: "100%" }} min={0} step={1} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="跳转链接(jumpLink)" name="jumpLink">
|
|
|
+ <Input placeholder="请输入跳转链接" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="优先级" name="priority">
|
|
|
+ <InputNumber style={{ width: "100%" }} min={0} step={1} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="生效时间" name="startTime">
|
|
|
+ <DatePicker showTime style={{ width: "100%" }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="失效时间" name="endTime">
|
|
|
+ <DatePicker showTime style={{ width: "100%" }} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="最低版本(APP)" name="minVersion">
|
|
|
+ <Input placeholder="例如:1.0.0" />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item label="最高版本(APP)" name="maxVersion">
|
|
|
+ <Input placeholder="例如:9.9.9" />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default BannerConfigPage;
|