CommonListBlock.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. <template>
  2. <!-- 通用列表页详情 -->
  3. <div v-show="show" >
  4. <div class="content mb-2">
  5. <!-- 搜素栏 -->
  6. <div class="row mt-3 align-items-center">
  7. <!-- 左栏 -->
  8. <div class="col-sm-12 col-md-6 col-lg-6">
  9. <!-- 分类 -->
  10. <TagBar
  11. :tags="tagsData || []"
  12. :margin="[30, 70]"
  13. v-model:selectedTag="selectedTag"
  14. />
  15. <!-- 标题 -->
  16. <div v-if="showNav" class="nav-back-title">
  17. <img src="@/assets/images/BackArrow.png" alt="返回" @click="router.back()" />
  18. <h2>{{ title }}</h2>
  19. </div>
  20. <!-- 标题 -->
  21. <div v-if="showTotal" class="nav-back-title">
  22. 共有 {{ newsLoader.total }} 个{{ title }}
  23. </div>
  24. </div>
  25. <!-- 右栏 -->
  26. <div class="col-sm-12 col-md-6 col-lg-6 d-flex flex-row justify-content-end align-items-start" style="gap:5px">
  27. <Dropdown
  28. v-for="(drop, k) in dropDownNames" :key="k"
  29. :selectedValue="dropDownValues[k]"
  30. :options="drop.options"
  31. labelKey="name"
  32. valueKey="id"
  33. style="max-width: 150px"
  34. @update:selectedValue="(v) => handleChangeDropDownValue(k, v)"
  35. />
  36. <SimpleInput v-if="showSearch" v-model="searchText" placeholder="请输入关键词" @search="handleSearch">
  37. <template #suffix>
  38. <IconSearch
  39. class="search-icon"
  40. src="@/assets/images/news/IconSearch.png"
  41. alt="搜索"
  42. @click="newsLoader.loadData(undefined, true)"
  43. />
  44. </template>
  45. </SimpleInput>
  46. <button class="tab-button" v-if="showTableSwitch" @click="tableListShow=!tableListShow">
  47. ▼ 清单
  48. </button>
  49. </div>
  50. </div>
  51. </div>
  52. <div
  53. :class="[
  54. 'content',
  55. 'news-list',
  56. rowCount === 1 ? '' : 'grid',
  57. ]"
  58. >
  59. <!-- 新闻列表 -->
  60. <SimplePageContentLoader :loader="newsLoader">
  61. <div v-if="tableListShow" class="table-list">
  62. <table>
  63. <thead>
  64. <tr>
  65. <th>序号</th>
  66. <th>{{ tableSwitchOptions.title ?? '标题'}}</th>
  67. <th v-for="(t, k) in newsLoader.list.value[0]?.addItems || []" :key="k">{{ t.name }}</th>
  68. </tr>
  69. </thead>
  70. <tbody>
  71. <tr v-for="(item, k) in newsLoader.list.value" :key="item.id">
  72. <td>{{ (newsLoader.page.value - 1) * 100 + k + 1 }}</td>
  73. <td :class="{ 'title-box': item.titleBox, }">{{ item.title }}</td>
  74. <td v-for="(t, k) in item.addItems || []" :key="k">{{ t.text }}</td>
  75. </tr>
  76. </tbody>
  77. </table>
  78. </div>
  79. <div v-else class="list">
  80. <div
  81. v-for="(item, k) in newsLoader.list.value"
  82. :key="item.id"
  83. :class="'item user-select-none main-clickable row-type'+rowType"
  84. :style="{ width: rowWidth }"
  85. @click="handleShowDetail(item)"
  86. >
  87. <a class="d-none" :href="router.resolve({ path: props.detailsPage, query: { id: item.id }}).href" />
  88. <ImageTitleBlock
  89. :image="item.image || defaultImage"
  90. />
  91. <TitleDescBlock
  92. :title="item.title"
  93. :titleBox="item.titleBox"
  94. :desc="item.desc"
  95. >
  96. <template #addon>
  97. <div v-if="item.bottomTags" class="tags">
  98. <div
  99. v-for="(tag, k) in item.bottomTags"
  100. :key="k"
  101. :class="tag ? '' : 'd-none'"
  102. >{{ tag }}</div>
  103. </div>
  104. <div v-if="item.addItems" class="extra">
  105. <div
  106. v-for="(addItem, k) in item.addItems"
  107. :key="k"
  108. class="d-flex flex-row align-items-center"
  109. :class="[
  110. addItem.text ? '' : 'd-none',
  111. ]"
  112. >
  113. <span class="desc">{{ addItem.name }}:</span>
  114. <span>{{ addItem.text }}</span>
  115. </div>
  116. </div>
  117. </template>
  118. </TitleDescBlock>
  119. </div>
  120. <div
  121. v-for="count of placeholderItemCount"
  122. :key="count"
  123. class="item empty"
  124. :style="{ width: rowWidth }"
  125. />
  126. </div>
  127. </SimplePageContentLoader>
  128. </div>
  129. <!-- 分页 -->
  130. <Pagination
  131. :currentPage="newsLoader.page.value"
  132. :totalPages="newsLoader.totalPages.value"
  133. @update:currentPage="handleChangePage"
  134. />
  135. </div>
  136. </template>
  137. <script setup lang="ts">
  138. import { computed, onMounted, ref, watch, type PropType } from 'vue';
  139. import { useSSrSimplePagerDataLoader } from '@/composeable/SimplePagerDataLoader';
  140. import TagBar from '../content/TagBar.vue';
  141. import Dropdown from '../controls/Dropdown.vue';
  142. import SimpleInput from '../controls/SimpleInput.vue';
  143. import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
  144. import Pagination from '../controls/Pagination.vue';
  145. import TitleDescBlock from '../parts/TitleDescBlock.vue';
  146. import IconSearch from '../icons/IconSearch.vue';
  147. export interface DropdownCommonItem {
  148. id: number;
  149. name: string;
  150. }
  151. export interface DropDownNames {
  152. options: (string|DropdownCommonItem)[],
  153. label?: string,
  154. defaultSelectedValue: number|string,
  155. }
  156. const tableListShow = ref(false);
  157. const props = defineProps({
  158. title: {
  159. type: String,
  160. default: '',
  161. },
  162. show: {
  163. type: Boolean,
  164. default: true,
  165. },
  166. showTableSwitch: {
  167. type: Boolean,
  168. default: false,
  169. },
  170. tableSwitchOptions: {
  171. type: Object,
  172. default: () => ({}),
  173. },
  174. showNav: {
  175. type: Boolean,
  176. default: false,
  177. },
  178. showTotal: {
  179. type: Boolean,
  180. default: false,
  181. },
  182. prevPage: {
  183. type: Object as PropType<{
  184. title: string,
  185. url?: string,
  186. }>,
  187. default: null,
  188. },
  189. dropDownNames: {
  190. type: Object as PropType<DropDownNames[]>,
  191. default: null,
  192. },
  193. showSearch: {
  194. type: Boolean,
  195. default: true,
  196. },
  197. tagsData: {
  198. type: Object as PropType<{
  199. id: number,
  200. name: string,
  201. }[]>,
  202. default: null,
  203. },
  204. pageSize: {
  205. type: Number,
  206. default: 8,
  207. },
  208. rowCount: {
  209. type: Number,
  210. default: 2,
  211. },
  212. rowType: {
  213. type: Number,
  214. default: 1,
  215. },
  216. defaultSelectTag: {
  217. type: Number,
  218. default: 1,
  219. },
  220. load: {
  221. type: Function as PropType<(
  222. page: number,
  223. pageSize: number,
  224. selectedTag: number,
  225. searchText: string,
  226. dropDownValues: number[],
  227. ) => Promise<{
  228. page: number,
  229. total: number,
  230. data: any[],
  231. }>>,
  232. required: true,
  233. },
  234. showDetail: {
  235. type: Function as PropType<(item: any) => void>,
  236. default: null,
  237. },
  238. subName: {
  239. type: String,
  240. default: '',
  241. },
  242. /**
  243. * 点击详情跳转页面路径
  244. */
  245. detailsPage: {
  246. type: String,
  247. default: '/news/detail'
  248. },
  249. /**
  250. * 详情跳转页面参数
  251. */
  252. detailsParams: {
  253. type: Object as PropType<Record<string, any>>,
  254. default: () => ({})
  255. },
  256. defaultImage: {
  257. type: String,
  258. default: 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png'
  259. },
  260. detailModelId: {
  261. type: Number,
  262. default: 0,
  263. }
  264. })
  265. const router = useRouter();
  266. const realRowCount = computed(() => {
  267. if (import.meta.client)
  268. if (window.innerWidth < 768)
  269. return 1;
  270. return props.rowCount;
  271. });
  272. const rowWidth = computed(() => {
  273. switch (realRowCount.value) {
  274. case 2:
  275. return `calc(50% - 25px)`;
  276. case 3:
  277. return `calc(33% - 25px)`;
  278. case 4:
  279. return `calc(25% - 25px)`;
  280. }
  281. });
  282. const placeholderItemCount = computed(() => {
  283. switch (realRowCount.value) {
  284. case 2:
  285. case 3:
  286. case 4:
  287. return newsLoader.list.value.length % realRowCount.value;
  288. }
  289. return 0;
  290. });
  291. const searchText = ref('');
  292. const dropDownValues = ref<any>([]);
  293. function handleSearch() {
  294. newsLoader.loadData(undefined, true);
  295. }
  296. function handleChangeDropDownValue(index: number, value: number) {
  297. dropDownValues.value[index] = value;
  298. newsLoader.loadData(undefined, true);
  299. }
  300. function handleShowDetail(item: any) {
  301. if (props.showDetail)
  302. return props.showDetail(item);
  303. router.push({
  304. path: props.detailsPage,
  305. query: {
  306. id: item.id,
  307. modelId: props.detailModelId,
  308. }
  309. });
  310. }
  311. function handleChangePage(page: number) {
  312. router.replace({
  313. query: {
  314. ...route.query,
  315. page,
  316. }
  317. })
  318. }
  319. //子分类
  320. const selectedTag = ref(props.defaultSelectTag);
  321. const pageSize = ref(props.pageSize);
  322. const route = useRoute();
  323. const newsLoader = await useSSrSimplePagerDataLoader(
  324. route.fullPath + '/list' + props.subName,
  325. Number(route.query.page || 1),
  326. pageSize,
  327. (page, size) => props.load(
  328. page, size,
  329. selectedTag.value,
  330. searchText.value,
  331. dropDownValues.value,
  332. )
  333. );
  334. watch(() => props.defaultSelectTag, (v) => {
  335. selectedTag.value = v;
  336. })
  337. watch(() => props.dropDownNames, () => {
  338. loadDropValues();
  339. })
  340. watch(selectedTag, () => {
  341. router.replace({
  342. query: {
  343. ...route.query,
  344. tag: selectedTag.value,
  345. }
  346. })
  347. newsLoader.loadData(undefined, true);
  348. })
  349. watch(tableListShow, (v) => {
  350. pageSize.value = v ? 100 : props.pageSize;
  351. newsLoader.loadData(undefined, true);
  352. })
  353. function loadDropValues() {
  354. dropDownValues.value = [];
  355. if (props.dropDownNames)
  356. for (const element of props.dropDownNames)
  357. dropDownValues.value.push(element.defaultSelectedValue);
  358. newsLoader.loadData(undefined, false);
  359. }
  360. onMounted(() => {
  361. if (route.query.tag)
  362. selectedTag.value = Number(route.query.tag);
  363. setTimeout(() => {
  364. loadDropValues();
  365. }, 600);
  366. })
  367. watch(route, () => {
  368. if (route.query.page)
  369. newsLoader.page.value = Number(route.query.page);
  370. loadDropValues();
  371. })
  372. defineExpose({
  373. reload() {
  374. newsLoader.loadData(undefined, true);
  375. }
  376. })
  377. </script>
  378. <style lang="scss">
  379. @use "@/assets/scss/colors";
  380. .nav-back-title {
  381. display: flex;
  382. flex-direction: row;
  383. align-items: center;
  384. justify-content: flex-start;
  385. h2 {
  386. font-size: 20px;
  387. font-family: SourceHanSerifCNBold;
  388. margin: 0;
  389. }
  390. img {
  391. width: 25px;
  392. height: 25px;
  393. cursor: pointer;
  394. margin-right: 10px;
  395. }
  396. }
  397. .search-icon {
  398. width: 25px;
  399. height: 25px;
  400. cursor: pointer;
  401. color: colors.$primary-color;
  402. }
  403. </style>