// ==UserScript== // @name 云朵商城助手 // @namespace https://shop.cloudbread.cn/ // @version 2.0.0 // @author 脆脆的猫猫鲨 // @description 在B站直播间快速使用云朵商城服务(歌单 + 商城 + 礼物数据上传),数据通过开放 API 实时获取 // @copyright 脆脆的猫猫鲨 // @icon https://shop.cloudbread.cn/favicon.png // @homepage https://shop.cloudbread.cn/ // @supportURL https://shop.cloudbread.cn/ // @match https://live.bilibili.com/* // @connect shop.cloudbread.cn // @connect api.live.bilibili.com // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @run-at document-idle // @noframes // @license MIT // ==/UserScript== (function () { 'use strict'; const VERSION = '2.0.0'; const API_BASE = 'https://shop.cloudbread.cn/api/v1/open'; const SITE_BASE = 'https://shop.cloudbread.cn'; const ICON_URL = 'https://shop.cloudbread.cn/favicon.png'; let enabled = GM_getValue('enabled', true); // ============ 日志工具 ============ const log = { info: (...e) => console.log('%c 云朵商城助手 %c 信息 %c ' + e.join(''), 'background:#F59E0B;color:#fff;padding:2px 2px;border-radius:6px 0 0 6px;', 'background:#2196f3;color:#fff;padding:2px 2px;border-radius:0 6px 6px 0;', 'color:#000;padding:2px 2px;'), success: (...e) => console.log('%c 云朵商城助手 %c 成功 %c ' + e.join(''), 'background:#F59E0B;color:#fff;padding:2px 2px;border-radius:6px 0 0 6px;', 'background:#4caf50;color:#fff;padding:2px 2px;border-radius:0 6px 6px 0;', 'color:#000;padding:2px 2px;'), warn: (...e) => console.warn('%c 云朵商城助手 %c 警告 %c ' + e.join(''), 'background:#F59E0B;color:#fff;padding:2px 2px;border-radius:6px 0 0 6px;', 'background:#fb8c00;color:#fff;padding:2px 2px;border-radius:0 6px 6px 0;', 'color:#000;padding:2px 2px;'), error: (...e) => console.error('%c 云朵商城助手 %c 错误 %c ' + e.join(''), 'background:#F59E0B;color:#fff;padding:2px 2px;border-radius:6px 0 0 6px;', 'background:#b00020;color:#fff;padding:2px 2px;border-radius:0 6px 6px 0;', 'color:#000;padding:2px 2px;') }; // ============ 样式注入 ============ const PANEL_CSS = ` .cb-panel{position:absolute;left:0;top:0;width:100%;height:100%;background:#fff;z-index:800;display:none;flex-direction:column;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif} .cb-panel.active{display:flex} .cb-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;background:linear-gradient(135deg,#F59E0B,#FBBF24);color:#fff;flex-shrink:0} .cb-panel-title{font-size:14px;font-weight:600;display:flex;align-items:center;gap:6px} .cb-panel-close{cursor:pointer;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:background .2s} .cb-panel-close:hover{background:rgba(255,255,255,.25)} .cb-panel-tabs{display:flex;background:#f5f5f5;border-bottom:1px solid #e0e0e0;flex-shrink:0} .cb-tab{flex:1;padding:8px 12px;text-align:center;font-size:13px;cursor:pointer;color:#666;border-bottom:2px solid transparent;transition:all .2s} .cb-tab.active{color:#F59E0B;border-bottom-color:#F59E0B;background:#fff;font-weight:600} .cb-tab:hover:not(.active){color:#F59E0B} .cb-tab .cb-tab-badge{display:inline-block;background:#eee;color:#888;border-radius:10px;padding:1px 6px;font-size:10px;margin-left:4px} .cb-tab.active .cb-tab-badge{background:#FFF3CD;color:#F59E0B} .cb-panel-fixed-header{flex-shrink:0;background:#fff;z-index:10} .cb-panel-body{flex:1;overflow-y:auto;overflow-x:hidden;-webkit-overflow-scrolling:touch} .cb-panel-body::-webkit-scrollbar{width:4px} .cb-panel-body::-webkit-scrollbar-thumb{background:#ddd;border-radius:2px} .cb-search-bar-wrap{display:flex;flex-direction:column;gap:6px;padding:8px 12px;background:#fafafa;border-bottom:1px solid #e0e0e0;flex-shrink:0} .cb-search-bar{display:flex;gap:6px} .cb-search-input{flex:1;border:1px solid #ddd;border-radius:6px;padding:4px 8px;font-size:12px;outline:none} .cb-search-input:focus{border-color:#F59E0B} .cb-search-btn{background:#F59E0B;color:#fff;border:none;border-radius:6px;padding:4px 12px;font-size:12px;cursor:pointer} .cb-search-btn:hover{background:#D97706} .cb-random-btn{background:linear-gradient(135deg,#8B5CF6,#EC4899);color:#fff;border:none;border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;transition:all .2s;align-self:flex-start} .cb-random-btn:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(139,92,246,.4)} .cb-random-btn:disabled{opacity:.6;cursor:not-allowed;transform:none;box-shadow:none} .cb-tag-bar{display:flex;padding:6px 12px;background:#fafafa;border-bottom:1px solid #e0e0e0;gap:6px;overflow-x:auto;flex-shrink:0;flex:1;min-width:0} .cb-tag-bar::-webkit-scrollbar{display:none} .cb-tag-bar-wrap{display:flex;align-items:center;background:#fafafa;border-bottom:1px solid #e0e0e0;flex-shrink:0} .cb-tag-bar-wrap .cb-tag-bar{border-bottom:none;padding:6px 8px} .cb-tag-scroll-btn{width:24px;height:24px;flex-shrink:0;border:none;background:transparent;color:#999;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:50%;transition:all .2s;padding:0;line-height:1} .cb-tag-scroll-btn:hover{background:#eee;color:#F59E0B} .cb-tag-scroll-btn:active{background:#ddd} .cb-tag-scroll-left{margin-left:4px} .cb-tag-scroll-right{margin-right:4px} .cb-tag-bar-wrap:not(.has-overflow) .cb-tag-scroll-btn{display:none} .cb-tag-item{cursor:pointer;white-space:nowrap;padding:3px 10px;border-radius:12px;font-size:11px;background:#eee;color:#666;border:1px solid transparent;transition:all .2s} .cb-tag-item.active{background:#F59E0B;color:#fff} .cb-tag-item:hover:not(.active){border-color:#F59E0B;color:#F59E0B} .cb-loading{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:#999} .cb-loading-spinner{width:28px;height:28px;border:3px solid #eee;border-top-color:#F59E0B;border-radius:50%;animation:cb-spin .8s linear infinite} @keyframes cb-spin{to{transform:rotate(360deg)}} .cb-list-loading{display:flex;align-items:center;justify-content:center;gap:8px;padding:16px 12px;color:#999;font-size:12px} .cb-list-loading-spinner{width:16px;height:16px;border:2px solid #eee;border-top-color:#F59E0B;border-radius:50%;animation:cb-spin .8s linear infinite} .cb-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 20px;color:#999;text-align:center} .cb-empty-icon{font-size:36px;margin-bottom:8px;opacity:.5} .cb-empty-text{font-size:13px;line-height:1.6} .cb-song-item{display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid #f0f0f0;cursor:pointer;transition:background .15s} .cb-song-item:hover{background:#FFF8E1} .cb-song-cover{width:40px;height:40px;border-radius:6px;object-fit:cover;flex-shrink:0;background:#eee} .cb-song-info{flex:1;min-width:0;margin-left:8px} .cb-song-title{font-size:13px;color:#333;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .cb-song-artist{font-size:11px;color:#999;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .cb-song-tags{display:flex;gap:4px;margin-top:3px;flex-wrap:wrap} .cb-song-tag{font-size:9px;padding:1px 5px;border-radius:3px;background:#eee;color:#666} .cb-song-meta{display:flex;align-items:center;gap:6px;margin-top:2px} .cb-song-prof{font-size:10px;padding:1px 6px;border-radius:8px} .cb-song-play{font-size:10px;color:#aaa} .cb-goods-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:8px 12px} .cb-goods-card{border-radius:8px;background:#fff;border:1px solid #f0f0f0;overflow:hidden;cursor:pointer;transition:all .2s} .cb-goods-card:hover{box-shadow:0 2px 8px rgba(0,0,0,.1);transform:translateY(-1px)} .cb-goods-card.sold-out{opacity:.7} .cb-goods-card-img-wrap{position:relative;width:100%;padding-top:100%;background:#f5f5f5;overflow:hidden} .cb-goods-card-img{position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover} .cb-goods-sold-out-mask{position:absolute;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);color:#fff;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:500} .cb-goods-card-info{padding:8px 10px} .cb-goods-card-title{font-size:12px;color:#333;font-weight:500;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-height:1.4;min-height:34px} .cb-goods-card-desc{font-size:10px;color:#999;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .cb-goods-card-bottom{display:flex;align-items:flex-end;justify-content:space-between;margin-top:6px} .cb-goods-card-price{display:flex;align-items:baseline;color:#F59E0B} .cb-goods-price-symbol{font-size:10px;font-weight:500} .cb-goods-price-num{font-size:16px;font-weight:700;margin-left:1px} .cb-goods-price-unit{font-size:10px;color:#999;margin-left:2px;font-weight:400} .cb-goods-card-sales{font-size:10px;color:#999} .cb-invite{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:30px 20px;text-align:center;height:100%} .cb-invite-icon{width:96px;height:96px;object-fit:contain;margin-bottom:12px;border-radius:16px;box-shadow:0 4px 16px rgba(0,0,0,.1)} .cb-invite-title{font-size:16px;font-weight:600;color:#333;margin-bottom:8px} .cb-invite-desc{font-size:13px;color:#666;line-height:1.8;margin-bottom:20px} .cb-invite-btn{display:inline-block;background:#F59E0B;color:#fff;border:none;border-radius:8px;padding:10px 24px;font-size:14px;cursor:pointer;text-decoration:none;transition:background .2s} .cb-invite-btn:hover{background:#D97706} .cb-contact{margin-top:24px;padding:16px;background:#fafafa;border-radius:8px;width:100%;max-width:280px} .cb-contact-title{font-size:12px;color:#999;margin-bottom:8px;text-align:left} .cb-contact-item{display:flex;align-items:center;font-size:12px;color:#666;margin-bottom:6px;text-align:left} .cb-contact-item:last-child{margin-bottom:0} .cb-contact-label{color:#999;width:60px;flex-shrink:0} .cb-contact-value{color:#333;word-break:break-all} .cb-contact-link{color:#F59E0B;text-decoration:none} .cb-contact-link:hover{text-decoration:underline} .cb-anchor-bar{display:flex;align-items:center;padding:6px 12px;background:#FFF8E1;border-bottom:1px solid #FFE082;flex-shrink:0;gap:8px} .cb-anchor-avatar{width:24px;height:24px;border-radius:50%;object-fit:cover} .cb-anchor-name{font-size:12px;color:#333;font-weight:500} .cb-anchor-medal{font-size:10px;color:#F59E0B;background:#FFF3CD;padding:1px 6px;border-radius:8px} .cb-anchor-detail-btn{margin-left:auto;font-size:11px;color:#F59E0B;cursor:pointer;padding:2px 8px;border:1px solid #F59E0B;border-radius:10px;transition:all .2s;text-decoration:none;line-height:1.4} .cb-anchor-detail-btn:hover{background:#F59E0B;color:#fff;text-decoration:none} .cb-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.8);color:#fff;padding:8px 16px;border-radius:6px;font-size:12px;z-index:100001;animation:cb-fade-in .3s ease} .cb-toast-success{background:rgba(16,185,129,.95)} .cb-toast-error{background:rgba(239,68,68,.95)} @keyframes cb-fade-in{from{opacity:0;transform:translate(-50%,-10px)}to{opacity:1;transform:translate(-50%,0)}} .cb-cloud-btn{height:24px;cursor:pointer;margin-left:6px;margin-top:2px;display:flex;align-items:center;justify-content:center;padding:0 10px;border-radius:12px;background:linear-gradient(135deg,#F59E0B,#FBBF24);color:#fff;font-size:12px;font-weight:500;transition:all .2s;white-space:nowrap} .cb-cloud-btn:hover{opacity:.9;transform:translateY(-1px)} .cb-cloud-btn.active{box-shadow:0 2px 8px rgba(245,158,11,.4)} .cb-cloud-btn-text{line-height:1} `; if (typeof GM_addStyle === 'function') { GM_addStyle(PANEL_CSS); } else { const style = document.createElement('style'); style.textContent = PANEL_CSS; (document.head || document.documentElement).appendChild(style); } // ============ GM_xmlhttpRequest 封装 ============ function gmRequest(url, options = {}) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: options.method || 'GET', url: url, headers: options.headers || {}, data: options.body || null, credentials: options.credentials || 'omit', timeout: 30000, onload: (resp) => { resolve({ status: resp.status, json: () => { try { return JSON.parse(resp.responseText); } catch (e) { return {}; } }, text: () => resp.responseText }); }, onerror: (err) => reject(new Error('网络请求失败')), ontimeout: () => reject(new Error('请求超时')) }); }); } // ============ API 请求封装 ============ async function apiGet(path, params = {}) { const url = new URL(API_BASE + path); Object.keys(params || {}).forEach(k => { if (params[k] !== undefined && params[k] !== null && params[k] !== '') { url.searchParams.append(k, params[k]); } }); const resp = await gmRequest(url.toString(), { method: 'GET', headers: { 'Accept': 'application/json' } }); const data = resp.json(); if (resp.status === 429) { const retryAfter = data?.data?.retry_after || 60; throw new Error(`请求过于频繁,请 ${retryAfter} 秒后重试`); } return data; } async function apiPost(path, params = {}) { const url = new URL(API_BASE + path); const resp = await gmRequest(url.toString(), { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(params) }); const data = resp.json(); if (resp.status === 429) { const retryAfter = data?.data?.retry_after || 60; throw new Error(`请求过于频繁,请 ${retryAfter} 秒后重试`); } return data; } // ============ Toast 提示 ============ function showToast(msg, type = 'success', duration = 2500) { const toast = document.createElement('div'); toast.className = `cb-toast cb-toast-${type}`; toast.textContent = msg; document.body.appendChild(toast); setTimeout(() => { toast.style.transition = 'opacity .3s'; toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); }, typeof type === 'number' ? type : duration); } // ============ 确认对话框 ============ function confirmDialog(message) { return new Promise(resolve => { const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:100000;display:flex;align-items:center;justify-content:center;'; overlay.innerHTML = `
提示 云朵商城助手
${message}
`; overlay.querySelector('.cb-dlg-cancel').addEventListener('click', () => { overlay.remove(); resolve(false); }); overlay.querySelector('.cb-dlg-ok').addEventListener('click', () => { overlay.remove(); resolve(true); }); document.body.appendChild(overlay); }); } // ============ HTML 转义 ============ function esc(str) { if (str === null || str === undefined) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } // ============ 状态管理 ============ const state = { roomId: '', panelCreated: false, panelVisible: false, activeTab: 'song', songData: null, goodsData: null, songTags: [], goodsCategories: [], contactInfo: null, anchorInfo: null, anchorNotFound: false, songLoading: false, goodsLoading: false, songKeyword: '', goodsKeyword: '', songTagId: '', goodsGroupId: '', initialized: false, initialLoading: false, hasSongsTab: false, hasGoodsTab: false }; // ============ 数据加载 ============ async function loadContactInfo() { if (state.contactInfo) return state.contactInfo; try { const resp = await apiGet('/contact'); if (resp.code === 200) { state.contactInfo = resp.data; } } catch (e) { log.warn('获取联系方式失败:', e.message); } return state.contactInfo; } async function loadSongList(page = 1, append = false) { if (state.songLoading) return; state.songLoading = true; try { const params = { room_id: state.roomId, page, page_size: 12 }; if (state.songKeyword) params.keyword = state.songKeyword; if (state.songTagId) params.tag_id = state.songTagId; const resp = await apiGet('/song/list', params); if (resp.code === 200) { if (append && state.songData) { state.songData.list = state.songData.list.concat(resp.data.list); state.songData.page = resp.data.page; } else { state.songData = resp.data; } if (resp.data.anchor_info) state.anchorInfo = resp.data.anchor_info; } else if (resp.code === 404) { state.anchorNotFound = true; } } catch (e) { log.error('加载歌单失败:', e.message); } finally { state.songLoading = false; } } async function loadGoodsList(page = 1, append = false) { if (state.goodsLoading) return; state.goodsLoading = true; try { const params = { room_id: state.roomId, page, page_size: 12 }; if (state.goodsKeyword) params.keyword = state.goodsKeyword; if (state.goodsGroupId) params.group_id = state.goodsGroupId; const resp = await apiGet('/goods/list', params); if (resp.code === 200) { if (append && state.goodsData) { state.goodsData.list = state.goodsData.list.concat(resp.data.list); state.goodsData.page = resp.data.page; } else { state.goodsData = resp.data; } if (resp.data.anchor_info) state.anchorInfo = resp.data.anchor_info; } else if (resp.code === 404) { state.anchorNotFound = true; } } catch (e) { log.error('加载商品失败:', e.message); } finally { state.goodsLoading = false; } } async function loadSongTags() { try { const resp = await apiGet('/song/tags', { room_id: state.roomId }); if (resp.code === 200) { state.songTags = resp.data.list || []; } } catch (e) { /* 忽略标签加载失败 */ } } async function loadGoodsCategories() { try { const resp = await apiGet('/goods/categories', { room_id: state.roomId }); if (resp.code === 200) { state.goodsCategories = resp.data.list || []; } } catch (e) { /* 忽略分类加载失败 */ } } async function initPanelData() { if (state.initialized) return; state.initialized = true; state.initialLoading = true; await Promise.all([ loadContactInfo(), loadSongList(1), loadGoodsList(1) ]); if (!state.anchorNotFound && state.anchorInfo) { await Promise.all([loadSongTags(), loadGoodsCategories()]); } state.initialLoading = false; const hasSongs = state.songData && state.songData.total > 0; const hasGoods = state.goodsData && state.goodsData.total > 0; state.hasSongsTab = hasSongs; state.hasGoodsTab = hasGoods; if (!state.hasSongsTab && state.hasGoodsTab) { state.activeTab = 'goods'; } else { state.activeTab = 'song'; } } // ============ 渲染函数 ============ function renderSongItem(song) { const tags = (song.tags || []).map(t => `${esc(t.tag_name)}` ).join(''); const profColors = { '生疏': '#ef4444', '一般': '#f59e0b', '熟悉': '#3b82f6', '熟练': '#8b5cf6', '精通': '#10b981' }; const profText = esc(song.proficiency_text || '未知'); const profColor = profColors[profText] || '#999'; return `
${esc(song.title)}
${song.artist ? `
${esc(song.artist)}
` : ''}
${profText} ${song.play_count > 0 ? `▶ ${song.play_count}` : ''}
${tags ? `
${tags}
` : ''}
`; } function renderGoodsItem(goods) { const stockText = goods.stock > 0 ? `库存 ${goods.stock}` : '已售罄'; const stockClass = goods.stock > 0 ? '' : 'sold-out'; return `
${goods.stock === 0 ? '
已售罄
' : ''}
${esc(goods.title)}
${goods.desc ? `
${esc(goods.desc)}
` : ''}
¥ ${goods.score} 积分
已售${goods.sold_numb}
`; } function renderLoading() { return `
加载中...
`; } function renderListLoading() { return `
加载中...
`; } function renderEmpty(text = '暂无数据') { return `
📋
${esc(text)}
`; } function renderInviteInterface() { const c = state.contactInfo || {}; return `
云朵商城
主播尚未上传内容
当前主播已开通云朵商城服务,
但还没有上传歌单或商品内容。
马上去推荐他/她上传内容吧!
前往云朵商城 ${renderContactBlock(c)}
`; } function renderNotFoundInterface() { const c = state.contactInfo || {}; return `
云朵商城
当前主播未使用云朵商城服务
马上去推荐给他/她吧!
让主播的歌单和商品直达粉丝直播间。
推荐给主播 ${renderContactBlock(c)}
`; } function renderContactBlock(c) { if (!c) return ''; const items = []; if (c.email) items.push(`
邮箱${esc(c.email)}
`); if (c.wechat) items.push(`
微信${esc(c.wechat)}
`); if (c.qq) items.push(`
QQ${esc(c.qq)}
`); if (c.qq_group) items.push(`
QQ群${esc(c.qq_group)}${c.qq_group_url ? ` 加入` : ''}
`); if (!items.length) return ''; return `
需要帮助 / 问题反馈 / 联系方式
${items.join('')} ${c.feedback_url ? `
问题反馈 →
` : ''}
`; } function renderAnchorBar() { if (!state.anchorInfo) return ''; const a = state.anchorInfo; const anchorId = a.anchor_id || a.uid || a.id || ''; return `
${esc(a.anchor_name || '主播')} ${a.fans_medal ? `${esc(a.fans_medal)}` : ''} 查看详情 →
`; } function renderSongTagBar() { if (!state.songTags.length) return ''; const tags = [`全部`]; state.songTags.forEach(t => { tags.push(`${esc(t.name)}`); }); return `
${tags.join('')}
`; } function renderGoodsCategoryBar() { if (!state.goodsCategories.length) return ''; const cats = [`全部`]; state.goodsCategories.forEach(g => { cats.push(`${esc(g.group_name)}`); }); return `
${cats.join('')}
`; } function renderSearchBar(type) { const keyword = type === 'song' ? state.songKeyword : state.goodsKeyword; const randomBtn = type === 'song' ? `` : ''; return `
${randomBtn}
`; } function renderSongContent() { if (state.songLoading && (!state.songData || !state.songData.list || state.songData.list.length === 0)) { return renderLoading(); } if (!state.songData || !state.songData.list || state.songData.list.length === 0) { return renderEmpty('暂无歌曲'); } let html = state.songData.list.map(renderSongItem).join(''); if (state.songLoading) { html += renderListLoading(); } return html; } function renderGoodsContent() { if (state.goodsLoading && (!state.goodsData || !state.goodsData.list || state.goodsData.list.length === 0)) { return renderLoading(); } if (!state.goodsData || !state.goodsData.list || state.goodsData.list.length === 0) { return renderEmpty('暂无商品'); } let html = `
${state.goodsData.list.map(renderGoodsItem).join('')}
`; if (state.goodsLoading) { html += renderListLoading(); } return html; } function renderTabBar() { if (!state.hasSongsTab && !state.hasGoodsTab) return ''; if (state.hasSongsTab && !state.hasGoodsTab) { return `
歌单
`; } if (!state.hasSongsTab && state.hasGoodsTab) { return `
商城
`; } const songCount = state.songData?.total || 0; const goodsCount = state.goodsData?.total || 0; return `
歌单${songCount}
商城${goodsCount}
`; } function renderPanel() { const panel = document.querySelector('.cb-panel'); if (!panel) return; if (state.initialLoading) { panel.innerHTML = `
☁️ 云朵商城
${renderLoading()}
`; bindPanelEvents(); return; } if (state.anchorNotFound) { panel.innerHTML = `
☁️ 云朵商城
${renderNotFoundInterface()}
`; bindPanelEvents(); return; } if (!state.hasSongsTab && !state.hasGoodsTab) { panel.innerHTML = `
☁️ 云朵商城
${renderInviteInterface()}
`; bindPanelEvents(); return; } let headerHtml = renderAnchorBar(); if (state.activeTab === 'song' && state.hasSongsTab) { headerHtml += renderSearchBar('song') + renderSongTagBar(); } else if (state.activeTab === 'goods' && state.hasGoodsTab) { headerHtml += renderSearchBar('goods') + renderGoodsCategoryBar(); } let contentHtml = ''; if (state.activeTab === 'song' && state.hasSongsTab) { contentHtml = `
${renderSongContent()}
`; } else if (state.activeTab === 'goods' && state.hasGoodsTab) { contentHtml = `
${renderGoodsContent()}
`; } panel.innerHTML = `
☁️ 云朵商城
${renderTabBar()}
${headerHtml}
${contentHtml}
`; bindPanelEvents(); setupInfiniteScroll(); } function setupInfiniteScroll() { const panelBody = document.querySelector('.cb-panel-body'); if (!panelBody) return; const existingHandler = panelBody.getAttribute('data-scroll-handler'); if (existingHandler && window[existingHandler]) { panelBody.removeEventListener('scroll', window[existingHandler]); delete window[existingHandler]; } const handlerName = `cb-scroll-handler-${Date.now()}`; window[handlerName] = () => { const isAtBottom = panelBody.scrollHeight - panelBody.scrollTop <= panelBody.clientHeight + 50; if (isAtBottom) { if (state.activeTab === 'song') { const data = state.songData; if (data && !state.songLoading && data.page < data.total_page) { loadSongList(data.page + 1, true).then(renderListContentOnly); } } else if (state.activeTab === 'goods') { const data = state.goodsData; if (data && !state.goodsLoading && data.page < data.total_page) { loadGoodsList(data.page + 1, true).then(renderListContentOnly); } } } }; panelBody.addEventListener('scroll', window[handlerName]); panelBody.setAttribute('data-scroll-handler', handlerName); } function renderListContentOnly() { const panelBody = document.querySelector('.cb-panel-body'); if (!panelBody) return; const scrollTop = panelBody.scrollTop; if (state.activeTab === 'song') { panelBody.innerHTML = `
${renderSongContent()}
`; } else if (state.activeTab === 'goods') { panelBody.innerHTML = `
${renderGoodsContent()}
`; } panelBody.scrollTop = scrollTop; bindListEvents(); } function bindListEvents() { const panel = document.querySelector('.cb-panel'); if (!panel) return; panel.querySelectorAll('.cb-song-item').forEach(item => { item.addEventListener('click', () => { const title = item.getAttribute('data-song-title'); const artist = item.getAttribute('data-song-artist'); const text = artist ? `点歌 ${title} - ${artist}` : `点歌 ${title}`; fillChatInput(text); }); }); panel.querySelectorAll('.cb-goods-card').forEach(item => { item.addEventListener('click', () => { const pid = item.getAttribute('data-product-id'); if (pid) { window.open(`${SITE_BASE}/shop/goods_detail/${pid}`, '_blank'); } }); }); } function bindPanelEvents() { const panel = document.querySelector('.cb-panel'); if (!panel) return; const closeBtn = panel.querySelector('.cb-panel-close'); if (closeBtn) { closeBtn.addEventListener('click', () => togglePanel(false)); } panel.querySelectorAll('.cb-tab').forEach(tab => { tab.addEventListener('click', () => { const target = tab.getAttribute('data-tab'); if (target && target !== state.activeTab) { state.activeTab = target; renderPanel(); } }); }); bindListEvents(); panel.querySelectorAll('.cb-search-btn').forEach(btn => { btn.addEventListener('click', () => { const type = btn.getAttribute('data-type'); const input = panel.querySelector(`.cb-search-input[data-type="${type}"]`); if (input) { if (type === 'song') { state.songKeyword = input.value.trim(); state.songData = null; panel.querySelectorAll('.cb-tag-item[data-tag-id]').forEach(t => t.classList.remove('active')); panel.querySelector('.cb-tag-item[data-tag-id=""]')?.classList.add('active'); loadSongList(1).then(renderListContentOnly); } else { state.goodsKeyword = input.value.trim(); state.goodsData = null; panel.querySelectorAll('.cb-tag-item[data-group-id]').forEach(c => c.classList.remove('active')); panel.querySelector('.cb-tag-item[data-group-id=""]')?.classList.add('active'); loadGoodsList(1).then(renderListContentOnly); } } }); }); panel.querySelectorAll('.cb-search-input').forEach(input => { input.addEventListener('keypress', e => { if (e.key === 'Enter') { const type = input.getAttribute('data-type'); panel.querySelector(`.cb-search-btn[data-type="${type}"]`)?.click(); } }); }); panel.querySelectorAll('.cb-random-btn').forEach(btn => { btn.addEventListener('click', handleRandomSong); }); panel.querySelectorAll('.cb-tag-item[data-tag-id]').forEach(tag => { tag.addEventListener('click', () => { panel.querySelectorAll('.cb-tag-item[data-tag-id]').forEach(t => t.classList.remove('active')); tag.classList.add('active'); state.songTagId = tag.getAttribute('data-tag-id') || ''; state.songData = null; loadSongList(1).then(renderListContentOnly); }); }); panel.querySelectorAll('.cb-tag-item[data-group-id]').forEach(cat => { cat.addEventListener('click', () => { panel.querySelectorAll('.cb-tag-item[data-group-id]').forEach(c => c.classList.remove('active')); cat.classList.add('active'); state.goodsGroupId = cat.getAttribute('data-group-id') || ''; state.goodsData = null; loadGoodsList(1).then(renderListContentOnly); }); }); initTagScrollButtons(panel); } function initTagScrollButtons(panel) { const wrapList = panel.querySelectorAll('.cb-tag-bar-wrap'); wrapList.forEach(wrap => { const tagBar = wrap.querySelector('.cb-tag-bar'); const leftBtn = wrap.querySelector('.cb-tag-scroll-left'); const rightBtn = wrap.querySelector('.cb-tag-scroll-right'); if (!tagBar || !leftBtn || !rightBtn) return; const updateButtons = () => { const { scrollLeft, scrollWidth, clientWidth } = tagBar; const canScrollLeft = scrollLeft > 2; const canScrollRight = scrollLeft + clientWidth < scrollWidth - 2; leftBtn.style.opacity = canScrollLeft ? '1' : '0'; leftBtn.style.pointerEvents = canScrollLeft ? 'auto' : 'none'; rightBtn.style.opacity = canScrollRight ? '1' : '0'; rightBtn.style.pointerEvents = canScrollRight ? 'auto' : 'none'; wrap.classList.toggle('has-overflow', scrollWidth > clientWidth + 2); }; leftBtn.addEventListener('click', () => { tagBar.scrollBy({ left: -120, behavior: 'smooth' }); }); rightBtn.addEventListener('click', () => { tagBar.scrollBy({ left: 120, behavior: 'smooth' }); }); tagBar.addEventListener('scroll', updateButtons); requestAnimationFrame(updateButtons); setTimeout(updateButtons, 100); }); } // ============ 弹幕框填充 ============ function findDanmakuInput() { const selectors = [ 'textarea.chat-input', 'div.chat-input-ctnr textarea', 'textarea.bili-dyn-list__item-input', 'input[type="text"][placeholder*="弹幕"]', 'textarea[placeholder*="发送弹幕"]', '.live-chat-input textarea', '#chat-input-area textarea', ]; for (const sel of selectors) { const el = document.querySelector(sel); if (el) return el; } return null; } async function fillChatInput(text) { const textarea = document.querySelector('div.chat-input-ctnr textarea.chat-input') || findDanmakuInput(); if (textarea) { if (textarea.value.trim() !== '') { const ok = await confirmDialog('当前弹幕框不为空,是否继续填入?'); if (!ok) return; } text = text.replace(/<[^>]*>/g, ''); const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set || Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set; if (setter) { setter.call(textarea, text); } else { textarea.value = text; } textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('change', { bubbles: true })); textarea.focus(); showToast('已填入弹幕框'); } else { const ok = await confirmDialog('未找到弹幕框,需要前往反馈吗?\n\n错误G-400-100,请联系开发者\n\n可能的原因:\n1. 未登录哔哩哔哩账号\n2. 脚本未适配当前版本哔哩哔哩'); if (ok) { window.open(state.contactInfo?.feedback_url || `${SITE_BASE}/`, '_blank'); } } } async function handleRandomSong(e) { const btn = e.currentTarget; if (!state.roomId) return; btn.disabled = true; btn.textContent = '🎲 随机中...'; try { const resp = await apiGet('/song/random', { room_id: state.roomId }); if (resp && resp.data && resp.data.song) { const song = resp.data.song; const text = song.artist ? `点歌 ${song.title} - ${song.artist}` : `点歌 ${song.title}`; await fillChatInput(text); log.success(`随机点歌: ${song.title} - ${song.artist}`); } } catch (e) { log.warn('随机获取歌曲失败:', e.message); showToast('随机获取失败', 'error'); } finally { btn.disabled = false; btn.textContent = '🎲 随机一首'; } } // ============ 面板控制 ============ async function togglePanel(show) { const panel = document.querySelector('.cb-panel'); const btn = document.querySelector('.cb-cloud-btn'); if (!panel) return; if (show === undefined) { state.panelVisible = !state.panelVisible; } else { state.panelVisible = show; } if (state.panelVisible) { panel.classList.add('active'); if (btn) btn.classList.add('active'); if (!state.initialized) { state.initialLoading = true; renderPanel(); await initPanelData(); renderPanel(); } } else { panel.classList.remove('active'); if (btn) btn.classList.remove('active'); } } function createPanel() { if (state.panelCreated) return; const asideArea = document.querySelector('.aside-area'); const chatControl = document.querySelector('.chat-control-panel'); if (!asideArea) { log.error('侧栏(弹幕列表)不存在'); return; } const panel = document.createElement('div'); panel.className = 'cb-panel'; if (chatControl) { panel.style.height = `calc(100% - ${chatControl.clientHeight}px)`; } panel.innerHTML = `
☁️ 云朵商城
${renderLoading()}
`; asideArea.appendChild(panel); state.panelCreated = true; panel.querySelector('.cb-panel-close').addEventListener('click', () => togglePanel(false)); } function createButton() { const iconLeftPart = document.querySelector('div.icon-left-part'); if (!iconLeftPart) return; if (document.querySelector('.cb-cloud-btn')) return; const btnWrap = document.createElement('div'); btnWrap.className = 'cb-cloud-btn'; btnWrap.innerHTML = `云朵商城`; btnWrap.title = '云朵商城'; btnWrap.addEventListener('click', () => { createPanel(); togglePanel(); }); iconLeftPart.appendChild(btnWrap); log.success('入口按钮已添加'); } // ============ B站礼物数据获取与上传 ============ async function fetchBilibiliGifts(roomId) { try { const url = `https://api.live.bilibili.com/xlive/web-room/v1/giftPanel/roomGiftList?platform=pc&room_id=${roomId}`; const resp = await gmRequest(url, { method: 'GET', headers: { 'Referer': `https://live.bilibili.com/${roomId}`, 'User-Agent': navigator.userAgent }, credentials: 'include' }); const data = resp.json(); if (data.code === 0 && data.data && data.data.gift_config && data.data.gift_config.base_config) { const gifts = data.data.gift_config.base_config.list || []; return { gifts }; } } catch (e) { log.error('获取B站礼物数据失败:', e.message); } return null; } async function fetchBilibiliBlindBoxes(gifts) { const blindBoxes = []; const blindGiftIds = gifts .filter(g => g.gift_type === 6) .map(g => g.id); for (const giftId of blindGiftIds) { try { const url = `https://api.live.bilibili.com/xlive/general-interface/v1/blindFirstWin/getInfo?gift_id=${giftId}`; const resp = await gmRequest(url, { method: 'GET', headers: { 'Referer': `https://live.bilibili.com/`, 'User-Agent': navigator.userAgent }, credentials: 'include' }); const data = resp.json(); if (data.code === 0 && data.data) { blindBoxes.push({ blind_gift_id: giftId, blind_price: data.data.blind_price || 0, blind_gift_name: data.data.blind_gift_name || '', note_text: data.data.note_text || '', gifts: data.data.gifts || [] }); } await new Promise(r => setTimeout(r, 1000)); } catch (e) { log.warn('获取盲盒概率数据失败 gift_id=', giftId, e.message); } } return blindBoxes; } async function fetchAndUploadGiftData(roomId) { try { const giftData = await fetchBilibiliGifts(roomId); if (giftData && giftData.gifts && giftData.gifts.length > 0) { await apiPost('/gift/upload', { room_id: roomId, gifts: giftData.gifts }); log.success('礼物数据上传成功:', giftData.gifts.length); const blindBoxData = await fetchBilibiliBlindBoxes(giftData.gifts); if (blindBoxData && blindBoxData.length > 0) { await apiPost('/blind_box/upload', { room_id: roomId, blind_boxes: blindBoxData }); log.success('盲盒数据上传成功:', blindBoxData.length); } } } catch (e) { log.warn('礼物数据获取或上传失败:', e.message); } } // ============ 脚本猫/油猴菜单命令 ============ if (typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('☁️ 打开/关闭云朵商城面板', () => { createPanel(); togglePanel(); }); GM_registerMenuCommand('⚙️ 切换插件启用状态', () => { enabled = !enabled; GM_setValue('enabled', enabled); showToast(`插件已${enabled ? '启用' : '禁用'},刷新页面后生效`, enabled ? 'success' : 'error'); }); } // ============ 初始化 ============ async function init() { log.info('版本号:', VERSION); log.info('开始加载...'); if (!enabled) { log.info('插件已在设置中禁用,跳过加载'); return; } const pathRoomId = window.location.pathname.replace(/[^\d]/g, ''); if (pathRoomId.length === 0) { log.warn('非直播间页面,退出加载'); return; } state.roomId = pathRoomId; log.success('当前房间号:', state.roomId); const asideVm = document.getElementById('aside-area-vm'); if (!asideVm) { log.error('侧栏(弹幕列表)不存在,退出加载'); return; } const observer = new MutationObserver(() => { const iconLeftPart = document.querySelector('div.icon-left-part'); if (!iconLeftPart) return; if (document.querySelector('.cb-cloud-btn')) return; createButton(); }); observer.observe(asideVm, { attributes: true, childList: true, subtree: true }); fetchAndUploadGiftData(state.roomId); log.success('加载完成,等待页面渲染...'); } init(); })();