feat: v0.12.4 素材库优化 + UI 修复

①素材组名字自动从火山同步(打开素材库时一次 API 调用)
②空素材组显示「暂无图片」替代烂图(列表页 + @搜索弹窗)
③@搜索支持英文角色名(去掉中文正则限制)
④素材上传页显示图片尺寸要求红字提示
⑤图片尺寸报错改为白话文案
⑥个人中心页面支持滚动

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
seaislee1209 2026-03-23 18:18:06 +08:00
parent 6d4142fff0
commit 0a1a3a266c
6 changed files with 62 additions and 11 deletions

View File

@ -2750,18 +2750,35 @@ def asset_groups_view(request):
team = request.user.team team = request.user.team
if request.method == 'GET': if request.method == 'GET':
groups = ( groups = list(
AssetGroup.objects AssetGroup.objects
.filter(team=team) .filter(team=team)
.annotate(asset_count=Count('assets')) .annotate(asset_count=Count('assets'))
.order_by('-created_at') .order_by('-created_at')
) )
# 从火山同步素材组名字(一次 API 调用)
remote_ids = [g.remote_group_id for g in groups if g.remote_group_id]
if remote_ids:
try:
from utils.assets_client import list_asset_groups
remote_items, _ = list_asset_groups(page=1, page_size=100)
remote_name_map = {item['Id']: item.get('Name', '') for item in remote_items if 'Id' in item}
for g in groups:
if g.remote_group_id and g.remote_group_id in remote_name_map:
remote_name = remote_name_map[g.remote_group_id]
if remote_name and remote_name != g.name:
g.name = remote_name
g.save(update_fields=['name'])
except Exception:
pass # 同步失败不影响列表展示
results = [] results = []
for g in groups: for g in groups:
results.append({ results.append({
'id': g.id, 'id': g.id,
'name': g.name, 'name': g.name,
'thumbnail_url': g.thumbnail_url, 'thumbnail_url': g.thumbnail_url if g.asset_count > 0 else '',
'asset_count': g.asset_count, 'asset_count': g.asset_count,
'remote_group_id': g.remote_group_id, 'remote_group_id': g.remote_group_id,
'created_at': g.created_at.isoformat(), 'created_at': g.created_at.isoformat(),
@ -2784,12 +2801,12 @@ def asset_groups_view(request):
w, h = img.size w, h = img.size
if w < 300 or h < 300: if w < 300 or h < 300:
return Response( return Response(
{'error': f'图片尺寸太小({w}x{h}),宽高均需 ≥ 300px'}, {'error': f'图片太小了,请上传更大的图片当前 {w}x{h},最小要求 300x300'},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if w > 6000 or h > 6000: if w > 6000 or h > 6000:
return Response( return Response(
{'error': f'图片尺寸太大({w}x{h}),宽高均需 ≤ 6000px'}, {'error': f'图片太大了,请压缩后重试当前 {w}x{h},最大支持 6000x6000'},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
file.seek(0) # Reset after PIL read file.seek(0) # Reset after PIL read
@ -2868,6 +2885,18 @@ def asset_group_detail_view(request, group_id):
return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': '素材组不存在'}, status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET': if request.method == 'GET':
# 同步火山端的素材组名字
if group.remote_group_id:
try:
from utils.assets_client import get_asset_group
remote = get_asset_group(group.remote_group_id)
remote_name = remote.get('Name', '')
if remote_name and remote_name != group.name:
group.name = remote_name
group.save(update_fields=['name'])
except Exception:
pass # 查不到就用本地名字
assets_qs = Asset.objects.filter(group=group).order_by('-created_at') assets_qs = Asset.objects.filter(group=group).order_by('-created_at')
asset_list = [] asset_list = []
for a in assets_qs: for a in assets_qs:
@ -2947,12 +2976,12 @@ def asset_group_add_asset_view(request, group_id):
w, h = img.size w, h = img.size
if w < 300 or h < 300: if w < 300 or h < 300:
return Response( return Response(
{'error': f'图片尺寸太小({w}x{h}),宽高均需 ≥ 300px'}, {'error': f'图片太小了,请上传更大的图片当前 {w}x{h},最小要求 300x300'},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
if w > 6000 or h > 6000: if w > 6000 or h > 6000:
return Response( return Response(
{'error': f'图片尺寸太大({w}x{h}),宽高均需 ≤ 6000px'}, {'error': f'图片太大了,请压缩后重试当前 {w}x{h},最大支持 6000x6000'},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
file.seek(0) file.seek(0)
@ -3053,6 +3082,7 @@ def asset_search_view(request):
groups = ( groups = (
AssetGroup.objects AssetGroup.objects
.filter(team=team, name__icontains=q) .filter(team=team, name__icontains=q)
.annotate(asset_count=Count('assets'))
.order_by('-created_at')[:20] .order_by('-created_at')[:20]
) )
results = [] results = []
@ -3060,7 +3090,8 @@ def asset_search_view(request):
results.append({ results.append({
'id': g.id, 'id': g.id,
'name': g.name, 'name': g.name,
'thumbnail_url': g.thumbnail_url, 'thumbnail_url': g.thumbnail_url if g.asset_count > 0 else '',
'asset_count': g.asset_count,
'remote_group_id': g.remote_group_id, 'remote_group_id': g.remote_group_id,
}) })
return Response({'results': results}) return Response({'results': results})

View File

@ -167,6 +167,12 @@ def get_asset(asset_id: str) -> dict:
return _do_request('GetAsset', body) return _do_request('GetAsset', body)
def get_asset_group(group_id: str) -> dict:
"""Get single asset group details."""
body = {'Id': group_id, 'ProjectName': PROJECT_NAME}
return _do_request('GetAssetGroup', body)
def update_asset_group(group_id: str, name: str = None, description: str = None): def update_asset_group(group_id: str, name: str = None, description: str = None):
"""Update an asset group's name and/or description.""" """Update an asset group's name and/or description."""
body = {'Id': group_id, 'ProjectName': PROJECT_NAME} body = {'Id': group_id, 'ProjectName': PROJECT_NAME}

View File

@ -211,7 +211,11 @@ export function AssetLibraryModal({ open, onClose }: Props) {
<div className={styles.grid}> <div className={styles.grid}>
{groups.map((group) => ( {groups.map((group) => (
<div key={group.id} className={styles.card} onClick={() => handleGroupClick(group)}> <div key={group.id} className={styles.card} onClick={() => handleGroupClick(group)}>
{group.asset_count === 0 ? (
<div className={styles.cardThumb} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--color-text-disabled)', fontSize: 12 }}></div>
) : (
<img src={tosThumb(group.thumbnail_url, 300)} alt={group.name} className={styles.cardThumb} /> <img src={tosThumb(group.thumbnail_url, 300)} alt={group.name} className={styles.cardThumb} />
)}
<div className={styles.cardInfo}> <div className={styles.cardInfo}>
{editingName && editingName.id === group.id ? ( {editingName && editingName.id === group.id ? (
<div className={styles.inlineEditWrap} onClick={(e) => e.stopPropagation()}> <div className={styles.inlineEditWrap} onClick={(e) => e.stopPropagation()}>
@ -406,6 +410,7 @@ export function AssetLibraryModal({ open, onClose }: Props) {
</> </>
)} )}
<div className={styles.dropZoneWarning}> </div> <div className={styles.dropZoneWarning}> </div>
<div className={styles.dropZoneWarning}> 300~6000 </div>
</div> </div>
<input <input
ref={fileInputRef} ref={fileInputRef}

View File

@ -125,6 +125,9 @@
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
background: #2a2a3a; background: #2a2a3a;
display: flex;
align-items: center;
justify-content: center;
} }
.thumbMedia { .thumbMedia {

View File

@ -271,8 +271,8 @@ export function PromptInput() {
typedAtRef.current = true; typedAtRef.current = true;
setMentionMode('references'); setMentionMode('references');
openMentionPopup(); openMentionPopup();
} else if (/[\u4e00-\u9fff]+/.test(textAfterAt) && !textAfterAt.includes(' ')) { } else if (textAfterAt.length > 0 && !textAfterAt.includes(' ')) {
// Chinese text after @, search assets // Text after @, search assets (Chinese + English)
if (searchTimerRef.current) clearTimeout(searchTimerRef.current); if (searchTimerRef.current) clearTimeout(searchTimerRef.current);
searchTimerRef.current = setTimeout(() => { searchTimerRef.current = setTimeout(() => {
assetsApi.search(textAfterAt).then((res) => { assetsApi.search(textAfterAt).then((res) => {
@ -641,7 +641,11 @@ export function PromptInput() {
}} }}
> >
<div className={styles.mentionThumb}> <div className={styles.mentionThumb}>
{group.thumbnail_url ? (
<img src={tosThumb(group.thumbnail_url, 72)} alt="" className={styles.thumbMedia} /> <img src={tosThumb(group.thumbnail_url, 72)} alt="" className={styles.thumbMedia} />
) : (
<span style={{ fontSize: 9, color: 'var(--color-text-disabled)' }}></span>
)}
</div> </div>
<span className={styles.mentionLabel}>{group.name}</span> <span className={styles.mentionLabel}>{group.name}</span>
<span className={styles.mentionType}></span> <span className={styles.mentionType}></span>

View File

@ -3,6 +3,8 @@
margin: 0 auto; margin: 0 auto;
padding: 24px 20px 60px; padding: 24px 20px 60px;
min-height: 100vh; min-height: 100vh;
height: 100vh;
overflow-y: auto;
} }
/* Header */ /* Header */