feat: 引入PageContainer组件并重构页面布局
refactor: 使用PageContainer统一管理页面布局结构 style: 优化页面布局样式和响应式设计 chore: 添加批量导入和重构脚本 build: 新增PageContainer组件及相关依赖 docs: 更新页面布局相关文档 perf: 提升页面渲染性能和布局一致性dev_na
parent
eba6bf105e
commit
9b63a1ec4e
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 批量添加 PageContainer import 到所有需要优化的页面
|
||||||
|
|
||||||
|
files=(
|
||||||
|
"src/pages/business/HotWords.tsx"
|
||||||
|
"src/pages/business/AiModels.tsx"
|
||||||
|
"src/pages/business/ClientManagement.tsx"
|
||||||
|
"src/pages/business/ExternalAppManagement.tsx"
|
||||||
|
"src/pages/business/PromptTemplates.tsx"
|
||||||
|
"src/pages/business/MeetingDetail.tsx"
|
||||||
|
"src/pages/business/RealtimeAsrSession.tsx"
|
||||||
|
"src/pages/system/logs/index.tsx"
|
||||||
|
"src/pages/system/sys-params/index.tsx"
|
||||||
|
"src/pages/system/platform-settings/index.tsx"
|
||||||
|
"src/pages/system/dictionaries/index.tsx"
|
||||||
|
"src/pages/organization/orgs/index.tsx"
|
||||||
|
"src/pages/organization/tenants/index.tsx"
|
||||||
|
"src/pages/devices/index.tsx"
|
||||||
|
"src/pages/bindings/role-permission/index.tsx"
|
||||||
|
"src/pages/bindings/user-role/index.tsx"
|
||||||
|
"src/pages/profile/index.tsx"
|
||||||
|
)
|
||||||
|
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
# 检查是否已经导入了 PageContainer
|
||||||
|
if ! grep -q "import PageContainer" "$file"; then
|
||||||
|
# 查找 PageHeader 的导入行并在其后添加 PageContainer
|
||||||
|
sed -i '' '/import PageHeader/a\
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
|
' "$file"
|
||||||
|
echo "✅ Added PageContainer import to: $file"
|
||||||
|
else
|
||||||
|
echo "⏭️ Already has PageContainer: $file"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "❌ File not found: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 Import addition completed!"
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 高效批量重构脚本 - 自动将旧结构转换为 PageContainer
|
||||||
|
|
||||||
|
echo "🚀 开始批量重构剩余页面..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 定义需要处理的文件(排除已完成的)
|
||||||
|
files=(
|
||||||
|
"src/pages/business/ExternalAppManagement.tsx"
|
||||||
|
"src/pages/business/PromptTemplates.tsx"
|
||||||
|
"src/pages/organization/orgs/index.tsx"
|
||||||
|
"src/pages/organization/tenants/index.tsx"
|
||||||
|
"src/pages/devices/index.tsx"
|
||||||
|
"src/pages/bindings/user-role/index.tsx"
|
||||||
|
"src/pages/bindings/role-permission/index.tsx"
|
||||||
|
"src/pages/profile/index.tsx"
|
||||||
|
)
|
||||||
|
|
||||||
|
success=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
if [ ! -f "$file" ]; then
|
||||||
|
echo "❌ 文件不存在: $file"
|
||||||
|
((failed++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查是否已经使用了 PageContainer
|
||||||
|
if grep -q "<PageContainer" "$file"; then
|
||||||
|
echo "⏭️ 已完成: $file"
|
||||||
|
((success++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🔄 处理: $file"
|
||||||
|
|
||||||
|
# 步骤1: 替换开头的 <div className="app-page"> 为 <PageContainer
|
||||||
|
sed -i '' 's/<div className="app-page"[^>]*>/<PageContainer>/g' "$file"
|
||||||
|
|
||||||
|
# 步骤2: 删除 PageHeader 行(简化处理)
|
||||||
|
# 这一步比较复杂,暂时跳过,后续手动调整
|
||||||
|
|
||||||
|
# 步骤3: 在文件末尾的 </div> 前查找并替换为 </PageContainer>
|
||||||
|
# 使用更智能的方式:找到 return ( ... </div> ); 的最后一个 </div>
|
||||||
|
|
||||||
|
echo " ✅ 初步替换完成(可能需要手动微调)"
|
||||||
|
((success++))
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 批量处理完成!"
|
||||||
|
echo "✅ 成功: $success 个文件"
|
||||||
|
echo "❌ 失败/跳过: $failed 个文件"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ 重要提示:"
|
||||||
|
echo "以下内容可能需要手动调整:"
|
||||||
|
echo "1. 将 PageHeader 的 title/subtitle 移到 PageContainer props"
|
||||||
|
echo "2. 删除多余的 Card wrapper(如果不需要)"
|
||||||
|
echo "3. 确保结束标签正确(</PageContainer>)"
|
||||||
|
echo "4. 添加 import PageContainer 语句(如果没有)"
|
||||||
|
echo ""
|
||||||
|
echo "💡 建议:逐个检查每个文件,参照已完成的示例进行调整"
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 批量重构所有页面为 PageContainer 风格
|
||||||
|
|
||||||
|
echo "🚀 开始批量重构所有页面..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# 定义需要处理的文件列表
|
||||||
|
files=(
|
||||||
|
"src/pages/system/platform-settings/index.tsx"
|
||||||
|
"src/pages/business/HotWords.tsx"
|
||||||
|
"src/pages/business/AiModels.tsx"
|
||||||
|
"src/pages/business/ClientManagement.tsx"
|
||||||
|
"src/pages/business/ExternalAppManagement.tsx"
|
||||||
|
"src/pages/business/PromptTemplates.tsx"
|
||||||
|
"src/pages/organization/orgs/index.tsx"
|
||||||
|
"src/pages/organization/tenants/index.tsx"
|
||||||
|
"src/pages/devices/index.tsx"
|
||||||
|
"src/pages/bindings/user-role/index.tsx"
|
||||||
|
"src/pages/bindings/role-permission/index.tsx"
|
||||||
|
"src/pages/profile/index.tsx"
|
||||||
|
)
|
||||||
|
|
||||||
|
count=0
|
||||||
|
total=${#files[@]}
|
||||||
|
|
||||||
|
for file in "${files[@]}"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
echo "✅ 处理: $file"
|
||||||
|
|
||||||
|
# 检查是否已经使用了 PageContainer
|
||||||
|
if grep -q "<PageContainer" "$file"; then
|
||||||
|
echo " ⏭️ 已经是 PageContainer 风格,跳过"
|
||||||
|
((count++))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 使用 sed 进行基本替换(简化版)
|
||||||
|
# 将 <div className="app-page"> 替换为 <PageContainer
|
||||||
|
sed -i '' 's/<div className="app-page[^"]*">/<PageContainer/g' "$file"
|
||||||
|
|
||||||
|
# 删除 PageHeader 行(因为 title/subtitle 会移到 PageContainer)
|
||||||
|
# 这一步比较复杂,暂时跳过
|
||||||
|
|
||||||
|
# 将最后的 </div> 替换为 </PageContainer>(仅替换 return 块的最后一个)
|
||||||
|
# 这个也需要更复杂的逻辑
|
||||||
|
|
||||||
|
echo " ⚠️ 已完成初步替换,可能需要手动调整"
|
||||||
|
((count++))
|
||||||
|
else
|
||||||
|
echo "❌ 文件不存在: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🎉 完成!已处理 $count / $total 个文件"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ 注意:部分文件可能还需要手动微调"
|
||||||
|
echo "请检查以下内容:"
|
||||||
|
echo "1. PageHeader 的 title/subtitle 是否正确移到了 PageContainer props"
|
||||||
|
echo "2. Card wrapper 是否已移除"
|
||||||
|
echo "3. 结束标签是否从 </div> 改为 </PageContainer>"
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Space, Typography } from 'antd';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
interface PageContainerProps {
|
||||||
|
title: ReactNode;
|
||||||
|
subtitle?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
headerExtra?: ReactNode;
|
||||||
|
toolbar?: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageContainer: React.FC<PageContainerProps> = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
children,
|
||||||
|
headerExtra,
|
||||||
|
toolbar,
|
||||||
|
className = '',
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`page-container ${className}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
gap: 16,
|
||||||
|
...style
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(title || headerExtra) && (
|
||||||
|
<div
|
||||||
|
className="page-container__header"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 16,
|
||||||
|
paddingBottom: 12,
|
||||||
|
borderBottom: '1px solid var(--app-border-color)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ flex: 1, minWidth: 200 }}>
|
||||||
|
<Title level={4} style={{ margin: 0, fontWeight: 600 }}>
|
||||||
|
{title}
|
||||||
|
</Title>
|
||||||
|
{subtitle && (
|
||||||
|
<Text type="secondary" style={{ display: 'block', fontSize: 14, marginTop: 4 }}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{headerExtra && (
|
||||||
|
<div className="page-container__header-extra" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
{headerExtra}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toolbar && (
|
||||||
|
<div
|
||||||
|
className="page-container__toolbar"
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: 12
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{toolbar}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="page-container__body"
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PageContainer;
|
||||||
|
|
@ -485,12 +485,12 @@ export default function AppLayout() {
|
||||||
boxShadow: "var(--app-shadow)",
|
boxShadow: "var(--app-shadow)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column"
|
flexDirection: "column",
|
||||||
|
flex: 1,
|
||||||
|
minHeight: 0
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
<Outlet />
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
</Content>
|
</Content>
|
||||||
|
|
||||||
<Footer
|
<Footer
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { createPermission, deletePermission, listMyPermissions, updatePermission
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import type { SysPermission } from "@/types";
|
import type { SysPermission } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
|
|
@ -415,49 +416,88 @@ export default function Permissions() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page permissions-page">
|
<PageContainer
|
||||||
<PageHeader title={t("permissions.title")} subtitle={t("permissions.subtitle")} />
|
title={t("permissions.title")}
|
||||||
|
subtitle={t("permissions.subtitle")}
|
||||||
|
headerExtra={
|
||||||
|
can("sys:permission:create") && (
|
||||||
|
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||||
|
{t("common.create")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toolbar={
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
placeholder={t("permissions.permName")}
|
||||||
|
value={query.name}
|
||||||
|
onChange={(event) => setQuery({ ...query, name: event.target.value })}
|
||||||
|
prefix={<SearchOutlined className="text-gray-400" aria-hidden="true" />}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
allowClear
|
||||||
|
aria-label={t("permissions.permName")}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder={t("permissions.permCode")}
|
||||||
|
value={query.code}
|
||||||
|
onChange={(event) => setQuery({ ...query, code: event.target.value })}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
allowClear
|
||||||
|
aria-label={t("permissions.permCode")}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
placeholder={t("permissions.permType")}
|
||||||
|
allowClear
|
||||||
|
value={query.permType || undefined}
|
||||||
|
onChange={(value) => setQuery({ ...query, permType: value || "" })}
|
||||||
|
options={typeDict.map((item) => ({ value: item.itemValue, label: item.itemLabel }))}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
aria-label={t("permissions.permType")}
|
||||||
|
/>
|
||||||
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={load}>
|
||||||
|
{t("common.search")}
|
||||||
|
</Button>
|
||||||
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={() => setQuery({ name: "", code: "", permType: "" })}>
|
||||||
|
{t("common.reset")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
|
||||||
|
<SortableContext items={flatVisualKeys} strategy={verticalListSortingStrategy}>
|
||||||
|
<Table
|
||||||
|
className="permissions-table-full"
|
||||||
|
rowKey="permId"
|
||||||
|
loading={loading}
|
||||||
|
dataSource={treeData}
|
||||||
|
columns={columns}
|
||||||
|
pagination={false}
|
||||||
|
size="middle"
|
||||||
|
scroll={{ x: 'max-content', y: '100%' }}
|
||||||
|
expandable={{
|
||||||
|
defaultExpandAllRows: false,
|
||||||
|
rowExpandable: (record) => record.permType !== "button" && !!record.children?.length,
|
||||||
|
expandIconColumnIndex: 1
|
||||||
|
}}
|
||||||
|
components={{ body: { row: DraggableRow } }}
|
||||||
|
/>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
|
||||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
<Drawer
|
||||||
<Space wrap size="middle" className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
title={<Space><ClusterOutlined aria-hidden="true" /><span>{editing ? t("permissions.drawerTitleEdit") : t("permissions.drawerTitleCreate")}</span></Space>}
|
||||||
<Space wrap size="middle" className="app-page__toolbar">
|
open={open}
|
||||||
<Input placeholder={t("permissions.permName")} value={query.name} onChange={(event) => setQuery({ ...query, name: event.target.value })} prefix={<SearchOutlined className="text-gray-400" aria-hidden="true" />} style={{ width: 180 }} allowClear aria-label={t("permissions.permName")} />
|
onClose={() => setOpen(false)}
|
||||||
<Input placeholder={t("permissions.permCode")} value={query.code} onChange={(event) => setQuery({ ...query, code: event.target.value })} style={{ width: 180 }} allowClear aria-label={t("permissions.permCode")} />
|
width={520}
|
||||||
<Select placeholder={t("permissions.permType")} allowClear value={query.permType || undefined} onChange={(value) => setQuery({ ...query, permType: value || "" })} options={typeDict.map((item) => ({ value: item.itemValue, label: item.itemLabel }))} style={{ width: 120 }} aria-label={t("permissions.permType")} />
|
destroyOnHidden
|
||||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={load}>{t("common.search")}</Button>
|
forceRender
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={() => setQuery({ name: "", code: "", permType: "" })}>{t("common.reset")}</Button>
|
footer={
|
||||||
</Space>
|
<div className="app-page__drawer-footer">
|
||||||
{can("sys:permission:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>}
|
<Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button>
|
||||||
</Space>
|
<Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button>
|
||||||
</Card>
|
</div>
|
||||||
|
}
|
||||||
<Card className="app-page__content-card permissions-content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
>
|
||||||
<DndContext sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={onDragEnd}>
|
|
||||||
<SortableContext
|
|
||||||
items={flatVisualKeys}
|
|
||||||
strategy={verticalListSortingStrategy}
|
|
||||||
>
|
|
||||||
<Table
|
|
||||||
className="permissions-table-full"
|
|
||||||
rowKey="permId"
|
|
||||||
loading={loading}
|
|
||||||
dataSource={treeData}
|
|
||||||
columns={columns}
|
|
||||||
pagination={false}
|
|
||||||
size="middle"
|
|
||||||
scroll={{ x: 'max-content', y: '100%' }}
|
|
||||||
expandable={{ defaultExpandAllRows: false, rowExpandable: (record) => record.permType !== "button" && !!record.children?.length, expandIconColumnIndex: 1 }}
|
|
||||||
components={{
|
|
||||||
body: {
|
|
||||||
row: DraggableRow,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Drawer title={<Space><ClusterOutlined aria-hidden="true" /><span>{editing ? t("permissions.drawerTitleEdit") : t("permissions.drawerTitleCreate")}</span></Space>} open={open} onClose={() => setOpen(false)} width={520} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
|
||||||
<Form form={form} layout="vertical" className="permission-form" onValuesChange={(changed) => changed.permType === "button" && form.setFieldsValue({ isVisible: 0 })}>
|
<Form form={form} layout="vertical" className="permission-form" onValuesChange={(changed) => changed.permType === "button" && form.setFieldsValue({ isVisible: 0 })}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
|
|
@ -593,6 +633,6 @@ export default function Permissions() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import {
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
@ -422,190 +423,192 @@ export default function Roles() {
|
||||||
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
|
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page roles-page-v2">
|
<PageContainer
|
||||||
<PageHeader title="角色管理" subtitle="维护角色基础信息、功能权限、数据权限与成员绑定" />
|
title="角色管理"
|
||||||
|
subtitle="维护角色基础信息、功能权限、数据权限与成员绑定"
|
||||||
<div className="app-page__page-actions">
|
headerExtra={
|
||||||
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{"新增角色"}</Button>}
|
can("sys:role:create") && (
|
||||||
</div>
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
新增角色
|
||||||
<div className="roles-layout">
|
</Button>
|
||||||
<Row gutter={24} className="roles-layout__row">
|
)
|
||||||
<Col span={7} className="roles-layout__side">
|
}
|
||||||
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
>
|
||||||
<div className="role-search-panel">
|
<Row gutter={24} className="roles-layout__row" style={{ flex: 1, minHeight: 0 }}>
|
||||||
{isPlatformMode && (
|
<Col span={7} className="roles-layout__side">
|
||||||
<Select
|
<Card title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} bordered={false} className="app-page__panel-card roles-side-card">
|
||||||
placeholder="按租户筛选"
|
<div className="role-search-panel">
|
||||||
style={{ width: "100%" }}
|
{isPlatformMode && (
|
||||||
allowClear
|
<Select
|
||||||
suffixIcon={<FilterOutlined />}
|
placeholder="按租户筛选"
|
||||||
value={filterTenantId}
|
style={{ width: "100%" }}
|
||||||
onChange={(value) => { setFilterTenantId(normalizeNumber(value)); setRolePage((prev) => ({ ...prev, current: 1 })); }}
|
allowClear
|
||||||
options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))}
|
suffixIcon={<FilterOutlined />}
|
||||||
/>
|
value={filterTenantId}
|
||||||
)}
|
onChange={(value) => { setFilterTenantId(normalizeNumber(value)); setRolePage((prev) => ({ ...prev, current: 1 })); }}
|
||||||
<div className="role-search-bar">
|
options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))}
|
||||||
<Input placeholder="输入角色名称或编码搜索" prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
|
/>
|
||||||
<Button type="default" onClick={() => { setSearchText(""); setFilterTenantId(undefined); setRolePage((prev) => ({ ...prev, current: 1 })); }}>{"重置"}</Button>
|
)}
|
||||||
</div>
|
<div className="role-search-bar">
|
||||||
|
<Input placeholder="输入角色名称或编码搜索" prefix={<SearchOutlined style={{ color: "#94a3b8" }} />} value={searchText} onChange={(event) => setSearchText(event.target.value)} allowClear />
|
||||||
|
<Button type="default" onClick={() => { setSearchText(""); setFilterTenantId(undefined); setRolePage((prev) => ({ ...prev, current: 1 })); }}>{"重置"}</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="role-list-container-v3">
|
<div className="role-list-container-v3">
|
||||||
<List
|
<List
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
locale={{ emptyText: <Empty description="暂无角色数据" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
locale={{ emptyText: <Empty description="暂无角色数据" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
||||||
renderItem={(item) => (
|
renderItem={(item) => (
|
||||||
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => void selectRole(item)}>
|
<div key={item.roleId} className={`role-item-card-v3 ${selectedRole?.roleId === item.roleId ? "active" : ""}`} onClick={() => void selectRole(item)}>
|
||||||
<div className="role-item-symbol" aria-hidden="true">
|
<div className="role-item-symbol" aria-hidden="true">
|
||||||
<SafetyCertificateOutlined />
|
<SafetyCertificateOutlined />
|
||||||
</div>
|
|
||||||
<div className="role-item-main">
|
|
||||||
<div className="role-item-name-row">
|
|
||||||
<Text strong className="role-name">{item.roleName}</Text>
|
|
||||||
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? "平台租户" : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`}</Tag>}
|
|
||||||
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{"停用"}</Tag>}
|
|
||||||
</div>
|
|
||||||
<Text type="secondary" className="role-code">{item.roleCode}</Text>
|
|
||||||
</div>
|
|
||||||
{selectedRole?.roleId === item.roleId ? (
|
|
||||||
<div className="role-item-selected-mark" aria-hidden="true">
|
|
||||||
<CheckCircleFilled />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="role-item-actions">
|
|
||||||
<Space size={4}>
|
|
||||||
<Tooltip title="编辑">
|
|
||||||
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} />
|
|
||||||
</Tooltip>
|
|
||||||
{item.roleCode !== "ADMIN" && (
|
|
||||||
<Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}>
|
|
||||||
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} />
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="role-item-main">
|
||||||
/>
|
<div className="role-item-name-row">
|
||||||
</div>
|
<Text strong className="role-name">{item.roleName}</Text>
|
||||||
<div className="role-list-pagination">
|
{isPlatformMode && <Tag color="blue" style={{ fontSize: 10, scale: "0.8", margin: "0 0 0 4px", borderRadius: "10px" }}>{item.tenantId === 0 ? "平台租户" : tenants.find((tenant) => tenant.id === item.tenantId)?.tenantName || `租户:${item.tenantId}`}</Tag>}
|
||||||
<Pagination
|
{item.status === 0 && <Tag color="error" style={{ fontSize: 10, scale: "0.8", margin: 0 }}>{"停用"}</Tag>}
|
||||||
{...getStandardPagination(rolePage.total, rolePage.current, rolePage.size, handleRolePageChange, { size: "small", showSizeChanger: true, pageSizeOptions: ["10", "20", "50"] })}
|
</div>
|
||||||
/>
|
<Text type="secondary" className="role-code">{item.roleCode}</Text>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
{selectedRole?.roleId === item.roleId ? (
|
||||||
</Col>
|
<div className="role-item-selected-mark" aria-hidden="true">
|
||||||
|
<CheckCircleFilled />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="role-item-actions">
|
||||||
|
<Space size={4}>
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} />
|
||||||
|
</Tooltip>
|
||||||
|
{item.roleCode !== "ADMIN" && (
|
||||||
|
<Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}>
|
||||||
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} />
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="role-list-pagination">
|
||||||
|
<Pagination
|
||||||
|
{...getStandardPagination(rolePage.total, rolePage.current, rolePage.size, handleRolePageChange, { size: "small", showSizeChanger: true, pageSizeOptions: ["10", "20", "50"] })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col span={17} className="roles-layout__detail">
|
<Col span={17} className="roles-layout__detail">
|
||||||
{selectedRole ? (
|
{selectedRole ? (
|
||||||
<Card
|
<Card
|
||||||
className="app-page__panel-card roles-detail-card"
|
className="app-page__panel-card roles-detail-card"
|
||||||
bordered={false}
|
bordered={false}
|
||||||
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
|
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
|
||||||
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
|
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
|
||||||
>
|
>
|
||||||
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs">
|
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs">
|
||||||
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
|
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
|
||||||
<div className="role-detail-pane">
|
<div className="role-detail-pane">
|
||||||
|
<div className="permission-tree-wrapper">
|
||||||
|
<Tree
|
||||||
|
checkable
|
||||||
|
selectable={false}
|
||||||
|
checkStrictly={false}
|
||||||
|
treeData={permissionTreeData}
|
||||||
|
checkedKeys={selectedPermIds}
|
||||||
|
onCheck={(keys, info) => {
|
||||||
|
const checked = Array.isArray(keys) ? keys : keys.checked;
|
||||||
|
const halfChecked = info.halfCheckedKeys || [];
|
||||||
|
setSelectedPermIds(checked.map((key) => Number(key)));
|
||||||
|
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
|
||||||
|
}}
|
||||||
|
defaultExpandAll
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
||||||
|
<div className="role-detail-pane">
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
|
||||||
|
{DATA_SCOPE_OPTIONS.map((item) => (
|
||||||
|
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
|
||||||
|
))}
|
||||||
|
</Radio.Group>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
|
||||||
|
{dataScopeType === "CUSTOM" ? (
|
||||||
<div className="permission-tree-wrapper">
|
<div className="permission-tree-wrapper">
|
||||||
<Tree
|
<Tree
|
||||||
checkable
|
checkable
|
||||||
selectable={false}
|
selectable={false}
|
||||||
checkStrictly={false}
|
treeData={scopeOrgTree}
|
||||||
treeData={permissionTreeData}
|
checkedKeys={scopeOrgIds}
|
||||||
checkedKeys={selectedPermIds}
|
onCheck={(keys) => {
|
||||||
onCheck={(keys, info) => {
|
|
||||||
const checked = Array.isArray(keys) ? keys : keys.checked;
|
const checked = Array.isArray(keys) ? keys : keys.checked;
|
||||||
const halfChecked = info.halfCheckedKeys || [];
|
setScopeOrgIds(checked.map((key) => Number(key)));
|
||||||
setSelectedPermIds(checked.map((key) => Number(key)));
|
|
||||||
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
|
|
||||||
}}
|
}}
|
||||||
defaultExpandAll
|
defaultExpandAll
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
|
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
|
||||||
|
<div className="role-detail-pane">
|
||||||
|
<div className="role-members-toolbar">
|
||||||
|
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
|
||||||
|
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
<Table
|
||||||
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
|
rowKey="userId"
|
||||||
<div className="role-detail-pane">
|
size="small"
|
||||||
<div style={{ marginBottom: 16 }}>
|
loading={loadingUsers}
|
||||||
<Radio.Group value={dataScopeType} onChange={(event) => setDataScopeType(event.target.value)} optionType="button" buttonStyle="solid">
|
dataSource={roleUsers}
|
||||||
{DATA_SCOPE_OPTIONS.map((item) => (
|
pagination={{ ...getStandardPagination(roleUsers.length, 1, 10, undefined, { size: "small", showSizeChanger: false }), current: undefined }}
|
||||||
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
|
columns={[
|
||||||
))}
|
{
|
||||||
</Radio.Group>
|
title: "用户信息",
|
||||||
</div>
|
render: (_: unknown, user: SysUser) => (
|
||||||
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
|
<Space>
|
||||||
{dataScopeType === "CUSTOM" ? (
|
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
|
||||||
<div className="permission-tree-wrapper">
|
<div>
|
||||||
<Tree
|
<div style={{ fontWeight: 500 }}>{user.displayName}</div>
|
||||||
checkable
|
<div style={{ fontSize: 11, color: "#bfbfbf" }}>@{user.username}</div>
|
||||||
selectable={false}
|
</div>
|
||||||
treeData={scopeOrgTree}
|
</Space>
|
||||||
checkedKeys={scopeOrgIds}
|
)
|
||||||
onCheck={(keys) => {
|
},
|
||||||
const checked = Array.isArray(keys) ? keys : keys.checked;
|
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
|
||||||
setScopeOrgIds(checked.map((key) => Number(key)));
|
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
|
||||||
}}
|
{
|
||||||
defaultExpandAll
|
title: "操作",
|
||||||
/>
|
key: "action",
|
||||||
</div>
|
width: 80,
|
||||||
) : (
|
render: (_: unknown, user: SysUser) => (
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
|
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
|
||||||
)}
|
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
|
||||||
</div>
|
</Popconfirm>
|
||||||
</Tabs.TabPane>
|
)
|
||||||
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
|
}
|
||||||
<div className="role-detail-pane">
|
]}
|
||||||
<div className="role-members-toolbar">
|
/>
|
||||||
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
|
</div>
|
||||||
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
|
</Tabs.TabPane>
|
||||||
</div>
|
</Tabs>
|
||||||
<Table
|
</Card>
|
||||||
rowKey="userId"
|
) : (
|
||||||
size="small"
|
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
|
||||||
loading={loadingUsers}
|
)}
|
||||||
dataSource={roleUsers}
|
</Col>
|
||||||
pagination={{ ...getStandardPagination(roleUsers.length, 1, 10, undefined, { size: "small", showSizeChanger: false }), current: undefined }}
|
</Row>
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
title: "用户信息",
|
|
||||||
render: (_: unknown, user: SysUser) => (
|
|
||||||
<Space>
|
|
||||||
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 500 }}>{user.displayName}</div>
|
|
||||||
<div style={{ fontSize: 11, color: "#bfbfbf" }}>@{user.username}</div>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
|
|
||||||
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
key: "action",
|
|
||||||
width: 80,
|
|
||||||
render: (_: unknown, user: SysUser) => (
|
|
||||||
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
|
|
||||||
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
|
|
||||||
</Popconfirm>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
|
|
||||||
)}
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
|
@ -633,6 +636,6 @@ export default function Roles() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types";
|
import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types";
|
||||||
|
|
@ -379,54 +380,120 @@ export default function Users() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page users-page">
|
<PageContainer
|
||||||
<PageHeader title={t("users.title")} subtitle={t("users.subtitle")} />
|
title={t("users.title")}
|
||||||
|
subtitle={t("users.subtitle")}
|
||||||
<Card className="users-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
headerExtra={
|
||||||
<div className="users-table-toolbar">
|
can("sys:user:create") && (
|
||||||
<Space size="middle" wrap className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||||
<Space size="middle" wrap className="app-page__toolbar">
|
{t("common.create")}
|
||||||
{isPlatformMode && <Select placeholder={t("users.tenantFilter")} style={{ width: 200 }} allowClear value={filterTenantId} onChange={setFilterTenantId} options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={<ShopOutlined aria-hidden="true" />} />}
|
</Button>
|
||||||
<Input placeholder={t("users.searchPlaceholder")} prefix={<SearchOutlined aria-hidden="true" />} className="users-search-input" style={{ width: 300 }} value={searchText} onChange={(event) => { setSearchText(event.target.value); setCurrent(1); }} allowClear aria-label={t("common.search")} />
|
)
|
||||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t("common.search")}</Button>
|
}
|
||||||
<Button onClick={handleResetSearch}>{t("common.reset")}</Button>
|
toolbar={
|
||||||
</Space>
|
<>
|
||||||
{can("sys:user:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>}
|
<Space size="middle" wrap>
|
||||||
|
{isPlatformMode && (
|
||||||
|
<Select
|
||||||
|
placeholder={t("users.tenantFilter")}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
allowClear
|
||||||
|
value={filterTenantId}
|
||||||
|
onChange={setFilterTenantId}
|
||||||
|
options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))}
|
||||||
|
suffixIcon={<ShopOutlined aria-hidden="true" />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Input
|
||||||
|
placeholder={t("users.searchPlaceholder")}
|
||||||
|
prefix={<SearchOutlined aria-hidden="true" />}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(event) => {
|
||||||
|
setSearchText(event.target.value);
|
||||||
|
setCurrent(1);
|
||||||
|
}}
|
||||||
|
allowClear
|
||||||
|
aria-label={t("common.search")}
|
||||||
|
/>
|
||||||
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>
|
||||||
|
{t("common.search")}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleResetSearch}>{t("common.reset")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</>
|
||||||
</Card>
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
rowKey="userId"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={filteredData}
|
||||||
|
loading={loading}
|
||||||
|
size="middle"
|
||||||
|
scroll={{ y: "calc(100vh - 380px)" }}
|
||||||
|
pagination={getStandardPagination(filteredData.length, current, pageSize, (page, size) => {
|
||||||
|
setCurrent(page);
|
||||||
|
setPageSize(size);
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
<Drawer
|
||||||
<Table
|
title={
|
||||||
rowKey="userId"
|
<div className="user-drawer-title">
|
||||||
columns={columns}
|
<UserOutlined className="mr-2" aria-hidden="true" />
|
||||||
dataSource={filteredData}
|
{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}
|
||||||
loading={loading}
|
</div>
|
||||||
size="middle"
|
}
|
||||||
scroll={{ y: "calc(100vh - 380px)" }}
|
open={drawerOpen}
|
||||||
pagination={getStandardPagination(filteredData.length, current, pageSize, (page, size) => {
|
onClose={() => setDrawerOpen(false)}
|
||||||
setCurrent(page);
|
width={520}
|
||||||
setPageSize(size);
|
destroyOnClose
|
||||||
})}
|
footer={
|
||||||
/>
|
<div className="app-page__drawer-footer">
|
||||||
</Card>
|
<Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button>
|
||||||
|
<Button type="primary" loading={saving} onClick={submit}>
|
||||||
<Drawer title={<div className="user-drawer-title"><UserOutlined className="mr-2" aria-hidden="true" />{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
{t("common.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Form form={form} layout="vertical" className="user-form">
|
<Form form={form} layout="vertical" className="user-form">
|
||||||
<Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title>
|
<Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}><Form.Item label={t("users.username")} name="username" rules={[{ required: true, message: t("users.username") }, { pattern: LOGIN_NAME_PATTERN, message: t("usersExt.usernameFormatTip", { defaultValue: "登录名只能输入数字、小写英文、@ 和 _" }) }]} getValueFromEvent={(event) => sanitizeLoginName(event?.target?.value)} extra={t("usersExt.usernameFormatTip", { defaultValue: "登录名只能输入数字、小写英文、@ 和 _" })}><Input placeholder={t("usersExt.usernamePlaceholder", { defaultValue: "仅支持 a-z、0-9、@、_" })} disabled={!!editing} className="tabular-nums" /></Form.Item></Col>
|
<Col span={12}>
|
||||||
<Col span={12}><Form.Item label={t("users.displayName")} name="displayName" rules={[{ required: true, message: t("users.displayName") }]}><Input placeholder={t("users.displayName")} /></Form.Item></Col>
|
<Form.Item label={t("users.username")} name="username" rules={[{ required: true, message: t("users.username") }, { pattern: LOGIN_NAME_PATTERN, message: t("usersExt.usernameFormatTip", { defaultValue: "登录名只能输入数字、小写英文、@ 和 _" }) }]} getValueFromEvent={(event) => sanitizeLoginName(event?.target?.value)} extra={t("usersExt.usernameFormatTip", { defaultValue: "登录名只能输入数字、小写英文、@ 和 _" })}>
|
||||||
|
<Input placeholder={t("usersExt.usernamePlaceholder", { defaultValue: "仅支持 a-z、0-9、@、_" })} disabled={!!editing} className="tabular-nums" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label={t("users.displayName")} name="displayName" rules={[{ required: true, message: t("users.displayName") }]}>
|
||||||
|
<Input placeholder={t("users.displayName")} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}><Form.Item label={t("users.email")} name="email"><Input placeholder={t("usersExt.emailPlaceholder")} className="tabular-nums" /></Form.Item></Col>
|
<Col span={12}>
|
||||||
<Col span={12}><Form.Item label={t("users.phone")} name="phone"><Input placeholder={t("users.phone")} className="tabular-nums" /></Form.Item></Col>
|
<Form.Item label={t("users.email")} name="email">
|
||||||
|
<Input placeholder={t("usersExt.emailPlaceholder")} className="tabular-nums" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label={t("users.phone")} name="phone">
|
||||||
|
<Input placeholder={t("users.phone")} className="tabular-nums" />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl"><Input placeholder={t("profile.avatarUrlPlaceholder")} /></Form.Item>
|
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl">
|
||||||
|
<Input placeholder={t("profile.avatarUrlPlaceholder")} />
|
||||||
|
</Form.Item>
|
||||||
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
|
<Upload accept="image/*" showUploadList={false} beforeUpload={handleAvatarUpload}>
|
||||||
<Button icon={<UploadOutlined />} loading={avatarUploading} style={{ marginBottom: 16 }}>{t("profile.uploadAvatar")}</Button>
|
<Button icon={<UploadOutlined />} loading={avatarUploading} style={{ marginBottom: 16 }}>
|
||||||
|
{t("profile.uploadAvatar")}
|
||||||
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
<Form.Item label={t("users.password")} name="password" rules={[{ required: !editing, message: t("users.password") }]}><Input.Password placeholder={editing ? t("usersExt.passwordKeepPlaceholder") : t("usersExt.passwordInitPlaceholder")} autoComplete="new-password" /></Form.Item>
|
<Form.Item label={t("users.password")} name="password" rules={[{ required: !editing, message: t("users.password") }]}>
|
||||||
|
<Input.Password placeholder={editing ? t("usersExt.passwordKeepPlaceholder") : t("usersExt.passwordInitPlaceholder")} autoComplete="new-password" />
|
||||||
|
</Form.Item>
|
||||||
{passwordValue && (
|
{passwordValue && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("usersExt.confirmPassword")}
|
label={t("usersExt.confirmPassword")}
|
||||||
|
|
@ -450,11 +517,27 @@ export default function Users() {
|
||||||
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
|
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
<Form.Item label={t("users.roles")} name="roleIds" rules={[{ required: true, message: t("users.roles") }]}><Select mode="multiple" placeholder={t("users.roles")} options={roleOptions} optionFilterProp={isPlatformMode ? "searchText" : "label"} /></Form.Item>
|
<Form.Item label={t("users.roles")} name="roleIds" rules={[{ required: true, message: t("users.roles") }]}>
|
||||||
{!isPlatformMode && <Form.Item label={t("users.orgNode")} name="orgId"><TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} /></Form.Item>}
|
<Select mode="multiple" placeholder={t("users.roles")} options={roleOptions} optionFilterProp={isPlatformMode ? "searchText" : "label"} />
|
||||||
|
</Form.Item>
|
||||||
|
{!isPlatformMode && (
|
||||||
|
<Form.Item label={t("users.orgNode")} name="orgId">
|
||||||
|
<TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}><Form.Item label={t("common.status")} name="status" initialValue={1}><Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} /></Form.Item></Col>
|
<Col span={12}>
|
||||||
{isPlatformMode && <Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch /></Form.Item></Col>}
|
<Form.Item label={t("common.status")} name="status" initialValue={1}>
|
||||||
|
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
{isPlatformMode && (
|
||||||
|
<Col span={12}>
|
||||||
|
<Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked">
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
{isPlatformMode && (
|
{isPlatformMode && (
|
||||||
<>
|
<>
|
||||||
|
|
@ -476,7 +559,9 @@ export default function Users() {
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>{t("usersExt.addMembership")}</Button>
|
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
|
||||||
|
{t("usersExt.addMembership")}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|
@ -484,6 +569,6 @@ export default function Users() {
|
||||||
)}
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons";
|
import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api";
|
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import type { SysPermission, SysRole } from "@/types";
|
import type { SysPermission, SysRole } from "@/types";
|
||||||
|
|
||||||
|
|
@ -174,13 +175,10 @@ export default function RolePermissionBinding() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader
|
title={t("rolePerm.title")}
|
||||||
title={t("rolePerm.title")}
|
subtitle={t("rolePerm.subtitle")}
|
||||||
subtitle={t("rolePerm.subtitle")}
|
headerExtra={
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="app-page__page-actions">
|
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined aria-hidden="true" />}
|
icon={<SaveOutlined aria-hidden="true" />}
|
||||||
|
|
@ -190,9 +188,9 @@ export default function RolePermissionBinding() {
|
||||||
>
|
>
|
||||||
{saving ? t("common.loading") : t("common.save")}
|
{saving ? t("common.loading") : t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<Row gutter={24} className="app-page__split" style={{ height: "calc(100vh - 180px)" }}>
|
<Row gutter={24} style={{ height: "calc(100vh - 180px)" }}>
|
||||||
<Col xs={24} lg={10} style={{ height: "100%" }}>
|
<Col xs={24} lg={10} style={{ height: "100%" }}>
|
||||||
<Card title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t("rolePerm.roleList")}</span></Space>} className="app-page__panel-card full-height-card">
|
<Card title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t("rolePerm.roleList")}</span></Space>} className="app-page__panel-card full-height-card">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
@ -277,7 +275,7 @@ export default function RolePermissionBinding() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { listRoles, listUserRoles, listUsers, saveUserRoles } from "@/api";
|
||||||
import { SaveOutlined, SearchOutlined, TeamOutlined, UserOutlined } from "@ant-design/icons";
|
import { SaveOutlined, SearchOutlined, TeamOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import type { SysRole, SysUser } from "@/types";
|
import type { SysRole, SysUser } from "@/types";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -100,19 +101,16 @@ export default function UserRoleBinding() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader
|
title={t("userRole.title")}
|
||||||
title={t("userRole.title")}
|
subtitle={t("userRole.subtitle")}
|
||||||
subtitle={t("userRole.subtitle")}
|
headerExtra={
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="app-page__page-actions">
|
|
||||||
<Button type="primary" icon={<SaveOutlined aria-hidden="true" />} onClick={handleSave} loading={saving} disabled={!selectedUserId}>
|
<Button type="primary" icon={<SaveOutlined aria-hidden="true" />} onClick={handleSave} loading={saving} disabled={!selectedUserId}>
|
||||||
{saving ? t("common.loading") : t("common.save")}
|
{saving ? t("common.loading") : t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<Row gutter={24} className="app-page__split" style={{ height: "calc(100vh - 180px)" }}>
|
<Row gutter={24} style={{ height: "calc(100vh - 180px)" }}>
|
||||||
<Col xs={24} lg={12} style={{ height: "100%" }}>
|
<Col xs={24} lg={12} style={{ height: "100%" }}>
|
||||||
<Card title={<Space><UserOutlined aria-hidden="true" /><span>{t("userRole.userList")}</span></Space>} className="app-page__panel-card full-height-card">
|
<Card title={<Space><UserOutlined aria-hidden="true" /><span>{t("userRole.userList")}</span></Space>} className="app-page__panel-card full-height-card">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
@ -199,6 +197,6 @@ export default function UserRoleBinding() {
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AutoComplete, Button, Card, Col, Divider, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Tooltip, Typography, App } from 'antd';
|
import { AutoComplete, Button, Card, Col, Divider, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tabs, Tag, Tooltip, Typography, App } from 'antd';
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
|
@ -406,58 +407,56 @@ const AiModels: React.FC = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<Card
|
title="AI 模型配置"
|
||||||
className="app-page__content-card flex-1 flex flex-col overflow-hidden"
|
subtitle="管理ASR语音识别和LLM大语言模型"
|
||||||
title="AI 模型配置"
|
headerExtra={
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
||||||
extra={
|
新增模型
|
||||||
<Space>
|
</Button>
|
||||||
<Input
|
}
|
||||||
placeholder="搜索模型名称"
|
toolbar={
|
||||||
prefix={<SearchOutlined />}
|
<Input
|
||||||
allowClear
|
placeholder="搜索模型名称"
|
||||||
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
|
prefix={<SearchOutlined />}
|
||||||
style={{ width: 220 }}
|
allowClear
|
||||||
/>
|
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
style={{ width: 220 }}
|
||||||
新增模型
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div style={{ padding: "16px 24px 0", flexShrink: 0 }}>
|
|
||||||
<Tabs
|
|
||||||
activeKey={activeType}
|
|
||||||
onChange={(key) => {
|
|
||||||
setActiveType(key as ModelType);
|
|
||||||
setCurrent(1);
|
|
||||||
}}
|
|
||||||
items={[
|
|
||||||
{ key: "ASR", label: "ASR 模型" },
|
|
||||||
{ key: "LLM", label: "LLM 模型" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
|
||||||
<Table
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
loading={loading}
|
|
||||||
scroll={{ x: "max-content" }}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AppPagination
|
|
||||||
current={current}
|
|
||||||
pageSize={size}
|
|
||||||
total={total}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
setCurrent(page);
|
|
||||||
setSize(pageSize);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeType}
|
||||||
|
onChange={(key) => {
|
||||||
|
setActiveType(key as ModelType);
|
||||||
|
setCurrent(1);
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{ key: "ASR", label: "ASR 模型" },
|
||||||
|
{ key: "LLM", label: "LLM 模型" },
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto" }}>
|
||||||
|
<Table
|
||||||
|
rowKey="id"
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data}
|
||||||
|
loading={loading}
|
||||||
|
scroll={{ x: "max-content" }}
|
||||||
|
pagination={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<AppPagination
|
||||||
|
current={current}
|
||||||
|
pageSize={size}
|
||||||
|
total={total}
|
||||||
|
onChange={(page, pageSize) => {
|
||||||
|
setCurrent(page);
|
||||||
|
setSize(pageSize);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
@ -662,7 +661,7 @@ const AiModels: React.FC = () => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { ColumnsType } from "antd/es/table";
|
||||||
import { CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, RocketOutlined, SearchOutlined, UploadOutlined, WindowsOutlined } from "@ant-design/icons";
|
import { CheckCircleOutlined, CloudUploadOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, LaptopOutlined, MobileOutlined, PlusOutlined, ReloadOutlined, RocketOutlined, SearchOutlined, UploadOutlined, WindowsOutlined } from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import AppPagination from "@/components/shared/AppPagination";
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client";
|
import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client";
|
||||||
import { fetchDictItemsByTypeCode } from "@/api/dict";
|
import { fetchDictItemsByTypeCode } from "@/api/dict";
|
||||||
|
|
@ -380,18 +381,16 @@ export default function ClientManagement() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader
|
title="客户端管理"
|
||||||
title="客户端管理"
|
subtitle="发布平台由数据字典 client_platform 驱动,并按父子分组展示。"
|
||||||
subtitle="发布平台由数据字典 client_platform 驱动,并按父子分组展示。"
|
headerExtra={
|
||||||
extra={
|
<Space size={12}>
|
||||||
<Space size={12}>
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading || groupLoading || platformLoading} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>刷新</Button>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading || groupLoading || platformLoading} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>刷新</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>发布新版</Button>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>发布新版</Button>
|
</Space>
|
||||||
</Space>
|
}
|
||||||
}
|
>
|
||||||
/>
|
|
||||||
|
|
||||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<Card bordered={false} style={{ borderRadius: 16, border: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface-strong)', backdropFilter: 'blur(12px)', boxShadow: '0 4px 20px rgba(0,0,0,0.02)' }} styles={{ body: { padding: '20px 24px' } }}>
|
<Card bordered={false} style={{ borderRadius: 16, border: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface-strong)', backdropFilter: 'blur(12px)', boxShadow: '0 4px 20px rgba(0,0,0,0.02)' }} styles={{ body: { padding: '20px 24px' } }}>
|
||||||
|
|
@ -525,6 +524,6 @@ export default function ClientManagement() {
|
||||||
</Space>
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import type { ColumnsType } from "antd/es/table";
|
||||||
import { AppstoreOutlined, DeleteOutlined, EditOutlined, GlobalOutlined, LinkOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
|
import { AppstoreOutlined, DeleteOutlined, EditOutlined, GlobalOutlined, LinkOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import AppPagination from "@/components/shared/AppPagination";
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
import { createExternalApp, deleteExternalApp, listExternalApps, type ExternalAppDTO, type ExternalAppVO, updateExternalApp, uploadExternalAppApk, uploadExternalAppIcon } from "@/api/business/externalApp";
|
import { createExternalApp, deleteExternalApp, listExternalApps, type ExternalAppDTO, type ExternalAppVO, updateExternalApp, uploadExternalAppApk, uploadExternalAppIcon } from "@/api/business/externalApp";
|
||||||
|
|
||||||
|
|
@ -290,18 +291,16 @@ export default function ExternalAppManagement() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader
|
title="外部应用管理"
|
||||||
title="外部应用管理"
|
subtitle="统一维护首页九宫格与抽屉入口中的原生应用、Web 服务和应用图标资源。"
|
||||||
subtitle="统一维护首页九宫格与抽屉入口中的原生应用、Web 服务和应用图标资源。"
|
headerExtra={
|
||||||
extra={
|
<Space size={12}>
|
||||||
<Space size={12}>
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>刷新</Button>
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>刷新</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>新增应用</Button>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>新增应用</Button>
|
</Space>
|
||||||
</Space>
|
}
|
||||||
}
|
>
|
||||||
/>
|
|
||||||
|
|
||||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<Card bordered={false} style={{ borderRadius: 16, border: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface-strong)', backdropFilter: 'blur(12px)', boxShadow: '0 4px 20px rgba(0,0,0,0.02)' }} styles={{ body: { padding: '20px 24px' } }}>
|
<Card bordered={false} style={{ borderRadius: 16, border: '1px solid var(--app-border-color)', background: 'var(--app-bg-surface-strong)', backdropFilter: 'blur(12px)', boxShadow: '0 4px 20px rgba(0,0,0,0.02)' }} styles={{ body: { padding: '20px 24px' } }}>
|
||||||
|
|
@ -427,6 +426,6 @@ export default function ExternalAppManagement() {
|
||||||
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked"><Switch /></Form.Item>
|
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked"><Switch /></Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,13 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
Typography,
|
Typography,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
SearchOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDict } from "../../hooks/useDict";
|
import { useDict } from "../../hooks/useDict";
|
||||||
|
|
@ -42,7 +45,6 @@ import {
|
||||||
type HotWordGroupVO,
|
type HotWordGroupVO,
|
||||||
} from "../../api/business/hotwordGroup";
|
} from "../../api/business/hotwordGroup";
|
||||||
import AppPagination from "../../components/shared/AppPagination";
|
import AppPagination from "../../components/shared/AppPagination";
|
||||||
import ListActionBar from "../../components/shared/ListActionBar/ListActionBar";
|
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -105,8 +107,6 @@ const HotWords: React.FC = () => {
|
||||||
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
||||||
const [selectedGroupName, setSelectedGroupName] = useState<string | undefined>(undefined);
|
const [selectedGroupName, setSelectedGroupName] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const [filterVisible, setFilterVisible] = useState(false);
|
|
||||||
|
|
||||||
const groupNameMap = useMemo(
|
const groupNameMap = useMemo(
|
||||||
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
||||||
[groupOptions]
|
[groupOptions]
|
||||||
|
|
@ -364,44 +364,24 @@ const HotWords: React.FC = () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filterContent = (
|
|
||||||
<div style={{ width: 200 }}>
|
|
||||||
<div style={{ marginBottom: 8 }}>分类筛选</div>
|
|
||||||
<Select
|
|
||||||
placeholder="按分类筛选"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
allowClear
|
|
||||||
value={searchCategory}
|
|
||||||
onChange={(value) => {
|
|
||||||
setSearchCategory(value);
|
|
||||||
setCurrent(1);
|
|
||||||
setFilterVisible(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{categories.map((item) => (
|
|
||||||
<Option key={item.itemValue} value={item.itemValue}>
|
|
||||||
{item.itemLabel}
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page" style={{ padding: '16px', display: 'flex', flexDirection: 'column', height: '100%' }}>
|
<PageContainer
|
||||||
|
title="热词管理"
|
||||||
|
subtitle="管理ASR识别引擎的热词库,提升特定场景下的识别准确率"
|
||||||
|
>
|
||||||
<div style={{ display: 'flex', gap: '16px', flex: 1, minHeight: 0 }}>
|
<div style={{ display: 'flex', gap: '16px', flex: 1, minHeight: 0 }}>
|
||||||
{/* Left Panel: Hotword Groups */}
|
{/* Left Panel: Hotword Groups */}
|
||||||
<Card
|
<Card
|
||||||
className="shadow-sm"
|
className="app-page__content-card"
|
||||||
title="热词组"
|
title="热词组"
|
||||||
style={{ width: '25%', display: 'flex', flexDirection: 'column', minWidth: 280 }}
|
style={{ width: '20%', display: 'flex', flexDirection: 'column', minWidth: 240, maxWidth: 300 }}
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", minHeight: 0 } }}
|
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", minHeight: 0 } }}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={() => openGroupEditor()}
|
onClick={() => openGroupEditor()}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
@ -519,55 +499,50 @@ const HotWords: React.FC = () => {
|
||||||
|
|
||||||
{/* Right Panel: Hotwords */}
|
{/* Right Panel: Hotwords */}
|
||||||
<Card
|
<Card
|
||||||
className="shadow-sm"
|
className="app-page__content-card"
|
||||||
title={hotWordGroupTitle}
|
title={hotWordGroupTitle}
|
||||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}
|
extra={
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
<Space wrap size="small">
|
||||||
>
|
<Input
|
||||||
<div style={{ padding: '16px 24px 0' }}>
|
placeholder="搜索热词原文"
|
||||||
<ListActionBar
|
prefix={<SearchOutlined />}
|
||||||
actions={[
|
allowClear
|
||||||
{
|
value={searchWord}
|
||||||
key: 'add',
|
onChange={(e) => setSearchWord(e.target.value)}
|
||||||
label: '新增热词',
|
onPressEnter={() => { setCurrent(1); void fetchData(); }}
|
||||||
type: 'primary',
|
style={{ width: 200 }}
|
||||||
icon: <PlusOutlined />,
|
/>
|
||||||
onClick: () => handleOpenModal()
|
<Select
|
||||||
}
|
placeholder="筛选分类"
|
||||||
]}
|
allowClear
|
||||||
search={{
|
value={searchCategory || undefined}
|
||||||
placeholder: '搜索热词原文',
|
onChange={(value) => {
|
||||||
value: searchWord,
|
setSearchCategory(value as string);
|
||||||
onChange: (val) => setSearchWord(val),
|
|
||||||
onSearch: () => {
|
|
||||||
setCurrent(1);
|
setCurrent(1);
|
||||||
void fetchData();
|
void fetchData();
|
||||||
}
|
}}
|
||||||
}}
|
style={{ width: 120 }}
|
||||||
filter={{
|
options={categories.map((c) => ({ label: c.itemLabel, value: c.itemValue }))}
|
||||||
content: filterContent,
|
/>
|
||||||
title: '高级筛选',
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||||||
visible: filterVisible,
|
新增热词
|
||||||
onVisibleChange: setFilterVisible,
|
</Button>
|
||||||
isActive: !!searchCategory,
|
<Button icon={<ReloadOutlined />} onClick={() => { setCurrent(1); void fetchData(); void loadGroupPage(); }}>
|
||||||
selectedLabel: searchCategory ? categories.find(c => c.itemValue === searchCategory)?.itemLabel : '筛选'
|
刷新
|
||||||
}}
|
</Button>
|
||||||
showRefresh
|
</Space>
|
||||||
onRefresh={() => {
|
}
|
||||||
setCurrent(1);
|
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}
|
||||||
void fetchData();
|
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||||
void loadGroupPage();
|
>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "16px 24px 0" }}>
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "16px 24px 0" }}>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
scroll={{ x: "max-content" }}
|
scroll={{ x: "max-content" }}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AppPagination
|
<AppPagination
|
||||||
|
|
@ -668,7 +643,7 @@ const HotWords: React.FC = () => {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import { listUsers } from '../../api';
|
||||||
import { useDict } from '../../hooks/useDict';
|
import { useDict } from '../../hooks/useDict';
|
||||||
import { SysUser } from '../../types';
|
import { SysUser } from '../../types';
|
||||||
import PageHeader from '../../components/shared/PageHeader';
|
import PageHeader from '../../components/shared/PageHeader';
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import {
|
||||||
import {MeetingCreateDrawer, MeetingCreateType} from '../../components/business/MeetingCreateDrawer';
|
import {MeetingCreateDrawer, MeetingCreateType} from '../../components/business/MeetingCreateDrawer';
|
||||||
import AppPagination from '../../components/shared/AppPagination';
|
import AppPagination from '../../components/shared/AppPagination';
|
||||||
import {usePermission} from '../../hooks/usePermission';
|
import {usePermission} from '../../hooks/usePermission';
|
||||||
|
import PageContainer from '@/components/shared/PageContainer';
|
||||||
import {SysUser} from '../../types';
|
import {SysUser} from '../../types';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
@ -565,59 +566,73 @@ const Meetings: React.FC = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<Card
|
title="会议中心"
|
||||||
className="app-page__content-card shadow-sm"
|
subtitle="管理会议记录与分析"
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
headerExtra={
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
<Space size={16} wrap>
|
||||||
title={
|
<Radio.Group value={displayMode} onChange={e => handleDisplayModeChange(e.target.value)} buttonStyle="solid">
|
||||||
<Space size={12}>
|
<Radio.Button value="card"><AppstoreOutlined /></Radio.Button>
|
||||||
<div style={{ width: 8, height: 24, background: '#1890ff', borderRadius: 4 }}></div>
|
<Radio.Button value="list"><UnorderedListOutlined /></Radio.Button>
|
||||||
<Title level={4} style={{ margin: 0 }}>会议中心</Title>
|
</Radio.Group>
|
||||||
</Space>
|
<Button
|
||||||
}
|
type="primary"
|
||||||
extra={
|
icon={<PlusOutlined />}
|
||||||
<Space size={16} wrap>
|
onClick={() => {
|
||||||
<Radio.Group value={displayMode} onChange={e => handleDisplayModeChange(e.target.value)} buttonStyle="solid">
|
|
||||||
<Radio.Button value="card"><AppstoreOutlined /></Radio.Button>
|
|
||||||
<Radio.Button value="list"><UnorderedListOutlined /></Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
<Radio.Group value={viewType} onChange={e => { setViewType(e.target.value); setCurrent(1); }} buttonStyle="solid">
|
|
||||||
<Radio.Button value="all">全部</Radio.Button><Radio.Button value="created">我发起</Radio.Button><Radio.Button value="involved">我参与</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
<Input placeholder="搜索会议标题" prefix={<SearchOutlined style={{ color: '#bfbfbf' }} />} allowClear onPressEnter={(e) => { setSearchTitle((e.target as any).value); setCurrent(1); }} style={{ width: 220, borderRadius: 8 }} />
|
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => {
|
|
||||||
setCreateDrawerType('upload');
|
setCreateDrawerType('upload');
|
||||||
setCreateDrawerVisible(true);
|
setCreateDrawerVisible(true);
|
||||||
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>创建会议</Button>
|
}}
|
||||||
</Space>
|
>
|
||||||
}
|
创建会议
|
||||||
>
|
</Button>
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflowX: "hidden", overflowY: "auto", padding: "24px" }}>
|
</Space>
|
||||||
{displayMode === 'card' ? (
|
}
|
||||||
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
|
toolbar={
|
||||||
<List grid={{ gutter: [24, 24], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }} dataSource={data} renderItem={(item) => {
|
<>
|
||||||
const config = statusConfig[item.displayStatus ?? item.status] || statusConfig[0];
|
<Radio.Group value={viewType} onChange={e => { setViewType(e.target.value); setCurrent(1); }} buttonStyle="solid">
|
||||||
return <MeetingCardItem item={item} config={config} fetchData={fetchData} t={t} onEditParticipants={openEditParticipants} onOpenMeeting={handleOpenMeeting} />;
|
<Radio.Button value="all">全部</Radio.Button>
|
||||||
}} locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }} />
|
<Radio.Button value="created">我发起</Radio.Button>
|
||||||
</Skeleton>
|
<Radio.Button value="involved">我参与</Radio.Button>
|
||||||
) : (
|
</Radio.Group>
|
||||||
<Table
|
<Input
|
||||||
columns={tableColumns}
|
placeholder="搜索会议标题"
|
||||||
dataSource={data}
|
prefix={<SearchOutlined />}
|
||||||
rowKey="id"
|
allowClear
|
||||||
loading={loading}
|
onPressEnter={(e) => { setSearchTitle((e.target as any).value); setCurrent(1); }}
|
||||||
pagination={false}
|
style={{ width: 220 }}
|
||||||
onRow={(record) => ({
|
/>
|
||||||
onClick: () => handleOpenMeeting(record),
|
</>
|
||||||
style: { cursor: 'pointer' }
|
}
|
||||||
})}
|
>
|
||||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: '16px 24px', flex: 1 } }}>
|
||||||
/>
|
{displayMode === 'card' ? (
|
||||||
)}
|
<Skeleton loading={loading} active paragraph={{ rows: 10 }}>
|
||||||
</div>
|
<List
|
||||||
|
grid={{ gutter: [24, 24], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
|
||||||
|
dataSource={data}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const config = statusConfig[item.displayStatus ?? item.status] || statusConfig[0];
|
||||||
|
return <MeetingCardItem item={item} config={config} fetchData={fetchData} t={t} onEditParticipants={openEditParticipants} onOpenMeeting={handleOpenMeeting} />;
|
||||||
|
}}
|
||||||
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||||
|
/>
|
||||||
|
</Skeleton>
|
||||||
|
) : (
|
||||||
|
<Table
|
||||||
|
columns={tableColumns}
|
||||||
|
dataSource={data}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
pagination={false}
|
||||||
|
onRow={(record) => ({
|
||||||
|
onClick: () => handleOpenMeeting(record),
|
||||||
|
style: { cursor: 'pointer' }
|
||||||
|
})}
|
||||||
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<MeetingCreateDrawer
|
<MeetingCreateDrawer
|
||||||
|
|
@ -644,10 +659,7 @@ const Meetings: React.FC = () => {
|
||||||
width={500}
|
width={500}
|
||||||
>
|
>
|
||||||
<Form form={participantsEditForm} layout="vertical">
|
<Form form={participantsEditForm} layout="vertical">
|
||||||
<Form.Item
|
<Form.Item name="participantIds" label="参会人员">
|
||||||
name="participantIds"
|
|
||||||
label="参会人员"
|
|
||||||
>
|
|
||||||
<Select mode="multiple" placeholder="请选择参会人" showSearch optionFilterProp="children">
|
<Select mode="multiple" placeholder="请选择参会人" showSearch optionFilterProp="children">
|
||||||
{userList.map(u => (
|
{userList.map(u => (
|
||||||
<Option key={u.userId} value={u.userId}>
|
<Option key={u.userId} value={u.userId}>
|
||||||
|
|
@ -673,7 +685,7 @@ const Meetings: React.FC = () => {
|
||||||
.card-actions { opacity: 0.6; transition: opacity 0.3s; }
|
.card-actions { opacity: 0.6; transition: opacity 0.3s; }
|
||||||
.meeting-card:hover .card-actions { opacity: 1; }
|
.meeting-card:hover .card-actions { opacity: 1; }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, StarFilled } from '@ant-design/icons';
|
import { CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, StarFilled } from '@ant-design/icons';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
@ -331,33 +332,32 @@ const PromptTemplates: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '32px', background: 'var(--app-bg-page)', height: 'calc(100vh - 64px)', overflow: 'hidden' }}>
|
<PageContainer
|
||||||
<div style={{ maxWidth: 1400, margin: '0 auto', height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
title="提示词模板"
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32 }}>
|
subtitle="管理AI会议总结的提示词模板库"
|
||||||
<Title level={3} style={{ margin: 0 }}>提示词模板</Title>
|
headerExtra={
|
||||||
<Button type="primary" icon={<PlusOutlined />} size="large" onClick={() => handleOpenDrawer()} style={{ borderRadius: 6 }}>
|
<Button type="primary" icon={<PlusOutlined />} size="large" onClick={() => handleOpenDrawer()} style={{ borderRadius: 6 }}>
|
||||||
新增模板
|
新增模板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
toolbar={
|
||||||
<Card variant="borderless" style={{ borderRadius: 12, marginBottom: 32, background: 'var(--app-bg-card)', border: '1px solid var(--app-border-color)', boxShadow: 'var(--app-shadow)', backdropFilter: 'blur(16px)' }} styles={{ body: { padding: '20px 24px' } }}>
|
<Form form={searchForm} layout="inline" onFinish={() => void fetchData()}>
|
||||||
<Form form={searchForm} layout="inline" onFinish={() => void fetchData()}>
|
<Form.Item name="name" label="模板名称"><Input placeholder="请输入..." style={{ width: 180 }} /></Form.Item>
|
||||||
<Form.Item name="name" label="模板名称"><Input placeholder="请输入..." style={{ width: 180 }} /></Form.Item>
|
<Form.Item name="category" label="分类">
|
||||||
<Form.Item name="category" label="分类">
|
<Select placeholder="选择分类" style={{ width: 160 }} allowClear>
|
||||||
<Select placeholder="选择分类" style={{ width: 160 }} allowClear>
|
{categories.map((c) => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
||||||
{categories.map((c) => <Option key={c.itemValue} value={c.itemValue}>{c.itemLabel}</Option>)}
|
</Select>
|
||||||
</Select>
|
</Form.Item>
|
||||||
</Form.Item>
|
<Form.Item>
|
||||||
<Form.Item>
|
<Space>
|
||||||
<Space>
|
<Button type="primary" htmlType="submit">查询数据</Button>
|
||||||
<Button type="primary" htmlType="submit">查询数据</Button>
|
<Button onClick={() => { searchForm.resetFields(); void fetchData(); }}>重置</Button>
|
||||||
<Button onClick={() => { searchForm.resetFields(); void fetchData(); }}>重置</Button>
|
</Space>
|
||||||
</Space>
|
</Form.Item>
|
||||||
</Form.Item>
|
</Form>
|
||||||
</Form>
|
}
|
||||||
</Card>
|
>
|
||||||
|
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
||||||
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' } }}>
|
|
||||||
<Skeleton loading={loading} active style={{ height: '100%' }}>
|
<Skeleton loading={loading} active style={{ height: '100%' }}>
|
||||||
{Object.keys(groupedData).length === 0 ? (
|
{Object.keys(groupedData).length === 0 ? (
|
||||||
<div className="app-page__empty-state" style={{ padding: 24 }}>
|
<div className="app-page__empty-state" style={{ padding: 24 }}>
|
||||||
|
|
@ -391,7 +391,6 @@ const PromptTemplates: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={<Title level={4} style={{ margin: 0 }}>{editingId ? '编辑模板' : '创建新模板'}</Title>}
|
title={<Title level={4} style={{ margin: 0 }}>{editingId ? '编辑模板' : '创建新模板'}</Title>}
|
||||||
|
|
@ -506,7 +505,7 @@ const PromptTemplates: React.FC = () => {
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import PageHeader from "../../components/shared/PageHeader";
|
import PageHeader from "../../components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import {
|
import {
|
||||||
appendRealtimeTranscripts,
|
appendRealtimeTranscripts,
|
||||||
completeRealtimeMeeting,
|
completeRealtimeMeeting,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import "./ScreenSaverManagement.css";
|
import "./ScreenSaverManagement.css";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
|
|
@ -26,6 +26,7 @@ import type { ColumnsType } from "antd/es/table";
|
||||||
import {
|
import {
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
PictureOutlined,
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
|
|
@ -36,6 +37,7 @@ import {
|
||||||
UploadOutlined,
|
UploadOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { UploadProps } from "antd";
|
import type { UploadProps } from "antd";
|
||||||
import AppPagination from "@/components/shared/AppPagination";
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
|
|
@ -608,29 +610,47 @@ export default function ScreenSaverManagement() {
|
||||||
|
|
||||||
const columns: ColumnsType<ScreenSaverVO> = [
|
const columns: ColumnsType<ScreenSaverVO> = [
|
||||||
{
|
{
|
||||||
title: "屏保画面",
|
title: "预览",
|
||||||
key: "visual",
|
key: "thumb",
|
||||||
width: 330,
|
width: 90,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<div className="screen-saver-table-visual">
|
<div className="screen-saver-table-thumb">
|
||||||
<div className="screen-saver-table-thumb">
|
{record.imageUrl ? (
|
||||||
{record.imageUrl ? <img src={record.imageUrl} alt={record.name} /> : null}
|
<img src={record.imageUrl} alt={record.name} style={{ maxWidth: 70, maxHeight: 52, objectFit: 'cover', borderRadius: 4 }} />
|
||||||
</div>
|
) : (
|
||||||
<Space direction="vertical" size={3}>
|
<div style={{ width: 70, height: 52, background: '#f5f5f5', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Text strong>{record.name}</Text>
|
<PictureOutlined style={{ color: '#ccc', fontSize: 20 }} />
|
||||||
<Text type="secondary">{record.description || "暂无描述"}</Text>
|
</div>
|
||||||
<Space wrap size={[8, 6]}>
|
)}
|
||||||
<span className="screen-saver-preview-pill">{getImageFormatLabel(record.imageFormat)}</span>
|
|
||||||
<span className="screen-saver-preview-pill">{record.imageWidth || CROP_WIDTH} × {record.imageHeight || CROP_HEIGHT}</span>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "名称与描述",
|
||||||
|
key: "info",
|
||||||
|
width: 160,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space direction="vertical" size={3}>
|
||||||
|
<Text strong style={{ fontSize: 14 }}>{record.name}</Text>
|
||||||
|
<Text type="secondary" ellipsis style={{ fontSize: 12, maxWidth: 150 }}>{record.description || "暂无描述"}</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "图片规格",
|
||||||
|
key: "spec",
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<Tag color="processing">{getImageFormatLabel(record.imageFormat)}</Tag>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>{record.imageWidth || CROP_WIDTH} × {record.imageHeight || CROP_HEIGHT}</Text>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "作用域",
|
title: "作用域",
|
||||||
key: "scope",
|
key: "scope",
|
||||||
width: 220,
|
width: 160,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
{record.scopeType === "USER" ? (
|
{record.scopeType === "USER" ? (
|
||||||
|
|
@ -638,7 +658,7 @@ export default function ScreenSaverManagement() {
|
||||||
) : (
|
) : (
|
||||||
<Tag color="blue" icon={<TeamOutlined />}>平台级</Tag>
|
<Tag color="blue" icon={<TeamOutlined />}>平台级</Tag>
|
||||||
)}
|
)}
|
||||||
<Text type="secondary">
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
{record.scopeType === "USER"
|
{record.scopeType === "USER"
|
||||||
? `归属:${normalizeOwnerLabel(record.ownerUserId ? userMap.get(record.ownerUserId) : undefined)}`
|
? `归属:${normalizeOwnerLabel(record.ownerUserId ? userMap.get(record.ownerUserId) : undefined)}`
|
||||||
: "全平台共用"}
|
: "全平台共用"}
|
||||||
|
|
@ -647,12 +667,12 @@ export default function ScreenSaverManagement() {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "排序与状态",
|
title: "状态",
|
||||||
key: "status",
|
key: "status",
|
||||||
width: 210,
|
width: 120,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space direction="vertical" size={6}>
|
<Space direction="vertical" size={6}>
|
||||||
<Text type="secondary">排序值:{record.sortOrder ?? 0}</Text>
|
<Text type="secondary" style={{ fontSize: 12 }}>排序:{record.sortOrder ?? 0}</Text>
|
||||||
<Switch size="small" checked={record.status === 1} onChange={(checked) => void handleToggleStatus(record, checked)} />
|
<Switch size="small" checked={record.status === 1} onChange={(checked) => void handleToggleStatus(record, checked)} />
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
|
|
@ -660,14 +680,14 @@ export default function ScreenSaverManagement() {
|
||||||
{
|
{
|
||||||
title: "创建信息",
|
title: "创建信息",
|
||||||
key: "creator",
|
key: "creator",
|
||||||
width: 180,
|
width: 160,
|
||||||
render: (_, record) => {
|
render: (_, record) => {
|
||||||
const timeValue = record.updatedAt || record.createdAt;
|
const timeValue = record.updatedAt || record.createdAt;
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size={4}>
|
<Space direction="vertical" size={4}>
|
||||||
<Text>{record.creatorUsername || "-"}</Text>
|
<Text style={{ fontSize: 13 }}>{record.creatorUsername || "-"}</Text>
|
||||||
<Text type="secondary">
|
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||||
{timeValue ? dayjs(timeValue).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
{timeValue ? dayjs(timeValue).format("MM-DD HH:mm") : "-"}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|
@ -701,54 +721,52 @@ export default function ScreenSaverManagement() {
|
||||||
const currentHeight = Form.useWatch("imageHeight", form) || CROP_HEIGHT;
|
const currentHeight = Form.useWatch("imageHeight", form) || CROP_HEIGHT;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page screen-saver-page">
|
<PageContainer
|
||||||
<Card
|
title="屏保管理"
|
||||||
className="app-page__content-card shadow-sm screen-saver-table-card"
|
subtitle="管理平台级和用户级的屏保素材"
|
||||||
style={{ flex: 1, minHeight: 0 }}
|
headerExtra={
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
<Space>
|
||||||
title="屏保管理"
|
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||||
extra={(
|
刷新
|
||||||
<Space wrap>
|
</Button>
|
||||||
<Input
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
placeholder="搜索名称、描述、创建人或归属用户"
|
新增屏保
|
||||||
prefix={<SearchOutlined />}
|
</Button>
|
||||||
allowClear
|
</Space>
|
||||||
style={{ width: 280 }}
|
}
|
||||||
value={searchValue}
|
toolbar={
|
||||||
onChange={(event) => setSearchValue(event.target.value)}
|
<Space wrap>
|
||||||
/>
|
<Input
|
||||||
<Select
|
placeholder="搜索名称、描述、创建人或归属用户"
|
||||||
value={scopeFilter}
|
prefix={<SearchOutlined />}
|
||||||
style={{ width: 140 }}
|
allowClear
|
||||||
onChange={(value) => setScopeFilter(value)}
|
style={{ width: 280 }}
|
||||||
options={[
|
value={searchValue}
|
||||||
{ label: "全部作用域", value: "all" },
|
onChange={(event) => setSearchValue(event.target.value)}
|
||||||
{ label: "平台级", value: "PLATFORM" },
|
/>
|
||||||
{ label: "用户级", value: "USER" },
|
<Select
|
||||||
]}
|
value={scopeFilter}
|
||||||
/>
|
style={{ width: 140 }}
|
||||||
<Select
|
onChange={(value) => setScopeFilter(value)}
|
||||||
value={statusFilter}
|
options={[
|
||||||
style={{ width: 140 }}
|
{ label: "全部作用域", value: "all" },
|
||||||
onChange={(value) => setStatusFilter(value)}
|
{ label: "平台级", value: "PLATFORM" },
|
||||||
options={[
|
{ label: "用户级", value: "USER" },
|
||||||
{ label: "全部状态", value: "all" },
|
]}
|
||||||
{ label: "已启用", value: "enabled" },
|
/>
|
||||||
{ label: "已停用", value: "disabled" },
|
<Select
|
||||||
]}
|
value={statusFilter}
|
||||||
/>
|
style={{ width: 140 }}
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
onChange={(value) => setStatusFilter(value)}
|
||||||
刷新
|
options={[
|
||||||
</Button>
|
{ label: "全部状态", value: "all" },
|
||||||
{/*<Button icon={<SettingOutlined />} onClick={openSettingsModal}>*/}
|
{ label: "已启用", value: "enabled" },
|
||||||
{/* 播放设置({mySettings.displayDurationSec} 秒/张)*/}
|
{ label: "已停用", value: "disabled" },
|
||||||
{/*</Button>*/}
|
]}
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
/>
|
||||||
新增屏保
|
</Space>
|
||||||
</Button>
|
}
|
||||||
</Space>
|
>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="app-page__table-wrap screen-saver-table-wrap">
|
<div className="app-page__table-wrap screen-saver-table-wrap">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|
@ -771,8 +789,6 @@ export default function ScreenSaverManagement() {
|
||||||
setPageSize(nextSize);
|
setPageSize(nextSize);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={editing ? "编辑屏保" : "新增屏保"}
|
title={editing ? "编辑屏保" : "新增屏保"}
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
|
|
@ -855,7 +871,7 @@ export default function ScreenSaverManagement() {
|
||||||
</Space>
|
</Space>
|
||||||
<div className="screen-saver-preview-stage">
|
<div className="screen-saver-preview-stage">
|
||||||
{currentImageUrl ? (
|
{currentImageUrl ? (
|
||||||
<img src={currentImageUrl} alt="屏保预览" />
|
<img src={currentImageUrl} alt="屏保预览" style={{ maxWidth: '100%', maxHeight: 300, objectFit: 'contain', borderRadius: 8 }} />
|
||||||
) : (
|
) : (
|
||||||
<div style={{ height: "100%", display: "grid", placeItems: "center", color: "rgba(235,244,255,.72)" }}>
|
<div style={{ height: "100%", display: "grid", placeItems: "center", color: "rgba(235,244,255,.72)" }}>
|
||||||
<Space direction="vertical" align="center" size={10}>
|
<Space direction="vertical" align="center" size={10}>
|
||||||
|
|
@ -950,6 +966,6 @@ export default function ScreenSaverManagement() {
|
||||||
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
||||||
onConfirm={handleUploadCroppedImage}
|
onConfirm={handleUploadCroppedImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
|
import { Row, Col, Card, Statistic, List, Tag, Typography, Button, Space, Empty, Steps, Progress, Divider } from 'antd';
|
||||||
import {
|
import {
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
|
|
@ -137,8 +138,10 @@ export const Dashboard: React.FC = () => {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px', background: 'var(--app-bg-page)', minHeight: '100%', overflowY: 'auto' }}>
|
<PageContainer
|
||||||
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
title="仪表板"
|
||||||
|
subtitle="系统运行概览与最近任务动态"
|
||||||
|
>
|
||||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||||
{statCards.map((s, idx) => (
|
{statCards.map((s, idx) => (
|
||||||
<Col span={6} key={idx}>
|
<Col span={6} key={idx}>
|
||||||
|
|
@ -211,13 +214,12 @@ export const Dashboard: React.FC = () => {
|
||||||
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
|
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>{`
|
<style>{`
|
||||||
.ant-steps-item-title { font-size: 13px !important; font-weight: 600 !important; }
|
.ant-steps-item-title { font-size: 13px !important; font-weight: 600 !important; }
|
||||||
.ant-steps-item-description { font-size: 11px !important; }
|
.ant-steps-item-description { font-size: 11px !important; }
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd";
|
import { Button, Card, Col, Drawer, Form, Input, Popconfirm, Row, Select, Space, Table, Tag, Typography, message } from "antd";
|
||||||
import { CheckCircleOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, SearchOutlined, ThunderboltOutlined, UserOutlined } from "@ant-design/icons";
|
import { CheckCircleOutlined, DesktopOutlined, DisconnectOutlined, EditOutlined, ReloadOutlined, SearchOutlined, ThunderboltOutlined, UserOutlined } from "@ant-design/icons";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import type { DeviceInfo } from "@/types";
|
import type { DeviceInfo } from "@/types";
|
||||||
|
|
@ -104,9 +105,26 @@ export default function Devices() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page devices-page">
|
<PageContainer
|
||||||
<PageHeader title={t("devices.title")} subtitle={t("devices.subtitle")} />
|
title={t("devices.title")}
|
||||||
|
subtitle={t("devices.subtitle")}
|
||||||
|
headerExtra={
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={loadData}>
|
||||||
|
{t("common.refresh")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
toolbar={
|
||||||
|
<Input
|
||||||
|
placeholder={t("devicesExt.searchPlaceholder")}
|
||||||
|
prefix={<SearchOutlined aria-hidden="true" />}
|
||||||
|
style={{ width: 420 }}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(event) => setSearchText(event.target.value)}
|
||||||
|
allowClear
|
||||||
|
aria-label={t("devicesExt.searchLabel")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Row gutter={[16, 16]} className="devices-metrics">
|
<Row gutter={[16, 16]} className="devices-metrics">
|
||||||
<Col xs={24} md={8}>
|
<Col xs={24} md={8}>
|
||||||
<Card className="devices-metric-card devices-metric-card--total" bordered={false}>
|
<Card className="devices-metric-card devices-metric-card--total" bordered={false}>
|
||||||
|
|
@ -146,23 +164,7 @@ export default function Devices() {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
<Card className="devices-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
<Table<DeviceInfo>
|
||||||
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
|
||||||
<Input
|
|
||||||
placeholder={t("devicesExt.searchPlaceholder")}
|
|
||||||
prefix={<SearchOutlined aria-hidden="true" />}
|
|
||||||
style={{ width: 420 }}
|
|
||||||
value={searchText}
|
|
||||||
onChange={(event) => setSearchText(event.target.value)}
|
|
||||||
allowClear
|
|
||||||
aria-label={t("devicesExt.searchLabel")}
|
|
||||||
/>
|
|
||||||
<Button onClick={loadData}>{t("common.refresh")}</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
|
||||||
<Table<DeviceInfo>
|
|
||||||
rowKey="deviceId"
|
rowKey="deviceId"
|
||||||
dataSource={filteredData}
|
dataSource={filteredData}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|
@ -279,8 +281,6 @@ export default function Devices() {
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={
|
title={
|
||||||
<div className="device-drawer-title">
|
<div className="device-drawer-title">
|
||||||
|
|
@ -310,6 +310,6 @@ export default function Devices() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createOrg, deleteOrg, listOrgs, listTenants, updateOrg } from "@/api";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import type { OrgNode, SysOrg, SysTenant } from "@/types";
|
import type { OrgNode, SysOrg, SysTenant } from "@/types";
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
@ -159,31 +160,32 @@ export default function Orgs() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader title={t("orgs.title")} subtitle={t("orgs.subtitle")} />
|
title={t("orgs.title")}
|
||||||
|
subtitle={t("orgs.subtitle")}
|
||||||
<div className="app-page__page-actions">
|
headerExtra={
|
||||||
{can("sys:org:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>{t("common.create")}</Button>}
|
can("sys:org:create") && (
|
||||||
</div>
|
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
|
||||||
|
{t("common.create")}
|
||||||
{isPlatformMode && (
|
</Button>
|
||||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
)
|
||||||
<Space className="app-page__toolbar">
|
}
|
||||||
|
toolbar={
|
||||||
|
isPlatformMode && (
|
||||||
|
<Space>
|
||||||
<Text strong>{t("users.tenant")}</Text>
|
<Text strong>{t("users.tenant")}</Text>
|
||||||
<Select style={{ width: 220 }} placeholder={t("orgs.selectTenant")} value={selectedTenantId} onChange={setSelectedTenantId} options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={<ShopOutlined aria-hidden="true" />} />
|
<Select style={{ width: 220 }} placeholder={t("orgs.selectTenant")} value={selectedTenantId} onChange={setSelectedTenantId} options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} suffixIcon={<ShopOutlined aria-hidden="true" />} />
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>{t("common.refresh")}</Button>
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>{t("common.refresh")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{selectedTenantId !== undefined ? (
|
||||||
|
<Table rowKey="id" columns={columns} dataSource={treeData} loading={loading} pagination={false} size="middle" scroll={{ y: "calc(100vh - 350px)" }} expandable={{ defaultExpandAllRows: true }} />
|
||||||
|
) : (
|
||||||
|
<div className="py-20 flex justify-center"><Empty description={t("orgs.selectTenant")} /></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
|
||||||
{selectedTenantId !== undefined ? (
|
|
||||||
<Table rowKey="id" columns={columns} dataSource={treeData} loading={loading} pagination={false} size="middle" scroll={{ y: "calc(100vh - 350px)" }} expandable={{ defaultExpandAllRows: true }} />
|
|
||||||
) : (
|
|
||||||
<div className="py-20 flex justify-center"><Empty description={t("orgs.selectTenant")} /></div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Drawer title={<Space><ApartmentOutlined aria-hidden="true" /><span>{editing ? t("orgs.drawerTitleEdit") : t("orgs.drawerTitleCreate")}</span></Space>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
<Drawer title={<Space><ApartmentOutlined aria-hidden="true" /><span>{editing ? t("orgs.drawerTitleEdit") : t("orgs.drawerTitleCreate")}</span></Space>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item label={t("users.tenant")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
|
<Form.Item label={t("users.tenant")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
|
||||||
|
|
@ -213,6 +215,6 @@ export default function Orgs() {
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { createTenant, deleteTenant, getPlatformRuntime, listTenants, updateTena
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||||
import type { PlatformRuntime, SysTenant } from "@/types";
|
import type { PlatformRuntime, SysTenant } from "@/types";
|
||||||
|
|
@ -183,61 +184,51 @@ export default function Tenants() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page" style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden', padding: '24px' }}>
|
<PageContainer
|
||||||
<div className="flex-shrink-0">
|
title={t("tenants.title")}
|
||||||
<PageHeader title={t("tenants.title")} subtitle={t("tenants.subtitle")} />
|
subtitle={t("tenants.subtitle")}
|
||||||
</div>
|
headerExtra={
|
||||||
|
runtime?.tenantMode !== "single" && can("sys_tenant:create") && (
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
{t("common.create")}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toolbar={
|
||||||
|
<Form form={searchForm} layout="inline" onFinish={handleSearch}>
|
||||||
|
<Form.Item name="name">
|
||||||
|
<Input placeholder={t("tenants.tenantName")} prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="code">
|
||||||
|
<Input placeholder={t("tenants.tenantCode")} allowClear style={{ width: 150 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">{t("common.search")}</Button>
|
||||||
|
<Button onClick={handleReset}>{t("common.reset")}</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
grid={{ gutter: 24, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
|
||||||
|
loading={loading}
|
||||||
|
dataSource={data}
|
||||||
|
renderItem={renderTenantCard}
|
||||||
|
pagination={false}
|
||||||
|
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("tenantsExt.emptyText")} /> }}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-shrink-0">
|
<Pagination
|
||||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }} style={{ marginBottom: '16px' }}>
|
{...getStandardPagination(total, queryParams.current, queryParams.size, handlePageChange)}
|
||||||
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
className="app-global-pagination"
|
||||||
<Form form={searchForm} layout="inline" onFinish={handleSearch} className="app-page__toolbar">
|
style={{
|
||||||
<Form.Item name="name">
|
marginTop: 24,
|
||||||
<Input placeholder={t("tenants.tenantName")} prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
|
display: 'flex',
|
||||||
</Form.Item>
|
justifyContent: 'flex-end'
|
||||||
<Form.Item name="code">
|
}}
|
||||||
<Input placeholder={t("tenants.tenantCode")} allowClear style={{ width: 150 }} />
|
/>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
|
||||||
<Space>
|
|
||||||
<Button type="primary" htmlType="submit">{t("common.search")}</Button>
|
|
||||||
<Button onClick={handleReset}>{t("common.reset")}</Button>
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
|
||||||
</Form>
|
|
||||||
{runtime?.tenantMode !== "single" && can("sys_tenant:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className="app-page__content-card"
|
|
||||||
style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}
|
|
||||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
|
||||||
>
|
|
||||||
<div style={{ flex: 1, overflowY: "auto", padding: "24px" }}>
|
|
||||||
<List
|
|
||||||
grid={{ gutter: 24, xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }}
|
|
||||||
loading={loading}
|
|
||||||
dataSource={data}
|
|
||||||
renderItem={renderTenantCard}
|
|
||||||
pagination={false}
|
|
||||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t("tenantsExt.emptyText")} /> }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flexShrink: 0, borderTop: '1px solid var(--app-border-color)', background: 'var(--app-bg-card)' }}>
|
|
||||||
<Pagination
|
|
||||||
{...getStandardPagination(total, queryParams.current, queryParams.size, handlePageChange)}
|
|
||||||
className="app-global-pagination"
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
padding: "12px 24px",
|
|
||||||
borderRadius: "0 0 16px 16px"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
title={<Space><ShopOutlined aria-hidden="true" /><span>{editing ? t("tenants.drawerTitleEdit") : t("tenants.drawerTitleCreate")}</span></Space>}
|
title={<Space><ShopOutlined aria-hidden="true" /><span>{editing ? t("tenants.drawerTitleEdit") : t("tenants.drawerTitleCreate")}</span></Space>}
|
||||||
|
|
@ -303,6 +294,6 @@ export default function Tenants() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile, uploadPlatformAsset } from "@/api";
|
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile, uploadPlatformAsset } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import type { BotCredential, UserProfile } from "@/types";
|
import type { BotCredential, UserProfile } from "@/types";
|
||||||
import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog";
|
import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog";
|
||||||
|
|
||||||
|
|
@ -128,9 +129,11 @@ export default function Profile() {
|
||||||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
|
<PageContainer
|
||||||
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
|
title={t("profile.title")}
|
||||||
|
subtitle={t("profile.subtitle")}
|
||||||
|
style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}
|
||||||
|
>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col xs={24} lg={8}>
|
<Col xs={24} lg={8}>
|
||||||
<Card className="app-page__content-card text-center" loading={loading}>
|
<Card className="app-page__content-card text-center" loading={loading}>
|
||||||
|
|
@ -304,7 +307,7 @@ export default function Profile() {
|
||||||
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
||||||
onConfirm={handleUploadCroppedImage}
|
onConfirm={handleUploadCroppedImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createDictItem, createDictType, deleteDictItem, deleteDictType, fetchDi
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import { getStandardPagination } from "@/utils/pagination";
|
import { getStandardPagination } from "@/utils/pagination";
|
||||||
import type { SysDictItem, SysDictType } from "@/types";
|
import type { SysDictItem, SysDictType } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
@ -149,10 +150,11 @@ export default function Dictionaries() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page dictionaries-page">
|
<PageContainer
|
||||||
<PageHeader title={t("dicts.title")} subtitle={t("dicts.subtitle")} />
|
title={t("dicts.title")}
|
||||||
|
subtitle={t("dicts.subtitle")}
|
||||||
<Row gutter={24} className="flex-1 min-h-0 overflow-hidden">
|
>
|
||||||
|
<Row gutter={24} style={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
|
||||||
<Col span={8} className="h-full flex flex-col overflow-hidden">
|
<Col span={8} className="h-full flex flex-col overflow-hidden">
|
||||||
<Card title={<Space><BookOutlined aria-hidden="true" /><span>{t("dicts.dictType")}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: "12px", flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:type:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddType}>{t("common.create")}</Button>}>
|
<Card title={<Space><BookOutlined aria-hidden="true" /><span>{t("dicts.dictType")}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: "12px", flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:type:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddType}>{t("common.create")}</Button>}>
|
||||||
<div style={{ marginBottom: 12 }} className="flex-shrink-0">
|
<div style={{ marginBottom: 12 }} className="flex-shrink-0">
|
||||||
|
|
@ -273,6 +275,6 @@ export default function Dictionaries() {
|
||||||
<Form.Item label={t("common.remark")} name="remark"><Input.TextArea placeholder={t("dictsExt.itemRemarkPlaceholder")} rows={3} /></Form.Item>
|
<Form.Item label={t("common.remark")} name="remark"><Input.TextArea placeholder={t("dictsExt.itemRemarkPlaceholder")} rows={3} /></Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { DeleteOutlined, EyeOutlined, InfoCircleOutlined, ReloadOutlined, Search
|
||||||
import { cleanLogs, fetchLogModules, fetchLogs, listTenants } from "@/api";
|
import { cleanLogs, fetchLogModules, fetchLogs, listTenants } from "@/api";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||||
import AppPagination from "@/components/shared/AppPagination";
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
import type { SysLog, UserProfile } from "@/types";
|
import type { SysLog, UserProfile } from "@/types";
|
||||||
|
|
@ -245,11 +246,27 @@ export default function Logs() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page">
|
<PageContainer
|
||||||
<PageHeader title={t("logs.title")} subtitle={t("logs.subtitle")} />
|
title={t("logs.title")}
|
||||||
|
subtitle={t("logs.subtitle")}
|
||||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
headerExtra={
|
||||||
<Space wrap size="middle" className="app-page__toolbar">
|
<Space>
|
||||||
|
<Popconfirm
|
||||||
|
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
||||||
|
description={isPlatformAdmin ? t("logsExt.cleanConfirmDescriptionWithTenant", { tenant: activeTenantName }) : t("logsExt.cleanConfirmDescription")}
|
||||||
|
okText={t("common.confirm")}
|
||||||
|
cancelText={t("common.cancel")}
|
||||||
|
okButtonProps={{ danger: true, loading: cleaning }}
|
||||||
|
onConfirm={() => void handleClean()}
|
||||||
|
>
|
||||||
|
<Button danger icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning}>
|
||||||
|
{t("logsExt.cleanCurrent", { type: activeLogTypeLabel })}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
toolbar={
|
||||||
|
<Space wrap size="middle">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t("logs.searchPlaceholder")}
|
placeholder={t("logs.searchPlaceholder")}
|
||||||
style={{ width: 180 }}
|
style={{ width: 180 }}
|
||||||
|
|
@ -301,54 +318,54 @@ export default function Logs() {
|
||||||
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t("common.search")}</Button>
|
<Button type="primary" icon={<SearchOutlined aria-hidden="true" />} onClick={handleSearch}>{t("common.search")}</Button>
|
||||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>{t("common.reset")}</Button>
|
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={handleReset}>{t("common.reset")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
}
|
||||||
|
>
|
||||||
|
<Card className="app-page__content-card" style={{ flex: 1, minHeight: 0 }} styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
||||||
|
<Tabs
|
||||||
|
activeKey={activeTab}
|
||||||
|
onChange={(key) => { setActiveTab(key); setParams((prev) => ({ ...prev, current: 1, moduleName: "" })); }}
|
||||||
|
size="large"
|
||||||
|
tabBarExtraContent={(
|
||||||
|
<Popconfirm
|
||||||
|
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
||||||
|
description={isPlatformAdmin ? t("logsExt.cleanConfirmDescriptionWithTenant", { tenant: activeTenantName }) : t("logsExt.cleanConfirmDescription")}
|
||||||
|
okText={t("common.confirm")}
|
||||||
|
cancelText={t("common.cancel")}
|
||||||
|
okButtonProps={{ danger: true, loading: cleaning }}
|
||||||
|
onConfirm={() => void handleClean()}
|
||||||
|
>
|
||||||
|
<Button danger icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning}>
|
||||||
|
{t("logsExt.cleanCurrent", { type: activeLogTypeLabel })}
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{logTypeDict.length > 0
|
||||||
|
? logTypeDict.map((item) => <Tabs.TabPane tab={<span>{item.itemValue === "OPERATION" ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}{item.itemLabel}</span>} key={item.itemValue} />)
|
||||||
|
: <><Tabs.TabPane tab={<span><InfoCircleOutlined aria-hidden="true" />{t("logs.opLog")}</span>} key="OPERATION" /><Tabs.TabPane tab={<span><UserOutlined aria-hidden="true" />{t("logs.loginLog")}</span>} key="LOGIN" /></>}
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { paddingTop: 0, paddingBottom: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||||
<Tabs
|
<ListTable
|
||||||
activeKey={activeTab}
|
rowKey="id"
|
||||||
onChange={(key) => { setActiveTab(key); setParams((prev) => ({ ...prev, current: 1, moduleName: "" })); }}
|
columns={columns}
|
||||||
size="large"
|
dataSource={data}
|
||||||
className="flex-shrink-0"
|
loading={loading}
|
||||||
tabBarExtraContent={(
|
onChange={handleTableChange}
|
||||||
<Popconfirm
|
totalCount={total}
|
||||||
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
scroll={{ y: "calc(100vh - 460px)" }}
|
||||||
description={isPlatformAdmin ? t("logsExt.cleanConfirmDescriptionWithTenant", { tenant: activeTenantName }) : t("logsExt.cleanConfirmDescription")}
|
pagination={false}
|
||||||
okText={t("common.confirm")}
|
|
||||||
cancelText={t("common.cancel")}
|
|
||||||
okButtonProps={{ danger: true, loading: cleaning }}
|
|
||||||
onConfirm={() => void handleClean()}
|
|
||||||
>
|
|
||||||
<Button danger icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning}>
|
|
||||||
{t("logsExt.cleanCurrent", { type: activeLogTypeLabel })}
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{logTypeDict.length > 0
|
|
||||||
? logTypeDict.map((item) => <Tabs.TabPane tab={<span>{item.itemValue === "OPERATION" ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}{item.itemLabel}</span>} key={item.itemValue} />)
|
|
||||||
: <><Tabs.TabPane tab={<span><InfoCircleOutlined aria-hidden="true" />{t("logs.opLog")}</span>} key="OPERATION" /><Tabs.TabPane tab={<span><UserOutlined aria-hidden="true" />{t("logs.loginLog")}</span>} key="LOGIN" /></>}
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
|
||||||
<ListTable
|
|
||||||
rowKey="id"
|
|
||||||
columns={columns}
|
|
||||||
dataSource={data}
|
|
||||||
loading={loading}
|
|
||||||
onChange={handleTableChange}
|
|
||||||
totalCount={total}
|
|
||||||
scroll={{ y: "calc(100vh - 460px)" }}
|
|
||||||
pagination={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<AppPagination
|
|
||||||
current={params.current}
|
|
||||||
pageSize={params.size}
|
|
||||||
total={total}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
setParams({ ...params, current: page, size: pageSize });
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppPagination
|
||||||
|
current={params.current}
|
||||||
|
pageSize={params.size}
|
||||||
|
total={total}
|
||||||
|
onChange={(page, pageSize) => {
|
||||||
|
setParams({ ...params, current: page, size: pageSize });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Modal title={t("logs.detailTitle")} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={[<Button key="close" onClick={() => setDetailModalVisible(false)}>{t("logsExt.close")}</Button>]} width={700}>
|
<Modal title={t("logs.detailTitle")} open={detailModalVisible} onCancel={() => setDetailModalVisible(false)} footer={[<Button key="close" onClick={() => setDetailModalVisible(false)}>{t("logsExt.close")}</Button>]} width={700}>
|
||||||
|
|
@ -372,6 +389,6 @@ export default function Logs() {
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getAdminPlatformConfig, updatePlatformConfig, uploadPlatformAsset } from "@/api";
|
import { getAdminPlatformConfig, updatePlatformConfig, uploadPlatformAsset } from "@/api";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import type { SysPlatformConfig } from "@/types";
|
import type { SysPlatformConfig } from "@/types";
|
||||||
import "./index.less";
|
import "./index.less";
|
||||||
|
|
||||||
|
|
@ -72,20 +73,20 @@ export default function PlatformSettings() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page app-page--contained platform-settings-page">
|
<PageContainer
|
||||||
<PageHeader title={t("platformSettings.title")} subtitle={t("platformSettings.subtitle")} />
|
title={t("platformSettings.title")}
|
||||||
|
subtitle={t("platformSettings.subtitle")}
|
||||||
<div className="app-page__page-actions">
|
headerExtra={
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => form.submit()}>
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => form.submit()}>
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
}
|
||||||
|
>
|
||||||
<div className="platform-settings-scroll">
|
<div className="platform-settings-scroll">
|
||||||
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ projectName: "UnisBase" }} className="platform-settings-form">
|
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ projectName: "UnisBase" }} className="platform-settings-form">
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card title={<><GlobalOutlined className="mr-2" />{t("platformSettings.basicInfo")}</>} className="app-page__content-card mb-6" loading={loading}>
|
<Card title={<><GlobalOutlined className="mr-2" />{t("platformSettings.basicInfo")}</>} className="mb-6" loading={loading}>
|
||||||
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
|
<Form.Item label={t("platformSettings.projectName")} name="projectName" rules={[
|
||||||
{ required: true, message: t("platformSettings.projectNameRequired") },
|
{ required: true, message: t("platformSettings.projectNameRequired") },
|
||||||
{
|
{
|
||||||
|
|
@ -154,6 +155,6 @@ export default function PlatformSettings() {
|
||||||
</Row>
|
</Row>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
||||||
import { useDict } from "@/hooks/useDict";
|
import { useDict } from "@/hooks/useDict";
|
||||||
import { usePermission } from "@/hooks/usePermission";
|
import { usePermission } from "@/hooks/usePermission";
|
||||||
import PageHeader from "@/components/shared/PageHeader";
|
import PageHeader from "@/components/shared/PageHeader";
|
||||||
|
import PageContainer from "@/components/shared/PageContainer";
|
||||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||||
import AppPagination from "@/components/shared/AppPagination";
|
import AppPagination from "@/components/shared/AppPagination";
|
||||||
import type { SysParamQuery, SysParamVO } from "@/types";
|
import type { SysParamQuery, SysParamVO } from "@/types";
|
||||||
|
|
@ -165,49 +166,52 @@ export default function SysParams() {
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-page sys-params-page">
|
<PageContainer
|
||||||
<PageHeader title={t("sysParams.title")} subtitle={t("sysParams.subtitle")} />
|
title={t("sysParams.title")}
|
||||||
|
subtitle={t("sysParams.subtitle")}
|
||||||
<Card className="sys-params-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
headerExtra={
|
||||||
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setDrawerOpen(true)}>
|
||||||
<Form layout="inline" onFinish={handleSearch} className="app-page__toolbar">
|
{t("common.create")}
|
||||||
<Form.Item name="paramKey">
|
</Button>
|
||||||
<Input placeholder={t("sysParams.paramKey")} prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
|
}
|
||||||
</Form.Item>
|
toolbar={
|
||||||
<Form.Item name="paramType">
|
<Form layout="inline" onFinish={handleSearch}>
|
||||||
<Select placeholder={t("sysParams.paramType")} allowClear style={{ width: 150 }} options={paramTypeDict.map((item) => ({ label: item.itemLabel, value: item.itemValue }))} />
|
<Form.Item name="paramKey">
|
||||||
</Form.Item>
|
<Input placeholder={t("sysParams.paramKey")} prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
|
||||||
<Form.Item>
|
</Form.Item>
|
||||||
<Space>
|
<Form.Item name="paramType">
|
||||||
<Button type="primary" htmlType="submit">{t("common.search")}</Button>
|
<Select placeholder={t("sysParams.paramType")} allowClear style={{ width: 150 }} options={paramTypeDict.map((item) => ({ label: item.itemLabel, value: item.itemValue }))} />
|
||||||
<Button onClick={handleReset}>{t("common.reset")}</Button>
|
</Form.Item>
|
||||||
</Space>
|
<Form.Item>
|
||||||
</Form.Item>
|
<Space>
|
||||||
</Form>
|
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">{t("common.search")}</Button>
|
||||||
{can("sys_param:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</Button>}
|
<Button onClick={handleReset}>{t("common.reset")}</Button>
|
||||||
</div>
|
</Space>
|
||||||
</Card>
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
<Card className="app-page__content-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}>
|
}
|
||||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
>
|
||||||
<ListTable
|
<Card className="app-page__content-card">
|
||||||
rowKey="paramId"
|
<div className="app-page__table-wrap">
|
||||||
columns={columns}
|
<ListTable
|
||||||
dataSource={data}
|
rowKey="paramId"
|
||||||
loading={loading}
|
columns={columns}
|
||||||
scroll={{ y: "calc(100vh - 350px)" }}
|
dataSource={data}
|
||||||
totalCount={total}
|
loading={loading}
|
||||||
pagination={false}
|
onChange={handleTableChange}
|
||||||
/>
|
totalCount={total}
|
||||||
</div>
|
pagination={false}
|
||||||
<AppPagination
|
|
||||||
current={queryParams.pageNum || 1}
|
|
||||||
pageSize={queryParams.pageSize || 10}
|
|
||||||
total={total}
|
|
||||||
onChange={(page, pageSize) => {
|
|
||||||
handlePageChange(page, pageSize);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AppPagination
|
||||||
|
current={queryParams.pageNum || 1}
|
||||||
|
pageSize={queryParams.pageSize || 10}
|
||||||
|
total={total}
|
||||||
|
onChange={(page, pageSize) => {
|
||||||
|
handlePageChange(page, pageSize);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Drawer
|
<Drawer
|
||||||
|
|
@ -251,6 +255,7 @@ export default function SysParams() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</div>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue