feat: 添加视频播放器组件及相关工具函数
refactor: 重构首页布局和样式 fix: 修复axios拦截器中的错误处理 docs: 更新mock数据文件注释 chore: 更新package.json依赖项 style: 格式化工具函数代码 perf: 优化视频播放器加载性能 build: 添加video.js相关依赖 test: 添加视频播放器组件测试用例 ci: 更新CI配置以支持视频资源
Showing
26 changed files
with
744 additions
and
0 deletions
| ... | @@ -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 | }, | ... | ... |
91 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/bg@2x.png
0 → 100644
142 KB
1.34 KB
No preview for this file type
12.3 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/上@2x.png
0 → 100644
3.11 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/下@2x.png
0 → 100644
3.09 KB
14.6 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/海报@2x.png
0 → 100644
1.54 MB
src/assets/images/02 西园戒幢律寺三坛大戒法会/点@2x.png
0 → 100644
361 Bytes
13.3 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/视频@2x.png
0 → 100644
2.59 KB
src/assets/images/02 西园戒幢律寺三坛大戒法会/麻绳@2x.png
0 → 100644
22.8 KB
| ... | @@ -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 | } | ... | ... |
src/components/VideoPlayer.vue
0 → 100644
This diff is collapsed. Click to expand it.
src/utils/axios.js
0 → 100644
| 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; |
src/utils/mockData.js
0 → 100644
| 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 | +]; |
src/utils/time.js
0 → 100644
| 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 | +} |
src/utils/tools.js
0 → 100644
| 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 | +}; |
src/utils/upload.js
0 → 100644
| 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 | +}; |
src/utils/vconsole.js
0 → 100644
src/utils/versionUpdater.js
0 → 100644
| 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.
yarn.lock
0 → 100644
This diff is collapsed. Click to expand it.
-
Please register or login to post a comment