完成基本功能

This commit is contained in:
renna 2025-05-14 17:25:59 +08:00
parent 4c3752d154
commit 3bb7032b78
60 changed files with 75113 additions and 1513 deletions

50
.cursorrules Normal file
View 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开发最佳实践。

View File

@ -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'

View File

@ -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
View File

@ -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
View 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']
}
}

View File

@ -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>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
<!-- 预加载关键资源 -->
<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>
</html>

829
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 722 B

2
public/js/aliplayer-h5-min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/js/aliplayer-min.css vendored Normal file

File diff suppressed because one or more lines are too long

2
public/js/aliplayer-min.js vendored Normal file

File diff suppressed because one or more lines are too long

1753
public/js/video-js.css Normal file

File diff suppressed because one or more lines are too long

64464
public/js/video.js Normal file

File diff suppressed because it is too large Load Diff

81
public/js/videojs-vr.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -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
View 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
})
}

View File

@ -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
View File

@ -0,0 +1,11 @@
import request from '../utils/request'
// 条件搜索
export function search(params) {
return request({
url: '/foreign/live/appSearch',
method: 'post',
data: params
});
}

View File

@ -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 {
@ -70,4 +73,4 @@ background-image: linear-gradient(to left, #4BB4FB, #1678ED);
// overflow: hidden;
// }
// }
// }

BIN
src/assets/img/bigstop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/img/unlike.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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) {

View File

@ -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);

View 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>

View 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>

View 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
View 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>

View File

@ -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.itemsmoreList
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>

View File

@ -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);
parseTime(totalTime.value);
timeInterval = setInterval(() => {
totalTime.value--;
if (totalTime.value < 0) {
clearInterval(timeInterval.value);
location.replace(location.href);
parseTime(totalTime.value);
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;
.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;
height:80px;
flex-direction: column;
.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;
}
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>

View File

@ -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%;

View File

@ -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;

View File

@ -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>

View 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||'游客'}}:&nbsp;&nbsp;</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; // loadingfalseloadMoreloading
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>

View File

@ -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||'游客'}}:&nbsp;&nbsp;</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,
};
commentList(params)
.then((res) => {
console.log("获取评论列表",res);
commentData.value =res.data;
imageAvatarUrl.value = res.anonymousUrl
})
.catch((error) => {
console.error("登录接口请求失败", error);
});
};
</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=()=> {
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));
thumbsUpAnimation.start();
// showNotify({ type: 'success', message: '' });
}
} catch (error) {
console.error('点赞失败:', error);
// showNotify({ type: 'danger', message: '' });
} finally {
// 300ms
setTimeout(() => {
clickDebounce.value = false;
}, 300);
}
};
// formData
let formdate = new FormData();
formdate.append("utoken", utoken);
formdate.append("sceneId", sceneId);
formdate.append("text", text);
//
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
});
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 = "";
});
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; // loadingfalseloadMoreloading
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;
.comment-card-main {
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;
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;

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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;

View File

@ -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);
}
}

View 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;
}

View 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;
}
}

View File

@ -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
View 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>

View File

@ -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;
// border: 1px solid black;
box-sizing: border-box;
}
#xgPlayerWrap {
flex: auto;
}
#xgPlayerWrap video {
width: 100%;
object-fit: cover;
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>

View File

@ -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();
});
// ID
watch(() => route.params.id, (newId, oldId) => {
if (newId && newId !== oldId) {
console.log(`路由ID从${oldId}更改为${newId},重新获取场景数据`);
getSenceList();
}
}, { immediate: false });
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,
// 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',
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,
},
{
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
},
]
}
]
//
errorTips: '视频加载失败,请刷新重试',
loadingTimeout: 20,
//
innerKernel: {
handleError: true,
handleKernelError: true,
probePriority: ['hls', 'mp4']
}
};
player = new VideoPlayer(playerProps);
//
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) {
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;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.xgplayer .xgplayer-controls {
height: 40px !important;
}
}
.prism-player .prism-ErrorMessage
{
/* top:20%; */
display: none !important;
.xgplayer .xgplayer-slider {
height: 2px !important;
}
.xgplayer .xgplayer-icon {
width: 24px !important;
height: 24px !important;
}
}
.prism-player .prism-big-play-btn
{
top:50%!important;
left:50%!important;
transform:translate(-50%, -50%);
}
.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>

View File

@ -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>

View 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>

View File

@ -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 }}
</td>
<td>{{ item.content }}</td>
<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.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 === "自定义") {
//
const toggleTopic = (topic) => {
if (topic === "不限") {
// ""
selectedTopics.value = topic === "不限" ? ["不限"] : [];
searchParams.channelName = "";
} else {
// """"
selectedTopics.value = selectedTopics.value.filter(t => t !== "不限");
showBottom.value = true;
showPickerGroup.value = true;
//
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 timeConfirm = (value) => {
//
console.log("确认的值:", value);
showPickerGroup.value = false; //
showBottom.value = false;
//
const onHotItemClick = (item) => {
if (!item || !item.id) return;
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>

View File

@ -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"),
}
},