|
|
@@ -18,7 +18,7 @@ import {
|
|
|
} from 'antd';
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
import dayjs from 'dayjs';
|
|
|
-import { Download, Plus, RefreshCw, RotateCcw, Trash2, Trophy, UserX } from 'lucide-react';
|
|
|
+import { Download, Pencil, Plus, RefreshCw, RotateCcw, Trash2, Trophy, UserX } from 'lucide-react';
|
|
|
import { useLocation } from '@umijs/max';
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
|
@@ -62,6 +62,9 @@ export default function GiveawaysPage() {
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
const [createForm] = Form.useForm();
|
|
|
+ const [editing, setEditing] = useState<Giveaway | null>(null);
|
|
|
+ const [editSaving, setEditSaving] = useState(false);
|
|
|
+ const [editForm] = Form.useForm();
|
|
|
const [selected, setSelected] = useState<Giveaway | null>(null);
|
|
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
|
|
const [banOpen, setBanOpen] = useState(false);
|
|
|
@@ -117,6 +120,16 @@ export default function GiveawaysPage() {
|
|
|
if (selected) await loadParticipants(selected);
|
|
|
};
|
|
|
|
|
|
+ const openEdit = (giveaway: Giveaway) => {
|
|
|
+ editForm.setFieldsValue({
|
|
|
+ title: giveaway.title,
|
|
|
+ description: giveaway.description || '',
|
|
|
+ starts_at: dayjs(giveaway.starts_at || giveaway.created_at),
|
|
|
+ ends_at: dayjs(giveaway.ends_at),
|
|
|
+ });
|
|
|
+ setEditing(giveaway);
|
|
|
+ };
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
apiRequest<Paged<ChatSummary>>('/chats?page_size=100').then((result) => {
|
|
|
const accessibleChats = result.items.filter((chat) => chat.accessible);
|
|
|
@@ -188,12 +201,19 @@ export default function GiveawaysPage() {
|
|
|
title: '操作',
|
|
|
key: 'actions',
|
|
|
fixed: 'right',
|
|
|
- width: 270,
|
|
|
+ width: 330,
|
|
|
render: (_, record) => (
|
|
|
<Space size={4} wrap>
|
|
|
<Button type="link" onClick={() => void loadParticipants(record)}>参与者</Button>
|
|
|
{record.status === 'running' ? (
|
|
|
<>
|
|
|
+ <Button
|
|
|
+ type="link"
|
|
|
+ icon={<Pencil size={15} />}
|
|
|
+ onClick={() => openEdit(record)}
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </Button>
|
|
|
<Button
|
|
|
type="link"
|
|
|
icon={<Trophy size={15} />}
|
|
|
@@ -357,7 +377,7 @@ export default function GiveawaysPage() {
|
|
|
loading={loading}
|
|
|
pagination={{ current: data.page, pageSize: data.page_size, total: data.total, showSizeChanger: false }}
|
|
|
onChange={(pagination) => void load(pagination.current || 1)}
|
|
|
- scroll={{ x: 1100 }}
|
|
|
+ scroll={{ x: 1180 }}
|
|
|
/>
|
|
|
</Surface>
|
|
|
<Modal
|
|
|
@@ -470,6 +490,121 @@ export default function GiveawaysPage() {
|
|
|
</Space>
|
|
|
</Form>
|
|
|
</Modal>
|
|
|
+ <Modal
|
|
|
+ title={editing ? `编辑抽奖 · #${editing.giveaway_id}` : '编辑抽奖'}
|
|
|
+ open={Boolean(editing)}
|
|
|
+ width={640}
|
|
|
+ okText="保存并刷新群消息"
|
|
|
+ confirmLoading={editSaving}
|
|
|
+ cancelButtonProps={{ disabled: editSaving }}
|
|
|
+ maskClosable={!editSaving}
|
|
|
+ onCancel={() => {
|
|
|
+ setEditing(null);
|
|
|
+ editForm.resetFields();
|
|
|
+ }}
|
|
|
+ onOk={() => editForm.submit()}
|
|
|
+ destroyOnHidden
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={editForm}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={async (values) => {
|
|
|
+ if (!editing) return;
|
|
|
+ const startsAt = dayjs(values.starts_at);
|
|
|
+ const endsAt = dayjs(values.ends_at);
|
|
|
+ setEditSaving(true);
|
|
|
+ try {
|
|
|
+ await apiRequest<Giveaway>(
|
|
|
+ `/giveaways/${editing.giveaway_id}`,
|
|
|
+ jsonOptions('PATCH', {
|
|
|
+ title: values.title,
|
|
|
+ description: values.description || '',
|
|
|
+ starts_at: startsAt.toISOString(),
|
|
|
+ ends_at: endsAt.toISOString(),
|
|
|
+ expected_updated_at: editing.updated_at,
|
|
|
+ confirm: true,
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ setEditing(null);
|
|
|
+ editForm.resetFields();
|
|
|
+ message.success('抽奖已更新,群内消息已刷新');
|
|
|
+ await load(data.page);
|
|
|
+ } catch (error) {
|
|
|
+ message.error(error instanceof Error ? error.message : '抽奖更新失败');
|
|
|
+ } finally {
|
|
|
+ setEditSaving(false);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ name="title"
|
|
|
+ label="标题"
|
|
|
+ rules={[{ required: true, whitespace: true, message: '请输入抽奖标题' }]}
|
|
|
+ >
|
|
|
+ <Input maxLength={200} />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="description"
|
|
|
+ label="说明"
|
|
|
+ extra="保存后会同步更新群内原抽奖消息"
|
|
|
+ >
|
|
|
+ <Input.TextArea rows={4} maxLength={4000} showCount />
|
|
|
+ </Form.Item>
|
|
|
+ <Space wrap align="start" size={16} style={{ display: 'flex' }}>
|
|
|
+ <Form.Item
|
|
|
+ name="starts_at"
|
|
|
+ label="报名开始时间"
|
|
|
+ extra={
|
|
|
+ editing && !dayjs(editing.starts_at || editing.created_at).isAfter(dayjs())
|
|
|
+ ? '报名已经开始,此时间不可修改'
|
|
|
+ : '按浏览器当前时区输入'
|
|
|
+ }
|
|
|
+ rules={[{ required: true, message: '请选择报名开始时间' }]}
|
|
|
+ style={{ flex: 1, minWidth: 240 }}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ disabled={Boolean(
|
|
|
+ editing && !dayjs(editing.starts_at || editing.created_at).isAfter(dayjs()),
|
|
|
+ )}
|
|
|
+ showTime={{ format: 'HH:mm' }}
|
|
|
+ format="YYYY-MM-DD HH:mm"
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ <Form.Item
|
|
|
+ name="ends_at"
|
|
|
+ label="开奖时间"
|
|
|
+ dependencies={['starts_at']}
|
|
|
+ extra="修改后按新时间自动开奖"
|
|
|
+ rules={[
|
|
|
+ { required: true, message: '请选择开奖时间' },
|
|
|
+ ({ getFieldValue }) => ({
|
|
|
+ validator(_, value) {
|
|
|
+ if (!value || !getFieldValue('starts_at')) return Promise.resolve();
|
|
|
+ if (!dayjs(value).isAfter(dayjs())) {
|
|
|
+ return Promise.reject(new Error('开奖时间必须晚于当前时间'));
|
|
|
+ }
|
|
|
+ if (!dayjs(value).isAfter(dayjs(getFieldValue('starts_at')))) {
|
|
|
+ return Promise.reject(new Error('开奖时间必须晚于报名开始时间'));
|
|
|
+ }
|
|
|
+ return Promise.resolve();
|
|
|
+ },
|
|
|
+ }),
|
|
|
+ ]}
|
|
|
+ style={{ flex: 1, minWidth: 240 }}
|
|
|
+ >
|
|
|
+ <DatePicker
|
|
|
+ showTime={{ format: 'HH:mm' }}
|
|
|
+ format="YYYY-MM-DD HH:mm"
|
|
|
+ style={{ width: '100%' }}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </Space>
|
|
|
+ <Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
|
|
+ 奖项与积分规则不可编辑,避免报名后改变抽奖条件。
|
|
|
+ </Typography.Paragraph>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
<Drawer
|
|
|
title={selected ? `参与者 · #${selected.giveaway_id}` : '参与者'}
|
|
|
open={Boolean(selected)}
|