automated_uniapp/uni_modules/cool-ui/components/cl-list-index/cl-list-index.vue

456 lines
8.5 KiB
Vue
Raw Normal View History

2025-01-09 16:16:11 +08:00
<template>
<view class="cl-list-index">
<!-- 搜索栏 -->
<view class="cl-list-index__search" v-if="searchBar">
<cl-input
v-model="keyWord"
:border="false"
round
:placeholder="placeholder"
background-color="#f6f7fa"
clearable
>
<template #prepend>
<text class="cl-icon-search"></text>
</template>
</cl-input>
</view>
<view class="cl-list-index__container">
<!-- 滚动视图 -->
<scroll-view
class="cl-list-index__scroller"
scroll-y
enable-back-to-top
scroll-with-animation
:scroll-into-view="`index-${label}`"
@scroll="onScroll"
>
<!-- 追加内容到头部 -->
<slot name="prepend"></slot>
<!-- 分组数据 -->
<view
class="group"
v-for="(item, index) in flist"
:key="index"
:id="`index-${item.label}`"
>
<!-- 关键字 -->
<view
class="header"
:class="{
'is-active': curr?.label == item.label,
}"
>
<slot name="header" :item="item" :isActive="curr?.label == item.label">
<text>{{ item.label }}</text>
</slot>
</view>
<!-- 数据列表 -->
<view class="container">
<view v-for="(item2, index2) in item.children" :key="index2">
<slot
name="item"
:item="item2"
:index="index2"
:group="item"
:isActive="curr?.label == item.label"
>
<view
class="item"
:class="{
'is-disabled': item2.disabled,
}"
@tap="onSelect(item2)"
>
<cl-checkbox
:model-value="isChecked(item2)"
:disabled="item2.disabled"
round
:margin="[0, 10, 0, 0]"
v-if="selectable"
/>
<view class="avatar">
<cl-avatar :src="item2[dict.avatar]"></cl-avatar>
</view>
<view class="text">
{{ item2[dict.name] }}
</view>
</view>
</slot>
</view>
</view>
</view>
<!-- 追加内容到尾部 -->
<slot name="append"></slot>
</scroll-view>
</view>
<!-- 索引栏 -->
<view class="cl-list-index__bar" v-if="indexBar">
<view class="list" @touchmove.stop.prevent="barMove" @touchend="barEnd">
<view
class="block"
:class="{
'is-active': curr?.label == item.label,
}"
v-for="(item, index) in flist"
:key="index"
:id="`${index}`"
@touchstart.stop.prevent="toRow(item)"
>
<text>{{ item.label }}</text>
</view>
</view>
</view>
<!-- 索引关键字 -->
<view class="cl-list-index__alert" v-show="alert && curr">{{ curr?.label }}</view>
</view>
</template>
<script lang="ts">
import {
computed,
defineComponent,
getCurrentInstance,
nextTick,
reactive,
ref,
watch,
type PropType,
} from "vue";
import py from "js-pinyin";
import { groupBy, isEmpty } from "lodash-es";
export default defineComponent({
name: "cl-list-index",
props: {
// 数据列表
data: {
type: Array as PropType<ClListIndex.Group>,
required: true,
default: () => [],
},
// 字典
dict: Object,
// 是否可选
selectable: Boolean,
// 已选列表
selection: {
type: Array,
default: () => [],
},
// 是否分组
isGroup: {
type: Boolean,
default: true,
},
// 显示序号栏
indexBar: {
type: Boolean,
default: true,
},
// 显示搜索栏
searchBar: {
type: Boolean,
default: true,
},
// 搜索占位内容
placeholder: {
type: String,
default: "搜索关键字",
},
// 延迟
delay: Number,
},
emits: ["select", "selection-change", "update:selection"],
setup(props, { emit }) {
const { proxy }: any = getCurrentInstance();
// 列表
const list = ref<ClListIndex.Group>([]);
// 已选列表
const selection = ref<any[]>([]);
// 字典
const dict = reactive<ClListIndex.Dict>({
id: "id",
avatar: "avatar",
name: "name",
...props.dict,
});
// 关键字
const keyWord = ref("");
// 标签
const label = ref("");
// 提示框
const alert = ref(false);
// 当前选择
const curr = ref<any>({});
// 条
const bar = reactive({
top: 0,
itemH: 0,
});
// 每项距离顶部的高度
const tops = ref<any[]>([]);
// 过滤列表
const flist = computed<any[]>(() => {
function match(e: any) {
return e
? (e[dict.name] || "").toLowerCase().includes(keyWord.value.toLowerCase())
: false;
}
return list.value
.filter((e) => e.children && e.children.find(match))
.map((e) => {
return {
...e,
children: e.children.filter(match),
};
});
});
// 监听滚动
function onScroll(e: { detail: { scrollTop: number } }) {
// 对比每个高度计算
let num =
tops.value.filter((top) => e.detail.scrollTop >= top - (props.searchBar ? 60 : 0))
.length - 1;
if (num < 0) {
num = 0;
}
// 设置当前
curr.value = list.value[num];
}
// 定位到某行
function toRow(item: any) {
alert.value = true;
curr.value = item;
}
// 选择行
function onSelect(item: any) {
if (item.disabled) {
return false;
}
if (props.selectable) {
const index = selection.value.findIndex((e) => e[dict.id] == item[dict.id]);
if (index < 0) {
selection.value.push(item);
} else {
selection.value.splice(index, 1);
}
emit("selection-change", selection.value);
emit("update:selection", selection.value);
} else {
emit("select", item);
}
}
// 移动
function barMove(e: TouchEvent) {
const max = list.value.length;
let index = parseInt(String((e.touches[0].clientY - bar.top) / bar.itemH));
if (index >= max) {
index = max - 1;
}
if (index < 0) {
index = 0;
}
curr.value = list.value[index];
}
// 离开
function barEnd() {
if (curr.value) {
label.value = curr.value.label;
}
alert.value = false;
}
// 整理布局
function doLayout() {
nextTick(() => {
setTimeout(() => {
// 获取索引栏大小
uni.createSelectorQuery()
.in(proxy)
.select(".cl-list-index__bar .list")
.boundingClientRect((res: any) => {
if (res) {
bar.top = res.top;
bar.itemH = res.height / list.value.length;
}
})
.exec();
// 获取当前距离顶部的高度
uni.createSelectorQuery()
.in(proxy)
.select(".cl-list-index")
.boundingClientRect((res: any) => {
// 获取每项距离顶部的高度
uni.createSelectorQuery()
.in(proxy)
.selectAll(".header")
.fields(
{
rect: true,
size: true,
},
() => {},
)
.exec((d) => {
tops.value = d[0].map(
(e: { top: number; height: number }) => e.top - res.top,
);
});
})
.exec();
}, props.delay || 0);
});
}
// 是否选中
function isChecked(item: any) {
return Boolean(selection.value.find((e) => e[dict.id] == item[dict.id]));
}
// 更新行数据
function updateRow(id: string | number, value: any) {
list.value.forEach((a) => {
if (a.children) {
const d = a.children.find((e) => e[dict.id] == id);
if (d) {
Object.assign(d, value);
}
}
});
}
// 刷新
function refresh() {
if (isEmpty(props.data)) {
list.value = [];
return false;
}
// 是否分组
if (!props.isGroup) {
list.value = props.data;
return false;
}
// 传入列表数据
const data = props.data.map((e: any) => {
return {
f: py.getCamelChars(e[dict.name] || "*")[0],
...e,
};
});
// 数据分组
const group = [];
const g = groupBy(data, "f");
for (const i in g) {
group.push({
label: i,
children: g[i],
});
}
list.value = group.sort((a, b) => {
const n1 = a.label.toUpperCase();
const n2 = b.label.toUpperCase();
if (n1 < n2) {
return -1;
}
if (n1 > n2) {
return 1;
}
return 0;
});
// 重做
doLayout();
}
// 监听列表数据变化
watch(() => props.data, refresh, {
immediate: true,
});
// 监听选择数据变化
watch(
() => props.selection,
(val) => {
selection.value = [...val];
// 更新列表数据
selection.value.forEach((e) => {
updateRow(e[dict.id], e);
});
},
{
immediate: true,
},
);
return {
dict,
list,
keyWord,
label,
alert,
curr,
bar,
flist,
refresh,
doLayout,
barEnd,
barMove,
toRow,
onScroll,
onSelect,
isChecked,
updateRow,
};
},
});
</script>