Commit 32d1f7d7 authored by lijin's avatar lijin

优化素材上传

parent 011cb0f5
......@@ -621,5 +621,23 @@ export function deleteYouTubeAccountById(params) {
params
})
}
export function fetchMaterialTags() {
return request({
url: process.env.PUTIN_API + '/material_tags/',
method: 'get'
})
}
export function addMaterial(data) {
return request({
url: process.env.PUTIN_API + '/material/youtube/addMaterial',
method: 'post',
data
})
}
// ----------------------------------------
<template>
<div>
<slot></slot>
<el-select
:value="value"
filterable
clearable
placeholder="请选择"
@change="handleChange"
>
<el-option
v-for="item in list"
:key="item.id"
:label="item.realName"
:value="item.id"
/>
</el-select>
</div>
</template>
<script>
import {
getMaterialDesigners,
} from '@/api/report';
export default {
name: "DesignerSelect",
model: {
prop: "value",
event: "change",
},
props: {
defaultValue: {
type: String,
required: false,
},
addAllOption: {
type: [Boolean, String],
required: false,
},
value: {
type: [String, Number],
},
},
data() {
return {
selectValue: parseInt(localStorage.getItem('uid')) || '',
list: [],
};
},
created() {
this.init();
},
// 子组件监控日期组件的值发生变化的时候,通过emit传给父组件,父组件用v-model接收。这样就把子组件的v-model功能,传给了父组件
watch: {
value(val) {
this.$emit("input", val);
},
},
methods: {
// 获取App名称配置列表
init: function() {
// this.selectValue = this.defaultValue;
getMaterialDesigners({
}).then((res) => {
this.list = res.result.data
this.$emit("list", this.list);
if (this.addAllOption == "true") {
this.list.unshift({
lable: "全部",
value: "",
});
}
});
},
handleChange(value) {
this.$emit("change", value);
this.$emit('input', value);
},
},
};
</script>
......@@ -218,11 +218,10 @@ export default {
// 构建成功文件的信息对象
const successFileInfo = {
name: file.name,
url: response.data.url || file.url,
cdnUrl: response.data.result.data.url,
type: file.type,
size: file.size,
response: response.data
cdnUrl: response.data.result.data.cdnUrl,
fileMd5: response.data.result.data.fileMd5,
type: response.data.result.data.type,
size: file.size
}
console.log("successFileInfo: ", successFileInfo)
// 更新成功文件列表
......
<template>
<div class="cascader-select">
<el-cascader
v-model="selectedValues"
:options="treeData"
:props="cascaderProps"
:placeholder="placeholder"
:show-all-levels="true"
:collapse-tags="true"
filterable
clearable
multiple
@change="handleChange"
>
<template slot-scope="{ node, data }">
<span>{{ data.name }}</span>
<span v-if="!node.isLeaf" class="custom-label">
({{ getChildLeafCount(data) }}个选项)
</span>
</template>
</el-cascader>
<!-- 选中标签的详细展示 -->
<div class="selected-tags" v-if="value.length">
<el-tag
v-for="id in value"
:key="id"
closable
size="small"
class="tag-item"
@close="handleRemoveTag(id)"
>
{{ getNodeFullPath(id) }}
</el-tag>
</div>
</div>
</template>
<script>
import { fetchMaterialTags } from '@/api/report';
export default {
name: 'MaterialTagSelect',
props: {
value: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请选择标签...'
}
},
data() {
return {
selectedValues: [], // 用于el-cascader的选中值(包含完整路径)
cascaderProps: {
multiple: true,
checkStrictly: true, // 可以选择任意一级节点
value: 'id',
label: 'name',
children: 'children',
emitPath: true, // 返回完整路径
leaf: 'isLeaf'
},
tagData: []
}
},
computed: {
// 转换为树形结构
treeData() {
return this.buildTree(this.tagData)
}
},
created() {
this.fetchTags()
},
watch: {
// 监听外部value变化,更新内部选中状态
value: {
immediate: true,
handler(newVal) {
this.updateSelectedValues(newVal)
}
}
},
methods: {
fetchTags() {
fetchMaterialTags().then(res => {
if (res.status === 200) {
this.tagData = res.result.data.map(item => ({
id: item.id,
name: item.name,
parentId: item.parentId || null // 确保null值标准化处理
}));
} else {
this.$message.error('获取素材标签失败');
}
}).catch(error => {
console.error("请求失败:", error);
this.$message.error('请求异常');
});
},
// 构建树形数据
buildTree(items) {
const itemMap = new Map()
const result = []
// 创建所有节点
items.forEach(item => {
const node = {
id: item.id,
name: item.name,
parentId: item.parentId,
children: [],
isLeaf: true // 默认为叶子节点,后面会更新
}
itemMap.set(item.id, node)
})
// 建立父子关系
items.forEach(item => {
const node = itemMap.get(item.id)
if (item.parentId === null) {
result.push(node)
} else {
const parent = itemMap.get(item.parentId)
if (parent) {
parent.children.push(node)
parent.isLeaf = false // 有子节点,不是叶子节点
}
}
})
return result
},
// 获取节点的所有叶子节点数量
getChildLeafCount(node) {
let count = 0
const countLeaves = (n) => {
if (n.isLeaf) {
count++
} else if (n.children) {
n.children.forEach(countLeaves)
}
}
countLeaves(node)
return count
},
// 处理选择变化
handleChange(values) {
// values 是一个数组,每个元素都是一个路径数组
const selectedIds = values
.map(path => path[path.length - 1]) // 获取每个路径的最后一个节点ID
.filter(id => this.isLeafNode(id)) // 只保留叶子节点
this.$emit('input', selectedIds)
this.$emit('change', selectedIds)
},
// 判断是否为叶子节点
isLeafNode(id) {
const findNode = (nodes) => {
for (const node of nodes) {
if (node.id === id) {
return node.isLeaf
}
if (node.children) {
const found = findNode(node.children)
if (found !== undefined) {
return found
}
}
}
}
return findNode(this.treeData)
},
// 获取节点完整路径名称
getNodeFullPath(id) {
const path = []
const findPath = (nodes) => {
for (const node of nodes) {
path.push(node.name)
if (node.id === id) {
return true
}
if (node.children && findPath(node.children)) {
return true
}
path.pop()
}
return false
}
findPath(this.treeData)
return path.join(' / ')
},
// 处理标签移除
handleRemoveTag(id) {
const newValue = this.value.filter(v => v !== id)
this.$emit('input', newValue)
this.$emit('change', newValue)
},
// 更新内部选中状态
updateSelectedValues(selectedIds) {
const values = []
const findPaths = (nodes, currentPath = []) => {
for (const node of nodes) {
const path = [...currentPath, node.id]
if (selectedIds.includes(node.id) && node.isLeaf) {
values.push(path)
}
if (node.children) {
findPaths(node.children, path)
}
}
}
findPaths(this.treeData)
this.selectedValues = values
}
}
}
</script>
<style scoped>
</style>
......@@ -120,6 +120,19 @@ export const constantRouterMap = [
name: "tools.Youtube",
component: () => import("@/views/createMaterial/AdMaterialManager"),
meta: { title: "Youtube管理", icon: "chart" }
},
{
path: "/materialUpload", // 资产上传
name: "materialUpload",
component: () => import('@/views/createMaterial/MaterialUpload'),
hidden: true
},
{
path: "/materialTag", // 素材标签管理
name: "materialTag",
component: () => import('@/views/materialTag'),
hidden: true
}
]
},
......@@ -134,19 +147,7 @@ export const constantRouterMap = [
}]
},
{
path: "/materialUpload", // 资产上传
name: "materialUpload",
component: () => import('@/views/createMaterial/MaterialUpload'),
hidden: true
},
{
path: "/materialTag", // 素材标签管理
name: "materialTag",
component: () => import('@/views/materialTag'),
hidden: true
}
];
Vue.use(Router);
......
<template>
<div class="upload-page">
<h2>文件上传</h2>
<!-- 使用上传组件 -->
<file-upload
:upload-url="uploadUrl"
:extra-data="extraData"
:headers="headers"
@update:files="handleFilesUpdate"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
@upload-complete="handleUploadComplete"
ref="uploadComponent"
>
<!-- 自定义上传按钮 -->
<template #trigger>
<el-button type="primary" icon="el-icon-upload">
选择图片或视频
</el-button>
</template>
</file-upload>
<el-form ref="uploadForm" :model="uploadForm" label-width="120px">
<el-form-item label="素材上传">
<!-- 使用上传组件 -->
<file-upload
:upload-url="uploadUrl"
:extra-data="extraData"
:headers="headers"
@update:files="handleFilesUpdate"
@upload-success="handleUploadSuccess"
@upload-error="handleUploadError"
@upload-complete="handleUploadComplete"
ref="uploadComponent"
>
<!-- 自定义上传按钮 -->
<template #trigger>
<el-button type="primary" icon="el-icon-upload">
选择图片或视频
</el-button>
</template>
</file-upload>
</el-form-item>
<el-form-item label="选择标签">
<material-tag-select
v-model="uploadForm.tags"
placeholder="请选择标签..."
@change="handleChange"
/>
</el-form-item>
<el-form-item label="设计师">
<desigher-select v-model="uploadForm.designer"
/>
</el-form-item>
<el-form-item label="目录" prop="directoryId" :rules="[{ required: true, message: '请选择目录', trigger: 'change' }]">
<el-cascader
v-model="selectedDirectoryIds"
:options="directories"
:props="{ expandTrigger: 'hover', value:'id',label:'name', children:'children' }"
clearable
@change="handleDirectoryChange"
placeholder="请选择目录"
style="width: 30%;"
></el-cascader>
</el-form-item>
<el-button @click="saveMaterial">提交</el-button>
</el-form>
<!-- 显示上传成功的文件列表 -->
<!-- <div class="success-files" v-if="successFiles.length > 0">-->
<!-- <div class="success-files" v-if="uploadForm.successFiles.length > 0">-->
<!-- <h3>已上传文件列表:</h3>-->
<!-- <el-table :data="successFiles" style="width: 100%">-->
<!-- <el-table :data="uploadForm.successFiles" style="width: 100%">-->
<!-- <el-table-column prop="name" label="文件名"></el-table-column>-->
<!-- <el-table-column prop="type" label="类型"></el-table-column>-->
<!-- <el-table-column prop="size" label="大小">-->
......@@ -51,34 +80,50 @@
<script>
import FileUpload from '../../components/FileUpload/index.vue'
import MaterialTagSelect from '../../components/MaterialTagSelect/index.vue'
import DesigherSelect from '../../components/DesignerSelect/index.vue'
import {
getMaterialDirectoriesTree,
addMaterial,
} from '@/api/report';
export default {
name: 'UploadPage',
components: {
FileUpload
FileUpload,
MaterialTagSelect,
DesigherSelect
},
data() {
return {
uploadUrl: 'http://localhost:8567/material/business/youtube/uploadVideo',
successFiles: [],
uploadUrl: 'http://localhost:8567/material/business/youtube/uploadToCDN',
extraData: {
tags: "abc, def",
director: '1',
resType: '1',
directoryId: '1'
},
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('token')
}
},
selectedDirectoryIds: [],
directories: [],
uploadForm: { // 上传视频表单数据
// file: null,
successFiles: [],
tags: [],
designer: null,
directoryId: null
},
}
},
created() {
this.fetchDirectories();
},
methods: {
// 处理文件列表更新
handleFilesUpdate(files) {
this.successFiles = files
this.uploadForm.successFiles = files
console.log("parent receive upload success", files)
},
......@@ -139,6 +184,32 @@ export default {
}
},
handleChange(value) {
console.log('选中的标签ID:', value)
},
fetchDirectories() {
getMaterialDirectoriesTree().then(res => {
if (res.status === 200) {
this.directories = res.result.data;
} else {
this.$message.error('获取目录失败');
}
});
},
handleDirectoryChange(value) {
this.uploadForm.directoryId = value[value.length - 1];
},
saveMaterial(){
addMaterial(this.uploadForm).then(res => {
if (res.status === 200) {
this.$message.success('保存成功');
} else {
this.$message.error('保存失败');
}
});
},
}
}
</script>
......
<template>
<div class="cascader-select">
<!-- 触发器按钮 -->
<el-popover
v-model="visible"
placement="bottom-start"
trigger="click"
:width="300"
>
<el-input
v-model="searchQuery"
size="small"
placeholder="搜索标签..."
prefix-icon="el-icon-search"
clearable
@clear="handleSearchClear"
class="mb-2"
/>
<!-- 面包屑导航 -->
<div v-if="currentPath.length" class="mb-2">
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-for="(item, index) in currentPath"
:key="item.id"
:class="{ 'is-link': index !== currentPath.length - 1 }"
@click.native="handlePathClick(index)"
>
{{ item.name }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<!-- 树形选择器 -->
<el-tree
ref="tree"
:data="currentLevelData"
:props="defaultProps"
:filter-node-method="filterNode"
node-key="id"
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<span slot-scope="{ node, data }" class="custom-tree-node">
<span>{{ node.label }}</span>
<span v-if="isLeaf(data)">
<el-checkbox
v-model="checkedMap[data.id]"
@change="(val) => handleCheck(data, val)"
@click.native.stop
/>
</span>
<i v-else class="el-icon-arrow-right" />
</span>
</el-tree>
<el-button
slot="reference"
:class="{ 'is-selected': selectedNodes.length }"
>
<span v-if="selectedNodes.length">
已选择 {{ selectedNodes.length }}
</span>
<span v-else>{{ placeholder }}</span>
<i class="el-icon-arrow-down el-icon--right" />
</el-button>
</el-popover>
<!-- 已选择标签展示区域 -->
<div class="selected-tags">
<el-tag
v-for="node in selectedNodes"
:key="node.id"
closable
size="small"
class="tag-item"
@close="handleRemoveTag(node)"
>
{{ getNodePath(node) }}
</el-tag>
</div>
</div>
</template>
<script>
export default {
name: 'CascaderSelect',
props: {
data: {
type: Array,
required: true,
default: () => []
},
value: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: '请选择标签...'
}
},
data() {
return {
visible: false,
searchQuery: '',
currentPath: [],
checkedMap: {},
defaultProps: {
children: 'children',
label: 'name'
}
}
},
computed: {
// 构建树形数据
treeData() {
return this.buildTree(this.data)
},
// 当前层级数据
currentLevelData() {
if (this.searchQuery) {
return this.filterTreeData(this.treeData, this.searchQuery)
}
if (this.currentPath.length === 0) {
return this.treeData
}
const current = this.currentPath[this.currentPath.length - 1]
return current.children || []
},
// 选中的节点
selectedNodes() {
return this.value.map(id => this.findNodeById(this.treeData, id)).filter(Boolean)
}
},
watch: {
value: {
immediate: true,
handler(val) {
// 更新选中状态
const checkedMap = {}
val.forEach(id => {
checkedMap[id] = true
})
this.checkedMap = checkedMap
}
},
searchQuery(val) {
this.$refs.tree && this.$refs.tree.filter(val)
}
},
methods: {
// 构建树形数据
buildTree(items) {
const itemMap = new Map()
const result = []
// 创建所有节点
items.forEach(item => {
itemMap.set(item.id, { ...item, children: [] })
})
// 建立父子关系
items.forEach(item => {
const node = itemMap.get(item.id)
if (item.parentId === null) {
result.push(node)
} else {
const parent = itemMap.get(item.parentId)
if (parent) {
parent.children.push(node)
}
}
})
return result
},
// 判断是否为叶子节点
isLeaf(node) {
return !node.children || node.children.length === 0
},
// 处理节点点击
handleNodeClick(data) {
if (!this.isLeaf(data)) {
this.currentPath.push(data)
}
},
// 处理路径点击
handlePathClick(index) {
this.currentPath = this.currentPath.slice(0, index + 1)
},
// 处理复选框变化
handleCheck(node, checked) {
const newValue = [...this.value]
if (checked) {
if (!newValue.includes(node.id)) {
newValue.push(node.id)
}
} else {
const index = newValue.indexOf(node.id)
if (index > -1) {
newValue.splice(index, 1)
}
}
this.$emit('input', newValue)
this.$emit('change', newValue)
},
// 处理标签移除
handleRemoveTag(node) {
const newValue = this.value.filter(id => id !== node.id)
this.$emit('input', newValue)
this.$emit('change', newValue)
},
// 获取节点完整路径
getNodePath(node) {
const path = this.findNodePath(this.treeData, node.id)
return path ? path.map(n => n.name).join(' / ') : ''
},
// 查找节点路径
findNodePath(nodes, targetId, path = []) {
for (const node of nodes) {
const newPath = [...path, node]
if (node.id === targetId) {
return newPath
}
if (node.children) {
const found = this.findNodePath(node.children, targetId, newPath)
if (found) {
return found
}
}
}
return null
},
// 根据ID查找节点
findNodeById(nodes, targetId) {
for (const node of nodes) {
if (node.id === targetId) {
return node
}
if (node.children) {
const found = this.findNodeById(node.children, targetId)
if (found) {
return found
}
}
}
return null
},
// 搜索过滤
filterNode(value, data) {
if (!value) return true
return data.name.indexOf(value) !== -1
},
// 过滤树数据
filterTreeData(nodes, query) {
return nodes.filter(node => {
if (node.name.indexOf(query) !== -1) {
return true
}
if (node.children) {
const filteredChildren = this.filterTreeData(node.children, query)
if (filteredChildren.length) {
node.children = filteredChildren
return true
}
}
return false
})
},
// 处理搜索清除
handleSearchClear() {
this.searchQuery = ''
this.currentPath = []
}
}
}
</script>
<style scoped>
.cascader-select {
display: flex;
flex-direction: column;
gap: 8px;
}
.custom-tree-node {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mb-2 {
margin-bottom: 8px;
}
.el-button.is-selected {
color: #409EFF;
border-color: #409EFF;
}
.el-breadcrumb__item.is-link {
cursor: pointer;
color: #409EFF;
}
.el-tree {
max-height: 300px;
overflow-y: auto;
}
</style>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment