Commit dedb7b4a by lijiabin

【需求 17679】 feat: 完善问吧、 scroll等

parent 82eec017
......@@ -13,7 +13,6 @@ const locale = ref(zhCn)
// userStore.fetchUserInfo().then((res) => {
// console.log(res)
// })
console.log('App.vue mounted')
onMounted(() => {
setTimeout(() => {
initWxConfig()
......
......@@ -147,6 +147,7 @@ export interface ArticleItemDto {
showComment?: boolean
relateColumn?: string
rewardNum: number
isExpand: boolean
}
/**
......
......@@ -3,7 +3,6 @@ import type { BackendServicePageResult, PageSearchParams } from '@/utils/request
import type {
ExchangeGoodsParams,
ExchangeGoodsRecordItemDto,
ShopItemDto,
ShopSearchParams,
YaBiData,
ExchangeYabiRecordItemDto,
......
......@@ -254,7 +254,7 @@
</div>
</div>
<div class="px-4">
<el-divider class="my-1" />
<el-divider class="my-2!" />
</div>
</div>
<!-- 底部分页 -->
......@@ -289,6 +289,10 @@ const { id, defaultSize = 10 } = defineProps<{
defaultSize?: number
}>()
const emit = defineEmits<{
(e: 'commentSuccess'): void
}>()
const total = defineModel<number>('total', { required: true, default: 0 })
const userStore = useUserStore()
......@@ -393,6 +397,7 @@ const handleMyComment = async () => {
refresh()
myComment.value = ''
total.value++
emit('commentSuccess')
}
const handleComment = async (index: number) => {
......@@ -405,9 +410,9 @@ const handleComment = async (index: number) => {
comment.value = ''
total.value++
handleBackTopChildren(index)
// 只需要刷新当前的评论
search()
emit('commentSuccess')
}
// 展开回复 获取子评论列表
......
......@@ -227,11 +227,11 @@ const handleDeleteImg = (img: string) => {
const validateForm = () => {
if (!form.value.title) {
ElMessage.error('请输入实践标题')
ElMessage.error('请输入标题')
return false
}
if (!form.value.content) {
ElMessage.error('请输入实践内容')
ElMessage.error('请输入内容')
return false
}
if (!form.value.mainTagId) {
......
......@@ -26,9 +26,13 @@ export const useScrollTop = (
) => {
const { compatFixedHeader = true } = options
const handleBackTop = (currentIndex: number = 0) => {
const handleBackTop = (currentIndex: number = 0): Promise<void> => {
return new Promise((resolve) => {
const initDoms = unref(el)
if (!initDoms) return
if (!initDoms) {
resolve()
return
}
let doms = []
......@@ -38,6 +42,20 @@ export const useScrollTop = (
doms = initDoms
}
const finish = () => {
console.log('scrollend')
resolve()
}
// 手动添加一次scrollend事件
window.addEventListener('scrollend', finish, { once: true })
// 有时候在滚轮就在原位置 不会触发 scrollend事件 所以手动触发一次
setTimeout(() => {
window.removeEventListener('scrollend', finish)
resolve()
}, 1000)
// 下面会触发scrollend事件一次
const dom = doms[currentIndex] as HTMLElement | Window
if (dom instanceof Window) {
window.scrollTo({
......@@ -50,15 +68,17 @@ export const useScrollTop = (
if (compatFixedHeader) {
const top = dom?.getBoundingClientRect?.().top + window.scrollY - 52
window.scrollTo({
top,
top, // 可以设置滚动的距离
behavior: 'smooth',
})
} else {
dom?.scrollIntoView?.({
// 只能滚动到dom的顶部 不能设置滚动的距离
behavior: 'smooth',
block: 'start',
})
}
})
}
return {
......
......@@ -106,13 +106,13 @@ const formatSeconds = computed(() => {
onMounted(async () => {
const { data } = await getTodayOnlineSeconds()
currentSeconds.value = parseInt(data)
currentSeconds.value = data
})
setInterval(() => {
currentSeconds.value++
}, 1000)
setTimeout(async () => {
setInterval(async () => {
heartbeat()
}, 1000 * 30)
</script>
......@@ -74,8 +74,18 @@
<el-dropdown-menu>
<el-dropdown-item :command="ArticleTypeEnum.POST">帖子</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.PRACTICE">实践</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.COLUMN">专栏</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.INTERVIEW">专访</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.VIDEO">视频</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.QUESTION">问吧</el-dropdown-item>
<el-dropdown-item
v-if="userInfo.isOfficialAccount"
:command="ArticleTypeEnum.COLUMN"
>专栏</el-dropdown-item
>
<el-dropdown-item
v-if="userInfo.isOfficialAccount"
:command="ArticleTypeEnum.INTERVIEW"
>专访</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
......@@ -133,9 +143,14 @@ const getSecondLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
return key
}
const handlePost = async (type: string) => {
// router.push(command)
const handlePost = async (type: ArticleTypeEnum) => {
if (type === ArticleTypeEnum.VIDEO) {
router.push('/publishVideo')
} else if (type === ArticleTypeEnum.QUESTION) {
router.push(`/homePage/askTab#tabsRef?t=${Date.now()}`)
} else {
PublishDialogRef.value?.open(type)
}
}
const isDropdownHover = ref(false)
</script>
......
......@@ -22,9 +22,9 @@ export function registerRouterGuards(router: Router) {
const code = parseCode(to.fullPath)
// code是否来自企业微信 1 不是 0 是 2 开发人员登录方式
const isCodeLogin = parseIsCodeLogin()
const cutEmail = parseIsCutEmail()
console.log(code, isCodeLogin, cutEmail)
// const isCodeLogin = parseIsCodeLogin()
// const cutEmail = parseIsCutEmail()
// console.log(code, isCodeLogin, cutEmail)
const userStore = useUserStore()
if (code) {
console.log('code', code)
......@@ -41,4 +41,8 @@ export function registerRouterGuards(router: Router) {
return true
}
})
// router.afterEach((to, from) => {
// console.log('afterEach to', to)
// console.log('afterEach from', from)
// })
}
......@@ -37,6 +37,10 @@ export function clearScrollPosition(path?: string): void {
*/
export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => {
return new Promise((resolve) => {
console.log('触发路由滚动')
// setTimeout(() => {
// console.log(document.querySelector('#tabsRef'))
// }, 1000)
// 1. 如果有浏览器保存的位置(前进/后退),优先使用
if (savedPosition) {
resolve(savedPosition)
......@@ -45,20 +49,26 @@ export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) =>
// 2. 如果有锚点,滚动到锚点
if (to.hash) {
console.log(to.hash)
setTimeout(() => {
resolve({
el: to.hash,
el: to.hash.split('?')[0], //去除?后面的查询字符串
behavior: 'smooth', // 平滑滚动
top: 52,
})
}, 800)
return
}
// 3. 检查是否有保存的滚动位置
const savedScrollY = scrollPositionMap.get(to.fullPath)
if (savedScrollY !== undefined) {
setTimeout(() => {
resolve({
top: savedScrollY,
behavior: 'smooth',
})
}, 300)
return
}
......
......@@ -260,3 +260,6 @@ a {
*/
/* End of reset.css */
body {
font-family: 'Segoe UI Emoji';
}
......@@ -22,7 +22,6 @@ export default class DhRequest {
this.instance.interceptors.request.use(
async (config) => {
const userStore = useUserStore()
console.log(userStore.token)
const token = userStore.token
if (token) {
config.headers.Authorization = token
......
......@@ -13,7 +13,7 @@
<!-- 主要内容区域 -->
<div class="mx-auto py-6">
<PublishBox :type="ArticleTypeEnum.QUESTION" />
<PublishBox :type="ArticleTypeEnum.QUESTION" ref="publishBoxRef" />
<div v-loading="loading" v-if="list.length">
<!-- 问题列表 -->
<div class="space-y-4">
......@@ -38,22 +38,29 @@
/>
<!-- 问题内容 -->
<div>
<p class="text-gray-600 text-base leading-relaxed">
<p
:ref="(e) => (contentRefList[index] = e as HTMLElement)"
class="text-gray-600 text-base leading-relaxed transition-all duration-300"
:class="{ 'line-clamp-3': !item.isExpand }"
>
{{ item.content }}
</p>
<!-- line-clamp-2 -->
<!-- 展开/收起按钮 -->
<!-- <el-button
v-if="item.content.length > 100 && !item.isExpand"
@click="handleReadMore(item)"
<!-- 展开/收起按钮 靠右边布局 -->
<div class="flex justify-end">
<el-button
v-if="isOverThreeLine(index)"
@click="handleExpand(item)"
type="primary"
text
size="small"
class="mt-2 p-0 text-blue-500"
class="text-blue-500"
>
阅读全文
<el-icon class="ml-1"><ArrowDown /></el-icon>
</el-button> -->
{{ item.isExpand ? '收起' : '阅读全文' }}
<el-icon class="ml-1" :class="{ 'rotate-180': item.isExpand }">
<ArrowDown />
</el-icon>
</el-button>
</div>
</div>
</div>
<!-- 底部信息栏 -->
......@@ -73,13 +80,13 @@
}}</span>
<!-- 操作按钮组 -->
<!-- <div class="flex items-center">
<div class="flex items-center">
<el-button size="small" plain>
<el-icon><Plus /></el-icon>
添加
</el-button>
<el-button size="small" plain @click="handleAnswer(index)">
<el-button size="small" plain @click="handleComment(index)">
<el-icon><Edit /></el-icon>
回答
</el-button>
......@@ -93,7 +100,7 @@
<el-icon><Warning /></el-icon>
举报
</el-button>
</div> -->
</div>
</div>
<!-- 右侧:统计信息 -->
......@@ -129,10 +136,11 @@
</div>
<Transition name="fade">
<Comment
v-if="item.showComment"
v-show="item.showComment"
:id="item.id"
:total="item.replyCount"
:defaultSize="5"
@commentSuccess="() => handleCommentSuccess(index)"
/>
</Transition>
</el-card>
......@@ -171,21 +179,19 @@
</div>
</template>
</div>
<el-tour v-model="open">
<el-tour-step :target="publishBoxRef?.$el" placement="right">
<div>在这里发布你的问题</div>
</el-tour-step>
<template #indicators></template>
</el-tour>
</div>
</template>
<script setup lang="ts" name="CultureAsk">
import Tabs from '@/components/common/Tabs'
import {
Plus,
Edit,
Star,
Warning,
View,
ChatDotRound,
ArrowDown,
Refresh,
} from '@element-plus/icons-vue'
import { Star, View, ChatDotRound, Refresh } from '@element-plus/icons-vue'
import Comment from '@/components/common/Comment/index.vue'
import { useScrollTop, usePageSearch } from '@/hooks'
import { TABS_REF_KEY } from '@/constants'
......@@ -193,7 +199,12 @@ import PublishBox from '@/components/common/PublishBox/index.vue'
import { ArticleTypeEnum } from '@/constants'
import dayjs from 'dayjs'
import { getArticleList, addOrCanceArticlelCollect } from '@/api'
import { ArticleItemDto } from '@/api/article/types'
import type { ArticleItemDto } from '@/api/article/types'
const route = useRoute()
const open = ref(false)
const publishBoxRef = useTemplateRef('publishBoxRef')
const activeTab = ref('最新')
const tabs = [
{ label: '最新', value: '最新' },
......@@ -209,6 +220,7 @@ const { list, total, searchParams, loading, refresh } = usePageSearch(getArticle
list.map((item) => ({
...item,
showComment: false,
isExpand: false,
})),
})
......@@ -227,13 +239,8 @@ const handleComment = (index: number) => {
list.value[index]!.showComment = !list.value[index]!.showComment
}
const handleAnswer = (index: number) => {
console.log(index)
}
const handleReadMore = (item: any) => {
// 直接展开
item.isExpand = true
const handleCommentSuccess = (index: number) => {
list.value[index]!.replyCount++
}
const handleRefresh = () => {
......@@ -243,6 +250,34 @@ const handleRefresh = () => {
const handleTabChange = () => {
handleRefresh()
}
const contentRefList = ref<HTMLElement[]>([])
// 检测当前是否超过三行 要用到具体的dom
const isOverThreeLine = (index: number) => {
if (!contentRefList.value[index]) return false
const lineHeight = parseFloat(getComputedStyle(contentRefList.value[index]).lineHeight)
const height = contentRefList.value[index]!.scrollHeight
const maxHeight = lineHeight * 3
return height > maxHeight
}
const handleExpand = (item: ArticleItemDto) => {
item.isExpand = !item.isExpand
}
// 监听路由变化 如果包含#tabsRef 则打开漫游
watch(
() => route.fullPath,
(newVal) => {
if (newVal.includes('#tabsRef')) {
setTimeout(() => {
open.value = true
}, 1500)
}
},
)
</script>
<style lang="scss" scoped>
.fade-enter-from,
......
......@@ -10,7 +10,11 @@
<div class="flex gap-3">
<div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500">
<div ref="tabsRef" class="tabs-container h-75px flex relative rounded-lg mb-3 shadow-md">
<div
id="tabsRef"
ref="tabsRef"
class="tabs-container h-75px flex relative rounded-lg mb-3 shadow-md"
>
<div
v-for="tab in tabs"
:key="tab.path"
......@@ -202,7 +206,7 @@
>
<div class="flex items-center min-w-0 flex-1">
<div class="h-70px flex items-center justify-center">
<svg-icon :name="item.svgName" size="50" />
<svg-icon :name="item.svgName" size="46" />
</div>
<div
class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1"
......@@ -228,7 +232,7 @@
class="w-72px h-32px shadow-[0_1px_8px_0_rgba(255,141,54,0.25)] border-none text-xs sm:text-sm rounded-full"
:class="[
item.currentCount === item.limitCount
? 'bg-#FFC5A1'
? 'bg-#FFC5A1 cursor-not-allowed'
: 'bg-[linear-gradient(to_right,#FFC5A1_0%,#FFB77F_100%)] hover:-translate-y-1 transition-all duration-200 cursor-pointer',
]"
@click="handleTask(item)"
......@@ -255,6 +259,13 @@
</div>
</div>
</div>
<el-tour v-model="open">
<el-tour-step :target="dailySignBtnRef">
<div>签到成功后,可以获得亚币奖励</div>
</el-tour-step>
<template #indicators></template>
</el-tour>
</div>
</template>
<script setup lang="ts">
......@@ -267,21 +278,20 @@ import { getTaskList, dailySign, getCarouselList, getUserAccountData, getRecordD
import { TaskTypeEnum, TaskDateLimitTypeText } from '@/constants'
import type { CarouselItemDto, TaskItemDto, UserAccountDataDto, UserRecordDataDto } from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop, useHintAnimation } from '@/hooks'
import { useScrollTop } from '@/hooks'
const route = useRoute()
const router = useRouter()
const open = ref(false)
const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef')
const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef')
const { handleBackTop } = useScrollTop(levelContainerRef)
const { triggerAnimation } = useHintAnimation(dailySignBtnRef, {
classes: ['scale-bounce', 'highlight', 'shake-y'],
})
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// console.log(route, '三级路由')
return route.fullPath
console.log(route.fullPath, '三级路由')
// console.log(route.path, 11111111111111)
// return route.fullPath // fullpath带有query参数
return route.path
}
const carouselList = ref<CarouselItemDto[]>([])
......@@ -364,21 +374,23 @@ const onDailySign = async () => {
const { data } = await getUserAccountData()
userAccountData.value = data
ElMessage.success('签到成功')
open.value = false
}
const handleTask = (item: TaskItemDto) => {
const handleTask = async (item: TaskItemDto) => {
console.log(item)
if (item.currentCount === item.limitCount) return
// 先暂时写死
if (item.svgName === 'daily_sign') {
//每日签到
handleBackTop()
triggerAnimation()
await handleBackTop()
open.value = true
// triggerAnimation()
} else if (item.svgName === 'valid_comments') {
// 发布评论
ElMessage.info('快去文章评论区去发表评论吧~')
} else if (item.svgName === 'topic_publish') {
router.push('/homePage/askTab')
router.push(`/homePage/askTab#tabsRef?t=${Date.now()}`) // 加一个时间戳 是因为对于不同的时间戳 用的keepalive是 path 都是同一个组件 在组件里面监听watch(()=>to.fullPath) 每次都能监听得到
} else if (item.svgName === 'answer_ask') {
// 回答问题
router.push('/homePage/askTab')
......@@ -417,9 +429,11 @@ const refreshTaskData = async (refreshRecordData = false) => {
specialTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.SPECIAL_TASK)
}
// 刷新任务进度
onActivated(() => {
refreshTaskData(false)
})
onMounted(() => {
initPage()
})
......
......@@ -2,11 +2,11 @@
* 确认兑换商品的弹窗内容
*/
import { ShopGoodsTypeEnum, regionListOptions } from '@/constants'
import type { ExchangeGoodsParams, ShopItemDto } from '@/api'
import type { BackendShopItemDto, ExchangeGoodsParams } from '@/api'
import type { SetupContext } from 'vue'
type ExchangeContentProps = {
item: ShopItemDto
item: BackendShopItemDto
modelValue: ExchangeGoodsParams
}
type ExchangeContentEvents = {
......@@ -90,7 +90,7 @@ export default function ExchangeContent(
ExchangeContent.props = {
item: {
type: Object as PropType<ShopItemDto>,
type: Object as PropType<BackendShopItemDto>,
required: true,
},
modelValue: {
......
<template>
<div class="min-h-screen bg-white">
<div class="max-w-[1440px] mx-auto">
<!-- 顶部积分卡片 -->
<div>
<div class="flex gap-6">
<!-- 左侧:商品列表区域 -->
<div class="flex-1 min-w-0">
<!-- 虚拟装饰区域 -->
<div
class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg p-5 shadow-sm mb-8 border border-purple-100"
>
<div class="flex justify-between items-center flex-wrap gap-4">
<div class="flex items-baseline gap-3">
<span class="text-gray-700 text-base font-medium">当前YA币:</span>
<span class="text-[#8b5cf6] text-4xl font-bold">{{ currentYaBi }}</span>
</div>
<div class="flex gap-3">
<button
class="cursor-pointer px-6 py-2.5 bg-white text-[#8b5cf6] rounded-full text-sm font-medium border-2 border-[#8b5cf6] hover:bg-[#8b5cf6] hover:text-white transition-all duration-300 shadow-sm hover:shadow-md"
@click="onOpenExchangeGoodsRecordDialog"
class="bg-white/90 backdrop-blur-sm rounded-2xl p-6 shadow-lg mb-6 border border-white/60"
>
商品领取列表
</button>
<button
class="cursor-pointer px-6 py-2.5 bg-gradient-to-r from-[#8b5cf6] to-[#6366f1] text-white rounded-full text-sm font-medium hover:shadow-lg transition-all duration-300"
@click="onOpenExchangeYabiRecordDialog"
>
YA币收支记录
</button>
</div>
</div>
</div>
<!-- 虚拟装饰区域 -->
<div class="mb-10">
<div class="flex items-center gap-3 mb-5">
<div class="w-1 h-6 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full"></div>
<div class="flex items-center gap-3 mb-6">
<div
class="w-1.5 h-7 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full shadow-sm"
></div>
<h2 class="text-xl font-bold text-gray-800">虚拟装饰</h2>
</div>
<div
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5"
>
<template v-if="virtualGoodsList.length">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<div
v-for="item in virtualGoodsList"
:key="item.id"
class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1"
class="group bg-white rounded-lg p-4 flex flex-col items-center shadow-md transition-all duration-200 cursor-pointer border border-gray-200 hover:border-blue-300"
@click="onExchangeGoods(item)"
>
<div class="w-24 h-24 mb-3 flex items-center justify-center">
<div
class="w-20 h-20 mb-3 flex items-center justify-center bg-blue-50/50 rounded-lg p-2"
>
<img
:src="item.imageUrl"
alt=""
class="rounded-lg w-full h-full object-contain group-hover:scale-110 transition-transform duration-300"
class="rounded-lg w-full h-full object-contain"
/>
</div>
<div
......@@ -55,17 +36,14 @@
>
{{ item.name }}
</div>
<div
class="bg-gradient-to-r from-pink-500 to-rose-500 text-white text-xs px-4 py-1.5 rounded-full font-medium shadow-sm"
>
{{ item.price }}积分
<div class="bg-blue-600 text-white text-xs px-4 py-1.5 rounded font-medium">
{{ item.price }} 积分
</div>
</div>
</div>
<!-- 分页 -->
<div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<div class="flex justify-end mt-4">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-2">
<el-pagination
size="small"
v-model:current-page="virtualGoodsSearchParams.current"
......@@ -76,49 +54,57 @@
/>
</div>
</div>
</template>
<div v-else class="flex justify-center items-center py-12">
<el-empty description="暂无" />
</div>
</div>
<!-- 实物奖品区域 -->
<div class="mb-10">
<div class="flex items-end gap-3 mb-5">
<div class="w-1 h-6 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full"></div>
<div class="bg-white/90 backdrop-blur-sm rounded-2xl p-6 shadow-lg border border-white/60">
<div class="flex items-center gap-3 mb-6">
<div
class="w-1.5 h-7 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full shadow-sm"
></div>
<h2 class="text-xl font-bold text-gray-800">亚声实物</h2>
</div>
<!-- 地区筛选 -->
<div class="flex gap-3 text-sm mb-6 items-center flex-wrap">
<span class="text-gray-600 font-medium">地区:</span>
<!-- <el-tabs type="card" v-model="realGoodsSearchParams.region" @change="onChangeRegion">
<el-tab-pane
<button
v-for="item in tabs"
:key="item.value"
:label="item.label"
:name="item.value"
/>
</el-tabs> -->
<Tabs
:tabs="tabs"
v-model="realGoodsSearchParams.region"
@change="onChangeRegion"
class="bg-pink-500"
/>
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="onChangeRegion(item.value)"
:class="{
'text-indigo-600 font-medium': realGoodsSearchParams.region === item.value,
'text-gray-600 hover:text-gray-900': realGoodsSearchParams.region !== item.value,
}"
>
{{ item.label }}
<span
v-if="realGoodsSearchParams.region === item.value"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1]"
></span>
</button>
</div>
<div v-show="realGoodsList.length">
<div
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5"
>
<template v-if="realGoodsList.length">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
<div
v-for="item in realGoodsList"
:key="item.id"
class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1"
class="group bg-white rounded-lg p-4 flex flex-col items-center shadow-md transition-all duration-200 cursor-pointer border border-gray-200 hover:border-blue-300"
@click="onExchangeGoods(item)"
>
<div class="w-24 h-24 mb-3 flex items-center justify-center">
<div
class="w-20 h-20 mb-3 flex items-center justify-center bg-blue-50/50 rounded-lg p-2"
>
<img
:src="item.imageUrl"
alt=""
class="rounded-lg w-full h-full object-contain group-hover:scale-110 transition-transform duration-300"
class="rounded-lg w-full h-full object-contain"
/>
</div>
<div
......@@ -126,17 +112,14 @@
>
{{ item.name }}
</div>
<div
class="bg-gradient-to-r from-pink-500 to-rose-500 text-white text-xs px-4 py-1.5 rounded-full font-medium shadow-sm"
>
{{ item.price }}积分
<div class="bg-blue-600 text-white text-xs px-4 py-1.5 rounded font-medium">
{{ item.price }} 积分
</div>
</div>
</div>
<!-- 分页 -->
<div class="flex justify-end mt-6">
<div class="bg-gray-50 rounded-lg shadow-sm border border-gray-200 p-3">
<div class="flex justify-end mt-4">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-2">
<el-pagination
v-model:current-page="realGoodsSearchParams.current"
v-model:page-size="realGoodsSearchParams.size"
......@@ -146,40 +129,93 @@
/>
</div>
</div>
</template>
<div v-else class="flex justify-center items-center py-12">
<el-empty description="暂无" />
</div>
<!-- 空状态 -->
<div v-show="!realGoodsList.length" class="flex justify-center items-center py-20">
<el-empty description="暂无数据" />
</div>
</div>
<!-- 底部提示 -->
<div class="mt-16 text-center">
<!-- 右侧:信息面板 -->
<div class="w-80 flex-shrink-0 space-y-6">
<!-- YA币信息卡片 -->
<div
class="inline-flex items-center gap-2 bg-blue-50 border border-blue-200 rounded-full px-6 py-3 text-sm text-gray-600"
class="bg-white/90 backdrop-blur-sm rounded-2xl p-6 shadow-lg border border-white/60 sticky top-[52px]"
>
<div class="text-center mb-6">
<div class="text-gray-500 text-sm mb-2">当前YA币</div>
<div
class="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-purple-600 text-5xl font-bold"
>
{{ currentYaBi }}
</div>
</div>
<!-- 操作按钮 -->
<div class="space-y-3 mb-6">
<button
class="cursor-pointer w-full px-4 py-3 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl text-sm font-medium hover:shadow-lg hover:scale-[1.02] transition-all duration-300"
@click="onOpenExchangeYabiRecordDialog"
>
YA币收支记录
</button>
<button
class="cursor-pointer w-full px-4 py-3 bg-white text-gray-700 rounded-xl text-sm font-medium border border-gray-200 hover:border-blue-400 hover:shadow-md transition-all duration-300"
@click="onOpenExchangeGoodsRecordDialog"
>
实物商品兑换后请联系相关负责人领取奖励 —
<span class="text-[#6366f1] cursor-pointer font-medium hover:underline">
联系人对照表
</span>
商品领取列表
</button>
</div>
<!-- 温馨提示 -->
<div class="bg-amber-50 border border-amber-200/60 rounded-xl p-4">
<div class="flex items-start gap-2">
<svg
class="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd"
/>
</svg>
<div class="text-sm text-gray-700">
<div class="font-medium mb-1">温馨提示</div>
<p class="text-xs leading-relaxed">
实物商品兑换后请联系相关负责人领取奖励
<span class="text-blue-600 cursor-pointer hover:underline ml-1">查看联系人</span>
</p>
</div>
</div>
</div>
<!-- 统计信息(可选) -->
<div class="mt-6 pt-6 border-t border-gray-100 space-y-3">
<div class="flex justify-between items-center text-sm">
<span class="text-gray-500">虚拟装饰</span>
<span class="font-medium text-gray-700">{{ virtualGoodsTotal }} 件</span>
</div>
<div class="flex justify-between items-center text-sm">
<span class="text-gray-500">实物商品</span>
<span class="font-medium text-gray-700">{{ realGoodsTotal }} 件</span>
</div>
</div>
</div>
</div>
</div>
<ExchangeGoodsRecordDialog ref="exchangeGoodsRecordDialogRef" />
<ExchangeYabiRecordDialog ref="exchangeYabiRecordDialogRef" />
</div>
</div>
</template>
<script setup lang="tsx">
import { exchangeGoods, getShopItemList, getYaBiData } from '@/api'
import { usePageSearch } from '@/hooks'
import { regionListOptions, ShopGoodsTypeEnum } from '@/constants'
import type { ExchangeGoodsParams, ShopItemDto } from '@/api'
import Tabs from '@/components/common/Tabs'
import type { ExchangeGoodsParams, BackendShopItemDto } from '@/api'
import ExchangeContent from './components/exchangeContent.tsx'
import ExchangeGoodsRecordDialog from './components/exchangeGoodsRecordDilaog.vue'
import ExchangeYabiRecordDialog from './components/exchangeYabiRecordDilaog.vue'
......@@ -221,7 +257,8 @@ const {
},
})
const onChangeRegion = () => {
const onChangeRegion = (region: string) => {
realGoodsSearchParams.value.region = region
realGoodsGoToPage(1)
}
......@@ -241,8 +278,7 @@ const getYaBiDataFn = async () => {
// 兑换商品
const onExchangeGoods = async (item: ShopItemDto) => {
console.log(item)
const onExchangeGoods = async (item: BackendShopItemDto) => {
const form = ref<ExchangeGoodsParams>({
itemId: item.id,
num: 1,
......
......@@ -29,13 +29,15 @@
<div class="reward-character">
<img
v-if="option.amount === 1"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='45' fill='%23e0e0e0'/%3E%3Ctext x='50' y='65' font-size='40' text-anchor='middle' fill='%23666'%3E🪙%3C/text%3E%3C/svg%3E"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='coin1' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23fef3c7'/%3E%3Cstop offset='30%25' style='stop-color:%23fde68a'/%3E%3Cstop offset='70%25' style='stop-color:%23fcd34d'/%3E%3Cstop offset='100%25' style='stop-color:%23f59e0b'/%3E%3C/linearGradient%3E%3CradialGradient id='highlight1' cx='35%25' cy='35%25'%3E%3Cstop offset='0%25' style='stop-color:%23ffffff;stop-opacity:0.9'/%3E%3Cstop offset='50%25' style='stop-color:%23ffffff;stop-opacity:0.3'/%3E%3Cstop offset='100%25' style='stop-color:%23ffffff;stop-opacity:0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='44' fill='none' stroke='%23f59e0b' stroke-width='3' opacity='0.4'/%3E%3Ccircle cx='50' cy='50' r='40' fill='url(%23coin1)' stroke='%23d97706' stroke-width='6'/%3E%3Ccircle cx='50' cy='50' r='33' fill='none' stroke='%23fbbf24' stroke-width='2' opacity='0.6'/%3E%3Ccircle cx='50' cy='50' r='27' fill='none' stroke='%23f59e0b' stroke-width='1.5' opacity='0.5'/%3E%3Cpath d='M50 30 L54 42 L67 42 L56 50 L60 62 L50 54 L40 62 L44 50 L33 42 L46 42 Z' fill='%23d97706' opacity='0.8'/%3E%3Ccircle cx='50' cy='50' r='40' fill='url(%23highlight1)' opacity='0.5'/%3E%3C/svg%3E"
alt="1YA币"
class="w-full h-full"
/>
<!-- 2YA币 - 双枚金币(星形居中版) -->
<img
v-else
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='35' cy='50' r='30' fill='%23ffd700'/%3E%3Ccircle cx='65' cy='50' r='30' fill='%23ffd700'/%3E%3Ctext x='50' y='65' font-size='30' text-anchor='middle' fill='%23fff'%3E🪙%3C/text%3E%3C/svg%3E"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='coin2' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%23fef3c7'/%3E%3Cstop offset='30%25' style='stop-color:%23fde68a'/%3E%3Cstop offset='70%25' style='stop-color:%23fcd34d'/%3E%3Cstop offset='100%25' style='stop-color:%23f59e0b'/%3E%3C/linearGradient%3E%3CradialGradient id='highlight2' cx='35%25' cy='35%25'%3E%3Cstop offset='0%25' style='stop-color:%23ffffff;stop-opacity:0.9'/%3E%3Cstop offset='50%25' style='stop-color:%23ffffff;stop-opacity:0.3'/%3E%3Cstop offset='100%25' style='stop-color:%23ffffff;stop-opacity:0'/%3E%3C/radialGradient%3E%3C/defs%3E%3Cg opacity='0.85'%3E%3Ccircle cx='35' cy='50' r='32' fill='none' stroke='%23f59e0b' stroke-width='2' opacity='0.4'/%3E%3Ccircle cx='35' cy='50' r='29' fill='url(%23coin2)' stroke='%23d97706' stroke-width='4'/%3E%3Ccircle cx='35' cy='50' r='24' fill='none' stroke='%23fbbf24' stroke-width='1.5' opacity='0.6'/%3E%3Ccircle cx='35' cy='50' r='20' fill='none' stroke='%23f59e0b' stroke-width='1' opacity='0.5'/%3E%3Cpath d='M35 33 L37.5 42 L47 42 L39 48 L41.5 57 L35 51 L28.5 57 L31 48 L23 42 L32.5 42 Z' fill='%23d97706' opacity='0.8'/%3E%3Ccircle cx='35' cy='50' r='29' fill='url(%23highlight2)' opacity='0.5'/%3E%3C/g%3E%3Cg%3E%3Ccircle cx='65' cy='50' r='32' fill='none' stroke='%23f59e0b' stroke-width='2' opacity='0.4'/%3E%3Ccircle cx='65' cy='50' r='29' fill='url(%23coin2)' stroke='%23d97706' stroke-width='4'/%3E%3Ccircle cx='65' cy='50' r='24' fill='none' stroke='%23fbbf24' stroke-width='1.5' opacity='0.6'/%3E%3Ccircle cx='65' cy='50' r='20' fill='none' stroke='%23f59e0b' stroke-width='1' opacity='0.5'/%3E%3Cpath d='M65 33 L67.5 42 L77 42 L69 48 L71.5 57 L65 51 L58.5 57 L61 48 L53 42 L62.5 42 Z' fill='%23d97706' opacity='0.8'/%3E%3Ccircle cx='65' cy='50' r='29' fill='url(%23highlight2)' opacity='0.5'/%3E%3C/g%3E%3C/svg%3E"
alt="2YA币"
class="w-full h-full"
/>
......@@ -58,7 +60,7 @@
</template>
<script setup lang="ts">
import { addOrCancelArticleReward, getYaBiData } from '@/api'
const rewardNum = defineModel<number>('rewardNum', { required: true })
interface RewardOption {
amount: number
icon: string
......@@ -109,6 +111,7 @@ const handleConfirm = async () => {
})
ElMessage.success('打赏成功!')
dialogVisible.value = false
rewardNum.value += selectedAmount.value
}
defineExpose({
......
......@@ -92,11 +92,99 @@
<span class="text-base">{{ videoDetail?.replyCount || 0 }}</span>
</el-button>
<!-- 打赏 -->
<el-button text class="flex items-center gap-2 transition-colors" @click="handleReward">
<!-- <el-icon><Star /></el-icon> -->
{{ videoDetail?.rewardNum }}
<span class="text-base"> 打赏</span>
<el-button
text
class="reward-button flex items-center gap-2 px-4 py-2 rounded-lg bg-white/40 hover:bg-white/70 backdrop-blur-sm border border-blue-100/30 hover:border-blue-200/50 transition-all duration-300 hover:scale-105 hover:shadow-lg hover:shadow-blue-100/50"
@click="handleReward"
>
<!-- 金币容器 - 带多重动画 -->
<div class="coin-wrapper relative">
<!-- 改进版金币图标 - 立体金币设计 -->
<svg class="coin-icon" viewBox="0 0 24 24" width="18" height="18">
<defs>
<!-- 金色渐变 -->
<linearGradient id="coinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color: #fef3c7" />
<stop offset="30%" style="stop-color: #fde68a" />
<stop offset="70%" style="stop-color: #fcd34d" />
<stop offset="100%" style="stop-color: #f59e0b" />
</linearGradient>
<!-- 高光效果 -->
<radialGradient id="highlight" cx="35%" cy="35%">
<stop offset="0%" style="stop-color: #ffffff; stop-opacity: 0.9" />
<stop offset="50%" style="stop-color: #ffffff; stop-opacity: 0.3" />
<stop offset="100%" style="stop-color: #ffffff; stop-opacity: 0" />
</radialGradient>
</defs>
<!-- 外圈装饰 -->
<circle
cx="12"
cy="12"
r="10.5"
fill="none"
stroke="#f59e0b"
stroke-width="0.8"
opacity="0.4"
/>
<!-- 金币主体 -->
<circle
cx="12"
cy="12"
r="9.5"
fill="url(#coinGradient)"
stroke="#d97706"
stroke-width="1.5"
/>
<!-- 内圈装饰线 -->
<circle
cx="12"
cy="12"
r="8"
fill="none"
stroke="#fbbf24"
stroke-width="0.5"
opacity="0.6"
/>
<circle
cx="12"
cy="12"
r="6.5"
fill="none"
stroke="#f59e0b"
stroke-width="0.3"
opacity="0.5"
/>
<!-- 星形图案(代替¥符号) -->
<path
d="M12 5 L13 9 L17 9 L14 11.5 L15 15 L12 13 L9 15 L10 11.5 L7 9 L11 9 Z"
fill="#d97706"
opacity="0.8"
/>
<!-- 高光 -->
<circle cx="12" cy="12" r="9.5" fill="url(#highlight)" opacity="0.5" />
</svg>
<!-- 悬停时的光晕效果 -->
<div class="coin-glow absolute inset-0 rounded-full"></div>
<!-- 闪光粒子效果 -->
<div class="sparkle sparkle-1"></div>
<div class="sparkle sparkle-2"></div>
<div class="sparkle sparkle-3"></div>
</div>
<span class="ml-2 reward-number font-medium text-gray-700">{{
videoDetail?.rewardNum
}}</span>
<span class="ml-1 reward-text text-sm text-gray-600">打赏</span>
</el-button>
<!-- 更多 -->
<button
class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-all"
......@@ -133,7 +221,7 @@
</div>
<Comment ref="commentRef" :id="videoId" v-model:total="videoDetail.replyCount" />
<RewardDialog ref="rewardDialogRef" />
<RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
</div>
</template>
......@@ -207,3 +295,181 @@ onMounted(async () => {
videoDetail.value = data
})
</script>
<style scoped>
/* 按钮整体动画 */
/* 保持之前的所有动画样式不变 */
.reward-button {
position: relative;
overflow: visible;
}
.coin-wrapper {
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.coin-icon {
font-size: 88px;
animation: coin-float 2.5s ease-in-out infinite;
transition: all 0.3s ease;
}
.reward-button:hover .coin-icon {
animation:
coin-spin 0.6s ease-in-out,
coin-float 2.5s ease-in-out infinite;
}
@keyframes coin-float {
0%,
100% {
transform: translateY(0px) rotate(0deg);
}
50% {
transform: translateY(-3px) rotate(5deg);
}
}
@keyframes coin-spin {
0% {
transform: rotateY(0deg);
}
100% {
transform: rotateY(360deg);
}
}
.coin-glow {
background: radial-gradient(circle, rgba(251, 191, 36, 0.4) 0%, transparent 70%);
opacity: 0;
transform: scale(0.8);
transition: all 0.3s ease;
}
.reward-button:hover .coin-glow {
opacity: 1;
transform: scale(1.5);
animation: glow-pulse 1.5s ease-in-out infinite;
}
@keyframes glow-pulse {
0%,
100% {
opacity: 0.6;
transform: scale(1.5);
}
50% {
opacity: 1;
transform: scale(1.8);
}
}
.sparkle {
position: absolute;
width: 4px;
height: 4px;
background: #fbbf24;
border-radius: 50%;
opacity: 0;
pointer-events: none;
}
.reward-button:hover .sparkle {
animation: sparkle-burst 0.8s ease-out;
}
.sparkle-1 {
top: -2px;
left: 50%;
animation-delay: 0s;
--tx: 0;
--ty: -10px;
}
.sparkle-2 {
top: 50%;
right: -2px;
animation-delay: 0.15s;
--tx: 10px;
--ty: 0;
}
.sparkle-3 {
bottom: -2px;
left: 50%;
animation-delay: 0.3s;
--tx: 0;
--ty: 10px;
}
@keyframes sparkle-burst {
0% {
opacity: 1;
transform: translate(0, 0) scale(0);
}
50% {
opacity: 1;
}
100% {
opacity: 0;
transform: translate(var(--tx), var(--ty)) scale(1);
}
}
.reward-number {
transition: all 0.2s ease;
}
.reward-button:hover .reward-number {
animation: number-bounce 0.5s ease;
color: #3b82f6;
}
@keyframes number-bounce {
0%,
100% {
transform: translateY(0);
}
25% {
transform: translateY(-3px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-2px);
}
}
.reward-text {
transition: all 0.3s ease;
}
.reward-button:hover .reward-text {
color: #3b82f6;
transform: translateX(2px);
}
.reward-button:active {
transform: scale(0.95);
}
.reward-button:active .coin-icon {
animation: coin-click 0.3s ease;
}
@keyframes coin-click {
0% {
transform: scale(1);
}
50% {
transform: scale(0.85) rotate(15deg);
}
100% {
transform: scale(1) rotate(0deg);
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment