fix(admin): 优化名片列表表格列宽与防折行配置,增加横向平滑滚动与操作栏固定

这个提交包含在:
wikiboss
2026-09-23 13:47:07 +08:00
父节点 2738c4b8db
当前提交 8ea9c25386
共有 2 个文件被更改,包括 365 次插入 和 6 次删除

查看文件

@@ -31,6 +31,8 @@ const DEFAULT_SIZE = 10;
const PAGE_API = '/api/admin/card/info/page';
const UPDATE_API = '/api/admin/card/info/update';
const SEED_API = '/api/admin/card/info/seed';
const VCARD_EXPORT_API = '/api/admin/card/info/vcard';
const QRCODE_SVG_API = '/api/open/card/qrcode/svg';
type CardListRow = CardProfile & {
slug?: string;
@@ -64,6 +66,24 @@ interface CoolUpdateResponse {
message?: string;
}
interface CoolVcardExportResponse {
code: number;
message?: string;
data?: {
vcard?: string;
filename?: string;
};
}
interface CoolQrcodeSvgResponse {
code: number;
message?: string;
data?: {
svg?: string;
url?: string;
};
}
interface CardPageQuery {
page: number;
size: number;
@@ -134,6 +154,11 @@ function CardListPage() {
const [filters, setFilters] = useState<CardListFilterForm>({});
const [reloadTick, setReloadTick] = useState(0);
const [seeding, setSeeding] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<Array<string | number>>(
[]
);
const [exporting, setExporting] = useState(false);
const [downloadingQr, setDownloadingQr] = useState(false);
const refreshList = useCallback(() => {
setReloadTick((tick) => tick + 1);
@@ -265,6 +290,75 @@ function CardListPage() {
[t]
);
const handleExportVcard = useCallback(async () => {
const selectedId = selectedRowKeys[0];
if (selectedId === undefined || selectedId === null || selectedId === '') {
Message.warning(t['cardList.export.selectFirst']);
return;
}
setExporting(true);
try {
const { data: payload } = await axios.post<CoolVcardExportResponse>(
VCARD_EXPORT_API,
{ id: selectedId }
);
if (payload.code !== 1000 || !payload.data?.vcard) {
Message.error(payload.message || t['cardList.export.fail']);
return;
}
const blob = new Blob([payload.data.vcard], {
type: 'text/vcard;charset=utf-8',
});
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = payload.data.filename || `card-${String(selectedId)}.vcf`;
link.click();
URL.revokeObjectURL(objectUrl);
Message.success(t['cardList.export.success']);
} catch (error: unknown) {
Message.error(resolveErrorMessage(error, t['cardList.export.fail']));
} finally {
setExporting(false);
}
}, [selectedRowKeys, t]);
const handleDownloadQrSvg = useCallback(async () => {
const selectedId = selectedRowKeys[0];
if (selectedId === undefined || selectedId === null || selectedId === '') {
Message.warning(t['cardList.qr.selectFirst']);
return;
}
const selectedRow = data.find(
(row) => String(row.id) === String(selectedId)
);
const slugOrId = selectedRow?.slug?.trim() || String(selectedId);
setDownloadingQr(true);
try {
const { data: payload } = await axios.get<CoolQrcodeSvgResponse>(
QRCODE_SVG_API,
{ params: { slugOrId } }
);
const svg = payload.data?.svg?.trim();
if (payload.code !== 1000 || !svg) {
Message.error(payload.message || t['cardList.qr.fail']);
return;
}
const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectUrl;
link.download = `card-${slugOrId}.svg`;
link.click();
URL.revokeObjectURL(objectUrl);
Message.success(t['cardList.qr.success']);
} catch (error: unknown) {
Message.error(resolveErrorMessage(error, t['cardList.qr.fail']));
} finally {
setDownloadingQr(false);
}
}, [data, selectedRowKeys, t]);
const handleToggleStatus = useCallback(
async (record: CardListRow) => {
const nextStatus: CardStatus | null =
@@ -303,39 +397,69 @@ function CardListPage() {
{
title: t['cardList.columns.name'],
dataIndex: 'name',
width: 170,
ellipsis: true,
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{value || '-'}</span>
),
},
{
title: t['cardList.columns.title'],
dataIndex: 'title',
width: 240,
ellipsis: true,
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{value || '-'}</span>
),
},
{
title: t['cardList.columns.company'],
dataIndex: 'company',
width: 220,
ellipsis: true,
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{value || '-'}</span>
),
},
{
title: t['cardList.columns.slug'],
dataIndex: 'slug',
width: 160,
ellipsis: true,
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{value || '-'}</span>
),
},
{
title: t['cardList.columns.status'],
dataIndex: 'status',
width: 110,
render: (value: CardStatus) => {
const status = isCardStatus(value) ? value : 'draft';
return (
<Badge
color={getStatusColor(status)}
text={t[`cardList.status.${status}`]}
/>
<span style={{ whiteSpace: 'nowrap' }}>
<Badge
color={getStatusColor(status)}
text={t[`cardList.status.${status}`]}
/>
</span>
);
},
},
{
title: t['cardList.columns.themeCode'],
dataIndex: 'themeCode',
width: 130,
ellipsis: true,
render: (value: string) => (
<span style={{ whiteSpace: 'nowrap' }}>{value || '-'}</span>
),
},
{
title: t['cardList.columns.operations'],
dataIndex: 'operations',
width: 340,
fixed: 'right',
render: (_value: unknown, record: CardListRow) => {
const canToggle =
record.status === 'draft' || record.status === 'published';
@@ -348,7 +472,7 @@ function CardListPage() {
? t['cardList.unpublish.confirm']
: t['cardList.publish.confirm'];
return (
<Space size={8}>
<Space size={8} wrap={false} style={{ whiteSpace: 'nowrap' }}>
<Button
type="text"
size="small"
@@ -370,6 +494,22 @@ function CardListPage() {
>
{t['cardList.columns.copyLink']}
</Button>
<Button
type="text"
size="small"
loading={exporting}
onClick={handleExportVcard}
>
{t['cardList.columns.exportVcard']}
</Button>
<Button
type="text"
size="small"
loading={downloadingQr}
onClick={handleDownloadQrSvg}
>
{t['cardList.columns.downloadQrSvg']}
</Button>
{canToggle ? (
<Popconfirm
title={confirmTitle}
@@ -387,7 +527,17 @@ function CardListPage() {
},
},
],
[handleCopyLink, handleOpenBuilder, handleToggleStatus, handleView, t]
[
downloadingQr,
exporting,
handleCopyLink,
handleDownloadQrSvg,
handleExportVcard,
handleOpenBuilder,
handleToggleStatus,
handleView,
t,
]
);
const pagination: PaginationProps = {
@@ -462,13 +612,32 @@ function CardListPage() {
</Form>
</FilterCard>
<ContentCard>
<style>{`
.card-list-table .arco-table-th,
.card-list-table .arco-table-td {
white-space: nowrap !important;
}
.card-list-table .arco-table-cell {
white-space: nowrap !important;
word-break: keep-all !important;
}
`}</style>
<Table
className="card-list-table"
rowKey="id"
loading={loading}
columns={columns}
data={data}
pagination={pagination}
scroll={{ x: 1370 }}
onChange={handleTableChange}
rowSelection={{
type: 'radio',
selectedRowKeys,
onChange: (keys: Array<string | number>) => {
setSelectedRowKeys(keys);
},
}}
/>
</ContentCard>
</PageShell>