فهرست منبع

feat: add PermTree component

BaiLuoYan 3 روز پیش
والد
کامیت
2c300396a3
1فایلهای تغییر یافته به همراه166 افزوده شده و 0 حذف شده
  1. 166 0
      src/pages/Admin/_shared/PermTree/index.tsx

+ 166 - 0
src/pages/Admin/_shared/PermTree/index.tsx

@@ -0,0 +1,166 @@
+// web/src/pages/Admin/_shared/PermTree/index.tsx
+import { useIntl } from '@umijs/max';
+import { Checkbox, Empty, Input, Tabs } from 'antd';
+import { useState } from 'react';
+import { groupPerms, sortPerms } from './lib/permUtils';
+import { usePermTree } from './usePermTree';
+
+export interface PermTreeProps {
+  mode: 'view' | 'edit';
+  allPerms: API.PermItem[];
+  checkedPermIds?: number[];
+  onChange?: (ids: number[]) => void;
+}
+
+const PermList = ({
+  perms,
+  checkedIds,
+  onToggle,
+  mode,
+}: {
+  perms: API.PermItem[];
+  checkedIds: Set<number>;
+  onToggle?: (p: API.PermItem) => void;
+  mode: 'view' | 'edit';
+}) => {
+  if (perms.length === 0) return null;
+  return (
+    <div className="flex flex-col gap-1">
+      {perms.map((p) => (
+        <label key={p.id} className="flex items-center gap-2 cursor-pointer py-0.5">
+          {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>
+        </label>
+      ))}
+    </div>
+  );
+};
+
+const TabContent = ({
+  tab,
+  filteredGroups,
+  allGroups,
+  checkedIds,
+  mode,
+  onToggle,
+}: {
+  tab: 'api' | 'data';
+  filteredGroups: ReturnType<typeof groupPerms>;
+  allGroups: ReturnType<typeof groupPerms>;
+  checkedIds: Set<number>;
+  mode: 'view' | 'edit';
+  onToggle?: (p: API.PermItem) => void;
+}) => {
+  const intl = useIntl();
+  const groups =
+    mode === 'view'
+      ? allGroups
+          .map((g) => ({
+            ...g,
+            apiPerms: g.apiPerms.filter((p) => checkedIds.has(p.id)),
+            dataPerms: g.dataPerms.filter((p) => checkedIds.has(p.id)),
+            fieldPerms: g.fieldPerms.filter((p) => checkedIds.has(p.id)),
+          }))
+          .filter(
+            (g) =>
+              (tab === 'api' ? g.apiPerms.length : g.dataPerms.length + g.fieldPerms.length) > 0,
+          )
+      : filteredGroups;
+
+  if (groups.length === 0)
+    return (
+      <Empty description={intl.formatMessage({ id: 'admin.permTree.empty' })} className="py-8" />
+    );
+
+  return (
+    <div className="flex flex-col gap-3">
+      {groups.map((g) => {
+        const perms = tab === 'api' ? g.apiPerms : [...g.dataPerms, ...g.fieldPerms];
+        if (perms.length === 0) return null;
+        const owned = 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">
+              {g.modelName}
+              <span className="text-xs text-gray-400 ml-1">
+                ({owned}
+                {mode === 'edit' ? `/${perms.length}` : ''})
+              </span>
+            </div>
+            <PermList perms={perms} checkedIds={checkedIds} onToggle={onToggle} mode={mode} />
+          </div>
+        );
+      })}
+    </div>
+  );
+};
+
+export const PermTree = ({ mode, allPerms, checkedPermIds = [], onChange }: PermTreeProps) => {
+  const intl = useIntl();
+  const {
+    keyword,
+    setKeyword,
+    checkedIds,
+    filteredGroups,
+    toggle,
+    selectAll,
+    invert,
+    isAllSelected,
+    isIndeterminate,
+  } = usePermTree({ allPerms, initialCheckedIds: checkedPermIds, onChange });
+
+  const allGroups = groupPerms(sortPerms(allPerms));
+  const [activeTab, setActiveTab] = useState<'api' | 'data'>('api');
+
+  const tabItems = [
+    { key: 'api', label: intl.formatMessage({ id: 'admin.permTree.apiTab' }) },
+    { key: 'data', label: intl.formatMessage({ id: 'admin.permTree.dataTab' }) },
+  ];
+
+  return (
+    <div className="flex flex-col gap-2">
+      {mode === 'edit' && (
+        <div className="flex items-center gap-2">
+          <Input.Search
+            placeholder={intl.formatMessage({ id: 'admin.permTree.search' })}
+            value={keyword}
+            onChange={(e) => setKeyword(e.target.value)}
+            allowClear
+            className="flex-1"
+          />
+          <Checkbox
+            checked={isAllSelected(activeTab)}
+            indeterminate={isIndeterminate(activeTab)}
+            onChange={() => selectAll(activeTab)}
+          >
+            {intl.formatMessage({ id: 'admin.permTree.selectAll' })}
+          </Checkbox>
+          <a onClick={() => invert(activeTab)} className="text-sm whitespace-nowrap">
+            {intl.formatMessage({ id: 'admin.permTree.invert' })}
+          </a>
+        </div>
+      )}
+      <Tabs
+        activeKey={activeTab}
+        onChange={(k) => setActiveTab(k as 'api' | 'data')}
+        items={tabItems.map((t) => ({
+          key: t.key,
+          label: t.label,
+          children: (
+            <TabContent
+              tab={t.key as 'api' | 'data'}
+              filteredGroups={filteredGroups}
+              allGroups={allGroups}
+              checkedIds={checkedIds}
+              mode={mode}
+              onToggle={toggle}
+            />
+          ),
+        }))}
+      />
+    </div>
+  );
+};