fix: verify cloud image flow and clarify save feedback
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# 赵府智家 · 视觉系统
|
# 赵府智家 · 视觉系统
|
||||||
|
|
||||||
|
## v0.1.32 · 真实网盘验收与保存反馈
|
||||||
|
|
||||||
|
- 修复家庭档案已保存、后续列表刷新失败时提示不可见的问题;立即提示保存结果,刷新失败单独提示,无需重复提交。
|
||||||
|
- 网盘设置中展示封面、平台外观、个人记事、数据库备份四类存储说明;修正“全部本地存储”的旧文案。
|
||||||
|
- 异常国家编码类型统一返回字段校验错误,不再触发服务器异常,失败保留原档案。
|
||||||
|
- 真实百度网盘验收:在应用内“验收资料”目录上传 100867 字节合成图片,清空进程缓存后读回校验一致;重复上传返回相同文件 ID。正式家庭的已上传封面也通过独立回读及浏览器刷新、重新打开验证。验收不修改正式家庭档案。
|
||||||
|
- 手机宽度检查覆盖已上传封面与编辑对话框。自动文件选择器仍受 Edge 扩展文件 URL 权限限制,不将接口上传或用户自行上传等同为自动选文件验收。
|
||||||
|
|
||||||
|
验证:150 项 Python、120 项 JavaScript 回归通过。真实网盘和正式封面回读校验通过;360px 网盘说明、390px 封面及对话框无横向溢出。上线前备份数据库,发布源码后部署。
|
||||||
|
|
||||||
## v0.1.31 · 地域分级选择与网盘图片
|
## v0.1.31 · 地域分级选择与网盘图片
|
||||||
|
|
||||||
- 家庭/场所、空间和家谱籍贯/出生地共用地域控件:中国省市区联动、世界国家/地区选择、自定义历史地名或其他天体。更换上级清空下级;服务端以编码校验父子关系。旧地名无法匹配时原样保留为自定义,权限仍按家庭/场所划分。
|
- 家庭/场所、空间和家谱籍贯/出生地共用地域控件:中国省市区联动、世界国家/地区选择、自定义历史地名或其他天体。更换上级清空下级;服务端以编码校验父子关系。旧地名无法匹配时原样保留为自定义,权限仍按家庭/场所划分。
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ def normalize(value, clean):
|
|||||||
children = item.get('children', [])
|
children = item.get('children', [])
|
||||||
elif mode == 'world':
|
elif mode == 'world':
|
||||||
code = value.get('countryCode', '')
|
code = value.get('countryCode', '')
|
||||||
if code not in DATA['countries']: raise ValueError('请选择国家或地区')
|
if not isinstance(code, str) or code not in DATA['countries']: raise ValueError('请选择国家或地区')
|
||||||
if code == 'CN': raise ValueError('中国地区请使用中国分级选择')
|
if code == 'CN': raise ValueError('中国地区请使用中国分级选择')
|
||||||
result.update(countryCode=code, country=DATA['countries'][code])
|
result.update(countryCode=code, country=DATA['countries'][code])
|
||||||
for field in ('province','city','district'):
|
for field in ('province','city','district'):
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ class GeographyUnitTests(unittest.TestCase):
|
|||||||
result=self.norm(dict(mode='custom',body='火星',path='基地 / 一区 / 一栋'))
|
result=self.norm(dict(mode='custom',body='火星',path='基地 / 一区 / 一栋'))
|
||||||
self.assertEqual(result['body'],'火星');self.assertEqual(result['countryCode'],'')
|
self.assertEqual(result['body'],'火星');self.assertEqual(result['countryCode'],'')
|
||||||
|
|
||||||
|
def test_invalid_country_types_have_a_validation_error(self):
|
||||||
|
for code in ([], {}, None, 42, True):
|
||||||
|
with self.subTest(code=code),self.assertRaises(ValueError):
|
||||||
|
self.norm(dict(mode='world',countryCode=code))
|
||||||
|
|
||||||
def test_legacy_edits_drop_stale_codes_and_other_updates_preserve(self):
|
def test_legacy_edits_drop_stale_codes_and_other_updates_preserve(self):
|
||||||
old=dict(geography=self.norm(dict(mode='china',provinceCode='37',cityCode='3706')),province='山东省',city='烟台市')
|
old=dict(geography=self.norm(dict(mode='china',provinceCode='37',cityCode='3706')),province='山东省',city='烟台市')
|
||||||
item={};geography.apply(item,{},old,app.clean_text,app.Problem)
|
item={};geography.apply(item,{},old,app.clean_text,app.Problem)
|
||||||
@@ -39,6 +44,12 @@ class GeographyIntegrationTests(unittest.TestCase):
|
|||||||
stop_server=appearance_fixture.AppearanceTests.stop_server
|
stop_server=appearance_fixture.AppearanceTests.stop_server
|
||||||
request=appearance_fixture.AppearanceTests.request
|
request=appearance_fixture.AppearanceTests.request
|
||||||
|
|
||||||
|
def test_bad_country_input_returns_400_and_preserves_profile(self):
|
||||||
|
old=app.get_object('households',self.aid)
|
||||||
|
status,result=self.request('/api/households',self.root,dict(id=self.aid,name='不应写入',geography=dict(mode='world',countryCode=[])))
|
||||||
|
self.assertEqual(status,400);self.assertIn('国家',result['error'])
|
||||||
|
self.assertEqual(app.get_object('households',self.aid),old)
|
||||||
|
|
||||||
def test_household_cover_and_path_roundtrip(self):
|
def test_household_cover_and_path_roundtrip(self):
|
||||||
v=dict(mode='china',provinceCode='37',cityCode='3706',districtCode='370682')
|
v=dict(mode='china',provinceCode='37',cityCode='3706',districtCode='370682')
|
||||||
status,saved=self.request('/api/households',self.root,dict(id=self.aid,name='封面与地域',geography=v,coverData=appearance_fixture.image_data()))
|
status,saved=self.request('/api/households',self.root,dict(id=self.aid,name='封面与地域',geography=v,coverData=appearance_fixture.image_data()))
|
||||||
|
|||||||
@@ -30,3 +30,46 @@ test('owner is visibly distinct from family and super administrators',()=>{
|
|||||||
assert.equal(c.label({id:'backup',role:'family_admin'}),'家庭管理员');
|
assert.equal(c.label({id:'backup',role:'family_admin'}),'家庭管理员');
|
||||||
assert.equal(c.label({id:'root',role:'admin'}),'超级管理员');
|
assert.equal(c.label({id:'root',role:'admin'}),'超级管理员');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function saveHarness({saveError,refreshError}={}){
|
||||||
|
const events=[],error={textContent:''},elements=[{disabled:false},{disabled:true}];
|
||||||
|
elements.id={value:'home'};elements.profileRevision={value:0};
|
||||||
|
const form={elements,querySelector:()=>error,closest:()=>({close:()=>events.push('closed')})};
|
||||||
|
let submit;
|
||||||
|
const state={households:[{id:'home',name:'原档案'}]};
|
||||||
|
const c=vm.createContext({coverSaving:false,coverDraft:'data:image/png;base64,test',sessionGeneration:1,
|
||||||
|
$:()=>({addEventListener:(_type,fn)=>{submit=fn;}}),imageJobPending:()=>false,imageJobs:new Map(),
|
||||||
|
formValues:()=>({id:'home',name:'已保存'}),state,
|
||||||
|
api:async()=>{if(saveError)throw saveError;return {id:'home',name:'已保存',profileRevision:1,coverRevision:'new',coverRef:{fsId:'1'}};},
|
||||||
|
renderHouseholds:()=>events.push('rendered'),toast:t=>events.push(t),
|
||||||
|
refresh:async()=>{events.push('refresh');if(refreshError)throw refreshError;}});
|
||||||
|
vm.runInContext(part(homes,"$('#household-form').addEventListener('submit'",'document.addEventListener'),c);
|
||||||
|
return {run:()=>submit({preventDefault(){},currentTarget:form}),events,error,elements,state,c};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('saved cover stays committed and success is visible before a failed refresh',async()=>{
|
||||||
|
const h=saveHarness({refreshError:Error('network')});await h.run();
|
||||||
|
assert.equal(h.state.households[0].name,'已保存');assert.equal(h.elements.profileRevision.value,1);
|
||||||
|
assert.match(h.state.households[0].coverUrl,/v=new/);
|
||||||
|
assert.ok(h.events.indexOf('封面已存百度网盘,归属档案已保存')<h.events.indexOf('refresh'));
|
||||||
|
assert.match(h.events.at(-1),/无需重复保存/);assert.ok(h.events.includes('closed'));
|
||||||
|
assert.deepEqual(h.elements.map(e=>e.disabled),[false,true]);assert.equal(h.c.coverSaving,false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed cover save keeps the draft open and does not claim success',async()=>{
|
||||||
|
const h=saveHarness({saveError:Error('网盘暂时不可用')});await h.run();
|
||||||
|
assert.equal(h.state.households[0].name,'原档案');assert.equal(h.error.textContent,'网盘暂时不可用');
|
||||||
|
assert.deepEqual(h.events,[]);assert.equal(h.c.coverDraft,'data:image/png;base64,test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale post-save refresh does not show a new-session warning',async()=>{
|
||||||
|
const h=saveHarness({refreshError:Object.assign(Error('stale'),{stale:true})});await h.run();
|
||||||
|
assert.equal(h.events.at(-1),'refresh');assert.equal(h.state.households[0].name,'已保存');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('storage guide names cloud categories and avoids old all-local claims',()=>{
|
||||||
|
const html=fs.readFileSync('web/index.html','utf8');
|
||||||
|
assert.ok(html.includes('id="baidu-storage-guide"'));
|
||||||
|
for(const label of ['家庭与场所:','平台外观:','个人记事:','数据库备份:'])assert.ok(html.includes(label));
|
||||||
|
assert.ok(!html.includes('自有部署 · 本地存储'));assert.ok(!app.includes('数据存于本机'));
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -94,7 +94,7 @@ function render() {
|
|||||||
$('#metric-used').textContent = state.storage.usedGB == null ? '—' : state.storage.usedGB + ' GB';
|
$('#metric-used').textContent = state.storage.usedGB == null ? '—' : state.storage.usedGB + ' GB';
|
||||||
$('#service-status').textContent = state.mediaOnline ? '视频服务已连接' : '视频服务未连接';
|
$('#service-status').textContent = state.mediaOnline ? '视频服务已连接' : '视频服务未连接';
|
||||||
$('#service-status').classList.toggle('off', !state.mediaOnline);
|
$('#service-status').classList.toggle('off', !state.mediaOnline);
|
||||||
$('#version').textContent = state.version ? 'v' + state.version + ' / 数据存于本机' : '独立部署 / 数据自有';
|
$('#version').textContent = state.version ? 'v' + state.version + ' / 独立部署 · 数据自有' : '独立部署 / 数据自有';
|
||||||
const warning = state.storage.paused ? state.storage.reason + ',已暂停录像。' : '';
|
const warning = state.storage.paused ? state.storage.reason + ',已暂停录像。' : '';
|
||||||
$('#banner').textContent = warning; $('#banner').hidden = !warning;
|
$('#banner').textContent = warning; $('#banner').hidden = !warning;
|
||||||
setOptions($('#site-filter'), '<option value="">全部空间</option>' + state.sites.map(s=>`<option value="${s.id}">${esc(siteName(s.id))}</option>`).join(''));
|
setOptions($('#site-filter'), '<option value="">全部空间</option>' + state.sites.map(s=>`<option value="${s.id}">${esc(siteName(s.id))}</option>`).join(''));
|
||||||
|
|||||||
+2
-1
@@ -64,7 +64,8 @@ $('#household-form').addEventListener('submit',async e=>{
|
|||||||
const view={...saved,coverUrl:saved.coverData||saved.coverRef?'/api/households/cover?id='+encodeURIComponent(saved.id)+'&v='+saved.coverRevision:''};
|
const view={...saved,coverUrl:saved.coverData||saved.coverRef?'/api/households/cover?id='+encodeURIComponent(saved.id)+'&v='+saved.coverRevision:''};
|
||||||
delete view.coverData;delete view.coverRef;const previous=(state.households||[]).find(x=>x.id===saved.id);
|
delete view.coverData;delete view.coverRef;const previous=(state.households||[]).find(x=>x.id===saved.id);
|
||||||
if(previous)Object.assign(previous,view);else (state.households||=[]).push(view);
|
if(previous)Object.assign(previous,view);else (state.households||=[]).push(view);
|
||||||
renderHouseholds();f.closest('dialog').close();await refresh();toast(payload.coverData?'封面已存百度网盘,归属档案已保存':payload.coverData===''?'归属档案已保存,已使用默认封面':'归属档案已保存');
|
renderHouseholds();f.closest('dialog').close();toast(payload.coverData?'封面已存百度网盘,归属档案已保存':payload.coverData===''?'归属档案已保存,已使用默认封面':'归属档案已保存');
|
||||||
|
try{await refresh();}catch(error){if(!error.stale&&generation===sessionGeneration)toast('档案已保存,列表刷新暂未完成,请稍后刷新页面。无需重复保存。');}
|
||||||
}catch(error){if(!error.stale&&generation===sessionGeneration)f.querySelector('.form-error').textContent=error.message;}
|
}catch(error){if(!error.stale&&generation===sessionGeneration)f.querySelector('.form-error').textContent=error.message;}
|
||||||
finally{coverSaving=false;controls.forEach((x,i)=>x.disabled=disabled[i]);}
|
finally{coverSaving=false;controls.forEach((x,i)=>x.disabled=disabled[i]);}
|
||||||
});
|
});
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@
|
|||||||
<button data-page="settings">⚙ <span>设置中心</span></button>
|
<button data-page="settings">⚙ <span>设置中心</span></button>
|
||||||
<button data-page="storage">▤ <span>平台存储与审计</span></button>
|
<button data-page="storage">▤ <span>平台存储与审计</span></button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="side-note"><span class="dot"></span> 自有部署 · 本地存储<p>按空间组织画面<br>让每一段记录有处可寻</p></div>
|
<div class="side-note"><span class="dot"></span> 独立部署 · 数据自有<p>按空间组织画面<br>让每一段记录有处可寻</p></div>
|
||||||
<p id="current-user" class="description"></p><button id="account-password" class="quiet" hidden>修改我的密码</button><button id="logout" class="quiet">退出登录</button>
|
<p id="current-user" class="description"></p><button id="account-password" class="quiet" hidden>修改我的密码</button><button id="logout" class="quiet">退出登录</button>
|
||||||
</aside>
|
</aside>
|
||||||
<main>
|
<main>
|
||||||
@@ -38,14 +38,14 @@
|
|||||||
<section id="page-settings" class="page" hidden>
|
<section id="page-settings" class="page" hidden>
|
||||||
<div class="section-head"><h2>把这里布置成你习惯的样子</h2><button id="settings-reload" class="quiet">重新读取设置</button></div><p id="settings-error" role="status"></p><div class="settings-sections" role="group" aria-label="设置分类"><button class="quiet" data-settings-section="preferences" aria-pressed="true">我的偏好</button><button class="quiet" data-settings-section="baidu" data-platform-setting hidden>网盘连接</button><button class="quiet" data-settings-section="brand" data-platform-setting hidden>品牌与外观</button><button class="quiet" data-settings-section="backup" data-platform-setting hidden>数据库备份</button></div>
|
<div class="section-head"><h2>把这里布置成你习惯的样子</h2><button id="settings-reload" class="quiet">重新读取设置</button></div><p id="settings-error" role="status"></p><div class="settings-sections" role="group" aria-label="设置分类"><button class="quiet" data-settings-section="preferences" aria-pressed="true">我的偏好</button><button class="quiet" data-settings-section="baidu" data-platform-setting hidden>网盘连接</button><button class="quiet" data-settings-section="brand" data-platform-setting hidden>品牌与外观</button><button class="quiet" data-settings-section="backup" data-platform-setting hidden>数据库备份</button></div>
|
||||||
<form id="preferences-form" class="panel"><h3>我的偏好</h3><p class="description">只影响当前账号,保存后换电脑或手机登录也会沿用。切换选项可以预览,离开未保存的预览会撤销。</p><div id="personal-choices" class="form-grid"></div><p id="preferences-status" role="status"></p><div class="device-actions"><button type="submit">保存我的偏好</button><button type="button" id="personal-follow" class="quiet">全部跟随平台</button><button type="button" id="personal-cancel" class="quiet">撤销预览</button></div></form>
|
<form id="preferences-form" class="panel"><h3>我的偏好</h3><p class="description">只影响当前账号,保存后换电脑或手机登录也会沿用。切换选项可以预览,离开未保存的预览会撤销。</p><div id="personal-choices" class="form-grid"></div><p id="preferences-status" role="status"></p><div class="device-actions"><button type="submit">保存我的偏好</button><button type="button" id="personal-follow" class="quiet">全部跟随平台</button><button type="button" id="personal-cancel" class="quiet">撤销预览</button></div></form>
|
||||||
<section id="baidu-admin" class="panel" hidden><div class="section-head"><h3>百度网盘 · 统一存储</h3><span class="badge">仅超级管理员配置</span></div><p class="description">连接一个网盘账号,保存各用户上传的照片、视频和语音。系统按用户、家庭和记事可见范围授权;网盘账号持有人本身可以管理网盘中的文件。服务器仅转发分片,不保存媒体原文件。</p><form id="baidu-config"><div class="form-grid"><label>AppKey<input name="appKey" autocomplete="off" maxlength="150" required></label><label>SecretKey<input name="secretKey" type="password" autocomplete="new-password" maxlength="150"></label><label>百度后台应用名称<input name="appFolder" value="智家" required maxlength="40"></label><label>OAuth 回调地址<input name="redirectUri" type="url" placeholder="https://你的域名/api/baidu/callback" required maxlength="400"></label></div><p class="description">回调地址须与百度后台完全一致。SecretKey 留空保持已保存值,SignKey 不需要填写。当前开发者应用仅限个人场景,适用范围以百度后台为准。</p><p id="baidu-config-status" role="status"></p><button type="submit">保存网盘应用配置</button></form><hr><p id="baidu-status" role="status"></p><div class="device-actions"><button id="baidu-connect">连接/重新授权百度网盘</button><button id="baidu-disconnect" class="quiet">解除本机连接</button></div><p class="description">解除连接不会删除云端文件。临时媒体地址由后台按需获取,令牌不发送给浏览器。网盘授权凭据单独保存在服务器受限目录,不包含在数据库备份或公开源码中。</p></section>
|
<section id="baidu-admin" class="panel" hidden><div class="section-head"><h3>百度网盘 · 统一存储</h3><span class="badge">仅超级管理员配置</span></div><p class="description">连接一个网盘账号,统一保存封面、Logo、图标及各用户上传的照片、视频和语音。系统按用户、家庭和记事可见范围授权;网盘账号持有人本身可以管理网盘中的文件。服务器仅转发分片,不保存媒体原文件。</p><details id="baidu-storage-guide"><summary>文件存在哪里?</summary><ul><li>家庭与场所:按家庭或场所保存封面。</li><li>平台外观:保存 Logo 和图标。</li><li>个人记事:按家庭、用户分组,再分照片、视频、语音。</li><li>数据库备份:单独存放;新备份上传并校验成功后,清理超过一个自然月的旧云备份。</li></ul><p class="description">文字、权限和文件引用保存在本机数据库;个人媒体原文件保存在百度网盘。封面和品牌图片仅做有限的内存缓存。更换封面、移除附件或删除记事不会删除网盘原文件。</p></details><form id="baidu-config"><div class="form-grid"><label>AppKey<input name="appKey" autocomplete="off" maxlength="150" required></label><label>SecretKey<input name="secretKey" type="password" autocomplete="new-password" maxlength="150"></label><label>百度后台应用名称<input name="appFolder" value="智家" required maxlength="40"></label><label>OAuth 回调地址<input name="redirectUri" type="url" placeholder="https://你的域名/api/baidu/callback" required maxlength="400"></label></div><p class="description">回调地址须与百度后台完全一致。SecretKey 留空保持已保存值,SignKey 不需要填写。当前开发者应用仅限个人场景,适用范围以百度后台为准。</p><p id="baidu-config-status" role="status"></p><button type="submit">保存网盘应用配置</button></form><hr><p id="baidu-status" role="status"></p><div class="device-actions"><button id="baidu-connect">连接/重新授权百度网盘</button><button id="baidu-disconnect" class="quiet">解除本机连接</button></div><p class="description">解除连接不会删除云端文件。临时媒体地址由后台按需获取,令牌不发送给浏览器。网盘授权凭据单独保存在服务器受限目录,不包含在数据库备份或公开源码中。</p></section>
|
||||||
<div id="settings-private" hidden>
|
<div id="settings-private" hidden>
|
||||||
<form id="brand-form" class="panel"><h3>发布与品牌配置</h3><p class="description">仅超级管理员可修改。名称、Logo、浏览器图标及登录文案会公开显示;适用于整套系统。已有个人偏好优先于平台默认。</p>
|
<form id="brand-form" class="panel"><h3>发布与品牌配置</h3><p class="description">仅超级管理员可修改。名称、Logo、浏览器图标及登录文案会公开显示;适用于整套系统。已有个人偏好优先于平台默认。</p>
|
||||||
<div class="form-grid"><label>系统名称<input name="name" required maxlength="40"></label><label>系统副标题<input name="subtitle" required maxlength="60"></label><label class="full">页首题词<input name="motto" maxlength="100"></label><label>登录标题<input name="loginTitle" required maxlength="40"></label><label>登录说明<input name="loginNote" maxlength="160"></label><label class="full">页脚说明<input name="footer" maxlength="120"></label></div>
|
<div class="form-grid"><label>系统名称<input name="name" required maxlength="40"></label><label>系统副标题<input name="subtitle" required maxlength="60"></label><label class="full">页首题词<input name="motto" maxlength="100"></label><label>登录标题<input name="loginTitle" required maxlength="40"></label><label>登录说明<input name="loginNote" maxlength="160"></label><label class="full">页脚说明<input name="footer" maxlength="120"></label></div>
|
||||||
<div class="brand-image-grid"><div><img id="brand-logo-preview" alt="Logo 预览" src="/zhao-icon.png"><label>更换系统 Logo<input id="brand-logo-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-logo-reset" class="quiet">使用默认 Logo</button></div><div><img id="brand-icon-preview" alt="浏览器图标预览" src="/zhao-icon.png"><label>更换浏览器图标<input id="brand-icon-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-icon-reset" class="quiet">使用默认图标</button></div></div><p class="description">支持 PNG、JPEG、WebP,原图最多 5 MB;上传时自动缩小,图片随系统数据库保存。</p>
|
<div class="brand-image-grid"><div><img id="brand-logo-preview" alt="Logo 预览" src="/zhao-icon.png"><label>更换系统 Logo<input id="brand-logo-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-logo-reset" class="quiet">使用默认 Logo</button></div><div><img id="brand-icon-preview" alt="浏览器图标预览" src="/zhao-icon.png"><label>更换浏览器图标<input id="brand-icon-file" type="file" accept="image/png,image/jpeg,image/webp"></label><button type="button" id="brand-icon-reset" class="quiet">使用默认图标</button></div></div><p class="description">支持 PNG、JPEG、WebP,原图最多 5 MB;上传时自动缩小,图片随系统数据库保存。</p>
|
||||||
<h3>新账号与跟随平台账号的默认外观</h3><div id="brand-defaults" class="form-grid"></div><p id="brand-status" role="status"></p><div class="device-actions"><button type="submit">保存发布配置</button><button type="button" id="brand-export" class="quiet">导出发布配置</button><label class="config-import">导入配置到表单<input id="brand-import" type="file" accept="application/json,.json"></label></div><p class="description">导入后先核对再保存。导出的配置只含品牌与默认外观,不含账号、设备凭据、家庭资料和网络备份目录。</p>
|
<h3>新账号与跟随平台账号的默认外观</h3><div id="brand-defaults" class="form-grid"></div><p id="brand-status" role="status"></p><div class="device-actions"><button type="submit">保存发布配置</button><button type="button" id="brand-export" class="quiet">导出发布配置</button><label class="config-import">导入配置到表单<input id="brand-import" type="file" accept="application/json,.json"></label></div><p class="description">导入后先核对再保存。导出的配置只含品牌与默认外观,不含账号、设备凭据、家庭资料和网络备份目录。</p>
|
||||||
</form>
|
</form>
|
||||||
<section id="backup-panel" class="panel"><h3>数据库备份</h3><p class="description">整台服务的数据库包含所有家庭、账号、设备连接凭据和图片,因此仅超级管理员可操作。录像文件不包含在数据库副本中;恢复后所有账号需要重新登录。</p><form id="backup-form"><div class="form-grid"><label class="check full"><input name="enabled" type="checkbox">启用每日定时备份</label><label class="check full"><input name="baidu" type="checkbox">同时上传到已连接的百度网盘</label><label>每日时间(北京时间)<input name="time" type="time" required value="03:00"></label><label>本机/挂载目录保留份数<input name="keep" type="number" min="1" max="90" required value="14"></label><label class="full">网络备份目录(可留空)<input name="networkPath" maxlength="500" placeholder="/var/lib/zhaovision/network-backup"></label></div><p class="description">填写本服务器已挂载的网络盘目录。使用百度接口备份只需勾选上方选项;此路径用于其他已挂载的网络存储,不能填网页地址或分享链接。写入网络目录成功不等于已确认百度云端同步完成。</p><p class="description">默认部署允许写入 /var/lib/zhaovision 下的目录。挂载和目录权限须由主机管理员配置;本页不会保存网盘登录密码。保留份数只清理本机和挂载目录中本系统生成的旧备份,百度网盘保留最近一个自然月;新备份上传并校验成功后才清理本应用的过期副本。</p><p id="backup-local-path" class="description"></p><p id="backup-save-status" role="status"></p><button type="submit">保存备份设置</button></form><div class="section-head"><h3>备份记录</h3><button id="backup-run">立即备份数据库</button></div><p id="backup-result" role="status"></p><div id="backup-history"></div></section>
|
<section id="backup-panel" class="panel"><h3>数据库备份</h3><p class="description">整台服务的数据库包含所有家庭、账号、设备连接配置和网盘文件引用,因此仅超级管理员可操作。网盘中的照片、视频、语音及录像文件不包含在新数据库副本中;旧版备份可能仍含历史内嵌图片。恢复后所有账号需要重新登录。</p><form id="backup-form"><div class="form-grid"><label class="check full"><input name="enabled" type="checkbox">启用每日定时备份</label><label class="check full"><input name="baidu" type="checkbox">同时上传到已连接的百度网盘</label><label>每日时间(北京时间)<input name="time" type="time" required value="03:00"></label><label>本机/挂载目录保留份数<input name="keep" type="number" min="1" max="90" required value="14"></label><label class="full">网络备份目录(可留空)<input name="networkPath" maxlength="500" placeholder="/var/lib/zhaovision/network-backup"></label></div><p class="description">填写本服务器已挂载的网络盘目录。使用百度接口备份只需勾选上方选项;此路径用于其他已挂载的网络存储,不能填网页地址或分享链接。写入网络目录成功不等于已确认百度云端同步完成。</p><p class="description">默认部署允许写入 /var/lib/zhaovision 下的目录。挂载和目录权限须由主机管理员配置;本页不会保存网盘登录密码。保留份数只清理本机和挂载目录中本系统生成的旧备份,百度网盘保留最近一个自然月;新备份上传并校验成功后才清理本应用的过期副本。</p><p id="backup-local-path" class="description"></p><p id="backup-save-status" role="status"></p><button type="submit">保存备份设置</button></form><div class="section-head"><h3>备份记录</h3><button id="backup-run">立即备份数据库</button></div><p id="backup-result" role="status"></p><div id="backup-history"></div></section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="page-journal" class="page" hidden>
|
<section id="page-journal" class="page" hidden>
|
||||||
|
|||||||
Reference in New Issue
Block a user