Files
chatroom/resources/views/chat/partials/shop-panel.blade.php
lkddi 29e43507ac 功能:商店完善戒指板块
迁移:
- 2026_03_01_153959:shop_items 增加 intimacy_bonus/charm_bonus 字段

Seeder(RingItemsSeeder):
- 银质戒指 500金  亲密+10 魅力+30
- 黄金戒指 2000金 亲密+30 魅力+80
- 红宝石戒指 8000金 亲密+80 魅力+200
- 钻石戒指 30000金 亲密+200 魅力+500
- 传说神戒 100000金 亲密+500 魅力+1000

ShopService:
- buyItem() 分支加 ring 类型
- buyRing():扣金币 + 写入 active UserPurchase(背包持有)

ShopController::items():
- 返回 intimacy_bonus/charm_bonus
- 统计 ring_counts(各戒指持有数量)

shop-panel.blade.php:
- 新增「💍 求婚戒指」分组(排在最后)
- 图标右上角红色数字徽章(持有时)
- 卡片下方显示亲密度/魅力加成
- 购买按钮与现有逻辑复用
2026-03-01 15:42:25 +08:00

552 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{{--
文件功能:商店面板视图(嵌入聊天室右侧)
展示单次卡、周卡、改名卡,支持购买和改名操作
采用紧凑卡片设计,适配窄侧边栏
--}}
<style>
/* ── 商店面板样式 ──────────────────────────────── */
#shop-panel {
display: none;
position: absolute;
/* 顶部 tab 栏高度约 26px底部状态栏约 22px */
top: 26px;
left: 0;
right: 0;
bottom: 22px;
flex-direction: column;
background: #0f0c29;
z-index: 10;
}
#shop-balance-bar {
padding: 6px 8px;
background: linear-gradient(135deg, #1e1b4b, #312e81);
border-bottom: 1px solid #4f46e5;
font-size: 11px;
color: #a5b4fc;
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
#shop-jjb {
color: #fbbf24;
font-weight: 800;
font-size: 12px;
}
#shop-week-badge {
display: none;
margin-left: auto;
font-size: 10px;
background: #4338ca;
padding: 1px 5px;
border-radius: 10px;
color: #c7d2fe;
white-space: nowrap;
}
#shop-toast {
display: none;
margin: 4px 6px;
padding: 5px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: bold;
flex-shrink: 0;
}
#shop-items-list {
flex: 1;
overflow-y: auto;
padding: 6px 5px;
scrollbar-width: thin;
scrollbar-color: #4338ca #0f0c29;
}
/* 分组标题 */
.shop-group-label {
font-size: 10px;
font-weight: 800;
color: #818cf8;
letter-spacing: .5px;
text-transform: uppercase;
padding: 4px 2px 3px;
border-bottom: 1px solid #312e81;
margin-bottom: 5px;
}
.shop-group-desc {
font-size: 9px;
color: #6b7280;
margin-bottom: 5px;
line-height: 1.3;
}
/* 商品卡片 */
.shop-card {
background: linear-gradient(135deg, #1e1b4b 60%, #2d2a6e);
border: 1px solid #3730a3;
border-radius: 8px;
padding: 7px 8px;
margin-bottom: 5px;
transition: border-color .2s, box-shadow .2s;
cursor: default;
}
.shop-card:hover {
border-color: #6366f1;
box-shadow: 0 0 8px rgba(99, 102, 241, .35);
}
.shop-card-row {
display: flex;
align-items: center;
gap: 5px;
}
.shop-card-icon {
font-size: 18px;
flex-shrink: 0;
width: 26px;
text-align: center;
line-height: 1;
}
.shop-card-name {
font-size: 11px;
font-weight: 700;
color: #e0e7ff;
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.shop-card-desc {
font-size: 9px;
color: #6b7280;
margin-top: 3px;
line-height: 1.35;
}
/* 购买按钮 */
.shop-btn {
display: inline-flex;
align-items: center;
gap: 2px;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: #fff;
border: none;
border-radius: 5px;
padding: 3px 7px;
cursor: pointer;
font-size: 10px;
font-weight: 800;
white-space: nowrap;
flex-shrink: 0;
transition: opacity .15s, transform .1s;
}
.shop-btn:hover {
opacity: .85;
transform: scale(1.04);
}
.shop-btn-use {
background: linear-gradient(135deg, #7c3aed, #a855f7);
}
/* 改名弹框 */
#rename-modal {
display: none;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, .75);
z-index: 300;
align-items: center;
justify-content: center;
}
#rename-modal-inner {
background: linear-gradient(160deg, #1e1b4b, #1a1060);
border: 1px solid #6366f1;
border-radius: 10px;
padding: 14px 12px;
width: 190px;
box-shadow: 0 8px 30px rgba(99, 102, 241, .4);
}
#rename-modal-title {
font-size: 12px;
font-weight: 800;
color: #c7d2fe;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 4px;
}
#rename-input {
width: 100%;
background: #312e81;
border: 1px solid #4f46e5;
border-radius: 5px;
padding: 5px 7px;
color: #e0e7ff;
font-size: 12px;
box-sizing: border-box;
margin-bottom: 8px;
outline: none;
}
#rename-input:focus {
border-color: #818cf8;
}
.rename-btn-row {
display: flex;
gap: 6px;
}
#rename-confirm {
flex: 1;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
color: #fff;
border: none;
border-radius: 5px;
padding: 5px;
cursor: pointer;
font-size: 11px;
font-weight: 700;
}
#rename-cancel {
flex: 1;
background: #374151;
color: #9ca3af;
border: none;
border-radius: 5px;
padding: 5px;
cursor: pointer;
font-size: 11px;
}
#rename-err {
color: #f87171;
font-size: 10px;
margin-top: 5px;
min-height: 14px;
}
</style>
<div id="shop-panel">
{{-- 余额栏 --}}
<div id="shop-balance-bar">
🪙 <span id="shop-jjb">--</span> 金币
<span id="shop-week-badge"></span>
</div>
{{-- Toast --}}
<div id="shop-toast"></div>
{{-- 商品列表 --}}
<div id="shop-items-list">
<div style="text-align:center;color:#6366f1;padding:20px 0;font-size:11px;">加载中…</div>
</div>
{{-- 改名弹框 --}}
<div id="rename-modal">
<div id="rename-modal-inner">
<div id="rename-modal-title">🎭 使用改名卡</div>
<input id="rename-input" type="text" maxlength="10" placeholder="输入新昵称1-10字">
<div class="rename-btn-row">
<button id="rename-confirm" onclick="submitRename()">确认改名</button>
<button id="rename-cancel" onclick="closeRenameModal()">取消</button>
</div>
<div id="rename-err"></div>
</div>
</div>
</div>
<script>
/**
* 商店面板前端逻辑
*/
(function() {
let shopLoaded = false;
/** 打开商店 Tab 时调用 */
window.loadShop = function() {
if (shopLoaded) return;
shopLoaded = true;
fetchShopData();
};
/** 拉取商品数据 */
function fetchShopData() {
fetch('{{ route('shop.items') }}', {
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': _csrf()
}
})
.then(r => r.json())
.then(data => renderShop(data))
.catch(() => showShopToast('⚠ 加载失败,请刷新重试', false));
}
/** 渲染商品列表 */
function renderShop(data) {
// 更新金币
document.getElementById('shop-jjb').textContent = Number(data.user_jjb).toLocaleString();
// 周卡状态徽章
const badge = document.getElementById('shop-week-badge');
if (data.active_week_effect) {
const icons = {
fireworks: '🎆',
rain: '🌧',
lightning: '⚡',
snow: '❄️'
};
badge.textContent = (icons[data.active_week_effect] ?? '') + ' 周卡生效中';
badge.style.display = 'inline-block';
}
const ringCounts = data.ring_counts || {};
const groups = [{
label: '⚡ 单次特效卡',
desc: '立即播放一次,仅自己可见',
type: 'instant'
},
{
label: '📅 周卡・7天登录自动播放',
desc: '同时只能激活一种,购新旧失效无退款',
type: 'duration'
},
{
label: '🎭 道具',
desc: '',
type: 'one_time'
},
{
label: '💍 求婚戒指',
desc: '购买后存入背包,求婚时消耗(若被拒绝则遗失)',
type: 'ring'
},
];
const itemsEl = document.getElementById('shop-items-list');
itemsEl.innerHTML = '';
groups.forEach(g => {
const items = data.items.filter(i => i.type === g.type);
if (!items.length) return;
const section = document.createElement('div');
section.style.marginBottom = '10px';
// 分组标题
const label = document.createElement('div');
label.className = 'shop-group-label';
label.textContent = g.label;
section.appendChild(label);
if (g.desc) {
const desc = document.createElement('div');
desc.className = 'shop-group-desc';
desc.textContent = g.desc;
section.appendChild(desc);
}
items.forEach(item => {
const isRename = item.slug === 'rename_card';
const canUseRename = isRename && data.has_rename_card;
const isRing = item.type === 'ring';
const ownedQty = isRing ? (ringCounts[item.id] || 0) : 0;
const card = document.createElement('div');
card.className = 'shop-card';
// 行:图标 + 名称 + 按钮
const row = document.createElement('div');
row.className = 'shop-card-row';
// 图标区(戒指加持有数徽标)
const iconWrap = document.createElement('span');
iconWrap.style.cssText =
'position:relative; flex-shrink:0; width:28px; text-align:center;';
const icon = document.createElement('span');
icon.className = 'shop-card-icon';
icon.textContent = item.icon;
iconWrap.appendChild(icon);
if (isRing && ownedQty > 0) {
const badge = document.createElement('span');
badge.style.cssText =
'position:absolute; top:-4px; right:-4px; background:#f43f5e; color:#fff; font-size:8px; font-weight:800; min-width:14px; height:14px; border-radius:7px; text-align:center; line-height:14px; padding:0 2px;';
badge.textContent = ownedQty;
iconWrap.appendChild(badge);
}
row.appendChild(iconWrap);
// 名称
const name = document.createElement('span');
name.className = 'shop-card-name';
name.title = item.name;
name.textContent = item.name;
row.appendChild(name);
// 按钮
const btn = document.createElement('button');
if (canUseRename) {
btn.className = 'shop-btn shop-btn-use';
btn.textContent = '使用';
btn.onclick = openRenameModal;
} else {
btn.className = 'shop-btn';
btn.innerHTML = `🪙 ${Number(item.price).toLocaleString()}`;
btn.onclick = () => buyItem(item.id, item.name, item.price);
}
row.appendChild(btn);
card.appendChild(row);
// 简介
if (item.description) {
const desc = document.createElement('div');
desc.className = 'shop-card-desc';
desc.textContent = item.description;
card.appendChild(desc);
}
// 戒指:加成信息行
if (isRing && (item.intimacy_bonus > 0 || item.charm_bonus > 0)) {
const bonus = document.createElement('div');
bonus.style.cssText =
'font-size:9px; color:#f43f5e; margin-top:3px; display:flex; gap:8px;';
if (item.intimacy_bonus > 0) {
const b1 = document.createElement('span');
b1.textContent = `💞 亲密 +${item.intimacy_bonus}`;
bonus.appendChild(b1);
}
if (item.charm_bonus > 0) {
const b2 = document.createElement('span');
b2.style.color = '#a855f7';
b2.textContent = `✨ 魅力 +${item.charm_bonus}`;
bonus.appendChild(b2);
}
card.appendChild(bonus);
}
section.appendChild(card);
});
itemsEl.appendChild(section);
});
}
/** 购买商品 */
window.buyItem = function(itemId, name, price) {
if (!confirm(`确定花费 ${Number(price).toLocaleString()} 金币购买【${name}】吗?`)) return;
fetch('{{ route('shop.buy') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
item_id: itemId
}),
})
.then(r => r.json())
.then(data => {
showShopToast(data.message, data.status === 'success');
if (data.status === 'success') {
if (data.jjb !== undefined)
document.getElementById('shop-jjb').textContent = Number(data.jjb)
.toLocaleString();
if (data.play_effect && window.EffectManager)
window.EffectManager.play(data.play_effect);
// 刷新商品状态
shopLoaded = false;
setTimeout(() => {
fetchShopData();
shopLoaded = true;
}, 800);
}
})
.catch(() => showShopToast('⚠ 网络异常,请重试', false));
};
/** Toast 通知 */
window.showShopToast = function(msg, ok) {
const el = document.getElementById('shop-toast');
el.textContent = msg;
el.style.display = 'block';
el.style.background = ok ? '#064e3b' : '#7f1d1d';
el.style.color = ok ? '#6ee7b7' : '#fca5a5';
clearTimeout(el._t);
el._t = setTimeout(() => {
el.style.display = 'none';
}, 3500);
};
/** 打开改名框 */
window.openRenameModal = function() {
const m = document.getElementById('rename-modal');
m.style.display = 'flex';
document.getElementById('rename-input').focus();
document.getElementById('rename-err').textContent = '';
};
/** 关闭改名框 */
window.closeRenameModal = function() {
document.getElementById('rename-modal').style.display = 'none';
};
/** 提交改名 */
window.submitRename = function() {
const newName = document.getElementById('rename-input').value.trim();
if (!newName) {
document.getElementById('rename-err').textContent = '请输入新昵称';
return;
}
fetch('{{ route('shop.rename') }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': _csrf()
},
body: JSON.stringify({
new_name: newName
}),
})
.then(r => r.json())
.then(data => {
if (data.status === 'success') {
closeRenameModal();
showShopToast(data.message, true);
shopLoaded = false;
setTimeout(() => window.location.reload(), 2000);
} else {
document.getElementById('rename-err').textContent = data.message;
}
})
.catch(() => {
document.getElementById('rename-err').textContent = '网络异常,请重试';
});
};
function _csrf() {
return document.querySelector('meta[name="csrf-token"]')?.content ?? '';
}
})();
</script>