index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. <route lang="json">
  2. {
  3. "style": {
  4. "navigationBarTitleText": "详情",
  5. "navigationBarBackgroundColor": "#fff",
  6. "navigationStyle": "custom"
  7. }
  8. }
  9. </route>
  10. <script setup lang="ts">
  11. import Tag from '@/components/tag.vue'
  12. import {
  13. createCircleReview,
  14. getCircle,
  15. getCircleReviews,
  16. getCircleUpvotes,
  17. shareCircle,
  18. } from '../../../core/libs/requests'
  19. import { handleShareClick, handleUpvoteClick } from '../../../core/libs/actions'
  20. import CommentItem from '../components/comment-item.vue'
  21. import { useDictStore, useUserStore } from '../../../store'
  22. import { storeToRefs } from 'pinia'
  23. import { isImageOrVideo, requestToast } from '../../../core/utils/common'
  24. import dayjs from 'dayjs'
  25. import SectionHeading from '@/components/section-heading.vue'
  26. import BottomAppBar from '@/components/bottom-app-bar.vue'
  27. import { likeActived, likeBlack } from '@designer-hub/assets/src/icons'
  28. import NavBarEvo from '@/components/navbar-evo.vue'
  29. import { useRouter } from '../../../core/utils/router'
  30. import { usePermissions } from '../../../composables/permissions'
  31. import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html.vue'
  32. import WdInput from 'wot-design-uni/components/wd-input/wd-input.vue'
  33. import { getRect, addUnit } from 'wot-design-uni/components/common/util'
  34. import Card from '@/components/card.vue'
  35. import { get } from 'radash'
  36. import { DictType } from '../../../core/libs/models'
  37. const { features, clickByPermission } = usePermissions()
  38. const userStore = useUserStore()
  39. const { userInfo } = storeToRefs(userStore)
  40. const router = useRouter()
  41. const dictStore = useDictStore()
  42. const { getOptionLabel } = dictStore
  43. const id = ref()
  44. const isShared = ref(false)
  45. const commeentRef = ref<InstanceType<typeof WdInput>>()
  46. const commentItemRef = ref<InstanceType<typeof CommentItem>[]>()
  47. const instance = getCurrentInstance()
  48. const focus = ref(false)
  49. const { data, run } = useRequest(() => getCircle(id.value), { initialData: {} })
  50. const { data: reviews, run: runGetReviews } = useRequest(
  51. () => getCircleReviews({ circleId: id.value }),
  52. {
  53. initialData: {
  54. list: [],
  55. },
  56. },
  57. )
  58. const { data: circleUpvotes, run: setCircleUpvotes } = useRequest(
  59. () => getCircleUpvotes(id.value),
  60. { initialData: { list: [], total: 0 } },
  61. )
  62. const swiperSizes = ref()
  63. const swiperStyle = ref()
  64. const reviewContent = ref('')
  65. const isVideo = ref(false)
  66. const reviewId = ref()
  67. const refreshIndex = ref<number>()
  68. const handleChange = ({ detail: { current } }) => {
  69. // swiperStyle.value = {
  70. // height: swiperSizes.value[current].height + 'px',
  71. // }
  72. }
  73. const setSwiperStyle = async () => {
  74. if (!data.value.bannerUrls.length) return
  75. const { screenWidth } = await uni.getSystemInfo()
  76. if (data.value.bannerUrls.length === 1 && isImageOrVideo(data.value.bannerUrls[0]) === 'video') {
  77. isVideo.value = true
  78. return
  79. }
  80. const { width, height } = await uni.getImageInfo({ src: data.value.bannerUrls.at(0) })
  81. console.log(screenWidth / width)
  82. swiperStyle.value = {
  83. height:
  84. height > width
  85. ? addUnit(500)
  86. : addUnit(
  87. screenWidth / width > 1
  88. ? height / (screenWidth / width)
  89. : height * (screenWidth / width),
  90. ),
  91. }
  92. console.log('swiperStyle', swiperStyle.value)
  93. }
  94. const handleSend = async () => {
  95. if (!reviewContent.value) {
  96. uni.showToast({ title: '请输入评论内容', icon: 'none' })
  97. return
  98. }
  99. const { code, msg } = await createCircleReview({
  100. circleId: id.value,
  101. userId: userInfo.value.userId,
  102. userName: userInfo.value.nickname,
  103. reviewContent: reviewContent.value,
  104. replayReviewId: reviewId.value,
  105. })
  106. if (code !== 0) {
  107. uni.showToast({ title: msg, icon: 'none' })
  108. } else {
  109. reviewContent.value = ''
  110. uni.showToast({ title: '评论成功', icon: 'none' })
  111. if (refreshIndex.value) {
  112. console.log(instance.refs)
  113. commentItemRef.value.at(refreshIndex.value).refresh()
  114. reviewId.value = undefined
  115. refreshIndex.value = undefined
  116. } else {
  117. await runGetReviews()
  118. }
  119. focus.value = false
  120. }
  121. }
  122. const handleReplay = async (options) => {
  123. reviewId.value = options.reviewId
  124. refreshIndex.value = options.index
  125. focus.value = true
  126. }
  127. const handleDelete = async (index?: number) => {
  128. if (index !== undefined) {
  129. commentItemRef.value.at(index).refresh()
  130. } else {
  131. await runGetReviews()
  132. }
  133. }
  134. const handleUpvote = async (index?: number) => {
  135. if (index !== undefined) {
  136. commentItemRef.value.at(index).refresh()
  137. } else {
  138. await run()
  139. }
  140. }
  141. onMounted(async () => {})
  142. onLoad(async (query: { id: string; isShared?: boolean }) => {
  143. id.value = query.id
  144. isShared.value = query.isShared
  145. await run()
  146. await setSwiperStyle()
  147. await runGetReviews()
  148. await setCircleUpvotes()
  149. })
  150. // onShareAppMessage(async () => {
  151. // await shareCircle(id.value)
  152. // return { title: data.value?.circleDesc }
  153. // })
  154. onShareAppMessage(async ({ from, target }) => {
  155. console.log('from', from)
  156. console.log('target', target)
  157. // if (!features.value.shareMoment) {
  158. // return handleShareClick()
  159. // }
  160. const res: Page.CustomShareContent = {}
  161. await shareCircle(id.value)
  162. res.path = `/pages/home/moment/index?id=${id.value}&isShared=true`
  163. res.imageUrl = data.value?.bannerUrls[0]
  164. res.title = `${data.value?.stylistName}: ${data.value?.circleDesc}`
  165. return res
  166. })
  167. </script>
  168. <template>
  169. <view class="bg-white flex-grow">
  170. <NavBarEvo placeholder :isShowBack="!isShared">
  171. <template #prepend>
  172. <div
  173. class="flex items-center gap-2"
  174. @click="
  175. ['1', '2'].includes(data.circleType) &&
  176. router.push(`/pages/mine/homepage/index?id=${data.stylistId}`)
  177. "
  178. >
  179. <wd-img width="24" height="24" round :src="data.headUrl"></wd-img>
  180. <div class="text-black/90 text-sm font-normal font-['PingFang_SC'] leading-[10.18px]">
  181. {{ data.stylistName || data.marketing }}
  182. </div>
  183. </div>
  184. </template>
  185. </NavBarEvo>
  186. <!-- <div class="my-4 text-black/90 text-lg font-normal font-['PingFang_SC'] leading-[10.18px]">
  187. {{ data?.detailsDesc }}
  188. </div> -->
  189. <template v-if="data.circleType === '1'">
  190. <template v-if="!isVideo">
  191. <div>
  192. <swiper class="" :style="swiperStyle" @change="handleChange">
  193. <template v-for="it of data?.bannerUrls" :key="it">
  194. <swiper-item>
  195. <wd-img width="100%" height="100%" :src="it" mode="aspectFill"></wd-img>
  196. </swiper-item>
  197. </template>
  198. </swiper>
  199. </div>
  200. </template>
  201. <template v-if="isVideo">
  202. <video width="100%" class="w-full aspect-[1.64/1]" :src="data?.bannerUrls[0]"></video>
  203. </template>
  204. </template>
  205. <template v-if="data.circleType === '2'">
  206. <div>
  207. <wd-img width="100%" mode="widthFix" :src="data.bannerUrls[0]"></wd-img>
  208. <div class="relative">
  209. <div
  210. class="absolute top-0 left-3.5 right-3.5 box-border h-full flex items-center justify-center"
  211. >
  212. <div class="w-full px-4 py-7 bg-white rounded-2xl shadow">
  213. <div class="text-black/90 text-xl font-normal font-['PingFang_SC']">
  214. 设计案例:{{ data?.caseName }}
  215. </div>
  216. <div class="mt-4 flex items-center justify-between text-black/40 text-sm">
  217. <div class="font-normal font-['PingFang_SC']">
  218. 类别:{{ getOptionLabel(DictType.circleSpaceType, data?.spaceType) }}
  219. </div>
  220. |
  221. <div class="text-black/40 text-sm font-normal font-['PingFang_SC']">
  222. 风格:{{ getOptionLabel(DictType.memberDesignStyle, data.designStyle) }}
  223. </div>
  224. |
  225. <div class="text-black/40 text-sm font-normal font-['PingFang_SC']">
  226. 面积:{{ data.spaceExtent }}
  227. </div>
  228. </div>
  229. </div>
  230. </div>
  231. </div>
  232. </div>
  233. </template>
  234. <div v-if="data.circleType === '2'" class="mt-24 px-3.5">
  235. <SectionHeading custom-class="" title="案例描述"></SectionHeading>
  236. </div>
  237. <view v-if="data.circleType === '3'">
  238. <mpHtml :content="data.detailsDesc"></mpHtml>
  239. </view>
  240. <view class="m-3.5" :class="data.circleType === '2' ? 'mx-7' : ''">
  241. <div class="text-black/90 text-base font-normal font-['PingFang_SC']">
  242. {{ data?.circleDesc }}
  243. </div>
  244. <view class="my-5.5 flex gap-3.5 flex-wrap">
  245. <!-- <TiltedButton>按钮</TiltedButton> -->
  246. <template v-if="data?.tagName !== ''">
  247. <template v-for="it of data?.tagName?.split(',')" :key="it">
  248. <Tag>{{ it }}</Tag>
  249. </template>
  250. </template>
  251. </view>
  252. </view>
  253. <div v-if="data.circleType === '2'" class="mb-4">
  254. <SectionHeading custom-class="mx-3.5 my-7" title="效果图"></SectionHeading>
  255. <template v-for="(it, i) in data?.bannerUrls.slice(1, data?.bannerUrls?.length)" :key="i">
  256. <wd-img width="100%" mode="widthFix" :src="it"></wd-img>
  257. </template>
  258. </div>
  259. <div class="mx-3.5">
  260. <div class="text-black/30 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]">
  261. {{ dayjs(data.createTime).format('YYYY-MM-DD HH:mm') }}
  262. </div>
  263. <!-- <view class="flex items-center my-4">
  264. <view class="flex items-center">
  265. <avatar-group-casual
  266. :show-number="3"
  267. :urls="[
  268. 'https://via.placeholder.com/20x20',
  269. 'https://via.placeholder.com/20x20',
  270. 'https://via.placeholder.com/20x20',
  271. ]"
  272. ></avatar-group-casual>
  273. <div
  274. class="ml-1 text-black/60 text-sm font-normal font-['PingFang_SC'] leading-[10.18px]"
  275. >
  276. {{ circleUpvotes.total }}人赞过
  277. </div>
  278. </view>
  279. <view class="flex-1"></view>
  280. <view><wd-icon class="text-black/65" name="arrow-right" size="22px"></wd-icon></view>
  281. </view> -->
  282. <div class="h-0.25 bg-[#dadada] my-7"></div>
  283. <SectionHeading :title="`评论`" size="base">
  284. <template #append>
  285. <view v-if="reviews?.list" class="flex">
  286. <div class="text-black/90 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]">
  287. 按热度
  288. </div>
  289. <div
  290. class="mx-2 text-black/40 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]"
  291. >
  292. |
  293. </div>
  294. <div class="text-black/40 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]">
  295. 按时间
  296. </div>
  297. </view>
  298. </template>
  299. </SectionHeading>
  300. <view clas="mt-8.25">
  301. <template v-if="reviews?.list.length">
  302. <template v-for="(it, i) in reviews?.list" :key="it.id">
  303. <CommentItem
  304. ref="commentItemRef"
  305. :options="it"
  306. :isChild="false"
  307. :index="i"
  308. @upvote="handleUpvote"
  309. @delete="handleDelete"
  310. @replay="handleReplay"
  311. ></CommentItem>
  312. <!-- <template v-for="child of it.childrens" :key="child.id">
  313. <CommentItem :options="child" :isChild="true"></CommentItem>
  314. </template> -->
  315. </template>
  316. </template>
  317. <template v-else>
  318. <view class="flex items-center justify-center mt-26 mb-36">
  319. <div class="text-black/40 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]">
  320. 这里空空的
  321. </div>
  322. <div
  323. class="ml-1.5 text-[#2f4471]/90 text-xs font-normal font-['PingFang_SC'] leading-[10.18px]"
  324. @click="focus = true"
  325. >
  326. 点击评论~
  327. </div>
  328. </view>
  329. </template>
  330. </view>
  331. </div>
  332. <BottomAppBar fixed placeholder border custom-class="">
  333. <div class="bg-white flex items-center">
  334. <div class="w-[168px] bg-[#f6f6f6] rounded-[60px] px-3.5 py-2 flex items-center">
  335. <wd-input
  336. ref="commeentRef"
  337. custom-class="bg-transparent!"
  338. no-border
  339. confirm-type="send"
  340. v-model="reviewContent"
  341. placeholder="说点什么..."
  342. :focus="focus"
  343. :cursor-spacing="16"
  344. @blur="focus = false"
  345. @confirm="handleSend"
  346. ></wd-input>
  347. </div>
  348. <view class="flex justify-around flex-1">
  349. <div>
  350. <button open-type="share" class="bg-transparent! p-0!">
  351. <view
  352. class="flex flex-col items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5"
  353. >
  354. <div class="w-4.5 h-4.5 flex items-center justify-center">
  355. <wd-img width="15" height="15" src="/static/svgs/share.svg"></wd-img>
  356. </div>
  357. <view class="">{{ data?.shareCount || 0 }}</view>
  358. </view>
  359. </button>
  360. </div>
  361. <view
  362. class="flex flex-col items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5"
  363. >
  364. <div class="w-4.5 h-4.5 flex items-center justify-center">
  365. <wd-img width="15" height="15" src="/static/svgs/comment.svg"></wd-img>
  366. </div>
  367. <view class="">{{ data?.reviewCount }}</view>
  368. </view>
  369. <view
  370. class="flex flex-col items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5"
  371. @click="
  372. clickByPermission('thumbsUp', () =>
  373. handleUpvoteClick(
  374. {
  375. upvote: data.ownUpvote,
  376. circleId: data.id,
  377. userId: userInfo.userId,
  378. userName: userInfo.nickname,
  379. },
  380. () => run(),
  381. ),
  382. )
  383. "
  384. >
  385. <template v-if="data.ownUpvote">
  386. <wd-img width="18" height="18" :src="likeActived"></wd-img>
  387. </template>
  388. <template v-else>
  389. <wd-img width="18" height="18" :src="likeBlack"></wd-img>
  390. </template>
  391. <view>{{ data.upvoteCount }}</view>
  392. </view>
  393. </view>
  394. </div>
  395. </BottomAppBar>
  396. </view>
  397. </template>