panhui 2 жил өмнө
parent
commit
2ad4049096

+ 2 - 0
package.json

@@ -26,10 +26,12 @@
     "@traptitech/markdown-it-katex": "^3.6.0",
     "@vicons/tabler": "^0.12.0",
     "@vueuse/core": "^9.13.0",
+    "date-fns": "^2.29.3",
     "highlight.js": "^11.7.0",
     "html2canvas": "^1.4.1",
     "katex": "^0.16.4",
     "markdown-it": "^13.0.1",
+    "mitt": "^3.0.0",
     "naive-ui": "^2.34.3",
     "pinia": "^2.0.33",
     "qrcode.vue": "^3.3.4",

+ 12 - 0
src/api/index.ts

@@ -103,4 +103,16 @@ export function fetchPay<T>(openid: string) {
         url: '/weixin/pay',
         data: { openid }
     })
+}
+
+export function fetchMyMember<T>(){
+    return get<T>({
+        url:'/membership/get'
+    })
+}
+
+export function fetchGetMemberships<T>(){
+    return get<Array<T>>({
+        url:'/membership/plans'
+    })
 }

BIN
src/assets/bg_vip.png


+ 32 - 7
src/components/common/UserAvatar.vue

@@ -1,11 +1,15 @@
 <script setup lang="ts">
 import { computed } from 'vue'
-import { NAvatar, avatarProps } from 'naive-ui'
-import { useUserStore } from '@/store'
+import { NAvatar, avatarProps, NButton } from 'naive-ui'
+import { useUserStore, useUserMemberStore } from '@/store'
 import defaultAvatar from '@/assets/avatar.png'
 import { isString } from '@/utils/is'
+import { useBasicLayout } from '@/hooks/useBasicLayout'
+import { useRouter } from 'vue-router'
+import { emitter } from '@/plugins'
 
 const userStore = useUserStore()
+const userMemberStore = useUserMemberStore()
 
 const userInfo = computed(() => userStore.userInfo)
 
@@ -19,6 +23,18 @@ const props = defineProps({
         default: false
     }
 })
+
+const { isMobile } = useBasicLayout()
+const router = useRouter()
+function goVip() {
+    if (isMobile.value) {
+        router.push({
+            name: 'vip'
+        })
+    } else {
+        emitter.emit('changeVipShow', true)
+    }
+}
 </script>
 
 <template>
