feat(ops): v4.0.2 活动图模板、合伙人选择持久化与用户管理主图
HQ 上传底图与码栏;合伙人单选写入 partner_account.activity_poster_id,用户管理下次登录仍显示同一张图。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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() {
|
||||
<Route path="/center/settlement" element={<SettlementPage />} />
|
||||
<Route path="/center/staff" element={<StaffListPage />} />
|
||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||
<Route path="/center/activity-posters" element={<ActivityPostersPage />} />
|
||||
<Route path="/center/assoc" element={<Navigate to="/users" replace />} />
|
||||
<Route path="/center/assoc/users" element={<Navigate to="/users" replace />} />
|
||||
<Route path="/users/orders" element={<AssocOrdersPage />} />
|
||||
|
||||
@@ -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<ActivityPosterItem[]>([]);
|
||||
const [qrcodeUrl, setQrcodeUrl] = useState<string | null>(null);
|
||||
const [partnerId, setPartnerId] = useState<string>('');
|
||||
const [selectedId, setSelectedId] = useState<string>(NONE_ID);
|
||||
const [previewUrls, setPreviewUrls] = useState<Record<string, string>>({});
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const blobsRef = useRef(new Map<string, Blob>());
|
||||
const urlsRef = useRef<string[]>([]);
|
||||
|
||||
useEffect(() => () => {
|
||||
urlsRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
Promise.all([
|
||||
listActivityPosters(),
|
||||
request<PartnerAssocSummary>('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 (
|
||||
<label className="partner-activity-radio-row">
|
||||
<span className="headline-md">{label}</span>
|
||||
<input
|
||||
type="radio"
|
||||
name="activity-poster"
|
||||
checked={selectedId === id}
|
||||
onChange={() => void selectPoster(id)}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
if (listLoading) {
|
||||
return <div className="empty">加载活动图…</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="partner-activity-picker">
|
||||
<section className="partner-bill-card" style={{ overflow: 'hidden' }}>
|
||||
{renderRadioRow(NONE_ID, '无')}
|
||||
{selectedId === NONE_ID && (
|
||||
<div className="partner-activity-panel">
|
||||
{qrcodeUrl ? (
|
||||
<div className="partner-activity-preview partner-activity-preview--qr">
|
||||
<img src={qrcodeUrl} alt="关联码" />
|
||||
</div>
|
||||
) : (
|
||||
<p className="body-md text-muted" style={{ marginBottom: 12 }}>关联码尚未生成</p>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
disabled={downloading || !qrcodeUrl}
|
||||
onClick={() => void downloadSelected()}
|
||||
>
|
||||
{downloading ? '下载中…' : '下载二维码'}
|
||||
</button>
|
||||
{isWechatEnv() && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
||||
微信内请长按上方图片保存
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{items.map((item) => (
|
||||
<section key={item.id} className="partner-bill-card" style={{ marginTop: 12, overflow: 'hidden' }}>
|
||||
{renderRadioRow(item.id, item.title)}
|
||||
{selectedId === item.id && (
|
||||
<div className="partner-activity-panel">
|
||||
<div className="partner-activity-preview">
|
||||
<img src={previewUrl || item.imageUrl} alt={item.title} />
|
||||
{previewLoading && !previewUrl && (
|
||||
<div className="partner-activity-preview-mask">正在贴入二维码…</div>
|
||||
)}
|
||||
</div>
|
||||
{item.copyText ? (
|
||||
<p className="body-md" style={{ whiteSpace: 'pre-wrap', margin: '12px 0' }}>{item.copyText}</p>
|
||||
) : (
|
||||
<p className="body-md text-muted" style={{ margin: '12px 0' }}>暂无推广文案</p>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-outline"
|
||||
onClick={() => {
|
||||
void copyActivityCopy(item.copyText)
|
||||
.then(() => toastSuccess('文案已复制'))
|
||||
.catch((e) => toastError(e instanceof Error ? e.message : '复制失败'));
|
||||
}}
|
||||
>
|
||||
复制文案
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-btn-primary"
|
||||
style={{ flex: 1, width: 'auto', padding: 0, height: 48 }}
|
||||
disabled={downloading || previewLoading}
|
||||
onClick={() => void downloadSelected()}
|
||||
>
|
||||
{downloading || previewLoading ? '生成中…' : '下载活动图'}
|
||||
</button>
|
||||
</div>
|
||||
{previewUrl && isWechatEnv() && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 12 }}>
|
||||
微信内请长按上方图片保存
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { ActivityPosterItem, ActivityPosterSelection } from '@dukang/shared-types';
|
||||
import { request } from './api';
|
||||
|
||||
export async function listActivityPosters() {
|
||||
const data = await request<ActivityPosterItem[]>('PARTNER_H5', '/partner/activity-posters', {
|
||||
silent: true,
|
||||
});
|
||||
return Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
export async function fetchAssocQrcodeImage(): Promise<Blob> {
|
||||
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<Blob> {
|
||||
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<ActivityPosterSelection>('PARTNER_H5', '/partner/activity-posters/selection', {
|
||||
silent: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveActivityPosterSelection(posterId: string | null) {
|
||||
return request<ActivityPosterSelection>('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);
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<PageHeader title="活动图" onBack={() => navigate(-1)} />
|
||||
<div style={{ padding: 16 }}>
|
||||
<ActivityPosterPicker initialId={initialId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -219,6 +219,15 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
<Link to="/center/activity-posters" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
<span className="material-symbols-outlined">image</span>
|
||||
</div>
|
||||
<span className="body-md" style={{ fontSize: 16 }}>活动图</span>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</Link>
|
||||
<Link to="/users" className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
|
||||
@@ -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<PartnerAssocSummary | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [heroUrl, setHeroUrl] = useState<string | null>(null);
|
||||
const [heroLoading, setHeroLoading] = useState(false);
|
||||
const [items, setItems] = useState<PartnerAssocUserItem[]>([]);
|
||||
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<PartnerAssocSummary>('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() {
|
||||
<p className="body-md text-muted" style={{ marginBottom: 12 }}>
|
||||
用户扫码后首次锁定,后续购酒计入关联订单
|
||||
</p>
|
||||
{summary?.qrcodeUrl ? (
|
||||
{summary?.activityPosterId && (previewUrl || heroUrl) ? (
|
||||
<img
|
||||
src={previewUrl || summary.qrcodeUrl}
|
||||
src={previewUrl || heroUrl || ''}
|
||||
alt="活动图"
|
||||
style={{ width: '100%', maxWidth: 360, background: '#f5f5f5' }}
|
||||
/>
|
||||
) : summary?.activityPosterId && heroLoading ? (
|
||||
<p className="body-md text-muted">正在生成活动图…</p>
|
||||
) : previewUrl || summary?.qrcodeUrl ? (
|
||||
<img
|
||||
src={previewUrl || summary?.qrcodeUrl || ''}
|
||||
alt="关联码"
|
||||
style={{ width: 200, height: 200, background: '#fff' }}
|
||||
/>
|
||||
@@ -165,14 +200,26 @@ export default function UsersManagePage() {
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||||
已关联 {summary?.userCount ?? total} 人
|
||||
</p>
|
||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadQr()}>
|
||||
下载二维码
|
||||
<button type="button" className="partner-btn-primary" style={{ marginTop: 16 }} onClick={() => void downloadMainImage()}>
|
||||
{summary?.activityPosterId ? '下载活动图' : '下载二维码'}
|
||||
</button>
|
||||
{previewUrl && isWechatEnv() && (
|
||||
<p className="label-md text-muted" style={{ marginTop: 8 }}>微信内请长按上方图片保存</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Link to="/center/activity-posters" className="partner-menu-card" style={{ display: 'block', marginBottom: 20, textDecoration: 'none', color: 'inherit' }}>
|
||||
<div className="partner-menu-item">
|
||||
<div className="partner-menu-item-left">
|
||||
<div className="partner-menu-icon">
|
||||
<span className="material-symbols-outlined">image</span>
|
||||
</div>
|
||||
<span className="body-md" style={{ fontSize: 16 }}>活动图</span>
|
||||
</div>
|
||||
<span className="material-symbols-outlined text-muted">chevron_right</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<h2 className="headline-md" style={{ marginBottom: 12 }}>已关联用户</h2>
|
||||
<div className="partner-search">
|
||||
<span className="material-symbols-outlined">search</span>
|
||||
|
||||
@@ -3971,3 +3971,119 @@ header:has(> .app-page-title:only-child),
|
||||
padding: 20px 20px calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.partner-activity-scroll {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.partner-activity-chip {
|
||||
flex: 0 0 92px;
|
||||
border: 1px solid var(--color-outline-variant, #eee);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.partner-activity-chip img {
|
||||
width: 100%;
|
||||
height: 92px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.partner-activity-chip span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.partner-activity-chip.is-active {
|
||||
border-color: var(--color-heritage-red);
|
||||
box-shadow: 0 0 0 1px var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.partner-activity-preview {
|
||||
position: relative;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.partner-activity-preview img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.partner-activity-preview-mask {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.62);
|
||||
color: var(--color-on-surface-variant, #666);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.partner-activity-center-card {
|
||||
flex: 0 0 120px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.partner-activity-center-card img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
border-radius: 12px;
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.partner-activity-center-card span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.partner-activity-radio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.partner-activity-radio-row input[type='radio'] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
accent-color: var(--color-heritage-red);
|
||||
}
|
||||
|
||||
.partner-activity-panel {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.partner-activity-preview--qr {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.partner-activity-preview--qr img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user