456 lines
8.5 KiB
Vue
456 lines
8.5 KiB
Vue
![]() |
<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>
|