+
+

+
onPointerDown('move', e)}
+ >
+
onPointerDown('resize', e)}
+ />
+
+
+
+ 拖拽移动码栏,拉右下角调整大小(边长相对图宽 {slot.qrSizePct}%)
+
+
+ );
+}
diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx
index f610a89..36a4d30 100644
--- a/apps/admin-web/src/layouts/AdminLayout.tsx
+++ b/apps/admin-web/src/layouts/AdminLayout.tsx
@@ -19,6 +19,7 @@ import {
SettingOutlined,
AccountBookOutlined,
ProjectOutlined,
+ PictureOutlined,
} from '@ant-design/icons';
import {
hasAnySystemSettingsPermission,
@@ -118,6 +119,7 @@ const MENU_ITEMS: MenuProps['items'] = [
},
{ key: '/invoices', icon:
, label: '发票' },
{ key: '/promo-codes', icon:
, label: '推广码' },
+ { key: '/activity-posters', icon:
, label: '活动图' },
{ key: '/wechat-bindings', icon:
, label: '微信绑定' },
{
key: 'wecom-group',
@@ -180,6 +182,7 @@ function menuAllowed(key: string, permissionKeys: string[]): boolean {
'/product-detail-templates': 'products',
'/orders': 'orders',
'/promo-codes': 'promo_codes',
+ '/activity-posters': 'activity_posters',
'/wecom/bots': 'wecom_bots',
'/wecom/pushes': 'wecom_bots',
'wecom-group': 'wecom_bots',
diff --git a/apps/admin-web/src/lib/constants.ts b/apps/admin-web/src/lib/constants.ts
index 490bd3c..57e4f01 100644
--- a/apps/admin-web/src/lib/constants.ts
+++ b/apps/admin-web/src/lib/constants.ts
@@ -120,6 +120,7 @@ export const RESOURCE_BIZ_TYPE_LABELS: Record
= {
QRCODE: '二维码',
SIGN_PHOTO: '签收照',
VIDEO: '视频',
+ ACTIVITY_POSTER: '活动图',
};
export const RESOURCE_STATUS_LABELS: Record = {
diff --git a/apps/admin-web/src/lib/display-labels.ts b/apps/admin-web/src/lib/display-labels.ts
index d238c1e..a41f472 100644
--- a/apps/admin-web/src/lib/display-labels.ts
+++ b/apps/admin-web/src/lib/display-labels.ts
@@ -43,6 +43,7 @@ export const REF_TYPE_LABELS: Record = {
INVOICE: '发票',
PROMO: '推广码',
PROMO_CODE: '推广码',
+ ACTIVITY_POSTER: '活动图',
HQ: '总部',
HQ_ACCOUNT: 'HQ 账号',
STORE_ACCOUNT: '门店账号',
diff --git a/apps/admin-web/src/lib/hq-log.ts b/apps/admin-web/src/lib/hq-log.ts
index d61b340..843ecd5 100644
--- a/apps/admin-web/src/lib/hq-log.ts
+++ b/apps/admin-web/src/lib/hq-log.ts
@@ -61,6 +61,10 @@ export const HQ_OPERATION_ACTION_OPTIONS = [
{ value: 'PARTNER_BILL_BATCH_MARK_PAID', label: '批量合伙人账单结算' },
{ value: 'WINERY_BILL_CONFIRM', label: '酒厂对账单确认打款' },
{ value: 'WINERY_BILL_BATCH_CONFIRM', label: '批量酒厂对账单打款' },
+ { value: 'ACTIVITY_POSTER_CREATE', label: '新增活动图' },
+ { value: 'ACTIVITY_POSTER_UPDATE', label: '编辑活动图' },
+ { value: 'ACTIVITY_POSTER_UPDATE_STATUS', label: '活动图上下架' },
+ { value: 'ACTIVITY_POSTER_DELETE', label: '删除活动图' },
] as const;
export const HQ_OPERATION_ACTION_LABELS: Record = Object.fromEntries(
diff --git a/apps/admin-web/src/pages/ActivityPostersPage.tsx b/apps/admin-web/src/pages/ActivityPostersPage.tsx
new file mode 100644
index 0000000..d0887cc
--- /dev/null
+++ b/apps/admin-web/src/pages/ActivityPostersPage.tsx
@@ -0,0 +1,242 @@
+import { useState } from 'react';
+import {
+ Button, Form, Image, Input, InputNumber, Modal, Popconfirm, Select, Space, Table, Tag, message,
+} from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import {
+ ACTIVITY_POSTER_STATUS_LABELS,
+ DEFAULT_ACTIVITY_POSTER_QR_SLOT,
+ type ActivityPosterItem,
+ type ActivityPosterQrSlot,
+ type ActivityPosterStatus,
+} from '@dukang/shared-types';
+import { request } from '../lib/api';
+import { fmtTime } from '../lib/constants';
+import { useAdminList } from '../lib/useAdminList';
+import { useAdminListColumns } from '../lib/useAdminListColumns';
+import { AdminListHeader } from '../components/AdminListHeader';
+import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
+import OssUpload from '../components/OssUpload';
+import ActivityPosterQrSlotEditor from '../components/ActivityPosterQrSlotEditor';
+
+type FormValues = {
+ title: string;
+ copyText?: string;
+ imageUrl: string;
+ qrXPct: number;
+ qrYPct: number;
+ qrSizePct: number;
+ sortOrder?: number;
+ status?: ActivityPosterStatus;
+};
+
+export default function ActivityPostersPage() {
+ const [form] = Form.useForm();
+ const [editForm] = Form.useForm();
+ const [filters, setFilters] = useState>({});
+ const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList(
+ '/admin/activity-posters',
+ () => {
+ const qs = new URLSearchParams();
+ if (filters.status) qs.set('status', filters.status);
+ return qs;
+ },
+ [filters],
+ );
+ const [modalOpen, setModalOpen] = useState(false);
+ const [editing, setEditing] = useState(null);
+
+ const imageUrl = Form.useWatch('imageUrl', editForm);
+ const qrXPct = Form.useWatch('qrXPct', editForm);
+ const qrYPct = Form.useWatch('qrYPct', editForm);
+ const qrSizePct = Form.useWatch('qrSizePct', editForm);
+ const slot: ActivityPosterQrSlot = {
+ qrXPct: qrXPct ?? DEFAULT_ACTIVITY_POSTER_QR_SLOT.qrXPct,
+ qrYPct: qrYPct ?? DEFAULT_ACTIVITY_POSTER_QR_SLOT.qrYPct,
+ qrSizePct: qrSizePct ?? DEFAULT_ACTIVITY_POSTER_QR_SLOT.qrSizePct,
+ };
+
+ function openCreate() {
+ setEditing(null);
+ editForm.setFieldsValue({
+ title: '',
+ copyText: '',
+ imageUrl: '',
+ ...DEFAULT_ACTIVITY_POSTER_QR_SLOT,
+ sortOrder: 0,
+ status: 'ACTIVE',
+ });
+ setModalOpen(true);
+ }
+
+ function openEdit(row: ActivityPosterItem) {
+ setEditing(row);
+ editForm.setFieldsValue({
+ title: row.title,
+ copyText: row.copyText,
+ imageUrl: row.imageUrl,
+ qrXPct: row.qrXPct,
+ qrYPct: row.qrYPct,
+ qrSizePct: row.qrSizePct,
+ sortOrder: row.sortOrder,
+ status: row.status,
+ });
+ setModalOpen(true);
+ }
+
+ async function save() {
+ const v = await editForm.validateFields();
+ const body = JSON.stringify(v);
+ if (editing) {
+ await request(`/admin/activity-posters/${editing.id}`, { method: 'PUT', body });
+ message.success('已保存');
+ } else {
+ await request('/admin/activity-posters', { method: 'POST', body });
+ message.success('已创建');
+ }
+ setModalOpen(false);
+ void reload();
+ }
+
+ const baseColumns: ColumnsType = [
+ {
+ title: '标题',
+ dataIndex: 'title',
+ width: 180,
+ render: (v, row) => openEdit(row)}>{v},
+ },
+ {
+ title: '封面',
+ dataIndex: 'imageUrl',
+ width: 90,
+ render: (url: string) => ,
+ },
+ {
+ title: '文案',
+ dataIndex: 'copyText',
+ width: 240,
+ render: (v: string) => v || '—',
+ },
+ { title: '排序', dataIndex: 'sortOrder', width: 70 },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ width: 80,
+ render: (s: ActivityPosterStatus) => (
+ {ACTIVITY_POSTER_STATUS_LABELS[s] || s}
+ ),
+ },
+ { title: '更新', dataIndex: 'updatedAt', width: 160, render: fmtTime },
+ {
+ title: '操作',
+ width: 200,
+ render: (_, row) => (
+
+
+
+ {
+ await request(`/admin/activity-posters/${row.id}`, { method: 'DELETE' });
+ message.success('已删除');
+ void reload();
+ }}
+ >
+
+
+
+ ),
+ },
+ ];
+
+ const { columns, settingsButton, settingsModal } = useAdminListColumns('activity-posters', baseColumns, {
+ page,
+ pageSize,
+ });
+
+ return (
+
+ {settingsModal}
+
新建活动图}
+ />
+
+
+
+
+ { setPage(p); setPageSize(ps); },
+ }}
+ />
+ setModalOpen(false)}
+ onOk={() => void save()}
+ width={720}
+ destroyOnClose
+ >
+
+
+
+
+
+
+
+
+
+
+ editForm.setFieldsValue(next)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/h5-partner/src/PartnerAppRoutes.tsx b/apps/h5-partner/src/PartnerAppRoutes.tsx
index e41ba19..16fcd9a 100644
--- a/apps/h5-partner/src/PartnerAppRoutes.tsx
+++ b/apps/h5-partner/src/PartnerAppRoutes.tsx
@@ -25,6 +25,7 @@ import LeaderboardPage from './pages/LeaderboardPage';
import UsersManagePage from './pages/UsersManagePage';
import AssocOrdersPage from './pages/AssocOrdersPage';
import CommissionOrdersPage from './pages/CommissionOrdersPage';
+import ActivityPostersPage from './pages/ActivityPostersPage';
function PrimaryRoutes() {
return (
@@ -41,6 +42,7 @@ function PrimaryRoutes() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/apps/h5-partner/src/components/ActivityPosterPicker.tsx b/apps/h5-partner/src/components/ActivityPosterPicker.tsx
new file mode 100644
index 0000000..92f1124
--- /dev/null
+++ b/apps/h5-partner/src/components/ActivityPosterPicker.tsx
@@ -0,0 +1,246 @@
+import { useEffect, useRef, useState } from 'react';
+import type { ActivityPosterItem, PartnerAssocSummary } from '@dukang/shared-types';
+import { request } from '../lib/api';
+import {
+ copyActivityCopy,
+ downloadBlob,
+ fetchActivityPosterImage,
+ fetchAssocQrcodeImage,
+ getActivityPosterSelection,
+ listActivityPosters,
+ saveActivityPosterSelection,
+} from '../lib/activity-posters';
+import { toastError, toastSuccess } from '../lib/toast';
+import { isWechatEnv } from '../lib/weixin';
+
+const NONE_ID = 'none';
+
+type Props = {
+ initialId?: string | null;
+};
+
+export default function ActivityPosterPicker({ initialId }: Props) {
+ const [items, setItems] = useState([]);
+ const [qrcodeUrl, setQrcodeUrl] = useState(null);
+ const [partnerId, setPartnerId] = useState('');
+ const [selectedId, setSelectedId] = useState(NONE_ID);
+ const [previewUrls, setPreviewUrls] = useState>({});
+ const [listLoading, setListLoading] = useState(true);
+ const [previewLoading, setPreviewLoading] = useState(false);
+ const [downloading, setDownloading] = useState(false);
+ const blobsRef = useRef(new Map());
+ const urlsRef = useRef([]);
+
+ useEffect(() => () => {
+ urlsRef.current.forEach((url) => URL.revokeObjectURL(url));
+ }, []);
+
+ useEffect(() => {
+ let cancelled = false;
+ Promise.all([
+ listActivityPosters(),
+ request('PARTNER_H5', '/partner/assoc', { silent: true }),
+ getActivityPosterSelection().catch(() => ({ posterId: null })),
+ ])
+ .then(([list, assoc, selection]) => {
+ if (cancelled) return;
+ setItems(list);
+ setQrcodeUrl(assoc.qrcodeUrl);
+ setPartnerId(assoc.partnerId);
+ const saved = selection.posterId && list.some((item) => item.id === selection.posterId)
+ ? selection.posterId
+ : null;
+ const fromQuery = initialId && list.some((item) => item.id === initialId) ? initialId : null;
+ const next = saved || fromQuery || NONE_ID;
+ setSelectedId(next);
+ if (!saved && fromQuery) {
+ void saveActivityPosterSelection(fromQuery).catch((err) => {
+ toastError(err instanceof Error ? err.message : '保存失败');
+ });
+ }
+ })
+ .catch((e) => toastError(e instanceof Error ? e.message : '加载活动图失败'))
+ .finally(() => {
+ if (!cancelled) setListLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [initialId]);
+
+ useEffect(() => {
+ if (selectedId === NONE_ID || previewUrls[selectedId] || blobsRef.current.has(selectedId)) return;
+ let cancelled = false;
+ setPreviewLoading(true);
+ fetchActivityPosterImage(selectedId)
+ .then((blob) => {
+ if (cancelled) return;
+ blobsRef.current.set(selectedId, blob);
+ const url = URL.createObjectURL(blob);
+ urlsRef.current.push(url);
+ setPreviewUrls((prev) => ({ ...prev, [selectedId]: url }));
+ })
+ .catch((e) => {
+ if (!cancelled) toastError(e instanceof Error ? e.message : '预览失败');
+ })
+ .finally(() => {
+ if (!cancelled) setPreviewLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ };
+ }, [selectedId, previewUrls]);
+
+ const selected = items.find((item) => item.id === selectedId) ?? null;
+ const previewUrl = selectedId !== NONE_ID ? previewUrls[selectedId] : undefined;
+
+ async function downloadSelected() {
+ setDownloading(true);
+ try {
+ if (selectedId === NONE_ID) {
+ const blob = await fetchAssocQrcodeImage();
+ if (isWechatEnv()) {
+ const url = URL.createObjectURL(blob);
+ urlsRef.current.push(url);
+ setQrcodeUrl(url);
+ toastSuccess('请长按上方图片保存到相册');
+ return;
+ }
+ downloadBlob(blob, `partner-assoc-${partnerId || 'qr'}.png`);
+ toastSuccess('已开始下载');
+ return;
+ }
+ if (!selected) return;
+ let blob = blobsRef.current.get(selected.id);
+ if (!blob) {
+ blob = await fetchActivityPosterImage(selected.id);
+ blobsRef.current.set(selected.id, blob);
+ }
+ if (isWechatEnv()) {
+ let url = previewUrls[selected.id];
+ if (!url) {
+ url = URL.createObjectURL(blob);
+ urlsRef.current.push(url);
+ setPreviewUrls((prev) => ({ ...prev, [selected.id]: url }));
+ }
+ toastSuccess('请长按上方图片保存到相册');
+ return;
+ }
+ downloadBlob(blob, `activity-poster-${selected.id}.png`);
+ toastSuccess('已开始下载');
+ } catch (e) {
+ toastError(e instanceof Error ? e.message : '下载失败');
+ } finally {
+ setDownloading(false);
+ }
+ }
+
+ async function selectPoster(id: string) {
+ const prev = selectedId;
+ setSelectedId(id);
+ try {
+ await saveActivityPosterSelection(id === NONE_ID ? null : id);
+ } catch (e) {
+ setSelectedId(prev);
+ toastError(e instanceof Error ? e.message : '保存失败');
+ }
+ }
+
+ function renderRadioRow(id: string, label: string) {
+ return (
+
+ );
+ }
+
+ if (listLoading) {
+ return 加载活动图…
;
+ }
+
+ return (
+
+
+ {renderRadioRow(NONE_ID, '无')}
+ {selectedId === NONE_ID && (
+
+ {qrcodeUrl ? (
+
+

+
+ ) : (
+
关联码尚未生成
+ )}
+
+ {isWechatEnv() && (
+
+ 微信内请长按上方图片保存
+
+ )}
+
+ )}
+
+
+ {items.map((item) => (
+
+ {renderRadioRow(item.id, item.title)}
+ {selectedId === item.id && (
+
+
+

+ {previewLoading && !previewUrl && (
+
正在贴入二维码…
+ )}
+
+ {item.copyText ? (
+
{item.copyText}
+ ) : (
+
暂无推广文案
+ )}
+
+
+
+
+ {previewUrl && isWechatEnv() && (
+
+ 微信内请长按上方图片保存
+
+ )}
+
+ )}
+
+ ))}
+
+ );
+}
diff --git a/apps/h5-partner/src/lib/activity-posters.ts b/apps/h5-partner/src/lib/activity-posters.ts
new file mode 100644
index 0000000..31f555a
--- /dev/null
+++ b/apps/h5-partner/src/lib/activity-posters.ts
@@ -0,0 +1,74 @@
+import type { ActivityPosterItem, ActivityPosterSelection } from '@dukang/shared-types';
+import { request } from './api';
+
+export async function listActivityPosters() {
+ const data = await request('PARTNER_H5', '/partner/activity-posters', {
+ silent: true,
+ });
+ return Array.isArray(data) ? data : [];
+}
+
+export async function fetchAssocQrcodeImage(): Promise {
+ const token = localStorage.getItem('accessToken');
+ const res = await fetch('/api/v1/partner/assoc/qrcode', {
+ headers: {
+ Authorization: token ? `Bearer ${token}` : '',
+ 'X-Client-App': 'PARTNER_H5',
+ },
+ });
+ if (!res.ok) throw new Error('下载二维码失败');
+ return res.blob();
+}
+
+export async function fetchActivityPosterImage(id: string): Promise {
+ const token = localStorage.getItem('accessToken');
+ const res = await fetch(`/api/v1/partner/activity-posters/${id}/image`, {
+ headers: {
+ Authorization: token ? `Bearer ${token}` : '',
+ 'X-Client-App': 'PARTNER_H5',
+ },
+ });
+ if (!res.ok) {
+ const json = await res.json().catch(() => null) as { message?: string } | null;
+ throw new Error(json?.message || '生成活动图失败');
+ }
+ return res.blob();
+}
+
+export function downloadBlob(blob: Blob, fileName: string) {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+export async function getActivityPosterSelection() {
+ return request('PARTNER_H5', '/partner/activity-posters/selection', {
+ silent: true,
+ });
+}
+
+export async function saveActivityPosterSelection(posterId: string | null) {
+ return request('PARTNER_H5', '/partner/activity-posters/selection', {
+ method: 'PUT',
+ body: JSON.stringify({ posterId }),
+ silent: true,
+ });
+}
+
+export async function copyActivityCopy(text: string) {
+ const value = text.trim();
+ if (!value) throw new Error('暂无文案');
+ try {
+ await navigator.clipboard.writeText(value);
+ } catch {
+ const ta = document.createElement('textarea');
+ ta.value = value;
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+ }
+}
diff --git a/apps/h5-partner/src/pages/ActivityPostersPage.tsx b/apps/h5-partner/src/pages/ActivityPostersPage.tsx
new file mode 100644
index 0000000..8fc6eff
--- /dev/null
+++ b/apps/h5-partner/src/pages/ActivityPostersPage.tsx
@@ -0,0 +1,25 @@
+import { useEffect } from 'react';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+import PageHeader from '@dukang/shared-ui/PageHeader';
+import ActivityPosterPicker from '../components/ActivityPosterPicker';
+import { usePartnerPageView } from '../lib/usePageView';
+
+export default function ActivityPostersPage() {
+ usePartnerPageView('partner_activity_posters_view');
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const initialId = searchParams.get('id');
+
+ useEffect(() => {
+ document.title = '活动图';
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/apps/h5-partner/src/pages/CenterPage.tsx b/apps/h5-partner/src/pages/CenterPage.tsx
index 2c5f672..a2067ae 100644
--- a/apps/h5-partner/src/pages/CenterPage.tsx
+++ b/apps/h5-partner/src/pages/CenterPage.tsx
@@ -219,6 +219,15 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
chevron_right
+
+
+ chevron_right
+
diff --git a/apps/h5-partner/src/pages/UsersManagePage.tsx b/apps/h5-partner/src/pages/UsersManagePage.tsx
index 2efe6de..f893893 100644
--- a/apps/h5-partner/src/pages/UsersManagePage.tsx
+++ b/apps/h5-partner/src/pages/UsersManagePage.tsx
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
-import { useNavigate, useSearchParams } from 'react-router-dom';
+import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
import type {
PartnerAssocSummary,
@@ -7,6 +7,11 @@ import type {
PartnerAssocUserSort,
} from '@dukang/shared-types';
import { request } from '../lib/api';
+import {
+ downloadBlob,
+ fetchActivityPosterImage,
+ fetchAssocQrcodeImage,
+} from '../lib/activity-posters';
import { toastError, toastSuccess } from '../lib/toast';
import { isWechatEnv } from '../lib/weixin';
import { usePartnerPageView } from '../lib/usePageView';
@@ -36,6 +41,8 @@ export default function UsersManagePage() {
const [searchParams] = useSearchParams();
const [summary, setSummary] = useState
(null);
const [previewUrl, setPreviewUrl] = useState(null);
+ const [heroUrl, setHeroUrl] = useState(null);
+ const [heroLoading, setHeroLoading] = useState(false);
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
@@ -54,6 +61,34 @@ export default function UsersManagePage() {
if (previewUrl) URL.revokeObjectURL(previewUrl);
}, [previewUrl]);
+ useEffect(() => {
+ const posterId = summary?.activityPosterId;
+ if (!posterId) {
+ setHeroUrl(null);
+ setHeroLoading(false);
+ return;
+ }
+ let cancelled = false;
+ const acc = { url: null as string | null };
+ setHeroLoading(true);
+ fetchActivityPosterImage(posterId)
+ .then((blob) => {
+ if (cancelled) return;
+ acc.url = URL.createObjectURL(blob);
+ setHeroUrl(acc.url);
+ })
+ .catch(() => {
+ if (!cancelled) setHeroUrl(null);
+ })
+ .finally(() => {
+ if (!cancelled) setHeroLoading(false);
+ });
+ return () => {
+ cancelled = true;
+ if (acc.url) URL.revokeObjectURL(acc.url);
+ };
+ }, [summary?.activityPosterId]);
+
const loadSummary = useCallback(async () => {
const data = await request('PARTNER_H5', '/partner/assoc');
setSummary(data);
@@ -93,31 +128,23 @@ export default function UsersManagePage() {
if (uid) navigate(`/users/${uid}/orders`, { replace: true });
}, [searchParams, navigate]);
- async function downloadQr() {
- const token = localStorage.getItem('accessToken');
+ async function downloadMainImage() {
+ const posterId = summary?.activityPosterId;
try {
- const res = await fetch('/api/v1/partner/assoc/qrcode', {
- headers: {
- Authorization: token ? `Bearer ${token}` : '',
- 'X-Client-App': 'PARTNER_H5',
- },
- });
- if (!res.ok) throw new Error('下载失败');
- const blob = await res.blob();
- const url = URL.createObjectURL(blob);
+ const blob = posterId
+ ? await fetchActivityPosterImage(posterId)
+ : await fetchAssocQrcodeImage();
if (isWechatEnv()) {
- setPreviewUrl(url);
+ setPreviewUrl(URL.createObjectURL(blob));
toastSuccess('请长按图片保存到相册');
return;
}
- const a = document.createElement('a');
- a.href = url;
- a.download = `partner-assoc-${summary?.partnerId || 'qr'}.png`;
- a.click();
- URL.revokeObjectURL(url);
+ downloadBlob(blob, posterId
+ ? `activity-poster-${posterId}.png`
+ : `partner-assoc-${summary?.partnerId || 'qr'}.png`);
toastSuccess('已开始下载');
} catch (e) {
- if (summary?.qrcodeUrl) {
+ if (!posterId && summary?.qrcodeUrl) {
setPreviewUrl(summary.qrcodeUrl);
toastSuccess('请长按图片保存到相册');
return;
@@ -153,9 +180,17 @@ export default function UsersManagePage() {
用户扫码后首次锁定,后续购酒计入关联订单
- {summary?.qrcodeUrl ? (
+ {summary?.activityPosterId && (previewUrl || heroUrl) ? (
+ ) : summary?.activityPosterId && heroLoading ? (
+ 正在生成活动图…
+ ) : previewUrl || summary?.qrcodeUrl ? (
+
@@ -165,14 +200,26 @@ export default function UsersManagePage() {
已关联 {summary?.userCount ?? total} 人
-