-
-
-
-
-
-
-
-
-
-
点击添加候选
+
+
+
+
+
-
-
-
![]()
-
-
+
+
+
+
-
- {{ getFileById(candidate.fileId)?.fileName }}
-
-
-
+
-
-
+
![]()
+
+
+
+
+ {{ getFileById(candidate.fileId)?.fileName }}
+
+
+
+
+
+
-
-
+
+
@@ -248,12 +254,66 @@
素材库
-
+
+
+
+
+
+
+
共 {{ currentLibraryMaterialCount }} 个
+
+
+
+
+
+
+
-
+
-
-
- 已使用 ×{{ getFileUsageCount(file.id) }}
-
+
+
+
+ 已使用 ×{{ getFileUsageCount(file.id) }}
+
+
@@ -280,7 +342,8 @@
-
+
+
@@ -289,61 +352,19 @@
-
-
-
-
-
-
-
- 全选
-
-
-
- 清空
-
-
-
- 智能填充
-
-
-
-
-
-
-
-
-
![]()
-
-
-
{{ file.displayName || file.fileName }}
-
-
-
-
-
-
-
-
-
-
+
@@ -352,6 +373,7 @@
import { ref, computed, watch, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { useRouter } from 'vue-router'
+import draggable from 'vuedraggable'
// 图标导入
import {
@@ -360,19 +382,19 @@ import {
PlusOutlined,
CloseOutlined,
ThunderboltOutlined,
- CheckOutlined,
SettingOutlined,
PlaySquareOutlined,
FolderOutlined,
ClearOutlined,
- SelectOutlined,
- StopOutlined
+ CopyOutlined,
+ HolderOutlined,
} from '@ant-design/icons-vue'
// API 服务
import { MaterialService, MaterialGroupService } from '@/api/material'
import { MixTaskService } from '@/api/mixTask'
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
+import SceneSelectorModal from './components/SceneSelectorModal.vue'
const router = useRouter()
@@ -404,7 +426,6 @@ const randomlySelectMaterials = (count, materials, seed) => {
// ==================== 表单数据 ====================
const formData = ref({
- groupId: null,
title: '',
produceCount: Number(localStorage.getItem('mix-produce-count')) || 3,
totalDuration: 15,
@@ -423,12 +444,19 @@ const uiState = ref({
submitting: false,
selectorVisible: false,
currentSceneIndex: -1,
- tempSelectedFiles: []
+ // 场景选中态:当前选中的场景索引(null表示未选中)
+ selectedSceneIndex: null,
+ // 素材搜索关键词
+ searchKeyword: '',
+ // 素材库当前选中的分组ID
+ selectedLibraryGroup: null
})
const dataState = ref({
groupList: [],
groupFiles: [],
+ // 新增:按分组缓存的素材
+ allGroupFiles: {},
scenes: []
})
@@ -447,6 +475,46 @@ const filledCount = computed(() =>
dataState.value.scenes.filter(s => s.candidates?.length > 0).length
)
+// 带搜索过滤的素材列表(根据素材库选中的分组)
+const filteredMaterials = computed(() => {
+ const keyword = uiState.value.searchKeyword.toLowerCase().trim()
+ // 根据素材库选中的分组获取素材
+ const groupId = uiState.value.selectedLibraryGroup
+ const materials = groupId
+ ? (dataState.value.allGroupFiles[groupId] || [])
+ : dataState.value.groupFiles
+
+ if (!keyword) return materials
+
+ return materials.filter(file =>
+ file.fileName?.toLowerCase().includes(keyword) ||
+ file.displayName?.toLowerCase().includes(keyword)
+ )
+})
+
+// 当前素材库分组的素材数量(用于空状态判断)
+const currentLibraryMaterialCount = computed(() => {
+ const groupId = uiState.value.selectedLibraryGroup
+ return groupId
+ ? (dataState.value.allGroupFiles[groupId]?.length || 0)
+ : dataState.value.groupFiles.length
+})
+
+// 选择目标场景
+const selectScene = (index) => {
+ uiState.value.selectedSceneIndex = index
+}
+
+// 场景拖拽结束处理
+const onSceneDragEnd = () => {
+ // 更新场景索引
+ dataState.value.scenes.forEach((scene, index) => {
+ scene.index = index
+ })
+ // 清除选中态,避免索引错乱
+ uiState.value.selectedSceneIndex = null
+}
+
// 监听场景数变化,自动调整场景数组
watch(sceneCount, (newCount) => {
const current = dataState.value.scenes.length
@@ -455,6 +523,7 @@ watch(sceneCount, (newCount) => {
dataState.value.scenes.push({
index: i,
duration: formData.value.clipDuration,
+ groupId: null, // 场景专属分组ID,null表示使用全局分组
candidates: []
})
}
@@ -464,8 +533,18 @@ watch(sceneCount, (newCount) => {
}, { immediate: true })
// 工具函数
-const getFileById = (fileId) =>
- dataState.value.groupFiles.find(f => f.id === fileId)
+const getFileById = (fileId) => {
+ // 先从当前素材库查找
+ let file = dataState.value.groupFiles.find(f => f.id === fileId)
+ if (file) return file
+
+ // 从所有缓存的素材中查找
+ for (const files of Object.values(dataState.value.allGroupFiles)) {
+ file = files.find(f => f.id === fileId)
+ if (file) return file
+ }
+ return null
+}
const isFileUsed = (fileId) =>
dataState.value.scenes.some(scene =>
@@ -473,13 +552,49 @@ const isFileUsed = (fileId) =>
)
const getFileUsageCount = (fileId) => {
- let count = 0
- dataState.value.scenes.forEach(scene => {
- if (scene.candidates) {
- count += scene.candidates.filter(c => c.fileId === fileId).length
+ return dataState.value.scenes.reduce((count, scene) => {
+ return count + (scene.candidates?.filter(c => c.fileId === fileId).length || 0)
+ }, 0)
+}
+
+// 获取素材在各场景的使用详情(用于 Tooltip)
+const getFileUsageDetails = (fileId) => {
+ const details = []
+ dataState.value.scenes.forEach((scene, index) => {
+ const count = scene.candidates?.filter(c => c.fileId === fileId).length || 0
+ if (count > 0) {
+ details.push(`场景${index + 1}: ${count}次`)
}
})
- return count
+ return details.join(', ') || '未使用'
+}
+
+// 当前场景的候选ID列表(用于弹窗)
+const currentSceneCandidateIds = computed(() => {
+ const index = uiState.value.currentSceneIndex
+ if (index < 0 || index >= dataState.value.scenes.length) return []
+ return dataState.value.scenes[index]?.candidates?.map(c => c.fileId) || []
+})
+
+// 弹窗确认处理
+// 弹窗确认处理
+const handleModalConfirm = ({ sceneIndex, fileIds, setGroupId }) => {
+ const scene = dataState.value.scenes[sceneIndex]
+ if (!scene) return
+
+ // 更新候选列表
+ const candidates = fileIds.map(fileId => ({
+ fileId,
+ fileUrl: getFileById(fileId)?.fileUrl
+ }))
+ scene.candidates = candidates
+
+ // 如果需要设置场景分组
+ if (setGroupId !== null && setGroupId !== undefined) {
+ scene.groupId = setGroupId
+ }
+
+ message.success(`已为场景${sceneIndex + 1}设置${candidates.length}个候选`)
}
// ==================== 数据操作 ====================
@@ -497,14 +612,60 @@ const loadGroups = async () => {
}
}
-const handleGroupChange = async (groupId) => {
- if (!groupId) {
- dataState.value.groupFiles = []
- clearScenes()
- return
+// 素材库分组切换(仅切换显示,不影响场景数据)
+const handleLibraryGroupChange = async (groupId) => {
+ uiState.value.selectedLibraryGroup = groupId
+
+ // 如果该分组素材未缓存,则加载
+ if (!dataState.value.allGroupFiles[groupId]) {
+ uiState.value.loadingFiles = true
+ try {
+ const res = await MaterialService.getFilePage({
+ groupId,
+ fileCategory: 'video',
+ pageNo: 1,
+ pageSize: 50
+ })
+ if (res.code === 0) {
+ dataState.value.allGroupFiles[groupId] = res.data.list || []
+ }
+ } catch {
+ message.error('加载素材失败')
+ } finally {
+ uiState.value.loadingFiles = false
+ }
+ }
+}
+
+// ==================== 场景级分组功能 ====================
+
+// 获取场景的有效分组ID(场景分组 > 素材库当前分组)
+const getSceneEffectiveGroupId = (scene) => {
+ return scene.groupId || uiState.value.selectedLibraryGroup
+}
+
+// 获取场景的有效分组名称(用于显示)
+const getSceneEffectiveGroupName = (scene) => {
+ const effectiveGroupId = getSceneEffectiveGroupId(scene)
+ return getGroupName(effectiveGroupId)
+}
+
+// 获取分组名称
+const getGroupName = (groupId) => {
+ if (!groupId) return '全局分组'
+ const group = dataState.value.groupList.find(g => g.id === groupId)
+ return group?.name || '未知分组'
+}
+
+// 加载指定分组的素材(带缓存)
+const loadGroupFiles = async (groupId) => {
+ if (!groupId) return []
+
+ // 如果已缓存,直接返回
+ if (dataState.value.allGroupFiles[groupId]) {
+ return dataState.value.allGroupFiles[groupId]
}
- uiState.value.loadingFiles = true
try {
const res = await MaterialService.getFilePage({
groupId,
@@ -513,214 +674,163 @@ const handleGroupChange = async (groupId) => {
pageSize: 50
})
if (res.code === 0) {
- dataState.value.groupFiles = res.data.list || []
- clearScenes()
+ const files = res.data.list || []
+ dataState.value.allGroupFiles[groupId] = files
+ return files
}
} catch {
message.error('加载素材失败')
- } finally {
- uiState.value.loadingFiles = false
}
+ return []
}
+// 场景分组切换处理
+const handleSceneGroupChange = (sceneIndex, groupId) => {
+ const scene = dataState.value.scenes[sceneIndex]
+ if (!scene) return
+
+ scene.candidates = []
+ scene.groupId = groupId || null
+
+ if (groupId) loadGroupFiles(groupId)
+ message.success(`场景${sceneIndex + 1}已切换到 ${getGroupName(groupId)}`)
+}
+
+// 应用分组设置到所有场景
+const applyGroupToAllScenes = (groupId) => {
+ dataState.value.scenes.forEach(scene => {
+ scene.groupId = groupId || null
+ scene.candidates = []
+ })
+ if (groupId) loadGroupFiles(groupId)
+ message.success(`已将所有场景设置为 ${getGroupName(groupId)}`)
+}
+
+// 获取所有已缓存的素材
+const getAllCachedFiles = () => {
+ const seenIds = new Set()
+ const allFiles = []
+
+ for (const files of Object.values(dataState.value.allGroupFiles)) {
+ for (const file of files) {
+ if (!seenIds.has(file.id)) {
+ seenIds.add(file.id)
+ allFiles.push(file)
+ }
+ }
+ }
+ return allFiles
+}
+
+// 根据分组ID获取素材(用于素材库显示)
+const getFilesByGroup = (groupId) =>
+ groupId ? (dataState.value.allGroupFiles[groupId] || []) : dataState.value.groupFiles
+
// ==================== 场景操作 ====================
const openSceneSelector = (index) => {
uiState.value.currentSceneIndex = index
- uiState.value.tempSelectedFiles = dataState.value.scenes[index]?.candidates?.map(c => c.fileId) || []
uiState.value.selectorVisible = true
}
-const getSceneCandidateCount = (sceneIndex) =>
- sceneIndex >= 0 && sceneIndex < dataState.value.scenes.length
- ? dataState.value.scenes[sceneIndex].candidates?.length || 0
- : 0
-
-const isFileSelectedForScene = (fileId) =>
- uiState.value.tempSelectedFiles.includes(fileId)
-
-const toggleFileForScene = (file) => {
- const index = uiState.value.tempSelectedFiles.indexOf(file.id)
- if (index > -1) {
- uiState.value.tempSelectedFiles.splice(index, 1)
- } else {
- if (uiState.value.tempSelectedFiles.length < constants.MAX_CANDIDATES_PER_SCENE) {
- uiState.value.tempSelectedFiles.push(file.id)
- } else {
- message.warning(`每个场景最多只能选择${constants.MAX_CANDIDATES_PER_SCENE}个候选`)
- }
- }
-}
-
-const selectAllForScene = () => {
- uiState.value.tempSelectedFiles = dataState.value.groupFiles.map(f => f.id)
-}
-
-const clearSceneSelection = () => {
- uiState.value.tempSelectedFiles = []
-}
-
-const autoFillCurrentScene = () => {
- const sceneIndex = uiState.value.currentSceneIndex
- if (sceneIndex < 0) return
-
- const currentScene = dataState.value.scenes[sceneIndex]
- if (!currentScene?.candidates) {
- message.warning('场景不存在')
- return
- }
-
- const availableMaterials = dataState.value.groupFiles.filter(
- material => !currentScene.candidates.some(c => c.fileId === material.id)
- )
-
- if (availableMaterials.length === 0) {
- message.warning('请先选择素材分组并加载素材库')
- return
- }
-
- const currentCount = currentScene.candidates.length
- const maxAddCount = constants.MAX_CANDIDATES_PER_SCENE - currentCount
- const targetCount = Math.min(3, maxAddCount)
-
- if (targetCount <= 0) {
- message.warning(`当前场景候选数量已达上限(${constants.MAX_CANDIDATES_PER_SCENE}个)`)
- return
- }
-
- const selected = randomlySelectMaterials(targetCount, availableMaterials, sceneIndex)
- uiState.value.tempSelectedFiles = selected.map(m => m.fileId)
-
- message.success(`已为场景${sceneIndex + 1}智能选择${selected.length}个候选素材`)
-}
-
-const confirmSceneSelection = () => {
- const sceneIndex = uiState.value.currentSceneIndex
- if (sceneIndex < 0) return
-
- const candidates = uiState.value.tempSelectedFiles.map(fileId => {
- const file = getFileById(fileId)
- return {
- fileId,
- fileUrl: file.fileUrl
- }
- })
-
- dataState.value.scenes[sceneIndex].candidates = candidates
- uiState.value.selectorVisible = false
- message.success(`已为场景${sceneIndex + 1}添加${candidates.length}个候选`)
-}
-
const clearScenes = () => {
dataState.value.scenes = Array(sceneCount.value).fill(null).map((_, index) => ({
index,
duration: formData.value.clipDuration,
+ groupId: null, // 场景专属分组ID,null表示使用全局分组
candidates: []
}))
}
const clearScene = (index) => {
- if (index >= 0 && index < dataState.value.scenes.length) {
- dataState.value.scenes[index].candidates = []
- }
+ const scene = dataState.value.scenes[index]
+ if (scene) scene.candidates = []
}
const removeCandidate = (sceneIndex, candidateIndex) => {
- if (sceneIndex >= 0 && sceneIndex < dataState.value.scenes.length &&
- candidateIndex >= 0 && candidateIndex < dataState.value.scenes[sceneIndex].candidates.length) {
- dataState.value.scenes[sceneIndex].candidates.splice(candidateIndex, 1)
+ const candidates = dataState.value.scenes[sceneIndex]?.candidates
+ if (candidates?.[candidateIndex] !== undefined) {
+ candidates.splice(candidateIndex, 1)
message.success('已删除候选素材')
}
}
const handleFileClick = (file) => {
- const targetScene = dataState.value.scenes.find(scene =>
- scene.candidates.length < constants.MAX_CANDIDATES_PER_SCENE
- )
+ // 优先使用选中的目标场景
+ const targetIndex = uiState.value.selectedSceneIndex
- if (!targetScene) {
- message.warning('所有场景都已满')
+ if (targetIndex !== null) {
+ // 有选中场景,添加到该场景(允许重复添加)
+ const scene = dataState.value.scenes[targetIndex]
+ if (!scene) return
+
+ if (scene.candidates.length >= constants.MAX_CANDIDATES_PER_SCENE) {
+ message.warning(`场景${targetIndex + 1}候选数量已达上限`)
+ return
+ }
+
+ scene.candidates.push({ fileId: file.id, fileUrl: file.fileUrl })
+ message.success(`已添加到场景${targetIndex + 1}`)
return
}
- const isAlreadySelected = targetScene.candidates.some(c => c.fileId === file.id)
- if (isAlreadySelected) {
- return
- }
-
- targetScene.candidates.push({
- fileId: file.id,
- fileUrl: file.fileUrl
- })
+ // 未选中场景,提示用户
+ message.info('请先点击选择目标场景')
}
-const autoFillScenes = (strategy = 'empty_only', targetCount = 3) => {
- let availableMaterials = [...dataState.value.groupFiles]
- if (availableMaterials.length === 0) {
+const autoFillScenes = async (strategy = 'empty_only', targetCount = 3) => {
+ const hasMaterials = dataState.value.groupFiles.length > 0 ||
+ Object.keys(dataState.value.allGroupFiles).length > 0
+ if (!hasMaterials) {
message.warning('请先选择素材分组并加载素材库')
return
}
- const fillResults = []
let totalFilledScenes = 0
let totalFilledCandidates = 0
+ const incompleteScenes = []
- dataState.value.scenes.forEach((scene, sceneIndex) => {
- if (!scene.candidates) {
- scene.candidates = []
- }
+ for (let sceneIndex = 0; sceneIndex < dataState.value.scenes.length; sceneIndex++) {
+ const scene = dataState.value.scenes[sceneIndex]
+ scene.candidates ??= []
- const currentCount = scene.candidates.length
- if (currentCount >= targetCount) {
- return
- }
+ if (scene.candidates.length >= targetCount) continue
+
+ const effectiveGroupId = scene.groupId || uiState.value.selectedLibraryGroup
+ let availableMaterials = effectiveGroupId
+ ? [...(dataState.value.allGroupFiles[effectiveGroupId] || await loadGroupFiles(effectiveGroupId))]
+ : [...dataState.value.groupFiles]
+
+ if (availableMaterials.length === 0) continue
const sceneUsedIds = new Set(scene.candidates.map(c => c.fileId))
- const availableForScene = availableMaterials.filter(
- material => !sceneUsedIds.has(material.id)
- )
-
- if (availableForScene.length === 0) {
- return
- }
-
- const needFillCount = targetCount - currentCount
- const actualFillCount = Math.min(needFillCount, availableForScene.length)
- const selectedMaterials = randomlySelectMaterials(
- actualFillCount,
- availableForScene,
- sceneIndex
- )
+ const availableForScene = availableMaterials.filter(m => !sceneUsedIds.has(m.id))
+ if (availableForScene.length === 0) continue
+ const actualFillCount = Math.min(targetCount - scene.candidates.length, availableForScene.length)
+ const selectedMaterials = randomlySelectMaterials(actualFillCount, availableForScene, sceneIndex)
scene.candidates.push(...selectedMaterials)
if (selectedMaterials.length > 0) {
- fillResults.push({
- sceneIndex,
- filledCount: selectedMaterials.length,
- totalCount: scene.candidates.length
- })
totalFilledScenes++
totalFilledCandidates += selectedMaterials.length
+ if (scene.candidates.length < targetCount) {
+ incompleteScenes.push(sceneIndex + 1)
+ }
}
- })
+ }
if (totalFilledScenes === 0) {
message.info('所有场景已满,无需填充')
} else {
- const messageText = `已为${totalFilledScenes}个场景填充${totalFilledCandidates}个候选`
- const incompleteScenes = fillResults.filter(r => r.totalCount < targetCount).length
-
- if (incompleteScenes > 0) {
- message.warning(`${messageText},其中${incompleteScenes}个场景未达到目标数量(素材不足)`)
- } else {
- message.success(messageText)
- }
+ const msg = `已为${totalFilledScenes}个场景填充${totalFilledCandidates}个候选`
+ incompleteScenes.length > 0
+ ? message.warning(`${msg},其中${incompleteScenes.length}个场景未达到目标数量(素材不足)`)
+ : message.success(msg)
}
}
const canSubmit = computed(() =>
- formData.value.groupId &&
- formData.value.title.trim() &&
+ uiState.value.selectedLibraryGroup &&
filledCount.value === sceneCount.value
)
@@ -729,24 +839,17 @@ const handleSubmit = async () => {
uiState.value.submitting = true
try {
- const sceneConfigs = dataState.value.scenes.map(scene => {
- const candidates = scene.candidates.map(candidate => {
- const file = getFileById(candidate.fileId)
- return {
- fileId: candidate.fileId,
- fileUrl: candidate.fileUrl,
- fileDuration: file?.duration || null
- }
- })
-
- return {
- duration: scene.duration,
- candidates
- }
- })
+ const sceneConfigs = dataState.value.scenes.map(scene => ({
+ duration: scene.duration,
+ candidates: scene.candidates.map(candidate => ({
+ fileId: candidate.fileId,
+ fileUrl: candidate.fileUrl,
+ fileDuration: getFileById(candidate.fileId)?.duration || null
+ }))
+ }))
const res = await MixTaskService.createTask({
- title: formData.value.title,
+ title: formData.value.title || `混剪视频_${new Date().toLocaleString()}`,
scenes: sceneConfigs,
produceCount: formData.value.produceCount,
cropMode: formData.value.cropMode
@@ -827,6 +930,32 @@ onMounted(() => {
flex-shrink: 0;
}
+// 表单标签带图标
+.label-with-icon {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-weight: 600;
+ color: var(--color-text);
+
+ .anticon {
+ color: var(--color-primary);
+ font-size: var(--font-size-lg);
+ }
+}
+
+// 标签帮助图标
+.label-help {
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-base);
+ cursor: help;
+ margin-left: var(--space-1);
+
+ &:hover {
+ color: var(--color-primary);
+ }
+}
+
.card {
background: var(--color-surface);
border: 1px solid var(--color-border);
@@ -838,6 +967,7 @@ onMounted(() => {
display: flex;
padding: var(--space-3) var(--space-4);
justify-content: space-between;
+ align-items: center;
border-bottom: 1px solid var(--color-border);
}
@@ -949,10 +1079,89 @@ onMounted(() => {
.scene-container {
width: 100%;
+ padding: var(--space-3);
+ border: 2px solid transparent;
+ border-radius: var(--radius-md);
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ background: rgba(var(--color-primary-rgb, 59, 130, 246), 0.03);
+ }
+
+ &--selected {
+ border-color: var(--color-primary);
+ background: rgba(var(--color-primary-rgb, 59, 130, 246), 0.08);
+ }
}
.scene-header {
margin-bottom: var(--space-3);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+}
+
+.scene-title-row {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+}
+
+// 拖拽手柄样式
+.scene-drag-handle {
+ cursor: grab;
+ color: var(--color-text-secondary);
+ padding: var(--space-1);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: var(--color-primary);
+ }
+
+ &:active {
+ cursor: grabbing;
+ }
+}
+
+// 拖拽时的幽灵样式
+.scene-ghost {
+ opacity: 0.5;
+ background: var(--color-primary-light);
+}
+
+// 场景素材源样式(只读显示)
+.scene-source {
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+
+ &__text {
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ }
+
+ .reset-source-btn,
+ .apply-all-btn {
+ padding: 0 4px;
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+
+ &:hover {
+ color: var(--color-primary);
+ }
+ }
+}
+
+.apply-all-btn {
+ padding: 2px 6px;
+ color: var(--color-text-3);
+
+ &:hover {
+ color: var(--color-primary);
+ }
}
.scene-title {
@@ -1117,6 +1326,83 @@ onMounted(() => {
}
}
+// 素材库筛选层样式
+.library-filter {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ padding: var(--space-2) var(--space-4);
+ background: var(--color-surface);
+ border-bottom: 1px solid var(--color-border);
+
+ &__tabs {
+ display: flex;
+ gap: var(--space-2);
+ flex: 1;
+ overflow-x: auto;
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+ }
+
+ &__actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ flex-shrink: 0;
+ }
+
+ &__count {
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ white-space: nowrap;
+ }
+}
+
+// 素材库操作层样式
+.library-actions {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+ padding: var(--space-2) var(--space-4);
+ background: var(--color-bg);
+ border-bottom: 1px solid var(--color-border);
+
+ &__scenes {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ flex: 1;
+ min-width: 0;
+ }
+
+ &__label {
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ flex-shrink: 0;
+ }
+
+ &__tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-1);
+ }
+}
+
+.library-tab {
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ user-select: none;
+
+ &:hover {
+ opacity: 0.8;
+ }
+}
+
.material-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
@@ -1374,4 +1660,24 @@ onMounted(() => {
gap: var(--space-1);
}
}
+
+// 场景 Tag 样式
+.scene-tag {
+ cursor: pointer;
+ transition: all 0.2s ease;
+ user-select: none;
+ padding: 0 6px;
+ font-size: var(--font-size-xs);
+ line-height: 20px;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &__count {
+ font-size: 10px;
+ opacity: 0.7;
+ margin-left: 2px;
+ }
+}
diff --git a/frontend/app/web-gold/src/views/material/components/SceneSelectorModal.vue b/frontend/app/web-gold/src/views/material/components/SceneSelectorModal.vue
new file mode 100644
index 0000000000..a64da25598
--- /dev/null
+++ b/frontend/app/web-gold/src/views/material/components/SceneSelectorModal.vue
@@ -0,0 +1,348 @@
+
+
+
+
+
+
+
+
+
+ {{ g.name }}
+
+
+
+
+
+ 全选
+
+
+
+ 清空
+
+
+
+ 智能填充
+
+
+
+
+
+
+
+
+
![]()
+
+
+
{{ file.displayName || file.fileName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+