feat(ops): v4.0.7 HQ 活动图快链与勾选导出

HQ 指定一张活动图为勾选主合伙人合成 PNG/zip;城市合伙人页增加快链与单下/导出。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-01 15:12:10 +08:00
parent eeee55e215
commit cdc4b82931
21 changed files with 1041 additions and 28 deletions
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import {
Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message,
Alert, Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
@@ -10,7 +11,7 @@ import {
type ActivityPosterQrSlot,
type ActivityPosterStatus,
} from '@dukang/shared-types';
import { request } from '../lib/api';
import { request, requestDownload } from '../lib/api';
import { fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
import { useAdminListColumns } from '../lib/useAdminListColumns';
@@ -30,7 +31,16 @@ type FormValues = {
status?: ActivityPosterStatus;
};
type PartnerHint = {
id: string;
companyName?: string | null;
name?: string;
phone?: string;
};
export default function ActivityPostersPage() {
const [searchParams] = useSearchParams();
const partnerId = searchParams.get('partnerId')?.trim() || '';
const [form] = Form.useForm();
const [editForm] = Form.useForm<FormValues>();
const [filters, setFilters] = useState<Record<string, string>>({});
@@ -45,6 +55,8 @@ export default function ActivityPostersPage() {
);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState<ActivityPosterItem | null>(null);
const [partnerHint, setPartnerHint] = useState<PartnerHint | null>(null);
const [downloadingId, setDownloadingId] = useState<string | null>(null);
const imageUrl = Form.useWatch('imageUrl', editForm);
const qrXPct = Form.useWatch('qrXPct', editForm);
@@ -56,6 +68,20 @@ export default function ActivityPostersPage() {
qrSizePct: qrSizePct ?? DEFAULT_ACTIVITY_POSTER_QR_SLOT.qrSizePct,
};
useEffect(() => {
if (!partnerId) {
setPartnerHint(null);
return;
}
void request<PartnerHint>(`/admin/partners/${partnerId}`)
.then(setPartnerHint)
.catch(() => setPartnerHint({ id: partnerId }));
}, [partnerId]);
const partnerLabel = partnerHint
? (partnerHint.companyName || partnerHint.name || partnerHint.phone || partnerHint.id)
: partnerId;
function openCreate() {
setEditing(null);
editForm.setFieldsValue({
@@ -98,6 +124,23 @@ export default function ActivityPostersPage() {
void reload();
}
async function downloadForPartner(posterId: string) {
if (!partnerId) return;
setDownloadingId(posterId);
try {
await requestDownload(
`/admin/activity-posters/${posterId}/image?partnerId=${encodeURIComponent(partnerId)}`,
{},
'activity-poster.png',
);
message.success('已开始下载');
} catch (e) {
message.error(e instanceof Error ? e.message : '下载失败');
} finally {
setDownloadingId(null);
}
}
const baseColumns: ColumnsType<ActivityPosterItem> = [
{
title: '标题',
@@ -129,10 +172,20 @@ export default function ActivityPostersPage() {
{ title: '更新', dataIndex: 'updatedAt', width: 160, render: fmtTime },
{
title: '操作',
width: 200,
width: partnerId ? 280 : 200,
render: (_, row) => (
<Space>
<Button type="link" size="small" onClick={() => openEdit(row)}></Button>
{partnerId && row.status === 'ACTIVE' ? (
<Button
type="link"
size="small"
loading={downloadingId === row.id}
onClick={() => void downloadForPartner(row.id)}
>
</Button>
) : null}
<Button
type="link"
size="small"
@@ -176,6 +229,19 @@ export default function ActivityPostersPage() {
settings={settingsButton}
actions={<Button type="primary" onClick={openCreate}></Button>}
/>
{partnerId ? (
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message={
<>
{partnerLabel}{' '}
<Link to="/city-partners"></Link>
</>
}
/>
) : null}
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="status" label="状态">
<Select