details.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. <template>
  2. <FlexCol backgroundColor="background.page">
  3. <FlexCol position="fixed" :top="0" :left="0" :right="0" :zIndex="100">
  4. <StatusBarSpace backgroundColor="transparent" />
  5. <NavBar leftButton="back" :iconProps="{ color: 'white', innerStyle: { backgroundColor: 'rgba(0,0,0,0.2)' } }" textColor="white" />
  6. </FlexCol>
  7. <FlexCol v-if="errorMessage" :padding="30" :gap="30" center height="100%">
  8. <Result status="error" :description="errorMessage" />
  9. <Button type="primary" @click="loadPageConfig">重新加载</Button>
  10. </FlexCol>
  11. <SimplePageContentLoader v-else :loader="loader">
  12. <template v-if="loader.content.value">
  13. <view class="d-flex flex-col">
  14. <!-- 头部 -->
  15. <template v-if="querys.showHead">
  16. <ImageSwiper
  17. v-if="loader.content.value.images && loader.content.value.images.length > 0"
  18. :images="loader.content.value.images"
  19. height="500rpx"
  20. />
  21. <Image
  22. v-else-if="loader.content.value.image"
  23. width="100%"
  24. height="500rpx"
  25. :radius="15"
  26. :src="loader.content.value.image"
  27. mode="widthFix"
  28. />
  29. <view v-else class="height-150"></view>
  30. </template>
  31. <!-- 标题区域 -->
  32. <view class="d-flex flex-col p-3">
  33. <view class="size-ll color-title-text">{{ loader.content.value.title }}</view>
  34. <view class="d-flex flex-row mt-2">
  35. <text class="size-s color-text-content-second text-nowrap">{{ DataDateUtils.formatDate(loader.content.value.publishAt, 'YYYY-MM-dd') }}</text>
  36. </view>
  37. </view>
  38. <!-- 归档 -->
  39. <view v-if="archiveInfo.hasArchive && querys.showArchive" class="mt-3">
  40. <Box2LineImageRightShadow
  41. class="w-100"
  42. titleColor="title-text"
  43. title2
  44. :image="archiveInfo.archiveIcon"
  45. :title="loader.content.value.title"
  46. desc="点击查看完整文档"
  47. @click="goArchive(loader.content.value.id)"
  48. />
  49. </view>
  50. <!-- 查询借阅功能按钮 -->
  51. <view v-if="querys.showBorrow" class="mt-3">
  52. <Box2LineImageRightShadow
  53. class="w-100"
  54. titleColor="title-text"
  55. title2
  56. :image="currentCommonCategoryContentDefine?.props.articleBorrow?.icon"
  57. :title="loader.content.value.title"
  58. :desc="currentCommonCategoryContentDefine?.props.articleBorrow?.text"
  59. @click="goBorrow(loader.content.value.title)"
  60. />
  61. </view>
  62. <!-- 源网页 -->
  63. <view v-if="currentCommonCategoryContentDefine?.props.topButtons" class="mt-3">
  64. <template v-for="button in currentCommonCategoryContentDefine?.props.topButtons">
  65. <Box2LineImageRightShadow
  66. v-if="evaluateButtonVisible(button.visible)"
  67. class="w-100"
  68. titleColor="title-text"
  69. title2
  70. :image="archiveInfo.archiveIcon"
  71. :title="button.text"
  72. :desc="loader.content.value.title"
  73. @click="executeButtonAction(button.expression)"
  74. />
  75. </template>
  76. </view>
  77. <!-- 内容 -->
  78. <view class="p-3 radius-ll bg-light mt-3">
  79. <Parse
  80. v-if="loader.content.value.content"
  81. :content="loader.content.value.content"
  82. />
  83. <text v-if="emptyContent">暂无简介</text>
  84. <text v-if="loader.content.value.from" class="size-s color-text-content-second mr-2 ">
  85. {{ appConfiguration?.articleMark }}
  86. {{ loader.content.value?.from }}
  87. </text>
  88. </view>
  89. <!-- 推荐 -->
  90. <view v-if="recommendListLoader.content.value?.length && querys.showRecommend" class="d-flex flex-col p-3">
  91. <text class="size-base text-bold mb-3">相关推荐</text>
  92. <Box2LineImageRightShadow
  93. class="w-100"
  94. titleColor="title-text"
  95. v-for="item in recommendListLoader.content.value"
  96. :key="item.id"
  97. :image="item.thumbnail || item.image"
  98. :title="item.title"
  99. :desc="item.desc"
  100. :wideImage="true"
  101. :tags="(item.bottomTags as string[])"
  102. @click="goDetails(item)"
  103. />
  104. </view>
  105. <ContentNote />
  106. <LikeFooter :content="loader.content.value">
  107. <template #left>
  108. <ArticleCorrect :content="loader.content.value" />
  109. </template>
  110. </LikeFooter>
  111. </view>
  112. </template>
  113. </SimplePageContentLoader>
  114. <Footer text="到底了~" />
  115. </FlexCol>
  116. </template>
  117. <script setup lang="ts">
  118. import { onShareTimeline, onShareAppMessage } from "@dcloudio/uni-app";
  119. import { DataDateUtils } from "@imengyu/js-request-transform";
  120. import { useSimplePageContentLoader } from "@/common/composeabe/SimplePageContentLoader";
  121. import { useLoadQuerys } from "@/common/composeabe/LoadQuerys";
  122. import { computed, onMounted, ref } from "vue";
  123. import { FormatUtils, StringUtils, waitTimeOut } from "@imengyu/imengyu-utils";
  124. import { injectAppConfiguration } from "@/api/system/useAppConfiguration";
  125. import { useSimpleDataLoader } from "@/common/composeabe/SimpleDataLoader";
  126. import { navTo, redirectTo } from "@/components/utils/PageAction";
  127. import { navCommonDetail, resolveCommonContentFormData } from "./common/CommonContent";
  128. import { injectCommonCategory } from "./data/CommonCategoryGlobalLoader";
  129. import { getIsDevtoolsPlatform } from "@/common/utils/MpVersions";
  130. import type { GetContentDetailItem, GetContentListItem } from "@/api/CommonContent";
  131. import type { IHomeCommonArticleDetailDefine } from "./data/CommonCategoryDefine";
  132. import CommonContent, { GetContentListParams } from "@/api/CommonContent";
  133. import Box2LineImageRightShadow from "../parts/Box2LineImageRightShadow.vue";
  134. import LikeFooter from "../parts/LikeFooter.vue";
  135. import Image from "@/components/basic/Image.vue";
  136. import ArticleCorrect from "../parts/ArticleCorrect.vue";
  137. import ImageSwiper from "../parts/ImageSwiper.vue";
  138. import IconExcel from '@/components/images/files/excel.png';
  139. import IconPowerpoint from '@/components/images/files/powerpoint.png';
  140. import IconUnknown from '@/components/images/files/unknown.png';
  141. import IconWord from '@/components/images/files/word.png';
  142. import IconPdf from '@/components/images/files/pdf.png';
  143. import NewsIndexContent from "@/api/news/NewsIndexContent";
  144. import SimplePageContentLoader from "@/common/components/SimplePageContentLoader.vue";
  145. import ContentNote from "../parts/ContentNote.vue";
  146. import Parse from "@/components/display/parse/Parse.vue";
  147. import Icon from "@/components/basic/Icon.vue";
  148. import FlexCol from "@/components/layout/FlexCol.vue";
  149. import Footer from "@/components/display/Footer.vue";
  150. import NavBar from "@/components/nav/NavBar.vue";
  151. import StatusBarSpace from "@/components/layout/space/StatusBarSpace.vue";
  152. import dynamicScript from "./data/CommonCategoryScript";
  153. export interface CommonArticleDetailProps {
  154. /**
  155. * 查询借阅功能按钮
  156. */
  157. articleBorrow: {
  158. /**
  159. * 查询借阅功能按钮链接
  160. */
  161. url: string,
  162. /**
  163. * 查询借阅功能按钮文本
  164. */
  165. text: string,
  166. /**
  167. * 查询借阅功能按钮图标
  168. */
  169. icon: string,
  170. },
  171. /**
  172. * 顶部按钮配置
  173. */
  174. topButtons: {
  175. /**
  176. * 按钮可见性表达式
  177. */
  178. visible: string,
  179. /**
  180. * 按钮文本
  181. */
  182. text: string,
  183. /**
  184. * 按钮图标
  185. */
  186. icon: string,
  187. /**
  188. * 按钮动作表达式
  189. */
  190. expression: string,
  191. }[],
  192. /**
  193. * TODO: 顶部显示信息配置
  194. */
  195. topShowInfos: {
  196. /**
  197. * 显示信息键
  198. */
  199. key: string,
  200. /**
  201. * 显示信息前缀
  202. */
  203. prefix: string,
  204. /**
  205. * 显示信息表达式
  206. */
  207. expression: string,
  208. }[],
  209. /**
  210. * 推荐详情处理函数
  211. */
  212. recommendDetailHandler: string,
  213. }
  214. const { querys } = useLoadQuerys({
  215. id: 0,
  216. mainBodyColumnId: 0,
  217. modelId: 0,
  218. /**
  219. * 是否显示推荐
  220. */
  221. showRecommend: true,
  222. /**
  223. * 是否显示头部
  224. * @default true
  225. */
  226. showHead: true,
  227. /**
  228. * 是否显示归档
  229. * @default true
  230. */
  231. showArchive: true,
  232. /**
  233. * 是否显示查询借阅功能按钮
  234. * @default false
  235. */
  236. showBorrow: false,
  237. /**
  238. * 是否自动跳转外部链接
  239. * @default 'auto'
  240. * @description 'auto' 自动跳转,'manual' 手动跳转,'none' 不跳转
  241. */
  242. navToExternalLink: 'auto',
  243. /**
  244. * 顶部显示信息
  245. * @default 'all'
  246. * @description 'all' 显示所有信息,'none' 不显示任何信息,多个信息用逗号分隔
  247. */
  248. topShowInfos: 'all',
  249. }, (t) => loader.loadData(t));
  250. const loader = useSimplePageContentLoader<
  251. GetContentDetailItem,
  252. { id: number }
  253. >(async (params) => {
  254. if (!params)
  255. throw new Error("!params");
  256. await waitTimeOut(200);
  257. const res = await NewsIndexContent.getContentDetail(params.id);
  258. uni.setNavigationBarTitle({ title: res.title });
  259. if (querys.value.navToExternalLink === 'auto' && (!res.content || res.content.trim() === '') && res.externalLink) {
  260. goExternalLink(res.externalLink);
  261. } else if (querys.value.navToExternalLink !== 'none' && res.externalLink) {
  262. goExternalLink(res.externalLink);
  263. }
  264. return res;
  265. });
  266. const currentCommonCategoryContentDefine = ref<IHomeCommonArticleDetailDefine>();
  267. const commonCategory = injectCommonCategory();
  268. const appConfiguration = injectAppConfiguration();
  269. const errorMessage = ref('');
  270. const emptyContent = computed(() => (loader.content.value?.content || '').trim() === '')
  271. const archiveInfo = computed(() => {
  272. const hasArchive = Boolean(loader.content.value?.archives);
  273. let fileIcon = IconUnknown;
  274. const ext = StringUtils.path.getFileExt(loader.content.value?.archives || '');
  275. switch (ext) {
  276. case 'excel':
  277. case 'xls':
  278. case 'xlsx':
  279. fileIcon = IconExcel;
  280. break;
  281. case 'powerpoint':
  282. case 'ppt':
  283. case 'pptx':
  284. fileIcon = IconPowerpoint;
  285. break;
  286. case 'word':
  287. case 'docx':
  288. case 'doc':
  289. case 'txt':
  290. case 'rtf':
  291. fileIcon = IconWord;
  292. break;
  293. case 'pdf':
  294. fileIcon = IconPdf;
  295. break;
  296. }
  297. return {
  298. hasArchive,
  299. archiveIcon: hasArchive ? fileIcon : IconUnknown,
  300. }
  301. })
  302. const recommendListLoader = useSimpleDataLoader(async () => {
  303. if (!querys.value.modelId)
  304. return []
  305. const res = (await CommonContent.getContentList(new GetContentListParams()
  306. .setModelId(querys.value.modelId)
  307. .setMainBodyColumnId(querys.value.mainBodyColumnId)
  308. , 1, 10)).list
  309. .filter((p) => p.id !== querys.value.id)
  310. return resolveCommonContentFormData(res);
  311. });
  312. function evaluateButtonVisible(expression: string) {
  313. return dynamicScript.execute(expression, {
  314. main: loader.content.value || {},
  315. }) as boolean;
  316. }
  317. function executeButtonAction(expression: string) {
  318. dynamicScript.execute(expression, {
  319. main: loader.content.value || {},
  320. }) as any;
  321. }
  322. function goExternalLink(url: string) {
  323. redirectTo('/pages/article/web/ewebview', { url });
  324. }
  325. function goArchive(id: number) {
  326. const archiveUrl = loader.content.value?.archives || '';
  327. if (!archiveUrl)
  328. return;
  329. const ext = StringUtils.path.getFileExt(archiveUrl);
  330. switch (ext) {
  331. case 'excel':
  332. case 'xls':
  333. case 'xlsx':
  334. case 'powerpoint':
  335. case 'ppt':
  336. case 'pptx':
  337. case 'word':
  338. case 'docx':
  339. case 'doc':
  340. case 'txt':
  341. case 'rtf':
  342. case 'pdf': {
  343. uni.showLoading({ title: '下载中...' })
  344. uni.downloadFile({
  345. url: archiveUrl,
  346. success: (res) => {
  347. uni.hideLoading()
  348. uni.openDocument({
  349. filePath: res.tempFilePath,
  350. })
  351. },
  352. fail: (res) => {
  353. console.error(res);
  354. uni.hideLoading()
  355. uni.showToast({
  356. title: '下载文件失败,请检查网络',
  357. icon: 'none',
  358. })
  359. },
  360. })
  361. break;
  362. }
  363. case 'htm':
  364. case 'html':
  365. navTo('/pages/article/web/ewebview', {
  366. url: archiveUrl,
  367. });
  368. default:
  369. uni.showToast({
  370. title: '抱歉,不支持打开该文件',
  371. icon: 'none',
  372. })
  373. break;
  374. }
  375. }
  376. function goDetails(item: GetContentListItem) {
  377. const script = currentCommonCategoryContentDefine.value?.props.recommendDetailHandler;
  378. if (script) {
  379. dynamicScript.execute(script, {
  380. main: item,
  381. }) as any;
  382. return;
  383. }
  384. navCommonDetail({
  385. id: item.id,
  386. mainBodyColumnId: item.mainBodyColumnId,
  387. modelId: item.modelId,
  388. });
  389. }
  390. function goBorrow(title: string) {
  391. navTo('/pages/article/web/ewebview', {
  392. url: FormatUtils.formatString(currentCommonCategoryContentDefine.value?.props.articleBorrow?.url || '', title)
  393. });
  394. }
  395. function getPageShareData() {
  396. if (!loader.content.value)
  397. return { title: '文章详情', imageUrl: '' }
  398. return {
  399. title: loader.content.value.title,
  400. imageUrl: loader.content.value.images[0],
  401. }
  402. }
  403. async function loadPageConfig() {
  404. if (getIsDevtoolsPlatform()) {
  405. await waitTimeOut(500);
  406. }
  407. const pageConfigName = 'common-details';
  408. let currentCommonCategoryDefine = commonCategory.value.page
  409. .find((item) => item.name === pageConfigName);
  410. if (!currentCommonCategoryDefine) {
  411. await waitTimeOut(1000);
  412. currentCommonCategoryDefine = commonCategory.value.page.find((item) => item.name === pageConfigName);
  413. }
  414. if (!currentCommonCategoryDefine) {
  415. errorMessage.value = '未找到指定的分类配置:' + pageConfigName;
  416. return;
  417. }
  418. if (currentCommonCategoryDefine.content.type !== 'CommonDetails') {
  419. errorMessage.value = '分类配置:' + pageConfigName + ' 不是默认详情类型';
  420. return;
  421. }
  422. currentCommonCategoryContentDefine.value = currentCommonCategoryDefine.content;
  423. uni.setNavigationBarTitle({
  424. title: currentCommonCategoryDefine.title || '',
  425. })
  426. }
  427. onMounted(() => {
  428. loadPageConfig();
  429. });
  430. onShareTimeline(() => {
  431. return getPageShareData();
  432. })
  433. onShareAppMessage(() => {
  434. return getPageShareData();
  435. })
  436. </script>