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)",
|
||||
overflow: "hidden",
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
flexDirection: "column",
|
||||
flex: 1,
|
||||
minHeight: 0
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</Content>
|
||||
|
||||
<Footer
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { createPermission, deletePermission, listMyPermissions, updatePermission
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import type { SysPermission } from "@/types";
|
||||
import "./index.less";
|
||||
|
||||
|
|
@ -415,28 +416,55 @@ export default function Permissions() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page permissions-page">
|
||||
<PageHeader title={t("permissions.title")} subtitle={t("permissions.subtitle")} />
|
||||
|
||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<Space wrap size="middle" className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||
<Space wrap size="middle" className="app-page__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>
|
||||
</Space>
|
||||
{can("sys:permission:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<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}
|
||||
<PageContainer
|
||||
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"
|
||||
|
|
@ -446,18 +474,30 @@ export default function Permissions() {
|
|||
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,
|
||||
},
|
||||
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>}>
|
||||
<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 })}>
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
|
|
@ -593,6 +633,6 @@ export default function Permissions() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import {
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { RoleDataScope, SysOrg, SysPermission, SysRole, SysTenant, SysUser } from "@/types";
|
||||
import "./index.less";
|
||||
|
|
@ -422,15 +423,18 @@ export default function Roles() {
|
|||
const saveLabel = activeTab === "dataScope" ? "保存数据权限" : "保存";
|
||||
|
||||
return (
|
||||
<div className="app-page roles-page-v2">
|
||||
<PageHeader title="角色管理" subtitle="维护角色基础信息、功能权限、数据权限与成员绑定" />
|
||||
|
||||
<div className="app-page__page-actions">
|
||||
{can("sys:role:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{"新增角色"}</Button>}
|
||||
</div>
|
||||
|
||||
<div className="roles-layout">
|
||||
<Row gutter={24} className="roles-layout__row">
|
||||
<PageContainer
|
||||
title="角色管理"
|
||||
subtitle="维护角色基础信息、功能权限、数据权限与成员绑定"
|
||||
headerExtra={
|
||||
can("sys:role:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新增角色
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Row gutter={24} className="roles-layout__row" style={{ flex: 1, minHeight: 0 }}>
|
||||
<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">
|
||||
|
|
@ -605,7 +609,6 @@ export default function Roles() {
|
|||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
|
|
@ -633,6 +636,6 @@ export default function Roles() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types";
|
||||
|
|
@ -379,24 +380,50 @@ export default function Users() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page users-page">
|
||||
<PageHeader title={t("users.title")} subtitle={t("users.subtitle")} />
|
||||
|
||||
<Card className="users-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<div className="users-table-toolbar">
|
||||
<Space size="middle" wrap className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||
<Space size="middle" wrap className="app-page__toolbar">
|
||||
{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" />} 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>
|
||||
<PageContainer
|
||||
title={t("users.title")}
|
||||
subtitle={t("users.subtitle")}
|
||||
headerExtra={
|
||||
can("sys:user:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
toolbar={
|
||||
<>
|
||||
<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>
|
||||
{can("sys:user:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>{t("common.create")}</Button>}
|
||||
</Space>
|
||||
</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
|
||||
rowKey="userId"
|
||||
columns={columns}
|
||||
|
|
@ -409,24 +436,64 @@ export default function Users() {
|
|||
setPageSize(size);
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<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>}>
|
||||
<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>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" className="user-form">
|
||||
<Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title>
|
||||
<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}><Form.Item label={t("users.displayName")} name="displayName" rules={[{ required: true, message: t("users.displayName") }]}><Input placeholder={t("users.displayName")} /></Form.Item></Col>
|
||||
<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}>
|
||||
<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 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}><Form.Item label={t("users.phone")} name="phone"><Input placeholder={t("users.phone")} className="tabular-nums" /></Form.Item></Col>
|
||||
<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}>
|
||||
<Form.Item label={t("users.phone")} name="phone">
|
||||
<Input placeholder={t("users.phone")} className="tabular-nums" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</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}>
|
||||
<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>
|
||||
<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 && (
|
||||
<Form.Item
|
||||
label={t("usersExt.confirmPassword")}
|
||||
|
|
@ -450,11 +517,27 @@ export default function Users() {
|
|||
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
|
||||
</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>
|
||||
{!isPlatformMode && <Form.Item label={t("users.orgNode")} name="orgId"><TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} /></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>
|
||||
{!isPlatformMode && (
|
||||
<Form.Item label={t("users.orgNode")} name="orgId">
|
||||
<TreeSelect placeholder={t("usersExt.selectOrgPlaceholder")} allowClear treeData={orgTreeData} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<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>
|
||||
{isPlatformMode && <Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch /></Form.Item></Col>}
|
||||
<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>
|
||||
{isPlatformMode && (
|
||||
<Col span={12}>
|
||||
<Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
{isPlatformMode && (
|
||||
<>
|
||||
|
|
@ -476,7 +559,9 @@ export default function Users() {
|
|||
</Row>
|
||||
</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>
|
||||
|
|
@ -484,6 +569,6 @@ export default function Users() {
|
|||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
|
|||
import { ClusterOutlined, KeyOutlined, SafetyCertificateOutlined, SaveOutlined, SearchOutlined } from "@ant-design/icons";
|
||||
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { SysPermission, SysRole } from "@/types";
|
||||
|
||||
|
|
@ -174,13 +175,10 @@ export default function RolePermissionBinding() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader
|
||||
<PageContainer
|
||||
title={t("rolePerm.title")}
|
||||
subtitle={t("rolePerm.subtitle")}
|
||||
/>
|
||||
|
||||
<div className="app-page__page-actions">
|
||||
headerExtra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined aria-hidden="true" />}
|
||||
|
|
@ -190,9 +188,9 @@ export default function RolePermissionBinding() {
|
|||
>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</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%" }}>
|
||||
<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">
|
||||
|
|
@ -277,7 +275,7 @@ export default function RolePermissionBinding() {
|
|||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { listRoles, listUserRoles, listUsers, saveUserRoles } from "@/api";
|
|||
import { SaveOutlined, SearchOutlined, TeamOutlined, UserOutlined } from "@ant-design/icons";
|
||||
import type { SysRole, SysUser } from "@/types";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -100,19 +101,16 @@ export default function UserRoleBinding() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader
|
||||
<PageContainer
|
||||
title={t("userRole.title")}
|
||||
subtitle={t("userRole.subtitle")}
|
||||
/>
|
||||
|
||||
<div className="app-page__page-actions">
|
||||
headerExtra={
|
||||
<Button type="primary" icon={<SaveOutlined aria-hidden="true" />} onClick={handleSave} loading={saving} disabled={!selectedUserId}>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</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%" }}>
|
||||
<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">
|
||||
|
|
@ -199,6 +197,6 @@ export default function UserRoleBinding() {
|
|||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 PageContainer from "@/components/shared/PageContainer";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
|
|
@ -406,13 +407,15 @@ const AiModels: React.FC = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<Card
|
||||
className="app-page__content-card flex-1 flex flex-col overflow-hidden"
|
||||
<PageContainer
|
||||
title="AI 模型配置"
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
extra={
|
||||
<Space>
|
||||
subtitle="管理ASR语音识别和LLM大语言模型"
|
||||
headerExtra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
||||
新增模型
|
||||
</Button>
|
||||
}
|
||||
toolbar={
|
||||
<Input
|
||||
placeholder="搜索模型名称"
|
||||
prefix={<SearchOutlined />}
|
||||
|
|
@ -420,13 +423,8 @@ const AiModels: React.FC = () => {
|
|||
onPressEnter={(event) => setSearchName((event.target as HTMLInputElement).value)}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => openDrawer()}>
|
||||
新增模型
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ padding: "16px 24px 0", flexShrink: 0 }}>
|
||||
<Tabs
|
||||
activeKey={activeType}
|
||||
onChange={(key) => {
|
||||
|
|
@ -437,9 +435,10 @@ const AiModels: React.FC = () => {
|
|||
{ key: "ASR", label: "ASR 模型" },
|
||||
{ key: "LLM", label: "LLM 模型" },
|
||||
]}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
<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}
|
||||
|
|
@ -662,7 +661,7 @@ const AiModels: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { createClientDownload, deleteClientDownload, listClientDownloads, type ClientDownloadDTO, type ClientDownloadVO, updateClientDownload, uploadClientPackage } from "@/api/business/client";
|
||||
import { fetchDictItemsByTypeCode } from "@/api/dict";
|
||||
|
|
@ -380,18 +381,16 @@ export default function ClientManagement() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader
|
||||
<PageContainer
|
||||
title="客户端管理"
|
||||
subtitle="发布平台由数据字典 client_platform 驱动,并按父子分组展示。"
|
||||
extra={
|
||||
headerExtra={
|
||||
<Space size={12}>
|
||||
<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>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
>
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
<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' } }}>
|
||||
|
|
@ -525,6 +524,6 @@ export default function ClientManagement() {
|
|||
</Space>
|
||||
</Form>
|
||||
</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 { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import { createExternalApp, deleteExternalApp, listExternalApps, type ExternalAppDTO, type ExternalAppVO, updateExternalApp, uploadExternalAppApk, uploadExternalAppIcon } from "@/api/business/externalApp";
|
||||
|
||||
|
|
@ -290,18 +291,16 @@ export default function ExternalAppManagement() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader
|
||||
<PageContainer
|
||||
title="外部应用管理"
|
||||
subtitle="统一维护首页九宫格与抽屉入口中的原生应用、Web 服务和应用图标资源。"
|
||||
extra={
|
||||
headerExtra={
|
||||
<Space size={12}>
|
||||
<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>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
>
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
<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' } }}>
|
||||
|
|
@ -427,6 +426,6 @@ export default function ExternalAppManagement() {
|
|||
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked"><Switch /></Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,13 @@ import {
|
|||
Tag,
|
||||
Typography,
|
||||
} from "antd";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDict } from "../../hooks/useDict";
|
||||
|
|
@ -42,7 +45,6 @@ import {
|
|||
type HotWordGroupVO,
|
||||
} from "../../api/business/hotwordGroup";
|
||||
import AppPagination from "../../components/shared/AppPagination";
|
||||
import ListActionBar from "../../components/shared/ListActionBar/ListActionBar";
|
||||
|
||||
const { Option } = Select;
|
||||
const { Text } = Typography;
|
||||
|
|
@ -105,8 +107,6 @@ const HotWords: React.FC = () => {
|
|||
const [editingGroupId, setEditingGroupId] = useState<number | null>(null);
|
||||
const [selectedGroupName, setSelectedGroupName] = useState<string | undefined>(undefined);
|
||||
|
||||
const [filterVisible, setFilterVisible] = useState(false);
|
||||
|
||||
const groupNameMap = useMemo(
|
||||
() => Object.fromEntries(groupOptions.map((item) => [item.id, item.groupName])) as Record<number, string>,
|
||||
[groupOptions]
|
||||
|
|
@ -364,37 +364,17 @@ 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 (
|
||||
<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 }}>
|
||||
{/* Left Panel: Hotword Groups */}
|
||||
<Card
|
||||
className="shadow-sm"
|
||||
className="app-page__content-card"
|
||||
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 } }}
|
||||
extra={
|
||||
<Button
|
||||
|
|
@ -519,47 +499,42 @@ const HotWords: React.FC = () => {
|
|||
|
||||
{/* Right Panel: Hotwords */}
|
||||
<Card
|
||||
className="shadow-sm"
|
||||
className="app-page__content-card"
|
||||
title={hotWordGroupTitle}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}
|
||||
extra={
|
||||
<Space wrap size="small">
|
||||
<Input
|
||||
placeholder="搜索热词原文"
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
value={searchWord}
|
||||
onChange={(e) => setSearchWord(e.target.value)}
|
||||
onPressEnter={() => { setCurrent(1); void fetchData(); }}
|
||||
style={{ width: 200 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="筛选分类"
|
||||
allowClear
|
||||
value={searchCategory || undefined}
|
||||
onChange={(value) => {
|
||||
setSearchCategory(value as string);
|
||||
setCurrent(1);
|
||||
void fetchData();
|
||||
}}
|
||||
style={{ width: 120 }}
|
||||
options={categories.map((c) => ({ label: c.itemLabel, value: c.itemValue }))}
|
||||
/>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => handleOpenModal()}>
|
||||
新增热词
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => { setCurrent(1); void fetchData(); void loadGroupPage(); }}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, overflow: 'hidden' }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
>
|
||||
<div style={{ padding: '16px 24px 0' }}>
|
||||
<ListActionBar
|
||||
actions={[
|
||||
{
|
||||
key: 'add',
|
||||
label: '新增热词',
|
||||
type: 'primary',
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => handleOpenModal()
|
||||
}
|
||||
]}
|
||||
search={{
|
||||
placeholder: '搜索热词原文',
|
||||
value: searchWord,
|
||||
onChange: (val) => setSearchWord(val),
|
||||
onSearch: () => {
|
||||
setCurrent(1);
|
||||
void fetchData();
|
||||
}
|
||||
}}
|
||||
filter={{
|
||||
content: filterContent,
|
||||
title: '高级筛选',
|
||||
visible: filterVisible,
|
||||
onVisibleChange: setFilterVisible,
|
||||
isActive: !!searchCategory,
|
||||
selectedLabel: searchCategory ? categories.find(c => c.itemValue === searchCategory)?.itemLabel : '筛选'
|
||||
}}
|
||||
showRefresh
|
||||
onRefresh={() => {
|
||||
setCurrent(1);
|
||||
void fetchData();
|
||||
void loadGroupPage();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "16px 24px 0" }}>
|
||||
<Table
|
||||
columns={columns}
|
||||
|
|
@ -668,7 +643,7 @@ const HotWords: React.FC = () => {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { listUsers } from '../../api';
|
|||
import { useDict } from '../../hooks/useDict';
|
||||
import { SysUser } from '../../types';
|
||||
import PageHeader from '../../components/shared/PageHeader';
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
const { Option } = Select;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import {
|
|||
import {MeetingCreateDrawer, MeetingCreateType} from '../../components/business/MeetingCreateDrawer';
|
||||
import AppPagination from '../../components/shared/AppPagination';
|
||||
import {usePermission} from '../../hooks/usePermission';
|
||||
import PageContainer from '@/components/shared/PageContainer';
|
||||
import {SysUser} from '../../types';
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
|
|
@ -565,41 +566,56 @@ const Meetings: React.FC = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<Card
|
||||
className="app-page__content-card shadow-sm"
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
title={
|
||||
<Space size={12}>
|
||||
<div style={{ width: 8, height: 24, background: '#1890ff', borderRadius: 4 }}></div>
|
||||
<Title level={4} style={{ margin: 0 }}>会议中心</Title>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<PageContainer
|
||||
title="会议中心"
|
||||
subtitle="管理会议记录与分析"
|
||||
headerExtra={
|
||||
<Space size={16} wrap>
|
||||
<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={() => {
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
setCreateDrawerType('upload');
|
||||
setCreateDrawerVisible(true);
|
||||
}} style={{ borderRadius: 8, height: 36, fontWeight: 500 }}>创建会议</Button>
|
||||
}}
|
||||
>
|
||||
创建会议
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
toolbar={
|
||||
<>
|
||||
<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 />}
|
||||
allowClear
|
||||
onPressEnter={(e) => { setSearchTitle((e.target as any).value); setCurrent(1); }}
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflowX: "hidden", overflowY: "auto", padding: "24px" }}>
|
||||
<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 }}>
|
||||
<List grid={{ gutter: [24, 24], xs: 1, sm: 2, md: 2, lg: 3, xl: 4, xxl: 4 }} dataSource={data} renderItem={(item) => {
|
||||
<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="开启您的第一场会议分析" /> }} />
|
||||
}}
|
||||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||
/>
|
||||
</Skeleton>
|
||||
) : (
|
||||
<Table
|
||||
|
|
@ -615,7 +631,6 @@ const Meetings: React.FC = () => {
|
|||
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AppPagination current={current} pageSize={size} total={total} onChange={(p, s) => { setCurrent(p); setSize(s); }} />
|
||||
</Card>
|
||||
|
|
@ -644,10 +659,7 @@ const Meetings: React.FC = () => {
|
|||
width={500}
|
||||
>
|
||||
<Form form={participantsEditForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="participantIds"
|
||||
label="参会人员"
|
||||
>
|
||||
<Form.Item name="participantIds" label="参会人员">
|
||||
<Select mode="multiple" placeholder="请选择参会人" showSearch optionFilterProp="children">
|
||||
{userList.map(u => (
|
||||
<Option key={u.userId} value={u.userId}>
|
||||
|
|
@ -673,7 +685,7 @@ const Meetings: React.FC = () => {
|
|||
.card-actions { opacity: 0.6; transition: opacity 0.3s; }
|
||||
.meeting-card:hover .card-actions { opacity: 1; }
|
||||
`}</style>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, PlusOutlined, SaveOutlined, StarFilled } from '@ant-design/icons';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -331,16 +332,15 @@ const PromptTemplates: React.FC = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '32px', background: 'var(--app-bg-page)', height: 'calc(100vh - 64px)', overflow: 'hidden' }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto', height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 32 }}>
|
||||
<Title level={3} style={{ margin: 0 }}>提示词模板</Title>
|
||||
<PageContainer
|
||||
title="提示词模板"
|
||||
subtitle="管理AI会议总结的提示词模板库"
|
||||
headerExtra={
|
||||
<Button type="primary" icon={<PlusOutlined />} size="large" onClick={() => handleOpenDrawer()} style={{ borderRadius: 6 }}>
|
||||
新增模板
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<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' } }}>
|
||||
}
|
||||
toolbar={
|
||||
<Form form={searchForm} layout="inline" onFinish={() => void fetchData()}>
|
||||
<Form.Item name="name" label="模板名称"><Input placeholder="请输入..." style={{ width: 180 }} /></Form.Item>
|
||||
<Form.Item name="category" label="分类">
|
||||
|
|
@ -355,8 +355,8 @@ const PromptTemplates: React.FC = () => {
|
|||
</Space>
|
||||
</Form.Item>
|
||||
</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' } }}>
|
||||
<Skeleton loading={loading} active style={{ height: '100%' }}>
|
||||
{Object.keys(groupedData).length === 0 ? (
|
||||
|
|
@ -391,7 +391,6 @@ const PromptTemplates: React.FC = () => {
|
|||
)}
|
||||
</Skeleton>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Drawer
|
||||
title={<Title level={4} style={{ margin: 0 }}>{editingId ? '编辑模板' : '创建新模板'}</Title>}
|
||||
|
|
@ -506,7 +505,7 @@ const PromptTemplates: React.FC = () => {
|
|||
</Row>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import dayjs from "dayjs";
|
||||
import PageHeader from "../../components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import {
|
||||
appendRealtimeTranscripts,
|
||||
completeRealtimeMeeting,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import "./ScreenSaverManagement.css";
|
||||
import "./ScreenSaverManagement.css";
|
||||
|
||||
import {
|
||||
App,
|
||||
|
|
@ -26,6 +26,7 @@ import type { ColumnsType } from "antd/es/table";
|
|||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PictureOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
|
|
@ -36,6 +37,7 @@ import {
|
|||
UploadOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { UploadProps } from "antd";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
|
|
@ -608,29 +610,47 @@ export default function ScreenSaverManagement() {
|
|||
|
||||
const columns: ColumnsType<ScreenSaverVO> = [
|
||||
{
|
||||
title: "屏保画面",
|
||||
key: "visual",
|
||||
width: 330,
|
||||
title: "预览",
|
||||
key: "thumb",
|
||||
width: 90,
|
||||
render: (_, record) => (
|
||||
<div className="screen-saver-table-visual">
|
||||
<div className="screen-saver-table-thumb">
|
||||
{record.imageUrl ? <img src={record.imageUrl} alt={record.name} /> : null}
|
||||
{record.imageUrl ? (
|
||||
<img src={record.imageUrl} alt={record.name} style={{ maxWidth: 70, maxHeight: 52, objectFit: 'cover', borderRadius: 4 }} />
|
||||
) : (
|
||||
<div style={{ width: 70, height: 52, background: '#f5f5f5', borderRadius: 4, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<PictureOutlined style={{ color: '#ccc', fontSize: 20 }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "名称与描述",
|
||||
key: "info",
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={3}>
|
||||
<Text strong>{record.name}</Text>
|
||||
<Text type="secondary">{record.description || "暂无描述"}</Text>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "作用域",
|
||||
key: "scope",
|
||||
width: 220,
|
||||
width: 160,
|
||||
render: (_, record) => (
|
||||
<Space direction="vertical" size={4}>
|
||||
{record.scopeType === "USER" ? (
|
||||
|
|
@ -638,7 +658,7 @@ export default function ScreenSaverManagement() {
|
|||
) : (
|
||||
<Tag color="blue" icon={<TeamOutlined />}>平台级</Tag>
|
||||
)}
|
||||
<Text type="secondary">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{record.scopeType === "USER"
|
||||
? `归属:${normalizeOwnerLabel(record.ownerUserId ? userMap.get(record.ownerUserId) : undefined)}`
|
||||
: "全平台共用"}
|
||||
|
|
@ -647,12 +667,12 @@ export default function ScreenSaverManagement() {
|
|||
),
|
||||
},
|
||||
{
|
||||
title: "排序与状态",
|
||||
title: "状态",
|
||||
key: "status",
|
||||
width: 210,
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<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)} />
|
||||
</Space>
|
||||
),
|
||||
|
|
@ -660,14 +680,14 @@ export default function ScreenSaverManagement() {
|
|||
{
|
||||
title: "创建信息",
|
||||
key: "creator",
|
||||
width: 180,
|
||||
width: 160,
|
||||
render: (_, record) => {
|
||||
const timeValue = record.updatedAt || record.createdAt;
|
||||
return (
|
||||
<Space direction="vertical" size={4}>
|
||||
<Text>{record.creatorUsername || "-"}</Text>
|
||||
<Text type="secondary">
|
||||
{timeValue ? dayjs(timeValue).format("YYYY-MM-DD HH:mm:ss") : "-"}
|
||||
<Text style={{ fontSize: 13 }}>{record.creatorUsername || "-"}</Text>
|
||||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||||
{timeValue ? dayjs(timeValue).format("MM-DD HH:mm") : "-"}
|
||||
</Text>
|
||||
</Space>
|
||||
);
|
||||
|
|
@ -701,13 +721,20 @@ export default function ScreenSaverManagement() {
|
|||
const currentHeight = Form.useWatch("imageHeight", form) || CROP_HEIGHT;
|
||||
|
||||
return (
|
||||
<div className="app-page screen-saver-page">
|
||||
<Card
|
||||
className="app-page__content-card shadow-sm screen-saver-table-card"
|
||||
style={{ flex: 1, minHeight: 0 }}
|
||||
styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }}
|
||||
<PageContainer
|
||||
title="屏保管理"
|
||||
extra={(
|
||||
subtitle="管理平台级和用户级的屏保素材"
|
||||
headerExtra={
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新增屏保
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
toolbar={
|
||||
<Space wrap>
|
||||
<Input
|
||||
placeholder="搜索名称、描述、创建人或归属用户"
|
||||
|
|
@ -737,17 +764,8 @@ export default function ScreenSaverManagement() {
|
|||
{ label: "已停用", value: "disabled" },
|
||||
]}
|
||||
/>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
||||
刷新
|
||||
</Button>
|
||||
{/*<Button icon={<SettingOutlined />} onClick={openSettingsModal}>*/}
|
||||
{/* 播放设置({mySettings.displayDurationSec} 秒/张)*/}
|
||||
{/*</Button>*/}
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
新增屏保
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<div className="app-page__table-wrap screen-saver-table-wrap">
|
||||
<Table
|
||||
|
|
@ -771,8 +789,6 @@ export default function ScreenSaverManagement() {
|
|||
setPageSize(nextSize);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={editing ? "编辑屏保" : "新增屏保"}
|
||||
open={drawerOpen}
|
||||
|
|
@ -855,7 +871,7 @@ export default function ScreenSaverManagement() {
|
|||
</Space>
|
||||
<div className="screen-saver-preview-stage">
|
||||
{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)" }}>
|
||||
<Space direction="vertical" align="center" size={10}>
|
||||
|
|
@ -950,6 +966,6 @@ export default function ScreenSaverManagement() {
|
|||
onCancel={() => setCropState((prev) => ({ ...prev, open: false, src: "" }))}
|
||||
onConfirm={handleUploadCroppedImage}
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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 {
|
||||
HistoryOutlined,
|
||||
|
|
@ -137,8 +138,10 @@ export const Dashboard: React.FC = () => {
|
|||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '24px', background: 'var(--app-bg-page)', minHeight: '100%', overflowY: 'auto' }}>
|
||||
<div style={{ maxWidth: 1400, margin: '0 auto' }}>
|
||||
<PageContainer
|
||||
title="仪表板"
|
||||
subtitle="系统运行概览与最近任务动态"
|
||||
>
|
||||
<Row gutter={24} style={{ marginBottom: 24 }}>
|
||||
{statCards.map((s, idx) => (
|
||||
<Col span={6} key={idx}>
|
||||
|
|
@ -211,13 +214,12 @@ export const Dashboard: React.FC = () => {
|
|||
locale={{ emptyText: <Empty description="暂无近期分析任务" /> }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.ant-steps-item-title { font-size: 13px !important; font-weight: 600 !important; }
|
||||
.ant-steps-item-description { font-size: 11px !important; }
|
||||
`}</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 { 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 { useTranslation } from "react-i18next";
|
||||
import { kickManagedDevice, listManagedDevices, updateManagedDevice } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import type { DeviceInfo } from "@/types";
|
||||
|
|
@ -104,9 +105,26 @@ export default function Devices() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page devices-page">
|
||||
<PageHeader title={t("devices.title")} subtitle={t("devices.subtitle")} />
|
||||
|
||||
<PageContainer
|
||||
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">
|
||||
<Col xs={24} md={8}>
|
||||
<Card className="devices-metric-card devices-metric-card--total" bordered={false}>
|
||||
|
|
@ -146,22 +164,6 @@ export default function Devices() {
|
|||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card className="devices-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<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"
|
||||
dataSource={filteredData}
|
||||
|
|
@ -279,8 +281,6 @@ export default function Devices() {
|
|||
}
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
title={
|
||||
<div className="device-drawer-title">
|
||||
|
|
@ -310,6 +310,6 @@ export default function Devices() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { createOrg, deleteOrg, listOrgs, listTenants, updateOrg } from "@/api";
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import type { OrgNode, SysOrg, SysTenant } from "@/types";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
|
@ -159,30 +160,31 @@ export default function Orgs() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader title={t("orgs.title")} subtitle={t("orgs.subtitle")} />
|
||||
|
||||
<div className="app-page__page-actions">
|
||||
{can("sys:org:create") && <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>{t("common.create")}</Button>}
|
||||
</div>
|
||||
|
||||
{isPlatformMode && (
|
||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<Space className="app-page__toolbar">
|
||||
<PageContainer
|
||||
title={t("orgs.title")}
|
||||
subtitle={t("orgs.subtitle")}
|
||||
headerExtra={
|
||||
can("sys:org:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
toolbar={
|
||||
isPlatformMode && (
|
||||
<Space>
|
||||
<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" />} />
|
||||
<Button icon={<ReloadOutlined aria-hidden="true" />} onClick={loadOrgs}>{t("common.refresh")}</Button>
|
||||
</Space>
|
||||
</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" } }}>
|
||||
)
|
||||
}
|
||||
>
|
||||
{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>}>
|
||||
<Form form={form} layout="vertical">
|
||||
|
|
@ -213,6 +215,6 @@ export default function Orgs() {
|
|||
</Row>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { createTenant, deleteTenant, getPlatformRuntime, listTenants, updateTena
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import { LOGIN_NAME_PATTERN, sanitizeLoginName } from "@/utils/loginName";
|
||||
import type { PlatformRuntime, SysTenant } from "@/types";
|
||||
|
|
@ -183,15 +184,18 @@ export default function Tenants() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page" style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden', padding: '24px' }}>
|
||||
<div className="flex-shrink-0">
|
||||
<PageHeader title={t("tenants.title")} subtitle={t("tenants.subtitle")} />
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }} style={{ marginBottom: '16px' }}>
|
||||
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||
<Form form={searchForm} layout="inline" onFinish={handleSearch} className="app-page__toolbar">
|
||||
<PageContainer
|
||||
title={t("tenants.title")}
|
||||
subtitle={t("tenants.subtitle")}
|
||||
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>
|
||||
|
|
@ -205,17 +209,8 @@ export default function Tenants() {
|
|||
</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}
|
||||
|
|
@ -224,20 +219,16 @@ export default function Tenants() {
|
|||
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"
|
||||
marginTop: 24,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Drawer
|
||||
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>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { generateMyBotCredential, getCurrentUser, getMyBotCredential, updateMyPassword, updateMyProfile, uploadPlatformAsset } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import type { BotCredential, UserProfile } from "@/types";
|
||||
import AvatarCropDialog, { type CropModalState } from "./AvatarCropDialog";
|
||||
|
||||
|
|
@ -128,9 +129,11 @@ export default function Profile() {
|
|||
const avatarUrl = avatarUrlValue?.trim() || undefined;
|
||||
|
||||
return (
|
||||
<div className="app-page app-page--contained" style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}>
|
||||
<PageHeader title={t("profile.title")} subtitle={t("profile.subtitle")} />
|
||||
|
||||
<PageContainer
|
||||
title={t("profile.title")}
|
||||
subtitle={t("profile.subtitle")}
|
||||
style={{ maxWidth: 1024, width: "100%", margin: "0 auto" }}
|
||||
>
|
||||
<Row gutter={24}>
|
||||
<Col xs={24} lg={8}>
|
||||
<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: "" }))}
|
||||
onConfirm={handleUploadCroppedImage}
|
||||
/>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { createDictItem, createDictType, deleteDictItem, deleteDictType, fetchDi
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import { getStandardPagination } from "@/utils/pagination";
|
||||
import type { SysDictItem, SysDictType } from "@/types";
|
||||
import "./index.less";
|
||||
|
|
@ -149,10 +150,11 @@ export default function Dictionaries() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page dictionaries-page">
|
||||
<PageHeader title={t("dicts.title")} subtitle={t("dicts.subtitle")} />
|
||||
|
||||
<Row gutter={24} className="flex-1 min-h-0 overflow-hidden">
|
||||
<PageContainer
|
||||
title={t("dicts.title")}
|
||||
subtitle={t("dicts.subtitle")}
|
||||
>
|
||||
<Row gutter={24} style={{ flex: 1, minHeight: 0, 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>}>
|
||||
<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>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { DeleteOutlined, EyeOutlined, InfoCircleOutlined, ReloadOutlined, Search
|
|||
import { cleanLogs, fetchLogModules, fetchLogs, listTenants } from "@/api";
|
||||
import { useDict } from "@/hooks/useDict";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import type { SysLog, UserProfile } from "@/types";
|
||||
|
|
@ -245,11 +246,27 @@ export default function Logs() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="app-page">
|
||||
<PageHeader title={t("logs.title")} subtitle={t("logs.subtitle")} />
|
||||
|
||||
<Card className="app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<Space wrap size="middle" className="app-page__toolbar">
|
||||
<PageContainer
|
||||
title={t("logs.title")}
|
||||
subtitle={t("logs.subtitle")}
|
||||
headerExtra={
|
||||
<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
|
||||
placeholder={t("logs.searchPlaceholder")}
|
||||
style={{ width: 180 }}
|
||||
|
|
@ -301,14 +318,13 @@ export default function Logs() {
|
|||
<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>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<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" } }}>
|
||||
}
|
||||
>
|
||||
<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"
|
||||
className="flex-shrink-0"
|
||||
tabBarExtraContent={(
|
||||
<Popconfirm
|
||||
title={t("logsExt.cleanConfirmTitle", { type: activeLogTypeLabel })}
|
||||
|
|
@ -341,6 +357,7 @@ export default function Logs() {
|
|||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AppPagination
|
||||
current={params.current}
|
||||
pageSize={params.size}
|
||||
|
|
@ -372,6 +389,6 @@ export default function Logs() {
|
|||
</Descriptions>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { getAdminPlatformConfig, updatePlatformConfig, uploadPlatformAsset } from "@/api";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import type { SysPlatformConfig } from "@/types";
|
||||
import "./index.less";
|
||||
|
||||
|
|
@ -72,20 +73,20 @@ export default function PlatformSettings() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="app-page app-page--contained platform-settings-page">
|
||||
<PageHeader title={t("platformSettings.title")} subtitle={t("platformSettings.subtitle")} />
|
||||
|
||||
<div className="app-page__page-actions">
|
||||
<PageContainer
|
||||
title={t("platformSettings.title")}
|
||||
subtitle={t("platformSettings.subtitle")}
|
||||
headerExtra={
|
||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => form.submit()}>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
}
|
||||
>
|
||||
<div className="platform-settings-scroll">
|
||||
<Form form={form} layout="vertical" onFinish={onFinish} initialValues={{ projectName: "UnisBase" }} className="platform-settings-form">
|
||||
<Row gutter={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={[
|
||||
{ required: true, message: t("platformSettings.projectNameRequired") },
|
||||
{
|
||||
|
|
@ -154,6 +155,6 @@ export default function PlatformSettings() {
|
|||
</Row>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { createParam, deleteParam, pageParams, updateParam } from "@/api";
|
|||
import { useDict } from "@/hooks/useDict";
|
||||
import { usePermission } from "@/hooks/usePermission";
|
||||
import PageHeader from "@/components/shared/PageHeader";
|
||||
import PageContainer from "@/components/shared/PageContainer";
|
||||
import ListTable from "@/components/shared/ListTable/ListTable";
|
||||
import AppPagination from "@/components/shared/AppPagination";
|
||||
import type { SysParamQuery, SysParamVO } from "@/types";
|
||||
|
|
@ -165,12 +166,16 @@ export default function SysParams() {
|
|||
];
|
||||
|
||||
return (
|
||||
<div className="app-page sys-params-page">
|
||||
<PageHeader title={t("sysParams.title")} subtitle={t("sysParams.subtitle")} />
|
||||
|
||||
<Card className="sys-params-table-card app-page__filter-card" styles={{ body: { padding: "16px" } }}>
|
||||
<div className="app-page__toolbar" style={{ justifyContent: "space-between", width: "100%" }}>
|
||||
<Form layout="inline" onFinish={handleSearch} className="app-page__toolbar">
|
||||
<PageContainer
|
||||
title={t("sysParams.title")}
|
||||
subtitle={t("sysParams.subtitle")}
|
||||
headerExtra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setDrawerOpen(true)}>
|
||||
{t("common.create")}
|
||||
</Button>
|
||||
}
|
||||
toolbar={
|
||||
<Form layout="inline" onFinish={handleSearch}>
|
||||
<Form.Item name="paramKey">
|
||||
<Input placeholder={t("sysParams.paramKey")} prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
|
||||
</Form.Item>
|
||||
|
|
@ -179,27 +184,26 @@ export default function SysParams() {
|
|||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">{t("common.search")}</Button>
|
||||
<Button type="primary" icon={<SearchOutlined />} htmlType="submit">{t("common.search")}</Button>
|
||||
<Button onClick={handleReset}>{t("common.reset")}</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{can("sys_param:create") && <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>{t("common.create")}</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" } }}>
|
||||
<div className="app-page__table-wrap" style={{ flex: 1, minHeight: 0, overflow: "auto", padding: "0 24px" }}>
|
||||
}
|
||||
>
|
||||
<Card className="app-page__content-card">
|
||||
<div className="app-page__table-wrap">
|
||||
<ListTable
|
||||
rowKey="paramId"
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
loading={loading}
|
||||
scroll={{ y: "calc(100vh - 350px)" }}
|
||||
onChange={handleTableChange}
|
||||
totalCount={total}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AppPagination
|
||||
current={queryParams.pageNum || 1}
|
||||
pageSize={queryParams.pageSize || 10}
|
||||
|
|
@ -251,6 +255,7 @@ export default function SysParams() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue