feat(ops): v4.0.2 活动图模板、合伙人选择持久化与用户管理主图

HQ 上传底图与码栏;合伙人单选写入 partner_account.activity_poster_id,用户管理下次登录仍显示同一张图。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-30 21:36:43 +08:00
parent 0ff61c2cd1
commit 3166467518
39 changed files with 1872 additions and 35 deletions
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { activityPosterQrSlotPx } from './activity-poster';
describe('activityPosterQrSlotPx', () => {
it('uses width for square size', () => {
expect(activityPosterQrSlotPx(1000, 2000, 10, 20, 18)).toEqual({
left: 100,
top: 400,
size: 180,
});
});
it('clamps slot inside the canvas', () => {
expect(activityPosterQrSlotPx(100, 80, 90, 90, 30)).toEqual({
left: 70,
top: 50,
size: 30,
});
});
});
@@ -0,0 +1,72 @@
export const ACTIVITY_POSTER_STATUSES = ['ACTIVE', 'DISABLED'] as const;
export type ActivityPosterStatus = (typeof ACTIVITY_POSTER_STATUSES)[number];
export const ACTIVITY_POSTER_STATUS_LABELS: Record<ActivityPosterStatus, string> = {
ACTIVE: '上架',
DISABLED: '下架',
};
export const DEFAULT_ACTIVITY_POSTER_QR_SLOT = {
qrXPct: 78,
qrYPct: 78,
qrSizePct: 18,
} as const;
export type ActivityPosterQrSlot = {
qrXPct: number;
qrYPct: number;
qrSizePct: number;
};
export type ActivityPosterItem = {
id: string;
title: string;
copyText: string;
imageUrl: string;
qrXPct: number;
qrYPct: number;
qrSizePct: number;
sortOrder: number;
status: ActivityPosterStatus;
createdAt: string;
updatedAt: string;
};
export type ActivityPosterUpsertRequest = {
title: string;
copyText?: string;
imageUrl: string;
qrXPct: number;
qrYPct: number;
qrSizePct: number;
sortOrder?: number;
status?: ActivityPosterStatus;
};
export type ActivityPosterStatusRequest = {
status: ActivityPosterStatus;
};
export type ActivityPosterSelection = {
posterId: string | null;
};
export type ActivityPosterSelectionRequest = {
posterId?: string | null;
};
/** 码栏百分比 → 像素。size 相对图宽,保证正方形;落点夹在画布内。 */
export function activityPosterQrSlotPx(
width: number,
height: number,
qrXPct: number,
qrYPct: number,
qrSizePct: number,
): { left: number; top: number; size: number } {
const w = Math.max(1, Math.round(width));
const h = Math.max(1, Math.round(height));
const size = Math.max(1, Math.min(w, Math.round((qrSizePct / 100) * w)));
const left = Math.max(0, Math.min(w - size, Math.round((qrXPct / 100) * w)));
const top = Math.max(0, Math.min(h - size, Math.round((qrYPct / 100) * h)));
return { left, top, size };
}
@@ -8,6 +8,7 @@ export const HQ_LIST_COLUMN_KEYS = [
'orders',
'promo-codes',
'promo-code-users',
'activity-posters',
'wecom-bots',
'wecom-pushes',
'llm-configs',
@@ -6,6 +6,7 @@ export const HQ_PERMISSION_CATALOG = [
{ key: 'products', label: '商品管理', group: '业务' },
{ key: 'orders', label: '订单管理', group: '业务' },
{ key: 'promo_codes', label: '推广码', group: '业务' },
{ key: 'activity_posters', label: '活动图', group: '业务' },
{ key: 'stores', label: '门店列表', group: '业务' },
{ key: 'store_audits', label: '门店审核', group: '业务' },
{ key: 'store_ratings', label: '门店评价', group: '业务' },
@@ -178,6 +179,7 @@ export const HQ_ROLE_DEFAULT_PERMISSIONS: Record<HqAdminRoleValue, HqPermissionK
'products',
'orders',
'promo_codes',
'activity_posters',
...OPS_STORE_KEYS,
'store_categories_delete',
'partners',
+1
View File
@@ -22,6 +22,7 @@ export * from './hq-permissions';
export * from './hq-list-columns';
export * from './partner';
export * from './partner-assoc';
export * from './activity-poster';
export * from './shop';
export * from './city-partner';
export * from './city-warehouse';
@@ -16,6 +16,8 @@ export type PartnerAssocSummary = {
userCount: number;
companyName?: string | null;
name: string;
/** 所选活动图;空表示用户管理主图只用关联码 */
activityPosterId?: string | null;
};
export type PartnerAssocStats = {