@@ -31,19 +47,28 @@ const props = defineProps({
                 <NAvatar :size="avatarType" round :src="defaultAvatar" />
             </template>
         </div>
-        <div class="flex-1 min-w-0 ml-2" v-if="!onlyAvatar" :class="[avatarType + '-text']">
-            <h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
+        <div class="flex-1 min-w-0 ml-2 flex-col flex" v-if="!onlyAvatar" :class="[avatarType + '-text']">
+            <h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap text-left leading-6">
                 {{ userInfo.name ?? 'ChenZhaoYu' }}
             </h2>
-            <p class="overflow-hidden text-xs text-ellipsis whitespace-nowrap">
-                <span>2023年05月01日到期</span>
+            <p class="overflow-hidden text-xs text-ellipsis whitespace-nowrap text-left" v-if="userMemberStore.isVip()">
+                <span>{{ userMemberStore.getExpireAt() }}</span>
             </p>
+            <n-button
+                v-else
+                size="small"
+                type="primary"
+                text
+                class="self-start text-left text-xs"
+                style="font-size: 12px"
+                @click.stop="goVip"
+                >开通会员</n-button
+            >
         </div>
     </div>
 </template>
 
 <style lang="less" scoped>
-
 .large-text {
     h2 {
         font-size: 16px;

+ 6 - 3
src/components/common/VipCard.vue

@@ -1,5 +1,5 @@
 <template>
-    <div class="vip-card">
+    <div class="vip-card rounded-[12px] overflow-hidden">
         <img class="vip-bg" src="@/assets/bg_vip.png" alt="" />
 
         <div class="vip-text">
@@ -23,7 +23,8 @@
 </template>
 <script setup lang="ts">
 import { NGradientText } from 'naive-ui'
-import { ref } from 'vue'
+import { useUserMemberStore } from '@/store'
+import { ref, computed } from 'vue'
 
 const emit = defineEmits(['goVip'])
 
@@ -34,7 +35,9 @@ const props = defineProps({
     }
 })
 
-const isVip = ref(false)
+const isVip = computed(() => {
+    return useUserMemberStore().isVip()
+})
 
 function goVip() {
     emit('goVip')

+ 60 - 15
src/components/common/VipPannel.vue

@@ -18,25 +18,41 @@
             </n-grid>
         </n-card>
 
-        <div class="btn-list flex items-align px-5 overflow-x-auto pt-5">
-            <div
-                class="choose-btn w-2/5 flex-shrink-0 flex-col items-center text-center pb-5 pt-4 border rounded-lg"
-                v-for="i in 3"
-                :key="i"
-            >
-                <div class="text-black dark:text-white text-base">3个月</div>
-                <div class="text-3xl font-medium"><span class="text-base">¥</span>399</div>
-            </div>
-        </div>
+        <n-carousel class="px-5 pt-9" :space-between="23" :show-dots="false" slides-per-view="auto" draggable>
+            <n-carousel-item style="width: 40%" v-for="item in memberships" :key="String(item.id)">
+                <div
+                    class="choose-btn flex-shrink-0 flex-col items-center text-center pb-5 pt-4 border rounded-lg"
+                    :class="{ prim: chooseMemberId === item.id }"
+                    @click="chooseMemberId = item.id"
+                >
+                    <div class="text-black dark:text-white text-base">{{ item.name }}</div>
+                    <div class="text-3xl font-medium"><span class="text-base">¥</span>{{ item.price }}</div>
+                </div>
+            </n-carousel-item>
+        </n-carousel>
+
+        <div class="btn-list flex items-align"></div>
 
         <div class="px-6 py-10">
-            <n-button type="primary" size="large" round block>¥399 立即开通</n-button>
+            <n-button type="primary" size="large" round block>¥{{ chooseMemberInfo?.price || 0 }} 立即开通</n-button>
         </div>
     </div>
 </template>
 <script setup lang="ts">
 import { VipCard } from '@/components/common'
-import { NCard, NEl, NPageHeader, NRow, NCol, NGrid, NGridItem, NButton, NIcon } from 'naive-ui'
+import {
+    NCard,
+    NEl,
+    NPageHeader,
+    NRow,
+    NCol,
+    NGrid,
+    NGridItem,
+    NButton,
+    NIcon,
+    NCarousel,
+    NCarouselItem
+} from 'naive-ui'
 import { useRouter } from 'vue-router'
 import imgItem01 from '@/assets/png-01.png'
 import imgQiye from '@/assets/qiye.png'
@@ -46,6 +62,10 @@ import imgItem03 from '@/assets/png-03.png'
 import imgItem05 from '@/assets/png-05.png'
 import imgItem06 from '@/assets/png-06.png'
 import imgItem07 from '@/assets/png-07.png'
+import { fetchGetMemberships } from '@/api'
+import { onMounted, ref, computed } from 'vue'
+import { useUserMemberStore } from '@/store'
+import type { MemberShip } from '@/store/modules/memberShip/helper'
 
 const router = useRouter()
 function handleBack() {
@@ -94,6 +114,27 @@ const items = [
         img: imgItem07
     }
 ]
+
+const chooseMemberId = ref(0)
+const chooseMemberInfo = computed(() => {
+    if (chooseMemberId.value) {
+        return memberships.value.find(item => {
+            return item.id === chooseMemberId.value
+        })
+    }
+    return null
+})
+
+const userMemberStore = useUserMemberStore()
+const memberships = ref<Array<MemberShip>>([])
+onMounted(() => {
+    userMemberStore.fetchMembers().then(res => {
+        memberships.value = res
+        if (res.length > 0) {
+            chooseMemberId.value = res[0].id
+        }
+    })
+})
 </script>
 
 <style lang="less" scoped>
@@ -114,10 +155,14 @@ const items = [
     --n-padding-bottom: 10px !important;
     --n-color-modal: transparent;
 }
-.choose-btn + .choose-btn {
-    margin-left: 23px;
-}
 ::-webkit-scrollbar {
     // display: none;
 }
+
+.choose-btn {
+    &.prim {
+        border-color: #c274fe;
+        background: linear-gradient(#ffbef230, #c274fe30);
+    }
+}
 </style>

+ 4 - 17
src/components/common/minePannel.vue

@@ -37,28 +37,17 @@
         </div>
 
         <share ref="shareRef" />
-
-        <n-modal
-            v-model:show="showVip"
-            :block-scroll="false"
-            transform-origin="center"
-            class="max-w-xl py-3"
-            style="width: 75%"
-        >
-            <n-card :border="false" content-style="padding:0" class="bg-white dark:bg-zinc-800 rounded-lg">
-                <vip-pannel></vip-pannel
-            ></n-card>
-        </n-modal>
     </div>
 </template>
 
 <script setup lang="ts">
-import { UserAvatar, VipCard, Share, VipPannel } from '@/components/common'
+import { UserAvatar, VipCard, Share } from '@/components/common'
 import { NCard, NEl, NPageHeader, NRow, NCol, useMessage, NButton, useDialog, NModal } from 'naive-ui'
 import { useRouter } from 'vue-router'
-import { ref } from 'vue'
+import { ref, onMounted } from 'vue'
 import { useUserStore } from '@/store'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
+import { emitter } from '@/plugins'
 
 const router = useRouter()
 function handleBack() {
@@ -74,7 +63,7 @@ function goVip() {
             name: 'vip'
         })
     } else {
-        showVip.value = true
+        emitter.emit('changeVipShow', true)
     }
 }
 
@@ -99,8 +88,6 @@ function logout() {
         }
     })
 }
