Selaa lähdekoodia

feat(app): 新增分享功能并优化用户界面

- 在首页和设计师主页添加分享按钮和相关功能
- 实现点赞功能并更新相关组件
- 优化导航栏,增加首页按钮
- 修复一些小问题,如默认头像等
EvilDragon 4 kuukautta sitten
vanhempi
commit
c69c72a169

+ 1 - 0
packages/app/package.json

@@ -111,6 +111,7 @@
     "radash": "^12.1.0",
     "sentry-uniapp": "^1.0.12",
     "uqrcodejs": "^4.0.7",
+    "validator": "^13.12.0",
     "vue": "^3.4.21",
     "vue-component-type-helpers": "^2.1.8",
     "vue-i18n": "^9.1.9",

+ 17 - 8
packages/app/src/components/moment-item.vue

@@ -39,7 +39,7 @@ const props = withDefaults(
   }>(),
   {},
 )
-const emits = defineEmits<{ delete: [id: number] }>()
+const emits = defineEmits<{ delete: [id: number]; like: [options: any] }>()
 const router = useRouter()
 const imgClass = ref('')
 const isVideo = ref(false)
@@ -52,9 +52,6 @@ const handleDelete = async () => {
   emits('delete', props.options.id)
 }
 onMounted(async () => {
-  // console.log('加载')
-  // console.log(currRoute().path)
-
   if (
     props.options.bannerUrls?.length === 1 &&
     isImageOrVideo(props.options.bannerUrls[0]) === 'image'
@@ -139,10 +136,21 @@ onMounted(async () => {
         </template>
       </view>
       <view class="flex justify-between">
-        <view class="flex items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5">
-          <wd-img width="15" height="15" src="/static/svgs/share.svg"></wd-img>
-          <view class="ml-1">{{ props.options.shareCount }}</view>
-        </view>
+        <div>
+          <button
+            open-type="share"
+            class="bg-transparent! p-0!"
+            :data-options="options"
+            @click.stop
+          >
+            <view
+              class="flex items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5"
+            >
+              <wd-img width="15" height="15" src="/static/svgs/share.svg"></wd-img>
+              <view class="ml-1">{{ props.options.shareCount }}</view>
+            </view>
+          </button>
+        </div>
         <view class="flex items-center text-[rgba(0,0,0,0.85)] text-3.5 font-400 line-height-5.5">
           <wd-img width="15" height="15" src="/static/svgs/comment.svg"></wd-img>
           <view class="ml-1">{{ props.options.reviewCount }}</view>
@@ -150,6 +158,7 @@ onMounted(async () => {
         <view
           class="flex items-center text-3.5 font-400 line-height-5.5"
           :class="[options.ownUpvote ? 'text-[#ca5141]' : 'text-[rgba(0,0,0,0.85)]']"
+          @click.stop="emits('like', { upvote: options.ownUpvote, circleId: options.id })"
         >
           <template v-if="options.ownUpvote">
             <wd-img width="18" height="18" :src="likeActived"></wd-img>

+ 36 - 1
packages/app/src/components/navbar-evo.vue

@@ -10,9 +10,18 @@ const props = defineProps<{
   placeholder?: boolean
 }>()
 const router = useRouter()
+const pages = computed(() => getCurrentPages())
 const themeVars = computed<ConfigProviderThemeVars>(() => ({
   navbarColor: props.dark ? 'white' : 'black',
+  // buttonIconSize: '100rpx',
 }))
+const handleToHome = () => {
+  uni.reLaunch({ url: '/pages/home/index' })
+}
+onMounted(() => {
+  // getCurrentPages()
+  // console.log(getCurrentPages())
+})
 </script>
 <template>
   <div>
@@ -25,7 +34,33 @@ const themeVars = computed<ConfigProviderThemeVars>(() => ({
         :custom-class="`${transparent ? 'bg-transparent!' : ''} `"
         v-bind="{ title }"
         @click-left="router.back()"
-      ></wd-navbar>
+      >
+        <!-- {{ pages }} -->
+        <template #capsule>
+          <div class="w-full h-full">
+            <!-- <wd-button
+              type="icon"
+              icon="home"
+              :custom-style="`color: ${dark ? 'white' : 'black'}`"
+              @click="handleToHome"
+            ></wd-button> -->
+            <wd-icon
+              v-if="pages.length === 1"
+              name="home"
+              size="50rpx"
+              :color="dark ? 'white' : 'blacak'"
+              @click="handleToHome"
+            ></wd-icon>
+            <wd-icon
+              v-else
+              name="arrow-left"
+              size="48rpx"
+              :color="dark ? 'white' : 'blacak'"
+              @click="router.back()"
+            ></wd-icon>
+          </div>
+        </template>
+      </wd-navbar>
       <template v-if="props.placeholder">
         <wd-navbar
           safe-area-inset-top

+ 1 - 0
packages/app/src/core/libs/messages.ts

@@ -0,0 +1 @@
+export const messages = { home: { shareTitle: '筑巢荟' }, moment: { imageNotExist: '请上传图片' } }

+ 3 - 0
packages/app/src/core/utils/common.ts

@@ -12,6 +12,9 @@ export const isImageOrVideo = (url) => {
     return 'unknown'
   }
 }
+export const toast = (msg: string, icon = 'none') => {
+  uni.showToast({ title: msg, icon })
+}
 export const requestToast = async <T>(
   func: () => Promise<IResData<T>>,
   options: { error?: boolean; errorTitle?: string; success?: boolean; successTitle?: string } = {

+ 0 - 1
packages/app/src/core/utils/router.ts

@@ -6,7 +6,6 @@ export const back = () => {
 export const useRouter = () => {
   const { routes, isLogined } = usePermissions()
   const push = async (url: string, switchTab = false) => {
-    // console.log(url)
     const path = url.split('?')[0]
     const route = routes.find((it) => it.path === path)
     if (route && !route.meta.canNotLogin && !isLogined.value) {

+ 28 - 3
packages/app/src/pages/home/index.vue

@@ -15,17 +15,22 @@ import HotActivity from '@/components/hot-activity.vue'
 import MomentItem from '@/components/moment-item.vue'
 import useRequest from '../../hooks/useRequest'
 import Menus from './components/menus.vue'
-import { getCircles, getSetIndexConfigs } from '../../core/libs/requests'
+import { getCircles, getSetIndexConfigs, shareCircle } from '../../core/libs/requests'
 import { logo } from '../../core/libs/svgs'
 import PageHelper from '@/components/page-helper.vue'
 import { ComponentExposed } from 'vue-component-type-helpers'
 import { usePermissions } from '../../composables/permissions'
 import { storeToRefs } from 'pinia'
+import { messages } from '../../core/libs/messages'
+import { handleUpvoteClick } from '../../core/libs/actions'
+import { useUserStore } from '../../store'
 
 defineOptions({
   name: 'Home',
 })
 const instance = getCurrentInstance()
+const userStore = useUserStore()
+const { userInfo } = storeToRefs(userStore)
 const { isLogined, isDesigner } = usePermissions()
 const pageHelperRef = ref<ComponentExposed<typeof PageHelper>>()
 const { data: indexConfigsData, run: setIndexConfigsData } = useRequest(
@@ -56,7 +61,27 @@ const handlePlay = () => {
   swiperData.value[swiperCurrent.value].videoContext.play()
   swiperData.value[swiperCurrent.value].playing = true
 }
-onShareAppMessage(() => ({}))
+const handleLike = async (options) => {
+  await handleUpvoteClick({
+    ...options,
+    userId: userInfo.value.userId,
+    userName: userInfo.value.nickname,
+  })
+  pageHelperRef.value?.refresh()
+}
+onShareAppMessage(async ({ from, target }) => {
+  const res: Page.CustomShareContent = {}
+  if (from === 'button') {
+    await shareCircle(target.id)
+    res.path = `/pages/home/moment/index?id=${target.id}`
+    res.imageUrl = target.dataset.options.bannerUrls[0]
+    res.title = `${target.dataset.options.stylistName}: ${target.dataset.options.circleDesc}`
+  }
+  if (from === 'menu') {
+    res.title = messages.home.shareTitle
+  }
+  return res
+})
 </script>
 
 <template>
@@ -124,7 +149,7 @@ onShareAppMessage(() => ({}))
           <template #default="{ source }">
             <template v-for="it of source.list" :key="it.id">
               <view class="my-6">
-                <MomentItem :options="it"></MomentItem>
+                <MomentItem :options="it" @like="handleLike"></MomentItem>
               </view>
             </template>
           </template>

+ 35 - 5
packages/app/src/pages/mine/homepage/index.vue

@@ -14,6 +14,7 @@ import {
   getDesignerInfo,
   getUserInfoById,
   updateDesignerInfo,
+  shareCircle,
 } from '../../../core/libs/requests'
 import { useUserStore } from '../../../store'
 import { storeToRefs } from 'pinia'
@@ -27,6 +28,7 @@ import { requestToast } from '../../../core/utils/common'
 import { ComponentExposed } from 'vue-component-type-helpers'
 import dayjs from 'dayjs'
 import wechatChannels from '@designer-hub/assets/src/libs/assets/wechatChannels'
+import { handleUpvoteClick } from '@/core/libs/actions'
 
 const { alert, confirm } = useMessage()
 const router = useRouter()
@@ -83,6 +85,14 @@ const handleMomentDelete = async (id) => {
     },
   })
 }
+const handleLike = async (options) => {
+  await handleUpvoteClick({
+    ...options,
+    userId: userInfo.value.userId,
+    userName: userInfo.value.nickname,
+  })
+  pageHelperRef.value?.refresh()
+}
 const handle2Video = () => {
   try {
     uni.openChannelsUserProfile({ finderUserName: designerInfo.value?.videoNumber })
@@ -138,10 +148,26 @@ onUnload(async () => {
     })
   }
 })
-onShareAppMessage(() => ({
-  title: `${userInfo.value.nickname}: “${designerInfo.value.designDesc}”`,
-  imageUrl: designerInfo.value?.sharePageUrl,
-}))
+// onShareAppMessage(() => ({
+//   title: `${userInfo.value.nickname}: “${designerInfo.value.designDesc}”`,
+//   imageUrl: designerInfo.value?.sharePageUrl,
+//   path: `/pages/mine/homepage/index?id=${id.value}`,
+// }))
+onShareAppMessage(async ({ from, target }) => {
+  const res: Page.CustomShareContent = {}
+  if (from === 'button') {
+    await shareCircle(target.id)
+    res.path = `/pages/home/moment/index?id=${target.id}`
+    res.imageUrl = target.dataset.options.bannerUrls[0]
+    res.title = `${target.dataset.options.stylistName}: ${target.dataset.options.circleDesc}`
+  }
+  if (from === 'menu') {
+    res.title = `${userInfo.value.nickname}: “${designerInfo.value.designDesc}”`
+    res.imageUrl = designerInfo.value?.sharePageUrl
+    res.path = `/pages/mine/homepage/index?id=${id.value}`
+  }
+  return res
+})
 onShareTimeline(() => ({}))
 defineExpose({
   navBarFixed: false,
@@ -222,7 +248,10 @@ defineExpose({
           <div class="w-[37.01px] h-[37.01px] bg-[#fa9d3b] rounded-lg">
             <wd-img width="100%" height="100%" :src="wechatChannels"></wd-img>
           </div>
-          <div v-if="(designerInfo?.videoNumber ?? '') !== ''" @click.stop="handleUnbundle">
+          <div
+            v-if="isOwn && (designerInfo?.videoNumber ?? '') !== ''"
+            @click.stop="handleUnbundle"
+          >
             <div
               class="text-[#da7e1e] text-[9px] font-normal font-['PingFang_SC'] leading-normal flex items-center"
             >
@@ -284,6 +313,7 @@ defineExpose({
                     :options="it"
                     :is-own="userInfo.userId === it.stylistId"
                     @delete="handleMomentDelete"
+                    @like="handleLike"
                   ></MomentItem>
                 </view>
               </template>

+ 1 - 1
packages/app/src/pages/mine/homepage/statistics/index.vue

@@ -73,7 +73,7 @@ onMounted(async () => {
                   class="flex-1 text-black text-sm font-normal font-['PingFang_SC'] leading-normal"
                 >
                   <!-- 银色飞行船 -->
-                  {{ it.stylistName }}
+                  {{ it.creatorName }}
                 </div>
                 <div class="text-black/40 text-xs font-normal font-['PingFang SC'] leading-normal">
                   浏览时长:{{ (Number(it.duration) / 60).toFixed(2) }}分钟

+ 2 - 2
packages/app/src/pages/mine/invite/index.vue

@@ -18,7 +18,7 @@ import { useMessage } from 'wot-design-uni'
 import { NetImages } from '../../../core/libs/net-images'
 import NavbarEvo from '@/components/navbar-evo.vue'
 import ImageEvo from '@/components/image-evo.vue'
-import { Canvas, CircleImage } from '@/core/utils/canvas'
+import { Canvas } from '../../../core/utils/canvas'
 
 const { alert } = useMessage()
 const userStore = useUserStore()
@@ -37,7 +37,7 @@ const createPoster = () => {
       uni.showLoading({ title: '生成中' })
       const [path, bgPath, logoPath, avatarPath, icon1, icon2, icon3, icon4] = await Promise.all(
         [
-          data.value.homePageUrl,
+          data.value.sharePageUrl,
           NetImages.InviteBg,
           NetImages.Logo,
           data.value.headImgUrl || userInfo.value?.avatar || NetImages.DefaultAvatar,

+ 3 - 4
packages/app/src/pages/mine/levels/index.vue

@@ -10,6 +10,7 @@ import LevelCard from './components/level-card.vue'
 import { getAppMemberLevelConfigs } from '../../../core/libs/requests'
 import { useUserStore } from '../../../store'
 import { storeToRefs } from 'pinia'
+import { sort } from 'radash'
 
 const router = useRouter()
 const userStore = useUserStore()
@@ -60,7 +61,7 @@ onMounted(async () => {
           previous-margin="24"
           next-margin="24"
         >
-          <template v-for="(it, i) in levelConfigs" :key="i">
+          <template v-for="(it, i) in sort(levelConfigs, (it) => it.memberLevel)" :key="i">
             <swiper-item class="">
               <div
                 class="px-1.25 h-full box-border"
@@ -70,9 +71,7 @@ onMounted(async () => {
                   class="w-full h-full bg-[length:100%_100%]"
                   :style="{ backgroundImage: `url(${it.memberBgImage})` }"
                 >
-                  <LevelCard
-                    :is-current="userInfo.level?.level === levelConfigs[i].memberLevel"
-                  ></LevelCard>
+                  <LevelCard :is-current="userInfo.level?.level === it.memberLevel"></LevelCard>
                 </div>
               </div>
             </swiper-item>

+ 2 - 3
packages/app/src/pages/mine/setting/index.vue

@@ -6,6 +6,7 @@ import SectionHeading from '@/components/section-heading.vue'
 import { useUserStore } from '../../../store'
 import { storeToRefs } from 'pinia'
 import { updateMemberUserInfo } from '../../../core/libs/requests'
+import { NetImages } from '../../../core/libs/net-images'
 
 const userStore = useUserStore()
 const { userInfo } = storeToRefs(userStore)
@@ -56,9 +57,7 @@ const handleLogout = () => {
           round
           width="97"
           height="97"
-          :src="
-            (userInfo.avatar ?? '') === '' ? 'https://via.placeholder.com/97x97' : userInfo.avatar
-          "
+          :src="(userInfo.avatar ?? '') === '' ? NetImages.DefaultAvatar : userInfo.avatar"
           custom-class="border border-white border-solid"
         ></wd-img>
       </button>

+ 6 - 0
packages/app/src/pages/publish/moment/index.vue

@@ -17,6 +17,8 @@ import DataForm from '@/components/data-form.vue'
 import { zipToObject } from 'radash'
 import { useRouter } from '../../../core/utils/router'
 import BottomAppBar from '@/components/bottom-app-bar.vue'
+import { toast } from '../../../core/utils/common'
+import { messages } from '../../../core/libs/messages'
 
 const router = useRouter()
 const userStore = useUserStore()
@@ -42,6 +44,10 @@ const handleChange = ({ fileList: files }) => {
 }
 const handleSubmit = async () => {
   publishing.value = true
+  if (!fileList.value.length) {
+    toast(messages.moment.imageNotExist)
+    return false
+  }
   const { code, msg } = await createCircle({
     stylistId: userInfo.value.userId,
     stylistName: userInfo.value.nickname,

+ 1 - 1
packages/app/src/utils/index.ts

@@ -1,5 +1,5 @@
 import { pages, subPackages, tabBar } from '@/pages.json'
-import { path } from 'node:path'
+
 const getLastPage = () => {
   // getCurrentPages() 至少有1个元素,所以不再额外判断
   // const lastPage = getCurrentPages().at(-1)

+ 12 - 0
pnpm-lock.yaml

@@ -95,6 +95,9 @@ importers:
       uqrcodejs:
         specifier: ^4.0.7
         version: 4.0.7
+      validator:
+        specifier: ^13.12.0
+        version: 13.12.0
       vue:
         specifier: ^3.4.21
         version: 3.4.21(typescript@4.9.5)
@@ -2015,6 +2018,7 @@ packages:
   '@esbuild/darwin-arm64@0.20.2':
     resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
     engines: {node: '>=12'}
+    cpu: [arm64]
     os: [darwin]
 
   '@esbuild/darwin-arm64@0.21.5':
@@ -2026,6 +2030,7 @@ packages:
   '@esbuild/darwin-x64@0.20.2':
     resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
     engines: {node: '>=12'}
+    cpu: [x64]
     os: [darwin]
 
   '@esbuild/darwin-x64@0.21.5':
@@ -2799,6 +2804,7 @@ packages:
 
   '@rollup/rollup-darwin-x64@4.24.0':
     resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==}
+    cpu: [x64]
     os: [darwin]
 
   '@rollup/rollup-linux-arm-gnueabihf@4.24.0':
@@ -8474,6 +8480,10 @@ packages:
   validate-npm-package-license@3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
+  validator@13.12.0:
+    resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
+    engines: {node: '>= 0.10'}
+
   vary@1.1.2:
     resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
     engines: {node: '>= 0.8'}
@@ -19249,6 +19259,8 @@ snapshots:
       spdx-correct: 3.2.0
       spdx-expression-parse: 3.0.1
 
+  validator@13.12.0: {}
+
   vary@1.1.2: {}
 
   verror@1.10.0: