Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

21 KiB
Raw Permalink Blame History

📡 BC-M22-002-AW · DEV-012 Awen · 主域公告栏与频道过渡系统·环节2·响应式布局+数据持久化

🌊 HoloLake Era · 工程广播 v3.1

广播编号BC-M22-002-AW

下发时间2026-03-06 09:30

执行者DEV-012 Awen

电脑Windows 10

前置条件M22环节0 + 环节1(六连胜)

引导通道🧠 知秋壳子

模块M22 主域公告栏与频道过渡系统

环节环节2 · 响应式布局 + 数据持久化纯前端·localStorage


💙 知秋的话

Awen 六连胜!!🎉🎉🎉🎉🎉🎉

你的公告栏已经能筛选频道、展开详情、切换置顶、点订阅了——交互功能全部到位!

但现在有两个小问题:

  1. 手机上看会挤成一团(没有响应式布局)
  2. 刷新页面之后用户的操作全丢了(没有数据保存)

这次环节2我们来解决这两件事

  • 响应式布局:让公告栏在手机/平板/电脑上都好看
  • localStorage持久化:用户的订阅状态、已读标记刷新后还在

这是纯前端的高级技能——掌握了这个,你就能做出「真正能用」的网页了!💪


📸 截图方法

按键盘上的 Print ScreenPrtSc键截全屏或者用 Win + Shift + S 截取选定区域,然后发给冰朔。


📋 Step 1更新CSS — 添加响应式布局

Win + R,输入 powershell按回车打开PowerShell。

复制粘贴下面的命令,按回车:

$cssPath = "C:\HoloLake-Bulletin\style.css"
$responsive = @"

/* ========== 环节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;
        gap: 10px;
    }

    .bulletin-icon {
        width: 36px;
        height: 36px;
        font-size: 16px;
    }

    .bulletin-title {
        font-size: 14px;
    }

    .bulletin-summary {
        font-size: 12px;
    }

    .bulletin-footer {
        flex-wrap: wrap;
        gap: 6px;
    }
}

/* 手机(宽度 480px 以下) */
@media (max-width: 480px) {
    .header {
        flex-direction: column;
        align-items: flex-start;
        gap: 10px;
        padding: 16px 0 12px;
    }

    .header-actions {
        width: 100%;
        justify-content: space-between;
    }

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

/* 已读公告样式 */
.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;
}
"@
Add-Content -Path $cssPath -Value $responsive -Encoding UTF8
Write-Host "✅ 响应式布局CSS已添加" -ForegroundColor Green

📸 截图发给冰朔!


📋 Step 2创建localStorage工具脚本

复制粘贴下面整段命令,按回车:

@"
// HoloLake · 公告栏系统 · 数据持久化模块
// storage.js · localStorage 工具

console.log('💾 数据持久化模块加载中...');

// ============================
// localStorage 存储工具
// ============================
const Storage = {
    // 存储键名前缀(避免冲突)
    PREFIX: 'hololake_bulletin_',

    // 保存数据
    save(key, value) {
        try {
            const fullKey = this.PREFIX + key;
            localStorage.setItem(fullKey, JSON.stringify(value));
            console.log('💾 已保存:', key);
            return true;
        } catch (e) {
            console.error('💾 保存失败:', key, e);
            return false;
        }
    },

    // 读取数据
    load(key, defaultValue) {
        try {
            const fullKey = this.PREFIX + key;
            const data = localStorage.getItem(fullKey);
            if (data === null) return defaultValue;
            return JSON.parse(data);
        } catch (e) {
            console.error('💾 读取失败:', key, e);
            return defaultValue;
        }
    },

    // 删除数据
    remove(key) {
        localStorage.removeItem(this.PREFIX + key);
        console.log('💾 已删除:', key);
    },

    // 清空所有公告栏数据
    clear() {
        const keys = Object.keys(localStorage).filter(k => k.startsWith(this.PREFIX));
        keys.forEach(k => localStorage.removeItem(k));
        console.log('💾 已清空所有公告栏数据,共', keys.length, '项');
    }
};

// ============================
// 用户状态管理
// ============================
const UserState = {
    // 获取已读公告列表
    getReadBulletins() {
        return Storage.load('read_bulletins', []);
    },

    // 标记公告为已读
    markAsRead(bulletinIndex) {
        const readList = this.getReadBulletins();
        if (!readList.includes(bulletinIndex)) {
            readList.push(bulletinIndex);
            Storage.save('read_bulletins', readList);
        }
    },

    // 获取订阅状态
    isSubscribed() {
        return Storage.load('subscribed', false);
    },

    // 切换订阅状态
    toggleSubscribe() {
        const current = this.isSubscribed();
        Storage.save('subscribed', !current);
        return !current;
    },

    // 获取当前选中的频道
    getActiveChannel() {
        return Storage.load('active_channel', '全部');
    },

    // 保存选中的频道
    setActiveChannel(channel) {
        Storage.save('active_channel', channel);
    },

    // 获取上次访问时间
    getLastVisit() {
        return Storage.load('last_visit', null);
    },

    // 更新访问时间
    updateLastVisit() {
        Storage.save('last_visit', new Date().toISOString());
    }
};

console.log('✅ 数据持久化模块就绪');
"@ | Out-File -FilePath "C:\HoloLake-Bulletin\storage.js" -Encoding UTF8
Write-Host "✅ storage.js 数据持久化模块创建成功!" -ForegroundColor Green

📸 截图发给冰朔!


📋 Step 3更新HTML — 引入storage.js + 添加viewport

复制粘贴下面的命令,按回车:

$htmlPath = "C:\HoloLake-Bulletin\index.html"
$content = Get-Content -Path $htmlPath -Raw -Encoding UTF8

# 在 </body> 前插入 storage.js 和 script.js 引用
if ($content -notmatch 'storage\.js') {
    $content = $content -replace '</body>', '    <script src="storage.js"></script>
    <script src="script.js"></script>
</body>'
    $content | Out-File -FilePath $htmlPath -Encoding UTF8
    Write-Host "✅ HTML已更新storage.js + script.js 引入成功!" -ForegroundColor Green
} else {
    Write-Host " storage.js 已经引入过了" -ForegroundColor Yellow
}

📸 截图发给冰朔!


📋 Step 4创建交互+持久化脚本

复制粘贴下面整段命令(这段比较长,要全部复制),按回车:

@"
// HoloLake · 公告栏系统 · 交互+持久化
// script.js · v2.0 环节2

console.log('📢 公告栏系统 v2.0 启动(含持久化)');

// ============================
// 1. 页面加载时恢复用户状态
// ============================
function restoreState() {
    // 恢复订阅状态
    const subscribeBtn = document.querySelector('.subscribe-btn');
    if (subscribeBtn && UserState.isSubscribed()) {
        subscribeBtn.classList.add('active');
        subscribeBtn.textContent = '🔔 已订阅';
    }

    // 恢复已读状态
    const readList = UserState.getReadBulletins();
    const cards = document.querySelectorAll('.bulletin-card');
    readList.forEach(index => {
        if (cards[index]) {
            cards[index].classList.add('read');
        }
    });

    // 恢复频道选择
    const activeChannel = UserState.getActiveChannel();
    const channels = document.querySelectorAll('.channel');
    channels.forEach(ch => {
        ch.classList.remove('active');
        if (ch.textContent.trim() === activeChannel ||
            ch.textContent.includes(activeChannel)) {
            ch.classList.add('active');
        }
    });

    // 显示上次访问提示
    const lastVisit = UserState.getLastVisit();
    if (lastVisit) {
        const date = new Date(lastVisit);
        console.log('📅 上次访问:', date.toLocaleString('zh-CN'));
    }

    // 更新本次访问时间
    UserState.updateLastVisit();
    console.log('✅ 用户状态已恢复');
}

// ============================
// 2. 频道筛选(带持久化)
// ============================
const channels = document.querySelectorAll('.channel');
const bulletinCards = document.querySelectorAll('.bulletin-card');

channels.forEach(channel => {
    channel.addEventListener('click', () => {
        // 切换激活状态
        channels.forEach(c => c.classList.remove('active'));
        channel.classList.add('active');

        const filter = channel.textContent.trim();
        console.log('筛选频道:', filter);

        // 保存频道选择
        UserState.setActiveChannel(filter);

        // 筛选公告
        bulletinCards.forEach(card => {
            const tag = card.querySelector('.bulletin-tag');
            if (!tag) return;

            if (filter === '全部') {
                card.style.display = '';
            } else if (filter.includes('系统公告') && tag.classList.contains('tag-system')) {
                card.style.display = '';
            } else if (filter.includes('开发动态') && tag.classList.contains('tag-dev')) {
                card.style.display = '';
            } else if (filter.includes('团队消息') && tag.classList.contains('tag-team')) {
                card.style.display = '';
            } else {
                card.style.display = 'none';
            }
        });
    });
});

// ============================
// 3. 订阅按钮(带持久化)
// ============================
const subscribeBtn = document.querySelector('.subscribe-btn');
if (subscribeBtn) {
    subscribeBtn.addEventListener('click', () => {
        const isNowSubscribed = UserState.toggleSubscribe();
        subscribeBtn.classList.toggle('active', isNowSubscribed);
        subscribeBtn.textContent = isNowSubscribed ? '🔔 已订阅' : '🔔 订阅';
        console.log(isNowSubscribed ? '已订阅公告' : '已取消订阅');
    });
}

