From 0b61aa4a14e12e4ebcb53e39870f1a28b45361aa Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 7 Jul 2026 18:31:55 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E7=94=A8=E6=88=B7=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E5=92=8C=E9=A2=86=E5=8F=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/frontend-react/src/App.tsx | 9 +- .../src/pages/admin/AdminAuditLogsPage.tsx | 231 ++++ .../src/pages/admin/AdminUsersPage.tsx | 454 +++++++ .../src/pages/claim/ClaimPage.tsx | 1162 +++++++++++++++++ apps/frontend-react/src/styles/main.css | 523 ++++++++ 5 files changed, 2376 insertions(+), 3 deletions(-) create mode 100644 apps/frontend-react/src/pages/admin/AdminAuditLogsPage.tsx create mode 100644 apps/frontend-react/src/pages/admin/AdminUsersPage.tsx create mode 100644 apps/frontend-react/src/pages/claim/ClaimPage.tsx diff --git a/apps/frontend-react/src/App.tsx b/apps/frontend-react/src/App.tsx index 61111624..721e29d4 100644 --- a/apps/frontend-react/src/App.tsx +++ b/apps/frontend-react/src/App.tsx @@ -6,11 +6,14 @@ import { getAdminRole, hasAdminSession } from '@/utils/admin-auth' import AdminLayout from '@/layouts/AdminLayout' const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage')) +const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')) const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage')) const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage')) const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage')) const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage')) const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage')) +const AdminUsersPage = lazy(() => import('@/pages/admin/AdminUsersPage')) +const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage')) const NotFoundPage = lazy(() => import('@/pages/NotFoundPage')) function RequireAdmin() { @@ -55,14 +58,14 @@ export default function App() { } /> } /> }> - } /> + } /> } /> } /> - } /> + } /> - } /> + } /> } /> diff --git a/apps/frontend-react/src/pages/admin/AdminAuditLogsPage.tsx b/apps/frontend-react/src/pages/admin/AdminAuditLogsPage.tsx new file mode 100644 index 00000000..8cea755d --- /dev/null +++ b/apps/frontend-react/src/pages/admin/AdminAuditLogsPage.tsx @@ -0,0 +1,231 @@ +import { ReloadOutlined, SearchOutlined } from '@ant-design/icons' +import { Alert, Button, Card, DatePicker, Empty, Form, Input, Select, Space, Table, Typography } from 'antd' +import type { TableColumnsType } from 'antd' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' +import { useMemo } from 'react' +import { useSearchParams } from 'react-router' + +import PageHeader from '@/components/admin/PageHeader' +import StatusTag from '@/components/admin/StatusTag' +import { fetchAdminAuditLogs } from '@/services/admin' +import type { AdminAuditLogItem } from '@/types/admin' +import { hasAdminRole } from '@/utils/admin-auth' +import { formatAuditAction, formatAuditTargetType } from '@/utils/admin-display' +import { adminAuditActionOptions, adminAuditTargetTypeOptions } from '@/utils/admin-options' +import { formatAdminDateTime } from '@/utils/admin-time' +import { stringifyDisplayJson } from '@/utils/date-time' + +type AuditFilterForm = { + actorUsername?: string + action?: string + targetType?: string + dateRange?: unknown +} + +export default function AdminAuditLogsPage() { + const isAdmin = hasAdminRole('admin') + const [searchParams, setSearchParams] = useSearchParams() + const page = Number(searchParams.get('page') || 1) || 1 + const pageSize = Number(searchParams.get('pageSize') || 20) || 20 + const filters = { + actorUsername: searchParams.get('actorUsername') || '', + action: searchParams.get('action') || '', + targetType: searchParams.get('targetType') || '', + dateFrom: searchParams.get('dateFrom') || '', + dateTo: searchParams.get('dateTo') || '', + } + const queryParams = useMemo( + () => ({ page, pageSize, ...filters }), + [ + filters.action, + filters.actorUsername, + filters.dateFrom, + filters.dateTo, + filters.targetType, + page, + pageSize, + ], + ) + const query = useQuery({ + queryKey: ['admin-audit-logs', queryParams], + enabled: isAdmin, + queryFn: () => fetchAdminAuditLogs(queryParams), + }) + const data = query.data?.data + + const columns: TableColumnsType = [ + { + title: '时间', + dataIndex: 'createdAt', + width: 180, + render: (value) => formatAdminDateTime(value), + }, + { + title: '操作人', + minWidth: 140, + render: (_, row) => ( +
+ {row.actorUsername || '-'} + +
+ ), + }, + { + title: '动作', + dataIndex: 'action', + minWidth: 180, + render: (value) => {formatAuditAction(value)}, + }, + { + title: '目标', + minWidth: 180, + render: (_, row) => ( + + {formatAuditTargetType(row.targetType)} #{row.targetId || '-'} + + ), + }, + { + title: '摘要', + minWidth: 260, + render: (_, row) => {formatPayload(row.payload)}, + }, + ] + + function applyFilters(values: AuditFilterForm) { + const [dateFrom, dateTo] = Array.isArray(values.dateRange) + ? values.dateRange.map((item: { format?: (pattern: string) => string }) => + item?.format ? item.format('YYYY-MM-DD') : '', + ) + : ['', ''] + + setSearchParams( + cleanParams({ + actorUsername: values.actorUsername, + action: values.action, + targetType: values.targetType, + dateFrom, + dateTo, + page: 1, + pageSize, + }), + ) + } + + function resetFilters() { + setSearchParams(cleanParams({ page: 1, pageSize })) + } + + return ( +
+ 共 {data?.pagination.total || 0} 条审计记录} + /> + + {!isAdmin ? ( + + ) : ( + <> + + + layout="inline" + initialValues={{ + actorUsername: filters.actorUsername, + action: filters.action, + targetType: filters.targetType, + dateRange: + filters.dateFrom && filters.dateTo + ? [dayjs(filters.dateFrom), dayjs(filters.dateTo)] + : undefined, + }} + onFinish={applyFilters} + className="filter-form" + > + + + + + + + + + + + + + + + + + + + {query.error ? ( + + ) : null} + + + + rowKey="logId" + loading={query.isLoading || query.isFetching} + columns={columns} + dataSource={data?.items || []} + locale={{ emptyText: }} + scroll={{ x: 980 }} + pagination={{ + current: data?.pagination.page || page, + pageSize: data?.pagination.pageSize || pageSize, + total: data?.pagination.total || 0, + showTotal: (total) => `共 ${total} 条`, + onChange: (nextPage, nextPageSize) => { + setSearchParams( + cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }), + ) + }, + }} + /> + + + )} +
+ ) +} + +function formatPayload(payload: Record) { + const text = stringifyDisplayJson(payload, 0) + return text.length > 140 ? `${text.slice(0, 140)}...` : text +} + +function cleanParams(params: Record) { + const next = new URLSearchParams() + Object.entries(params).forEach(([key, value]) => { + const normalized = String(value ?? '').trim() + if (normalized) { + next.set(key, normalized) + } + }) + return next +} diff --git a/apps/frontend-react/src/pages/admin/AdminUsersPage.tsx b/apps/frontend-react/src/pages/admin/AdminUsersPage.tsx new file mode 100644 index 00000000..f2a440ca --- /dev/null +++ b/apps/frontend-react/src/pages/admin/AdminUsersPage.tsx @@ -0,0 +1,454 @@ +import { + DownOutlined, + PlusOutlined, + ReloadOutlined, + SearchOutlined, +} from '@ant-design/icons' +import { + Alert, + App, + Button, + Card, + Dropdown, + Empty, + Form, + Input, + Modal, + Result, + Select, + Space, + Table, + Tag, + Typography, +} from 'antd' +import type { MenuProps, TableColumnsType } from 'antd' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import { useMemo, useState } from 'react' +import { useSearchParams } from 'react-router' + +import PageHeader from '@/components/admin/PageHeader' +import StatusTag from '@/components/admin/StatusTag' +import { + createAdminUser, + fetchAdminUsers, + resetAdminUserPassword, + updateAdminUserRole, + updateAdminUserStatus, +} from '@/services/admin' +import type { AdminRole, AdminUserListItem } from '@/types/admin' +import { getAdminUserId, hasAdminRole } from '@/utils/admin-auth' +import { adminUserRoleOptions, adminUserStatusOptions } from '@/utils/admin-options' +import { formatAdminDateTime } from '@/utils/admin-time' + +type UserFilterForm = { + username?: string + role?: string + status?: string +} + +type CreateUserForm = { + username: string + password: string + role: AdminRole +} + +const USER_ROLE_OPTIONS = [ + { label: '普通运营', value: 'operator' }, + { label: '客服', value: 'support' }, + { label: '管理员', value: 'admin' }, +] + +export default function AdminUsersPage() { + const isAdmin = hasAdminRole('admin') + const currentUserId = getAdminUserId() + const queryClient = useQueryClient() + const { message, modal } = App.useApp() + const [searchParams, setSearchParams] = useSearchParams() + const [createForm] = Form.useForm() + const [creating, setCreating] = useState(false) + const [actionLoadingId, setActionLoadingId] = useState(null) + const page = Number(searchParams.get('page') || 1) || 1 + const pageSize = Number(searchParams.get('pageSize') || 20) || 20 + const filters = { + username: searchParams.get('username') || '', + role: searchParams.get('role') || '', + status: searchParams.get('status') || '', + } + const queryParams = useMemo( + () => ({ page, pageSize, ...filters }), + [filters.role, filters.status, filters.username, page, pageSize], + ) + const query = useQuery({ + queryKey: ['admin-users', queryParams], + enabled: isAdmin, + queryFn: () => fetchAdminUsers(queryParams), + }) + const data = query.data?.data + + async function reloadUsers() { + await queryClient.invalidateQueries({ queryKey: ['admin-users'] }) + } + + async function submitCreate(values: CreateUserForm) { + setCreating(true) + + try { + await createAdminUser({ + username: values.username.trim(), + password: values.password.trim(), + role: values.role, + status: 'active', + }) + message.success('后台用户已创建') + createForm.resetFields() + setSearchParams(cleanParams({ ...filters, page: 1, pageSize })) + await reloadUsers() + } catch (error) { + message.error(error instanceof Error ? error.message : '创建后台用户失败') + } finally { + setCreating(false) + } + } + + function confirmUpdateRole(item: AdminUserListItem, nextRole: AdminRole) { + if (item.role === nextRole) return + + modal.confirm({ + title: '确认操作', + content: `确认将 ${item.username} 调整为${formatRoleLabel(nextRole)}吗?`, + okText: '继续执行', + cancelText: '取消', + centered: true, + onOk: async () => { + setActionLoadingId(item.userId) + try { + await updateAdminUserRole(item.userId, { role: nextRole }) + message.success('用户角色已更新') + await reloadUsers() + } catch (error) { + message.error(error instanceof Error ? error.message : '更新用户角色失败') + } finally { + setActionLoadingId(null) + } + }, + }) + } + + function confirmToggleStatus(item: AdminUserListItem) { + const nextStatus = item.status === 'active' ? 'disabled' : 'active' + + modal.confirm({ + title: '确认操作', + content: `确认将 ${item.username}${nextStatus === 'active' ? '启用' : '停用'}吗?`, + okText: '继续执行', + cancelText: '取消', + centered: true, + onOk: async () => { + setActionLoadingId(item.userId) + try { + await updateAdminUserStatus(item.userId, { status: nextStatus }) + message.success('用户状态已更新') + await reloadUsers() + } catch (error) { + message.error(error instanceof Error ? error.message : '更新用户状态失败') + } finally { + setActionLoadingId(null) + } + }, + }) + } + + function openResetPasswordModal(item: AdminUserListItem) { + const form = ModalFormStore.create<{ password: string }>() + + modal.confirm({ + title: '重置密码', + content: ( +
+ + + +
+ ), + okText: '提交', + cancelText: '取消', + centered: true, + onOk: async () => { + const values = await form.validate() + setActionLoadingId(item.userId) + try { + await resetAdminUserPassword(item.userId, { password: values.password.trim() }) + message.success('用户密码已重置') + await reloadUsers() + } catch (error) { + message.error(error instanceof Error ? error.message : '重置密码失败') + } finally { + setActionLoadingId(null) + } + }, + }) + } + + function applyFilters(values: UserFilterForm) { + setSearchParams(cleanParams({ ...values, page: 1, pageSize })) + } + + function resetFilters() { + setSearchParams(cleanParams({ page: 1, pageSize })) + } + + if (!isAdmin) { + return ( +
+ + +
+ ) + } + + const columns: TableColumnsType = [ + { + title: 'ID', + dataIndex: 'userId', + width: 72, + align: 'center', + }, + { + title: '账号', + dataIndex: 'username', + minWidth: 150, + render: (value, row) => ( + + {value} + {row.userId === currentUserId ? 当前账号 : null} + + ), + }, + { + title: '角色', + dataIndex: 'role', + width: 120, + render: (value: AdminRole) => formatRoleLabel(value), + }, + { + title: '状态', + dataIndex: 'status', + width: 140, + render: (value) => , + }, + { + title: '创建时间', + dataIndex: 'createdAt', + width: 170, + render: (value) => formatAdminDateTime(value), + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + width: 170, + render: (value) => formatAdminDateTime(value), + }, + { + title: '操作', + width: 310, + render: (_, row) => { + const isCurrent = row.userId === currentUserId + const roleMenuItems: MenuProps['items'] = USER_ROLE_OPTIONS.map((item) => ({ + key: item.value, + label: `设为${item.label}`, + disabled: row.role === item.value, + })) + + return ( + + confirmUpdateRole(row, key as AdminRole), + }} + > + + + + + + ) + }, + }, + ] + + return ( +
+ + + + + layout="inline" + initialValues={filters} + onFinish={applyFilters} + className="filter-form" + > + + + + + + + + + + + + + + + + + + form={createForm} + layout="inline" + initialValues={{ role: 'operator' }} + onFinish={submitCreate} + className="filter-form" + > + + + + + + + + onTicketCodeChange(event.target.value)} + onPressEnter={onSubmitTicket} + /> + + + + } + message={ + isBindingPreparing + ? '核销完成后会自动准备绑定资源。' + : '核销完成后会自动进入扫码绑定步骤。' + } + /> + + {flow.guideImages.length > 0 ? ( + +
+ {flow.guideImages.map((imageUrl, index) => ( +
+ {`步骤 +
步骤 {index + 1}
+
+ ))} +
+ + ), + }, + ]} + /> + ) : null} +
+ ) +} + +function ClaimBindingStep({ + snapshot, + qrCodeDataUrl, + refreshingRole, + confirmingRole, + rebindingRole, + onOpenBindUrl, + onRefreshRole, + onRebindRole, + onConfirmRole, +}: { + snapshot: ClaimSnapshot + qrCodeDataUrl: string + refreshingRole: boolean + confirmingRole: boolean + rebindingRole: boolean + onOpenBindUrl: (useCurrentPage?: boolean) => void + onRefreshRole: () => void + onRebindRole: () => void + onConfirmRole: () => void +}) { + const flow = snapshot.flow + if (!flow) { + return null + } + + return ( + + {snapshot.isBindingPrepared && qrCodeDataUrl ? ( +
+
+
Q区用Q扫
+ 绑定二维码 +
V区用V扫
+

长按保存相册进行扫码绑定

+
+ + +
+ ) : ( + } + message="系统正在准备绑定资源,请稍后自动刷新。" + /> + )} + +
+ + +
+ + {flow.role.isDefaultRole ? ( + + ) : null} + + + + + + + + + + + +
+ 最近刷新:{formatAdminDateTime(flow.role.refreshedAt)} + 链接有效期:{formatAdminDateTime(flow.binding.bindExpiresAt)} + {flow.binding.bindProbeMessage ? ( + + 链接检测:{flow.binding.bindProbeStatus || '-'} / {flow.binding.bindProbeMessage} + + ) : null} +
+
+ ) +} + +function ClaimConfirmStep({ + roleName, + roleId, + product, + redeeming, + rebindingRole, + onConfirmRedeem, + onRebindRole, +}: { + roleName: string + roleId: string + product: ClaimProductInfo | null + redeeming: boolean + rebindingRole: boolean + onConfirmRedeem: () => void + onRebindRole: () => void +}) { + return ( + + 第 3 步:确认兑换信息 +

请再次确认角色和商品信息,确认无误后再继续兑换。

+ +
+ + + + +
+ + + + + + + + + +
+ ) +} + +function ClaimResultStep({ snapshot }: { snapshot: ClaimSnapshot }) { + const flow = snapshot.flow + if (!flow) { + return null + } + + return ( + +
+ {snapshot.resultVariant === 'warning' ? ( + + ) : ( + + )} +
+ {snapshot.resultTitle} +

{snapshot.resultDescription}

+
+
+ +
+ + + + + + +
+ + + + +
+ ) +} + +function FeifeiClaimPanel({ + feifei, + order, + product, + taskLastError, + onOpen, +}: { + feifei: ClaimKuaishouFeifeiFlowInfo + order: ClaimOrderInfo | null + product: ClaimProductInfo | null + taskLastError?: string + onOpen: () => void +}) { + return ( + +
+ kuaishou-feifei + {product?.title || '商品领取'} +

{feifei.rechargeStatusLabel || '待领取'}

+
+ +
+ + +
+ + + + {taskLastError ? : null} +
+ ) +} + +function ClaimProductItems({ + product, + compact = false, +}: { + product: ClaimProductInfo | null + compact?: boolean +}) { + const items = getProductItems(product) + const hasMultipleItems = Boolean(product?.isBundle || items.length > 1) + + if (!items.length) { + return null + } + + return ( +
+
+ {hasMultipleItems ? '包含商品' : '商品明细'} + {hasMultipleItems ? {items.length} 项 : null} +
+ +
+ {items.map((item) => ( +
+ {item.name} + x{item.quantity} +
+ ))} +
+
+ ) +} + +function getProductItems(product: ClaimProductInfo | null) { + const items = Array.isArray(product?.items) ? product.items : [] + return items + .map((item) => ({ + key: item.cloudSkuId || item.name, + name: String(item.name || '').trim(), + quantity: Math.max(1, Number(item.quantity || 1) || 1), + })) + .filter((item) => item.name) +} + +function InfoTile({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +function InfoRow({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} diff --git a/apps/frontend-react/src/styles/main.css b/apps/frontend-react/src/styles/main.css index d02360f0..05127afd 100644 --- a/apps/frontend-react/src/styles/main.css +++ b/apps/frontend-react/src/styles/main.css @@ -172,6 +172,403 @@ select { margin-top: 0; } +.modal-form { + margin-top: 14px; +} + +.payload-text { + font-family: + ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 12px; + color: #4b5563; + word-break: break-all; +} + +.claim-page { + min-height: 100vh; + padding: 24px; + background: linear-gradient(180deg, #f8fafc 0%, #eef4fb 100%); + display: flex; + flex-direction: column; + align-items: center; + gap: 18px; +} + +.claim-header-card, +.claim-content-card { + width: min(680px, 100%); + border-color: rgba(148, 163, 184, 0.18); + box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08); +} + +.claim-header-card { + position: relative; +} + +.claim-header-card .ant-card-body, +.claim-content-card .ant-card-body { + display: flex; + flex-direction: column; + gap: 18px; +} + +.claim-header-copy { + min-width: 0; + padding-right: 220px; +} + +.claim-header-copy h1 { + margin: 0; + color: #0f172a; + font-size: 30px; + line-height: 1.25; + overflow-wrap: anywhere; +} + +.claim-header-copy p, +.claim-muted { + margin: 0; + color: #64748b; + line-height: 1.6; +} + +.claim-step-indicator { + position: absolute; + top: 24px; + right: 24px; + max-width: 210px; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 7px; +} + +.claim-step-dots { + display: flex; + gap: 8px; +} + +.claim-step-dot { + width: 32px; + height: 32px; + border-radius: 50%; + background: #e5e7eb; + color: #94a3b8; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; +} + +.claim-step-dot.active { + background: #1677ff; + color: #ffffff; +} + +.claim-step-progress-text { + color: #64748b; + font-size: 13px; + font-weight: 700; + line-height: 1.4; + text-align: right; + overflow-wrap: anywhere; +} + +.claim-content-card .ant-typography { + margin: 0; +} + +.claim-state-card .ant-card-body { + min-height: 240px; + align-items: center; + justify-content: center; + text-align: center; +} + +.claim-large-icon { + font-size: 44px; +} + +.claim-icon-success { + color: #16a34a; +} + +.claim-icon-warning { + color: #d97706; +} + +.claim-icon-info { + color: #1677ff; +} + +.claim-icon-error { + color: #dc2626; +} + +.claim-guide-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.claim-guide-figure { + margin: 0; + background: #f8fafc; + border: 1px solid #e5e7eb; + border-radius: 6px; + overflow: hidden; +} + +.claim-guide-figure .ant-image, +.claim-guide-figure img { + width: 100%; + display: block; +} + +.claim-guide-figure figcaption { + padding: 10px 12px 14px; + color: #475569; + font-size: 13px; +} + +.claim-qr-block { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 14px; + border-radius: 8px; + background: #f8fbff; + border: 1px solid #dbeafe; +} + +.claim-qr-guide-panel { + display: grid; + grid-template-columns: 42px minmax(0, 280px) 42px; + grid-template-areas: + 'left code right' + '. hint .'; + gap: 10px; + align-items: center; + justify-content: center; + width: 100%; + max-width: 410px; + min-width: 0; +} + +.claim-scan-label { + align-self: stretch; + min-height: 150px; + padding: 10px 6px; + color: #e11d48; + font-weight: 900; + font-size: 17px; + line-height: 1.2; + writing-mode: vertical-rl; + text-orientation: upright; + display: flex; + align-items: center; + justify-content: center; +} + +.claim-scan-label-left { + grid-area: left; +} + +.claim-scan-label-right { + grid-area: right; +} + +.claim-qr-code { + grid-area: code; + width: 100%; + max-width: 280px; + aspect-ratio: 1 / 1; + padding: 10px; + background: #ffffff; + border-radius: 8px; + border: 1px solid #dbeafe; +} + +.claim-save-hint { + grid-area: hint; + margin: 0; + justify-self: center; + color: #64748b; + font-size: 14px; + font-weight: 700; + line-height: 1.4; + text-align: center; +} + +.claim-role-panel { + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.claim-role-panel.pending { + opacity: 0.82; +} + +.claim-role-panel-item { + min-width: 0; + display: grid; + grid-template-columns: 88px minmax(0, 1fr); + align-items: center; + gap: 12px; +} + +.claim-role-panel-item span { + color: #64748b; + font-size: 13px; + line-height: 1.4; + white-space: nowrap; +} + +.claim-role-panel-item strong { + min-width: 0; + color: #0f172a; + font-size: 17px; + line-height: 1.45; + overflow-wrap: anywhere; +} + +.claim-action-row { + width: 100%; +} + +.claim-action-tooltip-wrap { + display: inline-flex; +} + +.claim-meta-footer { + display: flex; + flex-wrap: wrap; + gap: 4px 10px; + justify-content: center; + color: #94a3b8; + font-size: 11px; + line-height: 1.5; +} + +.claim-info-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.claim-info-item { + min-width: 0; + padding: 16px; + border-radius: 6px; + background: #f8fafc; + border: 1px solid #e5e7eb; + display: flex; + flex-direction: column; + gap: 6px; +} + +.claim-info-item span { + color: #64748b; + font-size: 13px; +} + +.claim-info-item strong { + color: #0f172a; + font-size: 17px; + overflow-wrap: anywhere; +} + +.claim-result-header { + display: flex; + align-items: flex-start; + gap: 14px; +} + +.claim-feifei-main { + display: grid; + gap: 8px; +} + +.claim-feifei-main p { + margin: 0; + color: #1677ff; + font-weight: 700; +} + +.claim-feifei-label { + color: #64748b; + font-size: 13px; +} + +.claim-product-items { + display: flex; + flex-direction: column; + gap: 10px; +} + +.claim-product-items-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + color: #64748b; + font-size: 13px; +} + +.claim-product-items-header strong { + color: #1677ff; + font-weight: 700; +} + +.claim-product-items-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.claim-product-item-row { + min-height: 44px; + padding: 10px 12px; + border: 1px solid #e5e7eb; + border-radius: 6px; + background: #f8fafc; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.claim-product-item-row span { + min-width: 0; + color: #0f172a; + font-weight: 700; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.claim-product-item-row strong { + flex: 0 0 auto; + color: #0f172a; + font-size: 15px; +} + +.claim-product-items.compact { + margin-top: 10px; + gap: 8px; +} + +.claim-product-items.compact .claim-product-items-list { + gap: 6px; +} + +.claim-product-items.compact .claim-product-item-row { + min-height: 36px; + padding: 8px 10px; + background: rgba(248, 250, 252, 0.72); +} + @media (max-width: 900px) { .admin-sider { position: fixed !important; @@ -193,3 +590,129 @@ select { display: grid; } } + +@media (max-width: 720px) { + .claim-page { + padding: 14px; + } + + .claim-header-card .ant-card-body, + .claim-content-card .ant-card-body { + gap: 14px; + } + + .claim-header-copy { + padding-right: 150px; + } + + .claim-header-copy h1 { + font-size: 24px; + line-height: 1.22; + } + + .claim-header-copy p { + font-size: 14px; + line-height: 1.4; + } + + .claim-step-indicator { + top: 16px; + right: 16px; + max-width: 140px; + } + + .claim-step-dots { + gap: 5px; + } + + .claim-step-dot { + width: 26px; + height: 26px; + font-size: 13px; + } + + .claim-step-progress-text { + font-size: 12px; + } + + .claim-guide-grid, + .claim-info-grid { + grid-template-columns: 1fr; + } + + .claim-qr-block { + padding: 12px; + } + + .claim-qr-guide-panel { + grid-template-columns: 34px minmax(0, 1fr) 34px; + gap: 8px; + max-width: 100%; + } + + .claim-scan-label { + min-height: 128px; + padding: 8px 4px; + font-size: 14px; + } + + .claim-qr-code { + max-width: 232px; + padding: 8px; + } + + .claim-save-hint { + font-size: 13px; + } + + .claim-role-panel-item { + grid-template-columns: 76px minmax(0, 1fr); + gap: 10px; + } + + .claim-role-panel-item span { + font-size: 12px; + } + + .claim-role-panel-item strong { + font-size: 16px; + } + + .claim-action-row { + display: flex; + flex-direction: column; + align-items: stretch; + } + + .claim-action-row .ant-space-item, + .claim-action-row .ant-btn, + .claim-action-tooltip-wrap { + width: 100%; + } + + .claim-meta-footer { + justify-content: flex-start; + } + + .claim-result-header { + display: grid; + } +} + +@media (max-width: 420px) { + .claim-header-copy { + padding-right: 124px; + } + + .claim-header-copy h1 { + font-size: 22px; + } + + .claim-header-copy p { + padding-right: 0; + } + + .claim-step-indicator { + max-width: 116px; + } +}