feat(ops): v4.0.7 HQ 活动图快链与勾选导出
HQ 指定一张活动图为勾选主合伙人合成 PNG/zip;城市合伙人页增加快链与单下/导出。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Form, Modal, Select, Typography, message } from 'antd';
|
||||
import type { ActivityPosterItem } from '@dukang/shared-types';
|
||||
import { request, requestDownload, type Paginated } from '../lib/api';
|
||||
import { ADMIN_OPTIONS_PAGE_SIZE } from '../lib/constants';
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
partnerIds: string[];
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export default function ActivityPosterDownloadModal({ open, partnerIds, onClose }: Props) {
|
||||
const [posters, setPosters] = useState<ActivityPosterItem[]>([]);
|
||||
const [posterId, setPosterId] = useState<string>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const multi = partnerIds.length > 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setPosterId(undefined);
|
||||
setLoading(true);
|
||||
void request<Paginated<ActivityPosterItem>>(
|
||||
`/admin/activity-posters?status=ACTIVE&page=1&pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`,
|
||||
)
|
||||
.then((res) => {
|
||||
const items = res.items ?? [];
|
||||
setPosters(items);
|
||||
setPosterId(items[0]?.id);
|
||||
})
|
||||
.catch((e) => message.error(e instanceof Error ? e.message : '加载活动图失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [open]);
|
||||
|
||||
async function submit() {
|
||||
if (!posterId) {
|
||||
message.error('请选择活动图');
|
||||
return;
|
||||
}
|
||||
if (!partnerIds.length) {
|
||||
message.error('请选择合伙人');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (partnerIds.length === 1) {
|
||||
await requestDownload(
|
||||
`/admin/activity-posters/${posterId}/image?partnerId=${encodeURIComponent(partnerIds[0])}`,
|
||||
{},
|
||||
'activity-poster.png',
|
||||
);
|
||||
} else {
|
||||
await requestDownload(
|
||||
`/admin/activity-posters/${posterId}/partner-pack`,
|
||||
{ method: 'POST', body: JSON.stringify({ partnerIds }) },
|
||||
'activity-poster-pack.zip',
|
||||
);
|
||||
}
|
||||
message.success('已开始下载');
|
||||
onClose();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '下载失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={multi ? `导出活动图(${partnerIds.length} 人)` : '下载活动图'}
|
||||
open={open}
|
||||
onCancel={onClose}
|
||||
onOk={() => void submit()}
|
||||
confirmLoading={submitting}
|
||||
okText="下载"
|
||||
okButtonProps={{ disabled: loading || !posterId }}
|
||||
destroyOnClose
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
{multi
|
||||
? '将把所选主合伙人的关联码贴进同一张活动图,打包为 zip。无关联码或测试账号会跳过。'
|
||||
: '将把该主合伙人的关联码贴进所选活动图后下载 PNG。'}
|
||||
</Typography.Paragraph>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="活动图" required>
|
||||
<Select
|
||||
loading={loading}
|
||||
placeholder={loading ? '加载中' : posters.length ? '选择已上架活动图' : '暂无上架活动图'}
|
||||
value={posterId}
|
||||
onChange={setPosterId}
|
||||
options={posters.map((row) => ({ value: row.id, label: row.title }))}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import type { ColumnsType } from 'antd/es/table';
|
||||
import type { PartnerAssocSummary, PartnerAssocUserItem } from '@dukang/shared-types';
|
||||
import { request, type HqProfile, type Paginated } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
import ActivityPosterDownloadModal from './ActivityPosterDownloadModal';
|
||||
|
||||
function formatNicknameWithRemark(row: PartnerAssocUserItem) {
|
||||
const name = row.nickname?.trim() || '—';
|
||||
@@ -24,6 +25,8 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [issuing, setIssuing] = useState(false);
|
||||
const [canEditAssoc, setCanEditAssoc] = useState(false);
|
||||
const [canDownloadPosters, setCanDownloadPosters] = useState(false);
|
||||
const [posterDownloadOpen, setPosterDownloadOpen] = useState(false);
|
||||
|
||||
const loadSummary = useCallback(async () => {
|
||||
const data = await request<PartnerAssocSummary>(`/admin/partners/${partnerId}/assoc`);
|
||||
@@ -46,8 +49,15 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
|
||||
useEffect(() => {
|
||||
void request<HqProfile>('/admin/auth/me')
|
||||
.then((p) => setCanEditAssoc((p.permissionKeys ?? []).includes('users_partner_assoc')))
|
||||
.catch(() => setCanEditAssoc(false));
|
||||
.then((p) => {
|
||||
const keys = p.permissionKeys ?? [];
|
||||
setCanEditAssoc(keys.includes('users_partner_assoc'));
|
||||
setCanDownloadPosters(keys.includes('activity_posters'));
|
||||
})
|
||||
.catch(() => {
|
||||
setCanEditAssoc(false);
|
||||
setCanDownloadPosters(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,6 +140,9 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
<Button loading={issuing} onClick={() => void reissue()}>
|
||||
{summary?.qrcodeUrl ? '补发关联码' : '生成关联码'}
|
||||
</Button>
|
||||
{canDownloadPosters ? (
|
||||
<Button onClick={() => setPosterDownloadOpen(true)}>下载活动图</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</Space>
|
||||
<Table
|
||||
@@ -145,6 +158,11 @@ export default function PartnerAssocPanel({ partnerId }: PartnerAssocPanelProps)
|
||||
onChange: (p) => void loadUsers(p),
|
||||
}}
|
||||
/>
|
||||
<ActivityPosterDownloadModal
|
||||
open={posterDownloadOpen}
|
||||
partnerIds={[partnerId]}
|
||||
onClose={() => setPosterDownloadOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user