This commit is contained in:
2026-09-24 16:25:22 +08:00
commit 7428184f01
1198 changed files with 314515 additions and 0 deletions
@@ -0,0 +1,645 @@
<script lang="ts">
import { type PropType } from "vue"
import { getDefaultColor } from "../../core/util/xCoreColorUtil.uts"
import { checkIsCssUnit, getUid } from "../../core/util/xCoreUtil.uts"
import { xConfig } from "../../config/xConfig.uts"
import { flatChildrensId, setChildrenByid, findParentNode } from "../x-tree/util.uts"
type callbackType = (id : string) => Promise<UTSJSONObject[]>
/**
* @name 树子节点 xTreeItem
* @description xTree内部子组件,不可引用。
* @page /pages/index/tree
* @category 表单组件
* @constant 平台兼容
* | Harmony | H5 | andriod | IOS | 小程序 | UTS | UNIAPP-X SDK | version |
| --- | --- | --- | --- | --- | --- | --- | --- |
| ☑ | ☑ | ☑️ | ☑️ | ☑️ | ☑️ | 4.76+ | 1.1.18 |
*/
export default {
data() {
return {
nowIds: [] as string[],
showChildren: true,
openFLoderIds: [] as string[],
tid: 0,
_list: [] as UTSJSONObject[],
nowediteItem:null as null|UTSJSONObject,
showModal:false,
nowediteItemText:"",
nowediteItemId:"",
editeType:"change",//change/add,
newItem:null as null|UTSJSONObject
}
},
emits: [
/**
* 选中切换时触发
* @@param {string[]} ids - 打开的id数组
*/
'change',
/**
* 节点标签被点击
* @@param {UTSJSONObject} item - 打开的id数组
*/
'childrenClick',
/**
* 父节点展开和关闭
* @@param {string[]} ids - 打开的id数组
*/
'openFolderChange',
'setChildrenData',
'update:modelValue'
],
props: {
idList: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
list: {
type: Array as PropType<UTSJSONObject[]>,
default: () : UTSJSONObject[] => [] as UTSJSONObject[]
},
idKey: {
type: String,
default: "id"
},
labelKey: {
type: String,
default: "text"
},
folderId: {
type: Array as PropType<string[]>,
default: () : string[] => [] as string[]
},
/**
* 选中时的高亮颜色或者主题
*/
color: {
type: String,
default: ""
},
/**
* 用户交互时是否禁用选中交互
* 但不影响你赋值选中
*/
disabledParentBox: {
type: Boolean,
default: false
},
/**
* 是否允许选中父级的时候把它的所有子级也选上。
*/
parentSelectedFullChildren: {
type: Boolean,
default: false
},
/**
* 是否停用选中功能,只能是已读状态了
* 但不影响你赋值选中
*/
disabled: {
type: Boolean,
default: false
},
beforeOpenFloder: {
type: Function as PropType<callbackType>,
default: (itemId : string) : Promise<UTSJSONObject[]> => {
return Promise.resolve([] as UTSJSONObject[])
}
},
/**
* 关闭和打开时图标/图片地址
* 必须为长度2,第一项关闭时图标,第2项关闭时图标
*/
floaderIcon:{
type:Array as PropType<string[]>,
default:():string[] => ['folder-add-line','folder-open-fill'] as string[]
},
/**
* 是否显示默认前置开和关的图标
*/
showFloaderIcon:{
type:Boolean,
default:true
},
/**
* 是否显示选中的box图标
*/
showChecked:{
type:Boolean,
default:false
},
/**
* 选中与未选中时的图标/图片地址
* 必须为长度2,第一项未选中,第二项选中时图标
*/
checkedIcon:{
type:Array as PropType<string[]>,
default:():string[] => ['checkbox-blank-line','checkbox-fill'] as string[]
},
},
computed: {
_checkedIcon() : string[] {
return this.checkedIcon
},
_showChecked() : boolean {
return this.showChecked
},
_showFloaderIcon() : boolean {
return this.showFloaderIcon
},
_floaderIcon() : string[] {
return this.floaderIcon;
},
_childrenList() : UTSJSONObject[] {
return this.list;
},
_folderId() : string[] {
return this.openFLoderIds
},
_color() : string {
return this.color
},
_disabledParentBox() : boolean {
return this.disabledParentBox
},
_disabled() : boolean {
return this.disabled
},
_parentSelectedFullChildren() : boolean {
return this.parentSelectedFullChildren
},
_fontSize() : string {
if (xConfig.fontScale == 1) return '16';
return (16 * xConfig.fontScale).toString()
},
_itemColor() : string {
if (xConfig.dark == 'dark') return 'white'
return '#666';
}
},
watch: {
folderId(newVal : string[]) {
if (newVal.join("") == this.openFLoderIds.join("")) return;
this.openFLoderIds = newVal
},
list() {
// this.$forceUpdate()
this._list = this.list;
},
idList(newVal : string[]) {
if (newVal.join("") == this.nowIds.join("")) return;
this.nowIds = newVal;
}
},
mounted() {
this._list = this.list;
this.openFLoderIds = this.folderId
this.nowIds = this.idList;
},
methods: {
async childrenEvent(item : UTSJSONObject) : Promise<any> {
let id = item.getString(this.idKey)
if (id == null) {
return Promise.resolve(true);
}
this.$emit("childrenClick", item)
let idsIndex = -1;
for (let i = 0; i < this.openFLoderIds.length; i++) {
if (this.openFLoderIds[i] == id) {
idsIndex = i;
break;
}
}
// 先判断 是不是父级,就是有没有children要展开。有就展开或者 关闭。没有不操作。
let children = this.getChildren(item)
let isOpenChildren = item.getArray('children')
// 只有有children字段的才可以异步打开继续加载数据。
if (isOpenChildren == null) {
this.checkBoxClickEvent(item)
return Promise.resolve(true);
}
if (idsIndex == -1 && children.length == 0) {
// 没有子级再判断有没有异步加载的数据。
let asyncList = await this.beforeOpenFloder(id)
let resultList = asyncList as UTSJSONObject[]
if (resultList.length > 0) {
let n = setChildrenByid(this._list, id, this.idKey, resultList)
item.set('children', resultList)
children = resultList
this.$emit('setChildrenData', id, this._list)
}
}
if (children.length == 0) {
// 没有子级触发title事件就行或者直接选中操作。
// this.checkBoxClickEvent(item)
return Promise.resolve(true);
}
if (idsIndex > -1) {
this.openFLoderIds.splice(idsIndex, 1)
} else {
this.openFLoderIds.push(id)
}
this.$emit('openFolderChange', this.openFLoderIds.slice(0), idsIndex == -1 ? 'open' : 'close', id)
return Promise.resolve(true);
},
setChildrenDataEnd(id : string, item : UTSJSONObject[]) {
let parent = findParentNode(this._list, id, this.idKey) as UTSJSONObject | null
if (parent == null) {
let index = this._list.findIndex((item) : boolean => id == item.getString(this.idKey)!)
if (index > -1) {
this._list[index].set('children', item)
this.$emit('setChildrenData', id, this._list)
}
} else {
let index = this._list.findIndex((item) : boolean => item!.getString(this.idKey) != parent!.getString(this.idKey)!)
if (index > -1) {
this._list[index].set('children', item)
this.$emit('setChildrenData', id, this._list)
}
}
},
isInOpenFloder(item : UTSJSONObject) : boolean {
let id = item.getString(this.idKey)
if (id == null) {
return false;
}
if (this.openFLoderIds.includes(id)) return true;
return false
},
getListItemHeight(item : UTSJSONObject) : number {
let id = item.getString(this.idKey)
if (id == null || !this.isInOpenFloder(item)) {
return 1;
}
let children = this.getChildren(item)
if (children.length == 0) return 1;
return children.length + 1
},
getChildren(item : UTSJSONObject) : UTSJSONObject[] {
let c = item.getArray<UTSJSONObject>('children')
if (c == null) return [] as UTSJSONObject[]
return c;
},
updateIds() {
// this.$emit()
},
openFolderChangeEnd(ids : string[], type : string, id : string) {
// let fullIds = new Set(this.openFLoderIds.concat(ids))
// let idsreal = [] as string[]
// fullIds.forEach((val : string) => {
// idsreal.push(val)
// })
// this.openFLoderIds = ids
this.$emit('openFolderChange', ids.slice(0), type, id)
},
childrenClickEnd(item : UTSJSONObject) {
this.$emit("childrenClick", item)
},
checkBoxClickEvent(item : UTSJSONObject) {
if (this.isDisabled(item)) {
return;
}
let id = item.getString(this.idKey)
if (id == null) {
return;
}
let idsIndex = -1;
for (let i = 0; i < this.nowIds.length; i++) {
if (this.nowIds[i] == id) {
idsIndex = i;
break;
}
}
let sids = this.nowIds.slice(0)
let indeterminate = this.isIndeterminate(item)
let selected = this.isSelected(item)
if(idsIndex>-1){
sids.splice(idsIndex, 1)
if (this._parentSelectedFullChildren && this.getChildren(item).length > 0) {
let tmepids = flatChildrensId(item, this.idKey)
sids = sids.filter((el) : boolean => !tmepids.includes(el))
}
//如果不存在选中中,它可能是半选状态或者完全没有选中.
}else{
sids.push(id)
// 如果允许单选父,不影响子,需要单独处理,其它交给父组件处理
if (this._parentSelectedFullChildren && this.getChildren(item).length > 0) {
let tmepids = flatChildrensId(item, this.idKey)
// tmepids = tmepids.filter((yid:string):string => !this.nowIds.includes(yid))
sids = sids.concat(tmepids)
}
}
this.nowIds = sids;
// 更新父节点状态
// this.updateParentNodeStatus();
this.$emit('change', this.nowIds.slice(0))
},
changeEnd(ids : string[]) {
// 当子节点状态变化时,更新当前节点状态
this.nowIds = ids.slice(0);
this.$emit('change', ids.slice(0))
},
isSelected(item : UTSJSONObject) : boolean {
let id = item.getString(this.idKey)
id = id == null ? "" : id;
return this.nowIds.includes(id)
},
// 检查节点是否处于半选状态(部分子节点被选中)
isIndeterminate(item : UTSJSONObject) : boolean {
// 如果节点已被选中,则不是半选状态
if (this.isSelected(item)) return false;
// 获取所有子节点ID
let children = this.getChildren(item);
if (children.length === 0) return false;
// 获取所有可选的子节点ID
let allChildrenIds = flatChildrensId(item, this.idKey);
if (allChildrenIds.length === 0) return false;
// 检查是否有部分子节点被选中
let selectedChildrenCount = 0;
for (let i = 0; i < allChildrenIds.length; i++) {
if (this.nowIds.includes(allChildrenIds[i])) {
selectedChildrenCount++;
}
}
// 如果有部分子节点被选中(不是全部也不是零个),则是半选状态
return selectedChildrenCount > 0 && selectedChildrenCount < allChildrenIds.length;
},
// 是否被禁用选择
isDisabled(item : UTSJSONObject) : boolean {
let children = this.getChildren(item)
// 自己的单独的禁用状态
let disabled = item.getBoolean("disabled")
disabled = disabled == null ? false : disabled
// 父级
if (children.length > 0) {
return this._disabledParentBox || disabled || this._disabled
}
return disabled || this._disabled
},
editeclick(){
if(this.nowediteItem==null) return;
if(this.nowediteItemText==''){
uni.showToast({title:"标题不能为空",icon:'none'})
return;
}
let item = this.nowediteItem!
let id = item.getString(this.idKey)!
if(this.editeType=='change'){
item.set(this.labelKey,this.nowediteItemText)
item.set(this.idKey,this.nowediteItemId)
let index = this._list.findIndex((item) : boolean => id == item.getString(this.idKey)!)
if (index > -1) {
this._list[index] = item
this.$emit('setChildrenData', id, this._list)
}
}else if(this.editeType == 'add'&&this.newItem!=null){
id = this.nowediteItemId
if(id==''){
id = Math.random().toString(16).substring(1,20)
}
this.newItem!.set(this.labelKey,this.nowediteItemText)
this.newItem!.set(this.idKey,id)
let nextChildre = this.getChildren(item)
if(nextChildre.length==0){
this.newItem!.set('children',[this.newItem!] as UTSJSONObject[])
}else{
nextChildre.push(this.newItem! as UTSJSONObject)
}
let index = this._list.findIndex((item) : boolean => id == item.getString(this.idKey)!)
if (index > -1) {
this._list[index] = item
this.$emit('setChildrenData', id, this._list)
}
this.nowediteItemId = ''
this.nowediteItemText = ''
}
},
isShowEdite(item:UTSJSONObject):Boolean{
let showEditeLabel = item.getBoolean('showEdite')
return showEditeLabel == true;
},
isShowAdd(item:UTSJSONObject):Boolean{
let showEditeLabel = item.getBoolean('showAdd')
return showEditeLabel == true;
},
isShowRemove(item:UTSJSONObject):Boolean{
let showEditeLabel = item.getBoolean('showRemove')
return showEditeLabel == true;
},
showModalEdte(item:UTSJSONObject,editeType:string){
if(editeType == 'add'){
let newitem = {
disabled:false,
showEdite:false,
showRemove:true
} as UTSJSONObject
newitem.set(this.labelKey,"")
newitem.set(this.idKey,"")
this.newItem = newitem
}
let disabled = this.isDisabled(item)
this.editeType = editeType;
if(disabled) return;
this.nowediteItem = item;
if(editeType == 'change'){
this.nowediteItemText = item.getString(this.labelKey)==null?'':item.getString(this.labelKey)!
this.nowediteItemId = item.getString(this.idKey)==null?'':item.getString(this.idKey)!
}else if(editeType == 'add'){
this.nowediteItemId = ''
this.nowediteItemText = ''
}
this.showModal = true;
},
removeItem(item:UTSJSONObject){
console.log(this._list)
let t = this;
uni.showModal({
title:"提醒",
content:"确认删除?",
success(res){
if(res.confirm){
let id = item.getString(t.idKey)!
let index = t._list.findIndex((item:UTSJSONObject) : boolean => id == item.getString(t.idKey)!)
if (index > -1) {
t._list.splice(index,1)
t.$emit('setChildrenData', id, t._list)
}
}
}
})
}
}
}
</script>
<template>
<view class="xTree">
<!-- 'add'?'添加下级':'修改内容'" -->
<x-modal height="auto" @confirm="editeclick"
:title="editeType=='add'?(i18n!.t('tmui4x.tree.addTitle')):(i18n!.t('tmui4x.tree.changeTitle'))"
v-model:show="showModal">
<view v-if="nowediteItem!=null">
<x-input dark-bg-color="" height="48" v-model="nowediteItemText">
<template v-slot:inputLeft>
<!-- 标题值 -->
<x-text _style="padding:0 12px">
{{i18n!.t('tmui4x.tree.inputTitle')}}
</x-text>
</template>
</x-input>
<view style="height:10px"></view>
<x-input dark-bg-color="" height="48" :placeholder="i18n!.t('tmui4x.tree.inputTips1')" v-model="nowediteItemId">
<template v-slot:inputLeft>
<!-- 标识ID -->
<x-text _style="padding:0 12px">{{i18n!.t('tmui4x.tree.inputId')}}</x-text>
</template>
</x-input>
</view>
</x-modal>
<view class="xTreeItemCell" v-for="(item,index) in _list" :key="index">
<view
:hover-start-time="5" :hover-stay-time="30" :hover-class="getChildren(item).length>0?'hoverClass':'hoverClassOff'"
@click.stop="childrenEvent(item)" :style="{height:'50px'}" class="xTreeItemCellWrap">
<view class="xTreeItemCellWrapLeft">
<view v-if="item.getArray('children')!=null&&_showFloaderIcon" class="xTreeItemCellWrapLeftIcon">
<x-icon color="#333333" v-if="!isInOpenFloder(item)" font-size="21"
:name="_floaderIcon[0]"></x-icon>
<x-icon :color="_color" v-if="isInOpenFloder(item)" font-size="21"
:name="_floaderIcon[1]"></x-icon>
</view>
<text :style="{color:isSelected(item)?_color:_itemColor,fontSize:_fontSize}"
class="xTreeItemCellWrapLeftText">{{item.getString(labelKey)}}</text>
</view>
<view v-if="isShowAdd(item)" :hover-start-time="5" :hover-stay-time="30" hover-class="hoverClass"
@click.stop="showModalEdte(item,'add')" style="margin-right:10px;" >
<x-icon :color="!isDisabled(item)?_color:'#cecece'" font-size="21"
name="add-large-line"></x-icon>
</view>
<view v-if="isShowEdite(item)" :hover-start-time="5" :hover-stay-time="30" hover-class="hoverClass"
@click.stop="showModalEdte(item,'change')" style="margin-right:10px;" >
<x-icon :color="!isDisabled(item)?_color:'#cecece'" font-size="21"
name="edit-box-line"></x-icon>
</view>
<view v-if="isShowRemove(item)" :hover-start-time="5" :hover-stay-time="30" hover-class="hoverClass"
@click.stop="removeItem(item)" style="margin-right:10px;" >
<x-icon :color="!isDisabled(item)?_color:'#cecece'" font-size="21"
name="delete-bin-2-line"></x-icon>
</view>
<view v-if="_showChecked" :hover-start-time="5" :hover-stay-time="30" hover-class="hoverClass"
@click.stop="checkBoxClickEvent(item)" :style="{opacity:isDisabled(item)?'0.3':'1'}">
<x-icon v-if="_parentSelectedFullChildren" :color="isSelected(item) || isIndeterminate(item) ? _color : '#cecece'" font-size="21"
:name="isSelected(item) ? _checkedIcon[1] : (isIndeterminate(item) ? 'indeterminate-circle-line' : _checkedIcon[0])"></x-icon>
<x-icon v-else :color="isSelected(item) ? _color : '#cecece'" font-size="21"
:name="isSelected(item) ? _checkedIcon[1] : _checkedIcon[0]"></x-icon>
</view>
</view>
<x-tree-item
:checkedIcon="checkedIcon"
:showChecked="_showChecked"
:showFloaderIcon="showFloaderIcon"
:floaderIcon="floaderIcon"
@childrenClick="childrenClickEnd" @openFolderChange="openFolderChangeEnd"
@setChildrenData="setChildrenDataEnd" :beforeOpenFloder="beforeOpenFloder"
:parentSelectedFullChildren="_parentSelectedFullChildren" :disabled="_disabled"
:disabledParentBox="_disabledParentBox" @change="changeEnd" :idList="nowIds" :color="_color"
:folderId="openFLoderIds" :idKey="idKey" :labelKey="labelKey" style="margin-left: 30px;"
v-if="isInOpenFloder(item)&&showChildren" :list="getChildren(item)" v-model="nowIds"></x-tree-item>
</view>
</view>
</template>
<style scoped>
.hoverClass {
background-color: rgba(0, 0, 0, 0.03);
}
.hoverClassOff {
background-color: transparent;
}
.xTreeItemCellWrapLeftText {
flex: 1;
text-overflow: ellipsis;
lines: 2;
/* #ifndef APP */
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
-webkit-line-clamp: 2;
/* #endif */
}
.xTreeItemCellWrapLeftIcon {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
width: 30px;
}
.xTreeItemCellWrapLeft {
flex: 1;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
.xTreeItemCellWrap {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
}
</style>