feat: 新增运动节拍训练工具套件与子工具页面

新增运动节拍RepClock全套小程序页面、后端API与数据库服务,新增subhub子工具汇总页面,优化全局主页样式与配置,为web静态资源添加版本缓存参数,补充配套调试脚本
This commit is contained in:
yangxiangyuan
2026-09-09 14:43:06 +08:00
parent db7648eb4d
commit d4bf3627fd
69 changed files with 5759 additions and 297 deletions
+21 -15
View File
@@ -13,14 +13,20 @@
const url = buildUrl(path)
const init = {
method: method || 'GET',
headers: opts.headers || {},
headers: Object.assign(
{
'Accept': 'application/json; charset=utf-8',
'X-Mp-Timeassistant': 'mp_web_time_assistant_v1_' + Math.random().toString(36).slice(2, 10),
},
opts.headers || {}
),
credentials: 'same-origin'
}
if (body !== null && body !== undefined) {
if (body instanceof FormData) {
init.body = body
} else {
init.headers['Content-Type'] = 'application/json'
init.headers['Content-Type'] = 'application/json; charset=utf-8'
init.body = JSON.stringify(body)
}
}
@@ -48,17 +54,17 @@
async meta() {
const res = await request('GET', '/api/mp_time_assistant/meta')
const payload = (res && res.data) || {}
return (payload && payload.ok) ? payload.data || {} : (payload || {})
return (payload && payload.ok) ? payload : (payload || {})
},
async getState() {
const res = await request('GET', '/api/mp_time_assistant/state')
const payload = (res && res.data) || {}
return (payload && payload.ok) ? payload.data || {} : (payload || {})
return (payload && payload.ok) ? payload : (payload || {})
},
async setActiveKit(kitId) {
const res = await request('PUT', '/api/mp_time_assistant/state/active-kit', { kitId })
const payload = (res && res.data) || {}
return (payload && payload.ok) ? payload.data || {} : (payload || {})
return (payload && payload.ok) ? payload : (payload || {})
}
}
@@ -67,31 +73,31 @@
const q = new URLSearchParams()
if (keyword) q.set('keyword', keyword)
const suffix = q.toString() ? '?' + q.toString() : ''
const res = await request('GET', '/api/mp_time_assistant/kits/personal_collect/items' + suffix)
const res = await request('GET', '/api/mp_personal_collect/kits/personal_collect/items' + suffix)
const payload = (res && res.data) || {}
if (payload && payload.ok) return payload.data || []
if (payload && payload.ok) return payload.items || payload.data || []
return Array.isArray(payload) ? payload : []
},
async get(id) {
const res = await request('GET', '/api/mp_time_assistant/kits/personal_collect/items/' + encodeURIComponent(id))
const res = await request('GET', '/api/mp_personal_collect/kits/personal_collect/items/' + encodeURIComponent(id))
const payload = (res && res.data) || {}
return (payload && payload.ok) ? (payload.data || null) : (payload || null)
return (payload && payload.ok) ? (payload.item || payload.data || null) : (payload || null)
},
async create(payload) {
const res = await request('POST', '/api/mp_time_assistant/kits/personal_collect/items', payload)
const res = await request('POST', '/api/mp_personal_collect/kits/personal_collect/items', payload)
const body = (res && res.data) || {}
if (body && body.ok) return body.data || body
if (body && body.ok) return body.item || body.data || body
throw new Error((body && body.error) || '创建失败')
},
async update(payload) {
const res = await request('PUT', '/api/mp_time_assistant/kits/personal_collect/items', payload)
const res = await request('PUT', '/api/mp_personal_collect/kits/personal_collect/items', payload)
const body = (res && res.data) || {}
if (body && body.ok) return body.data || body
if (body && body.ok) return body.item || body.data || body
throw new Error((body && body.error) || '更新失败')
},
async remove(id) {
const q = new URLSearchParams({ id })
const res = await request('DELETE', '/api/mp_time_assistant/kits/personal_collect/items?' + q.toString())
const res = await request('DELETE', '/api/mp_personal_collect/kits/personal_collect/items?' + q.toString())
const body = (res && res.data) || {}
if (body && body.ok) return body
throw new Error((body && body.error) || '删除失败')
@@ -100,7 +106,7 @@
const form = new FormData()
form.append('file', file)
if (groupId) form.append('groupId', groupId)
const url = buildUrl('/api/mp_time_assistant/kits/personal_collect/upload')
const url = buildUrl('/api/mp_personal_collect/kits/personal_collect/upload')
return new Promise((resolve, reject) => {
try {
const xhr = new XMLHttpRequest()
+152 -25
View File
@@ -18,7 +18,7 @@ body {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 48px;
margin-bottom: 36px;
padding: 24px 32px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(108, 122, 224, 0.15);
@@ -64,9 +64,85 @@ body {
border-color: rgba(255, 80, 100, 0.3);
}
.app-main {
display: flex;
flex-direction: column;
gap: 28px;
}
.hero-card {
position: relative;
display: flex;
align-items: center;
gap: 24px;
padding: 32px 36px;
border-radius: 24px;
background: linear-gradient(135deg, rgba(108, 122, 224, 0.14) 0%, rgba(176, 107, 224, 0.10) 100%);
border: 1px solid rgba(108, 122, 224, 0.28);
backdrop-filter: blur(10px);
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
top: -80px; right: -80px;
width: 280px; height: 280px;
background: radial-gradient(circle, rgba(176, 107, 224, 0.28) 0%, transparent 65%);
filter: blur(40px);
opacity: 0.9;
pointer-events: none;
}
.hero-card::after {
content: '';
position: absolute;
left: 16px; top: 20px; bottom: 20px;
width: 4px;
border-radius: 4px;
background: linear-gradient(180deg, #6c7ae0 0%, #b06be0 100%);
box-shadow: 0 0 18px rgba(108, 122, 224, 0.6);
}
.hero-icon {
width: 78px; height: 78px;
border-radius: 22px;
display: flex; align-items: center; justify-content: center;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.09);
flex-shrink: 0;
position: relative; z-index: 1;
}
.hero-emoji {
font-size: 42px;
line-height: 1;
filter: drop-shadow(0 6px 18px rgba(108, 122, 224, 0.45));
animation: heroBreath 5.2s ease-in-out infinite;
}
@keyframes heroBreath {
0%, 100% { transform: scale(1); opacity: 0.96; }
50% { transform: scale(1.08); opacity: 1; }
}
.hero-body { position: relative; z-index: 1; }
.hero-title {
font-size: 24px;
font-weight: 600;
color: #f0eefc;
letter-spacing: 0.6px;
margin-bottom: 10px;
}
.hero-desc {
font-size: 14px;
color: #a8a6c8;
letter-spacing: 0.3px;
}
.kit-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 24px;
}
@@ -80,10 +156,13 @@ body {
.kit-card {
position: relative;
display: flex;
align-items: stretch;
gap: 20px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 22px;
padding: 28px;
padding: 24px 22px 24px 26px;
cursor: pointer;
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
border-color 0.3s ease,
@@ -101,10 +180,23 @@ body {
transition: opacity 0.4s ease;
opacity: 0;
z-index: 0;
pointer-events: none;
}
.kit-card::after {
content: '';
position: absolute;
left: 12px; top: 20px; bottom: 20px;
width: 3px;
border-radius: 3px;
background: linear-gradient(180deg, #6c7ae0 0%, #b06be0 100%);
box-shadow: 0 0 14px rgba(108, 122, 224, 0.45);
z-index: 0;
pointer-events: none;
}
.kit-card:hover {
transform: translateY(-6px);
transform: translateY(-5px);
border-color: rgba(108, 122, 224, 0.35);
background: rgba(255, 255, 255, 0.06);
box-shadow: 0 20px 40px rgba(108, 122, 224, 0.15);
@@ -125,46 +217,72 @@ body {
font-size: 28px;
background: linear-gradient(135deg, rgba(108, 122, 224, 0.22) 0%, rgba(176, 107, 224, 0.18) 100%);
border: 1px solid rgba(108, 122, 224, 0.3);
margin-bottom: 18px;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.kit-name {
font-size: 19px;
font-weight: 600;
color: #f0f0f8;
margin-bottom: 8px;
.kit-body {
flex: 1 1 auto;
min-width: 0;
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.kit-name {
font-size: 18px;
font-weight: 600;
color: #f0f0f8;
}
.kit-desc {
font-size: 13.5px;
color: #9b9bb5;
line-height: 1.7;
position: relative;
z-index: 1;
}
.kit-arrow {
margin-top: 18px;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
.kit-meta {
margin-top: 4px;
padding: 6px 10px;
border-radius: 10px;
background: rgba(108, 122, 224, 0.08);
border: 1px solid rgba(108, 122, 224, 0.18);
font-size: 11.5px;
color: #a8b4ff;
position: relative;
z-index: 1;
opacity: 0.75;
transition: gap 0.3s ease, opacity 0.3s ease;
font-family: 'JetBrains Mono', Consolas, 'Courier New', monospace;
letter-spacing: 0.2px;
align-self: flex-start;
}
.kit-card:hover .kit-arrow {
gap: 12px;
.chevron {
flex-shrink: 0;
align-self: center;
color: #a8b4ff;
font-size: 26px;
line-height: 1;
opacity: 0.75;
transition: transform 0.3s ease, opacity 0.3s ease;
position: relative; z-index: 1;
padding-left: 4px;
}
.kit-card:hover .chevron {
transform: translateX(6px);
opacity: 1;
}
.footer {
margin-top: 8px;
padding: 18px 4px 0;
text-align: center;
color: #6f6f92;
font-size: 12px;
letter-spacing: 0.5px;
}
@media (max-width: 600px) {
.app-root { padding: 20px 16px 60px; }
.app-header {
@@ -174,6 +292,15 @@ body {
gap: 12px;
}
.app-header-left .app-title { font-size: 24px; }
.hero-card {
padding: 22px 22px 22px 28px;
gap: 16px;
flex-direction: column;
align-items: flex-start;
}
.hero-icon { width: 64px; height: 64px; border-radius: 18px; }
.hero-emoji { font-size: 34px; }
.hero-title { font-size: 20px; }
.kit-grid { grid-template-columns: 1fr; gap: 16px; }
.kit-card { padding: 22px; }
.kit-card { padding: 20px 18px 20px 24px; }
}
+19 -5
View File
@@ -4,15 +4,15 @@
<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="hub.css">
<link rel="stylesheet" href="/common/common.css?v=1.10">
<link rel="stylesheet" href="hub.css?v=1.10">
</head>
<body>
<div class="app-root">
<header class="app-header">
<div class="app-header-left">
<h1 class="app-title">MilesYang时光助手</h1>
<p class="app-subtitle">手机 · 电脑,实时联动的小工具集合</p>
<p class="app-subtitle">手机 · 电脑,实时联动的小工具集合 · 完全独立 · 零共享</p>
</div>
<div class="app-header-right">
<span id="status-tag" class="status-tag">加载中...</span>
@@ -20,13 +20,27 @@
</header>
<main class="app-main">
<section class="hero-card">
<div class="hero-icon">
<span class="hero-emoji">🧘</span>
</div>
<div class="hero-body">
<div class="hero-title">每一次节奏,都是一次呼吸。</div>
<div class="hero-desc">独立工具集合,互不干扰,可独立部署,可独立关闭。</div>
</div>
</section>
<div id="kit-grid" class="kit-grid">
<div class="kit-empty">⏳ 工具列表加载中...</div>
</div>
<footer class="footer muted small">
独立部署 · 独立数据库 · 独立运行锁
</footer>
</main>
</div>
<script src="api.js"></script>
<script src="hub.js"></script>
<script src="api.js?v=1.10"></script>
<script src="hub.js?v=1.10"></script>
</body>
</html>
+27 -17
View File
@@ -4,18 +4,34 @@
const $status = document.getElementById('status-tag')
const KIT_PAGES = {
personal_collect: 'kits/personal_collect/list.html'
personal_collect: 'kits/personal_collect/list.html',
rep_clock: 'kits/rep_clock/cards.html',
}
const KIT_ICONS = {
personal_collect: '📁'
personal_collect: '📁',
rep_clock: '⏱️',
}
const HERO_EMOJI = '🧘'
const HERO_TITLE = '每一次节奏,都是一次呼吸。'
const HERO_DESC = '独立工具集合,互不干扰,可独立部署,可独立关闭。'
function setStatus(text, cls) {
$status.textContent = text
$status.className = 'status-tag' + (cls ? ' ' + cls : '')
}
function escapeHtml(s) {
if (s === null || s === undefined) return ''
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function renderKits(kits) {
if (!kits || !kits.length) {
$grid.innerHTML = '<div class="kit-empty">暂无可用工具</div>'
@@ -23,13 +39,17 @@
}
$grid.innerHTML = kits.map((kit) => {
const page = KIT_PAGES[kit.id] || '#'
const icon = KIT_ICONS[kit.id] || '🧰'
const icon = kit.icon || KIT_ICONS[kit.id] || '🧰'
const meta = kit.meta ? `<div class="kit-meta">${escapeHtml(kit.meta)}</div>` : ''
return (
`<div class="kit-card" data-kit="${kit.id}" data-page="${page}">` +
`<div class="kit-card" data-kit="${escapeHtml(kit.id)}" data-page="${escapeHtml(page)}">` +
`<div class="kit-icon">${icon}</div>` +
`<div class="kit-name">${escapeHtml(kit.name || kit.id)}</div>` +
`<div class="kit-desc">${escapeHtml(kit.description || '')}</div>` +
`<div class="kit-arrow"><span>进入</span><span>→</span></div>` +
`<div class="kit-body">` +
`<div class="kit-name">${escapeHtml(kit.name || kit.id)}</div>` +
`<div class="kit-desc">${escapeHtml(kit.description || '')}</div>` +
meta +
`</div>` +
`<div class="chevron">›</div>` +
`</div>`
)
}).join('')
@@ -47,16 +67,6 @@
})
}
function escapeHtml(s) {
if (s === null || s === undefined) return ''
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
async function main() {
try {
if (!api || !api.hub) throw new Error('API 未加载')
+19 -5
View File
@@ -4,15 +4,15 @@
<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="hub.css">
<link rel="stylesheet" href="/common/common.css?v=1.10">
<link rel="stylesheet" href="hub.css?v=1.10">
</head>
<body>
<div class="app-root">
<header class="app-header">
<div class="app-header-left">
<h1 class="app-title">MilesYang时光助手</h1>
<p class="app-subtitle">手机 · 电脑,实时联动的小工具集合</p>
<p class="app-subtitle">手机 · 电脑,实时联动的小工具集合 · 完全独立 · 零共享</p>
</div>
<div class="app-header-right">
<span id="status-tag" class="status-tag">加载中...</span>
@@ -20,13 +20,27 @@
</header>
<main class="app-main">
<section class="hero-card">
<div class="hero-icon">
<span class="hero-emoji">🧘</span>
</div>
<div class="hero-body">
<div class="hero-title">每一次节奏,都是一次呼吸。</div>
<div class="hero-desc">独立工具集合,互不干扰,可独立部署,可独立关闭。</div>
</div>
</section>
<div id="kit-grid" class="kit-grid">
<div class="kit-empty">⏳ 工具列表加载中...</div>
</div>
<footer class="footer muted small">
独立部署 · 独立数据库 · 独立运行锁
</footer>
</main>
</div>
<script src="api.js"></script>
<script src="hub.js"></script>
<script src="api.js?v=1.10"></script>
<script src="hub.js?v=1.10"></script>
</body>
</html>
@@ -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>`
)
})