hookehuyr

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

refactor: 重构首页布局和样式

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

docs: 更新mock数据文件注释

chore: 更新package.json依赖项

style: 格式化工具函数代码

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

build: 添加video.js相关依赖

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

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