feat: 新增运动节拍训练工具套件与子工具页面
新增运动节拍RepClock全套小程序页面、后端API与数据库服务,新增subhub子工具汇总页面,优化全局主页样式与配置,为web静态资源添加版本缓存参数,补充配套调试脚本
This commit is contained in:
@@ -1,41 +1,80 @@
|
||||
.detail-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: rgba(32, 32, 52, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 18px;
|
||||
padding: 24px 26px;
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
|
||||
transition: border-color 0.25s ease, transform 0.25s ease;
|
||||
}
|
||||
.detail-card:hover {
|
||||
border-color: rgba(108, 122, 224, 0.22);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #8a8aa8;
|
||||
color: #9a9abd;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.detail-section-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(180deg, #6c7ae0 0%, #b06be0 100%);
|
||||
box-shadow: 0 0 10px rgba(108, 122, 224, 0.5);
|
||||
}
|
||||
|
||||
.detail-card-title {
|
||||
background: linear-gradient(145deg, rgba(52, 52, 86, 0.75) 0%, rgba(36, 36, 60, 0.7) 100%);
|
||||
border: 1px solid rgba(108, 122, 224, 0.22);
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
.detail-title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
color: #f4f4fa;
|
||||
letter-spacing: 0.2px;
|
||||
color: #fafaff;
|
||||
letter-spacing: 0.3px;
|
||||
text-shadow: 0 2px 14px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.detail-title.empty {
|
||||
color: #6a6a88;
|
||||
color: #787898;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
font-size: 22px;
|
||||
font-style: italic;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.detail-card-meta {
|
||||
background: rgba(28, 28, 46, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.detail-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 13px;
|
||||
color: #8a8aa8;
|
||||
margin-top: 14px;
|
||||
font-size: 13.5px;
|
||||
color: #9a9abd;
|
||||
}
|
||||
.detail-meta span b {
|
||||
color: #c8c8e0;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.detail-tags {
|
||||
@@ -45,24 +84,30 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.detail-card-remark {
|
||||
background: linear-gradient(145deg, rgba(44, 40, 60, 0.75) 0%, rgba(34, 30, 50, 0.7) 100%);
|
||||
border: 1px solid rgba(180, 150, 220, 0.16);
|
||||
}
|
||||
.detail-remark {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
color: #dcdceb;
|
||||
font-size: 15px;
|
||||
line-height: 1.9;
|
||||
color: #e4e4f0;
|
||||
font-size: 15.5px;
|
||||
line-height: 2;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.detail-remark.empty {
|
||||
color: #6a6a88;
|
||||
color: #787898;
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
padding: 40px 24px;
|
||||
padding: 28px 24px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.detail-card-images {
|
||||
background: rgba(24, 24, 42, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
@@ -74,27 +119,36 @@
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
cursor: zoom-in;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
transition: transform 0.25s ease, border-color 0.25s ease;
|
||||
transition: transform 0.25s ease, border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
.image-item:hover {
|
||||
transform: translateY(-3px);
|
||||
border-color: rgba(108, 122, 224, 0.45);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.image-item:hover { transform: translateY(-2px); border-color: rgba(108, 122, 224, 0.4); }
|
||||
.image-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.image-item .name-badge {
|
||||
position: absolute;
|
||||
left: 8px; bottom: 8px;
|
||||
max-width: calc(100% - 16px);
|
||||
padding: 4px 10px;
|
||||
padding: 5px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
font-size: 11.5px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
color: #fff;
|
||||
backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(6px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.detail-card-files {
|
||||
background: rgba(24, 24, 42, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.file-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -104,51 +158,58 @@
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 18px;
|
||||
background: rgba(108, 122, 224, 0.06);
|
||||
border: 1px solid rgba(108, 122, 224, 0.16);
|
||||
background: rgba(40, 44, 72, 0.5);
|
||||
border: 1px solid rgba(108, 122, 224, 0.14);
|
||||
border-radius: 14px;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
.file-item:hover {
|
||||
background: rgba(108, 122, 224, 0.12);
|
||||
border-color: rgba(108, 122, 224, 0.32);
|
||||
background: rgba(52, 58, 96, 0.6);
|
||||
border-color: rgba(108, 122, 224, 0.34);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.file-icon {
|
||||
width: 44px; height: 44px;
|
||||
width: 46px; height: 46px;
|
||||
border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(108, 122, 224, 0.18);
|
||||
background: linear-gradient(145deg, rgba(108, 122, 224, 0.3) 0%, rgba(160, 110, 224, 0.25) 100%);
|
||||
flex-shrink: 0;
|
||||
font-size: 22px;
|
||||
border: 1px solid rgba(108, 122, 224, 0.25);
|
||||
}
|
||||
.file-info { flex: 1; min-width: 0; }
|
||||
.file-name {
|
||||
font-size: 14.5px;
|
||||
font-weight: 500;
|
||||
color: #e8e8f4;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #ececf6;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.file-meta { font-size: 12px; color: #8a8aa8; margin-top: 4px; }
|
||||
.file-meta { font-size: 12.5px; color: #9090b0; margin-top: 5px; }
|
||||
.file-download {
|
||||
padding: 8px 12px;
|
||||
padding: 9px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(108, 122, 224, 0.2);
|
||||
color: #a8b4ff;
|
||||
font-size: 12px;
|
||||
background: linear-gradient(135deg, rgba(108, 122, 224, 0.35) 0%, rgba(160, 110, 224, 0.3) 100%);
|
||||
color: #d0d8ff;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(108, 122, 224, 0.3);
|
||||
border: 1px solid rgba(108, 122, 224, 0.4);
|
||||
flex-shrink: 0;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
.file-download:hover {
|
||||
background: linear-gradient(135deg, rgba(108, 122, 224, 0.5) 0%, rgba(160, 110, 224, 0.45) 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.file-download:hover { background: rgba(108, 122, 224, 0.3); }
|
||||
|
||||
.lightbox-mask {
|
||||
position: fixed; inset: 0; z-index: 99999;
|
||||
background: rgba(0, 0, 0, 0.88);
|
||||
backdrop-filter: blur(8px);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
@@ -162,48 +223,55 @@
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 30px 100px rgba(0,0,0,0.6);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 30px 100px rgba(0,0,0,0.7);
|
||||
}
|
||||
.lightbox-close, .lightbox-nav {
|
||||
position: absolute;
|
||||
width: 44px; height: 44px;
|
||||
width: 46px; height: 46px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
border: 1px solid rgba(255,255,255,0.16);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
cursor: pointer;
|
||||
backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
.lightbox-close:hover, .lightbox-nav:hover { background: rgba(255,255,255,0.18); transform: scale(1.06); }
|
||||
.lightbox-close { top: 16px; right: 16px; }
|
||||
.lightbox-close:hover, .lightbox-nav:hover { background: rgba(255,255,255,0.22); transform: scale(1.08); }
|
||||
.lightbox-close { top: 18px; right: 18px; }
|
||||
.lightbox-nav { top: 50%; transform: translateY(-50%); }
|
||||
.lightbox-nav:hover { transform: translateY(-50%) scale(1.06); }
|
||||
.lightbox-nav.prev { left: 16px; }
|
||||
.lightbox-nav.next { right: 16px; }
|
||||
.lightbox-nav:hover { transform: translateY(-50%) scale(1.08); }
|
||||
.lightbox-nav.prev { left: 18px; }
|
||||
.lightbox-nav.next { right: 18px; }
|
||||
.lightbox-caption {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 80vw;
|
||||
padding: 8px 18px;
|
||||
padding: 10px 22px;
|
||||
border-radius: 999px;
|
||||
background: rgba(0,0,0,0.65);
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
backdrop-filter: blur(6px);
|
||||
backdrop-filter: blur(8px);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.detail-title { font-size: 20px; }
|
||||
.detail-remark { padding: 18px; font-size: 14.5px; }
|
||||
.detail-card { padding: 18px 18px; border-radius: 15px; }
|
||||
.detail-title { font-size: 22px; }
|
||||
.detail-remark { padding: 0; font-size: 14.5px; line-height: 1.9; }
|
||||
.detail-remark.empty { padding: 22px 12px; }
|
||||
.image-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.lightbox-close, .lightbox-nav { width: 40px; height: 40px; font-size: 18px; }
|
||||
.file-item { padding: 12px 14px; gap: 12px; }
|
||||
.file-icon { width: 40px; height: 40px; font-size: 19px; border-radius: 10px; }
|
||||
.file-name { font-size: 14px; }
|
||||
.file-download { padding: 7px 11px; font-size: 12px; }
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>收藏详情 | MilesYang时光助手</title>
|
||||
<link rel="stylesheet" href="/common/common.css">
|
||||
<link rel="stylesheet" href="../../shared.css">
|
||||
<link rel="stylesheet" href="detail.css">
|
||||
<link rel="stylesheet" href="/common/common.css?v=1.10">
|
||||
<link rel="stylesheet" href="../../shared.css?v=1.10">
|
||||
<link rel="stylesheet" href="detail.css?v=1.10">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@@ -43,8 +43,8 @@
|
||||
<div id="modal-slot"></div>
|
||||
<div id="interaction-dialog" class="interaction-dialog hidden"></div>
|
||||
|
||||
<script src="../../api.js"></script>
|
||||
<script src="../../kits/common/dialog.js"></script>
|
||||
<script src="detail.js"></script>
|
||||
<script src="../../api.js?v=1.10"></script>
|
||||
<script src="../../kits/common/dialog.js?v=1.10"></script>
|
||||
<script src="detail.js?v=1.10"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -21,6 +21,101 @@
|
||||
const formatTime = dlg.formatTime
|
||||
const humanSize = dlg.humanSize
|
||||
|
||||
function _scoreCN(str) {
|
||||
if (!str) return -Infinity
|
||||
let cjk = 0, asc = 0, ext = 0, weird = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charCodeAt(i)
|
||||
if (c >= 0x4e00 && c <= 0x9fff) cjk++
|
||||
else if (c >= 0x3400 && c <= 0x4dbf) cjk++
|
||||
else if (c >= 0x3000 && c <= 0x303f) cjk++
|
||||
else if (c >= 0xff00 && c <= 0xffef) cjk++
|
||||
else if (c === 0xb7 || c === 0x2022) cjk++
|
||||
else if (c >= 0x20 && c <= 0x7e) asc++
|
||||
else if (c < 0x20 && c !== 9 && c !== 10 && c !== 13) weird++
|
||||
else if (c >= 0x80 && c <= 0xff) ext++
|
||||
else if (c >= 0x100 && c < 0x3000) ext++
|
||||
else ext++
|
||||
}
|
||||
return cjk * 3 + asc * 1 - ext * 2 - weird * 10
|
||||
}
|
||||
|
||||
function _decodeBytes(bytes, enc) {
|
||||
try {
|
||||
const dec = new TextDecoder(enc, { fatal: false })
|
||||
const s = dec.decode(new Uint8Array(bytes))
|
||||
return (s == null ? '' : String(s)).replace(/\u0000/g, '')
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
function fixFileName(raw) {
|
||||
const name = String(raw == null ? '' : raw).trim()
|
||||
if (!name) return '未命名文件'
|
||||
|
||||
const em = name.match(/(\.[A-Za-z0-9]{1,10})$/)
|
||||
const ext = em ? em[1] : ''
|
||||
const base = ext ? name.slice(0, name.length - ext.length) : name
|
||||
if (!base) return name
|
||||
|
||||
const cands = [{ s: name, src: 'origin' }]
|
||||
|
||||
const bytes = []
|
||||
for (let i = 0; i < base.length; i++) {
|
||||
const c = base.charCodeAt(i)
|
||||
if (c >= 0xd800 && c <= 0xdbff && i + 1 < base.length) {
|
||||
const low = base.charCodeAt(i + 1)
|
||||
if (low >= 0xdc00 && low <= 0xdfff) {
|
||||
const cp = 0x10000 + (c - 0xd800) * 0x400 + (low - 0xdc00)
|
||||
bytes.push(0xff & (cp >> 8), 0xff & cp)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
bytes.push(0xff & c)
|
||||
}
|
||||
|
||||
const tryDec = (encList, srcPrefix) => {
|
||||
encList.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) cands.push({ s: s + ext, src: srcPrefix + '_' + enc })
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
if (bytes.length) {
|
||||
tryDec(['gbk', 'gb18030', 'utf-8', 'big5', 'shift-jis', 'euc-kr'], 'raw')
|
||||
const rev = bytes.slice().reverse()
|
||||
const encs2 = ['gbk', 'gb18030']
|
||||
encs2.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) {
|
||||
const bytes2 = []
|
||||
for (let i = 0; i < s.length; i++) bytes2.push(0xff & s.charCodeAt(i))
|
||||
const s2 = _decodeBytes(bytes2, 'utf-8')
|
||||
if (s2) cands.push({ s: s2 + ext, src: 'doub_' + enc + '_utf8' })
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
let best = name, bestSc = _scoreCN(name) - (
|
||||
/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(name) ? 15 : 0
|
||||
) - (/[\u4e00-\u9fa5]/.test(name) && /[çèéêëìíîïðñòóôõöøùúûüýþÿÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞß]/.test(name) ? 25 : 0)
|
||||
|
||||
cands.forEach(c => {
|
||||
if (!c || !c.s) return
|
||||
const s = c.s
|
||||
if (/\uFFFD/.test(s) && c.src !== 'origin') return
|
||||
let sc = _scoreCN(s)
|
||||
if (/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(s)) sc -= 10
|
||||
if (sc > bestSc) { bestSc = sc; best = s }
|
||||
})
|
||||
|
||||
return best.length > 255 ? best.slice(0, 250) + (ext || '') : best
|
||||
}
|
||||
|
||||
const q = new URLSearchParams(location.search)
|
||||
const id = q.get('id')
|
||||
let current = null
|
||||
@@ -98,17 +193,17 @@
|
||||
const filePath = a.filePath || a.file_path
|
||||
return {
|
||||
src: a.url || api.fileUrl(filePath),
|
||||
name: a.fileName || a.file_name || ''
|
||||
name: fixFileName(a.fileName || a.file_name || '')
|
||||
}
|
||||
})
|
||||
return (
|
||||
`<section class="detail-section">` +
|
||||
`<section class="detail-section detail-card detail-card-images">` +
|
||||
`<div class="detail-section-title">图片</div>` +
|
||||
`<div class="image-grid">` +
|
||||
images.map((a, i) => {
|
||||
const filePath = a.filePath || a.file_path
|
||||
const src = a.url || api.fileUrl(filePath)
|
||||
const name = a.fileName || a.file_name || ('image_' + (i+1))
|
||||
const name = fixFileName(a.fileName || a.file_name || ('image_' + (i+1)))
|
||||
const badge = (name && name.length > 4)
|
||||
? `<div class="name-badge">${escapeHtml(name)}</div>` : ''
|
||||
return `<div class="image-item" data-img-idx="${i}" title="${escapeHtml(name)}"><img src="${src}" alt="${escapeHtml(name)}" loading="lazy">${badge}</div>`
|
||||
@@ -121,13 +216,13 @@
|
||||
function renderFileSection(files) {
|
||||
if (!files || !files.length) return ''
|
||||
return (
|
||||
`<section class="detail-section">` +
|
||||
`<section class="detail-section detail-card detail-card-files">` +
|
||||
`<div class="detail-section-title">文件 (${files.length})</div>` +
|
||||
`<div class="file-list">` +
|
||||
files.map(a => {
|
||||
const filePath = a.filePath || a.file_path
|
||||
const src = a.url || api.fileUrl(filePath)
|
||||
const name = a.fileName || a.file_name || '未命名文件'
|
||||
const name = fixFileName(a.fileName || a.file_name || '未命名文件')
|
||||
const size = Number(a.fileSize || a.file_size || 0)
|
||||
const mimeType = a.mimeType || a.mime_type || ''
|
||||
return (
|
||||
@@ -161,15 +256,15 @@
|
||||
: ''
|
||||
|
||||
const remarkBlock = (item.remark || '').trim()
|
||||
? `<section class="detail-section"><div class="detail-section-title">备注</div><div class="detail-remark">${escapeHtml(item.remark)}</div></section>`
|
||||
: `<section class="detail-section"><div class="detail-section-title">备注</div><div class="detail-remark empty">(无备注)</div></section>`
|
||||
? `<section class="detail-section detail-card detail-card-remark"><div class="detail-section-title">备注</div><div class="detail-remark">${escapeHtml(item.remark)}</div></section>`
|
||||
: `<section class="detail-section detail-card detail-card-remark"><div class="detail-section-title">备注</div><div class="detail-remark empty">(无备注)</div></section>`
|
||||
|
||||
const titleBlock = dispTitle
|
||||
? `<section class="detail-section"><div class="detail-title">${escapeHtml(dispTitle)}</div></section>`
|
||||
: `<section class="detail-section"><div class="detail-title empty">(该条收藏未设置标题)</div></section>`
|
||||
? `<section class="detail-section detail-card detail-card-title"><div class="detail-title">${escapeHtml(dispTitle)}</div></section>`
|
||||
: `<section class="detail-section detail-card detail-card-title"><div class="detail-title empty">(该条收藏未设置标题)</div></section>`
|
||||
|
||||
const metaBlock =
|
||||
`<section class="detail-section">` +
|
||||
`<section class="detail-section detail-card detail-card-meta">` +
|
||||
`<div class="detail-meta">` +
|
||||
`<span><b>创建时间</b>${formatTime(createdAt)}</span>` +
|
||||
`<span><b>更新时间</b>${formatTime(updatedAt)}</span>` +
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>新增 / 编辑收藏 | MilesYang时光助手</title>
|
||||
<link rel="stylesheet" href="/common/common.css">
|
||||
<link rel="stylesheet" href="../../shared.css">
|
||||
<link rel="stylesheet" href="edit.css">
|
||||
<link rel="stylesheet" href="/common/common.css?v=1.10">
|
||||
<link rel="stylesheet" href="../../shared.css?v=1.10">
|
||||
<link rel="stylesheet" href="edit.css?v=1.10">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@@ -70,8 +70,8 @@
|
||||
<div id="modal-slot"></div>
|
||||
<div id="interaction-dialog" class="interaction-dialog hidden"></div>
|
||||
|
||||
<script src="../../api.js"></script>
|
||||
<script src="../../kits/common/dialog.js"></script>
|
||||
<script src="edit.js"></script>
|
||||
<script src="../../api.js?v=1.10"></script>
|
||||
<script src="../../kits/common/dialog.js?v=1.10"></script>
|
||||
<script src="edit.js?v=1.10"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -20,6 +20,100 @@
|
||||
const escapeHtml = dlg.escapeHtml
|
||||
const humanSize = dlg.humanSize
|
||||
|
||||
function _scoreCN(str) {
|
||||
if (!str) return -Infinity
|
||||
let cjk = 0, asc = 0, ext = 0, weird = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charCodeAt(i)
|
||||
if (c >= 0x4e00 && c <= 0x9fff) cjk++
|
||||
else if (c >= 0x3400 && c <= 0x4dbf) cjk++
|
||||
else if (c >= 0x3000 && c <= 0x303f) cjk++
|
||||
else if (c >= 0xff00 && c <= 0xffef) cjk++
|
||||
else if (c === 0xb7 || c === 0x2022) cjk++
|
||||
else if (c >= 0x20 && c <= 0x7e) asc++
|
||||
else if (c < 0x20 && c !== 9 && c !== 10 && c !== 13) weird++
|
||||
else if (c >= 0x80 && c <= 0xff) ext++
|
||||
else if (c >= 0x100 && c < 0x3000) ext++
|
||||
else ext++
|
||||
}
|
||||
return cjk * 3 + asc * 1 - ext * 2 - weird * 10
|
||||
}
|
||||
|
||||
function _decodeBytes(bytes, enc) {
|
||||
try {
|
||||
const dec = new TextDecoder(enc, { fatal: false })
|
||||
const s = dec.decode(new Uint8Array(bytes))
|
||||
return (s == null ? '' : String(s)).replace(/\u0000/g, '')
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
function fixFileName(raw) {
|
||||
const name = String(raw == null ? '' : raw).trim()
|
||||
if (!name) return '未命名文件'
|
||||
|
||||
const em = name.match(/(\.[A-Za-z0-9]{1,10})$/)
|
||||
const ext = em ? em[1] : ''
|
||||
const base = ext ? name.slice(0, name.length - ext.length) : name
|
||||
if (!base) return name
|
||||
|
||||
const cands = [{ s: name, src: 'origin' }]
|
||||
|
||||
const bytes = []
|
||||
for (let i = 0; i < base.length; i++) {
|
||||
const c = base.charCodeAt(i)
|
||||
if (c >= 0xd800 && c <= 0xdbff && i + 1 < base.length) {
|
||||
const low = base.charCodeAt(i + 1)
|
||||
if (low >= 0xdc00 && low <= 0xdfff) {
|
||||
const cp = 0x10000 + (c - 0xd800) * 0x400 + (low - 0xdc00)
|
||||
bytes.push(0xff & (cp >> 8), 0xff & cp)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
bytes.push(0xff & c)
|
||||
}
|
||||
|
||||
const tryDec = (encList, srcPrefix) => {
|
||||
encList.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) cands.push({ s: s + ext, src: srcPrefix + '_' + enc })
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
if (bytes.length) {
|
||||
tryDec(['gbk', 'gb18030', 'utf-8', 'big5', 'shift-jis', 'euc-kr'], 'raw')
|
||||
const encs2 = ['gbk', 'gb18030']
|
||||
encs2.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) {
|
||||
const bytes2 = []
|
||||
for (let i = 0; i < s.length; i++) bytes2.push(0xff & s.charCodeAt(i))
|
||||
const s2 = _decodeBytes(bytes2, 'utf-8')
|
||||
if (s2) cands.push({ s: s2 + ext, src: 'doub_' + enc + '_utf8' })
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
let best = name, bestSc = _scoreCN(name) - (
|
||||
/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(name) ? 15 : 0
|
||||
) - (/[\u4e00-\u9fa5]/.test(name) && /[çèéêëìíîïðñòóôõöøùúûüýþÿÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞß]/.test(name) ? 25 : 0)
|
||||
|
||||
cands.forEach(c => {
|
||||
if (!c || !c.s) return
|
||||
const s = c.s
|
||||
if (/\uFFFD/.test(s) && c.src !== 'origin') return
|
||||
let sc = _scoreCN(s)
|
||||
if (/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(s)) sc -= 10
|
||||
if (sc > bestSc) { bestSc = sc; best = s }
|
||||
})
|
||||
|
||||
return best.length > 255 ? best.slice(0, 250) + (ext || '') : best
|
||||
}
|
||||
|
||||
const q = new URLSearchParams(location.search)
|
||||
const editId = q.get('id')
|
||||
const isEdit = !!editId
|
||||
@@ -81,17 +175,18 @@
|
||||
const tempId = a.tempId
|
||||
const isImg = !!a.isImage
|
||||
const displayUrl = a.url || a.previewUrl || ''
|
||||
const ext = (a.fileName || '').split('.').pop().toUpperCase()
|
||||
const fn = fixFileName(a.fileName)
|
||||
const ext = (fn || '').split('.').pop().toUpperCase()
|
||||
const statusTextMap = { pending: '等待上传', uploading: '上传中', ok: '上传成功', err: '上传失败' }
|
||||
const statusText = statusTextMap[a.status] || a.status
|
||||
const showProgress = a.status === 'uploading'
|
||||
return (
|
||||
`<div class="attach-item ${isImg ? 'image' : 'file'}" data-temp="${escapeHtml(tempId)}">` +
|
||||
(isImg
|
||||
? `<div class="preview"><img src="${displayUrl}" alt="${escapeHtml(a.fileName || '')}" loading="lazy"></div>`
|
||||
: `<div class="preview"><span class="big">${fileIcon(a.fileName)}</span><span class="ext">${ext || 'FILE'}</span></div>`) +
|
||||
? `<div class="preview"><img src="${displayUrl}" alt="${escapeHtml(fn || '')}" loading="lazy"></div>`
|
||||
: `<div class="preview"><span class="big">${fileIcon(fn)}</span><span class="ext">${ext || 'FILE'}</span></div>`) +
|
||||
`<div class="attach-info">` +
|
||||
`<div class="attach-name" title="${escapeHtml(a.fileName || '')}">${escapeHtml(a.fileName || '未命名')}</div>` +
|
||||
`<div class="attach-name" title="${escapeHtml(fn || '')}">${escapeHtml(fn || '未命名')}</div>` +
|
||||
`<div class="attach-meta">` +
|
||||
`<span>${humanSize(a.fileSize)}</span>` +
|
||||
`<span class="attach-status ${a.status}">${statusText}</span>` +
|
||||
@@ -212,7 +307,7 @@
|
||||
let title = ''
|
||||
if (total === 1) {
|
||||
const only = atts[0]
|
||||
const raw = only.fileName || '附件'
|
||||
const raw = fixFileName(only.fileName || '附件')
|
||||
const dot = raw.lastIndexOf('.')
|
||||
const stem = dot > 0 ? raw.slice(0, dot) : raw
|
||||
if (only.isImage) title = `图片 - ${ts}`
|
||||
@@ -220,7 +315,7 @@
|
||||
} else if (ic > 0 && fc === 0) title = `${ic} 张图片 - ${ts}`
|
||||
else if (fc > 0 && ic === 0) {
|
||||
if (fc === 1) {
|
||||
const only = files[0]; const raw = only.fileName || '附件'
|
||||
const only = files[0]; const raw = fixFileName(only.fileName || '附件')
|
||||
const dot = raw.lastIndexOf('.'); const stem = dot > 0 ? raw.slice(0, dot) : raw
|
||||
title = stem || raw
|
||||
} else title = `${fc} 个文件 - ${ts}`
|
||||
@@ -230,7 +325,7 @@
|
||||
if (ic) lines.push(`图片:${ic} 张`)
|
||||
if (fc) lines.push(`文件:${fc} 个`)
|
||||
lines.push('', '附件清单:')
|
||||
atts.slice(0, 9).forEach((a, i) => lines.push(` ${i+1}. ${a.fileName || '未命名'}(${humanSize(a.fileSize)})`))
|
||||
atts.slice(0, 9).forEach((a, i) => lines.push(` ${i+1}. ${fixFileName(a.fileName || '未命名')}(${humanSize(a.fileSize)})`))
|
||||
if (atts.length > 9) lines.push(` ... 另有 ${atts.length - 9} 个附件未列出`)
|
||||
lines.push('', `整理时间:${ts}`)
|
||||
return { title, remark: lines.join('\n') }
|
||||
@@ -440,7 +535,7 @@
|
||||
filePath: a.filePath || a.file_path || '',
|
||||
mimeType: a.mimeType || a.mime_type || '',
|
||||
isImage: typeof a.isImage !== 'undefined' ? !!a.isImage : !!a.is_image,
|
||||
fileName: a.fileName || a.file_name || '未命名',
|
||||
fileName: fixFileName(a.fileName || a.file_name || '未命名'),
|
||||
fileSize: Number(a.fileSize || a.file_size || 0),
|
||||
previewUrl: '',
|
||||
url: a.url || api.fileUrl(a.filePath || a.file_path || ''),
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>微信收藏夹 | MilesYang时光助手</title>
|
||||
<link rel="stylesheet" href="/common/common.css">
|
||||
<link rel="stylesheet" href="../../shared.css">
|
||||
<link rel="stylesheet" href="list.css">
|
||||
<link rel="stylesheet" href="/common/common.css?v=1.10">
|
||||
<link rel="stylesheet" href="../../shared.css?v=1.10">
|
||||
<link rel="stylesheet" href="list.css?v=1.10">
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
@@ -38,8 +38,8 @@
|
||||
<div id="modal-slot"></div>
|
||||
<div id="interaction-dialog" class="interaction-dialog hidden"></div>
|
||||
|
||||
<script src="../../api.js"></script>
|
||||
<script src="../../kits/common/dialog.js"></script>
|
||||
<script src="list.js"></script>
|
||||
<script src="../../api.js?v=1.10"></script>
|
||||
<script src="../../kits/common/dialog.js?v=1.10"></script>
|
||||
<script src="list.js?v=1.10"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -16,6 +16,100 @@
|
||||
const formatTime = dlg.formatTime
|
||||
const humanSize = dlg.humanSize
|
||||
|
||||
function _scoreCN(str) {
|
||||
if (!str) return -Infinity
|
||||
let cjk = 0, asc = 0, ext = 0, weird = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const c = str.charCodeAt(i)
|
||||
if (c >= 0x4e00 && c <= 0x9fff) cjk++
|
||||
else if (c >= 0x3400 && c <= 0x4dbf) cjk++
|
||||
else if (c >= 0x3000 && c <= 0x303f) cjk++
|
||||
else if (c >= 0xff00 && c <= 0xffef) cjk++
|
||||
else if (c === 0xb7 || c === 0x2022) cjk++
|
||||
else if (c >= 0x20 && c <= 0x7e) asc++
|
||||
else if (c < 0x20 && c !== 9 && c !== 10 && c !== 13) weird++
|
||||
else if (c >= 0x80 && c <= 0xff) ext++
|
||||
else if (c >= 0x100 && c < 0x3000) ext++
|
||||
else ext++
|
||||
}
|
||||
return cjk * 3 + asc * 1 - ext * 2 - weird * 10
|
||||
}
|
||||
|
||||
function _decodeBytes(bytes, enc) {
|
||||
try {
|
||||
const dec = new TextDecoder(enc, { fatal: false })
|
||||
const s = dec.decode(new Uint8Array(bytes))
|
||||
return (s == null ? '' : String(s)).replace(/\u0000/g, '')
|
||||
} catch { return '' }
|
||||
}
|
||||
|
||||
function fixFileName(raw) {
|
||||
const name = String(raw == null ? '' : raw).trim()
|
||||
if (!name) return '未命名文件'
|
||||
|
||||
const em = name.match(/(\.[A-Za-z0-9]{1,10})$/)
|
||||
const ext = em ? em[1] : ''
|
||||
const base = ext ? name.slice(0, name.length - ext.length) : name
|
||||
if (!base) return name
|
||||
|
||||
const cands = [{ s: name, src: 'origin' }]
|
||||
|
||||
const bytes = []
|
||||
for (let i = 0; i < base.length; i++) {
|
||||
const c = base.charCodeAt(i)
|
||||
if (c >= 0xd800 && c <= 0xdbff && i + 1 < base.length) {
|
||||
const low = base.charCodeAt(i + 1)
|
||||
if (low >= 0xdc00 && low <= 0xdfff) {
|
||||
const cp = 0x10000 + (c - 0xd800) * 0x400 + (low - 0xdc00)
|
||||
bytes.push(0xff & (cp >> 8), 0xff & cp)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
bytes.push(0xff & c)
|
||||
}
|
||||
|
||||
const tryDec = (encList, srcPrefix) => {
|
||||
encList.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) cands.push({ s: s + ext, src: srcPrefix + '_' + enc })
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
if (bytes.length) {
|
||||
tryDec(['gbk', 'gb18030', 'utf-8', 'big5', 'shift-jis', 'euc-kr'], 'raw')
|
||||
const encs2 = ['gbk', 'gb18030']
|
||||
encs2.forEach(enc => {
|
||||
try {
|
||||
const s = _decodeBytes(bytes, enc)
|
||||
if (s) {
|
||||
const bytes2 = []
|
||||
for (let i = 0; i < s.length; i++) bytes2.push(0xff & s.charCodeAt(i))
|
||||
const s2 = _decodeBytes(bytes2, 'utf-8')
|
||||
if (s2) cands.push({ s: s2 + ext, src: 'doub_' + enc + '_utf8' })
|
||||
}
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
|
||||
let best = name, bestSc = _scoreCN(name) - (
|
||||
/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(name) ? 15 : 0
|
||||
) - (/[\u4e00-\u9fa5]/.test(name) && /[çèéêëìíîïðñòóôõöøùúûüýþÿÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞß]/.test(name) ? 25 : 0)
|
||||
|
||||
cands.forEach(c => {
|
||||
if (!c || !c.s) return
|
||||
const s = c.s
|
||||
if (/\uFFFD/.test(s) && c.src !== 'origin') return
|
||||
let sc = _scoreCN(s)
|
||||
if (/[ÃÂÄÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßçèéêëìíîïðñòóôõöøùúûüýþÿ]/.test(s)) sc -= 10
|
||||
if (sc > bestSc) { bestSc = sc; best = s }
|
||||
})
|
||||
|
||||
return best.length > 255 ? best.slice(0, 250) + (ext || '') : best
|
||||
}
|
||||
|
||||
function goBack() { window.location.href = '../../index.html' }
|
||||
function goAdd() { window.location.href = 'edit.html' }
|
||||
function goEdit(id) { window.location.href = 'edit.html?id=' + encodeURIComponent(id) }
|
||||
@@ -43,14 +137,16 @@
|
||||
const tiles = []
|
||||
images.slice(0, 4).forEach((a) => {
|
||||
const url = api.fileUrl(a.url || a.filePath)
|
||||
tiles.push(`<div class="thumb"><img src="${url}" alt="${escapeHtml(a.fileName || 'image')}" loading="lazy"></div>`)
|
||||
const fn = fixFileName(a.fileName)
|
||||
tiles.push(`<div class="thumb"><img src="${url}" alt="${escapeHtml(fn || 'image')}" loading="lazy"></div>`)
|
||||
})
|
||||
const fileSlots = Math.max(0, 4 - tiles.length)
|
||||
files.slice(0, fileSlots).forEach((a) => {
|
||||
const fn = fixFileName(a.fileName)
|
||||
tiles.push(
|
||||
`<div class="thumb file" title="${escapeHtml(a.fileName || '')}">` +
|
||||
`<span class="icon">${fileIcon(a.fileName)}</span>` +
|
||||
`<span class="name">${escapeHtml(a.fileName || '未命名')}</span>` +
|
||||
`<div class="thumb file" title="${escapeHtml(fn || '')}">` +
|
||||
`<span class="icon">${fileIcon(fn)}</span>` +
|
||||
`<span class="name">${escapeHtml(fn || '未命名')}</span>` +
|
||||
`</div>`
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user