Files
squad-rain-ops-mini/wwwroot/cdk-manager.html

447 lines
21 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CDK 管理</title>
<link rel="stylesheet" href="/lib/css/bootstrap.min.css">
<link rel="stylesheet" href="/lib/css/font-awesome.min.css">
<script src="/lib/js/vue.global.prod.js"></script>
<script src="/lib/js/bootstrap.bundle.min.js"></script>
<style>
body { padding: 20px; background-color: #f8f9fa; }
.card { border: none; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.table th { font-weight: 600; background-color: #f8f9fa; }
.status-unused { color: #28a745; font-weight: 500; }
.status-used { color: #6c757d; text-decoration: line-through; }
</style>
</head>
<body>
<div id="app" class="container-fluid">
<div class="card">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h5 class="mb-0"><i class="fa fa-key text-primary me-2"></i>CDK 管理</h5>
<div>
<button class="btn btn-outline-secondary btn-sm me-2" @click="copySelected" :disabled="selected.length === 0">
<i class="fa fa-copy me-1"></i> 复制选中 ({{ selected.length }})
</button>
<button class="btn btn-danger btn-sm me-2" v-if="checkPermission('cdks.delete')" @click="deleteSelected" :disabled="selected.length === 0">
<i class="fa fa-trash me-1"></i> 删除选中 ({{ selected.length }})
</button>
<button class="btn btn-primary btn-sm" v-if="checkPermission('cdks.add')" @click="showGenerateModal">
<i class="fa fa-plus me-1"></i> 生成 CDK
</button>
</div>
</div>
<div class="card-body">
<!-- Filters -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<div class="input-group">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input type="text" class="form-control" v-model="filters.search" @input="debounceSearch" placeholder="搜索 CDK/原因/创建人...">
</div>
</div>
<div class="col-md-2">
<select class="form-select" v-model="filters.type" @change="fetchData(1)">
<option value="">所有类型</option>
<option value="VIP">VIP</option>
<option value="SVIP">SVIP</option>
</select>
</div>
<div class="col-md-2">
<select class="form-select" v-model="filters.status" @change="fetchData(1)">
<option value="">所有状态</option>
<option value="Unused">未使用</option>
<option value="Used">已使用</option>
</select>
</div>
</div>
<!-- Table -->
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th style="width: 40px;">
<input type="checkbox" class="form-check-input" :checked="isAllSelected" @change="toggleAll" :disabled="list.length === 0">
</th>
<th>CDK</th>
<th>类型</th>
<th>时长 (天)</th>
<th>状态</th>
<th>生成原因</th>
<th>创建人</th>
<th>创建时间</th>
<th>兑换人</th>
<th>兑换时间</th>
</tr>
</thead>
<tbody>
<tr v-for="item in list" :key="item.code" :class="{'table-active': selected.includes(item.code)}">
<td>
<input type="checkbox" class="form-check-input" :value="item.code" v-model="selected">
</td>
<td class="font-monospace user-select-all">{{ item.code }}</td>
<td>
<span class="badge" :class="item.type === 'SVIP' ? 'bg-warning text-dark' : 'bg-info'">{{ item.type }}</span>
</td>
<td>{{ item.durationDays }}</td>
<td>
<span :class="item.status === 'Unused' ? 'status-unused' : 'status-used'">
{{ item.status === 'Unused' ? '未使用' : '已使用' }}
</span>
</td>
<td>{{ item.reason }}</td>
<td>{{ item.creator }}</td>
<td>{{ formatDate(item.createdAt) }}</td>
<td>{{ item.redeemedBy || '-' }}</td>
<td>{{ formatDate(item.redeemedAt) }}</td>
</tr>
<tr v-if="list.length === 0">
<td colspan="10" class="text-center py-4 text-muted">暂无数据</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<nav v-if="totalPages > 1" class="mt-3">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: page === 1 }">
<a class="page-link" href="#" @click.prevent="fetchData(page - 1)">上一页</a>
</li>
<li class="page-item disabled"><span class="page-link">{{ page }} / {{ totalPages }}</span></li>
<li class="page-item" :class="{ disabled: page === totalPages }">
<a class="page-link" href="#" @click.prevent="fetchData(page + 1)">下一页</a>
</li>
</ul>
</nav>
</div>
</div>
<!-- Generate Modal -->
<div class="modal fade" id="generateModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">生成 CDK</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form @submit.prevent="generate">
<div class="mb-3">
<label class="form-label">类型</label>
<select class="form-select" v-model="form.type" required>
<option value="VIP">VIP</option>
<option value="SVIP">SVIP</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">时长 (天)</label>
<input type="number" class="form-control" v-model.number="form.durationDays" min="1" required>
</div>
<div class="mb-3">
<label class="form-label">数量 (1-100)</label>
<input type="number" class="form-control" v-model.number="form.count" min="1" max="100" required>
</div>
<div class="mb-3">
<label class="form-label">生成原因</label>
<input type="text" class="form-control" v-model="form.reason" placeholder="例如:活动奖励" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" :disabled="loading">
<span v-if="loading" class="spinner-border spinner-border-sm me-1"></span>
确认生成
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Result Modal -->
<div class="modal fade" id="resultModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">生成成功</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-success">已成功生成 {{ resultKeys.length }} 个 CDK</p>
<textarea class="form-control" rows="10" readonly>{{ resultKeys.join('\n') }}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" @click="copyGenerated">
<i class="fa fa-copy me-1"></i> 一键复制
</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<!-- Copy Modal -->
<div class="modal fade" id="copyModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">复制成功</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-success">已将以下内容复制到剪贴板:</p>
<textarea class="form-control" rows="10" readonly>{{ copyContent }}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, onMounted, computed } = Vue;
// Permission Check
window.checkPermission = function(key) {
const isSuper = localStorage.getItem('isSuperAdmin') === 'true';
if (isSuper) return true;
try {
const perms = JSON.parse(localStorage.getItem('permissions') || '[]');
return perms.includes('all') || perms.includes(key);
} catch { return false; }
};
(function() {
const required = 'cdks.view';
if (!checkPermission(required)) {
document.body.innerHTML = '<div style="display:flex;justify-content:center;align-items:center;height:100vh;color:red;font-size:1.5rem;">Access Denied: Permission "cdks.view" required.</div>';
throw new Error("Access Denied");
}
})();
createApp({
setup() {
const list = ref([]);
const page = ref(1);
const pageSize = 20;
const total = ref(0);
const totalPages = ref(1);
const loading = ref(false);
const resultKeys = ref([]);
const selected = ref([]);
const filters = reactive({
type: '',
status: '',
search: ''
});
const form = reactive({
type: 'VIP',
durationDays: 30,
count: 1,
reason: ''
});
let generateModal = null;
let resultModal = null;
let searchTimeout = null;
const isAllSelected = computed(() => {
return list.value.length > 0 && list.value.every(item => selected.value.includes(item.code));
});
onMounted(() => {
generateModal = new bootstrap.Modal(document.getElementById('generateModal'));
resultModal = new bootstrap.Modal(document.getElementById('resultModal'));
fetchData();
});
const fetchData = async (p = 1) => {
page.value = p;
// Clear selection on page change/refresh to avoid confusion
selected.value = [];
try {
let url = `/api/cdk/list?page=${page.value}&pageSize=${pageSize}`;
if (filters.type) url += `&type=${filters.type}`;
if (filters.status) url += `&status=${filters.status}`;
if (filters.search) url += `&search=${encodeURIComponent(filters.search)}`;
const res = await fetch(url);
if (res.status === 401) {
alert('无权访问');
window.location.href = '/login.html';
return;
}
if (res.ok) {
const data = await res.json();
list.value = data.list;
total.value = data.total;
totalPages.value = Math.ceil(data.total / pageSize);
}
} catch (error) {
console.error(error);
}
};
const debounceSearch = () => {
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
fetchData(1);
}, 500);
};
const toggleAll = () => {
if (isAllSelected.value) {
selected.value = [];
} else {
selected.value = list.value.map(item => item.code);
}
};
const copySelected = async () => {
if (selected.value.length === 0) return;
const codes = selected.value.join('\n');
const url = window.location.origin + '/cdk-redeem.html';
const text = `[CDK列表]
${codes}
激活地址:${url}
注意事项:
1. 每个批次的激活码同一个账号仅限激活一次。
2. 可在游戏内聊天直接发送 CDK 进行激活。`;
try {
await navigator.clipboard.writeText(text);
// Show modal with the copied content for verification
resultKeys.value = text.split('\n'); // Hack to reuse result modal logic partially or just alert
// Let's create a custom alert or reuse result modal with a twist
// But simply alerting is fine as requested "并弹窗显示"
// I'll show it in a modal instead of alert for better UX
showCopyModal(text);
} catch (err) {
alert('复制失败,请手动复制');
}
};
const deleteSelected = async () => {
if (selected.value.length === 0) return;
if (!confirm(`确定要删除选中的 ${selected.value.length} 个 CDK 吗?此操作不可恢复。`)) return;
try {
const res = await fetch('/api/cdk/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ codes: selected.value })
});
if (res.ok) {
alert('删除成功');
selected.value = []; // Clear selection
fetchData(page.value); // Refresh current page
} else {
const err = await res.json();
alert('删除失败: ' + (err.error || 'Unknown error'));
}
} catch (e) {
alert('请求错误: ' + e.message);
}
};
// Add a new reactive variable for copy modal content
const copyContent = ref('');
let copyModalInstance = null;
onMounted(() => {
generateModal = new bootstrap.Modal(document.getElementById('generateModal'));
resultModal = new bootstrap.Modal(document.getElementById('resultModal'));
copyModalInstance = new bootstrap.Modal(document.getElementById('copyModal'));
fetchData();
});
const showCopyModal = (text) => {
copyContent.value = text;
copyModalInstance.show();
};
const copyGenerated = async () => {
if (resultKeys.value.length === 0) return;
const codes = resultKeys.value.join('\n');
const url = window.location.origin + '/cdk-redeem.html';
const text = `[CDK列表]
${codes}
激活地址:${url}
注意事项:
1. 每个批次的激活码同一个账号仅限激活一次。
2. 可在游戏内聊天直接发送 CDK 进行激活。`;
try {
await navigator.clipboard.writeText(text);
alert('已复制所有新生成的 CDK 及相关信息!');
} catch (err) {
alert('复制失败,请手动复制');
}
};
const showGenerateModal = () => {
form.type = 'VIP';
form.durationDays = 30;
form.count = 1;
form.reason = '';
generateModal.show();
};
const generate = async () => {
loading.value = true;
try {
const res = await fetch('/api/cdk/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(form)
});
const data = await res.json();
if (res.ok) {
generateModal.hide();
resultKeys.value = data.keys;
resultModal.show();
fetchData(1);
} else {
alert(data.error || '生成失败');
}
} catch (error) {
alert('请求失败: ' + error.message);
} finally {
loading.value = false;
}
};
const formatDate = (dateStr) => {
if (!dateStr) return '';
return new Date(dateStr).toLocaleString();
};
return {
list, page, totalPages, filters, form, loading, resultKeys, selected, isAllSelected, copyContent,
fetchData, showGenerateModal, generate, formatDate, debounceSearch, toggleAll, copySelected, copyGenerated, deleteSelected,
checkPermission // Expose to template
};
}
}).mount('#app');
</script>
</body>
</html>