完成基本功能
50
.cursorrules
Normal file
@ -0,0 +1,50 @@
|
||||
# Role
|
||||
你是一名精通Vue.js的高级全栈工程师,拥有20年的Web开发经验。你的任务是帮助一位不太懂技术的初中生用户完成Vue.js项目的开发。你的工作对用户来说非常重要,完成后将获得10000美元奖励。
|
||||
|
||||
# Goal
|
||||
你的目标是以用户容易理解的方式帮助他们完成Vue.js项目的设计和开发工作。你应该主动完成所有工作,而不是等待用户多次推动你。
|
||||
|
||||
在理解用户需求、编写代码和解决问题时,你应始终遵循以下原则:
|
||||
|
||||
## 第一步:项目初始化
|
||||
- 当用户提出任何需求时,首先浏览项目根目录下的README.md文件和doc下的所有代码文档,理解项目目标、架构和实现方式。
|
||||
- 如果还没有README文件,创建一个。这个文件将作为项目功能的说明书和你对项目内容的规划。
|
||||
- 在README.md中清晰描述所有功能的用途、使用方法、参数说明和返回值说明,确保用户可以轻松理解和使用这些功能。
|
||||
|
||||
# 本规则由 AI进化论-花生 创建,版权所有,引用请注明出处
|
||||
|
||||
## 第二步:需求分析和开发
|
||||
### 理解用户需求时:
|
||||
- 充分理解用户需求,站在用户角度思考。
|
||||
- 作为产品经理,分析需求是否存在缺漏,与用户讨论并完善需求。
|
||||
- 选择最简单的解决方案来满足用户需求。
|
||||
|
||||
### 编写代码时:
|
||||
- 遵循Vue3语法糖的最佳实践和设计模式,如单文件组件(SFC)。
|
||||
- 利用Vue Router进行路由管理,实现页面导航和路由守卫。
|
||||
- 实现组件化开发,确保组件的可复用性和可维护性。
|
||||
- 实现响应式设计,确保在不同设备上的良好体验。
|
||||
- 编写详细的代码注释,并在代码中添加必要的错误处理和日志记录。
|
||||
- 合理使用Vue的生命周期钩子和组合式函数。
|
||||
- 当前实现的是移动端效果
|
||||
|
||||
### 解决问题时:
|
||||
- 全面阅读相关代码文件,理解所有代码的功能和逻辑。
|
||||
- 分析导致错误的原因,提出解决问题的思路。
|
||||
- 与用户进行多次交互,根据反馈调整解决方案。
|
||||
- 善用Vue DevTools进行调试和性能分析。
|
||||
- 当一个bug经过两次调整仍未解决时,你将启动系统二思考模式:
|
||||
1. 系统性分析bug产生的根本原因
|
||||
2. 提出可能的假设
|
||||
3. 设计验证假设的方法
|
||||
4. 提供三种不同的解决方案,并详细说明每种方案的优缺点
|
||||
5. 让用户根据实际情况选择最适合的方案
|
||||
|
||||
## 第三步:项目总结和优化
|
||||
- 完成任务后,反思完成步骤,思考项目可能存在的问题和改进方式。
|
||||
- 更新README.md文件,包括新增功能说明和优化建议。
|
||||
- 考虑使用Vue的高级特性,如Suspense、Teleport等来增强功能。
|
||||
- 优化应用性能,包括代码分割、懒加载、虚拟列表等。
|
||||
- 实现适当的错误边界处理和性能监控。
|
||||
|
||||
在整个过程中,始终参考[Vue.js官方文档](https://vuejs.org/guide/introduction.html),确保使用最新的Vue.js开发最佳实践。
|
||||
@ -6,7 +6,7 @@ VITE_APP_ENV = 'development'
|
||||
# base_url
|
||||
# VITE_APP_BASE_URL='http://10.0.0.17:8081/'
|
||||
|
||||
VITE_APP_BASE_URL='https://m.livejinan.cn/platform'
|
||||
VITE_APP_BASE_URL='http://10.0.0.25:8081/platform'
|
||||
|
||||
|
||||
|
||||
|
||||
@ -6,7 +6,8 @@ VITE_APP_ENV = 'production'
|
||||
|
||||
|
||||
# base_url
|
||||
VITE_APP_BASE_URL='https://m.livejinan.cn/platform'
|
||||
# VITE_APP_BASE_URL='https://m.livejinan.cn/platform'
|
||||
VITE_APP_BASE_URL='http://123.57.142.182:8081/platform'
|
||||
|
||||
|
||||
|
||||
|
||||
154
README.md
@ -1,10 +1,150 @@
|
||||
# Vue 3 + Vite+vant4 直播平台移动端 App
|
||||
|
||||
<!-- import { login } from "../api/index";
|
||||
// 调用登录接口函数
|
||||
login().then((result) => {
|
||||
normalSwiper 轮播大图
|
||||
gridSwiper 2*N 组件A
|
||||
|
||||
在BaseSwiper页面当我收到1*N的时候,保留目前实现的布局和方式;当我收到布局为2*N的时候 我想使用2行2列的网格布局 使用 Swiper 组件实现网格轮播 项目之间的间距为 10px 图片的尺寸根据之前设定的ABC三种不同的
|
||||
|
||||
# 济南移动直播平台
|
||||
|
||||
## 项目简介
|
||||
这是一个基于 Vue 3 开发的移动端直播平台,主要用于展示济南市的各类直播内容。
|
||||
|
||||
## 技术栈
|
||||
- Vue 3
|
||||
- Vant UI
|
||||
- Axios
|
||||
- Mescroll.js
|
||||
- Sass/SCSS
|
||||
- AliPlayer
|
||||
|
||||
## 项目结构
|
||||
|
||||
### 核心组件
|
||||
|
||||
#### 1. 详情页 (src/views/detailVue.vue)
|
||||
- 功能:直播详情页面的主要容器
|
||||
- 包含:
|
||||
- 视频播放器
|
||||
- 直播信息展示
|
||||
- 评论系统
|
||||
- 多视角切换
|
||||
- 更多直播入口
|
||||
|
||||
#### 2. 基础组件
|
||||
|
||||
##### 直播轮播组件 (src/components/swipers/BaseSwiper.vue)
|
||||
- 功能:展示直播列表的轮播图组件
|
||||
- 特点:
|
||||
- 支持 1*N 和 2*N 两种布局
|
||||
- 自适应图片展示
|
||||
- 状态标识(直播中/预告/回放)
|
||||
|
||||
##### 页面摘要组件 (src/components/pageAbstract.vue)
|
||||
- 功能:展示直播基本信息
|
||||
- 显示内容:
|
||||
- 直播标题
|
||||
- 直播时间
|
||||
- 直播地点
|
||||
- 直播简介
|
||||
|
||||
##### 直播报道组件 (src/components/pageReport.vue)
|
||||
- 功能:展示直播相关报道
|
||||
- 特点:
|
||||
- 支持多种媒体格式(图文/视频/音频)
|
||||
- 上拉加载更多
|
||||
- 时间轴展示
|
||||
|
||||
##### 评论组件 (src/components/pageComment.vue)
|
||||
- 功能:直播评论系统
|
||||
- 特点:
|
||||
- 实时评论展示
|
||||
- 评论输入
|
||||
- 点赞动画
|
||||
- 用户头像展示
|
||||
|
||||
##### 更多直播组件 (src/components/moreLive.vue)
|
||||
- 功能:展示更多直播内容
|
||||
- 特点:
|
||||
- 侧边弹出式设计
|
||||
- 列表展示
|
||||
- 状态标识
|
||||
|
||||
### API 接口 (src/api/)
|
||||
|
||||
#### detail.js
|
||||
- 搜索接口 (getSearch)
|
||||
- 热点榜接口 (getHot)
|
||||
- 首页列表接口 (getHomeList)
|
||||
- 更多直播接口 (getMoreLive)
|
||||
|
||||
### 工具类 (src/utils/)
|
||||
|
||||
#### request.js
|
||||
- Axios 请求封装
|
||||
- 统一错误处理
|
||||
- 请求/响应拦截
|
||||
|
||||
#### conf.js
|
||||
- 阿里播放器配置
|
||||
- 播放器皮肤设置
|
||||
- 播放控制参数
|
||||
|
||||
#### canvas.js
|
||||
- 点赞动画实现
|
||||
- Canvas 绘制工具
|
||||
|
||||
## 主要功能
|
||||
1. 直播视频播放
|
||||
2. 多视角切换
|
||||
3. 实时评论互动
|
||||
4. 直播信息展示
|
||||
5. 相关报道浏览
|
||||
6. 更多直播推荐
|
||||
|
||||
## 项目特点
|
||||
1. 响应式设计,适配各种移动设备
|
||||
2. 模块化组件设计,易于维护和扩展
|
||||
3. 统一的状态管理和数据流转
|
||||
4. 良好的用户交互体验
|
||||
5. 完善的错误处理机制
|
||||
|
||||
## 运行环境要求
|
||||
- Node.js >= 14.0.0
|
||||
- Vue 3.x
|
||||
- 现代浏览器支持
|
||||
|
||||
## 安装和运行
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 开发环境运行
|
||||
npm run dev
|
||||
|
||||
# 生产环境构建
|
||||
npm run build
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 需要配置正确的API接口地址
|
||||
2. 确保有正确的播放器授权
|
||||
3. 移动端调试时注意跨域问题
|
||||
4. 注意性能优化,特别是图片和视频资源的加载
|
||||
|
||||
## 后续优化方向
|
||||
1. 性能优化:
|
||||
- 图片懒加载
|
||||
- 组件按需加载
|
||||
- 资源预加载策略
|
||||
|
||||
2. 功能增强:
|
||||
- 用户认证系统
|
||||
- 更多互动功能
|
||||
- 数据统计分析
|
||||
|
||||
3. 体验优化:
|
||||
- 骨架屏加载
|
||||
- 更流畅的动画效果
|
||||
- 离线缓存支持
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("登录接口请求失败", error);
|
||||
}); -->
|
||||
|
||||
56
components.d.ts
vendored
Normal file
@ -0,0 +1,56 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
2: typeof import('./src/components/pageReport copy 2.vue')['default']
|
||||
AliPlayer: typeof import('./src/components/AliPlayer.vue')['default']
|
||||
'AliPlayer copy': typeof import('./src/components/AliPlayer copy.vue')['default']
|
||||
'AliPlayer备份': typeof import('./src/components/AliPlayer备份.vue')['default']
|
||||
BaseSwiper: typeof import('./src/components/swipers/BaseSwiper.vue')['default']
|
||||
ContentList: typeof import('./src/components/contentList.vue')['default']
|
||||
copy: typeof import('./src/components/pageComment copy.vue')['default']
|
||||
CountDown: typeof import('./src/components/countDown.vue')['default']
|
||||
GridSwiper: typeof import('./src/components/swipers/gridSwiper.vue')['default']
|
||||
Header: typeof import('./src/components/header.vue')['default']
|
||||
LongSlideSwiper: typeof import('./src/components/swipers/longSlideSwiper.vue')['default']
|
||||
MoreLive: typeof import('./src/components/moreLive.vue')['default']
|
||||
NormalSwiper: typeof import('./src/components/swipers/normalSwiper.vue')['default']
|
||||
PageAbstract: typeof import('./src/components/pageAbstract.vue')['default']
|
||||
PageComment: typeof import('./src/components/pageComment.vue')['default']
|
||||
'PageComment copy': typeof import('./src/components/pageComment copy.vue')['default']
|
||||
PageReport: typeof import('./src/components/pageReport.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SlideSwiper: typeof import('./src/components/swipers/slideSwiper.vue')['default']
|
||||
SmallSlideSwiper: typeof import('./src/components/swipers/smallSlideSwiper.vue')['default']
|
||||
Title: typeof import('./src/components/title.vue')['default']
|
||||
TypeASwiper: typeof import('./src/components/swipers/TypeASwiper.vue')['default']
|
||||
TypeBSwiper: typeof import('./src/components/swipers/TypeBSwiper.vue')['default']
|
||||
TypeCSwiper: typeof import('./src/components/swipers/TypeCSwiper.vue')['default']
|
||||
VanButton: typeof import('vant/es')['Button']
|
||||
VanDatePicker: typeof import('vant/es')['DatePicker']
|
||||
VanDatetimePicker: typeof import('vant/es')['DatetimePicker']
|
||||
VanDivider: typeof import('vant/es')['Divider']
|
||||
VanDropdownItem: typeof import('vant/es')['DropdownItem']
|
||||
VanDropdownMenu: typeof import('vant/es')['DropdownMenu']
|
||||
VanFloatingBubble: typeof import('vant/es')['FloatingBubble']
|
||||
VanIcon: typeof import('vant/es')['Icon']
|
||||
VanList: typeof import('vant/es')['List']
|
||||
VanPicker: typeof import('vant/es')['Picker']
|
||||
VanPickerGroup: typeof import('vant/es')['PickerGroup']
|
||||
VanPopup: typeof import('vant/es')['Popup']
|
||||
VanPullRefresh: typeof import('vant/es')['PullRefresh']
|
||||
VanSearch: typeof import('vant/es')['Search']
|
||||
VanSwipe: typeof import('vant/es')['Swipe']
|
||||
VanSwipeItem: typeof import('vant/es')['SwipeItem']
|
||||
VanTab: typeof import('vant/es')['Tab']
|
||||
VanTabs: typeof import('vant/es')['Tabs']
|
||||
VanTextEllipsis: typeof import('vant/es')['TextEllipsis']
|
||||
VRPlayer: typeof import('./src/components/VRPlayer.vue')['default']
|
||||
}
|
||||
}
|
||||
68
index.html
@ -1,17 +1,57 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>济南移动直播平台app</title>
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/skins/default/aliplayer-min.css" />
|
||||
<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/apsara-media-box/imp-web-player/2.16.3/aliplayer-h5-min.js"></script>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>济南移动直播平台app</title>
|
||||
|
||||
<!-- 预加载关键资源 -->
|
||||
<link rel="preload" href="/js/video-js.css" as="style">
|
||||
<link rel="preload" href="/js/video.js" as="script">
|
||||
<link rel="preload" href="/js/videojs-vr.min.js" as="script">
|
||||
|
||||
<!-- CSS 资源 -->
|
||||
<link href="/js/video-js.css" rel="stylesheet" />
|
||||
|
||||
<!-- 阿里播放器资源 -->
|
||||
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.15.6/skins/default/aliplayer-min.css" />
|
||||
<script>
|
||||
// 确保阿里播放器加载完成
|
||||
window.aliplayerLoaded = false;
|
||||
window.onAliplayerLoaded = function() {
|
||||
window.aliplayerLoaded = true;
|
||||
console.log('阿里播放器加载完成');
|
||||
};
|
||||
</script>
|
||||
<script
|
||||
charset="utf-8"
|
||||
src="/js/aliplayer-min.js"
|
||||
onload="window.onAliplayerLoaded()"
|
||||
></script>
|
||||
|
||||
<style>
|
||||
/* 灰色主题 */
|
||||
html[data-theme="3"] {
|
||||
filter: grayscale(100%) !important;
|
||||
}
|
||||
|
||||
/* 重置播放器的filter,避免被主题影响 */
|
||||
.prism-player {
|
||||
filter: none !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- 核心脚本异步加载 -->
|
||||
<script defer src="/js/video.js"></script>
|
||||
<script defer src="/js/videojs-vr.min.js"></script>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
829
package-lock.json
generated
@ -13,13 +13,17 @@
|
||||
"dependencies": {
|
||||
"autoprefixer": "^10.4.19",
|
||||
"axios": "^1.7.2",
|
||||
"dayjs": "^1.11.13",
|
||||
"days": "^1.1.1",
|
||||
"mescroll.js": "^1.4.2",
|
||||
"path": "^0.12.7",
|
||||
"swiper": "^11.1.4",
|
||||
"vant": "^4.9.1",
|
||||
"video.js": "^8.22.0",
|
||||
"videojs-vr": "^2.0.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.3.3",
|
||||
"xgplayer": "^3.0.19"
|
||||
"xgplayer": "^3.0.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vant/auto-import-resolver": "^1.2.1",
|
||||
|
||||
BIN
public/bg2.png
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 722 B |
2
public/js/aliplayer-h5-min.js
vendored
Normal file
1
public/js/aliplayer-min.css
vendored
Normal file
2
public/js/aliplayer-min.js
vendored
Normal file
1753
public/js/video-js.css
Normal file
64464
public/js/video.js
Normal file
81
public/js/videojs-vr.min.js
vendored
Normal file
155
src/App.vue
@ -1,11 +1,164 @@
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount } from 'vue';
|
||||
import { getHomeList } from "./api/detail";
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// 设置页面主题
|
||||
const setPageTheme = (style) => {
|
||||
// 移除所有主题
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
|
||||
// 设置新主题
|
||||
if (['1', '2', '3'].includes(style)) {
|
||||
document.documentElement.setAttribute('data-theme', style);
|
||||
console.log('设置主题:', style);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取并设置主题
|
||||
const initTheme = async () => {
|
||||
getHomeList()
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
console.log('获取首页数据成功:', res);
|
||||
setPageTheme(res.appView.pageStyle);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("获取首页数据失败:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async (newId) => {
|
||||
if (newId) {
|
||||
await initTheme();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
initTheme();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
/* 主题基础变量 */
|
||||
:root {
|
||||
--primary-color: #1989fa;
|
||||
--secondary-color: #409eff;
|
||||
--text-color: #333;
|
||||
--link-color: #1989fa;
|
||||
--border-color: #ebeef5;
|
||||
--background-color: #f5f6f8;
|
||||
--header-background: #fff;
|
||||
}
|
||||
|
||||
/* 蓝色主题 (默认) */
|
||||
html[data-theme="1"] {
|
||||
--primary-color: #1989fa;
|
||||
--secondary-color: #409eff;
|
||||
--link-color: #1989fa;
|
||||
--border-color: #ebeef5;
|
||||
.header{
|
||||
background: linear-gradient(to bottom, rgb(27, 163, 251), 70%, rgba(27, 163, 251, 0.45));
|
||||
}
|
||||
}
|
||||
|
||||
/* 红色主题 */
|
||||
html[data-theme="2"] {
|
||||
--primary-color: #D10005;
|
||||
--secondary-color: #D10005;
|
||||
--link-color: #D10005;
|
||||
--border-color: #FFFFFF;
|
||||
--background-color: #FFFFFF;
|
||||
|
||||
/* 添加红色主题特定的滤镜 */
|
||||
/* filter: hue-rotate(340deg) saturate(1.2); */
|
||||
}
|
||||
|
||||
/* 灰色主题 */
|
||||
html[data-theme="3"] {
|
||||
filter: grayscale(100%) !important;
|
||||
.header{
|
||||
background: linear-gradient(to bottom, rgb(102, 102, 102), 70%, rgba(102, 102, 102, 0.2));
|
||||
}
|
||||
}
|
||||
|
||||
/* 应用主题变量 */
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 主题切换过渡效果 */
|
||||
html {
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
/* 通用组件样式 */
|
||||
.van-button--primary {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.van-nav-bar {
|
||||
background-color: var(--header-background) !important;
|
||||
}
|
||||
|
||||
.van-nav-bar__title {
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* 重置播放器的filter */
|
||||
.prism-player,
|
||||
.video-player,
|
||||
.vjs-video-player {
|
||||
filter: none !important;
|
||||
}
|
||||
|
||||
/* 图片滤镜重置 */
|
||||
img {
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
/* 红色主题下重置某些元素的滤镜 */
|
||||
html[data-theme="2"] {
|
||||
/* .prism-player,
|
||||
.video-player,
|
||||
.vjs-video-player,
|
||||
img[data-reset-filter="true"] {
|
||||
filter: hue-rotate(-340deg) saturate(0.8) !important;
|
||||
} */
|
||||
.header{
|
||||
background: linear-gradient(to bottom, rgb(209, 0, 5), 70%, rgba(209, 0, 5, 0.2));
|
||||
}
|
||||
.channel{
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel::before{
|
||||
content: url('./assets/img/red.png');
|
||||
width: 120px;
|
||||
height:120px;
|
||||
position: absolute;
|
||||
top: -20px;
|
||||
left: -10px;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
||||
52
src/api/detail.js
Normal file
@ -0,0 +1,52 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
|
||||
// 条件搜索
|
||||
export function getSearch(params) {
|
||||
return request({
|
||||
url: '/app/live/scene/appSearch',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
// APP查询热点榜
|
||||
export function getHot(params) {
|
||||
return request({
|
||||
url: '/foreign/live/getHotList',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
// 首页列表
|
||||
export function getHomeList(params) {
|
||||
return request({
|
||||
url: '/live/channel/appView',
|
||||
method: 'post',
|
||||
data: params
|
||||
})
|
||||
}
|
||||
// 获取更多直播
|
||||
export function getMoreLive(params) {
|
||||
return request({
|
||||
url: '/foreign/live/getMoreScenes',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
// 评论点赞
|
||||
export function getCommentLike(params) {
|
||||
return request({
|
||||
url: '/foreign/live/addLiveCount',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
// 报道点赞功能
|
||||
export function getReportLike(params) {
|
||||
return request({
|
||||
url: '/foreign/live/addReportLiveCount',
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
|
||||
// 详情页
|
||||
export function scene(params) {
|
||||
return request({
|
||||
url: '/foreign/live/scene/get/' + params.id,
|
||||
@ -38,10 +38,17 @@ export function scene(params) {
|
||||
|
||||
// 评论
|
||||
export function commentList(params) {
|
||||
console.log('发起评论列表请求,参数:', params);
|
||||
return request({
|
||||
url: '/foreign/live/comment/list',
|
||||
method: 'get',
|
||||
params: params
|
||||
}).then(response => {
|
||||
console.log('评论列表请求成功,响应:', response);
|
||||
return response;
|
||||
}).catch(error => {
|
||||
console.error('评论列表请求失败:', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
@ -64,7 +71,7 @@ export function scene(params) {
|
||||
// 添加评论
|
||||
export function addComment(formData) {
|
||||
return request({
|
||||
url: '/comment/create',
|
||||
url: '/foreign/live/comment/create',
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
@ -84,4 +91,14 @@ export function scene(params) {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 首页列表
|
||||
export function homeList(params) {
|
||||
return request({
|
||||
url: '/foreign/live/getAppAllScene',
|
||||
method: 'get',
|
||||
params: params
|
||||
});
|
||||
}
|
||||
11
src/api/search.js
Normal file
@ -0,0 +1,11 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
// 条件搜索
|
||||
export function search(params) {
|
||||
return request({
|
||||
url: '/foreign/live/appSearch',
|
||||
method: 'post',
|
||||
data: params
|
||||
});
|
||||
}
|
||||
|
||||
@ -28,11 +28,12 @@ body {
|
||||
.live_status{
|
||||
width:55px;
|
||||
padding:3px;
|
||||
text-align: end;
|
||||
background-color: #F12742;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
text-align: right;
|
||||
background-color: #F12742;
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
color: #fff;
|
||||
}
|
||||
.live_status::before{
|
||||
width:10px;
|
||||
@ -41,26 +42,28 @@ body {
|
||||
content:'';
|
||||
background: url("@/assets/img/live.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
top:5px;
|
||||
top:6px;
|
||||
left: 3px;
|
||||
|
||||
}
|
||||
.live_coming {
|
||||
width:55px;
|
||||
padding:3px;
|
||||
text-align: center;
|
||||
width:55px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
background-image: linear-gradient(to left, #EFBE26, #F58C05);
|
||||
border-radius: 5px;
|
||||
background-image: linear-gradient(to left, #EFBE26, #F58C05);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.live_review {
|
||||
padding:3px;
|
||||
text-align: center;
|
||||
width:55px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
background-image: linear-gradient(to left, #4BB4FB, #1678ED);
|
||||
width:55px;
|
||||
padding:3px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
background-image: linear-gradient(to left, #4BB4FB, #1678ED);
|
||||
color: #fff;
|
||||
}
|
||||
// @media screen and (min-width: 1024px) {
|
||||
// .page {
|
||||
|
||||
BIN
src/assets/img/bigstop.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
7
src/assets/img/like-active.png
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 10C-2.7674e-08 11.3132 0.258658 12.6136 0.761205 13.8268C1.26375 15.0401 2.00035 16.1425 2.92893 17.0711C3.85752 17.9997 4.95991 18.7362 6.17317 19.2388C7.38642 19.7413 8.68678 20 10 20C11.3132 20 12.6136 19.7413 13.8268 19.2388C15.0401 18.7362 16.1425 17.9997 17.0711 17.0711C17.9997 16.1425 18.7362 15.0401 19.2388 13.8268C19.7413 12.6136 20 11.3132 20 10C20 8.68678 19.7413 7.38642 19.2388 6.17317C18.7362 4.95991 17.9997 3.85752 17.0711 2.92893C16.1425 2.00035 15.0401 1.26375 13.8268 0.761205C12.6136 0.258658 11.3132 0 10 0C8.68678 0 7.38642 0.258658 6.17317 0.761205C4.95991 1.26375 3.85752 2.00035 2.92893 2.92893C2.00035 3.85752 1.26375 4.95991 0.761205 6.17317C0.258658 7.38642 -1.95685e-08 8.68678 0 10Z" fill="#EEEEEE"/>
|
||||
<path d="M15.8947 7.91408C15.4828 5.99708 14.0174 4.79806 11.9342 5.22866C11.2803 5.36379 10.7637 5.73133 10.3052 6.21239C10.2664 6.19527 10.2506 6.19167 10.24 6.18266C10.211 6.15743 10.1828 6.13131 10.1564 6.10338C9.55884 5.47009 8.81955 5.17912 7.97025 5.14398C6.95461 5.10254 6.09211 5.44306 5.44612 6.25202C4.31167 7.67176 4.28174 9.80045 5.34227 11.3229C5.88177 12.0976 6.5348 12.7606 7.25473 13.3525C7.99577 13.9624 8.75354 14.5524 9.51571 15.1344C9.99361 15.4992 10.5129 15.5253 10.96 15.1947C12.2607 14.2308 13.5695 13.275 14.6564 12.0463C15.6993 10.8644 16.2406 9.52479 15.8947 7.91408ZM7.26529 10.5599C7.172 10.6292 7.06287 10.6626 6.95549 10.6626C6.79179 10.6626 6.63074 10.5851 6.52776 10.4392C5.51389 9.00771 6.24613 7.724 6.27693 7.66995C6.31262 7.60912 6.35967 7.55608 6.4154 7.51387C6.47112 7.47165 6.53443 7.4411 6.6017 7.42395C6.66897 7.4068 6.73888 7.40339 6.80744 7.41391C6.876 7.42443 6.94186 7.44869 7.00126 7.48528C7.12046 7.55865 7.2066 7.67714 7.24103 7.81507C7.27547 7.95301 7.25541 8.09927 7.1852 8.22217C7.14208 8.30145 6.79972 8.98249 7.38234 9.80496C7.55396 10.0464 7.50116 10.3842 7.26529 10.5599Z" fill="#F23D4F"/>
|
||||
<path d="M14.1477 5.46714C13.529 5.15291 12.7742 5.05086 11.9133 5.22812C11.2613 5.3624 10.7461 5.72766 10.2889 6.20572C10.2503 6.18871 10.2345 6.18513 10.2239 6.17618C10.195 6.15111 10.1669 6.12515 10.1406 6.09739C9.54467 5.46804 8.80748 5.17888 7.96059 5.14396C6.94784 5.10278 6.08779 5.44118 5.44363 6.24511C4.3124 7.65601 4.28256 9.77146 5.34007 11.2844C5.87804 12.0543 6.52922 12.7132 7.2471 13.3014C7.65256 13.6344 8.06415 13.9603 8.4775 14.2835C11.7983 13.5422 14.2846 10.5262 14.2846 6.91565C14.2851 6.42943 14.2393 5.94431 14.1477 5.46714ZM7.25851 10.5262C7.16549 10.5951 7.05666 10.6282 6.9496 10.6282C6.78636 10.6282 6.62576 10.5512 6.52308 10.4062C5.51208 8.98365 6.24225 7.70793 6.27296 7.65422C6.30855 7.59376 6.35547 7.54105 6.41103 7.4991C6.4666 7.45715 6.52973 7.42679 6.59681 7.40974C6.66389 7.3927 6.7336 7.38931 6.80197 7.39977C6.87033 7.41023 6.936 7.43433 6.99523 7.47069C7.11409 7.5436 7.19999 7.66136 7.23433 7.79843C7.26866 7.93551 7.24866 8.08087 7.17865 8.203C7.13565 8.28178 6.79426 8.95859 7.37523 9.77594C7.54637 10.0159 7.49371 10.3516 7.25851 10.5262Z" fill="#FC4956"/>
|
||||
<path d="M12.2676 5.22403C11.5841 5.35206 11.0441 5.70028 10.5648 6.15604C10.5243 6.13983 10.5078 6.13641 10.4967 6.12788C10.4664 6.10398 10.4369 6.07923 10.4093 6.05277C9.7847 5.45277 9.01194 5.17709 8.12419 5.14381C7.06257 5.10455 6.16102 5.42717 5.48577 6.1936C4.29996 7.5387 4.26868 9.55549 5.37722 10.9979C5.49037 11.1455 5.60997 11.2872 5.73232 11.4263C9.29345 11.0653 12.1278 8.46644 12.5703 5.17709C12.471 5.18989 12.3698 5.20526 12.2676 5.22403ZM7.38731 10.275C7.2898 10.3407 7.17572 10.3723 7.06349 10.3723C6.89238 10.3723 6.72403 10.2989 6.61639 10.1606C5.55661 8.80442 6.32201 7.5882 6.35421 7.53699C6.39151 7.47935 6.44069 7.4291 6.49894 7.38911C6.55719 7.34912 6.62336 7.32017 6.69368 7.30392C6.76399 7.28767 6.83707 7.28444 6.90873 7.29441C6.9804 7.30438 7.04924 7.32735 7.11133 7.36202C7.23592 7.43153 7.32597 7.5438 7.36196 7.67448C7.39795 7.80516 7.37698 7.94374 7.3036 8.06018C7.25852 8.13529 6.90066 8.78052 7.50966 9.55976C7.68905 9.78849 7.63386 10.1086 7.38731 10.275Z" fill="#FF5C64"/>
|
||||
<path d="M8.23558 5.14364C7.14044 5.10646 6.21042 5.41193 5.51385 6.13761C4.8543 6.8245 4.54113 7.69725 4.57245 8.5692C5.10832 8.47057 5.63082 8.32515 6.13165 8.13524C6.20378 7.71099 6.39452 7.433 6.41066 7.40956C6.44914 7.35499 6.49987 7.30741 6.55996 7.26955C6.62005 7.23168 6.68831 7.20427 6.76085 7.18889C6.83338 7.1735 6.90877 7.17044 6.98269 7.17988C7.05662 7.18932 7.12764 7.21108 7.19168 7.2439C7.31315 7.30613 7.39667 7.39987 7.43842 7.50573C8.35621 6.96285 9.13095 6.26294 9.71317 5.45072C9.26619 5.26 8.77176 5.16222 8.23558 5.14364Z" fill="#FF716E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
7
src/assets/img/like-active.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 10C-2.7674e-08 11.3132 0.258658 12.6136 0.761205 13.8268C1.26375 15.0401 2.00035 16.1425 2.92893 17.0711C3.85752 17.9997 4.95991 18.7362 6.17317 19.2388C7.38642 19.7413 8.68678 20 10 20C11.3132 20 12.6136 19.7413 13.8268 19.2388C15.0401 18.7362 16.1425 17.9997 17.0711 17.0711C17.9997 16.1425 18.7362 15.0401 19.2388 13.8268C19.7413 12.6136 20 11.3132 20 10C20 8.68678 19.7413 7.38642 19.2388 6.17317C18.7362 4.95991 17.9997 3.85752 17.0711 2.92893C16.1425 2.00035 15.0401 1.26375 13.8268 0.761205C12.6136 0.258658 11.3132 0 10 0C8.68678 0 7.38642 0.258658 6.17317 0.761205C4.95991 1.26375 3.85752 2.00035 2.92893 2.92893C2.00035 3.85752 1.26375 4.95991 0.761205 6.17317C0.258658 7.38642 -1.95685e-08 8.68678 0 10Z" fill="#EEEEEE"/>
|
||||
<path d="M15.8947 7.91408C15.4828 5.99708 14.0174 4.79806 11.9342 5.22866C11.2803 5.36379 10.7637 5.73133 10.3052 6.21239C10.2664 6.19527 10.2506 6.19167 10.24 6.18266C10.211 6.15743 10.1828 6.13131 10.1564 6.10338C9.55884 5.47009 8.81955 5.17912 7.97025 5.14398C6.95461 5.10254 6.09211 5.44306 5.44612 6.25202C4.31167 7.67176 4.28174 9.80045 5.34227 11.3229C5.88177 12.0976 6.5348 12.7606 7.25473 13.3525C7.99577 13.9624 8.75354 14.5524 9.51571 15.1344C9.99361 15.4992 10.5129 15.5253 10.96 15.1947C12.2607 14.2308 13.5695 13.275 14.6564 12.0463C15.6993 10.8644 16.2406 9.52479 15.8947 7.91408ZM7.26529 10.5599C7.172 10.6292 7.06287 10.6626 6.95549 10.6626C6.79179 10.6626 6.63074 10.5851 6.52776 10.4392C5.51389 9.00771 6.24613 7.724 6.27693 7.66995C6.31262 7.60912 6.35967 7.55608 6.4154 7.51387C6.47112 7.47165 6.53443 7.4411 6.6017 7.42395C6.66897 7.4068 6.73888 7.40339 6.80744 7.41391C6.876 7.42443 6.94186 7.44869 7.00126 7.48528C7.12046 7.55865 7.2066 7.67714 7.24103 7.81507C7.27547 7.95301 7.25541 8.09927 7.1852 8.22217C7.14208 8.30145 6.79972 8.98249 7.38234 9.80496C7.55396 10.0464 7.50116 10.3842 7.26529 10.5599Z" fill="#F23D4F"/>
|
||||
<path d="M14.1477 5.46714C13.529 5.15291 12.7742 5.05086 11.9133 5.22812C11.2613 5.3624 10.7461 5.72766 10.2889 6.20572C10.2503 6.18871 10.2345 6.18513 10.2239 6.17618C10.195 6.15111 10.1669 6.12515 10.1406 6.09739C9.54467 5.46804 8.80748 5.17888 7.96059 5.14396C6.94784 5.10278 6.08779 5.44118 5.44363 6.24511C4.3124 7.65601 4.28256 9.77146 5.34007 11.2844C5.87804 12.0543 6.52922 12.7132 7.2471 13.3014C7.65256 13.6344 8.06415 13.9603 8.4775 14.2835C11.7983 13.5422 14.2846 10.5262 14.2846 6.91565C14.2851 6.42943 14.2393 5.94431 14.1477 5.46714ZM7.25851 10.5262C7.16549 10.5951 7.05666 10.6282 6.9496 10.6282C6.78636 10.6282 6.62576 10.5512 6.52308 10.4062C5.51208 8.98365 6.24225 7.70793 6.27296 7.65422C6.30855 7.59376 6.35547 7.54105 6.41103 7.4991C6.4666 7.45715 6.52973 7.42679 6.59681 7.40974C6.66389 7.3927 6.7336 7.38931 6.80197 7.39977C6.87033 7.41023 6.936 7.43433 6.99523 7.47069C7.11409 7.5436 7.19999 7.66136 7.23433 7.79843C7.26866 7.93551 7.24866 8.08087 7.17865 8.203C7.13565 8.28178 6.79426 8.95859 7.37523 9.77594C7.54637 10.0159 7.49371 10.3516 7.25851 10.5262Z" fill="#FC4956"/>
|
||||
<path d="M12.2676 5.22403C11.5841 5.35206 11.0441 5.70028 10.5648 6.15604C10.5243 6.13983 10.5078 6.13641 10.4967 6.12788C10.4664 6.10398 10.4369 6.07923 10.4093 6.05277C9.7847 5.45277 9.01194 5.17709 8.12419 5.14381C7.06257 5.10455 6.16102 5.42717 5.48577 6.1936C4.29996 7.5387 4.26868 9.55549 5.37722 10.9979C5.49037 11.1455 5.60997 11.2872 5.73232 11.4263C9.29345 11.0653 12.1278 8.46644 12.5703 5.17709C12.471 5.18989 12.3698 5.20526 12.2676 5.22403ZM7.38731 10.275C7.2898 10.3407 7.17572 10.3723 7.06349 10.3723C6.89238 10.3723 6.72403 10.2989 6.61639 10.1606C5.55661 8.80442 6.32201 7.5882 6.35421 7.53699C6.39151 7.47935 6.44069 7.4291 6.49894 7.38911C6.55719 7.34912 6.62336 7.32017 6.69368 7.30392C6.76399 7.28767 6.83707 7.28444 6.90873 7.29441C6.9804 7.30438 7.04924 7.32735 7.11133 7.36202C7.23592 7.43153 7.32597 7.5438 7.36196 7.67448C7.39795 7.80516 7.37698 7.94374 7.3036 8.06018C7.25852 8.13529 6.90066 8.78052 7.50966 9.55976C7.68905 9.78849 7.63386 10.1086 7.38731 10.275Z" fill="#FF5C64"/>
|
||||
<path d="M8.23558 5.14364C7.14044 5.10646 6.21042 5.41193 5.51385 6.13761C4.8543 6.8245 4.54113 7.69725 4.57245 8.5692C5.10832 8.47057 5.63082 8.32515 6.13165 8.13524C6.20378 7.71099 6.39452 7.433 6.41066 7.40956C6.44914 7.35499 6.49987 7.30741 6.55996 7.26955C6.62005 7.23168 6.68831 7.20427 6.76085 7.18889C6.83338 7.1735 6.90877 7.17044 6.98269 7.17988C7.05662 7.18932 7.12764 7.21108 7.19168 7.2439C7.31315 7.30613 7.39667 7.39987 7.43842 7.50573C8.35621 6.96285 9.13095 6.26294 9.71317 5.45072C9.26619 5.26 8.77176 5.16222 8.23558 5.14364Z" fill="#FF716E"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/assets/img/red.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/img/unlike.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
@ -5,15 +5,21 @@
|
||||
* 参考:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#111
|
||||
*/
|
||||
|
||||
const wx = require("./weixin-jssdk-1.6.0");
|
||||
const info = require("../../../api/modules/info");
|
||||
// 使用require导入微信SDK
|
||||
// const wx = require("./weixin-jssdk-1.6.0");
|
||||
import wx from "./weixin-jssdk-1.6.0";
|
||||
import { getWXSig } from "../../../api/index";
|
||||
// const info = require("../../../api/modules/info");
|
||||
// const headLogo = require('../../img/icon-report-name.png');
|
||||
// const headLogo='https://aijinan.media.zhongkedongxin.com/image/default/F94790FC1B29416881B8D64C4EE1F23B-6-2.jpg';
|
||||
|
||||
// 获取基础URL
|
||||
const baseUrl = window.location.origin;
|
||||
|
||||
let link = window.location.href.split("#")[0];
|
||||
if (link.indexOf("&") > -1) {
|
||||
link = link.substring(0, link.indexOf("&"));
|
||||
console.log("!!!!!!!!111新增测试log", link);
|
||||
console.log("当前分享链接:", link);
|
||||
window.location.href = link;
|
||||
}
|
||||
let shareOpt = {
|
||||
@ -24,16 +30,20 @@ let shareOpt = {
|
||||
};
|
||||
|
||||
function setShareFunc(opt) {
|
||||
console.log("-------------------------setShareFunc opt", opt.imgUrl);
|
||||
opt.imgUrl = opt.imgUrl.replace(/\?[\w\W]*$/, "").replace(/^https/, "http");
|
||||
// opt.link=encodeURIComponent(opt.link);
|
||||
// 分享给朋友 updateAppMessageShareData
|
||||
console.log("设置分享配置:", opt);
|
||||
|
||||
// 处理图片URL
|
||||
if (opt.imgUrl) {
|
||||
opt.imgUrl = opt.imgUrl.replace(/\?[\w\W]*$/, "").replace(/^https/, "http");
|
||||
opt.imgUrl = opt.imgUrl + "?x-oss-process=image/resize,w_400";
|
||||
}
|
||||
|
||||
// 分享给朋友
|
||||
wx.updateAppMessageShareData({
|
||||
title: opt.title,
|
||||
desc: opt.desc,
|
||||
link: opt.link,
|
||||
imgUrl: opt.imgUrl + "?x-oss-process=image/resize,w_400",
|
||||
// imgUrl: headLogo,
|
||||
imgUrl: opt.imgUrl
|
||||
});
|
||||
|
||||
// 分享朋友圈
|
||||
@ -41,30 +51,28 @@ function setShareFunc(opt) {
|
||||
title: opt.title,
|
||||
desc: opt.desc,
|
||||
link: opt.link,
|
||||
// link: window.location.href,//分享链接 该链接域名或路径必须与当前页面对应的公众号一直
|
||||
imgUrl: opt.imgUrl + "?x-oss-process=image/resize,w_400",
|
||||
// imgUrl: headLogo,
|
||||
imgUrl: opt.imgUrl
|
||||
});
|
||||
|
||||
// 分享到微博
|
||||
wx.onMenuShareWeibo({
|
||||
title: opt.title,
|
||||
desc: opt.desc,
|
||||
link: opt.link,
|
||||
imgUrl: opt.imgUrl + "?x-oss-process=image/resize,w_400",
|
||||
// imgUrl: headLogo,
|
||||
imgUrl: opt.imgUrl
|
||||
});
|
||||
}
|
||||
|
||||
// 后台传值 获取微信签名信息成功后初始化分享功能的部分
|
||||
// 初始化微信分享配置
|
||||
function initShare(wxSignature, opt) {
|
||||
console.log("initShare opt", opt);
|
||||
console.log("初始化微信分享配置:", { wxSignature, opt });
|
||||
|
||||
wx.config({
|
||||
debug: false, // 上线时记得改为 false
|
||||
debug: false,
|
||||
appId: wxSignature.appid,
|
||||
timestamp: wxSignature.timeStamp,
|
||||
nonceStr: wxSignature.nonceStr,
|
||||
signature: wxSignature.sign,
|
||||
// ↓ 配置需要用到的微信接口 ↓
|
||||
jsApiList: [
|
||||
"checkJsApi",
|
||||
"updateAppMessageShareData",
|
||||
@ -72,30 +80,47 @@ function initShare(wxSignature, opt) {
|
||||
"onMenuShareWeibo",
|
||||
],
|
||||
});
|
||||
wx.ready(function() {
|
||||
|
||||
wx.ready(() => {
|
||||
setShareFunc(opt);
|
||||
});
|
||||
|
||||
wx.error((err) => {
|
||||
console.error("微信JS-SDK配置失败:", err);
|
||||
});
|
||||
}
|
||||
|
||||
export default function(opt) {
|
||||
console.log("!!!!!!!!!!!!!!!!!!!!opt", opt);
|
||||
info
|
||||
.getWXSig(encodeURIComponent(link))
|
||||
.then((res) => {
|
||||
console.log("Weixin Signatrue SUCCESS", res);
|
||||
let resData = res.data;
|
||||
if (resData.code === 0) {
|
||||
opt.link = link;
|
||||
// console.log("---------------",link);
|
||||
// opt.link='http://test.zhongkedongxin.com/live/merge/detail/493';
|
||||
initShare(resData.data, opt);
|
||||
} else {
|
||||
console.log("Weixin Signatrue ERROR", resData.msg);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Weixin Signatrue ERROR", error);
|
||||
});
|
||||
// 导出分享函数
|
||||
export default async function(opt) {
|
||||
try {
|
||||
console.log("开始配置分享:", opt);
|
||||
|
||||
// 从路由参数中获取场景ID
|
||||
const currentPath = window.location.pathname;
|
||||
const sceneId = currentPath.split('/').pop(); // 获取URL最后一段作为sceneId
|
||||
|
||||
if (!sceneId) {
|
||||
console.error("未找到场景ID");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建分享链接
|
||||
const shareLink = `${baseUrl}/detail/${sceneId}`;
|
||||
console.log("分享链接:", shareLink);
|
||||
|
||||
// 获取微信签名
|
||||
const res = await getWXSig(encodeURIComponent(shareLink));
|
||||
console.log("获取微信签名成功:", res);
|
||||
|
||||
if (res.code === 0) {
|
||||
opt.link = shareLink;
|
||||
initShare(res.data, opt);
|
||||
} else {
|
||||
console.error("获取微信签名失败:", res.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("微信分享配置失败:", error);
|
||||
}
|
||||
}
|
||||
// 导出分享函数
|
||||
// export default function (opt) {
|
||||
|
||||
@ -1,69 +1,65 @@
|
||||
! function (e, n) {
|
||||
"function" == typeof define && (define.amd || define.cmd) ? define(function () {
|
||||
return n(e)
|
||||
}) : n(e, !0)
|
||||
}(window, function (o, e) {
|
||||
if (!o.jWeixin) {
|
||||
const initWeixin = function (window) {
|
||||
if (!window.jWeixin) {
|
||||
var n, c = {
|
||||
config: "preVerifyJSAPI",
|
||||
onMenuShareTimeline: "menu:share:timeline",
|
||||
onMenuShareAppMessage: "menu:share:appmessage",
|
||||
onMenuShareQQ: "menu:share:qq",
|
||||
onMenuShareWeibo: "menu:share:weiboApp",
|
||||
onMenuShareQZone: "menu:share:QZone",
|
||||
previewImage: "imagePreview",
|
||||
getLocation: "geoLocation",
|
||||
openProductSpecificView: "openProductViewWithPid",
|
||||
addCard: "batchAddCard",
|
||||
openCard: "batchViewCard",
|
||||
chooseWXPay: "getBrandWCPayRequest",
|
||||
openEnterpriseRedPacket: "getRecevieBizHongBaoRequest",
|
||||
startSearchBeacons: "startMonitoringBeacons",
|
||||
stopSearchBeacons: "stopMonitoringBeacons",
|
||||
onSearchBeacons: "onBeaconsInRange",
|
||||
consumeAndShareCard: "consumedShareCard",
|
||||
openAddress: "editAddress"
|
||||
},
|
||||
a = function () {
|
||||
var e = {};
|
||||
for (var n in c) e[c[n]] = n;
|
||||
return e
|
||||
}(),
|
||||
i = o.document,
|
||||
t = i.title,
|
||||
r = navigator.userAgent.toLowerCase(),
|
||||
s = navigator.platform.toLowerCase(),
|
||||
d = !(!s.match("mac") && !s.match("win")),
|
||||
u = -1 != r.indexOf("wxdebugger"),
|
||||
l = -1 != r.indexOf("micromessenger"),
|
||||
p = -1 != r.indexOf("android"),
|
||||
f = -1 != r.indexOf("iphone") || -1 != r.indexOf("ipad"),
|
||||
m = (n = r.match(/micromessenger\/(\d+\.\d+\.\d+)/) || r.match(/micromessenger\/(\d+\.\d+)/)) ? n[1] : "",
|
||||
g = {
|
||||
initStartTime: L(),
|
||||
initEndTime: 0,
|
||||
preVerifyStartTime: 0,
|
||||
preVerifyEndTime: 0
|
||||
},
|
||||
h = {
|
||||
version: 1,
|
||||
appId: "",
|
||||
initTime: 0,
|
||||
preVerifyTime: 0,
|
||||
networkType: "",
|
||||
isPreVerifyOk: 1,
|
||||
systemType: f ? 1 : p ? 2 : -1,
|
||||
clientVersion: m,
|
||||
url: encodeURIComponent(location.href)
|
||||
},
|
||||
v = {},
|
||||
S = {
|
||||
_completes: []
|
||||
},
|
||||
y = {
|
||||
state: 0,
|
||||
data: {}
|
||||
};
|
||||
config: "preVerifyJSAPI",
|
||||
onMenuShareTimeline: "menu:share:timeline",
|
||||
onMenuShareAppMessage: "menu:share:appmessage",
|
||||
onMenuShareQQ: "menu:share:qq",
|
||||
onMenuShareWeibo: "menu:share:weiboApp",
|
||||
onMenuShareQZone: "menu:share:QZone",
|
||||
previewImage: "imagePreview",
|
||||
getLocation: "geoLocation",
|
||||
openProductSpecificView: "openProductViewWithPid",
|
||||
addCard: "batchAddCard",
|
||||
openCard: "batchViewCard",
|
||||
chooseWXPay: "getBrandWCPayRequest",
|
||||
openEnterpriseRedPacket: "getRecevieBizHongBaoRequest",
|
||||
startSearchBeacons: "startMonitoringBeacons",
|
||||
stopSearchBeacons: "stopMonitoringBeacons",
|
||||
onSearchBeacons: "onBeaconsInRange",
|
||||
consumeAndShareCard: "consumedShareCard",
|
||||
openAddress: "editAddress"
|
||||
},
|
||||
a = function () {
|
||||
var e = {};
|
||||
for (var n in c) e[c[n]] = n;
|
||||
return e
|
||||
}(),
|
||||
i = window.document,
|
||||
t = i.title,
|
||||
r = navigator.userAgent.toLowerCase(),
|
||||
s = navigator.platform.toLowerCase(),
|
||||
d = !(!s.match("mac") && !s.match("win")),
|
||||
u = -1 != r.indexOf("wxdebugger"),
|
||||
l = -1 != r.indexOf("micromessenger"),
|
||||
p = -1 != r.indexOf("android"),
|
||||
f = -1 != r.indexOf("iphone") || -1 != r.indexOf("ipad"),
|
||||
m = (n = r.match(/micromessenger\/(\d+\.\d+\.\d+)/) || r.match(/micromessenger\/(\d+\.\d+)/)) ? n[1] : "",
|
||||
g = {
|
||||
initStartTime: L(),
|
||||
initEndTime: 0,
|
||||
preVerifyStartTime: 0,
|
||||
preVerifyEndTime: 0
|
||||
},
|
||||
h = {
|
||||
version: 1,
|
||||
appId: "",
|
||||
initTime: 0,
|
||||
preVerifyTime: 0,
|
||||
networkType: "",
|
||||
isPreVerifyOk: 1,
|
||||
systemType: f ? 1 : p ? 2 : -1,
|
||||
clientVersion: m,
|
||||
url: encodeURIComponent(location.href)
|
||||
},
|
||||
v = {},
|
||||
S = {
|
||||
_completes: []
|
||||
},
|
||||
y = {
|
||||
state: 0,
|
||||
data: {}
|
||||
};
|
||||
O(function () {
|
||||
g.initEndTime = L()
|
||||
});
|
||||
@ -111,9 +107,9 @@
|
||||
S._completes = []
|
||||
}
|
||||
}), w.invoke || (w.invoke = function (e, n, i) {
|
||||
o.WeixinJSBridge && WeixinJSBridge.invoke(e, x(n), i)
|
||||
window.WeixinJSBridge && WeixinJSBridge.invoke(e, x(n), i)
|
||||
}, w.on = function (e, n) {
|
||||
o.WeixinJSBridge && WeixinJSBridge.on(e, n)
|
||||
window.WeixinJSBridge && WeixinJSBridge.on(e, n)
|
||||
})
|
||||
},
|
||||
ready: function (e) {
|
||||
@ -579,7 +575,7 @@
|
||||
getEnv: function (e) {
|
||||
O(function () {
|
||||
e({
|
||||
miniprogram: "miniprogram" === o.__wxjs_environment
|
||||
miniprogram: "miniprogram" === window.__wxjs_environment
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -587,7 +583,7 @@
|
||||
},
|
||||
T = 1,
|
||||
k = {};
|
||||
return i.addEventListener("error", function (e) {
|
||||
return window.addEventListener("error", function (e) {
|
||||
if (!p) {
|
||||
var n = e.target,
|
||||
i = n.tagName,
|
||||
@ -607,7 +603,7 @@
|
||||
})
|
||||
}
|
||||
}
|
||||
}, !0), i.addEventListener("load", function (e) {
|
||||
}, !0), window.addEventListener("load", function (e) {
|
||||
if (!p) {
|
||||
var n = e.target,
|
||||
i = n.tagName;
|
||||
@ -617,17 +613,17 @@
|
||||
t && (k[t] = !1)
|
||||
}
|
||||
}
|
||||
}, !0), e && (o.wx = o.jWeixin = w), w
|
||||
}, !0), window.wx = window.jWeixin = w, w
|
||||
}
|
||||
|
||||
function M(n, e, i) {
|
||||
o.WeixinJSBridge ? WeixinJSBridge.invoke(n, x(e), function (e) {
|
||||
window.WeixinJSBridge ? WeixinJSBridge.invoke(n, x(e), function (e) {
|
||||
A(n, e, i)
|
||||
}) : B(n, i)
|
||||
}
|
||||
|
||||
function P(n, i, t) {
|
||||
o.WeixinJSBridge ? WeixinJSBridge.on(n, function (e) {
|
||||
window.WeixinJSBridge ? WeixinJSBridge.on(n, function (e) {
|
||||
t && t.trigger && t.trigger(e), A(n, e, i)
|
||||
}) : B(n, t || i)
|
||||
}
|
||||
@ -697,6 +693,9 @@
|
||||
}
|
||||
|
||||
function O(e) {
|
||||
l && (o.WeixinJSBridge ? e() : i.addEventListener && i.addEventListener("WeixinJSBridgeReady", e, !1))
|
||||
l && (window.WeixinJSBridge ? e() : i.addEventListener && i.addEventListener("WeixinJSBridgeReady", e, !1))
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 导出微信SDK
|
||||
export default initWeixin(window);
|
||||
371
src/components/AliPlayer copy.vue
Normal file
@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="ali-player-container" ref="playerContainer">
|
||||
<div :id="playerId"></div>
|
||||
<!-- 封面图 -->
|
||||
<div v-if="!playerReady" class="player-cover">
|
||||
<img :src="poster" alt="视频封面" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
videoUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['ready', 'error', 'play', 'pause', 'timeupdate']);
|
||||
|
||||
const playerContainer = ref(null);
|
||||
const playerReady = ref(false);
|
||||
let player = null;
|
||||
|
||||
// 生成唯一播放器ID
|
||||
const playerId = `ali-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const initPlayer = async () => {
|
||||
// 等待DOM渲染完成
|
||||
await nextTick();
|
||||
|
||||
// 确保容器存在
|
||||
if (!document.getElementById(playerId)) {
|
||||
console.log('等待播放器容器创建...');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保阿里播放器已加载
|
||||
if (!window.Aliplayer) {
|
||||
console.log('等待阿里播放器加载...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理视频URL
|
||||
let videoUrl = props.videoUrl;
|
||||
if (videoUrl.startsWith('https://')) {
|
||||
videoUrl = videoUrl.replace('https://', 'http://');
|
||||
}
|
||||
|
||||
// 播放器配置
|
||||
const options = {
|
||||
id: playerId,
|
||||
source: videoUrl,
|
||||
cover: props.poster,
|
||||
autoplay: false, // 初始不自动播放
|
||||
isLive: props.isLive,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
playsinline: true,
|
||||
preload: true,
|
||||
controlBarVisibility: 'hover',
|
||||
useH5Prism: true,
|
||||
x5_video_position: 'top',
|
||||
x5_type: 'h5',
|
||||
x5_fullscreen: true,
|
||||
encryptType: 1,
|
||||
format: 'm3u8',
|
||||
mediaType: 'video/x-mpegURL',
|
||||
hlsOption: {
|
||||
debug: false,
|
||||
enableWorker: true,
|
||||
enableStashBuffer: true,
|
||||
stashInitialSize: 1024 * 64,
|
||||
autoStartLoad: true,
|
||||
maxBufferSize: 1024 * 1024 * 50,
|
||||
maxBufferLength: 60,
|
||||
defaultAudioCodec: 'mp4a.40.2',
|
||||
xhrSetup: function(xhr, url) {
|
||||
xhr.withCredentials = false;
|
||||
},
|
||||
fetchSetup: function(context, initParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(context.url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Accept': 'application/x-mpegURL,application/vnd.apple.mpegurl',
|
||||
'Content-Type': 'application/x-mpegURL'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
},
|
||||
// skinLayout: [
|
||||
// { name: "bigPlayButton", align: "cc" },
|
||||
// { name: "H5Loading", align: "cc" },
|
||||
// { name: "errorDisplay", align: "tlabs" },
|
||||
// { name: "controlBar", align: "blabs", x: 0, y: 0, children: [
|
||||
// ...(props.isLive ? [] : [{ name: "progress", align: "tlabs", x: 0, y: 0 }]),
|
||||
// { name: "playButton", align: "tl", x: 15, y: 12 },
|
||||
// ...(props.isLive ? [] : [{ name: "timeDisplay", align: "tl", x: 10, y: 7 }]),
|
||||
// { name: "fullScreenButton", align: "tr", x: 10, y: 12 },
|
||||
// { name: "volume", align: "tr", x: 5, y: 10 }
|
||||
// ]}
|
||||
// ]
|
||||
skinLayout: [
|
||||
{
|
||||
name: "bigPlayButton",
|
||||
align: "cc",
|
||||
// align: "blabs",
|
||||
// x: 30,
|
||||
// y: 80,
|
||||
},
|
||||
{
|
||||
name: "H5Loading",
|
||||
align: "cc",
|
||||
},
|
||||
{
|
||||
name: "errorDisplay",
|
||||
align: "tlabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
name: "infoDisplay",
|
||||
},
|
||||
{
|
||||
name: "tooltip",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 56,
|
||||
},
|
||||
{
|
||||
name: "thumbnail",
|
||||
},
|
||||
{
|
||||
name: "controlBar",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
children: [
|
||||
{
|
||||
name: "progress",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 44,
|
||||
},
|
||||
{
|
||||
name: "playButton",
|
||||
align: "tl",
|
||||
x: 15,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "timeDisplay",
|
||||
align: "tl",
|
||||
x: 10,
|
||||
y: 7,
|
||||
},
|
||||
{
|
||||
name: "fullScreenButton",
|
||||
align: "tr",
|
||||
x: 10,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "volume",
|
||||
align: "tr",
|
||||
x: 5,
|
||||
y: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 创建播放器实例
|
||||
player = new window.Aliplayer(options, function(player) {
|
||||
console.log('播放器创建成功');
|
||||
});
|
||||
|
||||
// 监听视频加载成功
|
||||
player.on('ready', () => {
|
||||
console.log('视频准备就绪,开始播放');
|
||||
playerReady.value = true;
|
||||
// 视频准备就绪后自动播放
|
||||
player.play();
|
||||
emit('ready', player);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
player.on('error', (error) => {
|
||||
console.log('播放器错误,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
});
|
||||
|
||||
// 添加更多事件监听
|
||||
player.on('play', () => {
|
||||
console.log('开始播放');
|
||||
emit('play');
|
||||
});
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('暂停播放');
|
||||
emit('pause');
|
||||
});
|
||||
|
||||
player.on('waiting', () => {
|
||||
console.log('视频缓冲中...');
|
||||
});
|
||||
|
||||
player.on('timeupdate', () => {
|
||||
emit('timeupdate', player.getCurrentTime());
|
||||
});
|
||||
|
||||
player.on('ended', () => {
|
||||
console.log('播放结束');
|
||||
if (props.isLive) {
|
||||
setTimeout(() => {
|
||||
player.loadByUrl(videoUrl);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('初始化播放器失败,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听视频源变化
|
||||
watch(() => props.videoUrl, (newUrl, oldUrl) => {
|
||||
if (newUrl && newUrl !== oldUrl) {
|
||||
playerReady.value = false; // 重置状态,显示封面图
|
||||
if (player) {
|
||||
try {
|
||||
player.loadByUrl(newUrl);
|
||||
} catch (error) {
|
||||
console.log('加载新视频源失败,保持显示封面图:', error);
|
||||
emit('error', error);
|
||||
}
|
||||
} else {
|
||||
// 如果播放器不存在,重新初始化
|
||||
nextTick(() => {
|
||||
initPlayer();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 对外暴露的方法
|
||||
defineExpose({
|
||||
play: () => player?.play(),
|
||||
pause: () => player?.pause(),
|
||||
seek: (time) => player?.seek(time),
|
||||
getCurrentTime: () => player?.getCurrentTime(),
|
||||
getDuration: () => player?.getDuration(),
|
||||
getPlayer: () => player
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟初始化播放器,确保DOM已渲染
|
||||
nextTick(() => {
|
||||
const initInterval = setInterval(() => {
|
||||
if (document.getElementById(playerId) && window.Aliplayer) {
|
||||
clearInterval(initInterval);
|
||||
initPlayer();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 设置超时,避免无限等待
|
||||
setTimeout(() => {
|
||||
clearInterval(initInterval);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player) {
|
||||
try {
|
||||
player.dispose();
|
||||
} catch (error) {
|
||||
console.log('销毁播放器失败:', error);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// .ali-player-container {
|
||||
// position: relative;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// background: #000;
|
||||
|
||||
// [id^="ali-player"] {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// }
|
||||
|
||||
// .player-cover {
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// left: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// z-index: 2;
|
||||
// background: #000;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
|
||||
// img {
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// object-fit: contain;
|
||||
// }
|
||||
// }
|
||||
|
||||
// :deep(.prism-player) {
|
||||
// .prism-controlbar {
|
||||
// width: 90%;
|
||||
// position: absolute;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%); // 修改为-50%实现真正居中
|
||||
|
||||
// .prism-progress {
|
||||
// .prism-progress-played {
|
||||
// background: #F1253E !important;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// @media screen and (orientation: landscape) {
|
||||
// .ali-player-container {
|
||||
// height: 100vh;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
</style>
|
||||
507
src/components/AliPlayer.vue
Normal file
@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="ali-player-container" ref="playerContainer">
|
||||
<div :id="playerId" class="player-element"></div>
|
||||
<!-- 封面图 -->
|
||||
<div v-if="!playerReady" class="player-cover">
|
||||
<img :src="poster" alt="视频封面" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
videoUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['ready', 'error', 'play', 'pause', 'timeupdate']);
|
||||
|
||||
const playerContainer = ref(null);
|
||||
const playerReady = ref(false);
|
||||
let player = null;
|
||||
|
||||
// 生成唯一播放器ID
|
||||
const playerId = `ali-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// 检测当前设备
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
|
||||
// 定义全屏变化处理函数
|
||||
const handleFullScreenChange = () => {
|
||||
const isFullScreen = document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement;
|
||||
|
||||
if (isFullScreen) {
|
||||
console.log('进入全屏模式');
|
||||
// 可以添加全屏时的特殊处理
|
||||
} else {
|
||||
console.log('退出全屏模式');
|
||||
// 可以添加退出全屏时的特殊处理
|
||||
}
|
||||
};
|
||||
|
||||
const initPlayer = async () => {
|
||||
// 等待DOM渲染完成
|
||||
await nextTick();
|
||||
|
||||
// 确保容器存在
|
||||
if (!document.getElementById(playerId)) {
|
||||
console.log('等待播放器容器创建...');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保阿里播放器已加载
|
||||
if (!window.Aliplayer) {
|
||||
console.log('等待阿里播放器加载...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理视频URL
|
||||
let videoUrl = props.videoUrl;
|
||||
if (videoUrl.startsWith('https://')) {
|
||||
videoUrl = videoUrl.replace('https://', 'http://');
|
||||
}
|
||||
|
||||
// 针对移动端的特殊配置
|
||||
const mobileConfig = {
|
||||
// iOS特有配置
|
||||
...(isIOS ? {
|
||||
// iOS需要的特殊配置
|
||||
playsinline: true, // iOS上内联播放
|
||||
// iOS 10+支持的内联播放配置
|
||||
"webkit-playsinline": true,
|
||||
// iOS微信特殊处理
|
||||
"x5-playsinline": true,
|
||||
} : {}),
|
||||
|
||||
// Android特有配置
|
||||
...(isAndroid ? {
|
||||
// 腾讯X5内核配置
|
||||
"x5-video-player-type": "h5-page", // 使用H5播放器
|
||||
"x5-video-player-fullscreen": true, // 全屏模式
|
||||
"x5-video-orientation": "landscape", // 横屏
|
||||
// 华为浏览器配置
|
||||
"x5-video-ignore-metadata-size": true,
|
||||
} : {})
|
||||
};
|
||||
|
||||
// 播放器配置
|
||||
const options = {
|
||||
id: playerId,
|
||||
source: videoUrl,
|
||||
cover: props.poster,
|
||||
autoplay: false, // 初始不自动播放
|
||||
isLive: props.isLive,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
playsinline: true, // iOS内联播放
|
||||
"webkit-playsinline": true, // iOS Safari
|
||||
"x5-playsinline": true, // 微信浏览器
|
||||
"x5-video-player-type": "h5", // 启用H5播放器
|
||||
"x5-video-player-fullscreen": true, // 全屏时使用H5播放器
|
||||
"x5-video-orientation": "portraint", // 竖屏播放
|
||||
preload: true,
|
||||
controlBarVisibility: 'hover',
|
||||
useH5Prism: true, // 使用H5播放器
|
||||
encryptType: 1,
|
||||
format: 'm3u8',
|
||||
mediaType: 'video/x-mpegURL',
|
||||
rePlay: false, // 播放结束后不自动重播
|
||||
// 合并针对移动端的特殊配置
|
||||
...mobileConfig,
|
||||
hlsOption: {
|
||||
debug: false,
|
||||
enableWorker: true,
|
||||
enableStashBuffer: true,
|
||||
stashInitialSize: 1024 * 64,
|
||||
autoStartLoad: true,
|
||||
maxBufferSize: 1024 * 1024 * 50,
|
||||
maxBufferLength: 60,
|
||||
defaultAudioCodec: 'mp4a.40.2',
|
||||
xhrSetup: function(xhr, url) {
|
||||
xhr.withCredentials = false;
|
||||
},
|
||||
fetchSetup: function(context, initParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(context.url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Accept': 'application/x-mpegURL,application/vnd.apple.mpegurl',
|
||||
'Content-Type': 'application/x-mpegURL'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
},
|
||||
skinLayout: [
|
||||
{
|
||||
name: "bigPlayButton",
|
||||
align: "cc",
|
||||
},
|
||||
{
|
||||
name: "H5Loading",
|
||||
align: "cc",
|
||||
},
|
||||
{
|
||||
name: "errorDisplay",
|
||||
align: "tlabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
name: "infoDisplay",
|
||||
},
|
||||
{
|
||||
name: "tooltip",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 56,
|
||||
},
|
||||
{
|
||||
name: "thumbnail",
|
||||
},
|
||||
{
|
||||
name: "controlBar",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
children: [
|
||||
{
|
||||
name: "progress",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 44,
|
||||
},
|
||||
{
|
||||
name: "playButton",
|
||||
align: "tl",
|
||||
x: 15,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "timeDisplay",
|
||||
align: "tl",
|
||||
x: 10,
|
||||
y: 7,
|
||||
},
|
||||
{
|
||||
name: "fullScreenButton",
|
||||
align: "tr",
|
||||
x: 10,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "volume",
|
||||
align: "tr",
|
||||
x: 5,
|
||||
y: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 创建播放器实例
|
||||
player = new window.Aliplayer(options, function(player) {
|
||||
console.log('播放器创建成功');
|
||||
|
||||
// 添加事件监听,处理全屏进入退出
|
||||
document.addEventListener('fullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('webkitfullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('mozfullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('MSFullscreenChange', handleFullScreenChange);
|
||||
});
|
||||
|
||||
// 监听视频加载成功
|
||||
player.on('ready', () => {
|
||||
console.log('视频准备就绪,开始播放');
|
||||
playerReady.value = true;
|
||||
|
||||
// 针对iOS设备的特殊处理
|
||||
if (isIOS) {
|
||||
const video = document.querySelector(`#${playerId} video`);
|
||||
if (video) {
|
||||
video.setAttribute('playsinline', 'playsinline');
|
||||
video.setAttribute('webkit-playsinline', 'true');
|
||||
video.setAttribute('x5-playsinline', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// 视频准备就绪后自动播放
|
||||
player.play();
|
||||
emit('ready', player);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
player.on('error', (error) => {
|
||||
console.log('播放器错误,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
});
|
||||
|
||||
// 添加更多事件监听
|
||||
player.on('play', () => {
|
||||
console.log('开始播放');
|
||||
emit('play');
|
||||
});
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('暂停播放');
|
||||
emit('pause');
|
||||
});
|
||||
|
||||
player.on('waiting', () => {
|
||||
console.log('视频缓冲中...');
|
||||
});
|
||||
|
||||
player.on('timeupdate', () => {
|
||||
emit('timeupdate', player.getCurrentTime());
|
||||
});
|
||||
|
||||
player.on('ended', () => {
|
||||
console.log('播放结束');
|
||||
if (props.isLive) {
|
||||
setTimeout(() => {
|
||||
player.loadByUrl(videoUrl);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听全屏按钮点击
|
||||
player.on('requestFullScreen', () => {
|
||||
console.log('用户请求全屏');
|
||||
|
||||
// 针对iOS的特殊处理
|
||||
if (isIOS) {
|
||||
const video = document.querySelector(`#${playerId} video`);
|
||||
if (video && video.webkitEnterFullscreen) {
|
||||
try {
|
||||
video.webkitEnterFullscreen();
|
||||
} catch (e) {
|
||||
console.error('iOS进入全屏失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('初始化播放器失败,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听视频源变化
|
||||
watch(() => props.videoUrl, (newUrl, oldUrl) => {
|
||||
if (newUrl && newUrl !== oldUrl) {
|
||||
playerReady.value = false; // 重置状态,显示封面图
|
||||
if (player) {
|
||||
try {
|
||||
player.loadByUrl(newUrl);
|
||||
} catch (error) {
|
||||
console.log('加载新视频源失败,保持显示封面图:', error);
|
||||
emit('error', error);
|
||||
}
|
||||
} else {
|
||||
// 如果播放器不存在,重新初始化
|
||||
nextTick(() => {
|
||||
initPlayer();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 对外暴露的方法
|
||||
defineExpose({
|
||||
play: () => player?.play(),
|
||||
pause: () => player?.pause(),
|
||||
seek: (time) => player?.seek(time),
|
||||
getCurrentTime: () => player?.getCurrentTime(),
|
||||
getDuration: () => player?.getDuration(),
|
||||
getPlayer: () => player,
|
||||
requestFullscreen: () => {
|
||||
if (player) {
|
||||
player.requestFullScreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟初始化播放器,确保DOM已渲染
|
||||
nextTick(() => {
|
||||
const initInterval = setInterval(() => {
|
||||
if (document.getElementById(playerId) && window.Aliplayer) {
|
||||
clearInterval(initInterval);
|
||||
initPlayer();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 设置超时,避免无限等待
|
||||
setTimeout(() => {
|
||||
clearInterval(initInterval);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除全屏事件监听
|
||||
document.removeEventListener('fullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('webkitfullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('mozfullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('MSFullscreenChange', handleFullScreenChange);
|
||||
|
||||
if (player) {
|
||||
try {
|
||||
player.dispose();
|
||||
} catch (error) {
|
||||
console.log('销毁播放器失败:', error);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ali-player-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
z-index: 1;
|
||||
|
||||
.player-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[id^="ali-player"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.player-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义阿里播放器样式 */
|
||||
:deep(.prism-player) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.prism-controlbar {
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(5%);
|
||||
bottom: 10px;
|
||||
|
||||
.prism-progress {
|
||||
.prism-progress-played {
|
||||
background: #F1253E !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增样式:播放按钮左对齐
|
||||
.prism-play-btn.playing {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.prism-play-btn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
// 新增样式:全屏按钮右对齐
|
||||
.prism-fullscreen-btn {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏模式样式 */
|
||||
:deep(.prism-player .prism-fullscreen) {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
|
||||
video {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式优化 */
|
||||
@media screen and (orientation: landscape) {
|
||||
.ali-player-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
:deep(.prism-player) {
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS特殊处理 */
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
.ali-player-container {
|
||||
/* iOS特有CSS属性 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:deep(.prism-player video) {
|
||||
/* iOS视频全屏处理 */
|
||||
position: static !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
507
src/components/AliPlayer备份.vue
Normal file
@ -0,0 +1,507 @@
|
||||
<template>
|
||||
<div class="ali-player-container" ref="playerContainer">
|
||||
<div :id="playerId" class="player-element"></div>
|
||||
<!-- 封面图 -->
|
||||
<div v-if="!playerReady" class="player-cover">
|
||||
<img :src="poster" alt="视频封面" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
videoUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['ready', 'error', 'play', 'pause', 'timeupdate']);
|
||||
|
||||
const playerContainer = ref(null);
|
||||
const playerReady = ref(false);
|
||||
let player = null;
|
||||
|
||||
// 生成唯一播放器ID
|
||||
const playerId = `ali-player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// 检测当前设备
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
|
||||
// 定义全屏变化处理函数
|
||||
const handleFullScreenChange = () => {
|
||||
const isFullScreen = document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement;
|
||||
|
||||
if (isFullScreen) {
|
||||
console.log('进入全屏模式');
|
||||
// 可以添加全屏时的特殊处理
|
||||
} else {
|
||||
console.log('退出全屏模式');
|
||||
// 可以添加退出全屏时的特殊处理
|
||||
}
|
||||
};
|
||||
|
||||
const initPlayer = async () => {
|
||||
// 等待DOM渲染完成
|
||||
await nextTick();
|
||||
|
||||
// 确保容器存在
|
||||
if (!document.getElementById(playerId)) {
|
||||
console.log('等待播放器容器创建...');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保阿里播放器已加载
|
||||
if (!window.Aliplayer) {
|
||||
console.log('等待阿里播放器加载...');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理视频URL
|
||||
let videoUrl = props.videoUrl;
|
||||
if (videoUrl.startsWith('https://')) {
|
||||
videoUrl = videoUrl.replace('https://', 'http://');
|
||||
}
|
||||
|
||||
// 针对移动端的特殊配置
|
||||
const mobileConfig = {
|
||||
// iOS特有配置
|
||||
...(isIOS ? {
|
||||
// iOS需要的特殊配置
|
||||
playsinline: true, // iOS上内联播放
|
||||
// iOS 10+支持的内联播放配置
|
||||
"webkit-playsinline": true,
|
||||
// iOS微信特殊处理
|
||||
"x5-playsinline": true,
|
||||
} : {}),
|
||||
|
||||
// Android特有配置
|
||||
...(isAndroid ? {
|
||||
// 腾讯X5内核配置
|
||||
"x5-video-player-type": "h5-page", // 使用H5播放器
|
||||
"x5-video-player-fullscreen": true, // 全屏模式
|
||||
"x5-video-orientation": "landscape", // 横屏
|
||||
// 华为浏览器配置
|
||||
"x5-video-ignore-metadata-size": true,
|
||||
} : {})
|
||||
};
|
||||
|
||||
// 播放器配置
|
||||
const options = {
|
||||
id: playerId,
|
||||
source: videoUrl,
|
||||
cover: props.poster,
|
||||
autoplay: false, // 初始不自动播放
|
||||
isLive: props.isLive,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
playsinline: true, // iOS内联播放
|
||||
"webkit-playsinline": true, // iOS Safari
|
||||
"x5-playsinline": true, // 微信浏览器
|
||||
"x5-video-player-type": "h5", // 启用H5播放器
|
||||
"x5-video-player-fullscreen": true, // 全屏时使用H5播放器
|
||||
"x5-video-orientation": "portraint", // 竖屏播放
|
||||
preload: true,
|
||||
controlBarVisibility: 'hover',
|
||||
useH5Prism: true, // 使用H5播放器
|
||||
encryptType: 1,
|
||||
format: 'm3u8',
|
||||
mediaType: 'video/x-mpegURL',
|
||||
rePlay: false, // 播放结束后不自动重播
|
||||
// 合并针对移动端的特殊配置
|
||||
...mobileConfig,
|
||||
hlsOption: {
|
||||
debug: false,
|
||||
enableWorker: true,
|
||||
enableStashBuffer: true,
|
||||
stashInitialSize: 1024 * 64,
|
||||
autoStartLoad: true,
|
||||
maxBufferSize: 1024 * 1024 * 50,
|
||||
maxBufferLength: 60,
|
||||
defaultAudioCodec: 'mp4a.40.2',
|
||||
xhrSetup: function(xhr, url) {
|
||||
xhr.withCredentials = false;
|
||||
},
|
||||
fetchSetup: function(context, initParams) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(context.url, {
|
||||
method: 'GET',
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
headers: {
|
||||
'Accept': 'application/x-mpegURL,application/vnd.apple.mpegurl',
|
||||
'Content-Type': 'application/x-mpegURL'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
},
|
||||
skinLayout: [
|
||||
{
|
||||
name: "bigPlayButton",
|
||||
align: "cc",
|
||||
},
|
||||
{
|
||||
name: "H5Loading",
|
||||
align: "cc",
|
||||
},
|
||||
{
|
||||
name: "errorDisplay",
|
||||
align: "tlabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
{
|
||||
name: "infoDisplay",
|
||||
},
|
||||
{
|
||||
name: "tooltip",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 56,
|
||||
},
|
||||
{
|
||||
name: "thumbnail",
|
||||
},
|
||||
{
|
||||
name: "controlBar",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
children: [
|
||||
{
|
||||
name: "progress",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 44,
|
||||
},
|
||||
{
|
||||
name: "playButton",
|
||||
align: "tl",
|
||||
x: 15,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "timeDisplay",
|
||||
align: "tl",
|
||||
x: 10,
|
||||
y: 7,
|
||||
},
|
||||
{
|
||||
name: "fullScreenButton",
|
||||
align: "tr",
|
||||
x: 10,
|
||||
y: 12,
|
||||
},
|
||||
{
|
||||
name: "volume",
|
||||
align: "tr",
|
||||
x: 5,
|
||||
y: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 创建播放器实例
|
||||
player = new window.Aliplayer(options, function(player) {
|
||||
console.log('播放器创建成功');
|
||||
|
||||
// 添加事件监听,处理全屏进入退出
|
||||
document.addEventListener('fullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('webkitfullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('mozfullscreenchange', handleFullScreenChange);
|
||||
document.addEventListener('MSFullscreenChange', handleFullScreenChange);
|
||||
});
|
||||
|
||||
// 监听视频加载成功
|
||||
player.on('ready', () => {
|
||||
console.log('视频准备就绪,开始播放');
|
||||
playerReady.value = true;
|
||||
|
||||
// 针对iOS设备的特殊处理
|
||||
if (isIOS) {
|
||||
const video = document.querySelector(`#${playerId} video`);
|
||||
if (video) {
|
||||
video.setAttribute('playsinline', 'playsinline');
|
||||
video.setAttribute('webkit-playsinline', 'true');
|
||||
video.setAttribute('x5-playsinline', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
// 视频准备就绪后自动播放
|
||||
player.play();
|
||||
emit('ready', player);
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
player.on('error', (error) => {
|
||||
console.log('播放器错误,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
});
|
||||
|
||||
// 添加更多事件监听
|
||||
player.on('play', () => {
|
||||
console.log('开始播放');
|
||||
emit('play');
|
||||
});
|
||||
|
||||
player.on('pause', () => {
|
||||
console.log('暂停播放');
|
||||
emit('pause');
|
||||
});
|
||||
|
||||
player.on('waiting', () => {
|
||||
console.log('视频缓冲中...');
|
||||
});
|
||||
|
||||
player.on('timeupdate', () => {
|
||||
emit('timeupdate', player.getCurrentTime());
|
||||
});
|
||||
|
||||
player.on('ended', () => {
|
||||
console.log('播放结束');
|
||||
if (props.isLive) {
|
||||
setTimeout(() => {
|
||||
player.loadByUrl(videoUrl);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听全屏按钮点击
|
||||
player.on('requestFullScreen', () => {
|
||||
console.log('用户请求全屏');
|
||||
|
||||
// 针对iOS的特殊处理
|
||||
if (isIOS) {
|
||||
const video = document.querySelector(`#${playerId} video`);
|
||||
if (video && video.webkitEnterFullscreen) {
|
||||
try {
|
||||
video.webkitEnterFullscreen();
|
||||
} catch (e) {
|
||||
console.error('iOS进入全屏失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.log('初始化播放器失败,保持显示封面图:', error);
|
||||
playerReady.value = false;
|
||||
emit('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听视频源变化
|
||||
watch(() => props.videoUrl, (newUrl, oldUrl) => {
|
||||
if (newUrl && newUrl !== oldUrl) {
|
||||
playerReady.value = false; // 重置状态,显示封面图
|
||||
if (player) {
|
||||
try {
|
||||
player.loadByUrl(newUrl);
|
||||
} catch (error) {
|
||||
console.log('加载新视频源失败,保持显示封面图:', error);
|
||||
emit('error', error);
|
||||
}
|
||||
} else {
|
||||
// 如果播放器不存在,重新初始化
|
||||
nextTick(() => {
|
||||
initPlayer();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 对外暴露的方法
|
||||
defineExpose({
|
||||
play: () => player?.play(),
|
||||
pause: () => player?.pause(),
|
||||
seek: (time) => player?.seek(time),
|
||||
getCurrentTime: () => player?.getCurrentTime(),
|
||||
getDuration: () => player?.getDuration(),
|
||||
getPlayer: () => player,
|
||||
requestFullscreen: () => {
|
||||
if (player) {
|
||||
player.requestFullScreen();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 延迟初始化播放器,确保DOM已渲染
|
||||
nextTick(() => {
|
||||
const initInterval = setInterval(() => {
|
||||
if (document.getElementById(playerId) && window.Aliplayer) {
|
||||
clearInterval(initInterval);
|
||||
initPlayer();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 设置超时,避免无限等待
|
||||
setTimeout(() => {
|
||||
clearInterval(initInterval);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 移除全屏事件监听
|
||||
document.removeEventListener('fullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('webkitfullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('mozfullscreenchange', handleFullScreenChange);
|
||||
document.removeEventListener('MSFullscreenChange', handleFullScreenChange);
|
||||
|
||||
if (player) {
|
||||
try {
|
||||
player.dispose();
|
||||
} catch (error) {
|
||||
console.log('销毁播放器失败:', error);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ali-player-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
z-index: 1;
|
||||
|
||||
.player-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
[id^="ali-player"] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.player-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/* 自定义阿里播放器样式 */
|
||||
:deep(.prism-player) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.prism-controlbar {
|
||||
width: 90%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(5%);
|
||||
bottom: 10px;
|
||||
|
||||
.prism-progress {
|
||||
.prism-progress-played {
|
||||
background: #F1253E !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增样式:播放按钮左对齐
|
||||
.prism-play-btn.playing {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
.prism-play-btn {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
// 新增样式:全屏按钮右对齐
|
||||
.prism-fullscreen-btn {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 全屏模式样式 */
|
||||
:deep(.prism-player .prism-fullscreen) {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
|
||||
video {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式优化 */
|
||||
@media screen and (orientation: landscape) {
|
||||
.ali-player-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
:deep(.prism-player) {
|
||||
height: 100vh !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS特殊处理 */
|
||||
@supports (-webkit-overflow-scrolling: touch) {
|
||||
.ali-player-container {
|
||||
/* iOS特有CSS属性 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
:deep(.prism-player video) {
|
||||
/* iOS视频全屏处理 */
|
||||
position: static !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
388
src/components/VRPlayer.vue
Normal file
@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="vr-player-container" ref="playerContainer">
|
||||
<video
|
||||
ref="videoElement"
|
||||
class="video-js vjs-default-skin"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
crossorigin="anonymous"
|
||||
>
|
||||
</video>
|
||||
|
||||
<!-- iOS陀螺仪权限提示 -->
|
||||
<div class="gyro-tip" v-if="showGyroTip" @click="requestGyroPermission">
|
||||
<div class="tip-content">
|
||||
<span class="tip-icon">📱</span>
|
||||
<span>点击开启陀螺仪,体验更好的VR效果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
videoUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isLive: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
projection: {
|
||||
type: String,
|
||||
default: '360' // 可选: '360', '180', '360_LR', '360_TB', 'EAC', 'EAC_LR'等
|
||||
},
|
||||
autoplay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
poster: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['ready', 'error', 'play', 'pause', 'timeupdate', 'fullscreenchange']);
|
||||
|
||||
const playerContainer = ref(null);
|
||||
const videoElement = ref(null);
|
||||
const showGyroTip = ref(false);
|
||||
let player = null;
|
||||
|
||||
// 初始化播放器
|
||||
const initPlayer = () => {
|
||||
if (!videoElement.value) {
|
||||
console.error('视频元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始初始化VR播放器:', {
|
||||
videoUrl: props.videoUrl,
|
||||
isLive: props.isLive,
|
||||
projection: props.projection
|
||||
});
|
||||
|
||||
try {
|
||||
// 检查videojs是否可用
|
||||
if (!window.videojs) {
|
||||
throw new Error('Video.js未加载');
|
||||
}
|
||||
|
||||
// 播放器配置
|
||||
const options = {
|
||||
controls: true,
|
||||
autoplay: props.autoplay,
|
||||
fluid: true,
|
||||
playsinline: true,
|
||||
poster: props.poster,
|
||||
sources: [{
|
||||
src: props.videoUrl,
|
||||
type: props.isLive ? 'application/x-mpegURL' : 'video/mp4'
|
||||
}],
|
||||
controlBar: {
|
||||
volumePanel: {
|
||||
inline: true
|
||||
},
|
||||
pictureInPictureToggle: false
|
||||
}
|
||||
};
|
||||
|
||||
console.log('播放器配置:', options);
|
||||
|
||||
// 如果已存在播放器实例,先销毁
|
||||
if (player) {
|
||||
console.log('销毁已存在的播放器实例');
|
||||
player.dispose();
|
||||
player = null;
|
||||
}
|
||||
|
||||
// 创建播放器实例
|
||||
player = window.videojs(videoElement.value, options, function onPlayerReady() {
|
||||
console.log('播放器准备就绪');
|
||||
|
||||
// 初始化VR插件
|
||||
try {
|
||||
this.vr({
|
||||
projection: props.projection,
|
||||
debug: true,
|
||||
forceCardboard: false,
|
||||
motionControls: true,
|
||||
sphereDetail: 32,
|
||||
initFov: 100,
|
||||
crossOrigin: 'anonymous'
|
||||
});
|
||||
|
||||
console.log('VR插件初始化成功');
|
||||
|
||||
// 监听VR模式就绪
|
||||
this.on('vrready', () => {
|
||||
console.log('VR模式就绪');
|
||||
checkDeviceSupport();
|
||||
});
|
||||
} catch (vrError) {
|
||||
console.error('VR插件初始化失败:', vrError);
|
||||
emit('error', vrError);
|
||||
}
|
||||
});
|
||||
|
||||
// 事件监听
|
||||
player.on('ready', () => {
|
||||
console.log('播放器ready事件触发');
|
||||
emit('ready', player);
|
||||
});
|
||||
|
||||
player.on('error', (error) => {
|
||||
console.error('播放器错误:', error);
|
||||
emit('error', error);
|
||||
});
|
||||
|
||||
player.on('loadedmetadata', () => {
|
||||
console.log('视频元数据加载完成');
|
||||
if (player.tech(true).vhs) {
|
||||
const qualities = player.tech(true).vhs.representations();
|
||||
console.log('可用清晰度:', qualities);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听全屏变化
|
||||
player.on('fullscreenchange', () => {
|
||||
emit('fullscreenchange', player.isFullscreen());
|
||||
if (player.isFullscreen()) {
|
||||
lockLandscape();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化播放器失败:', error);
|
||||
emit('error', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查设备支持
|
||||
const checkDeviceSupport = () => {
|
||||
const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
||||
const hasGyro = window.DeviceOrientationEvent !== undefined;
|
||||
|
||||
if (isMobile && hasGyro) {
|
||||
if (videojs.browser.IS_IOS) {
|
||||
checkGyroPermission();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 检查陀螺仪权限
|
||||
const checkGyroPermission = () => {
|
||||
if (typeof DeviceOrientationEvent !== 'undefined' &&
|
||||
typeof DeviceOrientationEvent.requestPermission === 'function') {
|
||||
showGyroTip.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// 请求陀螺仪权限
|
||||
const requestGyroPermission = async () => {
|
||||
try {
|
||||
const permission = await DeviceOrientationEvent.requestPermission();
|
||||
if (permission === 'granted') {
|
||||
showGyroTip.value = false;
|
||||
console.log('陀螺仪权限已授权');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取陀螺仪权限失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 锁定横屏
|
||||
const lockLandscape = () => {
|
||||
try {
|
||||
if (screen.orientation && screen.orientation.lock) {
|
||||
screen.orientation.lock('landscape').catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('横屏锁定失败:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听属性变化
|
||||
watch(() => props.videoUrl, (newUrl, oldUrl) => {
|
||||
console.log('视频URL变化:', { newUrl, oldUrl });
|
||||
if (player && newUrl && newUrl !== oldUrl) {
|
||||
console.log('更新视频源');
|
||||
player.src({
|
||||
src: newUrl,
|
||||
type: props.isLive ? 'application/x-mpegURL' : 'video/mp4'
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
watch(() => props.projection, (newProjection) => {
|
||||
if (player && player.vr()) {
|
||||
player.vr().projection(newProjection);
|
||||
}
|
||||
});
|
||||
|
||||
// 对外暴露的方法
|
||||
defineExpose({
|
||||
play: () => player?.play(),
|
||||
pause: () => player?.pause(),
|
||||
seek: (time) => player?.currentTime(time),
|
||||
toggleFullscreen: () => player?.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(),
|
||||
getPlayer: () => player
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
initPlayer();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player) {
|
||||
player.dispose();
|
||||
player = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.vr-player-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
|
||||
:deep(.video-js) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 优化移动端控制栏
|
||||
.vjs-control-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.vjs-progress-control {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 5px;
|
||||
|
||||
.vjs-progress-holder {
|
||||
margin: 0;
|
||||
border-radius: 2px;
|
||||
|
||||
.vjs-play-progress {
|
||||
background: #ff0000;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.vjs-load-progress {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-play-control {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.vjs-current-time {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.vjs-time-divider {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.vjs-duration {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.vjs-volume-panel {
|
||||
order: 5;
|
||||
}
|
||||
|
||||
.vjs-fullscreen-control {
|
||||
order: 6;
|
||||
}
|
||||
}
|
||||
|
||||
// VR模式按钮样式
|
||||
.vjs-vr-controls {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 1;
|
||||
|
||||
button {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gyro-tip {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
|
||||
.tip-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
|
||||
.tip-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式优化 */
|
||||
@media screen and (orientation: landscape) {
|
||||
.vr-player-container {
|
||||
:deep(.video-js) {
|
||||
.vjs-control-bar {
|
||||
height: 50px;
|
||||
padding: 0 15px;
|
||||
|
||||
.vjs-progress-control {
|
||||
top: -15px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.vjs-play-control,
|
||||
.vjs-volume-panel,
|
||||
.vjs-fullscreen-control {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.vjs-current-time,
|
||||
.vjs-duration {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,114 +1,170 @@
|
||||
<template>
|
||||
<div class="all_list">
|
||||
<div class="grid_list">
|
||||
<div class="list_item" v-for="(item, index) in moreList" :key="index">
|
||||
<img :src="item.img" alt="Event 1" @click="goDetail(item)" />
|
||||
<p>{{item.title}}</p>
|
||||
<div class="list_item" v-for="(item, index) in displayList" :key="index">
|
||||
<div class="status-tag" :class="getStatusClass(item.liveState)">
|
||||
{{ getStatusText(item.liveState) }}
|
||||
</div>
|
||||
<img :src="item.liveImageShow" alt="Event Cover" @click="goDetail(item)" />
|
||||
<p>{{ item.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="displayList.length === 0" class="empty-list">
|
||||
<p>暂无内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import pic from '../assets/img/top1.png';
|
||||
import {useRouter} from 'vue-router';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const moreList =ref([
|
||||
{
|
||||
id: 1,
|
||||
title: "章丘区高官寨甜瓜季乡村好时节活动",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "中华人民共和国第十四届全国人民代表大会",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "泉在济南·龙山文化旅游季活动",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "乡村好时节·花开北市樱花旅游节",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "国际学校名校展",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "爱牙医生来",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: "宏观经济形势与政策",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "学在济南·聚在泉城",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "文明济南·礼安齐鲁",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "民主主题记者会",
|
||||
img:pic,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
title: "学在济南·聚在泉城",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
title: "文明济南·礼安齐鲁",
|
||||
img:pic
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
title: "民主主题记者会",
|
||||
img:pic,
|
||||
}
|
||||
])
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
});
|
||||
|
||||
const moreList = ref([]);
|
||||
|
||||
// 计算属性:根据props.items或moreList显示内容
|
||||
const displayList = computed(() => {
|
||||
return props.items.length > 0 ? props.items : moreList.value;
|
||||
});
|
||||
|
||||
// 根据直播状态获取状态文本
|
||||
const getStatusText = (liveState) => {
|
||||
switch (Number(liveState)) {
|
||||
case 0:
|
||||
return '预告';
|
||||
case 1:
|
||||
return '直播中';
|
||||
case 2:
|
||||
return '回顾';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 根据直播状态获取CSS类名
|
||||
const getStatusClass = (liveState) => {
|
||||
switch (Number(liveState)) {
|
||||
case 0:
|
||||
return 'live_coming';
|
||||
case 1:
|
||||
return 'live_status';
|
||||
case 2:
|
||||
return 'live_review';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const goDetail = (item) => {
|
||||
console.log(item)
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
query: { item: item.id }
|
||||
console.log('跳转到详情页:', item);
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
});
|
||||
};
|
||||
|
||||
})
|
||||
}
|
||||
// 外部调用的方法,更新列表数据
|
||||
const updateList = (newList) => {
|
||||
if (Array.isArray(newList)) {
|
||||
moreList.value = newList;
|
||||
console.log('更新列表数据:', moreList.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
updateList
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.all_list {
|
||||
width: 90%;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
margin: 10px auto;
|
||||
height: calc(100vh - 120px);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.grid_list {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
gap: 5px 10px;
|
||||
|
||||
.list_item {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 102px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
transition: transform 0.3s ease;
|
||||
border-radius: 8px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
|
||||
&.live_coming {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
&.live_status {
|
||||
background: #f44336;
|
||||
}
|
||||
|
||||
&.live_review {
|
||||
background: #4caf50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-list {
|
||||
width: 100%;
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -15,12 +15,12 @@
|
||||
</div> -->
|
||||
<div class="main">
|
||||
<div class="main_left">
|
||||
<P> 距离直播开始还有:</P>
|
||||
<p class="time">{{ h }}:{{ m }}:{{ s }}</p>
|
||||
<p>距离直播开始还有:</p>
|
||||
<p class="time">{{ formatTime() }}</p>
|
||||
</div>
|
||||
<div class="main_left">
|
||||
<p class="views">500人次观看</p>
|
||||
<button>预告</button>
|
||||
<div class="main_right">
|
||||
<p class="views" v-if="viewCount">{{ viewCount }}人次观看</p>
|
||||
<div class="status-badge">预告</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -28,33 +28,47 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
const props = defineProps(["countdownTime"]);
|
||||
const showTime = ref(0);
|
||||
const timeInterval = ref(null);
|
||||
const props = defineProps({
|
||||
countdownTime: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
viewCount: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
});
|
||||
|
||||
// 定义emit事件
|
||||
const emit = defineEmits(['countdown-end']);
|
||||
|
||||
const totalTime = ref(0);
|
||||
const d = ref("00");
|
||||
const h = ref("00");
|
||||
const m = ref("00");
|
||||
const s = ref("00");
|
||||
let timeInterval = null;
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timeInterval.value);
|
||||
clearInterval(timeInterval);
|
||||
});
|
||||
|
||||
function init() {
|
||||
console.log("[init] --> ", props.countdownTime);
|
||||
totalTime.value = props.countdownTime;
|
||||
timeInterval.value = setInterval(() => {
|
||||
parseTime(totalTime.value);
|
||||
|
||||
timeInterval = setInterval(() => {
|
||||
totalTime.value--;
|
||||
parseTime(totalTime.value);
|
||||
|
||||
totalTime.value--;
|
||||
if (totalTime.value < 0) {
|
||||
clearInterval(timeInterval.value);
|
||||
location.replace(location.href);
|
||||
if (totalTime.value <= 0) {
|
||||
clearInterval(timeInterval);
|
||||
// 不再刷新页面,而是触发事件通知父组件
|
||||
emit('countdown-end');
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@ -72,39 +86,61 @@ function parseTime(total) {
|
||||
m.value = min < 10 ? "0" + min : min;
|
||||
s.value = second < 10 ? "0" + second : second;
|
||||
}
|
||||
|
||||
function formatTime() {
|
||||
if (d.value !== "00") {
|
||||
return `${d.value}天 ${h.value}:${m.value}:${s.value}`;
|
||||
}
|
||||
return `${h.value}:${m.value}:${s.value}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main{
|
||||
width: 100%;
|
||||
padding:10px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height:80px;
|
||||
.main {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.main_left{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
.time{
|
||||
font-size: 36px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
.views{
|
||||
font-size: 12px;
|
||||
}
|
||||
p{
|
||||
font-size: 16px;
|
||||
}
|
||||
button{
|
||||
padding:5px 15px;
|
||||
background-color: #D7000F;
|
||||
border:none;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.main_left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.main_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
.views {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
background-color: #D7000F;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="header">
|
||||
<div class="top"></div>
|
||||
<!-- <div class="top"></div> -->
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<img src="../assets/img/logo.png" alt="" />
|
||||
@ -39,15 +39,29 @@ const onSearch = () => {
|
||||
::v-deep(.van-search){
|
||||
padding:0;
|
||||
}
|
||||
::v-deep(.van-search__content){
|
||||
background: rgba($color: #fff, $alpha: 0.3);
|
||||
color:#fff;
|
||||
.van-icon-search {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.van-field__control::placeholder {
|
||||
color: #fff !important;
|
||||
opacity: 0.8;
|
||||
|
||||
}
|
||||
}
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(27, 163, 251, 100%),
|
||||
70%,
|
||||
rgba(27, 163, 251, 45%)
|
||||
);
|
||||
// background: linear-gradient(
|
||||
|
||||
// to bottom,
|
||||
// rgba(27, 163, 251, 100%),
|
||||
// 70%,
|
||||
// rgba(27, 163, 251, 45%)
|
||||
// );
|
||||
display: flex;
|
||||
.header-content {
|
||||
width: 90%;
|
||||
|
||||
@ -2,13 +2,12 @@
|
||||
<div class="moreLive">
|
||||
<p>更多直播</p>
|
||||
<div class="live_list">
|
||||
<div class="moreLive-list" v-for="(item, index) in lives" :key="index" @click="goDetail(item)">
|
||||
<img src="../assets/img/big1.png" />
|
||||
<div class="moreLive-list" v-for="(item, index) in liveList" :key="index" @click="goDetail(item)">
|
||||
<img :src="item.liveImageShow" :alt="item.title">
|
||||
<div class="live_info">
|
||||
<div class="live_title">
|
||||
<!-- <p :class="{ 'live_status': item.status === '直播中', 'live_coming': item.status === '预告', 'live_review': item.status === '精彩回顾' }">{{ item.status }}</p> -->
|
||||
<p :class="{ 'live_status': getStatusClass(item.status) === '直播中', 'live_coming': getStatusClass(item.status) === '预告', 'live_review': getStatusClass(item.status) === '精彩回顾' }">{{ getStatusText(item.status) }}</p>
|
||||
<p class="live_see">{{item.viewers}}人次观看</p>
|
||||
<p :class="{ 'live_status': getStatusClass(item.liveState) === '直播中', 'live_coming': getStatusClass(item.liveState) === '预告', 'live_review': getStatusClass(item.liveState) === '精彩回顾' }">{{ getStatusText(item.liveState) }}</p>
|
||||
<p class="live_see">{{item.views_show}}人次观看</p>
|
||||
</div>
|
||||
<p>{{item.title}}</p>
|
||||
</div>
|
||||
@ -17,7 +16,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {ref} from 'vue';
|
||||
import {ref,onMounted} from 'vue';
|
||||
import { getMoreLive } from '../api/detail';
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const lives = ref([
|
||||
@ -29,9 +29,9 @@ const lives = ref([
|
||||
])
|
||||
// 定义状态码与状态文本的映射
|
||||
const statusMap = {
|
||||
'0': '预告',
|
||||
'1': '直播中',
|
||||
'2': '精彩回顾',
|
||||
0: '预告',
|
||||
1: '直播中',
|
||||
2: '精彩回顾',
|
||||
};
|
||||
|
||||
|
||||
@ -41,20 +41,41 @@ const getStatusText = (status) => statusMap[status] || '';
|
||||
// 根据状态码返回对应的CSS类名
|
||||
const getStatusClass = (status) => statusMap[status] || '';
|
||||
|
||||
const goDetail=(item)=>{
|
||||
console.log(item)
|
||||
// router.push({
|
||||
// name:'Detail',
|
||||
// query:{id:item.id}
|
||||
// })
|
||||
}
|
||||
const emit = defineEmits(['select-live']);
|
||||
|
||||
const goDetail = (item) => {
|
||||
console.log('选择直播:', item);
|
||||
// 确保发送完整的直播信息
|
||||
emit('select-live', {
|
||||
id: item.id,
|
||||
videoUrl: item.playUrl || item.videoUrl, // 确保有播放地址
|
||||
liveState: item.liveState,
|
||||
titleImage: item.liveImageShow,
|
||||
title: item.title
|
||||
});
|
||||
};
|
||||
|
||||
const liveList = ref([]);
|
||||
// 获取更多直播列表
|
||||
const getMoreLiveList = async () => {
|
||||
try {
|
||||
const res = await getMoreLive();
|
||||
if (res.code === 0) {
|
||||
liveList.value = res.MoreScenes || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取更多直播列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getMoreLiveList();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.moreLive {
|
||||
padding: 10px;
|
||||
// p{
|
||||
// font-size: 16px;
|
||||
// }
|
||||
|
||||
.live_list{
|
||||
width:100%;
|
||||
height: calc(100vh - 50px);
|
||||
@ -64,6 +85,12 @@ const goDetail=(item)=>{
|
||||
height: 128px;
|
||||
margin:10px 0;
|
||||
position: relative;
|
||||
img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.live_info{
|
||||
padding:5px;
|
||||
position: absolute;
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="abstract">
|
||||
<div class="title">{{ title }}济南移动直播平台移动端开发的标题</div>
|
||||
<div class="abstract" v-if="sceneData">
|
||||
<div class="title">{{ sceneData.title }}</div>
|
||||
<div class="info">
|
||||
<div class="info-item">
|
||||
<span class="name">直播时间:</span>
|
||||
<span class="info-item-text">2024年5约我接地极{{ infoDate }}</span>
|
||||
<span class="info-item-text">{{ sceneData.liveStart }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="name">直播地点:</span>
|
||||
<span class="info-item-text">济南{{ infoPlace }}</span>
|
||||
<span class="info-item-text">{{ sceneData.livePosition }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="name">直播简介:</span>
|
||||
@ -16,15 +16,44 @@
|
||||
class="info-item-text"
|
||||
style="display: inline;"
|
||||
rows="3"
|
||||
:content="text"
|
||||
:content="sceneData.liveDesc"
|
||||
expand-text="展开"
|
||||
collapse-text="收起"
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="info-item" v-if="sceneData.liveDescHref">
|
||||
<span class="name">相关链接:</span>
|
||||
<a :href="sceneData.liveDescHref" target="_blank" class="info-item-text">
|
||||
{{ sceneData.liveDescHref }}
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { defineProps, ref, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
sceneData: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
});
|
||||
|
||||
const isLoaded = ref(false);
|
||||
|
||||
// 暴露给父组件的方法,用于主动标记已加载
|
||||
const loadData = () => {
|
||||
isLoaded.value = true;
|
||||
console.log('简介组件加载完成');
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadData
|
||||
});
|
||||
|
||||
console.log('简介组件数据:', props.sceneData);
|
||||
const text =
|
||||
"那一天我二十一岁,在我一生的黄金时代。我有好多奢望。我想爱,想吃,还想在一瞬间变成天上半明半暗的云。后来我才知道,生活就是个缓慢受锤的过程,人一天天老下去,奢望也一天天消失,最后变得像挨了锤的牛一样。可是我过二十一岁生日时没有预见到这一点。我觉得自己会永远生猛下去,什么也锤不了我。";
|
||||
</script>
|
||||
|
||||
381
src/components/pageComment copy.vue
Normal file
@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<div class="comment">
|
||||
<div class="comment-scroll">
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多评论了"
|
||||
@load="loadMore"
|
||||
>
|
||||
<div class="scroll-main" id="dataListComment">
|
||||
<div class="comment-card" v-for="(item, index) in getCommentList" :key="index">
|
||||
<div class="comment-card-main">
|
||||
<div class="headImg">
|
||||
<img v-if="item.headImg" :src="item.headImg" alt />
|
||||
<img v-else :src="defaultAvatar" alt />
|
||||
</div>
|
||||
<span class="top-name">{{item.userName||'游客'}}: </span>
|
||||
<span class="content-bottom">{{item.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<div class="comment-input">
|
||||
<img :src="defaultAvatar"/>
|
||||
<form @submit.prevent="submitComment">
|
||||
<van-search
|
||||
class="input-class"
|
||||
v-model="commentText"
|
||||
type="text"
|
||||
placeholder="我来说几句......"
|
||||
clearable
|
||||
background="#ededed"
|
||||
left-icon
|
||||
@search="submitComment"
|
||||
/>
|
||||
<canvas id="thumsCanvas" width="200" height="1000"></canvas>
|
||||
<div class="iconShare">
|
||||
<img src="../assets/img/like.svg" alt @click="clickLikeThumb" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, watch, onUnmounted} from 'vue';
|
||||
import {commentList, addComment} from '../api/index';
|
||||
import {getCommentLike} from '../api/detail';
|
||||
import {showNotify} from 'vant';
|
||||
import defaultAvatar from '../assets/img/logo.png';
|
||||
import ThumbsUpAni from '../utils/canvas';
|
||||
|
||||
const getCommentList = ref([]);
|
||||
const commentText = ref('');
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
let thumbsUpAnimation = null;
|
||||
let thumbsUpInterval = null;
|
||||
const clickDebounce = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
sceneData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// 点赞功能
|
||||
const clickLikeThumb = async () => {
|
||||
if (clickDebounce.value) return;
|
||||
clickDebounce.value = true;
|
||||
|
||||
try {
|
||||
const res = await getCommentLike({
|
||||
sceneId: props.sceneData.id
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
// 播放点赞动画
|
||||
if (!thumbsUpAnimation) {
|
||||
thumbsUpAnimation = new ThumbsUpAni();
|
||||
}
|
||||
thumbsUpAnimation.start();
|
||||
|
||||
// showNotify({ type: 'success', message: '点赞成功' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error);
|
||||
// showNotify({ type: 'danger', message: '点赞失败,请稍后重试' });
|
||||
} finally {
|
||||
// 300ms后重置防抖标志
|
||||
setTimeout(() => {
|
||||
clickDebounce.value = false;
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载评论列表
|
||||
const loadMore = async () => {
|
||||
console.log('loadMore被调用,当前场景ID:', props.sceneData?.id);
|
||||
|
||||
if (!props.sceneData?.id) {
|
||||
console.log('场景ID不存在,终止加载评论');
|
||||
loading.value = false;
|
||||
finished.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
console.log('正在加载中,忽略此次加载请求');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
console.log('开始加载评论列表,页码:', page.value, '每页数量:', pageSize.value);
|
||||
|
||||
try {
|
||||
const res = await commentList({
|
||||
sceneId: props.sceneData.id,
|
||||
page: page.value,
|
||||
size: pageSize.value
|
||||
});
|
||||
|
||||
if (res && res.code === 0) {
|
||||
const processedComments = initCommentLikeStatus(res.data || []);
|
||||
|
||||
if (page.value === 1) {
|
||||
getCommentList.value = processedComments;
|
||||
} else {
|
||||
getCommentList.value.push(...processedComments);
|
||||
}
|
||||
|
||||
if (!res.data || res.data.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
console.log("没有更多评论数据,设置完成状态");
|
||||
} else {
|
||||
page.value += 1;
|
||||
console.log("还有更多评论,页码增加到:", page.value);
|
||||
}
|
||||
} else {
|
||||
// API返回错误码
|
||||
console.error("评论列表API返回错误:", res);
|
||||
finished.value = true;
|
||||
loading.value = false;
|
||||
|
||||
if (res) {
|
||||
showNotify({ type: 'warning', message: res.msg || '获取评论失败' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评论列表失败:', error);
|
||||
showNotify({ type: 'danger', message: '获取评论失败' });
|
||||
finished.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
console.log('评论加载完成,loading:', loading.value, 'finished:', finished.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交评论
|
||||
const submitComment = async () => {
|
||||
if (!commentText.value.trim()) {
|
||||
showNotify({ type: 'warning', message: '请输入评论内容' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('sceneId', props.sceneData.id);
|
||||
formData.append('text', commentText.value);
|
||||
|
||||
const res = await addComment(formData);
|
||||
if (res.code === 0) {
|
||||
showNotify({ type: 'success', message: '评论成功' });
|
||||
commentText.value = '';
|
||||
// 重新加载评论列表
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
getCommentList.value = [];
|
||||
await loadMore();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交评论失败:', error);
|
||||
showNotify({ type: 'danger', message: '评论失败,请稍后重试' });
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化评论列表时,添加点赞状态
|
||||
const initCommentLikeStatus = (comments) => {
|
||||
if (!comments) return [];
|
||||
|
||||
return comments.map(comment => {
|
||||
const storageKey = `comment_liked_${comment.id}`;
|
||||
const isLiked = localStorage.getItem(storageKey) === 'true';
|
||||
return {
|
||||
...comment,
|
||||
isLiked,
|
||||
likeCount: comment.likeCount || 0
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 父组件调用的加载数据方法
|
||||
const loadData = () => {
|
||||
console.log('loadData被调用,当前场景ID:', props.sceneData?.id);
|
||||
if (!props.sceneData?.id) {
|
||||
console.log('场景ID不存在,无法加载评论');
|
||||
loading.value = false;
|
||||
finished.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 强制重置状态,无论是否已有数据都重新加载
|
||||
console.log('重置评论列表状态,准备重新加载数据');
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
loading.value = false; // 确保loading为false,避免loadMore中的loading检查阻止加载
|
||||
getCommentList.value = []; // 清空现有数据
|
||||
|
||||
// 立即执行loadMore,不需要延迟
|
||||
loadMore();
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadData
|
||||
});
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '';
|
||||
const date = new Date(time);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (thumbsUpInterval) {
|
||||
clearInterval(thumbsUpInterval);
|
||||
thumbsUpInterval = null;
|
||||
}
|
||||
if (thumbsUpAnimation) {
|
||||
thumbsUpAnimation = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.comment {
|
||||
height: 100%;
|
||||
|
||||
.comment-scroll {
|
||||
height: calc(100vh - 175px);
|
||||
overflow: scroll;
|
||||
.scroll-main {
|
||||
height: 100%;
|
||||
|
||||
.comment-card {
|
||||
padding: 10px 20px;
|
||||
|
||||
.comment-card-main {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
width: auto;
|
||||
background-color: #EEEEEE;
|
||||
border-radius: 10px;
|
||||
|
||||
.headImg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.top-name {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
color: #777882;
|
||||
}
|
||||
|
||||
.content-bottom {
|
||||
font-family: MicrosoftYaHei-Bold;
|
||||
font-size: 16px;
|
||||
color: rgba(0,0,0,80%);
|
||||
line-height: 19px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
width: 85%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.iconShare {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.input-class.van-search {
|
||||
padding: 0;
|
||||
width: 85%;
|
||||
|
||||
.van-search__content {
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
border-radius: 15px;
|
||||
color: #454545;
|
||||
|
||||
.van-cell {
|
||||
font-size: 12px !important;
|
||||
padding: 5px 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#thumsCanvas {
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.van-pull-refresh__track {
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@ -1,244 +1,339 @@
|
||||
<template>
|
||||
<div class="comment">
|
||||
<div class="comment-scroll">
|
||||
<!-- <mescroll-vue class="mescrollPadding" ref="mescroll" :up="mescrollUp" @init="mescrollInit"> -->
|
||||
<van-list
|
||||
v-model:loading="loading"
|
||||
:finished="finished"
|
||||
finished-text="没有更多评论了"
|
||||
@load="loadMore"
|
||||
>
|
||||
<div class="scroll-main" id="dataListComment">
|
||||
<div class="comment-card" v-for="(item, index) in commentData" :key="index">
|
||||
<div class="comment-card" v-for="(item, index) in getCommentList" :key="index">
|
||||
<div class="comment-card-main">
|
||||
<!-- 评论头像 -->
|
||||
<div class="headImg">
|
||||
<img v-if="item.headImg" :src="item.headImg" alt />
|
||||
<img v-else :src="imageAvatarUrl" alt />
|
||||
<img v-else :src="defaultAvatar" alt />
|
||||
</div>
|
||||
<span class="top-name">{{item.userName||'游客'}}: </span>
|
||||
<span class="content-bottom">{{item.text}}</span>
|
||||
<!-- <div class="content">
|
||||
<div class="content-top">
|
||||
<div class="top-name">{{item.userName||'游客'}}</div>
|
||||
<div class="content-bottom">{{item.text}}</div>
|
||||
</div>
|
||||
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="botton-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </mescroll-vue> -->
|
||||
</van-list>
|
||||
</div>
|
||||
|
||||
<div class="comment-input">
|
||||
<img src="../assets/img/logo.png"/>
|
||||
<form action="/">
|
||||
<img :src="defaultAvatar"/>
|
||||
<form @submit.prevent="submitComment">
|
||||
<van-search
|
||||
class="input-class"
|
||||
v-model="showSubmitMsg"
|
||||
v-model="commentText"
|
||||
type="text"
|
||||
placeholder="我来说几句......"
|
||||
clearable
|
||||
background="#ededed"
|
||||
left-icon
|
||||
id="vanInput"
|
||||
@search="inputCommentHandler"
|
||||
@search="submitComment"
|
||||
/>
|
||||
<canvas id="thumsCanvas" width="200" height="1000"></canvas>
|
||||
<div class="iconShare" @click="inputCommentHandler">
|
||||
<img src="../assets/img/like.svg" alt @click="clickLikeThumb" />
|
||||
<canvas id="thumsCanvas" width="200" height="1000"></canvas>
|
||||
<div class="iconShare">
|
||||
<img src="../assets/img/like.svg" alt @click="clickLikeThumb" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {onMounted, ref} from 'vue';
|
||||
import {commentList,addComment} from '../api/index';
|
||||
import ThumbsUpAni from '../utils/canvas'
|
||||
const imageAvatarUrl = ref(null);
|
||||
const commentData= ref([]);
|
||||
const showSubmitMsg = ref('');
|
||||
// 获取评论列表
|
||||
const getCommentList = async () => {
|
||||
let params = {
|
||||
sceneId:2057,
|
||||
size: 10,
|
||||
page: 1,
|
||||
</template>
|
||||
|
||||
};
|
||||
commentList(params)
|
||||
.then((res) => {
|
||||
console.log("获取评论列表",res);
|
||||
commentData.value =res.data;
|
||||
imageAvatarUrl.value = res.anonymousUrl
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("登录接口请求失败", error);
|
||||
});
|
||||
};
|
||||
<script setup>
|
||||
import {ref, onMounted, watch, onUnmounted} from 'vue';
|
||||
import {commentList, addComment} from '../api/index';
|
||||
import {getCommentLike} from '../api/detail';
|
||||
import {showNotify} from 'vant';
|
||||
import defaultAvatar from '../assets/img/logo.png';
|
||||
import ThumbsUpAni from '../utils/canvas';
|
||||
|
||||
const getCommentList = ref([]);
|
||||
const commentText = ref('');
|
||||
const loading = ref(false);
|
||||
const finished = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
let thumbsUpAnimation = null;
|
||||
let thumbsUpInterval = null;
|
||||
const clickDebounce = ref(false);
|
||||
|
||||
const props = defineProps({
|
||||
sceneData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
});
|
||||
|
||||
// 点赞功能
|
||||
const clickLikeThumb=()=> {
|
||||
const clickLikeThumb = async () => {
|
||||
if (clickDebounce.value) return;
|
||||
clickDebounce.value = true;
|
||||
|
||||
const thumbsUpAni = new ThumbsUpAni();
|
||||
console.log("点击",thumbsUpAni);
|
||||
setInterval(() => {
|
||||
thumbsUpAni.start();
|
||||
}, 300);
|
||||
try {
|
||||
const res = await getCommentLike({
|
||||
sceneId: props.sceneData.id
|
||||
});
|
||||
|
||||
}
|
||||
onMounted(()=>{
|
||||
getCommentList();
|
||||
})
|
||||
// 评论输入
|
||||
const inputCommentHandler =()=>{
|
||||
console.log("评论输入",showSubmitMsg.value);
|
||||
// 检测是否填写内容
|
||||
if (showSubmitMsg.value.length < 1) {
|
||||
return false;
|
||||
if (res.code === 0) {
|
||||
// 播放点赞动画
|
||||
if (!thumbsUpAnimation) {
|
||||
thumbsUpAnimation = new ThumbsUpAni();
|
||||
}
|
||||
let utoken = null;
|
||||
let sceneId = 2057;
|
||||
let text = JSON.parse(JSON.stringify(showSubmitMsg.value));
|
||||
|
||||
// 拼装 formData
|
||||
let formdate = new FormData();
|
||||
formdate.append("utoken", utoken);
|
||||
formdate.append("sceneId", sceneId);
|
||||
formdate.append("text", text);
|
||||
|
||||
let vanInput = document.querySelector("#vanInput");
|
||||
vanInput.blur();
|
||||
window.scroll(0, 0);
|
||||
// 调用api
|
||||
addComment(formdate)
|
||||
.then(({ data }) => {
|
||||
showNotify({ type: "primary", message: "评论成功" });
|
||||
this.mescroll.triggerDownScroll(); // 调用upCallback
|
||||
showSubmitMsg.value = "";
|
||||
})
|
||||
.catch(e => {
|
||||
// 联网失败的回调,隐藏下拉刷新和上拉加载的状态;
|
||||
this.mescroll.endDownScroll();
|
||||
console.log("%c 提交评论 error ", "color:red", e, "提交失败");
|
||||
showNotify({ type: "danger", message: "网络繁忙,请稍后再试。" });
|
||||
showSubmitMsg.value = "";
|
||||
});
|
||||
thumbsUpAnimation.start();
|
||||
|
||||
// showNotify({ type: 'success', message: '点赞成功' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error);
|
||||
// showNotify({ type: 'danger', message: '点赞失败,请稍后重试' });
|
||||
} finally {
|
||||
// 300ms后重置防抖标志
|
||||
setTimeout(() => {
|
||||
clickDebounce.value = false;
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载评论列表
|
||||
const loadMore = async () => {
|
||||
console.log('loadMore被调用,当前场景ID:', props.sceneData?.id);
|
||||
|
||||
if (!props.sceneData?.id) {
|
||||
console.log('场景ID不存在,终止加载评论');
|
||||
loading.value = false;
|
||||
finished.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading.value) {
|
||||
console.log('正在加载中,忽略此次加载请求');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
console.log('开始加载评论列表,页码:', page.value, '每页数量:', pageSize.value);
|
||||
|
||||
try {
|
||||
const res = await commentList({
|
||||
sceneId: props.sceneData.id,
|
||||
page: page.value,
|
||||
size: pageSize.value
|
||||
});
|
||||
|
||||
if (res && res.code === 0) {
|
||||
const processedComments = initCommentLikeStatus(res.data || []);
|
||||
|
||||
if (page.value === 1) {
|
||||
getCommentList.value = processedComments;
|
||||
} else {
|
||||
getCommentList.value.push(...processedComments);
|
||||
}
|
||||
|
||||
if (!res.data || res.data.length < pageSize.value) {
|
||||
finished.value = true;
|
||||
console.log("没有更多评论数据,设置完成状态");
|
||||
} else {
|
||||
page.value += 1;
|
||||
console.log("还有更多评论,页码增加到:", page.value);
|
||||
}
|
||||
} else {
|
||||
// API返回错误码
|
||||
console.error("评论列表API返回错误:", res);
|
||||
finished.value = true;
|
||||
loading.value = false;
|
||||
|
||||
if (res) {
|
||||
showNotify({ type: 'warning', message: res.msg || '获取评论失败' });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评论列表失败:', error);
|
||||
showNotify({ type: 'danger', message: '获取评论失败' });
|
||||
finished.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
console.log('评论加载完成,loading:', loading.value, 'finished:', finished.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 提交评论
|
||||
const submitComment = async () => {
|
||||
if (!commentText.value.trim()) {
|
||||
showNotify({ type: 'warning', message: '请输入评论内容' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('sceneId', props.sceneData.id);
|
||||
formData.append('text', commentText.value);
|
||||
|
||||
const res = await addComment(formData);
|
||||
if (res.code === 0) {
|
||||
showNotify({ type: 'success', message: '评论成功' });
|
||||
commentText.value = '';
|
||||
// 重新加载评论列表
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
getCommentList.value = [];
|
||||
await loadMore();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交评论失败:', error);
|
||||
showNotify({ type: 'danger', message: '评论失败,请稍后重试' });
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化评论列表时,添加点赞状态
|
||||
const initCommentLikeStatus = (comments) => {
|
||||
if (!comments) return [];
|
||||
|
||||
return comments.map(comment => {
|
||||
const storageKey = `comment_liked_${comment.id}`;
|
||||
const isLiked = localStorage.getItem(storageKey) === 'true';
|
||||
return {
|
||||
...comment,
|
||||
isLiked,
|
||||
likeCount: comment.likeCount || 0
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 父组件调用的加载数据方法
|
||||
const loadData = () => {
|
||||
console.log('loadData被调用,当前场景ID:', props.sceneData?.id);
|
||||
if (!props.sceneData?.id) {
|
||||
console.log('场景ID不存在,无法加载评论');
|
||||
loading.value = false;
|
||||
finished.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 强制重置状态,无论是否已有数据都重新加载
|
||||
console.log('重置评论列表状态,准备重新加载数据');
|
||||
page.value = 1;
|
||||
finished.value = false;
|
||||
loading.value = false; // 确保loading为false,避免loadMore中的loading检查阻止加载
|
||||
getCommentList.value = []; // 清空现有数据
|
||||
|
||||
// 立即执行loadMore,不需要延迟
|
||||
loadMore();
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadData
|
||||
});
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '';
|
||||
const date = new Date(time);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
if (thumbsUpInterval) {
|
||||
clearInterval(thumbsUpInterval);
|
||||
thumbsUpInterval = null;
|
||||
}
|
||||
if (thumbsUpAnimation) {
|
||||
thumbsUpAnimation = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#thumsCanvas {
|
||||
position:fixed;
|
||||
bottom:50px;
|
||||
right:0;
|
||||
width: 100px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent!important;
|
||||
}
|
||||
|
||||
|
||||
.comment {
|
||||
height: 100%;
|
||||
|
||||
.comment-scroll {
|
||||
height: calc(100vh - 175px);
|
||||
overflow: scroll;
|
||||
background-color: #fff;
|
||||
.scroll-main {
|
||||
height: 100%;
|
||||
|
||||
.comment-card {
|
||||
// border: 1px solid blue;
|
||||
padding:10px 20px;
|
||||
padding: 10px 20px;
|
||||
|
||||
.comment-card-main {
|
||||
display:inline-block;
|
||||
padding:5px 10px;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
width: auto;
|
||||
background-color: #EEEEEE;
|
||||
border-radius: 10px;
|
||||
|
||||
.headImg {
|
||||
display: inline-block;
|
||||
vertical-align:middle;
|
||||
vertical-align: middle;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
|
||||
}
|
||||
}
|
||||
.top-name{
|
||||
|
||||
.top-name {
|
||||
margin-left: 10px;
|
||||
font-size: 16px;
|
||||
color: #777882;
|
||||
|
||||
}
|
||||
.content-bottom {
|
||||
font-family: MicrosoftYaHei-Bold;
|
||||
font-size: 16px;
|
||||
color: rgba(0,0,0,80%);
|
||||
line-height: 19px;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
// .content {
|
||||
// width: 268px;
|
||||
// .content-top {
|
||||
// display: flex;
|
||||
// justify-content: space-between;
|
||||
// margin-bottom: 10px;
|
||||
// font-size: 15px;
|
||||
// color: #5c5c5c;
|
||||
// }
|
||||
// .content-bottom {
|
||||
// font-family: MicrosoftYaHei-Bold;
|
||||
// font-size: 17px;
|
||||
// color: #454545;
|
||||
// line-height: 19px;
|
||||
// letter-spacing: 0px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// .botton-line {
|
||||
// height: 1px;
|
||||
// width: 100%;
|
||||
// margin-left: 9px;
|
||||
// background-color: rgba(160, 160, 160, 0.52);
|
||||
// }
|
||||
.content-bottom {
|
||||
font-family: MicrosoftYaHei-Bold;
|
||||
font-size: 16px;
|
||||
color: rgba(0,0,0,80%);
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-input {
|
||||
position: fixed;
|
||||
bottom:0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
padding:0 10px;
|
||||
padding: 0 10px;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
img{
|
||||
width:40px;
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
width: 85%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
// padding: 0 12px;
|
||||
|
||||
.iconShare {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -246,13 +341,12 @@ const clickLikeThumb=()=> {
|
||||
}
|
||||
|
||||
.input-class.van-search {
|
||||
// padding-top: 7px;
|
||||
padding: 0;
|
||||
width: 85%;
|
||||
.van-search__content {
|
||||
background-color: #fff;
|
||||
border-radius: 15px;
|
||||
|
||||
.van-search__content {
|
||||
background-color: rgba(255,255,255,0.3);
|
||||
border-radius: 15px;
|
||||
color: #454545;
|
||||
|
||||
.van-cell {
|
||||
@ -264,7 +358,23 @@ const clickLikeThumb=()=> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#thumsCanvas {
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 0;
|
||||
width: 100px;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.van-pull-refresh__track {
|
||||
height: 100% !important;
|
||||
|
||||
350
src/components/swipers/BaseSwiper.vue
Normal file
@ -0,0 +1,350 @@
|
||||
<template>
|
||||
<div class="base-swiper" >
|
||||
<swiper
|
||||
v-if="layout === '1*N'"
|
||||
:slidesPerView="'auto'"
|
||||
:spaceBetween="10"
|
||||
:pagination="{
|
||||
clickable: true,
|
||||
}"
|
||||
class="mySwiper"
|
||||
>
|
||||
<swiper-slide
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
:class="{
|
||||
'layout-single': layout === '1*N',
|
||||
}"
|
||||
>
|
||||
<div class="swiper-item">
|
||||
<div class="image-container">
|
||||
<img
|
||||
:src="item.titleImage"
|
||||
:style="imageStyle"
|
||||
@click="handleItemClick(item)"
|
||||
/>
|
||||
<span class="status" :class="getStatusClass(item.liveState)">
|
||||
{{ getStatusText(item.liveState) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-container">
|
||||
<p class="title">{{ item.title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
<swiper
|
||||
v-else
|
||||
:slidesPerView="'auto'"
|
||||
:grid="{
|
||||
rows: 2,
|
||||
fill: 'row'
|
||||
}"
|
||||
:spaceBetween="10"
|
||||
:pagination="{
|
||||
clickable: true,
|
||||
}"
|
||||
:modules="modules"
|
||||
class="mySwiper grid-layout"
|
||||
>
|
||||
<swiper-slide v-for="(item, index) in items" :key="index">
|
||||
<div class="tri">
|
||||
<img :src="item.titleImage" alt="" @click="handleItemClick(item)" />
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<span class="status" :class="getStatusClass(item.liveState)">
|
||||
{{ getStatusText(item.liveState) }}
|
||||
</span>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
import "swiper/css";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/grid";
|
||||
import { Grid } from "swiper/modules";
|
||||
import { useRouter } from "vue-router";
|
||||
import { computed } from 'vue';
|
||||
|
||||
const router = useRouter();
|
||||
const modules = [Grid];
|
||||
|
||||
const props = defineProps({
|
||||
// 数据源
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
// 布局方式:'1*N' 或 '2*N'
|
||||
layout: {
|
||||
type: String,
|
||||
default: "1*N",
|
||||
},
|
||||
// 图片样式
|
||||
imageStyle: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
// 页面样式
|
||||
pageStyle: {
|
||||
type: String,
|
||||
default: "1"
|
||||
},
|
||||
// 每页显示数量
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
// 是否显示指示器
|
||||
showIndicators: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
// 轮播容器宽度
|
||||
swiperWidth: {
|
||||
type: Number,
|
||||
default: 375,
|
||||
},
|
||||
});
|
||||
// 计算分页数据
|
||||
// const pages = computed(() => {
|
||||
// const result = [];
|
||||
// const items = props.items || [];
|
||||
// for (let i = 0; i < items.length; i += props.pageSize) {
|
||||
// result.push(items.slice(i, i + props.pageSize));
|
||||
// }
|
||||
// return result;
|
||||
// });
|
||||
|
||||
// 根据pageStyle返回对应的样式类名
|
||||
// const getPageStyleClass = computed(() => {
|
||||
// const styleMap = {
|
||||
// '1': 'blue-style',
|
||||
// '2': 'red-style',
|
||||
// '3': 'gray-style'
|
||||
// };
|
||||
// return styleMap[props.pageStyle] || 'blue-style';
|
||||
// });
|
||||
|
||||
// 状态文字映射
|
||||
const statusTextMap = {
|
||||
0: "预告",
|
||||
1: "直播中",
|
||||
2: "精彩回顾",
|
||||
};
|
||||
|
||||
// 获取状态文字
|
||||
const getStatusText = (status) => statusTextMap[status] || "";
|
||||
|
||||
// 获取状态样式类
|
||||
const getStatusClass = (status) => {
|
||||
const classMap = {
|
||||
0: "live_coming",
|
||||
1: "live_status",
|
||||
2: "live_review",
|
||||
};
|
||||
return classMap[status] || "";
|
||||
};
|
||||
|
||||
// 点击处理
|
||||
const handleItemClick = (item) => {
|
||||
console.log("点击的item:", item.id);
|
||||
router.push({
|
||||
name: "Detail",
|
||||
params: { id: item.id },
|
||||
});
|
||||
|
||||
// router.push({
|
||||
// name: "Detail",
|
||||
// query: { item: item.id },
|
||||
// });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.base-swiper {
|
||||
width: 100%;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&.blue-style {
|
||||
background-color: #007aff;
|
||||
}
|
||||
|
||||
&.red-style {
|
||||
background-color: #ff3b30;
|
||||
}
|
||||
|
||||
&.gray-style {
|
||||
background-color: #8e8e93;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
width: 100%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
&.grid-layout {
|
||||
height: auto;
|
||||
// padding-bottom: 40px;
|
||||
|
||||
.swiper-slide {
|
||||
height: auto !important;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.tri {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
margin: 4px 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 10px;
|
||||
height: auto !important;
|
||||
|
||||
.tri {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mySwiper {
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:deep(.swiper-slide) {
|
||||
height: auto !important;
|
||||
background: #fff;
|
||||
|
||||
&.layout-single {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.swiper-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
// height: auto;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
z-index: 1;
|
||||
|
||||
&.status-live {
|
||||
background: #ff3b30;
|
||||
}
|
||||
|
||||
&.status-preview {
|
||||
background: #007aff;
|
||||
}
|
||||
|
||||
&.status-review {
|
||||
background: #34c759;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-container {
|
||||
padding: 8px 0;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
text-align: left;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.swiper) {
|
||||
overflow: visible !important;
|
||||
}
|
||||
</style>
|
||||
63
src/components/swipers/TypeASwiper.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<base-swiper
|
||||
:items="items"
|
||||
:layout="layout"
|
||||
:image-style="imageStyle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseSwiper from './BaseSwiper.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: '1*N'
|
||||
}
|
||||
});
|
||||
|
||||
const imageStyle = {
|
||||
width: '100%',
|
||||
height: '92px',
|
||||
objectFit: 'cover'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.swiper) {
|
||||
&.grid-layout {
|
||||
.swiper-slide {
|
||||
width: 164px !important;
|
||||
|
||||
.tri {
|
||||
width: 164px !important;
|
||||
|
||||
img {
|
||||
width: 164px !important;
|
||||
height: 92px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mySwiper {
|
||||
.swiper-slide {
|
||||
width: 164px !important;
|
||||
|
||||
.image-container {
|
||||
width: 164px !important;
|
||||
height: 92px !important;
|
||||
|
||||
img {
|
||||
width: 164px !important;
|
||||
height: 92px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
src/components/swipers/TypeBSwiper.vue
Normal file
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<base-swiper
|
||||
:items="items"
|
||||
:layout="layout"
|
||||
:image-style="imageStyle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseSwiper from './BaseSwiper.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: '1*N'
|
||||
}
|
||||
});
|
||||
|
||||
const imageStyle = {
|
||||
width: '346px',
|
||||
height: '195px',
|
||||
objectFit: 'cover'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.swiper-slide) {
|
||||
width: 346px !important;
|
||||
|
||||
.image-container {
|
||||
height: 195px;
|
||||
|
||||
img {
|
||||
height: 195px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.swiper) {
|
||||
&.grid-layout {
|
||||
.swiper-slide {
|
||||
width: 346px !important;
|
||||
|
||||
.tri {
|
||||
width: 346px !important;
|
||||
|
||||
img {
|
||||
width: 346px !important;
|
||||
height: 195px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mySwiper {
|
||||
.swiper-slide {
|
||||
width: 346px !important;
|
||||
|
||||
.image-container {
|
||||
width: 346px !important;
|
||||
height: 195px !important;
|
||||
|
||||
img {
|
||||
width: 346px !important;
|
||||
height: 195px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
src/components/swipers/TypeCSwiper.vue
Normal file
@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<base-swiper
|
||||
:items="items"
|
||||
:layout="layout"
|
||||
:image-style="imageStyle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BaseSwiper from './BaseSwiper.vue';
|
||||
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
layout: {
|
||||
type: String,
|
||||
default: '1*N'
|
||||
}
|
||||
});
|
||||
|
||||
const imageStyle = {
|
||||
width: '161px',
|
||||
height: '286px',
|
||||
objectFit: 'cover'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.swiper-slide) {
|
||||
width: 161px !important;
|
||||
|
||||
.image-container {
|
||||
height: 286px;
|
||||
|
||||
img {
|
||||
height: 286px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.swiper) {
|
||||
// 2*N布局时的特殊样式
|
||||
.swiper-slide {
|
||||
.tri {
|
||||
img {
|
||||
height: 286px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -36,9 +36,10 @@ import "swiper/css/pagination";
|
||||
|
||||
// import required modules
|
||||
import { Grid } from "swiper/modules";
|
||||
const modules = [Grid];
|
||||
import {useRouter} from 'vue-router';
|
||||
const router = useRouter();
|
||||
const modules = [Grid];
|
||||
|
||||
|
||||
const slides = ref([
|
||||
{
|
||||
@ -85,6 +86,11 @@ const slides = ref([
|
||||
imageUrl: grid4,
|
||||
title: "防空紧急疏散",
|
||||
},
|
||||
{id:'8',
|
||||
status: '1',
|
||||
imageUrl: grid4,
|
||||
title: "防空紧急疏散",
|
||||
},
|
||||
|
||||
]);
|
||||
// 定义状态码与状态文本的映射
|
||||
@ -110,15 +116,13 @@ const goDetail = (item) => {
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
<style lang="scss" >
|
||||
.swiper {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.swiper-slide {
|
||||
.swiper-slide {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
background: #fff;
|
||||
@ -147,4 +151,7 @@ const goDetail = (item) => {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<van-swipe :autoplay="3000" lazy-render>
|
||||
<van-swipe-item class="van-swipe-item" v-for="(item,index) in images" :key="index">
|
||||
<img :src="item.image" @click="goDetail(item)" />
|
||||
<p class="status" :class="{ 'live_status': getStatusClass(item.status) === '直播中', 'live_coming': getStatusClass(item.status) === '预告', 'live_review': getStatusClass(item.status) === '精彩回顾' }">{{ getStatusText(item.status) }}</p>
|
||||
<van-swipe-item class="van-swipe-item" v-for="(item,index) in items" :key="index">
|
||||
<img :src="item.liveImageShow" @click="goDetail(item)" />
|
||||
<p class="status" :class="{ 'live_status': getStatusClass(item.liveState) === '直播中', 'live_coming': getStatusClass(item.liveState) === '预告', 'live_review': getStatusClass(item.liveState) === '精彩回顾' }">{{ getStatusText(item.liveState) }}</p>
|
||||
</van-swipe-item>
|
||||
</van-swipe>
|
||||
</template>
|
||||
@ -10,6 +10,12 @@
|
||||
import pic from '../../assets/img/top1.png'
|
||||
import {ref} from 'vue';
|
||||
import {useRouter} from 'vue-router';
|
||||
const props = defineProps({
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const router = useRouter();
|
||||
const images = ref([
|
||||
{
|
||||
@ -28,9 +34,9 @@ const images = ref([
|
||||
])
|
||||
// 定义状态码与状态文本的映射
|
||||
const statusMap = {
|
||||
'0': '预告',
|
||||
'1': '直播中',
|
||||
'2': '精彩回顾',
|
||||
0: '预告',
|
||||
1: '直播中',
|
||||
2: '精彩回顾',
|
||||
};
|
||||
|
||||
|
||||
@ -40,12 +46,11 @@ const getStatusText = (status) => statusMap[status] || '';
|
||||
// 根据状态码返回对应的CSS类名
|
||||
const getStatusClass = (status) => statusMap[status] || '';
|
||||
const goDetail = (item) => {
|
||||
console.log(item)
|
||||
console.log('跳转到详情页:', item);
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
query: { item: item.id }
|
||||
|
||||
})
|
||||
params: { id: item.id }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@ -54,14 +59,16 @@ const goDetail = (item) => {
|
||||
height: 212px;
|
||||
position: relative;
|
||||
img {
|
||||
width: 100%;
|
||||
width: 100%!important;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.status{
|
||||
top:10px;
|
||||
left:25px;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
position: absolute;
|
||||
color:#fff;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,16 +1,34 @@
|
||||
<template>
|
||||
<div class="title">
|
||||
<p>直播推荐,热门精选</p>
|
||||
<p>{{title}}</p>
|
||||
<img src="../assets/img/arraw.svg" alt="" @click="goToMore"/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
channelId: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
channelName: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const goToMore=()=>{
|
||||
console.log('跳转到更多直播')
|
||||
console.log('跳转到栏目列表页', props.channelId)
|
||||
router.push({
|
||||
name:'More'
|
||||
name: 'ChannelList',
|
||||
query: {
|
||||
channelId: props.channelId,
|
||||
title: props.channelName || props.title
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@ -19,8 +37,9 @@ const router = useRouter()
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding:10px 0;
|
||||
padding-top:10px;
|
||||
font-size: 16px;
|
||||
font-weight: bolder;
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
src/doc/V1.3_直播平台(二期)产品需求PRD 2405.docx
Normal file
29
src/main.js
@ -1,14 +1,37 @@
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import './assets/css/index.scss'
|
||||
import App from './App.vue'
|
||||
// 导入上面新建的路由文件
|
||||
import router from './router/index'
|
||||
import {
|
||||
Picker,
|
||||
Popup,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Search,
|
||||
Icon,
|
||||
Button,
|
||||
Toast
|
||||
} from 'vant'
|
||||
import 'vant/lib/index.css'
|
||||
|
||||
let app = createApp(App)
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有需要的组件
|
||||
const components = [
|
||||
Picker,
|
||||
Popup,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
Search,
|
||||
Icon,
|
||||
Button,
|
||||
Toast
|
||||
]
|
||||
|
||||
components.forEach(component => {
|
||||
app.use(component)
|
||||
})
|
||||
app.use(router)
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
@ -1,4 +1,4 @@
|
||||
import { createWebHashHistory, createRouter } from 'vue-router'
|
||||
import { createWebHashHistory, createRouter, createWebHistory } from 'vue-router'
|
||||
//createWebHashHistory
|
||||
import HomeView from '../views/home.vue'
|
||||
import SearchView from '../views/searchVue.vue'
|
||||
@ -14,11 +14,16 @@ const routes = [
|
||||
},
|
||||
{ path: '/search', name:'Search', component: SearchView },
|
||||
{ path: '/more', name:'More', component: MoreView },
|
||||
{ path: '/detail', name:'Detail', component: DetailView },
|
||||
{ path: '/detail/:id', name:'Detail', component: DetailView },
|
||||
{
|
||||
path: '/channelList',
|
||||
name: 'ChannelList',
|
||||
component: () => import('../views/channelList.vue')
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
export default router;
|
||||
@ -1,11 +1,9 @@
|
||||
function getRandom(min, max) {
|
||||
return min + Math.floor(Math.random() * (max - min + 1));
|
||||
}
|
||||
}
|
||||
|
||||
export default class ThumbsUpAni {
|
||||
export default class ThumbsUpAni {
|
||||
constructor() {
|
||||
this.loadImages(); // 预加载图片
|
||||
|
||||
// 读取 canvas
|
||||
const canvas = document.getElementById('thumsCanvas');
|
||||
this.context = canvas.getContext('2d');
|
||||
@ -13,63 +11,86 @@ function getRandom(min, max) {
|
||||
this.height = canvas.height;
|
||||
|
||||
this.imgsList = []; // 点赞图像列表
|
||||
this.renderList = []; // 渲染对象雷彪
|
||||
// scaleTime - 百分比。图片从开始放大到最终大小,所用时长。
|
||||
// 设置为 0.1 ,表示总共运行时间前面的 10% 的时间,点赞图片逐步放大
|
||||
this.renderList = []; // 渲染对象列表
|
||||
this.scaleTime = 0.1;
|
||||
this.scanning = false; // 扫描器扫描标识,防止开启多个扫描器
|
||||
this.imagesLoaded = false; // 添加图片加载状态标志
|
||||
|
||||
// 在构造函数中立即开始加载图片
|
||||
this.loadImages();
|
||||
}
|
||||
|
||||
// 预加载图片,获取图片宽高,如果某一图片加载失败,则不显示该图片
|
||||
loadImages() {
|
||||
// const images = [
|
||||
// 'jfs/t1/93992/8/9049/4680/5e0aea04Ec9dd2be8/608efd890fd61486.png',
|
||||
// 'jfs/t1/108305/14/2849/4908/5e0aea04Efb54912c/bfa59f27e654e29c.png',
|
||||
// 'jfs/t1/98805/29/8975/5106/5e0aea05Ed970e2b4/98803f8ad07147b9.png',
|
||||
// 'jfs/t1/94291/26/9105/4344/5e0aea05Ed64b9187/5165fdf5621d5bbf.png',
|
||||
// 'jfs/t1/102753/34/8504/5522/5e0aea05E0b9ef0b4/74a73178e31bd021.png',
|
||||
// 'jfs/t1/102954/26/9241/5069/5e0aea05E7dde8bda/720fcec8bc5be9d4.png',
|
||||
// ];
|
||||
const images = [
|
||||
'./bg1.png',
|
||||
'./bg2.png',
|
||||
'./bg3.png',
|
||||
'./bg4.png',
|
||||
'./bg5.png',
|
||||
'./bg6.png',
|
||||
|
||||
'/bg1.png',
|
||||
'/bg2.png',
|
||||
'/bg3.png',
|
||||
'/bg4.png',
|
||||
'/bg5.png',
|
||||
'/bg6.png',
|
||||
];
|
||||
const promiseAll = [];
|
||||
|
||||
let loadedCount = 0;
|
||||
const totalImages = images.length;
|
||||
|
||||
images.forEach((src) => {
|
||||
const p = new Promise(function(resolve) {
|
||||
const img = new Image();
|
||||
img.onerror = img.onload = resolve.bind(null, img);
|
||||
// img.src = 'https://img12.360buyimg.com/img/' + src;
|
||||
img.src = src;
|
||||
console.log("src",img.src);
|
||||
});
|
||||
promiseAll.push(p);
|
||||
});
|
||||
Promise.all(promiseAll).then((imgsList) => {
|
||||
this.imgsList = imgsList.filter((d) => {
|
||||
if (d && d.width > 0) return true;
|
||||
return false;
|
||||
});
|
||||
if (this.imgsList.length == 0) {
|
||||
dLog('error', 'imgsList load all error');
|
||||
return;
|
||||
}
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
if (img.width > 0) {
|
||||
this.imgsList.push(img);
|
||||
}
|
||||
loadedCount++;
|
||||
|
||||
// 当所有图片都加载完成时
|
||||
if (loadedCount === totalImages) {
|
||||
this.imagesLoaded = true;
|
||||
if (this.imgsList.length > 0) {
|
||||
console.log('点赞动画图片加载完成');
|
||||
// 如果有等待的动画,立即执行
|
||||
if (this.pendingAnimation) {
|
||||
this.start();
|
||||
this.pendingAnimation = false;
|
||||
}
|
||||
} else {
|
||||
console.warn('没有可用的点赞图片');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.warn('图片加载失败:', src);
|
||||
loadedCount++;
|
||||
|
||||
// 即使图片加载失败,也要检查是否所有图片都处理完成
|
||||
if (loadedCount === totalImages && this.pendingAnimation) {
|
||||
this.imagesLoaded = true;
|
||||
// 如果所有图片都加载失败,使用默认图片
|
||||
const defaultImg = new Image();
|
||||
defaultImg.onload = () => {
|
||||
this.imgsList.push(defaultImg);
|
||||
this.start();
|
||||
this.pendingAnimation = false;
|
||||
};
|
||||
defaultImg.src = '/like-active.svg'; // 使用点赞图标作为默认图片
|
||||
}
|
||||
};
|
||||
|
||||
// 使用完整的URL路径
|
||||
const baseUrl = window.location.origin;
|
||||
img.src = baseUrl + src;
|
||||
console.log("加载图片:", img.src);
|
||||
});
|
||||
}
|
||||
|
||||
createRender() {
|
||||
if (this.imgsList.length == 0) return null;
|
||||
if (this.imgsList.length === 0) {
|
||||
console.warn('没有可用的点赞图片');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 当运行时间 diffTime 小于设置的 scaleTime 的时候,按比例随着时间增大,scale 变大。超过设置的时间阈值,则返回最终大小。
|
||||
const basicScale = [0.6, 0.9, 1.2][getRandom(0, 2)];
|
||||
const getScale = (diffTime) => {
|
||||
// diffTime - 百分比。表示从动画开始运行到当前时间过了多长时间。实际值是从 0 --> 1 逐步增大。
|
||||
// scaleTime - 百分比。图片从开始放大到最终大小,所用时长。
|
||||
if (diffTime < this.scaleTime) {
|
||||
return +(diffTime / this.scaleTime).toFixed(2) * basicScale;
|
||||
} else {
|
||||
@ -78,31 +99,26 @@ function getRandom(min, max) {
|
||||
};
|
||||
|
||||
const context = this.context;
|
||||
// 随机读取一个图片,进行渲染
|
||||
const image = this.imgsList[getRandom(0, this.imgsList.length - 1)];
|
||||
const offset = 20; // x轴偏移量
|
||||
const offset = 20;
|
||||
const basicX = this.width / 2 + getRandom(-offset, offset);
|
||||
const angle = getRandom(2, 10); // 角度系数
|
||||
const angle = getRandom(2, 10);
|
||||
let ratio = getRandom(10, 30) * (getRandom(0, 1) ? 1 : -1);
|
||||
|
||||
// 随机平滑 X 轴偏移 - 通过正弦( Math.sin )函数来实现均匀曲线
|
||||
const getTranslateX = (diffTime) => {
|
||||
if (diffTime < this.scaleTime) {
|
||||
// 放大期间,不进行摇摆位移
|
||||
return basicX;
|
||||
} else {
|
||||
return basicX + ratio * Math.sin(angle * (diffTime - this.scaleTime));
|
||||
}
|
||||
};
|
||||
|
||||
// Y 轴偏移 - 运行偏移从 this.height --> image.height / 2 ,即从最底部,运行到顶部留下。
|
||||
const getTranslateY = (diffTime) => {
|
||||
return (
|
||||
image.height / 2 + (this.height - image.height / 2) * (1 - diffTime)
|
||||
);
|
||||
};
|
||||
|
||||
// 淡出
|
||||
const fadeOutStage = getRandom(14, 18) / 100;
|
||||
const getAlpha = (diffTime) => {
|
||||
let left = 1 - +diffTime;
|
||||
@ -114,23 +130,17 @@ function getRandom(min, max) {
|
||||
};
|
||||
|
||||
return (diffTime) => {
|
||||
// diffTime : 百分比。表示从动画开始运行到当前时间过了多长时间。实际值是从 0 --> 1 逐步增大。
|
||||
// diffTime 为 0.4 的时候,说明是已经运行了 40% 的时间
|
||||
// 时间差值满了,即:动画结束了(0 --> 1)
|
||||
if (diffTime >= 1) return true;
|
||||
|
||||
context.save();
|
||||
|
||||
const scale = getScale(diffTime);
|
||||
// const rotate = getRotate();
|
||||
const translateX = getTranslateX(diffTime);
|
||||
const translateY = getTranslateY(diffTime);
|
||||
context.translate(translateX, translateY); // 偏移
|
||||
context.scale(scale, scale); // 缩放
|
||||
// context.rotate(rotate * Math.PI / 180);
|
||||
context.globalAlpha = getAlpha(diffTime); // 淡出
|
||||
context.translate(translateX, translateY);
|
||||
context.scale(scale, scale);
|
||||
context.globalAlpha = getAlpha(diffTime);
|
||||
|
||||
// 绘制
|
||||
context.drawImage(
|
||||
image,
|
||||
-image.width / 2,
|
||||
@ -138,12 +148,10 @@ function getRandom(min, max) {
|
||||
image.width,
|
||||
image.height
|
||||
);
|
||||
context.restore(); // 恢复画布(canvas)状态。
|
||||
context.restore();
|
||||
};
|
||||
}
|
||||
|
||||
// 实时绘制扫描器
|
||||
// 开启实时绘制扫描器,将创建的渲染对象放入 renderList 数组,数组不为空,说明 canvas 上还有动画,就需要不停的去执行 scan,直到 canvas 上没有动画结束为止。
|
||||
scan() {
|
||||
this.context.clearRect(0, 0, this.width, this.height);
|
||||
this.context.fillStyle = 'transparent';
|
||||
@ -159,8 +167,6 @@ function getRandom(min, max) {
|
||||
this.scanning = false;
|
||||
}
|
||||
|
||||
// diffTime = (Date.now() - render.timestamp) / render.duration
|
||||
// 如果开始的时间戳是 10000,当前是100100,则说明已经运行了 100 毫秒了,如果动画本来需要执行 1000 毫秒,那么 diffTime = 0.1,代表动画已经运行了 10%。
|
||||
while (index < length) {
|
||||
const child = this.renderList[index];
|
||||
if (
|
||||
@ -168,36 +174,45 @@ function getRandom(min, max) {
|
||||
!child.render ||
|
||||
child.render.call(null, (Date.now() - child.timestamp) / child.duration)
|
||||
) {
|
||||
// 动画结束,则删除该动画
|
||||
this.renderList.splice(index, 1);
|
||||
length--;
|
||||
} else {
|
||||
// 继续执行动画
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始/增加动画
|
||||
// 调用一次 start 方法来生成渲染实例,放进渲染实例数组。
|
||||
// 如果当前扫描器未开启,则需要启动扫描器,使用了 scanning 变量,防止开启多个扫描器。
|
||||
start() {
|
||||
// 如果图片还没有加载完成,标记为等待动画并返回
|
||||
if (!this.imagesLoaded) {
|
||||
console.log('图片未加载完成,等待中...');
|
||||
this.pendingAnimation = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
const render = this.createRender();
|
||||
if (!render) {
|
||||
console.warn('无法创建点赞动画');
|
||||
return this;
|
||||
}
|
||||
|
||||
const duration = getRandom(1500, 3000);
|
||||
this.renderList.push({
|
||||
render,
|
||||
duration,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
if (!this.scanning) {
|
||||
this.scanning = true;
|
||||
requestFrame(this.scan.bind(this));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestFrame(cb) {
|
||||
function requestFrame(cb) {
|
||||
return (
|
||||
window.requestAnimationFrame ||
|
||||
window.webkitRequestAnimationFrame ||
|
||||
@ -205,5 +220,5 @@ function getRandom(min, max) {
|
||||
window.setTimeout(callback, 1000 / 60);
|
||||
}
|
||||
)(cb);
|
||||
}
|
||||
}
|
||||
|
||||
56
src/utils/plugin/audio-bar/audio-bar.css
Normal file
@ -0,0 +1,56 @@
|
||||
.equalizer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
/* align-items: center; */
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
.equalizer-bar {
|
||||
width: 15px;
|
||||
margin: 0 3px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.equalizer-bar span {
|
||||
display: block;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
margin: 2px 0;
|
||||
background-color: #FFF;
|
||||
transition: .2s ease all;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(1) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(2) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(3) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(4) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(5) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(6) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(7) span {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.equalizer-bar:nth-child(8) span {
|
||||
background: #fff;
|
||||
}
|
||||
62
src/utils/plugin/audio-bar/audio-bar.js
Normal file
@ -0,0 +1,62 @@
|
||||
const MAX_BAR_HEIGHT = 18;
|
||||
|
||||
addBarSpans();
|
||||
|
||||
setInterval(() => {
|
||||
setRandomBars();
|
||||
}, 200);
|
||||
|
||||
// Main programm (repeats)
|
||||
function setRandomBars(maxBarHeight) {
|
||||
const bars = document.getElementsByClassName('equalizer-bar');
|
||||
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
let spans = bars[i].getElementsByTagName('span');
|
||||
let activeSpanCount = getActiveSpans(spans);
|
||||
let newHeight = getRandomHeight(MAX_BAR_HEIGHT);
|
||||
|
||||
for (let j = 0; j < spans.length; j++) {
|
||||
if (newHeight > activeSpanCount) {
|
||||
spans[j].style.opacity = '1';
|
||||
} else if (j > newHeight) {
|
||||
spans[j].style.opacity = '0';
|
||||
}
|
||||
|
||||
// set little opacity
|
||||
let upperSpan = MAX_BAR_HEIGHT - j;
|
||||
if (newHeight > MAX_BAR_HEIGHT - 5 && upperSpan < 5) {
|
||||
spans[j].style.opacity = '0.' + upperSpan;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the number of active spans
|
||||
function getActiveSpans(spans) {
|
||||
let counter = 0;
|
||||
|
||||
for (let i = 0; i < spans.length; i++) {
|
||||
if (spans[i].style.opacity > 0) counter++;
|
||||
}
|
||||
|
||||
return counter;
|
||||
}
|
||||
|
||||
// Returns a random number between 1 and 20
|
||||
function getRandomHeight(maxBarHeight) {
|
||||
return Math.round(Math.random() * (maxBarHeight - 1)) + 1;
|
||||
}
|
||||
|
||||
// Add the default spans
|
||||
function addBarSpans() {
|
||||
const bars = document.getElementsByClassName('equalizer-bar');
|
||||
|
||||
let html = '';
|
||||
for (let j = 0; j < MAX_BAR_HEIGHT; j++) {
|
||||
html += '<span></span>';
|
||||
}
|
||||
|
||||
for (let i = 0; i < bars.length; i++) {
|
||||
bars[i].innerHTML = html;
|
||||
}
|
||||
}
|
||||
@ -13,11 +13,20 @@ const request = axios.create({
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在请求发送之前做些什么
|
||||
console.log('准备发送请求:', {
|
||||
url: config.url,
|
||||
method: config.method,
|
||||
params: config.params,
|
||||
baseURL: config.baseURL,
|
||||
headers: config.headers
|
||||
});
|
||||
|
||||
// 可以在请求头中添加token等信息
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
// 对请求错误做些什么
|
||||
console.error('请求拦截器捕获错误:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
@ -26,10 +35,20 @@ request.interceptors.request.use(
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
// 对响应数据做些什么
|
||||
console.log('收到响应:', {
|
||||
status: response.status,
|
||||
url: response.config.url,
|
||||
data: response.data
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
(error) => {
|
||||
// 对响应错误做些什么
|
||||
console.error('响应拦截器捕获错误:', error);
|
||||
if (error.response) {
|
||||
console.error('错误响应状态:', error.response.status);
|
||||
console.error('错误响应数据:', error.response.data);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
103
src/views/channelList.vue
Normal file
@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div class="page" v-wechat-title="$route.meta.title">
|
||||
|
||||
<contentList :items="channelData" class="content-list" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import contentList from '../components/contentList.vue'
|
||||
import { getHomeList } from '../api/detail'
|
||||
|
||||
const route = useRoute()
|
||||
const channelData = ref([])
|
||||
const contentListRef = ref(null)
|
||||
|
||||
const fetchChannelData = async () => {
|
||||
try {
|
||||
const channelId = route.query.channelId
|
||||
const channelName = route.query.title
|
||||
|
||||
if (!channelId || !channelName) {
|
||||
console.error('未找到栏目信息')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await getHomeList({
|
||||
channelId: channelId,
|
||||
channelName: channelName
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
// 从返回数据中找到对应频道的数据
|
||||
channelData.value = res.appView?.carouselScene
|
||||
console.log('栏目数据', channelData.value)
|
||||
|
||||
// if (channelData?.liveScenes) {
|
||||
// contentListRef.value?.updateList(channelData.liveScenes)
|
||||
// } else {
|
||||
// contentListRef.value?.updateList([])
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取栏目数据失败:', error)
|
||||
// contentListRef.value?.updateList([])
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchChannelData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.channel-list-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f5f6f8;
|
||||
|
||||
.nav-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 44px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.back {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-list {
|
||||
padding-top: 54px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,43 +1,58 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page" v-wechat-title="$route.meta.title">
|
||||
<!-- <div class="page" v-wechat-title="$route.meta.title"> -->
|
||||
<div class="top">
|
||||
<div id="xgPlayerWrap"></div>
|
||||
<div class="player-container">
|
||||
<!-- VR播放器 -->
|
||||
<VRPlayer
|
||||
v-if="isVRVideo"
|
||||
:videoUrl="currentVideoUrl"
|
||||
:poster="coverImage"
|
||||
:isLive="sceneData?.liveState === '1'"
|
||||
:autoplay="false"
|
||||
@error="handlePlayerError"
|
||||
/>
|
||||
<!-- 阿里播放器 -->
|
||||
<AliPlayer
|
||||
v-else
|
||||
:videoUrl="currentVideoUrl"
|
||||
:poster="coverImage"
|
||||
:isLive="sceneData?.liveState === '1'"
|
||||
:autoplay="false"
|
||||
@error="handlePlayerError"
|
||||
@ready="handlePlayerReady"
|
||||
/>
|
||||
</div>
|
||||
<countDown ref="coCountdown" :countdownTime="countdownTime"></countDown>
|
||||
<van-tabs v-model:active="activeName" offset-top="0">
|
||||
<!-- 预告状态下显示倒计时 -->
|
||||
<countDown v-if="countdownTime > 0" :countdownTime="countdownTime" :viewCount="sceneData?.viewsShow" @countdown-end="handleCountdownEnd"></countDown>
|
||||
|
||||
<van-tabs v-model:active="activeName" offset-top="0" @change="handleTabChange">
|
||||
<!-- 动态显示简介和评论,当countdownTime有值时 -->
|
||||
<van-tab v-if="countdownTime > 0" title="简介" name="a">
|
||||
<pageAbstract></pageAbstract>
|
||||
<van-tab title="简介" name="a">
|
||||
<pageAbstract v-if="loadedTabs.includes('a')" :sceneData="sceneData" ref="abstractTab"></pageAbstract>
|
||||
</van-tab>
|
||||
<van-tab v-if="countdownTime > 0" title="评论" name="c">
|
||||
<pageComment />
|
||||
<van-tab title="现场直播" name="b">
|
||||
<pageReport v-if="loadedTabs.includes('b')" :sceneData="sceneData" @change-view="handleViewChange" ref="reportTab"></pageReport>
|
||||
</van-tab>
|
||||
<van-tab title="评论" name="c">
|
||||
<pageComment v-if="loadedTabs.includes('c')" :sceneData="sceneData" ref="commentTab"></pageComment>
|
||||
</van-tab>
|
||||
<!-- 当countdownTime无值时,显示现场直播 -->
|
||||
<template v-else>
|
||||
<van-tab title="现场直播" name="b">
|
||||
<pageReport />
|
||||
</van-tab>
|
||||
<van-tab title="评论" name="c">
|
||||
<pageComment />
|
||||
</van-tab>
|
||||
</template>
|
||||
</van-tabs>
|
||||
<van-popup
|
||||
v-model:show="showRight"
|
||||
position="right"
|
||||
:style="{ width: '70%', height: '100%' }"
|
||||
>
|
||||
<moreLiveVue />
|
||||
<moreLiveVue @select-live="handleSelectLive" />
|
||||
</van-popup>
|
||||
<van-floating-bubble
|
||||
|
||||
</div>
|
||||
<van-floating-bubble
|
||||
v-model:offset="offset"
|
||||
class="van-floating-bubble"
|
||||
class="van-floating-bubble custom-bubble"
|
||||
@click="onClick"
|
||||
>
|
||||
<p class="bubble-content">更多直播</p>
|
||||
</van-floating-bubble>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import countDown from "../components/countDown.vue";
|
||||
@ -45,88 +60,658 @@ import pageAbstract from "../components/pageAbstract.vue";
|
||||
import pageReport from "../components/pageReport.vue";
|
||||
import pageComment from "../components/pageComment.vue";
|
||||
import moreLiveVue from "../components/moreLive.vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import Player, { Events } from "../xgplayer"; // 引入西瓜视频模块
|
||||
import "../xgplayer/dist/index.min.css"; // 引入西瓜视频样式
|
||||
import { scene } from "../api/index";
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed } from "vue";
|
||||
import { scene } from "../api/index";
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMoreLive } from "../api/detail";
|
||||
import { showNotify } from 'vant';
|
||||
import VideoPlayer from "../utils/conf"; // 配置文件单独拎出来一个js文件
|
||||
import VRPlayer from "../components/VRPlayer.vue";
|
||||
import Player from 'xgplayer';
|
||||
import AliPlayer from '../components/AliPlayer备份.vue';
|
||||
import wxShare from "../assets/js/weixinShare/index";
|
||||
|
||||
import { conf } from "../utils/conf"; // 配置文件单独拎出来一个js文件
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
let countdownTimer = null;
|
||||
|
||||
// 动态更新 conf 对象的 poster 属性
|
||||
conf.poster =
|
||||
"https://th.bing.com/th/id/R.9de53f9726576696b318a8d95c0946cb?rik=sWB3V9KSxHbThw&riu=http%3a%2f%2fpic.bizhi360.com%2fbbpic%2f95%2f9995_1.jpg&ehk=GcPUjJED69TBvg9XxQr2klzDzfRsQWhAfLKlIAUWHJQ%3d&risl=&pid=ImgRaw&r=0"; // 根据需求动态设置封面图片的 URL
|
||||
// 组件引用
|
||||
const abstractTab = ref(null);
|
||||
const reportTab = ref(null);
|
||||
const commentTab = ref(null);
|
||||
|
||||
// 已加载的标签页
|
||||
const loadedTabs = ref([]);
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabChange = (tabName) => {
|
||||
console.log('标签切换到:', tabName);
|
||||
|
||||
// 如果标签页尚未加载,将其添加到已加载列表
|
||||
if (!loadedTabs.value.includes(tabName)) {
|
||||
loadedTabs.value.push(tabName);
|
||||
}
|
||||
|
||||
// 无论标签页是否已加载,都强制调用相应组件的loadData方法
|
||||
console.log('强制重新加载标签页数据:', tabName);
|
||||
|
||||
// 延迟调用加载方法,确保组件已挂载
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 根据标签名调用对应组件的loadData方法
|
||||
if (tabName === 'a' && abstractTab.value) {
|
||||
console.log('重新调用简介组件loadData方法');
|
||||
abstractTab.value.loadData();
|
||||
} else if (tabName === 'b' && reportTab.value) {
|
||||
console.log('重新调用直播组件loadData方法');
|
||||
reportTab.value.loadData();
|
||||
} else if (tabName === 'c' && commentTab.value) {
|
||||
console.log('重新调用评论组件loadData方法');
|
||||
commentTab.value.loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`调用${tabName}标签页loadData方法失败:`, error);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
console.log("进入详情页");
|
||||
getSenceList();
|
||||
});
|
||||
|
||||
let player = null; // 实例
|
||||
// 监听路由参数变化,当ID改变时重新获取场景数据
|
||||
watch(() => route.params.id, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
console.log(`路由ID从${oldId}更改为${newId},重新获取场景数据`);
|
||||
getSenceList();
|
||||
}
|
||||
}, { immediate: false });
|
||||
|
||||
console.log("Events", Events);
|
||||
let player;
|
||||
|
||||
const init = () => {
|
||||
player = new Player({
|
||||
...conf,
|
||||
// 判断是否为VR视频
|
||||
const isVRVideo = computed(() => {
|
||||
const isVR = sceneData.value?.vrLive === 'true' || sceneData.value?.vrLive === true;
|
||||
console.log('VR视频判断:', {
|
||||
vrLive: sceneData.value?.vrLive,
|
||||
vrLiveType: typeof sceneData.value?.vrLive,
|
||||
sceneData: sceneData.value,
|
||||
isVR
|
||||
});
|
||||
player.on(Events.PLAY, (ev) => {
|
||||
console.log("-播放开始-", ev);
|
||||
return isVR;
|
||||
});
|
||||
|
||||
// 获取当前播放的视频URL
|
||||
const currentVideoUrl = computed(() => {
|
||||
// 获取视频URL的通用逻辑
|
||||
const url = sceneData.value?.liveList?.[0]?.playUrl || sceneData.value?.videoUrl || '';
|
||||
|
||||
console.log('视频URL:', {
|
||||
isVRVideo: isVRVideo.value,
|
||||
url,
|
||||
liveList: sceneData.value?.liveList,
|
||||
videoUrl: sceneData.value?.videoUrl,
|
||||
sceneData: sceneData.value
|
||||
});
|
||||
player.on(Events.PAUSE, (ev) => {
|
||||
console.log("-播放结束-", ev);
|
||||
|
||||
return url;
|
||||
});
|
||||
|
||||
// 获取封面图
|
||||
const coverImage = computed(() => {
|
||||
// 获取封面图的通用逻辑
|
||||
const cover = sceneData.value?.liveList?.[0]?.coverUrl || sceneData.value?.titleImage || '';
|
||||
|
||||
console.log('视频封面:', cover);
|
||||
return cover;
|
||||
});
|
||||
|
||||
// 添加播放器初始化状态
|
||||
const playerInitialized = ref(false);
|
||||
|
||||
// 添加重试次数控制
|
||||
const retryCount = ref(0);
|
||||
const MAX_RETRY = 3;
|
||||
|
||||
// 修改初始化播放器方法
|
||||
const initPlayer = (isPreview = false, viewData = null) => {
|
||||
console.log('初始化播放器:', {
|
||||
isVRVideo: isVRVideo.value,
|
||||
isPreview,
|
||||
viewData,
|
||||
sceneData: sceneData.value
|
||||
});
|
||||
player.on("loadedmetadata", (ev) => {
|
||||
console.log("-媒体数据加载好了-", ev);
|
||||
});
|
||||
player.on(Events.SEEKED, (ev) => {
|
||||
console.log("-跳着播放-", ev);
|
||||
});
|
||||
// 等各种监听事件
|
||||
|
||||
// 如果是VR视频,不初始化普通播放器
|
||||
if (isVRVideo.value) {
|
||||
console.log('检测到VR视频,跳过普通播放器初始化');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果存在播放器实例,先销毁
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy();
|
||||
playerInitialized.value = false;
|
||||
} catch (e) {
|
||||
console.error('销毁播放器失败:', e);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
|
||||
// 预告状态下只显示封面图
|
||||
if (isPreview) {
|
||||
playerInitialized.value = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保播放器容器存在
|
||||
const playerContainer = document.getElementById('xg-player');
|
||||
if (!playerContainer) {
|
||||
console.error('播放器容器不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建播放器配置
|
||||
const source = viewData?.playUrl || sceneData.value?.videoUrl || "";
|
||||
if (!source) {
|
||||
console.error('视频源不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('正在初始化播放器,视频源:', source);
|
||||
|
||||
const config = {
|
||||
id: 'xg-player',
|
||||
url: source,
|
||||
poster: viewData?.coverUrl || sceneData.value?.titleImage || '',
|
||||
playsinline: true,
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
fluid: true,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
volume: 0.6,
|
||||
videoInit: true,
|
||||
preloadTime: 30,
|
||||
isLive: sceneData.value?.liveState === '1',
|
||||
ignores: ['replay'],
|
||||
closeVideoClick: false,
|
||||
closeVideoTouch: false,
|
||||
lang: 'zh-cn',
|
||||
// 添加视频类型自动识别
|
||||
mediaType: source.includes('.m3u8') ? 'hls' :
|
||||
source.includes('.flv') ? 'flv' :
|
||||
'mp4',
|
||||
// 添加跨域支持
|
||||
cors: true,
|
||||
// 添加错误重试机制
|
||||
retry: 3,
|
||||
retryCount: 0,
|
||||
retryDelay: 1000,
|
||||
// 添加预加载设置
|
||||
preload: 'auto',
|
||||
// 添加错误处理配置
|
||||
errorTips: '视频加载失败,请刷新重试',
|
||||
loadingTimeout: 20,
|
||||
// 添加播放器核心配置
|
||||
innerKernel: {
|
||||
handleError: true,
|
||||
handleKernelError: true,
|
||||
probePriority: ['hls', 'mp4']
|
||||
}
|
||||
};
|
||||
|
||||
// 创建播放器实例
|
||||
const xgPlayer = new Player(config);
|
||||
|
||||
// 监听播放器事件
|
||||
xgPlayer.on('ready', () => {
|
||||
console.log('播放器准备就绪');
|
||||
playerInitialized.value = true;
|
||||
retryCount.value = 0;
|
||||
});
|
||||
|
||||
// 视频元数据加载完成
|
||||
xgPlayer.on('loadedmetadata', () => {
|
||||
console.log('视频元数据加载完成');
|
||||
playerInitialized.value = true;
|
||||
});
|
||||
|
||||
// 视频加载中
|
||||
xgPlayer.on('waiting', () => {
|
||||
console.log('视频加载中...');
|
||||
});
|
||||
|
||||
// 视频可以播放
|
||||
xgPlayer.on('canplay', () => {
|
||||
console.log('视频可以播放');
|
||||
playerInitialized.value = true;
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
xgPlayer.on('error', async (e) => {
|
||||
console.error('播放器错误:', e);
|
||||
|
||||
if (retryCount.value < MAX_RETRY) {
|
||||
console.log(`第${retryCount.value + 1}次重试初始化播放器`);
|
||||
retryCount.value++;
|
||||
|
||||
// 延迟重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
try {
|
||||
// 确保旧实例完全销毁
|
||||
if (xgPlayer) {
|
||||
xgPlayer.pause();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
xgPlayer.destroy();
|
||||
}
|
||||
|
||||
// 重新创建播放器实例
|
||||
const newConfig = {...config}; // 创建配置的副本
|
||||
const newPlayer = new Player(newConfig);
|
||||
return newPlayer;
|
||||
} catch (error) {
|
||||
console.error('重试初始化播放器失败:', error);
|
||||
}
|
||||
} else {
|
||||
playerInitialized.value = false;
|
||||
console.error('播放器初始化失败,已达到最大重试次数');
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '播放器初始化失败,请刷新页面重试'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 销毁事件
|
||||
xgPlayer.on('destroy', () => {
|
||||
console.log('播放器销毁');
|
||||
playerInitialized.value = false;
|
||||
});
|
||||
|
||||
return xgPlayer;
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化播放器失败:', error);
|
||||
playerInitialized.value = false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player) {
|
||||
try {
|
||||
player.pause();
|
||||
setTimeout(() => {
|
||||
player.destroy();
|
||||
player = null;
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error('组件卸载时清理播放器失败:', e);
|
||||
}
|
||||
}
|
||||
clearInterval(countdownTimer);
|
||||
});
|
||||
|
||||
const showRight = ref(false);
|
||||
const hasCountdown = ref(false); //判断是否有倒计时
|
||||
const countdownTime = ref(0);
|
||||
const activeName = ref("b");
|
||||
const offset = ref({ x: 375, y: 350 });
|
||||
const onClick = () => {
|
||||
showRight.value = true;
|
||||
const activeName = ref("a"); // 默认显示简介
|
||||
const offset = ref({ x: '100%', y: '70%' });
|
||||
|
||||
// 点击更多直播按钮
|
||||
const onClick = async () => {
|
||||
try {
|
||||
// 获取更多直播列表
|
||||
const res = await getMoreLive();
|
||||
|
||||
if (res.code === 0 && res.MoreScenes && res.MoreScenes.length > 0) {
|
||||
// 显示弹窗
|
||||
showRight.value = true;
|
||||
} else {
|
||||
showNotify({ type: 'warning', message: '没有更多直播可以观看' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取更多直播失败:', error);
|
||||
showNotify({ type: 'danger', message: '获取更多直播失败' });
|
||||
}
|
||||
};
|
||||
// 获取sence
|
||||
const getSenceList =()=>{
|
||||
let params = {
|
||||
id:2057
|
||||
};
|
||||
scene(params)
|
||||
.then((res) => {
|
||||
console.log("获取sence",res);
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("登录接口请求失败", error);
|
||||
// 处理直播选择
|
||||
const handleSelectLive = (item) => {
|
||||
showRight.value = false; // 关闭弹窗
|
||||
|
||||
if (item && item.id) {
|
||||
console.log('选择了新的直播:', item);
|
||||
|
||||
// 无论是否是当前ID,都更新路由参数并重新获取数据
|
||||
router.replace({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
}).then(() => {
|
||||
// 路由更新后重新获取场景数据
|
||||
getSenceList();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sceneData = ref(null);
|
||||
|
||||
// 获取场景数据
|
||||
const getSenceList = async () => {
|
||||
const sceneId = route.params.id;
|
||||
console.log("当前场景ID:", sceneId);
|
||||
|
||||
if (!sceneId) return;
|
||||
|
||||
try {
|
||||
const res = await scene({ id: sceneId });
|
||||
|
||||
if(res.code == 0){
|
||||
// 清空已加载的标签页列表,以便重新加载
|
||||
loadedTabs.value = [];
|
||||
|
||||
sceneData.value = res.data;
|
||||
console.log("获取视频播放页面场景数据:", sceneData.value);
|
||||
|
||||
// 计算是否是预告状态
|
||||
const liveState = sceneData.value.liveState;
|
||||
const startTime = new Date(sceneData.value.liveStart).getTime();
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (liveState === '0' && startTime > now) {
|
||||
// 预告状态
|
||||
countdownTime.value = Math.floor((startTime - now) / 1000);
|
||||
activeName.value = "a"; // 默认显示简介
|
||||
loadedTabs.value = ['a']; // 预加载简介标签
|
||||
initPlayer(true); // 预告状态只显示封面图
|
||||
startCountdown(); // 开始倒计时
|
||||
|
||||
// 延迟加载简介数据
|
||||
setTimeout(() => {
|
||||
if (abstractTab.value) {
|
||||
abstractTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
// 直播状态
|
||||
countdownTime.value = 0;
|
||||
activeName.value = "b"; // 默认显示直播
|
||||
loadedTabs.value = ['b']; // 预加载直播标签
|
||||
|
||||
// 如果有直播列表,使用第一个直播源初始化播放器
|
||||
if (sceneData.value.liveList && sceneData.value.liveList.length > 0) {
|
||||
const firstLive = sceneData.value.liveList[0];
|
||||
console.log("使用第一个直播源初始化:", firstLive);
|
||||
|
||||
initPlayer(false, {
|
||||
playUrl: firstLive.playUrl,
|
||||
coverUrl: firstLive.coverUrl,
|
||||
name: firstLive.name
|
||||
});
|
||||
} else {
|
||||
// 没有直播列表,使用默认视频源
|
||||
const newPlayer = initPlayer(false);
|
||||
}
|
||||
|
||||
// 延迟加载现场直播数据
|
||||
setTimeout(() => {
|
||||
if (reportTab.value) {
|
||||
reportTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 配置微信分享
|
||||
wxShare({
|
||||
title: route.meta.title,
|
||||
desc: sceneData.value.liveDesc || sceneData.value.title,
|
||||
imgUrl: sceneData.value.titleImage
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取场景数据失败:", error);
|
||||
showNotify({ type: 'danger', message: '获取场景数据失败' });
|
||||
}
|
||||
};
|
||||
|
||||
// 开始倒计时
|
||||
const startCountdown = () => {
|
||||
if (countdownTime.value <= 0) return;
|
||||
|
||||
countdownTimer = setInterval(() => {
|
||||
countdownTime.value--;
|
||||
|
||||
if (countdownTime.value <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
// 倒计时结束,重新获取场景数据并自动播放
|
||||
getSenceList();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 监听倒计时变化
|
||||
watch(countdownTime, (newValue) => {
|
||||
if (newValue <= 0 && countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
// 倒计时结束,重新获取场景数据并自动播放
|
||||
getSenceList();
|
||||
}
|
||||
});
|
||||
|
||||
// 处理视角切换
|
||||
const handleViewChange = (viewData) => {
|
||||
if (!viewData || !viewData.playUrl) {
|
||||
console.error('视频地址不存在:', viewData);
|
||||
showNotify({ type: 'warning', message: '视频地址不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('切换视角:', viewData);
|
||||
const newPlayer = initPlayer(false, viewData);
|
||||
if (newPlayer) {
|
||||
player = newPlayer;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换视频源失败:', error);
|
||||
showNotify({ type: 'danger', message: '切换视频源失败' });
|
||||
}
|
||||
};
|
||||
|
||||
// 处理倒计时结束
|
||||
const handleCountdownEnd = () => {
|
||||
// 重新获取场景数据并初始化播放器
|
||||
getSenceList();
|
||||
};
|
||||
|
||||
// 监听场景数据变化,当数据变化时刷新当前标签页
|
||||
watch(sceneData, (newData) => {
|
||||
if (newData) {
|
||||
console.log('场景数据已更新,刷新当前标签页:', activeName.value);
|
||||
// 根据当前激活的标签页重新加载数据
|
||||
setTimeout(() => {
|
||||
if (activeName.value === 'a' && abstractTab.value) {
|
||||
abstractTab.value.loadData();
|
||||
} else if (activeName.value === 'b' && reportTab.value) {
|
||||
reportTab.value.loadData();
|
||||
} else if (activeName.value === 'c' && commentTab.value) {
|
||||
commentTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 监听路由变化,更新微信标题
|
||||
watch(() => route.meta.title, (newTitle) => {
|
||||
if (newTitle) {
|
||||
document.title = newTitle;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 处理播放器错误
|
||||
const handlePlayerError = (error) => {
|
||||
console.error('VR播放器错误:', error);
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '播放器错误,请检查网络连接或刷新页面重试'
|
||||
});
|
||||
};
|
||||
|
||||
// 处理播放器就绪
|
||||
const handlePlayerReady = (player) => {
|
||||
console.log('播放器就绪');
|
||||
playerInitialized.value = true;
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
/* 播放器容器样式 */
|
||||
.player-container {
|
||||
width: 100%;
|
||||
height: 211px;
|
||||
position: relative;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 西瓜播放器样式 */
|
||||
.xg-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 封面图样式 */
|
||||
.player-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 自定义西瓜播放器控制栏样式 */
|
||||
.xgplayer .xgplayer-controls {
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
height: 44px !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 加载中样式 */
|
||||
.xgplayer .xgplayer-loading {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-loading .xgplayer-loading-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 错误提示样式 */
|
||||
.xgplayer .xgplayer-error {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.xgplayer .xgplayer-controls {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-slider {
|
||||
height: 2px !important;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-icon {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式优化 */
|
||||
@media screen and (orientation: landscape) {
|
||||
.player-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.xg-player,
|
||||
.player-cover {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-controls {
|
||||
height: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.van-floating-bubble {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0 !important;
|
||||
width: 50px;
|
||||
height: 80px;
|
||||
background: #eeeeee;
|
||||
transform: translate3d(325.812px, 50vh, 0px) !important;
|
||||
width: 50px !important;
|
||||
height: 80px !important;
|
||||
background: #eeeeee !important;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: 4px !important;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.custom-bubble.van-floating-bubble::before {
|
||||
border-radius: 4px !important;
|
||||
background: #eeeeee !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.van-tabs__content {
|
||||
width: 100%;
|
||||
height: calc(100vh - 130px);
|
||||
/* border:1px solid red; */
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* 顶部播放器容器 */
|
||||
.top {
|
||||
width: 100%;
|
||||
height: 211px;
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* VR播放器样式 */
|
||||
.top .vr-player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.top .video-js {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bubble-content {
|
||||
color: rgba(0, 0, 0, 80%);
|
||||
@ -134,36 +719,25 @@ const getSenceList =()=>{
|
||||
width: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.bubble-content::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 30%;
|
||||
background: url("../assets/img/jt_black.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
left: -15px;
|
||||
top: 30%;
|
||||
position: absolute;
|
||||
}
|
||||
// 顶部播放器
|
||||
.top {
|
||||
width: 100%;
|
||||
// height: 211px;
|
||||
// height: 211px;
|
||||
// padding-top: 56.25%; /* 16:9 aspect ratio (9 / 16 * 100) */
|
||||
// position: relative;
|
||||
// overflow: hidden;
|
||||
writing-mode: vertical-lr;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 2px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
// border: 1px solid black;
|
||||
box-sizing: border-box;
|
||||
|
||||
|
||||
}
|
||||
#xgPlayerWrap {
|
||||
flex: auto;
|
||||
}
|
||||
#xgPlayerWrap video {
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
&::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 30%;
|
||||
background: url("../assets/img/jt_black.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
left: -15px;
|
||||
top: 35%;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,44 +1,55 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="page" v-wechat-title="$route.meta.title">
|
||||
<!-- <div class="page" v-wechat-title="$route.meta.title"> -->
|
||||
<div class="top">
|
||||
<div
|
||||
class="prism-player"
|
||||
id="J_prismPlayer"
|
||||
style="width: 100%; height: 100%"
|
||||
></div>
|
||||
<div class="player-container">
|
||||
<!-- VR播放器 -->
|
||||
<VRPlayer
|
||||
v-if="isVRVideo"
|
||||
:videoUrl="currentVideoUrl"
|
||||
:poster="coverImage"
|
||||
:isLive="sceneData?.liveState === '1'"
|
||||
:autoplay="false"
|
||||
@error="handlePlayerError"
|
||||
/>
|
||||
<!-- 阿里播放器 -->
|
||||
<AliPlayer
|
||||
v-else
|
||||
ref="aliPlayer"
|
||||
:videoUrl="currentVideoUrl"
|
||||
:poster="coverImage"
|
||||
:isLive="sceneData?.liveState === '1'"
|
||||
:autoplay="false"
|
||||
@error="handlePlayerError"
|
||||
@ready="handlePlayerReady"
|
||||
/>
|
||||
</div>
|
||||
<countDown ref="coCountdown" :countdownTime="countdownTime"></countDown>
|
||||
<van-tabs v-model:active="activeName" offset-top="0">
|
||||
<!-- 预告状态下显示倒计时 -->
|
||||
<countDown v-if="countdownTime > 0" :countdownTime="countdownTime" :viewCount="sceneData?.viewsShow" @countdown-end="handleCountdownEnd"></countDown>
|
||||
|
||||
<van-tabs v-model:active="activeName" offset-top="0" @change="handleTabChange">
|
||||
<!-- 动态显示简介和评论,当countdownTime有值时 -->
|
||||
<van-tab v-if="countdownTime > 0" title="简介" name="a">
|
||||
<pageAbstract></pageAbstract>
|
||||
<van-tab title="简介" name="a">
|
||||
<pageAbstract v-if="loadedTabs.includes('a')" :sceneData="sceneData" ref="abstractTab"></pageAbstract>
|
||||
</van-tab>
|
||||
<van-tab v-if="countdownTime > 0" title="评论" name="c">
|
||||
<pageComment />
|
||||
<van-tab title="现场直播" name="b">
|
||||
<pageReport v-if="loadedTabs.includes('b')" :sceneData="sceneData" @change-view="handleViewChange" ref="reportTab"></pageReport>
|
||||
</van-tab>
|
||||
<van-tab title="评论" name="c">
|
||||
<pageComment v-if="loadedTabs.includes('c')" :sceneData="sceneData" ref="commentTab"></pageComment>
|
||||
</van-tab>
|
||||
<!-- 当countdownTime无值时,显示现场直播 -->
|
||||
<template v-else>
|
||||
<van-tab title="现场直播" name="b">
|
||||
<pageReport />
|
||||
</van-tab>
|
||||
<van-tab title="评论" name="c">
|
||||
<pageComment />
|
||||
</van-tab>
|
||||
</template>
|
||||
</van-tabs>
|
||||
<van-popup
|
||||
v-model:show="showRight"
|
||||
position="right"
|
||||
:style="{ width: '70%', height: '100%' }"
|
||||
>
|
||||
<moreLiveVue />
|
||||
<moreLiveVue @select-live="handleSelectLive" />
|
||||
</van-popup>
|
||||
|
||||
</div>
|
||||
<van-floating-bubble
|
||||
v-model:offset="offset"
|
||||
class="van-floating-bubble"
|
||||
class="van-floating-bubble custom-bubble"
|
||||
@click="onClick"
|
||||
>
|
||||
<p class="bubble-content">更多直播</p>
|
||||
@ -50,197 +61,714 @@ import pageAbstract from "../components/pageAbstract.vue";
|
||||
import pageReport from "../components/pageReport.vue";
|
||||
import pageComment from "../components/pageComment.vue";
|
||||
import moreLiveVue from "../components/moreLive.vue";
|
||||
import { ref, onMounted,onBeforeUnmount } from "vue";
|
||||
import { ref, onMounted, onBeforeUnmount, watch, computed, nextTick } from "vue";
|
||||
import { scene } from "../api/index";
|
||||
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getMoreLive } from "../api/detail";
|
||||
import { showNotify } from 'vant';
|
||||
import VideoPlayer from "../utils/conf"; // 配置文件单独拎出来一个js文件
|
||||
import VRPlayer from "../components/VRPlayer.vue";
|
||||
import Player from 'xgplayer';
|
||||
import AliPlayer from '../components/AliPlayer.vue';
|
||||
import wxShare from "../assets/js/weixinShare/index";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
let countdownTimer = null;
|
||||
|
||||
// 组件引用
|
||||
const abstractTab = ref(null);
|
||||
const reportTab = ref(null);
|
||||
const commentTab = ref(null);
|
||||
|
||||
// 已加载的标签页
|
||||
const loadedTabs = ref([]);
|
||||
|
||||
// 处理标签页切换
|
||||
const handleTabChange = (tabName) => {
|
||||
console.log('标签切换到:', tabName);
|
||||
|
||||
// 如果标签页尚未加载,将其添加到已加载列表
|
||||
if (!loadedTabs.value.includes(tabName)) {
|
||||
loadedTabs.value.push(tabName);
|
||||
}
|
||||
|
||||
// 无论标签页是否已加载,都强制调用相应组件的loadData方法
|
||||
console.log('强制重新加载标签页数据:', tabName);
|
||||
|
||||
// 延迟调用加载方法,确保组件已挂载
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// 根据标签名调用对应组件的loadData方法
|
||||
if (tabName === 'a' && abstractTab.value) {
|
||||
console.log('重新调用简介组件loadData方法');
|
||||
abstractTab.value.loadData();
|
||||
} else if (tabName === 'b' && reportTab.value) {
|
||||
console.log('重新调用直播组件loadData方法');
|
||||
reportTab.value.loadData();
|
||||
} else if (tabName === 'c' && commentTab.value) {
|
||||
console.log('重新调用评论组件loadData方法');
|
||||
commentTab.value.loadData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`调用${tabName}标签页loadData方法失败:`, error);
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
console.log("进入详情页");
|
||||
getSenceList();
|
||||
});
|
||||
|
||||
let player;
|
||||
const init = () => {
|
||||
const playerProps =
|
||||
{
|
||||
id: "J_prismPlayer",
|
||||
cover:'https://th.bing.com/th/id/R.9de53f9726576696b318a8d95c0946cb?rik=sWB3V9KSxHbThw&riu=http%3a%2f%2fpic.bizhi360.com%2fbbpic%2f95%2f9995_1.jpg&ehk=GcPUjJED69TBvg9XxQr2klzDzfRsQWhAfLKlIAUWHJQ%3d&risl=&pid=ImgRaw&r=0',
|
||||
source: "https://player.alicdn.com/resource/player/qupai.mp4",
|
||||
width: "100%",
|
||||
height: "500px",
|
||||
autoplay: true,
|
||||
isLive: false,
|
||||
rePlay: false,
|
||||
playsinline: true,
|
||||
preload: 'auto',
|
||||
useH5Prism: true,
|
||||
useFlashPrism:false,
|
||||
controlBarVisibility: "hover", // 鼠标悬停时显示控制栏
|
||||
controlBarVisibilityOnFs: "hover", // 即使全屏也在鼠标悬停时显示控制栏
|
||||
showBarTime: 5000, // 控制栏显示时间
|
||||
useFlashPrism: false, // 使用 HTML5 播放器而非 Flash 播放器
|
||||
language: "zh-cn", // 设置语言为简体中文
|
||||
x5_video_position:'normal',
|
||||
x5_type:'h5',//通过 video 属性 “x5-video-player-type” 声明启用同层H5播放器
|
||||
skinLayout: [
|
||||
// Specify the UI elements to show/hide
|
||||
{
|
||||
name: "bigPlayButton",
|
||||
align: "blabs",
|
||||
x: 30,
|
||||
y: 80
|
||||
},
|
||||
{
|
||||
name: "H5Loading",
|
||||
align: "cc"
|
||||
},
|
||||
{
|
||||
name: "errorDisplay",
|
||||
align: "tlabs",
|
||||
x: 0,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
name: "infoDisplay"
|
||||
},
|
||||
{
|
||||
name: "tooltip",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 56
|
||||
},
|
||||
{
|
||||
name: "thumbnail"
|
||||
},
|
||||
{
|
||||
name: "controlBar",
|
||||
align: "blabs",
|
||||
x: 0,
|
||||
y: 0,
|
||||
children: [
|
||||
{
|
||||
name: "progress",
|
||||
align: "blabs",
|
||||
x: 10, // 设置进度条的起始位置
|
||||
y: 44,
|
||||
// 监听路由参数变化,当ID改变时重新获取场景数据
|
||||
watch(() => route.params.id, (newId, oldId) => {
|
||||
if (newId && newId !== oldId) {
|
||||
console.log(`路由ID从${oldId}更改为${newId},重新获取场景数据`);
|
||||
getSenceList();
|
||||
}
|
||||
}, { immediate: false });
|
||||
|
||||
},
|
||||
{
|
||||
name: "playButton",
|
||||
align: "tl",
|
||||
x: 15,
|
||||
y: 12
|
||||
},
|
||||
{
|
||||
name: "timeDisplay",
|
||||
align: "tl",
|
||||
x: 10,
|
||||
y: 7
|
||||
},
|
||||
{
|
||||
name: "fullScreenButton",
|
||||
align: "tr",
|
||||
x: 10,
|
||||
y: 12
|
||||
},
|
||||
{
|
||||
name: "volume",
|
||||
align: "tr",
|
||||
x: 10,
|
||||
y: 10
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
let player;
|
||||
|
||||
// 判断是否为VR视频
|
||||
const isVRVideo = computed(() => {
|
||||
const isVR = sceneData.value?.vrLive === 'true' || sceneData.value?.vrLive === true;
|
||||
console.log('VR视频判断:', {
|
||||
vrLive: sceneData.value?.vrLive,
|
||||
vrLiveType: typeof sceneData.value?.vrLive,
|
||||
sceneData: sceneData.value,
|
||||
isVR
|
||||
});
|
||||
return isVR;
|
||||
});
|
||||
|
||||
// 获取当前播放的视频URL
|
||||
const currentVideoUrl = ref('');
|
||||
const coverImage = ref('');
|
||||
|
||||
// 添加阿里播放器引用
|
||||
const aliPlayer = ref(null);
|
||||
|
||||
// 添加播放器初始化状态
|
||||
const playerInitialized = ref(false);
|
||||
|
||||
// 添加重试次数控制
|
||||
const retryCount = ref(0);
|
||||
const MAX_RETRY = 3;
|
||||
|
||||
// 修改初始化播放器方法
|
||||
const initPlayer = (isPreview = false, viewData = null) => {
|
||||
console.log('初始化播放器:', {
|
||||
isVRVideo: isVRVideo.value,
|
||||
isPreview,
|
||||
viewData,
|
||||
sceneData: sceneData.value
|
||||
});
|
||||
|
||||
// 如果是VR视频,不初始化普通播放器
|
||||
if (isVRVideo.value) {
|
||||
console.log('检测到VR视频,跳过普通播放器初始化');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果存在播放器实例,先销毁
|
||||
if (player) {
|
||||
try {
|
||||
player.destroy();
|
||||
playerInitialized.value = false;
|
||||
} catch (e) {
|
||||
console.error('销毁播放器失败:', e);
|
||||
}
|
||||
player = null;
|
||||
}
|
||||
|
||||
// 预告状态下只显示封面图
|
||||
if (isPreview) {
|
||||
playerInitialized.value = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 确保播放器容器存在
|
||||
const playerContainer = document.getElementById('xg-player');
|
||||
if (!playerContainer) {
|
||||
console.error('播放器容器不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建播放器配置
|
||||
const source = viewData?.playUrl || sceneData.value?.videoUrl || "";
|
||||
if (!source) {
|
||||
console.error('视频源不存在');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('正在初始化播放器,视频源:', source);
|
||||
|
||||
const config = {
|
||||
id: 'xg-player',
|
||||
url: source,
|
||||
poster: viewData?.coverUrl || sceneData.value?.titleImage || '',
|
||||
playsinline: true,
|
||||
loop: false,
|
||||
autoplay: false,
|
||||
fluid: true,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
volume: 0.6,
|
||||
videoInit: true,
|
||||
preloadTime: 30,
|
||||
isLive: sceneData.value?.liveState === '1',
|
||||
ignores: ['replay'],
|
||||
closeVideoClick: false,
|
||||
closeVideoTouch: false,
|
||||
lang: 'zh-cn',
|
||||
// 添加视频类型自动识别
|
||||
mediaType: source.includes('.m3u8') ? 'hls' :
|
||||
source.includes('.flv') ? 'flv' :
|
||||
'mp4',
|
||||
// 添加跨域支持
|
||||
cors: true,
|
||||
// 添加错误重试机制
|
||||
retry: 3,
|
||||
retryCount: 0,
|
||||
retryDelay: 1000,
|
||||
// 添加预加载设置
|
||||
preload: 'auto',
|
||||
// 添加错误处理配置
|
||||
errorTips: '视频加载失败,请刷新重试',
|
||||
loadingTimeout: 20,
|
||||
// 添加播放器核心配置
|
||||
innerKernel: {
|
||||
handleError: true,
|
||||
handleKernelError: true,
|
||||
probePriority: ['hls', 'mp4']
|
||||
}
|
||||
};
|
||||
|
||||
// 创建播放器实例
|
||||
const xgPlayer = new Player(config);
|
||||
|
||||
player = new VideoPlayer(playerProps);
|
||||
// 监听播放器事件
|
||||
xgPlayer.on('ready', () => {
|
||||
console.log('播放器准备就绪');
|
||||
playerInitialized.value = true;
|
||||
retryCount.value = 0;
|
||||
});
|
||||
|
||||
// 视频元数据加载完成
|
||||
xgPlayer.on('loadedmetadata', () => {
|
||||
console.log('视频元数据加载完成');
|
||||
playerInitialized.value = true;
|
||||
});
|
||||
|
||||
// 视频加载中
|
||||
xgPlayer.on('waiting', () => {
|
||||
console.log('视频加载中...');
|
||||
});
|
||||
|
||||
// 视频可以播放
|
||||
xgPlayer.on('canplay', () => {
|
||||
console.log('视频可以播放');
|
||||
playerInitialized.value = true;
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
xgPlayer.on('error', async (e) => {
|
||||
console.error('播放器错误:', e);
|
||||
|
||||
if (retryCount.value < MAX_RETRY) {
|
||||
console.log(`第${retryCount.value + 1}次重试初始化播放器`);
|
||||
retryCount.value++;
|
||||
|
||||
// 延迟重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
try {
|
||||
// 确保旧实例完全销毁
|
||||
if (xgPlayer) {
|
||||
xgPlayer.pause();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
xgPlayer.destroy();
|
||||
}
|
||||
|
||||
// 重新创建播放器实例
|
||||
const newConfig = {...config}; // 创建配置的副本
|
||||
const newPlayer = new Player(newConfig);
|
||||
return newPlayer;
|
||||
} catch (error) {
|
||||
console.error('重试初始化播放器失败:', error);
|
||||
}
|
||||
} else {
|
||||
playerInitialized.value = false;
|
||||
console.error('播放器初始化失败,已达到最大重试次数');
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '播放器初始化失败,请刷新页面重试'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 销毁事件
|
||||
xgPlayer.on('destroy', () => {
|
||||
console.log('播放器销毁');
|
||||
playerInitialized.value = false;
|
||||
});
|
||||
|
||||
return xgPlayer;
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化播放器失败:', error);
|
||||
playerInitialized.value = false;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (player) {
|
||||
player.dispose();
|
||||
try {
|
||||
player.pause();
|
||||
setTimeout(() => {
|
||||
player.destroy();
|
||||
player = null;
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
console.error('组件卸载时清理播放器失败:', e);
|
||||
}
|
||||
}
|
||||
clearInterval(countdownTimer);
|
||||
});
|
||||
|
||||
const showRight = ref(false);
|
||||
const hasCountdown = ref(false); //判断是否有倒计时
|
||||
const countdownTime = ref(0);
|
||||
const activeName = ref("b");
|
||||
const activeName = ref("a"); // 默认显示简介
|
||||
const offset = ref({ x: '100%', y: '70%' });
|
||||
const onClick = () => {
|
||||
showRight.value = true;
|
||||
|
||||
// 点击更多直播按钮
|
||||
const onClick = async () => {
|
||||
try {
|
||||
// 获取更多直播列表
|
||||
const res = await getMoreLive();
|
||||
|
||||
if (res.code === 0 && res.MoreScenes && res.MoreScenes.length > 0) {
|
||||
// 显示弹窗
|
||||
showRight.value = true;
|
||||
} else {
|
||||
showNotify({ type: 'warning', message: '没有更多直播可以观看' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取更多直播失败:', error);
|
||||
showNotify({ type: 'danger', message: '获取更多直播失败' });
|
||||
}
|
||||
};
|
||||
// 获取sence
|
||||
const getSenceList = () => {
|
||||
let params = {
|
||||
id: 2057,
|
||||
};
|
||||
scene(params)
|
||||
.then((res) => {
|
||||
console.log("获取sence", res);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("登录接口请求失败", error);
|
||||
|
||||
// 处理直播选择
|
||||
const handleSelectLive = async (item) => {
|
||||
showRight.value = false; // 关闭弹窗
|
||||
|
||||
if (item && item.id) {
|
||||
console.log('选择了新的直播:', item);
|
||||
|
||||
try {
|
||||
// 更新当前视频URL和封面图
|
||||
currentVideoUrl.value = item.videoUrl;
|
||||
coverImage.value = item.titleImage;
|
||||
|
||||
// 等待DOM更新
|
||||
await nextTick();
|
||||
|
||||
// 更新路由
|
||||
await router.replace({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
});
|
||||
|
||||
// 重新获取场景数据并初始化播放器
|
||||
await getSenceList();
|
||||
|
||||
// 如果有阿里播放器实例,尝试加载新的视频源
|
||||
if (aliPlayer.value) {
|
||||
try {
|
||||
const player = aliPlayer.value.getPlayer();
|
||||
if (player) {
|
||||
player.loadByUrl(currentVideoUrl.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换视频源失败:', error);
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '切换视频源失败,请刷新页面重试'
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换直播失败:', error);
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '切换直播失败,请刷新重试'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const sceneData = ref(null);
|
||||
|
||||
// 获取场景数据
|
||||
const getSenceList = async () => {
|
||||
const sceneId = route.params.id;
|
||||
console.log("当前场景ID:", sceneId);
|
||||
|
||||
if (!sceneId) return;
|
||||
|
||||
try {
|
||||
const res = await scene({ id: sceneId });
|
||||
|
||||
if(res.code == 0){
|
||||
// 清空已加载的标签页列表,以便重新加载
|
||||
loadedTabs.value = [];
|
||||
|
||||
sceneData.value = res.data;
|
||||
console.log("获取视频播放页面场景数据:", sceneData.value);
|
||||
|
||||
// 初始化视频URL和封面图
|
||||
currentVideoUrl.value = sceneData.value?.liveList?.[0]?.playUrl || sceneData.value?.videoUrl || '';
|
||||
coverImage.value = sceneData.value?.liveList?.[0]?.coverUrl || sceneData.value?.titleImage || '';
|
||||
|
||||
// 计算是否是预告状态
|
||||
const liveState = sceneData.value.liveState;
|
||||
const startTime = new Date(sceneData.value.liveStart).getTime();
|
||||
const now = new Date().getTime();
|
||||
|
||||
if (liveState === '0' && startTime > now) {
|
||||
// 预告状态
|
||||
countdownTime.value = Math.floor((startTime - now) / 1000);
|
||||
activeName.value = "a"; // 默认显示简介
|
||||
loadedTabs.value = ['a']; // 预加载简介标签
|
||||
initPlayer(true); // 预告状态只显示封面图
|
||||
startCountdown(); // 开始倒计时
|
||||
|
||||
// 延迟加载简介数据
|
||||
setTimeout(() => {
|
||||
if (abstractTab.value) {
|
||||
abstractTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
// 直播状态
|
||||
countdownTime.value = 0;
|
||||
activeName.value = "b"; // 默认显示直播
|
||||
loadedTabs.value = ['b']; // 预加载直播标签
|
||||
|
||||
// 如果有直播列表,使用第一个直播源初始化播放器
|
||||
if (sceneData.value.liveList && sceneData.value.liveList.length > 0) {
|
||||
const firstLive = sceneData.value.liveList[0];
|
||||
console.log("使用第一个直播源初始化:", firstLive);
|
||||
|
||||
initPlayer(false, {
|
||||
playUrl: firstLive.playUrl,
|
||||
coverUrl: firstLive.coverUrl,
|
||||
name: firstLive.name
|
||||
});
|
||||
} else {
|
||||
// 没有直播列表,使用默认视频源
|
||||
const newPlayer = initPlayer(false);
|
||||
}
|
||||
|
||||
// 延迟加载现场直播数据
|
||||
setTimeout(() => {
|
||||
if (reportTab.value) {
|
||||
reportTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 配置微信分享
|
||||
wxShare({
|
||||
title: route.meta.title,
|
||||
desc: sceneData.value.liveDesc || sceneData.value.title,
|
||||
imgUrl: sceneData.value.titleImage
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取场景数据失败:", error);
|
||||
showNotify({ type: 'danger', message: '获取场景数据失败' });
|
||||
}
|
||||
};
|
||||
|
||||
// 开始倒计时
|
||||
const startCountdown = () => {
|
||||
if (countdownTime.value <= 0) return;
|
||||
|
||||
countdownTimer = setInterval(() => {
|
||||
countdownTime.value--;
|
||||
|
||||
if (countdownTime.value <= 0) {
|
||||
clearInterval(countdownTimer);
|
||||
// 倒计时结束,重新获取场景数据并自动播放
|
||||
getSenceList();
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 监听倒计时变化
|
||||
watch(countdownTime, (newValue) => {
|
||||
if (newValue <= 0 && countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
// 倒计时结束,重新获取场景数据并自动播放
|
||||
getSenceList();
|
||||
}
|
||||
});
|
||||
|
||||
// 修改视角切换处理函数
|
||||
const handleViewChange = async (viewData) => {
|
||||
if (!viewData || !viewData.playUrl) {
|
||||
console.error('视频地址不存在:', viewData);
|
||||
showNotify({ type: 'warning', message: '视频地址不存在' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('切换视角:', viewData);
|
||||
|
||||
// 更新视频URL和封面图
|
||||
currentVideoUrl.value = viewData.playUrl;
|
||||
if (viewData.coverUrl) {
|
||||
coverImage.value = viewData.coverUrl;
|
||||
}
|
||||
|
||||
// 等待DOM更新完成
|
||||
await nextTick();
|
||||
|
||||
// 通知阿里播放器切换视频源
|
||||
if (aliPlayer.value) {
|
||||
try {
|
||||
// 检查播放器实例的方法
|
||||
console.log('阿里播放器实例方法:', Object.keys(aliPlayer.value));
|
||||
|
||||
// 尝试使用播放器的source属性或方法来切换视频源
|
||||
if (typeof aliPlayer.value.loadSource === 'function') {
|
||||
await aliPlayer.value.loadSource(viewData.playUrl);
|
||||
} else if (typeof aliPlayer.value.source === 'function') {
|
||||
await aliPlayer.value.source(viewData.playUrl);
|
||||
} else if (typeof aliPlayer.value.load === 'function') {
|
||||
await aliPlayer.value.load(viewData.playUrl);
|
||||
} else {
|
||||
// 如果没有合适的方法,则重新创建播放器实例
|
||||
console.log('重新创建播放器实例');
|
||||
const newPlayer = initPlayer(false, viewData);
|
||||
if (newPlayer) {
|
||||
player = newPlayer;
|
||||
}
|
||||
}
|
||||
console.log('视角切换成功');
|
||||
} catch (error) {
|
||||
console.error('阿里播放器切换视频源失败:', error);
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new Error('阿里播放器实例不存在');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换视频源失败:', error);
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '切换视频源失败,请刷新页面重试'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理倒计时结束
|
||||
const handleCountdownEnd = () => {
|
||||
// 重新获取场景数据并初始化播放器
|
||||
getSenceList();
|
||||
};
|
||||
|
||||
// 监听场景数据变化,当数据变化时刷新当前标签页
|
||||
watch(sceneData, (newData) => {
|
||||
if (newData) {
|
||||
console.log('场景数据已更新,刷新当前标签页:', activeName.value);
|
||||
// 根据当前激活的标签页重新加载数据
|
||||
setTimeout(() => {
|
||||
if (activeName.value === 'a' && abstractTab.value) {
|
||||
abstractTab.value.loadData();
|
||||
} else if (activeName.value === 'b' && reportTab.value) {
|
||||
reportTab.value.loadData();
|
||||
} else if (activeName.value === 'c' && commentTab.value) {
|
||||
commentTab.value.loadData();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
// 监听路由变化,更新微信标题
|
||||
watch(() => route.meta.title, (newTitle) => {
|
||||
if (newTitle) {
|
||||
document.title = newTitle;
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 修改播放器就绪处理函数
|
||||
const handlePlayerReady = (player) => {
|
||||
console.log('播放器就绪:', player);
|
||||
playerInitialized.value = true;
|
||||
};
|
||||
|
||||
// 修改播放器错误处理函数
|
||||
const handlePlayerError = (error) => {
|
||||
console.error('播放器错误:', error);
|
||||
showNotify({
|
||||
type: 'danger',
|
||||
message: '播放器错误,请检查网络连接或刷新页面重试'
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
/* AliPlayer */
|
||||
.prism-player .prism-controlbar .prism-setting,
|
||||
.prism-player .prism-controlbar .prism-subtitle {
|
||||
/* 播放器容器样式 */
|
||||
.player-container {
|
||||
width: 100%;
|
||||
height: 211px;
|
||||
position: relative;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 西瓜播放器样式 */
|
||||
.xg-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* 封面图样式 */
|
||||
.player-cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 2;
|
||||
background: #000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.player-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 自定义西瓜播放器控制栏样式 */
|
||||
.xgplayer .xgplayer-controls {
|
||||
position: absolute !important;
|
||||
bottom: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
height: 44px !important;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 加载中样式 */
|
||||
.xgplayer .xgplayer-loading {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-loading .xgplayer-loading-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 错误提示样式 */
|
||||
.xgplayer .xgplayer-error {
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
.prism-player .prism-ErrorMessage
|
||||
{
|
||||
/* top:20%; */
|
||||
display: none !important;
|
||||
|
||||
}
|
||||
|
||||
.prism-player .prism-big-play-btn
|
||||
{
|
||||
top:50%!important;
|
||||
left:50%!important;
|
||||
transform:translate(-50%, -50%);
|
||||
/* 移动端适配 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.xgplayer .xgplayer-controls {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-slider {
|
||||
height: 2px !important;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-icon {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.prism-progress-cursor
|
||||
{
|
||||
margin-left:0px !important;
|
||||
/* 横屏模式优化 */
|
||||
@media screen and (orientation: landscape) {
|
||||
.player-container {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.xg-player,
|
||||
.player-cover {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.xgplayer .xgplayer-controls {
|
||||
height: 50px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.prism-player video
|
||||
{
|
||||
object-fit: cover;
|
||||
object-position: 0 0;
|
||||
}
|
||||
|
||||
|
||||
.van-floating-bubble {
|
||||
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0 !important;
|
||||
transform: translate3d(325.812px, 50vh, 0px)!important;
|
||||
width: 50px;
|
||||
height: 80px;
|
||||
background: #eeeeee;
|
||||
transform: translate3d(325.812px, 50vh, 0px) !important;
|
||||
width: 50px !important;
|
||||
height: 80px !important;
|
||||
background: #eeeeee !important;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: 4px !important;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.custom-bubble.van-floating-bubble::before {
|
||||
border-radius: 4px !important;
|
||||
background: #eeeeee !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.van-tabs__content {
|
||||
width: 100%;
|
||||
height: calc(100vh - 130px);
|
||||
/* border:1px solid red; */
|
||||
overflow-y: scroll;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* 顶部播放器容器 */
|
||||
.top {
|
||||
width: 100%;
|
||||
height: 211px;
|
||||
box-sizing: border-box;
|
||||
background: #000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* VR播放器样式 */
|
||||
.top .vr-player-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.top .video-js {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bubble-content {
|
||||
color: rgba(0, 0, 0, 80%);
|
||||
@ -248,28 +776,25 @@ const getSenceList = () => {
|
||||
width: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 17px;
|
||||
}
|
||||
.bubble-content::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 30%;
|
||||
background: url("../assets/img/jt_black.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
left: -15px;
|
||||
top: 30%;
|
||||
position: absolute;
|
||||
}
|
||||
// 顶部播放器
|
||||
.top {
|
||||
width: 100%;
|
||||
height: 211px;
|
||||
|
||||
box-sizing: border-box;
|
||||
.prism-player {
|
||||
width: 100%;
|
||||
height: 56.26667vw;
|
||||
writing-mode: vertical-lr;
|
||||
text-orientation: upright;
|
||||
letter-spacing: 2px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
width: 10px;
|
||||
height: 30%;
|
||||
background: url("../assets/img/jt_black.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
left: -15px;
|
||||
top: 35%;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,162 +1,312 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="homePage">
|
||||
<headerVue></headerVue>
|
||||
<div class="home_scroll">
|
||||
<normalSwiper />
|
||||
<div class="channel bg">
|
||||
<titleVue />
|
||||
<gridSwiper />
|
||||
<div class="homePage">
|
||||
<headerVue></headerVue>
|
||||
<div class="home_scroll" @scroll="handleScroll">
|
||||
<!-- 轮播图部分 -->
|
||||
<normalSwiper :items="carouselScene" />
|
||||
|
||||
<!-- 栏目卡片部分 -->
|
||||
<div
|
||||
v-for="(channel, index) in sceneGroupByChannel"
|
||||
:key="channel.channelId"
|
||||
class="channel bg"
|
||||
v-show="
|
||||
channel.isDisplay === 1 &&
|
||||
channel.liveScenes &&
|
||||
channel.liveScenes.length > 0
|
||||
"
|
||||
>
|
||||
<titleVue
|
||||
:title="channel.titleSetting || channel.channelName"
|
||||
:channel-id="channel.channelId"
|
||||
:channel-name="channel.channelName"
|
||||
/>
|
||||
<component
|
||||
:is="getComponentType(channel.componentType)"
|
||||
:items="channel.liveScenes"
|
||||
:layout="channel.componentLayout"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 直播列表部分 -->
|
||||
<div class="live-list-section">
|
||||
<div class="live-list">
|
||||
<div
|
||||
v-for="(item, index) in liveList"
|
||||
:key="index"
|
||||
class="live-item bg"
|
||||
@click="navigateToDetail(item)"
|
||||
>
|
||||
<div class="live-cover">
|
||||
<img
|
||||
:src="item.titleImageShow"
|
||||
:alt="item.title"
|
||||
/>
|
||||
<div
|
||||
class="live-status"
|
||||
:class="getLiveStateClass(item.liveState)"
|
||||
>
|
||||
{{ getLiveStatus(item.liveState) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="live-info">
|
||||
<div class="live-title">{{ item.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<div v-if="hasMore" class="load-more" @click="loadMore">
|
||||
{{ loading ? "加载中..." : "点击加载更多" }}
|
||||
</div>
|
||||
<div v-else class="no-more">没有更多数据了</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="channel bg">
|
||||
<titleVue />
|
||||
<smallSlideSwiper />
|
||||
</div>
|
||||
<div class="bg">
|
||||
<titleVue style="padding: 10px 20px" />
|
||||
<slideSwiperVue />
|
||||
</div>
|
||||
<div class="channel bg">
|
||||
<titleVue />
|
||||
<gridSwiper />
|
||||
</div>
|
||||
<div class="bg">
|
||||
<titleVue style="padding: 10px 20px" />
|
||||
<slideSwiperVue />
|
||||
</div>
|
||||
<div class="channel bg">
|
||||
<titleVue />
|
||||
<longSlideSwiper />
|
||||
</div>
|
||||
<div class="channel bg">
|
||||
<titleVue />
|
||||
<smallSlideSwiper />
|
||||
</div>
|
||||
<div
|
||||
class="bg normal_item"
|
||||
v-for="(item, index) in simpleList"
|
||||
:key="index"
|
||||
>
|
||||
<!-- <div class="tri"> -->
|
||||
<img :src="item.img" @click="goDetail(item)" />
|
||||
<p class="title">{{ item.title }}</p>
|
||||
<!-- </div> -->
|
||||
<p
|
||||
class="status"
|
||||
:class="{
|
||||
live_status: getStatusClass(item.status) === '直播中',
|
||||
live_coming: getStatusClass(item.status) === '预告',
|
||||
live_review: getStatusClass(item.status) === '精彩回顾',
|
||||
}"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="more">点击加载更多</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import headerVue from "../components/header.vue";
|
||||
import normalSwiper from "../components/swipers/normalSwiper.vue";
|
||||
import gridSwiper from "../components/swipers/gridSwiper.vue";
|
||||
import slideSwiperVue from "../components/swipers/slideSwiper.vue";
|
||||
import smallSlideSwiper from "../components/swipers/smallSlideSwiper.vue";
|
||||
import longSlideSwiper from "../components/swipers/longSlideSwiper.vue";
|
||||
import titleVue from "../components/title.vue";
|
||||
import grid1 from "../assets/img/top1.png";
|
||||
import grid2 from "../assets/img/top1.png";
|
||||
import grid3 from "../assets/img/top1.png";
|
||||
import grid4 from "../assets/img/top1.png";
|
||||
import {useRouter} from 'vue-router';
|
||||
import TypeASwiper from "../components/swipers/TypeASwiper.vue";
|
||||
import TypeBSwiper from "../components/swipers/TypeBSwiper.vue";
|
||||
import TypeCSwiper from "../components/swipers/TypeCSwiper.vue";
|
||||
import { homeList } from "../api/index";
|
||||
import { getHomeList } from "../api/detail";
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
const simpleList = ref([
|
||||
{
|
||||
id: "0",
|
||||
status: "0",
|
||||
img: grid1,
|
||||
title: "防空警报",
|
||||
},
|
||||
{
|
||||
id: "1",
|
||||
status: "1",
|
||||
img: grid2,
|
||||
title: "防空警报",
|
||||
},
|
||||
{ id: "2", status: "2", img: grid3, title: "防空警报" },
|
||||
{ id: "3", status: "1", img: grid4, title: "防空警报" },
|
||||
]);
|
||||
// 定义状态码与状态文本的映射
|
||||
const statusMap = {
|
||||
0: "直播中",
|
||||
1: "预告",
|
||||
2: "精彩回顾",
|
||||
const carouselScene = ref([]);
|
||||
const sceneGroupByChannel = ref([]);
|
||||
const liveList = ref([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
// 导航到详情页
|
||||
const navigateToDetail = (item) => {
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
});
|
||||
};
|
||||
|
||||
// 根据状态码返回状态文本
|
||||
const getStatusText = (status) => statusMap[status] || "";
|
||||
const getLiveStateClass = (state) => {
|
||||
const stateMap = {
|
||||
0: "live_coming",
|
||||
1: "live_status",
|
||||
2: "live_review",
|
||||
};
|
||||
return stateMap[state] || "";
|
||||
};
|
||||
|
||||
// 根据状态码返回对应的CSS类名
|
||||
const getStatusClass = (status) => statusMap[status] || "";
|
||||
const goDetail = (item) => {
|
||||
console.log(item)
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
query: { item: item.id }
|
||||
// 获取对应的组件类型
|
||||
const getComponentType = (type) => {
|
||||
const componentMap = {
|
||||
组件A: TypeASwiper,
|
||||
组件B: TypeBSwiper,
|
||||
组件C: TypeCSwiper,
|
||||
};
|
||||
return componentMap[type] || TypeASwiper;
|
||||
};
|
||||
|
||||
// 获取首页数据
|
||||
const fetchHomeData = () => {
|
||||
getHomeList()
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
carouselScene.value = res.appView.carouselScene;
|
||||
sceneGroupByChannel.value = res.appView.sceneGroupByChannel;
|
||||
}
|
||||
})
|
||||
}
|
||||
.catch((error) => {
|
||||
console.error("获取首页数据失败:", error);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取直播列表数据
|
||||
const fetchLiveList = async (page = 1) => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const res = await homeList({
|
||||
page: page,
|
||||
size: pageSize.value,
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
if (page === 1) {
|
||||
liveList.value = res.AppAllScene || [];
|
||||
} else {
|
||||
liveList.value = [...liveList.value, ...(res.AppAllScene || [])];
|
||||
}
|
||||
hasMore.value = (res.AppAllScene || []).length >= pageSize.value;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取直播列表失败:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !loading.value) {
|
||||
currentPage.value += 1;
|
||||
fetchLiveList(currentPage.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time) => {
|
||||
const date = new Date(time);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(
|
||||
2,
|
||||
"0"
|
||||
)}-${String(date.getDate()).padStart(2, "0")} ${String(
|
||||
date.getHours()
|
||||
).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
// 获取直播状态
|
||||
const getLiveStatus = (status) => {
|
||||
const statusMap = {
|
||||
0: "预告",
|
||||
1: "直播中",
|
||||
2: "精彩回顾",
|
||||
};
|
||||
return statusMap[status] || "";
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchHomeData();
|
||||
fetchLiveList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.homePage {
|
||||
width: 100%;
|
||||
// width:720px;
|
||||
// margin: 0 auto;
|
||||
height: auto;
|
||||
background-color: #f5f6f8;
|
||||
.home_scroll{
|
||||
height:calc(100vh - 4rem);
|
||||
|
||||
.home_scroll {
|
||||
height: calc(100vh - 4rem);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
|
||||
.bg {
|
||||
margin-top: 10px;
|
||||
padding:0 0 10px 0;
|
||||
background: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
.normal_item {
|
||||
width: 100%;
|
||||
// height: 212px;
|
||||
// padding: 10px 0;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 212px;
|
||||
}
|
||||
.title {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
.status {
|
||||
top:10px;
|
||||
left:25px;
|
||||
position: absolute;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.channel {
|
||||
padding: 0 20px;
|
||||
|
||||
height: auto;
|
||||
padding: 0 20px 0px;
|
||||
margin-bottom: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
.more {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
color: gray;
|
||||
|
||||
.live-list-section {
|
||||
.live-list {
|
||||
.live-item {
|
||||
margin-bottom: 15px;
|
||||
// border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.live-cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 56.25%; // 16:9 比例
|
||||
background: #f0f0f0;
|
||||
|
||||
img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 0px!important;
|
||||
}
|
||||
|
||||
.live-status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.live-info {
|
||||
padding: 12px;
|
||||
|
||||
.live-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.live-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
|
||||
.live-position {
|
||||
background: #f5f5f5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.live-time {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more,
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
margin: 10px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
// background: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1989fa;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #e8f2ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
77
src/views/moreLiveView.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div class="more-live-view">
|
||||
<div class="nav-header">
|
||||
<div class="back-button" @click="goBack">
|
||||
<van-icon name="arrow-left" size="20" />
|
||||
</div>
|
||||
<div class="page-title">更多直播</div>
|
||||
<div class="empty-space"></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<moreLive @select-live="goToDetail" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router';
|
||||
import moreLive from '../components/moreLive.vue';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// 跳转到直播详情页
|
||||
const goToDetail = (item) => {
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.more-live-view {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-header {
|
||||
height: 50px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 15px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
background-color: #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.empty-space {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div class="header1">
|
||||
<div class="search">
|
||||
<van-search
|
||||
v-model="value"
|
||||
@ -18,7 +18,7 @@
|
||||
<p @click="onSearch">搜索</p>
|
||||
</div>
|
||||
|
||||
<van-dropdown-menu ref="menuRef" >
|
||||
<van-dropdown-menu ref="menuRef" class="filter-menu">
|
||||
<van-dropdown-item @click="onConfirm" v-model="isDropdownOpen" title="筛选" ref="itemRef">
|
||||
<div class="choice_list">
|
||||
<div class="tri">
|
||||
@ -80,31 +80,67 @@
|
||||
</div>
|
||||
</van-dropdown-item>
|
||||
</van-dropdown-menu>
|
||||
<contentList v-show="showSearchList"/>
|
||||
<contentList v-show="showSearchList" ref="contentListRef"/>
|
||||
|
||||
<!-- 自定义时间选择弹窗 -->
|
||||
<van-popup
|
||||
v-model:show="showBottom"
|
||||
v-model:show="showCustomDatePicker"
|
||||
position="bottom"
|
||||
:style="{ height: '50%' }"
|
||||
round
|
||||
closeable
|
||||
:style="{ height: '45%' }"
|
||||
>
|
||||
<van-picker-group
|
||||
v-if="showPickerGroup"
|
||||
:tabs="['开始日期', '结束日期']"
|
||||
@confirm="timeConfirm"
|
||||
@cancel="timeCancel"
|
||||
:style="{ height: '100%' }"
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="startDate"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
/>
|
||||
<van-date-picker
|
||||
v-model="endDate"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
/>
|
||||
</van-picker-group>
|
||||
<div class="custom-time-picker">
|
||||
<div class="picker-title">选择时间范围</div>
|
||||
<div class="time-range-buttons">
|
||||
<div class="time-button" @click="openStartPicker">
|
||||
<span class="label">开始时间</span>
|
||||
<span class="value">{{ formatDisplayDate(customStartDate) }}</span>
|
||||
</div>
|
||||
<div class="time-button" @click="openEndPicker">
|
||||
<span class="label">结束时间</span>
|
||||
<span class="value">{{ formatDisplayDate(customEndDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<van-button block type="default" @click="cancelCustomDate">取消</van-button>
|
||||
<van-button block type="primary" color="#004098" @click="confirmCustomDate">确定</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-popup>
|
||||
|
||||
<!-- 开始日期选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showStartPicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="aastartDate"
|
||||
title="选择开始时间"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="onStartDateConfirm"
|
||||
@cancel="showStartPicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 结束日期选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showEndPicker"
|
||||
position="bottom"
|
||||
round
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="aaendDate"
|
||||
title="选择结束时间"
|
||||
:min-date="minDateEnd"
|
||||
:max-date="maxDateEnd"
|
||||
@confirm="onEndDateConfirm"
|
||||
@cancel="showEndPicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<div class="hot_list" v-show="!showSearchList">
|
||||
<div class="hot_title">
|
||||
<img aria-hidden="true" alt="fire-icon" src="../assets/img/hot.svg" />
|
||||
@ -112,14 +148,14 @@
|
||||
</div>
|
||||
|
||||
<table cellspacing="0" cellpadding="">
|
||||
<tr v-for="item in topList" :key="item.id">
|
||||
<td class="order" :style="{ color: getColor(item.id) }">
|
||||
{{ item.id }}
|
||||
<tr v-for="(item,index) in topList" :key="index" @click="onHotItemClick(item)">
|
||||
<td class="order" :style="{ color: getColor(index + 1) }">
|
||||
{{ index + 1 }}
|
||||
</td>
|
||||
<td>{{ item.content }}</td>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>
|
||||
<span :style="{ background: getBgColor(item.id) }" class="bg_color">{{
|
||||
getTipText(item.id)
|
||||
<span :style="{ background: getBgColor(index + 1) }" class="bg_color">{{
|
||||
getTipText(index + 1)
|
||||
}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@ -127,62 +163,339 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref,watch } from "vue";
|
||||
import { ref, watch, onMounted, reactive, computed } from "vue";
|
||||
import contentList from "../components/contentList.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getHot } from "../api/detail";
|
||||
import { search } from "../api/search";
|
||||
import { showToast } from 'vant';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const menuRef = ref(null);
|
||||
const itemRef = ref(null);
|
||||
const value = ref(null);
|
||||
const value = ref(""); // 初始化为空字符串
|
||||
const showSearchList = ref(false);
|
||||
const onSearch = () => {
|
||||
console.log("搜索",value.value);
|
||||
showSearchList.value = true;
|
||||
const loading = ref(false);
|
||||
const searchResults = ref([]);
|
||||
const contentListRef = ref(null);
|
||||
|
||||
// 分页参数
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
// 搜索参数
|
||||
const searchParams = reactive({
|
||||
liveDesc: "",
|
||||
startTime: "",
|
||||
endTime: "",
|
||||
channelName: "",
|
||||
page: "1",
|
||||
size: "10",
|
||||
timeState: 0
|
||||
});
|
||||
|
||||
// 日期选择器状态
|
||||
const showCustomDatePicker = ref(false);
|
||||
const showStartPicker = ref(false);
|
||||
const showEndPicker = ref(false);
|
||||
|
||||
// 获取当前日期
|
||||
const now = new Date();
|
||||
|
||||
// 日期选择器的值
|
||||
const aastartDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
|
||||
const aaendDate = ref([new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]);
|
||||
|
||||
// 日期选择器范围 - 使用ref包装,避免重复创建
|
||||
const minDate = ref(new Date(2020, 0, 1));
|
||||
const maxDate = ref(new Date());
|
||||
|
||||
// 日期选择相关的状态
|
||||
const customStartDate = ref(new Date());
|
||||
const customEndDate = ref(new Date());
|
||||
|
||||
// 结束时间选择器的值
|
||||
const minDateEnd = ref(new Date(2020, 0, 1));
|
||||
const maxDateEnd = ref(new Date());
|
||||
|
||||
// 格式化日期为字符串(用于API请求)
|
||||
const formatDate = (date) => {
|
||||
if (!date) return '';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} 00:00:00`;
|
||||
};
|
||||
|
||||
// 打开开始时间选择器
|
||||
const openStartPicker = () => {
|
||||
if (customStartDate.value) {
|
||||
const date = new Date(customStartDate.value);
|
||||
aastartDate.value = [date.getFullYear(), date.getMonth() + 1, date.getDate()];
|
||||
}
|
||||
showStartPicker.value = true;
|
||||
};
|
||||
|
||||
// 打开结束时间选择器
|
||||
const openEndPicker = () => {
|
||||
if (customEndDate.value) {
|
||||
const date = new Date(customEndDate.value);
|
||||
aaendDate.value = [date.getFullYear(), date.getMonth() + 1, date.getDate()];
|
||||
}
|
||||
showEndPicker.value = true;
|
||||
};
|
||||
|
||||
// 开始日期确认
|
||||
const onStartDateConfirm = (result) => {
|
||||
try {
|
||||
console.log('开始日期选择结果:', result);
|
||||
if (result && result.selectedValues) {
|
||||
const [year, month, day] = result.selectedValues;
|
||||
const selectedDate = new Date(year, month - 1, day);
|
||||
|
||||
if (!isNaN(selectedDate.getTime())) {
|
||||
customStartDate.value = selectedDate;
|
||||
searchParams.startTime = formatDate(selectedDate);
|
||||
console.log('设置的开始日期:', formatDate(selectedDate));
|
||||
showStartPicker.value = false;
|
||||
} else {
|
||||
console.error('创建的开始日期无效:', year, month, day);
|
||||
}
|
||||
} else {
|
||||
console.error('无效的开始日期结果:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('开始日期处理错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 结束日期确认
|
||||
const onEndDateConfirm = (result) => {
|
||||
try {
|
||||
console.log('结束日期选择结果:', result);
|
||||
if (result && result.selectedValues) {
|
||||
const [year, month, day] = result.selectedValues;
|
||||
const selectedDate = new Date(year, month - 1, day);
|
||||
|
||||
if (!isNaN(selectedDate.getTime())) {
|
||||
customEndDate.value = selectedDate;
|
||||
searchParams.endTime = formatDate(selectedDate);
|
||||
console.log('设置的结束日期:', formatDate(selectedDate));
|
||||
showEndPicker.value = false;
|
||||
} else {
|
||||
console.error('创建的结束日期无效:', year, month, day);
|
||||
}
|
||||
} else {
|
||||
console.error('无效的结束日期结果:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('结束日期处理错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化显示日期(仅用于显示,不包含时分秒)
|
||||
const formatDisplayDate = (date) => {
|
||||
if (!date) return '请选择';
|
||||
const d = new Date(date);
|
||||
if (isNaN(d.getTime())) return '请选择';
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 时间筛选逻辑
|
||||
const selectTime = (time) => {
|
||||
selectedTime.value = time;
|
||||
|
||||
if (time === "不限") {
|
||||
searchParams.startTime = "";
|
||||
searchParams.endTime = "";
|
||||
searchParams.timeState = 0;
|
||||
executeSearch();
|
||||
} else if (time === "最近一天") {
|
||||
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
searchParams.startTime = formatDate(oneDayAgo);
|
||||
searchParams.endTime = formatDate(now);
|
||||
searchParams.timeState = 1;
|
||||
executeSearch();
|
||||
} else if (time === "最近一周") {
|
||||
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
searchParams.startTime = formatDate(oneWeekAgo);
|
||||
searchParams.endTime = formatDate(now);
|
||||
searchParams.timeState = 2;
|
||||
executeSearch();
|
||||
} else if (time === "最近一月") {
|
||||
const oneMonthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate());
|
||||
searchParams.startTime = formatDate(oneMonthAgo);
|
||||
searchParams.endTime = formatDate(now);
|
||||
searchParams.timeState = 3;
|
||||
executeSearch();
|
||||
} else if (time === "自定义") {
|
||||
showCustomDatePicker.value = true;
|
||||
customStartDate.value = new Date();
|
||||
customEndDate.value = new Date();
|
||||
searchParams.timeState = 4;
|
||||
}
|
||||
};
|
||||
|
||||
// 确认自定义日期范围
|
||||
const confirmCustomDate = () => {
|
||||
try {
|
||||
if (customStartDate.value > customEndDate.value) {
|
||||
showToast('开始时间不能晚于结束时间');
|
||||
return;
|
||||
}
|
||||
|
||||
searchParams.startTime = formatDate(customStartDate.value);
|
||||
searchParams.endTime = formatDate(customEndDate.value);
|
||||
showCustomDatePicker.value = false;
|
||||
executeSearch();
|
||||
} catch (error) {
|
||||
console.error('处理日期选择时出错:', error);
|
||||
showToast('日期选择出错,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 取消自定义日期选择
|
||||
const cancelCustomDate = () => {
|
||||
showCustomDatePicker.value = false;
|
||||
if (!searchParams.startTime && !searchParams.endTime) {
|
||||
selectedTime.value = '不限';
|
||||
searchParams.timeState = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取热点榜数据
|
||||
const getHotList = async () => {
|
||||
getHot()
|
||||
.then((res) => {
|
||||
if(res.code === 0){
|
||||
topList.value = res.hotSpot;
|
||||
console.log("获取热点榜", topList.value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("获取热点榜失败", error);
|
||||
});
|
||||
};
|
||||
|
||||
// 执行搜索接口调用
|
||||
const executeSearch = async () => {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
// 构建搜索参数
|
||||
searchParams.liveDesc = value.value;
|
||||
searchParams.page = page.value.toString();
|
||||
searchParams.size = pageSize.value.toString();
|
||||
|
||||
console.log("执行搜索,参数:", searchParams);
|
||||
|
||||
const res = await search(searchParams);
|
||||
|
||||
if (res.code === 0) {
|
||||
// 根据实际接口返回结构获取搜索结果
|
||||
searchResults.value = res.appSearch || [];
|
||||
total.value = res.appSearch?.length || 0;
|
||||
|
||||
// 将搜索结果传递给contentList组件
|
||||
if (contentListRef.value) {
|
||||
contentListRef.value.updateList(searchResults.value);
|
||||
}
|
||||
|
||||
showSearchList.value = true;
|
||||
console.log("搜索结果:", searchResults.value);
|
||||
} else {
|
||||
console.error("搜索失败:", res.msg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("搜索请求出错:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 点击搜索按钮事件
|
||||
const onSearch = () => {
|
||||
console.log("搜索关键词:", value.value);
|
||||
page.value = 1; // 重置页码
|
||||
executeSearch();
|
||||
};
|
||||
|
||||
// 清空搜索
|
||||
const onClear = () => {
|
||||
value.value = ""; // 清空搜索框的值
|
||||
showSearchList.value = false; // 显示热点榜
|
||||
};
|
||||
const onConfirm = () => {
|
||||
if( itemRef.value.toggle(false)){
|
||||
// 清空选中的数据
|
||||
// 重置搜索参数
|
||||
searchParams.liveDesc = "";
|
||||
searchParams.startTime = "";
|
||||
searchParams.endTime = "";
|
||||
searchParams.channelName = "";
|
||||
searchParams.timeState = 0;
|
||||
selectedTopics.value = [];
|
||||
selectedTime.value = "";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// 确认筛选
|
||||
const onConfirm = () => {
|
||||
executeSearch();
|
||||
itemRef.value.toggle(false); // 关闭筛选菜单
|
||||
};
|
||||
|
||||
const isDropdownOpen = ref(false); // 初始化下拉菜单状态为关闭
|
||||
const showPickerGroup = ref(false);
|
||||
const topics = ["不限", "推荐", "政务", "综合", "县区", "社会", "活动", "拍客"];
|
||||
const selectedTopics = ref([]);
|
||||
const timeOptions = ["不限", "最近一天", "最近一周", "最近一月", "自定义"];
|
||||
const selectedTime = ref("");
|
||||
const showBottom = ref(false);//底部弹出框
|
||||
const selectTime = (time,event) => {
|
||||
selectedTime.value = time;
|
||||
if (time === "自定义") {
|
||||
|
||||
showBottom.value = true;
|
||||
showPickerGroup.value = true;
|
||||
// 专题栏目筛选
|
||||
const toggleTopic = (topic) => {
|
||||
if (topic === "不限") {
|
||||
// 如果选择的是"不限",清空其他选项
|
||||
selectedTopics.value = topic === "不限" ? ["不限"] : [];
|
||||
searchParams.channelName = "";
|
||||
} else {
|
||||
// 如果选择的不是"不限",移除"不限"选项
|
||||
selectedTopics.value = selectedTopics.value.filter(t => t !== "不限");
|
||||
|
||||
// 切换选中状态
|
||||
if (selectedTopics.value.includes(topic)) {
|
||||
selectedTopics.value = selectedTopics.value.filter(t => t !== topic);
|
||||
} else {
|
||||
selectedTopics.value.push(topic);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新搜索参数
|
||||
if (selectedTopics.value.includes("不限") || selectedTopics.value.length === 0) {
|
||||
searchParams.channelName = "";
|
||||
} else {
|
||||
// 将选中的专题拼接为正确的格式
|
||||
searchParams.channelName = JSON.stringify(selectedTopics.value.join(','));
|
||||
}
|
||||
|
||||
// 实时执行搜索
|
||||
executeSearch();
|
||||
};
|
||||
|
||||
const startDate = ref(["2024", "05", "01"]);
|
||||
const endDate = ref(["2024", "06", "01"]);
|
||||
// 监听热榜项点击
|
||||
const onHotItemClick = (item) => {
|
||||
if (!item || !item.id) return;
|
||||
|
||||
const timeConfirm = (value) => {
|
||||
// 处理确认逻辑
|
||||
console.log("确认的值:", value);
|
||||
showPickerGroup.value = false; // 隐藏日历
|
||||
showBottom.value = false;
|
||||
console.log("点击热点榜项目:", item);
|
||||
|
||||
// 直接跳转到详情页
|
||||
router.push({
|
||||
name: 'Detail',
|
||||
params: { id: item.id }
|
||||
});
|
||||
};
|
||||
|
||||
const timeCancel = () => {
|
||||
// 取消时的处理
|
||||
showPickerGroup.value = false; // 隐藏日历
|
||||
showBottom.value = false;
|
||||
};
|
||||
// 组件挂载时获取热点榜数据
|
||||
onMounted(() => {
|
||||
getHotList();
|
||||
});
|
||||
|
||||
const topList = ref([
|
||||
{
|
||||
@ -249,41 +562,6 @@ const getBgColor = (index) => {
|
||||
return "transparent";
|
||||
}
|
||||
};
|
||||
const toggleTopic = (topic) => {
|
||||
if (selectedTopics.value.includes(topic)) {
|
||||
selectedTopics.value = selectedTopics.value.filter((t) => t !== topic);
|
||||
} else {
|
||||
selectedTopics.value.push(topic);
|
||||
}
|
||||
};
|
||||
watch([selectedTopics, selectedTime], () => {
|
||||
|
||||
// 根据选定的筛选条件更新列表
|
||||
// const filteredList = originalList.filter(item => {
|
||||
// // 根据选定的筛选条件进行过滤逻辑
|
||||
// const matchesTopic = selectedTopics.includes(item.topic);
|
||||
// const matchesTime = item.time >= selectedTime.start && item.time <= selectedTime.end;
|
||||
// return matchesTopic && matchesTime;
|
||||
// });
|
||||
showSearchList.value = true;
|
||||
// 更新展示的列表
|
||||
// if (filteredList.length > 0) {
|
||||
// showSearchList.value = true;
|
||||
// contentList.value = filteredList;
|
||||
// } else {
|
||||
// showSearchList.value = false;
|
||||
// contentList.value = originalList; // 如果筛选结果为空,则显示原始列表
|
||||
// }
|
||||
});
|
||||
// 根据选择条件筛选列表
|
||||
const onSearchList = () => {
|
||||
console.log(selectedTopics.value);
|
||||
console.log(selectedTime.value);
|
||||
itemRef.value.toggle(false); // 添加这行代码来关闭下拉菜单
|
||||
showPickerGroup.value = false; // 如果需要,确保时间选择器也同时关闭
|
||||
showBottom.value = false; // 关闭底部弹窗
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@ -303,14 +581,80 @@ const onSearchList = () => {
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
::v-deep(.van-search__content){
|
||||
background: #fff;
|
||||
background:#fff;
|
||||
}
|
||||
|
||||
// /* 新增图标和提示词样式 */
|
||||
// .van-icon-search {
|
||||
// color: #fff !important;
|
||||
// }
|
||||
|
||||
// .van-field__control::placeholder {
|
||||
// color: #fff !important;
|
||||
// opacity: 0.8;
|
||||
|
||||
// }
|
||||
// }
|
||||
::v-deep(.van-search__action){
|
||||
font-size: 18px;
|
||||
transform: translateX(-30px);
|
||||
color: #cccccc;
|
||||
color: #fff;
|
||||
}
|
||||
.header {
|
||||
|
||||
/* 日期选择器样式 */
|
||||
.date-picker-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-picker-title {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #f2f2f2;
|
||||
}
|
||||
|
||||
.date-picker-section {
|
||||
flex: 1;
|
||||
padding: 10px 15px;
|
||||
|
||||
.date-picker-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.date-picker-buttons {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
gap: 12px;
|
||||
border-top: 1px solid #f2f2f2;
|
||||
|
||||
.van-button {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep(.van-date-picker) {
|
||||
.van-picker__title {
|
||||
color: #004098;
|
||||
}
|
||||
|
||||
.van-picker-column__item--selected {
|
||||
color: #004098;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.header1 {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -327,6 +671,29 @@ background: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 筛选菜单样式 */
|
||||
.filter-menu {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
::v-deep(.van-popup) {
|
||||
z-index: 2000 !important;
|
||||
}
|
||||
|
||||
::v-deep(.van-overlay) {
|
||||
z-index: 1999 !important;
|
||||
}
|
||||
|
||||
::v-deep(.van-dropdown-menu__bar) {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
::v-deep(.van-dropdown-item__content) {
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.choice_list {
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
@ -339,6 +706,8 @@ background: #fff;
|
||||
justify-content: space-between;
|
||||
p {
|
||||
padding: 10px 0;
|
||||
color:#183B86;
|
||||
font-weight: bold;
|
||||
}
|
||||
// align-items: center;
|
||||
.grid {
|
||||
@ -355,6 +724,7 @@ background: #fff;
|
||||
border: 1px solid #183b86 !important;
|
||||
color: #183b86;
|
||||
background: #e4ebf4;
|
||||
font-weight: bold;
|
||||
}
|
||||
.color1 {
|
||||
color: #f43760;
|
||||
@ -413,4 +783,59 @@ background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-time-picker {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
|
||||
.picker-title {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.time-range-buttons {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
|
||||
.time-button {
|
||||
background: #f5f5f5;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
|
||||
.van-button {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -2,27 +2,30 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path';
|
||||
import AutoImport from 'unplugin-auto-import/vite';
|
||||
import Components from 'unplugin-vue-components/vite';
|
||||
import { VantResolver } from '@vant/auto-import-resolver';
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { VantResolver } from 'unplugin-vue-components/resolvers'
|
||||
// 安装 postcss-pxtorem 和 autoprefixer
|
||||
|
||||
// npm install postcss-pxtorem --save
|
||||
// npm i autoprefixer
|
||||
import pxtoviewport from 'postcss-px-to-viewport';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base:'./',
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
resolvers: [VantResolver()],
|
||||
imports: ['vue', 'vue-router'],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [VantResolver()],
|
||||
// 禁用自动导入样式
|
||||
dts: true,
|
||||
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
|
||||
exclude: [/node_modules/],
|
||||
}),
|
||||
|
||||
|
||||
],
|
||||
// css: {
|
||||
|
||||
@ -36,17 +39,27 @@ export default defineConfig({
|
||||
// },
|
||||
|
||||
// },
|
||||
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
port: 8123,
|
||||
|
||||
open: true,
|
||||
cors: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.1.100:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias:{
|
||||
// 配置src目录
|
||||
"@": path.resolve(__dirname,"src"),
|
||||
"@": fileURLToPath(new URL('./src', import.meta.url)),
|
||||
// 导入其他目录
|
||||
"components": path.resolve(__dirname, "components")
|
||||
"components": path.resolve(__dirname, "components"),
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
|
||||