index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. <route lang="json">
  2. {
  3. "style": {
  4. "navigationStyle": "custom"
  5. }
  6. }
  7. </route>
  8. <script setup lang="ts">
  9. import MomentItem from '@/components/moment-item.vue'
  10. import {
  11. createBrowseHistory,
  12. deleteCircle,
  13. getCircles,
  14. getDesignerInfo,
  15. getUserInfoById,
  16. updateDesignerInfo,
  17. shareCircle,
  18. getOwnBadges,
  19. } from '../../../core/libs/requests'
  20. import { useUserStore } from '../../../store'
  21. import { storeToRefs } from 'pinia'
  22. import { NetImages } from '../../../core/libs/net-images'
  23. import PageHelper from '@/components/page-helper.vue'
  24. import BottomAppBar from '@/components/bottom-app-bar.vue'
  25. import { useRouter } from '../../../core/utils/router'
  26. import NavbarEvo from '@/components/navbar-evo.vue'
  27. import { useMessage } from 'wot-design-uni'
  28. import { requestToast } from '../../../core/utils/common'
  29. import { ComponentExposed } from 'vue-component-type-helpers'
  30. import dayjs from 'dayjs'
  31. import wechatChannels from '@designer-hub/assets/src/libs/assets/wechatChannels'
  32. import { handleUpvoteClick } from '../../../core/libs/actions'
  33. import { usePermissions } from '../../../composables/permissions'
  34. import ImageEvo from '@/components/image-evo.vue'
  35. import more from '@designer-hub/assets/src/libs/assets/more'
  36. import qrCode from '@designer-hub/assets/src/libs/assets/qrCode'
  37. import { useShare } from '@/composables/share'
  38. import { useMemberLevelsStore } from '../../../store/member-levles'
  39. import { getByDictType } from '@/core/libs/requests'
  40. import { DictType } from '@/core/libs/models'
  41. const memberLevelsStore = useMemberLevelsStore()
  42. const { getMemberAvatarFrame } = memberLevelsStore
  43. const dictMemberDesignStyle = ref<any[]>()
  44. const dictCircleSpaceType = ref<any[]>()
  45. const { features, clickByPermission } = usePermissions()
  46. const { shareAppMessage } = useShare()
  47. const { alert, confirm } = useMessage()
  48. const router = useRouter()
  49. const userStore = useUserStore()
  50. const { userInfo } = storeToRefs(userStore)
  51. const pageHelperRef = ref<ComponentExposed<typeof PageHelper>>()
  52. const id = ref()
  53. const isShared = ref(false)
  54. const videoNumberShare = ref(false)
  55. const tab = ref('2')
  56. const tabs = ref([
  57. { label: '案例', value: '2' },
  58. { label: '动态', value: '1' },
  59. // { label: '视频', value: '0' },
  60. ])
  61. const viewDuration = ref(0)
  62. const viewStartAt = ref<Date>()
  63. const shareOptions = ref()
  64. const { data: memberInfo, run: setMemberInfo } = useRequest(() => getUserInfoById(id.value), {
  65. initialData: {},
  66. })
  67. const { data: designerInfo, run: setDesignerInfo } = useRequest(() => getDesignerInfo(id.value), {
  68. initialData: {},
  69. })
  70. const { data: badges, run: setBadges } = useRequest(() => getOwnBadges({ userId: id.value }))
  71. const isOwn = computed(() => String(userInfo.value?.userId) === id.value)
  72. const skills = computed(() =>
  73. [
  74. {
  75. label: '从业年限',
  76. value: designerInfo.value?.serviceYears,
  77. show: designerInfo.value?.serviceYears,
  78. },
  79. {
  80. label: '客户',
  81. value: designerInfo.value?.serviceCustomerCount,
  82. show: designerInfo.value?.serviceCustomerCount,
  83. },
  84. {
  85. label: '设计费',
  86. value: `${designerInfo.value?.designFee}元/㎡`,
  87. show: designerInfo.value?.designFee,
  88. },
  89. ].filter(({ show }) => show),
  90. )
  91. const shareContent = computed(() => {
  92. return {
  93. title: designerInfo.value?.homePageName
  94. ? designerInfo.value?.homePageName
  95. : memberInfo.value.nickname,
  96. imageUrl: designerInfo.value?.sharePageUrl ?? designerInfo.value?.homePageUrl,
  97. path: `/pages-sub/mine/homepage/index?id=${id.value}&isShared=true`,
  98. }
  99. })
  100. const shareMessage = () => {
  101. const promise = new Promise((resolve, reject) => {
  102. if (userInfo.value?.level?.level < 2) {
  103. uni.showToast({ title: '普通会员无法分享', icon: 'none' })
  104. reject()
  105. } else {
  106. const res: Page.CustomShareContent = {}
  107. res.title = designerInfo.value?.homePageName
  108. ? designerInfo.value?.homePageName
  109. : memberInfo.value.nickname
  110. res.imageUrl = designerInfo.value.sharePageUrl ?? designerInfo.value?.homePageUrl
  111. res.path = `/pages-sub/mine/homepage/index?id=${id.value}&isShared=true`
  112. resolve(res)
  113. }
  114. })
  115. return { promise }
  116. }
  117. const query = computed(() => ({
  118. circleType: tab.value,
  119. stylistId: id.value,
  120. }))
  121. const handleMomentDelete = async (id) => {
  122. confirm({
  123. title: '警告',
  124. msg: '确定要删除吗?',
  125. beforeConfirm: async ({ resolve }) => {
  126. await requestToast(() => deleteCircle(id))
  127. await pageHelperRef.value?.refresh()
  128. resolve(true)
  129. },
  130. })
  131. }
  132. const handleLike = async (options) => {
  133. await handleUpvoteClick({
  134. ...options,
  135. userId: userInfo.value.userId,
  136. userName: userInfo.value.nickname,
  137. })
  138. await pageHelperRef.value?.refresh()
  139. }
  140. const handleShare = (options: any) => {
  141. console.log('options 分享', options)
  142. shareOptions.value = options
  143. }
  144. const handle2Video = () => {
  145. try {
  146. uni.openChannelsUserProfile({
  147. finderUserName: designerInfo.value?.videoNumber,
  148. success: (res) => {
  149. console.log('success', res)
  150. },
  151. fail: (error) => {
  152. let { errMsg } = error
  153. if (
  154. errMsg === 'openChannelsUserProfile:fail cancel' ||
  155. errMsg === 'openChannelsUserProfile:fail user cancel'
  156. ) {
  157. console.log('取消报错')
  158. } else if (
  159. errMsg === 'openChannelsUserProfile:fail rejected due to no permission currently'
  160. ) {
  161. uni.showToast({
  162. title: '请前往小程序使用完整功能',
  163. icon: 'none',
  164. })
  165. }
  166. },
  167. })
  168. } catch (e) {
  169. uni.showToast({
  170. title: '打开失败',
  171. icon: 'none',
  172. })
  173. }
  174. }
  175. const handle2VideoNumber = (share: boolean) => {
  176. handle2Video()
  177. }
  178. const handleUnbundle = async () => {
  179. confirm({
  180. title: '警告',
  181. msg: '确定要解绑吗?',
  182. beforeConfirm: async ({ resolve }) => {
  183. await requestToast(
  184. () =>
  185. updateDesignerInfo({
  186. id: designerInfo.value.id,
  187. userId: designerInfo.value.userId,
  188. videoNumber: '',
  189. }),
  190. { success: true, successTitle: '解绑成功' },
  191. )
  192. await setDesignerInfo()
  193. resolve(true)
  194. },
  195. })
  196. }
  197. onLoad(async (query: { id: string; isShared?: string, scene?:string }) => {
  198. if (query.id) {
  199. id.value = query.id
  200. } else {
  201. id.value = userInfo.value.userId
  202. }
  203. if(query.scene){
  204. console.log(decodeURIComponent(query.scene))
  205. id.value = decodeURIComponent(query.scene).split("=")[1]
  206. isShared.value = true
  207. }
  208. if (query.isShared) {
  209. isShared.value = true
  210. videoNumberShare.value = true
  211. } else {
  212. videoNumberShare.value = false
  213. }
  214. if (!isOwn.value) {
  215. viewStartAt.value = new Date()
  216. }
  217. await Promise.all([setMemberInfo(), setBadges()])
  218. let value1 = await getByDictType(DictType.circleSpaceType)
  219. dictCircleSpaceType.value = value1.data
  220. let value2 = await getByDictType(DictType.memberDesignStyle)
  221. dictMemberDesignStyle.value = value2.data
  222. // 若 等级 小于 2 则无法分享
  223. if (userInfo.value?.level?.level < 2) {
  224. uni.hideShareMenu({
  225. hideShareItems: ['shareAppMessage', 'shareTimeline'],
  226. })
  227. }
  228. })
  229. onShow(async () => {
  230. await setDesignerInfo()
  231. await pageHelperRef.value?.refresh()
  232. })
  233. onShareAppMessage(async (share) => {
  234. if (share.from === 'button') {
  235. return await shareAppMessage(share)
  236. } else if (share.from === 'menu') {
  237. return shareContent.value
  238. }
  239. })
  240. onUnload(async () => {
  241. viewDuration.value = dayjs().diff(viewStartAt.value, 'seconds')
  242. const { data, code } = await createBrowseHistory({
  243. stylistId: id.value,
  244. bizType: 3,
  245. // bizId: '1',
  246. duration: viewDuration.value.toString(),
  247. })
  248. })
  249. onHide(async () => {
  250. viewDuration.value = dayjs().diff(viewStartAt.value, 'seconds')
  251. const { data, code } = await createBrowseHistory({
  252. stylistId: id.value,
  253. bizType: 3,
  254. // bizId: '1',
  255. duration: viewDuration.value.toString(),
  256. })
  257. })
  258. onShareTimeline(() => ({
  259. title: designerInfo.value?.homePageName
  260. ? designerInfo.value?.homePageName
  261. : memberInfo.value.nickname,
  262. imageUrl: designerInfo.value.sharePageUrl,
  263. query: 'id=' + id.value + '&isShared=true',
  264. }))
  265. defineExpose({
  266. navBarFixed: false,
  267. })
  268. </script>
  269. <template>
  270. <div class="flex-grow flex flex-col">
  271. <NavbarEvo transparent dark :isShowBack="!isShared"></NavbarEvo>
  272. <div class="relative">
  273. <!-- <wd-img width="100%" custom-class="aspect-[1.14/1]" /> -->
  274. <div class="aspect-[1.14/1]">
  275. <ImageEvo
  276. :src="designerInfo?.homePageUrl || NetImages.DesignerHomepageDefaultBg"
  277. mode="aspectFill"
  278. ></ImageEvo>
  279. </div>
  280. <div class="absolute bottom-0 left-0 right-0">
  281. <div class="bg-gradient-to-t from-black to-transparent">
  282. <div class="flex min-h-27 px-3.5 gap-3.5">
  283. <div class="relative">
  284. <wd-img
  285. custom-class="overflow-hidden rounded-full "
  286. :width="72"
  287. :height="72"
  288. :src="memberInfo?.avatar || NetImages.DefaultAvatar"
  289. ></wd-img>
  290. <wd-img
  291. v-if="getMemberAvatarFrame(memberInfo?.levelId)"
  292. custom-class="vertical-bottom absolute! level-circle-one"
  293. :width="79"
  294. :height="82"
  295. :src="getMemberAvatarFrame(memberInfo?.levelId) || ''"
  296. ></wd-img>
  297. </div>
  298. <div class="pb-8 flex-1 overflow-hidden">
  299. <div class="flex items-center justify-between">
  300. <div class="text-white text-2xl font-normal font-['PingFang_SC'] leading-normal">
  301. {{ designerInfo.homePageName || memberInfo.nickname }}
  302. </div>
  303. <div
  304. v-if="isOwn && features.personalCode"
  305. class="flex items-center"
  306. @click="router.push(`/pages-sub/mine/homepage/qr-code/index`)"
  307. >
  308. <wd-img width="22" height="22" :src="qrCode"></wd-img>
  309. <wd-icon name="chevron-right" color="white" size="16"></wd-icon>
  310. </div>
  311. </div>
  312. <div
  313. class="mt-2.5 flex gap-4 overflow-x-auto whitespace-nowrap"
  314. v-if="designerInfo?.personalIdentity != ''"
  315. >
  316. <template v-for="(it, i) in designerInfo?.personalIdentity?.split('、')" :key="i">
  317. <div
  318. class="inline-block h-6 px-2 bg-black/10 rounded-[30px] border border-solid border-white/60 justify-center items-center box-border inline-flex"
  319. >
  320. <div
  321. class="text-center text-white text-[10px] font-normal font-['PingFang_SC']"
  322. >
  323. {{ it }}
  324. </div>
  325. </div>
  326. </template>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. </div>
  332. </div>
  333. <div class="flex-grow flex flex-col bg-white rounded-t-2xl relative bottom-4 gap-5 px-3.5 pt-5">
  334. <div class="flex gap-4" v-if="skills?.length">
  335. <template v-for="(it, i) in skills" :key="i">
  336. <div>
  337. <span
  338. class="mr-0.575 text-black/90 text-base font-normal font-['PingFang_SC'] leading-[26.98px]"
  339. >
  340. {{ it.value }}
  341. </span>
  342. <span
  343. class="text-center text-black/40 text-sm font-normal font-['PingFang_SC'] leading-[26.98px]"
  344. >
  345. {{ it.label }}
  346. </span>
  347. </div>
  348. <div v-if="i < skills?.length - 1" class="leading-[26.98px] text-black/60">|</div>
  349. </template>
  350. </div>
  351. <div class="text-black/80 text-sm font-normal font-['PingFang_SC'] leading-normal">
  352. {{ designerInfo?.designDesc }}
  353. </div>
  354. <div v-if="badges?.length" class="h-[42px] relative mr--3.5">
  355. <div
  356. class="h-full left-0 pl-20 pr-6 right-20 top-0 bottom-0 absolute bg-gradient-to-r from-[#ffe9e9] via-[#fff7f7] to-[#fff8f8] rounded-tl-md rounded-bl-md flex flex-col justify-center"
  357. >
  358. <div class="">
  359. <div class="flex items-center gap-4">
  360. <template v-for="(it, i) in badges?.slice(0, badges?.length > 5 ? 3 : 4)" :key="i">
  361. <!-- <div class="bg-[#fa9d3b]"> -->
  362. <wd-img width="26" mode="widthFix" :src="it.badgeYesObtainedImage"></wd-img>
  363. <!-- </div> -->
  364. </template>
  365. <div v-if="badges?.length > 5" class="flex">
  366. <wd-img custom-class="m-a" width="26" mode="widthFix" :src="more"></wd-img>
  367. </div>
  368. </div>
  369. </div>
  370. </div>
  371. <div
  372. class="w-[61px] h-2 left-[14px] top-[23px] absolute bg-gradient-to-r from-[#ff9e91] via-[#ffe5d7] to-[#ffe4d6] rounded-tl-[20px] rounded-bl-[20px]"
  373. ></div>
  374. <div
  375. class="w-12 h-[19px] left-[14px] top-[9px] absolute text-center text-black text-xs font-normal font-['PingFang_SC'] leading-normal"
  376. >
  377. 荣誉徽章
  378. </div>
  379. <div
  380. class="w-[76px] h-[30px] px-3 right-0 top-1.5 absolute bg-black/90 rounded-tl-[30px] rounded-bl-[30px] justify-center items-center gap-2.5 inline-flex"
  381. @click="
  382. router.push(`/pages-sub/mine/honors/index?id=${id}${isShared ? '&isShared=true' : ''}`)
  383. "
  384. >
  385. <div class="text-center text-white text-xs font-normal font-['PingFang_SC']">
  386. 查看荣誉
  387. </div>
  388. </div>
  389. </div>
  390. <div
  391. v-if="isOwn || designerInfo?.videoNumber"
  392. class="bg-gradient-to-t from-[#fdf6ee] to-[#fefdfc] rounded-[10px] border border-[#fff4e6] border-solid flex items-center px-3.5 py-5 gap-3"
  393. @click="handle2Video"
  394. >
  395. <div>
  396. <div class="w-[37.01px] h-[37.01px] bg-[#fa9d3b] rounded-lg">
  397. <wd-img width="100%" height="100%" :src="wechatChannels"></wd-img>
  398. </div>
  399. <div
  400. v-if="isOwn && (designerInfo?.videoNumber ?? '') !== ''"
  401. @click.stop="handleUnbundle"
  402. >
  403. <div
  404. class="text-[#da7e1e] text-[9px] font-normal font-['PingFang_SC'] leading-normal flex items-center"
  405. >
  406. 解绑
  407. <wd-icon name="arrow-right" size="12"></wd-icon>
  408. </div>
  409. </div>
  410. </div>
  411. <div class="flex-1">
  412. <div class="text-black/90 text-sm font-normal font-['PingFang_SC'] leading-normal">
  413. {{ designerInfo?.videoNumber ? '个人视频号' : '视频号' }}
  414. </div>
  415. <div class="text-black/40 text-xs font-normal font-['PingFang_SC'] leading-normal">
  416. {{
  417. designerInfo?.videoNumber
  418. ? '案例分享,打造专属生活美学空间'
  419. : '您还没有关联视频号 , 快去关联吧~'
  420. }}
  421. </div>
  422. </div>
  423. <div>
  424. <div
  425. v-if="designerInfo?.videoNumber"
  426. class="text-[#e08e38] text-xs font-normal font-['PingFang_SC'] leading-normal"
  427. @click.stop="handle2VideoNumber(videoNumberShare)"
  428. >
  429. 去看看
  430. </div>
  431. <div
  432. v-else
  433. class="h-7 px-4 py-0.5 bg-[#fa9d3b] rounded-[20px] justify-center items-center gap-2.5 inline-flex"
  434. @click.stop="router.push('/pages-sub/mine/homepage/channels/index')"
  435. >
  436. <div
  437. class="text-center text-white text-xs font-normal font-['PingFang_SC'] leading-normal"
  438. >
  439. 去关联
  440. </div>
  441. </div>
  442. </div>
  443. </div>
  444. <div>
  445. <wd-tabs v-model="tab" custom-class="bg-transparent!">
  446. <template v-for="({ label, value }, index) in tabs" :key="index">
  447. <wd-tab :title="label" :name="value"></wd-tab>
  448. </template>
  449. </wd-tabs>
  450. <PageHelper
  451. ref="pageHelperRef"
  452. class="flex-grow flex flex-col bg-[#f6f6f6] mx--3.5"
  453. custom-class=""
  454. :request="getCircles"
  455. :query="query"
  456. >
  457. <template #default="{ source }">
  458. <div class="p-3.5 flex flex-col bg-[#f6f6f6] gap-3.5">
  459. <template v-for="it of source.list" :key="it.id">
  460. <view class="">
  461. <MomentItem
  462. :dict="{ spaceType: dictCircleSpaceType, designStyle: dictMemberDesignStyle }"
  463. :options="it"
  464. :is-own="userInfo.userId === it.stylistId"
  465. :is-shared="isShared"
  466. @share="handleShare"
  467. @delete="handleMomentDelete"
  468. @like="handleLike"
  469. ></MomentItem>
  470. </view>
  471. </template>
  472. </div>
  473. </template>
  474. </PageHelper>
  475. </div>
  476. </div>
  477. <BottomAppBar fixed placeholder>
  478. <div class="flex gap-7.5">
  479. <div class="flex-1" v-if="isOwn && !isShared">
  480. <wd-button
  481. block
  482. :round="false"
  483. @click="router.push(`/pages-sub/mine/homepage/edit/index`)"
  484. >
  485. 编辑
  486. </wd-button>
  487. </div>
  488. <div class="flex-1" v-if="isOwn && !isShared">
  489. <button
  490. v-if="features.shareMoment"
  491. class="p-0 after:b-none"
  492. block
  493. :round="false"
  494. data-type="homepage"
  495. :data-share-content="shareContent"
  496. :open-type="features.shareMoment ? 'share' : ''"
  497. :data-options="{ userId: userInfo.userId, homepageId: id }"
  498. :data-level="userInfo.level.level"
  499. @click="clickByPermission('share', () => {})"
  500. >
  501. <wd-button block :round="false">分享</wd-button>
  502. </button>
  503. <template v-else>
  504. <!-- 1-->
  505. <wd-button block :round="false" @click="clickByPermission('share', () => {})">
  506. 分享
  507. </wd-button>
  508. </template>
  509. </div>
  510. <div class="flex-1" v-if="!isOwn || isShared">
  511. <wd-button
  512. block
  513. :round="false"
  514. @click="router.push(`/pages-sub/mine/homepage/consult/index?id=${id}`)"
  515. >
  516. 预约咨询
  517. </wd-button>
  518. </div>
  519. </div>
  520. </BottomAppBar>
  521. </div>
  522. </template>
  523. <style scoped>
  524. :deep(.level-circle-one) {
  525. top: -20rpx;
  526. left: -10rpx;
  527. }
  528. :deep(.level-circle) {
  529. top: -10rpx;
  530. left: -4rpx;
  531. }
  532. </style>