From b2ce11c3ee62cb9274bc4cc2f1201726f653c8a1 Mon Sep 17 00:00:00 2001 From: Awen <644405261@qq.com> Date: Mon, 9 Mar 2026 01:10:34 +0800 Subject: [PATCH] =?UTF-8?q?M22=E7=8E=AF=E8=8A=823=E5=AE=8C=E6=88=90?= =?UTF-8?q?=EF=BC=9A=E5=85=89=E6=B9=96=E6=B0=B4=E9=9D=A2=E6=95=88=E6=9E=9C?= =?UTF-8?q?+=E8=BF=87=E6=B8=A1=E5=8A=A8=E7=94=BB=EF=BC=8C=E5=85=AB?= =?UTF-8?q?=E8=BF=9E=E8=83=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/portal/data.json | 23 ++ modules/portal/index.html | 134 ++++++++++++ modules/portal/index.js | 48 +++++ modules/portal/script.js | 204 ++++++++++++++++++ modules/portal/storage.js | 128 +++++++++++ modules/portal/style.css | 440 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 977 insertions(+) create mode 100644 modules/portal/data.json create mode 100644 modules/portal/index.html create mode 100644 modules/portal/index.js create mode 100644 modules/portal/script.js create mode 100644 modules/portal/storage.js create mode 100644 modules/portal/style.css diff --git a/modules/portal/data.json b/modules/portal/data.json new file mode 100644 index 00000000..242c313d --- /dev/null +++ b/modules/portal/data.json @@ -0,0 +1,23 @@ +[ + { + "id": 1, + "title": "HoloLake 2.0 发布!", + "content": "全新界面,极致体验。", + "channel": "tech", + "pinned": true + }, + { + "id": 2, + "title": "AI 会议延期通知", + "content": "原定下周的会议推迟到月底。", + "channel": "news", + "pinned": false + }, + { + "id": 3, + "title": "VS Code 插件推荐", + "content": "Live Server 是开发神器。", + "channel": "tech", + "pinned": false + } +] \ No newline at end of file diff --git a/modules/portal/index.html b/modules/portal/index.html new file mode 100644 index 00000000..bc24daed --- /dev/null +++ b/modules/portal/index.html @@ -0,0 +1,134 @@ + + + + + + HoloLake公告栏 + + + +
+ +
+

公告栏

+
+ 2条置顶 + +
+
+ + +
+ 全部 + 系统公告 + 开发动态 + 团队消息 +
+ + +
+ +
+
置顶
+
+
+
+ 光湖系统MVP v0.4.0发布公告 + 2小时前 +
+

主域公告栏与频道过渡系统(M22)正式进入环节0开发。新增公告置顶、频道分类、时间线展示功能,信息展示层逐步完善。

+ +
+
+ + +
+
置顶
+
+
+
+ SYSLOG提交流程v2上线通知 + 5小时前 +
+

SYSLOG生成方式已更新为「知秋主动提问制」完成任务后由知秋引导填写,无需手动套用模板。即日起生效。

+ +
+
+ + +
+
+
+
+ 欢迎Awen加入光湖团队! + 1天前 +
+

DEV-012 Awen正式加入光湖开发团队,成为第12位协作者。首个模块M09消息通知中心已全通关!

+
+
+ + +
+
+
+
+ M08数据统计面板全通关 + 2天前 +
+

完成M08全部环节,七连胜达成!响应式布局+SVG环形图+CSV导出一次通过,正式前端毕业

+ +
+
+ + +
+
+
+
+ HTTPS配置与飞书Webhook联调完成 + 3天前 +
+

光湖主域已完成HTTPS配置与飞书Webhook全链路联调,Nginx+PM2+Certbot部署完毕,系统脊梁正式贯通。

+ +
+
+ + +
+
+
+
+ 本周开发者进度总览 + 3天前 +
+

12位开发者中8位活跃,累计完成30+个环节。肥猫五连胜、桔子七连胜、小草莓五连胜、Awen四连胜团队势头强劲!

