Browse Source

feat: 页面优化

BaiLuoYan 1 day ago
parent
commit
5ed09b3ba9

+ 1 - 1
config/config.ts

@@ -284,7 +284,7 @@ export default defineConfig({
    * @description 内置了 babel import 插件
    * @doc https://umijs.org/docs/max/antd#antd
    */
-  antd: {},
+  antd: { configProvider: { theme: { cssVar: true } } },
   /**
    * @name 网络请求配置
    * @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。

+ 6 - 3
src/pages/Admin/Dept/components/UserTable.tsx

@@ -33,13 +33,14 @@ export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) =
       key: 'productRoles',
       render: (_, r) => {
         const info = userProductInfo.get(r.id) ?? [];
-        if (info.length === 0) return <span className="text-gray-400 text-xs">无</span>;
+        if (info.length === 0)
+          return <span className="text-(--ant-color-text-quaternary) text-xs">无</span>;
         return (
           <div className="flex flex-col gap-2">
             {info.map((p) => (
               <div
                 key={p.productName}
-                className="flex flex-row gap-1 rounded border border-dashed border-gray-400 bg-gray-50 px-2 py-2"
+                className="flex flex-row gap-1 rounded border border-dashed border-(--ant-color-border) bg-(--ant-color-fill-quaternary) px-2 py-2"
               >
                 <Tag color="purple" className="mr-0 self-center">
                   {p.productName}
@@ -58,7 +59,9 @@ export const DeptUserTable = ({ users, productRolesBase }: DeptUserTableProps) =
                       </Tag>
                     ))
                   ) : (
-                    <span className="text-gray-400 text-xs whitespace-nowrap">无角色</span>
+                    <span className="text-(--ant-color-text-quaternary) text-xs whitespace-nowrap">
+                      无角色
+                    </span>
                   )}
                 </div>
               </div>

+ 61 - 47
src/pages/Admin/Dept/index.tsx

@@ -1,7 +1,8 @@
 import { fetchDeleteDept } from '@/services/dept';
-import { PageContainer } from '@ant-design/pro-components';
+import { ReloadOutlined } from '@ant-design/icons';
+import { PageContainer, ProCard } from '@ant-design/pro-components';
 import { useIntl } from '@umijs/max';
-import { Button, Empty, Popconfirm, Space, Spin, Tree, message } from 'antd';
+import { Button, Empty, Popconfirm, Space, Spin, Tooltip, Tree, message } from 'antd';
 import type { DataNode } from 'antd/es/tree';
 import { DeptForm } from './components/Form';
 import { DeptUserTable } from './components/UserTable';
@@ -31,21 +32,23 @@ export default function DeptPage() {
   const renderTitle = (node: API.DeptItem) => (
     <Space>
       <span>{node.name}</span>
-      <DeptForm
-        mode="edit"
-        initialValues={node}
-        onSuccess={loadData}
-        trigger={<a className="text-xs">编辑</a>}
-      />
-      <DeptForm
-        mode="add"
-        initialValues={{ parentId: node.id }}
-        onSuccess={loadData}
-        trigger={<a className="text-xs">新建子部门</a>}
-      />
-      <Popconfirm title="确认删除?" onConfirm={() => handleDelete(node.id)}>
-        <a className="text-xs text-red-500">删除</a>
-      </Popconfirm>
+      <Space onClick={(e) => e.stopPropagation()}>
+        <DeptForm
+          mode="edit"
+          initialValues={node}
+          onSuccess={loadData}
+          trigger={<a className="text-xs">编辑</a>}
+        />
+        <DeptForm
+          mode="add"
+          initialValues={{ parentId: node.id }}
+          onSuccess={loadData}
+          trigger={<a className="text-xs">新建子部门</a>}
+        />
+        <Popconfirm title={`确认删除「${node.name}」?`} onConfirm={() => handleDelete(node.id)}>
+          <a className="text-xs text-(--ant-color-error)">删除</a>
+        </Popconfirm>
+      </Space>
     </Space>
   );
 
@@ -58,38 +61,49 @@ export default function DeptPage() {
 
   return (
     <PageContainer title={intl.formatMessage({ id: 'admin.dept.title' })}>
-      <div className="mb-4">
-        <DeptForm
-          mode="add"
-          initialValues={{ parentId: 0 }}
-          onSuccess={loadData}
-          trigger={
-            <Button type="primary">{intl.formatMessage({ id: 'admin.dept.createRoot' })}</Button>
-          }
-        />
-      </div>
-      <div className="flex gap-4">
-        <div className="w-80 shrink-0 border border-gray-100 rounded p-3 bg-white">
-          <Spin spinning={loading}>
-            {treeData.length > 0 && (
-              <Tree
-                treeData={renderNodes(treeData)}
-                defaultExpandAll
-                showLine
-                selectedKeys={selectedDeptId ? [selectedDeptId] : []}
-                onSelect={(keys) => setSelectedDeptId(keys[0] as number | undefined)}
-              />
-            )}
-          </Spin>
+      <ProCard>
+        <div className="flex items-center justify-between mb-3">
+          <DeptForm
+            mode="add"
+            initialValues={{ parentId: 0 }}
+            onSuccess={loadData}
+            trigger={
+              <Button type="primary">{intl.formatMessage({ id: 'admin.dept.createRoot' })}</Button>
+            }
+          />
+          <Tooltip title="刷新">
+            <span
+              className="cursor-pointer text-(--ant-color-text) hover:text-(--ant-color-primary) transition-colors"
+              onClick={loadData}
+            >
+              <ReloadOutlined style={{ fontSize: 16 }} />
+            </span>
+          </Tooltip>
         </div>
-        <div className="flex-1 min-w-0">
-          {selectedDeptIds ? (
-            <DeptUserTable users={filteredUsers} productRolesBase={productRolesBase} />
-          ) : (
-            <Empty description="请在左侧选择部门" className="py-16" />
-          )}
+        <div className="flex gap-4">
+          <div className="w-80 shrink-0 bg-(--ant-color-fill-quaternary) rounded p-2">
+            <Spin spinning={loading}>
+              {treeData.length > 0 && (
+                <Tree
+                  className="bg-transparent!"
+                  treeData={renderNodes(treeData)}
+                  defaultExpandAll
+                  showLine
+                  selectedKeys={selectedDeptId ? [selectedDeptId] : []}
+                  onSelect={(keys) => setSelectedDeptId(keys[0] as number | undefined)}
+                />
+              )}
+            </Spin>
+          </div>
+          <div className="flex-1 min-w-0">
+            {selectedDeptIds ? (
+              <DeptUserTable users={filteredUsers} productRolesBase={productRolesBase} />
+            ) : (
+              <Empty description="请在左侧选择部门" className="py-16" />
+            )}
+          </div>
         </div>
-      </div>
+      </ProCard>
     </PageContainer>
   );
 }

+ 5 - 2
src/pages/Admin/Product/Detail/components/CredentialModal.tsx

@@ -11,8 +11,11 @@ interface CredentialModalProps {
 
 const CopyField = ({ label, value }: { label: string; value: string }) => (
   <div className="mb-3">
-    <div className="text-xs text-gray-500 mb-1">{label}</div>
-    <Paragraph copyable className="font-mono bg-gray-50 px-3 py-2 rounded mb-0">
+    <div className="text-xs text-(--ant-color-text-tertiary) mb-1">{label}</div>
+    <Paragraph
+      copyable
+      className="font-mono bg-(--ant-color-fill-quaternary) px-3 py-2 rounded mb-0"
+    >
       {value}
     </Paragraph>
   </div>

+ 19 - 12
src/pages/Admin/Product/Detail/index.tsx

@@ -1,6 +1,6 @@
 import { fetchProductDetail } from '@/services/product';
-import { PageContainer } from '@ant-design/pro-components';
-import { useNavigate, useParams } from '@umijs/max';
+import { PageContainer, ProCard } from '@ant-design/pro-components';
+import { useLocation, useNavigate, useParams } from '@umijs/max';
 import { Spin, Tabs, Tag } from 'antd';
 import { useEffect, useState } from 'react';
 import { ProductForm } from '../components/Form';
@@ -11,6 +11,7 @@ import { RoleTab } from './tabs/RoleTab';
 export default function ProductDetailPage() {
   const { id } = useParams<{ id: string }>();
   const navigate = useNavigate();
+  const location = useLocation();
   const [product, setProduct] = useState<API.ProductItem | null>(null);
   const [loading, setLoading] = useState(false);
 
@@ -35,14 +36,14 @@ export default function ProductDetailPage() {
     <Spin spinning={loading}>
       <PageContainer
         title={
-          <span>
+          <div className="flex items-center gap-1">
             {product?.name}
             <Tag color={product?.status === 1 ? 'success' : 'default'} className="ml-2">
               {product?.status === 1 ? '启用' : '禁用'}
             </Tag>
-          </span>
+          </div>
         }
-        onBack={() => navigate('/admin/products')}
+        onBack={() => navigate('/admin/products', { state: location.state })}
         extra={
           product && (
             <ProductForm
@@ -55,13 +56,19 @@ export default function ProductDetailPage() {
         }
       >
         {product && (
-          <Tabs
-            items={[
-              { key: 'roles', label: '角色', children: <RoleTab productCode={product.code} /> },
-              { key: 'perms', label: '权限', children: <PermTab productCode={product.code} /> },
-              { key: 'members', label: '成员', children: <MemberTab productCode={product.code} /> },
-            ]}
-          />
+          <ProCard>
+            <Tabs
+              items={[
+                { key: 'roles', label: '角色', children: <RoleTab productCode={product.code} /> },
+                { key: 'perms', label: '权限', children: <PermTab productCode={product.code} /> },
+                {
+                  key: 'members',
+                  label: '成员',
+                  children: <MemberTab productCode={product.code} />,
+                },
+              ]}
+            />
+          </ProCard>
         )}
       </PageContainer>
     </Spin>

+ 4 - 3
src/pages/Admin/Product/Detail/tabs/MemberTab.tsx

@@ -99,7 +99,8 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
       render: (_, r) => {
         const roleIds = userRoleIds.get(r.userId) ?? [];
         const names = roleIds.map((id) => roleMap.get(id)).filter(Boolean) as string[];
-        if (names.length === 0) return <span className="text-gray-400 text-xs">无</span>;
+        if (names.length === 0)
+          return <span className="text-(--ant-color-text-quaternary) text-xs">无</span>;
         return (
           <div className="flex flex-row flex-wrap gap-1">
             {names.map((name) => (
@@ -162,7 +163,7 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
         </a>,
         <Popconfirm
           key="remove"
-          title="确认移除该成员?"
+          title={`确认移除「${r.username}」?`}
           onConfirm={async () => {
             const res = await fetchRemoveMember({ id: r.id });
             if (!res.success) return;
@@ -170,7 +171,7 @@ export const MemberTab = ({ productCode }: MemberTabProps) => {
             actionRef.current?.reload();
           }}
         >
-          <a className="text-red-500">移除</a>
+          <a>移除</a>
         </Popconfirm>,
       ],
     },

+ 3 - 1
src/pages/Admin/Product/Detail/tabs/PermTab.tsx

@@ -23,7 +23,9 @@ export const PermTab = ({ productCode }: PermTabProps) => {
       <Typography.Text type="secondary" className="block mb-3">
         共 {perms.length} 条权限(由产品服务端同步,只读)
       </Typography.Text>
-      <PermTree mode="view" allPerms={perms} checkedPermIds={perms.map((p) => p.id)} />
+      <div className="bg-(--ant-color-fill-quaternary) rounded p-4">
+        <PermTree mode="view" allPerms={perms} checkedPermIds={perms.map((p) => p.id)} />
+      </div>
     </Spin>
   );
 };

+ 2 - 2
src/pages/Admin/Product/Detail/tabs/RoleTab.tsx

@@ -114,7 +114,7 @@ export const RoleTab = ({ productCode }: RoleTabProps) => {
         </a>,
         <Popconfirm
           key="del"
-          title="确认删除?"
+          title={`确认删除「${r.name}」?`}
           onConfirm={async () => {
             const res = await fetchDeleteRole({ id: r.id });
             if (!res.success) return;
@@ -122,7 +122,7 @@ export const RoleTab = ({ productCode }: RoleTabProps) => {
             actionRef.current?.reload();
           }}
         >
-          <a className="text-red-500">删除</a>
+          <a>删除</a>
         </Popconfirm>,
       ],
     },

+ 28 - 4
src/pages/Admin/Product/index.tsx

@@ -1,17 +1,28 @@
 import { fetchInitialCredentials, fetchProductList, fetchUpdateProduct } from '@/services/product';
 import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
-import { useIntl, useNavigate } from '@umijs/max';
+import { useIntl, useLocation, useNavigate } from '@umijs/max';
 import { Button, Popconfirm, Tag, message } from 'antd';
 import { useRef, useState } from 'react';
 import { ProductForm } from './components/Form';
 import { CredentialModal } from './Detail/components/CredentialModal';
 
+interface LocationState {
+  page?: number;
+  pageSize?: number;
+}
+
 export default function ProductPage() {
   const intl = useIntl();
   const navigate = useNavigate();
+  const location = useLocation();
+  const state = (location.state ?? {}) as LocationState;
   const actionRef = useRef<ActionType>();
   const [credData, setCredData] = useState<API.FetchInitialCredentialsResp | null>(null);
   const [credOpen, setCredOpen] = useState(false);
+  const [pagination, setPagination] = useState({
+    current: state.page ?? 1,
+    pageSize: state.pageSize ?? 20,
+  });
 
   const handleCreateSuccess = async (ticket?: string) => {
     actionRef.current?.reload();
@@ -37,7 +48,17 @@ export default function ProductPage() {
       title: '名称',
       dataIndex: 'name',
       width: 160,
-      render: (_, r) => <a onClick={() => navigate(`/admin/products/${r.id}`)}>{r.name}</a>,
+      render: (_, r) => (
+        <a
+          onClick={() =>
+            navigate(`/admin/products/${r.id}`, {
+              state: { page: pagination.current, pageSize: pagination.pageSize },
+            })
+          }
+        >
+          {r.name}
+        </a>
+      ),
     },
     { title: 'Code', dataIndex: 'code', width: 160, copyable: true },
     { title: 'App Key', dataIndex: 'appKey', width: 300, copyable: true },
@@ -65,10 +86,10 @@ export default function ProductPage() {
         />,
         <Popconfirm
           key="status"
-          title={r.status === 1 ? '确认禁用?' : '确认启用?'}
+          title={r.status === 1 ? `确认禁用「${r.name}」?` : `确认启用「${r.name}」?`}
           onConfirm={() => handleToggleStatus(r)}
         >
-          <a className={r.status === 1 ? 'text-red-500' : ''}>{r.status === 1 ? '禁用' : '启用'}</a>
+          <a>{r.status === 1 ? '禁用' : '启用'}</a>
         </Popconfirm>,
       ],
     },
@@ -85,9 +106,12 @@ export default function ProductPage() {
         scroll={{ x: 900 }}
         tableLayout="fixed"
         pagination={{
+          current: pagination.current,
+          pageSize: pagination.pageSize,
           defaultPageSize: 20,
           pageSizeOptions: [10, 20, 50, 100],
           showSizeChanger: true,
+          onChange: (page, pageSize) => setPagination({ current: page, pageSize }),
         }}
         toolBarRender={() => [
           <ProductForm

+ 10 - 5
src/pages/Admin/User/index.tsx

@@ -53,13 +53,14 @@ export default function UserPage() {
       key: 'productRoles',
       render: (_, r) => {
         const info = userProductInfo.get(r.id) ?? [];
-        if (info.length === 0) return <span className="text-gray-400 text-xs">无</span>;
+        if (info.length === 0)
+          return <span className="text-(--ant-color-text-quaternary) text-xs">无</span>;
         return (
           <div className="flex flex-col gap-1">
             {info.map((p) => (
               <div
                 key={p.productName}
-                className="flex flex-row gap-1 rounded border border-dashed border-gray-400 bg-gray-50 px-2 py-2"
+                className="flex flex-row gap-1 rounded border border-dashed border-(--ant-color-border) bg-(--ant-color-fill-quaternary) px-2 py-2"
               >
                 <Tag color="purple" className="mr-0 self-center">
                   {p.productName}
@@ -78,7 +79,9 @@ export default function UserPage() {
                       </Tag>
                     ))
                   ) : (
-                    <span className="text-gray-400 text-xs whitespace-nowrap">无角色</span>
+                    <span className="text-(--ant-color-text-quaternary) text-xs whitespace-nowrap">
+                      无角色
+                    </span>
                   )}
                 </div>
               </div>
@@ -115,10 +118,12 @@ export default function UserPage() {
         </a>,
         <Popconfirm
           key="status"
-          title={r.status === 1 ? '确认冻结?' : '确认解冻?'}
+          title={r.status === 1 ? `确认冻结「${r.username}」?` : `确认解冻「${r.username}」?`}
           onConfirm={() => handleToggleStatus(r)}
         >
-          <a className={r.status === 1 ? 'text-red-500' : ''}>{r.status === 1 ? '冻结' : '解冻'}</a>
+          <a className={r.status === 1 ? 'text-(--ant-color-error)' : ''}>
+            {r.status === 1 ? '冻结' : '解冻'}
+          </a>
         </Popconfirm>,
       ],
     },

+ 7 - 3
src/pages/Admin/_shared/BindRolesDrawer.tsx

@@ -115,7 +115,9 @@ export const BindRolesDrawer = ({
       }
     >
       {products.length === 0 && !defaultProductCode ? (
-        <span className="text-gray-400 text-sm">用户不属于任何产品,无法分配角色</span>
+        <span className="text-(--ant-color-text-quaternary) text-sm">
+          用户不属于任何产品,无法分配角色
+        </span>
       ) : (
         <>
           {!defaultProductCode && (
@@ -139,11 +141,13 @@ export const BindRolesDrawer = ({
                     onChange={() => toggle(r.id)}
                   >
                     <span>{r.name}</span>
-                    <span className="text-xs text-gray-400 ml-2">级别 {r.permsLevel}</span>
+                    <span className="text-xs text-(--ant-color-text-quaternary) ml-2">
+                      级别 {r.permsLevel}
+                    </span>
                   </Checkbox>
                 ))}
                 {roles.length === 0 && !loading && (
-                  <span className="text-gray-400 text-sm">该产品暂无角色</span>
+                  <span className="text-(--ant-color-text-quaternary) text-sm">该产品暂无角色</span>
                 )}
               </div>
             </Spin>

+ 2 - 2
src/pages/Admin/_shared/PermTree/components/PermList.tsx

@@ -17,8 +17,8 @@ export const PermList = memo(({ perms, checkedIds, onToggle, mode }: PermListPro
           {mode === 'edit' && (
             <Checkbox checked={checkedIds.has(p.id)} onChange={() => onToggle?.(p)} />
           )}
-          <span className="text-xs font-mono text-blue-500">{p.code}</span>
-          <span className="text-xs text-gray-500">{p.name}</span>
+          <span className="text-xs font-mono text-(--ant-color-primary)">{p.code}</span>
+          <span className="text-xs text-(--ant-color-text-tertiary)">{p.name}</span>
         </label>
       ))}
     </div>

+ 6 - 3
src/pages/Admin/_shared/PermTree/components/TabContent.tsx

@@ -56,10 +56,13 @@ export const TabContent = memo(
         {displayGroups.map((g) => {
           const owned = g.perms.filter((p) => checkedIds.has(p.id)).length;
           return (
-            <div key={g.modelName} className="border border-gray-100 rounded p-3">
-              <div className="text-sm font-medium text-gray-700 mb-2">
+            <div
+              key={g.modelName}
+              className="border border-(--ant-color-border-secondary) rounded p-3"
+            >
+              <div className="text-sm font-medium text-(--ant-color-text-secondary) mb-2">
                 {g.modelName}
-                <span className="text-xs text-gray-400 ml-1">
+                <span className="text-xs text-(--ant-color-text-quaternary) ml-1">
                   ({owned}
                   {mode === 'edit' ? `/${g.perms.length}` : ''})
                 </span>

+ 3 - 1
src/pages/Admin/_shared/UserPermDrawer.tsx

@@ -136,7 +136,9 @@ export const UserPermDrawer = ({
       }
     >
       {products.length === 0 && !defaultProductCode ? (
-        <span className="text-gray-400 text-sm">用户不属于任何产品,无法设置权限</span>
+        <span className="text-(--ant-color-text-quaternary) text-sm">
+          用户不属于任何产品,无法设置权限
+        </span>
       ) : (
         <>
           {!defaultProductCode && (