Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12556bf10f | |||
| 7d4329f773 |
@@ -0,0 +1,7 @@
|
|||||||
|
# v0.1.39 — 手机录像筛选布局
|
||||||
|
|
||||||
|
在 v0.1.38 录像修复基础上调整小屏幕回放表单:镜头、录像来源、日期和查询按钮纵向排列,控件保持完整宽度,避免三个选择框挤在一行。
|
||||||
|
|
||||||
|
本版仅调整前端;保留 v0.1.38 的 30 天默认留存、容量保护、录像状态与时间精度修复。设备数据及录像配置不迁移。生产更新仅重启网页服务,持续录像的 MediaMTX 进程保留。
|
||||||
|
|
||||||
|
验证:141 项 JavaScript 回归测试与 Vue / Bootstrap 生产构建通过;后端沿用已通过 173 项回归的 v0.1.38 实现。
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# v0.1.40 · 家谱画布稳定性与连续回放修复
|
||||||
|
|
||||||
|
- 修复全屏家谱因小数像素取整而反复产生滚动条、重算居中的抖动。
|
||||||
|
- 合并画布尺寸通知,尺寸不变时不重复适配;手动缩放与拖动位置保持。
|
||||||
|
- 修复浏览器不支持原生全屏时,兼容全屏画布被侧栏遮挡的问题。
|
||||||
|
- 保留手机端家谱画布及人物档案的独立滚动区域。
|
||||||
|
- 修复录像回放以直播速度读取历史 HTTP 文件,造成长片段上游写入超时而提前截断的问题;保留转换并发、线程和生命周期上限。
|
||||||
|
- 新增画布小数像素、尺寸通知合并、手动视角、隐藏和重置回归测试。
|
||||||
|
|
||||||
|
录像保留策略仍以当前平台设置为准:30 天为最长保留时间;录像容量上限和磁盘预留可提前触发暂停,不能据此保证存满 30 天。
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "zhao-steward-ui",
|
"name": "zhao-steward-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.38",
|
"version": "0.1.40",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
Vendored
+5
@@ -219,6 +219,11 @@ html[data-density="compact"] .card-body { padding: 18px; }
|
|||||||
.home-card-actions { gap: 8px; flex-wrap: wrap; }
|
.home-card-actions { gap: 8px; flex-wrap: wrap; }
|
||||||
.btn { min-height: 44px; }
|
.btn { min-height: 44px; }
|
||||||
.form-control, .form-select { min-height: 44px; font-size: 16px; }
|
.form-control, .form-select { min-height: 44px; font-size: 16px; }
|
||||||
|
#page-playback .playback-controls { display: grid; grid-template-columns: minmax(0, 1fr); gap: 14px; }
|
||||||
|
#page-playback .playback-controls label { width: 100%; min-width: 0; }
|
||||||
|
#page-playback .playback-controls input, #page-playback .playback-controls select { width: 100%; }
|
||||||
|
#page-playback .playback-controls button { width: 100%; }
|
||||||
|
#play-info { overflow-wrap: anywhere; }
|
||||||
.device-view-controls { padding: 16px; }
|
.device-view-controls { padding: 16px; }
|
||||||
.network-map { padding: 14px; }
|
.network-map { padding: 14px; }
|
||||||
.network-platform { padding: 16px; }
|
.network-platform { padding: 16px; }
|
||||||
|
|||||||
@@ -15,6 +15,6 @@ export function enhance(root){
|
|||||||
export async function loadControllers(){
|
export async function loadControllers(){
|
||||||
enhance(document.getElementById('app'))
|
enhance(document.getElementById('app'))
|
||||||
new MutationObserver(records=>{for(const record of records)for(const node of record.addedNodes)enhance(node)}).observe(document.getElementById('app'),{childList:true,subtree:true})
|
new MutationObserver(records=>{for(const record of records)for(const node of record.addedNodes)enhance(node)}).observe(document.getElementById('app'),{childList:true,subtree:true})
|
||||||
for(const path of [...scripts,'/vue-bridge.js'])await new Promise((resolve,reject)=>{const script=document.createElement('script');script.src=path+'?ui=0.1.38';script.onload=resolve;script.onerror=()=>reject(new Error('Could not load '+path));document.head.append(script)})
|
for(const path of [...scripts,'/vue-bridge.js'])await new Promise((resolve,reject)=>{const script=document.createElement('script');script.src=path+'?ui=0.1.40';script.onload=resolve;script.onerror=()=>reject(new Error('Could not load '+path));document.head.append(script)})
|
||||||
await window.ZhaoVue.boot()
|
await window.ZhaoVue.boot()
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -33,7 +33,9 @@ def command(source, authorization, duration):
|
|||||||
'-threads', '1', '-filter_threads', '1'] + acceleration + [
|
'-threads', '1', '-filter_threads', '1'] + acceleration + [
|
||||||
'-rw_timeout', '15000000', '-headers', 'Authorization: ' + authorization + '\r\n',
|
'-rw_timeout', '15000000', '-headers', 'Authorization: ' + authorization + '\r\n',
|
||||||
'-analyzeduration', '1000000', '-probesize', '1000000',
|
'-analyzeduration', '1000000', '-probesize', '1000000',
|
||||||
'-readrate', '1', '-i', source, '-t', str(duration),
|
# Recordings are finite HTTP downloads. Pacing their input like a
|
||||||
|
# live source stalls upstream writes and truncates long responses.
|
||||||
|
'-i', source, '-t', str(duration),
|
||||||
'-map', '0:v:0', '-map', '0:a:0?', '-sn', '-dn'] + video + [
|
'-map', '0:v:0', '-map', '0:a:0?', '-sn', '-dn'] + video + [
|
||||||
'-c:a', 'aac', '-ar', '48000', '-ac', '1', '-b:a', '64k',
|
'-c:a', 'aac', '-ar', '48000', '-ac', '1', '-b:a', '64k',
|
||||||
'-movflags', '+frag_keyframe+empty_moov+default_base_moof',
|
'-movflags', '+frag_keyframe+empty_moov+default_base_moof',
|
||||||
|
|||||||
@@ -258,6 +258,8 @@ class WebTests(unittest.TestCase):
|
|||||||
self.assertIn('h264_vaapi', command)
|
self.assertIn('h264_vaapi', command)
|
||||||
self.assertIn('pipe:1', command)
|
self.assertIn('pipe:1', command)
|
||||||
self.assertEqual(command[command.index('-analyzeduration') + 1], '1000000')
|
self.assertEqual(command[command.index('-analyzeduration') + 1], '1000000')
|
||||||
|
self.assertNotIn('-readrate', command)
|
||||||
|
self.assertNotIn('-re', command)
|
||||||
|
|
||||||
def test_ptz_requires_both_space_and_control_permission(self):
|
def test_ptz_requires_both_space_and_control_permission(self):
|
||||||
app.save_object('sites', dict(id='permitted', name='Permitted'))
|
app.save_object('sites', dict(id='permitted', name='Permitted'))
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),vm=require('node:vm');
|
||||||
|
function fixture(){
|
||||||
|
const space={style:{}},surface={style:{}},label={},frames=new Map();let next=0,observer;
|
||||||
|
const viewport={clientWidth:1550,clientHeight:601,rect:{width:1550,height:600.5234375},scrollLeft:0,scrollTop:0,
|
||||||
|
addEventListener(){},getBoundingClientRect(){return this.rect;},
|
||||||
|
querySelector(s){return s==='.family-map-space'?space:surface;},replaceChildren(){},
|
||||||
|
scrollTo(x,y){this.scrollLeft=x;this.scrollTop=y;}};
|
||||||
|
const scope=vm.createContext({$:()=>label,ResizeObserver:class{constructor(fn){observer=fn;}observe(){}},
|
||||||
|
requestAnimationFrame(fn){frames.set(++next,fn);return next;},cancelAnimationFrame(id){frames.delete(id);}});
|
||||||
|
vm.runInContext(fs.readFileSync('web/family-map.js','utf8')+';this.MapClass=FamilyMap',scope);
|
||||||
|
const map=new scope.MapClass(viewport,()=>{});map.graph={width:400,height:264,nodes:[]};
|
||||||
|
return {map,viewport,space,surface,frames,resize:()=>observer(),flush(){const jobs=[...frames.values()];frames.clear();jobs.forEach(fn=>fn());}};
|
||||||
|
}
|
||||||
|
test('fractional fullscreen size never creates a too-tall scroll surface',()=>{
|
||||||
|
const f=fixture();for(let i=0;i<20;i++){f.map.fit();assert.equal(f.space.style.height,'600px');assert.ok(parseFloat(f.space.style.height)<=f.viewport.rect.height);}
|
||||||
|
assert.equal(f.surface.style.transform,'translate(575px,168px) scale(1)');
|
||||||
|
});
|
||||||
|
test('resize notifications coalesce and unchanged dimensions do not refit',()=>{
|
||||||
|
const f=fixture();let calls=0;const fit=f.map.fit.bind(f.map);f.map.fit=()=>{calls++;fit();};
|
||||||
|
f.resize();f.resize();assert.equal(f.frames.size,1);f.flush();assert.equal(calls,1);
|
||||||
|
f.resize();f.flush();assert.equal(calls,1);
|
||||||
|
f.viewport.rect.height=700.7;f.viewport.clientHeight=701;f.resize();f.flush();assert.equal(calls,2);
|
||||||
|
});
|
||||||
|
test('manual zoom and scroll survive a resize notification',()=>{
|
||||||
|
const f=fixture();f.map.autoFit=false;f.map.scale=1.5;f.viewport.scrollLeft=15;f.viewport.scrollTop=30;
|
||||||
|
f.resize();f.flush();assert.equal(f.map.scale,1.5);assert.equal(f.map.autoFit,false);
|
||||||
|
assert.equal(f.viewport.scrollLeft,15);assert.equal(f.viewport.scrollTop,30);
|
||||||
|
});
|
||||||
|
test('hidden canvas does not resize and pending work is cancelled on reset',()=>{
|
||||||
|
const f=fixture();f.viewport.clientWidth=0;f.resize();f.flush();assert.equal(f.space.style.width,undefined);
|
||||||
|
f.resize();f.map.reset();assert.equal(f.frames.size,0);assert.equal(f.map.graph,null);
|
||||||
|
});
|
||||||
|
test('scaled map clips unscaled overflow and fullscreen fallback clears the sidebar',()=>{
|
||||||
|
const css=fs.readFileSync('web/family-map.css','utf8');
|
||||||
|
assert.match(css,/\.family-map-space\{min-width:0;min-height:0;overflow:clip\}/);
|
||||||
|
assert.match(css,/scrollbar-gutter:stable/);assert.match(css,/\.family-workspace\.family-expanded\{z-index:1100\}/);
|
||||||
|
});
|
||||||
@@ -74,3 +74,9 @@ test('recording timeline paints with theme tokens and keeps range geometry', ()
|
|||||||
]);
|
]);
|
||||||
assert.equal(ctx.strokeStyle, '#d3ccbc');
|
assert.equal(ctx.strokeStyle, '#d3ccbc');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mobile playback filters keep full-width readable controls',()=>{
|
||||||
|
const css=fs.readFileSync('frontend/src/bootstrap-polish.css','utf8');
|
||||||
|
assert.match(css,/#page-playback \.playback-controls \{ display: grid; grid-template-columns: minmax\(0, 1fr\)/);
|
||||||
|
assert.match(css,/#page-playback \.playback-controls input, #page-playback \.playback-controls select \{ width: 100%/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,6 +7,18 @@
|
|||||||
|
|
||||||
.family-workspace.family-expanded{position:fixed;inset:0;z-index:8;}
|
.family-workspace.family-expanded{position:fixed;inset:0;z-index:8;}
|
||||||
|
|
||||||
|
/* Keep scaled descendants and fractional fullscreen sizes out of the
|
||||||
|
scrollable overflow calculation. Reserve one gutter instead of alternating. */
|
||||||
|
.family-map-viewport{scrollbar-gutter:stable;overflow-anchor:none}
|
||||||
|
.family-map-space{min-width:0;min-height:0;overflow:clip}
|
||||||
|
.family-workspace:is(:fullscreen,.family-expanded){min-height:0;grid-template-rows:minmax(0,1fr)}
|
||||||
|
.family-workspace:is(:fullscreen,.family-expanded) .family-canvas-panel{min-height:0}
|
||||||
|
.family-workspace:is(:fullscreen,.family-expanded) .family-map-heading,
|
||||||
|
.family-workspace:is(:fullscreen,.family-expanded) .family-map-controls,
|
||||||
|
.family-workspace:is(:fullscreen,.family-expanded) .family-map-footer{flex-shrink:0}
|
||||||
|
.family-workspace.family-expanded{z-index:1100}
|
||||||
|
@media(max-width:1000px){.family-workspace:is(:fullscreen,.family-expanded) .family-canvas-panel{min-height:65vh}}
|
||||||
|
|
||||||
/* Versions remain readable on a small screen, including long family narratives. */
|
/* Versions remain readable on a small screen, including long family narratives. */
|
||||||
#history-dialog{width:min(1040px,94vw)}
|
#history-dialog{width:min(1040px,94vw)}
|
||||||
#history-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;max-height:260px;overflow:auto;margin:16px 0}
|
#history-list{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;max-height:260px;overflow:auto;margin:16px 0}
|
||||||
|
|||||||
+21
-6
@@ -10,9 +10,24 @@ class FamilyMap {
|
|||||||
viewport.addEventListener('pointermove',e=>{if(!this.drag)return;viewport.scrollLeft=this.drag.left+this.drag.x-e.clientX;viewport.scrollTop=this.drag.top+this.drag.y-e.clientY;});
|
viewport.addEventListener('pointermove',e=>{if(!this.drag)return;viewport.scrollLeft=this.drag.left+this.drag.x-e.clientX;viewport.scrollTop=this.drag.top+this.drag.y-e.clientY;});
|
||||||
const release=()=>{this.drag=null;viewport.classList.remove('panning');};
|
const release=()=>{this.drag=null;viewport.classList.remove('panning');};
|
||||||
viewport.addEventListener('pointerup',release);viewport.addEventListener('pointercancel',release);viewport.addEventListener('lostpointercapture',release);
|
viewport.addEventListener('pointerup',release);viewport.addEventListener('pointercancel',release);viewport.addEventListener('lostpointercapture',release);
|
||||||
this.resize=new ResizeObserver(()=>{if(this.autoFit)this.fit();else this.applyScale();});this.resize.observe(viewport);
|
this.resizeFrame=0;this.resizeSize='';
|
||||||
|
this.resize=new ResizeObserver(()=>this.scheduleResize());this.resize.observe(viewport,{box:'border-box'});
|
||||||
|
}
|
||||||
|
reset(){cancelAnimationFrame(this.resizeFrame);this.resizeFrame=0;this.resizeSize='';this.key='';this.data=null;this.graph=null;this.viewport.replaceChildren();this.autoFit=true;this.drag=null;$('#family-zoom-value').textContent='100%';}
|
||||||
|
viewportSize(){
|
||||||
|
const v=this.viewport,r=v.getBoundingClientRect();
|
||||||
|
// clientHeight rounds fractional pixels up; using it as a child height can
|
||||||
|
// create a scrollbar, remove it on the next fit, and repeat indefinitely.
|
||||||
|
return {width:Math.floor(Math.min(v.clientWidth,r.width)),height:Math.floor(Math.min(v.clientHeight,r.height))};
|
||||||
|
}
|
||||||
|
scheduleResize(){
|
||||||
|
if(this.resizeFrame)return;
|
||||||
|
this.resizeFrame=requestAnimationFrame(()=>{
|
||||||
|
this.resizeFrame=0;const {width,height}=this.viewportSize(),key=width+':'+height;
|
||||||
|
if(!width||!height||key===this.resizeSize)return;
|
||||||
|
this.resizeSize=key;if(this.autoFit)this.fit();else this.applyScale();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
reset(){this.key='';this.data=null;this.viewport.replaceChildren();this.autoFit=true;this.drag=null;$('#family-zoom-value').textContent='100%';}
|
|
||||||
render(data) {
|
render(data) {
|
||||||
this.data=data;
|
this.data=data;
|
||||||
const key=JSON.stringify([data.people,data.links,data.showArchived,data.selfId]);
|
const key=JSON.stringify([data.people,data.links,data.showArchived,data.selfId]);
|
||||||
@@ -46,16 +61,16 @@ class FamilyMap {
|
|||||||
}
|
}
|
||||||
applyScale() {
|
applyScale() {
|
||||||
const v=this.viewport,g=this.graph,space=v.querySelector('.family-map-space'),surface=v.querySelector('.family-map-surface');
|
const v=this.viewport,g=this.graph,space=v.querySelector('.family-map-space'),surface=v.querySelector('.family-map-surface');
|
||||||
if(!g||!space||!v.clientWidth)return;
|
const {width,height}=this.viewportSize();if(!g||!space||!width||!height)return;
|
||||||
const w=Math.max(v.clientWidth,g.width*this.scale+32),h=Math.max(v.clientHeight,g.height*this.scale+32);
|
const w=Math.max(width,Math.ceil(g.width*this.scale+32)),h=Math.max(height,Math.ceil(g.height*this.scale+32));
|
||||||
space.style.width=w+'px';space.style.height=h+'px';
|
space.style.width=w+'px';space.style.height=h+'px';
|
||||||
this.left=(w-g.width*this.scale)/2;this.top=(h-g.height*this.scale)/2;
|
this.left=(w-g.width*this.scale)/2;this.top=(h-g.height*this.scale)/2;
|
||||||
surface.style.transform=`translate(${this.left}px,${this.top}px) scale(${this.scale})`;
|
surface.style.transform=`translate(${this.left}px,${this.top}px) scale(${this.scale})`;
|
||||||
$('#family-zoom-value').textContent=Math.round(this.scale*100)+'%';
|
$('#family-zoom-value').textContent=Math.round(this.scale*100)+'%';
|
||||||
}
|
}
|
||||||
fit() {
|
fit() {
|
||||||
const v=this.viewport,g=this.graph;if(!g||!v.clientWidth||!v.clientHeight)return;
|
const v=this.viewport,g=this.graph,{width,height}=this.viewportSize();if(!g||!width||!height)return;
|
||||||
this.autoFit=true;this.scale=Math.max(.001,Math.min(1,(v.clientWidth-40)/g.width,(v.clientHeight-40)/g.height));
|
this.autoFit=true;this.scale=Math.max(.001,Math.min(1,(width-40)/g.width,(height-40)/g.height));
|
||||||
this.applyScale();v.scrollTo(0,0);
|
this.applyScale();v.scrollTo(0,0);
|
||||||
}
|
}
|
||||||
zoom(factor) {
|
zoom(factor) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-DKr_acag.js"></script>
|
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#244d43"><link rel="icon" href="/zhao-icon.png"><link rel="apple-touch-icon" href="/zhao-icon.png"><title>赵府 · 智能数字管家</title> <script type="module" crossorigin src="/ui/assets/index-BN-llATb.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/ui/assets/index-XTjkr4dE.css">
|
<link rel="stylesheet" crossorigin href="/ui/assets/index-B2d9n-y0.css">
|
||||||
</head><body><div id="app"></div></body></html>
|
</head><body><div id="app"></div></body></html>
|
||||||
|
|||||||
Reference in New Issue
Block a user