+ +
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/modules/portal/index.js b/modules/portal/index.js new file mode 100644 index 00000000..61aa9518 --- /dev/null +++ b/modules/portal/index.js @@ -0,0 +1,48 @@ +// 频道标签过滤 +document.querySelectorAll('.channel-tag').forEach(tag => { + tag.addEventListener('click', () => { + const channel = tag.dataset.channel; + document.querySelectorAll('.announcement').forEach(ann => { + ann.style.display = channel === 'all' || ann.dataset.channel === channel ? 'block' : 'none'; + }); + }); +}); + +// 置顶悬浮动画 +document.querySelectorAll('.pin-icon').forEach(pin => { + pin.addEventListener('click', () => { + const announcement = pin.closest('.announcement'); + announcement.classList.toggle('pinned'); + }); +}); + +// ✨ 动态数据加载(M24 核心!) +fetch('data.json') + .then(response => response.json()) + .then(data => { + const announcements = document.querySelector('.announcements-container'); + announcements.innerHTML = ''; + + data.forEach(item => { + const announcement = document.createElement('div'); + announcement.className = nnouncement ; + announcement.dataset.channel = item.channel; + announcement.innerHTML = +
+

+

+
+ + ; + announcements.appendChild(announcement); + }); + + // 重新绑定事件 + document.querySelectorAll('.pin-icon').forEach(pin => { + pin.addEventListener('click', () => { + const announcement = pin.closest('.announcement'); + announcement.classList.toggle('pinned'); + pin.textContent = announcement.classList.contains('pinned') ? '⭐' : '✨'; + }); + }); + }); diff --git a/modules/portal/script.js b/modules/portal/script.js new file mode 100644 index 00000000..8d0aba08 --- /dev/null +++ b/modules/portal/script.js @@ -0,0 +1,204 @@ +// ========== 确保 HoloLake 命名空间存在 ========== +window.HoloLake = window.HoloLake || {}; + +// ========== 页面加载完成后执行 ========== +document.addEventListener('DOMContentLoaded', function() { + console.log('🚀 script.js 初始化'); + + // 恢复所有状态 + restoreState(); + + // 绑定事件 + bindEvents(); + + // 更新布局信息 + updateLayoutInfo(); + + // 监听窗口大小变化 + window.addEventListener('resize', function() { + updateLayoutInfo(); + }); +}); + +// ========== 恢复状态 ========== +function restoreState() { + console.log('🔍 restoreState 开始...'); + + try { + // 1. 恢复订阅状态 + const isSubscribed = HoloLake.UserState.isSubscribed(); + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + if (isSubscribed) { + subscribeBtn.classList.add('active'); + subscribeBtn.textContent = '已订阅'; + } else { + subscribeBtn.classList.remove('active'); + subscribeBtn.textContent = '订阅'; + } + } + + // 2. 恢复已读公告 + const readList = HoloLake.UserState.getReadBulletins(); + const bulletinCards = document.querySelectorAll('.bulletin-card'); + bulletinCards.forEach((card, index) => { + const bulletinId = `bulletin-${index}`; + if (readList.includes(bulletinId)) { + card.classList.add('read'); + } + }); + + // 3. 恢复选中频道 + const activeChannel = HoloLake.UserState.getActiveChannel(); + const channels = document.querySelectorAll('.channel'); + channels.forEach(channel => { + const channelText = channel.textContent.trim(); + if (channelText === activeChannel || (activeChannel === '全部' && channelText === '全部')) { + channel.classList.add('active'); + } else { + channel.classList.remove('active'); + } + }); + + // 4. 更新最后访问时间 + HoloLake.UserState.updateLastVisit(); + + console.log('✅ 恢复已完成'); + } catch (e) { + console.error('❌ 恢复失败:', e); + } +} + +// ========== 绑定事件 ========== +function bindEvents() { + console.log('🔗 绑定事件...'); + + // 订阅按钮 + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + subscribeBtn.addEventListener('click', function() { + const newState = HoloLake.UserState.toggleSubscribe(); + if (newState) { + this.classList.add('active'); + this.textContent = '已订阅'; + } else { + this.classList.remove('active'); + this.textContent = '订阅'; + } + console.log('📌 订阅状态:', newState ? '已订阅' : '未订阅'); + }); + } + + // 频道点击 + const channels = document.querySelectorAll('.channel'); + channels.forEach(channel => { + channel.addEventListener('click', function() { + // 更新UI + channels.forEach(c => c.classList.remove('active')); + this.classList.add('active'); + + // 保存状态 + const channelName = this.textContent.trim(); + HoloLake.UserState.setActiveChannel(channelName); + + // 筛选公告(可选功能,这里先做简单筛选) + filterBulletinsByChannel(channelName); + + console.log('📺 切换到频道:', channelName); + }); + }); + + // 公告点击(标记已读) + const bulletinCards = document.querySelectorAll('.bulletin-card'); + bulletinCards.forEach((card, index) => { + card.addEventListener('click', function() { + const bulletinId = `bulletin-${index}`; + + // 标记已读 + if (!this.classList.contains('read')) { + this.classList.add('read'); + HoloLake.UserState.markAsRead(bulletinId); + console.log('📖 已读公告:', bulletinId); + } + }); + }); +} + +// ========== 按频道筛选 ========== +function filterBulletinsByChannel(channelName) { + const cards = document.querySelectorAll('.bulletin-card'); + + if (channelName === '全部') { + cards.forEach(card => { + card.style.display = 'flex'; + }); + return; + } + + cards.forEach(card => { + const tag = card.querySelector('.bulletin-tag'); + if (tag && tag.textContent.trim() === channelName) { + card.style.display = 'flex'; + } else { + card.style.display = 'none'; + } + }); +} + +// ========== 更新布局信息 ========== +function updateLayoutInfo() { + const width = window.innerWidth; + let mode = '桌面'; + + if (width <= 480) { + mode = '手机'; + } else if (width <= 768) { + mode = '平板'; + } + + console.log(`📱 当前布局: ${mode}模式 (${width}px)`); +} +// ========== 环节3:动画控制 ========== + +// 初始化动画 +function initAnimations() { + // 检测用户是否开启了「减少动画」 + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + + if (prefersReducedMotion) { + console.log('♿ 用户开启了减少动画,跳过动画'); + return; + } + + // 为所有公告卡片设置递增的动画延迟 + const cards = document.querySelectorAll('.bulletin-card'); + cards.forEach((card, index) => { + // 如果已经有内联样式,先保留原有样式,只加动画延迟 + const delay = 0.1 * (index + 1); + card.style.animationDelay = `${delay}s`; + }); + + console.log('🎬 动画已初始化,延迟已设置'); +} + +// 订阅按钮脉冲效果 +function addPulseEffect(btn) { + btn.classList.add('pulse'); + setTimeout(() => { + btn.classList.remove('pulse'); + }, 300); +} + +// 在页面加载时调用 +document.addEventListener('DOMContentLoaded', function() { + // 已有的初始化代码... + initAnimations(); + + // 为订阅按钮添加脉冲效果 + const subscribeBtn = document.querySelector('.subscribe-btn'); + if (subscribeBtn) { + subscribeBtn.addEventListener('click', function() { + addPulseEffect(this); + }); + } +}); \ No newline at end of file diff --git a/modules/portal/storage.js b/modules/portal/storage.js new file mode 100644 index 00000000..865bd567 --- /dev/null +++ b/modules/portal/storage.js @@ -0,0 +1,128 @@ +/** + * HoloLake Bulletin 数据持久化模块 + * storage.js - localStorage 封装 + 用户状态管理 + * 模块 ID: BC-M22-002-AW + */ + +window.HoloLake = window.HoloLake || {}; + +// ========== Storage 工具对象(带前缀) ========== +HoloLake.Storage = { + prefix: 'hololake_bulletin_', + + // 保存数据 + save: function(key, value) { + try { + const serialized = JSON.stringify(value); + localStorage.setItem(this.prefix + key, serialized); + console.log(`[Storage] 已保存: ${key} =`, value); + } catch (e) { + console.error('[Storage] 保存失败:', e); + } + }, + + // 读取数据 + load: function(key, defaultValue = null) { + try { + const serialized = localStorage.getItem(this.prefix + key); + if (serialized === null) { + console.log(`[Storage] 无数据: ${key},返回默认值`); + return defaultValue; + } + const value = JSON.parse(serialized); + console.log(`[Storage] 已加载: ${key} =`, value); + return value; + } catch (e) { + console.error('[Storage] 读取失败:', e); + return defaultValue; + } + }, + + // 删除指定键 + remove: function(key) { + try { + localStorage.removeItem(this.prefix + key); + console.log(`[Storage] 已删除: ${key}`); + } catch (e) { + console.error('[Storage] 删除失败:', e); + } + }, + + // 清空所有带前缀的数据 + clear: function() { + try { + const keysToRemove = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith(this.prefix)) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); + console.log('[Storage] 已清空所有公告栏数据'); + } catch (e) { + console.error('[Storage] 清空失败:', e); + } + } +}; + +// ========== 用户状态管理 ========== +HoloLake.UserState = { + // 已读公告列表 + getReadBulletins: function() { + return HoloLake.Storage.load('read_bulletins', []); + }, + + markAsRead: function(bulletinId) { + const readList = this.getReadBulletins(); + if (!readList.includes(bulletinId)) { + readList.push(bulletinId); + HoloLake.Storage.save('read_bulletins', readList); + } + return readList; + }, + + // 订阅状态 + isSubscribed: function() { + return HoloLake.Storage.load('is_subscribed', false); + }, + + toggleSubscribe: function() { + const current = this.isSubscribed(); + const newState = !current; + HoloLake.Storage.save('is_subscribed', newState); + return newState; + }, + + // 当前频道 + getActiveChannel: function() { + return HoloLake.Storage.load('active_channel', 'all'); + }, + + setActiveChannel: function(channelId) { + HoloLake.Storage.save('active_channel', channelId); + }, + + // 上次访问时间 + getLastVisit: function() { + return HoloLake.Storage.load('last_visit', null); + }, + + updateLastVisit: function() { + const now = new Date().toISOString(); + HoloLake.Storage.save('last_visit', now); + return now; + }, + + // 重置所有状态(测试用) + resetAll: function() { + HoloLake.Storage.remove('read_bulletins'); + HoloLake.Storage.remove('is_subscribed'); + HoloLake.Storage.remove('active_channel'); + HoloLake.Storage.remove('last_visit'); + console.log('[UserState] 已重置所有状态'); + } +}; + +// 初始化时记录访问时间(可选,页面加载时由 script.js 调用) +console.log('✅ storage.js 已加载,HoloLake.Storage 和 HoloLake.UserState 已就绪'); \ No newline at end of file diff --git a/modules/portal/style.css b/modules/portal/style.css new file mode 100644 index 00000000..7e3db577 --- /dev/null +++ b/modules/portal/style.css @@ -0,0 +1,440 @@ +/* HoloLake公告栏系统深色科技风 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif; + background: #0a1628; + color: #e0e6ed; + min-height: 100vh; +} + +.app-container { + max-width: 580px; + margin: 0 auto; + padding: 0 16px; +} + +/* ========== 顶部标题栏 ========== */ +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px 0 16px; + border-bottom: 1px solid rgba(255,255,255,0.08); +} + +.header h1 { + font-size: 22px; + font-weight: 700; + color: #e0e6ed; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.pinned-badge { + background: rgba(255, 183, 77, 0.15); + color: #ffb74d; + font-size: 12px; + padding: 4px 10px; + border-radius: 12px; + font-weight: 600; +} + +.subscribe-btn { + background: rgba(79, 195, 247, 0.1); + color: #4fc3f7; + border: 1px solid rgba(79, 195, 247, 0.2); + padding: 6px 14px; + border-radius: 8px; + font-size: 13px; + cursor: pointer; +} + +.subscribe-btn:hover { + background: rgba(79, 195, 247, 0.2); +} + +/* ========== 频道栏 ========== */ +.channel-bar { + display: flex; + gap: 8px; + padding: 16px 0; + border-bottom: 1px solid rgba(255,255,255,0.06); + overflow-x: auto; +} + +.channel { + padding: 8px 18px; + border-radius: 20px; + border: 1px solid rgba(255,255,255,0.1); + background: transparent; + color: #8899aa; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.channel:hover { + border-color: rgba(255, 183, 77, 0.3); + color: #b0bec5; +} + +.channel.active { + background: rgba(255,183,77,0.15); + border-color: #ffb74d; + color: #ffb74d; + font-weight: 600; +} + +/* ========== 公告列表 ========== */ +.bulletin-list { + padding: 16px 0; +} + +.bulletin-card { + position: relative; + display: flex; + gap: 16px; + padding: 16px; + border-radius: 12px; + background: rgba(20, 30, 45, 0.6); + border: 1px solid rgba(255,255,255,0.04); + margin-bottom: 12px; + transition: all 0.2s ease; +} + +.bulletin-card.pinned { + border: 1px solid rgba(255, 183, 77, 0.3); + background: rgba(255, 183, 77, 0.02); +} + +.pin-indicator { + position: absolute; + top: 8px; + right: 12px; + font-size: 11px; + color: #ffb74d; + font-weight: 600; +} + +/* 公告图标 */ +.bulletin-icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + flex-shrink: 0; +} + +.bulletin-icon.system { background: rgba(156,39,176,0.12); } +.bulletin-icon.dev { background: rgba(76,175,80,0.12); } +.bulletin-icon.team { background: rgba(79,195,247,0.12); } + +/* 公告内容 */ +.bulletin-content { + flex: 1; + min-width: 0; +} + +.bulletin-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.bulletin-title { + font-size: 15px; + font-weight: 600; + color: #e0e6ed; +} + +.bulletin-time { + font-size: 12px; + color: #556677; + flex-shrink: 0; + margin-left: 12px; +} + +.bulletin-summary { + font-size: 13px; + color: #8899aa; + line-height: 1.6; + margin-bottom: 10px; +} + +/* 公告底部 */ +.bulletin-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.bulletin-tag { + display: inline-block; + font-size: 11px; + padding: 2px 8px; + border-radius: 6px; + font-weight: 500; +} + +.tag-system { background: rgba(156,39,176,0.12); color: #ce93d8; } +.tag-dev { background: rgba(76,175,80,0.12); color: #81c784; } +.tag-team { background: rgba(79,195,247,0.12); color: #4fc3f7; } + +.bulletin-author { + font-size: 11px; + color: #556677; +} + +/* ========== 底部状态栏 ========== */ +.footer { + display: flex; + justify-content: space-between; + padding: 20px 0; + border-top: 1px solid rgba(255,255,255,0.06); + font-size: 12px; + color: #556677; +} + +.footer-brand { + color: rgba(255, 183, 77, 0.5); +} + +/* ========== 环节2:响应式布局(爸爸的深色科技风) ========== */ + +/* 平板(宽度 768px 以下) */ +@media (max-width: 768px) { + .app-container { + max-width: 100%; + padding: 0 12px; + } + .header h1 { + font-size: 18px; + } + .channel-bar { + gap: 6px; + padding: 12px 0; + } + .channel { + padding: 6px 14px; + font-size: 12px; + } + .bulletin-card { + padding: 12px 10px; + } + .channel-bar { + flex-wrap: nowrap; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + } + .channel-bar::-webkit-scrollbar { + display: none; + } + .bulletin-card { + flex-direction: column; + gap: 8px; + } + .bulletin-icon { + width: 32px; + height: 32px; + font-size: 14px; + } + .bulletin-header { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + .bulletin-time { + margin-left: 0; + } + .pin-indicator { + top: 6px; + right: 8px; + font-size: 10px; + } + .footer { + flex-direction: column; + gap: 4px; + text-align: center; + } +} + +/* 手机(宽度 480px 以下)- 复用平板样式,只调更小字体 */ +@media (max-width: 480px) { + .header h1 { + font-size: 16px; + } + .subscribe-btn { + padding: 4px 10px; + font-size: 12px; + } + .channel { + padding: 4px 10px; + font-size: 11px; + } + .bulletin-title { + font-size: 14px; + } + .bulletin-summary { + font-size: 12px; + } +} + +/* 已读公告样式 */ +.bulletin-card.read { + opacity: 0.6; +} +.bulletin-card.read .bulletin-title { + color: #8899aa; +} + +/* 订阅按钮激活状态 */ +.subscribe-btn.active { + background: rgba(79, 195, 247, 0.25); + border-color: #4fc3f7; + color: #4fc3f7; +} + +/* ========== 环节3:过渡动画 ========== */ + +/* 公告卡片:悬停上移 + 阴影加深 */ +.bulletin-card { + transition: all 0.3s ease; +} +.bulletin-card:hover { + transform: translateY(-4px); /* 从 -2px 改为 -4px,更明显 */ + box-shadow: 0 12px 24px rgba(0, 160, 255, 0.2); /* 蓝色光晕 */ +} +/* 订阅按钮:缩放反馈 + 颜色过渡 */ +.subscribe-btn { + transition: all 0.2s ease; +} + +.subscribe-btn:hover { + transform: scale(1.05); +} + +.subscribe-btn:active { + transform: scale(0.95); +} + +/* 频道标签:背景色/文字色平滑切换 */ +.channel { + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* 已读公告:淡入淡出效果 */ +.bulletin-card.read { + transition: opacity 0.5s ease; +} +/* ========== 环节3:关键帧动画 ========== */ + +/* 淡入浮起动画 */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* 为每个公告卡片添加动画 */ +.bulletin-card { + animation: fadeInUp 0.6s ease forwards; + opacity: 0; /* 动画开始前隐藏 */ +} + +/* 按索引依次延迟出现 */ +.bulletin-card:nth-child(1) { animation-delay: 0.1s; } +.bulletin-card:nth-child(2) { animation-delay: 0.2s; } +.bulletin-card:nth-child(3) { animation-delay: 0.3s; } +.bulletin-card:nth-child(4) { animation-delay: 0.4s; } +.bulletin-card:nth-child(5) { animation-delay: 0.5s; } +.bulletin-card:nth-child(6) { animation-delay: 0.6s; } +/* 如果卡片超过6张,继续往后加 */ +/* ========== 环节3:光湖水面效果 ========== */ + +/* Header 光湖呼吸渐变 */ +.header { + background: linear-gradient(135deg, #0a2a4a 0%, #1b4a6b 50%, #0a2a4a 100%); + background-size: 200% 200%; + animation: breathe 8s ease-in-out infinite; + position: relative; + overflow: hidden; +} + +/* 呼吸动画 */ +@keyframes breathe { + 0% { background-position: 0% 0%; } + 50% { background-position: 100% 100%; } + 100% { background-position: 0% 0%; } +} + +/* 微光扫过效果 */ +.header::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.1), + transparent + ); + transform: translateX(-100%); + animation: shimmer 6s infinite; + pointer-events: none; +} + +@keyframes shimmer { + 0% { transform: translateX(-100%); } + 20% { transform: translateX(100%); } + 100% { transform: translateX(100%); } +} + +/* 频道栏底部边缘(水面倒影) */ +.channel-bar { + border-bottom: 1px solid rgba(79, 195, 247, 0.2); + position: relative; +} + +.channel-bar::after { + content: ''; + position: absolute; + bottom: -1px; + left: 0; + width: 100%; + height: 2px; + background: linear-gradient(90deg, transparent, #4fc3f7, transparent); + opacity: 0.3; +} +/* 订阅按钮脉冲效果 */ +.subscribe-btn.pulse { + animation: pulse 0.3s ease; +} + +@keyframes pulse { + 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 195, 247, 0.7); } + 70% { transform: scale(1.1); box-shadow: 0 0 10px 5px rgba(79, 195, 247, 0); } + 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(79, 195, 247, 0); } +} \ No newline at end of file