CommonListBlock.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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="请输入关键词" @enter="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>{{ 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. <img
  89. :src="item.image || defaultImage" alt="新闻图片"
  90. />
  91. <TitleDescBlock
  92. :title="item.title"
  93. :desc="item.desc"
  94. >
  95. <template #addon>
  96. <div v-if="item.bottomTags" class="tags">
  97. <div
  98. v-for="(tag, k) in item.bottomTags"
  99. :key="k"
  100. :class="tag ? '' : 'd-none'"
  101. >{{ tag }}</div>
  102. </div>
  103. <div v-if="item.addItems" class="extra">
  104. <div
  105. v-for="(addItem, k) in item.addItems"
  106. :key="k"
  107. class="d-flex flex-row align-items-center"
  108. :class="[
  109. addItem.text ? '' : 'd-none',
  110. ]"
  111. >
  112. <span class="desc">{{ addItem.name }}:</span>
  113. <span>{{ addItem.text }}</span>
  114. </div>
  115. </div>
  116. </template>
  117. </TitleDescBlock>
  118. </div>
  119. <div
  120. v-for="count of placeholderItemCount"
  121. :key="count"
  122. class="item empty"
  123. :style="{ width: rowWidth }"
  124. />
  125. </div>
  126. </SimplePageContentLoader>
  127. </div>
  128. <!-- 分页 -->
  129. <Pagination
  130. v-model:currentPage="newsLoader.page.value"
  131. :totalPages="newsLoader.totalPages.value"
  132. />
  133. </div>
  134. </template>
  135. <script setup lang="ts">
  136. import { computed, onMounted, ref, watch, type PropType } from 'vue';
  137. import { useSSrSimplePagerDataLoader } from '@/composeable/SimplePagerDataLoader';
  138. import DateUtils from '@/common/utils/DateUtils';
  139. import TagBar from '../content/TagBar.vue';
  140. import Dropdown from '../controls/Dropdown.vue';
  141. import SimpleInput from '../controls/SimpleInput.vue';
  142. import SimplePageContentLoader from '@/components/content/SimplePageContentLoader.vue';
  143. import Pagination from '../controls/Pagination.vue';
  144. import TitleDescBlock from '../parts/TitleDescBlock.vue';
  145. import IconSearch from '../icons/IconSearch.vue';
  146. export interface DropdownCommonItem {
  147. id: number;
  148. name: string;
  149. }
  150. export interface DropDownNames {
  151. options: (string|DropdownCommonItem)[],
  152. label?: string,
  153. defaultSelectedValue: number|string,
  154. }
  155. const tableListShow = ref(false);
  156. const props = defineProps({
  157. title: {
  158. type: String,
  159. default: '',
  160. },
  161. show: {
  162. type: Boolean,
  163. default: true,
  164. },
  165. showTableSwitch: {
  166. type: Boolean,
  167. default: false,
  168. },
  169. tableSwitchOptions: {
  170. type: Object,
  171. default: () => ({}),
  172. },
  173. showNav: {
  174. type: Boolean,
  175. default: false,
  176. },
  177. showTotal: {
  178. type: Boolean,
  179. default: false,
  180. },
  181. prevPage: {
  182. type: Object as PropType<{
  183. title: string,
  184. url?: string,
  185. }>,
  186. default: null,
  187. },
  188. dropDownNames: {
  189. type: Object as PropType<DropDownNames[]>,
  190. default: null,
  191. },
  192. showSearch: {
  193. type: Boolean,
  194. default: true,
  195. },
  196. tagsData: {
  197. type: Object as PropType<{
  198. id: number,
  199. name: string,
  200. }[]>,
  201. default: null,
  202. },
  203. pageSize: {
  204. type: Number,
  205. default: 8,
  206. },
  207. rowCount: {
  208. type: Number,
  209. default: 2,
  210. },
  211. rowType: {
  212. type: Number,
  213. default: 1,
  214. },
  215. defaultSelectTag: {
  216. type: Number,
  217. default: 1,
  218. },
  219. load: {
  220. type: Function as PropType<(
  221. page: number,
  222. pageSize: number,
  223. selectedTag: number,
  224. searchText: string,
  225. dropDownValues: number[],
  226. ) => Promise<{
  227. page: number,
  228. total: number,
  229. data: any[],
  230. }>>,
  231. required: true,
  232. },
  233. showDetail: {
  234. type: Function as PropType<(item: any) => void>,
  235. default: null,
  236. },
  237. subName: {
  238. type: String,
  239. default: '',
  240. },
  241. /**
  242. * 点击详情跳转页面路径
  243. */
  244. detailsPage: {
  245. type: String,
  246. default: '/news/detail'
  247. },
  248. /**
  249. * 详情跳转页面参数
  250. */
  251. detailsParams: {
  252. type: Object as PropType<Record<string, any>>,
  253. default: () => ({})
  254. },
  255. defaultImage: {
  256. type: String,
  257. default: 'https://mncdn.wenlvti.net/app_static/minnan/EmptyImage.png'
  258. },
  259. })
  260. const router = useRouter();
  261. const realRowCount = computed(() => {
  262. if (import.meta.client)
  263. if (window.innerWidth < 768)
  264. return 1;
  265. return props.rowCount;
  266. });
  267. const rowWidth = computed(() => {
  268. switch (realRowCount.value) {
  269. case 2:
  270. return `calc(50% - 25px)`;
  271. case 3:
  272. return `calc(33% - 25px)`;
  273. case 4:
  274. return `calc(25% - 25px)`;
  275. }
  276. });
  277. const placeholderItemCount = computed(() => {
  278. switch (realRowCount.value) {
  279. case 2:
  280. case 3:
  281. case 4:
  282. return newsLoader.list.value.length % realRowCount.value;
  283. }
  284. return 0;
  285. });
  286. const searchText = ref('');
  287. const dropDownValues = ref<any>([]);
  288. function handleSearch() {
  289. newsLoader.loadData(undefined, true);
  290. }
  291. function handleChangeDropDownValue(index: number, value: number) {
  292. dropDownValues.value[index] = value;
  293. newsLoader.loadData(undefined, true);
  294. }
  295. function handleShowDetail(item: any) {
  296. if (props.showDetail)
  297. return props.showDetail(item);
  298. router.push({
  299. path: props.detailsPage,
  300. query: {
  301. id: item.id,
  302. }
  303. });
  304. }
  305. //子分类
  306. const selectedTag = ref(props.defaultSelectTag);
  307. const pageSize = ref(props.pageSize);
  308. const route = useRoute();
  309. const newsLoader = await useSSrSimplePagerDataLoader(route.fullPath + '/list' + props.subName, Number(route.query.page || 1), pageSize, (page, size) => props.load(
  310. page, size,
  311. selectedTag.value,
  312. searchText.value,
  313. dropDownValues.value,
  314. ));
  315. watch(() => props.defaultSelectTag, (v) => {
  316. selectedTag.value = v;
  317. })
  318. watch(() => props.dropDownNames, () => {
  319. loadDropValues();
  320. })
  321. watch(selectedTag, () => {
  322. newsLoader.loadData(undefined, true);
  323. })
  324. watch(tableListShow, (v) => {
  325. pageSize.value = v ? 100 : props.pageSize;
  326. newsLoader.loadData(undefined, true);
  327. })
  328. function loadDropValues() {
  329. dropDownValues.value = [];
  330. if (props.dropDownNames)
  331. for (const element of props.dropDownNames)
  332. dropDownValues.value.push(element.defaultSelectedValue);
  333. newsLoader.loadData(undefined, true);
  334. }
  335. onMounted(() => {
  336. setTimeout(() => {
  337. loadDropValues();
  338. }, 600);
  339. })
  340. watch(route, () => {
  341. loadDropValues();
  342. })
  343. defineExpose({
  344. reload() {
  345. newsLoader.loadData(undefined, true);
  346. }
  347. })
  348. </script>
  349. <style lang="scss">
  350. @use "@/assets/scss/colors";
  351. .nav-back-title {
  352. display: flex;
  353. flex-direction: row;
  354. align-items: center;
  355. justify-content: flex-start;
  356. h2 {
  357. font-size: 20px;
  358. font-family: SourceHanSerifCNBold;
  359. margin: 0;
  360. }
  361. img {
  362. width: 25px;
  363. height: 25px;
  364. cursor: pointer;
  365. margin-right: 10px;
  366. }
  367. }
  368. .search-icon {
  369. width: 25px;
  370. height: 25px;
  371. cursor: pointer;
  372. color: colors.$primary-color;
  373. }
  374. </style>