混剪优化
This commit is contained in:
@@ -36,6 +36,7 @@
|
|||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"vue": "^3.5.22",
|
"vue": "^3.5.22",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
|
"vuedraggable": "^4.1.0",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"xlsx-js-style": "^1.2.0"
|
"xlsx-js-style": "^1.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<a-modal
|
||||||
|
:open="visible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:footer="null"
|
||||||
|
width="800px"
|
||||||
|
class="selector-modal"
|
||||||
|
@update:open="$emit('update:visible', $event)"
|
||||||
|
>
|
||||||
|
<div class="selector-container">
|
||||||
|
<!-- 分组选择器 + 批量操作按钮 -->
|
||||||
|
<div class="selector-actions">
|
||||||
|
<a-space>
|
||||||
|
<!-- 分组选择器 -->
|
||||||
|
<a-select
|
||||||
|
:value="selectorGroupId"
|
||||||
|
placeholder="使用场景默认分组"
|
||||||
|
size="small"
|
||||||
|
style="width: 160px;"
|
||||||
|
allow-clear
|
||||||
|
@change="handleSelectorGroupChange"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="g in groupList" :key="g.id" :value="g.id">
|
||||||
|
{{ g.name }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<a-divider type="vertical" />
|
||||||
|
<a-button size="small" @click="handleSelectAll">
|
||||||
|
<template #icon><SelectOutlined /></template>
|
||||||
|
全选
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="handleClearSelection">
|
||||||
|
<template #icon><StopOutlined /></template>
|
||||||
|
清空
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="handleAutoFill">
|
||||||
|
<template #icon><ThunderboltOutlined /></template>
|
||||||
|
智能填充
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 候选网格 -->
|
||||||
|
<div class="selector-grid">
|
||||||
|
<div
|
||||||
|
v-for="file in selectorFiles"
|
||||||
|
:key="file.id"
|
||||||
|
class="selector-item"
|
||||||
|
:class="{ 'selector-item--selected': isSelected(file.id) }"
|
||||||
|
@click="toggleSelection(file)"
|
||||||
|
>
|
||||||
|
<div class="selector-thumb">
|
||||||
|
<img v-if="file.coverBase64" :src="file.coverBase64" />
|
||||||
|
<VideoCameraOutlined v-else />
|
||||||
|
</div>
|
||||||
|
<div class="selector-name">{{ file.displayName || file.fileName }}</div>
|
||||||
|
<div v-if="isSelected(file.id)" class="selector-checkmark">
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<div class="selector-actions-footer">
|
||||||
|
<a-space>
|
||||||
|
<a-button @click="$emit('update:visible', false)">取消</a-button>
|
||||||
|
<a-button type="primary" @click="handleConfirm">确定</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import {
|
||||||
|
VideoCameraOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
CheckOutlined,
|
||||||
|
SelectOutlined,
|
||||||
|
StopOutlined
|
||||||
|
} from '@ant-design/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
sceneIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1
|
||||||
|
},
|
||||||
|
sceneGroupId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
globalGroupId: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
groupList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
allGroupFiles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
groupFiles: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
initialCandidates: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
maxCandidates: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'confirm', 'load-group-files'])
|
||||||
|
|
||||||
|
// 弹窗分组选择
|
||||||
|
const selectorGroupId = ref(null)
|
||||||
|
|
||||||
|
// 临时选中的文件ID列表
|
||||||
|
const tempSelectedFiles = ref([])
|
||||||
|
|
||||||
|
// 弹窗标题
|
||||||
|
const modalTitle = computed(() =>
|
||||||
|
`场景${props.sceneIndex + 1} - 已选择 ${tempSelectedFiles.value.length}/${props.maxCandidates} 个候选`
|
||||||
|
)
|
||||||
|
|
||||||
|
// 弹窗中显示的素材列表
|
||||||
|
const selectorFiles = computed(() => {
|
||||||
|
const groupId = selectorGroupId.value
|
||||||
|
if (groupId) {
|
||||||
|
return props.allGroupFiles[groupId] || []
|
||||||
|
}
|
||||||
|
// 使用场景分组或全局分组
|
||||||
|
const effectiveGroupId = props.sceneGroupId || props.globalGroupId
|
||||||
|
if (effectiveGroupId) {
|
||||||
|
return props.allGroupFiles[effectiveGroupId] || props.groupFiles
|
||||||
|
}
|
||||||
|
return props.groupFiles
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听弹窗打开,初始化状态
|
||||||
|
watch(() => props.visible, (visible) => {
|
||||||
|
if (visible) {
|
||||||
|
// 初始化选中列表
|
||||||
|
tempSelectedFiles.value = [...props.initialCandidates]
|
||||||
|
// 设置初始分组
|
||||||
|
selectorGroupId.value = props.sceneGroupId || props.globalGroupId || null
|
||||||
|
// 如果分组素材未缓存,通知父组件加载
|
||||||
|
if (selectorGroupId.value && !props.allGroupFiles[selectorGroupId.value]) {
|
||||||
|
emit('load-group-files', selectorGroupId.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 判断是否选中
|
||||||
|
const isSelected = (fileId) => tempSelectedFiles.value.includes(fileId)
|
||||||
|
|
||||||
|
// 切换选中状态
|
||||||
|
const toggleSelection = (file) => {
|
||||||
|
const index = tempSelectedFiles.value.indexOf(file.id)
|
||||||
|
if (index > -1) {
|
||||||
|
tempSelectedFiles.value.splice(index, 1)
|
||||||
|
} else {
|
||||||
|
if (tempSelectedFiles.value.length < props.maxCandidates) {
|
||||||
|
tempSelectedFiles.value.push(file.id)
|
||||||
|
} else {
|
||||||
|
message.warning(`每个场景最多只能选择${props.maxCandidates}个候选`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全选
|
||||||
|
const handleSelectAll = () => {
|
||||||
|
tempSelectedFiles.value = selectorFiles.value.map(f => f.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空
|
||||||
|
const handleClearSelection = () => {
|
||||||
|
tempSelectedFiles.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 智能填充
|
||||||
|
const handleAutoFill = () => {
|
||||||
|
if (props.sceneIndex < 0) return
|
||||||
|
|
||||||
|
const availableMaterials = props.groupFiles.filter(
|
||||||
|
material => !tempSelectedFiles.value.includes(material.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (availableMaterials.length === 0) {
|
||||||
|
message.warning('没有更多可用素材')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxAddCount = props.maxCandidates - tempSelectedFiles.value.length
|
||||||
|
const targetCount = Math.min(3, maxAddCount)
|
||||||
|
|
||||||
|
if (targetCount <= 0) {
|
||||||
|
message.warning(`候选数量已达上限(${props.maxCandidates}个)`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单随机选择
|
||||||
|
const shuffled = [...availableMaterials].sort(() => Math.random() - 0.5)
|
||||||
|
const selected = shuffled.slice(0, targetCount)
|
||||||
|
tempSelectedFiles.value.push(...selected.map(m => m.id))
|
||||||
|
message.success(`已智能选择${selected.length}个候选素材`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组切换
|
||||||
|
const handleSelectorGroupChange = (groupId) => {
|
||||||
|
selectorGroupId.value = groupId
|
||||||
|
tempSelectedFiles.value = []
|
||||||
|
if (groupId && !props.allGroupFiles[groupId]) {
|
||||||
|
emit('load-group-files', groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认选择
|
||||||
|
const handleConfirm = () => {
|
||||||
|
emit('confirm', {
|
||||||
|
sceneIndex: props.sceneIndex,
|
||||||
|
fileIds: [...tempSelectedFiles.value]
|
||||||
|
})
|
||||||
|
emit('update:visible', false)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.selector-container {
|
||||||
|
.selector-actions {
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: var(--space-2);
|
||||||
|
max-height: 480px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-2);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all var(--duration-base) ease;
|
||||||
|
position: relative;
|
||||||
|
background: var(--color-surface);
|
||||||
|
box-shadow: var(--shadow-inset-card);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
box-shadow: var(--shadow-inset-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--selected {
|
||||||
|
border-color: var(--color-success);
|
||||||
|
box-shadow: var(--shadow-inset-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-thumb {
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background: var(--color-bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-2xl);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-name {
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: var(--space-2);
|
||||||
|
right: var(--space-2);
|
||||||
|
background: var(--color-success);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-base);
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 4px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-actions-footer {
|
||||||
|
margin-top: var(--space-2);
|
||||||
|
padding: var(--space-4);
|
||||||
|
background: var(--color-surface);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: right;
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.selector-grid {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.selector-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user