feat: 优化

This commit is contained in:
2026-03-04 19:25:38 +08:00
parent 2d7ba035ca
commit fab8480f83
2 changed files with 124 additions and 69 deletions

View File

@@ -148,24 +148,30 @@
<!-- 场景内容 --> <!-- 场景内容 -->
<div class="scene-body"> <div class="scene-body">
<template v-if="scene.candidates?.length > 0"> <template v-if="scene.candidates?.length > 0">
<div <!-- 候选素材可拖拽排序 -->
v-for="(candidate, cIndex) in scene.candidates" <draggable
:key="cIndex" v-model="scene.candidates"
class="candidate-thumb" item-key="fileId"
@click.stop="removeCandidate(index, cIndex)" animation="200"
class="candidates-draggable"
:group="{ name: `scene-${index}` }"
> >
<img <template #item="{ element: candidate, index: cIndex }">
v-if="getFileById(candidate.fileId)?.coverBase64" <div class="candidate-thumb">
:src="getFileById(candidate.fileId).coverBase64" <img
/> v-if="getFileById(candidate.fileId)?.coverBase64"
<div v-else class="thumb-placeholder"> :src="getFileById(candidate.fileId).coverBase64"
<VideoCameraOutlined /> />
</div> <div v-else class="thumb-placeholder">
<div class="thumb-order">{{ cIndex + 1 }}</div> <VideoCameraOutlined />
<div class="thumb-remove"> </div>
<CloseOutlined /> <div class="thumb-order">{{ cIndex + 1 }}</div>
</div> <div class="thumb-remove" @click.stop="removeCandidate(index, cIndex)">
</div> <CloseOutlined />
</div>
</div>
</template>
</draggable>
<div <div
v-if="scene.candidates.length < constants.MAX_CANDIDATES_PER_SCENE" v-if="scene.candidates.length < constants.MAX_CANDIDATES_PER_SCENE"
class="candidate-add" class="candidate-add"
@@ -420,19 +426,18 @@ watch(sceneCount, (newCount) => {
} }
}, { immediate: true }) }, { immediate: true })
// 当 clipDuration 变化时,同步更新所有场景的 duration并保存到本地 // 同步场景时长并保存参数到本地
watch(() => formData.value.clipDuration, (newDuration) => { watch([
localStorage.setItem('mix-clip-duration', newDuration.toString()) () => formData.value.clipDuration,
() => formData.value.totalDuration
], ([clipDuration, totalDuration]) => {
localStorage.setItem('mix-clip-duration', clipDuration.toString())
localStorage.setItem('mix-total-duration', totalDuration.toString())
dataState.value.scenes.forEach(scene => { dataState.value.scenes.forEach(scene => {
scene.duration = newDuration scene.duration = clipDuration
}) })
}) })
// 当 totalDuration 变化时,保存到本地
watch(() => formData.value.totalDuration, (newDuration) => {
localStorage.setItem('mix-total-duration', newDuration.toString())
})
const onSceneDragEnd = () => { const onSceneDragEnd = () => {
dataState.value.scenes.forEach((scene, index) => { dataState.value.scenes.forEach((scene, index) => {
scene.index = index scene.index = index
@@ -450,11 +455,8 @@ const clearScenes = () => {
} }
const removeCandidate = (sceneIndex, candidateIndex) => { const removeCandidate = (sceneIndex, candidateIndex) => {
const candidates = dataState.value.scenes[sceneIndex]?.candidates dataState.value.scenes[sceneIndex]?.candidates?.splice(candidateIndex, 1)
if (candidates?.[candidateIndex] !== undefined) { message.success('已移除候选')
candidates.splice(candidateIndex, 1)
message.success('已移除候选')
}
} }
const openSceneSelector = (index) => { const openSceneSelector = (index) => {
@@ -462,20 +464,6 @@ const openSceneSelector = (index) => {
uiState.value.selectorVisible = true uiState.value.selectorVisible = true
} }
const openGroupSelector = (index) => {
const groups = dataState.value.groupList
if (groups.length === 0) return
const currentGroupId = dataState.value.scenes[index].groupId
const currentIndex = groups.findIndex(g => g.id === currentGroupId)
const nextIndex = (currentIndex + 1) % groups.length
const nextGroup = groups[nextIndex]
dataState.value.scenes[index].groupId = nextGroup.id
dataState.value.scenes[index].candidates = []
loadGroupFiles(nextGroup.id)
message.success(`场景${index + 1} 切换到 ${nextGroup.name}`)
}
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
// 素材操作 // 素材操作
@@ -607,7 +595,7 @@ const handleModalConfirm = ({ sceneIndex, fileIds, setGroupId }) => {
fileUrl: getFileById(fileId)?.fileUrl fileUrl: getFileById(fileId)?.fileUrl
})) }))
if (setGroupId !== null && setGroupId !== undefined) { if (setGroupId != null) {
scene.groupId = setGroupId scene.groupId = setGroupId
} }
@@ -959,6 +947,12 @@ onMounted(() => {
min-height: 90px; min-height: 90px;
overflow-x: auto; overflow-x: auto;
// 可拖拽容器
.candidates-draggable {
display: flex;
gap: var(--space-2);
}
.candidate-thumb { .candidate-thumb {
position: relative; position: relative;
width: 100px; width: 100px;
@@ -966,10 +960,14 @@ onMounted(() => {
border-radius: var(--radius-button); border-radius: var(--radius-button);
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
cursor: pointer; cursor: grab;
border: 2px solid transparent; border: 2px solid transparent;
transition: all var(--duration-base) ease; transition: all var(--duration-base) ease;
&:active {
cursor: grabbing;
}
img { img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -1001,24 +999,38 @@ onMounted(() => {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
font-weight: 600; font-weight: 600;
border-radius: 50%; border-radius: 50%;
z-index: 2;
} }
// 🗑️ 删除遮罩 // 🗑️ 删除按钮 - 右上角小按钮
.thumb-remove { .thumb-remove {
position: absolute; position: absolute;
inset: 0; top: 2px;
right: 2px;
width: 20px;
height: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: rgba(239, 68, 68, 0.9); background: rgba(0, 0, 0, 0.6);
color: var(--color-text-inverse); color: var(--color-text-inverse);
font-size: var(--font-size-md); font-size: 10px;
border-radius: 50%;
opacity: 0; opacity: 0;
transition: opacity var(--duration-base) ease; z-index: 3;
cursor: pointer;
transition: all var(--duration-fast) ease;
&:hover {
background: var(--color-error-500);
transform: scale(1.1);
}
} }
// hover 效果 - 柔和的高亮边框
&:hover { &:hover {
border-color: @danger; border-color: var(--color-primary-400);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
.thumb-remove { .thumb-remove {
opacity: 1; opacity: 1;

View File

@@ -21,15 +21,15 @@
<!-- 分组切换 --> <!-- 分组切换 -->
<div v-if="groupList.length > 1" class="group-tabs"> <div v-if="groupList.length > 1" class="group-tabs">
<a-tag <div
v-for="g in groupList" v-for="g in groupList"
:key="g.id" :key="g.id"
:color="selectorGroupId === g.id ? 'blue' : 'default'" class="group-tab-item"
style="cursor: pointer;" :class="{ 'group-tab-item--active': selectorGroupId === g.id }"
@click="handleSelectorGroupChange(g.id)" @click="handleSelectorGroupChange(g.id)"
> >
{{ g.name }} {{ g.name }}
</a-tag> </div>
</div> </div>
<a-button type="text" size="small" @click="$emit('update:visible', false)"> <a-button type="text" size="small" @click="$emit('update:visible', false)">
@@ -134,6 +134,8 @@ const props = defineProps({
const emit = defineEmits(['update:visible', 'confirm', 'load-group-files']) const emit = defineEmits(['update:visible', 'confirm', 'load-group-files'])
const LOCAL_STORAGE_KEY = 'mix-scene-selector-group-id'
const selectorGroupId = ref(null) const selectorGroupId = ref(null)
const tempSelectedFiles = ref([]) const tempSelectedFiles = ref([])
@@ -152,7 +154,28 @@ const selectorFiles = computed(() => {
watch(() => props.visible, (visible) => { watch(() => props.visible, (visible) => {
if (visible) { if (visible) {
tempSelectedFiles.value = [...props.initialCandidates] tempSelectedFiles.value = [...props.initialCandidates]
selectorGroupId.value = props.sceneGroupId || props.globalGroupId || null
// 优先级1. 场景自带分组 2. 本地存储的分组 3. 全局分组 4. 第一个分组
let targetGroupId = props.sceneGroupId
if (!targetGroupId) {
const savedGroupId = localStorage.getItem(LOCAL_STORAGE_KEY)
if (savedGroupId && props.groupList.some(g => String(g.id) === savedGroupId)) {
targetGroupId = savedGroupId
}
}
if (!targetGroupId && props.globalGroupId) {
targetGroupId = props.globalGroupId
}
if (!targetGroupId && props.groupList.length > 0) {
targetGroupId = props.groupList[0].id
}
selectorGroupId.value = targetGroupId
// 加载分组文件
if (selectorGroupId.value && !props.allGroupFiles[selectorGroupId.value]) { if (selectorGroupId.value && !props.allGroupFiles[selectorGroupId.value]) {
emit('load-group-files', selectorGroupId.value) emit('load-group-files', selectorGroupId.value)
} }
@@ -174,11 +197,6 @@ onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
const isSelected = (fileId) => tempSelectedFiles.value.includes(fileId) const isSelected = (fileId) => tempSelectedFiles.value.includes(fileId)
const getSelectionOrder = (fileId) => tempSelectedFiles.value.indexOf(fileId) + 1 const getSelectionOrder = (fileId) => tempSelectedFiles.value.indexOf(fileId) + 1
const getFileName = (fileId) => {
const file = selectorFiles.value.find(f => f.id === fileId)
return file ? (file.displayName || file.fileName) : '未知'
}
const toggleSelection = (file) => { const toggleSelection = (file) => {
const index = tempSelectedFiles.value.indexOf(file.id) const index = tempSelectedFiles.value.indexOf(file.id)
if (index > -1) { if (index > -1) {
@@ -192,11 +210,6 @@ const toggleSelection = (file) => {
} }
} }
const removeSelection = (fileId) => {
const index = tempSelectedFiles.value.indexOf(fileId)
if (index > -1) tempSelectedFiles.value.splice(index, 1)
}
const handleQuickConfirm = (file) => { const handleQuickConfirm = (file) => {
if (!isSelected(file.id)) toggleSelection(file) if (!isSelected(file.id)) toggleSelection(file)
handleConfirm() handleConfirm()
@@ -236,6 +249,8 @@ const handleAutoFill = () => {
const handleSelectorGroupChange = (groupId) => { const handleSelectorGroupChange = (groupId) => {
selectorGroupId.value = groupId selectorGroupId.value = groupId
// 保存到本地存储
localStorage.setItem(LOCAL_STORAGE_KEY, String(groupId))
tempSelectedFiles.value = [] tempSelectedFiles.value = []
if (groupId && !props.allGroupFiles[groupId]) { if (groupId && !props.allGroupFiles[groupId]) {
emit('load-group-files', groupId) emit('load-group-files', groupId)
@@ -345,7 +360,35 @@ const handleConfirm = () => {
.group-tabs { .group-tabs {
display: flex; display: flex;
gap: var(--space-1); gap: var(--space-2);
flex: 1;
justify-content: center;
}
.group-tab-item {
padding: var(--space-2) var(--space-4);
font-size: var(--font-size-sm);
font-weight: 500;
color: @text-secondary;
background: var(--color-gray-100);
border-radius: var(--radius-button);
cursor: pointer;
transition: all var(--duration-base) ease;
user-select: none;
&:hover {
background: var(--color-gray-200);
color: @text;
}
&--active {
background: @primary;
color: var(--color-text-inverse);
&:hover {
background: @primary-hover;
}
}
} }
} }