提交 2c95eb73 authored 作者: 王鹏飞's avatar 王鹏飞

refactor(admin): migrate access role module

上级 163696c5
# access/role migration report
## Status
Migrated `/jurisdiction/role` into `modules/access/role`. The active access route
now lazy-loads the module route and view. The old role page was removed after a
successful module build; its request file remains as a compatibility dependency
for the separately owned teachers module.
## Preserved contract
- List API remains `POST /user/role/getList` through `utils/request.js` with a
`qs.stringify` body and `page`, `page_size`, `name`, `start_time`, `end_time`.
- Add, detail, edit, delete, status, selected-permission, permission-save, and
all-permissions endpoints are unchanged.
- Status mutation still sends string `"1"` for enabled and `"2"` for disabled.
- Permission save still sends `{ role_id, power_list }`.
- Existing business success/error messages and false-return behavior remain in
the API adapter.
- Filters remain role name and creation time; date values use local-day second
timestamps. Pagination remains 1/10 with 10/20/30/40 choices.
- All seven columns remain in order. The displayed ID remains the page-relative
sequence number, and remarks retain the 300px three-line Popover treatment.
- Permissions remain exact: `changeStatus`, `getInfoById`, `getPowerByRoleId`,
`del`, and `add` under `/jurisdiction/role/*`.
- Add/edit validation, deletion confirmation, role types, and mutation refresh
behavior are retained.
- The authorization tree remains a module-private component with strict parent/
child checking, descendant selection, ancestor selection, and the original
fixed submit footer. It is not part of AppList.
## Architecture
- `api.ts`: legacy request protocol and awaited response handling.
- `query.ts`: role filter mapping.
- `useRoleList.ts`: controlled rows, filters, pagination, loading, error, retry,
and stale-response protection.
- `views/RoleListView.tsx`: permissions, mutations, AppList, and drawer control.
- `components/`: role form, delete confirmation, and permission tree drawer.
- `routes.tsx`: module-owned leaf route with unchanged public URL.
## Verification
- `modules/access/role` oxlint and oxfmt checks pass.
- `pnpm test` passed all 11 global Node tests.
- Post-delete `pnpm build` passed with only the existing formula-editor and
large-chunk warnings.
- `pnpm exec tsc --noEmit` contains no access/role diagnostics; unrelated
existing/concurrent workspace diagnostics remain.
## Concerns
1. No API/query/Hook tests were added, per instruction.
2. `apps/admin/src/modules/teachers/list/views/TeacherListView.jsx` still imports
`pages/jurisdiction/role/request.js` for `getAllList`. That request file is
therefore retained; removing the cross-module dependency requires a teachers
task and is outside this scope.
3. Deprecated `src/routes/routes.jsx` and `src/config/component.js` still contain
the old role page path as inactive metadata. Active routes and builds use the
new module.
import { message } from "antd";
// @ts-expect-error The legacy workspace does not install the qs declaration package.
import qs from "qs";
import request from "@/utils/request.js";
import type { RoleQuery } from "./query";
export interface RoleRow {
id: number;
name?: string;
type?: number | string;
remarks?: string;
status?: number | string;
create_time?: string;
}
export interface PermissionRecord {
id: number;
label?: string;
children?: PermissionRecord[];
[key: string]: unknown;
}
export interface RoleListResult {
rows: RoleRow[];
total: number;
}
type ApiParams = Record<string, unknown>;
async function getPayload<T>(url: string, params: ApiParams): Promise<T | false> {
try {
const response = await request.post(url, qs.stringify(params));
const body = response.data as { code?: number; data?: T; message?: string };
const code = body?.code ?? 0;
const data = (body?.data ?? []) as T;
const text = body?.message ?? "数据获取失败!";
if (code === 200) return data;
message.error(text);
return false;
} catch {
return false;
}
}
async function mutate(url: string, params: ApiParams) {
try {
const response = await request.post(url, qs.stringify(params));
const body = response.data as { code?: number; message?: string };
const code = body?.code ?? 0;
const text = body?.message ?? "数据获取失败!";
if (code === 200) {
message.success(text);
return true;
}
message.error(text);
return false;
} catch {
return false;
}
}
export async function getRoleList(params: RoleQuery): Promise<RoleListResult | false> {
const data = await getPayload<{ list?: RoleRow[]; total?: number }>("/user/role/getList", {
...params,
});
if (data === false) return false;
return { rows: data.list ?? [], total: data.total ?? 0 };
}
export function getRoleInfo(id: number) {
return getPayload<RoleRow>("/user/role/getInfoById", { id });
}
export function addRole(params: ApiParams) {
return mutate("/user/role/add", params);
}
export function editRole(params: ApiParams) {
return mutate("/user/role/edit", params);
}
export function deleteRole(id: number) {
return mutate("/user/role/del", { id });
}
export function changeRoleStatus(id: number, status: "1" | "2") {
return mutate("/user/role/changeStatus", { status, id });
}
export function getRolePermissionIds(roleId: number) {
return getPayload<Array<string | number>>("/user/role/getPowerByRoleId", { role_id: roleId });
}
export function setRolePermissions(roleId: number, powerList: Array<string | number>) {
return mutate("/user/role/setRolePower", { role_id: roleId, power_list: powerList });
}
export function getAllPermissions() {
return getPayload<PermissionRecord[]>("/system/power/getListAll", {});
}
import { Modal, Row, Space } from "antd";
interface DeleteRoleModalProps {
open: boolean;
onCancel: () => void;
onConfirm: () => void | Promise<void>;
}
export default function DeleteRoleModal({ open, onCancel, onConfirm }: DeleteRoleModalProps) {
return (
<Modal
mask={false}
centered
open={open}
onOk={onConfirm}
onCancel={onCancel}
footer={(_, { OkBtn, CancelBtn }) => (
<Row justify="center" className="delModal" style={{ marginBottom: "30px" }}>
<Space size={20}>
<CancelBtn />
<OkBtn />
</Space>
</Row>
)}
okText="确认"
>
<p style={{ textAlign: "center", padding: "30px 0 18px", fontSize: 16 }}>
角色将永久删除,是否继续?
</p>
</Modal>
);
}
import { Button, Drawer, Form, Space, Tree } from "antd";
import type { DataNode } from "antd/es/tree";
import type { Key } from "react";
import { findParents } from "@/utils/common.js";
export type PermissionTreeNode = DataNode & {
key: Key;
children?: PermissionTreeNode[];
};
interface PermissionDrawerProps {
open: boolean;
treeData: PermissionTreeNode[];
checkedKeys: Key[];
onCheckedKeysChange: (keys: Key[]) => void;
onClose: () => void;
onSubmit: () => void | Promise<void>;
}
function collectNodeKeys(node: PermissionTreeNode) {
const keys: Key[] = [node.key];
node.children?.forEach((child) => keys.push(...collectNodeKeys(child)));
return keys;
}
export default function PermissionDrawer({
open,
treeData,
checkedKeys,
onCheckedKeysChange,
onClose,
onSubmit,
}: PermissionDrawerProps) {
return (
<Drawer placement="right" open={open} onClose={onClose} mask={false}>
<Form labelCol={{ span: 6 }}>
<Form.Item name="power_list" style={{ padding: 10, paddingTop: 0 }}>
<Tree
checkable
checkedKeys={checkedKeys}
defaultExpandAll={false}
treeData={treeData}
checkStrictly
onCheck={(nextCheckedKeys, info) => {
const nextKeys = Array.isArray(nextCheckedKeys)
? nextCheckedKeys
: nextCheckedKeys.checked;
const node = info.node as PermissionTreeNode;
const nodeAndChildren = collectNodeKeys(node);
if (info.checked) {
const parents = (findParents(treeData, "key", node.key) ?? []) as Key[];
onCheckedKeysChange([...nextKeys, ...nodeAndChildren, ...parents]);
return;
}
onCheckedKeysChange(nextKeys.filter((key) => !nodeAndChildren.includes(key)));
}}
/>
</Form.Item>
<Form.Item
wrapperCol={{ offset: 7, span: 16 }}
style={{
position: "fixed",
bottom: 0,
width: "30%",
background: "#fff",
paddingBottom: 20,
margin: 0,
}}
>
<Space size={20}>
<Button onClick={onClose}>取消</Button>
<Button type="primary" htmlType="submit" onClick={onSubmit}>
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
import { Button, Drawer, Form, Input, Radio, Space } from "antd";
import { useEffect } from "react";
const { TextArea } = Input;
export interface RoleFormValues {
name: string;
type: string;
remarks?: string;
}
interface RoleFormDrawerProps {
open: boolean;
initialRole: Partial<RoleFormValues> | null;
onClose: () => void;
onSubmit: (values: RoleFormValues) => void | Promise<void>;
}
export default function RoleFormDrawer({
open,
initialRole,
onClose,
onSubmit,
}: RoleFormDrawerProps) {
const [form] = Form.useForm<RoleFormValues>();
useEffect(() => {
if (!open) {
form.resetFields();
return;
}
if (initialRole) form.setFieldsValue(initialRole);
}, [form, initialRole, open]);
return (
<Drawer placement="right" onClose={onClose} open={open} mask={false}>
<Form form={form} onFinish={onSubmit} labelCol={{ span: 6 }}>
<Form.Item
name="name"
label="角色名称"
rules={[
{ required: true, message: "请输入角色名称" },
{ max: 50, message: "角色名称最多可输入50个字符" },
]}
>
<Input autoComplete="off" placeholder="请输入角色名称" />
</Form.Item>
<Form.Item
name="type"
label="角色类型"
rules={[{ required: true, message: "请选择角色类型" }]}
>
<Radio.Group style={{ textAlign: "center" }}>
<Radio value="1">管理员</Radio>
<Radio value="2">老师</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="remarks"
label="备注"
rules={[
{ required: false, message: "请输入备注" },
{ max: 100, message: "备注最多可输入100个字符" },
]}
>
<TextArea autoSize={{ minRows: 5, maxRows: 8 }} placeholder="请输入备注" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 12, span: 16 }}>
<Space size={20}>
<Button onClick={onClose}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
);
}
import type { Dayjs } from "dayjs";
export interface RoleFilters {
name?: string;
time?: [Dayjs | null, Dayjs | null] | null;
}
export interface RoleQuery {
page: number;
page_size: number;
name?: string;
start_time?: number;
end_time?: number;
}
interface BuildRoleQueryInput {
page: number;
pageSize: number;
filters: RoleFilters;
}
export function buildRoleQuery({ page, pageSize, filters }: BuildRoleQueryInput): RoleQuery {
const query: RoleQuery = { page, page_size: pageSize };
if (filters.name) query.name = filters.name;
if (filters.time?.[0] && filters.time[1]) {
query.start_time = filters.time[0].startOf("day").unix();
query.end_time = filters.time[1].endOf("day").unix();
}
return query;
}
import { Navigate, useRoutes } from "react-router";
import RoleListView from "./views/RoleListView";
export default function RoleRoutes() {
return useRoutes([
{ index: true, Component: RoleListView },
{ path: "*", element: <Navigate to="/jurisdiction/role" /> },
]);
}
import { useCallback, useEffect, useRef, useState } from "react";
import { getRoleList, type RoleListResult, type RoleRow } from "./api";
import { buildRoleQuery, type RoleFilters, type RoleQuery } from "./query";
const INITIAL_PAGE = 1;
const INITIAL_PAGE_SIZE = 10;
function isCleared(previous: RoleFilters, next: RoleFilters) {
const keys = new Set([...Object.keys(previous), ...Object.keys(next)]) as Set<keyof RoleFilters>;
return [...keys].some((key) => previous[key] !== next[key] && !next[key]);
}
export function useRoleList() {
const [filters, setFilterState] = useState<RoleFilters>({});
const [appliedFilters, setAppliedFilters] = useState<RoleFilters>({});
const [page, setPage] = useState(INITIAL_PAGE);
const [pageSize, setPageSize] = useState(INITIAL_PAGE_SIZE);
const [rows, setRows] = useState<RoleRow[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const requestId = useRef(0);
const lastQuery = useRef<RoleQuery>(
buildRoleQuery({ page: INITIAL_PAGE, pageSize: INITIAL_PAGE_SIZE, filters: {} }),
);
const load = useCallback(async (query: RoleQuery) => {
const currentRequest = ++requestId.current;
lastQuery.current = query;
setLoading(true);
setError(null);
try {
const result: RoleListResult | false = await getRoleList(query);
if (currentRequest !== requestId.current) return;
if (result === false) {
setError(new Error("数据获取失败!"));
return;
}
setRows(result.rows);
setTotal(result.total);
} catch (reason) {
if (currentRequest === requestId.current) {
setError(reason instanceof Error ? reason : new Error("数据获取失败!"));
}
} finally {
if (currentRequest === requestId.current) setLoading(false);
}
}, []);
const loadFor = useCallback(
(nextPage: number, nextPageSize: number, nextFilters: RoleFilters) =>
load(buildRoleQuery({ page: nextPage, pageSize: nextPageSize, filters: nextFilters })),
[load],
);
useEffect(() => {
void load(lastQuery.current);
return () => {
requestId.current += 1;
};
}, [load]);
const setFilters = useCallback(
(nextFilters: RoleFilters) => {
const shouldReload = isCleared(filters, nextFilters);
setFilterState(nextFilters);
if (shouldReload) {
setAppliedFilters(nextFilters);
void loadFor(page, pageSize, nextFilters);
}
},
[filters, loadFor, page, pageSize],
);
const search = useCallback(() => {
setAppliedFilters(filters);
setPage(INITIAL_PAGE);
void loadFor(INITIAL_PAGE, pageSize, filters);
}, [filters, loadFor, pageSize]);
const reset = useCallback(() => {
const emptyFilters: RoleFilters = {};
setFilterState(emptyFilters);
setAppliedFilters(emptyFilters);
setPage(INITIAL_PAGE);
void loadFor(INITIAL_PAGE, pageSize, emptyFilters);
}, [loadFor, pageSize]);
const refresh = useCallback(() => {
setAppliedFilters(filters);
void loadFor(page, pageSize, filters);
}, [filters, loadFor, page, pageSize]);
const refreshUnfiltered = useCallback(() => {
void loadFor(page, pageSize, {});
}, [loadFor, page, pageSize]);
const changePage = useCallback(
(nextPage: number) => {
setPage(nextPage);
void loadFor(nextPage, pageSize, appliedFilters);
},
[appliedFilters, loadFor, pageSize],
);
const changePageSize = useCallback(
(nextPageSize: number) => {
setPage(INITIAL_PAGE);
setPageSize(nextPageSize);
void loadFor(INITIAL_PAGE, nextPageSize, appliedFilters);
},
[appliedFilters, loadFor],
);
const retry = useCallback(() => void load(lastQuery.current), [load]);
return {
filters,
setFilters,
page,
pageSize,
rows,
total,
loading,
error,
search,
reset,
refresh,
refreshUnfiltered,
changePage,
changePageSize,
retry,
};
}
import { Button, DatePicker, Input, Popover, Space, Switch } from "antd";
import type { ColumnsType } from "antd/es/table";
import type { Key } from "react";
import { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { AppList, type AppListFilterItem } from "@/shared/components/AppList";
import { convertToAntdTreeData } from "@/utils/common.js";
import {
addRole,
changeRoleStatus,
deleteRole,
editRole,
getAllPermissions,
getRoleInfo,
getRolePermissionIds,
setRolePermissions,
type RoleRow,
} from "../api";
import DeleteRoleModal from "../components/DeleteRoleModal";
import PermissionDrawer, { type PermissionTreeNode } from "../components/PermissionDrawer";
import RoleFormDrawer, { type RoleFormValues } from "../components/RoleFormDrawer";
import type { RoleFilters } from "../query";
import { useRoleList } from "../useRoleList";
interface RootState {
user: {
operationPermissionsList?: string[];
};
}
const { RangePicker } = DatePicker;
export default function RoleListView() {
const list = useRoleList();
const permissions = useSelector((state: RootState) => state.user.operationPermissionsList ?? []);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [formOpen, setFormOpen] = useState(false);
const [isAdd, setIsAdd] = useState(false);
const [initialRole, setInitialRole] = useState<Partial<RoleFormValues> | null>(null);
const [deleteOpen, setDeleteOpen] = useState(false);
const [permissionOpen, setPermissionOpen] = useState(false);
const [permissionTree, setPermissionTree] = useState<PermissionTreeNode[]>([]);
const [checkedKeys, setCheckedKeys] = useState<Key[]>([]);
useEffect(() => {
let active = true;
void getAllPermissions().then((result) => {
if (active && result !== false) {
setPermissionTree(convertToAntdTreeData(result) as PermissionTreeNode[]);
}
});
return () => {
active = false;
};
}, []);
const handleEdit = async (id: number) => {
const role = await getRoleInfo(id);
if (role === false) return;
setSelectedId(id);
setIsAdd(false);
setInitialRole({
name: role.name,
remarks: role.remarks,
type: String(role.type),
});
setFormOpen(true);
};
const handleFormSubmit = async (values: RoleFormValues) => {
const params: Record<string, unknown> = { ...values };
if (!params.remarks) delete params.remarks;
const success = isAdd ? await addRole(params) : await editRole({ ...params, id: selectedId });
if (!success) return;
list.refreshUnfiltered();
setFormOpen(false);
};
const handleDelete = async () => {
if (selectedId === null) return;
const success = await deleteRole(selectedId);
if (success) list.refreshUnfiltered();
setDeleteOpen(false);
};
const handleStatus = async (checked: boolean, id: number) => {
const success = await changeRoleStatus(id, checked ? "1" : "2");
if (success) list.refreshUnfiltered();
};
const handleEmpower = async (id: number) => {
setSelectedId(id);
setPermissionOpen(true);
const result = await getRolePermissionIds(id);
setCheckedKeys(result === false ? [] : result);
};
const submitPermissions = async () => {
if (selectedId === null) return;
const success = await setRolePermissions(selectedId, checkedKeys as Array<string | number>);
if (!success) return;
list.refreshUnfiltered();
setPermissionOpen(false);
};
const columns: ColumnsType<RoleRow> = [
{
title: "ID",
key: "id",
dataIndex: "id",
render: (_, __, index) => list.pageSize * (list.page - 1) + index + 1,
},
{
title: "角色名称",
key: "name",
align: "center",
dataIndex: "name",
width: 300,
},
{
title: "角色类型",
key: "type",
align: "center",
dataIndex: "type",
width: 300,
render: (value) => {
if (Number(value) === 1) return "管理员";
if (Number(value) === 2) return "老师";
return "未知";
},
},
{
title: "备注",
key: "remarks",
align: "center",
dataIndex: "remarks",
width: 300,
render: (text: string) => (
<Popover content={text} overlayStyle={{ maxWidth: "400px" }}>
<div
style={{
display: "-webkit-box",
WebkitLineClamp: "3",
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center",
}}
>
{text}
</div>
</Popover>
),
},
{
title: "状态",
align: "center",
key: "status",
dataIndex: "status",
width: 300,
render: (_, record) => (
<Switch
checked={Number(record.status) === 1}
onChange={(checked) => void handleStatus(checked, record.id)}
disabled={!permissions.includes("/jurisdiction/role/changeStatus")}
/>
),
},
{
title: "创建时间",
key: "create_time",
align: "center",
dataIndex: "create_time",
},
{
title: "操作",
key: "handle",
align: "center",
dataIndex: "handle",
width: 400,
render: (_, record) => (
<Space>
{permissions.includes("/jurisdiction/role/getInfoById") ? (
<Button onClick={() => void handleEdit(record.id)}>编辑</Button>
) : null}
{permissions.includes("/jurisdiction/role/getPowerByRoleId") ? (
<Button onClick={() => void handleEmpower(record.id)}>授权</Button>
) : null}
{permissions.includes("/jurisdiction/role/del") ? (
<Button
onClick={() => {
setSelectedId(record.id);
setDeleteOpen(true);
}}
>
删除
</Button>
) : null}
</Space>
),
},
];
const filterItems: AppListFilterItem<RoleFilters>[] = [
{
name: "name",
label: "角色名称",
render: (field) => (
<Input
autoComplete="off"
allowClear
value={field.value ?? ""}
onChange={(event) => field.onChange(event.target.value)}
placeholder="请输入角色名称"
id="name"
/>
),
},
{
name: "time",
label: "创建时间",
render: (field) => (
<RangePicker
value={field.value}
onChange={(value) => field.onChange(value)}
id="createTime"
/>
),
},
];
const toolbar = permissions.includes("/jurisdiction/role/add") ? (
<Button
type="primary"
ghost
onClick={() => {
setSelectedId(null);
setInitialRole(null);
setIsAdd(true);
setFormOpen(true);
}}
>
添加
</Button>
) : undefined;
return (
<>
<AppList<RoleRow, RoleFilters>
rowKey="id"
columns={columns}
data={{
rows: list.rows,
total: list.total,
loading: list.loading,
error: list.error,
}}
filters={{
value: list.filters,
items: filterItems,
onChange: list.setFilters,
onSearch: list.search,
onReset: list.reset,
onRefresh: list.refresh,
}}
pagination={{
page: list.page,
pageSize: list.pageSize,
pageSizeOptions: [10, 20, 30, 40],
onChange: list.changePage,
onPageSizeChange: list.changePageSize,
}}
toolbar={toolbar}
onRetry={list.retry}
/>
<RoleFormDrawer
open={formOpen}
initialRole={initialRole}
onClose={() => setFormOpen(false)}
onSubmit={handleFormSubmit}
/>
<DeleteRoleModal
open={deleteOpen}
onCancel={() => setDeleteOpen(false)}
onConfirm={handleDelete}
/>
<PermissionDrawer
open={permissionOpen}
treeData={permissionTree}
checkedKeys={checkedKeys}
onCheckedKeysChange={setCheckedKeys}
onClose={() => setPermissionOpen(false)}
onSubmit={submitPermissions}
/>
</>
);
}
......@@ -4,7 +4,7 @@ import ModuleBoundary from "@/app/ModuleBoundary";
const AccessLayout = lazy(() => import("@/pages/jurisdiction/index"));
const AdminRoutes = lazy(() => import("./admin/routes"));
const Role = lazy(() => import("@/pages/jurisdiction/role/index"));
const RoleRoutes = lazy(() => import("./role/routes"));
export default function AccessRoutes() {
const routes = useRoutes([
......@@ -13,7 +13,7 @@ export default function AccessRoutes() {
children: [
{ index: true, element: <Navigate to="/jurisdiction/role" /> },
{ path: "admin/*", Component: AdminRoutes },
{ path: "role", Component: Role },
{ path: "role/*", Component: RoleRoutes },
],
},
{ path: "*", element: <Navigate to="/" /> },
......
import React, { useEffect, useState, useRef } from "react";
import {
Tree,
Table,
Input,
Button,
Row,
Col,
Space,
DatePicker,
Popover,
Drawer,
Form,
Modal,
Switch,
Radio,
Image,
} from "antd";
import { useNavigate } from "react-router";
const { RangePicker } = DatePicker;
const { TextArea } = Input;
import PaginationCom from "@/common/Pagination";
import dayjs from "dayjs";
import TableCom from "@/common/TableCom/index";
import { convertToAntdTreeData } from "@/utils/common.js";
import { useSelector } from "react-redux";
import {
getList,
addRole,
infoRole,
editRole,
changeStatus,
delRole,
getPowerByRoleId,
setRolePower,
getAllrole,
} from "./request";
import reload from "@/assets/images/icon/reload.png";
import reset from "@/assets/images/icon/reset.png";
import filter from "@/assets/images/icon/filter.png";
import add from "@/assets/images/icon/add.png";
import { findParents, findNodeById } from "@/utils/common";
import { PlusOutlined, CaretDownOutlined, CaretRightOutlined } from "@ant-design/icons";
const RoleList = () => {
const [data, setData] = useState([]);
const navigate = useNavigate();
const [page_size, setpage_size] = useState(10);
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [showModal, setshowModal] = useState(false);
const [isAdd, setIsAdd] = useState(false);
const [delMoadal, setDelModal] = useState(false);
const [empower, setempower] = useState(false);
const [form] = Form.useForm();
const flag = useRef(true);
const [checkedKeys, setcheckedKeys] = useState([]);
// 获取操作权限
const { operationPermissionsList } = useSelector((state) => state.user);
const [selectedKeys, setSelectedKeys] = useState([]);
const treesubmit = async () => {
const bool = await setRolePower({ role_id: id, power_list: checkedKeys });
if (!bool) return;
init();
setempower(false);
};
const [id, setId] = useState(0);
const [initialValues, setinitialValues] = useState({
name: "",
remarks: "",
});
const [filterObj, setfilterObj] = useState({
name: "",
start_time: "",
end_time: "",
});
const [dateVal, setDateVal] = useState("");
const [treeData, settreeData] = useState([]);
// 过滤
const handleFilter = (flag, val) => {
if (flag == "startandend") {
let val2 = val || [];
filterObj["start_time"] = parseInt(
new Date(dayjs(val2[0]).format("YYYY-MM-DD") + " 00:00:00").getTime() / 1000,
);
const endDate = new Date(dayjs(val2[1])).getDate();
const endTime = parseInt(
new Date(dayjs(val2[1]).format("YYYY-MM-") + endDate + " 23:59:59").getTime() / 1000,
);
filterObj["end_time"] = endTime;
setDateVal(val);
if (!val2.length) {
delete filterObj["start_time"];
delete filterObj["end_time"];
}
} else {
filterObj[flag] = val;
}
for (const key in filterObj) {
if (!filterObj[key]) {
delete filterObj[key];
}
}
setfilterObj({ ...filterObj });
if (!val) init(filterObj);
};
// 重置
const handleReset = () => {
setfilterObj({ real_name: "", tel: "" });
setDateVal("");
init();
};
const init = async (obj = {}) => {
console.log(obj);
setLoading(true);
let filter = {};
if (obj.name) filter.name = obj.name;
if (obj.start_time && obj.end_time) {
filter.start_time = obj.start_time;
filter.end_time = obj.end_time;
}
const res = await getList({ page, page_size, ...filter });
setTotal(res.total);
setData(res.list);
setLoading(false);
};
const handleEdit = async (id) => {
const data = await infoRole({ id });
setIsAdd(false);
setshowModal(true);
setId(id);
// setinitialValues(data); // Update the initialValues state with the new data
// 回显角色类型
form.setFieldsValue({
name: data.name,
remarks: data.remarks,
type: data.type.toString(), // 将角色类型转换为字符串以匹配 Radio.Group 的值
});
};
const onClose = () => {
setshowModal(false);
};
const submitForm = async (obj) => {
if (!obj.remarks) {
delete obj.remarks; // 如果备注为空,则从表单数据中删除该字段
}
let bool;
if (isAdd) {
bool = await addRole({ ...obj });
} else {
bool = await editRole({ ...obj, id });
}
if (!bool) return;
init();
setshowModal(false);
};
const delSuccess = async () => {
const bool = await delRole({ id });
bool && init();
setDelModal(false);
};
const handleStatus = async (status, id) => {
const bool = await changeStatus({ status: status ? "1" : "2", id });
bool && init();
};
// // 父子关联,子未全选父有样式
// const ontreeCheck = (keys, { checked, checkedNodes, node, halfCheckedKeys }) => {
// console.log(node, 'node');
// console.log('keys', keys);
// setcheckedKeys(keys);
// };
// 父子节点不关联,子空一个父就全空,目前实现点击父全部选中子,但当子全取消父并未取消
const ontreeCheck = (checkedKeys, { checked, checkedNodes, node, halfCheckedKeys }) => {
const handleCheck = (parentNode) => {
const keys = [];
const traverse = (currentNode) => {
keys.push(currentNode.key);
if (currentNode.children) {
currentNode.children.forEach((childNode) => {
traverse(childNode, currentNode.key);
});
}
};
traverse(parentNode);
return keys;
};
if (checked) {
const keysToCheck = handleCheck(node);
const parentNodes = findParents(treeData, "key", node.key);
setcheckedKeys([...checkedKeys.checked, ...keysToCheck, ...parentNodes]);
// 处理父节点选中状态
if (node.parentNode) {
ontreeCheck(checkedKeys, {
checked: true,
node: node.parentNode,
});
}
} else {
const keysToUncheck = handleCheck(node);
const newCheckedKeys = checkedKeys.checked.filter((key) => !keysToUncheck.includes(key));
setcheckedKeys(newCheckedKeys);
// 处理父节点选中状态
// if (node.parentNode && !halfCheckedKeys.includes(node.parentNode.key)) {
// ontreeCheck(checkedKeys, {
// checked: false,
// node: node.parentNode,
// });
// }
console.log("checkedKeys", checkedKeys);
}
};
// 点击授权
const handleEmpower = async (id) => {
// 默认勾选
setempower(true);
setId(id);
const data = await getPowerByRoleId({ role_id: id });
setcheckedKeys(data);
};
// 获取所有权限
const getRoleList = async () => {
const data = await getAllrole();
let arr = convertToAntdTreeData(data);
settreeData(arr);
};
useEffect(() => {
// if(!flag.current){
getRoleList();
// }else{
// flag.current=false
// }
}, []);
useEffect(() => {
if (!showModal) {
form.setFieldsValue({ ...initialValues });
}
}, [showModal]);
const columns = [
{
title: "ID",
key: "id",
dataIndex: "id",
render: (_, record, index) => {
const cindex = page_size * (page - 1) + (index + 1);
return <span>{cindex}</span>;
},
},
{
title: "角色名称",
key: "name",
align: "center",
dataIndex: "name",
width: 300,
},
{
title: "角色类型",
key: "type",
align: "center",
dataIndex: "type",
width: 300,
render: (text) => {
if (text === 1) {
return "管理员";
} else if (text === 2) {
return "老师";
} else {
return "未知";
}
},
},
{
title: "备注",
key: "remarks",
align: "center",
dataIndex: "remarks",
width: 300,
render: (text) => (
<Popover content={text} overlayStyle={{ maxWidth: "400px" }}>
<div
style={{
display: "-webkit-box",
WebkitLineClamp: "3",
WebkitBoxOrient: "vertical",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "center",
}}
>
{text}
</div>
</Popover>
),
},
{
title: "状态",
align: "center",
key: "status",
dataIndex: "status",
width: 300,
render(_, { id, status }) {
return (
<Switch
defaultChecked={status}
onChange={(val) => handleStatus(val, id)}
disabled={!operationPermissionsList.includes("/jurisdiction/role/changeStatus")}
/>
);
},
},
{
title: "创建时间",
key: "create_time",
align: "center",
dataIndex: "create_time",
},
{
title: "操作",
key: "handle",
align: "center",
dataIndex: "handle",
width: 400,
render: (_, record) => {
return (
<Space>
{operationPermissionsList.includes("/jurisdiction/role/getInfoById") && (
<Button onClick={() => handleEdit(record.id)}>编辑</Button>
)}
{operationPermissionsList.includes("/jurisdiction/role/getPowerByRoleId") && (
<Button onClick={() => handleEmpower(record.id)}>授权</Button>
)}
{operationPermissionsList.includes("/jurisdiction/role/del") && (
<Button
onClick={() => {
setId(record.id);
setDelModal(true);
}}
>
删除
</Button>
)}
</Space>
);
},
},
];
return (
<div>
<Row justify={"space-between"}>
<Col className="form-devices-inline">
<Form form={form} layout="inline">
<Form.Item label="角色名称">
<Input
autoComplete="off"
allowClear
value={filterObj.name}
onChange={(ev) => handleFilter("name", ev.target.value)}
placeholder="请输入角色名称"
id="name"
></Input>
</Form.Item>
<Form.Item label="创建时间">
<RangePicker
value={dateVal}
onChange={(ev) => handleFilter("startandend", ev)}
id="createTime"
></RangePicker>
</Form.Item>
<Space>
<Button
type="primary"
ghost
icon={
<span
style={{
display: "inline-block",
width: "11px",
height: "10px",
pointerEvents: "none",
}}
>
<Image src={reset} />
</span>
}
onClick={handleReset}
>
重置
</Button>
<Button
type="primary"
icon={
<span
style={{
display: "inline-block",
width: "13px",
height: "12px",
pointerEvents: "none",
}}
>
<Image src={filter} />
</span>
}
ghost
onClick={() => {
setPage(1);
init(filterObj);
// Object.values(filterObj).some((item) => item) ? init(filterObj) : init();
}}
>
筛选
</Button>
<Button
type="primary"
ghost
icon={
<span
style={{
display: "inline-block",
width: "10px",
height: "12px",
pointerEvents: "none",
}}
>
<Image src={reload} />
</span>
}
onClick={() => {
init(filterObj);
}}
>
刷新
</Button>
{/* <Button
type='primary'
ghost
icon={
<span
style={{
display: 'inline-block',
width: '13px',
height: '12px',
pointerEvents: 'none',
}}
>
<Image src={add} />
</span>
}
onClick={() => {
setIsAdd(true);
setshowModal(true);
form.resetFields();
}}
>
添加
</Button> */}
</Space>
</Form>
</Col>
<Col>
<Space>
{operationPermissionsList.includes("/jurisdiction/role/add") && (
<Button
type="primary"
ghost
icon={
<span
style={{
display: "inline-block",
width: "13px",
height: "12px",
pointerEvents: "none",
}}
>
<Image src={add} />
</span>
}
onClick={() => {
setIsAdd(true);
setshowModal(true);
form.resetFields();
}}
>
添加
</Button>
)}
</Space>
</Col>
</Row>
<br />
{TableCom({ columns, loading, data })}
<Modal
mask={false}
centered
open={delMoadal}
onOk={delSuccess}
onCancel={() => setDelModal(false)}
footer={(_, { OkBtn, CancelBtn }) => {
return (
<Row justify={"center"} className="delModal" style={{ marginBottom: "30px" }}>
<Space size={20}>
<CancelBtn />
<OkBtn />
</Space>
</Row>
);
}}
okText="确认"
>
<p style={{ textAlign: "center", padding: "30px 0 18px", fontSize: 16 }}>
角色将永久删除,是否继续?
</p>
</Modal>
<br />
<PaginationCom
total={total}
page_size={page_size}
setpage_size={setpage_size}
page={page}
setPage={setPage}
init={init}
filterObj={filterObj}
/>
<Drawer placement="right" onClose={onClose} open={showModal} mask={false}>
<Form form={form} onFinish={submitForm} labelCol={{ span: 6 }}>
<Form.Item
name="name"
label="角色名称"
rules={[
{ required: true, message: "请输入角色名称" },
{ max: 50, message: "角色名称最多可输入50个字符" },
]}
>
<Input autoComplete="off" placeholder="请输入角色名称"></Input>
</Form.Item>
<Form.Item
name="type"
label="角色类型"
rules={[{ required: true, message: "请选择角色类型" }]}
>
<Radio.Group style={{ textAlign: "center" }}>
<Radio value={"1"}>管理员</Radio>
<Radio value={"2"}>老师</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
name="remarks"
label="备注"
rules={[
// { required: !isAdd, message: '请输入备注' },
{ required: false, message: "请输入备注" }, //备注非必选
{ max: 100, message: "备注最多可输入100个字符" }, // 设置最大长度为100
]}
>
<TextArea
autoSize={{
minRows: 5,
maxRows: 8,
}}
placeholder="请输入备注"
></TextArea>
</Form.Item>
<Form.Item wrapperCol={{ offset: 12, span: 16 }}>
<Space size={20}>
<Button onClick={() => setshowModal(false)}>取消</Button>
<Button type="primary" htmlType="submit">
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
<Drawer placement="right" open={empower} onClose={() => setempower(false)} mask={false}>
<Form labelCol={{ span: 6 }}>
<Form.Item name="power_list" style={{ padding: 10, paddingTop: 0 }}>
<Tree
checkable
checkedKeys={checkedKeys}
defaultExpandAll={false} //让授权后的弹窗只展示根标签
treeData={treeData}
// multiple // 支持多选。当父子不关联,添加后,点击父,不能控制所有子
// showLine //删除这里,树形结构左侧的下拉线消失,图标从+-更改为默认的△
checkStrictly // 开启后,父子节点不关联(子空一个,父就空)
onCheck={ontreeCheck}
/>
</Form.Item>
<Form.Item
wrapperCol={{ offset: 7, span: 16 }}
style={{
position: "fixed",
bottom: 0,
width: "30%",
background: "#fff",
paddingBottom: 20,
margin: 0,
}}
>
<Space size={20}>
<Button onClick={() => setempower(false)}>取消</Button>
<Button type="primary" htmlType="submit" onClick={treesubmit}>
提交
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
</div>
);
};
export default RoleList;
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论