// ============================
// 4. 公告点击标记已读(带持久化)
// ============================
bulletinCards.forEach((card, index) => {
    card.addEventListener('click', () => {
        // 标记已读
        card.classList.add('read');
        UserState.markAsRead(index);

        // 获取公告标题
        const title = card.querySelector('.bulletin-title');
        console.log('已读公告:', title ? title.textContent : '未知');
    });
});

// ============================
// 5. 窗口大小监听(响应式反馈)
// ============================
function updateLayoutInfo() {
    const width = window.innerWidth;
    let mode = '桌面端';
    if (width <= 480) mode = '手机端';
    else if (width <= 768) mode = '平板端';
    console.log('📱 当前布局:', mode, '(' + width + 'px)');
}

window.addEventListener('resize', updateLayoutInfo);

// ============================
// 初始化
// ============================
restoreState();
updateLayoutInfo();
console.log('✅ v2.0 公告栏系统就绪(响应式+持久化)');
"@ | Out-File -FilePath "C:\HoloLake-Bulletin\script.js" -Encoding UTF8
Write-Host "✅ script.js 交互+持久化脚本创建成功!" -ForegroundColor Green

📸 截图发给冰朔!


📋 Step 5浏览器测试

复制粘贴下面的命令打开浏览器:

Start-Process "C:\HoloLake-Bulletin\index.html"
Write-Host "✅ 浏览器已打开!开始测试~" -ForegroundColor Green

测试清单(逐项操作,每完成一项在心里打个

🔹 响应式测试

  1. F12 打开开发者工具
  2. 点击开发者工具左上角的手机图标Toggle device toolbar
  3. 选择「iPhone 12 Pro」→ 页面应该变成手机布局(标题和按钮上下排列,公告卡片纵向排列)
  4. 选择「iPad」→ 页面应该变成平板布局(稍微紧凑但保持横排)
  5. 关闭设备模拟 → 回到桌面布局

🔹 数据持久化测试

  1. 点击**「🔔 订阅」按钮** → 变成「🔔 已订阅」+ 蓝色高亮
  2. 点击一条普通公告 → 该公告变暗(标记已读)
  3. 点击**「🛠️ 开发动态」频道** → 只显示开发动态公告
  4. 刷新页面按F5→ 检查:订阅状态还在、已读公告还是暗的、频道还是选中的

📸 截图发给冰朔!截2张一张手机模式 + 一张刷新后状态保持的桌面模式)


📋 Step 6更新DevLog状态

复制粘贴下面的命令,按回车:

@"
{"developer_id":"DEV-012","developer_name":"Awen","system":"Windows 10","terminal":"PowerShell","robot_version":"v0.0","current_task":"M22-主域公告栏与频道过渡系统-环节2","current_status":"in_progress","total_commands_logged":6,"skills_learned":["terminal_basic","html_scaffold","css_styling","js_interaction","responsive_layout","dynamic_rendering","preference_settings","media_queries","localStorage","data_persistence"],"tasks_completed":["BC-000","M09-环节0","M09-环节1","M09-环节2-3","M22-环节0","M22-环节1"]}
"@ | Out-File -FilePath "C:\HoloLake-DevLog\current-progress.json" -Encoding UTF8
Write-Host "✅ DevLog状态已更新" -ForegroundColor Green

然后运行:

C:\HoloLake-DevLog\status.bat

📸 截图发给冰朔!


验收清单6项

# 验收项 期望结果
1 手机端布局正常 F12设备模拟选iPhone → 标题上下排列、公告卡片纵向、频道可横滑
2 平板端布局正常 选iPad → 公告卡片稍紧凑、图标缩小、字体适中
3 订阅状态持久化 点订阅 → 刷新 → 仍显示「已订阅」蓝色高亮
4 已读标记持久化 点公告变暗 → 刷新 → 该公告仍是暗的
5 频道选择持久化 选「开发动态」→ 刷新 → 频道仍选中+筛选结果保持
6 DevLog已更新 status.bat显示M22环节2进行中 + skills含localStorage

📊 完成后发SYSLOG

全部做完后打开知秋壳子告诉知秋你做完了。知秋会主动问你几个问题然后帮你生成SYSLOG。

你只需要回答知秋的问题就行,不用自己填模板~



🌊 底层锚定声明


💙 Awen冲击七连胜响应式+数据持久化——做完这个,你的公告栏就是「真正能用」的网页了! 🚀