koishi-plugin-memesluna 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +564 -237
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1288,7 +1288,7 @@ function buildAdminHtml(basePath) {
|
|
|
1288
1288
|
<head>
|
|
1289
1289
|
<meta charset="utf-8" />
|
|
1290
1290
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1291
|
-
<title>图床转发 -
|
|
1291
|
+
<title>图床转发 - 合集管理</title>
|
|
1292
1292
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
1293
1293
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
1294
1294
|
<style>
|
|
@@ -1323,36 +1323,69 @@ function buildAdminHtml(basePath) {
|
|
|
1323
1323
|
max-width: 1320px;
|
|
1324
1324
|
}
|
|
1325
1325
|
|
|
1326
|
-
.
|
|
1327
|
-
.main-panel,
|
|
1328
|
-
.sub-card {
|
|
1326
|
+
.panel {
|
|
1329
1327
|
border-radius: 14px;
|
|
1330
1328
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
1331
1329
|
background: rgba(255, 255, 255, 0.72);
|
|
1332
1330
|
-webkit-backdrop-filter: blur(10px) saturate(130%);
|
|
1333
1331
|
backdrop-filter: blur(10px) saturate(130%);
|
|
1334
1332
|
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1333
|
+
padding: 1rem;
|
|
1335
1334
|
}
|
|
1336
1335
|
|
|
1337
|
-
.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1336
|
+
.sub-card {
|
|
1337
|
+
border-radius: 12px;
|
|
1338
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
1339
|
+
background: rgba(255, 255, 255, 0.72);
|
|
1340
|
+
-webkit-backdrop-filter: blur(10px) saturate(130%);
|
|
1341
|
+
backdrop-filter: blur(10px) saturate(130%);
|
|
1342
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1343
|
+
padding: 0.9rem;
|
|
1341
1344
|
}
|
|
1342
1345
|
|
|
1343
|
-
.
|
|
1344
|
-
|
|
1346
|
+
.folder-grid {
|
|
1347
|
+
display: grid;
|
|
1348
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
1349
|
+
gap: 1rem;
|
|
1345
1350
|
}
|
|
1346
1351
|
|
|
1347
|
-
.
|
|
1352
|
+
.folder-card {
|
|
1353
|
+
border-radius: 12px;
|
|
1354
|
+
overflow: hidden;
|
|
1355
|
+
border: 1px solid rgba(148, 163, 184, 0.3);
|
|
1356
|
+
background: rgba(255, 255, 255, 0.88);
|
|
1348
1357
|
cursor: pointer;
|
|
1358
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
|
1349
1359
|
}
|
|
1350
1360
|
|
|
1351
|
-
.
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1361
|
+
.folder-card:hover {
|
|
1362
|
+
transform: translateY(-3px);
|
|
1363
|
+
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.folder-card .folder-icon {
|
|
1367
|
+
display: flex;
|
|
1368
|
+
align-items: center;
|
|
1369
|
+
justify-content: center;
|
|
1370
|
+
height: 100px;
|
|
1371
|
+
background: linear-gradient(135deg, #e0e7ff 0%, #f0f4ff 100%);
|
|
1372
|
+
font-size: 2.5rem;
|
|
1373
|
+
color: #6366f1;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
.folder-card .folder-info {
|
|
1377
|
+
padding: 0.6rem 0.8rem;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
.folder-card .folder-name {
|
|
1355
1381
|
font-weight: 700;
|
|
1382
|
+
font-size: 0.95rem;
|
|
1383
|
+
color: #334155;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
.folder-card .folder-meta {
|
|
1387
|
+
font-size: 0.78rem;
|
|
1388
|
+
color: #64748b;
|
|
1356
1389
|
}
|
|
1357
1390
|
|
|
1358
1391
|
.image-grid {
|
|
@@ -1376,10 +1409,6 @@ function buildAdminHtml(basePath) {
|
|
|
1376
1409
|
background: #f1f5f9;
|
|
1377
1410
|
}
|
|
1378
1411
|
|
|
1379
|
-
.sub-card {
|
|
1380
|
-
padding: 0.9rem;
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
1412
|
.code-url {
|
|
1384
1413
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1385
1414
|
font-size: 0.8rem;
|
|
@@ -1396,6 +1425,21 @@ function buildAdminHtml(basePath) {
|
|
|
1396
1425
|
color: #64748b;
|
|
1397
1426
|
font-size: 0.9rem;
|
|
1398
1427
|
}
|
|
1428
|
+
|
|
1429
|
+
.drop-zone {
|
|
1430
|
+
border: 2px dashed #94a3b8;
|
|
1431
|
+
border-radius: 12px;
|
|
1432
|
+
padding: 2rem;
|
|
1433
|
+
text-align: center;
|
|
1434
|
+
color: #64748b;
|
|
1435
|
+
transition: border-color 0.2s, background 0.2s;
|
|
1436
|
+
cursor: pointer;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
.drop-zone.drag-over {
|
|
1440
|
+
border-color: #6366f1;
|
|
1441
|
+
background: rgba(99, 102, 241, 0.06);
|
|
1442
|
+
}
|
|
1399
1443
|
</style>
|
|
1400
1444
|
</head>
|
|
1401
1445
|
<body>
|
|
@@ -1409,75 +1453,72 @@ function buildAdminHtml(basePath) {
|
|
|
1409
1453
|
<ul class="navbar-nav me-auto">
|
|
1410
1454
|
<li class="nav-item"><a class="nav-link" href="${basePath}/">首页</a></li>
|
|
1411
1455
|
<li class="nav-item"><a class="nav-link active" href="${basePath}/admin">管理</a></li>
|
|
1456
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/admin/endpoint">端点</a></li>
|
|
1412
1457
|
</ul>
|
|
1413
1458
|
</div>
|
|
1414
1459
|
</div>
|
|
1415
1460
|
</nav>
|
|
1416
1461
|
|
|
1417
1462
|
<div class="container admin-shell mt-3 pb-4">
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
<
|
|
1423
|
-
|
|
1424
|
-
<
|
|
1425
|
-
|
|
1426
|
-
<div id="collection-list" class="list-group mb-3"></div>
|
|
1427
|
-
<button id="delete-collection" class="btn btn-outline-danger w-100" disabled>删除当前合集</button>
|
|
1428
|
-
|
|
1429
|
-
<hr>
|
|
1430
|
-
<h6 class="mb-2">合集描述</h6>
|
|
1431
|
-
<div class="input-group mb-3">
|
|
1432
|
-
<input id="collection-description" class="form-control" placeholder="为当前合集添加描述" disabled />
|
|
1433
|
-
<button id="save-description" class="btn btn-primary" disabled>保存</button>
|
|
1434
|
-
</div>
|
|
1435
|
-
|
|
1436
|
-
<hr>
|
|
1437
|
-
<h6 class="mb-2">快捷信息</h6>
|
|
1438
|
-
<div class="small text-muted">
|
|
1439
|
-
<div>管理链接:<code>${basePath}/admin</code></div>
|
|
1440
|
-
<div class="mt-1">随机访问:<code id="collection-random-url">-</code></div>
|
|
1463
|
+
<!-- View: folder list (main) -->
|
|
1464
|
+
<div id="view-folders">
|
|
1465
|
+
<div class="panel mb-3">
|
|
1466
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
1467
|
+
<h5 class="mb-0">合集管理</h5>
|
|
1468
|
+
<div class="d-flex gap-2">
|
|
1469
|
+
<input id="new-collection-name" class="form-control form-control-sm" style="width:180px" placeholder="新合集名称" />
|
|
1470
|
+
<button id="create-collection" class="btn btn-sm btn-primary">创建</button>
|
|
1441
1471
|
</div>
|
|
1442
1472
|
</div>
|
|
1473
|
+
<div id="folder-grid" class="folder-grid"></div>
|
|
1474
|
+
<div id="folders-empty" class="empty-tip mt-2">暂无合集,请先创建</div>
|
|
1443
1475
|
</div>
|
|
1476
|
+
</div>
|
|
1444
1477
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1478
|
+
<!-- View: collection detail -->
|
|
1479
|
+
<div id="view-detail" style="display:none">
|
|
1480
|
+
<div class="panel mb-3">
|
|
1481
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
1482
|
+
<div class="d-flex align-items-center gap-2">
|
|
1483
|
+
<button id="back-to-folders" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回</button>
|
|
1484
|
+
<h5 class="mb-0" id="detail-title"></h5>
|
|
1450
1485
|
</div>
|
|
1486
|
+
<div class="d-flex gap-2">
|
|
1487
|
+
<button id="refresh-resources" class="btn btn-sm btn-outline-primary"><i class="bi bi-arrow-clockwise"></i> 刷新缓存</button>
|
|
1488
|
+
<button id="delete-collection" class="btn btn-sm btn-outline-danger">删除合集</button>
|
|
1489
|
+
</div>
|
|
1490
|
+
</div>
|
|
1451
1491
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
<
|
|
1458
|
-
<div
|
|
1492
|
+
<div class="row g-3 mb-3">
|
|
1493
|
+
<div class="col-md-6">
|
|
1494
|
+
<div class="sub-card h-100">
|
|
1495
|
+
<div class="section-title mb-2">上传图片</div>
|
|
1496
|
+
<div id="drop-zone" class="drop-zone mb-2">
|
|
1497
|
+
<i class="bi bi-cloud-arrow-up" style="font-size:1.5rem"></i>
|
|
1498
|
+
<div>拖放图片到此处,或点击选择文件</div>
|
|
1499
|
+
<input id="upload-files" type="file" class="d-none" multiple accept="image/*" />
|
|
1459
1500
|
</div>
|
|
1460
1501
|
</div>
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1502
|
+
</div>
|
|
1503
|
+
<div class="col-md-6">
|
|
1504
|
+
<div class="sub-card h-100">
|
|
1505
|
+
<div class="section-title mb-2">添加外链</div>
|
|
1506
|
+
<textarea id="links-input" class="form-control mb-2" rows="3" placeholder="每行一个 http/https 链接"></textarea>
|
|
1507
|
+
<button id="add-links" class="btn btn-primary btn-sm w-100">添加外链</button>
|
|
1467
1508
|
</div>
|
|
1468
1509
|
</div>
|
|
1510
|
+
</div>
|
|
1469
1511
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1512
|
+
<div class="sub-card mb-3">
|
|
1513
|
+
<div class="section-title mb-2">本地图片</div>
|
|
1514
|
+
<div id="images-grid" class="image-grid"></div>
|
|
1515
|
+
<div id="images-empty" class="empty-tip mt-2">暂无本地图片</div>
|
|
1516
|
+
</div>
|
|
1475
1517
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
</div>
|
|
1518
|
+
<div class="sub-card">
|
|
1519
|
+
<div class="section-title mb-2">外链列表</div>
|
|
1520
|
+
<div id="links-list" class="list-group"></div>
|
|
1521
|
+
<div id="links-empty" class="empty-tip mt-2">暂无外链</div>
|
|
1481
1522
|
</div>
|
|
1482
1523
|
</div>
|
|
1483
1524
|
</div>
|
|
@@ -1490,7 +1531,6 @@ function buildAdminHtml(basePath) {
|
|
|
1490
1531
|
const state = {
|
|
1491
1532
|
collectionNames: [],
|
|
1492
1533
|
collections: [],
|
|
1493
|
-
endpoints: [],
|
|
1494
1534
|
selectedCollection: '',
|
|
1495
1535
|
images: [],
|
|
1496
1536
|
links: [],
|
|
@@ -1498,173 +1538,170 @@ function buildAdminHtml(basePath) {
|
|
|
1498
1538
|
|
|
1499
1539
|
const byId = (id) => document.getElementById(id)
|
|
1500
1540
|
|
|
1501
|
-
function showAlert(message, type
|
|
1502
|
-
|
|
1541
|
+
function showAlert(message, type) {
|
|
1542
|
+
type = type || 'info'
|
|
1543
|
+
var el = byId('admin-alert')
|
|
1503
1544
|
el.className = 'alert alert-' + type + ' mt-3'
|
|
1504
1545
|
el.textContent = message
|
|
1505
1546
|
el.classList.remove('d-none')
|
|
1506
|
-
setTimeout(()
|
|
1547
|
+
setTimeout(function() { el.classList.add('d-none') }, 2200)
|
|
1507
1548
|
}
|
|
1508
1549
|
|
|
1509
|
-
async function request(url, options
|
|
1510
|
-
|
|
1511
|
-
|
|
1550
|
+
async function request(url, options) {
|
|
1551
|
+
options = options || {}
|
|
1552
|
+
var headers = Object.assign({}, options.headers || {})
|
|
1512
1553
|
if (options.body && !headers['Content-Type']) {
|
|
1513
1554
|
headers['Content-Type'] = 'application/json'
|
|
1514
1555
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
try {
|
|
1519
|
-
data = await res.json()
|
|
1520
|
-
console.log('Response data:', data)
|
|
1521
|
-
} catch (e) {
|
|
1522
|
-
console.log('JSON parse error:', e)
|
|
1523
|
-
data = null
|
|
1524
|
-
}
|
|
1556
|
+
var res = await fetch(url, Object.assign({}, options, { headers: headers }))
|
|
1557
|
+
var data = null
|
|
1558
|
+
try { data = await res.json() } catch(e) { data = null }
|
|
1525
1559
|
if (!res.ok) {
|
|
1526
1560
|
throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
|
|
1527
1561
|
}
|
|
1528
1562
|
return data
|
|
1529
1563
|
}
|
|
1530
1564
|
|
|
1565
|
+
function readFileAsDataURL(file) {
|
|
1566
|
+
return new Promise(function(resolve, reject) {
|
|
1567
|
+
var reader = new FileReader()
|
|
1568
|
+
reader.onload = function() { resolve(reader.result) }
|
|
1569
|
+
reader.onerror = function() { reject(new Error('读取文件失败')) }
|
|
1570
|
+
reader.readAsDataURL(file)
|
|
1571
|
+
})
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/* --- Folder list view --- */
|
|
1531
1575
|
async function refreshState() {
|
|
1532
|
-
|
|
1576
|
+
var data = await request(BASE_PATH + '/api/admin/state')
|
|
1533
1577
|
state.collectionNames = Array.isArray(data.collectionNames) ? data.collectionNames : []
|
|
1534
1578
|
state.collections = Array.isArray(data.collections) ? data.collections : []
|
|
1535
|
-
|
|
1579
|
+
renderFolderGrid()
|
|
1580
|
+
}
|
|
1536
1581
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1582
|
+
function renderFolderGrid() {
|
|
1583
|
+
var grid = byId('folder-grid')
|
|
1584
|
+
var empty = byId('folders-empty')
|
|
1585
|
+
grid.textContent = ''
|
|
1586
|
+
empty.style.display = state.collections.length ? 'none' : 'block'
|
|
1587
|
+
|
|
1588
|
+
state.collections.forEach(function(col) {
|
|
1589
|
+
var card = document.createElement('div')
|
|
1590
|
+
card.className = 'folder-card'
|
|
1591
|
+
card.addEventListener('click', function() { enterCollection(col.name) })
|
|
1592
|
+
|
|
1593
|
+
var icon = document.createElement('div')
|
|
1594
|
+
icon.className = 'folder-icon'
|
|
1595
|
+
icon.innerHTML = '<i class="bi bi-folder-fill"></i>'
|
|
1596
|
+
card.appendChild(icon)
|
|
1597
|
+
|
|
1598
|
+
var info = document.createElement('div')
|
|
1599
|
+
info.className = 'folder-info'
|
|
1600
|
+
var nameEl = document.createElement('div')
|
|
1601
|
+
nameEl.className = 'folder-name'
|
|
1602
|
+
nameEl.textContent = col.name
|
|
1603
|
+
info.appendChild(nameEl)
|
|
1604
|
+
var meta = document.createElement('div')
|
|
1605
|
+
meta.className = 'folder-meta'
|
|
1606
|
+
meta.textContent = '本地 ' + (col.localCount || 0) + ' / 外链 ' + (col.linkCount || 0)
|
|
1607
|
+
info.appendChild(meta)
|
|
1608
|
+
card.appendChild(info)
|
|
1609
|
+
grid.appendChild(card)
|
|
1610
|
+
})
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function enterCollection(name) {
|
|
1614
|
+
state.selectedCollection = name
|
|
1615
|
+
byId('view-folders').style.display = 'none'
|
|
1616
|
+
byId('view-detail').style.display = 'block'
|
|
1617
|
+
byId('detail-title').textContent = name
|
|
1618
|
+
refreshCollectionResources()
|
|
1619
|
+
}
|
|
1540
1620
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1621
|
+
function backToFolders() {
|
|
1622
|
+
state.selectedCollection = ''
|
|
1623
|
+
byId('view-detail').style.display = 'none'
|
|
1624
|
+
byId('view-folders').style.display = 'block'
|
|
1625
|
+
refreshState()
|
|
1544
1626
|
}
|
|
1545
1627
|
|
|
1628
|
+
/* --- Detail view --- */
|
|
1546
1629
|
async function refreshCollectionResources() {
|
|
1547
|
-
if (!state.selectedCollection) {
|
|
1548
|
-
|
|
1549
|
-
state.links = []
|
|
1550
|
-
renderImages()
|
|
1551
|
-
renderLinks()
|
|
1552
|
-
return
|
|
1553
|
-
}
|
|
1554
|
-
const data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
|
|
1630
|
+
if (!state.selectedCollection) { state.images = []; state.links = []; renderImages(); renderLinks(); return }
|
|
1631
|
+
var data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
|
|
1555
1632
|
state.images = Array.isArray(data.images) ? data.images : []
|
|
1556
1633
|
state.links = Array.isArray(data.links) ? data.links : []
|
|
1557
1634
|
renderImages()
|
|
1558
1635
|
renderLinks()
|
|
1559
1636
|
}
|
|
1560
1637
|
|
|
1561
|
-
function syncSelectedCollectionUi() {
|
|
1562
|
-
const selected = state.selectedCollection
|
|
1563
|
-
byId('selected-collection-badge').textContent = selected || '未选择'
|
|
1564
|
-
byId('delete-collection').disabled = !selected
|
|
1565
|
-
byId('upload-images').disabled = !selected
|
|
1566
|
-
byId('add-links').disabled = !selected
|
|
1567
|
-
byId('collection-random-url').textContent = selected ? BASE_PATH + '/' + selected : '-'
|
|
1568
|
-
byId('collection-description').disabled = !selected
|
|
1569
|
-
byId('save-description').disabled = !selected
|
|
1570
|
-
|
|
1571
|
-
const collectionInfo = state.collections.find((c) => c.name === selected)
|
|
1572
|
-
byId('collection-description').value = collectionInfo ? (collectionInfo.description || '') : ''
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
function renderCollectionList() {
|
|
1576
|
-
const list = byId('collection-list')
|
|
1577
|
-
list.textContent = ''
|
|
1578
|
-
if (!state.collectionNames.length) {
|
|
1579
|
-
const empty = document.createElement('div')
|
|
1580
|
-
empty.className = 'text-muted small'
|
|
1581
|
-
empty.textContent = '暂无合集'
|
|
1582
|
-
list.appendChild(empty)
|
|
1583
|
-
return
|
|
1584
|
-
}
|
|
1585
|
-
state.collectionNames.forEach((name) => {
|
|
1586
|
-
const button = document.createElement('button')
|
|
1587
|
-
button.type = 'button'
|
|
1588
|
-
button.className = 'list-group-item list-group-item-action collection-item' + (name === state.selectedCollection ? ' active' : '')
|
|
1589
|
-
button.textContent = name
|
|
1590
|
-
button.addEventListener('click', async () => {
|
|
1591
|
-
state.selectedCollection = name
|
|
1592
|
-
renderCollectionList()
|
|
1593
|
-
syncSelectedCollectionUi()
|
|
1594
|
-
await refreshCollectionResources()
|
|
1595
|
-
})
|
|
1596
|
-
list.appendChild(button)
|
|
1597
|
-
})
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
1638
|
function imagePreviewUrl(filename) {
|
|
1601
1639
|
return BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(filename)
|
|
1602
1640
|
}
|
|
1603
1641
|
|
|
1604
1642
|
function renderImages() {
|
|
1605
|
-
|
|
1606
|
-
|
|
1643
|
+
var grid = byId('images-grid')
|
|
1644
|
+
var empty = byId('images-empty')
|
|
1607
1645
|
grid.textContent = ''
|
|
1608
1646
|
empty.style.display = state.images.length ? 'none' : 'block'
|
|
1609
1647
|
|
|
1610
|
-
state.images.forEach((name)
|
|
1611
|
-
|
|
1648
|
+
state.images.forEach(function(name) {
|
|
1649
|
+
var card = document.createElement('div')
|
|
1612
1650
|
card.className = 'image-card'
|
|
1613
1651
|
|
|
1614
|
-
|
|
1652
|
+
var img = document.createElement('img')
|
|
1615
1653
|
img.src = imagePreviewUrl(name)
|
|
1616
1654
|
img.alt = name
|
|
1617
1655
|
card.appendChild(img)
|
|
1618
1656
|
|
|
1619
|
-
|
|
1657
|
+
var body = document.createElement('div')
|
|
1620
1658
|
body.className = 'p-2'
|
|
1621
|
-
|
|
1659
|
+
var title = document.createElement('div')
|
|
1622
1660
|
title.className = 'small text-truncate mb-2'
|
|
1623
1661
|
title.textContent = name
|
|
1624
1662
|
body.appendChild(title)
|
|
1625
1663
|
|
|
1626
|
-
|
|
1664
|
+
var row = document.createElement('div')
|
|
1627
1665
|
row.className = 'd-flex gap-1'
|
|
1628
1666
|
|
|
1629
|
-
|
|
1667
|
+
var moveSelect = document.createElement('select')
|
|
1630
1668
|
moveSelect.className = 'form-select form-select-sm'
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
moveSelect.appendChild(
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
opt.value =
|
|
1639
|
-
opt.textContent = target
|
|
1669
|
+
var others = state.collectionNames.filter(function(c) { return c !== state.selectedCollection })
|
|
1670
|
+
var ph = document.createElement('option')
|
|
1671
|
+
ph.value = ''
|
|
1672
|
+
ph.textContent = '移动到...'
|
|
1673
|
+
moveSelect.appendChild(ph)
|
|
1674
|
+
others.forEach(function(t) {
|
|
1675
|
+
var opt = document.createElement('option')
|
|
1676
|
+
opt.value = t; opt.textContent = t
|
|
1640
1677
|
moveSelect.appendChild(opt)
|
|
1641
1678
|
})
|
|
1642
1679
|
|
|
1643
|
-
|
|
1680
|
+
var moveBtn = document.createElement('button')
|
|
1644
1681
|
moveBtn.className = 'btn btn-sm btn-outline-primary'
|
|
1645
1682
|
moveBtn.textContent = '移动'
|
|
1646
|
-
moveBtn.disabled =
|
|
1647
|
-
moveBtn.addEventListener('click', async ()
|
|
1648
|
-
|
|
1649
|
-
if (!
|
|
1683
|
+
moveBtn.disabled = others.length === 0
|
|
1684
|
+
moveBtn.addEventListener('click', async function() {
|
|
1685
|
+
var tc = moveSelect.value
|
|
1686
|
+
if (!tc) return
|
|
1650
1687
|
await request(
|
|
1651
1688
|
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name) + '/move',
|
|
1652
|
-
{ method: 'POST', body: JSON.stringify({ targetCollection }) }
|
|
1689
|
+
{ method: 'POST', body: JSON.stringify({ targetCollection: tc }) }
|
|
1653
1690
|
)
|
|
1654
1691
|
showAlert('图片已移动', 'success')
|
|
1655
|
-
|
|
1692
|
+
refreshCollectionResources()
|
|
1656
1693
|
})
|
|
1657
1694
|
|
|
1658
|
-
|
|
1695
|
+
var delBtn = document.createElement('button')
|
|
1659
1696
|
delBtn.className = 'btn btn-sm btn-outline-danger'
|
|
1660
1697
|
delBtn.textContent = '删除'
|
|
1661
|
-
delBtn.addEventListener('click', async ()
|
|
1698
|
+
delBtn.addEventListener('click', async function() {
|
|
1662
1699
|
await request(
|
|
1663
1700
|
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name),
|
|
1664
1701
|
{ method: 'DELETE' }
|
|
1665
1702
|
)
|
|
1666
1703
|
showAlert('图片已删除', 'success')
|
|
1667
|
-
|
|
1704
|
+
refreshCollectionResources()
|
|
1668
1705
|
})
|
|
1669
1706
|
|
|
1670
1707
|
row.appendChild(moveSelect)
|
|
@@ -1677,40 +1714,40 @@ function buildAdminHtml(basePath) {
|
|
|
1677
1714
|
}
|
|
1678
1715
|
|
|
1679
1716
|
function renderLinks() {
|
|
1680
|
-
|
|
1681
|
-
|
|
1717
|
+
var wrap = byId('links-list')
|
|
1718
|
+
var empty = byId('links-empty')
|
|
1682
1719
|
wrap.textContent = ''
|
|
1683
1720
|
empty.style.display = state.links.length ? 'none' : 'block'
|
|
1684
|
-
state.links.forEach((link)
|
|
1685
|
-
|
|
1721
|
+
state.links.forEach(function(link) {
|
|
1722
|
+
var row = document.createElement('div')
|
|
1686
1723
|
row.className = 'list-group-item d-flex justify-content-between align-items-center gap-2'
|
|
1687
1724
|
|
|
1688
|
-
|
|
1725
|
+
var text = document.createElement('div')
|
|
1689
1726
|
text.className = 'code-url flex-grow-1'
|
|
1690
1727
|
text.textContent = link
|
|
1691
1728
|
|
|
1692
|
-
|
|
1729
|
+
var actions = document.createElement('div')
|
|
1693
1730
|
actions.className = 'd-flex gap-1'
|
|
1694
1731
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1732
|
+
var openEl = document.createElement('a')
|
|
1733
|
+
openEl.className = 'btn btn-sm btn-outline-primary'
|
|
1734
|
+
openEl.textContent = '查看'
|
|
1735
|
+
openEl.href = link
|
|
1736
|
+
openEl.target = '_blank'
|
|
1700
1737
|
|
|
1701
|
-
|
|
1738
|
+
var del = document.createElement('button')
|
|
1702
1739
|
del.className = 'btn btn-sm btn-outline-danger'
|
|
1703
1740
|
del.textContent = '删除'
|
|
1704
|
-
del.addEventListener('click', async ()
|
|
1741
|
+
del.addEventListener('click', async function() {
|
|
1705
1742
|
await request(
|
|
1706
1743
|
BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links',
|
|
1707
|
-
{ method: 'DELETE', body: JSON.stringify({ link }) }
|
|
1744
|
+
{ method: 'DELETE', body: JSON.stringify({ link: link }) }
|
|
1708
1745
|
)
|
|
1709
1746
|
showAlert('外链已删除', 'success')
|
|
1710
|
-
|
|
1747
|
+
refreshCollectionResources()
|
|
1711
1748
|
})
|
|
1712
1749
|
|
|
1713
|
-
actions.appendChild(
|
|
1750
|
+
actions.appendChild(openEl)
|
|
1714
1751
|
actions.appendChild(del)
|
|
1715
1752
|
row.appendChild(text)
|
|
1716
1753
|
row.appendChild(actions)
|
|
@@ -1718,87 +1755,372 @@ function buildAdminHtml(basePath) {
|
|
|
1718
1755
|
})
|
|
1719
1756
|
}
|
|
1720
1757
|
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1758
|
+
/* --- Upload with drag/drop --- */
|
|
1759
|
+
async function uploadFiles(files) {
|
|
1760
|
+
if (!state.selectedCollection || !files || !files.length) return
|
|
1761
|
+
var images = []
|
|
1762
|
+
for (var i = 0; i < files.length; i++) {
|
|
1763
|
+
var b64 = await readFileAsDataURL(files[i])
|
|
1764
|
+
images.push({ base64: b64, originalName: files[i].name })
|
|
1765
|
+
}
|
|
1766
|
+
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images', {
|
|
1767
|
+
method: 'POST',
|
|
1768
|
+
body: JSON.stringify({ images: images }),
|
|
1727
1769
|
})
|
|
1770
|
+
showAlert('图片上传成功', 'success')
|
|
1771
|
+
refreshCollectionResources()
|
|
1728
1772
|
}
|
|
1729
1773
|
|
|
1730
|
-
byId('
|
|
1731
|
-
|
|
1774
|
+
var dropZone = byId('drop-zone')
|
|
1775
|
+
var fileInput = byId('upload-files')
|
|
1776
|
+
|
|
1777
|
+
dropZone.addEventListener('click', function() { fileInput.click() })
|
|
1778
|
+
fileInput.addEventListener('change', function() { uploadFiles(fileInput.files); fileInput.value = '' })
|
|
1779
|
+
|
|
1780
|
+
dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('drag-over') })
|
|
1781
|
+
dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); dropZone.classList.remove('drag-over') })
|
|
1782
|
+
dropZone.addEventListener('drop', function(e) {
|
|
1783
|
+
e.preventDefault()
|
|
1784
|
+
dropZone.classList.remove('drag-over')
|
|
1785
|
+
if (e.dataTransfer && e.dataTransfer.files) uploadFiles(e.dataTransfer.files)
|
|
1786
|
+
})
|
|
1787
|
+
|
|
1788
|
+
/* --- Buttons --- */
|
|
1789
|
+
byId('create-collection').addEventListener('click', async function() {
|
|
1790
|
+
var name = byId('new-collection-name').value.trim()
|
|
1732
1791
|
if (!name) return
|
|
1733
1792
|
await request(BASE_PATH + '/api/admin/collections', {
|
|
1734
1793
|
method: 'POST',
|
|
1735
|
-
body: JSON.stringify({ name }),
|
|
1794
|
+
body: JSON.stringify({ name: name }),
|
|
1736
1795
|
})
|
|
1737
1796
|
byId('new-collection-name').value = ''
|
|
1738
1797
|
showAlert('合集创建成功', 'success')
|
|
1739
|
-
|
|
1798
|
+
refreshState()
|
|
1740
1799
|
})
|
|
1741
1800
|
|
|
1742
|
-
byId('delete-collection').addEventListener('click', async ()
|
|
1801
|
+
byId('delete-collection').addEventListener('click', async function() {
|
|
1743
1802
|
if (!state.selectedCollection) return
|
|
1803
|
+
if (!confirm('确定删除合集 ' + state.selectedCollection + ' ?')) return
|
|
1744
1804
|
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection), {
|
|
1745
1805
|
method: 'DELETE',
|
|
1746
1806
|
})
|
|
1747
1807
|
showAlert('合集已删除', 'success')
|
|
1748
|
-
|
|
1808
|
+
backToFolders()
|
|
1749
1809
|
})
|
|
1750
1810
|
|
|
1751
|
-
byId('
|
|
1752
|
-
if (!state.selectedCollection) return
|
|
1753
|
-
const description = byId('collection-description').value.trim()
|
|
1754
|
-
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/description', {
|
|
1755
|
-
method: 'PATCH',
|
|
1756
|
-
body: JSON.stringify({ description }),
|
|
1757
|
-
})
|
|
1758
|
-
showAlert('描述已保存', 'success')
|
|
1759
|
-
await refreshState()
|
|
1760
|
-
})
|
|
1811
|
+
byId('back-to-folders').addEventListener('click', backToFolders)
|
|
1761
1812
|
|
|
1762
|
-
byId('
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
if (!files || !files.length) return
|
|
1766
|
-
const images = []
|
|
1767
|
-
for (const file of files) {
|
|
1768
|
-
const base64 = await readFileAsDataURL(file)
|
|
1769
|
-
images.push({ base64, originalName: file.name })
|
|
1770
|
-
}
|
|
1771
|
-
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images', {
|
|
1772
|
-
method: 'POST',
|
|
1773
|
-
body: JSON.stringify({ images }),
|
|
1774
|
-
})
|
|
1775
|
-
byId('upload-files').value = ''
|
|
1776
|
-
showAlert('图片上传成功', 'success')
|
|
1777
|
-
await refreshCollectionResources()
|
|
1778
|
-
await refreshState()
|
|
1813
|
+
byId('refresh-resources').addEventListener('click', function() {
|
|
1814
|
+
refreshCollectionResources()
|
|
1815
|
+
showAlert('已刷新', 'success')
|
|
1779
1816
|
})
|
|
1780
1817
|
|
|
1781
|
-
byId('add-links').addEventListener('click', async ()
|
|
1818
|
+
byId('add-links').addEventListener('click', async function() {
|
|
1782
1819
|
if (!state.selectedCollection) return
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
if (!links.length) return
|
|
1820
|
+
var text = byId('links-input').value
|
|
1821
|
+
var lines = text.split(/\\r?\\n/g).map(function(l) { return l.trim() }).filter(Boolean)
|
|
1822
|
+
if (!lines.length) return
|
|
1787
1823
|
await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links', {
|
|
1788
1824
|
method: 'POST',
|
|
1789
|
-
body: JSON.stringify({ links }),
|
|
1825
|
+
body: JSON.stringify({ links: lines }),
|
|
1790
1826
|
})
|
|
1791
1827
|
byId('links-input').value = ''
|
|
1792
1828
|
showAlert('外链添加成功', 'success')
|
|
1793
|
-
|
|
1794
|
-
await refreshState()
|
|
1829
|
+
refreshCollectionResources()
|
|
1795
1830
|
})
|
|
1796
1831
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1832
|
+
refreshState().catch(function(error) {
|
|
1833
|
+
showAlert(error instanceof Error ? error.message : String(error), 'danger')
|
|
1799
1834
|
})
|
|
1835
|
+
</script>
|
|
1836
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
1837
|
+
</body>
|
|
1838
|
+
</html>`;
|
|
1839
|
+
}
|
|
1840
|
+
__name(buildAdminHtml, "buildAdminHtml");
|
|
1841
|
+
function buildAdminEndpointHtml(basePath) {
|
|
1842
|
+
return `<!doctype html>
|
|
1843
|
+
<html lang="zh-CN">
|
|
1844
|
+
<head>
|
|
1845
|
+
<meta charset="utf-8" />
|
|
1846
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1847
|
+
<title>图床转发 - 302 端点管理</title>
|
|
1848
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
1849
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.3/font/bootstrap-icons.css">
|
|
1850
|
+
<style>
|
|
1851
|
+
body {
|
|
1852
|
+
min-height: 100vh;
|
|
1853
|
+
margin: 0;
|
|
1854
|
+
color: #1f2937;
|
|
1855
|
+
background-color: #f4f6fb;
|
|
1856
|
+
background-image: url('/project_bg/default_background.jpg');
|
|
1857
|
+
background-size: cover;
|
|
1858
|
+
background-position: center;
|
|
1859
|
+
background-repeat: no-repeat;
|
|
1860
|
+
background-attachment: fixed;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
body::before {
|
|
1864
|
+
content: '';
|
|
1865
|
+
position: fixed;
|
|
1866
|
+
inset: 0;
|
|
1867
|
+
background-color: rgba(255, 255, 255, 0.78);
|
|
1868
|
+
z-index: -1;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
.acrylic-navbar {
|
|
1872
|
+
background-color: rgba(248, 249, 250, 0.68);
|
|
1873
|
+
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
|
1874
|
+
backdrop-filter: blur(12px) saturate(150%);
|
|
1875
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
.layout-shell {
|
|
1879
|
+
max-width: 1320px;
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
.panel {
|
|
1883
|
+
border-radius: 14px;
|
|
1884
|
+
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
1885
|
+
background: rgba(255, 255, 255, 0.72);
|
|
1886
|
+
-webkit-backdrop-filter: blur(10px) saturate(130%);
|
|
1887
|
+
backdrop-filter: blur(10px) saturate(130%);
|
|
1888
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
1889
|
+
padding: 1rem;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
.sidebar-panel {
|
|
1893
|
+
position: sticky;
|
|
1894
|
+
top: 1rem;
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
.code-text {
|
|
1898
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
1899
|
+
font-size: 0.8rem;
|
|
1900
|
+
word-break: break-all;
|
|
1901
|
+
}
|
|
1902
|
+
</style>
|
|
1903
|
+
</head>
|
|
1904
|
+
<body>
|
|
1905
|
+
<nav class="navbar navbar-expand-lg navbar-light acrylic-navbar">
|
|
1906
|
+
<div class="container layout-shell">
|
|
1907
|
+
<a class="navbar-brand" href="${basePath}/">MemesLuna</a>
|
|
1908
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavEndpoint">
|
|
1909
|
+
<span class="navbar-toggler-icon"></span>
|
|
1910
|
+
</button>
|
|
1911
|
+
<div class="collapse navbar-collapse" id="navbarNavEndpoint">
|
|
1912
|
+
<ul class="navbar-nav me-auto">
|
|
1913
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/">首页</a></li>
|
|
1914
|
+
<li class="nav-item"><a class="nav-link" href="${basePath}/admin">管理</a></li>
|
|
1915
|
+
<li class="nav-item"><a class="nav-link active" href="${basePath}/admin/endpoint">端点</a></li>
|
|
1916
|
+
</ul>
|
|
1917
|
+
</div>
|
|
1918
|
+
</div>
|
|
1919
|
+
</nav>
|
|
1800
1920
|
|
|
1801
|
-
|
|
1921
|
+
<div class="container layout-shell mt-3 pb-4">
|
|
1922
|
+
<div class="row g-3">
|
|
1923
|
+
<div class="col-lg-3">
|
|
1924
|
+
<div class="panel sidebar-panel">
|
|
1925
|
+
<h5 class="mb-3">添加 / 编辑端点</h5>
|
|
1926
|
+
<input id="endpoint-name" class="form-control mb-2" placeholder="端点名称" />
|
|
1927
|
+
<input id="endpoint-description" class="form-control mb-2" placeholder="描述" />
|
|
1928
|
+
<input id="endpoint-url" class="form-control mb-2" placeholder="目标 URL" />
|
|
1929
|
+
|
|
1930
|
+
<div class="d-grid gap-2 mt-3">
|
|
1931
|
+
<button id="save-endpoint" class="btn btn-primary">创建</button>
|
|
1932
|
+
<button id="reset-endpoint" class="btn btn-outline-secondary">清空</button>
|
|
1933
|
+
</div>
|
|
1934
|
+
</div>
|
|
1935
|
+
</div>
|
|
1936
|
+
|
|
1937
|
+
<div class="col-lg-9">
|
|
1938
|
+
<div class="panel">
|
|
1939
|
+
<h4 class="mb-3">302 跳转端点管理</h4>
|
|
1940
|
+
<p class="text-muted mb-3">通过 <code>${basePath}/端点名称</code> 访问,自动 302 跳转到目标 URL。</p>
|
|
1941
|
+
|
|
1942
|
+
<div class="table-responsive">
|
|
1943
|
+
<table class="table table-sm align-middle">
|
|
1944
|
+
<thead>
|
|
1945
|
+
<tr>
|
|
1946
|
+
<th>名称</th>
|
|
1947
|
+
<th>描述</th>
|
|
1948
|
+
<th>目标 URL</th>
|
|
1949
|
+
<th>访问</th>
|
|
1950
|
+
<th></th>
|
|
1951
|
+
</tr>
|
|
1952
|
+
</thead>
|
|
1953
|
+
<tbody id="endpoint-table"></tbody>
|
|
1954
|
+
</table>
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
</div>
|
|
1958
|
+
</div>
|
|
1959
|
+
|
|
1960
|
+
<div id="endpoint-alert" class="alert mt-3 d-none"></div>
|
|
1961
|
+
</div>
|
|
1962
|
+
|
|
1963
|
+
<script>
|
|
1964
|
+
var BASE_PATH = '${basePath}'
|
|
1965
|
+
var endpointState = {
|
|
1966
|
+
endpoints: [],
|
|
1967
|
+
editingName: '',
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
var byId = function(id) { return document.getElementById(id) }
|
|
1971
|
+
|
|
1972
|
+
function showAlert(message, type) {
|
|
1973
|
+
type = type || 'info'
|
|
1974
|
+
var el = byId('endpoint-alert')
|
|
1975
|
+
el.className = 'alert alert-' + type + ' mt-3'
|
|
1976
|
+
el.textContent = message
|
|
1977
|
+
el.classList.remove('d-none')
|
|
1978
|
+
setTimeout(function() { el.classList.add('d-none') }, 2400)
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
async function request(url, options) {
|
|
1982
|
+
options = options || {}
|
|
1983
|
+
var headers = Object.assign({}, options.headers || {})
|
|
1984
|
+
if (options.body && !headers['Content-Type']) {
|
|
1985
|
+
headers['Content-Type'] = 'application/json'
|
|
1986
|
+
}
|
|
1987
|
+
var res = await fetch(url, Object.assign({}, options, { headers: headers }))
|
|
1988
|
+
var data = null
|
|
1989
|
+
try { data = await res.json() } catch(e) { data = null }
|
|
1990
|
+
if (!res.ok) {
|
|
1991
|
+
throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
|
|
1992
|
+
}
|
|
1993
|
+
return data
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function resetForm() {
|
|
1997
|
+
endpointState.editingName = ''
|
|
1998
|
+
byId('endpoint-name').value = ''
|
|
1999
|
+
byId('endpoint-description').value = ''
|
|
2000
|
+
byId('endpoint-url').value = ''
|
|
2001
|
+
byId('save-endpoint').textContent = '创建'
|
|
2002
|
+
byId('endpoint-name').disabled = false
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
function fillForm(item) {
|
|
2006
|
+
endpointState.editingName = item.name || ''
|
|
2007
|
+
byId('endpoint-name').value = item.name || ''
|
|
2008
|
+
byId('endpoint-description').value = item.description || ''
|
|
2009
|
+
byId('endpoint-url').value = item.url || ''
|
|
2010
|
+
byId('save-endpoint').textContent = '更新'
|
|
2011
|
+
byId('endpoint-name').disabled = true
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function renderTable() {
|
|
2015
|
+
var body = byId('endpoint-table')
|
|
2016
|
+
body.textContent = ''
|
|
2017
|
+
|
|
2018
|
+
if (!endpointState.endpoints.length) {
|
|
2019
|
+
var tr = document.createElement('tr')
|
|
2020
|
+
var td = document.createElement('td')
|
|
2021
|
+
td.colSpan = 5
|
|
2022
|
+
td.className = 'text-muted'
|
|
2023
|
+
td.textContent = '暂无端点'
|
|
2024
|
+
tr.appendChild(td)
|
|
2025
|
+
body.appendChild(tr)
|
|
2026
|
+
return
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
endpointState.endpoints.forEach(function(item) {
|
|
2030
|
+
var tr = document.createElement('tr')
|
|
2031
|
+
var visitUrl = BASE_PATH + '/' + encodeURIComponent(item.name || '')
|
|
2032
|
+
|
|
2033
|
+
tr.innerHTML =
|
|
2034
|
+
'<td class="code-text"></td>' +
|
|
2035
|
+
'<td></td>' +
|
|
2036
|
+
'<td class="code-text"></td>' +
|
|
2037
|
+
'<td></td>' +
|
|
2038
|
+
'<td></td>'
|
|
2039
|
+
|
|
2040
|
+
tr.children[0].textContent = item.name || ''
|
|
2041
|
+
tr.children[1].textContent = item.description || '-'
|
|
2042
|
+
tr.children[2].textContent = item.url || ''
|
|
2043
|
+
|
|
2044
|
+
var visitLink = document.createElement('a')
|
|
2045
|
+
visitLink.href = visitUrl
|
|
2046
|
+
visitLink.target = '_blank'
|
|
2047
|
+
visitLink.className = 'btn btn-sm btn-outline-secondary'
|
|
2048
|
+
visitLink.innerHTML = '<i class="bi bi-box-arrow-up-right"></i>'
|
|
2049
|
+
tr.children[3].appendChild(visitLink)
|
|
2050
|
+
|
|
2051
|
+
var actionWrap = document.createElement('div')
|
|
2052
|
+
actionWrap.className = 'd-flex gap-1 justify-content-end'
|
|
2053
|
+
|
|
2054
|
+
var editBtn = document.createElement('button')
|
|
2055
|
+
editBtn.className = 'btn btn-sm btn-outline-primary'
|
|
2056
|
+
editBtn.textContent = '编辑'
|
|
2057
|
+
editBtn.addEventListener('click', function() { fillForm(item) })
|
|
2058
|
+
|
|
2059
|
+
var delBtn = document.createElement('button')
|
|
2060
|
+
delBtn.className = 'btn btn-sm btn-outline-danger'
|
|
2061
|
+
delBtn.textContent = '删除'
|
|
2062
|
+
delBtn.addEventListener('click', async function() {
|
|
2063
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(item.name || ''), {
|
|
2064
|
+
method: 'DELETE',
|
|
2065
|
+
})
|
|
2066
|
+
showAlert('端点已删除', 'success')
|
|
2067
|
+
await loadEndpoints()
|
|
2068
|
+
if (endpointState.editingName === item.name) resetForm()
|
|
2069
|
+
})
|
|
2070
|
+
|
|
2071
|
+
actionWrap.appendChild(editBtn)
|
|
2072
|
+
actionWrap.appendChild(delBtn)
|
|
2073
|
+
tr.children[4].appendChild(actionWrap)
|
|
2074
|
+
body.appendChild(tr)
|
|
2075
|
+
})
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
async function loadEndpoints() {
|
|
2079
|
+
var data = await request(BASE_PATH + '/api/admin/endpoints')
|
|
2080
|
+
endpointState.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
|
|
2081
|
+
renderTable()
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
byId('save-endpoint').addEventListener('click', async function() {
|
|
2085
|
+
var name = byId('endpoint-name').value.trim()
|
|
2086
|
+
var url = byId('endpoint-url').value.trim()
|
|
2087
|
+
if (!name || !url) {
|
|
2088
|
+
showAlert('名称与目标 URL 必填', 'warning')
|
|
2089
|
+
return
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
var payload = {
|
|
2093
|
+
name: name,
|
|
2094
|
+
description: byId('endpoint-description').value.trim(),
|
|
2095
|
+
url: url,
|
|
2096
|
+
method: 'redirect',
|
|
2097
|
+
urlConstruction: 'normal',
|
|
2098
|
+
modelName: '',
|
|
2099
|
+
queryParams: [],
|
|
2100
|
+
proxySettings: { fallbackAction: 'returnJson' },
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (endpointState.editingName) {
|
|
2104
|
+
await request(BASE_PATH + '/api/admin/endpoints/' + encodeURIComponent(endpointState.editingName), {
|
|
2105
|
+
method: 'PATCH',
|
|
2106
|
+
body: JSON.stringify(payload),
|
|
2107
|
+
})
|
|
2108
|
+
showAlert('端点更新成功', 'success')
|
|
2109
|
+
} else {
|
|
2110
|
+
await request(BASE_PATH + '/api/admin/endpoints', {
|
|
2111
|
+
method: 'POST',
|
|
2112
|
+
body: JSON.stringify(payload),
|
|
2113
|
+
})
|
|
2114
|
+
showAlert('端点创建成功', 'success')
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
await loadEndpoints()
|
|
2118
|
+
resetForm()
|
|
2119
|
+
})
|
|
2120
|
+
|
|
2121
|
+
byId('reset-endpoint').addEventListener('click', resetForm)
|
|
2122
|
+
|
|
2123
|
+
loadEndpoints().catch(function(error) {
|
|
1802
2124
|
showAlert(error instanceof Error ? error.message : String(error), 'danger')
|
|
1803
2125
|
})
|
|
1804
2126
|
</script>
|
|
@@ -1806,7 +2128,7 @@ function buildAdminHtml(basePath) {
|
|
|
1806
2128
|
</body>
|
|
1807
2129
|
</html>`;
|
|
1808
2130
|
}
|
|
1809
|
-
__name(
|
|
2131
|
+
__name(buildAdminEndpointHtml, "buildAdminEndpointHtml");
|
|
1810
2132
|
async function updateMemesVariable(ctx, config, service) {
|
|
1811
2133
|
const baseUrl = toAbsoluteBaseUrl(ctx, config);
|
|
1812
2134
|
const inventory = await service.buildRouteInventory(config.backendPath);
|
|
@@ -2153,6 +2475,11 @@ function applyServer(ctx, config, service) {
|
|
|
2153
2475
|
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2154
2476
|
koa.body = buildAdminHtml(basePath);
|
|
2155
2477
|
});
|
|
2478
|
+
ctx.server.get(`${basePath}/admin/endpoint`, async (koa) => {
|
|
2479
|
+
koa.status = 200;
|
|
2480
|
+
koa.set("Content-Type", "text/html; charset=utf-8");
|
|
2481
|
+
koa.body = buildAdminEndpointHtml(basePath);
|
|
2482
|
+
});
|
|
2156
2483
|
ctx.server.get(`${basePath}/api/collections/:name/resources`, async (koa) => {
|
|
2157
2484
|
const collectionName = koa.params.name;
|
|
2158
2485
|
const images = await service.getCollectionImages(collectionName);
|
package/package.json
CHANGED