refactor: 更新用户页面组件和分页逻辑

- 优化 `index.tsx` 中的导入和表单字段
- 使用 `Table` 组件替换 `ListTable` 和 `AppPagination`
- 添加密码确认字段及其验证规则
- 调整工具栏和表格布局以提高一致性
dev_na
chenhao 2026-04-21 10:00:47 +08:00
parent 900f092d5e
commit ce4743c5ea
1 changed files with 51 additions and 36 deletions

View File

@ -1,4 +1,4 @@
import { Avatar, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Tag, TreeSelect, Typography, Upload, message } from "antd"; import { Avatar, Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Switch, Table, Tag, TreeSelect, Typography, Upload, message } from "antd";
import type { DefaultOptionType } from "antd/es/select"; import type { DefaultOptionType } from "antd/es/select";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -7,9 +7,8 @@ import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants
import { useDict } from "@/hooks/useDict"; import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission"; import { usePermission } from "@/hooks/usePermission";
import PageHeader from "@/components/shared/PageHeader"; import PageHeader from "@/components/shared/PageHeader";
import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination";
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName"; import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
import { getStandardPagination } from "@/utils/pagination";
import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types"; import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types";
import "./index.less"; import "./index.less";
@ -102,6 +101,7 @@ export default function Users() {
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []); const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
const selectedTenantId = Form.useWatch("tenantId", form); const selectedTenantId = Form.useWatch("tenantId", form);
const memberships = (Form.useWatch("memberships", form) || []) as Membership[]; const memberships = (Form.useWatch("memberships", form) || []) as Membership[];
const passwordValue = Form.useWatch("password", form);
const tenantMap = useMemo(() => { const tenantMap = useMemo(() => {
const map: Record<number, string> = {}; const map: Record<number, string> = {};
@ -216,6 +216,7 @@ export default function Users() {
...detail, ...detail,
roleIds: roleIds || [], roleIds: roleIds || [],
password: "", password: "",
confirmPassword: "",
tenantId: (detail as any).tenantId || detail.memberships?.[0]?.tenantId, tenantId: (detail as any).tenantId || detail.memberships?.[0]?.tenantId,
orgId: (detail as any).orgId || detail.memberships?.[0]?.orgId, orgId: (detail as any).orgId || detail.memberships?.[0]?.orgId,
memberships: detail.memberships || [] memberships: detail.memberships || []
@ -304,24 +305,24 @@ export default function Users() {
}, },
...(isPlatformMode ...(isPlatformMode
? [{ ? [{
title: t("users.tenant"), title: t("users.tenant"),
key: "tenant", key: "tenant",
render: (_: any, record: SysUser) => { render: (_: any, record: SysUser) => {
if (record.memberships && record.memberships.length > 0) { if (record.memberships && record.memberships.length > 0) {
return ( return (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{record.memberships.slice(0, 2).map((membership: any) => ( {record.memberships.slice(0, 2).map((membership: any) => (
<Tag key={membership.tenantId} color="blue" style={{ margin: 0, padding: "0 4px", fontSize: 11 }}> <Tag key={membership.tenantId} color="blue" style={{ margin: 0, padding: "0 4px", fontSize: 11 }}>
{tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`} {tenantMap[membership.tenantId] || `Tenant ${membership.tenantId}`}
</Tag> </Tag>
))} ))}
{record.memberships.length > 2 && <Text type="secondary" style={{ fontSize: 11 }}>+{record.memberships.length - 2} more</Text>} {record.memberships.length > 2 && <Text type="secondary" style={{ fontSize: 11 }}>+{record.memberships.length - 2} more</Text>}
</div> </div>
); );
}
return <Text type="secondary">{t("usersExt.noTenant")}</Text>;
} }
}] return <Text type="secondary">{t("usersExt.noTenant")}</Text>;
}
}]
: []), : []),
{ {
title: t("users.orgNode"), title: t("users.orgNode"),
@ -385,10 +386,10 @@ export default function Users() {
<div className="users-table-toolbar"> <div className="users-table-toolbar">
<Space size="middle" wrap className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}> <Space size="middle" wrap className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
<Space size="middle" wrap className="app-page__toolbar"> <Space size="middle" wrap className="app-page__toolbar">
{isPlatformMode && <Select placeholder={t("users.tenantFilter")} style={{ width: 200 }} allowClear value={filterTenantId} onChange={setFilterTenantId} options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={<ShopOutlined aria-hidden="true" />} />} {isPlatformMode && <Select placeholder={t("users.tenantFilter")} style={{ width: 200 }} allowClear value={filterTenantId} onChange={setFilterTenantId} options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={<ShopOutlined aria-hidden="true" />} />}
<Input placeholder={t("users.searchPlaceholder")} prefix={<SearchOutlined aria-hidden="true" />} className="users-search-input" style={{ width: 300 }} value={searchText} onChange={(event) => { setSearchText(event.target.value); setCurrent(1); }} allowClear aria-label={t("common.search")} /> <Input placeholder={t("users.searchPlaceholder")} prefix={<SearchOutlined aria-hidden="true" />} className="users-search-input" style={{ width: 300 }} value={searchText} onChange={(event) => { setSearchText(event.target.value); setCurrent(1); }} allowClear aria-label={t("common.search")} />
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t("common.search")}</Button> <Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t("common.search")}</Button>
<Button onClick={handleResetSearch}>{t("common.reset")}</Button> <Button onClick={handleResetSearch}>{t("common.reset")}</Button>
</Space> </Space>
{can("sys:user:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>} {can("sys:user:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>}
</Space> </Space>
@ -396,18 +397,9 @@ export default function Users() {
</Card> </Card>
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}> <Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}> <div className="flex-1 min-h-0 h-full">
<ListTable rowKey="userId" columns={columns} dataSource={filteredData} loading={loading} scroll={{ y: "calc(100vh - 420px)" }} pagination={false} /> <Table rowKey="userId" columns={columns} dataSource={filteredData} loading={loading} size="middle" scroll={{ y: "calc(100vh - 420px)" }} pagination={getStandardPagination(filteredData.length, current, pageSize, (page, size) => { setCurrent(page); setPageSize(size); })} />
</div> </div>
<AppPagination
current={current}
pageSize={pageSize}
total={filteredData.length}
onChange={(page, size) => {
setCurrent(page);
setPageSize(size);
}}
/>
</Card> </Card>
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}> <Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
@ -425,7 +417,30 @@ export default function Users() {
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}> <Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
<Button icon={<UploadOutlined />} loading={avatarUploading} style={{ marginBottom: 16 }}>{t("profile.uploadAvatar")}</Button> <Button icon={<UploadOutlined />} loading={avatarUploading} style={{ marginBottom: 16 }}>{t("profile.uploadAvatar")}</Button>
</Upload> </Upload>
<Form.Item label={t("users.password")} name="password" rules={[{ required: !editing, message: t("users.password") }]}><Input.Password placeholder={editing ? t("usersExt.passwordKeepPlaceholder") : t("usersExt.passwordInitPlaceholder")} /></Form.Item> <Form.Item label={t("users.password")} name="password" rules={[{ required: !editing, message: t("users.password") }]}><Input.Password placeholder={editing ? t("usersExt.passwordKeepPlaceholder") : t("usersExt.passwordInitPlaceholder")} autoComplete="new-password" /></Form.Item>
{passwordValue && (
<Form.Item
label={t("usersExt.confirmPassword")}
name="confirmPassword"
dependencies={["password"]}
rules={[
({ getFieldValue }) => ({
required: !editing || !!getFieldValue("password"),
message: t("usersExt.confirmPassword"),
}),
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue("password") === value) {
return Promise.resolve();
}
return Promise.reject(new Error(t("profile.passwordsDoNotMatch")));
},
}),
]}
>
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
</Form.Item>
)}
<Form.Item label={t("users.roles")} name="roleIds" rules={[{ required: true, message: t("users.roles") }]}><Select mode="multiple" placeholder={t("users.roles")} options={roleOptions} optionFilterProp={isPlatformMode ? "searchText" : "label"} /></Form.Item> <Form.Item label={t("users.roles")} name="roleIds" rules={[{ required: true, message: t("users.roles") }]}><Select mode="multiple" placeholder={t("users.roles")} options={roleOptions} optionFilterProp={isPlatformMode ? "searchText" : "label"} /></Form.Item>
{!isPlatformMode && <Form.Item label={t("users.orgNode")} name="orgId"><TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} /></Form.Item>} {!isPlatformMode && <Form.Item label={t("users.orgNode")} name="orgId"><TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} /></Form.Item>}
<Row gutter={16}> <Row gutter={16}>