| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386 |
- <template>
- <FixedVirtualList
- :data="groupedData.list"
- dataKey="id"
- :containerStyle="innerStyle"
- :itemSize="itemHeight"
- :bufferSize="2"
- :scrollTop="scrollCurrentValue"
- @scroll="handleScroll"
- v-bind="$attrs"
- >
- <template #list="{ visibleItems, className, itemStyle }">
- <slot
- name="list"
- :visibleItems="visibleItems"
- :className="className"
- :itemStyle="itemStyle"
- :onItemPress="onItemPress"
- >
- <view
- v-for="(row, index) in visibleItems"
- :key="row.key"
- :style="itemStyle"
- :class="className"
- >
- <slot v-if="row.item.isHeader" name="header" :header="row.item.header" :id="row.item.id">
- <text
- :id="row.item.id"
- :style="{
- ...themeStyles.header.value,
- ...headerStyle,
- }"
- >
- {{ row.item.header }}
- </text>
- </slot>
- <slot v-else name="item" :item="row.item.data" :index="index" :id="row.item.id">
- <SimpleListItem
- :id="row.item.id"
- :item="row.item.data"
- :index="index"
- :dataDisplayProp="dataDisplayProp"
- :colorProp="colorProp"
- :disabledProp="disabledProp"
- :showCheck="mode !== 'select'"
- :checked="checkedList.indexOf(row.item) >= 0"
- @click="onItemPress(row.item, index)"
- >
- <template v-if="$slots.itemContent" #itemContent>
- <slot name="itemContent" :item="row.item.data" :index="index" :id="row.item.id" />
- </template>
- </SimpleListItem>
- </slot>
- </view>
- </slot>
- </template>
- <template #empty>
- <slot name="empty">
- <Empty :description="emptyText" />
- </slot>
- </template>
- <template #inner>
- <IndexBar
- :activeIndex="activeIndex"
- :data="groupedData.index"
- @drag="handleDrag"
- />
- </template>
- </FixedVirtualList>
- </template>
- <script setup lang="ts" generic="T">
- import { computed, nextTick, provide, ref, type Ref } from 'vue';
- import { useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
- import { DynamicColor, DynamicSize, DynamicSize2, DynamicVar } from '../theme/ThemeTools';
- import type { CheckBoxDefaultButtonProps } from '../form/CheckBoxDefaultButton.vue';
- import type { SimpleListContext } from './SimpleList.vue';
- import Empty from '../feedback/Empty.vue';
- import FixedVirtualList from './FixedVirtualList.vue';
- import SimpleListItem from './SimpleListItem.vue';
- import IndexBar from '../nav/IndexBar.vue';
- export interface IndexedListGrouperedData<T> {
- isHeader?: boolean,
- id: string,
- header?: string,
- headerIndex?: number,
- data?: T
- }
- export interface SimpleListProps<T> {
- /**
- * 分组标题的自定义样式
- */
- groupStyle?: ViewStyle;
- /**
- * 分组标题文字的自定义样式
- */
- groupTextStyle?: TextStyle;
- /**
- * 条目的自定义样式
- */
- itemStyle?: ViewStyle;
- /**
- * 条目文字的自定义样式
- */
- textStyle?: TextStyle;
- /**
- * 选中的条目的自定义样式
- */
- checkedItemStyle?: ViewStyle;
- /**
- * 选中的条目文字的自定义样式
- */
- checkedTextStyle?: TextStyle;
- /**
- * 空数据时显示的文字
- */
- emptyText?: string,
- /**
- * 源数据
- */
- data: T[],
- /**
- * 数据项的唯一键名,用于vue循环优化
- */
- dataKey?: string,
- /**
- * 显示数据的prop,如果为空,则尝试直接把数据当 string 显示。
- */
- dataDisplayProp?: string,
- /**
- * 控制数据条目颜色的字段名称,为空则使用默认颜色。
- */
- colorProp?: string,
- /**
- * 控制是否禁用数据条目的字段名称,为空则不禁用。
- */
- disabledProp?: string,
- /**
- * 每个条目的高度px
- * @default 40
- */
- itemHeight?: number;
- /**
- * 分组标题的高度px
- * @default 30
- */
- headerHeight?: number;
-
- /**
- * 对数据进行分组。每个条目会调用一次这个回调,你需要在此回调中返回此条目所在分组的名字。
- */
- groupDataBy: (item: T) => string,
- /**
- * 对组进行排序
- */
- sortGroup?: (headers: string[]) => string[],
- /**
- * 列表的选择模式
- *
- * * select 点击选择模式
- * * single-check 单选选择模式,条目右边有选择框
- * * mulit-check 多选选择模式,条目右边有选择框
- * @default 'select'
- */
- mode?: 'select'|'single-check'|'mulit-check',
- /**
- * 当列表显示选择框时,选择框的自定义属性
- * @default {
- * borderColor: 'border.cell',
- * checkColor: 'white',
- * color: 'primary',
- * size: 20,
- * iconSize: mode === 'single-check' ? 10 : 16,
- * type: mode === 'single-check' ? 'radio' : 'icon',
- * disabled: false,
- * }
- */
- checkProps?: CheckBoxDefaultButtonProps,
- /**
- * 当用使用选择框模式时,默认选中条目
- * @default []
- */
- defaultSelect?: any[],
- innerStyle?: ViewStyle;
- }
- const props = withDefaults(defineProps<SimpleListProps<T>>(), {
- mode: 'select',
- virtual: false,
- itemHeight: 40,
- headerHeight: 30,
- });
- const emit = defineEmits([ 'itemClick', 'selectedItemChanged' ]);
- const themeContext = useTheme();
- const themeStyles = themeContext.useThemeStyles({
- list: {
- backgroundColor: DynamicColor('IndexedListBackgroundColor', 'white'),
- },
- header: {
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: DynamicColor('IndexedListHeaderBackgroundColor', 'background.page'),
- padding: DynamicSize2('IndexedListHeaderPaddingVertical', 'IndexedListHeaderPaddingHorizontal', 0, 30),
- fontSize: DynamicSize('IndexedListHeaderFontSize', 24),
- color: DynamicColor('IndexedListHeaderColor', 'text.second'),
- position: 'sticky',
- },
- item: {
- display: 'flex',
- flexDirection: 'row',
- alignItems: 'center',
- paddingLeft: DynamicSize('IndexedListItemPaddingHorizontal', 30),
- paddingRight: DynamicSize('IndexedListItemPaddingHorizontal', 30),
- fontSize: DynamicSize('IndexedListItemFontSize', 28),
- borderTopWidth: DynamicSize('IndexedListItemBorderTopWidth', 0),
- borderBottomStyle: 'solid',
- borderBottomWidth: DynamicSize('IndexedListItemBorderBottomWidth', 2),
- borderColor: DynamicColor('IndexedListItemBorderColor', 'border.cell'),
- color: DynamicColor('IndexedListItemColor', 'text.content'),
- backgroundColor: DynamicColor('IndexedListItemBackgroundColor', 'white'),
- },
- itemText: {
- },
- itemTextChecked: {
- fontSize: DynamicSize('IndexedListItemCheckedTextFontSize', 28),
- fontWeight: DynamicVar('IndexedListItemCheckedTextFontWeight', 'bold'),
- color: DynamicColor('IndexedListItemCheckedTextColor', 'primary'),
- },
- });
- let flushLock = false;
- const activeIndex = ref(-1);
- const scrollCurrentValue = ref(0);
- function handleScroll(e: any) {
- if (scrollCurrentValue.value != e.detail.scrollTop) {
- scrollCurrentValue.value = e.detail.scrollTop;
- if (!flushLock) {
- const index = Math.floor(scrollCurrentValue.value / props.itemHeight);
- for (let i = index; i >= 0; i--) {
- if (groupedData.value.list[i].isHeader) {
- activeIndex.value = groupedData.value.list[i].headerIndex || -1;
- break;
- }
- }
- }
- }
- }
- function handleDrag(v: number) {
- flushLock = true;
- nextTick(() => {
- activeIndex.value = v;
- const index = groupedData.value.list.findIndex(p => p.header === groupedData.value.index[v])
- if (index >= 0)
- scrollCurrentValue.value = index * props.itemHeight;
- nextTick(() => {
- flushLock = false;
- });
- });
- }
- const headerStyle = computed(() => ({
- ...themeStyles.header.value,
- height: `${props.headerHeight}px`,
- } as ViewStyle));
- const itemStyle = computed(() => ({
- ...themeStyles.item.value,
- height: `${props.itemHeight}px`,
- ...props.itemStyle,
- } as ViewStyle));
- const textStyle = computed(() => ({
- ...themeStyles.itemText.value,
- ...props.textStyle,
- } as ViewStyle));
- const checkedItemStyle = computed(() => ({
- ...themeStyles.item.value,
- height: themeContext.resolveSize(props.itemHeight),
- ...props.checkedItemStyle,
- } as ViewStyle));
- const checkedTextStyle = computed(() => ({
- ...themeStyles.itemText.value,
- ...themeStyles.itemTextChecked.value,
- ...props.checkedTextStyle,
- } as ViewStyle));
- const checkProps = computed(() => ({
- borderColor: 'border.cell',
- checkColor: 'white',
- color: 'primary',
- size: 40,
- iconSize: props.mode === 'single-check' ? 20 : 32,
- type: props.mode === 'single-check' ? 'radio' : 'icon',
- disabled: false,
- ...props.checkProps,
- } as CheckBoxDefaultButtonProps));
- const pressedColor = computed(() => {
- return themeContext.resolveThemeColor('SimpleListItemPressedColor', 'pressed.white')!;
- });
- const groupedData = computed(() => {
- const map = new Map<string, T[]>();
- //归组数据
- props.data.forEach((model) => {
- const groupKey = props.groupDataBy(model);
- let arr = map.get(groupKey);
- if (!arr){
- arr = [];
- map.set(groupKey, arr);
- }
- arr.push(model);
- });
- //填充数据
- const arr = [] as IndexedListGrouperedData<T>[];
- const arrIndex = [] as string[];
- //排序分组
- const mapKeys = Array.from(map.keys());
- const groups = props.sortGroup ? props.sortGroup(mapKeys) : mapKeys;
- let i = 0, j = 0;
- for (const k of groups) {
- const v = map.get(k);
- if (!v) continue;
- arr.push({
- id: `indexListNav${k}`,
- isHeader: true,
- headerIndex: j,
- header: k,
- });
- v.forEach((c) => {
- arr.push({
- id: `indexListItem${i++}`,
- data: c,
- });
- });
- arrIndex.push(k);
- j++;
- }
- return {
- list: arr,
- index: arrIndex,
- };
- });
- provide<SimpleListContext>('SimpleListContext', {
- itemStyle,
- textStyle,
- checkedItemStyle,
- checkedTextStyle,
- checkProps,
- pressedColor,
- });
- const checkedList = ref(props.defaultSelect || []) as Ref<IndexedListGrouperedData<T>[]>;
- function onItemPress(item: IndexedListGrouperedData<T>, index: number) {
- if (props.mode === 'single-check') {
- checkedList.value = ([ item ]);
- emit('selectedItemChanged', [ item.data ]);
- } else if (props.mode === 'mulit-check') {
- checkedList.value = ((prev) => {
- let arr : IndexedListGrouperedData<T>[];
- if (prev.indexOf(item) >= 0)
- arr = prev.filter(k => k !== item);
- else
- arr = prev.concat([ item ]);
- setTimeout(() => {
- emit('selectedItemChanged', arr.map(p => p.data));
- }, 100);
- return arr;
- })(checkedList.value);
- }
- emit('itemClick', item.data, index);
- }
- </script>
|