@wendongfly/myhi 1.0.93 → 1.0.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin.html +262 -0
- package/dist/index.js +116 -112
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/admin.html
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>myhi 管理</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: -apple-system, system-ui, sans-serif; background: #0d1117; color: #e6edf3; min-height: 100vh; }
|
|
10
|
+
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 1rem; }
|
|
11
|
+
.login-box { background: #161b22; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 2rem; width: 100%; max-width: 360px; }
|
|
12
|
+
.login-box h2 { text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem; }
|
|
13
|
+
.login-box input { width: 100%; padding: 0.8rem; background: #0d1117; border: 1px solid rgba(255,255,255,0.15); border-radius: 10px; color: #e6edf3; font-size: 1rem; margin-bottom: 1rem; }
|
|
14
|
+
.login-box button { width: 100%; padding: 0.8rem; background: #7c3aed; color: #fff; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
|
15
|
+
.login-box .err { color: #f85149; font-size: 0.85rem; text-align: center; margin-bottom: 0.5rem; min-height: 1.2em; }
|
|
16
|
+
.wrap { max-width: 640px; margin: 0 auto; padding: 1rem; }
|
|
17
|
+
.header { display: flex; align-items: center; justify-content: space-between; padding: 1rem 0; border-bottom: 1px solid rgba(255,255,255,0.08); margin-bottom: 1.5rem; }
|
|
18
|
+
.header h1 { font-size: 1.1rem; }
|
|
19
|
+
.header .logout { background: none; border: 1px solid rgba(255,255,255,0.15); color: #8b949e; padding: 0.3rem 0.8rem; border-radius: 8px; cursor: pointer; font-size: 0.8rem; }
|
|
20
|
+
.ver-card { background: #161b22; border: 1px solid rgba(255,255,255,0.1); border-radius: 14px; padding: 1.2rem 1.5rem; margin-bottom: 1.2rem; }
|
|
21
|
+
.ver-big { font-size: 2rem; font-weight: 800; color: #7c3aed; }
|
|
22
|
+
.ver-sub { font-size: 0.85rem; color: #8b949e; margin-top: 0.5rem; }
|
|
23
|
+
.ver-sub b { color: #3fb950; }
|
|
24
|
+
.actions { display: flex; gap: 0.8rem; margin-bottom: 1.5rem; }
|
|
25
|
+
.actions button { flex: 1; padding: 0.8rem; border: none; border-radius: 10px; font-size: 0.9rem; font-weight: 600; cursor: pointer; }
|
|
26
|
+
.btn-up { background: #7c3aed; color: #fff; }
|
|
27
|
+
.btn-up:disabled { background: #333; color: #666; cursor: not-allowed; }
|
|
28
|
+
.btn-up.glow { background: #3fb950; animation: pulse 2s infinite; }
|
|
29
|
+
.btn-re { background: rgba(255,255,255,0.08); color: #e6edf3; border: 1px solid rgba(255,255,255,0.15) !important; }
|
|
30
|
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.7} }
|
|
31
|
+
.card { background: #161b22; border: 1px solid rgba(255,255,255,0.1); border-radius: 14px; padding: 1rem 1.2rem; margin-bottom: 1rem; }
|
|
32
|
+
.card h3 { font-size: 0.9rem; font-weight: 700; margin-bottom: 0.8rem; }
|
|
33
|
+
.row { font-size: 0.82rem; color: #b1bac4; padding: 0.5rem 0; line-height: 1.5; display: flex; justify-content: space-between; align-items: center; }
|
|
34
|
+
.row + .row { border-top: 1px solid rgba(255,255,255,0.04); }
|
|
35
|
+
.row-actions { display: flex; gap: 0.4rem; }
|
|
36
|
+
.row-actions button { background: none; border: 1px solid rgba(255,255,255,0.12); color: #8b949e; padding: 0.2rem 0.5rem; border-radius: 6px; cursor: pointer; font-size: 0.75rem; }
|
|
37
|
+
.row-actions button:hover { color: #e6edf3; border-color: rgba(255,255,255,0.3); }
|
|
38
|
+
.row-actions .del { color: #f85149; border-color: rgba(248,81,73,0.3); }
|
|
39
|
+
.inp { padding: 0.5rem 0.7rem; background: #0d1117; border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; color: #e6edf3; font-size: 0.85rem; width: 100%; }
|
|
40
|
+
.inp-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }
|
|
41
|
+
.inp-row .inp { flex: 1; }
|
|
42
|
+
.btn-sm { padding: 0.5rem 1rem; background: #7c3aed; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 0.85rem; white-space: nowrap; }
|
|
43
|
+
.msg { font-size: 0.8rem; margin-top: 0.3rem; min-height: 1.2em; }
|
|
44
|
+
.msg-ok { color: #3fb950; }
|
|
45
|
+
.msg-err { color: #f85149; }
|
|
46
|
+
.status-msg { text-align: center; padding: 1rem; color: #8b949e; font-size: 0.85rem; }
|
|
47
|
+
.uptime { font-size: 0.78rem; color: #6e7681; text-align: center; margin-top: 1rem; }
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
|
|
52
|
+
<div class="login-wrap" id="pg-login">
|
|
53
|
+
<div class="login-box">
|
|
54
|
+
<h2>myhi 管理</h2>
|
|
55
|
+
<div class="err" id="login-err"></div>
|
|
56
|
+
<input type="password" id="admin-pwd" placeholder="输入管理密码" autofocus>
|
|
57
|
+
<button onclick="doLogin()">登录</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div class="wrap" id="pg-admin" style="display:none">
|
|
62
|
+
<div class="header">
|
|
63
|
+
<h1>myhi 管理</h1>
|
|
64
|
+
<button class="logout" onclick="doLogout()">退出</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- 版本 + 升级 -->
|
|
68
|
+
<div class="ver-card">
|
|
69
|
+
<div style="font-size:0.8rem;color:#8b949e">当前版本</div>
|
|
70
|
+
<div class="ver-big" id="v-cur">--</div>
|
|
71
|
+
<div class="ver-sub" id="v-latest"></div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="actions">
|
|
74
|
+
<button class="btn-up" id="btn-up" onclick="doUpgrade()" disabled>已是最新</button>
|
|
75
|
+
<button class="btn-re" onclick="doRestart()">重启</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div id="op-msg" class="status-msg" style="display:none"></div>
|
|
78
|
+
|
|
79
|
+
<!-- 用户管理 -->
|
|
80
|
+
<div class="card">
|
|
81
|
+
<h3>用户管理</h3>
|
|
82
|
+
<div id="user-list"></div>
|
|
83
|
+
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:0.5rem;padding-top:0.8rem">
|
|
84
|
+
<div style="font-size:0.82rem;color:#8b949e;margin-bottom:0.5rem">添加用户</div>
|
|
85
|
+
<div class="inp-row">
|
|
86
|
+
<input class="inp" id="u-pwd" placeholder="密码">
|
|
87
|
+
<input class="inp" id="u-name" placeholder="名称">
|
|
88
|
+
</div>
|
|
89
|
+
<div class="inp-row">
|
|
90
|
+
<input class="inp" id="u-dir" placeholder="工作目录">
|
|
91
|
+
<button class="btn-sm" onclick="addUser()">添加</button>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="msg" id="u-msg"></div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- 修改管理密码 -->
|
|
98
|
+
<div class="card">
|
|
99
|
+
<h3>修改管理密码</h3>
|
|
100
|
+
<div class="inp-row">
|
|
101
|
+
<input type="password" class="inp" id="new-pwd" placeholder="新密码">
|
|
102
|
+
<button class="btn-sm" onclick="changePwd()">保存</button>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="msg" id="pwd-msg"></div>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="uptime" id="uptime"></div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<script>
|
|
111
|
+
let T = sessionStorage.getItem('myhi_admin');
|
|
112
|
+
const H = () => ({ 'Authorization': 'Bearer ' + T, 'Content-Type': 'application/json' });
|
|
113
|
+
|
|
114
|
+
document.getElementById('admin-pwd').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
|
115
|
+
|
|
116
|
+
async function doLogin() {
|
|
117
|
+
const pwd = document.getElementById('admin-pwd').value;
|
|
118
|
+
if (!pwd) return;
|
|
119
|
+
try {
|
|
120
|
+
const r = await fetch('/api/admin/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: pwd }) });
|
|
121
|
+
const d = await r.json();
|
|
122
|
+
if (d.ok) { T = d.token; sessionStorage.setItem('myhi_admin', T); showAdmin(); }
|
|
123
|
+
else document.getElementById('login-err').textContent = d.error || '密码错误';
|
|
124
|
+
} catch { document.getElementById('login-err').textContent = '连接失败'; }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function doLogout() {
|
|
128
|
+
sessionStorage.removeItem('myhi_admin'); T = null;
|
|
129
|
+
document.getElementById('pg-admin').style.display = 'none';
|
|
130
|
+
document.getElementById('pg-login').style.display = '';
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function showAdmin() {
|
|
134
|
+
document.getElementById('pg-login').style.display = 'none';
|
|
135
|
+
document.getElementById('pg-admin').style.display = '';
|
|
136
|
+
await refresh();
|
|
137
|
+
setInterval(refresh, 10000);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function refresh() {
|
|
141
|
+
try {
|
|
142
|
+
const r = await fetch('/api/admin/status', { headers: H() });
|
|
143
|
+
if (r.status === 401) { doLogout(); return; }
|
|
144
|
+
const d = await r.json();
|
|
145
|
+
|
|
146
|
+
// 版本
|
|
147
|
+
document.getElementById('v-cur').textContent = 'v' + d.version.current;
|
|
148
|
+
const btn = document.getElementById('btn-up');
|
|
149
|
+
if (d.version.updateAvailable) {
|
|
150
|
+
document.getElementById('v-latest').innerHTML = '最新版本: <b>v' + d.version.latest + '</b>';
|
|
151
|
+
btn.disabled = false; btn.classList.add('glow'); btn.textContent = '升级到 v' + d.version.latest;
|
|
152
|
+
} else {
|
|
153
|
+
document.getElementById('v-latest').innerHTML = '<span style="color:#3fb950">已是最新版本</span>';
|
|
154
|
+
btn.disabled = true; btn.classList.remove('glow'); btn.textContent = '已是最新';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 用户列表
|
|
158
|
+
renderUsers(d.users || []);
|
|
159
|
+
|
|
160
|
+
// 运行时长
|
|
161
|
+
if (d.uptime) {
|
|
162
|
+
const h = Math.floor(d.uptime / 3600), m = Math.floor((d.uptime % 3600) / 60);
|
|
163
|
+
document.getElementById('uptime').textContent = `服务运行 ${h}小时${m}分钟`;
|
|
164
|
+
}
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function renderUsers(users) {
|
|
169
|
+
const el = document.getElementById('user-list');
|
|
170
|
+
if (!users.length) { el.innerHTML = '<div class="row" style="color:#6e7681">暂无用户</div>'; return; }
|
|
171
|
+
el.innerHTML = users.map(u => `
|
|
172
|
+
<div class="row">
|
|
173
|
+
<span><b style="color:#e6edf3">${esc(u.name)}</b> <span style="color:#6e7681;font-size:0.75rem">${esc(u.password)}</span> <span style="color:#6e7681;font-size:0.72rem">${esc(u.dir)}</span></span>
|
|
174
|
+
<div class="row-actions">
|
|
175
|
+
<button onclick="editUserPwd('${esc(u.name)}')">改密</button>
|
|
176
|
+
<button class="del" onclick="delUser('${esc(u.name)}')">删除</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
`).join('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
183
|
+
|
|
184
|
+
async function addUser() {
|
|
185
|
+
const pwd = document.getElementById('u-pwd').value.trim();
|
|
186
|
+
const name = document.getElementById('u-name').value.trim();
|
|
187
|
+
const dir = document.getElementById('u-dir').value.trim();
|
|
188
|
+
const msg = document.getElementById('u-msg');
|
|
189
|
+
if (!pwd || !name || !dir) { msg.className = 'msg msg-err'; msg.textContent = '密码、名称、目录都必填'; return; }
|
|
190
|
+
try {
|
|
191
|
+
const r = await fetch('/api/admin/user', { method: 'POST', headers: H(), body: JSON.stringify({ password: pwd, name, dir }) });
|
|
192
|
+
const d = await r.json();
|
|
193
|
+
if (d.ok) { msg.className = 'msg msg-ok'; msg.textContent = '已添加'; document.getElementById('u-pwd').value = ''; document.getElementById('u-name').value = ''; document.getElementById('u-dir').value = ''; refresh(); }
|
|
194
|
+
else { msg.className = 'msg msg-err'; msg.textContent = d.error || '失败'; }
|
|
195
|
+
} catch { msg.className = 'msg msg-err'; msg.textContent = '连接失败'; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function editUserPwd(name) {
|
|
199
|
+
const newPwd = prompt(`修改 ${name} 的密码:`);
|
|
200
|
+
if (!newPwd) return;
|
|
201
|
+
try {
|
|
202
|
+
const r = await fetch('/api/admin/user/password', { method: 'POST', headers: H(), body: JSON.stringify({ name, password: newPwd }) });
|
|
203
|
+
const d = await r.json();
|
|
204
|
+
if (d.ok) { alert('密码已修改'); refresh(); }
|
|
205
|
+
else alert(d.error || '失败');
|
|
206
|
+
} catch { alert('连接失败'); }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function delUser(name) {
|
|
210
|
+
if (!confirm(`确定删除用户 ${name}?`)) return;
|
|
211
|
+
try {
|
|
212
|
+
const r = await fetch('/api/admin/user', { method: 'DELETE', headers: H(), body: JSON.stringify({ name }) });
|
|
213
|
+
const d = await r.json();
|
|
214
|
+
if (d.ok) refresh();
|
|
215
|
+
else alert(d.error || '失败');
|
|
216
|
+
} catch { alert('连接失败'); }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function changePwd() {
|
|
220
|
+
const pwd = document.getElementById('new-pwd').value.trim();
|
|
221
|
+
const msg = document.getElementById('pwd-msg');
|
|
222
|
+
if (!pwd || pwd.length < 4) { msg.className = 'msg msg-err'; msg.textContent = '密码至少4位'; return; }
|
|
223
|
+
try {
|
|
224
|
+
const r = await fetch('/api/admin/password', { method: 'POST', headers: H(), body: JSON.stringify({ password: pwd }) });
|
|
225
|
+
const d = await r.json();
|
|
226
|
+
if (d.ok) { msg.className = 'msg msg-ok'; msg.textContent = '已修改'; document.getElementById('new-pwd').value = ''; }
|
|
227
|
+
else { msg.className = 'msg msg-err'; msg.textContent = d.error || '失败'; }
|
|
228
|
+
} catch { msg.className = 'msg msg-err'; msg.textContent = '连接失败'; }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function doUpgrade() {
|
|
232
|
+
const btn = document.getElementById('btn-up'), msg = document.getElementById('op-msg');
|
|
233
|
+
btn.disabled = true; btn.textContent = '升级中...'; msg.textContent = '正在下载安装...'; msg.style.display = '';
|
|
234
|
+
try {
|
|
235
|
+
const r = await fetch('/api/admin/upgrade', { method: 'POST', headers: H() });
|
|
236
|
+
const d = await r.json();
|
|
237
|
+
if (d.ok) { msg.textContent = '升级完成,重启中...'; waitReload(); }
|
|
238
|
+
else { msg.textContent = '失败: ' + (d.error||''); btn.disabled = false; btn.textContent = '重试'; }
|
|
239
|
+
} catch { msg.textContent = '连接失败'; btn.disabled = false; btn.textContent = '重试'; }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function doRestart() {
|
|
243
|
+
if (!confirm('确定重启?')) return;
|
|
244
|
+
const msg = document.getElementById('op-msg');
|
|
245
|
+
msg.textContent = '重启中...'; msg.style.display = '';
|
|
246
|
+
try { await fetch('/api/admin/restart', { method: 'POST', headers: H() }); } catch {}
|
|
247
|
+
waitReload();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function waitReload() {
|
|
251
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
252
|
+
for (let i = 0; i < 30; i++) {
|
|
253
|
+
try { const r = await fetch('/api/version'); if (r.ok) { location.reload(); return; } } catch {}
|
|
254
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
255
|
+
}
|
|
256
|
+
document.getElementById('op-msg').textContent = '重启超时,请手动刷新';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (T) showAdmin();
|
|
260
|
+
</script>
|
|
261
|
+
</body>
|
|
262
|
+
</html>
|