Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Sign in / Register
Toggle navigation
Z
zxn-adputin
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Packages
Packages
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lijin
zxn-adputin
Commits
011cb0f5
Commit
011cb0f5
authored
Feb 24, 2025
by
lijin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
增加素材标签
parent
e6633eb4
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
1288 additions
and
1 deletion
+1288
-1
index.vue
src/components/FileUpload/index.vue
+440
-0
main.js
src/main.js
+1
-1
index.js
src/router/index.js
+14
-0
MaterialUpload.vue
src/views/createMaterial/MaterialUpload.vue
+158
-0
CascaderSelect.vue
src/views/materialTag/components/CascaderSelect.vue
+346
-0
index.vue
src/views/materialTag/index.vue
+329
-0
No files found.
src/components/FileUpload/index.vue
0 → 100644
View file @
011cb0f5
<
template
>
<div
class=
"upload-container"
>
<el-upload
ref=
"upload"
class=
"upload-area"
drag
action=
"#"
:auto-upload=
"false"
:show-file-list=
"false"
:on-change=
"handleFileChange"
:accept=
"accept"
multiple
>
<i
class=
"el-icon-upload"
></i>
<div
class=
"el-upload__text"
>
将文件拖到此处,或
<em>
点击上传
</em></div>
<div
class=
"el-upload__tip"
slot=
"tip"
>
只能上传图片和视频文件,且不超过500M
</div>
</el-upload>
<div
class=
"preview-list"
v-if=
"fileList.length > 0"
>
<div
v-for=
"(file, index) in fileList"
:key=
"index"
class=
"preview-item"
>
<div
class=
"delete-btn"
@
click=
"handleDelete(index)"
>
<i
class=
"el-icon-delete"
></i>
</div>
<div
v-if=
"file.type.includes('image')"
class=
"preview-content"
>
<img
v-if=
"file.status == 'success'"
:src=
"file.url"
:alt=
"file.name"
>
</div>
<div
v-else-if=
"file.type.includes('video')"
class=
"preview-content"
>
<video
v-if=
"file.status == 'success'"
:src=
"file.url"
controls
preload=
"metadata"
></video>
</div>
<el-progress
v-if=
"file.status === 'uploading' || file.status === 'waiting'"
type=
"circle"
:percentage=
"file.displayProgress"
:width=
"80"
class=
"progress"
:format=
"percentFormat"
></el-progress>
<div
v-else-if=
"file.status === 'success'"
class=
"upload-success"
>
<i
class=
"el-icon-check"
></i>
</div>
<div
v-else-if=
"file.status === 'error'"
class=
"upload-error"
>
<el-tooltip
:content=
"file.errorMessage"
placement=
"top"
>
<i
class=
"el-icon-close"
></i>
</el-tooltip>
</div>
<div
class=
"file-name"
>
{{
file
.
name
}}
</div>
</div>
</div>
</div>
</
template
>
<
script
>
import
axios
from
'axios'
export
default
{
name
:
'FileUpload'
,
props
:
{
// 接受的文件类型
accept
:
{
type
:
String
,
default
:
'image/*,video/*'
},
// 上传地址
uploadUrl
:
{
type
:
String
,
required
:
true
},
// 额外的上传参数
extraData
:
{
type
:
Object
,
default
:
()
=>
({})
},
// 自定义请求头
headers
:
{
type
:
Object
,
default
:
()
=>
({})
}
},
data
()
{
return
{
fileList
:
[],
uploadQueue
:
[],
successFiles
:
[],
// 存储上传成功的文件信息
isProcessing
:
false
,
// 是否有上传中的文件
}
},
methods
:
{
// 重置组件状态
reset
()
{
this
.
cancelAllUploads
()
this
.
fileList
=
[]
this
.
uploadQueue
=
[]
this
.
successFiles
=
[]
},
handleFileChange
(
file
)
{
const
fileObj
=
{
raw
:
file
.
raw
,
name
:
file
.
name
,
type
:
file
.
raw
.
type
,
url
:
URL
.
createObjectURL
(
file
.
raw
),
status
:
'waiting'
,
progress
:
0
,
displayProgress
:
0
,
errorMessage
:
''
,
progressTimer
:
null
,
size
:
file
.
raw
.
size
}
this
.
fileList
.
push
(
fileObj
)
this
.
uploadQueue
.
push
(
fileObj
)
this
.
processUploadQueue
()
// if (this.uploadQueue.length === 1) {
// this.processUploadQueue()
// }
},
handleDelete
(
index
)
{
const
file
=
this
.
fileList
[
index
]
if
(
file
.
progressTimer
)
{
clearInterval
(
file
.
progressTimer
)
}
if
(
file
.
cancelToken
)
{
file
.
cancelToken
.
cancel
(
'用户取消上传'
)
}
URL
.
revokeObjectURL
(
file
.
url
)
this
.
fileList
.
splice
(
index
,
1
)
// 如果是已经上传成功的文件,也从成功列表中移除
const
successIndex
=
this
.
successFiles
.
findIndex
(
f
=>
f
.
url
===
file
.
url
)
if
(
successIndex
>
-
1
)
{
this
.
successFiles
.
splice
(
successIndex
,
1
)
this
.
$emit
(
'update:files'
,
[...
this
.
successFiles
])
}
const
queueIndex
=
this
.
uploadQueue
.
indexOf
(
file
)
if
(
queueIndex
>
-
1
)
{
this
.
uploadQueue
.
splice
(
queueIndex
,
1
)
}
},
startProgressSimulation
(
file
)
{
let
simulatedProgress
=
0
const
maxSimulatedProgress
=
95
file
.
progressTimer
=
setInterval
(()
=>
{
if
(
simulatedProgress
<
maxSimulatedProgress
)
{
const
remainingProgress
=
maxSimulatedProgress
-
simulatedProgress
const
increment
=
Math
.
random
()
*
(
remainingProgress
*
0.1
)
simulatedProgress
+=
increment
simulatedProgress
=
Math
.
min
(
simulatedProgress
,
maxSimulatedProgress
)
file
.
displayProgress
=
Math
.
floor
(
simulatedProgress
)
}
},
200
+
Math
.
random
()
*
300
)
},
async
processUploadQueue
()
{
while
(
this
.
uploadQueue
.
length
>
0
)
{
if
(
this
.
isProcessing
)
return
this
.
isProcessing
=
true
await
this
.
processSingleFile
(
this
.
uploadQueue
[
0
])
this
.
uploadQueue
.
shift
()
this
.
isProcessing
=
false
}
// 所有文件上传完成后触发事件
if
(
this
.
uploadQueue
.
length
===
0
)
{
this
.
$emit
(
'upload-complete'
,
this
.
successFiles
)
}
},
async
processSingleFile
(
file
){
file
.
status
=
'uploading'
this
.
startProgressSimulation
(
file
)
try
{
const
response
=
await
this
.
uploadFile
(
file
)
if
(
file
.
progressTimer
)
{
clearInterval
(
file
.
progressTimer
)
}
// 校验业务状态码
if
(
response
.
data
.
status
!==
200
)
{
// 假设1表示成功
file
.
displayProgress
=
100
file
.
status
=
'error'
this
.
$emit
(
'upload-error'
,
{
file
:
file
,
error
:
new
Error
(
response
.
data
.
msg
||
'业务状态异常'
)
})
console
.
log
(
"文件上传失败,返回信息:"
,
response
.
data
)
}
else
{
file
.
displayProgress
=
100
file
.
status
=
'success'
console
.
log
(
"response: "
,
response
)
// 构建成功文件的信息对象
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
}
console
.
log
(
"successFileInfo: "
,
successFileInfo
)
// 更新成功文件列表
this
.
successFiles
.
push
(
successFileInfo
)
// 触发更新事件
this
.
$emit
(
'update:files'
,
[...
this
.
successFiles
])
this
.
$emit
(
'upload-success'
,
successFileInfo
)
}
}
catch
(
error
)
{
if
(
file
.
progressTimer
)
{
clearInterval
(
file
.
progressTimer
)
}
if
(
!
axios
.
isCancel
(
error
))
{
file
.
status
=
'error'
// file.errorMessage = error.response?.data?.message || '上传失败'
this
.
$emit
(
'upload-error'
,
{
file
:
file
,
error
:
error
})
}
}
},
uploadFile
(
file
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
const
formData
=
new
FormData
()
formData
.
append
(
'files'
,
file
.
raw
)
// 添加额外的上传参数
Object
.
entries
(
this
.
extraData
).
forEach
(([
key
,
value
])
=>
{
formData
.
append
(
key
,
value
)
})
const
CancelToken
=
axios
.
CancelToken
file
.
cancelToken
=
CancelToken
.
source
()
axios
.
post
(
this
.
uploadUrl
,
formData
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
,
...
this
.
headers
},
cancelToken
:
file
.
cancelToken
.
token
,
onUploadProgress
:
(
progressEvent
)
=>
{
if
(
progressEvent
.
lengthComputable
)
{
file
.
progress
=
Math
.
round
(
(
progressEvent
.
loaded
*
100
)
/
progressEvent
.
total
)
}
}
}
)
.
then
(
resolve
)
.
catch
(
reject
)
})
},
cancelAllUploads
()
{
this
.
fileList
.
forEach
(
file
=>
{
if
(
file
.
progressTimer
)
{
clearInterval
(
file
.
progressTimer
)
}
if
(
file
.
status
===
'uploading'
&&
file
.
cancelToken
)
{
file
.
cancelToken
.
cancel
(
'批量取消上传'
)
}
})
},
percentFormat
(
percent
){
return
percent
==
0
?
'等待中'
:
`
${
percent
}
%`
},
beforeDestroy
()
{
this
.
cancelAllUploads
()
this
.
fileList
.
forEach
(
file
=>
{
if
(
file
.
url
&&
file
.
url
.
startsWith
(
'blob:'
))
{
URL
.
revokeObjectURL
(
file
.
url
)
}
})
}
},
}
</
script
>
<
style
scoped
>
/* 样式代码保持不变 */
.upload-container
{
padding
:
20px
;
}
.preview-list
{
display
:
flex
;
flex-wrap
:
wrap
;
margin-top
:
20px
;
gap
:
20px
;
}
.preview-item
{
position
:
relative
;
width
:
200px
;
height
:
200px
;
border
:
1px
solid
#dcdfe6
;
border-radius
:
4px
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
}
.upload-area
{
position
:
relative
;
width
:
200px
;
height
:
200px
;
border
:
1px
solid
#dcdfe6
;
border-radius
:
4px
;
overflow
:
hidden
;
display
:
flex
;
flex-direction
:
column
;
align-items
:
center
;
}
.preview-content
{
width
:
100%
;
height
:
150px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
background-color
:
#f5f7fa
;
}
.preview-content
img
,
.preview-content
video
{
max-width
:
100%
;
max-height
:
100%
;
object-fit
:
contain
;
}
.delete-btn
{
position
:
absolute
;
top
:
5px
;
right
:
5px
;
width
:
24px
;
height
:
24px
;
background-color
:
rgba
(
0
,
0
,
0
,
0.5
);
border-radius
:
50%
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
z-index
:
1
;
}
.delete-btn
i
{
color
:
white
;
font-size
:
14px
;
}
.progress
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
background-color
:
rgba
(
255
,
255
,
255
,
0.9
);
border-radius
:
50%
;
padding
:
10px
;
}
.upload-success
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
color
:
#67c23a
;
font-size
:
30px
;
}
.upload-error
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
color
:
#f56c6c
;
font-size
:
30px
;
cursor
:
pointer
;
}
upload-waiting
{
position
:
absolute
;
top
:
50%
;
left
:
50%
;
transform
:
translate
(
-50%
,
-50%
);
color
:
#f56c6c
;
font-size
:
30px
;
cursor
:
pointer
;
}
.file-name
{
margin-top
:
auto
;
padding
:
8px
;
width
:
100%
;
text-align
:
center
;
font-size
:
12px
;
color
:
#606266
;
background-color
:
#f5f7fa
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
</
style
>
src/main.js
View file @
011cb0f5
...
...
@@ -47,7 +47,7 @@ Vue.filter("toFixed", function (price, limit) {
});
router
.
beforeEach
((
to
,
from
,
next
)
=>
{
if
(
to
.
path
==
"/login"
)
{
if
(
to
.
path
==
"/login"
||
to
.
path
==
"/materialUpload"
||
to
.
path
==
"/materialTag"
)
{
next
();
}
else
{
gatewayUserRouters
().
then
(
res
=>
{
...
...
src/router/index.js
View file @
011cb0f5
...
...
@@ -132,6 +132,20 @@ export const constantRouterMap = [
path
:
''
,
component
:
()
=>
import
(
'@/views/layout/components/Sidebar/redirect'
)
}]
},
{
path
:
"/materialUpload"
,
// 资产上传
name
:
"materialUpload"
,
component
:
()
=>
import
(
'@/views/createMaterial/MaterialUpload'
),
hidden
:
true
},
{
path
:
"/materialTag"
,
// 素材标签管理
name
:
"materialTag"
,
component
:
()
=>
import
(
'@/views/materialTag'
),
hidden
:
true
}
];
...
...
src/views/createMaterial/MaterialUpload.vue
0 → 100644
View file @
011cb0f5
<
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>
<!-- 显示上传成功的文件列表 -->
<!-- <div class="success-files" v-if="successFiles.length > 0">-->
<!-- <h3>已上传文件列表:</h3>-->
<!-- <el-table :data="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="大小">-->
<!-- <template slot-scope="scope">-->
<!-- {{ formatFileSize(scope.row.size) }}-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- <el-table-column label="预览">-->
<!-- <template slot-scope="scope">-->
<!-- <el-button type="text" @click="previewFile(scope.row)">-->
<!-- 预览-->
<!-- </el-button>-->
<!-- </template>-->
<!-- </el-table-column>-->
<!-- </el-table>-->
<!-- <!– 清空按钮 –>-->
<!-- <el-button type="danger" @click="clearFiles" class="clear-btn">-->
<!-- 清空文件-->
<!-- </el-button>-->
<!-- </div>-->
</div>
</template>
<
script
>
import
FileUpload
from
'../../components/FileUpload/index.vue'
export
default
{
name
:
'UploadPage'
,
components
:
{
FileUpload
},
data
()
{
return
{
uploadUrl
:
'http://localhost:8567/material/business/youtube/uploadVideo'
,
successFiles
:
[],
extraData
:
{
tags
:
"abc, def"
,
director
:
'1'
,
resType
:
'1'
,
directoryId
:
'1'
},
headers
:
{
'Authorization'
:
'Bearer '
+
localStorage
.
getItem
(
'token'
)
}
}
},
methods
:
{
// 处理文件列表更新
handleFilesUpdate
(
files
)
{
this
.
successFiles
=
files
console
.
log
(
"parent receive upload success"
,
files
)
},
// 处理单个文件上传成功
handleUploadSuccess
(
file
)
{
// this.$message.success(`文件 ${file.name} 上传成功`)
},
// 处理上传错误
handleUploadError
({
file
,
error
})
{
// this.$message.error(`文件 ${file.name} 上传失败:${error.message}`)
},
// 处理所有文件上传完成
handleUploadComplete
(
files
)
{
// this.$message.success(`所有文件上传完成,共 ${files.length} 个文件`)
},
// 格式化文件大小
formatFileSize
(
bytes
)
{
if
(
bytes
===
0
)
return
'0 B'
const
k
=
1024
const
sizes
=
[
'B'
,
'KB'
,
'MB'
,
'GB'
,
'TB'
]
const
i
=
Math
.
floor
(
Math
.
log
(
bytes
)
/
Math
.
log
(
k
))
return
parseFloat
((
bytes
/
Math
.
pow
(
k
,
i
)).
toFixed
(
2
))
+
' '
+
sizes
[
i
]
},
// 预览文件
previewFile
(
file
)
{
if
(
file
.
type
.
includes
(
'image'
))
{
// 使用 Element UI 的 Image 预览
const
h
=
this
.
$createElement
this
.
$msgbox
({
title
:
file
.
name
,
message
:
h
(
'img'
,
{
attrs
:
{
src
:
file
.
url
,
style
:
'max-width: 100%'
}
}),
showCancelButton
:
false
,
confirmButtonText
:
'关闭'
})
}
else
if
(
file
.
type
.
includes
(
'video'
))
{
// 使用自定义对话框预览视频
this
.
$msgbox
({
title
:
file
.
name
,
message
:
h
(
'video'
,
{
attrs
:
{
src
:
file
.
url
,
controls
:
true
,
style
:
'max-width: 100%'
}
}),
showCancelButton
:
false
,
confirmButtonText
:
'关闭'
})
}
},
}
}
</
script
>
<
style
scoped
>
.upload-page
{
padding
:
20px
;
}
.success-files
{
margin-top
:
30px
;
}
.clear-btn
{
margin-top
:
20px
;
}
</
style
>
src/views/materialTag/components/CascaderSelect.vue
0 → 100644
View file @
011cb0f5
<
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
src/views/materialTag/index.vue
0 → 100644
View file @
011cb0f5
<
template
>
<div
class=
"tag-management"
>
<el-row
:gutter=
"20"
>
<!-- 左侧树形结构 -->
<el-col
:span=
"6"
>
<el-card
class=
"tree-card"
>
<div
slot=
"header"
class=
"card-header"
>
<span>
标签树形结构
</span>
<el-button
type=
"text"
@
click=
"handleAddRoot"
>
添加根标签
</el-button>
</div>
<el-tree
:data=
"treeData"
:props=
"defaultProps"
@
node-click=
"handleNodeClick"
default-expand-all
></el-tree>
</el-card>
</el-col>
<!-- 右侧表单和列表 -->
<el-col
:span=
"18"
>
<el-card>
<div
slot=
"header"
class=
"card-header"
>
<span>
标签列表
</span>
<el-button
type=
"primary"
size=
"small"
@
click=
"handleAdd"
>
新增标签
</el-button>
</div>
<!-- 搜索表单 -->
<el-form
:inline=
"true"
:model=
"searchForm"
class=
"search-form"
>
<el-form-item
label=
"标签名称"
>
<el-input
v-model=
"searchForm.name"
placeholder=
"请输入标签名称"
></el-input>
</el-form-item>
<el-form-item>
<el-button
type=
"primary"
@
click=
"handleSearch"
>
查询
</el-button>
<el-button
@
click=
"resetSearch"
>
重置
</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
:data=
"tableData"
border
style=
"width: 100%"
>
<el-table-column
prop=
"id"
label=
"ID"
width=
"80"
></el-table-column>
<el-table-column
prop=
"name"
label=
"标签名称"
></el-table-column>
<el-table-column
prop=
"remark"
label=
"标签备注1"
></el-table-column>
<el-table-column
prop=
"remark2"
label=
"标签备注2"
></el-table-column>
<el-table-column
prop=
"created_at"
label=
"创建时间"
width=
"180"
></el-table-column>
<el-table-column
label=
"操作"
width=
"200"
>
<template
slot-scope=
"scope"
>
<el-button
type=
"text"
@
click=
"handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type=
"text"
@
click=
"handleAddChild(scope.row)"
>
添加子标签
</el-button>
<el-button
type=
"text"
class=
"delete-btn"
@
click=
"handleDelete(scope.row)"
>
删除
</el-button>
</
template
>
</el-table-column>
</el-table>
<!-- 分页 -->
<div
class=
"pagination-container"
>
<el-pagination
@
size-change=
"handleSizeChange"
@
current-change=
"handleCurrentChange"
:current-page=
"pagination.currentPage"
:page-sizes=
"[10, 20, 50, 100]"
:page-size=
"pagination.pageSize"
layout=
"total, sizes, prev, pager, next, jumper"
:total=
"pagination.total"
>
</el-pagination>
</div>
</el-card>
</el-col>
</el-row>
<!-- 新增/编辑对话框 -->
<el-dialog
:title=
"dialogTitle"
:visible
.
sync=
"dialogVisible"
width=
"500px"
>
<el-form
:model=
"form"
:rules=
"rules"
ref=
"form"
label-width=
"100px"
>
<el-form-item
label=
"标签名称"
prop=
"name"
>
<el-input
v-model=
"form.name"
></el-input>
</el-form-item>
<el-form-item
label=
"标签备注1"
prop=
"remark"
>
<el-input
v-model=
"form.remark"
></el-input>
</el-form-item>
<el-form-item
label=
"标签备注2"
prop=
"remark2"
>
<el-input
v-model=
"form.remark2"
></el-input>
</el-form-item>
</el-form>
<div
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click=
"dialogVisible = false"
>
取 消
</el-button>
<el-button
type=
"primary"
@
click=
"submitForm"
>
确 定
</el-button>
</div>
</el-dialog>
</div>
</template>
<
script
>
export
default
{
name
:
'TagManagement'
,
data
()
{
return
{
// 树形数据
treeData
:
[],
defaultProps
:
{
children
:
'children'
,
label
:
'name'
},
// 搜索表单
searchForm
:
{
name
:
''
},
// 表格数据
tableData
:
[],
// 分页
pagination
:
{
currentPage
:
1
,
pageSize
:
10
,
total
:
0
},
// 对话框
dialogVisible
:
false
,
dialogTitle
:
''
,
form
:
{
id
:
null
,
name
:
''
,
remark
:
''
,
remark2
:
''
,
parent_id
:
null
},
// 表单验证规则
rules
:
{
name
:
[
{
required
:
true
,
message
:
'请输入标签名称'
,
trigger
:
'blur'
},
{
min
:
1
,
max
:
50
,
message
:
'长度在 1 到 50 个字符'
,
trigger
:
'blur'
}
]
},
// 当前选中的节点
currentNode
:
null
}
},
created
()
{
this
.
fetchData
()
this
.
fetchTreeData
()
},
methods
:
{
// 获取表格数据
async
fetchData
()
{
try
{
// 这里替换为实际的API调用
const
response
=
await
this
.
axios
.
get
(
'/api/material-tags'
,
{
params
:
{
page
:
this
.
pagination
.
currentPage
,
pageSize
:
this
.
pagination
.
pageSize
,
name
:
this
.
searchForm
.
name
}
})
this
.
tableData
=
response
.
data
.
list
this
.
pagination
.
total
=
response
.
data
.
total
}
catch
(
error
)
{
this
.
$message
.
error
(
'获取数据失败'
)
}
},
// 获取树形数据
async
fetchTreeData
()
{
try
{
// 这里替换为实际的API调用
const
response
=
await
this
.
axios
.
get
(
'/api/material-tags/tree'
)
this
.
treeData
=
response
.
data
}
catch
(
error
)
{
this
.
$message
.
error
(
'获取树形数据失败'
)
}
},
// 处理搜索
handleSearch
()
{
this
.
pagination
.
currentPage
=
1
this
.
fetchData
()
},
// 重置搜索
resetSearch
()
{
this
.
searchForm
=
{
name
:
''
}
this
.
handleSearch
()
},
// 处理分页大小变化
handleSizeChange
(
val
)
{
this
.
pagination
.
pageSize
=
val
this
.
fetchData
()
},
// 处理页码变化
handleCurrentChange
(
val
)
{
this
.
pagination
.
currentPage
=
val
this
.
fetchData
()
},
// 处理节点点击
handleNodeClick
(
data
)
{
this
.
currentNode
=
data
this
.
searchForm
.
name
=
''
this
.
fetchData
()
},
// 新增标签
handleAdd
()
{
this
.
dialogTitle
=
'新增标签'
this
.
form
=
{
id
:
null
,
name
:
''
,
remark
:
''
,
remark2
:
''
,
parent_id
:
this
.
currentNode
?
this
.
currentNode
.
id
:
null
}
this
.
dialogVisible
=
true
},
// 添加根标签
handleAddRoot
()
{
this
.
dialogTitle
=
'添加根标签'
this
.
form
=
{
id
:
null
,
name
:
''
,
remark
:
''
,
remark2
:
''
,
parent_id
:
null
}
this
.
dialogVisible
=
true
},
// 添加子标签
handleAddChild
(
row
)
{
this
.
dialogTitle
=
'添加子标签'
this
.
form
=
{
id
:
null
,
name
:
''
,
remark
:
''
,
remark2
:
''
,
parent_id
:
row
.
id
}
this
.
dialogVisible
=
true
},
// 编辑标签
handleEdit
(
row
)
{
this
.
dialogTitle
=
'编辑标签'
this
.
form
=
{
...
row
}
this
.
dialogVisible
=
true
},
// 删除标签
handleDelete
(
row
)
{
this
.
$confirm
(
'确认删除该标签吗?'
,
'提示'
,
{
type
:
'warning'
}).
then
(
async
()
=>
{
try
{
// 这里替换为实际的API调用
await
this
.
axios
.
delete
(
`/api/material-tags/
${
row
.
id
}
`
)
this
.
$message
.
success
(
'删除成功'
)
this
.
fetchData
()
this
.
fetchTreeData
()
}
catch
(
error
)
{
this
.
$message
.
error
(
'删除失败'
)
}
}).
catch
(()
=>
{})
},
// 提交表单
submitForm
()
{
this
.
$refs
.
form
.
validate
(
async
(
valid
)
=>
{
if
(
valid
)
{
try
{
if
(
this
.
form
.
id
)
{
// 编辑
await
this
.
axios
.
put
(
`/api/material-tags/
${
this
.
form
.
id
}
`
,
this
.
form
)
}
else
{
// 新增
await
this
.
axios
.
post
(
'/api/material-tags'
,
this
.
form
)
}
this
.
$message
.
success
(
'保存成功'
)
this
.
dialogVisible
=
false
this
.
fetchData
()
this
.
fetchTreeData
()
}
catch
(
error
)
{
this
.
$message
.
error
(
'保存失败'
)
}
}
})
}
}
}
</
script
>
<
style
scoped
>
.tag-management
{
padding
:
20px
;
}
.tree-card
{
min-height
:
400px
;
}
.card-header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
}
.search-form
{
margin-bottom
:
20px
;
}
.pagination-container
{
margin-top
:
20px
;
text-align
:
right
;
}
.delete-btn
{
color
:
#F56C6C
;
}
</
style
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment