feat: 新增运动节拍训练工具套件与子工具页面
新增运动节拍RepClock全套小程序页面、后端API与数据库服务,新增subhub子工具汇总页面,优化全局主页样式与配置,为web静态资源添加版本缓存参数,补充配套调试脚本
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
if (!api || !api.hub) throw new Error('API 未加载')
|
||||
|
||||
@@ -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>`
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user