koishi-plugin-memesluna 0.2.6 → 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.
Files changed (2) hide show
  1. package/lib/index.js +268 -238
  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,69 @@ 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;
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>
@@ -1416,69 +1460,65 @@ function buildAdminHtml(basePath) {
1416
1460
  </nav>
1417
1461
 
1418
1462
  <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>
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>
1442
1471
  </div>
1443
1472
  </div>
1473
+ <div id="folder-grid" class="folder-grid"></div>
1474
+ <div id="folders-empty" class="empty-tip mt-2">暂无合集,请先创建</div>
1444
1475
  </div>
1476
+ </div>
1445
1477
 
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>
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>
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>
1451
1489
  </div>
1490
+ </div>
1452
1491
 
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>
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/*" />
1460
1500
  </div>
1461
1501
  </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>
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>
1468
1508
  </div>
1469
1509
  </div>
1510
+ </div>
1470
1511
 
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>
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>
1476
1517
 
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>
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>
1482
1522
  </div>
1483
1523
  </div>
1484
1524
  </div>
@@ -1491,7 +1531,6 @@ function buildAdminHtml(basePath) {
1491
1531
  const state = {
1492
1532
  collectionNames: [],
1493
1533
  collections: [],
1494
- endpoints: [],
1495
1534
  selectedCollection: '',
1496
1535
  images: [],
1497
1536
  links: [],
@@ -1499,173 +1538,170 @@ function buildAdminHtml(basePath) {
1499
1538
 
1500
1539
  const byId = (id) => document.getElementById(id)
1501
1540
 
1502
- function showAlert(message, type = 'info') {
1503
- const el = byId('admin-alert')
1541
+ function showAlert(message, type) {
1542
+ type = type || 'info'
1543
+ var el = byId('admin-alert')
1504
1544
  el.className = 'alert alert-' + type + ' mt-3'
1505
1545
  el.textContent = message
1506
1546
  el.classList.remove('d-none')
1507
- setTimeout(() => el.classList.add('d-none'), 2200)
1547
+ setTimeout(function() { el.classList.add('d-none') }, 2200)
1508
1548
  }
1509
1549
 
1510
- async function request(url, options = {}) {
1511
- console.log('Requesting:', url)
1512
- const headers = Object.assign({}, options.headers || {})
1550
+ async function request(url, options) {
1551
+ options = options || {}
1552
+ var headers = Object.assign({}, options.headers || {})
1513
1553
  if (options.body && !headers['Content-Type']) {
1514
1554
  headers['Content-Type'] = 'application/json'
1515
1555
  }
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
- }
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 }
1526
1559
  if (!res.ok) {
1527
1560
  throw new Error(data && data.error ? data.error : String(res.status) + ' ' + String(res.statusText))
1528
1561
  }
1529
1562
  return data
1530
1563
  }
1531
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 --- */
1532
1575
  async function refreshState() {
1533
- const data = await request(BASE_PATH + '/api/admin/state')
1576
+ var data = await request(BASE_PATH + '/api/admin/state')
1534
1577
  state.collectionNames = Array.isArray(data.collectionNames) ? data.collectionNames : []
1535
1578
  state.collections = Array.isArray(data.collections) ? data.collections : []
1536
- state.endpoints = Array.isArray(data.endpoints) ? data.endpoints : []
1579
+ renderFolderGrid()
1580
+ }
1537
1581
 
1538
- if (!state.selectedCollection || !state.collectionNames.includes(state.selectedCollection)) {
1539
- state.selectedCollection = state.collectionNames[0] || ''
1540
- }
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
+ }
1541
1620
 
1542
- renderCollectionList()
1543
- await refreshCollectionResources()
1544
- syncSelectedCollectionUi()
1621
+ function backToFolders() {
1622
+ state.selectedCollection = ''
1623
+ byId('view-detail').style.display = 'none'
1624
+ byId('view-folders').style.display = 'block'
1625
+ refreshState()
1545
1626
  }
1546
1627
 
1628
+ /* --- Detail view --- */
1547
1629
  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')
1630
+ if (!state.selectedCollection) { state.images = []; state.links = []; renderImages(); renderLinks(); return }
1631
+ var data = await request(BASE_PATH + '/api/collections/' + encodeURIComponent(state.selectedCollection) + '/resources')
1556
1632
  state.images = Array.isArray(data.images) ? data.images : []
1557
1633
  state.links = Array.isArray(data.links) ? data.links : []
1558
1634
  renderImages()
1559
1635
  renderLinks()
