会员服务器:上传肥牛应用源码 v1.5.1(fnnas.huiyuan 完整 fpk 工程结构)

含 app/server/server.cjs 服务端、app/www 网页(admin/auth/项目云/库备份/settings)、
app/ui 图标、cmd 安装脚本、config、wizard、manifest、图标与对接文档。
按仓库规则排除构建产物 *.fpk 与 .mimosa 缓存。
This commit is contained in:
dqb-dev
2026-10-02 00:03:20 +08:00
parent d9be73be89
commit 65755bf520
30 changed files with 2718 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>会员管理</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="wrap">
<header>
<h2>会员管理</h2>
<span class="badge" id="h-ver"></span>
<nav class="nav">
<a class="cur" href="/admin.html">会员管理</a>
<a href="/xiangmu-cloud.html">项目云管理</a>
<a href="/lib-cloud.html">库备份管理</a>
<a href="/settings.html">设置</a>
</nav>
</header>
<div class="card card--table">
<div class="bar">
手机号: <input id="q手机号" style="width:130px">
<button onclick="查询()">查询</button>
<button class="primary" onclick="新增()">新增会员</button>
<button onclick="加载()">刷新列表</button>
<button onclick="保存()">保存修改</button>
<span id="tip"></span>
</div>
<table id="tbl">
<thead>
<tr><th style="width:40px"></th><th>手机号</th><th title="密码加密存储不可查看;输入新值即重置该会员密码,留空保持不变">密码</th><th>会员类型</th><th>有效期</th><th>注册时间</th><th>备注</th><th>操作</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<dialog id="dlg新增">
<h3>新增会员</h3>
<div><label>手机号:</label><input id="n手机号"></div>
<div><label>密码:</label><input id="n密码"></div>
<div><label>会员类型:</label>
<select id="n类型" onchange="n类型改()">
<option>试用会员</option><option>月会员</option><option>季会员</option><option>年会员</option><option>永久会员</option>
</select>
</div>
<div><label>有效期:</label><input id="n有效期" placeholder="2027-05-13"></div>
<div class="dlg-actions">
<button onclick="dlg新增.close()">取消</button>
<button class="primary" onclick="确定新增()">确定</button>
</div>
</dialog>
<script src="/auth.js"></script>
<script>
var 数据 = [];
function 提示(s, ok) {
var t = document.getElementById('tip');
t.textContent = s; t.style.color = ok ? '#2a7' : '#c33';
setTimeout(function(){ t.textContent=''; }, 3000);
}
// 会员类型 → 有效期天数(永久会员=永久);改类型时自动算有效期
var 类型天数 = { '试用会员': 30, '月会员': 1, '季会员': 3, '年会员': 12, '永久会员': 0 };
function 今天加月(n) {
var d = new Date(); d.setMonth(d.getMonth() + n);
return d.getFullYear() + '-' + ('0'+(d.getMonth()+1)).slice(-2) + '-' + ('0'+d.getDate()).slice(-2);
}
function 改类型(i, v) {
if (!数据[i]) return;
数据[i]['会员类型'] = v;
var n = 类型天数[v];
if (n === undefined) return; // 自定义类型不动有效期
数据[i]['有效期'] = n === 0 ? '永久' : 今天加月(n);
渲染(); // 重渲染让有效期输入框同步显示新值
}
function 加载() {
fetch('/api/list').then(r => r.json()).then(j => {
数据 = j['会员'] || [];
渲染();
});
}
function 渲染() {
var q = document.getElementById('q手机号').value.trim();
var tb = document.getElementById('tbl').getElementsByTagName('tbody')[0];
tb.innerHTML = '';
数据.forEach(function (m, i) {
if (q && (m['手机号']||'').indexOf(q) < 0) return;
var tr = tb.insertRow();
tr.insertCell().innerHTML = '<input type="checkbox" data-i="'+i+'" class="sel">';
tr.insertCell().innerHTML = '<input value="'+(m['手机号']||'')+'" onchange="改('+i+',\'手机号\',this.value)" style="width:120px">';
tr.insertCell().innerHTML = '<input type="password" value="" placeholder="留空不变" title="密码加密存储不可查看;输入新值即重置,留空保持不变" onchange="改('+i+',\'密码\',this.value)" style="width:100px">';
tr.insertCell().innerHTML = '<select onchange="改类型('+i+',this.value)" style="width:90px">'
+ ['试用会员','月会员','季会员','年会员','永久会员'].map(function(t){ return '<option'+((m['会员类型']||'')===t?' selected':'')+'>'+t+'</option>'; }).join('')
+ '</select>';
tr.insertCell().innerHTML = '<input value="'+(m['有效期']||'')+'" onchange="改('+i+',\'有效期\',this.value)" style="width:100px">';
tr.insertCell().textContent = m['注册时间'] || '';
tr.insertCell().innerHTML = '<input value="'+(m['备注']||'')+'" onchange="改('+i+',\'备注\',this.value)" style="width:120px">';
var op = tr.insertCell(); op.className = 'op';
var del = document.createElement('button');
del.textContent = '删除';
del.className = 'del';
del.onclick = function () {
if (!confirm('确定删除会员 ' + m['手机号'] + ' ?')) return;
数据.splice(i, 1); 渲染(); 保存();
};
op.appendChild(del);
});
}
function 改(i, k, v) { if (数据[i]) { 数据[i][k] = v; } }
// 新增弹窗:选类型 → 自动填有效期(手改过则以手改为准,切类型才覆盖)
function n类型改() {
var v = document.getElementById('n类型').value;
var n = 类型天数[v];
if (n === undefined) return;
document.getElementById('n有效期').value = n === 0 ? '永久' : 今天加月(n);
}
function 保存() {
fetch('/api/save', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ '会员': 数据 }) })
.then(r => r.json()).then(j => 提示(j.ok ? '已保存' : '保存失败:'+(j.msg||''), j.ok))
.catch(function (e) { 提示('请求失败: ' + e, false); });
}
function 查询() { 渲染(); }
function 新增() { document.getElementById('dlg新增').showModal(); }
function 确定新增() {
var 手机号 = document.getElementById('n手机号').value.trim();
var 密码 = document.getElementById('n密码').value.trim();
if (!手机号 || !密码) { alert('手机号和密码不能为空'); return; }
var 类型 = document.getElementById('n类型').value;
var 有效期 = document.getElementById('n有效期').value.trim();
fetch('/api/admin/addmember', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ 手机号: 手机号, 密码: 密码, 会员类型: 类型, 有效期: 有效期 || n类型初值(类型) }) })
.then(r => r.json())
.then(function (j) {
if (!j.ok) { alert(j.msg || '创建失败'); return; }
document.getElementById('dlg新增').close();
提示('已创建会员 ' + 手机号, true);
加载();
})
.catch(function (e) { alert('请求失败: ' + e); });
}
function n类型初值(类型) {
var n = 类型天数[类型];
return n === undefined || n === 0 ? '永久' : 今天加月(n);
}
fetch('/api/version').then(r => r.json()).then(function (j) {
var b = document.getElementById('h-ver');
if (b && j.version) b.textContent = 'v' + j.version;
}).catch(function () {});
加载();
</script>
</body>
</html>