feat: upgrade to V1.2 - Tags, Click Stats, and Robust WebDAV
- add Tagging system (backend and frontend) - add Click count statistics and redirection logic - add config.example.py - fix WebDAV MKCOL 405 error and response handling - fix redirection loop during force password change - audit SQL queries for security
This commit is contained in:
@@ -18,6 +18,13 @@
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 强制改密提示 -->
|
||||
{% if session.get('must_change') %}
|
||||
<div class="alert alert-danger" style="margin-bottom: 20px; padding: 15px; background: #fff2f0; border: 1px solid #ffccc7; color: #ff4d4f; border-radius: 10px; font-weight: bold;">
|
||||
⚠️ 为了账户安全,首次登录请先修改默认密码。在修改完成前,其他管理功能将被禁用。
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
@@ -27,6 +34,7 @@
|
||||
<div class="stat-label">总服务数</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not session.get('must_change') %}
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">✅</div>
|
||||
<div class="stat-info">
|
||||
@@ -41,9 +49,11 @@
|
||||
<div class="stat-label">分类数</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
{% if not session.get('must_change') %}
|
||||
<div class="quick-actions">
|
||||
<button class="action-btn" onclick="location.href='/admin/services'">
|
||||
<span class="action-icon">📡</span>
|
||||
@@ -57,7 +67,18 @@
|
||||
<span class="action-icon">🔍</span>
|
||||
<span class="action-label">健康检测</span>
|
||||
</button>
|
||||
|
||||
<!-- 备份操作 -->
|
||||
<button class="action-btn" onclick="showBackupModal()">
|
||||
<span class="action-icon">☁️</span>
|
||||
<span class="action-label">备份设置</span>
|
||||
</button>
|
||||
<button class="action-btn" onclick="backupLocal()">
|
||||
<span class="action-icon">💾</span>
|
||||
<span class="action-label">本地备份</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- 修改密码 -->
|
||||
@@ -73,320 +94,248 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 备份设置弹窗 -->
|
||||
<div class="modal" id="backupModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" style="background: #1a1a1a; color: #fff; padding: 20px; border-radius: 20px 20px 0 0; display: flex; justify-content: space-between; align-items: center;">
|
||||
<h2>☁️ 云端备份设置 (WebDAV)</h2>
|
||||
<button class="close-btn" onclick="closeBackupModal()" style="background:none; border:none; color:#fff; font-size:24px; cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 25px;">
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="display:block; margin-bottom:5px; font-size:14px;">WebDAV URL *</label>
|
||||
<input type="url" id="webdavUrl" class="input" placeholder="https://example.com/webdav/tonav/" required>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 15px;">
|
||||
<label style="display:block; margin-bottom:5px; font-size:14px;">用户名</label>
|
||||
<input type="text" id="webdavUser" class="input" placeholder="username">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 20px;">
|
||||
<label style="display:block; margin-bottom:5px; font-size:14px;">密码 / Token</label>
|
||||
<input type="password" id="webdavPass" class="input" placeholder="password">
|
||||
</div>
|
||||
<div class="form-actions" style="display: flex; gap: 10px; border-top: 1px solid #eee; pt: 15px; margin-bottom: 20px;">
|
||||
<button class="btn" onclick="testWebDAV()" id="btnTest">连通性测试</button>
|
||||
<button class="btn btn-primary" onclick="saveBackupSettings()">保存配置</button>
|
||||
<button class="btn" style="background: #52c41a; color: #fff;" onclick="runCloudBackup()">立即备份</button>
|
||||
</div>
|
||||
|
||||
<div class="cloud-backups">
|
||||
<h3 style="font-size: 14px; margin-bottom: 10px; color: #666;">📜 云端历史备份</h3>
|
||||
<div id="backupFileList" style="max-height: 200px; overflow-y: auto; border: 1px solid #f0f0f0; border-radius: 8px;">
|
||||
<div style="padding: 15px; text-align: center; color: #999; font-size: 13px;">正在获取列表...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.admin-layout {
|
||||
max-width: 900px;
|
||||
}
|
||||
.admin-layout { max-width: 900px; }
|
||||
.admin-header { background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%); color: #fff; padding: 25px 30px; border-radius: 20px 20px 0 0; display: flex; justify-content: space-between; align-items: center; }
|
||||
.header-left h1 { font-size: 22px; font-weight: 700; margin-bottom: 5px; }
|
||||
.username { font-size: 13px; color: #8c8c8c; }
|
||||
.main-content { padding: 30px; background: #fff; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 20px; }
|
||||
.stat-card { display: flex; align-items: center; gap: 15px; padding: 20px; background: #fafafa; border-radius: 15px; transition: all 0.3s; height: 100px; }
|
||||
.stat-card:hover { background: #f0f0f0; transform: translateY(-2px); }
|
||||
.stat-icon { font-size: 36px; }
|
||||
.stat-value { font-size: 32px; font-weight: 800; color: var(--main-red); line-height: 1; }
|
||||
.stat-label { font-size: 13px; color: #8c8c8c; margin-top: 5px; }
|
||||
.quick-actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; }
|
||||
.action-btn { background: #fff; border: 2px solid #f0f0f0; border-radius: 15px; padding: 20px; cursor: pointer; transition: all 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; height: 120px; }
|
||||
.action-btn:hover { border-color: var(--main-red); background: #fff2f0; transform: translateY(-3px); box-shadow: 0 4px 15px rgba(255, 77, 79, 0.2); }
|
||||
.action-icon { font-size: 32px; }
|
||||
.action-label { font-size: 14px; font-weight: 600; color: #262626; }
|
||||
.settings-card { background: #fff; padding: 30px; border-radius: 0 0 20px 20px; }
|
||||
.settings-card h2 { font-size: 18px; font-weight: 600; margin-bottom: 20px; color: #262626; }
|
||||
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }
|
||||
.input { width: 100%; padding: 12px 15px; border: 1px solid #d9d9d9; border-radius: 10px; font-size: 14px; transition: all 0.3s; }
|
||||
.input:focus { outline: none; border-color: var(--main-red); box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.1); }
|
||||
.header-actions { display: flex; gap: 10px; align-items: center; }
|
||||
.btn-outline { background: transparent; border: 1px solid rgba(255,255,255,0.3); color: #fff; }
|
||||
.btn-outline:hover { background: rgba(255,255,255,0.1); border-color: #fff; }
|
||||
.btn { padding: 12px 24px; border: none; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s; }
|
||||
.btn-primary { background: var(--main-red); color: #fff; box-shadow: 0 4px 15px rgba(255, 77, 79, 0.4); }
|
||||
|
||||
/* Modal */
|
||||
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 1000; align-items: center; justify-content: center; }
|
||||
.modal.active { display: flex; }
|
||||
.modal-content { background: #fff; border-radius: 20px; width: 90%; max-width: 500px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); }
|
||||
|
||||
.admin-header {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
color: #fff;
|
||||
padding: 25px 30px;
|
||||
border-radius: 20px 20px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
border-radius: 15px;
|
||||
transition: all 0.3s;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
background: #f0f0f0;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--main-red);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #8c8c8c;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: #fff;
|
||||
border: 2px solid #f0f0f0;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: var(--main-red);
|
||||
background: #fff2f0;
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.2);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
background: #fff;
|
||||
padding: 30px;
|
||||
border-radius: 0 0 20px 20px;
|
||||
}
|
||||
|
||||
.settings-card h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--main-red);
|
||||
box-shadow: 0 0 0 3px rgba(255, 77, 79, 0.1);
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--main-red);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 15px rgba(255, 77, 79, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid,
|
||||
.quick-actions,
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stat-card,
|
||||
.action-btn {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) { .stats-grid, .quick-actions, .form-row { grid-template-columns: 1fr; } .admin-header { flex-direction: column; align-items: flex-start; gap: 15px; } .stat-card, .action-btn { height: auto; } }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadStats();
|
||||
loadUsername();
|
||||
});
|
||||
|
||||
// 加载用户名
|
||||
async function loadUsername() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/login/status');
|
||||
const response = await fetch('/api/admin/login/status?t=' + new Date().getTime());
|
||||
const data = await response.json();
|
||||
if (data.logged_in) {
|
||||
document.getElementById('username').textContent = data.username;
|
||||
} else {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
} catch (err) {
|
||||
window.location.href = '/admin/login';
|
||||
}
|
||||
if (!data.must_change) loadStats();
|
||||
} else { window.location.href = '/admin/login'; }
|
||||
} catch (err) { window.location.href = '/admin/login'; }
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
async function loadStats() {
|
||||
try {
|
||||
const [servicesResp, categoriesResp] = await Promise.all([
|
||||
fetch('/api/admin/services'),
|
||||
fetch('/api/admin/categories')
|
||||
fetch('/api/admin/services?t=' + new Date().getTime()),
|
||||
fetch('/api/admin/categories?t=' + new Date().getTime())
|
||||
]);
|
||||
|
||||
const services = await servicesResp.json();
|
||||
const categories = await categoriesResp.json();
|
||||
|
||||
document.getElementById('totalServices').textContent = services.length;
|
||||
document.getElementById('enabledServices').textContent =
|
||||
services.filter(s => s.is_enabled).length;
|
||||
document.getElementById('totalCategories').textContent = categories.length;
|
||||
} catch (err) {
|
||||
console.error('加载统计失败:', err);
|
||||
}
|
||||
if (document.getElementById('enabledServices')) {
|
||||
document.getElementById('enabledServices').textContent = services.filter(s => s.is_enabled).length;
|
||||
document.getElementById('totalCategories').textContent = categories.length;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
// 健康检测
|
||||
async function runHealthCheck() {
|
||||
const btn = event.target.closest('.action-btn');
|
||||
const icon = btn.querySelector('.action-icon');
|
||||
const label = btn.querySelector('.action-label');
|
||||
|
||||
try {
|
||||
icon.textContent = '⏳';
|
||||
label.textContent = '检测中...';
|
||||
|
||||
const response = await fetch('/api/admin/health-check', {
|
||||
method: 'POST'
|
||||
});
|
||||
const response = await fetch('/api/admin/health-check', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
||||
if (data.results) {
|
||||
const online = data.results.filter(r => r.status === 'online').length;
|
||||
const offline = data.results.filter(r => r.status === 'offline').length;
|
||||
alert(`检测完成:\n在线: ${online}\n离线: ${offline}`);
|
||||
}
|
||||
} catch (err) { alert('检测失败: ' + err.message); }
|
||||
}
|
||||
|
||||
// 备份相关逻辑
|
||||
function backupLocal() { window.location.href = '/api/admin/backup/local'; }
|
||||
|
||||
async function showBackupModal() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings');
|
||||
const data = await response.json();
|
||||
document.getElementById('webdavUrl').value = data.webdav_url || '';
|
||||
document.getElementById('webdavUser').value = data.webdav_user || '';
|
||||
document.getElementById('webdavPass').value = data.webdav_password || '';
|
||||
document.getElementById('backupModal').classList.add('active');
|
||||
loadBackupList();
|
||||
} catch (err) { alert('加载配置失败'); }
|
||||
}
|
||||
|
||||
async function loadBackupList() {
|
||||
const listDiv = document.getElementById('backupFileList');
|
||||
try {
|
||||
const response = await fetch('/api/admin/backup/list');
|
||||
const data = await response.json();
|
||||
if (data.files && data.files.length > 0) {
|
||||
let html = '';
|
||||
data.files.forEach(file => {
|
||||
html += `
|
||||
<div style="padding: 10px 15px; border-bottom: 1px solid #f9f9f9; display: flex; justify-content: space-between; align-items: center;">
|
||||
<span style="font-size: 13px; font-family: monospace;">${file}</span>
|
||||
<button onclick="restoreFromCloud('${file}')" style="background: #faad14; color: #fff; border: none; border-radius: 4px; padding: 4px 10px; font-size: 12px; cursor: pointer;">恢复</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
listDiv.innerHTML = html;
|
||||
} else {
|
||||
listDiv.innerHTML = '<div style="padding: 15px; text-align: center; color: #999; font-size: 13px;">云端暂无备份包</div>';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('检测失败: ' + err.message);
|
||||
} finally {
|
||||
icon.textContent = '🔍';
|
||||
label.textContent = '健康检测';
|
||||
listDiv.innerHTML = '<div style="padding: 15px; text-align: center; color: #ff4d4f; font-size: 13px;">获取列表失败</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreFromCloud(filename) {
|
||||
if (!confirm(`⚠️ 警告:确定要从备份 ${filename} 恢复吗?当前所有数据将被覆盖!系统将自动备份当前版本为 tonav.db.bak`)) return;
|
||||
try {
|
||||
const response = await fetch('/api/admin/backup/restore', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({filename: filename})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) { alert(data.message); location.reload(); }
|
||||
else { alert('恢复失败: ' + data.error); }
|
||||
} catch (err) { alert('请求异常'); }
|
||||
}
|
||||
|
||||
function closeBackupModal() { document.getElementById('backupModal').classList.remove('active'); }
|
||||
|
||||
async function saveBackupSettings() {
|
||||
const data = {
|
||||
webdav_url: document.getElementById('webdavUrl').value,
|
||||
webdav_user: document.getElementById('webdavUser').value,
|
||||
webdav_password: document.getElementById('webdavPass').value
|
||||
};
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
if (response.ok) alert('配置保存成功');
|
||||
else alert('保存失败');
|
||||
} catch (err) { alert('请求错误'); }
|
||||
}
|
||||
|
||||
async function testWebDAV() {
|
||||
const data = {
|
||||
webdav_url: document.getElementById('webdavUrl').value,
|
||||
webdav_user: document.getElementById('webdavUser').value,
|
||||
webdav_password: document.getElementById('webdavPass').value
|
||||
};
|
||||
const btn = document.getElementById('btnTest');
|
||||
btn.disabled = true; btn.textContent = '测试中...';
|
||||
try {
|
||||
const response = await fetch('/api/admin/backup/test', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const res = await response.json();
|
||||
if (response.ok) alert(res.message);
|
||||
else alert(res.error);
|
||||
} catch (err) { alert('测试异常'); }
|
||||
finally { btn.disabled = false; btn.textContent = '连通性测试'; }
|
||||
}
|
||||
|
||||
async function runCloudBackup() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/backup/webdav', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
alert(data.message || '云端备份成功!');
|
||||
loadBackupList();
|
||||
} else {
|
||||
alert('备份失败: ' + (data.error || '未知错误'));
|
||||
}
|
||||
} catch (err) {
|
||||
alert('请求失败: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
document.getElementById('changePasswordForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/change-password', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({old_password: oldPassword, new_password: newPassword})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
alert('密码修改成功,请重新登录');
|
||||
this.reset();
|
||||
} else {
|
||||
alert(data.error || '修改失败');
|
||||
}
|
||||
} catch (err) {
|
||||
alert('请求失败: ' + err.message);
|
||||
}
|
||||
if (response.ok) { alert('密码修改成功'); location.reload(); }
|
||||
else { alert(data.error || '修改失败'); }
|
||||
} catch (err) { alert('请求失败'); }
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
<input type="text" id="serviceDesc" class="input" placeholder="简短描述">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>标签 (英文逗号分隔)</label>
|
||||
<input type="text" id="serviceTags" class="input" placeholder="工具, 开发, 常用">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>图标(emoji)</label>
|
||||
@@ -593,6 +598,7 @@
|
||||
document.getElementById('serviceName').value = service.name;
|
||||
document.getElementById('serviceUrl').value = service.url;
|
||||
document.getElementById('serviceDesc').value = service.description || '';
|
||||
document.getElementById('serviceTags').value = service.tags || '';
|
||||
document.getElementById('serviceIcon').value = service.icon || '';
|
||||
document.getElementById('serviceCategory').value = service.category;
|
||||
document.getElementById('serviceSort').value = service.sort_order;
|
||||
@@ -611,6 +617,7 @@
|
||||
name: document.getElementById('serviceName').value,
|
||||
url: document.getElementById('serviceUrl').value,
|
||||
description: document.getElementById('serviceDesc').value,
|
||||
tags: document.getElementById('serviceTags').value,
|
||||
icon: document.getElementById('serviceIcon').value,
|
||||
category: document.getElementById('serviceCategory').value,
|
||||
sort_order: parseInt(document.getElementById('serviceSort').value) || 0,
|
||||
|
||||
Reference in New Issue
Block a user