// ==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 ``;
}
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(``);
if (!items.length) return '';
return `
`;
}
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 `
`;
}
function renderGoodsCategoryBar() {
if (!state.goodsCategories.length) return '';
const cats = [`全部`];
state.goodsCategories.forEach(g => {
cats.push(`${esc(g.group_name)}`);
});
return `
`;
}
function renderSearchBar(type) {
const keyword = type === 'song' ? state.songKeyword : state.goodsKeyword;
const randomBtn = type === 'song' ? `` : '';
return `
`;
}
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()}
${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();
})();