hookehuyr

feat: 添加视频播放器组件及相关工具函数

refactor: 重构首页布局和样式

fix: 修复axios拦截器中的错误处理

docs: 更新mock数据文件注释

chore: 更新package.json依赖项

style: 格式化工具函数代码

perf: 优化视频播放器加载性能

build: 添加video.js相关依赖

test: 添加视频播放器组件测试用例

ci: 更新CI配置以支持视频资源
......@@ -14,6 +14,7 @@
"dependencies": {
"@vant/area-data": "^1.3.1",
"@vant/touch-emulator": "^1.4.0",
"@videojs-player/vue": "^1.0.0",
"@vueuse/core": "^10.7.2",
"axios": "^1.6.7",
"dayjs": "^1.11.10",
......@@ -21,6 +22,7 @@
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"vant": "^4.9.1",
"video.js": "^8.23.4",
"vue": "^3.4.15",
"vue-router": "^4.2.5"
},
......
......@@ -41,5 +41,6 @@ declare module 'vue' {
VanTabbarItem: typeof import('vant/es')['TabbarItem']
VanTabs: typeof import('vant/es')['Tabs']
VanTag: typeof import('vant/es')['Tag']
VideoPlayer: typeof import('./components/VideoPlayer.vue')['default']
}
}
......
This diff is collapsed. Click to expand it.
/*
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2022-05-28 10:17:40
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-07-01 16:23:59
* @FilePath: /mlaj/src/utils/axios.js
* @Description:
*/
import axios from 'axios';
import router from '@/router';
// import qs from 'Qs'
// import { strExist } from '@/utils/tools'
// axios.defaults.baseURL = 'http://localhost:3000/api';
axios.defaults.params = {
f: 'behalo',
};
/**
* 设置用户认证信息到请求头
* @param {string} userId - 用户ID
* @param {string} userToken - 用户Token
*/
export const setAuthHeaders = (userId, userToken) => {
if (userId && userToken) {
axios.defaults.headers['User-Id'] = userId;
axios.defaults.headers['User-Token'] = userToken;
}
};
/**
* 清除用户认证信息
*/
export const clearAuthHeaders = () => {
delete axios.defaults.headers['User-Id'];
delete axios.defaults.headers['User-Token'];
};
/**
* @description 请求拦截器
*/
axios.interceptors.request.use(
config => {
/**
* 司总授权信息
* 动态获取 user_info 并设置到请求头
* 确保每个请求都带上最新的 user_info
*/
const user_info = localStorage.getItem('user_info') ? JSON.parse(localStorage.getItem('user_info')) : {};
if (user_info) {
config.headers['User-Id'] = user_info.user_id;
config.headers['User-Token'] = user_info.HTTP_USER_TOKEN;
}
// const url_params = parseQueryString(location.href);
// GET请求默认打上时间戳,避免从缓存中拿数据。
const timestamp = config.method === 'get' ? (new Date()).valueOf() : '';
/**
* POST PHP需要修改数据格式
* 序列化POST请求时需要屏蔽上传相关接口,上传相关接口序列化后报错
*/
// config.data = config.method === 'post' && !strExist(['a=upload', 'upload.qiniup.com'], config.url) ? qs.stringify(config.data) : config.data;
// 绑定默认请求头
config.params = { ...config.params, timestamp }
return config;
},
error => {
// 请求错误处理
return Promise.reject(error);
});
/**
* @description 响应拦截器
*/
axios.interceptors.response.use(
response => {
if (response.data && response.data.code === 401) {
// 清除用户登录信息
localStorage.removeItem('currentUser');
// 清除认证请求头
clearAuthHeaders();
// 跳转到登录页面,并携带当前路由信息
const currentPath = router.currentRoute.value.fullPath;
router.push(`/login?redirect=${encodeURIComponent(currentPath)}`);
// router.push(`/login`);
}
return response;
},
error => {
// 响应错误处理
return Promise.reject(error);
});
export default axios;
/**
* Mock data for the parent-child education app
* This file contains sample data for courses, activities, and users
*/
// Featured course for the top banner
export const featuredCourse = {
id: 'featured-1',
title: '传承之道',
subtitle: '大理鸡足山游学',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg',
liveTime: '14:00:00'
};
// User recommendations
export const userRecommendations = [
{ title: "亲子阅读技巧入门", duration: "15分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg" },
{ title: "3-6岁孩子的情绪管理", duration: "20分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg" },
{ title: "趣味数学启蒙课", duration: "12分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg" },
{ title: "儿童英语绘本故事", duration: "18分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg" },
{ title: "*亲子阅读技巧入门", duration: "15分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/zMRLZh40kms.jpg" },
{ title: "*3-6岁孩子的情绪管理", duration: "20分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/27kCu7bXGEI.jpg" },
{ title: "*趣味数学启蒙课", duration: "12分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg" },
{ title: "*儿童英语绘本故事", duration: "18分钟", image: "https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg" },
];
// Live streaming sessions
export const liveStreams = [
{
id: 'live-1',
title: '无界之世',
subtitle: '敦煌行',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg',
isLive: true,
viewers: 272
},
{
id: 'live-2',
title: '慧眼读书会',
subtitle: '',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg',
isLive: true,
viewers: 272
}
];
// Course list data
export const courses = [
{
id: 'course-1',
title: '好分凭借力,陪你跃龙门!',
subtitle: '4.17-6.18 美乐考前赋能营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/jbwr0qZvpD4.jpg', // Updated with new image
price: 365,
updatedLessons: 16,
subscribers: 1140,
isPurchased: true,
isReviewed: true,
expireDate: '2025-04-17 00:00:00',
description: `【美乐考前赋能营】
结合平台多年亲子教育的实践经验,
针对近在眼前的高考,
和为学业考试而焦虑的群体
精心打造、最为定制的专项课程。
旨在帮助家长更好地理解,支持孩子,
有效管理自己的情绪和压力;
进而引导孩子的情绪状态,
以最佳的心理状态迎接挑战。
在考试的最后冲刺阶段,
给世界一个爱赏识他们的理由,
共同助力每个学子梦想成真!`,
sections: [
{ title: '课程介绍', content: '课程详细描述内容...' },
{ title: '课程目录', content: '第1章:心态准备\n第2章:考前减压\n第3章:家庭支持' }
]
},
{
id: 'course-2',
title: '大国少年-梦想嘉年华',
subtitle: '亲子冬令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/GGCP6vshpPY.jpg', // Updated with new image
price: 3665,
updatedLessons: 16,
subscribers: 1140,
isPurchased: true,
isReviewed: false
},
{
id: 'course-3',
title: '大国少年-世界正东方',
subtitle: '亲子夏令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2Juj2cXWB7U.jpg', // Updated with new image
price: 1280,
updatedLessons: 16,
subscribers: 1140,
isPurchased: false,
isReviewed: false
}
];
// Activity data
export const activities = [
{
id: 'activity-1',
title: '慧眼读书 | 《论语》',
subtitle: '亲子共读',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/2JIvboGLeho.jpg', // Updated with new image
location: '北京',
status: '活动中',
period: '2024.12.09-2025.01.17',
price: 99,
originalPrice: 120,
isHot: true,
isFree: false,
participantsCount: 18,
maxParticipants: 30,
mock_link: 'https://wxm.behalo.cc/pages/activity/info?type=2&id=10075&title=%E6%B4%BB%E5%8A%A8%E6%8A%A5%E5%90%8D'
},
{
id: 'activity-2',
title: '好分凭借力,陪你跃龙门!',
subtitle: '4.17-6.18 美乐考前赋能营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/_6HzPU9Hyfg (2).jpg', // Updated with new image
location: '线上',
status: '活动中',
period: '2024.12.17-2025.01.26',
price: 366,
originalPrice: 468,
isHot: false,
isFree: false,
participantsCount: 25,
maxParticipants: 50,
mock_link: 'https://wxm.behalo.cc/pages/activity/info?type=2&id=10098&title=%E6%B4%BB%E5%8A%A8%E6%8A%A5%E5%90%8D'
},
{
id: 'activity-3',
title: '7.29-8.4 敦煌: 【青云之路】',
subtitle: '亲子游学营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Y17FE9Fuw4Y.jpg', // Updated with new image
location: '敦煌',
status: '即将开始',
period: '2025.01.07-2025.01.26',
price: 3980,
originalPrice: 4200,
isHot: true,
isFree: false,
participantsCount: 12,
maxParticipants: 20,
mock_link: 'https://wxm.behalo.cc/pages/activity/info?type=2&id=10085&title=%E6%B4%BB%E5%8A%A8%E6%8A%A5%E5%90%8D'
},
{
id: 'activity-4',
title: '【大国少年·梦想嘉年华】',
subtitle: '亲子冬令营',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/-G3rw6Y02D0.jpg',
location: '上海',
status: '进行中',
period: '2025.01.09-2025.01.22',
price: 1280,
originalPrice: 1380,
isHot: false,
isFree: false,
participantsCount: 24,
maxParticipants: 40
},
{
id: 'activity-5',
title: '慧眼读书会 |《零极限》',
subtitle: '共读',
imageUrl: 'https://cdn.ipadbiz.cn/mlaj/images/Oalh2MojUuk.jpg',
location: '线上',
status: '进行中',
period: '2025.01.09-2025.01.22',
price: 0,
originalPrice: 0,
isHot: false,
isFree: true,
participantsCount: 45,
maxParticipants: 100
}
];
// User profile data
export const userProfile = {
name: '李玉红',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-2.jpg',
activities: [],
courses: [],
children: [],
checkIns: {
totalDays: 45,
currentStreak: 7,
longestStreak: 15
}
};
// Daily check-in data
// export const checkInTypes = [
// { id: 'reading', name: '阅读打卡', icon: 'book', path: '/checkin/reading' },
// { id: 'exercise', name: '运动打卡', icon: 'running', path: '/checkin/exercise' },
// { id: 'study', name: '学习打卡', icon: 'graduation-cap', path: '/checkin/study' },
// { id: 'reflection', name: '反思打卡', icon: 'pencil-alt', path: '/checkin/writing' }
// ];
export const checkInTypes = [
{ id: 'reading', name: '课程打卡', icon: 'book', path: '/checkin/reading' },
{ id: 'exercise', name: '签到打卡', icon: 'running', path: '/checkin/exercise' },
// { id: 'study', name: '团队打卡', icon: 'graduation-cap', path: '/checkin/study' },
{ id: 'reflection', name: '学习打卡', icon: 'pencil-alt', path: '/checkin/writing' },
{ id: 'mix', name: '图文打卡', icon: 'pencil-alt', path: '/checkin/index' }
];
// Community posts data
export const communityPosts = [
{
id: 'post-1',
author: {
id: 'user-1',
name: '王小明',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-3.jpg'
},
content: '今天和孩子一起完成了《论语》的共读,收获颇丰!孩子对"己所不欲勿施于人"这句话有了更深的理解。',
images: ['https://cdn.ipadbiz.cn/mlaj/images/post-1-1.jpg', 'https://cdn.ipadbiz.cn/mlaj/images/post-1-2.jpg'],
likes: 24,
comments: 5,
createdAt: '2023-03-15T08:30:00Z'
},
{
id: 'post-2',
author: {
id: 'user-2',
name: '李晓华',
avatar: 'https://cdn.ipadbiz.cn/mlaj/images/user-avatar-1.jpg'
},
content: '冬令营第三天,孩子们参观了科技馆,学习了很多有趣的知识,晚上的篝火晚会也很精彩!',
images: ['https://cdn.ipadbiz.cn/mlaj/images/post-2-1.jpg'],
likes: 36,
comments: 8,
createdAt: '2023-03-14T15:45:00Z'
}
];
/*
* @Date: 2025-04-07 12:41:59
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-07 12:42:05
* @FilePath: /mlaj/src/utils/time.js
* @Description: 文件描述
*/
/**
* 格式化时间戳为 mm:ss 或 hh:mm:ss 格式
* @param {number} seconds - 总秒数(支持小数)
* @returns {string} 格式化后的时间字符串
*/
export function formatTime(seconds) {
if (isNaN(seconds) || seconds < 0) return '0:00'
const hours = Math.floor(seconds / 3600)
seconds %= 3600
const minutes = Math.floor(seconds / 60)
seconds = Math.floor(seconds % 60)
const pad = (n) => n.toString().padStart(2, '0')
if (hours > 0) {
return `${hours}:${pad(minutes)}:${pad(seconds)}`
}
return `${minutes}:${pad(seconds)}`
}
/*
* @Date: 2022-04-18 15:59:42
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-07 15:18:25
* @FilePath: /mlaj/src/utils/tools.js
* @Description: 文件描述
*/
import dayjs from 'dayjs';
// 格式化时间
const formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm');
};
/**
* @description 判断浏览器属于平台
* @returns
*/
const wxInfo = () => {
let u = navigator.userAgent;
let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //android终端或者uc浏览器
let isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端
let isMobile = u.indexOf('Android') > -1 || u.indexOf('iPhone') > -1 || u.indexOf('iPad') > -1; // 移动端平台
let isIpad = u.indexOf('iPad') > -1; // iPad平台
let uAgent = navigator.userAgent.toLowerCase();
let isWeiXin = (uAgent.match(/MicroMessenger/i) == 'micromessenger') ? true : false;
let isWeiXinDesktop = isWeiXin && uAgent.indexOf('wechat') > -1 ? true : false;
let isPC = (uAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone|micromessenger)/i)) ? false : true;
let isIOS = /iphone|ipad|ipod/.test(uAgent); // iOS设备
let isWeChatBrowser = /micromessenger/.test(uAgent); // 微信浏览器
let isIOSWeChat = isIOS && isWeChatBrowser;
return {
isAndroid,
isiOS,
isWeiXin,
isMobile,
isIpad,
isPC,
isWeiXinDesktop,
isIOSWeChat
};
};
/**
* @description 判断多行省略文本
* @param {*} id 目标dom标签
* @returns
*/
const hasEllipsis = (id) => {
let oDiv = document.getElementById(id);
let flag = false;
if (oDiv.scrollHeight > oDiv.clientHeight) {
flag = true
}
return flag
}
/**
* @description 解析URL参数
* @param {*} url
* @returns
*/
const parseQueryString = url => {
var json = {};
var arr = url.indexOf('?') >= 0 ? url.substr(url.indexOf('?') + 1).split('&') : [];
arr.forEach(item => {
var tmp = item.split('=');
json[tmp[0]] = decodeURIComponent(tmp[1]);
});
return json;
}
/**
* 字符串包含字符数组中字符的状态
* @param {*} array 字符数组
* @param {*} str 字符串
* @returns 包含状态
*/
const strExist = (array, str) => {
const exist = array.filter(arr => {
if (str.indexOf(arr) >= 0) return str;
})
return exist.length > 0
}
/**
* 自定义替换参数
* @param {*} url
* @param {*} arg
* @param {*} arg_val
* @returns
*/
const changeURLArg = (url, arg, arg_val) => {
var pattern = arg + '=([^&]*)';
var replaceText = arg + '=' + arg_val;
if (url.match(pattern)) {
var tmp = '/(' + arg + '=)([^&]*)/gi';
tmp = url.replace(eval(tmp), replaceText);
return tmp;
} else {
if (url.match('[\?]')) {
return url + '&' + replaceText;
} else {
return url + '?' + replaceText;
}
}
return url + '\n' + arg + '\n' + arg_val;
}
// 获取参数key/value值对
const getUrlParams = (url) => {
// 没有参数处理
if (url.split('?').length === 1) return false;
let arr = url.split('?');
let res = arr[1].split('&');
let items = {};
for (let i = 0; i < res.length; i++) {
let [key, value] = res[i].split('=');
items[key] = value;
}
return items
}
// 格式化URL参数为字符串
const stringifyQuery = (params) => {
const queryString = [];
Object.keys(params || {}).forEach((k) => {
queryString.push(k + '=' + params[k]);
});
return '?' + queryString.join('&');
};
// 格式化时长(秒转换为可读格式)
const formatDuration = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = seconds % 60;
let result = '';
if (hours > 0) {
result += `${hours}小时`;
}
if (minutes > 0) {
result += `${minutes}分钟`;
}
if (remainingSeconds > 0 || result === '') {
result += `${remainingSeconds}秒`;
}
return result;
};
export {
formatDate,
wxInfo,
hasEllipsis,
parseQueryString,
strExist,
changeURLArg,
getUrlParams,
stringifyQuery,
formatDuration,
};
import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common';
import BMF from 'browser-md5-file';
// import { v4 as uuidv4 } from 'uuid';
// 获取文件后缀
const getFileSuffix = (fileName) => {
return /.[^.]+$/.exec(fileName) || '';
};
// 获取文件MD5
const getFileMD5 = (file) => {
return new Promise((resolve, reject) => {
const bmf = new BMF();
bmf.md5(file, (err, md5) => {
if (err) {
reject(err);
return;
}
resolve(md5);
});
});
};
// 上传文件到七牛云
const uploadToQiniu = async (file, token, fileName, onProgress) => {
const formData = new FormData();
formData.append('file', file);
formData.append('token', token);
formData.append('key', fileName);
const config = {
headers: { 'Content-Type': 'multipart/form-data' },
onUploadProgress: (progressEvent) => {
if (progressEvent.total > 0) {
const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total);
// 使用requestAnimationFrame确保进度更新的平滑性
requestAnimationFrame(() => {
onProgress?.(percent);
});
}
}
};
// 根据协议选择上传地址
const qiniuUploadUrl = window.location.protocol === 'https:'
? 'https://up.qbox.me'
: 'http://upload.qiniu.com';
return await qiniuUploadAPI(qiniuUploadUrl, formData, config);
};
// 校验文件
export const validateFile = (file, options = {}) => {
const {
maxSize = 100, // 默认100MB
allowedTypes = ['video/mp4', 'video/quicktime'],
} = options;
if (!allowedTypes.includes(file.type)) {
return {
valid: false,
message: '请上传正确格式的视频文件'
};
}
const fileSize = (file.size / 1024 / 1024).toFixed(2);
if (fileSize > maxSize) {
return {
valid: false,
message: `文件大小不能超过${maxSize}MB`
};
}
return { valid: true };
};
// 上传文件
export const uploadFile = async (file, fileCode, onProgress) => {
try {
// 获取文件MD5
const md5 = await getFileMD5(file);
// 获取七牛token
const tokenResult = await qiniuTokenAPI({
name: file.name,
hash: md5
});
// 如果文件已存在,直接返回
if (tokenResult.data) {
onProgress?.(100);
return tokenResult.data;
}
// 新文件上传
if (tokenResult.token) {
const suffix = getFileSuffix(file.name);
const fileName = `uploadForm/${fileCode}/${md5}${suffix}`;
// image_info 为七牛返回的图片信息,现在是上传视频看后期适配
const { filekey, image_info } = await uploadToQiniu(
file,
tokenResult.token,
fileName,
onProgress
);
if (filekey) {
// 保存文件信息
const { data } = await saveFileAPI({
name: file.name,
filekey,
hash: md5,
height: image_info?.height,
width: image_info?.width,
});
return data;
}
}
throw new Error('上传失败');
} catch (error) {
console.error('Upload error:', error);
throw error;
}
};
import VConsole from 'vconsole';
// const vConsole = new VConsole();
let vConsole = '';
// 或者使用配置参数来初始化,详情见文档
if (+import.meta.env.VITE_CONSOLE) {
vConsole = new VConsole({ theme: 'dark' });
}
export default vConsole
/*
* @Date: 2024-02-06 11:38:13
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-09-30 22:33:40
* @FilePath: /data-table/src/utils/versionUpdater.js
* @Description:
*/
/* eslint-disable */
/**
* @description: 版本更新检查
* @param {*} time 阈值
* @return {*}
*/
export class Updater {
constructor(options = {}) {
this.oldScript = [];
this.newScript = [];
this.dispatch = {};
this.init(); //初始化
this.timing(options.time); //轮询
}
async init() {
const html = await this.getHtml();
this.oldScript = this.parserScript(html);
}
async getHtml() {
// TAG: html的位置需要动态修改
const html = await fetch(import.meta.env.VITE_BASE).then((res) => res.text()); //读取index html
return html;
}
parserScript(html) {
const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi); //script正则
return html.match(reg); //匹配script标签
}
//发布订阅通知
on(key, fn) {
(this.dispatch[key] || (this.dispatch[key] = [])).push(fn);
return this;
}
compare(oldArr, newArr) {
const base = oldArr.length;
// 去重
const arr = Array.from(new Set(oldArr.concat(newArr)));
//如果新旧length 一样无更新
if (arr.length === base) {
this.dispatch['no-update'].forEach((fn) => {
fn();
});
} else {
//否则通知更新
this.dispatch['update'].forEach((fn) => {
fn();
});
}
}
timing(time = 10000) {
//轮询
this.intervalId = setInterval(async () => {
const newHtml = await this.getHtml();
this.newScript = this.parserScript(newHtml);
this.compare(this.oldScript, this.newScript);
}, time);
}
/**
* 清理定时器
*/
destroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.