Commit 26f215a7 by 王立鹏

Merge branch 'feature/21096-YAYA文化岛自动获取亚币及碎片功能(包含碎片兑换)' into 'master'

Feature/21096 yaya文化岛自动获取亚币及碎片功能(包含碎片兑换)

See merge request !12
parents cf442883 0df8d442
---
description: UnoCSS 与手写 CSS 的分工——渐变按钮、老 WebView、兜底样式
alwaysApply: true
---
# UnoCSS vs 手写 CSS(本仓库约定)
## 口诀
**「钱在按钮上、字要看得见」** → 关键交互在 `<style>` 里写 **实色兜底 + 渐变**;Uno 优先管布局与次要样式。
## 不要只靠 Uno 写(改用手写 CSS 或 Uno + 手写兜底)
### 1. 渐变背景(尤其主按钮、发表/提交、支付、关闭弹窗)
- Uno 的 `bg-gradient-to-*`、`from-*`、`to-*` 常编译为 **`linear-gradient` + CSS 变量**。
- **旧 WebView / 老内核**可能整段渐变失效 → 背景透明或变白,出现「只剩阴影、按钮像白板」。
- **做法**:在 scoped CSS 中:
- 先 **`background-color`** 用 **hex 实色**兜底;
- 再 **`background-image: -webkit-linear-gradient(...)`** + **`linear-gradient(...)`**(颜色尽量写死 hex,少依赖变量链)。
### 2. 必须「坏了也能用」的控件
- 除渐变外,建议显式:**`color`**、**`border`**、**`-webkit-appearance: none; appearance: none`**(原生 `button` 重置),不指望仅靠原子类扛兼容。
### 3. 强依赖新特性的装饰(老机可降级)
若需兼容旧环境,用手写 **`@supports`** 或单独降级类,不要只靠 Uno 一条类名:
- **`backdrop-filter`**
- **`filter: blur` / `drop-shadow`**(大面积)
- **`min()` / `max()` / `clamp()`** 作为**唯一**尺寸方案(可改为 `width` + `max-width` 兜底)
- **`inset`**(极老可用 `top/right/bottom/left`)
### 4. 动画里的 CSS 变量
- `@keyframes` 中使用 **`var(--x)`**、**`calc(var(--x) + …)`** 在部分老 WebView 可能无效。
- 关键动效可接受老机静止;若必须动,keyframes 内尽量用**固定数值**。
### 5. 多层 `background` 叠加
- 一层不渲染可能导致「整块没底」。
- **至少保留一层 `background-color` 实色**;重要文案区域保证对比度。
## Uno 继续放心用的场景
- 布局:`flex`、`grid`、`gap`、`p`/`m`、`w`/`h`(且不把 `min()` 当唯一依赖时)
- 普通文字色、间距、圆角
- **单色**背景:如 `bg-blue-500`(非渐变变量链)
- 非关键装饰、仅在新浏览器上好看的动效
## 参考实现
- 评论「发表」按钮:`src/components/common/Comment/index.vue` 中 `.comment-publish-btn`
- 全屏奖励 toast 按钮兜底:`src/views/videoDetail/components/rewardFullSetToast.vue` 中 `.continue-btn` 与样式顶部注释
<template>
<Progress />
<Notivue v-slot="item">
<NotivueSwipe :item="item">
<Notification :item="item" :theme="pastelTheme"> </Notification>
......@@ -14,6 +15,7 @@
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { initWxConfig } from '@/utils/wxUtil/initWXConfig'
import { Notivue, NotivueSwipe, Notification, pastelTheme } from 'notivue'
import Progress from '@/components/common/Progress/index.vue'
const locale = ref(zhCn)
......
......@@ -20,6 +20,7 @@ import type {
UpdateArticleRecommendAndSortDto,
} from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
import { SpecificVideoRewardEnum } from '@/constants'
// 文章相关的接口(帖子 视频 实践等)
......@@ -355,3 +356,16 @@ export const getCommentDetail = (id: number) => {
},
})
}
/**
* 获取特定视频观看奖励
*/
export const getSpecificVideoWatchReward = (pageKey: SpecificVideoRewardEnum) => {
return service.request<boolean>({
url: `/api/yaCulture/bossVideo/view`,
method: 'POST',
data: {
pageKey,
},
})
}
......@@ -255,7 +255,6 @@ const { articleDetail } = defineProps<{
articleDetail: ArticleItemDto
isAudit: boolean // 是否是审核页面
}>()
console.log(typeof articleDetail, 111)
const articleType = computed(() => {
return articleTypeListOptions.find((item) => item.value === articleDetail.type)?.label
})
......
......@@ -73,7 +73,8 @@
>
<template #submit>
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
type="button"
class="comment-publish-btn cursor-pointer disabled:opacity-50 px-6 py-2 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!myComment?.trim() || loading"
@click="handleMyComment"
>
......@@ -314,7 +315,8 @@
>
<template #submit>
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
type="button"
class="comment-publish-btn cursor-pointer disabled:opacity-50 px-6 py-2 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!commentToOther.trim() || commentToOtherLoading"
@click="handleComment(index)"
>
......@@ -410,7 +412,6 @@ const isReal = computed(
type === ArticleTypeEnum.QUESTION,
)
const userAvatar = computed(() => {
console.log(isReal.value ? userInfo.value.avatar : userInfo.value.hiddenAvatar)
return isReal.value ? userInfo.value.avatar : userInfo.value.hiddenAvatar
})
......@@ -423,7 +424,6 @@ const { handleBackTop } = useScrollTop(commentRef)
// 回滚到子评论框
const { handleBackTop: handleBackTopChildren } = useScrollTop(commentItemRefList)
console.log(commentId, 'commentId')
const {
list,
searchParams,
......@@ -619,6 +619,28 @@ defineExpose({
})
</script>
<style scoped lang="scss">
/**
* 发表按钮:不用纯 Uno 渐变工具类。
* 旧版内核 / 部分企业微信 WebView 对「依赖 CSS 变量的 linear-gradient」解析失败时,
* 背景会变成透明或无效,原生 button 呈白底,易出现「只剩阴影、几乎看不见按钮」。
* 这里用实色 background-color 作兜底,并写死 hex + -webkit- 前缀渐变。
*/
.comment-publish-btn {
-webkit-appearance: none;
appearance: none;
margin: 0;
border: 1px solid rgba(255, 255, 255, 0.35);
/* 与 Tailwind blue-500 / purple-500 接近的实色兜底 */
background-color: #6366f1;
background-image: -webkit-linear-gradient(left, #3b82f6, #a855f7);
background-image: linear-gradient(to right, #3b82f6, #a855f7);
color: #ffffff;
}
.comment-publish-btn:disabled {
cursor: not-allowed;
}
/* 进入 & 离开公共属性 */
.fadeToComment-enter-active,
.fadeToComment-leave-active {
......
......@@ -80,7 +80,6 @@ const handleSubmit = async () => {
content: commentStr.value,
imgUrl: commentImgStr.value,
})
console.log('评论内容:', commentStr.value)
push.success('评论发表成功')
handleClose()
emit('commentSuccess')
......
......@@ -286,7 +286,6 @@ const open = async () => {
// size: 10,
// articleId: articleId,
// })
// console.log('res', data)
// list.value = data.list
// parentComment.value = item
......@@ -294,12 +293,10 @@ const open = async () => {
// currentInlineReplyId.value = null
// commentStr.value = ''
// bottomCommentContent.value = ''
console.log('pid', pid)
await nextTick()
searchParams.value.pid = pid
search()
const { data } = await getCommentDetail(pid)
console.log('data', data)
parentComment.value = data
visible.value = true
}
......@@ -313,7 +310,6 @@ const handleReplyInline = (item: CommentItemDto, index: number) => {
commentStr.value = '' // Clear previous
imgUrl.value = ''
// 聚焦到输入框
console.log('replyToOtherBoxRefList', replyToOtherBoxRefList.value[index]?.focus)
replyToOtherBoxRefList.value[index]?.focus()
}
}
......
<template>
<Teleport to="body">
<Transition name="confirm" @after-leave="emit('afterLeave')">
<div v-if="visible" class="fixed inset-0 z-3000 flex items-center justify-center p-4">
<!-- Backdrop -->
<div class="confirm-backdrop absolute inset-0 bg-black/20" />
<Transition
name="confirm"
:duration="{ enter: 520, leave: 520 }"
@after-leave="emit('afterLeave')"
>
<div
v-if="visible"
class="confirm-overlay-root fixed inset-0 z-3000 flex items-center justify-center p-4"
>
<!-- 蒙层:企微等 WebView 常不支持 backdrop-filter,必须保证纯色半透明始终可见 -->
<div class="confirm-backdrop absolute inset-0" aria-hidden="true" />
<!-- Card -->
<div
......@@ -134,16 +141,6 @@ const normalizedMousePosition = computed(() => {
}
})
onMounted(() => {
console.log('onMounted')
})
onUpdated(() => {
console.log('onUpdated')
})
onUnmounted(() => {
console.log('onUnmounted')
})
const normalizedMessage = computed(() => {
if (!message) return ''
......@@ -191,32 +188,66 @@ defineExpose({ open, close })
</script>
<style scoped>
/* 背景透明度过渡:打开时 0→1,关闭时 1→0 */
.confirm-enter-active,
.confirm-leave-active {
transition: opacity 0.2s ease;
/* 独立合成层,部分 WebView 下 fixed 遮罩更稳定 */
.confirm-overlay-root {
isolation: isolate;
}
.confirm-enter-from {
opacity: 0;
/* 蒙层淡入淡出(不依赖整层 opacity,避免与卡片动画打架) */
.confirm-enter-active .confirm-backdrop,
.confirm-leave-active .confirm-backdrop {
transition: opacity 0.38s cubic-bezier(0.4, 0, 0.2, 1);
}
.confirm-leave-to {
.confirm-enter-from .confirm-backdrop,
.confirm-leave-to .confirm-backdrop {
opacity: 0;
}
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.confirm-enter-active .confirm-backdrop,
.confirm-leave-active .confirm-backdrop {
transition:
opacity 0.38s cubic-bezier(0.4, 0, 0.2, 1),
backdrop-filter 0.45s cubic-bezier(0.4, 0, 0.2, 1),
-webkit-backdrop-filter 0.45s cubic-bezier(0.4, 0, 0.2, 1);
}
.confirm-enter-from .confirm-backdrop,
.confirm-leave-to .confirm-backdrop {
-webkit-backdrop-filter: blur(0px) saturate(1);
backdrop-filter: blur(0px) saturate(1);
}
}
/* 基础蒙层:无模糊支持时也能明显压暗(企微常见) */
.confirm-backdrop {
background-color: #0003;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
.confirm-backdrop {
/* 有模糊时可略浅一点 */
background-color: #0003;
-webkit-backdrop-filter: blur(10px) saturate(1.12);
backdrop-filter: blur(10px) saturate(1.12);
}
}
/* card 动画 */
.confirm-enter-active .confirm-card {
transition:
transform 0.2s cubic-bezier(0.22, 1.2, 0.36, 1),
opacity 0.2s ease;
transform 0.5s cubic-bezier(0.22, 1.2, 0.36, 1),
opacity 0.5s ease;
}
.confirm-leave-active .confirm-card {
transition:
transform 0.2s ease,
opacity 0.2s ease;
transform 0.5s ease,
opacity 0.5s ease;
}
/* 从点击点展开(先用固定坐标 200px,200px 测试动画是否生效) */
......
<template>
<Teleport to="body">
<!-- 整条(含主条)一起显隐,避免 finish 后先把 visible 关掉但主条仍显示,percent 归零时出现 100→0 的 width 过渡 -->
<div
v-show="pageProgressVisible"
class="pointer-events-none fixed left-0 right-0 top-0 z-[10000] h-3px overflow-hidden"
role="progressbar"
:aria-hidden="!pageProgressVisible"
:aria-valuenow="clamped"
aria-valuemin="0"
aria-valuemax="100"
>
<!-- 轨道微光 -->
<div class="absolute inset-0 bg-gradient-to-b from-white/25 to-transparent opacity-40"></div>
<!-- 主进度条 -->
<div
class="page-progress-bar relative h-full origin-left rounded-r-full shadow-[0_0_14px_rgba(99,102,241,0.55)]"
:style="barStyle"
>
<!-- 流光 -->
<div class="absolute inset-0 overflow-hidden rounded-r-full" aria-hidden="true">
<div
class="page-progress-shimmer absolute inset-y-0 w-[40%] -skew-x-[20deg] bg-gradient-to-r from-transparent via-white/35 to-transparent"
/>
</div>
</div>
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
finishPageProgress,
pageProgressPercent,
pageProgressVisible,
startPageProgress,
} from './usePageProgress'
const clamped = computed(() => Math.min(100, Math.max(0, Math.round(pageProgressPercent.value))))
const barStyle = computed(() => ({
width: `${clamped.value}%`,
transition:
pageProgressPercent.value >= 100
? 'width 0.22s cubic-bezier(0.22, 1, 0.36, 1)'
: 'width 0.2s ease-out',
}))
defineExpose({
start: startPageProgress,
finish: finishPageProgress,
})
</script>
<style scoped>
.page-progress-bar {
background: linear-gradient(90deg, #6366f1 0%, #8b5cf6 45%, #22d3ee 100%);
}
.page-progress-shimmer {
animation: page-progress-shimmer 1.25s ease-in-out infinite;
}
@keyframes page-progress-shimmer {
0% {
transform: translateX(-120%) skewX(-20deg);
}
100% {
transform: translateX(320%) skewX(-20deg);
}
}
</style>
// 直接把变量写在js文件里面 可以在组件中 或者 其他js文件使用
/** 是否显示顶栏 */
export const pageProgressVisible = ref(false)
/** 0 ~ 100 */
export const pageProgressPercent = ref(0)
let fakeTimer: ReturnType<typeof setInterval> | undefined
let hideTimer: ReturnType<typeof setTimeout> | undefined
function clearTimers() {
if (fakeTimer != null) {
clearInterval(fakeTimer)
fakeTimer = undefined
}
if (hideTimer != null) {
clearTimeout(hideTimer)
hideTimer = undefined
}
}
/** 路由开始时调用:显示并开始「假进度」到 ~88% */
export async function startPageProgress() {
clearTimers()
pageProgressVisible.value = true
pageProgressPercent.value = 0
await nextTick()
pageProgressPercent.value = 12
fakeTimer = setInterval(() => {
if (pageProgressPercent.value < 88) {
pageProgressPercent.value += Math.random() * 12 + 3
if (pageProgressPercent.value > 88) pageProgressPercent.value = 88
}
}, 300)
}
/** 路由结束后调用:拉满并淡出 */
export function finishPageProgress() {
clearTimers()
if (!pageProgressVisible.value) return
pageProgressPercent.value = 100
hideTimer = setTimeout(() => {
pageProgressVisible.value = false
pageProgressPercent.value = 0
}, 450)
}
......@@ -32,12 +32,7 @@
<div class="flex gap-3 mb-2 items-start">
<!-- 用户头像 -->
<el-avatar
:size="48"
:src="userAvatar"
class="flex-shrink-0"
@click="() => console.log(form)"
>
<el-avatar :size="48" :src="userAvatar" class="flex-shrink-0">
<el-icon>
<IEpUser />
</el-icon>
......@@ -299,7 +294,6 @@ const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
// resetForm()
forReset()
// form.value.imgUrl = ''
console.log(form.value)
} catch (error) {
console.error(error)
} finally {
......
......@@ -105,7 +105,6 @@ watch(
)
const handleExceed: UploadProps['onExceed'] = (uploadFiles) => {
console.log('uploadFiles', uploadFiles)
if (uploadFiles.length > props.limit) {
push.error(`最多上传 ${props.limit} 个文件`)
return
......@@ -113,7 +112,6 @@ const handleExceed: UploadProps['onExceed'] = (uploadFiles) => {
}
const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) => {
console.log('uploadFiles', uploadFiles)
if (uploadFiles.length > props.limit) {
push.error(`最多上传 ${props.limit} 个文件`)
const index = fileList.value.findIndex((file) => file.uid === uploadFile.uid)
......@@ -134,14 +132,12 @@ const handleChange: UploadProps['onChange'] = async (uploadFile, uploadFiles) =>
const { promise } = uploadFileApi(uploadFile.raw, {
onProgress: (progress) => {
console.log('progress', progress)
uploadPercent.value = progress
},
})
const data = await promise
console.log('data', data)
const url = data.filePath || ''
const name = data.finalName || ''
......
......@@ -166,7 +166,6 @@ const startUpload = async () => {
})
cancelUploadController = cancel
const data = await promise
console.log(data)
// 获取视频元数据
const metadata = await getVideoMetadata(data.filePath)
......
......@@ -151,3 +151,15 @@ export enum GoodsDistributionTypeEnum {
// 竞拍
AUCTION = 'auction',
}
// 特定视频奖励枚举
export enum SpecificVideoRewardEnum {
// QA碎片
QA_CHIP = 'QA_CHIP',
// 初心碎片
ORIGINAL_CHIP = 'ORIGINAL_CHIP',
// 本分&包容碎片
MODEST_CHIP = 'MODEST_CHIP',
// 创新碎片
INNOVATION_CHIP = 'INNOVATION_CHIP',
}
......@@ -6,6 +6,7 @@ import {
UsageStatusEnum,
AuctionStatusEnum,
GoodsDistributionTypeEnum,
SpecificVideoRewardEnum,
} from './enums'
// 地区列表
......@@ -233,3 +234,32 @@ export const goodsDistributionTypeListOptions: {
value: GoodsDistributionTypeEnum.AUCTION,
},
]
// 特定视频奖励列表
export const specificVideoRewardListOptions: {
label: string
value: SpecificVideoRewardEnum
title: string
}[] = [
{
label: 'QA碎片',
value: SpecificVideoRewardEnum.QA_CHIP,
// 匹配的标题
title: '虎虎实锤重磅来袭',
},
{
label: '初心碎片',
value: SpecificVideoRewardEnum.ORIGINAL_CHIP,
title: '跌宕起伏的创业前传',
},
{
label: '本分&包容碎片',
value: SpecificVideoRewardEnum.MODEST_CHIP,
title: '老板的MBTI终于暴露了',
},
{
label: '创新碎片',
value: SpecificVideoRewardEnum.INNOVATION_CHIP,
title: '从诗词歌赋聊到人生哲学',
},
]
......@@ -43,7 +43,6 @@ export const useScrollTop = (
}
const finish = () => {
console.log('scrollend')
resolve()
}
......
......@@ -188,13 +188,10 @@ const getSecondLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// 取第一级作为key homePage
const pathSegments = route.path.split('/').filter(Boolean)
// console.log(route)
// console.log(pathSegments)
// 如果有params 则取params的id
const key = Object.keys(route.params).length
? pathSegments.slice(0, 2).join('/')
: pathSegments.slice(0, 1).join('/')
// console.log(key, '*********************')
return key
}
......
......@@ -3,14 +3,16 @@ import type { Router } from 'vue-router'
import { saveScrollPosition } from './scrollStorage'
import { parseCode } from '@/utils/wxUtil'
import { useUserStore } from '@/stores'
import { finishPageProgress, startPageProgress } from '@/components/common/Progress/usePageProgress'
// 白名单
const WHITE_LIST: string[] = ['/aa']
export function registerRouterGuards(router: Router) {
router.beforeEach(async (to, from) => {
// console.log('to', to)
// console.log('from', from)
if (to.fullPath !== from.fullPath) {
startPageProgress()
}
// 保存当前页面的滚动位置
if (from.fullPath) {
saveScrollPosition(from.fullPath, window.scrollY)
......@@ -24,13 +26,10 @@ export function registerRouterGuards(router: Router) {
// code是否来自企业微信 1 不是 0 是 2 开发人员登录方式
// const isCodeLogin = parseIsCodeLogin()
// const cutEmail = parseIsCutEmail()
// console.log(code, isCodeLogin, cutEmail)
const userStore = useUserStore()
if (code) {
// 如果有token 就不需要重新获取用户信息
// console.log('userStore.token', userStore.token)
// if (!userStore.token || !userStore.refreshToken) {
// console.log('code', code)
await userStore.getUserInfoByCode({
code,
isCodeLogin: 0,
......@@ -44,4 +43,12 @@ export function registerRouterGuards(router: Router) {
return true
}
})
router.afterEach(() => {
finishPageProgress()
})
router.onError(() => {
finishPageProgress()
})
}
......@@ -37,8 +37,6 @@ export function clearScrollPosition(path?: string): void {
*/
export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) => {
return new Promise((resolve) => {
console.log('触发路由滚动')
// 1. 如果有浏览器保存的位置(前进/后退),优先使用
if (savedPosition) {
resolve(savedPosition)
......@@ -47,7 +45,6 @@ export const scrollBehavior: RouterScrollBehavior = (to, from, savedPosition) =>
// 2. 如果有锚点, 约定 默认不滚动 然后在具体的组件里面onActivated里面 或者watch处理滚动逻辑 以及漫游逻辑等
if (to.hash) {
// console.log(to.hash, window.scrollY)
// resolve({
// top: window.scrollY,
// })
......
......@@ -16,7 +16,6 @@ export const useColumnStore = defineStore('column', () => {
try {
const { data } = await getColumnOptions()
columnList.value = data
console.log(columnList.value, 'columnList')
} catch (error) {
console.error(error)
} finally {
......
......@@ -16,7 +16,6 @@ export const useInterviewStore = defineStore('interview', () => {
try {
const { data } = await getInterviewOptions()
interviewList.value = data
console.log(interviewList.value, 'interviewList')
} catch (error) {
console.error(error)
} finally {
......
......@@ -21,11 +21,9 @@ export const useTagsStore = defineStore('tags', () => {
isLoading = true
try {
const { data } = await getTagList()
console.log(data, 'data')
tagList.value = data.filter((i) => i.type === 'culture')
relatedScenariosTagList.value = data.filter((i) => i.type === 'related_scenarios')
yearRecommendTagList.value = data.filter((i) => i.type === 'year_recommend')
console.log(tagList.value, 'tagList')
} catch (error) {
console.error(error)
} finally {
......
......@@ -16,7 +16,6 @@ export const useVideoStore = defineStore('video', () => {
try {
const { data } = await getVideoOptions()
videoList.value = data
console.log(videoList.value, 'videoList')
} catch (error) {
console.error(error)
} finally {
......
......@@ -95,12 +95,10 @@ export function handleRequestError<T>(axiosError: AxiosError<BackendServiceResul
})
} else if (error.code === 401) {
// 处理401的
console.log(error, '这里是401么', axiosError)
// 重新发送一遍请求
// service.request(axiosError.config as AxiosRequestConfig)
return handleUnAuthorized(axiosError)
} else {
console.log(error, axiosError, '这里是其他错误么')
showErrorMsg(error)
}
// 鉴权错误
......
......@@ -2,7 +2,6 @@ import { app_config } from '@/config'
import Axios from './axios'
// 'http://192.168.2.168:8089'
const baseUrl = app_config[import.meta.env.MODE]?.baseUrl
console.log('baseUrl', baseUrl)
export default new Axios({
baseURL: baseUrl,
timeout: 1000 * 60,
......
......@@ -20,7 +20,6 @@ interface IShareWxOption {
// option.link = url + '#' + option.link
// wx.invoke('shareAppMessage', option, function (res: any) {
// if (res.err_msg == 'openExistedChatWithMsg:ok') {
// console.log(res)
// }
// })
// }
......
......@@ -6,7 +6,6 @@ import * as ww from '@wecom/jssdk'
// export async function initWxConfig() {
// const url = location.href.split('#')[0]
// const response = await getWxSignature(url)
// console.log('response', response, wx)
// const timestamp = response.data.timestamp //时间戳
// const nonceStr = response.data.nonceStr //随机字符串
// const signature = response.data.signature //签名
......@@ -31,7 +30,6 @@ import * as ww from '@wecom/jssdk'
export async function initWxConfig() {
const url = location.href.split('#')[0]
const response = await getWxSignature(url!)
// console.log('response', response)
const timestamp = response.data.timestamp //时间戳
const nonceStr = response.data.nonceStr //随机字符串
const signature = response.data.signature //签名
......
......@@ -100,7 +100,6 @@ const timeRange = computed({
}
},
set(value) {
console.log(value)
if (!value) {
searchParams.value.receiveTimeStart = 0
searchParams.value.receiveTimeEnd = 0
......
......@@ -97,8 +97,8 @@ const onBid = async (item: AuctionItemDto) => {
showCancelButton: false,
showConfirmButton: false,
})
const { data } = await getAuctionDetail(item.id)
console.log(data, 'data')
const { data: auctionDetail } = await getAuctionDetail(item.id)
const data = reactive(auctionDetail)
const val = ref(0)
ElMessageBox.confirm('确定参与竞拍吗?', {
confirmButtonText: '确认出价',
......@@ -176,7 +176,9 @@ const onBid = async (item: AuctionItemDto) => {
if (val.value > yabiData.value.currentValue) return push.error('您的YA币余额不足')
if (val.value < data.minIncrement + (data.currentPrice || data.startingPrice))
return push.error('出价必须大于等于前最高出价+最低加价幅度')
return push.error(
`出价必须大于等于前最高出价+最低加价幅度: 至少【${data.minIncrement + (data.currentPrice || data.startingPrice)}】YA币`,
)
instance.confirmButtonLoading = true
instance.confirmButtonText = 'Loading...'
......@@ -184,9 +186,15 @@ const onBid = async (item: AuctionItemDto) => {
await participateAuction(item.id, val.value)
refresh()
yabiStore.fetchYaBiData()
push.success('出价成功')
done()
} catch (error) {
} catch (error: any) {
console.log(error)
if (error.code === 400 && error.message.includes('检测到刚才有人出价')) {
// 重新获取当前竞拍信息
const { data: newAuctionDetail } = await getAuctionDetail(item.id)
data.currentPrice = newAuctionDetail.currentPrice
}
refresh()
} finally {
instance.confirmButtonLoading = false
......
......@@ -101,7 +101,6 @@ const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
// 函数弹窗 v-html
const handleShowContent = (row: ArticleItemDto) => {
console.log(row.content)
ElMessageBox.alert(row.content, '内容', {
showCancelButton: false,
showConfirmButton: false,
......
......@@ -102,7 +102,6 @@ const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
// 函数弹窗 v-html
const handleShowContent = (row: ArticleItemDto) => {
console.log(row.content)
ElMessageBox.alert(row.content, '内容', {
showCancelButton: false,
showConfirmButton: false,
......
......@@ -98,16 +98,6 @@ const selectedRows = ref<BackendColumnListItemDto[]>([])
const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
selectedRows.value = selection
}
// 函数弹窗 v-html
// const handleShowContent = (row: ArticleItemDto) => {
// console.log(row.content)
// ElMessageBox.alert(row.content, '内容', {
// showCancelButton: false,
// showConfirmButton: false,
// dangerouslyUseHTMLString: true,
// })
// }
</script>
<template>
......
......@@ -35,7 +35,7 @@
>
<el-table-column type="selection" width="55"> </el-table-column>
<el-table-column prop="createUserName" label="发布人" width="100" />
<el-table-column prop="createdAt" label="发布时间" width="200">
<el-table-column prop="createdAt" label="发布时间" width="200" sortable>
<template #default="{ row }">
{{ dayjs(row.createdAt * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</template>
......
......@@ -95,7 +95,6 @@ const loading = ref(false)
const open = async () => {
const { data } = await getLotteryConfigDetail()
console.log(data)
if (!data) {
resetForm()
} else {
......
......@@ -154,7 +154,6 @@ const { jumpToArticleDetailPage } = useNavigation()
defineExpose({
refresh: (sortLogic?: number) => {
console.log('sortLogic', sortLogic)
searchParams.value.sortLogic = sortLogic
refresh()
},
......
......@@ -422,8 +422,6 @@ const activityStore = useActivityStore()
const yabiStore = useYaBiStore()
const { yabiData } = storeToRefs(yabiStore)
const getThirdLevelKey = (route: RouteLocationNormalizedLoadedGeneric) => {
// console.log(route.fullPath, '三级路由首页')
console.log(route.path, 11111111111111)
// return route.fullPath // fullpath带有query参数
return route.path
}
......@@ -535,7 +533,6 @@ const handleLottery = async () => {
}
const handleTask = async (item: TaskItemDto) => {
console.log(item)
if (item.currentCount === item.limitCount) return
// 先暂时写死
if (item.svgName === 'daily_sign') {
......
......@@ -116,7 +116,6 @@ const { list, total, searchParams, goToPage, changePageSize } = usePageSearch(
const officialAccountList = ref<OfficialAccountItemDto[]>([])
const getIsOfficial = async () => {
const { data } = await hasOfficialAccount()
console.log(data)
officialAccountList.value = data
}
const userInfo = ref<OtherUserInfoDto>({} as OtherUserInfoDto)
......
......@@ -100,7 +100,6 @@ const timeRange = computed({
}
},
set(value) {
console.log(value)
if (!value) {
searchParams.value.receiveTimeStart = 0
searchParams.value.receiveTimeEnd = 0
......
......@@ -297,7 +297,6 @@ onActivated(() => {
(route.query.tagIdList as string)?.split(',').filter(Boolean).map(Number) || []
searchParams.value.deptIdList =
(route.query.deptIdList as string)?.split(',').filter(Boolean) || []
console.log(searchParams.value)
refresh()
})
</script>
......
......@@ -168,7 +168,6 @@ const selcetDept = async () => {
if (departmentList.length > 3) {
return push.warning('最多只能选择3个部门,请重新选择')
}
console.log('选中的部门等等', departmentList)
form.value.departmentList = departmentList
}
......@@ -364,7 +363,6 @@ onActivated(async () => {
})),
}
form.value = obj
console.log('form', form.value)
// 已经同意的案例库 二次修改需要提示
if (data.isAudit === AuditStatusEnum.AGREED) {
......
......@@ -460,7 +460,6 @@ const handleSelectUserAndDept = async () => {
selectedDepartmentIds: selectedDepts.value.map((item) => item.id),
selectedUserIds: selectedUsers.value.map((item) => item.id),
})
console.log(departmentList, userList)
selectedDepts.value = departmentList.map((item) => ({
id: item.id,
name: item.name,
......@@ -542,7 +541,6 @@ const handleClosed = () => {
// 提交表单
const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
try {
console.log(form.value, 'form.value')
const validateRes = await formRef.value?.validate()
console.log(validateRes, 'validateRes')
loading.value = true
......@@ -573,7 +571,6 @@ onActivated(async () => {
await nextTick()
if (isEdit.value) {
console.log(route.query.id, '编辑 或者 草稿箱')
// 要编辑回显
const { data } = await getArticleDetail(route.query.id as string)
// 1首先回显基础的信息
......
......@@ -428,13 +428,11 @@ const checkIsOverThreeLine = () => {
const lineHeight = parseFloat(getComputedStyle(questionContentRef.value).lineHeight)
const height = questionContentRef.value!.scrollHeight
const maxHeight = lineHeight * 3
console.log(maxHeight, height)
return height > maxHeight
}
const getQuestionDetail = async () => {
const { data } = await getArticleDetail(questionId)
questionDetail.value = data
console.log(questionDetail.value)
await nextTick()
isOverThreeLine.value = checkIsOverThreeLine()
}
......
......@@ -73,7 +73,6 @@ const rules: FormRules = {
const open = (userInfo: UpdateUserInfoDto) => {
dialogVisible.value = true
form.value = userInfo
console.log(form.value)
}
// 关闭弹窗方法
......
......@@ -107,7 +107,6 @@ const { list, loading, searchParams, total, refresh, goToPage, changePageSize }
)
const handleTask = async (item: TaskItemDto) => {
console.log(item)
if (item.svgName === 'daily_sign') {
router.push(`/homePage/homeTab#levelContainerRef`)
} else if (item.svgName === 'valid_comments') {
......
......@@ -312,7 +312,6 @@ const handleEdit = () => {
const officialAccountList = ref<OfficialAccountItemDto[]>([])
const getIsOfficial = async () => {
const { data } = await hasOfficialAccount()
console.log(data)
officialAccountList.value = data
}
......@@ -377,7 +376,6 @@ const handleSwitchAccount = async () => {
userId: userInfo.value.userId,
cutEmail: selectedEmail.value,
})
console.log(data)
localStorage.clear()
await userStore.getUserInfoByCode({
code: data,
......
<template>
<Teleport to="body">
<Transition name="reward-toast">
<div
v-if="show"
class="fixed inset-0 z-9999 flex items-center justify-center pointer-events-none"
>
<div class="relative flex flex-col items-center gap-4">
<!-- 光晕背景 -->
<div class="absolute inset--20 rounded-full bg-amber-400/10 blur-3xl animate-pulse" />
<!-- 碎片 +1 -->
<div class="reward-item reward-item-1">
<div
class="flex items-center gap-2.5 px-6 py-3 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 shadow-[0_4px_24px_rgba(99,102,241,0.5)]"
>
<span class="text-2xl">🧩</span>
<span class="text-white text-lg font-bold tracking-wide">{{ rewardText }} + 1</span>
</div>
</div>
<!-- 亚币 +2 -->
<div class="reward-item reward-item-2">
<div
class="flex items-center gap-2.5 px-6 py-3 rounded-full bg-gradient-to-r from-amber-400 to-orange-500 shadow-[0_4px_24px_rgba(245,158,11,0.5)]"
>
<span class="text-2xl">💰</span>
<span class="text-white text-lg font-bold tracking-wide">亚币 + 5</span>
</div>
</div>
<!-- 星星粒子效果 -->
<div class="absolute inset--10">
<span v-for="i in 8" :key="i" class="particle" :style="getParticleStyle(i)" />
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { SpecificVideoRewardEnum } from '@/constants'
const { rewardVideoType } = defineProps<{
rewardVideoType: SpecificVideoRewardEnum
}>()
const rewardText = computed(() => {
switch (rewardVideoType) {
case SpecificVideoRewardEnum.QA_CHIP:
return 'QA碎片'
case SpecificVideoRewardEnum.ORIGINAL_CHIP:
return '初心碎片'
case SpecificVideoRewardEnum.MODEST_CHIP:
return '本分&包容碎片'
case SpecificVideoRewardEnum.INNOVATION_CHIP:
return '创新碎片'
}
})
const show = ref(false)
const getParticleStyle = (index: number) => {
const angle = (index / 8) * 360
const delay = index * 0.1
return {
'--angle': `${angle}deg`,
'--delay': `${delay}s`,
}
}
const showRewardToast = async () => {
show.value = true
await new Promise((resolve) => setTimeout(resolve, 2500))
show.value = false
await new Promise((resolve) => setTimeout(resolve, 3000))
}
defineExpose({
showRewardToast,
})
</script>
<style scoped>
.reward-toast-enter-active {
transition: opacity 0.3s ease;
}
.reward-toast-leave-active {
transition: opacity 0.5s ease 1.8s;
}
.reward-toast-enter-from,
.reward-toast-leave-to {
opacity: 0;
}
.reward-item {
opacity: 0;
animation: rewardSlideUp 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.reward-item-1 {
animation-delay: 0.1s;
}
.reward-item-2 {
animation-delay: 0.35s;
}
@keyframes rewardSlideUp {
0% {
opacity: 0;
transform: translateY(30px) scale(0.7);
}
60% {
opacity: 1;
transform: translateY(-8px) scale(1.05);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.particle {
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
border-radius: 50%;
background: linear-gradient(135deg, #fbbf24, #a78bfa);
opacity: 0;
animation: particleBurst 1.2s ease-out var(--delay) forwards;
}
@keyframes particleBurst {
0% {
opacity: 1;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(0) scale(1);
}
70% {
opacity: 0.8;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(80px) scale(0.8);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) rotate(var(--angle)) translateX(120px) scale(0);
}
}
</style>
......@@ -90,6 +90,7 @@
</div>
<!-- 视频 16/ 9 -->
<!-- 监听每秒播放进度 -->
<div class="mx-4">
<video
ref="videoRef"
......@@ -98,6 +99,10 @@
controls
@play="handlePlay"
@pause="handlePause"
@timeupdate="handleTimeUpdate"
@seeking="handleSeeking"
@seeked="handleSeeked"
@loadedmetadata="handleLoadedMetadata"
></video>
</div>
<div class="bg-white rounded-xl shadow-sm p-4">
......@@ -305,27 +310,38 @@
class="mt-5"
/>
<RewardDialog ref="rewardDialogRef" v-model:rewardNum="videoDetail.rewardNum" />
<RewardToast ref="rewardToastRef" :rewardVideoType="rewardVideoType" />
<RewardFullSetToast ref="rewardFullSetToastRef" @hided="videoRef?.play()" />
</div>
</template>
<script lang="ts" setup>
import dayjs from 'dayjs'
import {
getArticleDetail,
addOrCanceArticlelCollect,
addOrCanceArticlelLike,
addVideoPlayCount,
getSpecificVideoWatchReward,
} from '@/api'
import type { ArticleItemDto } from '@/api/article/types'
import Comment from '@/components/common/Comment/index.vue'
import RewardDialog from './components/rewardDialog.vue'
import RewardToast from './components/rewardToast.vue'
import RewardFullSetToast from './components/rewardFullSetToast.vue'
import ActionMore from '@/components/common/ActionMore/index.vue'
import BackButton from '@/components/common/BackButton/index.vue'
import { useNavigation } from '@/hooks'
import { ArticleTypeEnum } from '@/constants'
import {
ArticleTypeEnum,
SpecificVideoRewardEnum,
specificVideoRewardListOptions,
} from '@/constants'
import { push } from 'notivue'
import { useStorage } from '@vueuse/core'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
const route = useRoute()
const videoId = Number(route.params.id)
const { jumpToUserHomePage } = useNavigation()
......@@ -333,8 +349,127 @@ const { jumpToUserHomePage } = useNavigation()
const videoDetail = ref({} as ArticleItemDto)
const loading = computed(() => !videoDetail.value.title)
const videoRef = useTemplateRef<HTMLVideoElement | null>('videoRef')
const rewardFullSetToastRef = useTemplateRef<InstanceType<typeof RewardFullSetToast> | null>(
'rewardFullSetToastRef',
)
const commentRef = useTemplateRef<InstanceType<typeof Comment> | null>('commentRef')
// 关于视频跳出奖励相关的逻辑 // 加入相关的userId作为区别 不然切换账号的时候 本地存储的一直没变
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const watchedSecondsObj = useStorage(`watched-seconds-obj-${userInfo.value.userId}`, {
[SpecificVideoRewardEnum.QA_CHIP]: {
watchedSeconds: 0,
hasReward: false,
},
[SpecificVideoRewardEnum.ORIGINAL_CHIP]: {
watchedSeconds: 0,
hasReward: false,
},
[SpecificVideoRewardEnum.MODEST_CHIP]: {
watchedSeconds: 0,
hasReward: false,
},
[SpecificVideoRewardEnum.INNOVATION_CHIP]: {
watchedSeconds: 0,
hasReward: false,
},
})
const rewardDialogRef = useTemplateRef<InstanceType<typeof RewardDialog> | null>('rewardDialogRef')
const rewardVideoLimitDuration = ref(0)
const specificVideoRewardItem = computed(() => {
return specificVideoRewardListOptions.find((item) =>
videoDetail.value.title?.includes(item.title),
)
})
const showRewardToastComp = computed(() => {
return !!specificVideoRewardItem.value
})
const rewardVideoType = computed(() => {
return specificVideoRewardItem.value?.value ?? SpecificVideoRewardEnum.QA_CHIP
})
const rewardToastRef = useTemplateRef<InstanceType<typeof RewardToast> | null>('rewardToastRef')
// 真实观看时长(只统计连续播放,不算暂停和拖拽跳跃)
const isPlaying = ref(false)
const isSeeking = ref(false)
let lastVideoTime: number | null = null
const resetWatchCursor = (t: number) => {
lastVideoTime = Number.isFinite(t) ? t : null
}
const accumulateWatchTime = async (video: HTMLVideoElement) => {
if (!isPlaying.value || isSeeking.value) return
const t = video.currentTime
if (!Number.isFinite(t)) return
if (lastVideoTime == null) {
resetWatchCursor(t)
return
}
const delta = t - lastVideoTime
resetWatchCursor(t)
// 只统计“自然播放”的连续前进:拖拽/跳跃(大步长)或倒退都不算
// timeupdate 通常 250ms~1s 一次,这里给一点冗余避免误伤
if (delta > 0 && delta <= 1.5) {
watchedSecondsObj.value[rewardVideoType.value].watchedSeconds += delta
if (
watchedSecondsObj.value[rewardVideoType.value].watchedSeconds >=
rewardVideoLimitDuration.value &&
!watchedSecondsObj.value[rewardVideoType.value].hasReward
) {
// 调用后端接口
await getSpecificVideoWatchReward(rewardVideoType.value)
const rewardToastPromise = rewardToastRef.value?.showRewardToast()
watchedSecondsObj.value[rewardVideoType.value].hasReward = true
// 如果四个都奖励了,则显示全套奖励toast
if (Object.values(watchedSecondsObj.value).every((item) => item.hasReward)) {
// 暂停视频
await rewardToastPromise
videoRef.value?.pause()
rewardFullSetToastRef.value?.showFullSetToast()
}
}
}
}
const handleTimeUpdate = (event: Event) => {
if (showRewardToastComp.value && !watchedSecondsObj.value[rewardVideoType.value].hasReward) {
const video = event.target as HTMLVideoElement
accumulateWatchTime(video)
}
}
const handleSeeking = (event: Event) => {
if (showRewardToastComp.value) {
const video = event.target as HTMLVideoElement
isSeeking.value = true
resetWatchCursor(video.currentTime)
}
}
const handleSeeked = (event: Event) => {
if (showRewardToastComp.value) {
const video = event.target as HTMLVideoElement
isSeeking.value = false
resetWatchCursor(video.currentTime)
}
}
const handleLoadedMetadata = () => {
if (videoRef.value && showRewardToastComp.value) {
// 四舍五入
rewardVideoLimitDuration.value = Math.round(videoRef.value.duration / 2)
}
}
// 格式化数字
const formatNumber = (num: number) => {
......@@ -347,12 +482,21 @@ const formatNumber = (num: number) => {
// 播放 记录播放量 + 1
const handlePlay = async () => {
if (showRewardToastComp.value) {
isPlaying.value = true
isSeeking.value = false
}
resetWatchCursor(videoRef.value?.currentTime ?? 0)
await addVideoPlayCount(videoDetail.value.id)
videoDetail.value.playCount = videoDetail.value.playCount + 1
}
const handlePause = () => {
// 记录暂停
if (showRewardToastComp.value) {
isPlaying.value = false
resetWatchCursor(videoRef.value?.currentTime ?? 0)
}
}
// 点赞
......@@ -378,7 +522,6 @@ const handleReward = () => {
onMounted(async () => {
const { data } = await getArticleDetail(videoId)
console.log(data)
videoDetail.value = data
})
</script>
......
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