index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <route lang="json">
  2. {
  3. "style": {
  4. "navigationStyle": "custom"
  5. }
  6. }
  7. </route>
  8. <script setup lang="ts">
  9. import NavbarEvo from '@/components/navbar-evo.vue'
  10. import {
  11. activitySignup,
  12. getActivity,
  13. getActivitySignups,
  14. getAppMemberLevelConfigs,
  15. getStudyTour,
  16. getStudyTourSignups,
  17. studyTourSignup,
  18. } from '../../../../core/libs/requests'
  19. import { bell, map, rightFill } from '@designer-hub/assets/src/assets/svgs'
  20. import TiltedButton from '@/components/tilted-button.vue'
  21. import dayjs from 'dayjs'
  22. import BottomAppBar from '@/components/bottom-app-bar.vue'
  23. import { useRouter } from '../../../../core/utils/router'
  24. import PageHelper from '@/components/page-helper.vue'
  25. import { ConfigProviderThemeVars } from 'wot-design-uni'
  26. import SectionHeading from '@/components/section-heading.vue'
  27. import AvatarGroupCasual from '@/components/avatar-group-casual/avatar-group-casual.vue'
  28. import { calendar, clock, funnel, location, user } from '@designer-hub/assets/src/icons'
  29. import { signupSuccessDialogBg } from '@designer-hub/assets/src/bgs'
  30. import { NetImages } from '../../../../core/libs/net-images'
  31. import signupListDialogBg from '@designer-hub/assets/src/libs/assets/signupListDialogBg'
  32. import { getActivityStatusText, getCountsArr } from '../../../../core/utils/common'
  33. import { extractColorsFromImageData } from 'extract-colors/lib/extract-colors.mjs'
  34. import { replace, sort } from 'radash'
  35. import { Activity, StudyTour } from '../../../../core/libs/models'
  36. import mapLocation from '@designer-hub/assets/src/libs/assets/mapLocation'
  37. import cameraWhite from '@designer-hub/assets/src/libs/assets/cameraWhite'
  38. import ImgBtnEvo from '@/components/img-btn-evo.vue'
  39. import ButtonEvo from '@/components/button-evo.vue'
  40. const themeVars = ref<ConfigProviderThemeVars>({
  41. tableBorderColor: 'white',
  42. tabsNavLineBgColor: 'white',
  43. tabsNavColor: 'white',
  44. })
  45. const router = useRouter()
  46. const id = ref()
  47. const type = ref<'activity' | 'studyTour'>()
  48. const activityTypes = ref({ activity: '活动', studyTour: '游学' })
  49. const tab = ref(0)
  50. const request = ref<() => Promise<IResData<Partial<StudyTour> & Partial<Activity>>>>()
  51. const { data, run: setData } = useRequest(() => request.value(), { initialData: {} })
  52. const { data: signups, run: setSignups } = useRequest(
  53. () => getActivitySignups({ activityId: id.value }),
  54. { initialData: { list: [], total: 0 } },
  55. )
  56. const { data: levels, run: setLevels } = useRequest(() => getAppMemberLevelConfigs(), {
  57. initialData: [],
  58. })
  59. const show = ref(false)
  60. const successShow = ref(false)
  61. const listShow = ref(false)
  62. const dominantColor = ref()
  63. const palette = ref()
  64. const isActivity = computed(() => type.value === 'activity')
  65. const isStudyTour = computed(() => type.value === 'studyTour')
  66. const levelsById = computed(() =>
  67. levels.value.reduce((acc, item) => {
  68. acc[item.id] = item
  69. return acc
  70. }, {}),
  71. )
  72. const levelsByMemberLevel = computed(() =>
  73. levels.value.reduce((acc, item) => {
  74. acc[item.memberLevel] = item
  75. return acc
  76. }, {}),
  77. )
  78. const places = computed(() => {
  79. if (isActivity.value && data.value?.activityAllowType === '1') {
  80. return data.value?.activityAllowCount
  81. }
  82. if (isStudyTour.value && data.value?.studyAllowType === '1') {
  83. return data.value?.studyAllowCount
  84. }
  85. return '不限制'
  86. })
  87. const remainedCount = computed(() => {
  88. if (isActivity.value && data.value?.activityAllowType === '1') {
  89. return data.value?.activityAllowCount - signups.value.total
  90. }
  91. if (isStudyTour.value && data.value?.studyAllowType === '1') {
  92. return data.value?.studyAllowCount - signups.value.total
  93. }
  94. return '不限制'
  95. })
  96. const infos = computed(() => [
  97. {
  98. icon: clock,
  99. title: '报名时间',
  100. content: [
  101. dayjs(data.value.applyStartTime).format('YYYY.MM.DD HH:mm'),
  102. // dayjs(data.value.applyEndTime).format('YYYY.MM.DD'),
  103. ],
  104. visable: true,
  105. },
  106. {
  107. icon: calendar,
  108. title: `${activityTypes.value[type.value]}时间`,
  109. content: [
  110. dayjs(
  111. data.value.activityStartTime || data.value.studyStartTime || data.value.planStudyStartTime,
  112. ).format('YYYY.MM.DD'),
  113. dayjs(
  114. data.value.activityEndTime || data.value.studyEndTime || data.value.planStudyEndTime,
  115. ).format('YYYY.MM.DD'),
  116. ],
  117. visable: true,
  118. },
  119. {
  120. icon: location,
  121. title: `${activityTypes.value[type.value]}地点`,
  122. content: [data.value.activityAddr || ''],
  123. visable: isActivity.value,
  124. },
  125. {
  126. icon: user,
  127. title: `${activityTypes.value[type.value]}名额`,
  128. content: [
  129. places.value === '不限制' ? `不限制` : `${places.value}人/剩余${remainedCount.value}人`,
  130. ],
  131. visable: true,
  132. },
  133. {
  134. icon: funnel,
  135. title: `等级限制`,
  136. content: [
  137. data.value.memberLevel
  138. ?.map((it) => levelsByMemberLevel.value[String(it)]?.memberLevelName)
  139. .join('、') || '',
  140. ],
  141. visable: true,
  142. },
  143. ])
  144. const handleConfirm = async () => {
  145. const { data, code, msg } = await (isActivity.value ? activitySignup : studyTourSignup)({
  146. id: id.value,
  147. })
  148. console.log(msg)
  149. if (code === 0) {
  150. // todo: 报名成功弹框
  151. show.value = false
  152. successShow.value = true
  153. }
  154. await setData()
  155. }
  156. onLoad(async (query: { id: string; type: 'activity' | 'studyTour' }) => {
  157. id.value = query.id
  158. type.value = query.type
  159. if (type.value === 'activity') {
  160. request.value = () => getActivity(id.value)
  161. }
  162. if (type.value === 'studyTour') {
  163. request.value = () => getStudyTour(id.value)
  164. }
  165. await setData()
  166. const { path } = await uni.getImageInfo({ src: data.value.backgroundUrl })
  167. const ctx = uni.createCanvasContext('firstCanvas')
  168. uni
  169. .createSelectorQuery()
  170. .select('#firstCanvas')
  171. .fields({ size: true }, async ({ width, height }: any) => {
  172. // ctx.setFillStyle('#ffffff')
  173. ctx.drawImage(path, 0, 0, width, height)
  174. ctx.draw(true, async () => {
  175. const res1 = await uni.canvasGetImageData({
  176. canvasId: 'firstCanvas',
  177. x: 0,
  178. y: 0,
  179. width: width.toFixed(0),
  180. height: height.toFixed(0),
  181. })
  182. const { data: imageData } = res1
  183. dominantColor.value = `rgb(${getCountsArr(imageData, width, height)})`
  184. console.log(res1)
  185. const a = await extractColorsFromImageData(res1, {
  186. pixels: 1000000,
  187. distance: 0.22,
  188. colorValidator: (red, green, blue, alpha = 255) => alpha > 250,
  189. saturationDistance: 0.2,
  190. lightnessDistance: 0.2,
  191. hueDistance: 0.083333333,
  192. })
  193. console.log(a)
  194. const colors = sort(a, (it: any) => it.intensity, true)
  195. dominantColor.value = a[0].hex
  196. })
  197. })
  198. .exec()
  199. await setSignups()
  200. await setLevels()
  201. })
  202. </script>
  203. <template>
  204. <div
  205. class="flex-grow bg-white px-3.5 bg-[length:100%_100%]"
  206. :style="{
  207. backgroundColor: `${dominantColor}`,
  208. }"
  209. >
  210. <NavbarEvo transparent dark></NavbarEvo>
  211. <div class="aspect-[1.26/1] relative mx--3.5 relative">
  212. <!-- <wd-img width="100%" height="100%" :src="data.bannerUrl?.at(0)"></wd-img> -->
  213. <canvas
  214. class="w-full h-full absolute top--1000"
  215. canvas-id="firstCanvas"
  216. id="firstCanvas"
  217. ></canvas>
  218. <wd-img width="100%" height="100%" :src="data?.backgroundUrl"></wd-img>
  219. <div class="absolute left-3.5 bottom-3" @click="listShow = true">
  220. <div
  221. v-if="isStudyTour"
  222. class="bg-white/20 rounded-[20px] backdrop-blur-[6px] px-3.5 py-1 flex gap-2.5"
  223. >
  224. <wd-img width="20" height="20" :src="bell"></wd-img>
  225. <div class="text-[#c1c1c1] text-base font-normal font-['PingFang_SC'] leading-normal">
  226. 白金会员王凯峰已报名
  227. </div>
  228. <div class="w-6 bg-black aspect-square rounded-full flex items-center justify-center">
  229. <wd-img width="18" height="18" :src="rightFill"></wd-img>
  230. </div>
  231. </div>
  232. <div v-if="isActivity" class="flex items-center gap-1.25">
  233. <AvatarGroupCasual
  234. :urls="signups.list.map((it) => it.headImgUrl || NetImages.DefaultAvatar)"
  235. :width="40"
  236. :height="40"
  237. ></AvatarGroupCasual>
  238. <div class="text-white/60 text-sm font-normal font-['PingFang_SC'] leading-[10.18px]">
  239. {{ signups.total }}人已报名
  240. </div>
  241. </div>
  242. </div>
  243. </div>
  244. <div class="h-9">
  245. <div v-if="type === 'studyTour'" class="flex items-center h-full mt-9">
  246. <wd-img width="18" height="18" :src="map"></wd-img>
  247. <div class="text-[#c1c1c1] text-base font-normal font-['PingFang_SC'] leading-normal">
  248. 第一站
  249. </div>
  250. </div>
  251. </div>
  252. <div
  253. class="w-[347px] text-white text-[26px] font-normal font-['PingFang_SC'] leading-[44px] flex items-center gap-4"
  254. >
  255. <!-- 日本研学·东京艺术大学设计游学 -->
  256. <div class="inline-block">{{ data?.name }}</div>
  257. <div class="inline-block py-1.5 px-4 bg-white rounded-[20px] backdrop-blur-[15px]">
  258. <div class="text-[#a60707] text-sm font-normal font-['PingFang_SC'] leading-relaxed">
  259. {{ getActivityStatusText(data?.applyStartTime, data?.applyEndTime) }}
  260. </div>
  261. </div>
  262. </div>
  263. <div
  264. class="px-4 py-6 bg-[#010102]/30 backdrop-blur-[20px] rounded-2xl my-8 flex flex-col gap-3"
  265. >
  266. <!-- {{ levelsById }} -->
  267. <template v-for="(it, i) in infos" :key="i">
  268. <div v-if="it.visable" class="flex items-center gap-1.5">
  269. <wd-img width="16" height="16" :src="it.icon"></wd-img>
  270. <div
  271. class="w-17.5 whitespace-nowrap text-[#c1c1c1] text-base font-normal font-['PingFang_SC'] leading-normal"
  272. >
  273. {{ it.title }}
  274. </div>
  275. <div class="w-3"></div>
  276. <div
  277. class="flex-1 flex break-all items-center text-white text-base font-normal font-['PingFang_SC'] leading-[34px]"
  278. >
  279. <template v-if="it.content.length === 2">
  280. <div class="w-22 text-start">{{ it.content[0] }}</div>
  281. <wd-icon name="play" size="22px"></wd-icon>
  282. <div class="w-22 text-center">{{ it.content[0] }}</div>
  283. </template>
  284. <template v-else>{{ it.content[0] }}</template>
  285. </div>
  286. </div>
  287. </template>
  288. </div>
  289. <div v-if="isStudyTour" class="w-50%">
  290. <wd-config-provider :themeVars="themeVars">
  291. <wd-tabs v-model="tab" class="bg-transparent!" custom-class="bg-transparent!">
  292. <wd-tab title="活动介绍"></wd-tab>
  293. <wd-tab title="行程安排"></wd-tab>
  294. </wd-tabs>
  295. </wd-config-provider>
  296. </div>
  297. <SectionHeading v-if="isActivity" size="lg" title="活动介绍"></SectionHeading>
  298. <div class="mt-5 mx-3.5">
  299. <div
  300. v-if="tab === 0"
  301. class="text-justify text-[#c1c1c1] text-base font-normal font-['PingFang_SC'] leading-relaxed"
  302. v-html="data['activityDesc']"
  303. ></div>
  304. <div v-if="tab === 1 && 'studyTravelList' in data">
  305. <template v-for="(it, i) in data.studyTravelList" :key="i">
  306. <div class="flex flex-col gap-6">
  307. <div class="text-white text-base font-normal font-['PingFang_SC'] leading-normal">
  308. <!-- 6月26日 第一天 -->
  309. {{ dayjs(it?.travelTime).format('MM月DD日') }}
  310. <span class="ml-1">{{ `第${i + 1}天` }}</span>
  311. </div>
  312. <div class="flex gap-2">
  313. <div class="w-7 h-7 bg-white/10 rounded-full flex items-center justify-center">
  314. <wd-img width="82%" height="82%" :src="mapLocation"></wd-img>
  315. </div>
  316. <div class="flex-1 flex flex-col gap-4">
  317. <div class="h-7 flex items-center gap-2.5">
  318. <div class="text-white text-sm font-normal font-['PingFang_SC'] leading-normal">
  319. 9:00
  320. </div>
  321. <div class="text-white text-sm font-normal font-['PingFang_SC'] leading-normal">
  322. <!-- 早稻田大学课程 -->
  323. {{ it.title }}
  324. </div>
  325. </div>
  326. <div class="">
  327. <span
  328. class="text-[#c1c1c1] text-sm font-normal font-['PingFang_SC'] leading-[23px]"
  329. >
  330. 行程介绍:
  331. </span>
  332. <span
  333. class="text-[#ababab] text-sm font-normal font-['PingFang_SC'] leading-[23px]"
  334. >
  335. <!-- 是位于日本东京都新宿区的一所著名的私立大学。它由早稻田大学的创始人大隈重信于1882年创立,是日本超级国际化大学计划(Top
  336. Global University Project)选定的大学之一,也是日本顶尖的高等教育机构之一。 -->
  337. {{ it.travelDesc }}
  338. </span>
  339. </div>
  340. <div class="flex items-center gap-1">
  341. <wd-img width="16" height="16" :src="cameraWhite"></wd-img>
  342. <div class="text-white text-xs font-normal font-['PingFang_SC'] leading-normal">
  343. 打卡示例
  344. </div>
  345. </div>
  346. <img class="w-full rounded-2xl border" :src="it.clockExplainUrl" />
  347. </div>
  348. </div>
  349. </div>
  350. </template>
  351. </div>
  352. </div>
  353. <BottomAppBar fixed placeholder transparent>
  354. <div
  355. class="h-[63px] bg-white/90 rounded-2xl backdrop-blur-[20px] flex items-center gap-1 px-4 box-border"
  356. >
  357. <div class="text-[#ef4343] text-2xl font-normal font-['D-DIN_Exp'] leading-normal">
  358. {{ data.needPointsCount || 0 }}
  359. </div>
  360. <div class="text-black/40 text-base font-normal font-['PingFang_SC'] leading-[34px]">
  361. 积分
  362. </div>
  363. <div class="flex-1"></div>
  364. <div @click="show = true">
  365. <!-- <ImgBtnEvo>{{ data?.ifSingnUp ? '已报名' : '立即报名' }}</ImgBtnEvo> -->
  366. <ButtonEvo>{{ data?.ifSingnUp ? '已报名' : '立即报名' }}</ButtonEvo>
  367. <!-- <TiltedButton size="large">{{ data?.ifSingnUp ? '已报名' : '立即报名' }}</TiltedButton> -->
  368. </div>
  369. </div>
  370. </BottomAppBar>
  371. <wd-action-sheet v-model="show">
  372. <view class="px-3.5 py-10">
  373. <div class="flex gap-5 mb-13.5">
  374. <div class="w-[110px] h-[94px] bg-[#f6f6f6] rounded-2xl">
  375. <wd-img width="100%" height="100%" :src="data.thumbnailUrl"></wd-img>
  376. </div>
  377. <div class="flex flex-col justify-between flex-1">
  378. <div class="text-black text-base font-normal font-['PingFang_SC'] leading-normal">
  379. {{ data.name }}
  380. </div>
  381. <div class="flex items-end gap-1">
  382. <div class="text-[#ef4343] text-[22px] font-normal leading-[22px]">
  383. {{ data.needPointsCount || 0 }}
  384. </div>
  385. <div class="text-black/40 text-sm font-normal font-['PingFang_SC']">积分</div>
  386. <div class="ml-1 text-black/40 text-xs font-normal font-['PingFang_SC']">
  387. 剩余:{{ remainedCount || 0 }}
  388. </div>
  389. <div class="flex-1"></div>
  390. </div>
  391. </div>
  392. </div>
  393. <wd-button block :round="false" @click="handleConfirm">确认报名</wd-button>
  394. </view>
  395. </wd-action-sheet>
  396. <wd-overlay :show="listShow" @click="listShow = false">
  397. <view class="flex px-10 h-full items-center justify-center">
  398. <div class="w-full flex flex-col gap-5 aspect-[0.71/1] relative">
  399. <div class="absolute top-0 left-0 right-0 bottom-0 z--1">
  400. <wd-img width="100%" height="100%" :src="signupListDialogBg"></wd-img>
  401. </div>
  402. <div class="h-full box-border py-5 px-7.25 flex flex-col justify-between">
  403. <div class="flex justify-between">
  404. <div class="text-justify text-white text-2xl font-bold font-['Alimama_ShuHeiTi']">
  405. 报名详情
  406. </div>
  407. </div>
  408. <div class="flex flex-col justify-center aspect-[0.7/1] gap-5 p-6.5">
  409. <PageHelper
  410. :request="isActivity ? getActivitySignups : getStudyTourSignups"
  411. :query="isActivity ? { activityId: id } : { studyId: id }"
  412. class="flex-grow flex flex-col"
  413. >
  414. <template #default="{ source }">
  415. <div class="flex flex-col gap-5">
  416. <template v-for="(it, i) in source.list" :key="i">
  417. <div
  418. class="text-black text-sm font-normal font-['PingFang_SC'] leading-normal"
  419. >
  420. {{ dayjs(it.createTime).format('YYYY-MM-DD') }} {{ it.name }}已报名
  421. </div>
  422. </template>
  423. </div>
  424. </template>
  425. </PageHelper>
  426. </div>
  427. </div>
  428. </div>
  429. </view>
  430. </wd-overlay>
  431. <wd-overlay :show="successShow" @click="successShow = false">
  432. <view class="flex mx-10 h-full items-center justify-center">
  433. <div class="w-full flex flex-col gap-5 aspect-[1.12/1] relative">
  434. <div class="absolute top-0 left-0 right-0 bottom-0 z--1">
  435. <wd-img width="100%" height="100%" :src="signupSuccessDialogBg"></wd-img>
  436. </div>
  437. <div class="h-full box-border py-5 px-7.25 flex flex-col justify-between">
  438. <div class="flex justify-between">
  439. <div class="text-justify text-white text-2xl font-bold font-['Alimama_ShuHeiTi']">
  440. 报名成功
  441. </div>
  442. <wd-icon name="close" color="white" size="22px"></wd-icon>
  443. </div>
  444. <div class="flex flex-col justify-center aspect-[1.46/1] gap-5">
  445. <div class="flex gap-1.5">
  446. <wd-icon name="error-circle" size="22px"></wd-icon>
  447. <div
  448. class="w-[151px] h-[21px] text-justify text-black text-base font-normal font-['PingFang_SC'] leading-[21px]"
  449. >
  450. 请准时参加活动!
  451. </div>
  452. </div>
  453. <div
  454. class="w-[237px] h-[60px] text-justify text-black/60 text-base font-normal font-['PingFang_SC'] leading-normal"
  455. >
  456. 如有问题可咨询官方客服或您的专属经纪人!
  457. </div>
  458. </div>
  459. </div>
  460. </div>
  461. </view>
  462. </wd-overlay>
  463. </div>
  464. </template>