CommonListPage.vue 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  1. <template>
  2. <!-- 通用列表页 -->
  3. <view
  4. :class="[
  5. 'common-list-page d-flex flex-column',
  6. hasBg ? 'bg-base p-3' : ''
  7. ]"
  8. >
  9. <view v-if="showTab && tabs" class="top-tab bg-base">
  10. <Tabs
  11. :tabs="tabs"
  12. :width="700"
  13. v-model:currentIndex="tabCurrentIndex"
  14. :autoScroll="false"
  15. :defaultItemWidth="180"
  16. :autoItemWidth="tabsScrollable ? false : true"
  17. @click="handleTabClick"
  18. />
  19. </view>
  20. <!-- 搜索 -->
  21. <view v-if="showSearch" class="d-flex flex-col">
  22. <SearchBar
  23. v-model="searchValue"
  24. :placeholder="`输入关键词搜索${title}`"
  25. @search="doSearch"
  26. @cancel="doSearch"
  27. />
  28. </view>
  29. <!-- 下拉框 -->
  30. <view
  31. v-if="dropDownNames.length > 0"
  32. class="d-flex flex-row justify-between align-center mt-2"
  33. :class="[
  34. dropDownVisibleCount >= 3 ? 'justify-around' : ('justify-between')
  35. ]"
  36. >
  37. <template v-for="(drop, k) in dropDownNames" :key="k" >
  38. <SimpleDropDownPicker
  39. v-if="!drop.activeTab || drop.activeTab.includes(tabCurrentIndex)"
  40. :modelValue="dropDownValues[k]"
  41. :columns="drop.options"
  42. :style="{maxWidth: `${100/dropDownNames.length}%`}"
  43. @update:modelValue="(v) => handleChangeDropDownValue(k, v)"
  44. />
  45. </template>
  46. <view
  47. v-if="(showTotal && dropDownVisibleCount < 3 && dropDownVisibleCount != 0)"
  48. class="d-flex flex-row align-center mt-3 size-s color-primary text-bold"
  49. >
  50. <text>总共有 {{ listLoader.total }} 个</text>
  51. </view>
  52. </view>
  53. <view
  54. v-if="showTotal && (dropDownVisibleCount >= 3 || dropDownVisibleCount == 0)"
  55. class="d-flex flex-row justify-center align-center mt-3 size-s color-primary text-bold"
  56. >
  57. <text>总共有 {{ listLoader.total }} 个</text>
  58. </view>
  59. <!-- 列表 -->
  60. <view class="position-relative d-flex flex-row flex-wrap justify-between align-stretch mt-3">
  61. <slot name="list" />
  62. <view
  63. v-for="(item, i) in listLoader.list.value"
  64. :key="item.id"
  65. :class="[
  66. 'position-relative d-flex flex-grow-1',
  67. itemType.endsWith('-2') ? 'width-1-2' : 'w-100'
  68. ]"
  69. >
  70. <Box2LineLargeImageUserShadow
  71. v-if="itemType.startsWith('image-large')"
  72. class="w-100"
  73. titleColor="title-text"
  74. :classNames="getItemClass(i)"
  75. :image="getImage(item)"
  76. :titleBox="item.titleBox"
  77. :titlePrefix="item.titlePrefix"
  78. :title="item.title"
  79. :desc="item.desc"
  80. :tags="item.bottomTags"
  81. :badge="item.badge"
  82. @click="goDetails(item, item.id)"
  83. />
  84. <Box2LineImageRightShadow
  85. v-else-if="itemType.startsWith('article-common')"
  86. class="w-100"
  87. titleColor="title-text"
  88. :titleBox="item.titleBox"
  89. :titlePrefix="item.titlePrefix"
  90. :classNames="getItemClass(i)"
  91. :image="getImage(item)"
  92. :title="item.title"
  93. :desc="item.desc"
  94. :tags="item.bottomTags"
  95. :badge="item.badge"
  96. :wideImage="true"
  97. @click="goDetails(item, item.id)"
  98. />
  99. <Box2LineImageRightShadow
  100. v-else-if="itemType.startsWith('article-character')"
  101. class="w-100"
  102. :classNames="getItemClass(i)"
  103. :image="getImage(item)"
  104. titleColor="title-text"
  105. :title="item.title"
  106. :titlePrefix="item.titlePrefix"
  107. :titleBox="item.titleBox"
  108. :tags="item.bottomTags || item.keywords"
  109. :desc="item.desc"
  110. :badge="item.badge"
  111. @click="goDetails(item, item.id)"
  112. />
  113. </view>
  114. <view v-if="itemType.endsWith('-2') && listLoader.list.value.length % 2 != 0" class="width-1-2" />
  115. </view>
  116. <SimplePageListLoader :loader="listLoader" />
  117. </view>
  118. </template>
  119. <script setup lang="ts">
  120. import { computed, nextTick, onMounted, ref, watch, type PropType } from 'vue';
  121. import { useSimplePageListLoader } from '@/common/composeabe/SimplePageListLoader';
  122. import { navTo } from '@/components/utils/PageAction';
  123. import SimplePageListLoader from '@/common/components/SimplePageListLoader.vue';
  124. import Box2LineLargeImageUserShadow from '@/pages/parts/Box2LineLargeImageUserShadow.vue';
  125. import Box2LineImageRightShadow from '@/pages/parts/Box2LineImageRightShadow.vue';
  126. import SimpleDropDownPicker, { type SimpleDropDownPickerItem } from '@/common/components/SimpleDropDownPicker.vue';
  127. import AppCofig from '@/common/config/AppCofig';
  128. import Tabs from '@/components/nav/Tabs.vue';
  129. import SearchBar from '@/components/form/SearchBar.vue';
  130. function getImage(item: any) {
  131. return item.thumbnail || item.image || AppCofig.defaultImage
  132. }
  133. function getItemClass(index: number) {
  134. return props.itemType.endsWith('-2') ? (index % 2 != 0 ? 'ml-1' : 'mr-1') : ''
  135. }
  136. export interface DropDownNames {
  137. options: SimpleDropDownPickerItem[],
  138. defaultSelectedValue: number|string,
  139. activeTab?: number[],
  140. }
  141. export interface CommonListItem extends Record<string, any> {
  142. id: number,
  143. image: string,
  144. title: string,
  145. }
  146. const props = defineProps({
  147. /**
  148. * 标题
  149. */
  150. title: {
  151. type: String,
  152. default: '',
  153. },
  154. /**
  155. * 分组标签
  156. */
  157. tabs: {
  158. type: Array as PropType<{
  159. id: number,
  160. text: string,
  161. onlyJump?: boolean,
  162. jump?: () => void,
  163. width?: number,
  164. }[]>,
  165. default: null,
  166. },
  167. tabsScrollable: {
  168. type: Boolean,
  169. default: false,
  170. },
  171. /**
  172. * 是否显示搜索框
  173. */
  174. showSearch: {
  175. type: Boolean,
  176. default: true,
  177. },
  178. /**
  179. * 是否显示Tab
  180. */
  181. showTab: {
  182. type: Boolean,
  183. default: true,
  184. },
  185. /**
  186. * 显示总数
  187. */
  188. showTotal: {
  189. type: Boolean,
  190. default: false,
  191. },
  192. /**
  193. * 下拉框选项控制
  194. */
  195. dropDownNames: {
  196. type: Object as PropType<DropDownNames[]>,
  197. default: () => ([]),
  198. },
  199. /**
  200. * 列表项类型
  201. */
  202. itemType: {
  203. type: String as PropType<'image-large-2'|'image-large'|'article-common'|'article-character'>,
  204. default: 'article-common',
  205. },
  206. /**
  207. * 分页大小
  208. */
  209. pageSize: {
  210. type: Number,
  211. default: 8,
  212. },
  213. /**
  214. * 加载数据函数
  215. * @param page 页码,从1开始
  216. * @param pageSize 分页大小
  217. * @param searchText 搜索文本
  218. * @param dropDownValues 下拉框值
  219. */
  220. load: {
  221. type: Function as PropType<(
  222. page: number,
  223. pageSize: number,
  224. searchText: string,
  225. dropDownValues: number[],
  226. tabSelect: number,
  227. ) => Promise<{ list: CommonListItem[], total: number }>>,
  228. required: true,
  229. },
  230. /**
  231. * 点击详情跳转页面路径
  232. */
  233. detailsPage: {
  234. type: [String,Object],
  235. default: '/pages/article/details'
  236. },
  237. /**
  238. * 详情跳转页面参数
  239. */
  240. detailsParams: {
  241. type: Object as PropType<Record<string, any>>,
  242. default: () => ({})
  243. },
  244. hasBg: {
  245. type: Boolean,
  246. default: true,
  247. },
  248. startTabIndex: {
  249. type: Number,
  250. default: undefined,
  251. },
  252. loadMounted: {
  253. type: Boolean,
  254. default: true,
  255. },
  256. })
  257. const emit = defineEmits([ 'goCustomDetails' ])
  258. const dropDownVisibleCount = computed(() => {
  259. let c = 0;
  260. for (const element of props.dropDownNames) {
  261. if (!element.activeTab || element.activeTab.includes(tabCurrentIndex.value))
  262. c++;
  263. }
  264. return c;
  265. })
  266. const dropDownValues = ref<any>([]);
  267. const searchValue = ref('');
  268. const listLoader = useSimplePageListLoader(props.pageSize, async (page, pageSize) => {
  269. return await props.load(
  270. page, pageSize,
  271. searchValue.value,
  272. dropDownValues.value,
  273. props.tabs?.[tabCurrentIndex.value]?.id ?? tabCurrentIndex.value,
  274. )
  275. });
  276. const tabCurrentIndex = ref(0)
  277. function handleChangeDropDownValue(index: number, value: number) {
  278. dropDownValues.value[index] = value;
  279. listLoader.loadData(undefined, true);
  280. }
  281. function handleTabClick(e: any) {
  282. nextTick(() => {
  283. if (e.jump) {
  284. e.jump?.();
  285. tabCurrentIndex.value = 0;
  286. return;
  287. }
  288. listLoader.loadData(undefined, true);
  289. })
  290. }
  291. function doSearch() {
  292. listLoader.loadData(undefined, true);
  293. }
  294. function goDetails(item: any, id: number) {
  295. if (props.detailsPage == 'disabled')
  296. return;
  297. if (props.detailsPage == 'custom') {
  298. emit('goCustomDetails', item, id)
  299. return;
  300. }
  301. if (typeof props.detailsPage === 'object' && typeof props.detailsPage[0] === 'string') {
  302. navTo(props.detailsPage[tabCurrentIndex.value], {
  303. ...props.detailsParams,
  304. id
  305. })
  306. return;
  307. }
  308. if (typeof props.detailsPage == 'object' && typeof props.detailsPage[0] === 'object') {
  309. const item = props.detailsPage[tabCurrentIndex.value];
  310. navTo(item.page, {
  311. ...item.params,
  312. id
  313. })
  314. return;
  315. }
  316. navTo(props.detailsPage as string, {
  317. ...props.detailsParams,
  318. id
  319. })
  320. }
  321. function loadDropDownValues() {
  322. dropDownValues.value = [];
  323. for (const element of props.dropDownNames) {
  324. dropDownValues.value.push(element.defaultSelectedValue);
  325. }
  326. }
  327. watch(tabCurrentIndex, () => {
  328. listLoader.loadData(undefined, true);
  329. });
  330. watch(() => props.startTabIndex, () => {
  331. if (props.startTabIndex) {
  332. tabCurrentIndex.value = props.startTabIndex;
  333. }
  334. });
  335. watch(() => props.dropDownNames.length, () => {
  336. loadDropDownValues();
  337. listLoader.loadData(undefined, true);
  338. });
  339. defineExpose({
  340. load: () => {
  341. listLoader.loadData(undefined, true);
  342. },
  343. })
  344. onMounted(() => {
  345. if (props.startTabIndex)
  346. tabCurrentIndex.value = props.startTabIndex;
  347. if (props.title)
  348. uni.setNavigationBarTitle({ title: props.title, })
  349. loadDropDownValues();
  350. if (props.loadMounted)
  351. listLoader.loadData(undefined, true);
  352. });
  353. </script>
  354. <style lang="scss">
  355. .common-list-page {
  356. min-height: 100vh;
  357. }
  358. </style>