chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">真人认证审核</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="searchForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<input class="layui-input" name="keyword" placeholder="真实姓名/昵称" autocomplete="off">
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<select name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="-1">未提交</option>
|
||||
<option value="0">待审核</option>
|
||||
<option value="1">已通过</option>
|
||||
<option value="2">已驳回</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="barTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="review">审核</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status == 1){ }}
|
||||
<span class="layui-badge layui-bg-green">已通过</span>
|
||||
{{# } else if(d.status == 2){ }}
|
||||
<span class="layui-badge layui-bg-red">已驳回</span>
|
||||
{{# } else if(d.status == 0){ }}
|
||||
<span class="layui-badge layui-bg-orange">待审核</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge">未提交</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="imgTpl">
|
||||
{{# if(d.front_img){ }}<img src="{{d.front_img}}" style="max-width:90px;max-height:90px;" onerror="this.style.display='none'">{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="reviewTpl">
|
||||
<div style="padding:20px;">
|
||||
<table class="layui-table">
|
||||
<tr><td>UID</td><td>{{d.uid}}</td></tr>
|
||||
<tr><td>昵称</td><td>{{d.nickname||'--'}}</td></tr>
|
||||
<tr><td>真实姓名</td><td>{{d.real_name}}</td></tr>
|
||||
<tr><td>证件号(脱敏)</td><td>{{d.id_card}}</td></tr>
|
||||
<tr><td>AI初筛</td><td>分数 {{d.ai_score}} / 结果 {{d.ai_result==1?'通过':'未通过'}}<br><small>{{d.ai_detail||''}}</small></td></tr>
|
||||
</table>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;">
|
||||
<div>证件正面<br><img src="{{d.front_img}}" style="width:110px;" onerror="this.src=''"></div>
|
||||
<div>证件反面<br><img src="{{d.back_img}}" style="width:110px;" onerror="this.src=''"></div>
|
||||
<div>手持证件<br><img src="{{d.hold_img}}" style="width:110px;" onerror="this.src=''"></div>
|
||||
<div>人脸照<br><img src="{{d.face_img}}" style="width:110px;" onerror="this.src=''"></div>
|
||||
</div>
|
||||
<form class="layui-form" id="reviewForm">
|
||||
<input type="hidden" name="id" value="{{d.id}}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">驳回原因</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="reason" placeholder="驳回时填写" class="layui-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-input-block">
|
||||
<button type="button" class="layui-btn layui-bg-green" id="passBtn">通过</button>
|
||||
<button type="button" class="layui-btn layui-btn-danger" id="rejectBtn">驳回</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
layui.use(['table','form','layer','laytpl','http'], function(){
|
||||
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, $ = layui.$, http = layui.http;
|
||||
var base = '/wxchat/backend/auth';
|
||||
|
||||
var tableIns = table.render({
|
||||
elem: '#dataTable',
|
||||
url: base + '/index',
|
||||
page: true,
|
||||
limit: 15,
|
||||
cols: [[
|
||||
{field:'id', title:'ID', width:70},
|
||||
{field:'uid', title:'UID', width:90},
|
||||
{field:'nickname', title:'昵称', width:120},
|
||||
{field:'real_name', title:'真实姓名', width:110},
|
||||
{field:'id_card', title:'证件号', width:160},
|
||||
{field:'ai_score', title:'AI分数', width:90},
|
||||
{field:'status', title:'状态', width:100, templet:'#statusTpl'},
|
||||
{field:'create_at', title:'提交时间', width:170, templet:function(d){return d.create_at?layui.util.toDateString(d.create_at*1000,'yyyy-MM-dd HH:mm'):'';}},
|
||||
{title:'操作', width:130, toolbar:'#barTpl'}
|
||||
]],
|
||||
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
|
||||
});
|
||||
|
||||
form.on('submit(searchBtn)', function(data){
|
||||
tableIns.reload({where: data.field, page:{curr:1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
function post(action, data){
|
||||
return http.post(base + '/'+action, data);
|
||||
}
|
||||
|
||||
table.on('tool(dataTable)', function(obj){
|
||||
var d = obj.data;
|
||||
if(obj.event==='del'){
|
||||
layer.confirm('确定删除该认证记录?', function(){
|
||||
post('del', {ids: d.id}).then(function(res){
|
||||
if(res.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(res.message||'删除失败',{icon:2});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(obj.event==='review'){
|
||||
post('detail', {id: d.id}).then(function(res){
|
||||
if(res.code!==0){ layer.msg(res.message||'获取失败',{icon:2}); return; }
|
||||
var tpl = laytpl($('#reviewTpl').html()).render(res.data);
|
||||
layer.open({
|
||||
type:1, title:'认证审核 #'+d.id, area:['640px','auto'], content: tpl,
|
||||
success:function(){
|
||||
$('#passBtn').on('click', function(){
|
||||
post('review', {id:d.id, pass:1}).done(function(r){
|
||||
if(r.code===0){ layer.msg('已通过',{icon:1}); layer.closeAll(); tableIns.reload(); }
|
||||
else layer.msg(r.message||'失败',{icon:2});
|
||||
});
|
||||
});
|
||||
$('#rejectBtn').on('click', function(){
|
||||
var reason = $('#reviewForm [name=reason]').val();
|
||||
post('review', {id:d.id, pass:0, reason:reason}).done(function(r){
|
||||
if(r.code===0){ layer.msg('已驳回',{icon:1}); layer.closeAll(); tableIns.reload(); }
|
||||
else layer.msg(r.message||'失败',{icon:2});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,171 @@
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">语音 / 视频房管理</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="searchForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<input class="layui-input" name="keyword" placeholder="房间标题" autocomplete="off">
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<select name="type">
|
||||
<option value="0">全部类型</option>
|
||||
<option value="1">语音</option>
|
||||
<option value="2">视频</option>
|
||||
<option value="3">游戏</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="barTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="members">成员</a>
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="close">关房</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status == 1){ }}
|
||||
<span class="layui-badge layui-bg-green">直播中</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge">已关闭</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="formTpl">
|
||||
<form class="layui-form" style="padding:20px;" id="roomForm" lay-filter="roomForm">
|
||||
<input type="hidden" name="id" value="{{d.id||''}}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">房间标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" class="layui-input" value="{{d.title||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">公告</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="notice" class="layui-input" value="{{d.notice||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">房间状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="radio" name="status" value="1" title="直播中" {{ d.status==undefined||d.status==1?'checked':'' }}>
|
||||
<input type="radio" name="status" value="0" title="关闭" {{ d.status==0?'checked':'' }}>
|
||||
</div>
|
||||
<label class="layui-form-label">弹幕</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="radio" name="danmaku_on" value="1" title="开" {{ d.danmaku_on==undefined||d.danmaku_on==1?'checked':'' }}>
|
||||
<input type="radio" name="danmaku_on" value="0" title="关" {{ d.danmaku_on==0?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="membersTpl">
|
||||
<div style="padding:15px;">
|
||||
<table class="layui-table">
|
||||
<thead><tr><th>UID</th><th>昵称</th><th>角色</th><th>加入时间</th></tr></thead>
|
||||
<tbody>
|
||||
{{# layui.each(d.members, function(i, m){ }}
|
||||
<tr>
|
||||
<td>{{m.uid}}</td>
|
||||
<td>{{m.nickname||'--'}}</td>
|
||||
<td>{{m.role==1?'主播':'观众'}}</td>
|
||||
<td>{{m.join_at?layui.util.toDateString(m.join_at*1000,'yyyy-MM-dd HH:mm'):''}}</td>
|
||||
</tr>
|
||||
{{# }); }}
|
||||
{{# if(d.members.length===0){ }}
|
||||
<tr><td colspan="4" style="text-align:center;">暂无成员</td></tr>
|
||||
{{# } }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
layui.use(['table','form','layer','laytpl','util','http'], function(){
|
||||
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, util = layui.util, $ = layui.$, http = layui.http;
|
||||
var base = '/wxchat/backend/room';
|
||||
|
||||
var tableIns = table.render({
|
||||
elem: '#dataTable',
|
||||
url: base + '/index',
|
||||
page: true,
|
||||
limit: 15,
|
||||
cols: [[
|
||||
{field:'id', title:'ID', width:70},
|
||||
{field:'title', title:'房间标题'},
|
||||
{field:'type', title:'类型', width:80, templet:function(d){return d.type==1?'语音':(d.type==2?'视频':'游戏');}},
|
||||
{field:'owner_name', title:'房主', width:120},
|
||||
{field:'member_count', title:'成员数', width:90},
|
||||
{field:'mic_count', title:'麦位数', width:80},
|
||||
{field:'hot', title:'热度', width:80},
|
||||
{field:'danmaku_on', title:'弹幕', width:70, templet:function(d){return d.danmaku_on==1?'开':'关';}},
|
||||
{field:'status', title:'状态', width:90, templet:'#statusTpl'},
|
||||
{field:'create_at', title:'创建时间', width:170, templet:function(d){return d.create_at?util.toDateString(d.create_at*1000,'yyyy-MM-dd HH:mm'):'';}},
|
||||
{title:'操作', width:230, toolbar:'#barTpl'}
|
||||
]],
|
||||
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
|
||||
});
|
||||
|
||||
form.on('submit(searchBtn)', function(data){
|
||||
tableIns.reload({where: data.field, page:{curr:1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
function post(action, data){
|
||||
return http.post(base + '/'+action, data);
|
||||
}
|
||||
|
||||
table.on('tool(dataTable)', function(obj){
|
||||
var d = obj.data;
|
||||
if(obj.event==='members'){
|
||||
post('detail', {id: d.id}).then(function(res){
|
||||
if(res.code!==0){ layer.msg(res.message||'获取失败',{icon:2}); return; }
|
||||
var tpl = laytpl($('#membersTpl').html()).render(res.data);
|
||||
layer.open({type:1, title:'房间成员 #'+d.id, area:['560px','auto'], content: tpl});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(obj.event==='edit'){
|
||||
var tpl = laytpl($('#formTpl').html()).render(d);
|
||||
layer.open({type:1, title:'编辑房间 #'+d.id, area:['520px','auto'], content: tpl,
|
||||
btn:['保存','取消'],
|
||||
success:function(){ form.render(); },
|
||||
yes:function(index){
|
||||
post('save', $('#roomForm').serialize()).then(function(r){
|
||||
if(r.code===0){ layer.close(index); layer.msg('保存成功',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(r.message||'失败',{icon:2});
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(obj.event==='close'){
|
||||
layer.confirm('确定关闭该房间?', function(){
|
||||
post('close', {id: d.id}).then(function(r){
|
||||
if(r.code===0){ layer.msg('已关闭',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(r.message||'失败',{icon:2});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if(obj.event==='del'){
|
||||
layer.confirm('确定删除该房间(含成员与麦位)?', function(){
|
||||
post('del', {ids: d.id}).then(function(r){
|
||||
if(r.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(r.message||'失败',{icon:2});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<div class="layui-card" style="margin:15px;">
|
||||
<div class="layui-card-header">任务赚钱管理</div>
|
||||
<div class="layui-card-body">
|
||||
<form class="layui-form" lay-filter="searchForm">
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-inline">
|
||||
<input class="layui-input" name="keyword" placeholder="任务标题" autocomplete="off">
|
||||
</div>
|
||||
<div class="layui-inline">
|
||||
<button class="layui-btn" lay-submit lay-filter="searchBtn">搜索</button>
|
||||
<button type="reset" class="layui-btn layui-btn-primary">重置</button>
|
||||
<button type="button" class="layui-btn layui-btn-normal" id="addBtn">新增任务</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<table class="layui-hide" id="dataTable" lay-filter="dataTable"></table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/html" id="barTpl">
|
||||
<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>
|
||||
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="del">删除</a>
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="statusTpl">
|
||||
{{# if(d.status == 1){ }}
|
||||
<span class="layui-badge layui-bg-green">启用</span>
|
||||
{{# } else { }}
|
||||
<span class="layui-badge">禁用</span>
|
||||
{{# } }}
|
||||
</script>
|
||||
|
||||
<script type="text/html" id="formTpl">
|
||||
<form class="layui-form" style="padding:20px;" id="taskForm" lay-filter="taskForm">
|
||||
<input type="hidden" name="id" value="{{d.id||''}}">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">任务标题</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="title" required lay-verify="required" placeholder="如:每日签到" class="layui-input" value="{{d.title||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">任务类型</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="type" required lay-verify="required" placeholder="daily/invite/profile" class="layui-input" value="{{d.type||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">任务描述</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="desc" class="layui-input" value="{{d.desc||''}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">奖励金币</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="number" name="reward_coins" class="layui-input" value="{{d.reward_coins||0}}">
|
||||
</div>
|
||||
<label class="layui-form-label">目标值</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="number" name="target" class="layui-input" value="{{d.target||1}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">排序</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="number" name="sort" class="layui-input" value="{{d.sort||0}}">
|
||||
</div>
|
||||
<label class="layui-form-label">状态</label>
|
||||
<div class="layui-input-inline">
|
||||
<input type="radio" name="status" value="1" title="启用" {{ d.status==undefined||d.status==1?'checked':'' }}>
|
||||
<input type="radio" name="status" value="0" title="禁用" {{ d.status==0?'checked':'' }}>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</script>
|
||||
|
||||
<script>
|
||||
layui.use(['table','form','layer','laytpl','http'], function(){
|
||||
var table = layui.table, form = layui.form, layer = layui.layer, laytpl = layui.laytpl, $ = layui.$, http = layui.http;
|
||||
var base = '/wxchat/backend/task';
|
||||
|
||||
var tableIns = table.render({
|
||||
elem: '#dataTable',
|
||||
url: base + '/index',
|
||||
page: true,
|
||||
limit: 15,
|
||||
cols: [[
|
||||
{field:'id', title:'ID', width:70},
|
||||
{field:'title', title:'标题'},
|
||||
{field:'type', title:'类型', width:100},
|
||||
{field:'reward_coins', title:'奖励金币', width:100},
|
||||
{field:'target', title:'目标值', width:80},
|
||||
{field:'participants', title:'参与人数', width:100},
|
||||
{field:'claimed', title:'已领取', width:80},
|
||||
{field:'sort', title:'排序', width:70},
|
||||
{field:'status', title:'状态', width:90, templet:'#statusTpl'},
|
||||
{title:'操作', width:140, toolbar:'#barTpl'}
|
||||
]],
|
||||
parseData: function(res){ return {code: res.code, msg: res.message, count: res.count||0, data: res.data||[]}; }
|
||||
});
|
||||
|
||||
form.on('submit(searchBtn)', function(data){
|
||||
tableIns.reload({where: data.field, page:{curr:1}});
|
||||
return false;
|
||||
});
|
||||
|
||||
function post(action, data){
|
||||
return http.post(base + '/'+action, data);
|
||||
}
|
||||
|
||||
$('#addBtn').on('click', function(){
|
||||
openForm({});
|
||||
});
|
||||
|
||||
function openForm(row){
|
||||
var tpl = laytpl($('#formTpl').html()).render(row);
|
||||
layer.open({
|
||||
type:1, title: row.id ? '编辑任务' : '新增任务', area:['520px','auto'], content: tpl,
|
||||
btn:['保存','取消'],
|
||||
success:function(){ form.render(); },
|
||||
yes:function(index){
|
||||
var data = $('#taskForm').serialize();
|
||||
post('save', data).then(function(res){
|
||||
if(res.code===0){ layer.close(index); layer.msg('保存成功',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(res.message||'保存失败',{icon:2});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
table.on('tool(dataTable)', function(obj){
|
||||
var d = obj.data;
|
||||
if(obj.event==='edit'){ openForm(d); }
|
||||
else if(obj.event==='del'){
|
||||
layer.confirm('确定删除该任务?', function(){
|
||||
post('del', {ids: d.id}).then(function(res){
|
||||
if(res.code===0){ layer.msg('已删除',{icon:1}); tableIns.reload(); }
|
||||
else layer.msg(res.message||'删除失败',{icon:2});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp<admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
namespace ywxapp\library;
|
||||
|
||||
use think\exception\HttpException;
|
||||
use think\facade\Db;
|
||||
use think\facade\Event;
|
||||
use ywxapp\library\Result;
|
||||
use ywxapp\model\Rule as MemberRule;
|
||||
use ywxapp\model\MemberUser as UsersModel;
|
||||
|
||||
/**
|
||||
* Member 类
|
||||
*
|
||||
* @author ywxapp <admin@ywxapp.cn>
|
||||
*/
|
||||
class Member
|
||||
{
|
||||
/***
|
||||
* 是否登录
|
||||
* @var bool
|
||||
*/
|
||||
protected $isLogin = false;
|
||||
|
||||
/**
|
||||
* Summary of config
|
||||
* @var array
|
||||
*/
|
||||
protected $config = [];
|
||||
|
||||
/**
|
||||
* Summary of options
|
||||
* @var array
|
||||
*/
|
||||
protected $options = [];
|
||||
|
||||
/**
|
||||
* 登录标识
|
||||
* @var bool
|
||||
*/
|
||||
protected $token;
|
||||
|
||||
/**
|
||||
* 个人信息
|
||||
* @return \ywxapp\model\Member
|
||||
*/
|
||||
protected $info;
|
||||
|
||||
/**
|
||||
* 权限规则
|
||||
* @return array
|
||||
*/
|
||||
protected $powers;
|
||||
|
||||
/**
|
||||
* 角色
|
||||
* @var array
|
||||
*/
|
||||
protected $roles;
|
||||
|
||||
/**
|
||||
* 用户资料,用于临时存放一些用户数据,比如用户IP地址,需要使用后销毁
|
||||
* @var array
|
||||
*/
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* 允许查询的字段
|
||||
* @var array
|
||||
*/
|
||||
protected $allowFields = ['id', 'account', 'nickname', 'roles', 'avatar', 'score'];
|
||||
|
||||
|
||||
public function __construct($options = [])
|
||||
{
|
||||
$this->info = new \stdClass();
|
||||
// if ($config = Config::get('member')) {
|
||||
// $this->config = array_merge($this->config, $config);
|
||||
// }
|
||||
// $this->options = array_merge($this->config, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化用户信息
|
||||
*/
|
||||
public function initUser($uid)
|
||||
{
|
||||
$this->direct($uid);
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接登录账号.
|
||||
*
|
||||
* @param int $id
|
||||
* @return void
|
||||
*/
|
||||
public function direct($uid)
|
||||
{
|
||||
$info = UsersModel::findOrEmpty($uid);
|
||||
if ($info->isEmpty()) {
|
||||
Result::instance()->error("Uid:$uid is incorrect");
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
if ($info->update_time < \ywxapp\utils\Date::unixtime('day')) {
|
||||
$info->successions = $info->logintime < \ywxapp\utils\Date::unixtime(
|
||||
'day',
|
||||
-1
|
||||
) ? 1 : $info->successions + 1;
|
||||
$info->maxsuccessions = max($info->successions, $info->maxsuccessions);
|
||||
}
|
||||
// $user->prevtime = $user->logintime;
|
||||
//记录本次登录的IP和时间
|
||||
//$user->loginip =request()->ip();
|
||||
//重置登录失败次数
|
||||
//$user->loginfailure = 0;
|
||||
$info->save();
|
||||
$this->info = $info;
|
||||
// $this->roles = $info->roles;
|
||||
// $this->powers = $info->roles;
|
||||
$this->isLogin = true;
|
||||
//登录成功的事件
|
||||
Event::trigger('user_login_successed', $this->info);
|
||||
Db::commit();
|
||||
} catch (\think\Exception $e) {
|
||||
Db::rollback();
|
||||
throw new HttpException(4001, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会员组别规则列表.
|
||||
* @return array
|
||||
*/
|
||||
public function getRuleList()
|
||||
{
|
||||
if ($this->powers) {
|
||||
return $this->powers;
|
||||
}
|
||||
|
||||
if (! $$this->roles) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$power = [];
|
||||
foreach ($this->roles as $role) {
|
||||
$powers = array_merge($powers, explode(',', $role->powers));
|
||||
}
|
||||
$powers = MemberRule::whereIn('id', implode(',', $power))
|
||||
->where('status', 0)
|
||||
->field('id,pid,name,title,ismenu')
|
||||
->select();
|
||||
$this->powers = $powers;
|
||||
return $this->powers;
|
||||
}
|
||||
|
||||
|
||||
private function getEncryptPassword($password, $salt = '')
|
||||
{
|
||||
return md5(md5($password) . $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户权限验证
|
||||
*/
|
||||
public function verifyAuth($noNeedLogin = ["*"], $noNeedRight = ["*"])
|
||||
{
|
||||
$request = app()->request;
|
||||
$modulename = app()->http->getName();
|
||||
$controllername = strtolower($request->controller());
|
||||
$actionname = strtolower($request->action());
|
||||
|
||||
$path = str_replace('.', '/', $controllername) . '/' . $actionname;
|
||||
if (! $this->match($noNeedLogin)) {
|
||||
if (! $this->isLogin) {
|
||||
Result::instance()->error('Please login first');
|
||||
}
|
||||
|
||||
// 判断是否需要验证权限
|
||||
if (! $this->match($noNeedRight) && ! $this->check($path)) {
|
||||
Result::instance()->error('You have no permission');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测当前控制器和方法是否匹配传递的数组.
|
||||
* @param array $arr 需要验证权限的数组
|
||||
* @return bool
|
||||
*/
|
||||
private function match($arr = [])
|
||||
{
|
||||
if (in_array(strtolower(app()->request->action()), $arr) || in_array('*', $arr)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否是否有对应权限.
|
||||
* @param string $path 控制器/方法
|
||||
* @param string $module 模块 默认为当前模块
|
||||
* @return bool
|
||||
*/
|
||||
private function check($path = null, $module = null)
|
||||
{
|
||||
$ruleList = $this->getRuleList();
|
||||
$rules = [];
|
||||
foreach ($ruleList as $k => $v) {
|
||||
$rules[] = $v['name'];
|
||||
}
|
||||
$url = strtolower(str_replace('.', '/', $path));
|
||||
return in_array($url, $rules) ? true : false;
|
||||
}
|
||||
|
||||
|
||||
public static function instance($options = [])
|
||||
{
|
||||
return app()->auth;
|
||||
}
|
||||
|
||||
|
||||
public function __get($name)
|
||||
{
|
||||
if ($name == 'info' && $this->info != null) {
|
||||
return $this->info;
|
||||
//return array_intersect_key($this->info->toArray(), array_flip($this->allowFields));
|
||||
}
|
||||
|
||||
return $this->$name;
|
||||
}
|
||||
|
||||
|
||||
public function __set($name, $value)
|
||||
{
|
||||
$this->$name = $value;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,482 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🔐 端到端加密聊天</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: "Microsoft YaHei", sans-serif
|
||||
}
|
||||
|
||||
body {
|
||||
background: #f0f2f5;
|
||||
padding: 20px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding: 15px;
|
||||
background: #1a73e8;
|
||||
color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1)
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
gap: 20px
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 3px 15px rgba(0, 0, 0, 0.1);
|
||||
padding: 20px;
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
color: #1a73e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px
|
||||
}
|
||||
|
||||
.title i {
|
||||
font-size: 20px
|
||||
}
|
||||
|
||||
#chatBox {
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
background: #fafafa;
|
||||
line-height: 1.6
|
||||
}
|
||||
|
||||
.msg {
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 15px;
|
||||
border-radius: 18px;
|
||||
max-width: 80%
|
||||
}
|
||||
|
||||
.sent {
|
||||
background: #e3f2fd;
|
||||
margin-left: auto;
|
||||
border-bottom-right-radius: 5px
|
||||
}
|
||||
|
||||
.received {
|
||||
background: #f1f8e9;
|
||||
border-bottom-left-radius: 5px
|
||||
}
|
||||
|
||||
.sys {
|
||||
background: #fff8e1;
|
||||
color: #5d4037;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
margin: 5px 0
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
resize: none;
|
||||
font-size: 15px;
|
||||
height: 45px
|
||||
}
|
||||
|
||||
button {
|
||||
background: #1a73e8;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 0 25px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0d5bbb;
|
||||
transform: scale(1.03)
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #b0b0b0;
|
||||
cursor: not-allowed;
|
||||
transform: none
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.status.connected {
|
||||
background: #e8f5e9;
|
||||
color: #2e7d32
|
||||
}
|
||||
|
||||
.status.connecting {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #ffebee;
|
||||
color: #c62828
|
||||
}
|
||||
|
||||
.debug {
|
||||
background: #2d2d2d;
|
||||
color: #64dd17;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
max-height: 150px;
|
||||
overflow: auto;
|
||||
display: none
|
||||
}
|
||||
|
||||
.badge {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
.chat-container {
|
||||
flex-direction: column
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🔐 端到端加密聊天 <span class="badge">微信级安全</span></h1>
|
||||
<div style="margin-top:8px; font-size:14px; opacity:0.9">AES-256-CBC加密 | SHA1签名验证 | 防篡改防重放</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-container">
|
||||
<div class="panel">
|
||||
<div class="title">💬 聊天窗口</div>
|
||||
<div id="chatBox"></div>
|
||||
<div class="input-area">
|
||||
<textarea id="messageInput" placeholder="输入消息... (按Ctrl+Enter发送)" disabled></textarea>
|
||||
<button id="sendBtn" disabled>发送</button>
|
||||
</div>
|
||||
<div id="statusBar" class="status connecting">⏳ 正在连接服务器...</div>
|
||||
<button id="toggleDebug" style="margin-top:10px; padding:5px 15px; background:#f5f5f5; color:#5f6368">显示调试日志</button>
|
||||
<div id="debugLog" class="debug"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="title">⚙️ 连接配置</div>
|
||||
<div style="margin-bottom:15px">
|
||||
<div style="margin-bottom:8px"><strong>用户ID:</strong></div>
|
||||
<input type="text" id="userId" value="user_<?= bin2hex(random_bytes(4)) ?>" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
|
||||
</div>
|
||||
<div style="margin-bottom:15px">
|
||||
<div style="margin-bottom:8px"><strong>服务器地址:</strong></div>
|
||||
<input type="text" id="serverUrl" value="wss://dev.xixingwl.cn:2346" style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px">
|
||||
</div>
|
||||
<button id="connectBtn" style="width:100%; background:#34a853">🔌 连接服务器</button>
|
||||
<div style="margin-top:20px; padding:12px; background:#e8f0fe; border-radius:8px; font-size:13px; line-height:1.5">
|
||||
<strong>🔒 安全说明:</strong><br>
|
||||
• 所有消息端到端加密,服务器无法查看内容<br>
|
||||
• 每条消息独立签名,防篡改防伪造<br>
|
||||
• 时间戳验证,防止消息重放攻击
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/assets/ywxapp/modules/aescgm.js"></script>
|
||||
<script>
|
||||
|
||||
// =============== 聊天客户端 ===============
|
||||
class ChatClient {
|
||||
constructor() {
|
||||
this.ws = null;
|
||||
this.crypto = null;
|
||||
this.userId = '';
|
||||
this.isConnected = false;
|
||||
this.initUI();
|
||||
}
|
||||
|
||||
initUI() {
|
||||
// DOM元素
|
||||
this.chatBox = document.getElementById('chatBox');
|
||||
this.messageInput = document.getElementById('messageInput');
|
||||
this.sendBtn = document.getElementById('sendBtn');
|
||||
this.statusBar = document.getElementById('statusBar');
|
||||
this.connectBtn = document.getElementById('connectBtn');
|
||||
this.toggleDebug = document.getElementById('toggleDebug');
|
||||
this.debugLog = document.getElementById('debugLog');
|
||||
|
||||
// 事件绑定
|
||||
this.connectBtn.onclick = () => this.connect();
|
||||
this.sendBtn.onclick = () => this.sendMessage();
|
||||
this.messageInput.onkeypress = (e) => {
|
||||
if (e.ctrlKey && e.key === 'Enter') this.sendMessage();
|
||||
};
|
||||
this.toggleDebug.onclick = () => {
|
||||
this.debugLog.style.display = this.debugLog.style.display === 'none' ? 'block' : 'none';
|
||||
};
|
||||
|
||||
// 初始化状态
|
||||
this.updateStatus('⏳ 未连接,请配置后点击连接', 'connecting');
|
||||
}
|
||||
|
||||
log(msg, type = 'info') {
|
||||
const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️';
|
||||
this.debugLog.innerHTML += `<span style="color:${type === 'error' ? '#ff5252' : type === 'success' ? '#4caf50' : '#aaa'}">${prefix} ${msg}</span><br>`;
|
||||
this.debugLog.scrollTop = this.debugLog.scrollHeight;
|
||||
console.log(`[ChatClient] ${msg}`);
|
||||
}
|
||||
|
||||
updateStatus(text, className) {
|
||||
this.statusBar.textContent = text;
|
||||
this.statusBar.className = `status ${className}`;
|
||||
}
|
||||
|
||||
addMessage(text, type = 'received', user = '系统') {
|
||||
const div = document.createElement('div');
|
||||
div.className = type === 'sys' ? 'sys' : `msg ${type}`;
|
||||
if (type === 'sys') {
|
||||
div.innerHTML = text;
|
||||
} else {
|
||||
div.innerHTML = `<strong>${user}:</strong> ${this.escapeHtml(text)}`;
|
||||
}
|
||||
this.chatBox.appendChild(div);
|
||||
this.chatBox.scrollTop = this.chatBox.scrollHeight;
|
||||
}
|
||||
|
||||
escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
async connect() {
|
||||
try {
|
||||
this.userId = document.getElementById('userId').value.trim() || 'anonymous';
|
||||
const serverUrl = document.getElementById('serverUrl').value.trim();
|
||||
|
||||
if (!serverUrl.startsWith('ws://') && !serverUrl.startsWith('wss://')) {
|
||||
throw new Error('服务器地址必须以 ws:// 或 wss:// 开头');
|
||||
}
|
||||
|
||||
this.updateStatus('🔄 正在连接...', 'connecting');
|
||||
this.connectBtn.disabled = true;
|
||||
|
||||
// 创建WebSocket
|
||||
this.ws = new WebSocket(serverUrl);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.log('WebSocket连接已建立');
|
||||
// 发送认证请求
|
||||
this.ws.send(JSON.stringify({
|
||||
action: 'auth',
|
||||
token: 'your_secure_token_123456', // 与服务端约定
|
||||
user_id: this.userId
|
||||
}));
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => this.handleMessage(event.data);
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
this.log(`连接错误: ${error.message}`, 'error');
|
||||
this.log(error);
|
||||
this.updateStatus('❌ 连接失败: ' + error.message, 'error');
|
||||
this.connectBtn.disabled = false;
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.isConnected = false;
|
||||
this.log('连接已关闭', 'error');
|
||||
if (this.isConnected) {
|
||||
this.updateStatus('⚠️ 连接意外断开', 'error');
|
||||
this.addMessage('连接已断开', 'sys');
|
||||
}
|
||||
this.connectBtn.disabled = false;
|
||||
this.messageInput.disabled = true;
|
||||
this.sendBtn.disabled = true;
|
||||
};
|
||||
|
||||
} catch (e) {
|
||||
this.log(`连接异常: ${e.message}`, 'error');
|
||||
this.updateStatus('❌ ' + e.message, 'error');
|
||||
this.connectBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(data) {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
// 认证响应
|
||||
if (msg.action === 'auth_success') {
|
||||
// 初始化加密模块 (使用服务端返回的密钥)
|
||||
this.crypto = new ChatCrypto(
|
||||
msg.encodingAesKey,
|
||||
'your_secure_token_123456',
|
||||
'CHAT_APP_2024'
|
||||
);
|
||||
|
||||
|
||||
this.isConnected = true;
|
||||
this.updateStatus(`✅ 已连接 | 会话ID: ${msg.session_id.substring(0, 8)}...`, 'connected');
|
||||
this.addMessage(`欢迎 ${this.userId}!端到端加密已启用`, 'sys');
|
||||
this.messageInput.disabled = false;
|
||||
this.sendBtn.disabled = false;
|
||||
this.connectBtn.textContent = '🔌 已连接';
|
||||
this.connectBtn.style.background = '#9e9e9e';
|
||||
this.connectBtn.disabled = true;
|
||||
this.log('认证成功,加密模块已初始化', 'success');
|
||||
return;
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
if (msg.error) {
|
||||
this.log(`服务端错误: ${msg.error} (代码: ${msg.code})`, 'error');
|
||||
if (msg.code === 401) {
|
||||
this.updateStatus('❌ 认证失败,请检查token', 'error');
|
||||
this.ws.close();
|
||||
}
|
||||
this.addMessage(`[系统] ${msg.error}`, 'sys');
|
||||
return;
|
||||
}
|
||||
|
||||
// =============== 接收聊天消息 ===============
|
||||
// 验证签名
|
||||
if (!this.crypto.verifySignature(
|
||||
msg.signature,
|
||||
msg.timestamp,
|
||||
msg.nonce,
|
||||
msg.encrypt
|
||||
)) {
|
||||
this.addMessage(`[警告] 消息签名验证失败,来源: ${msg.from_user}`, 'sys');
|
||||
this.log(`收到无效签名消息 (来源: ${msg.from_user})`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 防重放检查
|
||||
if (Math.abs(Date.now() / 1000 - msg.timestamp) > 300) {
|
||||
this.log(`收到过期消息 (时间差: ${Math.abs(Date.now() / 1000 - msg.timestamp)}秒)`, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 解密消息
|
||||
try {
|
||||
const plainText = this.crypto.decrypt(msg.encrypt);
|
||||
const chatData = JSON.parse(plainText);
|
||||
this.addMessage(chatData.content, 'received', chatData.from_user || msg.from_user);
|
||||
this.log(`收到消息: "${chatData.content.substring(0, 20)}..." (来源: ${msg.from_user})`, 'success');
|
||||
} catch (e) {
|
||||
this.addMessage(`[系统] 消息解密失败: ${e.message}`, 'sys');
|
||||
this.log(`解密异常: ${e.message}`, 'error');
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
this.log(`消息处理异常: ${e.message}`, 'error');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage() {
|
||||
const content = this.messageInput.value.trim();
|
||||
if (!content || !this.isConnected || !this.crypto) return;
|
||||
try {
|
||||
// 构建消息体
|
||||
const chatMsg = {
|
||||
content: content,
|
||||
type: 'text',
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
console.log('原始消息体:', chatMsg);
|
||||
console.log('加密前消息体字符串:', JSON.stringify(chatMsg));
|
||||
console.log('加密前消息体字符串:', window.encryptJson(chatMsg, 'your_secure_token_123456'));
|
||||
// 加密
|
||||
const encryptMsg = this.crypto.encrypt(JSON.stringify(chatMsg));
|
||||
// 生成签名参数
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = this.crypto.getRandomStr(10);
|
||||
const signature = this.crypto.generateSignature(timestamp, nonce, encryptMsg);
|
||||
// 发送
|
||||
this.ws.send(JSON.stringify({
|
||||
encrypt: encryptMsg,
|
||||
signature: signature,
|
||||
timestamp: timestamp,
|
||||
nonce: nonce
|
||||
}));
|
||||
// 显示到聊天框
|
||||
this.addMessage(content, 'sent', this.userId);
|
||||
this.messageInput.value = '';
|
||||
this.log(`发送消息: "${content.substring(0, 20)}..."`, 'success');
|
||||
|
||||
} catch (e) {
|
||||
this.log(`发送失败: ${e.message}`, 'error');
|
||||
alert('发送失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const aes = new AESGCM();
|
||||
// 加密示例
|
||||
aes.encrypt('Hello World!', 'mypassword')
|
||||
.then(encrypted => {
|
||||
console.log('加密结果:', encrypted);
|
||||
|
||||
// 解密示例
|
||||
return aes.decrypt(encrypted, 'mypassword');
|
||||
})
|
||||
.then(decrypted => {
|
||||
console.log('解密结果:', decrypted);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('操作失败:', error);
|
||||
});
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('页面加载完成,正在初始化聊天客户端...');
|
||||
window.chatClient = new ChatClient();
|
||||
document.getElementById('messageInput').focus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user