Files
freedom-web/public/delta-gun-codes.html
T
张家训 deefce7856
CI/CD Deploy (Web) / 构建前端镜像并部署 (push) Successful in 46s
feat: 三角洲改枪码管理页与免登录公开复制页
- 新增改枪码管理页面与 API(分页、新增、编辑、删除、复制)
- 新增免登录公开页 public/delta-gun-codes.html
- 新增通用剪贴板工具 src/common/js/clipboard.js
2026-09-10 09:27:31 +08:00

282 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>三角洲行动 · 改枪码速查复制</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: radial-gradient(1200px 600px at 80% -10%, #1d3a1d 0%, transparent 60%),
radial-gradient(900px 500px at -10% 110%, #14303a 0%, transparent 55%),
#0c1117;
color: #dbe4ee;
min-height: 100vh;
padding: 28px 16px 60px;
}
.wrap { max-width: 1200px; margin: 0 auto; }
/* 头部 */
.hero { text-align: center; margin-bottom: 26px; }
.hero .badge {
display: inline-block; font-size: 12px; letter-spacing: 2px; color: #7fd0a0;
border: 1px solid #2c6e4f; border-radius: 999px; padding: 4px 14px; margin-bottom: 14px;
background: rgba(44,110,79,.12);
}
.hero h1 { font-size: 30px; font-weight: 800; letter-spacing: 1px; }
.hero h1 span { color: #f0b04a; }
.hero p { margin-top: 10px; color: #8492a6; font-size: 14px; }
.hero .total { color: #7fd0a0; }
/* 工具栏 */
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; margin-bottom: 22px; }
.toolbar .search input {
width: 260px; max-width: 60vw; background: #131a22; border: 1px solid #27313d;
color: #dbe4ee; padding: 9px 14px; border-radius: 8px; outline: none; font-size: 14px;
}
.toolbar .search input:focus { border-color: #3aa76d; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.chip {
cursor: pointer; user-select: none; font-size: 13px; padding: 6px 14px;
border-radius: 999px; border: 1px solid #27313d; color: #9fb0c0; background: #131a22;
transition: all .15s;
}
.chip.active, .chip:hover { color: #0c1117; background: #7fd0a0; border-color: #7fd0a0; }
/* 卡片 */
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; }
.card {
background: #131a22; border: 1px solid #1f2a35; border-radius: 12px;
padding: 16px 16px 14px; display: flex; flex-direction: column; gap: 10px;
transition: transform .15s, border-color .15s;
}
.card:hover { transform: translateY(-2px); border-color: #2c6e4f; }
.card .top { display: flex; align-items: center; justify-content: space-between; }
.card .tags { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.tag { font-size: 12px; padding: 2px 10px; border-radius: 6px; }
.tag.cat { color: #ffd28a; background: rgba(240,176,74,.12); border: 1px solid rgba(240,176,74,.4); }
.tag.gun { color: #9fd8ff; background: rgba(80,160,240,.1); border: 1px solid rgba(80,160,240,.35); }
.count { font-size: 12px; color: #6a7888; white-space: nowrap; }
.card .name { font-size: 17px; font-weight: 700; }
.card .code {
background: #0b0f14; border: 1px solid #22303d; border-radius: 8px;
padding: 10px 12px; font-family: "SFMono-Regular", Consolas, "Courier New", monospace;
font-size: 13px; line-height: 1.6; color: #a8f0c8; word-break: break-all;
max-height: 140px; overflow: auto; white-space: pre-wrap;
}
.card .remark { font-size: 13px; color: #8aa0b5; }
.card .foot { display: flex; align-items: center; justify-content: space-between; margin-top: auto; }
.copy-btn {
cursor: pointer; border: none; width: 100%; border-radius: 8px;
background: linear-gradient(135deg, #2f8f5e, #1f7a4e); color: #fff;
padding: 10px 0; font-size: 14px; font-weight: 700; letter-spacing: 1px; transition: filter .15s;
}
.copy-btn:hover { filter: brightness(1.12); }
.copy-btn:active { transform: scale(.98); }
/* 空状态 / loading */
.empty, .loading { text-align: center; padding: 60px 0; color: #8492a6; }
.empty svg { width: 46px; height: 46px; margin-bottom: 10px; opacity: .5; }
/* toast */
.toast {
position: fixed; left: 50%; bottom: 42px; transform: translate(-50%, 20px);
background: #18a05e; color: #fff; padding: 11px 22px; border-radius: 8px;
font-size: 14px; opacity: 0; visibility: hidden; transition: all .25s; z-index: 99;
box-shadow: 0 8px 24px rgba(0,0,0,.4);
}
.toast.show { opacity: 1; visibility: visible; transform: translate(-50%, 0); }
.toast.error { background: #d84a4a; }
footer { text-align: center; margin-top: 30px; color: #56616e; font-size: 12px; }
@media (max-width: 720px) {
.hero h1 { font-size: 23px; }
.grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="hero">
<div class="badge">DELTA FORCE · GUN CODE</div>
<h1>三角洲行动 · <span>改枪码速查</span></h1>
<p>已收录 <span class="total" id="totalNum">0</span> 套配置,点击复制即可在游戏内粘贴使用</p>
</div>
<div class="toolbar">
<div class="search">
<input id="searchInput" type="text" placeholder="搜索名称 / 枪械 / 分类...">
</div>
<div class="chips" id="chipBox"></div>
</div>
<div id="container">
<div class="loading" id="loading">加载中...</div>
</div>
<footer>三角洲行动 · 改枪码共享站 &nbsp;|&nbsp; 本页面无需登录即可访问</footer>
</div>
<div class="toast" id="toast"></div>
<script>
// 接口地址:生产走同源 /api(nginx 反代去前缀);本地 8080 开发时直连后端 8800
var apiBase = ''
var apiPrefix = '/api'
if (location.port === '8080' && /^(localhost|127\.0\.0\.1)$/.test(location.hostname)) {
apiBase = 'http://localhost:8800'
apiPrefix = ''
}
var listApi = apiBase + apiPrefix + '/delta-gun-code/public/list'
var countApi = apiBase + apiPrefix + '/delta-gun-code/public/addCopyCount'
var allData = []
var activeCategory = '全部'
var keyword = ''
var container = document.getElementById('container')
var loading = document.getElementById('loading')
var chipBox = document.getElementById('chipBox')
var searchInput = document.getElementById('searchInput')
var toast = document.getElementById('toast')
var totalNum = document.getElementById('totalNum')
function showToast (msg, isError) {
toast.textContent = msg
toast.className = 'toast show' + (isError ? ' error' : '')
clearTimeout(showToast._t)
showToast._t = setTimeout(function () { toast.className = 'toast' }, 2000)
}
function copyText (text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text).then(function () { return true }).catch(function () { return fallbackCopy(text) })
}
return Promise.resolve(fallbackCopy(text))
}
function fallbackCopy (text) {
var ta = document.createElement('textarea')
ta.value = text
ta.setAttribute('readonly', '')
ta.style.position = 'fixed'
ta.style.top = '-9999px'
ta.style.opacity = '0'
document.body.appendChild(ta)
ta.select()
var ok = false
try { ok = document.execCommand('copy') } catch (e) { ok = false }
document.body.removeChild(ta)
return ok
}
function renderChips () {
var set = ['全部']
allData.forEach(function (item) {
if (item.category && set.indexOf(item.category) < 0) set.push(item.category)
})
chipBox.innerHTML = ''
set.forEach(function (c) {
var el = document.createElement('div')
el.className = 'chip' + (c === activeCategory ? ' active' : '')
el.textContent = c
el.onclick = function () {
activeCategory = c
renderChips()
renderCards()
}
chipBox.appendChild(el)
})
}
function filterData () {
var kw = keyword.trim().toLowerCase()
return allData.filter(function (item) {
if (activeCategory !== '全部' && item.category !== activeCategory) return false
if (!kw) return true
return [item.name, item.gunName, item.category, item.code].join(' ').toLowerCase().indexOf(kw) > -1
})
}
function renderCards () {
var list = filterData()
totalNum.textContent = list.length
if (!list.length) {
container.innerHTML =
'<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 6h18M6 6v12M18 6v12M6 18h12M10 9v6M14 9v6"/></svg>' +
'<div>没有找到对应的改枪码,稍后再来看看吧</div></div>'
return
}
var html = '<div class="grid">'
list.forEach(function (item) {
var cat = item.category ? '<span class="tag cat">' + esc(item.category) + '</span>' : ''
var gun = item.gunName ? '<span class="tag gun">' + esc(item.gunName) + '</span>' : ''
var remark = item.remark ? '<div class="remark">' + esc(item.remark) + '</div>' : ''
html +=
'<div class="card">' +
' <div class="top"><div class="tags">' + cat + gun + '</div>' +
' <span class="count">被复制 ' + (item.copyCount || 0) + ' 次</span></div>' +
' <div class="name">' + esc(item.name || '未命名配置') + '</div>' +
' <div class="code">' + esc(item.code || '') + '</div>' +
remark +
' <div class="foot"><button class="copy-btn" data-id="' + item.id + '">复制改枪码</button></div>' +
'</div>'
})
html += '</div>'
container.innerHTML = html
}
function esc (s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
})
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('.copy-btn')
if (!btn) return
var id = Number(btn.getAttribute('data-id'))
var item = allData.find(function (x) { return x.id === id })
if (!item) return
copyText(item.code).then(function (ok) {
if (ok) {
showToast('复制成功,去游戏里粘贴吧!')
// 异步上报复制次数,失败不影响用户
fetch(countApi + '?id=' + id, { method: 'GET' }).catch(function () {})
} else {
showToast('复制失败,请长按改枪码手动复制', true)
}
})
})
searchInput.addEventListener('input', function (e) {
keyword = e.target.value
renderCards()
})
function load () {
fetch(listApi, { method: 'GET' }).then(function (res) {
if (!res.ok) throw new Error('http ' + res.status)
return res.json()
}).then(function (res) {
if (res.code === 200) {
allData = (res.data || []).filter(function (x) { return x.code })
} else {
throw new Error(res.message || '加载失败')
}
renderChips()
renderCards()
}).catch(function (err) {
loading.style.display = 'none'
container.innerHTML =
'<div class="empty"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 8v4M12 16h.01"/></svg>' +
'<div>数据加载失败:' + esc(err.message || err) + '<br><small style="color:#56616e">请确认后端服务可用后刷新重试</small></div></div>'
})
}
load()
</script>
</body>
</html>