refactor: 更新用户页面组件和分页逻辑
- 优化 `index.tsx` 中的导入和表单字段 - 使用 `Table` 组件替换 `ListTable` 和 `AppPagination` - 添加密码确认字段及其验证规则 - 调整工具栏和表格布局以提高一致性dev_na
parent
900f092d5e
commit
ce4743c5ea
|
|
@ -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}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue