|
@@ -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>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|