fix(account-service): 管理页接口走部署前缀并改中文错误提示

公网 /account/ 下绝对路径 /api/admin 会被 OpenIM 接走,无条件 res.json()
把非 JSON 响应打成英文异常。按页面路径拼接前缀,并先看状态再解析。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
编码工程师
2026-08-31 09:27:28 +08:00
co-authored by Cursor multica-agent
parent 2f8a76da3f
commit 8842a74d68
6 changed files with 550 additions and 21 deletions
+94 -15
View File
@@ -87,17 +87,96 @@ $('token').value = localStorage.getItem('adminToken') || '';
function saveToken() { localStorage.setItem('adminToken', $('token').value.trim()); show('口令已保存', true); loadList(); }
function show(text, ok) { const m = $('msg'); m.textContent = text; m.className = 'msg ' + (ok ? 'okm' : 'err'); }
async function api(path, body) {
const res = await fetch(path, {
method: body ? 'POST' : 'GET',
headers: { 'Content-Type': 'application/json', 'admin-token': $('token').value.trim() },
body: body ? JSON.stringify(body) : undefined,
});
const j = await res.json();
if (j.code !== 0) throw new Error(j.msg || '请求失败');
function adminApiUrl(apiPath, pagePath) {
const pathname = pagePath != null
? pagePath
: (typeof location !== 'undefined' ? location.pathname : '/');
let prefix = String(pathname || '/');
if (prefix.length > 1 && prefix.endsWith('/')) prefix = prefix.slice(0, -1);
if (prefix === '/admin' || prefix.endsWith('/admin')) {
prefix = prefix.slice(0, -6);
}
if (prefix === '/') prefix = '';
const route = apiPath.charAt(0) === '/' ? apiPath : '/' + apiPath;
return prefix + route;
}
function httpStatusHint(status) {
const n = Number(status);
if (n === 401) return '管理口令错误或未提供';
if (n === 403) return '没有权限执行此操作';
if (n === 404) return '接口地址不正确,请从管理页重新打开';
if (n === 502 || n === 503 || n === 504) return '服务暂时不可用,请稍后重试';
if (n >= 500) return '服务暂时出错,请稍后重试';
if (n >= 400) return '请求失败,请稍后重试';
return '';
}
function toUserMessage(err) {
const raw = err && typeof err === 'object' && err.message != null
? String(err.message)
: String(err || '');
const s = raw.trim();
if (!s) return '请求失败,请稍后重试';
if (/unexpected end of json|failed to execute ['"]json['"]|json parse|not valid json|unexpected token/i.test(s)) {
return '服务器返回了无法识别的内容';
}
if (/failed to fetch|networkerror|load failed|network request failed/i.test(s)) {
return '网络异常,请检查连接后重试';
}
return s;
}
async function readAdminJson(res) {
const status = res.status;
const ctype = String((res.headers && res.headers.get && res.headers.get('content-type')) || '').toLowerCase();
let text = '';
try {
text = await res.text();
} catch {
throw new Error(httpStatusHint(status) || '无法读取服务器响应');
}
const trimmed = (text || '').trim();
if (!trimmed) {
throw new Error(httpStatusHint(status) || '服务器没有返回内容');
}
const looksJson = ctype.indexOf('json') !== -1 || trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[';
if (!looksJson) {
throw new Error(httpStatusHint(status) || '服务器返回了无法识别的内容');
}
let j;
try {
j = JSON.parse(trimmed);
} catch {
throw new Error(httpStatusHint(status) || '服务器返回了无法识别的内容');
}
if (!j || typeof j !== 'object') {
throw new Error(httpStatusHint(status) || '服务器返回了无法识别的内容');
}
if (j.code !== 0) {
throw new Error(j.msg || httpStatusHint(status) || '请求失败');
}
return j.data;
}
async function api(path, body) {
let res;
try {
res = await fetch(adminApiUrl(path), {
method: body ? 'POST' : 'GET',
headers: { 'Content-Type': 'application/json', 'admin-token': $('token').value.trim() },
body: body ? JSON.stringify(body) : undefined,
});
} catch (e) {
throw new Error(toUserMessage(e));
}
try {
return await readAdminJson(res);
} catch (e) {
throw new Error(toUserMessage(e));
}
}
async function loadList() {
try {
const d = await api('/api/admin/employees');
@@ -115,7 +194,7 @@ async function loadList() {
<button class="danger" onclick="deleteEmp('${e.staffNo}')">删除</button>
</td>
</tr>`).join('') || '<tr><td colspan="5" style="color:#98a2b3">暂无员工,请先导入</td></tr>';
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
async function toggle(staffNo, enable) {
@@ -123,7 +202,7 @@ async function toggle(staffNo, enable) {
await api('/api/admin/' + (enable ? 'enable' : 'disable'), { staffNo });
show((enable ? '已启用 ' : '已停用 ') + staffNo, true);
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
async function resetPwd(staffNo) {
@@ -133,7 +212,7 @@ async function resetPwd(staffNo) {
await api('/api/admin/reset_password', { staffNo, password: pwd });
show('已重置 ' + staffNo + ' 的密码', true);
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
async function addEmp() {
@@ -142,7 +221,7 @@ async function addEmp() {
show('已添加 ' + $('addNo').value.trim(), true);
$('addNo').value = $('addName').value = $('addPwd').value = '';
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
async function doImport() {
@@ -151,7 +230,7 @@ async function doImport() {
const bad = d.results.filter(r => !r.ok);
show('导入完成:成功 ' + d.imported + ' / ' + d.total + (bad.length ? '\n失败明细:\n' + bad.map(b => b.line + ' → ' + b.msg).join('\n') : ''), bad.length === 0);
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
// 直接选择模板 CSV 文件导入,不用复制粘贴
@@ -166,7 +245,7 @@ async function importFile(input) {
const bad = d.results.filter(r => !r.ok);
show('文件「' + file.name + '」导入完成:成功 ' + d.imported + ' / ' + d.total + (bad.length ? '\n失败明细:\n' + bad.map(b => b.line + ' → ' + b.msg).join('\n') : ''), bad.length === 0);
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
async function deleteEmp(staffNo) {
@@ -175,7 +254,7 @@ async function deleteEmp(staffNo) {
await api('/api/admin/delete', { staffNo });
show('已删除 ' + staffNo, true);
loadList();
} catch (e) { show(e.message, false); }
} catch (e) { show(toUserMessage(e), false); }
}
// 下载 CSV 导入模板(加 BOM 让 Excel 正确识别中文)