feat: add ban duration modal

This commit is contained in:
ngfchl
2026-07-07 23:45:23 +08:00
parent ff28a35a47
commit bbb33ef341
6 changed files with 422 additions and 355 deletions
+74 -23
View File
@@ -47,6 +47,15 @@ const activeTab = reactive({ shop: 'items', lottery: 'events' });
const toast = ref('');
const confirmState = reactive({ open: false, title: '', message: '', confirmText: '确认', danger: false, resolver: null as null | ((value: boolean) => void) });
const chatContextMenu = reactive({ open: false, x: 0, y: 0, item: null as ChatItem | null });
const banDialog = reactive({ open: false, mode: 'ban' as 'ban' | 'delete_ban', item: null as ChatItem | null, duration: 0, customMinutes: '' });
const banDurationOptions = [
{ label: '永久', value: 0 },
{ label: '1 小时', value: 60 },
{ label: '6 小时', value: 360 },
{ label: '1 天', value: 1440 },
{ label: '7 天', value: 10080 },
{ label: '30 天', value: 43200 },
];
const authReady = ref(false);
const authenticated = ref(false);
const loginForm = reactive({ username: '', password: '' });
@@ -645,38 +654,55 @@ async function contextDeleteMessage(ban = false) {
closeChatContextMenu();
if (item) await deleteChatMessage(item, ban);
}
async function pickBanDuration() {
const text = window.prompt('选择封禁时间:0=永久,60=1小时,360=6小时,1440=1天,10080=7天', '0');
if (text === null) return null;
const value = Number(String(text).trim());
if (!Number.isFinite(value) || value < 0) {
showToast('封禁时间无效,请输入 0 或正整数分钟');
return null;
}
return Math.floor(value);
}
function banDurationText(minutes: number) {
if (!minutes) return '永久';
if (minutes % 1440 === 0) return `${minutes / 1440}`;
if (minutes % 60 === 0) return `${minutes / 60} 小时`;
return `${minutes} 分钟`;
}
function openBanDialog(item: ChatItem, mode: 'ban' | 'delete_ban') {
if (!item.user_id) return showToast('当前记录没有用户 ID,无法封禁');
banDialog.item = item;
banDialog.mode = mode;
banDialog.duration = 0;
banDialog.customMinutes = '';
banDialog.open = true;
}
function closeBanDialog() {
banDialog.open = false;
banDialog.item = null;
banDialog.customMinutes = '';
}
function selectBanDuration(minutes: number) {
banDialog.duration = minutes;
banDialog.customMinutes = '';
}
function applyCustomBanDuration() {
const raw = String(banDialog.customMinutes || '').trim();
if (raw === '') return;
const value = Number(raw);
if (!Number.isFinite(value) || value < 0) return showToast('封禁时间无效,请输入 0 或正整数分钟');
banDialog.duration = Math.floor(value);
}
async function confirmBanDialog() {
const item = banDialog.item;
if (!item) return closeBanDialog();
applyCustomBanDuration();
const duration = banDialog.duration;
const mode = banDialog.mode;
closeBanDialog();
if (mode === 'delete_ban' && !item.deleted) await deleteChatMessage(item, true, duration, true);
else await banChatMessage(item, duration, true);
}
async function contextDeleteAndBanMessage() {
const item = chatContextMenu.item;
closeChatContextMenu();
if (!item) return;
const duration = await pickBanDuration();
if (duration === null) return;
if (item.deleted) await banChatMessage(item, duration);
else await deleteChatMessage(item, true, duration);
if (item) openBanDialog(item, 'delete_ban');
}
async function contextBanMessage() {
const item = chatContextMenu.item;
closeChatContextMenu();
if (!item) return;
const duration = await pickBanDuration();
if (duration === null) return;
await banChatMessage(item, duration);
if (item) openBanDialog(item, 'ban');
}
async function contextCopyText() {
const item = chatContextMenu.item;
@@ -699,9 +725,9 @@ function contextReplyMessage() {
}
function cancelReply() { replyTarget.value = null; }
async function banChatMessage(item: ChatItem, durationMinutes = 0) {
async function banChatMessage(item: ChatItem, durationMinutes = 0, confirmed = false) {
if (!item.user_id) return showToast('当前记录没有用户 ID,无法封禁');
if (!await askConfirm('封禁用户', `确认封禁 @${item.username || item.first_name || item.user_id}?时长:${banDurationText(durationMinutes)}`, { confirmText: '封禁', danger: true })) return;
if (!confirmed && !await askConfirm('封禁用户', `确认封禁 @${item.username || item.first_name || item.user_id}?时长:${banDurationText(durationMinutes)}`, { confirmText: '封禁', danger: true })) return;
item._deleting = true;
try {
await fetchJson(`/api/messages/${item.id}/ban`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ duration_minutes: durationMinutes }) });
@@ -731,11 +757,11 @@ async function addChatMessageToRules(item: ChatItem) {
}
}
async function deleteChatMessage(item: ChatItem, ban = false, durationMinutes = 0) {
async function deleteChatMessage(item: ChatItem, ban = false, durationMinutes = 0, confirmed = false) {
if (item.manually_deleted || item.deleted) return showToast('该消息已删除');
const action = ban ? '删除并封禁' : '删除';
const message = ban ? `确认删除这条消息并封禁用户?时长:${banDurationText(durationMinutes)}` : '确认删除这条消息?';
if (!await askConfirm(action, message, { confirmText: action, danger: true })) return;
if (!confirmed && !await askConfirm(action, message, { confirmText: action, danger: true })) return;
item._deleting = true;
try {
await fetchJson(`/api/messages/${item.id}/delete`, {
@@ -1080,6 +1106,31 @@ onBeforeUnmount(() => {
<SButton type="submit" color="primary" :loading="sending" :disabled="sending || !newMessage.trim()">发送</SButton>
</form>
<div v-if="banDialog.open" class="confirm-mask" @click.self="closeBanDialog">
<div class="confirm-card ban-dialog">
<div class="dialog-head">
<div>
<h3>{{ banDialog.mode === 'delete_ban' ? '删除并封禁' : '封禁用户' }}</h3>
<p>@{{ banDialog.item?.username || banDialog.item?.first_name || banDialog.item?.user_id }} · 选择封禁时长</p>
</div>
<button class="dialog-close" type="button" @click="closeBanDialog">×</button>
</div>
<div class="ban-duration-grid">
<button v-for="opt in banDurationOptions" :key="opt.value" type="button" :class="{ active: banDialog.duration === opt.value && !banDialog.customMinutes }" @click="selectBanDuration(opt.value)">{{ opt.label }}</button>
</div>
<label class="custom-duration">
<span>自定义分钟</span>
<input v-model="banDialog.customMinutes" class="form-input" placeholder="例如 300 为永久" @input="applyCustomBanDuration" />
</label>
<div class="ban-dialog-summary">将执行<b>{{ banDialog.mode === 'delete_ban' && !banDialog.item?.deleted ? '删除消息并封禁' : '封禁用户' }}</b> · 时长<b>{{ banDurationText(banDialog.duration) }}</b></div>
<div class="confirm-actions">
<SButton size="xs" variant="outline" @click="closeBanDialog">取消</SButton>
<SButton size="xs" color="destructive" @click="confirmBanDialog">确认执行</SButton>
</div>
</div>
</div>
<div v-if="shopDialogOpen" class="confirm-mask" @click.self="shopDialogOpen=false">
<div class="confirm-card editor-dialog"><h3>{{ shopForm.item_key ? '编辑商品' : '新增商品' }}</h3><p>库存填 -1 表示不限库存0 表示售罄</p><label>商品编号<SInput v-model="shopForm.item_key" placeholder="如 1 / vip_card" /></label><label>商品名称<SInput v-model="shopForm.name" placeholder="补签卡" /></label><label>积分价格<SInput v-model="shopForm.cost" type="number" placeholder="50" /></label><label>库存<SInput v-model="shopForm.stock" type="number" placeholder="-1 不限库存" /></label><label>说明<SInput v-model="shopForm.desc" placeholder="展示在 /shop 内" /></label><label class="switch-line"><input v-model="shopForm.enabled" type="checkbox" /> 上架销售</label><div class="confirm-actions"><SButton size="xs" variant="outline" :disabled="shopSaving" @click="shopDialogOpen=false">取消</SButton><SButtonLoading size="xs" color="primary" :loading="shopSaving" :disabled="shopSaving" @click="saveShopItem">保存</SButtonLoading></div></div>
</div>
+16
View File
@@ -643,3 +643,19 @@ body { background: #f3f6fb; }
.scope-chip { height: 36px; display: grid; align-content: center; gap: 1px; padding: 4px 10px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f8fafc; color: #64748b; font-size: 11px; }
.scope-chip strong { color: #0f172a; font-size: 12px; line-height: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Ban duration modal */
.ban-dialog { width: min(460px, calc(100vw - 28px)); }
.dialog-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.dialog-head h3 { margin: 0; font-size: 18px; color: #0f172a; }
.dialog-head p { margin: 4px 0 0; color: #64748b; font-size: 13px; }
.dialog-close { width: 28px; height: 28px; border: 0; border-radius: 10px; background: #f1f5f9; color: #64748b; cursor: pointer; font-size: 18px; line-height: 1; }
.ban-duration-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 10px 0; }
.ban-duration-grid button { height: 36px; border: 1px solid #dbe3ef; border-radius: 12px; background: #fff; color: #334155; font-weight: 800; cursor: pointer; }
.ban-duration-grid button.active { border-color: #ef4444; background: #fff1f2; color: #b91c1c; box-shadow: inset 0 0 0 1px rgba(239,68,68,.22); }
.custom-duration { display: grid; gap: 6px; margin-top: 10px; font-size: 12px; font-weight: 800; color: #334155; }
.custom-duration .form-input { height: 38px; }
.ban-dialog-summary { margin-top: 12px; padding: 10px 12px; border-radius: 12px; background: #fff7ed; color: #9a3412; font-size: 13px; }
.ban-dialog-summary b { color: #7c2d12; }
@media (max-width: 520px) { .ban-duration-grid { grid-template-columns: repeat(2, 1fr); } }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
<title>TG Spam Guard</title>
<script type="module" crossorigin src="/static/assets/index-BElvzQ0F.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-DK_IvB6m.css">
<script type="module" crossorigin src="/static/assets/index-gkDPm5Bg.js"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-CRDYB55K.css">
</head>
<body>
<div id="app"></div>