水印优化+定时任务删除+照片管理
This commit is contained in:
@@ -534,3 +534,72 @@ mark { background: #FFE9A8; color: inherit; padding: 0 1px; border-radius: 2px;
|
||||
.fin-bill .amt { font-size: 14px; font-weight: 600; flex-shrink: 0; }
|
||||
.fin-bill .amt.out { color: #FA3534; }
|
||||
.fin-bill .amt.in { color: #19BE6B; }
|
||||
|
||||
/* ---------- 相片管理 ---------- */
|
||||
.photo-main { flex: 1; min-width: 0; overflow-y: auto; padding: 16px; }
|
||||
|
||||
.photo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.photo-card {
|
||||
position: relative; border-radius: 10px; overflow: hidden;
|
||||
background: #F0EAE4; aspect-ratio: 1 / 1; cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.photo-card:hover { border-color: var(--brand); }
|
||||
.photo-card img {
|
||||
width: 100%; height: 100%; object-fit: cover; display: block;
|
||||
transition: transform .18s;
|
||||
}
|
||||
.photo-card:hover img { transform: scale(1.04); }
|
||||
|
||||
/* 悬浮时压一层信息条 */
|
||||
.photo-card .ov {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
padding: 16px 8px 6px; color: #fff; font-size: 11.5px;
|
||||
background: linear-gradient(transparent, rgba(0,0,0,.65));
|
||||
opacity: 0; transition: opacity .18s;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.photo-card:hover .ov { opacity: 1; }
|
||||
|
||||
.photo-empty {
|
||||
grid-column: 1 / -1; padding: 48px 16px; text-align: center;
|
||||
color: var(--text-dim); font-size: 13px;
|
||||
background: #fff; border-radius: 12px;
|
||||
}
|
||||
|
||||
.photo-more { text-align: center; padding: 16px 0 8px; }
|
||||
|
||||
/* 大图查看 */
|
||||
#photo-view {
|
||||
position: fixed; inset: 0; z-index: 200; display: none;
|
||||
background: rgba(20, 16, 12, .92);
|
||||
flex-direction: column;
|
||||
}
|
||||
#photo-view.open { display: flex; }
|
||||
|
||||
.pv-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 16px; color: #fff; font-size: 13px;
|
||||
}
|
||||
.pv-head .spacer { flex: 1; }
|
||||
.pv-title { font-weight: 600; }
|
||||
.pv-head button {
|
||||
background: rgba(255,255,255,.14); color: #fff; border: none;
|
||||
padding: 7px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
||||
}
|
||||
.pv-head button:hover { background: rgba(255,255,255,.26); }
|
||||
.pv-head button.danger { background: rgba(229,57,53,.85); }
|
||||
.pv-head button.danger:hover { background: #E53935; }
|
||||
|
||||
.pv-body { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 16px; }
|
||||
.pv-body img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 6px; }
|
||||
|
||||
.pv-meta {
|
||||
padding: 10px 16px 16px; color: rgba(255,255,255,.75);
|
||||
font-size: 12px; line-height: 1.7; text-align: center;
|
||||
}
|
||||
|
||||
@@ -1279,6 +1279,183 @@
|
||||
});
|
||||
if (name === 'ai') aiEnter();
|
||||
if (name === 'finance') finEnter();
|
||||
if (name === 'photos') photoEnter();
|
||||
}
|
||||
|
||||
// ================================================================ 相片管理
|
||||
//
|
||||
// 看 App「水印相机」上传的原图(**不带水印**,水印是 App 本地合成的)。
|
||||
// 页面只加载缩略图 —— 原图一张好几 MB,一屏铺开能把浏览器拖死。
|
||||
// 点开大图时才去加载原图,那时才需要清晰度。
|
||||
var photo = {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 0,
|
||||
hasMore: false,
|
||||
loading: false,
|
||||
entered: false,
|
||||
current: null // 正在大图查看的那一张
|
||||
};
|
||||
|
||||
var PHOTO_PAGE_SIZE = 24;
|
||||
|
||||
function photoEnter() {
|
||||
if (photo.entered) return;
|
||||
photo.entered = true;
|
||||
photoLoad(true);
|
||||
}
|
||||
|
||||
function photoLoad(reset) {
|
||||
if (photo.loading) return;
|
||||
photo.loading = true;
|
||||
if (reset) {
|
||||
photo.page = 0;
|
||||
photo.items = [];
|
||||
}
|
||||
|
||||
var next = photo.page + 1;
|
||||
$('photo-hint').textContent = '加载中…';
|
||||
|
||||
request('/photos?page=' + next + '&size=' + PHOTO_PAGE_SIZE)
|
||||
.then(function (data) {
|
||||
var list = (data && data.list) || [];
|
||||
photo.total = (data && data.total) || 0;
|
||||
photo.hasMore = !!(data && data.hasMore);
|
||||
photo.page = next;
|
||||
|
||||
// 按 id 去重:翻页期间如果有新照片传上来,可能两页里都有
|
||||
var seen = {};
|
||||
photo.items.forEach(function (p) { seen[p.id] = true; });
|
||||
list.forEach(function (p) {
|
||||
if (!seen[p.id]) photo.items.push(p);
|
||||
});
|
||||
|
||||
photoRender();
|
||||
})
|
||||
.catch(function (e) {
|
||||
$('photo-hint').textContent = e.message || '加载失败';
|
||||
})
|
||||
.then(function () { photo.loading = false; });
|
||||
}
|
||||
|
||||
function photoRender() {
|
||||
var box = $('photo-grid');
|
||||
box.innerHTML = '';
|
||||
|
||||
$('photo-hint').textContent = photo.total ? ('共 ' + photo.total + ' 张') : '';
|
||||
|
||||
if (!photo.items.length) {
|
||||
box.innerHTML = '<div class="photo-empty">' +
|
||||
'还没有照片。在 App 的「水印相机」里拍一张,原图会自动传到服务器。' +
|
||||
'</div>';
|
||||
$('photo-more').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
photo.items.forEach(function (p) {
|
||||
var el = document.createElement('div');
|
||||
el.className = 'photo-card';
|
||||
|
||||
// 缩略图没生成出来时退回原图(慢一点,但总比空着好)
|
||||
var thumb = p.thumbUrl || p.url;
|
||||
|
||||
el.innerHTML =
|
||||
'<img src="' + esc(thumb) + '" alt="" loading="lazy">' +
|
||||
'<div class="ov">' + esc(photoLabel(p)) + '</div>';
|
||||
|
||||
el.onclick = function () { photoOpen(p); };
|
||||
box.appendChild(el);
|
||||
});
|
||||
|
||||
$('photo-more').style.display = photo.hasMore ? '' : 'none';
|
||||
}
|
||||
|
||||
/** 卡片角标上显示的文案 */
|
||||
function photoLabel(p) {
|
||||
var parts = [];
|
||||
if (p.takenTime) parts.push(String(p.takenTime).substring(0, 16));
|
||||
if (p.address) parts.push(p.address);
|
||||
return parts.join(' · ') || ('照片 #' + p.id);
|
||||
}
|
||||
|
||||
// ---------------- 大图 ----------------
|
||||
|
||||
function photoOpen(p) {
|
||||
photo.current = p;
|
||||
$('pv-title').textContent = '照片 #' + p.id;
|
||||
// 这里才加载原图
|
||||
$('pv-img').src = p.url;
|
||||
|
||||
var meta = [];
|
||||
if (p.takenTime) meta.push('拍摄时间:' + p.takenTime);
|
||||
if (p.address) meta.push('地点:' + p.address);
|
||||
if (p.weather) meta.push('天气:' + p.weather);
|
||||
if (p.remark) meta.push('备注:' + p.remark);
|
||||
if (p.latitude != null && p.longitude != null) {
|
||||
meta.push('坐标:' + p.latitude + ', ' + p.longitude);
|
||||
}
|
||||
if (p.fileSize) meta.push('大小:' + photoSize(p.fileSize));
|
||||
$('pv-meta').textContent = meta.join(' | ');
|
||||
|
||||
$('photo-view').classList.add('open');
|
||||
}
|
||||
|
||||
function photoClose() {
|
||||
$('photo-view').classList.remove('open');
|
||||
// 清掉 src,否则大图会一直占着内存
|
||||
$('pv-img').src = '';
|
||||
photo.current = null;
|
||||
}
|
||||
|
||||
function photoSize(bytes) {
|
||||
if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
if (bytes >= 1024) return Math.round(bytes / 1024) + ' KB';
|
||||
return bytes + ' B';
|
||||
}
|
||||
|
||||
/** 下载原图。带上 token 用 fetch 取 blob —— 直接 <a href> 会 401。 */
|
||||
function photoDownload() {
|
||||
var p = photo.current;
|
||||
if (!p) return;
|
||||
|
||||
var name = 'photo_' + p.id + '.jpg';
|
||||
toast('开始下载…');
|
||||
|
||||
// p.url 是 /uploads/... 形式的同源相对路径,直接拼即可(不是 /api 下的)
|
||||
fetch(p.url, { headers: { 'Authorization': 'Bearer ' + state.token } })
|
||||
.then(function (res) {
|
||||
if (!res.ok) throw new Error('下载失败 ' + res.status);
|
||||
return res.blob();
|
||||
})
|
||||
.then(function (blob) {
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
// 等浏览器把下载接过去再释放,立刻释放会让下载变空文件
|
||||
setTimeout(function () { URL.revokeObjectURL(url); }, 5000);
|
||||
toast('已开始下载 ' + name);
|
||||
})
|
||||
.catch(function (e) { toast(e.message, true); });
|
||||
}
|
||||
|
||||
function photoDelete() {
|
||||
var p = photo.current;
|
||||
if (!p) return;
|
||||
if (!confirm('删除这张照片?\n记录和服务器上的原图、缩略图都会一并删掉,不可恢复。')) return;
|
||||
|
||||
request('/photos/' + p.id, { method: 'DELETE' })
|
||||
.then(function () {
|
||||
photoClose();
|
||||
photo.items = photo.items.filter(function (x) { return x.id !== p.id; });
|
||||
photo.total = Math.max(0, photo.total - 1);
|
||||
photoRender();
|
||||
toast('已删除');
|
||||
})
|
||||
.catch(function (e) { toast(e.message, true); });
|
||||
}
|
||||
|
||||
// ================================================================ AI 助手
|
||||
@@ -1883,6 +2060,17 @@
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 事件绑定
|
||||
// 相片管理
|
||||
$('photo-reload').onclick = function () { photoLoad(true); };
|
||||
$('photo-more-btn').onclick = function () { photoLoad(false); };
|
||||
$('pv-close').onclick = photoClose;
|
||||
$('pv-download').onclick = photoDownload;
|
||||
$('pv-delete').onclick = photoDelete;
|
||||
// 点背景关掉大图(点图片本身不关)
|
||||
$('photo-view').onclick = function (e) {
|
||||
if (e.target === this) photoClose();
|
||||
};
|
||||
|
||||
$('login-btn').onclick = doLogin;
|
||||
$('login-eye').onclick = togglePassword;
|
||||
$('login-password').onkeydown = function (e) { if (e.key === 'Enter') doLogin(); };
|
||||
|
||||
@@ -79,6 +79,10 @@
|
||||
<span class="mi-icon"><img class="ci" src="/admin/icons/bill.svg" width="18" height="18" alt=""></span>
|
||||
<span class="mi-text">财务管理</span>
|
||||
</button>
|
||||
<button class="module-item" data-module="photos">
|
||||
<span class="mi-icon"><img class="ci" src="/admin/icons/camera.svg" width="18" height="18" alt=""></span>
|
||||
<span class="mi-text">相片管理</span>
|
||||
</button>
|
||||
<button class="module-item" data-module="ai">
|
||||
<span class="mi-icon"><img class="ci" src="/admin/icons/assistant.svg" width="18" height="18"
|
||||
alt=""></span>
|
||||
@@ -175,6 +179,27 @@
|
||||
<!-- ==================== AI 助手模块 ====================
|
||||
和 App 端共用同一套后端接口(/api/ai/**),历史存在服务端的
|
||||
chat_session / chat_message 里 —— 所以两边看到的会话是同一批。 -->
|
||||
<!-- ==================== 相片管理 ==================== -->
|
||||
<section class="module-panel" data-module-panel="photos" style="display:none">
|
||||
|
||||
<div class="toolbar">
|
||||
<span class="toolbar-title">相片管理</span>
|
||||
<span class="toolbar-note" id="photo-hint"></span>
|
||||
<div class="spacer"></div>
|
||||
<button id="photo-reload">刷新</button>
|
||||
</div>
|
||||
|
||||
<div class="module-body">
|
||||
<main class="photo-main">
|
||||
<!-- 缩略图网格。点一张看大图,右上角可下载/删除 -->
|
||||
<div id="photo-grid" class="photo-grid"></div>
|
||||
<div id="photo-more" class="photo-more" style="display:none">
|
||||
<button id="photo-more-btn">加载更多</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="module-panel" data-module-panel="ai" style="display:none">
|
||||
|
||||
<div class="toolbar">
|
||||
@@ -226,6 +251,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 相片大图查看 ==================== -->
|
||||
<div id="photo-view">
|
||||
<div class="pv-head">
|
||||
<span class="pv-title" id="pv-title"></span>
|
||||
<div class="spacer"></div>
|
||||
<button id="pv-download">下载原图</button>
|
||||
<button class="danger" id="pv-delete">删除</button>
|
||||
<button id="pv-close">关闭</button>
|
||||
</div>
|
||||
<div class="pv-body">
|
||||
<img id="pv-img" alt="">
|
||||
</div>
|
||||
<div class="pv-meta" id="pv-meta"></div>
|
||||
</div>
|
||||
|
||||
<!-- ==================== 目录名输入 / 目录选择弹窗 ==================== -->
|
||||
<div id="dialog">
|
||||
<div class="dialog-box">
|
||||
|
||||
Reference in New Issue
Block a user