feat: 去掉角色权限点与菜单权限码配置
This commit is contained in:
@@ -1018,3 +1018,9 @@
|
|||||||
- 总数以接口返回 `total` 为准。
|
- 总数以接口返回 `total` 为准。
|
||||||
- 筛选条件(关键词/塔型/风险等级)或线路切换时,分页需自动回到第 1 页,避免落在无数据页。
|
- 筛选条件(关键词/塔型/风险等级)或线路切换时,分页需自动回到第 1 页,避免落在无数据页。
|
||||||
- 地图视图保留大页查询(当前 500 条)用于展示线路点位,不与表格分页参数共用同一页码。
|
- 地图视图保留大页查询(当前 500 条)用于展示线路点位,不与表格分页参数共用同一页码。
|
||||||
|
|
||||||
|
## 角色/菜单配置口径(2026-05-01)
|
||||||
|
|
||||||
|
- 角色管理页面(`/admin/roles`)当前仅提供角色基础信息与“可见菜单”配置,不再提供权限点(`permission_codes`)配置入口。
|
||||||
|
- 菜单管理页面(`/admin/menus`)当前不再提供菜单权限码(`permission_code`)配置入口与列表展示。
|
||||||
|
- 后端权限字段与接口兼容能力保留,作为历史数据与鉴权映射兜底;前端交互层默认不暴露该配置。
|
||||||
|
|||||||
@@ -419,3 +419,34 @@
|
|||||||
- 风险与影响:
|
- 风险与影响:
|
||||||
- `.img/.tif` 回填依赖 `rasterio`(及底层 GDAL 运行时),部署环境需确保镜像能成功安装该依赖。
|
- `.img/.tif` 回填依赖 `rasterio`(及底层 GDAL 运行时),部署环境需确保镜像能成功安装该依赖。
|
||||||
- 栅格 bbox 直接来自源栅格 CRS;非经纬度坐标系场景会返回告警,便于识别与后续治理。
|
- 栅格 bbox 直接来自源栅格 CRS;非经纬度坐标系场景会返回告警,便于识别与后续治理。
|
||||||
|
|
||||||
|
## Work Log - 去掉角色权限点与菜单权限码配置(2026-05-01)
|
||||||
|
|
||||||
|
- 背景:
|
||||||
|
- Issue `FL-139` 要求“去掉角色管理的权限点配置,菜单的权限码配置”。
|
||||||
|
|
||||||
|
- 本次改动(最小闭环):
|
||||||
|
- 角色管理页面:`web/src/app/admin/roles/page.tsx`
|
||||||
|
- 移除权限点配置入口与相关请求链路:
|
||||||
|
- 移除 `permissions` 状态与 `/api/v1/admin/permissions` 加载请求。
|
||||||
|
- 新建/编辑角色表单移除 `permission_codes` 字段,仅保留 `code/name/menu_ids`。
|
||||||
|
- 角色更新请求不再提交 `permission_codes`。
|
||||||
|
- 列表展示移除“权限”列,仅展示“角色编码/角色名称/菜单/操作”。
|
||||||
|
- 搜索口径同步调整为“角色编码、名称、菜单”。
|
||||||
|
- 菜单管理页面:`web/src/app/admin/menus/page.tsx`
|
||||||
|
- 移除菜单权限码配置入口:
|
||||||
|
- 菜单表单移除 `permission_code` 字段。
|
||||||
|
- 新建/编辑菜单提交 payload 不再包含 `permission_code`。
|
||||||
|
- 列表展示移除“权限码”列。
|
||||||
|
- 搜索口径移除权限码匹配,关键词仅匹配“编码/名称/路径”。
|
||||||
|
- 后台首页文案:`web/src/app/admin/page.tsx`
|
||||||
|
- 角色管理说明改为“配置角色并分配菜单可见范围”。
|
||||||
|
- 菜单管理说明改为“维护后台导航结构、菜单层级与展示状态”。
|
||||||
|
|
||||||
|
- 验证(遵循任务约束,未执行编译检查):
|
||||||
|
- `git diff` 已确认改动仅涉及上述三个前端文件。
|
||||||
|
- 代码扫描确认上述页面不再包含 `permission_codes` / `permission_code` 配置与展示逻辑。
|
||||||
|
|
||||||
|
- 风险与影响:
|
||||||
|
- 影响范围限定在前端管理页交互层;后端接口仍保持兼容,可继续返回权限相关字段但前端不再暴露配置入口。
|
||||||
|
- 若后续需彻底下线该能力(含后端字段/持久化),需单独评估接口契约与历史数据兼容。
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ type MenuFormValues = {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
cacheable: boolean;
|
cacheable: boolean;
|
||||||
component?: string;
|
component?: string;
|
||||||
permission_code?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const SORT_OPTIONS: Array<{ value: SortKey; label: string }> = [
|
const SORT_OPTIONS: Array<{ value: SortKey; label: string }> = [
|
||||||
@@ -108,7 +107,6 @@ const DEFAULT_FORM_VALUES: MenuFormValues = {
|
|||||||
visible: true,
|
visible: true,
|
||||||
cacheable: false,
|
cacheable: false,
|
||||||
component: "",
|
component: "",
|
||||||
permission_code: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function compareMenuIds(a: string, b: string): number {
|
function compareMenuIds(a: string, b: string): number {
|
||||||
@@ -166,7 +164,7 @@ export default function AdminMenusPage() {
|
|||||||
if (!query) {
|
if (!query) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const haystack = [menu.code, menu.name, menu.path ?? "", menu.permission_code ?? ""]
|
const haystack = [menu.code, menu.name, menu.path ?? ""]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
return haystack.includes(query);
|
return haystack.includes(query);
|
||||||
@@ -250,7 +248,6 @@ export default function AdminMenusPage() {
|
|||||||
visible: menu.visible,
|
visible: menu.visible,
|
||||||
cacheable: menu.cacheable,
|
cacheable: menu.cacheable,
|
||||||
component: menu.component ?? "",
|
component: menu.component ?? "",
|
||||||
permission_code: menu.permission_code ?? "",
|
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
}, [form]);
|
}, [form]);
|
||||||
@@ -273,7 +270,6 @@ export default function AdminMenusPage() {
|
|||||||
visible: values.visible,
|
visible: values.visible,
|
||||||
cacheable: values.cacheable,
|
cacheable: values.cacheable,
|
||||||
component: values.component?.trim() ? values.component.trim() : null,
|
component: values.component?.trim() ? values.component.trim() : null,
|
||||||
permission_code: values.permission_code?.trim() ? values.permission_code.trim() : null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = editingMenuId
|
const response = editingMenuId
|
||||||
@@ -362,12 +358,6 @@ export default function AdminMenusPage() {
|
|||||||
width: 220,
|
width: 220,
|
||||||
render: (value: string | null) => value || "-",
|
render: (value: string | null) => value || "-",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "权限码",
|
|
||||||
dataIndex: "permission_code",
|
|
||||||
width: 180,
|
|
||||||
render: (value: string | null) => value || "-",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "父菜单",
|
title: "父菜单",
|
||||||
dataIndex: "parent_id",
|
dataIndex: "parent_id",
|
||||||
@@ -488,7 +478,7 @@ export default function AdminMenusPage() {
|
|||||||
allowClear
|
allowClear
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(event) => setKeyword(event.currentTarget.value)}
|
onChange={(event) => setKeyword(event.currentTarget.value)}
|
||||||
placeholder="按编码/名称/路径/权限筛选"
|
placeholder="按编码/名称/路径筛选"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
@@ -634,12 +624,6 @@ export default function AdminMenusPage() {
|
|||||||
<Input placeholder="app/admin/users/page" />
|
<Input placeholder="app/admin/users/page" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={12}>
|
|
||||||
<Form.Item label="权限码" name="permission_code">
|
|
||||||
<Input placeholder="menu.read" />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
<Col xs={24} md={12}>
|
<Col xs={24} md={12}>
|
||||||
<Form.Item name="visible" valuePropName="checked">
|
<Form.Item name="visible" valuePropName="checked">
|
||||||
<Checkbox>可见</Checkbox>
|
<Checkbox>可见</Checkbox>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const CARDS: DashboardCard[] = [
|
|||||||
{
|
{
|
||||||
href: "/roles",
|
href: "/roles",
|
||||||
title: "角色管理",
|
title: "角色管理",
|
||||||
description: "配置角色、绑定权限点、分配菜单可见范围。",
|
description: "配置角色并分配菜单可见范围。",
|
||||||
category: "权限",
|
category: "权限",
|
||||||
icon: <SafetyCertificateOutlined />,
|
icon: <SafetyCertificateOutlined />,
|
||||||
visible: (hasPermission) => hasPermission("role.read") || hasPermission("role.manage"),
|
visible: (hasPermission) => hasPermission("role.read") || hasPermission("role.manage"),
|
||||||
@@ -51,7 +51,7 @@ const CARDS: DashboardCard[] = [
|
|||||||
{
|
{
|
||||||
href: "/menus",
|
href: "/menus",
|
||||||
title: "菜单管理",
|
title: "菜单管理",
|
||||||
description: "维护后台导航、菜单层级和菜单对应权限。",
|
description: "维护后台导航结构、菜单层级与展示状态。",
|
||||||
category: "权限",
|
category: "权限",
|
||||||
icon: <AppstoreOutlined />,
|
icon: <AppstoreOutlined />,
|
||||||
visible: (hasPermission) => hasPermission("menu.read") || hasPermission("menu.manage"),
|
visible: (hasPermission) => hasPermission("menu.read") || hasPermission("menu.manage"),
|
||||||
|
|||||||
@@ -27,25 +27,22 @@ import type { ComponentType } from "react";
|
|||||||
import { useAuth } from "@/components/auth-provider";
|
import { useAuth } from "@/components/auth-provider";
|
||||||
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
|
import { useTopicSubscription } from "@/hooks/use-topic-subscription";
|
||||||
import { readApiError } from "@/lib/api";
|
import { readApiError } from "@/lib/api";
|
||||||
import type { MenuItem, PermissionItem, RoleItem, RoleListResponse } from "@/types/auth";
|
import type { MenuItem, RoleItem, RoleListResponse } from "@/types/auth";
|
||||||
|
|
||||||
const AntCard = Card as unknown as ComponentType<CardProps>;
|
const AntCard = Card as unknown as ComponentType<CardProps>;
|
||||||
const AntResult = Result as unknown as ComponentType<ResultProps>;
|
const AntResult = Result as unknown as ComponentType<ResultProps>;
|
||||||
|
|
||||||
type PermissionResponse = { items: PermissionItem[] };
|
|
||||||
type MenuListResponse = { items: MenuItem[]; total: number };
|
type MenuListResponse = { items: MenuItem[]; total: number };
|
||||||
|
|
||||||
type RoleFormValues = {
|
type RoleFormValues = {
|
||||||
code: string;
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
permission_codes: string[];
|
|
||||||
menu_ids: string[];
|
menu_ids: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const EMPTY_FORM: RoleFormValues = {
|
const EMPTY_FORM: RoleFormValues = {
|
||||||
code: "",
|
code: "",
|
||||||
name: "",
|
name: "",
|
||||||
permission_codes: [],
|
|
||||||
menu_ids: [],
|
menu_ids: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,7 +52,6 @@ export default function AdminRolesPage() {
|
|||||||
const [form] = Form.useForm<RoleFormValues>();
|
const [form] = Form.useForm<RoleFormValues>();
|
||||||
const [roles, setRoles] = useState<RoleItem[]>([]);
|
const [roles, setRoles] = useState<RoleItem[]>([]);
|
||||||
const [searchKeyword, setSearchKeyword] = useState("");
|
const [searchKeyword, setSearchKeyword] = useState("");
|
||||||
const [permissions, setPermissions] = useState<PermissionItem[]>([]);
|
|
||||||
const [menus, setMenus] = useState<MenuItem[]>([]);
|
const [menus, setMenus] = useState<MenuItem[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
@@ -66,15 +62,6 @@ export default function AdminRolesPage() {
|
|||||||
const canRead = hasPermission("role.read") || hasPermission("role.manage");
|
const canRead = hasPermission("role.read") || hasPermission("role.manage");
|
||||||
const canManage = hasPermission("role.manage");
|
const canManage = hasPermission("role.manage");
|
||||||
|
|
||||||
const permissionOptions = useMemo(
|
|
||||||
() =>
|
|
||||||
permissions.map((permission) => ({
|
|
||||||
value: permission.code,
|
|
||||||
label: `${permission.name || permission.code} (${permission.code})`,
|
|
||||||
})),
|
|
||||||
[permissions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const menuOptions = useMemo(
|
const menuOptions = useMemo(
|
||||||
() => menus.map((menu) => ({ value: menu.id, label: `${menu.name} (${menu.code})` })),
|
() => menus.map((menu) => ({ value: menu.id, label: `${menu.name} (${menu.code})` })),
|
||||||
[menus],
|
[menus],
|
||||||
@@ -97,7 +84,6 @@ export default function AdminRolesPage() {
|
|||||||
const haystack = [
|
const haystack = [
|
||||||
role.code,
|
role.code,
|
||||||
role.name,
|
role.name,
|
||||||
role.permission_codes.join(" "),
|
|
||||||
menuNames,
|
menuNames,
|
||||||
]
|
]
|
||||||
.join(" ")
|
.join(" ")
|
||||||
@@ -115,28 +101,22 @@ export default function AdminRolesPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
const [roleRes, permissionRes, menuRes] = await Promise.all([
|
const [roleRes, menuRes] = await Promise.all([
|
||||||
fetchWithAuth("/api/v1/admin/roles"),
|
fetchWithAuth("/api/v1/admin/roles"),
|
||||||
fetchWithAuth("/api/v1/admin/permissions"),
|
|
||||||
fetchWithAuth("/api/v1/admin/menus"),
|
fetchWithAuth("/api/v1/admin/menus"),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!roleRes.ok) {
|
if (!roleRes.ok) {
|
||||||
throw new Error(await readApiError(roleRes));
|
throw new Error(await readApiError(roleRes));
|
||||||
}
|
}
|
||||||
if (!permissionRes.ok) {
|
|
||||||
throw new Error(await readApiError(permissionRes));
|
|
||||||
}
|
|
||||||
if (!menuRes.ok) {
|
if (!menuRes.ok) {
|
||||||
throw new Error(await readApiError(menuRes));
|
throw new Error(await readApiError(menuRes));
|
||||||
}
|
}
|
||||||
|
|
||||||
const rolePayload = (await roleRes.json()) as RoleListResponse;
|
const rolePayload = (await roleRes.json()) as RoleListResponse;
|
||||||
const permissionPayload = (await permissionRes.json()) as PermissionResponse;
|
|
||||||
const menuPayload = (await menuRes.json()) as MenuListResponse;
|
const menuPayload = (await menuRes.json()) as MenuListResponse;
|
||||||
|
|
||||||
setRoles(rolePayload.items);
|
setRoles(rolePayload.items);
|
||||||
setPermissions(permissionPayload.items);
|
|
||||||
setMenus(menuPayload.items);
|
setMenus(menuPayload.items);
|
||||||
} catch (candidate) {
|
} catch (candidate) {
|
||||||
setError(candidate instanceof Error ? candidate.message : "角色数据加载失败");
|
setError(candidate instanceof Error ? candidate.message : "角色数据加载失败");
|
||||||
@@ -183,7 +163,6 @@ export default function AdminRolesPage() {
|
|||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
code: role.code,
|
code: role.code,
|
||||||
name: role.name,
|
name: role.name,
|
||||||
permission_codes: role.permission_codes,
|
|
||||||
menu_ids: role.menu_ids,
|
menu_ids: role.menu_ids,
|
||||||
});
|
});
|
||||||
setDialogOpen(true);
|
setDialogOpen(true);
|
||||||
@@ -198,7 +177,6 @@ export default function AdminRolesPage() {
|
|||||||
const payload: RoleFormValues = {
|
const payload: RoleFormValues = {
|
||||||
code: values.code.trim(),
|
code: values.code.trim(),
|
||||||
name: values.name.trim(),
|
name: values.name.trim(),
|
||||||
permission_codes: values.permission_codes ?? [],
|
|
||||||
menu_ids: values.menu_ids ?? [],
|
menu_ids: values.menu_ids ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -208,7 +186,6 @@ export default function AdminRolesPage() {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: payload.name,
|
name: payload.name,
|
||||||
permission_codes: payload.permission_codes,
|
|
||||||
menu_ids: payload.menu_ids,
|
menu_ids: payload.menu_ids,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
@@ -282,22 +259,6 @@ export default function AdminRolesPage() {
|
|||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
width: 180,
|
width: 180,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "权限",
|
|
||||||
dataIndex: "permission_codes",
|
|
||||||
render: (value: string[]) => {
|
|
||||||
if (value.length === 0) {
|
|
||||||
return <Typography.Text type="secondary">未绑定权限</Typography.Text>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Space wrap size={[4, 4]}>
|
|
||||||
{value.map((permissionCode) => (
|
|
||||||
<Tag key={permissionCode}>{permissionCode}</Tag>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "菜单",
|
title: "菜单",
|
||||||
dataIndex: "menu_ids",
|
dataIndex: "menu_ids",
|
||||||
@@ -414,7 +375,7 @@ export default function AdminRolesPage() {
|
|||||||
<Space align="center" wrap>
|
<Space align="center" wrap>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
allowClear
|
allowClear
|
||||||
placeholder="搜索角色编码、名称、权限或菜单"
|
placeholder="搜索角色编码、名称或菜单"
|
||||||
style={{ width: 360, maxWidth: "100%" }}
|
style={{ width: 360, maxWidth: "100%" }}
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
onChange={(event) => setSearchKeyword(event.currentTarget.value)}
|
onChange={(event) => setSearchKeyword(event.currentTarget.value)}
|
||||||
@@ -482,16 +443,6 @@ export default function AdminRolesPage() {
|
|||||||
<Input placeholder="运营管理员" />
|
<Input placeholder="运营管理员" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label="权限点" name="permission_codes">
|
|
||||||
<Select
|
|
||||||
allowClear
|
|
||||||
mode="multiple"
|
|
||||||
optionFilterProp="label"
|
|
||||||
options={permissionOptions}
|
|
||||||
placeholder="请选择权限点"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
<Form.Item label="可见菜单" name="menu_ids">
|
<Form.Item label="可见菜单" name="menu_ids">
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
|
|||||||
Reference in New Issue
Block a user