archives.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  1. <template>
  2. <view class="app-page">
  3. <view class="e-fixed">
  4. <tm-tabs color="light-blue" v-model="tab.activeIndex" :list="tab.list" align="center"
  5. @change="fnOnTabChange"></tm-tabs>
  6. </view>
  7. <!-- 占位区域 -->
  8. <view style="width: 100vw;height: 90rpx;"></view>
  9. <!-- 骨架屏:加载区域 -->
  10. <view v-if="loading !== 'success'" class="loading-wrap">
  11. <tm-skeleton model="listAvatr"></tm-skeleton>
  12. <tm-skeleton model="listAvatr"></tm-skeleton>
  13. <tm-skeleton model="listAvatr"></tm-skeleton>
  14. </view>
  15. <!-- 加载完成区域 -->
  16. <block v-else>
  17. <view v-if="dataList.length === 0" class="list-empty flex flex-center">
  18. <tm-empty v-if="haloConfigs.basicConfig.auditModeEnabled" icon="icon-shiliangzhinengduixiang-" label="暂无归档的内容"></tm-empty>
  19. <tm-empty v-else icon="icon-shiliangzhinengduixiang-" label="暂无归档的文章"></tm-empty>
  20. </view>
  21. <view v-else class="e-timeline tm-timeline mt-24">
  22. <block v-for="(item, index) in dataList" :key="index">
  23. <view class="tm-timeline-item tm-timeline-item--leftDir">
  24. <view style="width: 160rpx;">
  25. <view :style="{ width: '24rpx', height: '24rpx' }" :class="[black_tmeme ? 'bk' : '']"
  26. class="flex-center rounded tm-timeline-jidian border-white-a-2 grey-lighten-2 light-blue shadow-primary-4">
  27. </view>
  28. <view :style="{ marginTop: '-24rpx' }"
  29. :class="[index !== dataList.length - 1 ? 'tm-timeline-item-boder' : '', black_tmeme ? 'bk' : '']"
  30. class="grey-lighten-2"></view>
  31. </view>
  32. <view class="tm-timeline-item-content relative">
  33. <view class="tm-timeline-item-left">
  34. <view class="flex time text-weight-b mb-24">
  35. <text>{{ item.year }}年</text>
  36. <text v-if="tab.activeIndex === 0">{{ item.month }}月</text>
  37. <view v-if="haloConfigs.basicConfig.auditModeEnabled" class="text-size-s text-grey-darken-1 ml-12">
  38. (共 {{ item.posts.length }} 篇内容)
  39. </view>
  40. <view v-else class="text-size-s text-grey-darken-1 ml-12">
  41. (共 {{ item.posts.length }} 篇文章)
  42. </view>
  43. </view>
  44. <block v-if="item.posts.length !== 0">
  45. <block v-for="(post, postIndex) in item.posts" :key="post.metadata.name">
  46. <view class="flex post shadow-3 pa-24 mb-24"
  47. :class="[globalAppSettings.layout.cardType]"
  48. @click="fnToArticleDetail(post)">
  49. <image class="post-thumbnail"
  50. :src="$utils.checkThumbnailUrl(post.spec.cover)" mode="aspectFill">
  51. </image>
  52. <view class="post-info pl-20">
  53. <view class="post-info_title text-overflow">{{ post.spec.title }}
  54. </view>
  55. <view
  56. class="post-info_summary text-overflow-2 mt-12 text-size-s text-grey-darken-1">
  57. {{ post.status.excerpt }}
  58. </view>
  59. <view class="post-info_time mt-12 text-size-s text-grey-darken-1">
  60. <text class="iconfont icon-clock text-size-s mr-6"></text>
  61. <text class="time-label">发布时间:</text>
  62. {{ {d: post.spec.publishTime, f: 'yyyy年MM月dd日 星期w'} | formatTime }}
  63. </view>
  64. </view>
  65. </view>
  66. </block>
  67. </block>
  68. <view v-else class="post-empty text-size-m text-grey-darken-1">该日期下暂无归档文章!</view>
  69. </view>
  70. </view>
  71. </view>
  72. </block>
  73. </view>
  74. <view class="load-text mt-12">{{ loadMoreText }}</view>
  75. <!-- 返回顶部 -->
  76. <tm-flotbutton @click="fnToTopPage" size="m" color="bg-gradient-light-blue-accent"
  77. icon="icon-angle-up"></tm-flotbutton>
  78. </block>
  79. </view>
  80. </template>
  81. <script>
  82. import tmSkeleton from '@/tm-vuetify/components/tm-skeleton/tm-skeleton.vue';
  83. import tmTranslate from '@/tm-vuetify/components/tm-translate/tm-translate.vue';
  84. import tmFlotbutton from '@/tm-vuetify/components/tm-flotbutton/tm-flotbutton.vue';
  85. import tmTabs from '@/tm-vuetify/components/tm-tabs/tm-tabs.vue';
  86. import tmEmpty from '@/tm-vuetify/components/tm-empty/tm-empty.vue';
  87. import qs from 'qs'
  88. export default {
  89. components: {
  90. tmSkeleton,
  91. tmTranslate,
  92. tmFlotbutton,
  93. tmTabs,
  94. tmEmpty
  95. },
  96. data() {
  97. return {
  98. loading: 'loading',
  99. tab: {
  100. activeIndex: 0,
  101. list: ['按月份查看', '按年份查看']
  102. },
  103. queryParams: {
  104. size: 10,
  105. page: 1
  106. },
  107. result: {},
  108. cacheDataList: [], // 所有请求的缓存数据
  109. dataList: [], // 显示的数据
  110. isLoadMore: false,
  111. loadMoreText: "加载中..."
  112. };
  113. },
  114. computed: {
  115. black_tmeme: function () {
  116. return this.$tm.vx.state().tmVuetify.black;
  117. },
  118. color_tmeme: function () {
  119. return this.$tm.vx.state().tmVuetify.color;
  120. },
  121. haloConfigs() {
  122. return this.$tm.vx.getters().getConfigs;
  123. },
  124. mockJson() {
  125. return this.$tm.vx.getters().getMockJson;
  126. }
  127. },
  128. created() {
  129. this.fnGetData();
  130. },
  131. onPullDownRefresh() {
  132. this.isLoadMore = false;
  133. this.queryParams.page = 1;
  134. this.fnGetData();
  135. },
  136. onReachBottom(e) {
  137. if (this.haloConfigs.basicConfig.auditModeEnabled) {
  138. uni.showToast({
  139. icon: 'none',
  140. title: '没有更多数据了'
  141. });
  142. return
  143. }
  144. if (this.result.hasNext) {
  145. this.queryParams.page += 1;
  146. this.isLoadMore = true;
  147. this.fnGetData();
  148. } else {
  149. uni.showToast({
  150. icon: 'none',
  151. title: '没有更多数据了'
  152. });
  153. }
  154. },
  155. methods: {
  156. fnOnTabChange(index) {
  157. this.fnResetSetAniWaitIndex();
  158. this.queryParams.page = 0;
  159. this.dataList = this.handleGetShowDataList(this.handleGetPosts(this.cacheDataList))
  160. this.fnToTopPage();
  161. },
  162. fnGetData() {
  163. if (this.haloConfigs.basicConfig.auditModeEnabled) {
  164. const dataList = this.mockJson.archives.list.map(item => {
  165. const date = new Date(item.time)
  166. const year = date.getFullYear()
  167. const month = date.getMonth() + 1
  168. return {
  169. metadata: {
  170. name: Date.now() * Math.random(),
  171. labels: {
  172. "content.halo.run/archive-year": year,
  173. "content.halo.run/archive-month": month
  174. }
  175. },
  176. spec: {
  177. pinned: false,
  178. cover: item.cover,
  179. title: item.title,
  180. publishTime: item.time
  181. },
  182. status: {
  183. excerpt: item.desc
  184. },
  185. stats: {
  186. visit: 0
  187. }
  188. }
  189. });
  190. const posts = this.handleGetPosts(dataList)
  191. this.dataList = []
  192. this.cacheDataList = dataList;
  193. this.dataList = this.handleGetShowDataList(posts)
  194. this.loading = 'success';
  195. this.loadMoreText = '呜呜,没有更多数据啦~';
  196. uni.hideLoading();
  197. uni.stopPullDownRefresh();
  198. return;
  199. }
  200. if (this.isLoadMore) {
  201. uni.showLoading({
  202. title: "加载中..."
  203. })
  204. } else {
  205. this.loading = 'loading';
  206. }
  207. this.loadMoreText = "加载中...";
  208. const paramsStr = qs.stringify(this.queryParams, {
  209. allowDots: true,
  210. encodeValuesOnly: true,
  211. skipNulls: true,
  212. encode: true,
  213. arrayFormat: 'repeat'
  214. })
  215. uni.request({
  216. url: this.$baseApiUrl + '/apis/api.content.halo.run/v1alpha1/posts?' + paramsStr,
  217. method: 'GET',
  218. success: (res) => {
  219. const data = res.data;
  220. this.result = data;
  221. const posts = this.handleGetPosts(data.items)
  222. const showDataList = this.handleGetShowDataList(posts)
  223. if (this.isLoadMore) {
  224. this.cacheDataList = this.handleUniqueCacheDatalist([
  225. ...this.cacheDataList, ...data.items
  226. ]);
  227. this.handleMergeDataList2(showDataList)
  228. } else {
  229. this.dataList = []
  230. this.cacheDataList = data.items;
  231. this.dataList = showDataList
  232. }
  233. this.loading = 'success';
  234. this.loadMoreText = data.hasNext ? '上拉加载更多' : '呜呜,没有更多数据啦~';
  235. uni.hideLoading();
  236. uni.stopPullDownRefresh();
  237. },
  238. fail: (err) => {
  239. this.loading = 'error';
  240. this.loadMoreText = '加载失败,请下拉刷新!';
  241. uni.$tm.toast(err.message || '数据加载失败!');
  242. uni.hideLoading();
  243. uni.stopPullDownRefresh();
  244. }
  245. })
  246. },
  247. // 处理数据分类
  248. handleGetPosts(dataList) {
  249. const posts = {}
  250. const postLabelYearKey = "content.halo.run/archive-year"
  251. const postLabelMonthKey = "content.halo.run/archive-month"
  252. dataList.forEach(item => {
  253. let postItemKey = ""
  254. if (this.tab.activeIndex === 0) {
  255. postItemKey =
  256. `${item.metadata.labels[postLabelYearKey]}-${item.metadata.labels[postLabelMonthKey]}`
  257. } else {
  258. postItemKey = `${item.metadata.labels[postLabelYearKey]}`
  259. }
  260. if (posts[postItemKey]) {
  261. posts[postItemKey].push(item)
  262. } else {
  263. posts[postItemKey] = [item]
  264. }
  265. })
  266. return posts;
  267. },
  268. // 根据分类的数据,处理成显示的数据
  269. handleGetShowDataList(posts) {
  270. const dataListResult = []
  271. Object.keys(posts).forEach((key) => {
  272. const postData = {
  273. sort: 0,
  274. key: key,
  275. year: key,
  276. month: "",
  277. posts: posts[key]
  278. }
  279. if (this.tab.activeIndex == 0) {
  280. const splitDate = key.split("-")
  281. postData.year = splitDate[0]
  282. postData.month = splitDate[1]
  283. postData.sort = Number(key.replace("-", ""))
  284. } else {
  285. postData.sort = Number(key)
  286. }
  287. dataListResult.push(postData)
  288. })
  289. dataListResult.sort((a, b) => {
  290. return Number(b.sort) - Number(a.sort)
  291. })
  292. return dataListResult;
  293. },
  294. handleMergeDataList(list1, list2) {
  295. // 将list1转换为以key为键的对象
  296. let merged = list1.reduce((acc, item) => {
  297. acc[item.key] = {
  298. ...item
  299. };
  300. return acc;
  301. }, {});
  302. // 遍历list2,合并posts数组或添加新对象
  303. list2.forEach(item => {
  304. if (merged[item.key]) {
  305. // 如果key已存在,合并posts数组
  306. merged[item.key].posts = [...merged[item.key].posts, ...item.posts];
  307. } else {
  308. // 如果key不存在,添加新对象
  309. merged[item.key] = {
  310. ...item
  311. };
  312. }
  313. });
  314. // 将对象转换回数组
  315. return Object.values(merged);
  316. },
  317. handleMergeDataList2(list) {
  318. list.forEach((item, index) => {
  319. const find = this.dataList.find(x => x.key == item.key)
  320. if (find) {
  321. item.posts.forEach(post => {
  322. if (!find.posts.find(x => x.metadata.name == post.metadata.name)) {
  323. find.posts.push(post)
  324. }
  325. })
  326. }
  327. })
  328. list.forEach(post => {
  329. if (!this.dataList.find(x => x.key === post.key)) {
  330. this.dataList.push(post)
  331. }
  332. })
  333. this.dataList.sort((a, b) => {
  334. return Number(b.sort) - Number(a.sort)
  335. })
  336. },
  337. handleUniqueCacheDatalist(dataList) {
  338. const seen = new Set();
  339. return dataList.filter(item => {
  340. return seen.has(item.metadata.name) ? false : seen.add(item.metadata.name);
  341. });
  342. },
  343. fnToArticleDetail(article) {
  344. if (this.haloConfigs.basicConfig.auditModeEnabled) {
  345. return;
  346. }
  347. uni.navigateTo({
  348. url: '/pagesA/article-detail/article-detail?name=' + article.metadata.name,
  349. animationType: 'slide-in-right'
  350. });
  351. }
  352. }
  353. };
  354. </script>
  355. <style lang="scss" scoped>
  356. .app-page {
  357. width: 100vw;
  358. min-height: 100vh;
  359. display: flex;
  360. flex-direction: column;
  361. background-color: #fafafd;
  362. }
  363. .auditModeEnabled {
  364. width: 100%;
  365. height: 80vh;
  366. display: flex;
  367. flex-direction: column;
  368. align-items: center;
  369. justify-content: center;
  370. }
  371. .loading-wrap {
  372. padding: 24rpx;
  373. }
  374. .list-empty {
  375. width: 100vw;
  376. height: 100vh;
  377. }
  378. .statistics {
  379. background-color: #ffffff;
  380. }
  381. .e-timeline {
  382. ::v-deep {
  383. .tm-timeline-item > view:first-child {
  384. width: 110rpx !important;
  385. }
  386. .tm-timeline-item-left {
  387. max-width: 580rpx !important;
  388. width: 100% !important;
  389. }
  390. }
  391. }
  392. .tm-timeline {
  393. .tm-timeline-item {
  394. .tm-timeline-item-left,
  395. .tm-timeline-item-right {
  396. width: 200rpx;
  397. flex-shrink: 0;
  398. }
  399. .tm-timeline-item-content {
  400. display: flex;
  401. justify-content: center;
  402. align-items: flex-start;
  403. align-content: flex-start;
  404. }
  405. .tm-timeline-jidian {
  406. margin: auto;
  407. }
  408. &.tm-timeline-item--leftDir {
  409. display: flex;
  410. flex-flow: row;
  411. &.endright {
  412. justify-content: flex-end;
  413. }
  414. .tm-timeline-item-left,
  415. .tm-timeline-item-right {
  416. width: auto;
  417. max-width: 400rpx;
  418. }
  419. .tm-timeline-item-boder {
  420. height: 100%;
  421. width: 1px;
  422. margin: auto;
  423. }
  424. .tm-timeline-jidian {
  425. position: relative;
  426. margin: auto;
  427. z-index: 2;
  428. }
  429. .tm-timeline-item-content {
  430. display: flex;
  431. justify-content: flex-start;
  432. align-items: flex-start;
  433. align-content: flex-start;
  434. }
  435. }
  436. }
  437. }
  438. .post {
  439. width: 560rpx;
  440. border-radius: 12rpx;
  441. background-color: #fff;
  442. &.lr_image_text {
  443. }
  444. &.lr_text_image {
  445. .post-thumbnail {
  446. order: 2;
  447. }
  448. .post-info {
  449. order: 1;
  450. padding-left: 0;
  451. padding-right: 24rpx;
  452. }
  453. }
  454. &.tb_image_text {
  455. flex-direction: column;
  456. .post-thumbnail {
  457. width: 100%;
  458. height: 220rpx;
  459. }
  460. .post-info {
  461. width: 100%;
  462. padding-left: 0;
  463. &_title {
  464. margin-top: 12rpx;
  465. }
  466. &_time {
  467. .iconfont {
  468. display: none;
  469. }
  470. .time-label {
  471. display: inline-block;
  472. }
  473. }
  474. }
  475. }
  476. &.tb_text_image {
  477. flex-direction: column;
  478. .post-thumbnail {
  479. order: 2;
  480. width: 100%;
  481. height: 220rpx;
  482. margin-top: 12rpx;
  483. }
  484. .post-info {
  485. order: 1;
  486. width: 100%;
  487. padding-left: 0;
  488. &_time {
  489. .iconfont {
  490. display: none;
  491. }
  492. .time-label {
  493. display: inline-block;
  494. }
  495. }
  496. }
  497. }
  498. &.only_text {
  499. .post-info {
  500. padding: 6rpx;
  501. &_time {
  502. margin-top: 20rpx;
  503. .iconfont {
  504. display: none;
  505. }
  506. .time-label {
  507. display: inline-block;
  508. }
  509. }
  510. }
  511. .post-thumbnail {
  512. display: none;
  513. }
  514. }
  515. }
  516. .post-thumbnail {
  517. border-radius: 6rpx;
  518. width: 200rpx;
  519. height: 170rpx;
  520. }
  521. .post-info {
  522. width: 0;
  523. flex-grow: 1;
  524. &_title {
  525. color: #303133;
  526. font-size: 28rpx;
  527. font-weight: bold;
  528. }
  529. &_summary {
  530. display: -webkit-box;
  531. line-height: 1.6;
  532. }
  533. &_time {
  534. .time-label {
  535. display: none;
  536. }
  537. }
  538. }
  539. .time {
  540. align-items: center;
  541. }
  542. </style>