- 全选功能
- 左边搜索滚动定位到具体城市
export function cloneDeep(data) {
return JSON.parse(JSON.stringify(data))
}
ChooseCity.vue子组件
<template>
<div>
<a-modal
:width="800"
:title="title"
:visible="show"
:confirm-loading="confirmLoading"
:loading="loading"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="transfer-box">
<a-transfer
:data-source="dataSourceTransfer"
:target-keys="filterTargetKeys"
:render="(item) => item.title"
:show-select-all="true"
@change="handleTransferChange"
:list-style="{
width: '300px',
height: '360px'
}"
show-search
:titles="['可选城市', '已选城市']"
@search="onSearch"
>
<template
slot="children"
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect, itemSelectAll } }"
>
<a-tree
v-if="direction === 'left'"
:key="searchValue"
blockNode
checkable
:defaultExpandAll="false"
:checkedKeys="[...selectedKeys, ...targetKeys]"
:treeData="cityOptions"
@expand="onExpand"
:expanded-keys.sync="expandedKeys"
:auto-expand-parent="autoExpandParent"
@check="
(_, props) => {
handleTreeChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll)
}
"
@select="
(_, props) => {
handleTreeChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll)
}
"
>
<template slot="title" slot-scope="{ title }">
<span v-if="title.indexOf(searchValue) > -1">
{{ title.substr(0, title.indexOf(searchValue)) }}
<span :id="title.indexOf(searchValue) > -1 ? 'target-elem' : ''" style="color: #f50">{{
searchValue
}}</span>
{{ title.substr(title.indexOf(searchValue) + searchValue.length) }}
</span>
<span v-else>{{ title }}</span>
</template>
</a-tree>
</template>
</a-transfer>
</div>
<!-- 底部 -->
<template slot="footer">
<a-button key="cancel" class="normal-btn" @click="handleCancel">取消</a-button>
<a-button key="submit" class="common-btn" :loading="confirmLoading" @click="handleOk">提交</a-button>
</template>
</a-modal>
</div>
</template>
<script>
import { cloneDeep } from '@/views/financialBillSystem/billingManagement/utils/mapping'
function isChecked(selectedKeys, eventKey) {
return selectedKeys.indexOf(eventKey) !== -1
}
function handleTreeData(data, targetKeys = []) {
data.forEach((item) => {
item['disabled'] = targetKeys.includes(item.key)
if (item.children) {
handleTreeData(item.children, targetKeys)
}
})
return data
}
export default {
name: 'ChooseCity',
props: {
sureCityList: {
required: true,
type: Array
},
noFilterKeys: {
required: true,
type: Array
},
cityList: {
required: true,
type: Array
}
},
data() {
return {
title: '选择城市',
show: true,
loading: false,
confirmLoading: false,
targetKeys: [],
dataSource: [],
cityOptions: [],
arr: [],
expandedKeys: [],
searchValue: '',
autoExpandParent: true,
dataList: [],
parentKeys: [], //父节点
filterTargetKeys: [], //过滤掉父节点后
dataSourceTransfer: [] //所有子节点原数据
}
},
created() {
console.log('sureCityList', this.sureCityList, this.cityList)
this.$nextTick(() => {
this.initCityTreeData()
})
},
methods: {
//左边搜索滚动到指定位置
scrollPosition() {
this.$nextTick(() => {
const elem = document.querySelector('#target-elem') // 获取目标元素
console.log('elem', elem)
if (!elem) return
const scrollableElem = document.querySelector('.ant-transfer-list-body-customize-wrapper ') // 获取目标元素的父元素(可滚动容器)
console.log(scrollableElem)
const scrollY = scrollableElem.scrollTop // 获取当前垂直方向上滚动的距离
const scrollHeight = scrollableElem.scrollHeight // 获取可滚动容器的实际高度
const clientHeight = scrollableElem.clientHeight // 获取可滚动容器的可视区域高度
const targetY = elem.offsetTop - scrollableElem.offsetTop // 获取目标元素相对于可滚动容器的上边距
const scrollToY = Math.min(targetY, scrollHeight - clientHeight) // 计算滚动条滚动目标位置
scrollableElem.scrollTo({
top: scrollToY,
behavior: 'smooth'
})
})
},
//源数据-转化为一层-父加子
flatten(list = []) {
list.forEach((item) => {
this.dataSource.push(item)
this.flatten(item.children)
})
},
generateData(_level, _preKey, _tns) {
const preKey = _preKey || '0'
const tns = _tns || this.cityOptions
const children = []
for (let i = 0; i < x; i++) {
const key = `${preKey}-${i}`
tns.push({ title: key, key, scopedSlots: { title: 'title' } })
if (i < y) {
children.push(key)
}
}
if (_level < 0) {
return tns
}
const level = _level - 1
children.forEach((key, index) => {
tns[index].children = []
return this.generateData(level, key, tns[index].children)
})
},
generateList(data) {
for (let i = 0; i < data.length; i++) {
const node = data[i]
const key = node.key
const title = node.title
this.dataList.push({ key, title })
if (node.children) {
this.generateList(node.children)
}
}
},
getParentKey(key, tree) {
let parentKey
for (let i = 0; i < tree.length; i++) {
const node = tree[i]
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key
} else if (this.getParentKey(key, node.children)) {
parentKey = this.getParentKey(key, node.children)
}
}
}
return parentKey
},
//搜索展开当前父节点
onExpand(expandedKeys) {
console.log('expandedKeys', expandedKeys)
this.expandedKeys = expandedKeys
this.autoExpandParent = false
},
handleSearch(e) {
setTimeout(() => {
const value = e
const expandedKeys = this.dataList
.map((item) => {
if (item.title.indexOf(value) > -1) {
return this.getParentKey(item.key, this.cityOptions)
}
return null
})
.filter((item, i, self) => item && self.indexOf(item) === i)
console.log(expandedKeys)
Object.assign(this, {
expandedKeys: value ? expandedKeys : [],
searchValue: value
})
this.scrollPosition()
console.log(this.autoExpandParent)
}, 200)
},
onSearch(dir, val) {
console.log(dir, val)
if (dir === 'left') {
this.handleSearch(val)
}
},
// 查找详情的父节点
checkDetailParentKey() {
let parKey = [] // 查找详情的父节点
if (this.sureCityList && this.sureCityList.length > 0) {
this.filterTargetKeys = this.sureCityList.map((item) => {
return item.key
})
parKey = this.searchParents(this.dataSource, this.filterTargetKeys, false)
console.log('初始化', this.targetKeys, this.filterTargetKeys, this.noFilterKeys, parKey)
}
return parKey
},
//获取子节点全部勾选的父节点
getAllSelectParentKey(parentList) {
//找出重复出现的次数,如果小于子节点的length则去除这个元素
//如果等于子节点的length则只保留一个这个元素
//获得元素以及出现了几次
let getRepeatObj = this.getShowAcount(parentList)
console.log(getRepeatObj)
let newObj = {} //只保留等于子节点长度的
for (let k in getRepeatObj) {
this.cityList.forEach((item) => {
if (item.key == k) {
item.children.length > getRepeatObj[k] ? (newObj[k] = 0) : (newObj[k] = getRepeatObj[k])
}
})
}
console.log('找到父节点的子节全被选择的父节点', newObj)
let allSelectParKey = []
let noAllSelectparKey = []
for (let k in newObj) {
if (newObj[k] !== 0) {
allSelectParKey.push(k)
} else {
noAllSelectparKey.push(k)
}
}
return [allSelectParKey, noAllSelectparKey]
},
//初始化树节点
initCityTreeData() {
this.cityOptions = cloneDeep(this.cityList)
this.dataSourceTransfer = this.getAllChild(cloneDeep(this.cityList)) //所有子节点原数据-控制左边的总数量的
this.generateList(this.cityOptions) //将树结构处理成需要的字段
this.flatten(this.cityList) //所有数据-父加子,将树结构转化为一级
this.parentKeys = this.cityOptions.map((item) => {
return item.key
}) //获取所有父节点的key
let parKey = this.checkDetailParentKey() // 查找详情的父节点
console.log(parKey)
let parKeys = this.getAllSelectParentKey(parKey)[0] //获取子节点全部勾选的父节点
console.log('parKeys', parKeys)
this.targetKeys = [...this.filterTargetKeys, ...parKeys]
console.log('最后的key', this.targetKeys)
this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys) //选择后让勾选禁用
},
//判断数组里元素出现的次数
getShowAcount(names) {
var countedNames = names.reduce((obj, name) => {
if (name in obj) {
obj[name]++
} else {
obj[name] = 1
}
return obj
}, {})
//reduce的第二个参数就是obj的初始值
return countedNames
},
// 树形结构数据过滤
filterTree(tree = [], targetKeys = [], validate = () => {}) {
console.log('tree', tree, targetKeys)
if (!tree.length) {
return []
}
const result = []
for (let item of tree) {
if (item.children && item.children.length) {
let node = {
...item,
children: [],
disabled: targetKeys.includes(item.key) // 禁用属性
}
// 子级处理
for (let o of item.children) {
if (!validate.apply(null, [o, targetKeys])) continue
node.children.push({ ...o, disabled: targetKeys.includes(o.key) })
}
if (node.children.length) {
result.push(node)
}
}
}
return result
},
formatOptions(data, children) {
const arr = children || []
data.forEach((e) => {
const item = {
key: e.id,
title: e.name,
scopedSlots: { title: 'title' }
// disabled: e.children ? true : false
}
if (e.children && e.children.length) {
item.children = []
this.formatOptions(e.children, item.children)
}
arr.push(item)
})
return arr
},
onChange(targetKeys) {
console.log('Target Keys:', targetKeys)
// this.targetKeys = targetKeys
// this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
},
handleTransferChange(targetKeys, direction, moveKeys) {
console.log('Target Keys:', targetKeys, direction, moveKeys)
// 已选城市移除
if (direction === 'left') {
//全选
if (this.isSelectAllCity(moveKeys, this.getAllChild(this.cityOptions))) {
this.targetKeys = []
this.filterTargetKeys = []
} else {
let parKey = this.searchParents(this.dataSource, moveKeys) //查找移除的父节点
this.targetKeys = this.targetKeys.filter((item) => ![...moveKeys, ...parKey].includes(item))
console.log('parKey', parKey, this.targetKeys)
this.filterTargetKeys = this.filterTargetKeys.filter((item) => !moveKeys.includes(item))
}
this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
} else {
//全选
if (this.isSelectAllCity(targetKeys, this.getAllChild(this.cityOptions))) {
console.log('全选')
this.targetKeys = [...targetKeys, ...this.parentKeys]
this.filterTargetKeys = targetKeys
} else {
console.log('非全选')
let parKey = this.searchParents(this.dataSource, targetKeys) //查找父节点
this.targetKeys = [...targetKeys, ...parKey]
this.filterTargetKeys = targetKeys
}
console.log(' this.targetKeys', this.targetKeys, ',this.filterTargetKeys', this.filterTargetKeys)
this.cityOptions = handleTreeData(this.cityOptions, this.targetKeys)
}
},
handleTreeChecked(keys, e, checkedKeys, itemSelect, itemSelectAll) {
// console.log('check', keys, e, checkedKeys, itemSelect, itemSelectAll)
// this.targetKeys = keys
const {
eventKey,
checked,
dataRef: { children }
} = e.node
if (this.parentKeys && this.parentKeys.includes(eventKey)) {
// 父节点选中:将所有子节点也选中
// let childKeys = children ? children.map((item) => item.key) : []
// if (childKeys.length) itemSelectAll(childKeys, !checked)
//当子节点已有被禁用勾选的,再次点击全选只勾选没被禁用的
let abledChildKeys = []
children.forEach((item) => {
if (!item.disabled) {
abledChildKeys.push(item.key)
}
})
// console.log('abledChildKeys', abledChildKeys)
let childKeys = abledChildKeys.length ? abledChildKeys : []
if (childKeys.length) itemSelectAll(childKeys, !checked)
} else {
itemSelect(eventKey, isChecked(keys, eventKey))
}
// itemSelect(eventKey, !isChecked(checkedKeys, eventKey)) // 子节点选中
},
//关闭编辑
closeEditModal(nodeData) {
this.isShowEditModal = nodeData
},
handleBillNameEdit(type) {
console.log('编辑')
this.type = type
this.isShowEditModal = true
},
//查找所有父节点
searchParents(list, moveKeys, flag = true) {
let arr = []
list.forEach((item) => {
if (moveKeys.includes(item.key)) {
if (item.key != '0') {
arr.push(item.parentId)
}
}
})
if (flag) {
arr = [...new Set(arr)]
}
return arr
},
// 关闭弹窗
handleCancel() {
this.$emit('closeCityModal', false)
},
handleOk() {
console.log(this.filterTargetKeys)
if (this.filterTargetKeys.length === 0) {
return this.$message.info('至少选择一个城市')
}
//穿梭框右边过滤出父节点的数据
let sureCityList = this.dataList.filter((item) => {
return this.filterTargetKeys.includes(item.key)
})
let flag = this.isSelectAllCity(sureCityList, this.getAllChild(this.cityOptions))
this.$emit('handleChangeCity', sureCityList, this.dataSource, flag)
this.$emit('closeCityModal', false)
},
//获取所有子节点
getAllChild(list) {
let arr = []
list.forEach((item) => {
if (item.children.length) {
item.children.forEach((child) => {
arr.push(child)
})
}
})
return arr
},
//判断是否选了全部城市
isSelectAllCity(selectCity, childAllCity) {
return selectCity.length === childAllCity.length
}
}
}
</script>
<style lang="less" scoped>
.transfer-box {
padding-bottom: 35px;
display: flex;
justify-content: center;
}
::v-deep .ant-transfer-list-body.ant-transfer-list-body-with-search {
overflow-y: hidden;
padding-top: 50px;
width: 100%;
}
::v-deep .ant-transfer-list-body-customize-wrapper {
overflow: auto;
height: 100%;
}
::v-deep .ant-transfer-list-content {
padding-bottom: 15px;
height: 100%;
}
::v-deep .ant-transfer-list-body.ant-transfer-list-body-with-search .ant-transfer-list-body-search-wrapper {
position: absolute;
z-index: 99;
width: 100%;
background-color: #fff;
}
</style>
父组件
<template>
<div ref="ruleConfigDetail" class="financial-bill">
<bread-crumb></bread-crumb>
<a-card title="" :bordered="false" class="card-box" :loading="pageLoading">
<a-form-model
class="collapse-search-form"
ref="ruleForm"
:model="form"
:rules="rules"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-card title="业务规则" :bordered="false">
<!-- 城市 -->
<a-row :gutter="60">
<a-col :lg="10" :md="12" :sm="24">
<a-form-model-item label="城市" prop="sureCityList">
<a-button class="common-btn" @click="openAddCityModal" :disabled="!isEdit">
<a-icon type="plus" />添加城市
</a-button>
</a-form-model-item>
</a-col>
</a-row>
<div class="city-tags" v-if="form.sureCityList.length > 0">
<template v-for="(tag, index) in form.sureCityList">
<a-tooltip :key="tag.key" :title="tag.title">
<a-tag :key="tag.key" :closable="isEdit" @close="deleteTag(index)">
{{ tag.title }}
</a-tag>
</a-tooltip>
</template>
</div>
</a-card>
</a-form-model>
<!-- 底部 -->
<div class="footer-btn" v-if="isEdit">
<a-button class="normal-btn" @click="handleCancel">取消</a-button>
<a-button class="common-btn" @click="handleSubmit">提交</a-button>
</div>
</a-card>
<!-- 选择城市 -->
<ChooseCity
ref="chooseCity"
v-if="isShowCityModal"
:cityList="cityList"
@closeCityModal="closeCityModal"
@handleChangeCity="handleChangeCity"
:sureCityList="form.sureCityList"
/>
</div>
</template>
<script>
import BreadCrumb from '@/components/tools/Breadcrumb'
import ChooseBillRuleModel from './components/ChooseBillRuleModel'
import ChooseCity from './components/ChooseCity.vue'
import { getRuleConfigDetail, addBillRuleConfig, editBillRuleConfig } from '@/api/financialBillSystem/billingManagement'
import { cloneDeep } from '@/views/financialBillSystem/billingManagement/utils/mapping'
export default {
name: 'BillingRuleConfigDetail',
components: {
BreadCrumb,
ChooseCity
},
data() {
return {
form: {
sureCityList: []
},
formItemLayout: {
labelCol: { span: 8 },
wrapperCol: { span: 16 }
},
isShowCityModal: false,
rules: {
sureCityList: [{ required: true, message: '城市不能为空', trigger: ['change', 'blur'] }]
},
dataSource: [], //源数据
cityList: [],
pageLoading: false,
wholeCountry: [{ cityId: '-1', cityName: '全国' }], //全国
isSelectAllFag: false
}
},
created() {
this.cityList = this.formatTreeOptions(this.$store.getters.cityList)
if (this.id) {
this.$nextTick(async () => {
await this.getDetailData()
})
}
},
methods: {
//取消
handleCancel() {
this.$router.go(-1)
},
//提交
handleSubmit() {
this.$refs.ruleForm.validate((valid) => {
console.log(valid)
if (!valid) return false
this.$confirm({
title: '是否确认生效规则?',
// content: 'Some descriptions',
getContainer: () => this.$refs.ruleConfigDetail,
okText: '确认',
// okType: 'danger',
cancelText: '取消',
onOk: async () => {
console.log('OK')
//校验成功后
let params = {
...this.form,
invoiceRuleId: this.chooseBill.id,
cityList: this.getSelectCity()
}
console.log('提交', params)
if (this.id) {
const { success, message, info } = await editBillRuleConfig(params)
if (success) {
this.$message.success(info ? info : message)
this.$router.back()
} else {
this.$message.error(message)
}
} else {
const { success, message, info } = await addBillRuleConfig(params)
if (success) {
this.$message.success(info ? info : message)
this.$router.back()
} else {
this.$message.error(message)
}
}
},
onCancel() {
console.log('Cancel')
}
})
})
},
//判断是否全选-处理成后端需要的数据
getSelectCity() {
let cityList = []
//全选
if (this.isSelectAllFag) {
cityList = this.wholeCountry
} else {
this.form.sureCityList.forEach((item) => {
cityList.push({ cityId: item.key, cityName: item.title })
})
}
return cityList
},
//处理成下拉框的格式
formaterSelectList(data) {
let arr = []
data.forEach((item) => {
arr.push({
value: item.dictCode,
label: item.dictValue
})
})
return arr
},
//处理穿梭框树形结构
formatTreeOptions(data, children, parentId = '0') {
// console.log(data);
const arr = children || []
data.forEach((e) => {
// console.log(e)
const item = {
key: e.cityId.toString(),
title: e.allName,
scopedSlots: { title: 'title' },
parentId
// disabled: e.children ? true : false
}
if (e.cityCommons && e.cityCommons.length) {
item.children = []
this.formatTreeOptions(e.cityCommons, item.children, e.cityId.toString())
}
arr.push(item)
})
return arr
},
//获取详情
async getDetailData() {
this.pageLoading = true
let params = {
id: this.id
}
const { success, message, info } = await getRuleConfigDetail(params)
if (success) {
// info.cityList = this.wholeCountry
let detailCityKeys = cloneDeep(this.getDetailChildKeys(info.cityList))
console.log('详情城市', detailCityKeys)
this.$set(this.form, 'sureCityList', detailCityKeys)
this.pageLoading=false
} else {
this.$message.error(message)
}
},
//判断是否有全国-获取详情的城市key
getDetailChildKeys(detailCityList) {
let arr = []
//全选
if (detailCityList[0].cityId == -1) {
this.$store.getters.cityList.forEach((item) => {
if (item.cityCommons.length) {
item.cityCommons.forEach((child) => {
arr.push({ title: child.allName, key: child.cityId.toString() })
})
}
})
} else {
//非全选
detailCityList.forEach((item) => {
arr.push({ title: item.cityName, key: item.cityId.toString() })
})
}
return arr
},
//获取所有子节点
getAllChild(list) {
let arr = []
list.forEach((item) => {
if (item.children.length) {
item.children.forEach((child) => {
arr.push(item.key)
})
}
})
return arr
},
//删除城市标签
deleteTag(index) {
this.form.sureCityList.splice(index, 1)
console.log(index, this.form.sureCityList)
this.$refs['ruleForm'].validateField('sureCityList') //解决校验不消失的问题
},
//确认已选择的城市
handleChangeCity(nodeData, dataSource, flag) {
console.log('是否全选', flag)
this.isSelectAllFag = flag
this.$set(this.form, 'sureCityList', nodeData)
this.$refs['ruleForm'].validateField('sureCityList') //解决校验不消失的问题
this.$forceUpdate()
this.dataSource = dataSource
},
//关闭选择城市弹窗
closeCityModal(nodeData) {
this.isShowCityModal = false
},
//打开选择城市弹窗
openAddCityModal() {
this.isShowCityModal = true
},
}
}
</script>
<style lang="less" scoped>
::v-deep .ant-card .ant-card-body {
padding: 16px 0 0;
}
.collapse-search-form {
padding: 15px 0 16px;
.ant-calendar-range-picker-separator {
color: #333;
vertical-align: baseline;
}
.button-group {
display: inline-block;
.ant-btn {
margin: 0 12px;
&.ant-btn-link .anticon {
margin-left: 2px;
font-size: 12px;
}
}
}
.line-box {
margin-top: 20px;
}
.tax-table {
padding: 15px 80px 20px;
}
.bill-rule-btn {
margin-left: 80px;
margin-bottom: 20px;
}
.city-tags {
border: 1px solid #ccc;
border-radius: 5px;
white-space: wrap;
margin: 0 230px 30px 175px;
min-height: 50px;
max-height: 200px;
padding: 10px;
overflow-y: scroll;
}
.ant-tag {
margin-bottom: 10px;
}
}
.footer-btn {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40px;
.ant-btn {
width: 86px;
}
.common-btn {
margin-left: 100px;
}
}
::v-deep .ant-modal-confirm .ant-modal-confirm-btns button + button {
background-image: linear-gradient(90deg, #ff921b 20%, #ff7000 85%);
border-width: 0px;
outline: none;
}
.normal-btn {
&:hover,
&:focus,
&:link,
&:active,
&:visited {
border-color: #ff6633;
color: #ff6633;
}
}
::v-deep .invoiceType .ant-form-item-control.has-error .ant-form-explain {
display: var(--primary);
}
</style>