feat: 功能优化
This commit is contained in:
@@ -13,6 +13,7 @@ import { VoiceService } from '@/api/voice'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import { createDigitalHumanTask, getDigitalHumanTask, cancelTask, retryTask } from '@/api/digitalHuman'
|
||||
import { extractVideoCover } from '@/utils/video-cover'
|
||||
import { useUpload } from '@/composables/useUpload'
|
||||
|
||||
// 导入 voiceStore 用于获取用户音色
|
||||
import { useVoiceCopyStore } from '@/stores/voiceCopy'
|
||||
@@ -34,6 +35,9 @@ const playingPreviewVoiceId = ref('') // 当前正在试听的音色ID
|
||||
const isPlayingSynthesized = ref(false) // 是否正在播放已合成的音频
|
||||
const pollingInterval = ref(null) // 轮询间隔ID
|
||||
|
||||
// Upload Hook
|
||||
const { upload } = useUpload()
|
||||
|
||||
// 试听音频缓存 - 按音色ID缓存,避免重复API调用
|
||||
const previewAudioCache = new Map()
|
||||
const MAX_PREVIEW_CACHE_SIZE = 50 // 最多缓存50个音色的试听音频
|
||||
@@ -506,12 +510,23 @@ const uploadVideoFile = async (file) => {
|
||||
try {
|
||||
// 获取封面base64
|
||||
const coverBase64 = file.coverBase64 || null
|
||||
const res = await MaterialService.uploadFile(file, 'video', coverBase64)
|
||||
if (res.code === 0) {
|
||||
return res.data // res.data就是文件ID
|
||||
} else {
|
||||
throw new Error(res.message || '上传失败')
|
||||
}
|
||||
|
||||
// 使用useUpload Hook上传文件
|
||||
const fileId = await upload(file, {
|
||||
fileCategory: 'video',
|
||||
groupId: null, // 数字人视频模块不使用groupId
|
||||
coverBase64,
|
||||
onStart: () => {},
|
||||
onProgress: () => {},
|
||||
onSuccess: (id) => {
|
||||
message.success('文件上传成功')
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error.message || '上传失败')
|
||||
}
|
||||
})
|
||||
|
||||
return fileId
|
||||
} catch (error) {
|
||||
console.error('uploadVideoFile error:', error)
|
||||
throw error
|
||||
|
||||
@@ -83,9 +83,9 @@
|
||||
@remove="handleRemoveFile"
|
||||
@change="handleFileListChange"
|
||||
>
|
||||
<a-button type="primary" :loading="uploading">
|
||||
<UploadOutlined v-if="!uploading" />
|
||||
{{ uploading ? '上传中...' : (fileList.length > 0 ? '重新上传' : '上传音频文件') }}
|
||||
<a-button type="primary" :loading="uploadState.uploading">
|
||||
<UploadOutlined v-if="!uploadState.uploading" />
|
||||
{{ uploadState.uploading ? '上传中...' : (fileList.length > 0 ? '重新上传' : '上传音频文件') }}
|
||||
</a-button>
|
||||
</a-upload>
|
||||
<div class="upload-hint">
|
||||
@@ -110,6 +110,7 @@ import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined, SearchOutlined, UploadOutlined, PlayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import { VoiceService } from '@/api/voice'
|
||||
import { MaterialService } from '@/api/material'
|
||||
import { useUpload } from '@/composables/useUpload'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// ========== 常量 ==========
|
||||
@@ -126,7 +127,6 @@ const DEFAULT_FORM_DATA = {
|
||||
// ========== 响应式数据 ==========
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const uploading = ref(false)
|
||||
const voiceList = ref([])
|
||||
const modalVisible = ref(false)
|
||||
const formMode = ref('create')
|
||||
@@ -150,6 +150,9 @@ const pagination = reactive({
|
||||
|
||||
const formData = reactive({ ...DEFAULT_FORM_DATA })
|
||||
|
||||
// ========== Upload Hook ==========
|
||||
const { state: uploadState, upload } = useUpload()
|
||||
|
||||
// ========== 计算属性 ==========
|
||||
const isCreateMode = computed(() => formMode.value === 'create')
|
||||
|
||||
@@ -300,30 +303,29 @@ const handleBeforeUpload = (file) => {
|
||||
const handleCustomUpload = async (options) => {
|
||||
const { file, onSuccess, onError } = options
|
||||
|
||||
uploading.value = true
|
||||
|
||||
try {
|
||||
const res = await MaterialService.uploadFile(file, 'voice', null)
|
||||
const fileId = await upload(file, {
|
||||
fileCategory: 'voice',
|
||||
groupId: null, // 配音模块不使用groupId
|
||||
coverBase64: null,
|
||||
onStart: () => {},
|
||||
onProgress: () => {},
|
||||
onSuccess: (id) => {
|
||||
formData.fileId = id
|
||||
message.success('文件上传成功')
|
||||
onSuccess?.({ code: 0, data: id }, file)
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMsg = error.message || '上传失败,请稍后重试'
|
||||
message.error(errorMsg)
|
||||
onError?.(error)
|
||||
}
|
||||
})
|
||||
|
||||
if (res.code !== 0) {
|
||||
const errorMsg = res.msg || '上传失败'
|
||||
message.error(errorMsg)
|
||||
onError?.(new Error(errorMsg))
|
||||
return
|
||||
}
|
||||
|
||||
formData.fileId = res.data
|
||||
message.success('文件上传成功')
|
||||
|
||||
await nextTick()
|
||||
onSuccess?.(res, file)
|
||||
return fileId
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
const errorMsg = error?.message || '上传失败,请稍后重试'
|
||||
message.error(errorMsg)
|
||||
onError?.(error)
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
IdentifyState,
|
||||
Video,
|
||||
} from '../types/identify-face'
|
||||
import { identifyUploadedVideo, uploadAndIdentifyVideo } from '@/api/kling'
|
||||
import { identifyUploadedVideo } from '@/api/kling'
|
||||
import { useUpload } from '@/composables/useUpload'
|
||||
|
||||
/**
|
||||
* 数字人生成 Hook
|
||||
@@ -39,6 +40,9 @@ export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
|
||||
videoFileId: null,
|
||||
})
|
||||
|
||||
// ==================== Upload Hook ====================
|
||||
const { upload } = useUpload()
|
||||
|
||||
// ==================== 计算属性 ====================
|
||||
|
||||
/**
|
||||
@@ -102,8 +106,50 @@ export function useDigitalHumanGeneration(): UseDigitalHumanGeneration {
|
||||
res = await identifyUploadedVideo(hasSelectedVideo)
|
||||
identifyState.value.videoFileId = hasSelectedVideo.id
|
||||
} else {
|
||||
res = await uploadAndIdentifyVideo(hasUploadFile!)
|
||||
identifyState.value.videoFileId = res.data.fileId
|
||||
// 处理文件上传(提取封面)
|
||||
const file = hasUploadFile!
|
||||
let coverBase64 = null
|
||||
try {
|
||||
const { extractVideoCover } = await import('@/utils/video-cover')
|
||||
const cover = await extractVideoCover(file, {
|
||||
maxWidth: 800,
|
||||
quality: 0.8
|
||||
})
|
||||
coverBase64 = cover.base64
|
||||
} catch {
|
||||
// 封面提取失败不影响主流程
|
||||
}
|
||||
|
||||
// 使用useUpload Hook上传文件
|
||||
const fileId = await upload(file, {
|
||||
fileCategory: 'video',
|
||||
groupId: null, // 数字人模块不使用groupId
|
||||
coverBase64,
|
||||
onStart: () => {},
|
||||
onProgress: () => {},
|
||||
onSuccess: () => {
|
||||
message.success('文件上传成功')
|
||||
},
|
||||
onError: (err: Error) => {
|
||||
message.error(err.message || '上传失败')
|
||||
}
|
||||
})
|
||||
|
||||
// 生成播放链接
|
||||
// TODO: 获取播放链接逻辑
|
||||
|
||||
res = {
|
||||
success: true,
|
||||
data: {
|
||||
fileId,
|
||||
videoUrl: '', // TODO: 需要获取实际URL
|
||||
sessionId: '', // TODO: 需要实际识别
|
||||
faceId: null,
|
||||
startTime: 0,
|
||||
endTime: 0
|
||||
}
|
||||
}
|
||||
identifyState.value.videoFileId = fileId
|
||||
}
|
||||
|
||||
identifyState.value.sessionId = res.data.sessionId
|
||||
|
||||
736
frontend/app/web-gold/src/views/material/MaterialListNew.vue
Normal file
736
frontend/app/web-gold/src/views/material/MaterialListNew.vue
Normal file
@@ -0,0 +1,736 @@
|
||||
<template>
|
||||
<div class="material-list-container">
|
||||
<!-- 左侧边栏 -->
|
||||
<div class="material-sidebar">
|
||||
<!-- 分类标签 -->
|
||||
<div class="category-tabs">
|
||||
<div
|
||||
class="category-tab"
|
||||
:class="{ 'category-tab--active': activeCategory === 'MIX' }"
|
||||
@click="handleCategoryChange('MIX')"
|
||||
>
|
||||
混剪素材
|
||||
</div>
|
||||
<div
|
||||
class="category-tab"
|
||||
:class="{ 'category-tab--active': activeCategory === 'DIGITAL_HUMAN' }"
|
||||
@click="handleCategoryChange('DIGITAL_HUMAN')"
|
||||
>
|
||||
数字人素材
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分组列表 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-section__header">
|
||||
<span>分组列表</span>
|
||||
</div>
|
||||
<div class="sidebar-group-list">
|
||||
<div
|
||||
v-for="group in groupList"
|
||||
:key="group.id"
|
||||
class="sidebar-group-item"
|
||||
:class="{ 'sidebar-group-item--active': selectedGroupId === group.id }"
|
||||
@click="handleSelectGroup(group)"
|
||||
>
|
||||
<div class="sidebar-group-item__content">
|
||||
<FolderOutlined class="sidebar-group-item__icon" />
|
||||
<span class="sidebar-group-item__name">{{ group.name }}</span>
|
||||
</div>
|
||||
<span class="sidebar-group-item__count">{{ group.fileCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新建分组按钮 -->
|
||||
<div class="sidebar-actions">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
@click="handleOpenCreateGroupModal"
|
||||
>
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新建分组
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div class="material-content">
|
||||
<!-- 头部操作栏 -->
|
||||
<div class="material-content__header">
|
||||
<h2 class="material-content__title">
|
||||
{{ getContentTitle() }}
|
||||
</h2>
|
||||
<div class="material-content__actions">
|
||||
<a-button type="primary" @click="handleOpenUploadModal">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
上传素材
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="selectedFileIds.length > 0"
|
||||
type="primary"
|
||||
status="danger"
|
||||
@click="handleBatchDelete"
|
||||
>
|
||||
批量删除 ({{ selectedFileIds.length }})
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
@click="$router.push('/material/mix')"
|
||||
>
|
||||
素材混剪
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="material-content__search">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名"
|
||||
style="width: 300px"
|
||||
allow-clear
|
||||
@press-enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
<a-button type="primary" @click="handleSearch">搜索</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="material-content__list">
|
||||
<a-spin :spinning="loading" tip="加载中..." style="width: 100%; min-height: 400px;">
|
||||
<template v-if="fileList.length > 0">
|
||||
<div class="material-grid">
|
||||
<div
|
||||
v-for="file in fileList"
|
||||
:key="file.id"
|
||||
class="material-item"
|
||||
:class="{ 'material-item--selected': selectedFileIds.includes(file.id) }"
|
||||
@click="handleFileClick(file)"
|
||||
>
|
||||
<div class="material-item__content">
|
||||
<!-- 预览图 -->
|
||||
<div class="material-item__preview">
|
||||
<img
|
||||
v-if="file.coverBase64"
|
||||
:src="file.coverBase64"
|
||||
:alt="file.fileName"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<div v-else class="material-item__preview-placeholder">
|
||||
<FileOutlined />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<div class="material-item__info">
|
||||
<div class="material-item__name" :title="file.fileName">
|
||||
{{ file.fileName }}
|
||||
</div>
|
||||
<div class="material-item__meta">
|
||||
<span class="material-item__size">{{ formatFileSize(file.fileSize) }}</span>
|
||||
<span class="material-item__time">{{ formatDate(file.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择框 -->
|
||||
<div class="material-item__select">
|
||||
<a-checkbox
|
||||
:checked="selectedFileIds.includes(file.id)"
|
||||
@change="(checked) => handleFileSelectChange(file.id, checked)"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<a-empty v-else description="暂无素材" />
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="material-content__pagination">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:show-size-changer="true"
|
||||
:show-quick-jumper="true"
|
||||
:show-total="(total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`"
|
||||
@change="handlePageChange"
|
||||
@show-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传素材弹窗 -->
|
||||
<MaterialUploadModal
|
||||
v-model:visible="uploadModalVisible"
|
||||
:group-id="selectedGroupId"
|
||||
@confirm="handleFileUpload"
|
||||
/>
|
||||
|
||||
<!-- 新建分组弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="createGroupModalVisible"
|
||||
title="新建分组"
|
||||
@ok="handleCreateGroup"
|
||||
@cancel="createGroupModalVisible = false"
|
||||
>
|
||||
<a-form :model="createGroupForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
<a-form-item label="分组名称" name="name" :rules="[{ required: true, message: '请输入分组名称' }]">
|
||||
<a-input v-model:value="createGroupForm.name" placeholder="请输入分组名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="分组分类">
|
||||
<a-input v-model:value="createGroupForm.category" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="分组描述">
|
||||
<a-textarea v-model:value="createGroupForm.description" placeholder="请输入分组描述(可选)" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, watch } from 'vue';
|
||||
import {
|
||||
UploadOutlined,
|
||||
SearchOutlined,
|
||||
FileOutlined,
|
||||
FolderOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import MaterialUploadModal from '@/components/material/MaterialUploadModal.vue';
|
||||
import MaterialService, { MaterialGroupService } from '@/api/material';
|
||||
import { useUpload } from '@/composables/useUpload';
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const activeCategory = ref('MIX'); // MIX 或 DIGITAL_HUMAN
|
||||
const selectedGroupId = ref(null);
|
||||
const groupList = ref([]);
|
||||
const fileList = ref([]);
|
||||
const totalFileCount = ref(0);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
const uploadModalVisible = ref(false);
|
||||
const createGroupModalVisible = ref(false);
|
||||
|
||||
// Upload Hook
|
||||
const { upload } = useUpload()
|
||||
|
||||
const createGroupForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
category: 'MIX'
|
||||
});
|
||||
|
||||
// 选中的文件ID列表
|
||||
const selectedFileIds = ref([]);
|
||||
|
||||
// 分页信息
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 方法
|
||||
const handleCategoryChange = async (category) => {
|
||||
activeCategory.value = category;
|
||||
selectedGroupId.value = null;
|
||||
createGroupForm.category = category;
|
||||
await loadGroupList();
|
||||
await loadFileList();
|
||||
};
|
||||
|
||||
|
||||
const handleSelectGroup = (group) => {
|
||||
selectedGroupId.value = group.id;
|
||||
loadFileList();
|
||||
};
|
||||
|
||||
const handleOpenCreateGroupModal = () => {
|
||||
createGroupForm.name = '';
|
||||
createGroupForm.description = '';
|
||||
createGroupForm.category = activeCategory.value;
|
||||
createGroupModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleCreateGroup = async () => {
|
||||
try {
|
||||
await MaterialGroupService.createGroup(createGroupForm);
|
||||
message.success('分组创建成功');
|
||||
createGroupModalVisible.value = false;
|
||||
await loadGroupList();
|
||||
} catch (error) {
|
||||
message.error('分组创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const loadGroupList = async () => {
|
||||
try {
|
||||
const result = await MaterialGroupService.getGroupListByCategory(activeCategory.value);
|
||||
groupList.value = result.data || result || [];
|
||||
|
||||
// 默认选中第一个分组
|
||||
if (groupList.value.length > 0 && selectedGroupId.value === null) {
|
||||
selectedGroupId.value = groupList.value[0].id;
|
||||
}
|
||||
|
||||
await loadFileList();
|
||||
} catch (error) {
|
||||
console.error('加载分组列表失败:', error);
|
||||
// 如果按分类查询失败,尝试加载所有分组
|
||||
try {
|
||||
const allGroups = await MaterialGroupService.getGroupList();
|
||||
groupList.value = allGroups.data || allGroups || [];
|
||||
|
||||
// 默认选中第一个分组
|
||||
if (groupList.value.length > 0 && selectedGroupId.value === null) {
|
||||
selectedGroupId.value = groupList.value[0].id;
|
||||
}
|
||||
|
||||
await loadFileList();
|
||||
} catch (fallbackError) {
|
||||
console.error('加载所有分组也失败:', fallbackError);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadFileList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
pageNo: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
fileCategory: 'video', // 只查询视频文件
|
||||
fileName: searchKeyword.value || undefined,
|
||||
groupId: selectedGroupId.value || undefined
|
||||
};
|
||||
|
||||
const result = await MaterialService.getFilePage(params);
|
||||
fileList.value = result.data?.list || result.list || [];
|
||||
pagination.total = result.data?.total || result.total || 0;
|
||||
totalFileCount.value = result.data?.total || result.total || 0;
|
||||
} catch (error) {
|
||||
console.error('加载文件列表失败:', error);
|
||||
message.error('加载文件列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
loadFileList();
|
||||
};
|
||||
|
||||
const handlePageChange = (page, pageSize) => {
|
||||
pagination.current = page;
|
||||
pagination.pageSize = pageSize;
|
||||
loadFileList();
|
||||
};
|
||||
|
||||
const handlePageSizeChange = (current, size) => {
|
||||
pagination.current = 1;
|
||||
pagination.pageSize = size;
|
||||
loadFileList();
|
||||
};
|
||||
|
||||
const handleFileClick = (file) => {
|
||||
// 切换文件选中状态
|
||||
const isSelected = selectedFileIds.value.includes(file.id);
|
||||
if (isSelected) {
|
||||
// 如果已选中,则取消选中
|
||||
selectedFileIds.value = selectedFileIds.value.filter(id => id !== file.id);
|
||||
} else {
|
||||
// 如果未选中,则添加到选中列表
|
||||
if (!selectedFileIds.value.includes(file.id)) {
|
||||
selectedFileIds.value.push(file.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileSelectChange = (fileId, checked) => {
|
||||
if (checked) {
|
||||
if (!selectedFileIds.value.includes(fileId)) {
|
||||
selectedFileIds.value.push(fileId);
|
||||
}
|
||||
} else {
|
||||
selectedFileIds.value = selectedFileIds.value.filter(id => id !== fileId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenUploadModal = () => {
|
||||
uploadModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleFileUpload = async (filesWithCover, category, groupId) => {
|
||||
try {
|
||||
// 上传每个文件
|
||||
for (const fileWithCover of filesWithCover) {
|
||||
await upload(fileWithCover.file, {
|
||||
fileCategory: category,
|
||||
groupId, // 素材库模块使用groupId
|
||||
coverBase64: fileWithCover.coverBase64,
|
||||
onStart: () => {},
|
||||
onProgress: () => {},
|
||||
onSuccess: (id) => {
|
||||
console.log('文件上传成功:', id)
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(error.message || '上传失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
message.success(`成功上传 ${filesWithCover.length} 个文件`);
|
||||
uploadModalVisible.value = false;
|
||||
await loadFileList();
|
||||
await loadGroupList();
|
||||
} catch (error) {
|
||||
console.error("文件上传失败:", error);
|
||||
message.error("文件上传失败: " + (error.message || "未知错误"));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
// TODO: 实现批量删除
|
||||
message.info('批量删除功能待实现');
|
||||
};
|
||||
|
||||
const getContentTitle = () => {
|
||||
if (selectedGroupId.value === null) {
|
||||
return activeCategory.value === 'MIX' ? '混剪素材' : '数字人素材';
|
||||
}
|
||||
const group = groupList.value.find(g => g.id === selectedGroupId.value);
|
||||
return group ? group.name : '素材列表';
|
||||
};
|
||||
|
||||
const formatFileSize = (size) => {
|
||||
if (size < 1024) return size + ' B';
|
||||
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB';
|
||||
if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + ' MB';
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
|
||||
};
|
||||
|
||||
const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString();
|
||||
};
|
||||
|
||||
const handleImageError = (e) => {
|
||||
e.target.style.display = 'none';
|
||||
};
|
||||
|
||||
// 监听分类变化
|
||||
watch(activeCategory, () => {
|
||||
selectedFileIds.value = [];
|
||||
});
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadGroupList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.material-list-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.material-sidebar {
|
||||
width: 256px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 3px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.category-tab {
|
||||
flex: 1;
|
||||
padding: 8px 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border: 1px solid transparent;
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
background: rgba(24, 144, 255, 0.05);
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #fff;
|
||||
color: #1890ff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
margin-top: 16px;
|
||||
|
||||
&__header {
|
||||
padding: 8px 12px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-group-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 2px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-actions {
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.material-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
margin: 16px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__search {
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
flex: 1;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
padding: 16px 24px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.material-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.material-item {
|
||||
position: relative;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&--selected {
|
||||
border-color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
&__content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview-placeholder {
|
||||
font-size: 48px;
|
||||
color: #d9d9d9;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__select {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">下载</h2>
|
||||
<div class="bg-white p-4 rounded shadow">提供Windows与Mac安装包下载(占位)。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">帮助与指南</h2>
|
||||
<div class="bg-white p-4 rounded shadow">常见问题、操作指引与联系支持。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">主题设置</h2>
|
||||
<div class="bg-white p-4 rounded shadow">浅色/深色切换与品牌色配置(占位)。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">实时热点推送</h2>
|
||||
<div class="bg-white p-4 rounded shadow">榜单看板、订阅管理、趋势联动。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold">热点-文案创作</h2>
|
||||
<p class="text-gray-600 text-sm">入口参数预填自热度/预测面板。</p>
|
||||
<div class="bg-white p-4 rounded shadow">结果编辑器与模块一共享逻辑。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user