Commit dedb7b4a by lijiabin

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

parent 82eec017
...@@ -13,7 +13,6 @@ const locale = ref(zhCn) ...@@ -13,7 +13,6 @@ const locale = ref(zhCn)
// userStore.fetchUserInfo().then((res) => { // userStore.fetchUserInfo().then((res) => {
// console.log(res) // console.log(res)
// }) // })
console.log('App.vue mounted')
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
initWxConfig() initWxConfig()
......
...@@ -147,6 +147,7 @@ export interface ArticleItemDto { ...@@ -147,6 +147,7 @@ export interface ArticleItemDto {
showComment?: boolean showComment?: boolean
relateColumn?: string relateColumn?: string
rewardNum: number rewardNum: number
isExpand: boolean
} }
/** /**
......
...@@ -3,7 +3,6 @@ import type { BackendServicePageResult, PageSearchParams } from '@/utils/request ...@@ -3,7 +3,6 @@ import type { BackendServicePageResult, PageSearchParams } from '@/utils/request
import type { import type {
ExchangeGoodsParams, ExchangeGoodsParams,
ExchangeGoodsRecordItemDto, ExchangeGoodsRecordItemDto,
ShopItemDto,
ShopSearchParams, ShopSearchParams,
YaBiData, YaBiData,
ExchangeYabiRecordItemDto, ExchangeYabiRecordItemDto,
......
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
</div> </div>
</div> </div>
<div class="px-4"> <div class="px-4">
<el-divider class="my-1" /> <el-divider class="my-2!" />
</div> </div>
</div> </div>
<!-- 底部分页 --> <!-- 底部分页 -->
...@@ -289,6 +289,10 @@ const { id, defaultSize = 10 } = defineProps<{ ...@@ -289,6 +289,10 @@ const { id, defaultSize = 10 } = defineProps<{
defaultSize?: number defaultSize?: number
}>() }>()
const emit = defineEmits<{
(e: 'commentSuccess'): void
}>()
const total = defineModel<number>('total', { required: true, default: 0 }) const total = defineModel<number>('total', { required: true, default: 0 })
const userStore = useUserStore() const userStore = useUserStore()
...@@ -393,6 +397,7 @@ const handleMyComment = async () => { ...@@ -393,6 +397,7 @@ const handleMyComment = async () => {
refresh() refresh()
myComment.value = '' myComment.value = ''
total.value++ total.value++
emit('commentSuccess')
} }
const handleComment = async (index: number) => { const handleComment = async (index: number) => {
...@@ -405,9 +410,9 @@ const handleComment = async (index: number) => { ...@@ -405,9 +410,9 @@ const handleComment = async (index: number) => {
comment.value = '' comment.value = ''
total.value++ total.value++
handleBackTopChildren(index) handleBackTopChildren(index)
// 只需要刷新当前的评论 // 只需要刷新当前的评论
search() search()
emit('commentSuccess')
} }
// 展开回复 获取子评论列表 // 展开回复 获取子评论列表
......
...@@ -227,11 +227,11 @@ const handleDeleteImg = (img: string) => { ...@@ -227,11 +227,11 @@ const handleDeleteImg = (img: string) => {
const validateForm = () => { const validateForm = () => {
if (!form.value.title) { if (!form.value.title) {
ElMessage.error('请输入实践标题') ElMessage.error('请输入标题')
return false return false
} }
if (!form.value.content) { if (!form.value.content) {
ElMessage.error('请输入实践内容') ElMessage.error('请输入内容')
return false return false
} }
if (!form.value.mainTagId) { if (!form.value.mainTagId) {
......
...@@ -26,9 +26,13 @@ export const useScrollTop = ( ...@@ -26,9 +26,13 @@ export const useScrollTop = (
) => { ) => {
const { compatFixedHeader = true } = options const { compatFixedHeader = true } = options
const handleBackTop = (currentIndex: number = 0) => { const handleBackTop = (currentIndex: number = 0): Promise<void> => {
return new Promise((resolve) => {
const initDoms = unref(el) const initDoms = unref(el)
if (!initDoms) return if (!initDoms) {
resolve()
return
}
let doms = [] let doms = []
...@@ -38,6 +42,20 @@ export const useScrollTop = ( ...@@ -38,6 +42,20 @@ export const useScrollTop = (
doms = initDoms 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 const dom = doms[currentIndex] as HTMLElement | Window
if (dom instanceof Window) { if (dom instanceof Window) {
window.scrollTo({ window.scrollTo({
...@@ -50,15 +68,17 @@ export const useScrollTop = ( ...@@ -50,15 +68,17 @@ export const useScrollTop = (
if (compatFixedHeader) { if (compatFixedHeader) {
const top = dom?.getBoundingClientRect?.().top + window.scrollY - 52 const top = dom?.getBoundingClientRect?.().top + window.scrollY - 52
window.scrollTo({ window.scrollTo({
top, top, // 可以设置滚动的距离
behavior: 'smooth', behavior: 'smooth',
}) })
} else { } else {
dom?.scrollIntoView?.({ dom?.scrollIntoView?.({
// 只能滚动到dom的顶部 不能设置滚动的距离
behavior: 'smooth', behavior: 'smooth',
block: 'start', block: 'start',
}) })
} }
})
} }
return { return {
......
...@@ -106,13 +106,13 @@ const formatSeconds = computed(() => { ...@@ -106,13 +106,13 @@ const formatSeconds = computed(() => {
onMounted(async () => { onMounted(async () => {
const { data } = await getTodayOnlineSeconds() const { data } = await getTodayOnlineSeconds()
currentSeconds.value = parseInt(data) currentSeconds.value = data
}) })
setInterval(() => { setInterval(() => {
currentSeconds.value++ currentSeconds.value++
}, 1000) }, 1000)
setTimeout(async () => { setInterval(async () => {
heartbeat() heartbeat()
}, 1000 * 30) }, 1000 * 30)
</script> </script>
...@@ -74,8 +74,18 @@ ...@@ -74,8 +74,18 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="ArticleTypeEnum.POST">帖子</el-dropdown-item> <el-dropdown-item :command="ArticleTypeEnum.POST">帖子</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.PRACTICE">实践</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.VIDEO">视频</el-dropdown-item>
<el-dropdown-item :command="ArticleTypeEnum.INTERVIEW">专访</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> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
...@@ -133,9 +143,14 @@ const getSecondLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => { ...@@ -133,9 +143,14 @@ const getSecondLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
return key return key
} }
const handlePost = async (type: string) => { const handlePost = async (type: ArticleTypeEnum) => {
// router.push(command) 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) PublishDialogRef.value?.open(type)
}
} }
const isDropdownHover = ref(false) const isDropdownHover = ref(false)
</script> </script>
......
...@@ -22,9 +22,9 @@ export function registerRouterGuards(router: Router) { ...@@ -22,9 +22,9 @@ export function registerRouterGuards(router: Router) {
const code = parseCode(to.fullPath) const code = parseCode(to.fullPath)
// code是否来自企业微信 1 不是 0 是 2 开发人员登录方式 // code是否来自企业微信 1 不是 0 是 2 开发人员登录方式
const isCodeLogin = parseIsCodeLogin() // const isCodeLogin = parseIsCodeLogin()
const cutEmail = parseIsCutEmail() // const cutEmail = parseIsCutEmail()
console.log(code, isCodeLogin, cutEmail) // console.log(code, isCodeLogin, cutEmail)
const userStore = useUserStore() const userStore = useUserStore()
if (code) { if (code) {
console.log('code', code) console.log('code', code)
...@@ -41,4 +41,8 @@ export function registerRouterGuards(router: Router) { ...@@ -41,4 +41,8 @@ export function registerRouterGuards(router: Router) {
return true 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 { ...@@ -37,6 +37,10 @@ export function clearScrollPosition(path?: string): void {
*/ */
export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => { export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => {
return new Promise((resolve) => { return new Promise((resolve) => {
console.log('触发路由滚动')
// setTimeout(() => {
// console.log(document.querySelector('#tabsRef'))
// }, 1000)
// 1. 如果有浏览器保存的位置(前进/后退),优先使用 // 1. 如果有浏览器保存的位置(前进/后退),优先使用
if (savedPosition) { if (savedPosition) {
resolve(savedPosition) resolve(savedPosition)
...@@ -45,20 +49,26 @@ export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => ...@@ -45,20 +49,26 @@ export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) =>
// 2. 如果有锚点,滚动到锚点 // 2. 如果有锚点,滚动到锚点
if (to.hash) { if (to.hash) {
console.log(to.hash)
setTimeout(() => {
resolve({ resolve({
el: to.hash, el: to.hash.split('?')[0], //去除?后面的查询字符串
behavior: 'smooth', // 平滑滚动 behavior: 'smooth', // 平滑滚动
top: 52,
}) })
}, 800)
return return
} }
// 3. 检查是否有保存的滚动位置 // 3. 检查是否有保存的滚动位置
const savedScrollY = scrollPositionMap.get(to.fullPath) const savedScrollY = scrollPositionMap.get(to.fullPath)
if (savedScrollY !== undefined) { if (savedScrollY !== undefined) {
setTimeout(() => {
resolve({ resolve({
top: savedScrollY, top: savedScrollY,
behavior: 'smooth', behavior: 'smooth',
}) })
}, 300)
return return
} }
......
...@@ -260,3 +260,6 @@ a { ...@@ -260,3 +260,6 @@ a {
*/ */
/* End of reset.css */ /* End of reset.css */
body {
font-family: 'Segoe UI Emoji';
}
...@@ -22,7 +22,6 @@ export default class DhRequest { ...@@ -22,7 +22,6 @@ export default class DhRequest {
this.instance.interceptors.request.use( this.instance.interceptors.request.use(
async (config) => { async (config) => {
const userStore = useUserStore() const userStore = useUserStore()
console.log(userStore.token)
const token = userStore.token const token = userStore.token
if (token) { if (token) {
config.headers.Authorization = token config.headers.Authorization = token
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<!-- 主要内容区域 --> <!-- 主要内容区域 -->
<div class="mx-auto py-6"> <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 v-loading="loading" v-if="list.length">
<!-- 问题列表 --> <!-- 问题列表 -->
<div class="space-y-4"> <div class="space-y-4">
...@@ -38,22 +38,29 @@ ...@@ -38,22 +38,29 @@
/> />
<!-- 问题内容 --> <!-- 问题内容 -->
<div> <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 }} {{ item.content }}
</p> </p>
<!-- line-clamp-2 --> <!-- 展开/收起按钮 靠右边布局 -->
<!-- 展开/收起按钮 --> <div class="flex justify-end">
<!-- <el-button <el-button
v-if="item.content.length > 100 && !item.isExpand" v-if="isOverThreeLine(index)"
@click="handleReadMore(item)" @click="handleExpand(item)"
type="primary" type="primary"
text text
size="small" size="small"
class="mt-2 p-0 text-blue-500" class="text-blue-500"
> >
阅读全文 {{ item.isExpand ? '收起' : '阅读全文' }}
<el-icon class="ml-1"><ArrowDown /></el-icon> <el-icon class="ml-1" :class="{ 'rotate-180': item.isExpand }">
</el-button> --> <ArrowDown />
</el-icon>
</el-button>
</div>
</div> </div>
</div> </div>
<!-- 底部信息栏 --> <!-- 底部信息栏 -->
...@@ -73,13 +80,13 @@ ...@@ -73,13 +80,13 @@
}}</span> }}</span>
<!-- 操作按钮组 --> <!-- 操作按钮组 -->
<!-- <div class="flex items-center"> <div class="flex items-center">
<el-button size="small" plain> <el-button size="small" plain>
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
添加 添加
</el-button> </el-button>
<el-button size="small" plain @click="handleAnswer(index)"> <el-button size="small" plain @click="handleComment(index)">
<el-icon><Edit /></el-icon> <el-icon><Edit /></el-icon>
回答 回答
</el-button> </el-button>
...@@ -93,7 +100,7 @@ ...@@ -93,7 +100,7 @@
<el-icon><Warning /></el-icon> <el-icon><Warning /></el-icon>
举报 举报
</el-button> </el-button>
</div> --> </div>
</div> </div>
<!-- 右侧:统计信息 --> <!-- 右侧:统计信息 -->
...@@ -129,10 +136,11 @@ ...@@ -129,10 +136,11 @@
</div> </div>
<Transition name="fade"> <Transition name="fade">
<Comment <Comment
v-if="item.showComment" v-show="item.showComment"
:id="item.id" :id="item.id"
:total="item.replyCount" :total="item.replyCount"
:defaultSize="5" :defaultSize="5"
@commentSuccess="() => handleCommentSuccess(index)"
/> />
</Transition> </Transition>
</el-card> </el-card>
...@@ -171,21 +179,19 @@ ...@@ -171,21 +179,19 @@
</div> </div>
</template> </template>
</div> </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> </div>
</template> </template>
<script setup lang="ts" name="CultureAsk"> <script setup lang="ts" name="CultureAsk">
import Tabs from '@/components/common/Tabs' import Tabs from '@/components/common/Tabs'
import { import { Star, View, ChatDotRound, Refresh } from '@element-plus/icons-vue'
Plus,
Edit,
Star,
Warning,
View,
ChatDotRound,
ArrowDown,
Refresh,
} from '@element-plus/icons-vue'
import Comment from '@/components/common/Comment/index.vue' import Comment from '@/components/common/Comment/index.vue'
import { useScrollTop, usePageSearch } from '@/hooks' import { useScrollTop, usePageSearch } from '@/hooks'
import { TABS_REF_KEY } from '@/constants' import { TABS_REF_KEY } from '@/constants'
...@@ -193,7 +199,12 @@ import PublishBox from '@/components/common/PublishBox/index.vue' ...@@ -193,7 +199,12 @@ import PublishBox from '@/components/common/PublishBox/index.vue'
import { ArticleTypeEnum } from '@/constants' import { ArticleTypeEnum } from '@/constants'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { getArticleList, addOrCanceArticlelCollect } from '@/api' 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 activeTab = ref('最新')
const tabs = [ const tabs = [
{ label: '最新', value: '最新' }, { label: '最新', value: '最新' },
...@@ -209,6 +220,7 @@ const { list, total, searchParams, loading, refresh } = usePageSearch(getArticle ...@@ -209,6 +220,7 @@ const { list, total, searchParams, loading, refresh } = usePageSearch(getArticle
list.map((item) => ({ list.map((item) => ({
...item, ...item,
showComment: false, showComment: false,
isExpand: false,
})), })),
}) })
...@@ -227,13 +239,8 @@ const handleComment = (index: number) => { ...@@ -227,13 +239,8 @@ const handleComment = (index: number) => {
list.value[index]!.showComment = !list.value[index]!.showComment list.value[index]!.showComment = !list.value[index]!.showComment
} }
const handleAnswer = (index: number) => { const handleCommentSuccess = (index: number) => {
console.log(index) list.value[index]!.replyCount++
}
const handleReadMore = (item: any) => {
// 直接展开
item.isExpand = true
} }
const handleRefresh = () => { const handleRefresh = () => {
...@@ -243,6 +250,34 @@ const handleRefresh = () => { ...@@ -243,6 +250,34 @@ const handleRefresh = () => {
const handleTabChange = () => { const handleTabChange = () => {
handleRefresh() 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.fade-enter-from, .fade-enter-from,
......
...@@ -10,7 +10,11 @@ ...@@ -10,7 +10,11 @@
<div class="flex gap-3"> <div class="flex gap-3">
<div class="left flex-1 basis-full xl:basis-3/4 transition-all duration-500"> <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 <div
v-for="tab in tabs" v-for="tab in tabs"
:key="tab.path" :key="tab.path"
...@@ -202,7 +206,7 @@ ...@@ -202,7 +206,7 @@
> >
<div class="flex items-center min-w-0 flex-1"> <div class="flex items-center min-w-0 flex-1">
<div class="h-70px flex items-center justify-center"> <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>
<div <div
class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1" class="flex flex-col items-start justify-center ml-2 sm:ml-3 min-w-0 flex-1"
...@@ -228,7 +232,7 @@ ...@@ -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="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="[ :class="[
item.currentCount === item.limitCount 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', : 'bg-[linear-gradient(to_right,#FFC5A1_0%,#FFB77F_100%)] hover:-translate-y-1 transition-all duration-200 cursor-pointer',
]" ]"
@click="handleTask(item)" @click="handleTask(item)"
...@@ -255,6 +259,13 @@ ...@@ -255,6 +259,13 @@
</div> </div>
</div> </div>
</div> </div>
<el-tour v-model="open">
<el-tour-step :target="dailySignBtnRef">
<div>签到成功后,可以获得亚币奖励</div>
</el-tour-step>
<template #indicators></template>
</el-tour>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
...@@ -267,21 +278,20 @@ import { getTaskList, dailySign, getCarouselList, getUserAccountData, getRecordD ...@@ -267,21 +278,20 @@ import { getTaskList, dailySign, getCarouselList, getUserAccountData, getRecordD
import { TaskTypeEnum, TaskDateLimitTypeText } from '@/constants' import { TaskTypeEnum, TaskDateLimitTypeText } from '@/constants'
import type { CarouselItemDto, TaskItemDto, UserAccountDataDto, UserRecordDataDto } from '@/api' import type { CarouselItemDto, TaskItemDto, UserAccountDataDto, UserRecordDataDto } from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants' import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop, useHintAnimation } from '@/hooks' import { useScrollTop } from '@/hooks'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const open = ref(false)
const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef') const levelContainerRef = useTemplateRef<HTMLElement>('levelContainerRef')
const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef') const dailySignBtnRef = useTemplateRef<HTMLElement>('dailySignBtnRef')
const { handleBackTop } = useScrollTop(levelContainerRef) const { handleBackTop } = useScrollTop(levelContainerRef)
const { triggerAnimation } = useHintAnimation(dailySignBtnRef, {
classes: ['scale-bounce', 'highlight', 'shake-y'],
})
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => { const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// console.log(route, '三级路由') console.log(route.fullPath, '三级路由')
return route.fullPath // console.log(route.path, 11111111111111)
// return route.fullPath // fullpath带有query参数
return route.path
} }
const carouselList = ref<CarouselItemDto[]>([]) const carouselList = ref<CarouselItemDto[]>([])
...@@ -364,21 +374,23 @@ const onDailySign = async () => { ...@@ -364,21 +374,23 @@ const onDailySign = async () => {
const { data } = await getUserAccountData() const { data } = await getUserAccountData()
userAccountData.value = data userAccountData.value = data
ElMessage.success('签到成功') ElMessage.success('签到成功')
open.value = false
} }
const handleTask = (item: TaskItemDto) => { const handleTask = async (item: TaskItemDto) => {
console.log(item) console.log(item)
if (item.currentCount === item.limitCount) return if (item.currentCount === item.limitCount) return
// 先暂时写死 // 先暂时写死
if (item.svgName === 'daily_sign') { if (item.svgName === 'daily_sign') {
//每日签到 //每日签到
handleBackTop() await handleBackTop()
triggerAnimation() open.value = true
// triggerAnimation()
} else if (item.svgName === 'valid_comments') { } else if (item.svgName === 'valid_comments') {
// 发布评论 // 发布评论
ElMessage.info('快去文章评论区去发表评论吧~') ElMessage.info('快去文章评论区去发表评论吧~')
} else if (item.svgName === 'topic_publish') { } 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') { } else if (item.svgName === 'answer_ask') {
// 回答问题 // 回答问题
router.push('/homePage/askTab') router.push('/homePage/askTab')
...@@ -417,9 +429,11 @@ const refreshTaskData = async (refreshRecordData = false) => { ...@@ -417,9 +429,11 @@ const refreshTaskData = async (refreshRecordData = false) => {
specialTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.SPECIAL_TASK) specialTaskList.value = data.filter((item) => item.taskType === TaskTypeEnum.SPECIAL_TASK)
} }
// 刷新任务进度
onActivated(() => { onActivated(() => {
refreshTaskData(false) refreshTaskData(false)
}) })
onMounted(() => { onMounted(() => {
initPage() initPage()
}) })
......
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
* 确认兑换商品的弹窗内容 * 确认兑换商品的弹窗内容
*/ */
import { ShopGoodsTypeEnum, regionListOptions } from '@/constants' import { ShopGoodsTypeEnum, regionListOptions } from '@/constants'
import type { ExchangeGoodsParams, ShopItemDto } from '@/api' import type { BackendShopItemDto, ExchangeGoodsParams } from '@/api'
import type { SetupContext } from 'vue' import type { SetupContext } from 'vue'
type ExchangeContentProps = { type ExchangeContentProps = {
item: ShopItemDto item: BackendShopItemDto
modelValue: ExchangeGoodsParams modelValue: ExchangeGoodsParams
} }
type ExchangeContentEvents = { type ExchangeContentEvents = {
...@@ -90,7 +90,7 @@ export default function ExchangeContent( ...@@ -90,7 +90,7 @@ export default function ExchangeContent(
ExchangeContent.props = { ExchangeContent.props = {
item: { item: {
type: Object as PropType<ShopItemDto>, type: Object as PropType<BackendShopItemDto>,
required: true, required: true,
}, },
modelValue: { modelValue: {
......
...@@ -29,13 +29,15 @@ ...@@ -29,13 +29,15 @@
<div class="reward-character"> <div class="reward-character">
<img <img
v-if="option.amount === 1" 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币" alt="1YA币"
class="w-full h-full" class="w-full h-full"
/> />
<!-- 2YA币 - 双枚金币(星形居中版) -->
<img <img
v-else 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币" alt="2YA币"
class="w-full h-full" class="w-full h-full"
/> />
...@@ -58,7 +60,7 @@ ...@@ -58,7 +60,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { addOrCancelArticleReward, getYaBiData } from '@/api' import { addOrCancelArticleReward, getYaBiData } from '@/api'
const rewardNum = defineModel<number>('rewardNum', { required: true })
interface RewardOption { interface RewardOption {
amount: number amount: number
icon: string icon: string
...@@ -109,6 +111,7 @@ const handleConfirm = async () => { ...@@ -109,6 +111,7 @@ const handleConfirm = async () => {
}) })
ElMessage.success('打赏成功!') ElMessage.success('打赏成功!')
dialogVisible.value = false dialogVisible.value = false
rewardNum.value += selectedAmount.value
} }
defineExpose({ defineExpose({
......
...@@ -92,11 +92,99 @@ ...@@ -92,11 +92,99 @@
<span class="text-base">{{ videoDetail?.replyCount || 0 }}</span> <span class="text-base">{{ videoDetail?.replyCount || 0 }}</span>
</el-button> </el-button>
<!-- 打赏 --> <!-- 打赏 -->
<el-button text class="flex items-center gap-2 transition-colors" @click="handleReward"> <el-button
<!-- <el-icon><Star /></el-icon> --> text
{{ videoDetail?.rewardNum }} 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"
<span class="text-base"> 打赏</span> @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> </el-button>
<!-- 更多 --> <!-- 更多 -->
<button <button
class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-all" class="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-50 rounded-lg transition-all"
...@@ -133,7 +221,7 @@ ...@@ -133,7 +221,7 @@
</div> </div>
<Comment ref="commentRef" :id="videoId" v-model:total="videoDetail.replyCount" /> <Comment ref="commentRef" :id="videoId" v-model:total="videoDetail.replyCount" />
<RewardDialog ref="rewardDialogRef" /> <RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
</div> </div>
</template> </template>
...@@ -207,3 +295,181 @@ onMounted(async () => { ...@@ -207,3 +295,181 @@ onMounted(async () => {
videoDetail.value = data videoDetail.value = data
}) })
</script> </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