2026-03-04 00:37:56 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
:open="visible"
|
|
|
|
|
|
:footer="null"
|
2026-03-04 02:13:16 +08:00
|
|
|
|
:closable="false"
|
2026-03-04 00:37:56 +08:00
|
|
|
|
width="800px"
|
2026-03-04 02:13:16 +08:00
|
|
|
|
centered
|
|
|
|
|
|
class="scene-selector-modal"
|
2026-03-04 00:37:56 +08:00
|
|
|
|
@update:open="$emit('update:visible', $event)"
|
|
|
|
|
|
>
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<!-- 顶部 -->
|
|
|
|
|
|
<div class="modal-header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<div class="scene-badge">场景 {{ sceneIndex + 1 }}</div>
|
|
|
|
|
|
<div class="counter">
|
|
|
|
|
|
<span class="counter-current">{{ tempSelectedFiles.length }}</span>
|
|
|
|
|
|
<span class="counter-sep">/</span>
|
|
|
|
|
|
<span class="counter-max">{{ maxCandidates }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分组切换 -->
|
|
|
|
|
|
<div v-if="groupList.length > 1" class="group-tabs">
|
2026-03-04 19:25:38 +08:00
|
|
|
|
<div
|
2026-03-04 02:13:16 +08:00
|
|
|
|
v-for="g in groupList"
|
|
|
|
|
|
:key="g.id"
|
2026-03-04 19:25:38 +08:00
|
|
|
|
class="group-tab-item"
|
|
|
|
|
|
:class="{ 'group-tab-item--active': selectorGroupId === g.id }"
|
2026-03-04 02:13:16 +08:00
|
|
|
|
@click="handleSelectorGroupChange(g.id)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ g.name }}
|
2026-03-04 19:25:38 +08:00
|
|
|
|
</div>
|
2026-03-04 00:37:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<a-button type="text" size="small" @click="$emit('update:visible', false)">
|
|
|
|
|
|
<CloseOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 工具栏 -->
|
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
|
<a-space size="small">
|
|
|
|
|
|
<a-button size="small" @click="handleSelectAll">
|
|
|
|
|
|
<template #icon><CheckSquareOutlined /></template>
|
|
|
|
|
|
全选
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button size="small" @click="handleClearSelection">
|
|
|
|
|
|
<template #icon><CloseSquareOutlined /></template>
|
|
|
|
|
|
清空
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
<a-button size="small" type="primary" ghost @click="handleAutoFill">
|
|
|
|
|
|
<template #icon><ThunderboltOutlined /></template>
|
|
|
|
|
|
随机
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
<span class="toolbar-hint">点击选择 · 双击确认 · Enter 提交</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 素材网格 -->
|
|
|
|
|
|
<div class="material-grid">
|
|
|
|
|
|
<template v-if="selectorFiles.length > 0">
|
2026-03-04 00:37:56 +08:00
|
|
|
|
<div
|
2026-03-04 02:13:16 +08:00
|
|
|
|
v-for="(file, index) in selectorFiles"
|
2026-03-04 00:37:56 +08:00
|
|
|
|
:key="file.id"
|
2026-03-04 02:13:16 +08:00
|
|
|
|
class="material-card"
|
|
|
|
|
|
:class="{ 'material-card--selected': isSelected(file.id) }"
|
|
|
|
|
|
:style="{ animationDelay: `${index * 20}ms` }"
|
2026-03-04 00:37:56 +08:00
|
|
|
|
@click="toggleSelection(file)"
|
2026-03-04 02:13:16 +08:00
|
|
|
|
@dblclick="handleQuickConfirm(file)"
|
2026-03-04 00:37:56 +08:00
|
|
|
|
>
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<div class="card-cover">
|
2026-03-05 21:01:34 +08:00
|
|
|
|
<img v-if="file.imgUrl" :src="file.imgUrl" />
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<div v-else class="cover-placeholder">
|
|
|
|
|
|
<VideoCameraOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 选中标记 -->
|
|
|
|
|
|
<div v-if="isSelected(file.id)" class="card-check">
|
|
|
|
|
|
<CheckOutlined />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 序号 -->
|
|
|
|
|
|
<div v-if="isSelected(file.id)" class="card-order">
|
|
|
|
|
|
{{ getSelectionOrder(file.id) }}
|
|
|
|
|
|
</div>
|
2026-03-04 00:37:56 +08:00
|
|
|
|
</div>
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<div class="card-name">{{ file.displayName || file.fileName }}</div>
|
2026-03-04 00:37:56 +08:00
|
|
|
|
</div>
|
2026-03-04 02:13:16 +08:00
|
|
|
|
</template>
|
2026-03-04 00:37:56 +08:00
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
<a-empty v-else description="暂无素材" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 底部 -->
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<a-space>
|
|
|
|
|
|
<a-button @click="$emit('update:visible', false)">取消</a-button>
|
|
|
|
|
|
<a-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:disabled="tempSelectedFiles.length === 0"
|
|
|
|
|
|
@click="handleConfirm"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #icon><CheckOutlined /></template>
|
|
|
|
|
|
确认选择
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-space>
|
2026-03-04 00:37:56 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-03-04 02:13:16 +08:00
|
|
|
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
2026-03-04 00:37:56 +08:00
|
|
|
|
import { message } from 'ant-design-vue'
|
|
|
|
|
|
import {
|
|
|
|
|
|
VideoCameraOutlined,
|
|
|
|
|
|
CheckOutlined,
|
2026-03-04 02:13:16 +08:00
|
|
|
|
CloseOutlined,
|
|
|
|
|
|
CheckSquareOutlined,
|
|
|
|
|
|
CloseSquareOutlined,
|
|
|
|
|
|
ThunderboltOutlined
|
2026-03-04 00:37:56 +08:00
|
|
|
|
} from '@ant-design/icons-vue'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
2026-03-04 02:13:16 +08:00
|
|
|
|
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 }
|
2026-03-04 00:37:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:visible', 'confirm', 'load-group-files'])
|
|
|
|
|
|
|
2026-03-04 19:25:38 +08:00
|
|
|
|
const LOCAL_STORAGE_KEY = 'mix-scene-selector-group-id'
|
|
|
|
|
|
|
2026-03-04 00:37:56 +08:00
|
|
|
|
const selectorGroupId = ref(null)
|
|
|
|
|
|
const tempSelectedFiles = ref([])
|
|
|
|
|
|
|
|
|
|
|
|
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]
|
2026-03-04 19:25:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 优先级: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
|
|
|
|
|
|
|
|
|
|
|
|
// 加载分组文件
|
2026-03-04 00:37:56 +08:00
|
|
|
|
if (selectorGroupId.value && !props.allGroupFiles[selectorGroupId.value]) {
|
|
|
|
|
|
emit('load-group-files', selectorGroupId.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// 键盘快捷键
|
|
|
|
|
|
const handleKeydown = (e) => {
|
|
|
|
|
|
if (!props.visible) return
|
|
|
|
|
|
if (e.key === 'Enter' && tempSelectedFiles.value.length > 0) {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
handleConfirm()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => window.addEventListener('keydown', handleKeydown))
|
|
|
|
|
|
onUnmounted(() => window.removeEventListener('keydown', handleKeydown))
|
|
|
|
|
|
|
2026-03-04 00:37:56 +08:00
|
|
|
|
const isSelected = (fileId) => tempSelectedFiles.value.includes(fileId)
|
2026-03-04 02:13:16 +08:00
|
|
|
|
const getSelectionOrder = (fileId) => tempSelectedFiles.value.indexOf(fileId) + 1
|
|
|
|
|
|
|
2026-03-04 00:37:56 +08:00
|
|
|
|
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 {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
message.warning(`最多选择 ${props.maxCandidates} 个素材`)
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
const handleQuickConfirm = (file) => {
|
|
|
|
|
|
if (!isSelected(file.id)) toggleSelection(file)
|
|
|
|
|
|
handleConfirm()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 00:37:56 +08:00
|
|
|
|
const handleSelectAll = () => {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
tempSelectedFiles.value = selectorFiles.value.slice(0, props.maxCandidates).map(f => f.id)
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleClearSelection = () => {
|
|
|
|
|
|
tempSelectedFiles.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleAutoFill = () => {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
const availableMaterials = selectorFiles.value.filter(
|
2026-03-04 00:37:56 +08:00
|
|
|
|
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) {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
message.warning('已达到上限')
|
2026-03-04 00:37:56 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const shuffled = [...availableMaterials].sort(() => Math.random() - 0.5)
|
|
|
|
|
|
const selected = shuffled.slice(0, targetCount)
|
|
|
|
|
|
tempSelectedFiles.value.push(...selected.map(m => m.id))
|
2026-03-04 02:13:16 +08:00
|
|
|
|
message.success(`已随机选择 ${selected.length} 个素材`)
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectorGroupChange = (groupId) => {
|
|
|
|
|
|
selectorGroupId.value = groupId
|
2026-03-04 19:25:38 +08:00
|
|
|
|
// 保存到本地存储
|
|
|
|
|
|
localStorage.setItem(LOCAL_STORAGE_KEY, String(groupId))
|
2026-03-04 00:37:56 +08:00
|
|
|
|
tempSelectedFiles.value = []
|
|
|
|
|
|
if (groupId && !props.allGroupFiles[groupId]) {
|
|
|
|
|
|
emit('load-group-files', groupId)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleConfirm = () => {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
if (tempSelectedFiles.value.length === 0) {
|
|
|
|
|
|
message.warning('请至少选择一个素材')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
emit('confirm', {
|
|
|
|
|
|
sceneIndex: props.sceneIndex,
|
2026-03-04 02:13:16 +08:00
|
|
|
|
fileIds: [...tempSelectedFiles.value],
|
|
|
|
|
|
setGroupId: selectorGroupId.value
|
2026-03-04 00:37:56 +08:00
|
|
|
|
})
|
|
|
|
|
|
emit('update:visible', false)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="less">
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 弹窗样式 - 使用项目设计系统
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
// 🎨 设计令牌引用
|
|
|
|
|
|
@primary: var(--color-primary-500);
|
|
|
|
|
|
@primary-light: var(--color-primary-50);
|
|
|
|
|
|
@primary-hover: var(--color-primary-400);
|
|
|
|
|
|
|
|
|
|
|
|
@success: var(--color-success-500);
|
|
|
|
|
|
@success-light: var(--color-success-50);
|
|
|
|
|
|
|
|
|
|
|
|
@warning: var(--color-warning-500);
|
|
|
|
|
|
|
|
|
|
|
|
@text: var(--color-text);
|
|
|
|
|
|
@text-secondary: var(--color-text-secondary);
|
|
|
|
|
|
@text-muted: var(--color-text-muted);
|
|
|
|
|
|
@text-disabled: var(--color-text-disabled);
|
|
|
|
|
|
|
|
|
|
|
|
@bg-card: var(--color-bg-card);
|
|
|
|
|
|
@bg-elevated: var(--color-gray-100);
|
|
|
|
|
|
|
|
|
|
|
|
@border: var(--color-border);
|
|
|
|
|
|
@border-light: var(--color-gray-100);
|
|
|
|
|
|
|
|
|
|
|
|
.scene-selector-modal {
|
|
|
|
|
|
:deep(.ant-modal-content) {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border-radius: var(--radius-card);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.ant-modal-body) {
|
|
|
|
|
|
padding: 0;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 顶部标题栏
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--space-4) var(--space-4);
|
|
|
|
|
|
background: @bg-elevated;
|
|
|
|
|
|
border-bottom: 1px solid @border-light;
|
|
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--space-3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.scene-badge {
|
|
|
|
|
|
padding: var(--space-1) var(--space-3);
|
|
|
|
|
|
background: @primary-light;
|
|
|
|
|
|
border: 1px solid var(--color-primary-200);
|
|
|
|
|
|
border-radius: var(--radius-tag);
|
|
|
|
|
|
font-size: var(--font-size-sm);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: @primary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.counter {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
.counter-current {
|
|
|
|
|
|
font-size: 22px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: @primary;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.counter-sep {
|
|
|
|
|
|
color: @text-disabled;
|
|
|
|
|
|
margin: 0 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.counter-max {
|
|
|
|
|
|
font-size: var(--font-size-base);
|
|
|
|
|
|
color: @text-muted;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.group-tabs {
|
|
|
|
|
|
display: flex;
|
2026-03-04 19:25:38 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-04 02:13:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 工具栏
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--space-2) var(--space-4);
|
|
|
|
|
|
background: @bg-card;
|
|
|
|
|
|
border-bottom: 1px solid @border-light;
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar-hint {
|
|
|
|
|
|
font-size: var(--font-size-xs);
|
|
|
|
|
|
color: @text-muted;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 素材网格
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
.material-grid {
|
2026-03-04 00:37:56 +08:00
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
2026-03-04 02:13:16 +08:00
|
|
|
|
gap: var(--space-3);
|
|
|
|
|
|
padding: var(--space-4);
|
|
|
|
|
|
min-height: 360px;
|
|
|
|
|
|
max-height: 360px;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
overflow-y: auto;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
background: @bg-card;
|
|
|
|
|
|
align-content: start;
|
|
|
|
|
|
|
|
|
|
|
|
&:deep(.ant-empty) {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--color-gray-300);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
.material-card {
|
2026-03-04 00:37:56 +08:00
|
|
|
|
cursor: pointer;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
border-radius: var(--radius-button);
|
2026-03-04 00:37:56 +08:00
|
|
|
|
overflow: hidden;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
background: @bg-elevated;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
transition: all var(--duration-base) ease;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
animation: fadeIn var(--duration-slow) ease backwards;
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(6px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
|
|
|
|
|
|
&:hover {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
border-color: @primary;
|
|
|
|
|
|
box-shadow: var(--shadow-blue);
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// ✅ 选中状态 - 青色主题
|
2026-03-04 00:37:56 +08:00
|
|
|
|
&--selected {
|
2026-03-04 02:13:16 +08:00
|
|
|
|
border-color: var(--color-primary-400);
|
|
|
|
|
|
background: var(--color-primary-50);
|
|
|
|
|
|
|
|
|
|
|
|
.card-check {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-order {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: scale(1);
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
.card-cover {
|
|
|
|
|
|
position: relative;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
aspect-ratio: 16/9;
|
|
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
2026-03-04 02:13:16 +08:00
|
|
|
|
|
|
|
|
|
|
.cover-placeholder {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: var(--color-gray-100);
|
|
|
|
|
|
color: @text-disabled;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ✓ 选中勾选
|
|
|
|
|
|
.card-check {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: var(--space-1);
|
|
|
|
|
|
left: var(--space-1);
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: @primary;
|
|
|
|
|
|
color: var(--color-text-inverse);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity var(--duration-base) ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 🏷️ 选中序号
|
|
|
|
|
|
.card-order {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: var(--space-1);
|
|
|
|
|
|
right: var(--space-1);
|
|
|
|
|
|
min-width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
padding: 0 var(--space-1);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: @warning;
|
|
|
|
|
|
color: var(--color-text-inverse);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: scale(0.8);
|
|
|
|
|
|
transition: all var(--duration-base) ease;
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
.card-name {
|
|
|
|
|
|
padding: var(--space-2);
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: @text-secondary;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
background: @bg-card;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
2026-03-04 02:13:16 +08:00
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 底部操作栏
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
padding: var(--space-3) var(--space-4);
|
|
|
|
|
|
background: @bg-elevated;
|
|
|
|
|
|
border-top: 1px solid @border-light;
|
|
|
|
|
|
|
|
|
|
|
|
.selected-preview {
|
2026-03-04 00:37:56 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-04 02:13:16 +08:00
|
|
|
|
gap: var(--space-1);
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-x: auto;
|
2026-03-04 00:37:56 +08:00
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
&::-webkit-scrollbar {
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.more-tag {
|
|
|
|
|
|
padding: 2px var(--space-2);
|
|
|
|
|
|
background: var(--color-gray-100);
|
|
|
|
|
|
border-radius: var(--radius-tag);
|
|
|
|
|
|
font-size: var(--font-size-xs);
|
|
|
|
|
|
color: @text-muted;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.no-selection {
|
|
|
|
|
|
font-size: var(--font-size-sm);
|
|
|
|
|
|
color: @text-disabled;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// 响应式适配
|
|
|
|
|
|
// ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@media (max-width: 800px) {
|
|
|
|
|
|
.material-grid {
|
2026-03-04 00:37:56 +08:00
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 02:13:16 +08:00
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
|
.material-grid {
|
2026-03-04 00:37:56 +08:00
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
}
|
2026-03-04 02:13:16 +08:00
|
|
|
|
|
|
|
|
|
|
.modal-header {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--space-2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.group-tabs {
|
|
|
|
|
|
order: 3;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
2026-03-04 00:37:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|