1、修改素材云样式

2、修改用户私域人像素材默认上限PRIVATE_PORTRAIT_DEFAULT_ASSET_LIMIT为50
This commit is contained in:
2026-07-11 12:16:13 +08:00
parent 32673f0f25
commit 39160822f7
9 changed files with 196 additions and 128 deletions
+2 -2
View File
@@ -191,7 +191,7 @@ const AdminUsers: React.FC = () => {
user_type: userType,
is_admin: userType === 'admin' ? (values.is_admin ?? false) : false,
frontend_user_kind: values.frontend_user_kind || 'external',
private_portrait_asset_limit: userType === 'frontend' ? Number(values.private_portrait_asset_limit ?? 5) : 0,
private_portrait_asset_limit: userType === 'frontend' ? Number(values.private_portrait_asset_limit ?? 50) : 0,
});
message.success('用户创建成功');
setCreateModal(false);
@@ -882,7 +882,7 @@ const AdminUsers: React.FC = () => {
<Form.Item
name="private_portrait_asset_limit"
label="私域人像素材总量上限"
initialValue={5}
initialValue={50}
extra="0 表示关闭私域人像素材库;大于 0 表示开启并限制该用户所有私域人像素材总量。"
rules={[{ required: true, message: '请输入私域人像素材总量上限' }]}
>
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
# 用户私域人像素材默认上限。users.private_portrait_asset_limit = 0 表示关闭模块;>0 表示启用并限制总素材量。
# 统计口径:真人 + 虚拟;图片 + 视频。音频当前业务暂不开放。
PRIVATE_PORTRAIT_DEFAULT_ASSET_LIMIT = 5
PRIVATE_PORTRAIT_DEFAULT_ASSET_LIMIT = 50
# 火山 Ark 私域素材 ProjectName:火山侧项目空间固定使用 default,并快照到各业务表 remote_project_name。
# 用户/项目隔离依赖本地 project_id 和火山返回的 Asset Group ID,不再动态拼接 ProjectName。
+1 -1
View File
@@ -41,7 +41,7 @@ class User(Base, TimestampMixin):
# 私域人像素材总量限制。0 表示关闭模块;>0 表示启用并限制真人/虚拟、图片/视频素材总量。
private_portrait_asset_limit: Mapped[int] = mapped_column(
Integer, default=5, server_default="5", nullable=False
Integer, default=50, server_default="50", nullable=False
)
@property
+2 -2
View File
@@ -58,7 +58,7 @@ class AdminUserOut(BaseModel):
last_login_at: NaiveDatetimeOptional = None
allowed_menus: list | None = None
resource_capacity: ResourceCapacityUsageOut | None = None
private_portrait_asset_limit: int = 5
private_portrait_asset_limit: int = 50
model_config = {"from_attributes": True}
@@ -73,7 +73,7 @@ class CreateUserRequest(BaseModel):
is_admin: bool = False
frontend_user_kind: str = Field(default="external", pattern="^(internal|external)$")
allowed_menus: list | None = None
private_portrait_asset_limit: int = Field(default=5, ge=0, le=9999, description="私域人像素材总量限制,真人/虚拟、图片/视频共用,0 表示关闭")
private_portrait_asset_limit: int = Field(default=50, ge=0, le=9999, description="私域人像素材总量限制,真人/虚拟、图片/视频共用,0 表示关闭")
class UpdateFrontendUserKindRequest(BaseModel):
+1 -1
View File
@@ -15,7 +15,7 @@ class UserOut(BaseModel):
allowed_menus: list | None = None
must_set_password: bool = False
resource_capacity: ResourceCapacityUsageOut | None = None
private_portrait_asset_limit: int = 5
private_portrait_asset_limit: int = 50
team_id: str | None = None
team_name: str | None = None
is_team_manager: bool = False
@@ -122,7 +122,14 @@ const RealPersonLibraryPanel: React.FC = () => {
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}></Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateOpen(true)}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
>
</Button>
</div>
<Row gutter={16}>
<Col xs={24} lg={5}>
@@ -392,7 +392,14 @@ const VirtualMaterialPanel: React.FC = () => {
<Typography.Title level={4} style={{ margin: 0 }}></Typography.Title>
<Typography.Text type="secondary">使</Typography.Text>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}></Button>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setCreateOpen(true)}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
>
</Button>
</div>
<Row gutter={[10, 10]}>
<Col xs={24} lg={5} >
@@ -446,11 +453,24 @@ const VirtualMaterialPanel: React.FC = () => {
title={selectedProject ? selectedProject.name : '素材资产'}
extra={(
<Space wrap>
{/* <Button icon={<ReloadOutlined />} onClick={() => { loadProjects(); loadAssets(assetPage, assetPageSize); }} loading={assetLoading}>刷新</Button> */}
<Button type="primary" icon={<UploadOutlined />} disabled={!selectedProjectId} onClick={() => setUploadOpen(true)}>/</Button>
<Button
type="primary"
icon={<UploadOutlined />}
disabled={!selectedProjectId}
onClick={() => setUploadOpen(true)}
style={{ borderRadius: 8, fontWeight: 600, borderColor: 'transparent' }}
>
/
</Button>
{selectedProjectId && (
<Popconfirm title="确认删除当前虚拟人像项目组吗?" onConfirm={handleDeleteProject}>
<Button danger icon={<DeleteOutlined />}></Button>
<Button
danger
icon={<DeleteOutlined />}
style={{ borderRadius: 8, fontWeight: 600 }}
>
</Button>
</Popconfirm>
)}
</Space>
@@ -464,14 +484,15 @@ const VirtualMaterialPanel: React.FC = () => {
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
onSearch={() => loadAssets(1, assetPageSize)}
style={{ width: 240 }}
style={{ width: 240, borderRadius: 8 }}
enterButton
/>
<Select
allowClear
placeholder="素材状态"
value={assetStatus}
onChange={(value) => setAssetStatus(value)}
style={{ width: 150 }}
style={{ width: 140, borderRadius: 8 }}
options={[
{ value: 'Processing', label: '处理中' },
{ value: 'Active', label: '可用' },
@@ -483,13 +504,19 @@ const VirtualMaterialPanel: React.FC = () => {
placeholder="素材类型"
value={assetType}
onChange={(value) => setAssetType(value)}
style={{ width: 130 }}
style={{ width: 120, borderRadius: 8 }}
options={[
{ value: 'Image', label: '图片' },
{ value: 'Video', label: '视频' },
]}
/>
<Button onClick={() => loadAssets(1, assetPageSize)}></Button>
<Button
type="primary"
onClick={() => loadAssets(1, assetPageSize)}
style={{ borderRadius: 8, fontWeight: 600, borderColor: 'transparent' }}
>
</Button>
</Space>
<Spin spinning={assetLoading}>
@@ -173,45 +173,93 @@ const UploadResourceHistoryPanel: React.FC = () => {
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'center', marginBottom: 16, flexWrap: 'wrap' }}>
<Space wrap>
<Select
value={resourceType}
style={{ width: 120 }}
options={[
{ label: '全部', value: '' },
{ label: '图片', value: 'image' },
{ label: '视频', value: 'video' },
{ label: '音频', value: 'audio' },
]}
onChange={(value) => {
setResourceType(value);
setPage(1);
}}
/>
<Input
{/* 统一筛选栏:媒体类型 + 搜索 + 批量操作 */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
marginBottom: 16,
padding: '12px 16px',
borderRadius: 12,
background: '#fff',
border: '1px solid #f0f0f5',
}}>
{/* 左侧:媒体类型按钮 */}
<Space size={8} wrap>
<Typography.Text style={{ color: '#94a3b8', fontSize: 14, marginRight: 4 }}></Typography.Text>
<Button
type={resourceType === '' ? 'primary' : 'default'}
onClick={() => { setResourceType(''); setPage(1); }}
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === '' ? 'transparent' : '#e2e8f0', color: resourceType === '' ? '#fff' : '#64748b' }}
>
</Button>
<Button
type={resourceType === 'image' ? 'primary' : 'default'}
onClick={() => { setResourceType('image'); setPage(1); }}
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'image' ? 'transparent' : '#e2e8f0', color: resourceType === 'image' ? '#fff' : '#64748b' }}
>
</Button>
<Button
type={resourceType === 'video' ? 'primary' : 'default'}
onClick={() => { setResourceType('video'); setPage(1); }}
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'video' ? 'transparent' : '#e2e8f0', color: resourceType === 'video' ? '#fff' : '#64748b' }}
>
</Button>
<Button
type={resourceType === 'audio' ? 'primary' : 'default'}
onClick={() => { setResourceType('audio'); setPage(1); }}
style={{ borderRadius: 8, fontWeight: 600, borderColor: resourceType === 'audio' ? 'transparent' : '#e2e8f0', color: resourceType === 'audio' ? '#fff' : '#64748b' }}
>
</Button>
</Space>
{/* 右侧:搜索 + 批量操作 */}
<Space size={8} wrap>
<Input.Search
allowClear
value={keyword}
prefix={<SearchOutlined />}
placeholder="搜索文件名或URL"
style={{ width: 240 }}
onChange={(e) => setKeyword(e.target.value)}
onPressEnter={() => {
setPage(1);
load();
}}
onSearch={() => { setPage(1); load(); }}
style={{ width: 240, borderRadius: 8 }}
enterButton
/>
{/* <Button icon={<ReloadOutlined />} onClick={load}>刷新</Button> */}
</Space>
<Space>
{batchMode ? (
<>
<Button onClick={selectAll}>{selectedIds.size && selectedIds.size === allItems.length ? '取消全选' : '全选'}</Button>
<Button danger icon={<DeleteOutlined />} onClick={handleBatchDelete}> ({selectedIds.size})</Button>
<Button onClick={() => { setBatchMode(false); setSelectedIds(new Set()); }}></Button>
<Button
onClick={selectAll}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
>
{selectedIds.size && selectedIds.size === allItems.length ? '取消全选' : '全选'}
</Button>
<Button
danger
onClick={handleBatchDelete}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #ef4444, #dc2626)', border: 'none', color: '#fff' }}
>
{selectedIds.size > 0 && `(${selectedIds.size})`}
</Button>
<Button
onClick={() => { setBatchMode(false); setSelectedIds(new Set()); }}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b' }}
>
</Button>
</>
) : (
<Button onClick={() => setBatchMode(true)}></Button>
<Button
type="primary"
onClick={() => setBatchMode(true)}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
>
</Button>
)}
</Space>
</div>
+68 -82
View File
@@ -911,88 +911,25 @@ const GeneratedRecord: React.FC = () => {
}, [filterType, filterMedia]);
return (
<div className="content_box" >
{/* 顶部:标签切换 + 批量操作按钮 */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'center',
gap: 12,
marginBottom: 12,
}}>
<Tabs
activeKey={filterType}
onChange={(key) => {
setFilterType(key as typeof filterType);
setIsSelectionMode(false);
setSelectedItems(new Set());
}}
items={[
{ key: 'project', label: <span><FolderOpenOutlined /> </span> },
{ key: 'creation', label: <span><FileTextOutlined /> </span> },
{ key: 'hot_opening_replicate', label: <span><StarOutlined /> </span> },
{ key: 'shot_replicate', label: <span><PlayCircleOutlined /> </span> },
{ key: 'private_portrait', label: <span><UserOutlined /> </span> },
{ key: 'upload_resource', label: <span><UploadOutlined /> </span> },
]}
size="middle"
/>
{filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{isSelectionMode ? (
<Space size={8}>
<Button
icon={selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? <ClearOutlined /> : <CheckOutlined />}
onClick={selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? () => { setSelectedItems(new Set()); } : handleSelectAll}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
>
{selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? '取消全选' : '全选'}
</Button>
<Button
onClick={() => { setIsSelectionMode(false); setSelectedItems(new Set()); }}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b' }}
>
</Button>
<Button
danger
icon={<DeleteOutlined />}
onClick={handleDeleteSelected}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #ef4444, #dc2626)', border: 'none', color: '#fff' }}
>
{selectedItems.size > 0 && `(${selectedItems.size})`}
</Button>
<Button
icon={<DownloadOutlined />}
onClick={handleDownloadSelected}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
>
{selectedItems.size > 0 && `(${selectedItems.size})`}
</Button>
<Button
type="primary"
icon={<UploadOutlined />}
onClick={handleBatchUploadSelected}
loading={uploading}
disabled={uploading || selectedItems.size === 0}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
>
{uploading ? '推送中...' : `推送至账户 ${selectedItems.size > 0 ? `(${selectedItems.size})` : ''}`}
</Button>
</Space>
) : (
<Button
type="primary"
icon={<UploadOutlined />}
onClick={() => setIsSelectionMode(true)}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
>
</Button>
)}
</div>
)}
</div>
{/* 顶部:标签切换 */}
<Tabs
activeKey={filterType}
onChange={(key) => {
setFilterType(key as typeof filterType);
setIsSelectionMode(false);
setSelectedItems(new Set());
}}
items={[
{ key: 'project', label: <span><FolderOpenOutlined /> </span> },
{ key: 'creation', label: <span><FileTextOutlined /> </span> },
{ key: 'hot_opening_replicate', label: <span><StarOutlined /> </span> },
{ key: 'shot_replicate', label: <span><PlayCircleOutlined /> </span> },
{ key: 'private_portrait', label: <span><UserOutlined /> </span> },
{ key: 'upload_resource', label: <span><UploadOutlined /> </span> },
]}
size="middle"
style={{ marginBottom: 12 }}
/>
{filterType === 'private_portrait' ? (
<PrivatePortraitLibraryPanel />
) : filterType === 'upload_resource' ? (
@@ -1067,6 +1004,55 @@ const GeneratedRecord: React.FC = () => {
>
</Button>
{/* 批量操作按钮 */}
{filterType !== 'private_portrait' && filterType !== 'upload_resource' && (
isSelectionMode ? (
<>
<Button
onClick={selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? () => { setSelectedItems(new Set()); } : handleSelectAll}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#334155' }}
>
{selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) && recordlist.length > 0 ? '取消全选' : '全选'}
</Button>
<Button
onClick={() => { setIsSelectionMode(false); setSelectedItems(new Set()); }}
style={{ borderRadius: 8, fontWeight: 600, background: '#f8f9fc', border: '1px solid #e2e8f0', color: '#64748b' }}
>
</Button>
<Button
danger
onClick={handleDeleteSelected}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #ef4444, #dc2626)', border: 'none', color: '#fff' }}
>
{selectedItems.size > 0 && `(${selectedItems.size})`}
</Button>
<Button
onClick={handleDownloadSelected}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
>
{selectedItems.size > 0 && `(${selectedItems.size})`}
</Button>
<Button
type="primary"
onClick={handleBatchUploadSelected}
loading={uploading}
disabled={uploading || selectedItems.size === 0}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none', color: '#fff' }}
>
{uploading ? '推送中...' : `推送至账户 ${selectedItems.size > 0 ? `(${selectedItems.size})` : ''}`}
</Button>
</>
) : (
<Button
type="primary"
onClick={() => setIsSelectionMode(true)}
style={{ borderRadius: 8, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}
>
</Button>
)
)}
</Space>
</div>
{/* Content area */}