CommonCategoryDetail.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <template>
  2. <!--详情页可配置主控-->
  3. <FlexCol>
  4. <FlexCol v-if="errorMessage" :padding="30" :gap="30" center height="100%">
  5. <Result status="error" :description="errorMessage" />
  6. <Button type="primary" @click="loadPageConfig">重新加载</Button>
  7. </FlexCol>
  8. <LoadingPage v-else-if="!loadState" />
  9. <DetailTabPage
  10. v-else
  11. ref="pageRef"
  12. :noInternalTabs="true"
  13. :showHead="currentCommonCategoryContentDefine?.props.showHead"
  14. :showDeadBox="currentCommonCategoryContentDefine?.props.showDeadBox"
  15. :load="load"
  16. :tabs="tabRenderDefinesArray"
  17. @loaded="onLoaded"
  18. >
  19. <template #extraTabs="{ content, tabCurrentId }">
  20. <template v-if="tabRenderDefines[tabCurrentId]?.type === 'intro'">
  21. <!-- 简介 -->
  22. <Parse
  23. v-if="content.intro"
  24. :content="(content.intro as string)"
  25. />
  26. <Parse
  27. v-if="content.content"
  28. :content="content.content"
  29. />
  30. <text v-if="!(content.intro || content.content)">暂无简介</text>
  31. <text v-if="content.from" class="size-s color-text-content-second mr-2 ">
  32. {{ appConfiguration?.articleMark }}
  33. {{ content.from }}
  34. </text>
  35. </template>
  36. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'images'">
  37. <!-- 图片 -->
  38. <view v-if="tabRenderDefines[tabCurrentId].prefix" class="d-flex flex-row justify-center align-center mt-2 mb-2">
  39. <text class="size-s font-bold color-text-content">{{ tabRenderDefines[tabCurrentId].prefix }}</text>
  40. </view>
  41. <ImageGrid
  42. :images="content.images"
  43. :rowCount="2"
  44. :preview="true"
  45. imageHeight="200rpx"
  46. />
  47. </template>
  48. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'video'">
  49. <!-- 视频 -->
  50. <video
  51. v-if="content.video"
  52. class="w-100 video"
  53. autoplay
  54. :poster="content.image"
  55. :src="content.video"
  56. controls
  57. />
  58. </template>
  59. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'audio'">
  60. <!-- 视频 -->
  61. <video
  62. v-if="content.audio"
  63. class="w-100 video"
  64. autoplay
  65. :poster="content.image"
  66. :src="content.audio"
  67. controls
  68. />
  69. </template>
  70. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'list'">
  71. <!-- 列表 -->
  72. <CommonCategoryListBlock
  73. v-if="currentCommonCategoryDefine"
  74. :currentCommonCategoryDefine="{
  75. title: '',
  76. name: 'default',
  77. content: tabRenderDefines[tabCurrentId].define
  78. }"
  79. :currentCommonCategoryContentDefine="tabRenderDefines[tabCurrentId].define"
  80. :pageQuerys="pageQuerys"
  81. :parentData="content"
  82. :hasPadding="false"
  83. :hasBg="false"
  84. @error="errorMessage = $event"
  85. />
  86. </template>
  87. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'rich'">
  88. <!-- 富文本 -->
  89. <view class="d-flex flex-col mt-3 mb-2">
  90. <Parse :content="(content[tabRenderDefines[tabCurrentId].key] as string)" />
  91. </view>
  92. </template>
  93. <template v-else-if="tabRenderDefines[tabCurrentId]?.type === 'nestCategory'">
  94. <!-- 嵌套分类 -->
  95. <CommonCategoryBlocks :categoryDefine="tabRenderDefines[tabCurrentId].categoryDefine" />
  96. </template>
  97. <template v-else-if="tabRenderDefines[tabCurrentId]?.key === 'vr'">
  98. <!-- VR参观 -->
  99. <view class="d-flex flex-row justify-center p-5">
  100. <Button @click="handleGoToVr(content.vr as string)">
  101. <text class="iconfont icon-go"></text>
  102. 点击参观
  103. </Button>
  104. </view>
  105. </template>
  106. <template v-else>
  107. <CommonCategoryDetailContentBlocks
  108. :define="tabRenderDefines[tabCurrentId]"
  109. :content="content"
  110. />
  111. </template>
  112. </template>
  113. <template #titleEnd="{ content }">
  114. <Tag
  115. v-if="currentCommonCategoryContentDefine?.props.showTag && content.levelText"
  116. :text="StringUtils.cutString(content.levelText as string, 4)"
  117. size="small" scheme="light" type="primary"
  118. class="flex-shrink-0"
  119. />
  120. </template>
  121. <template #titleExtra="{ content }">
  122. <view class="d-flex flex-col">
  123. <IntroBlock small :descItems="descItems" />
  124. <CommonCategoryDetailIntroBlocks
  125. :introBlocks="currentCommonCategoryContentDefine?.props.introBlocks"
  126. :content="content"
  127. />
  128. </view>
  129. </template>
  130. </DetailTabPage>
  131. </FlexCol>
  132. </template>
  133. <script setup lang="ts">
  134. import { computed, onMounted, ref, watch } from 'vue';
  135. import { navTo } from '@/components/utils/PageAction';
  136. import { injectAppConfiguration } from '@/api/system/useAppConfiguration';
  137. import { injectCommonCategory } from './CommonCategoryGlobalLoader';
  138. import { doLoadDynamicCategoryDataMergeTypeGetColumns, doLoadDynamicDetailData, doSerializeInternalVar, type IHomeCommonCategoryDynamicData } from './CommonCategoryDynamicData';
  139. import { formatError, StringUtils, waitTimeOut } from '@imengyu/imengyu-utils';
  140. import { getIsDevtoolsPlatform } from '@/common/utils/MpVersions';
  141. import type { IHomeCommonCategoryDefine, IHomeCommonCategoryListTabListDataSolve, IHomeCommonCategoryListTabNestCategoryItemDefine } from './CommonCategoryDefine';
  142. import type { IHomeCommonCategoryDetailDefine, IHomeCommonCategoryDetailTabItemDefine } from './defines/Details';
  143. import type { DetailTabPageProps } from '../common/DetailTabPage';
  144. import type { CategoryDefine } from './CommonCategoryBlocks';
  145. import LoadingPage from '@/components/display/loading/LoadingPage.vue';
  146. import FlexCol from '@/components/layout/FlexCol.vue';
  147. import Result from '@/components/feedback/Result.vue';
  148. import Button from '@/components/basic/Button.vue';
  149. import DetailTabPage from '../common/DetailTabPage.vue';
  150. import IntroBlock from '../common/IntroBlock.vue';
  151. import CommonCategoryDetailIntroBlocks from './CommonCategoryDetailIntroBlocks.vue';
  152. import Tag from '@/components/display/Tag.vue';
  153. import CommonCategoryBlocks from './CommonCategoryBlocks.vue';
  154. import Parse from '@/components/display/parse/Parse.vue';
  155. import CommonContent, { GetContentDetailItem, GetContentListParams } from '@/api/CommonContent';
  156. import CommonCategoryDetailContentBlocks from './CommonCategoryDetailContentBlocks.vue';
  157. import ImageGrid from '@/pages/parts/ImageGrid.vue';
  158. import CommonCategoryListBlock from './CommonCategoryListBlock.vue';
  159. import { resolveCommonContentSolveProps } from '../common/CommonContent';
  160. import { getDynamicScript, isDynamicScript } from './CommonCategoryScript';
  161. export interface CommonCategoryDetailProps extends DetailTabPageProps {
  162. /**
  163. * 简介块描述项
  164. */
  165. introBlockDescs?: {
  166. label: string;
  167. key: string;
  168. map?: Record<string|number, string>;
  169. visibleVia?: string;
  170. }[];
  171. /**
  172. * 简介下方块
  173. */
  174. introBlocks?: CommonCategoryDetailIntroBlocksDesc[];
  175. /**
  176. * 是否显示级别标签
  177. */
  178. showTag?: boolean;
  179. /**
  180. * 是否显示死亡框
  181. */
  182. showDeadBox?: boolean;
  183. /**
  184. * 自定义数据接口
  185. */
  186. data?: IHomeCommonCategoryDynamicData;
  187. /**
  188. * 内容处理
  189. */
  190. dataSolve?: IHomeCommonCategoryListTabListDataSolve[];
  191. }
  192. export interface CommonCategoryDetailIntroBlocksDesc {
  193. type: string;
  194. props?: Record<string, any>;
  195. }
  196. export type RenderTabDefine = IHomeCommonCategoryDetailTabItemDefine & {
  197. id: number;
  198. categoryDefine?: CategoryDefine[];
  199. };
  200. const pageRef = ref();
  201. const props = defineProps({
  202. pageConfigName: {
  203. type: String,
  204. },
  205. pageQuerys: {
  206. type: Object as () => Record<string, string|number|number[]|undefined>,
  207. default: () => ({}),
  208. },
  209. })
  210. const appConfiguration = injectAppConfiguration();
  211. const loadState = ref(false);
  212. const errorMessage = ref('');
  213. const currentCommonCategoryDefine = ref<IHomeCommonCategoryDefine['page'][0]>();
  214. const currentCommonCategoryContentDefine = ref<IHomeCommonCategoryDetailDefine>();
  215. const commonCategory = injectCommonCategory();
  216. const tabDefines = ref<IHomeCommonCategoryDetailTabItemDefine[]>([]);
  217. const tabVisibles = ref<boolean[]>([]);
  218. const tabRenderDefines = computed(() => {
  219. const result = {} as Record<number, RenderTabDefine>;
  220. try {
  221. tabDefines.value.forEach((item, i) => {
  222. const renderItem : RenderTabDefine = {
  223. ...item,
  224. visible: tabVisibles.value[i],
  225. id: i,
  226. };
  227. function loadNestCategoryData(items: IHomeCommonCategoryListTabNestCategoryItemDefine[]) {
  228. return items
  229. .filter((item) => item.visible !== false)
  230. .map((item) => {
  231. return {
  232. ...item,
  233. showTitle: item.showTitle !== false,
  234. title: item.text,
  235. content: item.data,
  236. type: item.type as CategoryDefine['type'],
  237. }
  238. });
  239. }
  240. switch (item.type) {
  241. case 'nestCategory':
  242. renderItem.categoryDefine = loadNestCategoryData(item.categorys);
  243. break;
  244. }
  245. result[i] = renderItem;
  246. });
  247. } catch (error) {
  248. errorMessage.value = formatError(error);
  249. }
  250. return result;
  251. });
  252. const tabRenderDefinesArray = computed(() => {
  253. return Object.values(tabRenderDefines.value) || [];
  254. });
  255. async function loadPageConfig() {
  256. if (!props.pageConfigName) {
  257. errorMessage.value = '配置有误';
  258. return;
  259. }
  260. currentCommonCategoryDefine.value = commonCategory.value.page.find((item) => item.name === props.pageConfigName);
  261. if (!currentCommonCategoryDefine.value) {
  262. await waitTimeOut(1000);
  263. currentCommonCategoryDefine.value = commonCategory.value.page.find((item) => item.name === props.pageConfigName);
  264. }
  265. if (!currentCommonCategoryDefine.value) {
  266. errorMessage.value = '未找到指定的分类配置:' + props.pageConfigName;
  267. return;
  268. }
  269. if (currentCommonCategoryDefine.value.content.type !== 'Details') {
  270. errorMessage.value = '分类配置:' + props.pageConfigName + ' 不是详情类型';
  271. return;
  272. }
  273. currentCommonCategoryContentDefine.value =
  274. currentCommonCategoryDefine.value.content as IHomeCommonCategoryDetailDefine;
  275. uni.setNavigationBarTitle({
  276. title: currentCommonCategoryDefine.value?.title || '',
  277. })
  278. const tabs = currentCommonCategoryContentDefine.value?.props.tabs || [];
  279. await waitTimeOut(50);
  280. try {
  281. //特殊处理
  282. let hasNestCategory = false;
  283. for (const tab of tabs) {
  284. if (tab.type === 'nestCategory') {
  285. tab.categorys = await doLoadDynamicCategoryDataMergeTypeGetColumns(tab.categorys)
  286. hasNestCategory = true;
  287. }
  288. }
  289. if (hasNestCategory)
  290. await waitTimeOut(50);
  291. loadState.value = true;
  292. await waitTimeOut(50);
  293. const content = (await pageRef.value?.load(props.pageQuerys)) as GetContentDetailItem;
  294. //Tab标签动态处理
  295. function loadByContentData(text: string) {
  296. let result = text;
  297. const keys = text.split(':');
  298. if (keys.length >= 2) {
  299. switch (keys[1]) {
  300. case 'byContentType':
  301. switch(content.type) {
  302. default:
  303. case GetContentListParams.TYPE_ARTICLE: result = '相关文章'; break;
  304. case GetContentListParams.TYPE_VIDEO: result = '视频'; break;
  305. }
  306. break;
  307. case 'bySubListType': {
  308. const subListKey = keys[2];
  309. if (subListKey) {
  310. const list = content?.[subListKey];
  311. if (Array.isArray(list) && list.length > 0) {
  312. switch(list[0].type) {
  313. default:
  314. case GetContentListParams.TYPE_ARTICLE: result = '相关文章'; break;
  315. case GetContentListParams.TYPE_ARCHIVE: result = '相关文档'; break;
  316. case GetContentListParams.TYPE_VIDEO: result = '视频'; break;
  317. case GetContentListParams.TYPE_IMAGE: result = '相册'; break;
  318. }
  319. } else {
  320. result = '相关';
  321. }
  322. }
  323. break;
  324. }
  325. case 'expression': {
  326. result = getDynamicScript().execute(keys[2], {
  327. main: content,
  328. customData: {
  329. customTabNameIdMap: currentCommonCategoryContentDefine.value?.props.customTabNameIdMap || {},
  330. },
  331. }) as any;
  332. break;
  333. }
  334. case 'idMap': {
  335. if (currentCommonCategoryContentDefine.value?.props.customTabNameIdMap) {
  336. result = currentCommonCategoryContentDefine.value.props.customTabNameIdMap[content.id];
  337. }
  338. }
  339. }
  340. }
  341. return result;
  342. }
  343. for (const tab of tabs) {
  344. if (tab.text.startsWith('dynamic')) {
  345. tab.text = loadByContentData(tab.text);
  346. }
  347. }
  348. await loadTabVisible(tabs, content);
  349. } catch (error) {
  350. console.error(error);
  351. loadState.value = false;
  352. errorMessage.value = formatError(error);
  353. }
  354. tabDefines.value = tabs;
  355. }
  356. watch(() => props.pageConfigName, loadPageConfig);
  357. onMounted(loadPageConfig);
  358. const content = ref<any>();
  359. const descItems = computed(() => (
  360. currentCommonCategoryContentDefine.value?.props.introBlockDescs || [])
  361. .map((item) => {
  362. let value = '';
  363. if (item.key.startsWith('expression:')) {
  364. value = getDynamicScript().execute(item.key.substring(11), {
  365. main: content.value,
  366. }) as any;
  367. }
  368. else
  369. value = content.value?.[item.key] || '';
  370. if (item.map)
  371. value = item.map[value] || '';
  372. return {
  373. ...item,
  374. value,
  375. }
  376. })
  377. .filter((item) => Boolean(item.value))
  378. .filter((item) => !item.visibleVia || item.visibleVia === 'auto' || getDynamicScript().execute(item.visibleVia, {
  379. main: item,
  380. }) as any)
  381. );
  382. function onLoaded(d: any) {
  383. content.value = d;
  384. }
  385. async function loadTabVisible(tabs: IHomeCommonCategoryDetailTabItemDefine[], d: any) {
  386. if (!d)
  387. return;
  388. for (let i = 0; i < tabs.length; i++) {
  389. const tab = tabs[i];
  390. const v = d[tab.key];
  391. let check = true
  392. let visibleCheckBy = 'auto';
  393. let visibleCheckKeys = [] as string[];
  394. let visibleCheckExpression = '';
  395. if (tab.visibleVia && tab.visibleVia.includes(':')) {
  396. visibleCheckKeys = tab.visibleVia.split(':');
  397. visibleCheckBy = visibleCheckKeys[0];
  398. } else if (tab.visibleVia && isDynamicScript(tab.visibleVia)) {
  399. visibleCheckExpression = tab.visibleVia;
  400. visibleCheckBy = 'expression';
  401. }
  402. switch (visibleCheckBy) {
  403. default:
  404. case 'auto': {
  405. switch (tab.type) {
  406. case 'intro': check = true; break;
  407. case 'audio': check = Boolean(d.audio); break;
  408. case 'video': check = Boolean(d.video); break;
  409. case 'images': check = Boolean(d.images) && (d.images as string[]).length > 1; break;
  410. case 'map': check = Boolean(d.latitude) && Boolean(d.longitude); break;
  411. default:
  412. if (!v)
  413. check = false;
  414. else if (Array.isArray(v))
  415. check = (v as any[]).length > 0;
  416. break;
  417. }
  418. break;
  419. }
  420. case 'expression': {
  421. check = getDynamicScript().execute(visibleCheckExpression, {
  422. main: d,
  423. }) as any;
  424. break;
  425. }
  426. case 'idMap': {
  427. const idMap = currentCommonCategoryContentDefine.value?.props.customTabVisibleViaIdMap;
  428. if (idMap && visibleCheckKeys.length >= 2)
  429. check = idMap[`${d.id}:${visibleCheckKeys[1]}`];
  430. break;
  431. }
  432. }
  433. tabVisibles.value[i] = tab.visible !== false && check;
  434. }
  435. }
  436. async function load(id: number) {
  437. if (isNaN(id) || id <= 0)
  438. throw new Error("请输入ID。如果正在测试,可在后台复制ID");
  439. if (currentCommonCategoryContentDefine.value?.props.data) {
  440. return await doLoadDynamicDetailData(
  441. currentCommonCategoryContentDefine.value?.props.data,
  442. id,
  443. props.pageQuerys.modelId && Number(props.pageQuerys.modelId) > 0 ? Number(props.pageQuerys.modelId) : undefined
  444. );
  445. }
  446. let res = await CommonContent.getContentDetail(
  447. id,
  448. undefined,
  449. props.pageQuerys.modelId && Number(props.pageQuerys.modelId) > 0 ? Number(props.pageQuerys.modelId) : undefined
  450. );
  451. if (currentCommonCategoryContentDefine.value?.props.dataSolve) {
  452. res = resolveCommonContentSolveProps([ res ], currentCommonCategoryContentDefine.value.props.dataSolve)[0];
  453. }
  454. return res;
  455. }
  456. function handleGoToVr(vr: string) {
  457. navTo('/pages/article/web/ewebview', { url: vr })
  458. }
  459. defineExpose({
  460. getPageShareData() {
  461. return pageRef.value?.getPageShareData() || {};
  462. }
  463. })
  464. </script>