Commit cfd7cac8 by qinjianhui

feat: 自定义cascader组件封装

parent f916b74e
<template>
<div class="custom-cascader-wrapper">
<el-input
v-model="displayText"
placeholder="请选择分类"
size="medium"
:clearable="clearable"
@focus="toggleDropdown">
<i
slot="suffix"
class="el-input__icon el-icon-arrow-up"
:style="{
transform: isVisible ? 'rotate(0deg)' : 'rotate(180deg)',
transition: 'transform 0.2s'
}"></i>
</el-input>
<transition name="cascader-dropdown">
<div v-show="isVisible" class="cascader-dropdown">
<div class="cascader-panels" v-if="panels.length > 0">
<div
v-for="(panel, panelIndex) in panels"
:key="panelIndex"
class="cascader-panel">
<div class="search-container">
<el-input
v-model="panel.searchText"
type="text"
class="search-input"
placeholder="搜索内容"
@input="handleSearch(panelIndex)"
@focus="handlePanelFocus"
size="small">
<i slot="suffix" class="el-input__icon el-icon-search"></i>
</el-input>
</div>
<div class="options-container">
<div
v-for="(option, optionIndex) in panel.filteredOptions"
:key="optionIndex"
class="option-item"
:class="{
selected: panel.selectedIndex === optionIndex,
'has-children':
option.childId ||
(option.children && option.children.length > 0)
}"
@click="handleOptionClick(panelIndex, optionIndex, option)">
<div class="option-item-left">
<span class="option-text">{{ option.browseNodeName }}</span>
<div v-if="option.productType" class="product-type">
Product Type: {{ option.productType }}
</div>
</div>
<i
v-if="
option.childId ||
(option.children && option.children.length > 0)
"
class="el-icon-arrow-right expand-icon"></i>
</div>
</div>
</div>
</div>
<div v-else class="no-data">暂无数据</div>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'CustomCascader',
model: {
prop: 'value',
event: 'change'
},
props: {
value: {
type: Array,
default: () => []
},
// 数据源
options: {
type: Array,
default: () => []
},
// 是否支持搜索
searchable: {
type: Boolean,
default: true
},
// 加载子节点的回调函数
loadData: {
type: Function,
default: null
},
// 占位符
placeholder: {
type: String,
default: '请选择'
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 分隔符
separator: {
type: String,
default: ' / '
},
clearable: {
type: Boolean,
default: true
}
},
data() {
return {
panels: [],
selectedPath: [],
isVisible: false,
selectedLabels: [],
displayText: '',
lastSelectedPathId: '',
isInternalChange: false
}
},
watch: {
options: {
handler() {
if (!this.value || this.value.length === 0) {
this.initPanels()
} else {
this.updatePanelsFromValue(this.value)
}
},
immediate: true
},
value: {
handler(newValue) {
if (!this.isInternalChange) {
this.updatePanelsFromValue(newValue)
}
this.isInternalChange = false
},
immediate: true
}
},
mounted() {
document.addEventListener('click', this.handleDocumentClick, true)
},
methods: {
async updatePanelsFromValue(value) {
if (!value || value.length === 0) {
this.panels = []
this.selectedPath = []
this.selectedLabels = []
this.displayText = ''
this.lastSelectedPathId = ''
this.initPanels()
return
}
// 清空所有状态
this.panels = []
this.selectedPath = []
this.selectedLabels = []
this.displayText = ''
if (!this.options || this.options.length === 0) {
return
}
this.initPanels()
// 逐级构建面板
for (let i = 0; i < value.length; i++) {
const currentValue = value[i]
const currentPanel = this.panels[i]
if (!currentPanel || !currentPanel.options) {
break
}
// 在当前面板中查找对应的选项
const currentOption = currentPanel.options.find(
(option) => option.browseNodeId === currentValue
)
if (!currentOption) {
break
}
// 更新选中状态
const optionIndex = currentPanel.options.findIndex(
(option) => option.browseNodeId === currentValue
)
currentPanel.selectedIndex = optionIndex
// 更新路径和标签
this.selectedPath = this.selectedPath.slice(0, i)
this.selectedPath.push(currentValue)
this.selectedLabels = this.selectedLabels.slice(0, i)
this.selectedLabels.push(currentOption.browseNodeName)
if (i < value.length - 1 && currentOption.childId) {
try {
await this.loadChildPanel(currentOption)
} catch (error) {
console.error('加载子面板失败:', error)
break
}
} else {
// 最后一级或没有子节点
this.lastSelectedPathId = currentValue
this.$emit('onUpdataValue', {
val: this.selectedPath,
resetCategoryAttributes: false
})
this.displayText = this.selectedLabels.join(this.separator)
break
}
}
},
// 加载子面板
loadChildPanel(parentOption) {
return new Promise((resolve, reject) => {
if (!this.loadData) {
reject(new Error('loadData方法未提供'))
return
}
this.loadData(parentOption, (children) => {
if (children && children.length > 0) {
const currentPanelIndex = this.panels.length - 1
const expectedPanelIndex = this.selectedPath.length
if (currentPanelIndex >= expectedPanelIndex) {
this.panels[expectedPanelIndex] = {
options: children,
filteredOptions: [...children],
searchText: '',
selectedIndex: -1
}
} else {
this.addPanel(children)
}
resolve()
} else {
reject(new Error('没有子节点数据'))
}
})
})
},
toggleDropdown() {
if (this.disabled) return
this.isVisible = true
},
handlePanelFocus() {
this.isVisible = true
},
// 初始化面板
initPanels() {
if (this.panels.length === 0 && this.options && this.options.length > 0) {
this.panels.push({
options: [...this.options],
filteredOptions: [...this.options],
searchText: '',
selectedIndex: -1
})
}
},
handleSearch(panelIndex) {
const panel = this.panels[panelIndex]
if (!panel.searchText.trim()) {
panel.filteredOptions = [...panel.options]
} else {
panel.filteredOptions = panel.options.filter((option) =>
option.browseNodeName
.toLowerCase()
.includes(panel.searchText.toLowerCase())
)
}
},
async handleOptionClick(panelIndex, optionIndex, option) {
const panel = this.panels[panelIndex]
panel.selectedIndex = optionIndex
this.selectedPath = this.selectedPath.slice(0, panelIndex)
this.selectedPath.push(option.browseNodeId)
this.selectedLabels = this.selectedLabels.slice(0, panelIndex)
this.selectedLabels.push(option.browseNodeName)
this.panels = this.panels.slice(0, panelIndex + 1)
if (option.childId && this.lastSelectedPathId !== option.browseNodeId) {
this.loadData(option, (children) => {
if (children && children.length > 0) {
this.addPanel(children)
} else {
this.lastSelectedPathId = option.browseNodeId
this.isInternalChange = true
this.$emit('change', this.selectedPath)
this.displayText = this.selectedLabels.join(this.separator)
this.isVisible = false
}
})
} else {
this.lastSelectedPathId = option.browseNodeId
this.isInternalChange = true
this.$emit('change', this.selectedPath)
this.displayText = this.selectedLabels.join(this.separator)
this.isVisible = false
}
},
addPanel(options) {
const existingPanel = this.panels.find(
(panel) =>
panel.options &&
panel.options.length > 0 &&
panel.options[0].browseNodeId === options[0].browseNodeId
)
if (!existingPanel) {
this.panels.push({
options: options,
filteredOptions: [...options],
searchText: '',
selectedIndex: -1
})
// 添加面板后自动滚动到新面板
this.$nextTick(() => {
this.scrollToNewPanel()
})
}
},
// 滚动到新添加的面板
scrollToNewPanel() {
const panelsContainer = this.$el.querySelector('.cascader-panels')
if (panelsContainer) {
const newPanel = panelsContainer.lastElementChild
if (newPanel) {
const containerWidth = panelsContainer.clientWidth
const newPanelLeft = newPanel.offsetLeft
if (
newPanelLeft + newPanel.offsetWidth >
panelsContainer.scrollLeft + containerWidth
) {
panelsContainer.scrollTo({
left: newPanelLeft - containerWidth + newPanel.offsetWidth,
behavior: 'smooth'
})
}
}
}
},
handleDocumentClick(event) {
if (this.$el.contains(event.target)) {
return
}
const target = event.target
const isModal =
target.closest('.el-message-box') ||
target.closest('.el-dialog') ||
target.closest('.el-message') ||
target.closest('[role="dialog"]') ||
target.closest('.modal') ||
target.closest('.error-modal')
if (isModal) {
return
}
// 其他情况关闭级联选择器
this.isVisible = false
},
getCheckedNodes() {
return this.panels.map((panel, index) => {
return panel.options.find(
(option) => option.browseNodeId === this.selectedPath[index]
)
})
}
},
beforeDestroy() {
document.removeEventListener('click', this.handleDocumentClick, true)
}
}
</script>
<style lang="scss" scoped>
.custom-cascader-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.cascader-input {
position: relative;
display: flex;
align-items: center;
width: 100%;
height: 32px;
padding: 0 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
transition: border-color 0.2s;
&:hover {
border-color: #c0c4cc;
}
&.is-focus {
border-color: #409eff;
}
&.is-disabled {
background-color: #f5f7fa;
border-color: #e4e7ed;
color: #c0c4cc;
cursor: not-allowed;
}
}
.input-content {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-text {
color: #606266;
font-size: 14px;
}
.placeholder {
color: #c0c4cc;
font-size: 14px;
}
.arrow-icon {
margin-left: 8px;
font-size: 12px;
color: #c0c4cc;
transition: transform 0.2s;
&.is-reverse {
transform: rotate(180deg);
}
}
.cascader-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 2000;
margin-top: 4px;
border: 1px solid #dcdfe6;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.cascader-panels {
display: flex;
overflow-x: auto;
}
.cascader-panel {
// flex: 1;
width: 300px;
min-width: 300px;
border-right: 1px solid #ebeef5;
}
.search-container {
position: relative;
padding: 8px;
border-bottom: 1px solid #ebeef5;
}
.search-icon {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
color: #c0c4cc;
}
.options-container {
max-height: 360px;
overflow-y: auto;
}
.option-item {
padding: 8px 12px;
cursor: pointer;
display: flex;
justify-content: space-between;
&:hover {
background-color: #f5f7fa;
}
&.selected {
background-color: #e6f7ff;
color: #1890ff;
}
&.selected i {
color: #1890ff;
}
&.has-children {
.option-text {
flex: 1;
}
}
}
.option-item-left {
display: flex;
flex-direction: column;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.option-text {
font-size: 14px;
line-height: 1.4;
}
.expand-icon {
color: #333;
font-size: 14px;
margin-left: 8px;
}
.product-type {
font-size: 10px;
color: #909399;
background: #f5f7fa;
padding: 1px 4px;
border-radius: 2px;
}
// 滚动条样式
.options-container::-webkit-scrollbar {
width: 6px;
}
.options-container::-webkit-scrollbar-track {
background: #f1f1f1;
}
.options-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.options-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
// 下拉动画
.cascader-dropdown-enter-active,
.cascader-dropdown-leave-active {
transition: opacity 0.2s, transform 0.2s;
}
.cascader-dropdown-enter,
.cascader-dropdown-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.no-data {
padding: 10px;
text-align: center;
color: #999;
}
</style>
...@@ -68,12 +68,15 @@ ...@@ -68,12 +68,15 @@
</div> </div>
<div style="flex: 1"> <div style="flex: 1">
<!-- <CustomCascader <CustomCascader
ref="customCascaderRef"
:options="amCateCascaders" :options="amCateCascaders"
v-model="aliCatePathIds" v-model="aliCatePathIds"
:loadData="customLazyLoad" :loadData="customLazyLoad"
@change="customCascaderChange"></CustomCascader> --> :clearable="false"
<el-cascader @onUpdataValue="onUpdataValue"
@change="customCascaderChange"></CustomCascader>
<!-- <el-cascader
size="medium" size="medium"
:options="amCateCascaders" :options="amCateCascaders"
ref="amCateCascadersRef" ref="amCateCascadersRef"
...@@ -87,7 +90,7 @@ ...@@ -87,7 +90,7 @@
lazy: true, lazy: true,
lazyLoad: lazyLoad lazyLoad: lazyLoad
}" }"
@change="categoryTypeChange"></el-cascader> @change="categoryTypeChange"></el-cascader> -->
</div> </div>
</div> </div>
<div class="category-item"> <div class="category-item">
...@@ -260,12 +263,12 @@ ...@@ -260,12 +263,12 @@
</template> </template>
<script> <script>
import { get, post } from '@/common/api/axios' import { get, post } from '@/common/api/axios'
// import CustomCascader from '@/common/components/base/CustomCascader.vue' import CustomCascader from '@/common/components/base/CustomCascader.vue'
export default { export default {
name: 'amazonAttributeGrouping', name: 'amazonAttributeGrouping',
components: { components: {
// CustomCascader CustomCascader
}, },
data() { data() {
return { return {
...@@ -438,6 +441,10 @@ export default { ...@@ -438,6 +441,10 @@ export default {
}, },
async searchCategory() { async searchCategory() {
if (!this.keyWord) return if (!this.keyWord) return
if (!this.shopId) {
this.$message.warning('请选择店铺')
return
}
const loading = this.$loading({ const loading = this.$loading({
lock: true lock: true
}) })
...@@ -458,6 +465,10 @@ export default { ...@@ -458,6 +465,10 @@ export default {
loading.close() loading.close()
} }
}, },
onUpdataValue() {
console.log('onUpdataValue')
this.customCascaderChange()
},
productTypeChange(val) { productTypeChange(val) {
if (!val) return if (!val) return
const item = this.productTypeList.find( const item = this.productTypeList.find(
...@@ -468,15 +479,15 @@ export default { ...@@ -468,15 +479,15 @@ export default {
?.split(',') ?.split(',')
?.slice(1) ?.slice(1)
?.map((e) => Number(e)) ?.map((e) => Number(e))
this.initCate(categoryPath || [], () => { // this.initCate([], () => {
if (item.productType && categoryPath) { if (item.productType && categoryPath) {
this.aliCatePathIds = categoryPath this.aliCatePathIds = categoryPath
this.categoryTypeChange(item.productType, true) // this.categoryTypeChange(item.productType, true)
} else { } else {
this.jsonSchema = {} this.jsonSchema = {}
this.groupList = [] this.groupList = []
} }
}) // })
}, },
getCateAttrs(id, categoryFullPath, callback) { getCateAttrs(id, categoryFullPath, callback) {
if (!id || id.length === 0) return if (!id || id.length === 0) return
...@@ -562,10 +573,43 @@ export default { ...@@ -562,10 +573,43 @@ export default {
} }
}) })
}, },
customCascaderChange(val) { customCascaderChange() {
console.log(val) this.$nextTick(() => {
const nodes = this.$refs.customCascaderRef.getCheckedNodes()
let targetNode
if (this.aliCatePathIds) {
targetNode = nodes[nodes.length - 1]
}
if (targetNode) {
const { productTypeDefinitions, browsePathByName } = targetNode
this.getCateAttrs(
productTypeDefinitions,
browsePathByName,
({ list, groupProps }) => {
if (list) {
this.jsonSchema = list
groupProps.forEach((item) => {
item.propertyList.forEach((e) => {
this.$set(this.dataGroupForm, e.propertyNameEn, item.id)
})
})
this.cloneDataGroupForm = JSON.parse(
JSON.stringify(this.dataGroupForm)
)
} else {
this.jsonSchema = {}
this.dataGroupForm = {}
}
}
)
} else {
this.jsonSchema = {}
this.dataGroupForm = {}
}
})
}, },
async initCate(ids, callback) { async initCate(ids, callback) {
console.log('initCate', ids)
const shopId = this.shopList.find( const shopId = this.shopList.find(
(item) => item.id === this.shopId (item) => item.id === this.shopId
)?.marketplaceId )?.marketplaceId
...@@ -573,13 +617,13 @@ export default { ...@@ -573,13 +617,13 @@ export default {
lock: true lock: true
}) })
const arr = [ const arr = [
get('amazon/category/getChildListByBrowseNodeId', { get('manage/rest/amazon/category/getChildListByBrowseNodeId', {
marketplaceId: shopId marketplaceId: shopId
}) })
] ]
for (let i = 0; i < ids.length - 1; i++) { for (let i = 0; i < ids.length - 1; i++) {
arr.push( arr.push(
get('amazon/category/getChildListByBrowseNodeId', { get('manage/rest/amazon/category/getChildListByBrowseNodeId', {
browseNodeId: ids[i], browseNodeId: ids[i],
marketplaceId: shopId marketplaceId: shopId
}) })
...@@ -639,10 +683,13 @@ export default { ...@@ -639,10 +683,13 @@ export default {
} }
}, },
async customLazyLoad(node, callback) { async customLazyLoad(node, callback) {
const shopId = this.shopList.find(
(item) => item.id === this.shopId
)?.marketplaceId
try { try {
const res = await get('amazon/category/getChildListByBrowseNodeId', { const res = await get('manage/rest/amazon/category/getChildListByBrowseNodeId', {
browseNodeId: node.browseNodeId, browseNodeId: node.browseNodeId,
marketplaceId: this.shopId marketplaceId: shopId
}) })
if (res.code !== 200) return if (res.code !== 200) return
callback(res.data) callback(res.data)
...@@ -669,7 +716,12 @@ export default { ...@@ -669,7 +716,12 @@ export default {
this.initCate([]) this.initCate([])
}, },
async saveGrouping() { async saveGrouping() {
const nodes = this.$refs.amCateCascadersRef.getCheckedNodes() // const nodes = this.$refs.amCateCascadersRef.getCheckedNodes()
// let targetNode
// if (this.aliCatePathIds) {
// targetNode = nodes[nodes.length - 1]
// }
const nodes = this.$refs.customCascaderRef.getCheckedNodes()
let targetNode let targetNode
if (this.aliCatePathIds) { if (this.aliCatePathIds) {
targetNode = nodes[nodes.length - 1] targetNode = nodes[nodes.length - 1]
...@@ -683,15 +735,15 @@ export default { ...@@ -683,15 +735,15 @@ export default {
updatePropertyList.push({ updatePropertyList.push({
propertyNameEn: item, propertyNameEn: item,
groupId: this.dataGroupForm[item] || undefined, groupId: this.dataGroupForm[item] || undefined,
productType: targetNode.data.productTypeDefinitions, productType: targetNode.productTypeDefinitions,
categoryFullPath: targetNode.data.browsePathByName categoryFullPath: targetNode.browsePathByName
}) })
} else { } else {
updatePropertyList.push({ updatePropertyList.push({
propertyNameEn: item, propertyNameEn: item,
groupId: this.dataGroupForm[item] || undefined, groupId: this.dataGroupForm[item] || undefined,
productType: targetNode.data.productTypeDefinitions, productType: targetNode.productTypeDefinitions,
categoryFullPath: targetNode.data.browsePathByName categoryFullPath: targetNode.browsePathByName
}) })
} }
}) })
...@@ -705,8 +757,8 @@ export default { ...@@ -705,8 +757,8 @@ export default {
if (!this.dataGroupForm[item]) { if (!this.dataGroupForm[item]) {
delPropertyList.push({ delPropertyList.push({
propertyNameEn: item, propertyNameEn: item,
productType: targetNode.data.productTypeDefinitions, productType: targetNode.productTypeDefinitions,
categoryFullPath: targetNode.data.browsePathByName, categoryFullPath: targetNode.browsePathByName,
groupId: this.cloneDataGroupForm[item] groupId: this.cloneDataGroupForm[item]
}) })
} }
......
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