地点库增加是否有图片列,修复批量导入图片时新增类型无法复用

This commit is contained in:
tamakyi
2026-09-18 18:00:28 +08:00
parent ad2531c113
commit dbc5f173ef
4 changed files with 52 additions and 4 deletions
+6 -2
View File
@@ -639,6 +639,9 @@ type Place struct {
TopIcon string TopIcon string
IsHome bool IsHome bool
IsWork bool IsWork bool
// HasImage 报告该地点有没有上传过图片(地点库列表的那一列)。
// 走 place_images 的 (dataset_id, src_pk) 索引,不必把图片行取回来。
HasImage bool
// SrcPK 是 rond 内部的地点 ID,跨数据集快照稳定,用于挂备注与别名 // SrcPK 是 rond 内部的地点 ID,跨数据集快照稳定,用于挂备注与别名
SrcPK int SrcPK int
// Alias / Note 来自 place_notes:别名对外替代原名显示,备注仅站长可见 // Alias / Note 来自 place_notes:别名对外替代原名显示,备注仅站长可见
@@ -662,7 +665,8 @@ const placeCols = `p.id, COALESCE(p.name,''), COALESCE(p.poi_category,''), COALE
WHERE v2.place_id=p.id GROUP BY a.icon ORDER BY count(*) DESC LIMIT 1),''), WHERE v2.place_id=p.id GROUP BY a.icon ORDER BY count(*) DESC LIMIT 1),''),
EXISTS (SELECT 1 FROM visits v3 WHERE v3.place_id=p.id AND v3.is_home), EXISTS (SELECT 1 FROM visits v3 WHERE v3.place_id=p.id AND v3.is_home),
EXISTS (SELECT 1 FROM visits v4 WHERE v4.place_id=p.id AND v4.is_work), EXISTS (SELECT 1 FROM visits v4 WHERE v4.place_id=p.id AND v4.is_work),
p.src_pk` p.src_pk,
EXISTS (SELECT 1 FROM place_images pi WHERE pi.dataset_id=p.dataset_id AND pi.src_pk=p.src_pk)`
// PlaceCols 与 ScanPlace 配对,供包外构造只取单个地点的查询。 // PlaceCols 与 ScanPlace 配对,供包外构造只取单个地点的查询。
const PlaceCols = placeCols const PlaceCols = placeCols
@@ -671,7 +675,7 @@ func scanPlace(s interface{ Scan(...any) error }) (Place, error) {
var p Place var p Place
err := s.Scan(&p.ID, &p.Name, &p.POICategory, &p.Lat, &p.Lon, &p.City, &p.Province, err := s.Scan(&p.ID, &p.Name, &p.POICategory, &p.Lat, &p.Lon, &p.City, &p.Province,
&p.District, &p.Timezone, &p.VisitCount, &p.DwellMinutes, &p.FirstAt, &p.LastAt, &p.District, &p.Timezone, &p.VisitCount, &p.DwellMinutes, &p.FirstAt, &p.LastAt,
&p.TopActivity, &p.TopColor, &p.TopIcon, &p.IsHome, &p.IsWork, &p.SrcPK) &p.TopActivity, &p.TopColor, &p.TopIcon, &p.IsHome, &p.IsWork, &p.SrcPK, &p.HasImage)
return p, err return p, err
} }
+4
View File
@@ -292,6 +292,10 @@ table.tb td.num, table.tb th.num { text-align: right; font-variant-numeric: tabu
每列压到一两个字宽(「一般公共场所」会排成 6 行、一行一个字)。有了下限,放不下就 每列压到一两个字宽(「一般公共场所」会排成 6 行、一行一个字)。有了下限,放不下就
交给上面的 .tw 横向滚动,列宽不再被榨干。 */ 交给上面的 .tw 横向滚动,列宽不再被榨干。 */
.tw > table.tb { min-width: 640px; } .tw > table.tb { min-width: 640px; }
/* 地点库这张有 8 列(多一列「图片」),640px 下「河池市」这种 3 字城市会被挤成两行
(实测 640 给到 54px、700 给到 59px,正好回到一行)。只给这张加下限,别的表不动 ——
列少的表跟着抬到 700 会平白多出一截横向滚动。 */
.tw > table.tb.places-table { min-width: 700px; }
/* 从地点详情页跳进来定位到的那一行(?open=):淡色底,长列表里一眼能认出目标。 /* 从地点详情页跳进来定位到的那一行(?open=):淡色底,长列表里一眼能认出目标。
用底色而不是动画——动画会被 prefers-reduced-motion 跳过,那时就完全看不出来了。 */ 用底色而不是动画——动画会被 prefers-reduced-motion 跳过,那时就完全看不出来了。 */
table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-soft); } table.tb tr.located td, table.tb tr.located:hover td { background: var(--accent-soft); }
+39
View File
@@ -244,6 +244,35 @@
}).catch(function (e) { alert('创建活动失败:' + e); }); }).catch(function (e) { alert('创建活动失败:' + e); });
} }
// 自定义类别填好之后,把它挂到**所有**类别下拉上(含隐藏的选项源),当前这行改成直接选中它。
// 一次导入几十张同类型的照片时,第 2 张起就能从下拉里直接选到,不必再输入一遍同样的话 ——
// 类别是自由文本(存的就是 poi_category 原串),所以选项的 value 就是那串文字本身。
function categoryExists(sel, label) {
return [].some.call(sel.options, function (o) { return o.value === label; });
}
function addCategoryOption(label) {
var src = document.getElementById('category-options');
var sels = [].slice.call(document.querySelectorAll('select[name^="category_"]'));
if (src) sels.unshift(src);
sels.forEach(function (s) {
if (categoryExists(s, label)) return;
var opt = document.createElement('option');
opt.value = label;
opt.textContent = label;
// 插在「自定义…」前面,别让新类别落到那个特殊项之后
var tail = s.querySelector('option[value="__custom__"]');
if (tail) s.insertBefore(opt, tail); else s.appendChild(opt);
});
}
function promoteCategory(sel, input, wrap) {
if (sel.value !== '__custom__') return;
var label = (input.value || '').trim();
if (!label) return;
addCategoryOption(label);
sel.value = label;
wrap.style.display = 'none';
}
function setVal(name, v) { function setVal(name, v) {
var el = rowsBox.querySelector('input[name="' + name + '"], select[name="' + name + '"]'); var el = rowsBox.querySelector('input[name="' + name + '"], select[name="' + name + '"]');
if (el && v) el.value = v; if (el && v) el.value = v;
@@ -368,6 +397,15 @@
catSel.addEventListener('change', function () { catSel.addEventListener('change', function () {
catCustomWrap.style.display = catSel.value === '__custom__' ? '' : 'none'; catCustomWrap.style.display = catSel.value === '__custom__' ? '' : 'none';
}); });
// 填完自定义类别(失焦或回车)就把它变成所有行都能直接选的一项
catCustom.addEventListener('change', function () {
promoteCategory(catSel, catCustom, catCustomWrap);
});
catCustom.addEventListener('keydown', function (e) {
if (e.key !== 'Enter') return;
e.preventDefault();
promoteCategory(catSel, catCustom, catCustomWrap);
});
td2.appendChild(groupCap('地点属性')); td2.appendChild(groupCap('地点属性'));
row1.appendChild(fieldLabel('类别', catSel, 'flex:0 1 160px')); row1.appendChild(fieldLabel('类别', catSel, 'flex:0 1 160px'));
row1.appendChild(catCustomWrap); row1.appendChild(catCustomWrap);
@@ -401,6 +439,7 @@
actNewBox.appendChild(actNewInput); actNewBox.appendChild(actNewInput);
actNewBox.appendChild(actNewBtn); actNewBox.appendChild(actNewBtn);
var actNewWrap = fieldLabel('新建活动', actNewBox, 'flex:0 1 190px'); var actNewWrap = fieldLabel('新建活动', actNewBox, 'flex:0 1 190px');
actNewWrap.classList.add('act-new');
actNewWrap.style.display = 'none'; actNewWrap.style.display = 'none';
actSel.addEventListener('change', function () { actSel.addEventListener('change', function () {
var on = actSel.value === '__new__'; var on = actSel.value === '__new__';
+3 -2
View File
@@ -39,10 +39,10 @@
{{if .Places}} {{if .Places}}
<div class="tw"> <div class="tw">
<table class="tb"> <table class="tb places-table">
<thead> <thead>
<tr> <tr>
<th>地点</th><th>城市</th><th>主要类型</th> <th>地点</th><th>城市</th><th>主要类型</th><th>图片</th>
<th class="num">到访</th><th class="num">累计停留</th> <th class="num">到访</th><th class="num">累计停留</th>
<th>首次</th><th>最近</th> <th>首次</th><th>最近</th>
</tr> </tr>
@@ -61,6 +61,7 @@
<td> <td>
{{if .TopActivity}}<span class="chip" style="padding:1px 8px"><span class="dot" style="background:{{colorof .TopColor}}"></span>{{.TopActivity}}</span>{{else}}<span class="muted">—</span>{{end}} {{if .TopActivity}}<span class="chip" style="padding:1px 8px"><span class="dot" style="background:{{colorof .TopColor}}"></span>{{.TopActivity}}</span>{{else}}<span class="muted">—</span>{{end}}
</td> </td>
<td>{{if .HasImage}}<span title="有图片">📷</span>{{else}}<span class="muted" title="还没有上传图片">—</span>{{end}}</td>
<td class="num">{{num .VisitCount}}</td> <td class="num">{{num .VisitCount}}</td>
<td class="num">{{dur .DwellMinutes}}</td> <td class="num">{{dur .DwellMinutes}}</td>
<td class="muted">{{if .FirstAt}}{{d .FirstAt}}{{else}}—{{end}}</td> <td class="muted">{{if .FirstAt}}{{d .FirstAt}}{{else}}—{{end}}</td>