refactor(layout): 重构应用布局和权限管理功能
- 更新AppLayout支持目录类型的菜单结构 - 实现菜单展开状态基于当前路径的自动管理 - 添加目录类型权限的过滤和渲染逻辑 - 优化页面布局的flexbox结构和滚动处理 - 修改权限管理页面支持目录、菜单、按钮三级结构 - 更新字典类型API支持分页查询参数 - 调整多个页面的表格布局和滚动配置 - 添加标准分页工具函数并统一使用 - 更新租户管理页面为卡片列表展示方式master
parent
1ae81909c2
commit
3f31ec0eb1
|
|
@ -1,13 +1,13 @@
|
||||||
package com.imeeting.controller;
|
package com.imeeting.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.imeeting.common.ApiResponse;
|
import com.imeeting.common.ApiResponse;
|
||||||
import com.imeeting.entity.SysDictType;
|
import com.imeeting.entity.SysDictType;
|
||||||
import com.imeeting.service.SysDictTypeService;
|
import com.imeeting.service.SysDictTypeService;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/dict-types")
|
@RequestMapping("/api/dict-types")
|
||||||
public class DictTypeController {
|
public class DictTypeController {
|
||||||
|
|
@ -19,8 +19,21 @@ public class DictTypeController {
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
|
@PreAuthorize("@ss.hasPermi('sys_dict:list')")
|
||||||
public ApiResponse<List<SysDictType>> list() {
|
public ApiResponse<Page<SysDictType>> list(
|
||||||
return ApiResponse.ok(sysDictTypeService.list());
|
@RequestParam(defaultValue = "1") Integer current,
|
||||||
|
@RequestParam(defaultValue = "10") Integer size,
|
||||||
|
@RequestParam(required = false) String typeCode,
|
||||||
|
@RequestParam(required = false) String typeName) {
|
||||||
|
Page<SysDictType> page = new Page<>(current, size);
|
||||||
|
LambdaQueryWrapper<SysDictType> queryWrapper = new LambdaQueryWrapper<>();
|
||||||
|
if (typeCode != null && !typeCode.isEmpty()) {
|
||||||
|
queryWrapper.like(SysDictType::getTypeCode, typeCode);
|
||||||
|
}
|
||||||
|
if (typeName != null && !typeName.isEmpty()) {
|
||||||
|
queryWrapper.like(SysDictType::getTypeName, typeName);
|
||||||
|
}
|
||||||
|
queryWrapper.orderByAsc(SysDictType::getTypeCode);
|
||||||
|
return ApiResponse.ok(sysDictTypeService.page(page, queryWrapper));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,9 @@ import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
|
||||||
@TableName("sys_platform_config")
|
@TableName("sys_platform_config")
|
||||||
public class SysPlatformConfig extends BaseEntity {
|
public class SysPlatformConfig {
|
||||||
@TableId
|
@TableId
|
||||||
private Long id;
|
private Long id;
|
||||||
private String projectName;
|
private String projectName;
|
||||||
|
|
@ -20,6 +20,4 @@ public class SysPlatformConfig extends BaseEntity {
|
||||||
private String copyrightInfo;
|
private String copyrightInfo;
|
||||||
private String systemDescription;
|
private String systemDescription;
|
||||||
|
|
||||||
@TableField(exist = false)
|
|
||||||
private Long tenantId;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ src
|
||||||
> 阅读对应的后端controller了解接口
|
> 阅读对应的后端controller了解接口
|
||||||
### 核心原则
|
### 核心原则
|
||||||
|
|
||||||
|
* 优先使用frontend/src/components/shared中的组件 如果是typeScript 需要修改为typeScript
|
||||||
* 清晰的意图胜于技巧性的实现
|
* 清晰的意图胜于技巧性的实现
|
||||||
* 组件简单直观优于过度抽象
|
* 组件简单直观优于过度抽象
|
||||||
* 奥卡姆剃刀:不必要的复杂度一律删除
|
* 奥卡姆剃刀:不必要的复杂度一律删除
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ import http from "./http";
|
||||||
import { SysDictType, SysDictItem } from "../types";
|
import { SysDictType, SysDictItem } from "../types";
|
||||||
|
|
||||||
// Dictionary Type APIs
|
// Dictionary Type APIs
|
||||||
export async function fetchDictTypes() {
|
export async function fetchDictTypes(params?: { current?: number; size?: number; typeCode?: string; typeName?: string }) {
|
||||||
const resp = await http.get("/api/dict-types");
|
const resp = await http.get("/api/dict-types", { params });
|
||||||
return resp.data.data as SysDictType[];
|
return resp.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDictType(data: Partial<SysDictType>) {
|
export async function createDictType(data: Partial<SysDictType>) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Input, Space, Popover } from 'antd'
|
import { Button, Input, Space, Popover } from 'antd'
|
||||||
import { ReloadOutlined, FilterOutlined } from '@ant-design/icons'
|
import { ReloadOutlined, FilterOutlined } from '@ant-design/icons'
|
||||||
import './ListActionBar.css'
|
import './ListActionBar.css'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
/* 列表表格容器 */
|
/* 列表表格容器 */
|
||||||
.list-table-container {
|
.list-table-container {
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
||||||
height: 626px;
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 行选中样式 */
|
/* 行选中样式 */
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import React from "react";
|
||||||
import { Table } from "antd";
|
import { Table } from "antd";
|
||||||
import type { TablePaginationConfig, TableProps } from "antd";
|
import type { TablePaginationConfig, TableProps } from "antd";
|
||||||
import "./ListTable.css";
|
import "./ListTable.css";
|
||||||
|
import i18n from "../../../i18n";
|
||||||
|
|
||||||
export type ListTableProps<T extends Record<string, any>> = {
|
export type ListTableProps<T extends Record<string, any>> = {
|
||||||
columns: TableProps<T>["columns"];
|
columns: TableProps<T>["columns"];
|
||||||
|
|
@ -13,11 +15,12 @@ export type ListTableProps<T extends Record<string, any>> = {
|
||||||
onSelectAllPages?: () => void;
|
onSelectAllPages?: () => void;
|
||||||
onClearSelection?: () => void;
|
onClearSelection?: () => void;
|
||||||
pagination?: TablePaginationConfig | false;
|
pagination?: TablePaginationConfig | false;
|
||||||
scroll?: { x?: number | true | string };
|
scroll?: { x?: number | true | string; y?: number | string };
|
||||||
onRowClick?: (record: T) => void;
|
onRowClick?: (record: T) => void;
|
||||||
selectedRow?: T | null;
|
selectedRow?: T | null;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onChange?: TableProps<T>["onChange"];
|
||||||
};
|
};
|
||||||
|
|
||||||
function ListTable<T extends Record<string, any>>({
|
function ListTable<T extends Record<string, any>>({
|
||||||
|
|
@ -40,6 +43,7 @@ function ListTable<T extends Record<string, any>>({
|
||||||
selectedRow,
|
selectedRow,
|
||||||
loading = false,
|
loading = false,
|
||||||
className = "",
|
className = "",
|
||||||
|
onChange,
|
||||||
}: ListTableProps<T>) {
|
}: ListTableProps<T>) {
|
||||||
const rowSelection: TableProps<T>["rowSelection"] = onSelectionChange
|
const rowSelection: TableProps<T>["rowSelection"] = onSelectionChange
|
||||||
? {
|
? {
|
||||||
|
|
@ -88,7 +92,9 @@ function ListTable<T extends Record<string, any>>({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<span className="selection-count">已选择 0 项</span>
|
<span className="selection-count">
|
||||||
|
{i18n.t("common.total", { total: totalCount || total })}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
|
|
@ -105,6 +111,7 @@ function ListTable<T extends Record<string, any>>({
|
||||||
pagination={mergedPagination}
|
pagination={mergedPagination}
|
||||||
scroll={scroll}
|
scroll={scroll}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
onChange={onChange}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onClick: () => onRowClick?.(record),
|
onClick: () => onRowClick?.(record),
|
||||||
className: selectedRow?.[rowKey] === record[rowKey] ? "row-selected" : "",
|
className: selectedRow?.[rowKey] === record[rowKey] ? "row-selected" : "",
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export default function AppLayout() {
|
||||||
|
|
||||||
// Filter visible menus and sort them
|
// Filter visible menus and sort them
|
||||||
const filtered = data
|
const filtered = data
|
||||||
.filter(p => p.permType === 'menu' && p.isVisible === 1 && p.status === 1)
|
.filter(p => (p.permType === 'menu' || p.permType === 'directory') && p.isVisible === 1 && p.status === 1)
|
||||||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
||||||
setMenus(filtered);
|
setMenus(filtered);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -144,14 +144,17 @@ export default function AppLayout() {
|
||||||
nodes.map((m) => {
|
nodes.map((m) => {
|
||||||
const key = m.path || m.code || String(m.permId);
|
const key = m.path || m.code || String(m.permId);
|
||||||
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
const icon = m.icon ? (iconMap[m.icon] || <SettingOutlined />) : <SettingOutlined />;
|
||||||
if (m.children && m.children.length > 0) {
|
|
||||||
|
// Directory type or item with children should not have a link if it's a directory
|
||||||
|
if (m.permType === 'directory' || (m.children && m.children.length > 0)) {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
icon,
|
icon,
|
||||||
label: m.name,
|
label: m.name,
|
||||||
children: toMenuItems(m.children),
|
children: m.children && m.children.length > 0 ? toMenuItems(m.children) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
icon,
|
icon,
|
||||||
|
|
@ -161,6 +164,28 @@ export default function AppLayout() {
|
||||||
|
|
||||||
const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]);
|
const menuItems = useMemo(() => toMenuItems(buildMenuTree(menus)), [menus, buildMenuTree, toMenuItems]);
|
||||||
|
|
||||||
|
// Calculate open keys based on current path
|
||||||
|
const [openKeys, setOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (menus.length > 0) {
|
||||||
|
const findParentKeys = (nodes: any[], path: string, parents: string[] = []): string[] | null => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.key === path) return parents;
|
||||||
|
if (node.children) {
|
||||||
|
const found = findParentKeys(node.children, path, [...parents, node.key]);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const keys = findParentKeys(menuItems, location.pathname);
|
||||||
|
if (keys) {
|
||||||
|
setOpenKeys(prev => Array.from(new Set([...prev, ...keys])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [location.pathname, menuItems, menus]);
|
||||||
|
|
||||||
const userMenuItems: MenuProps["items"] = useMemo(() => {
|
const userMenuItems: MenuProps["items"] = useMemo(() => {
|
||||||
const items: any[] = [
|
const items: any[] = [
|
||||||
{
|
{
|
||||||
|
|
@ -241,11 +266,13 @@ export default function AppLayout() {
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
|
openKeys={openKeys}
|
||||||
|
onOpenChange={setOpenKeys}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
style={{ borderRight: 0, marginTop: 16 }}
|
style={{ borderRight: 0, marginTop: 16 }}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout style={{ height: "100vh", overflow: "hidden" }}>
|
||||||
<Header style={{
|
<Header style={{
|
||||||
background: "#fff",
|
background: "#fff",
|
||||||
padding: '0 24px',
|
padding: '0 24px',
|
||||||
|
|
@ -253,7 +280,9 @@ export default function AppLayout() {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
boxShadow: '0 1px 4px rgba(0,21,41,.08)',
|
boxShadow: '0 1px 4px rgba(0,21,41,.08)',
|
||||||
zIndex: 9
|
zIndex: 9,
|
||||||
|
height: 64,
|
||||||
|
flexShrink: 0
|
||||||
}}>
|
}}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -295,10 +324,14 @@ export default function AppLayout() {
|
||||||
padding: 24,
|
padding: 24,
|
||||||
background: '#fff',
|
background: '#fff',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
minHeight: 280,
|
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
||||||
boxShadow: '0 1px 2px rgba(0,0,0,0.03)'
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
}}>
|
}}>
|
||||||
|
<div style={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
UserOutlined
|
UserOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -215,7 +216,7 @@ export default function Devices() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="devices-page p-6">
|
<div className="devices-page p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('devices.title')}
|
title={t('devices.title')}
|
||||||
subtitle={t('devices.subtitle')}
|
subtitle={t('devices.subtitle')}
|
||||||
|
|
@ -226,8 +227,8 @@ export default function Devices() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="devices-table-card shadow-sm">
|
<Card className="devices-table-card shadow-sm mb-4 flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<div className="devices-table-toolbar mb-4">
|
<div className="devices-table-toolbar">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('devices.searchPlaceholder')}
|
placeholder={t('devices.searchPlaceholder')}
|
||||||
prefix={<SearchOutlined aria-hidden="true" />}
|
prefix={<SearchOutlined aria-hidden="true" />}
|
||||||
|
|
@ -238,17 +239,17 @@ export default function Devices() {
|
||||||
aria-label={t('common.search')}
|
aria-label={t('common.search')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
<Table
|
<Table
|
||||||
rowKey="deviceId"
|
rowKey="deviceId"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="middle"
|
size="middle"
|
||||||
pagination={{
|
scroll={{ y: 'calc(100vh - 350px)' }}
|
||||||
showTotal: (total) => t('common.total', { total }),
|
pagination={getStandardPagination(filteredData.length, 1, 1000)}
|
||||||
pageSize: 10,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
Empty
|
Empty
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
createDictItem,
|
createDictItem,
|
||||||
|
|
@ -29,10 +29,11 @@ import {
|
||||||
updateDictType
|
updateDictType
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { usePermission } from "../hooks/usePermission";
|
import { usePermission } from "../hooks/usePermission";
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined } from "@ant-design/icons";
|
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
import { useDict } from "../hooks/useDict";
|
import { useDict } from "../hooks/useDict";
|
||||||
import type { SysDictItem, SysDictType } from "../types";
|
import type { SysDictItem, SysDictType } from "../types";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
import "./Dictionaries.css";
|
import "./Dictionaries.css";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
@ -45,6 +46,13 @@ export default function Dictionaries() {
|
||||||
const [selectedType, setSelectedType] = useState<SysDictType | null>(null);
|
const [selectedType, setSelectedType] = useState<SysDictType | null>(null);
|
||||||
const [loadingTypes, setLoadingTypes] = useState(false);
|
const [loadingTypes, setLoadingTypes] = useState(false);
|
||||||
const [loadingItems, setLoadingItems] = useState(false);
|
const [loadingItems, setLoadingItems] = useState(false);
|
||||||
|
const [typeTotal, setTypeTotal] = useState(0);
|
||||||
|
const [typeParams, setTypeParams] = useState({
|
||||||
|
current: 1,
|
||||||
|
size: 10,
|
||||||
|
typeCode: "",
|
||||||
|
typeName: ""
|
||||||
|
});
|
||||||
|
|
||||||
// Dictionaries
|
// Dictionaries
|
||||||
const { items: statusDict } = useDict("sys_common_status");
|
const { items: statusDict } = useDict("sys_common_status");
|
||||||
|
|
@ -59,18 +67,25 @@ export default function Dictionaries() {
|
||||||
const [editingItem, setEditingItem] = useState<SysDictItem | null>(null);
|
const [editingItem, setEditingItem] = useState<SysDictItem | null>(null);
|
||||||
const [itemForm] = Form.useForm();
|
const [itemForm] = Form.useForm();
|
||||||
|
|
||||||
const loadTypes = async () => {
|
const loadTypes = useCallback(async (params = typeParams) => {
|
||||||
setLoadingTypes(true);
|
setLoadingTypes(true);
|
||||||
try {
|
try {
|
||||||
const data = await fetchDictTypes();
|
const data = await fetchDictTypes(params);
|
||||||
setTypes(data || []);
|
setTypes(data.records || []);
|
||||||
if (data && data.length > 0 && !selectedType) {
|
setTypeTotal(data.total || 0);
|
||||||
setSelectedType(data[0]);
|
|
||||||
|
// If we have data and nothing is selected, select the first one
|
||||||
|
if (data.records && data.records.length > 0 && !selectedType) {
|
||||||
|
setSelectedType(data.records[0]);
|
||||||
|
} else if (selectedType) {
|
||||||
|
// Refresh selected type info if it still exists in the new list
|
||||||
|
const updatedSelected = data.records.find((t: SysDictType) => t.dictTypeId === selectedType.dictTypeId);
|
||||||
|
if (updatedSelected) setSelectedType(updatedSelected);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingTypes(false);
|
setLoadingTypes(false);
|
||||||
}
|
}
|
||||||
};
|
}, [selectedType, typeParams]);
|
||||||
|
|
||||||
const loadItems = async (typeCode: string) => {
|
const loadItems = async (typeCode: string) => {
|
||||||
setLoadingItems(true);
|
setLoadingItems(true);
|
||||||
|
|
@ -84,7 +99,7 @@ export default function Dictionaries() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTypes();
|
loadTypes();
|
||||||
}, []);
|
}, [typeParams.current, typeParams.size]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedType) {
|
if (selectedType) {
|
||||||
|
|
@ -125,6 +140,11 @@ export default function Dictionaries() {
|
||||||
loadTypes();
|
loadTypes();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTypeSearch = (val: string) => {
|
||||||
|
setTypeParams({ ...typeParams, current: 1, typeName: val });
|
||||||
|
loadTypes({ ...typeParams, current: 1, typeName: val });
|
||||||
|
};
|
||||||
|
|
||||||
// Item Actions
|
// Item Actions
|
||||||
const handleAddItem = () => {
|
const handleAddItem = () => {
|
||||||
if (!selectedType) {
|
if (!selectedType) {
|
||||||
|
|
@ -162,7 +182,7 @@ export default function Dictionaries() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dictionaries-page p-6">
|
<div className="dictionaries-page p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('dicts.title')}
|
title={t('dicts.title')}
|
||||||
subtitle={t('dicts.subtitle')}
|
subtitle={t('dicts.subtitle')}
|
||||||
|
|
@ -177,8 +197,8 @@ export default function Dictionaries() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
|
<Row gutter={24} className="flex-1 min-h-0 overflow-hidden">
|
||||||
<Col span={8} style={{ height: '100%' }}>
|
<Col span={8} className="h-full flex flex-col overflow-hidden">
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
|
|
@ -186,16 +206,31 @@ export default function Dictionaries() {
|
||||||
<span>{t('dicts.dictType')}</span>
|
<span>{t('dicts.dictType')}</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
className="full-height-card shadow-sm"
|
className="flex-1 flex flex-col overflow-hidden shadow-sm"
|
||||||
|
styles={{ body: { padding: '12px', flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
|
||||||
>
|
>
|
||||||
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
|
<div style={{ marginBottom: 12 }} className="flex-shrink-0">
|
||||||
|
<Input.Search
|
||||||
|
placeholder="搜索类型名称"
|
||||||
|
allowClear
|
||||||
|
onSearch={handleTypeSearch}
|
||||||
|
enterButton
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0">
|
||||||
<Table
|
<Table
|
||||||
rowKey="dictTypeId"
|
rowKey="dictTypeId"
|
||||||
loading={loadingTypes}
|
loading={loadingTypes}
|
||||||
dataSource={types}
|
dataSource={types}
|
||||||
pagination={false}
|
pagination={{
|
||||||
|
...getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size })),
|
||||||
|
simple: true,
|
||||||
|
size: 'small',
|
||||||
|
position: ['bottomCenter']
|
||||||
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
|
scroll={{ y: 'calc(100vh - 480px)' }}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onClick: () => setSelectedType(record),
|
onClick: () => setSelectedType(record),
|
||||||
className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}`
|
className: `cursor-pointer dict-type-row ${selectedType?.dictTypeId === record.dictTypeId ? "dict-type-row-selected" : ""}`
|
||||||
|
|
@ -203,9 +238,9 @@ export default function Dictionaries() {
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<div className="dict-type-item flex justify-between items-center p-2">
|
<div className="dict-type-item flex justify-between items-center p-1">
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="dict-type-name font-medium truncate">{record.typeName}</div>
|
<div className="dict-type-name font-medium truncate" style={{ fontSize: '14px' }}>{record.typeName}</div>
|
||||||
<div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div>
|
<div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dict-type-actions flex gap-1">
|
<div className="dict-type-actions flex gap-1">
|
||||||
|
|
@ -213,7 +248,7 @@ export default function Dictionaries() {
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined aria-hidden="true" />}
|
icon={<EditOutlined aria-hidden="true" style={{ fontSize: '12px' }} />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleEditType(record);
|
handleEditType(record);
|
||||||
|
|
@ -232,7 +267,7 @@ export default function Dictionaries() {
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined aria-hidden="true" />}
|
icon={<DeleteOutlined aria-hidden="true" style={{ fontSize: '12px' }} />}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
|
@ -247,7 +282,7 @@ export default function Dictionaries() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={16} style={{ height: '100%' }}>
|
<Col span={16} className="h-full flex flex-col overflow-hidden">
|
||||||
<Card
|
<Card
|
||||||
title={
|
title={
|
||||||
<Space>
|
<Space>
|
||||||
|
|
@ -255,7 +290,8 @@ export default function Dictionaries() {
|
||||||
<span>{t('dicts.dictItem')}{selectedType ? ` - ${selectedType.typeName}` : ""}</span>
|
<span>{t('dicts.dictItem')}{selectedType ? ` - ${selectedType.typeName}` : ""}</span>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
className="full-height-card shadow-sm"
|
className="flex-1 flex flex-col overflow-hidden shadow-sm"
|
||||||
|
styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}
|
||||||
extra={
|
extra={
|
||||||
can("sys_dict:item:create") && (
|
can("sys_dict:item:create") && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -271,13 +307,14 @@ export default function Dictionaries() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{selectedType ? (
|
{selectedType ? (
|
||||||
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
|
<div className="flex-1 overflow-hidden">
|
||||||
<Table
|
<Table
|
||||||
rowKey="dictItemId"
|
rowKey="dictItemId"
|
||||||
loading={loadingItems}
|
loading={loadingItems}
|
||||||
dataSource={items}
|
dataSource={items}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="middle"
|
size="middle"
|
||||||
|
scroll={{ y: 'calc(100vh - 320px)' }}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
title: t('dicts.itemLabel'),
|
title: t('dicts.itemLabel'),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd";
|
import { Card, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typography, Modal, Descriptions } from "antd";
|
||||||
import { useEffect, useState, useMemo } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { fetchLogs } from "../api";
|
import { fetchLogs } from "../api";
|
||||||
|
|
@ -6,6 +6,8 @@ import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOu
|
||||||
import { SysLog, UserProfile } from "../types";
|
import { SysLog, UserProfile } from "../types";
|
||||||
import { useDict } from "../hooks/useDict";
|
import { useDict } from "../hooks/useDict";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
import ListTable from "../components/shared/ListTable/ListTable";
|
||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
@ -18,7 +20,7 @@ export default function Logs() {
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [params, setParams] = useState({
|
const [params, setParams] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 10,
|
size: 20,
|
||||||
username: "",
|
username: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
startDate: "",
|
startDate: "",
|
||||||
|
|
@ -65,6 +67,11 @@ export default function Logs() {
|
||||||
loadData();
|
loadData();
|
||||||
}, [activeTab, params.current, params.size, params.sortField, params.sortOrder]);
|
}, [activeTab, params.current, params.size, params.sortField, params.sortOrder]);
|
||||||
|
|
||||||
|
const onTabChange = (key: string) => {
|
||||||
|
setActiveTab(key);
|
||||||
|
setParams(prev => ({ ...prev, current: 1 }));
|
||||||
|
};
|
||||||
|
|
||||||
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
const handleTableChange = (pagination: any, filters: any, sorter: any) => {
|
||||||
setParams({
|
setParams({
|
||||||
...params,
|
...params,
|
||||||
|
|
@ -83,7 +90,7 @@ export default function Logs() {
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
const resetParams = {
|
const resetParams = {
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 10,
|
size: 20,
|
||||||
username: "",
|
username: "",
|
||||||
status: undefined,
|
status: undefined,
|
||||||
startDate: "",
|
startDate: "",
|
||||||
|
|
@ -204,13 +211,13 @@ export default function Logs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('logs.title')}
|
title={t('logs.title')}
|
||||||
subtitle={t('logs.subtitle')}
|
subtitle={t('logs.subtitle')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="mb-4 shadow-sm">
|
<Card className="mb-4 shadow-sm flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<Space wrap size="middle">
|
<Space wrap size="middle">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('logs.searchPlaceholder')}
|
placeholder={t('logs.searchPlaceholder')}
|
||||||
|
|
@ -255,8 +262,8 @@ export default function Logs() {
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}>
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { paddingTop: 0, paddingBottom: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
|
<Tabs activeKey={activeTab} onChange={onTabChange} size="large" className="flex-shrink-0">
|
||||||
{logTypeDict.length > 0 ? (
|
{logTypeDict.length > 0 ? (
|
||||||
logTypeDict.map(item => (
|
logTypeDict.map(item => (
|
||||||
<Tabs.TabPane
|
<Tabs.TabPane
|
||||||
|
|
@ -283,21 +290,18 @@ export default function Logs() {
|
||||||
)}
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Table
|
<div className="flex-1 min-h-0 h-full">
|
||||||
|
<ListTable
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="middle"
|
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
pagination={{
|
totalCount={total}
|
||||||
current: params.current,
|
scroll={{ y: 'calc(100vh - 520px)' }}
|
||||||
pageSize: params.size,
|
pagination={getStandardPagination(total, params.current, params.size)}
|
||||||
total: total,
|
|
||||||
showSizeChanger: true,
|
|
||||||
showTotal: (total) => t('common.total', { total })
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ export default function Orgs() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('orgs.title')}
|
title={t('orgs.title')}
|
||||||
subtitle={t('orgs.subtitle')}
|
subtitle={t('orgs.subtitle')}
|
||||||
|
|
@ -250,7 +250,7 @@ export default function Orgs() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isPlatformMode && (
|
{isPlatformMode && (
|
||||||
<Card className="shadow-sm mb-4">
|
<Card className="shadow-sm mb-4 flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text strong>{t('users.tenant')}:</Text>
|
<Text strong>{t('users.tenant')}:</Text>
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -266,7 +266,7 @@ export default function Orgs() {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
{selectedTenantId !== undefined ? (
|
{selectedTenantId !== undefined ? (
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|
@ -275,6 +275,7 @@ export default function Orgs() {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="middle"
|
size="middle"
|
||||||
|
scroll={{ y: 'calc(100vh - 350px)' }}
|
||||||
expandable={{ defaultExpandAllRows: true }}
|
expandable={{ defaultExpandAllRows: true }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ import {
|
||||||
ClusterOutlined,
|
ClusterOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
CheckSquareOutlined,
|
CheckSquareOutlined,
|
||||||
InfoCircleOutlined
|
InfoCircleOutlined,
|
||||||
|
FolderOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -75,7 +77,6 @@ export default function Permissions() {
|
||||||
const [editing, setEditing] = useState<SysPermission | null>(null);
|
const [editing, setEditing] = useState<SysPermission | null>(null);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { can } = usePermission();
|
const { can } = usePermission();
|
||||||
const level = Form.useWatch("level", form);
|
|
||||||
|
|
||||||
// Dictionaries
|
// Dictionaries
|
||||||
const { items: statusDict } = useDict("sys_common_status");
|
const { items: statusDict } = useDict("sys_common_status");
|
||||||
|
|
@ -108,16 +109,23 @@ export default function Permissions() {
|
||||||
|
|
||||||
const treeData = useMemo(() => buildTree(filtered), [filtered]);
|
const treeData = useMemo(() => buildTree(filtered), [filtered]);
|
||||||
|
|
||||||
|
const currentPermType = Form.useWatch("permType", form);
|
||||||
|
|
||||||
const parentOptions = useMemo(() => {
|
const parentOptions = useMemo(() => {
|
||||||
return data
|
return data
|
||||||
.filter((p) => p.permType === "menu")
|
.filter((p) => {
|
||||||
|
if (currentPermType === "button") {
|
||||||
|
return p.permType === "menu";
|
||||||
|
}
|
||||||
|
return p.permType === "directory";
|
||||||
|
})
|
||||||
.map((p) => ({ value: p.permId, label: p.name }));
|
.map((p) => ({ value: p.permId, label: p.name }));
|
||||||
}, [data]);
|
}, [data, currentPermType]);
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue({ level: 1, permType: "menu", status: 1, isVisible: 1, sortOrder: 0 });
|
form.setFieldsValue({ level: 1, permType: "directory", status: 1, isVisible: 1, sortOrder: 0 });
|
||||||
setOpen(true);
|
setOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -130,10 +138,20 @@ export default function Permissions() {
|
||||||
const openAddChild = (record: SysPermission) => {
|
const openAddChild = (record: SysPermission) => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
|
||||||
|
// Auto-calculate level based on parent
|
||||||
|
const parentLevel = record.level || 1;
|
||||||
|
const newLevel = Math.min(parentLevel + 1, 3);
|
||||||
|
|
||||||
|
// Default type: Level 1 -> Directory/Menu, Level 2 -> Menu/Button, Level 3 -> Button
|
||||||
|
let defaultType = "menu";
|
||||||
|
if (newLevel === 3) defaultType = "button";
|
||||||
|
if (newLevel === 1) defaultType = "directory";
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
parentId: record.permId,
|
parentId: record.permId,
|
||||||
level: Math.min((record.level || 1) + 1, 3),
|
level: newLevel,
|
||||||
permType: record.level === 1 ? "menu" : "button",
|
permType: defaultType,
|
||||||
status: 1,
|
status: 1,
|
||||||
isVisible: 1,
|
isVisible: 1,
|
||||||
sortOrder: 0
|
sortOrder: 0
|
||||||
|
|
@ -145,12 +163,22 @@ export default function Permissions() {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
|
// Auto calculate level
|
||||||
|
let calculatedLevel = 1;
|
||||||
|
if (values.parentId) {
|
||||||
|
const parent = data.find(p => p.permId === values.parentId);
|
||||||
|
if (parent) {
|
||||||
|
calculatedLevel = (parent.level || 1) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const payload: Partial<SysPermission> = {
|
const payload: Partial<SysPermission> = {
|
||||||
parentId: values.level === 1 ? undefined : values.parentId,
|
parentId: values.parentId || 0, // Ensure 0 for roots if backend expects it
|
||||||
name: values.name,
|
name: values.name,
|
||||||
code: values.code,
|
code: values.code,
|
||||||
permType: values.permType,
|
permType: values.permType,
|
||||||
level: values.level,
|
level: calculatedLevel,
|
||||||
path: values.path,
|
path: values.path,
|
||||||
component: values.component,
|
component: values.component,
|
||||||
icon: values.icon,
|
icon: values.icon,
|
||||||
|
|
@ -194,15 +222,18 @@ export default function Permissions() {
|
||||||
title: t('permissions.permName'),
|
title: t('permissions.permName'),
|
||||||
dataIndex: "name",
|
dataIndex: "name",
|
||||||
key: "name",
|
key: "name",
|
||||||
render: (text: string, record: SysPermission) => (
|
render: (text: string, record: SysPermission) => {
|
||||||
|
let icon = <CheckSquareOutlined style={{ color: '#52c41a' }} aria-hidden="true" />;
|
||||||
|
if (record.permType === 'directory') icon = <FolderOutlined style={{ color: '#faad14' }} aria-hidden="true" />;
|
||||||
|
if (record.permType === 'menu') icon = <MenuOutlined style={{ color: '#1890ff' }} aria-hidden="true" />;
|
||||||
|
|
||||||
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
{record.permType === 'menu' ?
|
{icon}
|
||||||
<MenuOutlined style={{ color: '#1890ff' }} aria-hidden="true" /> :
|
|
||||||
<CheckSquareOutlined style={{ color: '#52c41a' }} aria-hidden="true" />
|
|
||||||
}
|
|
||||||
<Text strong={record.level === 1}>{text}</Text>
|
<Text strong={record.level === 1}>{text}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('permissions.permCode'),
|
title: t('permissions.permCode'),
|
||||||
|
|
@ -216,8 +247,12 @@ export default function Permissions() {
|
||||||
width: 90,
|
width: 90,
|
||||||
render: (type: string) => {
|
render: (type: string) => {
|
||||||
const item = typeDict.find(i => i.itemValue === type);
|
const item = typeDict.find(i => i.itemValue === type);
|
||||||
|
let color = 'warning';
|
||||||
|
if (type === 'directory') color = 'default';
|
||||||
|
if (type === 'menu') color = 'processing';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tag color={type === 'menu' ? 'processing' : 'warning'}>
|
<Tag color={color}>
|
||||||
{item ? item.itemLabel : type}
|
{item ? item.itemLabel : type}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
|
@ -264,7 +299,7 @@ export default function Permissions() {
|
||||||
fixed: "right" as const,
|
fixed: "right" as const,
|
||||||
render: (_: any, record: SysPermission) => (
|
render: (_: any, record: SysPermission) => (
|
||||||
<Space>
|
<Space>
|
||||||
{can("sys:permission:create") && record.permType === 'menu' && (
|
{can("sys:permission:create") && record.permType !== 'button' && (
|
||||||
<Tooltip title="添加子项">
|
<Tooltip title="添加子项">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -300,7 +335,7 @@ export default function Permissions() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('permissions.title')}
|
title={t('permissions.title')}
|
||||||
subtitle={t('permissions.subtitle')}
|
subtitle={t('permissions.subtitle')}
|
||||||
|
|
@ -315,7 +350,7 @@ export default function Permissions() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="mb-4 shadow-sm">
|
<Card className="mb-4 shadow-sm flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<Space wrap size="middle">
|
<Space wrap size="middle">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('permissions.permName')}
|
placeholder={t('permissions.permName')}
|
||||||
|
|
@ -359,7 +394,7 @@ export default function Permissions() {
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
<Table
|
<Table
|
||||||
rowKey="permId"
|
rowKey="permId"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|
@ -367,7 +402,11 @@ export default function Permissions() {
|
||||||
columns={columns}
|
columns={columns}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="middle"
|
size="middle"
|
||||||
expandable={{ defaultExpandAllRows: false }}
|
scroll={{ y: 'calc(100vh - 350px)' }}
|
||||||
|
expandable={{
|
||||||
|
defaultExpandAllRows: false,
|
||||||
|
rowExpandable: (record) => record.permType !== 'button' && !!record.children?.length
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -396,24 +435,13 @@ export default function Permissions() {
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
className="permission-form"
|
className="permission-form"
|
||||||
onValuesChange={(changed) => {
|
onValuesChange={(changed) => {
|
||||||
if (changed.level === 1) {
|
if (changed.permType === 'button') {
|
||||||
form.setFieldsValue({ parentId: undefined });
|
form.setFieldsValue({ isVisible: 0 });
|
||||||
}
|
|
||||||
if (changed.level === 3) {
|
|
||||||
form.setFieldsValue({ permType: "button" });
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={24}>
|
||||||
<Form.Item label={t('permissions.level')} name="level" rules={[{ required: true }]}>
|
|
||||||
<Select
|
|
||||||
options={levelDict.map(i => ({ value: Number(i.itemValue), label: i.itemLabel }))}
|
|
||||||
aria-label={t('permissions.level')}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={12}>
|
|
||||||
<Form.Item label={t('permissions.permType')} name="permType" rules={[{ required: true }]}>
|
<Form.Item label={t('permissions.permType')} name="permType" rules={[{ required: true }]}>
|
||||||
<Select
|
<Select
|
||||||
options={typeDict.map(i => ({ value: i.itemValue, label: i.itemLabel }))}
|
options={typeDict.map(i => ({ value: i.itemValue, label: i.itemLabel }))}
|
||||||
|
|
@ -426,20 +454,12 @@ export default function Permissions() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t('permissions.parentId')}
|
label={t('permissions.parentId')}
|
||||||
name="parentId"
|
name="parentId"
|
||||||
dependencies={["level"]}
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
required: getFieldValue("level") > 1,
|
|
||||||
message: "非一级入口必须选择父级"
|
|
||||||
})
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
allowClear
|
allowClear
|
||||||
showSearch
|
showSearch
|
||||||
placeholder={level === 1 ? "一级入口无须父级" : "请选择父级菜单…"}
|
placeholder="顶级权限请留空"
|
||||||
options={parentOptions}
|
options={parentOptions}
|
||||||
disabled={level === 1}
|
|
||||||
aria-label={t('permissions.parentId')}
|
aria-label={t('permissions.parentId')}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import {
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import type { SysParamVO, SysParamQuery } from "../types";
|
import type { SysParamVO, SysParamQuery } from "../types";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
import "./SysParams.css";
|
import "./SysParams.css";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
@ -216,7 +217,7 @@ export default function SysParams() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sys-params-page p-6">
|
<div className="sys-params-page p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('sysParams.title')}
|
title={t('sysParams.title')}
|
||||||
subtitle={t('sysParams.subtitle')}
|
subtitle={t('sysParams.subtitle')}
|
||||||
|
|
@ -227,11 +228,10 @@ export default function SysParams() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="sys-params-table-card shadow-sm mb-4">
|
<Card className="sys-params-table-card shadow-sm mb-4 flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<Form
|
<Form
|
||||||
layout="inline"
|
layout="inline"
|
||||||
onFinish={handleSearch}
|
onFinish={handleSearch}
|
||||||
className="mb-4"
|
|
||||||
>
|
>
|
||||||
<Form.Item name="paramKey">
|
<Form.Item name="paramKey">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -260,21 +260,17 @@ export default function SysParams() {
|
||||||
</Space>
|
</Space>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
<Table
|
<Table
|
||||||
rowKey="paramId"
|
rowKey="paramId"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="middle"
|
size="middle"
|
||||||
pagination={{
|
scroll={{ y: 'calc(100vh - 350px)' }}
|
||||||
current: queryParams.pageNum,
|
pagination={getStandardPagination(total, queryParams.pageNum || 1, queryParams.pageSize || 10, handlePageChange)}
|
||||||
pageSize: queryParams.pageSize,
|
|
||||||
total: total,
|
|
||||||
showTotal: (tTotal) => t('common.total', { total: tTotal }),
|
|
||||||
onChange: handlePageChange,
|
|
||||||
showSizeChanger: true,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,17 @@ import {
|
||||||
message,
|
message,
|
||||||
Popconfirm,
|
Popconfirm,
|
||||||
Space,
|
Space,
|
||||||
Table,
|
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
DatePicker,
|
DatePicker,
|
||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Select
|
Select,
|
||||||
|
List,
|
||||||
|
Avatar,
|
||||||
|
Tooltip,
|
||||||
|
Divider,
|
||||||
|
Empty
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
@ -29,13 +33,15 @@ import {
|
||||||
ShopOutlined,
|
ShopOutlined,
|
||||||
CalendarOutlined,
|
CalendarOutlined,
|
||||||
PhoneOutlined,
|
PhoneOutlined,
|
||||||
UserOutlined
|
UserOutlined,
|
||||||
|
ClockCircleOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import type { SysTenant } from "../types";
|
import type { SysTenant } from "../types";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text, Paragraph } = Typography;
|
||||||
|
|
||||||
export default function Tenants() {
|
export default function Tenants() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
@ -50,7 +56,7 @@ export default function Tenants() {
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [params, setParams] = useState({
|
const [params, setParams] = useState({
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 10,
|
size: 12,
|
||||||
name: "",
|
name: "",
|
||||||
code: ""
|
code: ""
|
||||||
});
|
});
|
||||||
|
|
@ -82,7 +88,7 @@ export default function Tenants() {
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
const resetParams = {
|
const resetParams = {
|
||||||
current: 1,
|
current: 1,
|
||||||
size: 10,
|
size: 12,
|
||||||
name: "",
|
name: "",
|
||||||
code: ""
|
code: ""
|
||||||
};
|
};
|
||||||
|
|
@ -141,132 +147,145 @@ export default function Tenants() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const renderTenantCard = (item: SysTenant) => {
|
||||||
{
|
const statusItem = statusDict.find(i => i.itemValue === String(item.status));
|
||||||
title: t('tenants.tenantInfo'),
|
const isExpired = item.expireTime && dayjs().isAfter(dayjs(item.expireTime));
|
||||||
key: "tenant",
|
|
||||||
render: (_: any, record: SysTenant) => (
|
|
||||||
<Space>
|
|
||||||
<div style={{ width: 40, height: 40, background: '#f0f5ff', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#1890ff', fontSize: 20 }}>
|
|
||||||
<ShopOutlined aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 600, color: '#262626' }}>{record.tenantName}</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#8c8c8c' }} className="tabular-nums">{record.tenantCode}</div>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('tenants.contact'),
|
|
||||||
key: "contact",
|
|
||||||
render: (_: any, record: SysTenant) => (
|
|
||||||
<div>
|
|
||||||
<div><UserOutlined style={{ marginRight: 4, color: '#8c8c8c' }} />{record.contactName || "-"}</div>
|
|
||||||
<div style={{ fontSize: 12, color: '#8c8c8c' }} className="tabular-nums"><PhoneOutlined style={{ marginRight: 4 }} />{record.contactPhone || "-"}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.status'),
|
|
||||||
dataIndex: "status",
|
|
||||||
width: 100,
|
|
||||||
render: (status: number) => {
|
|
||||||
const item = statusDict.find(i => i.itemValue === String(status));
|
|
||||||
return (
|
|
||||||
<Tag color={status === 1 ? "green" : "red"}>
|
|
||||||
{item ? item.itemLabel : (status === 1 ? "正常" : "禁用")}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('tenants.expireTime'),
|
|
||||||
dataIndex: "expireTime",
|
|
||||||
width: 180,
|
|
||||||
render: (text: string) => (
|
|
||||||
<Space>
|
|
||||||
<CalendarOutlined style={{ color: '#8c8c8c' }} />
|
|
||||||
<Text className="tabular-nums">{text ? text.substring(0, 10) : t('tenants.forever')}</Text>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('common.action'),
|
|
||||||
key: "action",
|
|
||||||
width: 120,
|
|
||||||
fixed: "right" as const,
|
|
||||||
render: (_: any, record: SysTenant) => (
|
|
||||||
<Space>
|
|
||||||
{can("sys_tenant:update") && (
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={<EditOutlined aria-hidden="true" />}
|
|
||||||
onClick={() => openEdit(record)}
|
|
||||||
aria-label={t('common.edit')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{can("sys_tenant:delete") && (
|
|
||||||
<Popconfirm title={`确定删除租户 "${record.tenantName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<List.Item>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
className="tenant-card shadow-sm border-0"
|
||||||
|
style={{ borderRadius: '12px', overflow: 'hidden' }}
|
||||||
|
actions={[
|
||||||
|
can("sys_tenant:update") && (
|
||||||
|
<Tooltip title={t('common.edit')} key="edit-tip">
|
||||||
|
<EditOutlined key="edit" onClick={() => openEdit(item)} style={{ color: '#1677ff' }} />
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
can("sys_tenant:delete") && (
|
||||||
|
<Popconfirm key="delete-pop" title={`确定删除租户 "${item.tenantName}" 吗?`} onConfirm={() => handleDelete(item.id)}>
|
||||||
|
<DeleteOutlined key="delete" style={{ color: '#ff4d4f' }} />
|
||||||
|
</Popconfirm>
|
||||||
|
)
|
||||||
|
].filter(Boolean) as React.ReactNode[]}
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'flex-start', marginBottom: 16 }}>
|
||||||
|
<Avatar
|
||||||
|
size={48}
|
||||||
|
icon={<ShopOutlined />}
|
||||||
|
style={{ backgroundColor: item.status === 1 ? '#e6f4ff' : '#fff1f0', color: item.status === 1 ? '#1677ff' : '#ff4d4f', marginRight: 12, borderRadius: '8px' }}
|
||||||
|
/>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<Title level={5} style={{ margin: 0, fontSize: '16px' }} ellipsis={{ tooltip: item.tenantName }}>
|
||||||
|
{item.tenantName}
|
||||||
|
</Title>
|
||||||
|
<Tag color={item.status === 1 ? "green" : "red"} style={{ margin: 0, borderRadius: '4px' }}>
|
||||||
|
{statusItem ? statusItem.itemLabel : (item.status === 1 ? "正常" : "禁用")}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px' }} className="tabular-nums">
|
||||||
|
CODE: {item.tenantCode}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-content" style={{ fontSize: '13px' }}>
|
||||||
|
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', color: '#595959' }}>
|
||||||
|
<UserOutlined style={{ marginRight: 8, color: '#bfbfbf' }} />
|
||||||
|
<Text ellipsis={{ tooltip: item.contactName || "-" }}>{item.contactName || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', color: '#595959' }}>
|
||||||
|
<PhoneOutlined style={{ marginRight: 8, color: '#bfbfbf' }} />
|
||||||
|
<Text className="tabular-nums">{item.contactPhone || "-"}</Text>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<ClockCircleOutlined style={{ marginRight: 8, color: '#bfbfbf' }} />
|
||||||
|
<Text type={isExpired ? "danger" : "secondary"}>
|
||||||
|
{item.expireTime ? item.expireTime.substring(0, 10) : t('tenants.forever')}
|
||||||
|
{isExpired && <Tag color="error" style={{ marginLeft: 8, transform: 'scale(0.8)' }}>已过期</Tag>}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.remark && (
|
||||||
|
<>
|
||||||
|
<Divider style={{ margin: '12px 0' }} />
|
||||||
|
<Paragraph ellipsis={{ rows: 2, tooltip: item.remark }} style={{ margin: 0, fontSize: '12px', color: '#8c8c8c', height: '36px' }}>
|
||||||
|
{item.remark}
|
||||||
|
</Paragraph>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('tenants.title')}
|
title={t('tenants.title')}
|
||||||
subtitle={t('tenants.subtitle')}
|
subtitle={t('tenants.subtitle')}
|
||||||
extra={can("sys_tenant:create") && (
|
extra={can("sys_tenant:create") && (
|
||||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate} style={{ borderRadius: '6px' }}>
|
||||||
{t('tenants.drawerTitleCreate')}
|
{t('tenants.drawerTitleCreate')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="shadow-sm mb-4">
|
<Card className="shadow-sm mb-6 border-0 flex-shrink-0" style={{ borderRadius: '12px' }} styles={{ body: { padding: '16px' } }}>
|
||||||
<Space wrap size="middle">
|
<Space wrap size="middle">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('tenants.tenantName')}
|
placeholder={t('tenants.tenantName')}
|
||||||
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
prefix={<SearchOutlined aria-hidden="true" className="text-gray-400" />}
|
||||||
style={{ width: 200 }}
|
style={{ width: 220, borderRadius: '6px' }}
|
||||||
value={params.name}
|
value={params.name}
|
||||||
onChange={e => setParams({ ...params, name: e.target.value })}
|
onChange={e => setParams({ ...params, name: e.target.value })}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('tenants.tenantCode')}
|
placeholder={t('tenants.tenantCode')}
|
||||||
style={{ width: 180 }}
|
style={{ width: 180, borderRadius: '6px' }}
|
||||||
value={params.code}
|
value={params.code}
|
||||||
onChange={e => setParams({ ...params, code: e.target.value })}
|
onChange={e => setParams({ ...params, code: e.target.value })}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t('common.search')}</Button>
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch} style={{ borderRadius: '6px' }}>
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>{t('common.reset')}</Button>
|
{t('common.search')}
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset} style={{ borderRadius: '6px' }}>
|
||||||
|
{t('common.reset')}
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card className="shadow-sm" styles={{ body: { padding: 0 } }}>
|
<div className="flex-1 overflow-y-auto pr-2">
|
||||||
<Table
|
<List
|
||||||
rowKey="id"
|
grid={{
|
||||||
columns={columns}
|
gutter: 24,
|
||||||
dataSource={data}
|
xs: 1,
|
||||||
|
sm: 2,
|
||||||
|
md: 2,
|
||||||
|
lg: 3,
|
||||||
|
xl: 4,
|
||||||
|
xxl: 4,
|
||||||
|
}}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="middle"
|
dataSource={data}
|
||||||
|
renderItem={renderTenantCard}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: params.current,
|
...getStandardPagination(total, params.current, params.size, (page, size) => setParams({ ...params, current: page, size })),
|
||||||
pageSize: params.size,
|
align: 'center',
|
||||||
total: total,
|
style: { marginTop: '24px', marginBottom: '24px' }
|
||||||
showSizeChanger: true,
|
}}
|
||||||
onChange: (page, size) => setParams({ ...params, current: page, size }),
|
locale={{
|
||||||
showTotal: (total) => t('common.total', { total })
|
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无租户数据" />
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import {
|
||||||
MinusCircleOutlined
|
MinusCircleOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import PageHeader from "../components/shared/PageHeader";
|
import PageHeader from "../components/shared/PageHeader";
|
||||||
|
import { getStandardPagination } from "../utils/pagination";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
|
@ -124,6 +125,10 @@ export default function Users() {
|
||||||
|
|
||||||
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
const activeTenantId = useMemo(() => Number(localStorage.getItem("activeTenantId") || 0), []);
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [current, setCurrent] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
const [filterTenantId, setFilterTenantId] = useState<number | undefined>(undefined);
|
const [filterTenantId, setFilterTenantId] = useState<number | undefined>(undefined);
|
||||||
|
|
@ -411,7 +416,7 @@ export default function Users() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="users-page p-6">
|
<div className="users-page p-6 flex flex-col h-full overflow-hidden">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={t('users.title')}
|
title={t('users.title')}
|
||||||
subtitle={t('users.subtitle')}
|
subtitle={t('users.subtitle')}
|
||||||
|
|
@ -422,8 +427,8 @@ export default function Users() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Card className="users-table-card shadow-sm">
|
<Card className="users-table-card shadow-sm mb-4 flex-shrink-0" styles={{ body: { padding: '16px' } }}>
|
||||||
<div className="users-table-toolbar mb-4">
|
<div className="users-table-toolbar">
|
||||||
<Space size="middle" wrap>
|
<Space size="middle" wrap>
|
||||||
{isPlatformMode && (
|
{isPlatformMode && (
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -442,25 +447,33 @@ export default function Users() {
|
||||||
className="users-search-input"
|
className="users-search-input"
|
||||||
style={{ width: 300 }}
|
style={{ width: 300 }}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
setCurrent(1); // Reset to first page on search
|
||||||
|
}}
|
||||||
allowClear
|
allowClear
|
||||||
aria-label={t('common.search')}
|
aria-label={t('common.search')}
|
||||||
/>
|
/>
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadUsersData}>{t('common.refresh')}</Button>
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadUsersData}>{t('common.refresh')}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="shadow-sm flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
|
<div className="flex-1 min-h-0 h-full">
|
||||||
<Table
|
<Table
|
||||||
rowKey="userId"
|
rowKey="userId"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
size="middle"
|
size="middle"
|
||||||
pagination={{
|
scroll={{ y: 'calc(100vh - 420px)' }}
|
||||||
showTotal: (total) => t('common.total', { total }),
|
pagination={getStandardPagination(filteredData.length, current, pageSize, (p, s) => {
|
||||||
pageSize: 10,
|
setCurrent(p);
|
||||||
}}
|
setPageSize(s);
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ export interface SysUser extends BaseEntity {
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
tenantId: number;
|
||||||
username: string;
|
username: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue