迁移用户审计和领取页
This commit is contained in:
@@ -6,11 +6,14 @@ import { getAdminRole, hasAdminSession } from '@/utils/admin-auth'
|
|||||||
import AdminLayout from '@/layouts/AdminLayout'
|
import AdminLayout from '@/layouts/AdminLayout'
|
||||||
|
|
||||||
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
|
const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage'))
|
||||||
|
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
||||||
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
||||||
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
||||||
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
||||||
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
|
const AdminTaskDetailPage = lazy(() => import('@/pages/admin/AdminTaskDetailPage'))
|
||||||
const AdminTasksPage = lazy(() => import('@/pages/admin/AdminTasksPage'))
|
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'))
|
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
|
||||||
|
|
||||||
function RequireAdmin() {
|
function RequireAdmin() {
|
||||||
@@ -55,14 +58,14 @@ export default function App() {
|
|||||||
<Route path="tasks" element={<AdminTasksPage />} />
|
<Route path="tasks" element={<AdminTasksPage />} />
|
||||||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||||||
<Route element={<RequireRole roles={['admin']} />}>
|
<Route element={<RequireRole roles={['admin']} />}>
|
||||||
<Route path="users" element={<NotFoundPage title="用户管理迁移中" />} />
|
<Route path="users" element={<AdminUsersPage />} />
|
||||||
<Route path="platform-shops" element={<NotFoundPage title="平台配置迁移中" />} />
|
<Route path="platform-shops" element={<NotFoundPage title="平台配置迁移中" />} />
|
||||||
<Route path="platform-fulfillment" element={<NotFoundPage title="履约配置迁移中" />} />
|
<Route path="platform-fulfillment" element={<NotFoundPage title="履约配置迁移中" />} />
|
||||||
<Route path="audit-logs" element={<NotFoundPage title="审计日志迁移中" />} />
|
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/claim/:token" element={<NotFoundPage title="领取页迁移中" />} />
|
<Route path="/claim/:token" element={<ClaimPage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -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<AdminAuditLogItem> = [
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
dataIndex: 'createdAt',
|
||||||
|
width: 180,
|
||||||
|
render: (value) => formatAdminDateTime(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作人',
|
||||||
|
minWidth: 140,
|
||||||
|
render: (_, row) => (
|
||||||
|
<div className="cell-stack">
|
||||||
|
<Typography.Text strong>{row.actorUsername || '-'}</Typography.Text>
|
||||||
|
<StatusTag value={row.actorRole} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '动作',
|
||||||
|
dataIndex: 'action',
|
||||||
|
minWidth: 180,
|
||||||
|
render: (value) => <Typography.Text strong>{formatAuditAction(value)}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '目标',
|
||||||
|
minWidth: 180,
|
||||||
|
render: (_, row) => (
|
||||||
|
<Typography.Text strong>
|
||||||
|
{formatAuditTargetType(row.targetType)} #{row.targetId || '-'}
|
||||||
|
</Typography.Text>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '摘要',
|
||||||
|
minWidth: 260,
|
||||||
|
render: (_, row) => <span className="payload-text">{formatPayload(row.payload)}</span>,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<section className="page-stack">
|
||||||
|
<PageHeader
|
||||||
|
title="操作审计"
|
||||||
|
description="集中查看高风险后台动作,便于排查谁在什么时间改了什么。"
|
||||||
|
extra={<Typography.Text type="secondary">共 {data?.pagination.total || 0} 条审计记录</Typography.Text>}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isAdmin ? (
|
||||||
|
<Alert type="warning" showIcon message="仅管理员可以访问操作审计" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Card title="筛选审计日志">
|
||||||
|
<Form<AuditFilterForm>
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
<Form.Item name="actorUsername">
|
||||||
|
<Input allowClear placeholder="操作账号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="action">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="动作"
|
||||||
|
options={adminAuditActionOptions}
|
||||||
|
style={{ minWidth: 180 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="targetType">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="目标类型"
|
||||||
|
options={adminAuditTargetTypeOptions}
|
||||||
|
style={{ minWidth: 150 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="dateRange">
|
||||||
|
<DatePicker.RangePicker />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{query.error ? (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
message={query.error instanceof Error ? query.error.message : '读取审计日志失败'}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Table<AdminAuditLogItem>
|
||||||
|
rowKey="logId"
|
||||||
|
loading={query.isLoading || query.isFetching}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.items || []}
|
||||||
|
locale={{ emptyText: <Empty description="暂无审计记录" /> }}
|
||||||
|
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 }),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPayload(payload: Record<string, unknown>) {
|
||||||
|
const text = stringifyDisplayJson(payload, 0)
|
||||||
|
return text.length > 140 ? `${text.slice(0, 140)}...` : text
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanParams(params: Record<string, unknown>) {
|
||||||
|
const next = new URLSearchParams()
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
const normalized = String(value ?? '').trim()
|
||||||
|
if (normalized) {
|
||||||
|
next.set(key, normalized)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
@@ -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<CreateUserForm>()
|
||||||
|
const [creating, setCreating] = useState(false)
|
||||||
|
const [actionLoadingId, setActionLoadingId] = useState<number | null>(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: (
|
||||||
|
<Form
|
||||||
|
layout="vertical"
|
||||||
|
preserve={false}
|
||||||
|
initialValues={{ password: '' }}
|
||||||
|
ref={form.bind}
|
||||||
|
className="modal-form"
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label={`请输入 ${item.username} 的新密码`}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{ min: 8, message: '密码至少 8 位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="至少 8 位" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
),
|
||||||
|
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 (
|
||||||
|
<section className="page-stack">
|
||||||
|
<PageHeader title="后台用户" description="管理员可维护后台账号、角色和启停状态。" />
|
||||||
|
<Result status="warning" title="仅管理员可以访问用户管理。" />
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumnsType<AdminUserListItem> = [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'userId',
|
||||||
|
width: 72,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '账号',
|
||||||
|
dataIndex: 'username',
|
||||||
|
minWidth: 150,
|
||||||
|
render: (value, row) => (
|
||||||
|
<Space>
|
||||||
|
<Typography.Text strong>{value}</Typography.Text>
|
||||||
|
{row.userId === currentUserId ? <Tag>当前账号</Tag> : null}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
dataIndex: 'role',
|
||||||
|
width: 120,
|
||||||
|
render: (value: AdminRole) => formatRoleLabel(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
width: 140,
|
||||||
|
render: (value) => <StatusTag value={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 (
|
||||||
|
<Space wrap>
|
||||||
|
<Dropdown
|
||||||
|
disabled={isCurrent}
|
||||||
|
menu={{
|
||||||
|
items: roleMenuItems,
|
||||||
|
onClick: ({ key }) => confirmUpdateRole(row, key as AdminRole),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button disabled={isCurrent} loading={actionLoadingId === row.userId}>
|
||||||
|
改角色 <DownOutlined />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
<Button
|
||||||
|
disabled={isCurrent}
|
||||||
|
loading={actionLoadingId === row.userId}
|
||||||
|
onClick={() => confirmToggleStatus(row)}
|
||||||
|
>
|
||||||
|
{row.status === 'active' ? '停用' : '启用'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={actionLoadingId === row.userId}
|
||||||
|
onClick={() => openResetPasswordModal(row)}
|
||||||
|
>
|
||||||
|
重置密码
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="page-stack">
|
||||||
|
<PageHeader
|
||||||
|
title="后台用户"
|
||||||
|
description="管理员可维护后台账号、角色和启停状态,避免继续使用单一口令。"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card title="筛选用户">
|
||||||
|
<Form<UserFilterForm>
|
||||||
|
layout="inline"
|
||||||
|
initialValues={filters}
|
||||||
|
onFinish={applyFilters}
|
||||||
|
className="filter-form"
|
||||||
|
>
|
||||||
|
<Form.Item name="username">
|
||||||
|
<Input allowClear placeholder="账号筛选" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="role">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="角色"
|
||||||
|
options={adminUserRoleOptions}
|
||||||
|
style={{ minWidth: 140 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="status">
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
placeholder="状态"
|
||||||
|
options={adminUserStatusOptions}
|
||||||
|
style={{ minWidth: 140 }}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">
|
||||||
|
查询
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={resetFilters}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="新增用户">
|
||||||
|
<Form<CreateUserForm>
|
||||||
|
form={createForm}
|
||||||
|
layout="inline"
|
||||||
|
initialValues={{ role: 'operator' }}
|
||||||
|
onFinish={submitCreate}
|
||||||
|
className="filter-form"
|
||||||
|
>
|
||||||
|
<Form.Item name="username" rules={[{ required: true, message: '请输入新账号' }]}>
|
||||||
|
<Input placeholder="新账号" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{ min: 8, message: '密码至少 8 位' },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="新密码,至少 8 位" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="role" rules={[{ required: true, message: '请选择角色' }]}>
|
||||||
|
<Select options={USER_ROLE_OPTIONS} style={{ minWidth: 140 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" htmlType="submit" icon={<PlusOutlined />} loading={creating}>
|
||||||
|
新增用户
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{query.error ? (
|
||||||
|
<Alert
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
message={query.error instanceof Error ? query.error.message : '读取后台用户失败'}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
title="用户列表"
|
||||||
|
extra={<Typography.Text type="secondary">共 {data?.pagination.total || 0} 个账号</Typography.Text>}
|
||||||
|
>
|
||||||
|
<Table<AdminUserListItem>
|
||||||
|
rowKey="userId"
|
||||||
|
loading={query.isLoading || query.isFetching}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.items || []}
|
||||||
|
locale={{ emptyText: <Empty description="暂无用户" /> }}
|
||||||
|
scroll={{ x: 1060 }}
|
||||||
|
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 }))
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRoleLabel(role: AdminRole) {
|
||||||
|
if (role === 'admin') return '管理员'
|
||||||
|
if (role === 'support') return '客服'
|
||||||
|
return '普通运营'
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanParams(params: Record<string, unknown>) {
|
||||||
|
const next = new URLSearchParams()
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
const normalized = String(value ?? '').trim()
|
||||||
|
if (normalized) {
|
||||||
|
next.set(key, normalized)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
class ModalFormStore<T extends object> {
|
||||||
|
private form: ReturnType<typeof Form.useForm<T>>[0] | null = null
|
||||||
|
|
||||||
|
static create<T extends object>() {
|
||||||
|
return new ModalFormStore<T>()
|
||||||
|
}
|
||||||
|
|
||||||
|
bind = (instance: unknown) => {
|
||||||
|
this.form = instance as ReturnType<typeof Form.useForm<T>>[0] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(): Promise<T> {
|
||||||
|
if (!this.form) {
|
||||||
|
return {} as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.form.validateFields()
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,403 @@ select {
|
|||||||
margin-top: 0;
|
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) {
|
@media (max-width: 900px) {
|
||||||
.admin-sider {
|
.admin-sider {
|
||||||
position: fixed !important;
|
position: fixed !important;
|
||||||
@@ -193,3 +590,129 @@ select {
|
|||||||
display: grid;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user