1560
1636
  }
1561
1637
 
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
1638
  function imagePreviewUrl(filename) {
1602
1639
  return BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(filename)
1603
1640
  }
1604
1641
 
1605
1642
  function renderImages() {
1606
- const grid = byId('images-grid')
1607
- const empty = byId('images-empty')
1643
+ var grid = byId('images-grid')
1644
+ var empty = byId('images-empty')
1608
1645
  grid.textContent = ''
1609
1646
  empty.style.display = state.images.length ? 'none' : 'block'
1610
1647
 
1611
- state.images.forEach((name) => {
1612
- const card = document.createElement('div')
1648
+ state.images.forEach(function(name) {
1649
+ var card = document.createElement('div')
1613
1650
  card.className = 'image-card'
1614
1651
 
1615
- const img = document.createElement('img')
1652
+ var img = document.createElement('img')
1616
1653
  img.src = imagePreviewUrl(name)
1617
1654
  img.alt = name
1618
1655
  card.appendChild(img)
1619
1656
 
1620
- const body = document.createElement('div')
1657
+ var body = document.createElement('div')
1621
1658
  body.className = 'p-2'
1622
- const title = document.createElement('div')
1659
+ var title = document.createElement('div')
1623
1660
  title.className = 'small text-truncate mb-2'
1624
1661
  title.textContent = name
1625
1662
  body.appendChild(title)
1626
1663
 
1627
- const row = document.createElement('div')
1664
+ var row = document.createElement('div')
1628
1665
  row.className = 'd-flex gap-1'
1629
1666
 
1630
- const moveSelect = document.createElement('select')
1667
+ var moveSelect = document.createElement('select')
1631
1668
  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
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
1641
1677
  moveSelect.appendChild(opt)
1642
1678
  })
1643
1679
 
1644
- const moveBtn = document.createElement('button')
1680
+ var moveBtn = document.createElement('button')
1645
1681
  moveBtn.className = 'btn btn-sm btn-outline-primary'
1646
1682
  moveBtn.textContent = '移动'
1647
- moveBtn.disabled = collections.length === 0
1648
- moveBtn.addEventListener('click', async () => {
1649
- const targetCollection = moveSelect.value
1650
- if (!targetCollection) return
1683
+ moveBtn.disabled = others.length === 0
1684
+ moveBtn.addEventListener('click', async function() {
1685
+ var tc = moveSelect.value
1686
+ if (!tc) return
1651
1687
  await request(
1652
1688
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name) + '/move',
1653
- { method: 'POST', body: JSON.stringify({ targetCollection }) }
1689
+ { method: 'POST', body: JSON.stringify({ targetCollection: tc }) }
1654
1690
  )
1655
1691
  showAlert('图片已移动', 'success')
1656
- await refreshState()
1692
+ refreshCollectionResources()
1657
1693
  })
1658
1694
 
1659
- const delBtn = document.createElement('button')
1695
+ var delBtn = document.createElement('button')
1660
1696
  delBtn.className = 'btn btn-sm btn-outline-danger'
1661
1697
  delBtn.textContent = '删除'
1662
- delBtn.addEventListener('click', async () => {
1698
+ delBtn.addEventListener('click', async function() {
1663
1699
  await request(
1664
1700
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/images/' + encodeURIComponent(name),
1665
1701
  { method: 'DELETE' }
1666
1702
  )
1667
1703
  showAlert('图片已删除', 'success')
1668
- await refreshCollectionResources()
1704
+ refreshCollectionResources()
1669
1705
  })
1670
1706
 
1671
1707
  row.appendChild(moveSelect)
@@ -1678,40 +1714,40 @@ function buildAdminHtml(basePath) {
1678
1714
  }
1679
1715
 
1680
1716
  function renderLinks() {
1681
- const wrap = byId('links-list')
1682
- const empty = byId('links-empty')
1717
+ var wrap = byId('links-list')
1718
+ var empty = byId('links-empty')
1683
1719
  wrap.textContent = ''
1684
1720
  empty.style.display = state.links.length ? 'none' : 'block'
1685
- state.links.forEach((link) => {
1686
- const row = document.createElement('div')
1721
+ state.links.forEach(function(link) {
1722
+ var row = document.createElement('div')
1687
1723
  row.className = 'list-group-item d-flex justify-content-between align-items-center gap-2'
1688
1724
 
1689
- const text = document.createElement('div')
1725
+ var text = document.createElement('div')
1690
1726
  text.className = 'code-url flex-grow-1'
1691
1727
  text.textContent = link
1692
1728
 
1693
- const actions = document.createElement('div')
1729
+ var actions = document.createElement('div')
1694
1730
  actions.className = 'd-flex gap-1'
1695
1731
 
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'
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'
1701
1737
 
1702
- const del = document.createElement('button')
1738
+ var del = document.createElement('button')
1703
1739
  del.className = 'btn btn-sm btn-outline-danger'
1704
1740
  del.textContent = '删除'
1705
- del.addEventListener('click', async () => {
1741
+ del.addEventListener('click', async function() {
1706
1742
  await request(
1707
1743
  BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links',
1708
- { method: 'DELETE', body: JSON.stringify({ link }) }
1744
+ { method: 'DELETE', body: JSON.stringify({ link: link }) }
1709
1745
  )
1710
1746
  showAlert('外链已删除', 'success')
1711
- await refreshCollectionResources()
1747
+ refreshCollectionResources()
1712
1748
  })
1713
1749
 
1714
- actions.appendChild(open)
1750
+ actions.appendChild(openEl)
1715
1751
  actions.appendChild(del)
1716
1752
  row.appendChild(text)
1717
1753
  row.appendChild(actions)
@@ -1719,87 +1755,81 @@ function buildAdminHtml(basePath) {
1719
1755
  })
1720
1756
  }
1721
1757
 
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)
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 }),
1728
1769
  })
1770
+ showAlert('图片上传成功', 'success')
1771
+ refreshCollectionResources()
1729
1772
  }
1730
1773
 
1731
- byId('create-collection').addEventListener('click', async () => {
1732
- const name = byId('new-collection-name').value.trim()
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()
1733
1791
  if (!name) return
1734
1792
  await request(BASE_PATH + '/api/admin/collections', {
1735
1793
  method: 'POST',
1736
- body: JSON.stringify({ name }),
1794
+ body: JSON.stringify({ name: name }),
1737
1795
  })
1738
1796
  byId('new-collection-name').value = ''
1739
1797
  showAlert('合集创建成功', 'success')
1740
- await refreshState()
1798
+ refreshState()
1741
1799
  })
1742
1800
 
1743
- byId('delete-collection').addEventListener('click', async () => {
1801
+ byId('delete-collection').addEventListener('click', async function() {
1744
1802
  if (!state.selectedCollection) return
1803
+ if (!confirm('确定删除合集 ' + state.selectedCollection + ' ?')) return
1745
1804
  await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection), {
1746
1805
  method: 'DELETE',
1747
1806
  })
1748
1807
  showAlert('合集已删除', 'success')
1749
- await refreshState()
1808
+ backToFolders()
1750
1809
  })
1751
1810
 
1752
- byId('save-description').addEventListener('click', async () => {
1753
- if (!state.selectedCollection) return
1754
- const description = byId('collection-description').value.trim()
1755
- await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/description', {
1756
- method: 'PATCH',
1757
- body: JSON.stringify({ description }),
1758
- })
1759
- showAlert('描述已保存', 'success')
1760
- await refreshState()
1761
- })
1811
+ byId('back-to-folders').addEventListener('click', backToFolders)
1762
1812
 
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()
1813
+ byId('refresh-resources').addEventListener('click', function() {
1814
+ refreshCollectionResources()
1815
+ showAlert('已刷新', 'success')
1780
1816
  })
1781
1817
 
1782
- byId('add-links').addEventListener('click', async () => {
1818
+ byId('add-links').addEventListener('click', async function() {
1783
1819
  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
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
1788
1823
  await request(BASE_PATH + '/api/admin/collections/' + encodeURIComponent(state.selectedCollection) + '/links', {
1789
1824
  method: 'POST',
1790
- body: JSON.stringify({ links }),
1825
+ body: JSON.stringify({ links: lines }),
1791
1826
  })
1792
1827
  byId('links-input').value = ''
1793
1828
  showAlert('外链添加成功', 'success')
1794
- await refreshCollectionResources()
1795
- await refreshState()
1796
- })
1797
-
1798
- byId('upload-files').addEventListener('change', () => {
1799
- byId('upload-images').disabled = !state.selectedCollection
1829
+ refreshCollectionResources()
1800
1830
  })
1801
1831
 
1802
- refreshState().catch((error) => {
1832
+ refreshState().catch(function(error) {
1803
1833
  showAlert(error instanceof Error ? error.message : String(error), 'danger')
1804
1834
  })
1805
1835
  </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.7",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "types": "lib/index.d.ts",