feat: add modal shop and lottery management
This commit is contained in:
+90
-42
@@ -16,8 +16,12 @@ const logs = ref<LogItem[]>([]);
|
||||
const groups = ref<GroupItem[]>([]);
|
||||
const selectedChatId = ref<number | null>(null);
|
||||
const shopItems = ref<ShopItem[]>([]);
|
||||
const shopTransactions = ref<Record<string, any>[]>([]);
|
||||
const lotteries = ref<LotteryItem[]>([]);
|
||||
const lotteryParticipants = ref<Record<string, any>[]>([]);
|
||||
const shopForm = reactive({ item_key: '', name: '', cost: 0, desc: '', enabled: true });
|
||||
const lotteryForm = reactive({ id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' });
|
||||
const activeModal = ref<'shop' | 'lottery' | 'bans' | null>(null);
|
||||
const newMessage = ref('');
|
||||
const sending = ref(false);
|
||||
const restarting = ref(false);
|
||||
@@ -107,6 +111,9 @@ async function loadGroups() {
|
||||
async function loadShopItems() {
|
||||
try { shopItems.value = await fetchJson(`/api/shop-items${selectedQuery.value}`); } catch {}
|
||||
}
|
||||
async function loadShopTransactions() {
|
||||
try { shopTransactions.value = await fetchJson(`/api/shop-transactions${selectedQuery.value}`); } catch {}
|
||||
}
|
||||
async function saveShopItem() {
|
||||
if (!shopForm.item_key || !shopForm.name) return alert('请填写商品编号和名称');
|
||||
await fetchJson('/api/shop-items', {
|
||||
@@ -117,9 +124,33 @@ async function saveShopItem() {
|
||||
await loadShopItems();
|
||||
}
|
||||
async function editShopItem(item: ShopItem) { Object.assign(shopForm, item); }
|
||||
async function deleteShopItem(item: ShopItem) {
|
||||
if (!window.confirm(`确认删除商品 ${item.name}?`)) return;
|
||||
await fetchJson(`/api/shop-items/${item.item_key}${selectedQuery.value}`, { method: 'DELETE' });
|
||||
await loadShopItems();
|
||||
}
|
||||
async function loadLotteries() {
|
||||
try { lotteries.value = await fetchJson(`/api/lotteries${selectedQuery.value}`); } catch {}
|
||||
}
|
||||
function resetLotteryForm() { Object.assign(lotteryForm, { id: 0, title: '', prize: '', description: '', condition_type: 'all', condition_value: 0, draw_type: 'people', end_type: 'people', end_value: 3, max_participants: 3, draw_at: '', status: 'draft' }); }
|
||||
function editLottery(item: LotteryItem) { Object.assign(lotteryForm, { ...item, id: item.id || 0 }); }
|
||||
async function saveLottery() {
|
||||
if (!lotteryForm.title || !lotteryForm.prize) return alert('请填写抽奖标题和奖品');
|
||||
lotteryForm.end_type = lotteryForm.draw_type;
|
||||
if (lotteryForm.draw_type === 'people') lotteryForm.max_participants = Number(lotteryForm.end_value || lotteryForm.max_participants || 1);
|
||||
const url = lotteryForm.id ? `/api/lotteries/${lotteryForm.id}` : '/api/lotteries';
|
||||
await fetchJson(url, { method: lotteryForm.id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...lotteryForm, chat_id: selectedChatId.value || 0 }) });
|
||||
resetLotteryForm();
|
||||
await loadLotteries();
|
||||
}
|
||||
async function deleteLottery(item: LotteryItem) {
|
||||
if (!window.confirm(`确认删除抽奖 ${item.title || item.prize}?`)) return;
|
||||
await fetchJson(`/api/lotteries/${item.id}`, { method: 'DELETE' });
|
||||
await loadLotteries();
|
||||
}
|
||||
async function loadLotteryParticipants(item: LotteryItem) {
|
||||
lotteryParticipants.value = await fetchJson(`/api/lotteries/${item.id}/participants`);
|
||||
}
|
||||
async function cancelLottery(item: LotteryItem) {
|
||||
if (!window.confirm('确认取消这个抽奖?')) return;
|
||||
await fetchJson(`/api/lotteries/${item.id}/cancel`, { method: 'POST' });
|
||||
@@ -130,7 +161,7 @@ async function drawLottery(item: LotteryItem) {
|
||||
await fetchJson(`/api/lotteries/${item.id}/draw`, { method: 'POST' });
|
||||
await loadLotteries();
|
||||
}
|
||||
function reloadByGroup() { loadChatHistory(); loadBans(); loadStats(); loadShopItems(); loadLotteries(); }
|
||||
function reloadByGroup() { loadChatHistory(); loadBans(); loadStats(); loadShopItems(); loadShopTransactions(); loadLotteries(); }
|
||||
|
||||
function connectSSE() {
|
||||
if (es) es.close();
|
||||
@@ -278,6 +309,9 @@ onBeforeUnmount(() => {
|
||||
<div class="top-actions">
|
||||
<select v-model="selectedChatId" class="form-select" @change="reloadByGroup"><option :value="null">全部群组</option><option v-for="g in groups" :key="g.chat_id" :value="g.chat_id">{{ g.label || g.name }}</option></select>
|
||||
<span class="conn" :class="sseConnected ? 'on' : 'off'">{{ sseConnected ? '实时连接' : '连接中' }}</span>
|
||||
<SButton size="xs" variant="outline" @click="activeModal='shop'; loadShopItems(); loadShopTransactions()">商城</SButton>
|
||||
<SButton size="xs" variant="outline" @click="activeModal='lottery'; loadLotteries()">抽奖</SButton>
|
||||
<SButton size="xs" variant="outline" @click="activeModal='bans'; loadBans()">封禁</SButton>
|
||||
<SButton size="xs" variant="outline" color="primary" :disabled="restartingBot" @click="restartBot">重启Bot</SButton>
|
||||
<SButton size="xs" variant="soft" color="warning" :disabled="restarting" @click="restartService">重启服务</SButton>
|
||||
</div>
|
||||
@@ -340,54 +374,68 @@ onBeforeUnmount(() => {
|
||||
</SCard>
|
||||
</section>
|
||||
|
||||
<section class="manage-grid">
|
||||
<SCard class="panel manage-panel" title="🛒 商品管理" description="按当前群组生效">
|
||||
<div class="shop-form">
|
||||
<SInput v-model="shopForm.item_key" placeholder="编号,如 1" />
|
||||
<SInput v-model="shopForm.name" placeholder="商品名称" />
|
||||
<SInput v-model="shopForm.cost" placeholder="积分价格" />
|
||||
<SInput v-model="shopForm.desc" placeholder="说明" />
|
||||
<label class="check"><input v-model="shopForm.enabled" type="checkbox" /> 启用</label>
|
||||
<SButton size="xs" color="primary" @click="saveShopItem">保存商品</SButton>
|
||||
<div v-if="activeModal" class="modal-mask" @click.self="activeModal=null">
|
||||
<div class="modal-card">
|
||||
<div class="modal-head">
|
||||
<h2>{{ activeModal === 'shop' ? '🛒 商城管理' : activeModal === 'lottery' ? '🎁 抽奖管理' : '🚫 封禁记录' }}</h2>
|
||||
<SButton size="xs" variant="outline" @click="activeModal=null">关闭</SButton>
|
||||
</div>
|
||||
<div class="mini-list">
|
||||
<div v-for="item in shopItems" :key="item.item_key" class="mini-row">
|
||||
<span><b>{{ item.item_key }}</b> {{ item.name }} — {{ item.cost }} 分 <em>{{ item.enabled ? '启用' : '停用' }}</em></span>
|
||||
<SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton>
|
||||
|
||||
<div v-if="activeModal === 'shop'" class="modal-body">
|
||||
<div class="shop-form">
|
||||
<SInput v-model="shopForm.item_key" placeholder="编号,如 1" />
|
||||
<SInput v-model="shopForm.name" placeholder="商品名称" />
|
||||
<SInput v-model="shopForm.cost" placeholder="积分价格" />
|
||||
<SInput v-model="shopForm.desc" placeholder="说明" />
|
||||
<label class="check"><input v-model="shopForm.enabled" type="checkbox" /> 启用</label>
|
||||
<SButton size="xs" color="primary" @click="saveShopItem">保存</SButton>
|
||||
</div>
|
||||
<h3>商品列表</h3>
|
||||
<div class="mini-list">
|
||||
<div v-for="item in shopItems" :key="item.item_key" class="mini-row">
|
||||
<span><b>{{ item.item_key }}</b> {{ item.name }} — {{ item.cost }} 分 <em>{{ item.enabled ? '启用' : '停用' }}</em></span>
|
||||
<div class="row-actions"><SButton size="xs" variant="outline" @click="editShopItem(item)">编辑</SButton><SButton size="xs" variant="soft" color="danger" @click="deleteShopItem(item)">删除</SButton></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>交易记录</h3>
|
||||
<div class="mini-list"><div v-for="tx in shopTransactions" :key="tx.id" class="mini-row"><span>@{{ tx.username || tx.user_id }} 兑换 {{ tx.item_name }},消耗 {{ tx.cost }} 分</span><em>{{ formatTime(tx.created_at) }}</em></div></div>
|
||||
</div>
|
||||
</SCard>
|
||||
<SCard class="panel manage-panel" title="🎁 抽奖管理" description="查看、取消、手动开奖">
|
||||
<div class="mini-list">
|
||||
<div v-if="!lotteries.length" class="empty">暂无抽奖</div>
|
||||
<div v-for="item in lotteries" :key="item.id" class="mini-row">
|
||||
<span>#{{ item.id }} {{ item.prize }}|{{ item.status }}|{{ formatTime(item.created_at) }}</span>
|
||||
<div class="row-actions">
|
||||
<SButton size="xs" variant="outline" :disabled="item.status !== 'active'" @click="drawLottery(item)">开奖</SButton>
|
||||
<SButton size="xs" variant="soft" color="warning" :disabled="item.status !== 'active'" @click="cancelLottery(item)">取消</SButton>
|
||||
|
||||
<div v-else-if="activeModal === 'lottery'" class="modal-body">
|
||||
<div class="lottery-form">
|
||||
<SInput v-model="lotteryForm.title" placeholder="抽奖标题" />
|
||||
<SInput v-model="lotteryForm.prize" placeholder="奖品" />
|
||||
<SInput v-model="lotteryForm.description" placeholder="描述/开奖条件说明" />
|
||||
<select v-model="lotteryForm.condition_type" class="form-select"><option value="all">所有人</option><option value="checkin">今日签到</option><option value="min_points">积分门槛</option></select>
|
||||
<SInput v-model="lotteryForm.condition_value" placeholder="条件值" />
|
||||
<select v-model="lotteryForm.draw_type" class="form-select"><option value="instant">即开即中</option><option value="people">人满开奖</option><option value="time">指定时间/延时开奖</option><option value="manual">手动开奖</option></select>
|
||||
<SInput v-model="lotteryForm.end_value" placeholder="人数或分钟数" />
|
||||
<SInput v-model="lotteryForm.draw_at" placeholder="指定时间,如 2026-07-07 20:00" />
|
||||
<select v-model="lotteryForm.status" class="form-select"><option value="draft">草稿</option><option value="active">进行中</option><option value="cancelled">取消</option></select>
|
||||
<SButton size="xs" color="primary" @click="saveLottery">保存抽奖</SButton>
|
||||
<SButton size="xs" variant="outline" @click="resetLotteryForm">清空</SButton>
|
||||
</div>
|
||||
<div class="mini-list">
|
||||
<div v-for="item in lotteries" :key="item.id" class="mini-row">
|
||||
<span>#{{ item.id }} {{ item.title || item.prize }}|{{ item.draw_type || item.end_type }}|{{ item.status }}|{{ formatTime(item.created_at) }}</span>
|
||||
<div class="row-actions"><SButton size="xs" variant="outline" @click="editLottery(item); loadLotteryParticipants(item)">编辑/参与</SButton><SButton size="xs" variant="outline" :disabled="item.status !== 'active'" @click="drawLottery(item)">开奖</SButton><SButton size="xs" variant="soft" color="warning" :disabled="item.status !== 'active'" @click="cancelLottery(item)">取消</SButton><SButton size="xs" variant="soft" color="danger" @click="deleteLottery(item)">删除</SButton></div>
|
||||
</div>
|
||||
</div>
|
||||
<h3>参与记录</h3>
|
||||
<div class="mini-list"><div v-for="p in lotteryParticipants" :key="p.id" class="mini-row"><span>@{{ p.username || p.first_name || p.user_id }}</span><em>{{ formatTime(p.created_at) }}</em></div></div>
|
||||
</div>
|
||||
|
||||
<div v-else class="modal-body">
|
||||
<div class="ban-list">
|
||||
<div v-if="!bans.length" class="empty">暂无封禁记录</div>
|
||||
<div v-for="(item, i) in bans" :key="item.id || i" class="ban-item">
|
||||
<div class="ban-main"><b>@{{ item.username || item.first_name || item.user_id }}</b><span>{{ item.reason || '-' }}</span></div>
|
||||
<div class="ban-meta"><span>{{ formatTime(item.time) }}</span><SButton v-if="item.auto_unban" size="xs" variant="outline" color="success" :disabled="item._loading" @click="unban(item)">解封</SButton><STag v-else size="xs" color="secondary" variant="soft">已处理</STag></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SCard>
|
||||
</section>
|
||||
|
||||
<SCard class="ban-panel" title="🚫 封禁记录" :description="`${bans.length} 条`">
|
||||
<template #extra><SButton size="xs" variant="outline" @click="loadBans">刷新</SButton></template>
|
||||
<div class="ban-list">
|
||||
<div v-if="!bans.length" class="empty">暂无封禁记录</div>
|
||||
<div v-for="(item, i) in bans" :key="item.id || i" class="ban-item">
|
||||
<div class="ban-main">
|
||||
<b>@{{ item.username || item.first_name || item.user_id }}</b>
|
||||
<span>{{ item.reason || '-' }}</span>
|
||||
</div>
|
||||
<div class="ban-meta">
|
||||
<span>{{ formatTime(item.time) }}</span>
|
||||
<SButton v-if="item.auto_unban" size="xs" variant="outline" color="success" :disabled="item._loading" @click="unban(item)">解封</SButton>
|
||||
<STag v-else size="xs" color="secondary" variant="soft">已处理</STag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SCard>
|
||||
</div>
|
||||
|
||||
<form class="send-box" @submit.prevent="sendMessage">
|
||||
<SInput v-model="newMessage" placeholder="发送消息到 TG 群" />
|
||||
|
||||
@@ -85,3 +85,16 @@ button { font-family: inherit; }
|
||||
.shop-form { grid-template-columns: 1fr; }
|
||||
.mini-row { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
|
||||
.modal-mask { position: fixed; inset: 0; z-index: 50; display: flex; align-items: center; justify-content: center; padding: 18px; background: rgba(15,23,42,.48); backdrop-filter: blur(8px); }
|
||||
.modal-card { width: min(1080px, 100%); max-height: calc(100vh - 36px); overflow: hidden; border-radius: 20px; background: rgba(255,255,255,.98); box-shadow: 0 28px 80px rgba(15,23,42,.32); border: 1px solid rgba(226,232,240,.9); }
|
||||
.modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid #e2e8f0; }
|
||||
.modal-head h2 { margin: 0; font-size: 18px; }
|
||||
.modal-body { max-height: calc(100vh - 112px); overflow: auto; padding: 14px 16px 18px; }
|
||||
.modal-body h3 { margin: 14px 0 8px; font-size: 14px; color: #334155; }
|
||||
.lottery-form { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; align-items: center; margin-bottom: 10px; }
|
||||
@media (max-width: 768px) {
|
||||
.modal-mask { align-items: stretch; padding: 8px; }
|
||||
.modal-card { max-height: calc(100vh - 16px); border-radius: 16px; }
|
||||
.lottery-form { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user