-
-const showVip = ref(false)
 </script>
 
 <style lang="less" scoped>

+ 5 - 0
src/plugins/emmiter.ts

@@ -0,0 +1,5 @@
+import mitt from 'mitt'
+
+const emitter = mitt()
+
+export default emitter

+ 2 - 1
src/plugins/index.ts

@@ -1,4 +1,5 @@
 import setupAssets from './assets'
 import setupScrollbarStyle from './scrollbarStyle'
+import emitter from './emmiter'
 
-export { setupAssets, setupScrollbarStyle }
+export { setupAssets, setupScrollbarStyle, emitter }

+ 9 - 1
src/router/permission.ts

@@ -1,10 +1,11 @@
 import { fetchMy } from '@/api'
 import type { Router } from 'vue-router'
-import { useUserStore } from '@/store'
+import { useUserStore, useUserMemberStore } from '@/store'
 
 export function setupPageGuard(router: Router) {
     router.beforeEach(async (to, from, next) => {
         const userStore = useUserStore()
+        const userMemberStore = useUserMemberStore()
         if (to.meta.public) {
             next()
             return
@@ -16,6 +17,13 @@ export function setupPageGuard(router: Router) {
             } catch (error) {
                 next({ name: 'login' })
             }
+        } else if (!userMemberStore.userMember.planId) {
+            try {
+                await userMemberStore.fetchMember()
+                next()
+            } catch (error) {
+                next()
+            }
         } else {
             next()
         }

+ 1 - 0
src/store/modules/index.ts

@@ -4,3 +4,4 @@ export * from './user'
 export * from './prompt'
 export * from './settings'
 export * from './auth'
+export * from './memberShip'

+ 40 - 0
src/store/modules/memberShip/helper.ts

@@ -0,0 +1,40 @@
+import { ss } from '@/utils/storage'
+
+const LOCAL_NAME = 'userMemberStorage'
+
+export interface MemberShip {
+    createdAt: Date
+    duration: number
+    id: number
+    name: String
+    price: number
+}
+
+export interface UserMember {
+    userId?: number | null
+    planId?: number | null
+    expireAt: any
+    createdAt: any
+}
+
+export interface UserMemberState {
+    userMember: UserMember
+}
+
+export function defaultSetting(): UserMemberState {
+    return {
+        userMember: {
+            expireAt: '2023-04-19T09:04:21.332Z',
+            createdAt: '2023-04-19T09:04:21.332Z'
+        }
+    }
+}
+
+export function getLocalState(): UserMemberState {
+    const localSetting: UserMemberState | undefined = ss.get(LOCAL_NAME)
+    return { ...defaultSetting(), ...localSetting }
+}
+
+export function setLocalState(setting: UserMemberState): void {
+    ss.set(LOCAL_NAME, setting)
+}

+ 51 - 0
src/store/modules/memberShip/index.ts

@@ -0,0 +1,51 @@
+import { defineStore } from 'pinia'
+import type { MemberShip, UserMember, UserMemberState } from './helper'
+import { defaultSetting, getLocalState, setLocalState } from './helper'
+import { fetchMyMember, fetchGetMemberships } from '@/api'
+import { isAfter, isEqual, format } from 'date-fns'
+
+export const useUserMemberStore = defineStore('userMember-store', {
+    state: (): UserMemberState => getLocalState(),
+    actions: {
+        setUserMember(userInfo: Partial<UserMember>) {
+            this.userMember = { ...this.userMember, ...userInfo }
+            this.recordState()
+        },
+
+        resetUserMembe() {
+            this.userMember = { ...defaultSetting().userMember }
+            this.recordState()
+        },
+
+        recordState() {
+            setLocalState(this.$state)
+        },
+
+        async fetchMember() {
+            const data = await fetchMyMember<UserMember>()
+            this.setUserMember(data)
+        },
+
+        fetchMembers() {
+            return fetchGetMemberships<MemberShip>()
+        },
+
+        getExpireAt() {
+            if (this.isVip()) {
+                return format(new Date(this.userMember.expireAt), 'yyyy年mm月dd日')
+            } else {
+                return '会员已过期'
+            }
+        },
+
+        isVip() {
+            if (this.userMember.expireAt) {
+                return (
+                    isAfter(new Date(this.userMember.expireAt), new Date()) ||
+                    isEqual(new Date(this.userMember.expireAt), new Date())
+                )
+            }
+            return false
+        }
+    }
+})

+ 2 - 1
src/store/modules/user/index.ts

@@ -3,6 +3,7 @@ import type { UserInfo, UserState } from './helper'
 import { defaultSetting, getLocalState, setLocalState } from './helper'
 import { fetchMy } from '@/api'
 import { useAuthStore } from '../auth'
+import { useUserMemberStore } from '../memberShip'
 
 export const useUserStore = defineStore('user-store', {
     state: (): UserState => getLocalState(),
@@ -24,12 +25,12 @@ export const useUserStore = defineStore('user-store', {
         async fetch() {
             const data = await fetchMy<UserInfo>()
             this.setUserInfo(data)
+            await useUserMemberStore().fetchMember()
         },
 
         logout() {
             this.resetUserInfo()
             useAuthStore().removeToken()
-            
         }
     }
 })

+ 26 - 13
src/views/chat/index.vue

@@ -11,11 +11,12 @@ import { useChat } from './hooks/useChat'
 import { useCopyCode } from './hooks/useCopyCode'
 import { useUsingContext } from './hooks/useUsingContext'
 import HeaderComponent from './components/Header/index.vue'
-import { HoverButton, SvgIcon, MinePannel } from '@/components/common'
+import { HoverButton, SvgIcon, MinePannel, VipPannel } from '@/components/common'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 import { useChatStore, useAppStore, usePromptStore, useAuthStore, AuthState } from '@/store'
 import { fetchChatAPIProcess } from '@/api'
 import { t } from '@/locales'
+import { emitter } from '@/plugins'
 
 let controller = new AbortController()
 
@@ -419,27 +420,33 @@ onMounted(() => {
 
 onUnmounted(() => {
     if (loading.value) controller.abort()
-    appStore.setMinePannel(false)
 })
 
 const appStore = useAppStore()
-const showMine = computed(() => appStore.minePannel)
+const showMine = ref(false)
 function goMine() {
     if (isMobile.value) {
         router.push({
             name: 'mine'
         })
     } else {
-        appStore.setMinePannel(true)
+        showMine.value = true
     }
 }
 
-function onMask() {
-    appStore.setMinePannel(false)
-}
-
 watch(isMobile, val => {
-    appStore.setMinePannel(false)
+    showMine.value = false
+    showVip.value = false
+})
+
+const showVip = ref(false)
+onMounted(() => {
+    emitter.on('changeVipShow', res => {
+        showVip.value = !!res
+    })
+    emitter.on('changeMineShow', res => {
+        showMine.value = !!res
+    })
 })
 </script>
 
@@ -538,16 +545,22 @@ watch(isMobile, val => {
             </div>
         </footer>
 
+        <n-modal v-model:show="showMine" transform-origin="center" class="max-w-xl py-3" style="width: 75%">
+            <n-card :border="false" content-style="padding:0" class="bg-white dark:bg-zinc-800 rounded-lg">
+                <mine-pannel></mine-pannel>
+            </n-card>
+        </n-modal>
+
         <n-modal
-            v-model:show="showMine"
+            v-model:show="showVip"
+            :block-scroll="false"
             transform-origin="center"
             class="max-w-xl py-3"
             style="width: 75%"
-            :on-mask-click="onMask"
         >
             <n-card :border="false" content-style="padding:0" class="bg-white dark:bg-zinc-800 rounded-lg">
-                <mine-pannel></mine-pannel>
-            </n-card>
+                <vip-pannel></vip-pannel
+            ></n-card>
         </n-modal>
     </div>
 </template>

+ 2 - 2
src/views/chat/layout/sider/Footer.vue

@@ -5,6 +5,7 @@ import { NButton } from 'naive-ui'
 import { useRouter } from 'vue-router'
 import { useBasicLayout } from '@/hooks/useBasicLayout'
 import { useAppStore } from '@/store'
+import { emitter } from '@/plugins'
 
 const Setting = defineAsyncComponent(() => import('@/components/common/Setting/SettingIndex.vue'))
 
@@ -19,8 +20,7 @@ function goMine() {
             name: 'mine'
         })
     } else {
-        console.log('e99')
-        appStore.setMinePannel(true)
+        emitter.emit('changeMineShow', true)
     }
 }
 </script>

+ 7 - 2
yarn.lock

@@ -2614,9 +2614,9 @@ date-fns-tz@^1.3.3:
   resolved "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz"
   integrity sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==
 
-date-fns@^2.28.0:
+date-fns@^2.28.0, date-fns@^2.29.3:
   version "2.29.3"
-  resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz"
+  resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
   integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
 
 de-indent@^1.0.2:
@@ -4541,6 +4541,11 @@ minipass@^5.0.0:
   resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz"
   integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
 
+mitt@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
+  integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
+
 ms@2.1.2, ms@^2.1.1:
   version "2.1.2"
   resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"