fix(ui): attach mobile nav to viewport bottom and localize api error messages

This commit is contained in:
OpenClaw Agent
2026-09-17 07:09:05 +08:00
parent d8cc416b20
commit a295f2d610
+47 -9
View File
@@ -743,19 +743,22 @@ select.input {
.mobile-nav { .mobile-nav {
position: fixed; position: fixed;
left: 12px; left: 0;
right: 12px; right: 0;
bottom: max(12px, env(safe-area-inset-bottom)); bottom: 0;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
gap: 4px; gap: 4px;
padding: 6px; padding: 6px 8px calc(6px + env(safe-area-inset-bottom, 0px));
background: rgba(15, 22, 36, 0.88); background: rgba(8, 12, 20, 0.95);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
border: 1px solid var(--border-subtle); border-top: 1px solid var(--border-subtle);
border-radius: var(--radius-lg); border-left: 0;
border-right: 0;
border-bottom: 0;
border-radius: 0;
z-index: 100; z-index: 100;
box-shadow: var(--shadow-md); box-shadow: 0 -4px 16px rgba(0, 0, 0, 0.3);
} }
.mobile-nav button { .mobile-nav button {
flex: 1; flex: 1;
@@ -1301,7 +1304,42 @@ select.input {
<script> <script>
const $=id=>document.getElementById(id);let searchPage=1,searchKeyword='',searchSource='kw',searchLoading=false,hasMore=false,taskFilter='all'; const $=id=>document.getElementById(id);let searchPage=1,searchKeyword='',searchSource='kw',searchLoading=false,hasMore=false,taskFilter='all';
async function api(u,o){const r=await fetch('/api'+u,o);if(r.status===401){showLogin();throw new Error('请先登录')}if(!r.ok)throw new Error(await r.text());return r.status===204?null:r.json()} function parseErrorMsg(msg) {
if (!msg) return '请求失败';
let str = String(msg).trim();
try {
const obj = JSON.parse(str);
if (obj && obj.error) str = String(obj.error);
else if (obj && obj.message) str = String(obj.message);
} catch(e) {}
const dict = {
'code already used or unavailable': '该兑换码已被使用或已失效',
'record not found': '兑换码不存在,请核对后重试',
'insufficient credits': '积分不足,请先兑换或充值积分',
'code expired': '该兑换码已过期',
'code required': '请输入兑换码',
'credits/count invalid': '积分或发行数量不合法',
'user not found': '用户不存在',
'user disabled': '账户已被禁用,无法执行此操作',
'amount must not be zero': '积分调整金额不能为 0',
'amount cannot be zero': '积分调整金额不能为 0',
'keyword required': '请输入搜索关键词',
'source/rid/title required': '歌曲信息不完整,无法加入下载队列',
'only failed tasks can be retried': '只有失败的任务才支持重试',
'invalid role': '无效的用户角色',
'invalid status': '无效的账户状态',
'invalid user id': '无效的用户 ID',
'invalid request body': '请求参数格式错误',
'username must be 4-10 English letters and password must be 8+ characters': '用户名必须为 4-10 位英文字母,密码至少 8 位',
'cannot remove the last active admin account': '系统必须保留至少一个激活的管理员账号',
'WebDAV URL、用户名或密码未配置': 'WebDAV URL、用户名或密码未配置'
};
for (const [k, v] of Object.entries(dict)) {
if (str.toLowerCase().includes(k.toLowerCase())) return v;
}
return str;
}
async function api(u,o){const r=await fetch('/api'+u,o);if(r.status===401){showLogin();throw new Error('请先登录')}if(!r.ok){const txt=await r.text();throw new Error(parseErrorMsg(txt))}return r.status===204?null:r.json()}
function showLogin(){document.getElementById('loginOverlay').style.display='flex'} function showLogin(){document.getElementById('loginOverlay').style.display='flex'}
async function loadAccount(){try{const c=await api('/credits');$('creditPanel').textContent='余额 '+c.balance+' · 预留 '+c.reserved+' · 可用 '+c.available;const u=window.currentUser||{};$('userDavCard').style.display=(u.role==='admin'||u.role==='privileged')?'':'none';if(u.role==='admin'||u.role==='privileged'){const d=await api('/webdav/config');$('userDavUrl').value=d.url||'';$('userDavUser').value=d.username||'';$('userDavRoot').value=d.rootPath||''}}catch(e){$('creditPanel').textContent=e.message}} async function loadAccount(){try{const c=await api('/credits');$('creditPanel').textContent='余额 '+c.balance+' · 预留 '+c.reserved+' · 可用 '+c.available;const u=window.currentUser||{};$('userDavCard').style.display=(u.role==='admin'||u.role==='privileged')?'':'none';if(u.role==='admin'||u.role==='privileged'){const d=await api('/webdav/config');$('userDavUrl').value=d.url||'';$('userDavUser').value=d.username||'';$('userDavRoot').value=d.rootPath||''}}catch(e){$('creditPanel').textContent=e.message}}
let adminUsersCache=[];let adminRoleFilter='all'; let adminUsersCache=[];let adminRoleFilter='all';