CommonListBlock.vue 9.2 KB

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