From f416397cc8b26d8a5518a89cf2e534aba6b23295 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 4 Oct 2026 20:03:52 +0800 Subject: [PATCH] =?UTF-8?q?feat(account):=20=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E9=87=8D=E6=8E=92=20+=20=E5=88=97=E8=A1=A8=E9=AB=98=E4=BB=B7?= =?UTF-8?q?=E5=80=BC=E5=A5=97=E8=A3=85=E5=88=97=20+=20=E5=85=91=E6=8D=A2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=82=B9=E5=87=BB=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 详情页重排(4 张卡,消除重复) 用户反馈「设计太乱、数据不好看、布局也乱」——根因是**同一份数据在一屏出现两次**: 积分/鱼翅/兑换余额在状态条和资产区各一次,角色绑定在状态条和信息表各一次。 新布局:**账号信息**(身份+状态+归属,一张 Descriptions)→ **资产与统计** (数字只出现一次,上面"现在有多少"、下面"历史累计")→ **高价值套装与兑换记录** (从最顶上移到这里,它是补充信息,不该抢主视觉)→ **账号时间线**。 零散的 `flex + gap` 统计行改成 `Row/Col` 栅格,列宽对齐。 ## 高价值套装:颜色按**价值**口径,别标反 用户澄清的业务语义与直觉相反: - **没兑换过** = 还能用来绑定兑换 = **高价值**(绿色「可兑换」) - 兑换过 1 个 → 价值降低;两个都兑换过 → 价值最低(橙色「已兑换」) 判定依据是**斗鱼官方兑换记录**(账号级)。⚠️ 不能用 `query_limited_goods` 的 `count`/`storeStatus` —— 那是**全局库存**(同一时刻不同账号看到的都一样), 用它判"这个账号有没有"必然给错结论。这个结论是推翻过一版才得到的: 第一版拿相隔两个月的两条快照对比,把**时间差异**当成了**账号差异**。 ## 列表 - 新增「高价值套装」列:🟢两个都可兑换 / 🟠某套装已兑换 / ⚪未查询 (「未查询」必须与「没兑换」分开,否则会把未知当成高价值) - 「兑换记录」列只显示官方口径,**点击弹窗**看全部(按作用域分组, 含商品/积分/真实兑换时间/当时用的昵称与平台),不用进详情页 ## 三个作用域的记录展示 面板与预览都按 精英宝典 / 电竞手册 / 和平小店 分三块展示, 每条带**真实兑换时间**与"使用昵称"(排查时有用)。 ## 顺带修 `exchange_records` 从数组改成按作用域分组的对象后,面板同步适配; `formatTime` 对 `0` 显示 `-`(斗鱼用 0 表示"没有这个时间", 直接格式化会显示成 1970/1/1)。 --- web/frontend/src/api/accounts.ts | 4 + web/frontend/src/api/types.ts | 23 + web/frontend/src/pages/AccountDetailPage.tsx | 397 +++++++++--------- web/frontend/src/pages/AccountsPage.tsx | 207 ++++++++- .../douyu-tasks/AccountOverviewPanel.tsx | 236 ++++++----- 5 files changed, 563 insertions(+), 304 deletions(-) diff --git a/web/frontend/src/api/accounts.ts b/web/frontend/src/api/accounts.ts index 6eb4b14..d85479b 100644 --- a/web/frontend/src/api/accounts.ts +++ b/web/frontend/src/api/accounts.ts @@ -1,5 +1,6 @@ import api from './client'; import type { + AccountExchangeRecords, AccountEventPage, AccountItem, AccountBulkSelection, @@ -50,6 +51,9 @@ export const accountApi = { api.put(`/accounts/${id}/automation`, { auto_managed }), // 详情页:状态卡 + 统计 overview: (id: number) => api.get(`/accounts/${id}/overview`), + // 三个作用域的**官方**兑换记录(点列表那一格时按需拉取,不进列表响应) + exchangeRecords: (id: number) => + api.get(`/accounts/${id}/exchange-records`), // 时间线(倒序分页),event_type 为空表示全部 events: ( id: number, diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index ace8ecb..7e2465c 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -176,6 +176,8 @@ export interface AccountOverview { stats: AccountOverviewStats; /** 最近一次「一键查询」的快照(结构与后端 query_account_overview 的 result 一致) */ overview_snapshot: import('../pages/douyu-tasks/AccountOverviewPanel').AccountOverviewResult | null; + /** 一键查询得到的已兑换高价值套装 */ + high_value_owned: string[]; overview_queried_at: string | null; /** 开通时间来自事件表(上线后才开始记录,更早的不显示) */ opened_at: { elite: string | null; esports: string | null }; @@ -278,6 +280,10 @@ export interface AccountItem { exchange_count: number; last_exchange_at: string | null; last_exchange_name: string; + /** 一键查询得到的「已兑换过的高价值套装」(账号表里没有,来自官方兑换记录) */ + high_value_owned: string[]; + /** 最近一次一键查询时间 —— 用来区分「查过但没兑换」和「还没查过」 */ + overview_queried_at: string | null; } export interface AssignmentsSummary { @@ -1094,3 +1100,20 @@ export interface AutomationManualQueue { total: number; groups: AutomationManualGroup[]; } + +/** 官方兑换记录(按作用域分组) */ +export interface AccountExchangeRecord { + title: string; + commodity_id: string; + commodity_name: string; + score: number | string | null; + show_name: string; + exchange_num: number | null; + occurred_at: string | null; +} + +export interface AccountExchangeRecords { + groups: Record; + total: number; + queried_at: string | null; +} diff --git a/web/frontend/src/pages/AccountDetailPage.tsx b/web/frontend/src/pages/AccountDetailPage.tsx index 2c9d54f..ec8d884 100644 --- a/web/frontend/src/pages/AccountDetailPage.tsx +++ b/web/frontend/src/pages/AccountDetailPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { - Button, Card, Descriptions, Empty, Input, Popconfirm, Select, Space, Spin, Statistic, Table, Tag, Tooltip, Typography, + Button, Card, Col, Descriptions, Divider, Empty, Input, Popconfirm, Row, Select, Space, Spin, Statistic, Table, Tag, Tooltip, Typography, } from 'antd'; import type { TableProps } from 'antd'; import { @@ -56,19 +56,6 @@ const STATUS_TONES: Record = { archived: 'default', }; -/** 单个作用域的绑定小标签:精/电/店。 */ -function BindChip({ scope, status }: { scope: string; status?: string }) { - const tone = status === 'bound' ? 'green' - : status === 'pending' ? 'orange' - : status === 'unbound' ? 'red' - : 'default'; - const mark = status === 'bound' ? '✓' - : status === 'pending' ? '…' - : status === 'unbound' ? '✗' - : '?'; - return {scope}{mark}; -} - /** 绑定状态 → 展示色(与后端 account_binding 的四个枚举对应)。 */ const BIND_TONES: Record = { bound: 'green', @@ -369,136 +356,19 @@ export default function AccountDetailPage() { - {/* ── 生命周期状态卡 ──────────────────────────────── - 这一屏要回答客服最关心的三个问题: - 1. 这账号现在能不能用?(状态 + CK) - 2. 要不要我处理?(原因 + 变更时间 + 自动化是否在管) - 3. 账上还有多少?(积分 / 鱼翅 / 兑换余额) */} + {/* ── 卡片 1:账号信息 ────────────────────────────── + 身份 + 状态 + 归属,一张表看完。**不重复展示资产**(见卡片 2)。 */} - - 高价值套装与兑换记录(一键查询) - - } - extra={ - - } - > - {ovError && ( - - {ovError} - - )} - {overview?.overview_snapshot ? ( - - ) : ( - - 实时向斗鱼查一次这个账号:鱼翅 / 积分 / 开通状态会**就地刷新到下面的账号信息**, - 这里只单独展示账号表里没有的两项 —— 「兔兔白日梦」「狂战萌兔」是否已获得, - 以及官方兑换记录。 - - )} - - - - - 生命周期状态 - - - {overview?.status_label || '—'} - - {overview?.auto_managed === false && 不参与自动化} - + + + {overview?.nickname || overview?.username || '—'} + - {formatTime(overview?.status_changed_at)} - {overview?.status_reason ? `(${overview.status_reason})` : ''} + #{overview?.id ?? '-'} · {overview?.username || '—'} - - - - CK - {overview?.has_cookie === false - ? 无 CK - : - {overview?.ck_check_status === 'invalid' ? '已失效' - : overview?.ck_check_status === 'valid' ? '正常' : '未检测'} - } - - 最近检测 {formatTime(overview?.stats?.last_ck_check_at)} - - - - - 角色绑定 - - - - - - - - - - - - {/* 人工操作:与工具栏的生命周期动作分开 —— 那些是「账号可用性」, - 这些是「归属与标注」。 */} - - 归属与标注 - - setTagDraft(e.target.value)} - onPressEnter={() => void handleSaveTag()} - /> - - - - ) : ( - - )} - - - - - - - {overview?.username || '—'} {overview?.status_label || '—'} @@ -506,19 +376,50 @@ export default function AccountDetailPage() { {overview?.auto_managed === false && 不参与自动化} } - style={{ marginBottom: 12 }} > - {overview?.nickname || '-'} {overview?.uid || '-'} - {formatTime(overview?.created_at)} {overview?.game_name || '-'} - {overview?.game_channel ? ({overview.game_channel}) : null} + {overview?.game_channel ? ( + ({overview.game_channel}) + ) : null} + + + {formatTime(overview?.created_at)} + + + + + + {formatTime(overview?.status_changed_at)} + {overview?.status_reason ? ( + ({overview.status_reason}) + ) : null} + + + + + + {overview?.has_cookie === false ? ( + 无 CK + ) : ( + + {overview?.ck_check_status === 'invalid' + ? '已失效' + : overview?.ck_check_status === 'valid' + ? '正常' + : '未检测'} + + )} + + {formatTime(stats?.last_ck_check_at)} + + - {/* 三列各管一个作用域,取值 unknown/unbound/pending/bound */} - + {/* 三个作用域各一列,取值 unknown/unbound/pending/bound */} + 精英:{overview?.role_bind_label || '未知'} @@ -530,6 +431,53 @@ export default function AccountDetailPage() { + + + setTagDraft(e.target.value)} + onPressEnter={() => void handleSaveTag()} + /> + + + + ) : ( + + {overview?.tag || '-'} + + + } + > + {ovError && ( + + {ovError} + + )} + {overview?.overview_snapshot ? ( + + ) : ( + + 点右上角「一键查询」实时向斗鱼查一次:鱼翅 / 积分 / 开通状态会就地刷新到上面的 + 「资产与统计」,这里只展示账号表里没有的两项 —— + 高价值套装是否已获得,以及官方兑换记录。 + + )} + + {scope}{mark}; } +/** 三个作用域的展示顺序与中文名(后端按 scope 分组返回)。 */ +const EXCHANGE_SCOPES: Array<{ key: string; label: string }> = [ + { key: 'elite', label: '精英宝典' }, + { key: 'esports', label: '电竞手册' }, + { key: 'peace', label: '和平小店' }, +]; + +const EXCHANGE_COLUMNS: TableProps['columns'] = [ + { + title: '商品', + dataIndex: 'commodity_name', + key: 'commodity_name', + render: (value: string) => value || '-', + }, + { + title: '积分', + dataIndex: 'score', + key: 'score', + width: 90, + render: (value: number | string | null) => value ?? '-', + }, + { + // 官方记录带**真实兑换时间** + title: '兑换时间', + dataIndex: 'occurred_at', + key: 'occurred_at', + width: 170, + render: (value: string | null) => formatTime(value), + }, + { + // 当时用的斗鱼昵称 + 平台/支付渠道,排查时有用 + title: '使用昵称', + dataIndex: 'show_name', + key: 'show_name', + render: (value: string) => value || '-', + }, +]; + export default function AccountsPage() { const navigate = useNavigate(); // App.useApp() 的 modal 实例继承主题,静态 Modal.confirm 在暗色模式下是白底 @@ -88,6 +141,23 @@ export default function AccountsPage() { const [ckFilter, setCkFilter] = useState(''); const [bindFilter, setBindFilter] = useState(''); const [searchText, setSearchText] = useState(''); + // 兑换记录预览:点列表那一格**按需拉取**(不进列表响应),弹窗按作用域分组 + const [exRecords, setExRecords] = useState(null); + const [exLoading, setExLoading] = useState(false); + const [exAccount, setExAccount] = useState(null); + const openExchangeRecords = async (record: AccountItem) => { + setExAccount(record); + setExRecords(null); + setExLoading(true); + try { + setExRecords(await accountApi.exchangeRecords(record.id)); + } catch (error) { + message.error(getErrorMessage(error)); + } finally { + setExLoading(false); + } + }; + const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [selectedAllMatching, setSelectedAllMatching] = useState(false); const [batchTagInput, setBatchTagInput] = useState(''); @@ -405,25 +475,78 @@ export default function AccountsPage() { ), }, { - // 兑换记录:次数 + 最近一次(商品名 + 日期)。 - // 数据源是账号自己的事件流(`account_events.exchange`),不是 douyu_tasks 的 JSON —— - // 后者是执行日志,S5 起业务事实已经在事件里了。完整历史看详情页时间线。 + // 高价值套装:这只关乎**账号价值** —— 没兑换过才值钱。 + // + // ⚠️ 语义与直觉相反,别标反了: + // **没兑换过** = 还能用来绑定兑换 = **高价值**(绿色) + // 兑换过 1 个 → 价值降低;两个都兑换过 → 价值最低 + // 数据来自一键查询拉的**斗鱼官方兑换记录**。 + title: '高价值套装', + dataIndex: 'high_value_owned', + width: 170, + render: (owned: string[] | undefined, record: AccountItem) => { + const used = owned ?? []; + // 没查过 ≠ 没兑换:必须分开说,否则会把"没数据"当成"高价值" + // 「有没有查过」看查询时间:只看 owned 是否为空的话, + // 「查过但没兑换」(高价值)和「没查过」(未知)就分不开了。 + const queried = Boolean(record.overview_queried_at); + if (!queried) { + return ( + + 未查询 + + ); + } + if (used.length === 0) { + return 两个都可兑换; + } + return ( + + {used.map((n) => ( + + {n.replace(/^套装-/, '')} 已兑换 + + ))} + + ); + }, + }, + { + // 兑换记录:次数 + 最近一次。 + // + // 只反映**斗鱼官方记录**(后端聚合已按 source=official_sync 过滤)—— + // 我们当场记的那些留给详情页时间线,列表要的是"官方权威且完整"的口径。 + // 点这一格弹窗看全部(按作用域分组),不用进详情页。 title: '兑换记录', dataIndex: 'exchange_count', width: 170, render: (_, record: AccountItem) => { - if (!record.exchange_count) return 未兑换; + if (!record.exchange_count) { + return 未兑换; + } return ( - - {record.exchange_count} 次 - - {formatTimeShort(record.last_exchange_at)} - {/* 列的标题已经是「兑换记录」,事件 title 里的「兑换 」前缀在这儿是重复的 */} - {record.last_exchange_name - ? ` ${record.last_exchange_name.replace(/^兑换\s*/, '')}` - : ''} - - + +
void openExchangeRecords(record)} + onKeyDown={(event) => { + if (event.key === 'Enter') void openExchangeRecords(record); + }} + > + + {record.exchange_count} 次 + + {formatTimeShort(record.last_exchange_at)} + {/* 列标题已是「兑换记录」,事件 title 里的「兑换 」前缀在这儿重复 */} + {record.last_exchange_name + ? ` ${record.last_exchange_name.replace(/^兑换\s*/, '')}` + : ''} + + +
+
); }, }, @@ -730,6 +853,54 @@ export default function AccountsPage() { options={tags.map((t) => ({ value: t, label: t }))} /> + + + 官方兑换记录 + {exAccount && ( + + {exAccount.username} + + )} + + } + open={Boolean(exAccount)} + onCancel={() => setExAccount(null)} + footer={} + width={760} + styles={{ body: { maxHeight: '65vh', overflowY: 'auto' } }} + > + {exLoading ? ( + + ) : !exRecords || exRecords.total === 0 ? ( + + 没有官方兑换记录。若还没查过这个账号,先点工具栏「批量一键查询」。 + + ) : ( + + {EXCHANGE_SCOPES.map(({ key, label }) => { + const rows = exRecords.groups[key] ?? []; + if (rows.length === 0) return null; + return ( +
+ + {label}({rows.length} 条) + + `${row.commodity_id}-${row.occurred_at ?? ''}`} + columns={EXCHANGE_COLUMNS} + dataSource={rows} + pagination={false} + /> + + ); + })} + + )} + ); } diff --git a/web/frontend/src/pages/douyu-tasks/AccountOverviewPanel.tsx b/web/frontend/src/pages/douyu-tasks/AccountOverviewPanel.tsx index b65fad1..0796b9e 100644 --- a/web/frontend/src/pages/douyu-tasks/AccountOverviewPanel.tsx +++ b/web/frontend/src/pages/douyu-tasks/AccountOverviewPanel.tsx @@ -1,4 +1,4 @@ -import { Alert, Card, Descriptions, Empty, Space, Table, Tag, Typography } from 'antd'; +import { Alert, Card, Empty, Space, Table, Tag, Tooltip, Typography } from 'antd'; import type { ColumnsType } from 'antd/es/table'; const { Text } = Typography; @@ -42,7 +42,8 @@ export interface AccountOverviewResult { }; high_value_goods?: HighValueGoods[]; limited_goods_total?: number; - exchange_records?: Array>; + /** 按作用域分组:elite / esports / peace */ + exchange_records?: Record>>; exchange_record_count?: number; /** 单项失败:**必须展示**,否则「没查到」会被读成「没有」 */ errors?: Record; @@ -90,93 +91,131 @@ function formatTime(value: string | number | null | undefined): string { * 我们没法只凭它断言账号真的拿到了。所以写「不可再兑」并把 count 一起显示, * 让运营自己判断 —— 这比给一个可能错的结论好。 */ -function HighValueCard({ goods }: { goods: HighValueGoods }) { - if (goods.missing) { - return ( - - - {goods.name} - 未查到该商品 - - {goods.reason || '限兑列表和官方兑换记录里都没有它'} - - - - ); - } - // ⚠️ 「是否已获得」只看官方兑换记录。`count`/`store_status` 是**全局库存**, - // 同一时刻不同账号看到的都一样,**不能**用来判断这个账号有没有。 - const owned = goods.owned === true; - return ( - {goods.name}} - extra={ - {owned ? '✅ 已获得' : '未获得'} - } - > - - {owned && ( - - {(goods.obtained_names || []).join('、') || '-'} - - )} - {goods.score ?? '-'} - - {goods.count ?? '-'} - {goods.count === 0 ? '(已售罄)' : ''} - - - {formatTime(goods.start_time)} ~ {formatTime(goods.end_time)} - - - {!goods.in_goods_list && ( - - 当前限兑列表里没有它(活动窗口可能已结束),但兑换记录仍在。 - - )} - - ); +/** + * 高价值套装的展示状态。 + * + * ⚠️ **颜色是「价值」口径,不是「有没有」口径** —— 别标反: + * 没兑换过 → 「可兑换」**绿色**(这个账号还值钱,能用来绑定兑换) + * 兑换过 → 「已兑换」**橙色**(价值降低) + * 同一件东西两个都兑换过,账号价值最低。 + * + * 「是否已获得」只看**官方兑换记录**:`count`/`store_status` 是全局库存, + * 同一时刻不同账号看到的都一样,不能用来判断这个账号有没有。 + */ +function goodsState(g: HighValueGoods): { text: string; color: string } { + if (g.missing) return { text: '未查到该商品', color: 'default' }; + return g.owned + ? { text: '已兑换(价值降低)', color: 'orange' } + : { text: '可兑换', color: 'green' }; } -/** - * 一键查询结果面板。 - * - * ⚠️ **只展示账号表里没有的东西**(高价值套装是否已获得、官方兑换记录)。 - * - * 鱼翅 / 积分 / 钱包余额 / 开通状态这些,账号表本来就有对应字段 - * (`gold_balance` / `points` / `exchange_balance` / `elite_opened` / `esports_opened`), - * 查询的作用是**刷新它们**,然后由账号详情里已有的界面直接反映出来 —— - * 在这里再复制一份展示是多余的,还会让人分不清哪份才是准的。 - */ +const goodsColumns: ColumnsType = [ + { + title: '高价值套装', + dataIndex: 'name', + key: 'name', + width: 200, + render: (name: string, g) => + g.missing ? ( + + {name} + + ) : ( + {name} + ), + }, + { + title: '可兑换 / 已兑换', + key: 'owned', + width: 130, + render: (_, g) => { + const state = goodsState(g); + return {state.text}; + }, + }, + { + title: '兑换记录里的名字', + key: 'obtained', + render: (_, g) => + g.obtained_names?.length ? ( + {g.obtained_names.join('、')} + ) : ( + — + ), + }, + { + title: '积分价', + dataIndex: 'score', + key: 'score', + width: 90, + render: (v: number | null | undefined) => v ?? '—', + }, + { + title: '当前库存', + dataIndex: 'count', + key: 'count', + width: 120, + render: (v: number | null | undefined, g) => + v == null ? ( + '—' + ) : ( + + {v} + {v === 0 && (售罄)} + {g.in_goods_list === false && (已下架)} + + ), + }, + { + title: '活动时间', + key: 'window', + width: 200, + render: (_, g) => + g.missing ? '—' : `${formatTime(g.start_time)} ~ ${formatTime(g.end_time)}`, + }, +]; + +/** 三个作用域的展示顺序(后端按 scope 分组返回)。 */ +const EXCHANGE_SCOPES: Array<{ key: string; label: string }> = [ + { key: 'elite', label: '精英宝典' }, + { key: 'esports', label: '电竞手册' }, + { key: 'peace', label: '和平小店' }, +]; + export default function AccountOverviewPanel({ result, - queriedAt, }: { result: AccountOverviewResult; - /** 查询时间(优先用账号上记录的,回落到结果里的) */ - queriedAt?: string | null; }) { if (!result) return ; const errors = result.errors || {}; const goods = result.high_value_goods || []; - const records = result.exchange_records || []; + const recordGroups = result.exchange_records || {}; const recordColumns: ColumnsType> = [ { title: '商品', - dataIndex: 'commodityName', - key: 'commodityName', - render: (v: unknown) => String(v ?? '-'), + key: 'name', + render: (_, row) => String(row.commodityName ?? row.commodity_name ?? '-'), }, { - title: '时间', + title: '积分', + key: 'score', + width: 90, + render: (_, row) => String(row.score ?? '-'), + }, + { + // 官方记录带**真实兑换时间** + title: '兑换时间', key: 'time', + width: 170, render: (_, row) => - formatTime( - (row.createTime ?? row.create_time ?? row.time) as string | number | null, - ), + formatTime((row.time ?? row.occurred_at) as string | number | null), + }, + { + title: '使用昵称', + key: 'show', + render: (_, row) => String(row.showName ?? row.show_name ?? '-'), }, ]; @@ -199,38 +238,47 @@ export default function AccountOverviewPanel({ /> )} - - {goods.length === 0 ? ( - 没有配置关注的高价值套装,或本次没取到 - ) : ( - - {goods.map((g) => ( - - ))} - - )} - - - {records.length === 0 ? ( - 官方暂无兑换记录 + title="🎯 高价值套装(没兑换过才值钱)" styles={{ body: { padding: 0 } }}> + {goods.length === 0 ? ( +
+ 没有配置关注的高价值套装,或本次没取到 +
) : (
String(i)} - columns={recordColumns} - dataSource={records} - pagination={{ pageSize: 10, size: 'small' }} + rowKey={(g) => g.commodity_id || g.name} + columns={goodsColumns} + dataSource={goods} + pagination={false} /> )} - - 查询时间:{formatTime(queriedAt ?? result.queried_at)} - + {EXCHANGE_SCOPES.map(({ key, label }) => { + const rows = recordGroups[key] ?? []; + return ( + + {rows.length === 0 ? ( + 官方暂无{label}兑换记录 + ) : ( +
String(i)} + columns={recordColumns} + dataSource={rows} + pagination={{ pageSize: 10, size: 'small' }} + /> + )} + + ); + })} ); }