fix(admin): 优化名片列表表格列宽与防折行配置,增加横向平滑滚动与操作栏固定
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户