|
@@ -0,0 +1,354 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <!-- 通用列表页 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'common-list-page d-flex flex-column',
|
|
|
|
|
+ hasBg ? 'bg-base p-3' : ''
|
|
|
|
|
+ ]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <view v-if="tabs" class="top-tab bg-base">
|
|
|
|
|
+ <Tabs
|
|
|
|
|
+ :tabs="tabs"
|
|
|
|
|
+ :width="700"
|
|
|
|
|
+ v-model:currentIndex="tabCurrentIndex"
|
|
|
|
|
+ :autoScroll="false"
|
|
|
|
|
+ @click="handleTabClick"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 搜索 -->
|
|
|
|
|
+ <view v-if="showSearch" class="d-flex flex-col">
|
|
|
|
|
+ <SearchBar
|
|
|
|
|
+ v-model="searchValue"
|
|
|
|
|
+ :placeholder="`输入关键词搜索${title}`"
|
|
|
|
|
+ @search="doSearch"
|
|
|
|
|
+ @cancel="doSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <!-- 下拉框 -->
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="dropDownNames.length > 0"
|
|
|
|
|
+ class="d-flex flex-row justify-between align-center mt-2"
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ dropDownVisibleCount >= 3 ? 'justify-around' : ('justify-between')
|
|
|
|
|
+ ]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <template v-for="(drop, k) in dropDownNames" :key="k" >
|
|
|
|
|
+ <SimpleDropDownPicker
|
|
|
|
|
+ v-if="!drop.activeTab || drop.activeTab.includes(tabCurrentIndex)"
|
|
|
|
|
+ :modelValue="dropDownValues[k]"
|
|
|
|
|
+ :columns="drop.options"
|
|
|
|
|
+ :style="{maxWidth: `${100/dropDownNames.length}%`}"
|
|
|
|
|
+ @update:modelValue="(v) => handleChangeDropDownValue(k, v)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="(showTotal && dropDownVisibleCount < 3)"
|
|
|
|
|
+ class="d-flex flex-row align-center mt-3 size-s color-primary text-bold"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text>总共有 {{ listLoader.total }} 个</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-if="(dropDownVisibleCount >= 3 || dropDownVisibleCount == 0)"
|
|
|
|
|
+ class="d-flex flex-row justify-center align-center mt-3 size-s color-primary text-bold"
|
|
|
|
|
+ >
|
|
|
|
|
+ <text>总共有 {{ listLoader.total }} 个</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 列表 -->
|
|
|
|
|
+ <view class="position-relative d-flex flex-row flex-wrap justify-between align-stretch mt-3">
|
|
|
|
|
+ <view
|
|
|
|
|
+ v-for="(item, i) in listLoader.list.value"
|
|
|
|
|
+ :key="item.id"
|
|
|
|
|
+ :class="[
|
|
|
|
|
+ 'position-relative d-flex flex-grow-1',
|
|
|
|
|
+ itemType.endsWith('-2') ? 'width-1-2' : 'w-100'
|
|
|
|
|
+ ]"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Box2LineLargeImageUserShadow
|
|
|
|
|
+ v-if="itemType.startsWith('image-large')"
|
|
|
|
|
+ class="w-100"
|
|
|
|
|
+ titleColor="title-text"
|
|
|
|
|
+ :classNames="getItemClass(i)"
|
|
|
|
|
+ :image="getImage(item)"
|
|
|
|
|
+ :titleBox="item.titleBox"
|
|
|
|
|
+ :title="item.title"
|
|
|
|
|
+ :desc="item.desc"
|
|
|
|
|
+ :tags="item.bottomTags"
|
|
|
|
|
+ :badge="item.badge"
|
|
|
|
|
+ @click="goDetails(item, item.id)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Box2LineImageRightShadow
|
|
|
|
|
+ v-else-if="itemType.startsWith('article-common')"
|
|
|
|
|
+ class="w-100"
|
|
|
|
|
+ titleColor="title-text"
|
|
|
|
|
+ :titleBox="item.titleBox"
|
|
|
|
|
+ :classNames="getItemClass(i)"
|
|
|
|
|
+ :image="getImage(item)"
|
|
|
|
|
+ :title="item.title"
|
|
|
|
|
+ :desc="item.desc"
|
|
|
|
|
+ :tags="item.bottomTags"
|
|
|
|
|
+ :badge="item.badge"
|
|
|
|
|
+ :wideImage="true"
|
|
|
|
|
+ @click="goDetails(item, item.id)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Box2LineImageRightShadow
|
|
|
|
|
+ v-else-if="itemType.startsWith('article-character')"
|
|
|
|
|
+ class="w-100"
|
|
|
|
|
+ :classNames="getItemClass(i)"
|
|
|
|
|
+ :image="getImage(item)"
|
|
|
|
|
+ titleColor="title-text"
|
|
|
|
|
+ :title="item.title"
|
|
|
|
|
+ :titleBox="item.titleBox"
|
|
|
|
|
+ :tags="item.bottomTags || item.keywords"
|
|
|
|
|
+ :desc="item.desc"
|
|
|
|
|
+ :badge="item.badge"
|
|
|
|
|
+ @click="goDetails(item, item.id)"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" class="width-1-2" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <SimplePageListLoader :loader="listLoader" />
|
|
|
|
|
+ </view>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, nextTick, onMounted, ref, watch, type PropType } from 'vue';
|
|
|
|
|
+import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
|
|
|
|
|
+import { navTo } from '@/components/utils/PageAction';
|
|
|
|
|
+import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
|
|
|
|
|
+import Box2LineLargeImageUserShadow from '@/common/components/parts/Box2LineLargeImageUserShadow.vue';
|
|
|
|
|
+import Box2LineImageRightShadow from '@/common/components/parts/Box2LineImageRightShadow.vue';
|
|
|
|
|
+import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
|
|
|
|
|
+import AppCofig from '@/common/config/AppCofig';
|
|
|
|
|
+import Tabs from '@/components/nav/Tabs.vue';
|
|
|
|
|
+import SearchBar from '@/components/form/SearchBar.vue';
|
|
|
|
|
+
|
|
|
|
|
+function getImage(item: any) {
|
|
|
|
|
+ return item.thumbnail || item.image || AppCofig.defaultImage
|
|
|
|
|
+}
|
|
|
|
|
+function getItemClass(index: number) {
|
|
|
|
|
+ return props.itemType.endsWith('-2') ? (index % 2 != 0 ? 'ml-1' : 'mr-1') : ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export interface DropDownNames {
|
|
|
|
|
+ options: SimpleDropDownPickerItem[],
|
|
|
|
|
+ defaultSelectedValue: number|string,
|
|
|
|
|
+ activeTab?: number[],
|
|
|
|
|
+}
|
|
|
|
|
+export interface CommonListItem extends Record<string, any> {
|
|
|
|
|
+ id: number,
|
|
|
|
|
+ image: string,
|
|
|
|
|
+ title: string,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const props = defineProps({
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 标题
|
|
|
|
|
+ */
|
|
|
|
|
+ title: {
|
|
|
|
|
+ type: String,
|
|
|
|
|
+ default: '',
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分组标签
|
|
|
|
|
+ */
|
|
|
|
|
+ tabs: {
|
|
|
|
|
+ type: Array as PropType<{
|
|
|
|
|
+ id: number,
|
|
|
|
|
+ text: string,
|
|
|
|
|
+ jump?: () => void,
|
|
|
|
|
+ }[]>,
|
|
|
|
|
+ default: null,
|
|
|
|
|
+ },
|
|
|
|
|
+ tabsScrollable: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 是否显示搜索框
|
|
|
|
|
+ */
|
|
|
|
|
+ showSearch: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 显示总数
|
|
|
|
|
+ */
|
|
|
|
|
+ showTotal: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: false,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 下拉框选项控制
|
|
|
|
|
+ */
|
|
|
|
|
+ dropDownNames: {
|
|
|
|
|
+ type: Object as PropType<DropDownNames[]>,
|
|
|
|
|
+ default: () => ([]),
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 列表项类型
|
|
|
|
|
+ */
|
|
|
|
|
+ itemType: {
|
|
|
|
|
+ type: String as PropType<'image-large-2'|'image-large'|'article-common'|'article-character'>,
|
|
|
|
|
+ default: 'article-common',
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分页大小
|
|
|
|
|
+ */
|
|
|
|
|
+ pageSize: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: 8,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 加载数据函数
|
|
|
|
|
+ * @param page 页码,从1开始
|
|
|
|
|
+ * @param pageSize 分页大小
|
|
|
|
|
+ * @param searchText 搜索文本
|
|
|
|
|
+ * @param dropDownValues 下拉框值
|
|
|
|
|
+ */
|
|
|
|
|
+ load: {
|
|
|
|
|
+ type: Function as PropType<(
|
|
|
|
|
+ page: number,
|
|
|
|
|
+ pageSize: number,
|
|
|
|
|
+ searchText: string,
|
|
|
|
|
+ dropDownValues: number[],
|
|
|
|
|
+ tabSelect: number,
|
|
|
|
|
+ ) => Promise<{ list: CommonListItem[], total: number }>>,
|
|
|
|
|
+ required: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 点击详情跳转页面路径
|
|
|
|
|
+ */
|
|
|
|
|
+ detailsPage: {
|
|
|
|
|
+ type: [String,Object],
|
|
|
|
|
+ default: '/pages/article/details'
|
|
|
|
|
+ },
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 详情跳转页面参数
|
|
|
|
|
+ */
|
|
|
|
|
+ detailsParams: {
|
|
|
|
|
+ type: Object as PropType<Record<string, any>>,
|
|
|
|
|
+ default: () => ({})
|
|
|
|
|
+ },
|
|
|
|
|
+ hasBg: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true,
|
|
|
|
|
+ },
|
|
|
|
|
+ startTabIndex: {
|
|
|
|
|
+ type: Number,
|
|
|
|
|
+ default: undefined,
|
|
|
|
|
+ },
|
|
|
|
|
+ loadMounted: {
|
|
|
|
|
+ type: Boolean,
|
|
|
|
|
+ default: true,
|
|
|
|
|
+ },
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const emit = defineEmits([ 'goCustomDetails' ])
|
|
|
|
|
+
|
|
|
|
|
+const dropDownVisibleCount = computed(() => {
|
|
|
|
|
+ let c = 0;
|
|
|
|
|
+ for (const element of props.dropDownNames) {
|
|
|
|
|
+ if (!element.activeTab || element.activeTab.includes(tabCurrentIndex.value))
|
|
|
|
|
+ c++;
|
|
|
|
|
+ }
|
|
|
|
|
+ return c;
|
|
|
|
|
+})
|
|
|
|
|
+const dropDownValues = ref<any>([]);
|
|
|
|
|
+const searchValue = ref('');
|
|
|
|
|
+const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize) => {
|
|
|
|
|
+ return await props.load(
|
|
|
|
|
+ page, pageSize,
|
|
|
|
|
+ searchValue.value,
|
|
|
|
|
+ dropDownValues.value,
|
|
|
|
|
+ props.tabs?.[tabCurrentIndex.value]?.id ?? tabCurrentIndex.value,
|
|
|
|
|
+ )
|
|
|
|
|
+});
|
|
|
|
|
+const tabCurrentIndex = ref(0)
|
|
|
|
|
+
|
|
|
|
|
+function handleChangeDropDownValue(index: number, value: number) {
|
|
|
|
|
+ dropDownValues.value[index] = value;
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+}
|
|
|
|
|
+function handleTabClick(e: any) {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (props.tabs?.[tabCurrentIndex.value]?.jump) {
|
|
|
|
|
+ props.tabs[tabCurrentIndex.value].jump?.();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+function doSearch() {
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+}
|
|
|
|
|
+function goDetails(item: any, id: number) {
|
|
|
|
|
+ if (props.detailsPage == 'custom') {
|
|
|
|
|
+ emit('goCustomDetails', item, id)
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof props.detailsPage === 'object' && typeof props.detailsPage[0] === 'string') {
|
|
|
|
|
+ navTo(props.detailsPage[tabCurrentIndex.value], {
|
|
|
|
|
+ ...props.detailsParams,
|
|
|
|
|
+ id
|
|
|
|
|
+ })
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof props.detailsPage == 'object' && typeof props.detailsPage[0] === 'object') {
|
|
|
|
|
+ const item = props.detailsPage[tabCurrentIndex.value];
|
|
|
|
|
+ navTo(item.page, {
|
|
|
|
|
+ ...item.params,
|
|
|
|
|
+ id
|
|
|
|
|
+ })
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ navTo(props.detailsPage as string, {
|
|
|
|
|
+ ...props.detailsParams,
|
|
|
|
|
+ id
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function loadDropDownValues() {
|
|
|
|
|
+ dropDownValues.value = [];
|
|
|
|
|
+ for (const element of props.dropDownNames) {
|
|
|
|
|
+ dropDownValues.value.push(element.defaultSelectedValue);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+watch(tabCurrentIndex, () => {
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+});
|
|
|
|
|
+watch(() => props.startTabIndex, () => {
|
|
|
|
|
+ if (props.startTabIndex) {
|
|
|
|
|
+ tabCurrentIndex.value = props.startTabIndex;
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+watch(() => props.dropDownNames.length, () => {
|
|
|
|
|
+ loadDropDownValues();
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+defineExpose({
|
|
|
|
|
+ load: () => {
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+ },
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ if (props.startTabIndex)
|
|
|
|
|
+ tabCurrentIndex.value = props.startTabIndex;
|
|
|
|
|
+ if (props.title)
|
|
|
|
|
+ uni.setNavigationBarTitle({ title: props.title, })
|
|
|
|
|
+ loadDropDownValues();
|
|
|
|
|
+ if (props.loadMounted)
|
|
|
|
|
+ listLoader.loadData(undefined, true);
|
|
|
|
|
+});
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style lang="scss">
|
|
|
|
|
+.common-list-page {
|
|
|
|
|
+ min-height: 100vh;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|