koishi-plugin-memesluna 0.2.6 → 0.2.8

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.
Files changed (2) hide show
  1. package/lib/index.js +326 -232
  2. 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>图床转发 - 管理</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,102 @@ function buildAdminHtml(basePath) {
1323
1323
  max-width: 1320px;
1324
1324
  }
1325
1325
 
1326
- .sidebar-panel,
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
- .sidebar-panel {
1338
- padding: 1rem;
1339
- position: sticky;
1340
- top: 1rem;
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
- .main-panel {
1344
- padding: 1rem;
1346
+ .folder-grid {
1347
+ display: grid;
1348
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
1349
+ gap: 1rem;
1345
1350
  }
1346
1351
 
1347
- .collection-item {
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;
1359
+ }
1360
+
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;
1349
1378
  }
1350
1379
 
1351
- .collection-item.active {
1352
- background: rgba(59, 130, 246, 0.14);
1353
- border-color: rgba(59, 130, 246, 0.35);
1354
- color: #1d4ed8;
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;
1389
+ }
1390
+
1391
+ .folder-card .folder-desc {
1392
+ font-size: 0.78rem;
1393
+ color: #94a3b8;
1394
+ margin-top: 2px;
1395
+ overflow: hidden;
1396
+ text-overflow: ellipsis;
1397
+ white-space: nowrap;
1398
+ }
1399
+
1400
+ .folder-card .folder-path {
1401
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
1402
+ font-size: 0.72rem;
1403
+ color: #a5b4fc;
1404
+ margin-top: 2px;
1405
+ }
1406
+
1407
+ .desc-editor {
1408
+ display: flex;
1409
+ align-items: center;
1410
+ gap: 0.5rem;
1411
+ margin-bottom: 0.8rem;
1412
+ }
1413
+
1414
+ .desc-editor input {
1415
+ flex: 1;
1416
+ }
1417
+
1418
+ .desc-display {
1419
+ font-size: 0.88rem;
1420
+ color: #64748b;
1421
+ margin-bottom: 0.5rem;
1356
1422
  }
1357
1423
 
1358
1424
  .image-grid {
@@ -1376,10 +1442,6 @@ function buildAdminHtml(basePath) {
1376
1442
  background: #f1f5f9;
1377
1443
  }
1378
1444
 
1379
- .sub-card {
1380
- padding: 0.9rem;
1381
- }
1382
-
1383
1445
  .code-url {
1384
1446
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1385
1447
  font-size: 0.8rem;
@@ -1396,6 +1458,21 @@ function buildAdminHtml(basePath) {
1396
1458
  color: #64748b;
1397
1459
  font-size: 0.9rem;
1398
1460
  }
1461
+
1462
+ .drop-zone {
1463
+ border: 2px dashed #94a3b8;
1464
+ border-radius: 12px;
1465
+ padding: 2rem;
1466
+ text-align: center;
1467
+ color: #64748b;
1468
+ transition: border-color 0.2s, background 0.2s;
1469
+ cursor: pointer;
1470
+ }
1471
+
1472
+ .drop-zone.drag-over {
1473
+ border-color: #6366f1;
1474
+ background: rgba(99, 102, 241, 0.06);
1475
+ }
1399
1476
  </style>
1400
1477
  </head>
1401
1478
  <body>
@@ -1416,69 +1493,70 @@ function buildAdminHtml(basePath) {
1416
1493
  </nav>
1417
1494
 
1418
1495
  <div class="container admin-shell mt-3 pb-4">
1419
- <div class="row g-3">
1420
- <div class="col-lg-3">
1421
- <div class="sidebar-panel">
1422
- <h5 class="mb-3">合集管理</h5>
1423
- <div class="input-group mb-3">
1424
- <input id="new-collection-name" class="form-control" placeholder="新合集名称" />
1425
- <button id="create-collection" class="btn btn-primary">创建</button>
1426
- </div>
1427
- <div id="collection-list" class="list-group mb-3"></div>
1428
- <button id="delete-collection" class="btn btn-outline-danger w-100" disabled>删除当前合集</button>
1429
-
1430
- <hr>
1431
- <h6 class="mb-2">合集描述</h6>
1432
- <div class="input-group mb-3">
1433
- <input id="collection-description" class="form-control" placeholder="为当前合集添加描述" disabled />
1434
- <button id="save-description" class="btn btn-primary" disabled>保存</button>
1435
- </div>
1436
-
1437
- <hr>
1438
- <h6 class="mb-2">快捷信息</h6>
1439
- <div class="small text-muted">
1440
- <div>管理链接:<code>${basePath}/admin</code></div>
1441
- <div class="mt-1">随机访问:<code id="collection-random-url">-</code></div>
1496
+ <!-- View: folder list (main) -->
1497
+ <div id="view-folders">
1498
+ <div class="panel mb-3">
1499
+ <div class="d-flex justify-content-between align-items-center mb-3">
1500
+ <h5 class="mb-0">合集管理</h5>
1501
+ <div class="d-flex gap-2">
1502
+ <input id="new-collection-name" class="form-control form-control-sm" style="width:180px" placeholder="新合集名称" />
1503
+ <button id="create-collection" class="btn btn-sm btn-primary">创建</button>
1442
1504
  </div>
1443
1505
  </div>
1506
+ <div id="folder-grid" class="folder-grid"></div>
1507
+ <div id="folders-empty" class="empty-tip mt-2">暂无合集,请先创建</div>
1444
1508
  </div>
1509
+ </div>
1445
1510
 
1446
- <div class="col-lg-9">
1447
- <div class="main-panel mb-3">
1448
- <div class="d-flex justify-content-between align-items-center">
1449
- <h5 class="mb-0">合集内容</h5>
1450
- <span id="selected-collection-badge" class="badge bg-primary">未选择</span>
1511
+ <!-- View: collection detail -->
1512
+ <div id="view-detail" style="display:none">
1513
+ <div class="panel mb-3">
1514
+ <div class="d-flex justify-content-between align-items-center mb-3">
1515
+ <div class="d-flex align-items-center gap-2">
1516
+ <button id="back-to-folders" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> 返回</button>
1517
+ <h5 class="mb-0" id="detail-title"></h5>
1451
1518
  </div>
1519
+ <div class="d-flex gap-2">
1520
+ <button id="refresh-resources" class="btn btn-sm btn-outline-primary"><i class="bi bi-arrow-clockwise"></i> 刷新缓存</button>
1521
+ <button id="delete-collection" class="btn btn-sm btn-outline-danger">删除合集</button>
1522
+ </div>
1523
+ </div>
1524
+ <div id="detail-path" class="desc-display" style="font-family:monospace;color:#a5b4fc;font-size:0.82rem"></div>
1525
+ <div class="desc-editor">
1526
+ <input id="desc-input" class="form-control form-control-sm" placeholder="为这个合集添加描述,例如:千恋万花的丛雨表情包" />
1527
+ <button id="save-desc" class="btn btn-sm btn-outline-primary">保存描述</button>
1528
+ </div>
1452
1529
 
1453
- <div class="row g-3 mt-1">
1454
- <div class="col-md-6">
1455
- <div class="sub-card h-100">
1456
- <div class="section-title mb-2">上传本地图片</div>
1457
- <input id="upload-files" type="file" class="form-control mb-2" multiple accept="image/*" />
1458
- <button id="upload-images" class="btn btn-primary w-100" disabled>上传到当前合集</button>
1459
- <div class="form-text">支持多选。上传后自动刷新列表。</div>
1530
+ <div class="row g-3 mb-3">
1531
+ <div class="col-md-6">
1532
+ <div class="sub-card h-100">
1533
+ <div class="section-title mb-2">上传图片</div>
1534
+ <div id="drop-zone" class="drop-zone mb-2">
1535
+ <i class="bi bi-cloud-arrow-up" style="font-size:1.5rem"></i>
1536
+ <div>拖放图片到此处,或点击选择文件</div>
1537
+ <input id="upload-files" type="file" class="d-none" multiple accept="image/*" />
1460
1538
  </div>
1461
1539
  </div>
1462
- <div class="col-md-6">
1463
- <div class="sub-card h-100">
1464
- <div class="section-title mb-2">添加外链</div>
1465
- <textarea id="links-input" class="form-control mb-2" rows="4" placeholder="每行一个 http/https 链接"></textarea>
1466
- <button id="add-links" class="btn btn-primary w-100" disabled>添加外链到当前合集</button>
1467
- </div>
1540
+ </div>
1541
+ <div class="col-md-6">
1542
+ <div class="sub-card h-100">
1543
+ <div class="section-title mb-2">添加外链</div>
1544
+ <textarea id="links-input" class="form-control mb-2" rows="3" placeholder="每行一个 http/https 链接"></textarea>
1545
+ <button id="add-links" class="btn btn-primary btn-sm w-100">添加外链</button>
1468
1546
  </div>
1469
1547
  </div>
1548
+ </div>
1470
1549
 
1471
- <div class="sub-card mt-3">
1472
- <div class="section-title mb-2">本地图片</div>
1473
- <div id="images-grid" class="image-grid"></div>
1474
- <div id="images-empty" class="empty-tip mt-2">暂无本地图片</div>
1475
- </div>
1550
+ <div class="sub-card mb-3">
1551
+ <div class="section-title mb-2">本地图片</div>
1552
+ <div id="images-grid" class="image-grid"></div>
1553
+ <div id="images-empty" class="empty-tip mt-2">暂无本地图片</div>
1554
+ </div>
1476
1555
 
1477
- <div class="sub-card mt-3">
1478
- <div class="section-title mb-2">外链列表</div>
1479
- <div id="links-list" class="list-group"></div>
1480
- <div id="links-empty" class="empty-tip mt-2">暂无外链</div>
1481
- </div>
1556
+ <div class="sub-card">
1557
+ <div class="section-title mb-2">外链列表</div>
1558
+ <div id="links-list" class="list-group"></div>
1559
+ <div id="links-empty" class="empty-tip mt-2">暂无外链</div>
1482
1560
  </div>
1483
1561
  </div>
1484
1562
  </div>
@@ -1491,7 +1569,6 @@ function buildAdminHtml(basePath) {
1491
1569
  const state = {
1492
1570
  collectionNames: [],
1493
1571
  collections: [],
1494
- endpoints: [],
1495
1572
  selectedCollection: '',
1496
1573
  images: [],
1497
1574
  links: [],
@@ -1499,173 +1576,184 @@ function buildAdminHtml(basePath) {
1499
1576
 
1500
1577
  const byId = (id) => document.getElementById(id)
1501
1578
 
1502
- function showAlert(message, type = 'info') {
1503
- const el = byId('admin-alert')
1579
+ function showAlert(message, type) {
1580
+ type = type || 'info'
1581
+ var el = byId('admin-alert')
1504
1582
  el.className = 'alert alert-' + type + ' mt-3'
1505
1583
  el.textContent = message
1506
1584
  el.classList.remove('d-none')
1507
- setTimeout(() => el.classList.add('d-none'), 2200)
1585
+ setTimeout(function() { el.classList.add('d-none') }, 2200)
1508
1586
  }
1509
1587
 
1510
- async function request(url, options = {}) {
1511
- console.log('Requesting:', url)
1512
- const headers = Object.assign({}, options.headers || {})
1588
+ async function request(url, options) {
1589
+ options = options || {}
1590
+ var headers = Object.assign({}, options.headers || {})
1513
1591
  if (options.body && !headers['Content-Type']) {
1514
1592
  headers['Content-Type'] = 'application/json'
1515
1593
  }
1516
- const res = await fetch(url, Object.assign({}, options, { headers }))
1517
- console.log('Response status:', res.status)
1518
- let data = null
1519
- try {
1520
- data = await res.json()
1521
- console.log('Response data:', data)
1522
- } catch (e) {
1523
- console.log('JSON parse error:', e)
1524
- data = null
1525
- }
1594
+ var res = await fetch(url, Object.assign({}, options, { headers: headers }))
1595
+ var data = null
1596
+ try { data = await res.json() } catch(e) { data = null }
1526
1597
  if (!res.ok) {
1527
1598
  throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
1528
1599
  }
1529
1600
  return data
1530
1601
  }
1531
1602
 
1603
+ function readFileAsDataURL(file) {
1604
+ return new Promise(function(resolve, reject) {
1605
+ var reader = new FileReader()
1606
+ reader.onload = function() { resolve(reader.result) }
1607
+ reader.onerror = function() { reject(new Error('读取文件失败')) }
1608
+ reader.readAsDataURL(file)
1609
+ })
1610
+ }
1611
+
1612
+ /* --- Folder list view --- */
1532
1613
  async function refreshState() {
1533
- const data = await request(BASE_PATH + '/api/admin/state')
1614
+ var data = await request(BASE_PATH + '/api/admin/state')
1534
1615
  state.collectionNames = Array.isArray(data.collectionNames) ? data.collectionNames : []
1535
1616
  state.collections = Array.isArray(data.collections) ? data.collections : []
1536
- state.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
1617
+ renderFolderGrid()
1618
+ }
1537
1619
 
1538
- if (!state.selectedCollection || !state.collectionNames.includes(state.selectedCollection)) {
1539
- state.selectedCollection = state.collectionNames[0] || ''
1540
- }
1620
+ function renderFolderGrid() {
1621
+ var grid = byId('folder-grid')
1622
+ var empty = byId('folders-empty')
1623
+ grid.textContent = ''
1624
+ empty.style.display = state.collections.length ? 'none' : 'block'
1625
+
1626
+ state.collections.forEach(function(col) {
1627
+ var card = document.createElement('div')
1628
+ card.className = 'folder-card'
1629
+ card.addEventListener('click', function() { enterCollection(col.name) })
1630
+
1631
+ var icon = document.createElement('div')
1632
+ icon.className = 'folder-icon'
1633
+ icon.innerHTML = '<i class="bi bi-folder-fill"></i>'
1634
+ card.appendChild(icon)
1635
+
1636
+ var info = document.createElement('div')
1637
+ info.className = 'folder-info'
1638
+ var nameEl = document.createElement('div')
1639
+ nameEl.className = 'folder-name'
1640
+ nameEl.textContent = col.name
1641
+ info.appendChild(nameEl)
1642
+ if (col.description) {
1643
+ var descEl = document.createElement('div')
1644
+ descEl.className = 'folder-desc'
1645
+ descEl.textContent = col.description
1646
+ descEl.title = col.description
1647
+ info.appendChild(descEl)
1648
+ }
1649
+ var pathEl = document.createElement('div')
1650
+ pathEl.className = 'folder-path'
1651
+ pathEl.textContent = BASE_PATH + '/' + col.name
1652
+ info.appendChild(pathEl)
1653
+ var meta = document.createElement('div')
1654
+ meta.className = 'folder-meta'
1655
+ meta.textContent = '本地 ' + (col.localCount || 0) + ' / 外链 ' + (col.linkCount || 0)
1656
+ info.appendChild(meta)
1657
+ card.appendChild(info)
1658
+ grid.appendChild(card)
1659
+ })
1660
+ }
1541
1661
 
1542
- renderCollectionList()
1543
- await refreshCollectionResources()
1544
- syncSelectedCollectionUi()
1662
+ function enterCollection(name) {
1663
+ state.selectedCollection = name
1664
+ byId('view-folders').style.display = 'none'
1665
+ byId('view-detail').style.display = 'block'
1666
+ byId('detail-title').textContent = name
1667
+ byId('detail-path').textContent = '端点路径: ' + BASE_PATH + '/' + name
1668
+ var col = state.collections.find(function(c) { return c.name === name })
1669
+ byId('desc-input').value = (col && col.description) ? col.description : ''
1670
+ refreshCollectionResources()
1545
1671
  }
1546
1672
 
1673
+ function backToFolders() {
1674
+ state.selectedCollection = ''
1675
+ byId('view-detail').style.display = 'none'
1676
+ byId('view-folders').style.display = 'block'
1677
+ refreshState()
1678
+ }
1679
+
1680
+ /* --- Detail view --- */
1547
1681
  async function refreshCollectionResources() {
1548
- if (!state.selectedCollection) {
1549
- state.images = []
1550
- state.links = []
1551
- renderImages()
1552
- renderLinks()
1553
- return
1554
- }
1555
- const data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
1682
+ if (!state.selectedCollection) { state.images = []; state.links = []; renderImages(); renderLinks(); return }
1683
+ var data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
1556
1684
  state.images = Array.isArray(data.images) ? data.images : []
1557
1685
  state.links = Array.isArray(data.links) ? data.links : []
1558
1686
  renderImages()
1559
1687
  renderLinks()
1560
1688
  }
1561
1689
 
1562
- function syncSelectedCollectionUi() {
1563
- const selected = state.selectedCollection
1564
- byId('selected-collection-badge').textContent = selected || '未选择'
1565
- byId('delete-collection').disabled = !selected
1566
- byId('upload-images').disabled = !selected
1567
- byId('add-links').disabled = !selected
1568
- byId('collection-random-url').textContent = selected ? BASE_PATH + '/' + selected : '-'
1569
- byId('collection-description').disabled = !selected
1570
- byId('save-description').disabled = !selected
1571
-
1572
- const collectionInfo = state.collections.find((c) => c.name === selected)
1573
- byId('collection-description').value = collectionInfo ? (collectionInfo.description || '') : ''
1574
- }
1575
-
1576
- function renderCollectionList() {
1577
- const list = byId('collection-list')
1578
- list.textContent = ''
1579
- if (!state.collectionNames.length) {
1580
- const empty = document.createElement('div')
1581
- empty.className = 'text-muted small'
1582
- empty.textContent = '暂无合集'
1583
- list.appendChild(empty)
1584
- return
1585
- }
1586
- state.collectionNames.forEach((name) => {
1587
- const button = document.createElement('button')
1588
- button.type = 'button'
1589
- button.className = 'list-group-item list-group-item-action collection-item' + (name === state.selectedCollection ? ' active' : '')
1590
- button.textContent = name
1591
- button.addEventListener('click', async () => {
1592
- state.selectedCollection = name
1593
- renderCollectionList()
1594
- syncSelectedCollectionUi()
1595
- await refreshCollectionResources()
1596
- })
1597
- list.appendChild(button)
1598
- })
1599
- }
1600
-
1601
1690
  function imagePreviewUrl(filename) {
1602
1691
  return BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(filename)
1603
1692
  }
1604
1693
 
1605
1694
  function renderImages() {
1606
- const grid = byId('images-grid')
1607
- const empty = byId('images-empty')
1695
+ var grid = byId('images-grid')
1696
+ var empty = byId('images-empty')
1608
1697
  grid.textContent = ''
1609
1698
  empty.style.display = state.images.length ? 'none' : 'block'
1610
1699
 
1611
- state.images.forEach((name) => {
1612
- const card = document.createElement('div')
1700
+ state.images.forEach(function(name) {
1701
+ var card = document.createElement('div')
1613
1702
  card.className = 'image-card'
1614
1703
 
1615
- const img = document.createElement('img')
1704
+ var img = document.createElement('img')
1616
1705
  img.src = imagePreviewUrl(name)
1617
1706
  img.alt = name
1618
1707
  card.appendChild(img)
1619
1708
 
1620
- const body = document.createElement('div')
1709
+ var body = document.createElement('div')
1621
1710
  body.className = 'p-2'
1622
- const title = document.createElement('div')
1711
+ var title = document.createElement('div')
1623
1712
  title.className = 'small text-truncate mb-2'
1624
1713
  title.textContent = name
1625
1714
  body.appendChild(title)
1626
1715
 
1627
- const row = document.createElement('div')
1716
+ var row = document.createElement('div')
1628
1717
  row.className = 'd-flex gap-1'
1629
1718
 
1630
- const moveSelect = document.createElement('select')
1719
+ var moveSelect = document.createElement('select')
1631
1720
  moveSelect.className = 'form-select form-select-sm'
1632
- const collections = state.collectionNames.filter((item) => item !== state.selectedCollection)
1633
- const placeholder = document.createElement('option')
1634
- placeholder.value = ''
1635
- placeholder.textContent = '移动到...'
1636
- moveSelect.appendChild(placeholder)
1637
- collections.forEach((target) => {
1638
- const opt = document.createElement('option')
1639
- opt.value = target
1640
- opt.textContent = target
1721
+ var others = state.collectionNames.filter(function(c) { return c !== state.selectedCollection })
1722
+ var ph = document.createElement('option')
1723
+ ph.value = ''
1724
+ ph.textContent = '移动到...'
1725
+ moveSelect.appendChild(ph)
1726
+ others.forEach(function(t) {
1727
+ var opt = document.createElement('option')
1728
+ opt.value = t; opt.textContent = t
1641
1729
  moveSelect.appendChild(opt)
1642
1730
  })
1643
1731
 
1644
- const moveBtn = document.createElement('button')
1732
+ var moveBtn = document.createElement('button')
1645
1733
  moveBtn.className = 'btn btn-sm btn-outline-primary'
1646
1734
  moveBtn.textContent = '移动'
1647
- moveBtn.disabled = collections.length === 0
1648
- moveBtn.addEventListener('click', async () => {
1649
- const targetCollection = moveSelect.value
1650
- if (!targetCollection) return
1735
+ moveBtn.disabled = others.length === 0
1736
+ moveBtn.addEventListener('click', async function() {
1737
+ var tc = moveSelect.value
1738
+ if (!tc) return
1651
1739
  await request(
1652
1740
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name) + '/move',
1653
- { method: 'POST', body: JSON.stringify({ targetCollection }) }
1741
+ { method: 'POST', body: JSON.stringify({ targetCollection: tc }) }
1654
1742
  )
1655
1743
  showAlert('图片已移动', 'success')
1656
- await refreshState()
1744
+ refreshCollectionResources()
1657
1745
  })
1658
1746
 
1659
- const delBtn = document.createElement('button')
1747
+ var delBtn = document.createElement('button')
1660
1748
  delBtn.className = 'btn btn-sm btn-outline-danger'
1661
1749
  delBtn.textContent = '删除'
1662
- delBtn.addEventListener('click', async () => {
1750
+ delBtn.addEventListener('click', async function() {
1663
1751
  await request(
1664
1752
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name),
1665
1753
  { method: 'DELETE' }
1666
1754
  )
1667
1755
  showAlert('图片已删除', 'success')
1668
- await refreshCollectionResources()
1756
+ refreshCollectionResources()
1669
1757
  })
1670
1758
 
1671
1759
  row.appendChild(moveSelect)
@@ -1678,40 +1766,40 @@ function buildAdminHtml(basePath) {
1678
1766
  }
1679
1767
 
1680
1768
  function renderLinks() {
1681
- const wrap = byId('links-list')
1682
- const empty = byId('links-empty')
1769
+ var wrap = byId('links-list')
1770
+ var empty = byId('links-empty')
1683
1771
  wrap.textContent = ''
1684
1772
  empty.style.display = state.links.length ? 'none' : 'block'
1685
- state.links.forEach((link) => {
1686
- const row = document.createElement('div')
1773
+ state.links.forEach(function(link) {
1774
+ var row = document.createElement('div')
1687
1775
  row.className = 'list-group-item d-flex justify-content-between align-items-center gap-2'
1688
1776
 
1689
- const text = document.createElement('div')
1777
+ var text = document.createElement('div')
1690
1778
  text.className = 'code-url flex-grow-1'
1691
1779
  text.textContent = link
1692
1780
 
1693
- const actions = document.createElement('div')
1781
+ var actions = document.createElement('div')
1694
1782
  actions.className = 'd-flex gap-1'
1695
1783
 
1696
- const open = document.createElement('a')
1697
- open.className = 'btn btn-sm btn-outline-primary'
1698
- open.textContent = '查看'
1699
- open.href = link
1700
- open.target = '_blank'
1784
+ var openEl = document.createElement('a')
1785
+ openEl.className = 'btn btn-sm btn-outline-primary'
1786
+ openEl.textContent = '查看'
1787
+ openEl.href = link
1788
+ openEl.target = '_blank'
1701
1789
 
1702
- const del = document.createElement('button')
1790
+ var del = document.createElement('button')
1703
1791
  del.className = 'btn btn-sm btn-outline-danger'
1704
1792
  del.textContent = '删除'
1705
- del.addEventListener('click', async () => {
1793
+ del.addEventListener('click', async function() {
1706
1794
  await request(
1707
1795
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links',
1708
- { method: 'DELETE', body: JSON.stringify({ link }) }
1796
+ { method: 'DELETE', body: JSON.stringify({ link: link }) }
1709
1797
  )
1710
1798
  showAlert('外链已删除', 'success')
1711
- await refreshCollectionResources()
1799
+ refreshCollectionResources()
1712
1800
  })
1713
1801
 
1714
- actions.appendChild(open)
1802
+ actions.appendChild(openEl)
1715
1803
  actions.appendChild(del)
1716
1804
  row.appendChild(text)
1717
1805
  row.appendChild(actions)
@@ -1719,87 +1807,93 @@ function buildAdminHtml(basePath) {
1719
1807
  })
1720
1808
  }
1721
1809
 
1722
- function readFileAsDataURL(file) {
1723
- return new Promise((resolve, reject) => {
1724
- const reader = new FileReader()
1725
- reader.onload = () => resolve(reader.result)
1726
- reader.onerror = () => reject(new Error('读取文件失败'))
1727
- reader.readAsDataURL(file)
1810
+ /* --- Upload with drag/drop --- */
1811
+ async function uploadFiles(files) {
1812
+ if (!state.selectedCollection || !files || !files.length) return
1813
+ var images = []
1814
+ for (var i = 0; i < files.length; i++) {
1815
+ var b64 = await readFileAsDataURL(files[i])
1816
+ images.push({ base64: b64, originalName: files[i].name })
1817
+ }
1818
+ await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images', {
1819
+ method: 'POST',
1820
+ body: JSON.stringify({ images: images }),
1728
1821
  })
1822
+ showAlert('图片上传成功', 'success')
1823
+ refreshCollectionResources()
1729
1824
  }
1730
1825
 
1731
- byId('create-collection').addEventListener('click', async () => {
1732
- const name = byId('new-collection-name').value.trim()
1826
+ var dropZone = byId('drop-zone')
1827
+ var fileInput = byId('upload-files')
1828
+
1829
+ dropZone.addEventListener('click', function() { fileInput.click() })
1830
+ fileInput.addEventListener('change', function() { uploadFiles(fileInput.files); fileInput.value = '' })
1831
+
1832
+ dropZone.addEventListener('dragover', function(e) { e.preventDefault(); dropZone.classList.add('drag-over') })
1833
+ dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); dropZone.classList.remove('drag-over') })
1834
+ dropZone.addEventListener('drop', function(e) {
1835
+ e.preventDefault()
1836
+ dropZone.classList.remove('drag-over')
1837
+ if (e.dataTransfer && e.dataTransfer.files) uploadFiles(e.dataTransfer.files)
1838
+ })
1839
+
1840
+ /* --- Buttons --- */
1841
+ byId('create-collection').addEventListener('click', async function() {
1842
+ var name = byId('new-collection-name').value.trim()
1733
1843
  if (!name) return
1734
1844
  await request(BASE_PATH + '/api/admin/collections', {
1735
1845
  method: 'POST',
1736
- body: JSON.stringify({ name }),
1846
+ body: JSON.stringify({ name: name }),
1737
1847
  })
1738
1848
  byId('new-collection-name').value = ''
1739
1849
  showAlert('合集创建成功', 'success')
1740
- await refreshState()
1850
+ refreshState()
1741
1851
  })
1742
1852
 
1743
- byId('delete-collection').addEventListener('click', async () => {
1853
+ byId('delete-collection').addEventListener('click', async function() {
1744
1854
  if (!state.selectedCollection) return
1855
+ if (!confirm('确定删除合集 ' + state.selectedCollection + ' ?')) return
1745
1856
  await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection), {
1746
1857
  method: 'DELETE',
1747
1858
  })
1748
1859
  showAlert('合集已删除', 'success')
1749
- await refreshState()
1860
+ backToFolders()
1750
1861
  })
1751
1862
 
1752
- byId('save-description').addEventListener('click', async () => {
1863
+ byId('back-to-folders').addEventListener('click', backToFolders)
1864
+
1865
+ byId('save-desc').addEventListener('click', async function() {
1753
1866
  if (!state.selectedCollection) return
1754
- const description = byId('collection-description').value.trim()
1867
+ var desc = byId('desc-input').value.trim()
1755
1868
  await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/description', {
1756
1869
  method: 'PATCH',
1757
- body: JSON.stringify({ description }),
1870
+ body: JSON.stringify({ description: desc }),
1758
1871
  })
1872
+ var col = state.collections.find(function(c) { return c.name === state.selectedCollection })
1873
+ if (col) col.description = desc
1759
1874
  showAlert('描述已保存', 'success')
1760
- await refreshState()
1761
1875
  })
1762
1876
 
1763
- byId('upload-images').addEventListener('click', async () => {
1764
- if (!state.selectedCollection) return
1765
- const files = byId('upload-files').files
1766
- if (!files || !files.length) return
1767
- const images = []
1768
- for (const file of files) {
1769
- const base64 = await readFileAsDataURL(file)
1770
- images.push({ base64, originalName: file.name })
1771
- }
1772
- await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images', {
1773
- method: 'POST',
1774
- body: JSON.stringify({ images }),
1775
- })
1776
- byId('upload-files').value = ''
1777
- showAlert('图片上传成功', 'success')
1778
- await refreshCollectionResources()
1779
- await refreshState()
1877
+ byId('refresh-resources').addEventListener('click', function() {
1878
+ refreshCollectionResources()
1879
+ showAlert('已刷新', 'success')
1780
1880
  })
1781
1881
 
1782
- byId('add-links').addEventListener('click', async () => {
1882
+ byId('add-links').addEventListener('click', async function() {
1783
1883
  if (!state.selectedCollection) return
1784
- const text = byId('links-input').value
1785
- const links = text.split(/\r?
1786
- /g).map((line) => line.trim()).filter(Boolean)
1787
- if (!links.length) return
1884
+ var text = byId('links-input').value
1885
+ var lines = text.split(/\\r?\\n/g).map(function(l) { return l.trim() }).filter(Boolean)
1886
+ if (!lines.length) return
1788
1887
  await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links', {
1789
1888
  method: 'POST',
1790
- body: JSON.stringify({ links }),
1889
+ body: JSON.stringify({ links: lines }),
1791
1890
  })
1792
1891
  byId('links-input').value = ''
1793
1892
  showAlert('外链添加成功', 'success')
1794
- await refreshCollectionResources()
1795
- await refreshState()
1796
- })
1797
-
1798
- byId('upload-files').addEventListener('change', () => {
1799
- byId('upload-images').disabled = !state.selectedCollection
1893
+ refreshCollectionResources()
1800
1894
  })
1801
1895
 
1802
- refreshState().catch((error) => {
1896
+ refreshState().catch(function(error) {
1803
1897
  showAlert(error instanceof Error ? error.message : String(error), 'danger')
1804
1898
  })
1805
1899
  </script>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-memesluna",
3
3
  "description": "Image Forward service for Koishi with ChatLuna integration",
4
- "version": "0.2.6",
4
+ "version": "0.2.8",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",